blob: 5e319aecb30e6060c227b96f08a081b765558e2a [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
68 salt-call modelutils.schema_list
69
70
71 """
72 output = {}
azvyagintsev0d683712017-12-11 22:13:55 +020073 schemas = glob.glob('{}/*/schemas/*.yaml'.format(_get_base_dir()))
Ales Komarek6041b1b2017-11-22 15:26:27 +010074 for schema in schemas:
75 if os.path.exists(schema):
76 role_name = schema.split('/')[-1].replace('.yaml', '')
77 service_name = schema.split('/')[-3]
78 print role_name, service_name
79 name = '{}-{}'.format(service_name, role_name)
80 output[name] = {
81 'service': service_name,
82 'role': role_name,
83 'path': schema,
84 'valid': schema_validate(service_name, role_name)[name]
85 }
86 return output
87
88
89def schema_get(service, role):
90 """
91 Returns pillar schema for given service and role. If no service and role
92 is specified, method will return all known schemas.
93
94 CLI Examples:
95
96 .. code-block:: bash
97
98 salt-call modelutils.schema_get ntp server
99
100 """
101 schema_path = 'salt://{}/schemas/{}.yaml'.format(service, role)
102 schema = __salt__['cp.get_file_str'](schema_path)
103 if schema:
104 try:
105 data = yaml.safe_load(schema)
106 except yaml.YAMLError as exc:
107 raise Exception("Failed to parse schema:{}\n"
108 "{}".format(schema_path, exc))
109 else:
110 raise Exception("Schema not found:{}".format(schema_path))
111 return {'{}-{}'.format(service, role): data}
112
113
114def schema_validate(service, role):
115 """
116 Validates pillar schema itself of given service and role.
117
118 CLI Examples:
119
120 .. code-block:: bash
121
122 salt-call modelutils.schema_validate ntp server
123
124 """
125
126 schema = schema_get(service, role)['{}-{}'.format(service, role)]
127 cls = _validator_for(schema)
128 LOG.debug("Validating schema..")
129 try:
130 cls.check_schema(schema)
131 LOG.debug("Schema is valid")
132 data = 'Schema is valid'
133 except SchemaError as exc:
134 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200135 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100136 return {'{}-{}'.format(service, role): data}
137
138
139def model_validate(service=None, role=None):
140 """
141 Validates pillar metadata by schema for given service and role. If
142 no service and role is specified, method will validate all defined
143 services.
144
145 CLI Example:
146 .. code-block:: bash
147 salt-run modelschema.model_validate keystone server
148
149 """
150 schema = schema_get(service, role)['{}-{}'.format(service, role)]
151 model = __salt__['pillar.get']('{}:{}'.format(service, role))
152 try:
153 _validate(model, schema)
154 data = 'Model is valid'
155 except SchemaError as exc:
156 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200157 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100158 except ValidationError as exc:
159 LOG.error("ValidationError:{}\nInstance:{}\n"
azvyagintsevb57aaa12017-12-26 15:06:47 +0200160 "Schema title:{}\n"
161 "SchemaPath:{}".format(exc.message,
162 exc.instance,
163 exc.schema.get(
164 "title",
165 "Schema title not set!"),
Ales Komarek6041b1b2017-11-22 15:26:27 +0100166 exc.schema_path))
167 raise Exception("ValidationError")
168 return {'{}-{}'.format(service, role): data}
169
170
171def data_validate(model, schema):
172 """
173 Validates model by given schema.
174
175 CLI Example:
176 .. code-block:: bash
177 salt-run modelschema.data_validate {'a': 'b'} {'a': 'b'}
178 """
179 try:
180 _validate(model, schema)
181 data = 'Model is valid'
182 except SchemaError as exc:
183 LOG.error("SchemaError:{}".format(exc))
azvyagintsevb57aaa12017-12-26 15:06:47 +0200184 raise Exception("SchemaError")
Ales Komarek6041b1b2017-11-22 15:26:27 +0100185 except ValidationError as exc:
186 LOG.error("ValidationError:{}\nInstance:{}\n"
azvyagintsevb57aaa12017-12-26 15:06:47 +0200187 "Schema title:{}\n"
188 "SchemaPath:{}".format(exc.message,
189 exc.instance,
190 exc.schema.get(
191 "title",
192 "Schema title not set!"),
Ales Komarek6041b1b2017-11-22 15:26:27 +0100193 exc.schema_path))
194 raise Exception("ValidationError")
195 return data
196
197
198def schema_from_tests(service):
199 """
200 Generate pillar schema skeleton for given service. Method iterates throught
201 test pillars and generates schema scaffold structure in JSON format that
202 can be passed to service like http://jsonschema.net/ to get the basic
203 schema for the individual roles of the service.
204
205 CLI Examples:
206
207 .. code-block:: bash
208
209 salt-call modelutils.schema_from_tests keystone
210 """
211 pillars = glob.glob(
212 '{}/{}/tests/pillar/*.sls'.format(_get_base_dir(), service))
213 raw_data = {}
214 for pillar in pillars:
215 if os.path.exists(pillar):
216 with open(pillar, 'r') as stream:
217 try:
218 data = yaml.load(stream)
219 except yaml.YAMLError as exc:
220 data = {}
221 LOG.error('{}: {}'.format(pillar, repr(exc)))
222 try:
223 _dict_deep_merge(raw_data, data)
224 except Exception as exc:
225 LOG.error('{}: {}'.format(pillar, repr(exc)))
226 if service not in raw_data.keys():
azvyagintsevb57aaa12017-12-26 15:06:47 +0200227 LOG.error("Could not find applicable data "
228 "for:{}\n at:{}".format(service, _get_base_dir()))
229 raise Exception("DataError")
230
Ales Komarek6041b1b2017-11-22 15:26:27 +0100231 data = raw_data[service]
232 output = {}
233 for role_name, role in data.items():
234 output[role_name] = json.dumps(role)
235 return output