cfgmgr.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. # Copyright (C) 2010 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. """This is the BIND 10 configuration manager, run by b10-cfgmgr.
  16. It stores the system configuration, and sends updates of the
  17. configuration to the modules that need them.
  18. """
  19. import isc
  20. import signal
  21. import ast
  22. import os
  23. import copy
  24. import tempfile
  25. import json
  26. import errno
  27. from isc.cc import data
  28. from isc.config import ccsession, config_data, module_spec
  29. from isc.util.file import path_search
  30. import bind10_config
  31. import isc.log
  32. from isc.log_messages.cfgmgr_messages import *
  33. logger = isc.log.Logger("cfgmgr")
  34. class ConfigManagerDataReadError(Exception):
  35. """This exception is thrown when there is an error while reading
  36. the current configuration on startup."""
  37. pass
  38. class ConfigManagerDataEmpty(Exception):
  39. """This exception is thrown when the currently stored configuration
  40. is not found, or appears empty."""
  41. pass
  42. class ConfigManagerData:
  43. """This class hold the actual configuration information, and
  44. reads it from and writes it to persistent storage"""
  45. def __init__(self, data_path, file_name):
  46. """Initialize the data for the configuration manager, and
  47. set the version and path for the data store. Initializing
  48. this does not yet read the database, a call to
  49. read_from_file is needed for that.
  50. In case the file_name is absolute, data_path is ignored
  51. and the directory where the file_name lives is used instead.
  52. """
  53. self.data = {}
  54. self.data['version'] = config_data.BIND10_CONFIG_DATA_VERSION
  55. if os.path.isabs(file_name):
  56. self.db_filename = file_name
  57. self.data_path = os.path.dirname(file_name)
  58. else:
  59. self.db_filename = data_path + os.sep + file_name
  60. self.data_path = data_path
  61. def read_from_file(data_path, file_name):
  62. """Read the current configuration found in the file file_name.
  63. If file_name is absolute, data_path is ignored. Otherwise
  64. we look for the file_name in data_path directory.
  65. If the file does not exist, a ConfigManagerDataEmpty exception is
  66. raised. If there is a parse error, or if the data in the file has
  67. the wrong version, a ConfigManagerDataReadError is raised. In the
  68. first case, it is probably safe to log and ignore. In the case of
  69. the second exception, the best way is probably to report the error
  70. and stop loading the system.
  71. """
  72. config = ConfigManagerData(data_path, file_name)
  73. file = None
  74. try:
  75. file = open(config.db_filename, 'r')
  76. file_config = json.loads(file.read())
  77. # handle different versions here
  78. # If possible, we automatically convert to the new
  79. # scheme and update the configuration
  80. # If not, we raise an exception
  81. if 'version' in file_config:
  82. if file_config['version'] == config_data.BIND10_CONFIG_DATA_VERSION:
  83. config.data = file_config
  84. elif file_config['version'] == 1:
  85. # only format change, no other changes necessary
  86. file_config['version'] = 2
  87. logger.info(CFGMGR_AUTOMATIC_CONFIG_DATABASE_UPDATE, 1, 2)
  88. config.data = file_config
  89. else:
  90. if config_data.BIND10_CONFIG_DATA_VERSION > file_config['version']:
  91. raise ConfigManagerDataReadError("Cannot load configuration file: version %d no longer supported" % file_config['version'])
  92. else:
  93. raise ConfigManagerDataReadError("Cannot load configuration file: version %d not yet supported" % file_config['version'])
  94. else:
  95. raise ConfigManagerDataReadError("No version information in configuration file " + config.db_filename)
  96. except IOError as ioe:
  97. # if IOError is 'no such file or directory', then continue
  98. # (raise empty), otherwise fail (raise error)
  99. if ioe.errno == errno.ENOENT:
  100. raise ConfigManagerDataEmpty("No configuration file found")
  101. else:
  102. raise ConfigManagerDataReadError("Can't read configuration file: " + str(ioe))
  103. except ValueError:
  104. raise ConfigManagerDataReadError("Configuration file out of date or corrupt, please update or remove " + config.db_filename)
  105. finally:
  106. if file:
  107. file.close();
  108. return config
  109. def write_to_file(self, output_file_name = None):
  110. """Writes the current configuration data to a file. If
  111. output_file_name is not specified, the file used in
  112. read_from_file is used."""
  113. filename = None
  114. try:
  115. file = tempfile.NamedTemporaryFile(mode='w',
  116. prefix="b10-config.db.",
  117. dir=self.data_path,
  118. delete=False)
  119. filename = file.name
  120. file.write(json.dumps(self.data))
  121. file.write("\n")
  122. file.close()
  123. if output_file_name:
  124. os.rename(filename, output_file_name)
  125. else:
  126. os.rename(filename, self.db_filename)
  127. except IOError as ioe:
  128. logger.error(CFGMGR_IOERROR_WHILE_WRITING_CONFIGURATION, ioe)
  129. except OSError as ose:
  130. logger.error(CFGMGR_OSERROR_WHILE_WRITING_CONFIGURATION, ose)
  131. try:
  132. if filename and os.path.exists(filename):
  133. os.remove(filename)
  134. except OSError:
  135. # Ok if we really can't delete it anymore, leave it
  136. pass
  137. def __eq__(self, other):
  138. """Returns True if the data contained is equal. data_path and
  139. db_filename may be different."""
  140. if type(other) != type(self):
  141. return False
  142. return self.data == other.data
  143. class ConfigManager:
  144. """Creates a configuration manager. The data_path is the path
  145. to the directory containing the configuraton file,
  146. database_filename points to the configuration file.
  147. If session is set, this will be used as the communication
  148. channel session. If not, a new session will be created.
  149. The ability to specify a custom session is for testing purposes
  150. and should not be needed for normal usage."""
  151. def __init__(self, data_path, database_filename, session=None):
  152. """Initialize the configuration manager. The data_path string
  153. is the path to the directory where the configuration is
  154. stored (in <data_path>/<database_filename> or in
  155. <database_filename>, if it is absolute). The dabase_filename
  156. is the config file to load. Session is an optional
  157. cc-channel session. If this is not given, a new one is
  158. created."""
  159. self.data_path = data_path
  160. self.database_filename = database_filename
  161. self.module_specs = {}
  162. # Virtual modules are the ones which have no process running. The
  163. # checking of validity is done by functions presented here instead
  164. # of some other process
  165. self.virtual_modules = {}
  166. self.config = ConfigManagerData(data_path, database_filename)
  167. if session:
  168. self.cc = session
  169. else:
  170. self.cc = isc.cc.Session()
  171. self.cc.group_subscribe("ConfigManager")
  172. self.cc.group_subscribe("Boss", "ConfigManager")
  173. self.running = False
  174. # As a core module, CfgMgr is different than other modules,
  175. # as it does not use a ModuleCCSession, and hence needs
  176. # to handle logging config on its own
  177. self.log_config_data = config_data.ConfigData(
  178. isc.config.module_spec_from_file(
  179. path_search('logging.spec',
  180. bind10_config.PLUGIN_PATHS)))
  181. # store the logging 'module' name for easier reference
  182. self.log_module_name = self.log_config_data.get_module_spec().get_module_name()
  183. def check_logging_config(self, config):
  184. if self.log_module_name in config:
  185. ccsession.default_logconfig_handler(config[self.log_module_name],
  186. self.log_config_data)
  187. def notify_boss(self):
  188. """Notifies the Boss module that the Config Manager is running"""
  189. self.cc.group_sendmsg({"running": "ConfigManager"}, "Boss")
  190. def set_module_spec(self, spec):
  191. """Adds a ModuleSpec"""
  192. self.module_specs[spec.get_module_name()] = spec
  193. def set_virtual_module(self, spec, check_func):
  194. """Adds a virtual module with its spec and checking function."""
  195. self.module_specs[spec.get_module_name()] = spec
  196. self.virtual_modules[spec.get_module_name()] = check_func
  197. def remove_module_spec(self, module_name):
  198. """Removes the full ModuleSpec for the given module_name.
  199. Also removes the virtual module check function if it
  200. was present.
  201. Does nothing if the module was not present."""
  202. if module_name in self.module_specs:
  203. del self.module_specs[module_name]
  204. if module_name in self.virtual_modules:
  205. del self.virtual_modules[module_name]
  206. def get_module_spec(self, module_name = None):
  207. """Returns the full ModuleSpec for the module with the given
  208. module_name. If no module name is given, a dict will
  209. be returned with 'name': module_spec values. If the
  210. module name is given, but does not exist, an empty dict
  211. is returned"""
  212. if module_name:
  213. if module_name in self.module_specs:
  214. return self.module_specs[module_name].get_full_spec()
  215. else:
  216. # TODO: log error?
  217. return {}
  218. else:
  219. result = {}
  220. for module in self.module_specs:
  221. result[module] = self.module_specs[module].get_full_spec()
  222. return result
  223. def get_config_spec(self, name = None):
  224. """Returns a dict containing 'module_name': config_spec for
  225. all modules. If name is specified, only that module will
  226. be included"""
  227. config_data = {}
  228. if name:
  229. if name in self.module_specs:
  230. config_data[name] = self.module_specs[name].get_config_spec()
  231. else:
  232. for module_name in self.module_specs.keys():
  233. config_data[module_name] = self.module_specs[module_name].get_config_spec()
  234. return config_data
  235. def get_commands_spec(self, name = None):
  236. """Returns a dict containing 'module_name': commands_spec for
  237. all modules. If name is specified, only that module will
  238. be included"""
  239. commands = {}
  240. if name:
  241. if name in self.module_specs:
  242. commands[name] = self.module_specs[name].get_commands_spec()
  243. else:
  244. for module_name in self.module_specs.keys():
  245. commands[module_name] = self.module_specs[module_name].get_commands_spec()
  246. return commands
  247. def get_statistics_spec(self, name = None):
  248. """Returns a dict containing 'module_name': statistics_spec for
  249. all modules. If name is specified, only that module will
  250. be included"""
  251. statistics = {}
  252. if name:
  253. if name in self.module_specs:
  254. statistics[name] = self.module_specs[name].get_statistics_spec()
  255. else:
  256. for module_name in self.module_specs.keys():
  257. statistics[module_name] = self.module_specs[module_name].get_statistics_spec()
  258. return statistics
  259. def read_config(self):
  260. """Read the current configuration from the file specificied at init()"""
  261. try:
  262. self.config = ConfigManagerData.read_from_file(self.data_path,
  263. self.\
  264. database_filename)
  265. self.check_logging_config(self.config.data);
  266. except ConfigManagerDataEmpty:
  267. # ok, just start with an empty config
  268. self.config = ConfigManagerData(self.data_path,
  269. self.database_filename)
  270. def write_config(self):
  271. """Write the current configuration to the file specificied at init()"""
  272. self.config.write_to_file()
  273. def _handle_get_module_spec(self, cmd):
  274. """Private function that handles the 'get_module_spec' command"""
  275. answer = {}
  276. if cmd != None:
  277. if type(cmd) == dict:
  278. if 'module_name' in cmd and cmd['module_name'] != '':
  279. module_name = cmd['module_name']
  280. spec = self.get_module_spec(cmd['module_name'])
  281. if type(spec) != type({}):
  282. # this is a ModuleSpec object. Extract the
  283. # internal spec.
  284. spec = spec.get_full_spec()
  285. answer = ccsession.create_answer(0, spec)
  286. else:
  287. answer = ccsession.create_answer(1, "Bad module_name in get_module_spec command")
  288. else:
  289. answer = ccsession.create_answer(1, "Bad get_module_spec command, argument not a dict")
  290. else:
  291. answer = ccsession.create_answer(0, self.get_module_spec())
  292. return answer
  293. def _handle_get_config_dict(self, cmd):
  294. """Private function that handles the 'get_config' command
  295. where the command has been checked to be a dict"""
  296. if 'module_name' in cmd and cmd['module_name'] != '':
  297. module_name = cmd['module_name']
  298. try:
  299. return ccsession.create_answer(0, data.find(self.config.data, module_name))
  300. except data.DataNotFoundError as dnfe:
  301. # no data is ok, that means we have nothing that
  302. # deviates from default values
  303. return ccsession.create_answer(0, { 'version': config_data.BIND10_CONFIG_DATA_VERSION })
  304. else:
  305. return ccsession.create_answer(1, "Bad module_name in get_config command")
  306. def _handle_get_config(self, cmd):
  307. """Private function that handles the 'get_config' command"""
  308. if cmd != None:
  309. if type(cmd) == dict:
  310. return self._handle_get_config_dict(cmd)
  311. else:
  312. return ccsession.create_answer(1, "Bad get_config command, argument not a dict")
  313. else:
  314. return ccsession.create_answer(0, self.config.data)
  315. def _handle_set_config_module(self, module_name, cmd):
  316. # the answer comes (or does not come) from the relevant module
  317. # so we need a variable to see if we got it
  318. answer = None
  319. # todo: use api (and check the data against the definition?)
  320. old_data = copy.deepcopy(self.config.data)
  321. conf_part = data.find_no_exc(self.config.data, module_name)
  322. update_cmd = None
  323. use_part = None
  324. if conf_part:
  325. data.merge(conf_part, cmd)
  326. use_part = conf_part
  327. else:
  328. conf_part = data.set(self.config.data, module_name, {})
  329. data.merge(conf_part[module_name], cmd)
  330. use_part = conf_part[module_name]
  331. # The command to send
  332. update_cmd = ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
  333. use_part)
  334. # TODO: This design might need some revisiting. We might want some
  335. # polymorphism instead of branching. But it just might turn out it
  336. # will get solved by itself when we move everything to virtual modules
  337. # (which is possible solution to the offline configuration problem)
  338. # or when we solve the incorect behaviour here when a config is
  339. # rejected (spying modules don't know it was rejected and some modules
  340. # might have been commited already).
  341. if module_name in self.virtual_modules:
  342. # The module is virtual, so call it to get the answer
  343. try:
  344. error = self.virtual_modules[module_name](use_part)
  345. if error is None:
  346. answer = ccsession.create_answer(0)
  347. # OK, it is successful, send the notify, but don't wait
  348. # for answer
  349. seq = self.cc.group_sendmsg(update_cmd, module_name)
  350. else:
  351. answer = ccsession.create_answer(1, error)
  352. # Make sure just a validating plugin don't kill the whole manager
  353. except Exception as excp:
  354. # Provide answer
  355. answer = ccsession.create_answer(1, "Exception: " + str(excp))
  356. else:
  357. # Real module, send it over the wire to it
  358. # send out changed info and wait for answer
  359. seq = self.cc.group_sendmsg(update_cmd, module_name)
  360. try:
  361. # replace 'our' answer with that of the module
  362. answer, env = self.cc.group_recvmsg(False, seq)
  363. except isc.cc.SessionTimeout:
  364. answer = ccsession.create_answer(1, "Timeout waiting for answer from " + module_name)
  365. except isc.cc.SessionError as se:
  366. logger.error(CFGMGR_BAD_UPDATE_RESPONSE_FROM_MODULE, module_name, se)
  367. answer = ccsession.create_answer(1, "Unable to parse response from " + module_name + ": " + str(se))
  368. if answer:
  369. rcode, val = ccsession.parse_answer(answer)
  370. if rcode == 0:
  371. self.write_config()
  372. else:
  373. self.config.data = old_data
  374. return answer
  375. def _handle_set_config_all(self, cmd):
  376. old_data = copy.deepcopy(self.config.data)
  377. got_error = False
  378. err_list = []
  379. # The format of the command is a dict with module->newconfig
  380. # sets, so we simply call set_config_module for each of those
  381. for module in cmd:
  382. if module != "version":
  383. answer = self._handle_set_config_module(module, cmd[module])
  384. if answer == None:
  385. got_error = True
  386. err_list.append("No answer message from " + module)
  387. else:
  388. rcode, val = ccsession.parse_answer(answer)
  389. if rcode != 0:
  390. got_error = True
  391. err_list.append(val)
  392. if not got_error:
  393. # if Logging config is in there, update our config as well
  394. self.check_logging_config(cmd);
  395. self.write_config()
  396. return ccsession.create_answer(0)
  397. else:
  398. # TODO rollback changes that did get through, should we re-send update?
  399. self.config.data = old_data
  400. return ccsession.create_answer(1, " ".join(err_list))
  401. def _handle_set_config(self, cmd):
  402. """Private function that handles the 'set_config' command"""
  403. answer = None
  404. if cmd == None:
  405. return ccsession.create_answer(1, "Wrong number of arguments")
  406. if len(cmd) == 2:
  407. answer = self._handle_set_config_module(cmd[0], cmd[1])
  408. elif len(cmd) == 1:
  409. answer = self._handle_set_config_all(cmd[0])
  410. else:
  411. answer = ccsession.create_answer(1, "Wrong number of arguments")
  412. if not answer:
  413. answer = ccsession.create_answer(1, "No answer message from " + cmd[0])
  414. return answer
  415. def _handle_module_spec(self, spec):
  416. """Private function that handles the 'module_spec' command"""
  417. # todo: validate? (no direct access to spec as
  418. # todo: use ModuleSpec class
  419. # todo: error checking (like keyerrors)
  420. answer = {}
  421. self.set_module_spec(spec)
  422. # We should make one general 'spec update for module' that
  423. # passes both specification and commands at once
  424. spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
  425. [ spec.get_module_name(), spec.get_full_spec() ])
  426. self.cc.group_sendmsg(spec_update, "Cmdctl")
  427. return ccsession.create_answer(0)
  428. def handle_msg(self, msg):
  429. """Handle a command from the cc channel to the configuration manager"""
  430. answer = {}
  431. cmd, arg = ccsession.parse_command(msg)
  432. if cmd:
  433. if cmd == ccsession.COMMAND_GET_COMMANDS_SPEC:
  434. answer = ccsession.create_answer(0, self.get_commands_spec())
  435. elif cmd == ccsession.COMMAND_GET_STATISTICS_SPEC:
  436. answer = ccsession.create_answer(0, self.get_statistics_spec())
  437. elif cmd == ccsession.COMMAND_GET_MODULE_SPEC:
  438. answer = self._handle_get_module_spec(arg)
  439. elif cmd == ccsession.COMMAND_GET_CONFIG:
  440. answer = self._handle_get_config(arg)
  441. elif cmd == ccsession.COMMAND_SET_CONFIG:
  442. answer = self._handle_set_config(arg)
  443. elif cmd == ccsession.COMMAND_SHUTDOWN:
  444. self.running = False
  445. answer = ccsession.create_answer(0)
  446. elif cmd == ccsession.COMMAND_MODULE_SPEC:
  447. try:
  448. answer = self._handle_module_spec(isc.config.ModuleSpec(arg))
  449. except isc.config.ModuleSpecError as dde:
  450. answer = ccsession.create_answer(1, "Error in data definition: " + str(dde))
  451. else:
  452. answer = ccsession.create_answer(1, "Unknown command: " + str(cmd))
  453. else:
  454. answer = ccsession.create_answer(1, "Unknown message format: " + str(msg))
  455. return answer
  456. def run(self):
  457. """Runs the configuration manager."""
  458. self.running = True
  459. while (self.running):
  460. # we just wait eternally for any command here, so disable
  461. # timeouts for this specific recv
  462. self.cc.set_timeout(0)
  463. msg, env = self.cc.group_recvmsg(False)
  464. # and set it back to whatever we default to
  465. self.cc.set_timeout(isc.cc.Session.MSGQ_DEFAULT_TIMEOUT)
  466. # ignore 'None' value (even though they should not occur)
  467. # and messages that are answers to questions we did
  468. # not ask
  469. if msg is not None and not 'result' in msg:
  470. answer = self.handle_msg(msg);
  471. self.cc.group_reply(env, answer)