| |
| 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 |