blob: 451f3c19347c9162a8b8507da6080a79fbcb9a2d [file] [log] [blame]
Ales Komarek6041b1b2017-11-22 15:26:27 +01001
2from __future__ import absolute_import
3
4import glob
5import json
6import logging
7import os.path
8import yaml
9
10# Import third party libs
11try:
12 from jsonschema import validate as _validate
13 from jsonschema.validators import validator_for as _validator_for
14 from jsonschema.exceptions import SchemaError, ValidationError
15 HAS_JSONSCHEMA = True
16except ImportError:
17 HAS_JSONSCHEMA = False
18
19__virtualname__ = 'modelschema'
20
21LOG = logging.getLogger(__name__)
22
23
24def __virtual__():
25 """
26 Only load if jsonschema library exist.
27 """
28 if not HAS_JSONSCHEMA:
29 return (
30 False,
31 'Can not load module jsonschema: jsonschema library not found')
32 return __virtualname__
33
34
35def _get_base_dir():
36 return __salt__['config.get']('pilllar_schema_path',
37 '/usr/share/salt-formulas/env')
38
39
40def _dict_deep_merge(a, b, path=None):
41 """
42 Merges dict(b) into dict(a)
43 """
44 if path is None:
45 path = []
46 for key in b:
47 if key in a:
48 if isinstance(a[key], dict) and isinstance(b[key], dict):
49 _dict_deep_merge(a[key], b[key], path + [str(key)])
50 elif a[key] == b[key]:
51 pass # same leaf value
52 else:
53 raise Exception(
54 'Conflict at {}'.format('.'.join(path + [str(key)])))
55 else:
56 a[key] = b[key]
57 return a
58
59
60def schema_list():
61 """
62 Returns list of all defined schema files.
63
64 CLI Examples:
65
66 .. code-block:: bash
67
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +000068 salt-call modelschema.schema_list
Ales Komarek6041b1b2017-11-22 15:26:27 +010069 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +000070
Ales Komarek6041b1b2017-11-22 15:26:27 +010071 output = {}
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +000072
73 def _parse_schema(pattern, helper, output):
74 schemas = glob.glob(pattern.format(_get_base_dir()))
75 for schema in schemas:
76 if os.path.exists(schema):
77 sc_splited = schema.split('/')
78 parsed = helper(sc_splited)
79 name = '{}-{}'.format(*parsed[:2])
80 output[name] = {
81 'service': parsed[0],
82 'role': parsed[1],
83 'path': schema,
84 'valid': schema_validate(*parsed)[name]
85 }
86
87 versioned_schemas_path = '{}/*/schemas/*/*.yaml'
88 _parse_schema(versioned_schemas_path, lambda sc: (sc[-4], sc[-1].replace('.yaml', ''), sc[-2]), output)
89
90 common_schemas_path = '{}/*/schemas/*.yaml'
91 _parse_schema(common_schemas_path, lambda sc: (sc[-3], sc[-1].replace('.yaml', '')), output)
92
Ales Komarek6041b1b2017-11-22 15:26:27 +010093 return output
94
95
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +000096def schema_get(service, role, version=None):
Ales Komarek6041b1b2017-11-22 15:26:27 +010097 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +000098 Returns pillar schema for given service and role.
Ales Komarek6041b1b2017-11-22 15:26:27 +010099
100 CLI Examples:
101
102 .. code-block:: bash
103
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000104 salt-call modelschema.schema_get ntp server
Ales Komarek6041b1b2017-11-22 15:26:27 +0100105
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000106 .. or ..
107
108 salt-call modelschema.schema_get keystone server pike
Ales Komarek6041b1b2017-11-22 15:26:27 +0100109 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000110
111 if version:
112 schema_path = 'salt://{}/schemas/{}/{}.yaml'.format(service, version, role)
113 else:
114 schema_path = 'salt://{}/schemas/{}.yaml'.format(service, role)
115
Ales Komarek6041b1b2017-11-22 15:26:27 +0100116 schema = __salt__['cp.get_file_str'](schema_path)
117 if schema:
118 try:
119 data = yaml.safe_load(schema)
120 except yaml.YAMLError as exc:
121 raise Exception("Failed to parse schema:{}\n"
122 "{}".format(schema_path, exc))
123 else:
124 raise Exception("Schema not found:{}".format(schema_path))
125 return {'{}-{}'.format(service, role): data}
126
127
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000128def schema_validate(service, role, version=None):
Ales Komarek6041b1b2017-11-22 15:26:27 +0100129 """
130 Validates pillar schema itself of given service and role.
131
132 CLI Examples:
133
134 .. code-block:: bash
135
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000136 salt-call modelschema.schema_validate ntp server
Ales Komarek6041b1b2017-11-22 15:26:27 +0100137
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000138 .. or ..
139
140 salt-call modelschema.schema_validate keystone server pike
Ales Komarek6041b1b2017-11-22 15:26:27 +0100141 """
142
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000143 schema = schema_get(service, role, version)['{}-{}'.format(service, role)]
Ales Komarek6041b1b2017-11-22 15:26:27 +0100144 cls = _validator_for(schema)
145 LOG.debug("Validating schema..")
146 try:
147 cls.check_schema(schema)
148 LOG.debug("Schema is valid")
149 data = 'Schema is valid'
150 except SchemaError as exc:
151 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200152 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100153 return {'{}-{}'.format(service, role): data}
154
155
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000156def model_validate(service, role, version=None):
Ales Komarek6041b1b2017-11-22 15:26:27 +0100157 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000158 Validates pillar metadata by schema for given service and role.
Ales Komarek6041b1b2017-11-22 15:26:27 +0100159
160 CLI Example:
Ales Komarek6041b1b2017-11-22 15:26:27 +0100161
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000162 .. code-block:: bash
163
164 salt-call modelschema.model_validate ntp server
165
166 .. or ..
167
168 salt-call modelschema.model_validate keystone server pike
Ales Komarek6041b1b2017-11-22 15:26:27 +0100169 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000170 schema = schema_get(service, role, version)['{}-{}'.format(service, role)]
Ales Komarek6041b1b2017-11-22 15:26:27 +0100171 model = __salt__['pillar.get']('{}:{}'.format(service, role))
172 try:
173 _validate(model, schema)
174 data = 'Model is valid'
175 except SchemaError as exc:
176 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200177 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100178 except ValidationError as exc:
179 LOG.error("ValidationError:{}\nInstance:{}\n"
azvyagintsevb57aaa12017-12-26 15:06:47 +0200180 "Schema title:{}\n"
181 "SchemaPath:{}".format(exc.message,
182 exc.instance,
183 exc.schema.get(
184 "title",
185 "Schema title not set!"),
Ales Komarek6041b1b2017-11-22 15:26:27 +0100186 exc.schema_path))
187 raise Exception("ValidationError")
188 return {'{}-{}'.format(service, role): data}
189
190
191def data_validate(model, schema):
192 """
193 Validates model by given schema.
194
195 CLI Example:
196 .. code-block:: bash
197 salt-run modelschema.data_validate {'a': 'b'} {'a': 'b'}
198 """
199 try:
200 _validate(model, schema)
201 data = 'Model is valid'
202 except SchemaError as exc:
203 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200204 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100205 except ValidationError as exc:
206 LOG.error("ValidationError:{}\nInstance:{}\n"
azvyagintsevb57aaa12017-12-26 15:06:47 +0200207 "Schema title:{}\n"
208 "SchemaPath:{}".format(exc.message,
209 exc.instance,
210 exc.schema.get(
211 "title",
212 "Schema title not set!"),
Ales Komarek6041b1b2017-11-22 15:26:27 +0100213 exc.schema_path))
214 raise Exception("ValidationError")
215 return data
216
217
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000218def schema_from_tests(service, version=None):
Ales Komarek6041b1b2017-11-22 15:26:27 +0100219 """
220 Generate pillar schema skeleton for given service. Method iterates throught
221 test pillars and generates schema scaffold structure in JSON format that
222 can be passed to service like http://jsonschema.net/ to get the basic
223 schema for the individual roles of the service.
224
225 CLI Examples:
226
227 .. code-block:: bash
228
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000229 salt-call modelschema.schema_from_tests keystone pike
230
231 .. or ..
232
233 salt-call modelschema.schema_from_tests ntp
Ales Komarek6041b1b2017-11-22 15:26:27 +0100234 """
Oleksandr Shyshkoe60e1ec2019-01-23 15:16:05 +0000235 if version:
236 pillars = glob.glob(
237 '{}/{}/tests/pillar/{}/*.sls'.format(_get_base_dir(), service, version))
238 else:
239 pillars = glob.glob(
240 '{}/{}/tests/pillar/*.sls'.format(_get_base_dir(), service))
Ales Komarek6041b1b2017-11-22 15:26:27 +0100241 raw_data = {}
242 for pillar in pillars:
243 if os.path.exists(pillar):
244 with open(pillar, 'r') as stream:
245 try:
246 data = yaml.load(stream)
247 except yaml.YAMLError as exc:
248 data = {}
249 LOG.error('{}: {}'.format(pillar, repr(exc)))
250 try:
251 _dict_deep_merge(raw_data, data)
252 except Exception as exc:
253 LOG.error('{}: {}'.format(pillar, repr(exc)))
254 if service not in raw_data.keys():
azvyagintsevb57aaa12017-12-26 15:06:47 +0200255 LOG.error("Could not find applicable data "
256 "for:{}\n at:{}".format(service, _get_base_dir()))
257 raise Exception("DataError")
258
Ales Komarek6041b1b2017-11-22 15:26:27 +0100259 data = raw_data[service]
260 output = {}
261 for role_name, role in data.items():
262 output[role_name] = json.dumps(role)
263 return output