New Saltstack Salt formula

236 lines
6.9KB

  1. from __future__ import absolute_import
  2. import glob
  3. import json
  4. import logging
  5. import os.path
  6. import yaml
  7. # Import third party libs
  8. try:
  9. from jsonschema import validate as _validate
  10. from jsonschema.validators import validator_for as _validator_for
  11. from jsonschema.exceptions import SchemaError, ValidationError
  12. HAS_JSONSCHEMA = True
  13. except ImportError:
  14. HAS_JSONSCHEMA = False
  15. __virtualname__ = 'modelschema'
  16. LOG = logging.getLogger(__name__)
  17. def __virtual__():
  18. """
  19. Only load if jsonschema library exist.
  20. """
  21. if not HAS_JSONSCHEMA:
  22. return (
  23. False,
  24. 'Can not load module jsonschema: jsonschema library not found')
  25. return __virtualname__
  26. def _get_base_dir():
  27. return __salt__['config.get']('pilllar_schema_path',
  28. '/usr/share/salt-formulas/env')
  29. def _dict_deep_merge(a, b, path=None):
  30. """
  31. Merges dict(b) into dict(a)
  32. """
  33. if path is None:
  34. path = []
  35. for key in b:
  36. if key in a:
  37. if isinstance(a[key], dict) and isinstance(b[key], dict):
  38. _dict_deep_merge(a[key], b[key], path + [str(key)])
  39. elif a[key] == b[key]:
  40. pass # same leaf value
  41. else:
  42. raise Exception(
  43. 'Conflict at {}'.format('.'.join(path + [str(key)])))
  44. else:
  45. a[key] = b[key]
  46. return a
  47. def schema_list():
  48. """
  49. Returns list of all defined schema files.
  50. CLI Examples:
  51. .. code-block:: bash
  52. salt-call modelutils.schema_list
  53. """
  54. output = {}
  55. schemas = glob.glob('{}/*/schemas/*.yaml'.format(_get_base_dir()))
  56. for schema in schemas:
  57. if os.path.exists(schema):
  58. role_name = schema.split('/')[-1].replace('.yaml', '')
  59. service_name = schema.split('/')[-3]
  60. print role_name, service_name
  61. name = '{}-{}'.format(service_name, role_name)
  62. output[name] = {
  63. 'service': service_name,
  64. 'role': role_name,
  65. 'path': schema,
  66. 'valid': schema_validate(service_name, role_name)[name]
  67. }
  68. return output
  69. def schema_get(service, role):
  70. """
  71. Returns pillar schema for given service and role. If no service and role
  72. is specified, method will return all known schemas.
  73. CLI Examples:
  74. .. code-block:: bash
  75. salt-call modelutils.schema_get ntp server
  76. """
  77. schema_path = 'salt://{}/schemas/{}.yaml'.format(service, role)
  78. schema = __salt__['cp.get_file_str'](schema_path)
  79. if schema:
  80. try:
  81. data = yaml.safe_load(schema)
  82. except yaml.YAMLError as exc:
  83. raise Exception("Failed to parse schema:{}\n"
  84. "{}".format(schema_path, exc))
  85. else:
  86. raise Exception("Schema not found:{}".format(schema_path))
  87. return {'{}-{}'.format(service, role): data}
  88. def schema_validate(service, role):
  89. """
  90. Validates pillar schema itself of given service and role.
  91. CLI Examples:
  92. .. code-block:: bash
  93. salt-call modelutils.schema_validate ntp server
  94. """
  95. schema = schema_get(service, role)['{}-{}'.format(service, role)]
  96. cls = _validator_for(schema)
  97. LOG.debug("Validating schema..")
  98. try:
  99. cls.check_schema(schema)
  100. LOG.debug("Schema is valid")
  101. data = 'Schema is valid'
  102. except SchemaError as exc:
  103. LOG.error("SchemaError:{}".format(exc))
  104. raise Exception("SchemaError")
  105. return {'{}-{}'.format(service, role): data}
  106. def model_validate(service=None, role=None):
  107. """
  108. Validates pillar metadata by schema for given service and role. If
  109. no service and role is specified, method will validate all defined
  110. services.
  111. CLI Example:
  112. .. code-block:: bash
  113. salt-run modelschema.model_validate keystone server
  114. """
  115. schema = schema_get(service, role)['{}-{}'.format(service, role)]
  116. model = __salt__['pillar.get']('{}:{}'.format(service, role))
  117. try:
  118. _validate(model, schema)
  119. data = 'Model is valid'
  120. except SchemaError as exc:
  121. LOG.error("SchemaError:{}".format(exc))
  122. raise Exception("SchemaError")
  123. except ValidationError as exc:
  124. LOG.error("ValidationError:{}\nInstance:{}\n"
  125. "Schema title:{}\n"
  126. "SchemaPath:{}".format(exc.message,
  127. exc.instance,
  128. exc.schema.get(
  129. "title",
  130. "Schema title not set!"),
  131. exc.schema_path))
  132. raise Exception("ValidationError")
  133. return {'{}-{}'.format(service, role): data}
  134. def data_validate(model, schema):
  135. """
  136. Validates model by given schema.
  137. CLI Example:
  138. .. code-block:: bash
  139. salt-run modelschema.data_validate {'a': 'b'} {'a': 'b'}
  140. """
  141. try:
  142. _validate(model, schema)
  143. data = 'Model is valid'
  144. except SchemaError as exc:
  145. LOG.error("SchemaError:{}".format(exc))
  146. raise Exception("SchemaError")
  147. except ValidationError as exc:
  148. LOG.error("ValidationError:{}\nInstance:{}\n"
  149. "Schema title:{}\n"
  150. "SchemaPath:{}".format(exc.message,
  151. exc.instance,
  152. exc.schema.get(
  153. "title",
  154. "Schema title not set!"),
  155. exc.schema_path))
  156. raise Exception("ValidationError")
  157. return data
  158. def schema_from_tests(service):
  159. """
  160. Generate pillar schema skeleton for given service. Method iterates throught
  161. test pillars and generates schema scaffold structure in JSON format that
  162. can be passed to service like http://jsonschema.net/ to get the basic
  163. schema for the individual roles of the service.
  164. CLI Examples:
  165. .. code-block:: bash
  166. salt-call modelutils.schema_from_tests keystone
  167. """
  168. pillars = glob.glob(
  169. '{}/{}/tests/pillar/*.sls'.format(_get_base_dir(), service))
  170. raw_data = {}
  171. for pillar in pillars:
  172. if os.path.exists(pillar):
  173. with open(pillar, 'r') as stream:
  174. try:
  175. data = yaml.load(stream)
  176. except yaml.YAMLError as exc:
  177. data = {}
  178. LOG.error('{}: {}'.format(pillar, repr(exc)))
  179. try:
  180. _dict_deep_merge(raw_data, data)
  181. except Exception as exc:
  182. LOG.error('{}: {}'.format(pillar, repr(exc)))
  183. if service not in raw_data.keys():
  184. LOG.error("Could not find applicable data "
  185. "for:{}\n at:{}".format(service, _get_base_dir()))
  186. raise Exception("DataError")
  187. data = raw_data[service]
  188. output = {}
  189. for role_name, role in data.items():
  190. output[role_name] = json.dumps(role)
  191. return output