ccsession.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. # Copyright (C) 2009 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. #
  16. # Client-side functionality for configuration and commands
  17. #
  18. # It keeps a cc-channel session with the configuration manager daemon,
  19. # and handles configuration updates and direct commands
  20. # modeled after ccsession.h/cc 'protocol' changes here need to be
  21. # made there as well
  22. """Classes and functions for handling configuration and commands
  23. This module provides the ModuleCCSession and UIModuleCCSession
  24. classes, as well as a set of utility functions to create and parse
  25. messages related to commands and configuration
  26. Modules should use the ModuleCCSession class to connect to the
  27. configuration manager, and receive updates and commands from
  28. other modules.
  29. Configuration user interfaces should use the UIModuleCCSession
  30. to connect to b10-cmdctl, and receive and send configuration and
  31. commands through that to the configuration manager.
  32. """
  33. from isc.cc import Session
  34. from isc.cc.proto_defs import *
  35. from isc.config.config_data import ConfigData, MultiConfigData, BIND10_CONFIG_DATA_VERSION
  36. import isc.config.module_spec
  37. import isc
  38. from isc.util.file import path_search
  39. import bind10_config
  40. from isc.log import log_config_update
  41. import json
  42. from isc.log_messages.config_messages import *
  43. logger = isc.log.Logger("config")
  44. class ModuleCCSessionError(Exception): pass
  45. class RPCError(ModuleCCSessionError):
  46. """
  47. An exception raised by rpc_call in case the remote side reports
  48. an error. It can be used to distinguish remote errors from protocol errors.
  49. Also, it holds the code as well as the error message.
  50. """
  51. def __init__(self, code, message):
  52. ModuleCCSessionError.__init__(self, message)
  53. self.__code = code
  54. def code(self):
  55. """
  56. The code as sent over the CC.
  57. """
  58. return self.__code
  59. class RPCRecipientMissing(RPCError):
  60. """
  61. Special version of the RPCError, for cases the recipient of the call
  62. isn't connected to the bus. The code is always
  63. isc.cc.proto_defs.CC_REPLY_NO_RECPT.
  64. """
  65. def __init__(self, message):
  66. RPCError.__init__(self, CC_REPLY_NO_RECPT, message)
  67. def parse_answer(msg):
  68. """Returns a tuple (rcode, value), where value depends on the
  69. command that was called. If rcode != 0, value is a string
  70. containing an error message"""
  71. if type(msg) != dict:
  72. raise ModuleCCSessionError("Answer message is not a dict: " + str(msg))
  73. if 'result' not in msg:
  74. raise ModuleCCSessionError("answer message does not contain 'result' element")
  75. elif type(msg['result']) != list:
  76. raise ModuleCCSessionError("wrong result type in answer message")
  77. elif len(msg['result']) < 1:
  78. raise ModuleCCSessionError("empty result list in answer message")
  79. elif type(msg['result'][0]) != int:
  80. raise ModuleCCSessionError("wrong rcode type in answer message")
  81. else:
  82. if len(msg['result']) > 1:
  83. if (msg['result'][0] != CC_REPLY_SUCCESS and
  84. type(msg['result'][1]) != str):
  85. raise ModuleCCSessionError("rcode in answer message is non-zero, value is not a string")
  86. return msg['result'][0], msg['result'][1]
  87. else:
  88. return msg['result'][0], None
  89. def create_answer(rcode, arg = None):
  90. """Creates an answer packet for config&commands. rcode must be an
  91. integer. If rcode == 0, arg is an optional value that depends
  92. on what the command or option was. If rcode != 0, arg must be
  93. a string containing an error message"""
  94. if type(rcode) != int:
  95. raise ModuleCCSessionError("rcode in create_answer() must be an integer")
  96. if rcode != CC_REPLY_SUCCESS and type(arg) != str:
  97. raise ModuleCCSessionError("arg in create_answer for rcode != 0 must be a string describing the error")
  98. if arg != None:
  99. return { 'result': [ rcode, arg ] }
  100. else:
  101. return { 'result': [ rcode ] }
  102. # 'fixed' commands
  103. """Fixed names for command and configuration messages"""
  104. COMMAND_CONFIG_UPDATE = "config_update"
  105. COMMAND_MODULE_SPECIFICATION_UPDATE = "module_specification_update"
  106. COMMAND_GET_COMMANDS_SPEC = "get_commands_spec"
  107. COMMAND_GET_STATISTICS_SPEC = "get_statistics_spec"
  108. COMMAND_GET_CONFIG = "get_config"
  109. COMMAND_SET_CONFIG = "set_config"
  110. COMMAND_GET_MODULE_SPEC = "get_module_spec"
  111. COMMAND_MODULE_SPEC = "module_spec"
  112. COMMAND_SHUTDOWN = "shutdown"
  113. COMMAND_MODULE_STOPPING = "stopping"
  114. def parse_command(msg):
  115. """Parses what may be a command message. If it looks like one,
  116. the function returns (command, value) where command is a
  117. string. If it is not, this function returns None, None"""
  118. if type(msg) == dict and len(msg.items()) == 1:
  119. cmd, value = msg.popitem()
  120. if cmd == "command" and type(value) == list:
  121. if len(value) == 1 and type(value[0]) == str:
  122. return value[0], None
  123. elif len(value) > 1 and type(value[0]) == str:
  124. return value[0], value[1]
  125. return None, None
  126. def create_command(command_name, params = None):
  127. """Creates a module command message with the given command name (as
  128. specified in the module's specification, and an optional params
  129. object"""
  130. # TODO: validate_command with spec
  131. if type(command_name) != str:
  132. raise ModuleCCSessionError("command in create_command() not a string")
  133. cmd = [ command_name ]
  134. if params:
  135. cmd.append(params)
  136. msg = { 'command': cmd }
  137. return msg
  138. def default_logconfig_handler(new_config, config_data):
  139. errors = []
  140. if config_data.get_module_spec().validate_config(False, new_config, errors):
  141. isc.log.log_config_update(json.dumps(new_config),
  142. json.dumps(config_data.get_module_spec().get_full_spec()))
  143. else:
  144. logger.error(CONFIG_LOG_CONFIG_ERRORS, errors)
  145. class ModuleCCSession(ConfigData):
  146. """This class maintains a connection to the command channel, as
  147. well as configuration options for modules. The module provides
  148. a specification file that contains the module name, configuration
  149. options, and commands. It also gives the ModuleCCSession two callback
  150. functions, one to call when there is a direct command to the
  151. module, and one to update the configuration run-time. These
  152. callbacks are called when 'check_command' is called on the
  153. ModuleCCSession"""
  154. def __init__(self, spec_file_name, config_handler, command_handler,
  155. cc_session=None, handle_logging_config=True,
  156. socket_file = None):
  157. """Initialize a ModuleCCSession. This does *NOT* send the
  158. specification and request the configuration yet. Use start()
  159. for that once the ModuleCCSession has been initialized.
  160. specfile_name is the path to the specification file.
  161. config_handler and command_handler are callback functions,
  162. see set_config_handler and set_command_handler for more
  163. information on their signatures.
  164. cc_session can be used to pass in an existing CCSession,
  165. if it is None, one will be set up. This is mainly intended
  166. for testing purposes.
  167. handle_logging_config: if True, the module session will
  168. automatically handle logging configuration for the module;
  169. it will read the system-wide Logging configuration and call
  170. the logger manager to apply it. It will also inform the
  171. logger manager when the logging configuration gets updated.
  172. The module does not need to do anything except intializing
  173. its loggers, and provide log messages. Defaults to true.
  174. socket_file: If cc_session was none, this optional argument
  175. specifies which socket file to use to connect to msgq. It
  176. will be overridden by the environment variable
  177. MSGQ_SOCKET_FILE. If none, and no environment variable is
  178. set, it will use the system default.
  179. """
  180. module_spec = isc.config.module_spec_from_file(spec_file_name)
  181. ConfigData.__init__(self, module_spec)
  182. self._module_name = module_spec.get_module_name()
  183. self.set_config_handler(config_handler)
  184. self.set_command_handler(command_handler)
  185. if not cc_session:
  186. self._session = Session(socket_file)
  187. else:
  188. self._session = cc_session
  189. self._session.group_subscribe(self._module_name, "*")
  190. self._remote_module_configs = {}
  191. self._remote_module_callbacks = {}
  192. if handle_logging_config:
  193. self.add_remote_config(path_search('logging.spec', bind10_config.PLUGIN_PATHS),
  194. default_logconfig_handler)
  195. def __del__(self):
  196. # If the CC Session obejct has been closed, it returns
  197. # immediately.
  198. if self._session._closed: return
  199. self._session.group_unsubscribe(self._module_name, "*")
  200. for module_name in self._remote_module_configs:
  201. self._session.group_unsubscribe(module_name)
  202. def start(self):
  203. """Send the specification for this module to the configuration
  204. manager, and request the current non-default configuration.
  205. The config_handler will be called with that configuration"""
  206. self.__send_spec()
  207. self.__request_config()
  208. def send_stopping(self):
  209. """Sends a 'stopping' message to the configuration manager. This
  210. message is just an FYI, and no response is expected. Any errors
  211. when sending this message (for instance if the msgq session has
  212. previously been closed) are logged, but ignored."""
  213. # create_command could raise an exception as well, but except for
  214. # out of memory related errors, these should all be programming
  215. # failures and are not caught
  216. msg = create_command(COMMAND_MODULE_STOPPING,
  217. self.get_module_spec().get_full_spec())
  218. try:
  219. self._session.group_sendmsg(msg, "ConfigManager")
  220. except Exception as se:
  221. # If the session was previously closed, obvously trying to send
  222. # a message fails. (TODO: check if session is open so we can
  223. # error on real problems?)
  224. logger.error(CONFIG_SESSION_STOPPING_FAILED, se)
  225. def get_socket(self):
  226. """Returns the socket from the command channel session. This
  227. should *only* be used for select() loops to see if there
  228. is anything on the channel. If that loop is not completely
  229. time-critical, it is strongly recommended to only use
  230. check_command(), and not look at the socket at all."""
  231. return self._session._socket
  232. def close(self):
  233. """Close the session to the command channel"""
  234. self._session.close()
  235. def check_command(self, nonblock=True):
  236. """Check whether there is a command or configuration update on
  237. the channel. This function does a read on the cc session, and
  238. returns nothing.
  239. It calls check_command_without_recvmsg()
  240. to parse the received message.
  241. If nonblock is True, it just checks if there's a command
  242. and does nothing if there isn't. If nonblock is False, it
  243. waits until it arrives. It temporarily sets timeout to infinity,
  244. because commands may not come in arbitrary long time."""
  245. timeout_orig = self._session.get_timeout()
  246. self._session.set_timeout(0)
  247. try:
  248. msg, env = self._session.group_recvmsg(nonblock)
  249. finally:
  250. self._session.set_timeout(timeout_orig)
  251. self.check_command_without_recvmsg(msg, env)
  252. def check_command_without_recvmsg(self, msg, env):
  253. """Parse the given message to see if there is a command or a
  254. configuration update. Calls the corresponding handler
  255. functions if present. Responds on the channel if the
  256. handler returns a message."""
  257. # should we default to an answer? success-by-default? unhandled error?
  258. if msg is not None and not 'result' in msg:
  259. answer = None
  260. try:
  261. module_name = env['group']
  262. cmd, arg = isc.config.ccsession.parse_command(msg)
  263. if cmd == COMMAND_CONFIG_UPDATE:
  264. new_config = arg
  265. # If the target channel was not this module
  266. # it might be in the remote_module_configs
  267. if module_name != self._module_name:
  268. if module_name in self._remote_module_configs:
  269. # no checking for validity, that's up to the
  270. # module itself.
  271. newc = self._remote_module_configs[module_name].get_local_config()
  272. isc.cc.data.merge(newc, new_config)
  273. self._remote_module_configs[module_name].set_local_config(newc)
  274. if self._remote_module_callbacks[module_name] != None:
  275. self._remote_module_callbacks[module_name](new_config,
  276. self._remote_module_configs[module_name])
  277. # For other modules, we're not supposed to answer
  278. return
  279. # ok, so apparently this update is for us.
  280. errors = []
  281. if not self._config_handler:
  282. answer = create_answer(2, self._module_name + " has no config handler")
  283. elif not self.get_module_spec().validate_config(False, new_config, errors):
  284. answer = create_answer(1, ", ".join(errors))
  285. else:
  286. isc.cc.data.remove_identical(new_config, self.get_local_config())
  287. answer = self._config_handler(new_config)
  288. rcode, val = parse_answer(answer)
  289. if rcode == CC_REPLY_SUCCESS:
  290. newc = self.get_local_config()
  291. isc.cc.data.merge(newc, new_config)
  292. self.set_local_config(newc)
  293. else:
  294. # ignore commands for 'remote' modules
  295. if module_name == self._module_name:
  296. if self._command_handler:
  297. answer = self._command_handler(cmd, arg)
  298. else:
  299. answer = create_answer(2, self._module_name + " has no command handler")
  300. except Exception as exc:
  301. answer = create_answer(1, str(exc))
  302. if answer:
  303. self._session.group_reply(env, answer)
  304. def set_config_handler(self, config_handler):
  305. """Set the config handler for this module. The handler is a
  306. function that takes the full configuration and handles it.
  307. It should return an answer created with create_answer()"""
  308. self._config_handler = config_handler
  309. # should we run this right now since we've changed the handler?
  310. def set_command_handler(self, command_handler):
  311. """Set the command handler for this module. The handler is a
  312. function that takes a command as defined in the .spec file
  313. and return an answer created with create_answer()"""
  314. self._command_handler = command_handler
  315. def _add_remote_config_internal(self, module_spec,
  316. config_update_callback=None):
  317. """The guts of add_remote_config and add_remote_config_by_name"""
  318. module_cfg = ConfigData(module_spec)
  319. module_name = module_spec.get_module_name()
  320. self._session.group_subscribe(module_name)
  321. # Get the current config for that module now
  322. seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": module_name }), "ConfigManager")
  323. try:
  324. answer, _ = self._session.group_recvmsg(False, seq)
  325. except isc.cc.SessionTimeout:
  326. raise ModuleCCSessionError("No answer from ConfigManager when "
  327. "asking about Remote module " +
  328. module_name)
  329. call_callback = False
  330. if answer:
  331. rcode, value = parse_answer(answer)
  332. if rcode == 0:
  333. if value != None:
  334. if module_spec.validate_config(False, value):
  335. module_cfg.set_local_config(value)
  336. call_callback = True
  337. else:
  338. raise ModuleCCSessionError("Bad config data for " +
  339. module_name + ": " +
  340. str(value))
  341. else:
  342. raise ModuleCCSessionError("Failure requesting remote " +
  343. "configuration data for " +
  344. module_name)
  345. # all done, add it
  346. self._remote_module_configs[module_name] = module_cfg
  347. self._remote_module_callbacks[module_name] = config_update_callback
  348. if call_callback and config_update_callback is not None:
  349. config_update_callback(value, module_cfg)
  350. def add_remote_config_by_name(self, module_name,
  351. config_update_callback=None):
  352. """
  353. This does the same as add_remote_config, but you provide the module name
  354. instead of the name of the spec file.
  355. """
  356. seq = self._session.group_sendmsg(create_command(COMMAND_GET_MODULE_SPEC,
  357. { "module_name":
  358. module_name }),
  359. "ConfigManager")
  360. try:
  361. answer, env = self._session.group_recvmsg(False, seq)
  362. except isc.cc.SessionTimeout:
  363. raise ModuleCCSessionError("No answer from ConfigManager when " +
  364. "asking about for spec of Remote " +
  365. "module " + module_name)
  366. if answer:
  367. rcode, value = parse_answer(answer)
  368. if rcode == 0:
  369. module_spec = isc.config.module_spec.ModuleSpec(value)
  370. if module_spec.get_module_name() != module_name:
  371. raise ModuleCCSessionError("Module name mismatch: " +
  372. module_name + " and " +
  373. module_spec.get_module_name())
  374. self._add_remote_config_internal(module_spec,
  375. config_update_callback)
  376. else:
  377. raise ModuleCCSessionError("Error code " + str(rcode) +
  378. "when asking for module spec of " +
  379. module_name)
  380. else:
  381. raise ModuleCCSessionError("No answer when asking for module " +
  382. "spec of " + module_name)
  383. # Just to be consistent with the add_remote_config
  384. return module_name
  385. def add_remote_config(self, spec_file_name, config_update_callback=None):
  386. """Gives access to the configuration of a different module.
  387. These remote module options can at this moment only be
  388. accessed through get_remote_config_value(). This function
  389. also subscribes to the channel of the remote module name
  390. to receive the relevant updates. It is not possible to
  391. specify your own handler for this right now, but you can
  392. specify a callback that is called after the change happened.
  393. start() must have been called on this CCSession
  394. prior to the call to this method.
  395. Returns the name of the module."""
  396. module_spec = isc.config.module_spec_from_file(spec_file_name)
  397. self._add_remote_config_internal(module_spec, config_update_callback)
  398. return module_spec.get_module_name()
  399. def remove_remote_config(self, module_name):
  400. """Removes the remote configuration access for this module"""
  401. if module_name in self._remote_module_configs:
  402. self._session.group_unsubscribe(module_name)
  403. del self._remote_module_configs[module_name]
  404. del self._remote_module_callbacks[module_name]
  405. def get_remote_config_value(self, module_name, identifier):
  406. """Returns the current setting for the given identifier at the
  407. given module. If the module has not been added with
  408. add_remote_config, a ModuleCCSessionError is raised"""
  409. if module_name in self._remote_module_configs:
  410. return self._remote_module_configs[module_name].get_value(identifier)
  411. else:
  412. raise ModuleCCSessionError("Remote module " + module_name +
  413. " not found")
  414. def __send_spec(self):
  415. """Sends the data specification to the configuration manager"""
  416. msg = create_command(COMMAND_MODULE_SPEC, self.get_module_spec().get_full_spec())
  417. seq = self._session.group_sendmsg(msg, "ConfigManager")
  418. try:
  419. answer, env = self._session.group_recvmsg(False, seq)
  420. except isc.cc.SessionTimeout:
  421. # TODO: log an error?
  422. pass
  423. def __request_config(self):
  424. """Asks the configuration manager for the current configuration, and call the config handler if set.
  425. Raises a ModuleCCSessionError if there is no answer from the configuration manager"""
  426. seq = self._session.group_sendmsg(create_command(COMMAND_GET_CONFIG, { "module_name": self._module_name }), "ConfigManager")
  427. try:
  428. answer, env = self._session.group_recvmsg(False, seq)
  429. if answer:
  430. rcode, value = parse_answer(answer)
  431. if rcode == 0:
  432. errors = []
  433. if value != None:
  434. if self.get_module_spec().validate_config(False,
  435. value,
  436. errors):
  437. self.set_local_config(value)
  438. if self._config_handler:
  439. self._config_handler(value)
  440. else:
  441. raise ModuleCCSessionError(
  442. "Wrong data in configuration: " +
  443. " ".join(errors))
  444. else:
  445. logger.error(CONFIG_GET_FAILED, value)
  446. else:
  447. raise ModuleCCSessionError("No answer from configuration manager")
  448. except isc.cc.SessionTimeout:
  449. raise ModuleCCSessionError("CC Session timeout waiting for configuration manager")
  450. def rpc_call(self, command, group, instance=CC_INSTANCE_WILDCARD,
  451. to=CC_TO_WILDCARD, params=None):
  452. """
  453. Create a command with the given name and parameters. Send it to a
  454. recipient, wait for the answer and parse it.
  455. This is a wrapper around the group_sendmsg and group_recvmsg on the CC
  456. session. It exists mostly for convenience.
  457. Params:
  458. - command: Name of the command to call on the remote side.
  459. - group, instance, to: Address specification of the recipient.
  460. - params: Parameters to pass to the command (as keyword arguments).
  461. Return: The return value of the remote call (just the value, no status
  462. code or anything). May be None.
  463. Raise:
  464. - RPCRecipientMissing if the given recipient doesn't exist.
  465. - RPCError if the other side sent an error response. The error string
  466. is in the exception.
  467. - ModuleCCSessionError in case of protocol errors, like malformed
  468. answer.
  469. """
  470. cmd = create_command(command, params)
  471. seq = self._session.group_sendmsg(cmd, group, instance=instance,
  472. to=to, want_answer=True)
  473. # For non-blocking, we'll have rpc_call_async (once the nonblock
  474. # actualy works)
  475. reply, rheaders = self._session.group_recvmsg(nonblock=False, seq=seq)
  476. code, value = parse_answer(reply)
  477. if code == CC_REPLY_NO_RECPT:
  478. raise RPCRecipientMissing(value)
  479. elif code != CC_REPLY_SUCCESS:
  480. raise RPCError(code, value)
  481. return value
  482. class UIModuleCCSession(MultiConfigData):
  483. """This class is used in a configuration user interface. It contains
  484. specific functions for getting, displaying, and sending
  485. configuration settings through the b10-cmdctl module."""
  486. def __init__(self, conn):
  487. """Initialize a UIModuleCCSession. The conn object that is
  488. passed must have send_GET and send_POST functions"""
  489. MultiConfigData.__init__(self)
  490. self._conn = conn
  491. self.update_specs_and_config()
  492. def request_specifications(self):
  493. """Clears the current list of specifications, and requests a new
  494. list from b10-cmdctl. As other actions may have caused modules
  495. to be stopped, or new modules to be added, this is expected to
  496. be run after each interaction (at this moment). It is usually
  497. also combined with request_current_config(). For that reason,
  498. we provide update_specs_and_config() which calls both."""
  499. specs = self._conn.send_GET('/module_spec')
  500. self.clear_specifications()
  501. for module in specs.keys():
  502. self.set_specification(isc.config.ModuleSpec(specs[module]))
  503. def request_current_config(self):
  504. """Requests the current configuration from the configuration
  505. manager through b10-cmdctl, and stores those as CURRENT. This
  506. does not modify any local changes, it just updates to the current
  507. state of the server itself."""
  508. config = self._conn.send_GET('/config_data')
  509. if 'version' not in config or config['version'] != BIND10_CONFIG_DATA_VERSION:
  510. raise ModuleCCSessionError("Bad config version")
  511. self._set_current_config(config)
  512. def update_specs_and_config(self):
  513. """Convenience function to both clear and update the known list of
  514. module specifications, and update the current configuration on
  515. the server side. There are a few cases where the caller might only
  516. want to run one of these tasks, but often they are both needed."""
  517. self.request_specifications()
  518. self.request_current_config()
  519. def _add_value_to_list(self, identifier, value, module_spec):
  520. cur_list, status = self.get_value(identifier)
  521. if not cur_list:
  522. cur_list = []
  523. if value is None and "list_item_spec" in module_spec:
  524. if "item_default" in module_spec["list_item_spec"]:
  525. value = module_spec["list_item_spec"]["item_default"]
  526. if value is None:
  527. raise isc.cc.data.DataNotFoundError(
  528. "No value given and no default for " + str(identifier))
  529. if value not in cur_list:
  530. cur_list.append(value)
  531. self.set_value(identifier, cur_list)
  532. else:
  533. raise isc.cc.data.DataAlreadyPresentError(str(value) +
  534. " already in "
  535. + str(identifier))
  536. def _add_value_to_named_set(self, identifier, value, item_value):
  537. if type(value) != str:
  538. raise isc.cc.data.DataTypeError("Name for named_set " +
  539. identifier +
  540. " must be a string")
  541. # fail on both None and empty string
  542. if not value:
  543. raise isc.cc.data.DataNotFoundError(
  544. "Need a name to add a new item to named_set " +
  545. str(identifier))
  546. else:
  547. cur_map, status = self.get_value(identifier)
  548. if not cur_map:
  549. cur_map = {}
  550. if value not in cur_map:
  551. cur_map[value] = item_value
  552. self.set_value(identifier, cur_map)
  553. else:
  554. raise isc.cc.data.DataAlreadyPresentError(value +
  555. " already in " +
  556. identifier)
  557. def add_value(self, identifier, value_str = None, set_value_str = None):
  558. """Add a value to a configuration list. Raises a DataTypeError
  559. if the value does not conform to the list_item_spec field
  560. of the module config data specification. If value_str is
  561. not given, we add the default as specified by the .spec
  562. file. Raises a DataNotFoundError if the given identifier
  563. is not specified in the specification as a map or list.
  564. Raises a DataAlreadyPresentError if the specified element
  565. already exists."""
  566. module_spec = self.find_spec_part(identifier)
  567. if module_spec is None:
  568. raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))
  569. # for type any, we determine the 'type' by what value is set
  570. # (which would be either list or dict)
  571. cur_value, _ = self.get_value(identifier)
  572. type_any = isc.config.config_data.spec_part_is_any(module_spec)
  573. # the specified element must be a list or a named_set
  574. if 'list_item_spec' in module_spec or\
  575. (type_any and type(cur_value) == list):
  576. value = None
  577. # in lists, we might get the value with spaces, making it
  578. # the third argument. In that case we interpret both as
  579. # one big string meant as the value
  580. if value_str is not None:
  581. if set_value_str is not None:
  582. value_str += set_value_str
  583. value = isc.cc.data.parse_value_str(value_str)
  584. self._add_value_to_list(identifier, value, module_spec)
  585. elif 'named_set_item_spec' in module_spec or\
  586. (type_any and type(cur_value) == dict):
  587. item_name = None
  588. item_value = None
  589. if value_str is not None:
  590. item_name = value_str
  591. if set_value_str is not None:
  592. item_value = isc.cc.data.parse_value_str(set_value_str)
  593. else:
  594. if 'item_default' in module_spec['named_set_item_spec']:
  595. item_value = module_spec['named_set_item_spec']['item_default']
  596. self._add_value_to_named_set(identifier, item_name,
  597. item_value)
  598. else:
  599. raise isc.cc.data.DataTypeError(str(identifier) + " is not a list or a named set")
  600. def _remove_value_from_list(self, identifier, value):
  601. if value is None:
  602. # we are directly removing a list index
  603. id, list_indices = isc.cc.data.split_identifier_list_indices(identifier)
  604. if list_indices is None:
  605. raise isc.cc.data.DataTypeError("identifier in remove_value() does not contain a list index, and no value to remove")
  606. else:
  607. self.set_value(identifier, None)
  608. else:
  609. cur_list, status = self.get_value(identifier)
  610. if not cur_list:
  611. cur_list = []
  612. elif value in cur_list:
  613. cur_list.remove(value)
  614. self.set_value(identifier, cur_list)
  615. def _remove_value_from_named_set(self, identifier, value):
  616. if value is None:
  617. raise isc.cc.data.DataNotFoundError("Need a name to remove an item from named_set " + str(identifier))
  618. elif type(value) != str:
  619. raise isc.cc.data.DataTypeError("Name for named_set " + identifier + " must be a string")
  620. else:
  621. cur_map, status = self.get_value(identifier)
  622. if not cur_map:
  623. cur_map = {}
  624. if value in cur_map:
  625. del cur_map[value]
  626. self.set_value(identifier, cur_map)
  627. else:
  628. raise isc.cc.data.DataNotFoundError(value + " not found in named_set " + str(identifier))
  629. def remove_value(self, identifier, value_str):
  630. """Remove a value from a configuration list or named set.
  631. The value string must be a string representation of the full
  632. item. Raises a DataTypeError if the value at the identifier
  633. is not a list, or if the given value_str does not match the
  634. list_item_spec """
  635. module_spec = self.find_spec_part(identifier)
  636. if module_spec is None:
  637. raise isc.cc.data.DataNotFoundError("Unknown item " + str(identifier))
  638. value = None
  639. if value_str is not None:
  640. value = isc.cc.data.parse_value_str(value_str)
  641. # for type any, we determine the 'type' by what value is set
  642. # (which would be either list or dict)
  643. cur_value, _ = self.get_value(identifier)
  644. type_any = isc.config.config_data.spec_part_is_any(module_spec)
  645. # there's two forms of 'remove from list'; the remove-value-from-list
  646. # form, and the 'remove-by-index' form. We can recognize the second
  647. # case by value is None
  648. if 'list_item_spec' in module_spec or\
  649. (type_any and type(cur_value) == list) or\
  650. value is None:
  651. if not type_any and value is not None:
  652. isc.config.config_data.check_type(module_spec['list_item_spec'], value)
  653. self._remove_value_from_list(identifier, value)
  654. elif 'named_set_item_spec' in module_spec or\
  655. (type_any and type(cur_value) == dict):
  656. self._remove_value_from_named_set(identifier, value_str)
  657. else:
  658. raise isc.cc.data.DataTypeError(str(identifier) + " is not a list or a named_set")
  659. def commit(self):
  660. """Commit all local changes, send them through b10-cmdctl to
  661. the configuration manager"""
  662. if self.get_local_changes():
  663. response = self._conn.send_POST('/ConfigManager/set_config',
  664. [ self.get_local_changes() ])
  665. answer = isc.cc.data.parse_value_str(response.read().decode())
  666. # answer is either an empty dict (on success), or one
  667. # containing errors
  668. if answer == {}:
  669. self.clear_local_changes()
  670. elif "error" in answer:
  671. raise ModuleCCSessionError("Error: " + str(answer["error"]) + "\n" + "Configuration not committed")
  672. else:
  673. raise ModuleCCSessionError("Unknown format of answer in commit(): " + str(answer))