cfgmgr.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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
  29. class ConfigManagerDataReadError(Exception):
  30. """This exception is thrown when there is an error while reading
  31. the current configuration on startup."""
  32. pass
  33. class ConfigManagerDataEmpty(Exception):
  34. """This exception is thrown when the currently stored configuration
  35. is not found, or appears empty."""
  36. pass
  37. class ConfigManagerData:
  38. """This class hold the actual configuration information, and
  39. reads it from and writes it to persistent storage"""
  40. def __init__(self, data_path, file_name = "b10-config.db"):
  41. """Initialize the data for the configuration manager, and
  42. set the version and path for the data store. Initializing
  43. this does not yet read the database, a call to
  44. read_from_file is needed for that."""
  45. self.data = {}
  46. self.data['version'] = config_data.BIND10_CONFIG_DATA_VERSION
  47. self.data_path = data_path
  48. self.db_filename = data_path + os.sep + file_name
  49. def read_from_file(data_path, file_name = "b10-config.db"):
  50. """Read the current configuration found in the file at
  51. data_path. If the file does not exist, a
  52. ConfigManagerDataEmpty exception is raised. If there is a
  53. parse error, or if the data in the file has the wrong
  54. version, a ConfigManagerDataReadError is raised. In the first
  55. case, it is probably safe to log and ignore. In the case of
  56. the second exception, the best way is probably to report the
  57. error and stop loading the system."""
  58. config = ConfigManagerData(data_path, file_name)
  59. file = None
  60. try:
  61. file = open(config.db_filename, 'r')
  62. file_config = json.loads(file.read())
  63. # handle different versions here
  64. # If possible, we automatically convert to the new
  65. # scheme and update the configuration
  66. # If not, we raise an exception
  67. if 'version' in file_config:
  68. if file_config['version'] == config_data.BIND10_CONFIG_DATA_VERSION:
  69. config.data = file_config
  70. elif file_config['version'] == 1:
  71. # only format change, no other changes necessary
  72. file_config['version'] = 2
  73. print("[b10-cfgmgr] Updating configuration database version from 1 to 2")
  74. config.data = file_config
  75. else:
  76. if config_data.BIND10_CONFIG_DATA_VERSION > file_config['version']:
  77. raise ConfigManagerDataReadError("Cannot load configuration file: version %d no longer supported" % file_config['version'])
  78. else:
  79. raise ConfigManagerDataReadError("Cannot load configuration file: version %d not yet supported" % file_config['version'])
  80. else:
  81. raise ConfigManagerDataReadError("No version information in configuration file " + config.db_filename)
  82. except IOError as ioe:
  83. # if IOError is 'no such file or directory', then continue
  84. # (raise empty), otherwise fail (raise error)
  85. if ioe.errno == errno.ENOENT:
  86. raise ConfigManagerDataEmpty("No configuration file found")
  87. else:
  88. raise ConfigManagerDataReadError("Can't read configuration file: " + str(ioe))
  89. except ValueError:
  90. raise ConfigManagerDataReadError("Configuration file out of date or corrupt, please update or remove " + config.db_filename)
  91. finally:
  92. if file:
  93. file.close();
  94. return config
  95. def write_to_file(self, output_file_name = None):
  96. """Writes the current configuration data to a file. If
  97. output_file_name is not specified, the file used in
  98. read_from_file is used."""
  99. filename = None
  100. try:
  101. file = tempfile.NamedTemporaryFile(mode='w',
  102. prefix="b10-config.db.",
  103. dir=self.data_path,
  104. delete=False)
  105. filename = file.name
  106. file.write(json.dumps(self.data))
  107. file.write("\n")
  108. file.close()
  109. if output_file_name:
  110. os.rename(filename, output_file_name)
  111. else:
  112. os.rename(filename, self.db_filename)
  113. except IOError as ioe:
  114. # TODO: log this (level critical)
  115. print("[b10-cfgmgr] Unable to write configuration file; configuration not stored: " + str(ioe))
  116. # TODO: debug option to keep file?
  117. except OSError as ose:
  118. # TODO: log this (level critical)
  119. print("[b10-cfgmgr] Unable to write configuration file; configuration not stored: " + str(ose))
  120. try:
  121. if filename and os.path.exists(filename):
  122. os.remove(filename)
  123. except OSError:
  124. # Ok if we really can't delete it anymore, leave it
  125. pass
  126. def __eq__(self, other):
  127. """Returns True if the data contained is equal. data_path and
  128. db_filename may be different."""
  129. if type(other) != type(self):
  130. return False
  131. return self.data == other.data
  132. class ConfigManager:
  133. """Creates a configuration manager. The data_path is the path
  134. to the directory containing the b10-config.db file.
  135. If session is set, this will be used as the communication
  136. channel session. If not, a new session will be created.
  137. The ability to specify a custom session is for testing purposes
  138. and should not be needed for normal usage."""
  139. def __init__(self, data_path, session = None):
  140. """Initialize the configuration manager. The data_path string
  141. is the path to the directory where the configuration is
  142. stored (in <data_path>/b10-config.db). Session is an optional
  143. cc-channel session. If this is not given, a new one is
  144. created"""
  145. self.data_path = data_path
  146. self.module_specs = {}
  147. self.config = ConfigManagerData(data_path)
  148. if session:
  149. self.cc = session
  150. else:
  151. self.cc = isc.cc.Session()
  152. self.cc.group_subscribe("ConfigManager")
  153. self.cc.group_subscribe("Boss", "ConfigManager")
  154. self.running = False
  155. def notify_boss(self):
  156. """Notifies the Boss module that the Config Manager is running"""
  157. self.cc.group_sendmsg({"running": "configmanager"}, "Boss")
  158. def set_module_spec(self, spec):
  159. """Adds a ModuleSpec"""
  160. self.module_specs[spec.get_module_name()] = spec
  161. def remove_module_spec(self, module_name):
  162. """Removes the full ModuleSpec for the given module_name.
  163. Does nothing if the module was not present."""
  164. if module_name in self.module_specs:
  165. del self.module_specs[module_name]
  166. def get_module_spec(self, module_name = None):
  167. """Returns the full ModuleSpec for the module with the given
  168. module_name. If no module name is given, a dict will
  169. be returned with 'name': module_spec values. If the
  170. module name is given, but does not exist, an empty dict
  171. is returned"""
  172. if module_name:
  173. if module_name in self.module_specs:
  174. return self.module_specs[module_name]
  175. else:
  176. # TODO: log error?
  177. return {}
  178. else:
  179. result = {}
  180. for module in self.module_specs:
  181. result[module] = self.module_specs[module].get_full_spec()
  182. return result
  183. def get_config_spec(self, name = None):
  184. """Returns a dict containing 'module_name': config_spec for
  185. all modules. If name is specified, only that module will
  186. be included"""
  187. config_data = {}
  188. if name:
  189. if name in self.module_specs:
  190. config_data[name] = self.module_specs[name].get_config_spec()
  191. else:
  192. for module_name in self.module_specs.keys():
  193. config_data[module_name] = self.module_specs[module_name].get_config_spec()
  194. return config_data
  195. def get_commands_spec(self, name = None):
  196. """Returns a dict containing 'module_name': commands_spec for
  197. all modules. If name is specified, only that module will
  198. be included"""
  199. commands = {}
  200. if name:
  201. if name in self.module_specs:
  202. commands[name] = self.module_specs[name].get_commands_spec()
  203. else:
  204. for module_name in self.module_specs.keys():
  205. commands[module_name] = self.module_specs[module_name].get_commands_spec()
  206. return commands
  207. def read_config(self):
  208. """Read the current configuration from the b10-config.db file
  209. at the path specificied at init()"""
  210. try:
  211. self.config = ConfigManagerData.read_from_file(self.data_path)
  212. except ConfigManagerDataEmpty:
  213. # ok, just start with an empty config
  214. self.config = ConfigManagerData(self.data_path)
  215. def write_config(self):
  216. """Write the current configuration to the b10-config.db file
  217. at the path specificied at init()"""
  218. self.config.write_to_file()
  219. def _handle_get_module_spec(self, cmd):
  220. """Private function that handles the 'get_module_spec' command"""
  221. answer = {}
  222. if cmd != None:
  223. if type(cmd) == dict:
  224. if 'module_name' in cmd and cmd['module_name'] != '':
  225. module_name = cmd['module_name']
  226. answer = ccsession.create_answer(0, self.get_module_spec(module_name))
  227. else:
  228. answer = ccsession.create_answer(1, "Bad module_name in get_module_spec command")
  229. else:
  230. answer = ccsession.create_answer(1, "Bad get_module_spec command, argument not a dict")
  231. else:
  232. answer = ccsession.create_answer(0, self.get_module_spec())
  233. return answer
  234. def _handle_get_config_dict(self, cmd):
  235. """Private function that handles the 'get_config' command
  236. where the command has been checked to be a dict"""
  237. if 'module_name' in cmd and cmd['module_name'] != '':
  238. module_name = cmd['module_name']
  239. try:
  240. return ccsession.create_answer(0, data.find(self.config.data, module_name))
  241. except data.DataNotFoundError as dnfe:
  242. # no data is ok, that means we have nothing that
  243. # deviates from default values
  244. return ccsession.create_answer(0, { 'version': config_data.BIND10_CONFIG_DATA_VERSION })
  245. else:
  246. return ccsession.create_answer(1, "Bad module_name in get_config command")
  247. def _handle_get_config(self, cmd):
  248. """Private function that handles the 'get_config' command"""
  249. if cmd != None:
  250. if type(cmd) == dict:
  251. return self._handle_get_config_dict(cmd)
  252. else:
  253. return ccsession.create_answer(1, "Bad get_config command, argument not a dict")
  254. else:
  255. return ccsession.create_answer(0, self.config.data)
  256. def _handle_set_config_module(self, module_name, cmd):
  257. # the answer comes (or does not come) from the relevant module
  258. # so we need a variable to see if we got it
  259. answer = None
  260. # todo: use api (and check the data against the definition?)
  261. old_data = copy.deepcopy(self.config.data)
  262. conf_part = data.find_no_exc(self.config.data, module_name)
  263. if conf_part:
  264. data.merge(conf_part, cmd)
  265. update_cmd = ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
  266. conf_part)
  267. seq = self.cc.group_sendmsg(update_cmd, module_name)
  268. try:
  269. answer, env = self.cc.group_recvmsg(False, seq)
  270. except isc.cc.SessionTimeout:
  271. answer = ccsession.create_answer(1, "Timeout waiting for answer from " + module_name)
  272. else:
  273. conf_part = data.set(self.config.data, module_name, {})
  274. data.merge(conf_part[module_name], cmd)
  275. # send out changed info
  276. update_cmd = ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
  277. conf_part[module_name])
  278. seq = self.cc.group_sendmsg(update_cmd, module_name)
  279. # replace 'our' answer with that of the module
  280. try:
  281. answer, env = self.cc.group_recvmsg(False, seq)
  282. except isc.cc.SessionTimeout:
  283. answer = ccsession.create_answer(1, "Timeout waiting for answer from " + module_name)
  284. if answer:
  285. rcode, val = ccsession.parse_answer(answer)
  286. if rcode == 0:
  287. self.write_config()
  288. else:
  289. self.config.data = old_data
  290. return answer
  291. def _handle_set_config_all(self, cmd):
  292. old_data = copy.deepcopy(self.config.data)
  293. got_error = False
  294. err_list = []
  295. # The format of the command is a dict with module->newconfig
  296. # sets, so we simply call set_config_module for each of those
  297. for module in cmd:
  298. if module != "version":
  299. answer = self._handle_set_config_module(module, cmd[module])
  300. if answer == None:
  301. got_error = True
  302. err_list.append("No answer message from " + module)
  303. else:
  304. rcode, val = ccsession.parse_answer(answer)
  305. if rcode != 0:
  306. got_error = True
  307. err_list.append(val)
  308. if not got_error:
  309. self.write_config()
  310. return ccsession.create_answer(0)
  311. else:
  312. # TODO rollback changes that did get through, should we re-send update?
  313. self.config.data = old_data
  314. return ccsession.create_answer(1, " ".join(err_list))
  315. def _handle_set_config(self, cmd):
  316. """Private function that handles the 'set_config' command"""
  317. answer = None
  318. if cmd == None:
  319. return ccsession.create_answer(1, "Wrong number of arguments")
  320. if len(cmd) == 2:
  321. answer = self._handle_set_config_module(cmd[0], cmd[1])
  322. elif len(cmd) == 1:
  323. answer = self._handle_set_config_all(cmd[0])
  324. else:
  325. answer = ccsession.create_answer(1, "Wrong number of arguments")
  326. if not answer:
  327. answer = ccsession.create_answer(1, "No answer message from " + cmd[0])
  328. return answer
  329. def _handle_module_spec(self, spec):
  330. """Private function that handles the 'module_spec' command"""
  331. # todo: validate? (no direct access to spec as
  332. # todo: use ModuleSpec class
  333. # todo: error checking (like keyerrors)
  334. answer = {}
  335. self.set_module_spec(spec)
  336. # We should make one general 'spec update for module' that
  337. # passes both specification and commands at once
  338. spec_update = ccsession.create_command(ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
  339. [ spec.get_module_name(), spec.get_full_spec() ])
  340. self.cc.group_sendmsg(spec_update, "Cmdctl")
  341. return ccsession.create_answer(0)
  342. def handle_msg(self, msg):
  343. """Handle a command from the cc channel to the configuration manager"""
  344. answer = {}
  345. cmd, arg = ccsession.parse_command(msg)
  346. if cmd:
  347. if cmd == ccsession.COMMAND_GET_COMMANDS_SPEC:
  348. answer = ccsession.create_answer(0, self.get_commands_spec())
  349. elif cmd == ccsession.COMMAND_GET_MODULE_SPEC:
  350. answer = self._handle_get_module_spec(arg)
  351. elif cmd == ccsession.COMMAND_GET_CONFIG:
  352. answer = self._handle_get_config(arg)
  353. elif cmd == ccsession.COMMAND_SET_CONFIG:
  354. answer = self._handle_set_config(arg)
  355. elif cmd == ccsession.COMMAND_SHUTDOWN:
  356. # TODO: logging
  357. #print("[b10-cfgmgr] Received shutdown command")
  358. self.running = False
  359. answer = ccsession.create_answer(0)
  360. elif cmd == ccsession.COMMAND_MODULE_SPEC:
  361. try:
  362. answer = self._handle_module_spec(isc.config.ModuleSpec(arg))
  363. except isc.config.ModuleSpecError as dde:
  364. answer = ccsession.create_answer(1, "Error in data definition: " + str(dde))
  365. else:
  366. answer = ccsession.create_answer(1, "Unknown command: " + str(cmd))
  367. else:
  368. answer = ccsession.create_answer(1, "Unknown message format: " + str(msg))
  369. return answer
  370. def run(self):
  371. """Runs the configuration manager."""
  372. self.running = True
  373. while (self.running):
  374. # we just wait eternally for any command here, so disable
  375. # timeouts for this specific recv
  376. self.cc.set_timeout(0)
  377. msg, env = self.cc.group_recvmsg(False)
  378. # and set it back to whatever we default to
  379. self.cc.set_timeout(isc.cc.Session.MSGQ_DEFAULT_TIMEOUT)
  380. # ignore 'None' value (even though they should not occur)
  381. # and messages that are answers to questions we did
  382. # not ask
  383. if msg is not None and not 'result' in msg:
  384. answer = self.handle_msg(msg);
  385. self.cc.group_reply(env, answer)