blob: 451f3c19347c9162a8b8507da6080a79fbcb9a2d [file] [log] [blame]
from __future__ import absolute_import
import glob
import json
import logging
import os.path
import yaml
# Import third party libs
try:
from jsonschema import validate as _validate
from jsonschema.validators import validator_for as _validator_for
from jsonschema.exceptions import SchemaError, ValidationError
HAS_JSONSCHEMA = True
except ImportError:
HAS_JSONSCHEMA = False
__virtualname__ = 'modelschema'
LOG = logging.getLogger(__name__)
def __virtual__():
"""
Only load if jsonschema library exist.
"""
if not HAS_JSONSCHEMA:
return (
False,
'Can not load module jsonschema: jsonschema library not found')
return __virtualname__
def _get_base_dir():
return __salt__['config.get']('pilllar_schema_path',
'/usr/share/salt-formulas/env')
def _dict_deep_merge(a, b, path=None):
"""
Merges dict(b) into dict(a)
"""
if path is None:
path = []
for key in b:
if key in a:
if isinstance(a[key], dict) and isinstance(b[key], dict):
_dict_deep_merge(a[key], b[key], path + [str(key)])
elif a[key] == b[key]:
pass # same leaf value
else:
raise Exception(
'Conflict at {}'.format('.'.join(path + [str(key)])))
else:
a[key] = b[key]
return a
def schema_list():
"""
Returns list of all defined schema files.
CLI Examples:
.. code-block:: bash
salt-call modelschema.schema_list
"""
output = {}
def _parse_schema(pattern, helper, output):
schemas = glob.glob(pattern.format(_get_base_dir()))
for schema in schemas:
if os.path.exists(schema):
sc_splited = schema.split('/')
parsed = helper(sc_splited)
name = '{}-{}'.format(*parsed[:2])
output[name] = {
'service': parsed[0],
'role': parsed[1],
'path': schema,
'valid': schema_validate(*parsed)[name]
}
versioned_schemas_path = '{}/*/schemas/*/*.yaml'
_parse_schema(versioned_schemas_path, lambda sc: (sc[-4], sc[-1].replace('.yaml', ''), sc[-2]), output)
common_schemas_path = '{}/*/schemas/*.yaml'
_parse_schema(common_schemas_path, lambda sc: (sc[-3], sc[-1].replace('.yaml', '')), output)
return output
def schema_get(service, role, version=None):
"""
Returns pillar schema for given service and role.
CLI Examples:
.. code-block:: bash
salt-call modelschema.schema_get ntp server
.. or ..
salt-call modelschema.schema_get keystone server pike
"""
if version:
schema_path = 'salt://{}/schemas/{}/{}.yaml'.format(service, version, role)
else:
schema_path = 'salt://{}/schemas/{}.yaml'.format(service, role)
schema = __salt__['cp.get_file_str'](schema_path)
if schema:
try:
data = yaml.safe_load(schema)
except yaml.YAMLError as exc:
raise Exception("Failed to parse schema:{}\n"
"{}".format(schema_path, exc))
else:
raise Exception("Schema not found:{}".format(schema_path))
return {'{}-{}'.format(service, role): data}
def schema_validate(service, role, version=None):
"""
Validates pillar schema itself of given service and role.
CLI Examples:
.. code-block:: bash
salt-call modelschema.schema_validate ntp server
.. or ..
salt-call modelschema.schema_validate keystone server pike
"""
schema = schema_get(service, role, version)['{}-{}'.format(service, role)]
cls = _validator_for(schema)
LOG.debug("Validating schema..")
try:
cls.check_schema(schema)
LOG.debug("Schema is valid")
data = 'Schema is valid'
except SchemaError as exc:
LOG.error("SchemaError:{}".format(exc))
raise Exception("SchemaError")
return {'{}-{}'.format(service, role): data}
def model_validate(service, role, version=None):
"""
Validates pillar metadata by schema for given service and role.
CLI Example:
.. code-block:: bash
salt-call modelschema.model_validate ntp server
.. or ..
salt-call modelschema.model_validate keystone server pike
"""
schema = schema_get(service, role, version)['{}-{}'.format(service, role)]
model = __salt__['pillar.get']('{}:{}'.format(service, role))
try:
_validate(model, schema)
data = 'Model is valid'
except SchemaError as exc:
LOG.error("SchemaError:{}".format(exc))
raise Exception("SchemaError")
except ValidationError as exc:
LOG.error("ValidationError:{}\nInstance:{}\n"
"Schema title:{}\n"
"SchemaPath:{}".format(exc.message,
exc.instance,
exc.schema.get(
"title",
"Schema title not set!"),
exc.schema_path))
raise Exception("ValidationError")
return {'{}-{}'.format(service, role): data}
def data_validate(model, schema):
"""
Validates model by given schema.
CLI Example:
.. code-block:: bash
salt-run modelschema.data_validate {'a': 'b'} {'a': 'b'}
"""
try:
_validate(model, schema)
data = 'Model is valid'
except SchemaError as exc:
LOG.error("SchemaError:{}".format(exc))
raise Exception("SchemaError")
except ValidationError as exc:
LOG.error("ValidationError:{}\nInstance:{}\n"
"Schema title:{}\n"
"SchemaPath:{}".format(exc.message,
exc.instance,
exc.schema.get(
"title",
"Schema title not set!"),
exc.schema_path))
raise Exception("ValidationError")
return data
def schema_from_tests(service, version=None):
"""
Generate pillar schema skeleton for given service. Method iterates throught
test pillars and generates schema scaffold structure in JSON format that
can be passed to service like http://jsonschema.net/ to get the basic
schema for the individual roles of the service.
CLI Examples:
.. code-block:: bash
salt-call modelschema.schema_from_tests keystone pike
.. or ..
salt-call modelschema.schema_from_tests ntp
"""
if version:
pillars = glob.glob(
'{}/{}/tests/pillar/{}/*.sls'.format(_get_base_dir(), service, version))
else:
pillars = glob.glob(
'{}/{}/tests/pillar/*.sls'.format(_get_base_dir(), service))
raw_data = {}
for pillar in pillars:
if os.path.exists(pillar):
with open(pillar, 'r') as stream:
try:
data = yaml.load(stream)
except yaml.YAMLError as exc:
data = {}
LOG.error('{}: {}'.format(pillar, repr(exc)))
try:
_dict_deep_merge(raw_data, data)
except Exception as exc:
LOG.error('{}: {}'.format(pillar, repr(exc)))
if service not in raw_data.keys():
LOG.error("Could not find applicable data "
"for:{}\n at:{}".format(service, _get_base_dir()))
raise Exception("DataError")
data = raw_data[service]
output = {}
for role_name, role in data.items():
output[role_name] = json.dumps(role)
return output