cfgmgr.py 17 KB

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