module_spec.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. # Copyright (C) 2009 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. """Module Specifications
  16. A module specification holds the information about what configuration
  17. a module can have, and what commands it understands. It provides
  18. functions to read it from a .spec file, and to validate a given
  19. set of data against the specification
  20. """
  21. import json
  22. import sys
  23. import time
  24. import isc.cc.data
  25. # file objects are passed around as _io.TextIOWrapper objects
  26. # import that so we can check those types
  27. class ModuleSpecError(Exception):
  28. """This exception is raised it the ModuleSpec fails to initialize
  29. or if there is a failure or parse error reading the specification
  30. file"""
  31. pass
  32. def module_spec_from_file(spec_file, check = True):
  33. """Returns a ModuleSpec object defined by the file at spec_file.
  34. If check is True, the contents are verified. If there is an error
  35. in those contents, a ModuleSpecError is raised.
  36. A ModuleSpecError is also raised if the file cannot be read, or
  37. if it is not valid JSON."""
  38. module_spec = None
  39. try:
  40. if hasattr(spec_file, 'read'):
  41. json_str = spec_file.read()
  42. module_spec = json.loads(json_str)
  43. elif type(spec_file) == str:
  44. file = open(spec_file)
  45. json_str = file.read()
  46. module_spec = json.loads(json_str)
  47. file.close()
  48. else:
  49. raise ModuleSpecError("spec_file not a str or file-like object")
  50. except ValueError as ve:
  51. raise ModuleSpecError("JSON parse error: " + str(ve))
  52. except IOError as ioe:
  53. raise ModuleSpecError("JSON read error: " + str(ioe))
  54. if 'module_spec' not in module_spec:
  55. raise ModuleSpecError("Data definition has no module_spec element")
  56. result = ModuleSpec(module_spec['module_spec'], check)
  57. return result
  58. class ModuleSpec:
  59. def __init__(self, module_spec, check = True):
  60. """Initializes a ModuleSpec object from the specification in
  61. the given module_spec (which must be a dict). If check is
  62. True, the contents are verified. Raises a ModuleSpec error
  63. if there is something wrong with the contents of the dict"""
  64. if type(module_spec) != dict:
  65. raise ModuleSpecError("module_spec is of type " + str(type(module_spec)) + ", not dict")
  66. if check:
  67. _check(module_spec)
  68. self._module_spec = module_spec
  69. def validate_config(self, full, data, errors = None):
  70. """Check whether the given piece of data conforms to this
  71. data definition. If so, it returns True. If not, it will
  72. return false. If errors is given, and is an array, a string
  73. describing the error will be appended to it. The current
  74. version stops as soon as there is one error so this list
  75. will not be exhaustive. If 'full' is true, it also errors on
  76. non-optional missing values. Set this to False if you want to
  77. validate only a part of a configuration tree (like a list of
  78. non-default values)"""
  79. data_def = self.get_config_spec()
  80. if data_def is not None:
  81. return _validate_spec_list(data_def, full, data, errors)
  82. else:
  83. # no spec, always bad
  84. if errors is not None:
  85. errors.append("No config_data specification")
  86. return False
  87. def validate_command(self, cmd_name, cmd_params, errors = None):
  88. '''Check whether the given piece of command conforms to this
  89. command definition. If so, it reutrns True. If not, it will
  90. return False. If errors is given, and is an array, a string
  91. describing the error will be appended to it. The current version
  92. stops as soon as there is one error.
  93. cmd_name is command name to be validated, cmd_params includes
  94. command's parameters needs to be validated. cmd_params must
  95. be a map, with the format like:
  96. {param1_name: param1_value, param2_name: param2_value}
  97. '''
  98. cmd_spec = self.get_commands_spec()
  99. if not cmd_spec:
  100. return False
  101. for cmd in cmd_spec:
  102. if cmd['command_name'] != cmd_name:
  103. continue
  104. return _validate_spec_list(cmd['command_args'], True, cmd_params, errors)
  105. return False
  106. def validate_statistics(self, full, stat, errors = None):
  107. """Check whether the given piece of data conforms to this
  108. data definition. If so, it returns True. If not, it will
  109. return false. If errors is given, and is an array, a string
  110. describing the error will be appended to it. The current
  111. version stops as soon as there is one error so this list
  112. will not be exhaustive. If 'full' is true, it also errors on
  113. non-optional missing values. Set this to False if you want to
  114. validate only a part of a statistics tree (like a list of
  115. non-default values). Also it checks 'item_format' in case
  116. of time"""
  117. stat_spec = self.get_statistics_spec()
  118. if stat_spec is not None:
  119. return _validate_spec_list(stat_spec, full, stat, errors)
  120. else:
  121. # no spec, always bad
  122. if errors is not None:
  123. errors.append("No statistics specification")
  124. return False
  125. def get_module_name(self):
  126. """Returns a string containing the name of the module as
  127. specified by the specification given at __init__()"""
  128. return self._module_spec['module_name']
  129. def get_module_description(self):
  130. """Returns a string containing the description of the module as
  131. specified by the specification given at __init__().
  132. Returns an empty string if there is no description.
  133. """
  134. if 'module_description' in self._module_spec:
  135. return self._module_spec['module_description']
  136. else:
  137. return ""
  138. def get_full_spec(self):
  139. """Returns a dict representation of the full module specification"""
  140. return self._module_spec
  141. def get_config_spec(self):
  142. """Returns a dict representation of the configuration data part
  143. of the specification, or None if there is none."""
  144. if 'config_data' in self._module_spec:
  145. return self._module_spec['config_data']
  146. else:
  147. return None
  148. def get_commands_spec(self):
  149. """Returns a dict representation of the commands part of the
  150. specification, or None if there is none."""
  151. if 'commands' in self._module_spec:
  152. return self._module_spec['commands']
  153. else:
  154. return None
  155. def get_statistics_spec(self):
  156. """Returns a dict representation of the statistics part of the
  157. specification, or None if there is none."""
  158. if 'statistics' in self._module_spec:
  159. return self._module_spec['statistics']
  160. else:
  161. return None
  162. def __str__(self):
  163. """Returns a string representation of the full specification"""
  164. return self._module_spec.__str__()
  165. def _check(module_spec):
  166. """Checks the full specification. This is a dict that contains the
  167. element "module_spec", which is in itself a dict that
  168. must contain at least a "module_name" (string) and optionally
  169. a "config_data", a "commands" and a "statistics" element, all
  170. of which are lists of dicts. Raises a ModuleSpecError if there
  171. is a problem."""
  172. if type(module_spec) != dict:
  173. raise ModuleSpecError("data specification not a dict")
  174. if "module_name" not in module_spec:
  175. raise ModuleSpecError("no module_name in module_spec")
  176. if "module_description" in module_spec and \
  177. type(module_spec["module_description"]) != str:
  178. raise ModuleSpecError("module_description is not a string")
  179. if "config_data" in module_spec:
  180. _check_config_spec(module_spec["config_data"])
  181. if "commands" in module_spec:
  182. _check_command_spec(module_spec["commands"])
  183. if "statistics" in module_spec:
  184. _check_statistics_spec(module_spec["statistics"])
  185. def _check_config_spec(config_data):
  186. # config data is a list of items represented by dicts that contain
  187. # things like "item_name", depending on the type they can have
  188. # specific subitems
  189. """Checks a list that contains the configuration part of the
  190. specification. Raises a ModuleSpecError if there is a
  191. problem."""
  192. if type(config_data) != list:
  193. raise ModuleSpecError("config_data is of type " + str(type(config_data)) + ", not a list of items")
  194. for config_item in config_data:
  195. _check_item_spec(config_item)
  196. def _check_command_spec(commands):
  197. """Checks the list that contains a set of commands. Raises a
  198. ModuleSpecError is there is an error"""
  199. if type(commands) != list:
  200. raise ModuleSpecError("commands is not a list of commands")
  201. for command in commands:
  202. if type(command) != dict:
  203. raise ModuleSpecError("command in commands list is not a dict")
  204. if "command_name" not in command:
  205. raise ModuleSpecError("no command_name in command item")
  206. command_name = command["command_name"]
  207. if type(command_name) != str:
  208. raise ModuleSpecError("command_name not a string: " + str(type(command_name)))
  209. if "command_description" in command:
  210. if type(command["command_description"]) != str:
  211. raise ModuleSpecError("command_description not a string in " + command_name)
  212. if "command_args" in command:
  213. if type(command["command_args"]) != list:
  214. raise ModuleSpecError("command_args is not a list in " + command_name)
  215. for command_arg in command["command_args"]:
  216. if type(command_arg) != dict:
  217. raise ModuleSpecError("command argument not a dict in " + command_name)
  218. _check_item_spec(command_arg)
  219. else:
  220. raise ModuleSpecError("command_args missing in " + command_name)
  221. pass
  222. def _check_item_spec(config_item):
  223. """Checks the dict that defines one config item
  224. (i.e. containing "item_name", "item_type", etc.
  225. Raises a ModuleSpecError if there is an error"""
  226. if type(config_item) != dict:
  227. raise ModuleSpecError("item spec not a dict")
  228. if "item_name" not in config_item:
  229. raise ModuleSpecError("no item_name in config item")
  230. if type(config_item["item_name"]) != str:
  231. raise ModuleSpecError("item_name is not a string: " + str(config_item["item_name"]))
  232. item_name = config_item["item_name"]
  233. if "item_type" not in config_item:
  234. raise ModuleSpecError("no item_type in config item")
  235. item_type = config_item["item_type"]
  236. if type(item_type) != str:
  237. raise ModuleSpecError("item_type in " + item_name + " is not a string: " + str(type(item_type)))
  238. if item_type not in ["integer", "real", "boolean", "string", "list", "map", "named_set", "any"]:
  239. raise ModuleSpecError("unknown item_type in " + item_name + ": " + item_type)
  240. if "item_optional" in config_item:
  241. if type(config_item["item_optional"]) != bool:
  242. raise ModuleSpecError("item_default in " + item_name + " is not a boolean")
  243. if not config_item["item_optional"] and "item_default" not in config_item:
  244. raise ModuleSpecError("no default value for non-optional item " + item_name)
  245. else:
  246. raise ModuleSpecError("item_optional not in item " + item_name)
  247. if "item_default" in config_item:
  248. item_default = config_item["item_default"]
  249. if (item_type == "integer" and type(item_default) != int) or \
  250. (item_type == "real" and type(item_default) != float) or \
  251. (item_type == "boolean" and type(item_default) != bool) or \
  252. (item_type == "string" and type(item_default) != str) or \
  253. (item_type == "list" and type(item_default) != list) or \
  254. (item_type == "map" and type(item_default) != dict):
  255. raise ModuleSpecError("Wrong type for item_default in " + item_name)
  256. # TODO: once we have check_type, run the item default through that with the list|map_item_spec
  257. if item_type == "list":
  258. if "list_item_spec" not in config_item:
  259. raise ModuleSpecError("no list_item_spec in list item " + item_name)
  260. if type(config_item["list_item_spec"]) != dict:
  261. raise ModuleSpecError("list_item_spec in " + item_name + " is not a dict")
  262. _check_item_spec(config_item["list_item_spec"])
  263. if item_type == "map":
  264. if "map_item_spec" not in config_item:
  265. raise ModuleSpecError("no map_item_sepc in map item " + item_name)
  266. if type(config_item["map_item_spec"]) != list:
  267. raise ModuleSpecError("map_item_spec in " + item_name + " is not a list")
  268. for map_item in config_item["map_item_spec"]:
  269. if type(map_item) != dict:
  270. raise ModuleSpecError("map_item_spec element is not a dict")
  271. _check_item_spec(map_item)
  272. if 'item_format' in config_item and 'item_default' in config_item:
  273. item_format = config_item["item_format"]
  274. item_default = config_item["item_default"]
  275. if not _check_format(item_default, item_format):
  276. raise ModuleSpecError(
  277. "Wrong format for " + str(item_default) + " in " + str(item_name))
  278. def _check_statistics_spec(statistics):
  279. # statistics is a list of items represented by dicts that contain
  280. # things like "item_name", depending on the type they can have
  281. # specific subitems
  282. """Checks a list that contains the statistics part of the
  283. specification. Raises a ModuleSpecError if there is a
  284. problem."""
  285. if type(statistics) != list:
  286. raise ModuleSpecError("statistics is of type " + str(type(statistics))
  287. + ", not a list of items")
  288. for stat_item in statistics:
  289. _check_item_spec(stat_item)
  290. # Additionally checks if there are 'item_title' and
  291. # 'item_description'
  292. for item in [ 'item_title', 'item_description' ]:
  293. if item not in stat_item:
  294. raise ModuleSpecError("no " + item + " in statistics item")
  295. def _check_format(value, format_name):
  296. """Check if specified value and format are correct. Return True if
  297. is is correct."""
  298. # TODO: should be added other format types if necessary
  299. time_formats = { 'date-time' : "%Y-%m-%dT%H:%M:%SZ",
  300. 'date' : "%Y-%m-%d",
  301. 'time' : "%H:%M:%S" }
  302. for fmt in time_formats:
  303. if format_name == fmt:
  304. try:
  305. # reverse check
  306. return value == time.strftime(
  307. time_formats[fmt],
  308. time.strptime(value, time_formats[fmt]))
  309. except (ValueError, TypeError):
  310. break
  311. return False
  312. def _validate_type(spec, value, errors):
  313. """Returns true if the value is of the correct type given the
  314. specification"""
  315. data_type = spec['item_type']
  316. if data_type == "integer" and type(value) != int:
  317. if errors is not None:
  318. errors.append(str(value) + " should be an integer")
  319. return False
  320. elif data_type == "real" and type(value) != float:
  321. if errors is not None:
  322. errors.append(str(value) + " should be a real")
  323. return False
  324. elif data_type == "boolean" and type(value) != bool:
  325. if errors is not None:
  326. errors.append(str(value) + " should be a boolean")
  327. return False
  328. elif data_type == "string" and type(value) != str:
  329. if errors is not None:
  330. errors.append(str(value) + " should be a string")
  331. return False
  332. elif data_type == "list" and type(value) != list:
  333. if errors is not None:
  334. errors.append(str(value) + " should be a list")
  335. return False
  336. elif data_type == "map" and type(value) != dict:
  337. if errors is not None:
  338. errors.append(str(value) + " should be a map")
  339. return False
  340. elif data_type == "named_set" and type(value) != dict:
  341. if errors != None:
  342. errors.append(str(value) + " should be a map")
  343. return False
  344. else:
  345. return True
  346. def _validate_format(spec, value, errors):
  347. """Returns true if the value is of the correct format given the
  348. specification. And also return true if no 'item_format'"""
  349. if "item_format" in spec:
  350. item_format = spec['item_format']
  351. if not _check_format(value, item_format):
  352. if errors is not None:
  353. errors.append("format type of " + str(value)
  354. + " should be " + item_format)
  355. return False
  356. return True
  357. def _validate_item(spec, full, data, errors):
  358. if not _validate_type(spec, data, errors):
  359. return False
  360. elif type(data) == list:
  361. list_spec = spec['list_item_spec']
  362. for data_el in data:
  363. if not _validate_type(list_spec, data_el, errors):
  364. return False
  365. if not _validate_format(list_spec, data_el, errors):
  366. return False
  367. if list_spec['item_type'] == "map":
  368. if not _validate_item(list_spec, full, data_el, errors):
  369. return False
  370. elif type(data) == dict:
  371. if 'map_item_spec' in spec:
  372. if not _validate_spec_list(spec['map_item_spec'], full, data, errors):
  373. return False
  374. else:
  375. named_set_spec = spec['named_set_item_spec']
  376. for data_el in data.values():
  377. if not _validate_type(named_set_spec, data_el, errors):
  378. return False
  379. if not _validate_item(named_set_spec, full, data_el, errors):
  380. return False
  381. elif not _validate_format(spec, data, errors):
  382. return False
  383. return True
  384. def _validate_spec(spec, full, data, errors):
  385. item_name = spec['item_name']
  386. item_optional = spec['item_optional']
  387. if not data and item_optional:
  388. return True
  389. elif item_name in data:
  390. return _validate_item(spec, full, data[item_name], errors)
  391. elif full and not item_optional:
  392. if errors is not None:
  393. errors.append("non-optional item " + item_name + " missing")
  394. return False
  395. else:
  396. return True
  397. def _validate_spec_list(module_spec, full, data, errors):
  398. # we do not return immediately, there may be more errors
  399. # so we keep a boolean to keep track if we found errors
  400. validated = True
  401. # check if the known items are correct
  402. for spec_item in module_spec:
  403. if not _validate_spec(spec_item, full, data, errors):
  404. validated = False
  405. # check if there are items in our data that are not in the
  406. # specification
  407. if data is not None:
  408. for item_name in data:
  409. found = False
  410. for spec_item in module_spec:
  411. if spec_item["item_name"] == item_name:
  412. found = True
  413. if not found and item_name != "version":
  414. if errors is not None:
  415. errors.append("unknown item " + item_name)
  416. validated = False
  417. return validated