cfgmgr.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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 pprint
  23. import os
  24. import copy
  25. from isc.cc import data
  26. class ConfigManagerDataReadError(Exception):
  27. """This exception is thrown when there is an error while reading
  28. the current configuration on startup."""
  29. pass
  30. class ConfigManagerDataEmpty(Exception):
  31. """This exception is thrown when the currently stored configuration
  32. is not found, or appears empty."""
  33. pass
  34. class ConfigManagerData:
  35. """This class hold the actual configuration information, and
  36. reads it from and writes it to persistent storage"""
  37. CONFIG_VERSION = 1
  38. def __init__(self, data_path, file_name = "b10-config.db"):
  39. """Initialize the data for the configuration manager, and
  40. set the version and path for the data store. Initializing
  41. this does not yet read the database, a call to
  42. read_from_file is needed for that."""
  43. self.data = {}
  44. self.data['version'] = ConfigManagerData.CONFIG_VERSION
  45. self.data_path = data_path
  46. self.db_filename = data_path + os.sep + file_name
  47. def read_from_file(data_path, file_name = "b10-config.db"):
  48. """Read the current configuration found in the file at
  49. data_path. If the file does not exist, a
  50. ConfigManagerDataEmpty exception is raised. If there is a
  51. parse error, or if the data in the file has the wrong
  52. version, a ConfigManagerDataReadError is raised. In the first
  53. case, it is probably safe to log and ignore. In the case of
  54. the second exception, the best way is probably to report the
  55. error and stop loading the system."""
  56. config = ConfigManagerData(data_path, file_name)
  57. try:
  58. file = open(config.db_filename, 'r')
  59. file_config = ast.literal_eval(file.read())
  60. if 'version' in file_config and \
  61. file_config['version'] == ConfigManagerData.CONFIG_VERSION:
  62. config.data = file_config
  63. else:
  64. # We can put in a migration path here for old data
  65. raise ConfigManagerDataReadError("[b10-cfgmgr] Old version of data found")
  66. file.close()
  67. except IOError as ioe:
  68. raise ConfigManagerDataEmpty("No config file found")
  69. except:
  70. raise ConfigManagerDataReadError("Config file unreadable")
  71. return config
  72. def write_to_file(self, output_file_name = None):
  73. """Writes the current configuration data to a file. If
  74. output_file_name is not specified, the file used in
  75. read_from_file is used."""
  76. try:
  77. tmp_filename = self.db_filename + ".tmp"
  78. file = open(tmp_filename, 'w');
  79. pp = pprint.PrettyPrinter(indent=4)
  80. s = pp.pformat(self.data)
  81. file.write(s)
  82. file.write("\n")
  83. file.close()
  84. if output_file_name:
  85. os.rename(tmp_filename, output_file_name)
  86. else:
  87. os.rename(tmp_filename, self.db_filename)
  88. except IOError as ioe:
  89. # TODO: log this (level critical)
  90. print("[b10-cfgmgr] Unable to write config file; configuration not stored: " + str(ioe))
  91. except OSError as ose:
  92. # TODO: log this (level critical)
  93. print("[b10-cfgmgr] Unable to write config file; configuration not stored: " + str(ose))
  94. def __eq__(self, other):
  95. """Returns True if the data contained is equal. data_path and
  96. db_filename may be different."""
  97. if type(other) != type(self):
  98. return False
  99. return self.data == other.data
  100. class ConfigManager:
  101. """Creates a configuration manager. The data_path is the path
  102. to the directory containing the b10-config.db file.
  103. If session is set, this will be used as the communication
  104. channel session. If not, a new session will be created.
  105. The ability to specify a custom session is for testing purposes
  106. and should not be needed for normal usage."""
  107. def __init__(self, data_path, session = None):
  108. """Initialize the configuration manager. The data_path string
  109. is the path to the directory where the configuration is
  110. stored (in <data_path>/b10-config.db). Session is an optional
  111. cc-channel session. If this is not given, a new one is
  112. created"""
  113. self.data_path = data_path
  114. self.module_specs = {}
  115. self.config = ConfigManagerData(data_path)
  116. if session:
  117. self.cc = session
  118. else:
  119. self.cc = isc.cc.Session()
  120. self.cc.group_subscribe("ConfigManager")
  121. self.cc.group_subscribe("Boss", "ConfigManager")
  122. self.running = False
  123. def notify_boss(self):
  124. """Notifies the Boss module that the Config Manager is running"""
  125. self.cc.group_sendmsg({"running": "configmanager"}, "Boss")
  126. def set_module_spec(self, spec):
  127. """Adds a ModuleSpec"""
  128. self.module_specs[spec.get_module_name()] = spec
  129. def remove_module_spec(self, module_name):
  130. """Removes the full ModuleSpec for the given module_name.
  131. Does nothing if the module was not present."""
  132. if module_name in self.module_specs:
  133. del self.module_specs[module_name]
  134. def get_module_spec(self, module_name):
  135. """Returns the full ModuleSpec for the module with the given
  136. module_name"""
  137. if module_name in self.module_specs:
  138. return self.module_specs[module_name]
  139. def get_config_spec(self, name = None):
  140. """Returns a dict containing 'module_name': config_spec for
  141. all modules. If name is specified, only that module will
  142. be included"""
  143. config_data = {}
  144. if name:
  145. if name in self.module_specs:
  146. config_data[name] = self.module_specs[name].get_config_spec()
  147. else:
  148. for module_name in self.module_specs.keys():
  149. config_data[module_name] = self.module_specs[module_name].get_config_spec()
  150. return config_data
  151. def get_commands_spec(self, name = None):
  152. """Returns a dict containing 'module_name': commands_spec for
  153. all modules. If name is specified, only that module will
  154. be included"""
  155. commands = {}
  156. if name:
  157. if name in self.module_specs:
  158. commands[name] = self.module_specs[name].get_commands_spec()
  159. else:
  160. for module_name in self.module_specs.keys():
  161. commands[module_name] = self.module_specs[module_name].get_commands_spec()
  162. return commands
  163. def read_config(self):
  164. """Read the current configuration from the b10-config.db file
  165. at the path specificied at init()"""
  166. try:
  167. self.config = ConfigManagerData.read_from_file(self.data_path)
  168. except ConfigManagerDataEmpty:
  169. # ok, just start with an empty config
  170. self.config = ConfigManagerData(self.data_path)
  171. def write_config(self):
  172. """Write the current configuration to the b10-config.db file
  173. at the path specificied at init()"""
  174. self.config.write_to_file()
  175. def _handle_get_module_spec(self, cmd):
  176. """Private function that handles the 'get_module_spec' command"""
  177. answer = {}
  178. if cmd != None:
  179. if type(cmd) == dict:
  180. if 'module_name' in cmd and cmd['module_name'] != '':
  181. module_name = cmd['module_name']
  182. answer = isc.config.ccsession.create_answer(0, self.get_config_spec(module_name))
  183. else:
  184. answer = isc.config.ccsession.create_answer(1, "Bad module_name in get_module_spec command")
  185. else:
  186. answer = isc.config.ccsession.create_answer(1, "Bad get_module_spec command, argument not a dict")
  187. else:
  188. answer = isc.config.ccsession.create_answer(0, self.get_config_spec())
  189. return answer
  190. def _handle_get_config(self, cmd):
  191. """Private function that handles the 'get_config' command"""
  192. answer = {}
  193. if cmd != None:
  194. if type(cmd) == dict:
  195. if 'module_name' in cmd and cmd['module_name'] != '':
  196. module_name = cmd['module_name']
  197. try:
  198. answer = isc.config.ccsession.create_answer(0, data.find(self.config.data, module_name))
  199. except data.DataNotFoundError as dnfe:
  200. # no data is ok, that means we have nothing that
  201. # deviates from default values
  202. answer = isc.config.ccsession.create_answer(0, {})
  203. else:
  204. answer = isc.config.ccsession.create_answer(1, "Bad module_name in get_config command")
  205. else:
  206. answer = isc.config.ccsession.create_answer(1, "Bad get_config command, argument not a dict")
  207. else:
  208. answer = isc.config.ccsession.create_answer(0, self.config.data)
  209. return answer
  210. def _handle_set_config(self, cmd):
  211. """Private function that handles the 'set_config' command"""
  212. answer = None
  213. if cmd == None:
  214. return isc.config.ccsession.create_answer(1, "Wrong number of arguments")
  215. if len(cmd) == 2:
  216. # todo: use api (and check the data against the definition?)
  217. old_data = copy.deepcopy(self.config.data)
  218. module_name = cmd[0]
  219. conf_part = data.find_no_exc(self.config.data, module_name)
  220. if conf_part:
  221. data.merge(conf_part, cmd[1])
  222. update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, conf_part)
  223. self.cc.group_sendmsg(update_cmd, module_name)
  224. answer, env = self.cc.group_recvmsg(False)
  225. else:
  226. conf_part = data.set(self.config.data, module_name, {})
  227. data.merge(conf_part[module_name], cmd[1])
  228. # send out changed info
  229. update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, conf_part[module_name])
  230. self.cc.group_sendmsg(update_cmd, module_name)
  231. # replace 'our' answer with that of the module
  232. answer, env = self.cc.group_recvmsg(False)
  233. if answer:
  234. rcode, val = isc.config.ccsession.parse_answer(answer)
  235. if rcode == 0:
  236. self.write_config()
  237. else:
  238. self.config.data = old_data
  239. elif len(cmd) == 1:
  240. old_data = copy.deepcopy(self.config.data)
  241. data.merge(self.config.data, cmd[0])
  242. # send out changed info
  243. got_error = False
  244. err_list = []
  245. for module in self.config.data:
  246. if module != "version" and self.config.data[module] != old_data[module]:
  247. update_cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, self.config.data[module])
  248. print("[XX] send update: " + str(update_cmd))
  249. print("[XX] to: " + str(module))
  250. self.cc.group_sendmsg(update_cmd, module)
  251. answer, env = self.cc.group_recvmsg(False)
  252. if answer == None:
  253. got_error = True
  254. err_list.append("No answer message from " + module)
  255. else:
  256. rcode, val = isc.config.ccsession.parse_answer(answer)
  257. if rcode != 0:
  258. got_error = True
  259. err_list.append(val)
  260. if not got_error:
  261. self.write_config()
  262. answer = isc.config.ccsession.create_answer(0)
  263. else:
  264. # TODO rollback changes that did get through, should we re-send update?
  265. self.config.data = old_data
  266. answer = isc.config.ccsession.create_answer(1, " ".join(err_list))
  267. else:
  268. answer = isc.config.ccsession.create_answer(1, "Wrong number of arguments")
  269. if not answer:
  270. answer = isc.config.ccsession.create_answer(1, "No answer message from " + cmd[0])
  271. return answer
  272. def _handle_module_spec(self, spec):
  273. """Private function that handles the 'module_spec' command"""
  274. # todo: validate? (no direct access to spec as
  275. # todo: use ModuleSpec class
  276. # todo: error checking (like keyerrors)
  277. answer = {}
  278. self.set_module_spec(spec)
  279. # We should make one general 'spec update for module' that
  280. # passes both specification and commands at once
  281. spec_update = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_SPECIFICATION_UPDATE,
  282. [ spec.get_module_name(), spec.get_config_spec() ])
  283. self.cc.group_sendmsg(spec_update, "Cmd-Ctrld")
  284. cmds_update = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_COMMANDS_UPDATE,
  285. [ spec.get_module_name(), spec.get_commands_spec() ])
  286. self.cc.group_sendmsg(cmds_update, "Cmd-Ctrld")
  287. answer = isc.config.ccsession.create_answer(0)
  288. return answer
  289. def handle_msg(self, msg):
  290. """Handle a command from the cc channel to the configuration manager"""
  291. answer = {}
  292. cmd, arg = isc.config.ccsession.parse_command(msg)
  293. if cmd:
  294. if cmd == isc.config.ccsession.COMMAND_GET_COMMANDS_SPEC:
  295. answer = isc.config.ccsession.create_answer(0, self.get_commands_spec())
  296. elif cmd == isc.config.ccsession.COMMAND_GET_MODULE_SPEC:
  297. answer = self._handle_get_module_spec(arg)
  298. elif cmd == isc.config.ccsession.COMMAND_GET_CONFIG:
  299. answer = self._handle_get_config(arg)
  300. elif cmd == isc.config.ccsession.COMMAND_SET_CONFIG:
  301. answer = self._handle_set_config(arg)
  302. elif cmd == "shutdown":
  303. # TODO: logging
  304. #print("[b10-cfgmgr] Received shutdown command")
  305. self.running = False
  306. answer = isc.config.ccsession.create_answer(0)
  307. elif cmd == isc.config.ccsession.COMMAND_MODULE_SPEC:
  308. try:
  309. answer = self._handle_module_spec(isc.config.ModuleSpec(arg))
  310. except isc.config.ModuleSpecError as dde:
  311. answer = isc.config.ccsession.create_answer(1, "Error in data definition: " + str(dde))
  312. else:
  313. answer = isc.config.ccsession.create_answer(1, "Unknown command: " + str(cmd))
  314. else:
  315. answer = isc.config.ccsession.create_answer(1, "Unknown message format: " + str(msg))
  316. return answer
  317. def run(self):
  318. """Runs the configuration manager."""
  319. self.running = True
  320. while (self.running):
  321. msg, env = self.cc.group_recvmsg(False)
  322. if msg and not 'result' in msg:
  323. answer = self.handle_msg(msg);
  324. self.cc.group_reply(env, answer)
  325. else:
  326. self.running = False