config_data.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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. """
  16. Classes to store configuration data and module specifications
  17. Used by the config manager, (python) modules, and UI's (those last
  18. two through the classes in ccsession)
  19. """
  20. import isc.cc.data
  21. import isc.config.module_spec
  22. class ConfigDataError(Exception): pass
  23. BIND10_CONFIG_DATA_VERSION = 2
  24. def check_type(spec_part, value):
  25. """Does nothing if the value is of the correct type given the
  26. specification part relevant for the value. Raises an
  27. isc.cc.data.DataTypeError exception if not. spec_part can be
  28. retrieved with find_spec_part()"""
  29. if type(spec_part) == dict and 'item_type' in spec_part:
  30. data_type = spec_part['item_type']
  31. else:
  32. raise isc.cc.data.DataTypeError(str("Incorrect specification part for type checking"))
  33. if data_type == "integer" and type(value) != int:
  34. raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
  35. elif data_type == "real" and type(value) != float:
  36. raise isc.cc.data.DataTypeError(str(value) + " is not a real")
  37. elif data_type == "boolean" and type(value) != bool:
  38. raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
  39. elif data_type == "string" and type(value) != str:
  40. raise isc.cc.data.DataTypeError(str(value) + " is not a string")
  41. elif data_type == "list":
  42. if type(value) != list:
  43. raise isc.cc.data.DataTypeError(str(value) + " is not a list")
  44. else:
  45. for element in value:
  46. check_type(spec_part['list_item_spec'], element)
  47. elif data_type == "map" and type(value) != dict:
  48. # todo: check types of map contents too
  49. raise isc.cc.data.DataTypeError(str(value) + " is not a map")
  50. def convert_type(spec_part, value):
  51. """Convert the give value(type is string) according specification
  52. part relevant for the value. Raises an isc.cc.data.DataTypeError
  53. exception if conversion failed.
  54. """
  55. if type(spec_part) == dict and 'item_type' in spec_part:
  56. data_type = spec_part['item_type']
  57. else:
  58. raise isc.cc.data.DataTypeError(str("Incorrect specification part for type convering"))
  59. try:
  60. if data_type == "integer":
  61. return int(value)
  62. elif data_type == "real":
  63. return float(value)
  64. elif data_type == "boolean":
  65. return str.lower(str(value)) != 'false'
  66. elif data_type == "string":
  67. return str(value)
  68. elif data_type == "list":
  69. ret = []
  70. if type(value) == list:
  71. for item in value:
  72. ret.append(convert_type(spec_part['list_item_spec'], item))
  73. elif type(value) == str:
  74. value = value.split(',')
  75. for item in value:
  76. sub_value = item.split()
  77. for sub_item in sub_value:
  78. ret.append(convert_type(spec_part['list_item_spec'], sub_item))
  79. if ret == []:
  80. raise isc.cc.data.DataTypeError(str(value) + " is not a list")
  81. return ret
  82. elif data_type == "map":
  83. return dict(value)
  84. # todo: check types of map contents too
  85. else:
  86. return value
  87. except ValueError as err:
  88. raise isc.cc.data.DataTypeError(str(err))
  89. except TypeError as err:
  90. raise isc.cc.data.DataTypeError(str(err))
  91. def find_spec_part(element, identifier):
  92. """find the data definition for the given identifier
  93. returns either a map with 'item_name' etc, or a list of those"""
  94. if identifier == "":
  95. return element
  96. id_parts = identifier.split("/")
  97. id_parts[:] = (value for value in id_parts if value != "")
  98. cur_el = element
  99. for id in id_parts:
  100. if type(cur_el) == dict and 'map_item_spec' in cur_el.keys():
  101. found = False
  102. for cur_el_item in cur_el['map_item_spec']:
  103. if cur_el_item['item_name'] == id:
  104. cur_el = cur_el_item
  105. found = True
  106. if not found:
  107. raise isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
  108. elif type(cur_el) == list:
  109. found = False
  110. for cur_el_item in cur_el:
  111. if cur_el_item['item_name'] == id:
  112. cur_el = cur_el_item
  113. found = True
  114. if not found:
  115. raise isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
  116. else:
  117. raise isc.cc.data.DataNotFoundError("Not a correct config specification")
  118. return cur_el
  119. def spec_name_list(spec, prefix="", recurse=False):
  120. """Returns a full list of all possible item identifiers in the
  121. specification (part). Raises a ConfigDataError if spec is not
  122. a correct spec (as returned by ModuleSpec.get_config_spec()"""
  123. result = []
  124. if prefix != "" and not prefix.endswith("/"):
  125. prefix += "/"
  126. if type(spec) == dict:
  127. if 'map_item_spec' in spec:
  128. for map_el in spec['map_item_spec']:
  129. name = map_el['item_name']
  130. if map_el['item_type'] == 'map':
  131. name += "/"
  132. if recurse and 'map_item_spec' in map_el:
  133. result.extend(spec_name_list(map_el['map_item_spec'], prefix + map_el['item_name'], recurse))
  134. else:
  135. result.append(prefix + name)
  136. else:
  137. for name in spec:
  138. result.append(prefix + name + "/")
  139. if recurse:
  140. result.extend(spec_name_list(spec[name],name, recurse))
  141. elif type(spec) == list:
  142. for list_el in spec:
  143. if 'item_name' in list_el:
  144. if list_el['item_type'] == "map" and recurse:
  145. result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
  146. else:
  147. name = list_el['item_name']
  148. result.append(prefix + name)
  149. else:
  150. raise ConfigDataError("Bad specification")
  151. else:
  152. raise ConfigDataError("Bad specication")
  153. return result
  154. class ConfigData:
  155. """This class stores the module specs and the current non-default
  156. config values. It provides functions to get the actual value or
  157. the default value if no non-default value has been set"""
  158. def __init__(self, specification):
  159. """Initialize a ConfigData instance. If specification is not
  160. of type ModuleSpec, a ConfigDataError is raised."""
  161. if type(specification) != isc.config.ModuleSpec:
  162. raise ConfigDataError("specification is of type " + str(type(specification)) + ", not ModuleSpec")
  163. self.specification = specification
  164. self.data = {}
  165. def get_value(self, identifier):
  166. """Returns a tuple where the first item is the value at the
  167. given identifier, and the second item is a bool which is
  168. true if the value is an unset default. Raises an
  169. isc.cc.data.DataNotFoundError if the identifier is bad"""
  170. value = isc.cc.data.find_no_exc(self.data, identifier)
  171. if value != None:
  172. return value, False
  173. spec = find_spec_part(self.specification.get_config_spec(), identifier)
  174. if spec and 'item_default' in spec:
  175. return spec['item_default'], True
  176. return None, False
  177. def get_module_spec(self):
  178. """Returns the ModuleSpec object associated with this ConfigData"""
  179. return self.specification
  180. def set_local_config(self, data):
  181. """Set the non-default config values, as passed by cfgmgr"""
  182. self.data = data
  183. def get_local_config(self):
  184. """Returns the non-default config values in a dict"""
  185. return self.data;
  186. def get_item_list(self, identifier = None, recurse = False):
  187. """Returns a list of strings containing the full identifiers of
  188. all 'sub'options at the given identifier. If recurse is True,
  189. it will also add all identifiers of all children, if any"""
  190. if identifier:
  191. spec = find_spec_part(self.specification.get_config_spec(), identifier)
  192. return spec_name_list(spec, identifier + "/")
  193. return spec_name_list(self.specification.get_config_spec(), "", recurse)
  194. def get_full_config(self):
  195. """Returns a dict containing identifier: value elements, for
  196. all configuration options for this module. If there is
  197. a local setting, that will be used. Otherwise the value
  198. will be the default as specified by the module specification.
  199. If there is no default and no local setting, the value will
  200. be None"""
  201. items = self.get_item_list(None, True)
  202. result = {}
  203. for item in items:
  204. value, default = self.get_value(item)
  205. result[item] = value
  206. return result
  207. class MultiConfigData:
  208. """This class stores the module specs, current non-default
  209. configuration values and 'local' (uncommitted) changes for
  210. multiple modules"""
  211. LOCAL = 1
  212. CURRENT = 2
  213. DEFAULT = 3
  214. NONE = 4
  215. def __init__(self):
  216. self._specifications = {}
  217. self._current_config = {}
  218. self._local_changes = {}
  219. def set_specification(self, spec):
  220. """Add or update a ModuleSpec. Raises a ConfigDataError is spec is not a ModuleSpec"""
  221. if type(spec) != isc.config.ModuleSpec:
  222. raise ConfigDataError("not a datadef: " + str(type(spec)))
  223. self._specifications[spec.get_module_name()] = spec
  224. def remove_specification(self, module_name):
  225. """Removes the specification with the given module name. Does nothing if it wasn't there."""
  226. if module_name in self._specifications:
  227. del self._specifications[module_name]
  228. def have_specification(self, module_name):
  229. """Returns True if we have a specification for the module with the given name.
  230. Returns False if we do not."""
  231. return module_name in self._specifications
  232. def get_module_spec(self, module):
  233. """Returns the ModuleSpec for the module with the given name.
  234. If there is no such module, it returns None"""
  235. if module in self._specifications:
  236. return self._specifications[module]
  237. else:
  238. return None
  239. def find_spec_part(self, identifier):
  240. """Returns the specification for the item at the given
  241. identifier, or None if not found. The first part of the
  242. identifier (up to the first /) is interpreted as the module
  243. name. Returns None if not found, or if identifier is not a
  244. string."""
  245. if type(identifier) != str:
  246. return None
  247. if identifier[0] == '/':
  248. identifier = identifier[1:]
  249. module, sep, id = identifier.partition("/")
  250. try:
  251. return find_spec_part(self._specifications[module].get_config_spec(), id)
  252. except isc.cc.data.DataNotFoundError as dnfe:
  253. return None
  254. except KeyError as ke:
  255. return None
  256. # this function should only be called by __request_config
  257. def _set_current_config(self, config):
  258. """Replace the full current config values."""
  259. self._current_config = config
  260. def get_current_config(self):
  261. """Returns the current configuration as it is known by the
  262. configuration manager. It is a dict where the first level is
  263. the module name, and the value is the config values for
  264. that module"""
  265. return self._current_config
  266. def get_local_changes(self):
  267. """Returns the local config changes, i.e. those that have not
  268. been committed yet and are not known by the configuration
  269. manager or the modules."""
  270. return self._local_changes
  271. def clear_local_changes(self):
  272. """Reverts all local changes"""
  273. self._local_changes = {}
  274. def get_local_value(self, identifier):
  275. """Returns a specific local (uncommitted) configuration value,
  276. as specified by the identifier. If the local changes do not
  277. contain a new setting for this identifier, or if the
  278. identifier cannot be found, None is returned. See
  279. get_value() for a general way to find a configuration value
  280. """
  281. return isc.cc.data.find_no_exc(self._local_changes, identifier)
  282. def get_current_value(self, identifier):
  283. """Returns the current non-default value as known by the
  284. configuration manager, or None if it is not set.
  285. See get_value() for a general way to find a configuration
  286. value
  287. """
  288. return isc.cc.data.find_no_exc(self._current_config, identifier)
  289. def get_default_value(self, identifier):
  290. """Returns the default value for the given identifier as
  291. specified by the module specification, or None if there is
  292. no default or the identifier could not be found.
  293. See get_value() for a general way to find a configuration
  294. value
  295. """
  296. if identifier[0] == '/':
  297. identifier = identifier[1:]
  298. module, sep, id = identifier.partition("/")
  299. try:
  300. spec = find_spec_part(self._specifications[module].get_config_spec(), id)
  301. if 'item_default' in spec:
  302. return spec['item_default']
  303. else:
  304. return None
  305. except isc.cc.data.DataNotFoundError as dnfe:
  306. return None
  307. def get_value(self, identifier):
  308. """Returns a tuple containing value,status.
  309. The value contains the configuration value for the given
  310. identifier. The status reports where this value came from;
  311. it is one of: LOCAL, CURRENT, DEFAULT or NONE, corresponding
  312. (local change, current setting, default as specified by the
  313. specification, or not found at all)."""
  314. value = self.get_local_value(identifier)
  315. if value != None:
  316. return value, self.LOCAL
  317. value = self.get_current_value(identifier)
  318. if value != None:
  319. return value, self.CURRENT
  320. value = self.get_default_value(identifier)
  321. if value != None:
  322. return value, self.DEFAULT
  323. return None, self.NONE
  324. def get_value_maps(self, identifier = None):
  325. """Returns a list of dicts, containing the following values:
  326. name: name of the entry (string)
  327. type: string containing the type of the value (or 'module')
  328. value: value of the entry if it is a string, int, double or bool
  329. modified: true if the value is a local change
  330. default: true if the value has been changed
  331. TODO: use the consts for those last ones
  332. Throws DataNotFoundError if the identifier is bad
  333. """
  334. result = []
  335. if not identifier:
  336. # No identifier, so we need the list of current modules
  337. for module in self._specifications.keys():
  338. entry = {}
  339. entry['name'] = module
  340. entry['type'] = 'module'
  341. entry['value'] = None
  342. entry['modified'] = False
  343. entry['default'] = False
  344. result.append(entry)
  345. else:
  346. if identifier[0] == '/':
  347. identifier = identifier[1:]
  348. module, sep, id = identifier.partition('/')
  349. spec = self.get_module_spec(module)
  350. if spec:
  351. spec_part = find_spec_part(spec.get_config_spec(), id)
  352. if type(spec_part) == list:
  353. for item in spec_part:
  354. entry = {}
  355. entry['name'] = item['item_name']
  356. entry['type'] = item['item_type']
  357. value, status = self.get_value("/" + identifier + "/" + item['item_name'])
  358. entry['value'] = value
  359. if status == self.LOCAL:
  360. entry['modified'] = True
  361. else:
  362. entry['modified'] = False
  363. if status == self.DEFAULT:
  364. entry['default'] = False
  365. else:
  366. entry['default'] = False
  367. result.append(entry)
  368. elif type(spec_part) == dict:
  369. item = spec_part
  370. if item['item_type'] == 'list':
  371. li_spec = item['list_item_spec']
  372. item_list, status = self.get_value("/" + identifier)
  373. if item_list != None:
  374. for value in item_list:
  375. result_part2 = {}
  376. result_part2['name'] = li_spec['item_name']
  377. result_part2['value'] = value
  378. result_part2['type'] = li_spec['item_type']
  379. result_part2['default'] = False
  380. result_part2['modified'] = False
  381. result.append(result_part2)
  382. else:
  383. entry = {}
  384. entry['name'] = item['item_name']
  385. entry['type'] = item['item_type']
  386. #value, status = self.get_value("/" + identifier + "/" + item['item_name'])
  387. value, status = self.get_value("/" + identifier)
  388. entry['value'] = value
  389. if status == self.LOCAL:
  390. entry['modified'] = True
  391. else:
  392. entry['modified'] = False
  393. if status == self.DEFAULT:
  394. entry['default'] = False
  395. else:
  396. entry['default'] = False
  397. result.append(entry)
  398. return result
  399. def set_value(self, identifier, value):
  400. """Set the local value at the given identifier to value. If
  401. there is a specification for the given identifier, the type
  402. is checked."""
  403. spec_part = self.find_spec_part(identifier)
  404. if spec_part != None:
  405. check_type(spec_part, value)
  406. isc.cc.data.set(self._local_changes, identifier, value)
  407. def get_config_item_list(self, identifier = None, recurse = False):
  408. """Returns a list of strings containing the item_names of
  409. the child items at the given identifier. If no identifier is
  410. specified, returns a list of module names. The first part of
  411. the identifier (up to the first /) is interpreted as the
  412. module name"""
  413. if identifier and identifier != "/":
  414. if identifier.startswith("/"):
  415. identifier = identifier[1:]
  416. spec = self.find_spec_part(identifier)
  417. return spec_name_list(spec, identifier + "/", recurse)
  418. else:
  419. if recurse:
  420. id_list = []
  421. for module in self._specifications.keys():
  422. id_list.extend(spec_name_list(self.find_spec_part(module), module, recurse))
  423. return id_list
  424. else:
  425. return list(self._specifications.keys())