# Copyright (c) 2016 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_utils import uuidutils import abc import collections import copy import functools import itertools from os import path import jsonschema import six import yaml from sahara import exceptions as ex from sahara.i18n import _ from sahara.plugins import exceptions as p_ex from sahara.utils import files def transform_exception(from_type, to_type, transform_func=None): """Decorator to transform exception types. :param from_type: The type of exception to catch and transform. :param to_type: The type of exception to raise instead. :param transform_func: A function to transform from_type into to_type, which must be of the form func(exc, to_type). Defaults to: lambda exc, new_type: new_type(exc.message) """ if not transform_func: transform_func = lambda exc, new_type: new_type(exc.message) def decorator(func): @functools.wraps(func) def handler(*args, **kwargs): try: func(*args, **kwargs) except from_type as exc: raise transform_func(exc, to_type) return handler return decorator def validate_instance(instance, validators, test_only=False, **kwargs): """Runs all validators against the specified instance. :param instance: An instance to validate. :param validators: A sequence of ImageValidators. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :raises ImageValidationError: If validation fails. """ with instance.remote() as remote: for validator in validators: validator.validate(remote, test_only=test_only, **kwargs) class ImageArgument(object): """An argument used by an image manifest.""" SPEC_SCHEMA = { "type": "object", "items": { "type": "object", "properties": { "target_variable": { "type": "string", "minLength": 1 }, "description": { "type": "string", "minLength": 1 }, "default": { "type": "string", "minLength": 1 }, "required": { "type": "boolean", "minLength": 1 }, "choices": { "type": "array", "minLength": 1, "items": { "type": "string" } } } } } @classmethod def from_spec(cls, spec): """Constructs and returns a set of arguments from a specification. :param spec: The specification for the argument set. :return: A dict of arguments built to the specification. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) arguments = {name: cls(name, arg.get('description'), arg.get('default'), arg.get('required'), arg.get('choices')) for name, arg in six.iteritems(spec)} reserved_names = ['distro', 'test_only'] for name, arg in six.iteritems(arguments): if name in reserved_names: raise p_ex.ImageValidationSpecificationError( _("The following argument names are reserved: " "{names}").format(reserved_names)) if not arg.default and not arg.required: raise p_ex.ImageValidationSpecificationError( _("Argument {name} is not required and must specify a " "default value.").format(name=arg.name)) if arg.choices and arg.default and arg.default not in arg.choices: raise p_ex.ImageValidationSpecificationError( _("Argument {name} specifies a default which is not one " "of its choices.").format(name=arg.name)) return arguments def __init__(self, name, description=None, default=None, required=False, choices=None): self.name = name self.description = description self.default = default self.required = required self.choices = choices @six.add_metaclass(abc.ABCMeta) class ImageValidator(object): """Validates the image spawned to an instance via a set of rules.""" @abc.abstractmethod def validate(self, remote, test_only=False, **kwargs): """Validates the image. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :raises ImageValidationError: If validation fails. """ pass @six.add_metaclass(abc.ABCMeta) class SaharaImageValidatorBase(ImageValidator): """Base class for Sahara's native image validation.""" DISTRO_KEY = 'distro' TEST_ONLY_KEY = 'test_only' ORDERED_VALIDATORS_SCHEMA = { "type": "array", "items": { "type": "object", "minProperties": 1, "maxProperties": 1 } } _DISTRO_FAMILES = { 'centos': 'redhat', 'centos7': 'redhat', 'fedora': 'redhat', 'redhat': 'redhat', 'rhel': 'redhat', 'redhatenterpriseserver': 'redhat', 'ubuntu': 'debian' } @staticmethod def get_validator_map(custom_validator_map=None): """Gets the map of validator name token to validator class. :param custom_validator_map: A map of validator names and classes to add to the ones Sahara provides by default. These will take precedence over the base validators in case of key overlap. :return: A map of validator names and classes. """ default_validator_map = { 'package': SaharaPackageValidator, 'script': SaharaScriptValidator, 'copy_script': SaharaCopyScriptValidator, 'any': SaharaAnyValidator, 'all': SaharaAllValidator, 'os_case': SaharaOSCaseValidator, 'argument_case': SaharaArgumentCaseValidator, 'argument_set': SaharaArgumentSetterValidator, } if custom_validator_map: default_validator_map.update(custom_validator_map) return default_validator_map @classmethod def from_yaml(cls, yaml_path, validator_map=None, resource_roots=None): """Constructs and returns a validator from the provided yaml file. :param yaml_path: The relative path to a yaml file. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A SaharaImageValidator built to the yaml specification. """ validator_map = validator_map or {} resource_roots = resource_roots or [] file_text = files.get_file_text(yaml_path) spec = yaml.safe_load(file_text) validator_map = cls.get_validator_map(validator_map) return cls.from_spec(spec, validator_map, resource_roots) @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Constructs and returns a validator from a specification object. :param spec: The specification for the validator. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A validator built to the specification. """ pass @classmethod def from_spec_list(cls, specs, validator_map, resource_roots): """Constructs a list of validators from a list of specifications. :param specs: A list of validator specifications, each of which will be a dict of size 1, where the key represents the validator type and the value respresents its specification. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A list of validators. """ validators = [] for spec in specs: validator_class, validator_spec = cls.get_class_from_spec( spec, validator_map) validators.append(validator_class.from_spec( validator_spec, validator_map, resource_roots)) return validators @classmethod def get_class_from_spec(cls, spec, validator_map): """Gets the class and specification from a validator dict. :param spec: A validator specification including its type: a dict of size 1, where the key represents the validator type and the value respresents its configuration. :param validator_map: A map of validator name to class. :return: A tuple of validator class and configuration. """ key, value = list(six.iteritems(spec))[0] validator_class = validator_map.get(key, None) if not validator_class: raise p_ex.ImageValidationSpecificationError( _("Validator type %s not found.") % validator_class) return validator_class, value class ValidationAttemptFailed(object): """An object representing a failed validation attempt. Primarily for use by the SaharaAnyValidator, which must aggregate failures for error exposition purposes. """ def __init__(self, exception): self.exception = exception def __bool__(self): return False def __nonzero__(self): return False def try_validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate, but returns rather than raising on failure. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :return: True if successful, ValidationAttemptFailed object if failed. """ try: self.validate( remote, test_only=test_only, image_arguments=image_arguments, **kwargs) return True except p_ex.ImageValidationError as exc: return self.ValidationAttemptFailed(exc) class SaharaImageValidator(SaharaImageValidatorBase): """The root of any tree of SaharaImageValidators. This validator serves as the root of the tree for SaharaImageValidators, and provides any needed initialization (such as distro retrieval.) """ SPEC_SCHEMA = { "title": "SaharaImageValidator", "type": "object", "properties": { "validators": SaharaImageValidatorBase.ORDERED_VALIDATORS_SCHEMA }, "required": ["validators"] } def get_argument_list(self): return [argument for name, argument in six.iteritems(self.arguments)] @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Constructs and returns a validator from a specification object. :param spec: The specification for the validator: a dict containing the key "validators", which contains a list of validator specifications. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A SaharaImageValidator containing all specified validators. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) arguments_spec = spec.get('arguments', {}) arguments = ImageArgument.from_spec(arguments_spec) validators_spec = spec['validators'] validator = SaharaAllValidator.from_spec( validators_spec, validator_map, resource_roots) return cls(validator, arguments) def __init__(self, validator, arguments): """Constructor method. :param validator: A SaharaAllValidator containing the specified validators. """ self.validator = validator self.validators = validator.validators self.arguments = arguments @transform_exception(ex.RemoteCommandException, p_ex.ImageValidationError) def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate the image. Before deferring to contained validators, performs one-time setup steps such as distro discovery. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ argument_values = {} for name, argument in six.iteritems(self.arguments): if name not in image_arguments: if argument.required: raise p_ex.ImageValidationError( _("Argument {name} is required for image " "processing.").format(name=name)) else: argument_values[name] = argument.default else: value = image_arguments[name] choices = argument.choices if choices and value not in choices: raise p_ex.ImageValidationError( _("Value for argument {name} must be one of " "{choices}.").format(name=name, choices=choices)) else: argument_values[name] = value argument_values[self.DISTRO_KEY] = remote.get_os_distrib() self.validator.validate(remote, test_only=test_only, image_arguments=argument_values) class SaharaPackageValidator(SaharaImageValidatorBase): """A validator that checks package installation state on the instance.""" class Package(object): def __init__(self, name, version=None): self.name = name self.version = version def __str__(self): return ("%s-%s" % (self.name, self.version) if self.version else self.name) _SINGLE_PACKAGE_SCHEMA = { "oneOf": [ { "type": "object", "minProperties": 1, "maxProperties": 1, "additionalProperties": { "type": "object", "properties": { "version": { "type": "string", "minLength": 1 }, } }, }, { "type": "string", "minLength": 1 } ] } SPEC_SCHEMA = { "title": "SaharaPackageValidator", "oneOf": [ _SINGLE_PACKAGE_SCHEMA, { "type": "array", "items": _SINGLE_PACKAGE_SCHEMA, "minLength": 1 } ] } @classmethod def _package_from_spec(cls, spec): """Builds a single package object from a specification. :param spec: May be a string or single-length dictionary of name to configuration values. :return: A package object. """ if isinstance(spec, six.string_types): return cls.Package(spec, None) else: package, properties = list(six.iteritems(spec))[0] version = properties.get('version', None) return cls.Package(package, version) @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds a package validator from a specification. :param spec: May be a string, a single-length dictionary of name to configuration values, or a list containing any number of either or both of the above. Configuration values may include: version: The version of the package to check and/or install. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A validator that will check that the specified package or packages are installed. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) packages = ([cls._package_from_spec(package_spec) for package_spec in spec] if isinstance(spec, list) else [cls._package_from_spec(spec)]) return cls(packages) def __init__(self, packages): self.packages = packages @transform_exception(ex.RemoteCommandException, p_ex.ImageValidationError) def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate package installation on the image. Even if test_only=False, attempts to verify previous package installation offline before using networked tools to validate or install new packages. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ env_distro = image_arguments[self.DISTRO_KEY] env_family = self._DISTRO_FAMILES[env_distro] check, install = self._DISTRO_TOOLS[env_family] if not env_family: raise p_ex.ImageValidationError( _("Unknown distro: cannot verify or install packages.")) try: check(self, remote) except (ex.SubprocessException, ex.RemoteCommandException, RuntimeError): if not test_only: install(self, remote) check(self, remote) else: raise def _dpkg_check(self, remote): check_cmd = ("dpkg -s %s" % " ".join(str(package) for package in self.packages)) return _sudo(remote, check_cmd) def _rpm_check(self, remote): check_cmd = ("rpm -q %s" % " ".join(str(package) for package in self.packages)) return _sudo(remote, check_cmd) def _yum_install(self, remote): install_cmd = ( "yum install -y %s" % " ".join(str(package) for package in self.packages)) _sudo(remote, install_cmd) def _apt_install(self, remote): install_cmd = ( "apt-get -y install %s" % " ".join(str(package) for package in self.packages)) return _sudo(remote, install_cmd) _DISTRO_TOOLS = { "redhat": (_rpm_check, _yum_install), "debian": (_dpkg_check, _apt_install) } class SaharaScriptValidator(SaharaImageValidatorBase): """A validator that runs a script on the instance.""" _DEFAULT_ENV_VARS = [SaharaImageValidatorBase.TEST_ONLY_KEY, SaharaImageValidatorBase.DISTRO_KEY] SPEC_SCHEMA = { "title": "SaharaScriptValidator", "oneOf": [ { "type": "object", "minProperties": 1, "maxProperties": 1, "additionalProperties": { "type": "object", "properties": { "env_vars": { "type": "array", "items": { "type": "string" } }, "output": { "type": "string", "minLength": 1 }, "inline": { "type": "string", "minLength": 1 } }, } }, { "type": "string" } ] } @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds a script validator from a specification. :param spec: May be a string or a single-length dictionary of name to configuration values. Configuration values include: env_vars: A list of environment variable names to send to the script. output: A key into which to put the stdout of the script in the image_arguments of the validation run. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A validator that will run a script on the image. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) script_contents = None if isinstance(spec, six.string_types): script_path = spec env_vars, output_var = cls._DEFAULT_ENV_VARS, None else: script_path, properties = list(six.iteritems(spec))[0] env_vars = cls._DEFAULT_ENV_VARS + properties.get('env_vars', []) output_var = properties.get('output', None) script_contents = properties.get('inline') if not script_contents: for root in resource_roots: file_path = path.join(root, script_path) script_contents = files.try_get_file_text(file_path) if script_contents: break if not script_contents: raise p_ex.ImageValidationSpecificationError( _("Script %s not found in any resource roots.") % script_path) return SaharaScriptValidator(script_contents, env_vars, output_var) def __init__(self, script_contents, env_vars=None, output_var=None): """Constructor method. :param script_contents: A string representation of the script. :param env_vars: A list of environment variables to send to the script. :param output_var: A key into which to put the stdout of the script in the image_arguments of the validation run. :return: A SaharaScriptValidator. """ self.script_contents = script_contents self.env_vars = env_vars or [] self.output_var = output_var @transform_exception(ex.RemoteCommandException, p_ex.ImageValidationError) def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate by running a script on the image. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. Note that the key SIV_TEST_ONLY will be set to 1 if the script should test_only and 0 otherwise; all scripts should act on this input if possible. The key SIV_DISTRO will also contain the distro representation, per `lsb_release -is`. :raises ImageValidationError: If validation fails. """ arguments = copy.deepcopy(image_arguments) arguments[self.TEST_ONLY_KEY] = 1 if test_only else 0 script = "\n".join(["%(env_vars)s", "%(script)s"]) env_vars = "\n".join("export %s=%s" % (key, value) for (key, value) in six.iteritems(arguments) if key in self.env_vars) script = script % {"env_vars": env_vars, "script": self.script_contents} path = '/tmp/%s.sh' % uuidutils.generate_uuid() remote.write_file_to(path, script, run_as_root=True) _sudo(remote, 'chmod +x %s' % path) code, stdout = _sudo(remote, path) if self.output_var: image_arguments[self.output_var] = stdout class SaharaCopyScriptValidator(SaharaImageValidatorBase): """A validator that copy a script to the instance.""" SPEC_SCHEMA = { "title": "SaharaCopyScriptValidator", "oneOf": [ { "type": "object", "minProperties": 1, "maxProperties": 1, "additionalProperties": { "type": "object", "properties": { "output": { "type": "string", "minLength": 1 }, "inline": { "type": "string", "minLength": 1 } }, } }, { "type": "string" } ] } @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds a copy script validator from a specification. :param spec: May be a string or a single-length dictionary of name to configuration values. Configuration values include: env_vars: A list of environment variable names to send to the script. output: A key into which to put the stdout of the script in the image_arguments of the validation run. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A validator that will copy a script to the image. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) script_contents = None if isinstance(spec, six.string_types): script_path = spec output_var = None else: script_path, properties = list(six.iteritems(spec))[0] output_var = properties.get('output', None) script_contents = properties.get('inline') if not script_contents: for root in resource_roots: file_path = path.join(root, script_path) script_contents = files.try_get_file_text(file_path) if script_contents: break script_name = script_path.split('/')[2] if not script_contents: raise p_ex.ImageValidationSpecificationError( _("Script %s not found in any resource roots.") % script_path) return SaharaCopyScriptValidator(script_contents, script_name, output_var) def __init__(self, script_contents, script_name, output_var=None): """Constructor method. :param script_contents: A string representation of the script. :param output_var: A key into which to put the stdout of the script in the image_arguments of the validation run. :return: A SaharaScriptValidator. """ self.script_contents = script_contents self.script_name = script_name self.output_var = output_var @transform_exception(ex.RemoteCommandException, p_ex.ImageValidationError) def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate by running a script on the image. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. Note that the key SIV_TEST_ONLY will be set to 1 if the script should test_only and 0 otherwise; all scripts should act on this input if possible. The key SIV_DISTRO will also contain the distro representation, per `lsb_release -is`. :raises ImageValidationError: If validation fails. """ arguments = copy.deepcopy(image_arguments) arguments[self.TEST_ONLY_KEY] = 1 if test_only else 0 script = "\n".join(["%(script)s"]) script = script % {"script": self.script_contents} path = '/tmp/%s' % self.script_name remote.write_file_to(path, script, run_as_root=True) @six.add_metaclass(abc.ABCMeta) class SaharaAggregateValidator(SaharaImageValidatorBase): """An abstract class representing an ordered list of other validators.""" SPEC_SCHEMA = SaharaImageValidator.ORDERED_VALIDATORS_SCHEMA @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds the aggregate validator from a specification. :param spec: A list of validator definitions, each of which is a single-length dictionary of name to configuration values. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: An aggregate validator. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) validators = cls.from_spec_list(spec, validator_map, resource_roots) return cls(validators) def __init__(self, validators): self.validators = validators class SaharaAnyValidator(SaharaAggregateValidator): """A list of validators, only one of which must succeed.""" def _try_all(self, remote, test_only=False, image_arguments=None, **kwargs): results = [] for validator in self.validators: result = validator.try_validate(remote, test_only=test_only, image_arguments=image_arguments, **kwargs) results.append(result) if result: break return results def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate any of the contained validators. Note that if test_only=False, this validator will first run all contained validators using test_only=True, and succeed immediately should any pass validation. If all fail, it will only then run them using test_only=False, and again succeed immediately should any pass. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ results = self._try_all(remote, test_only=True, image_arguments=image_arguments) if not test_only and not any(results): results = self._try_all(remote, test_only=False, image_arguments=image_arguments) if not any(results): raise p_ex.AllValidationsFailedError(result.exception for result in results) class SaharaAllValidator(SaharaAggregateValidator): """A list of validators, all of which must succeed.""" def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate all of the contained validators. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ for validator in self.validators: validator.validate(remote, test_only=test_only, image_arguments=image_arguments) class SaharaOSCaseValidator(SaharaImageValidatorBase): """A validator which will take different actions depending on distro.""" _distro_tuple = collections.namedtuple('Distro', ['distro', 'validator']) SPEC_SCHEMA = { "type": "array", "minLength": 1, "items": { "type": "object", "minProperties": 1, "maxProperties": 1, "additionalProperties": SaharaImageValidator.ORDERED_VALIDATORS_SCHEMA, } } @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds an os_case validator from a specification. :param spec: A list of single-length dictionaries. The key of each is a distro or family name and the value under each key is a list of validators (all of which must succeed.) :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A SaharaOSCaseValidator. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) distros = itertools.chain(*(six.iteritems(distro_spec) for distro_spec in spec)) distros = [ cls._distro_tuple(key, SaharaAllValidator.from_spec( value, validator_map, resource_roots)) for (key, value) in distros] return cls(distros) def __init__(self, distros): """Constructor method. :param distros: A list of distro tuples (distro, list of validators). """ self.distros = distros def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate depending on distro. May match the OS by specific distro or by family (centos may match "centos" or "redhat", for instance.) If multiple keys match the distro, only the validators under the first matched key will be run. If no keys match, no validators are run, and validation proceeds. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ env_distro = image_arguments[self.DISTRO_KEY] family = self._DISTRO_FAMILES.get(env_distro) matches = {env_distro, family} if family else {env_distro} for distro, validator in self.distros: if distro in matches: validator.validate( remote, test_only=test_only, image_arguments=image_arguments) break class SaharaArgumentCaseValidator(SaharaImageValidatorBase): """A validator which will take different actions depending on distro.""" SPEC_SCHEMA = { "type": "object", "properties": { "argument_name": { "type": "string", "minLength": 1 }, "cases": { "type": "object", "minProperties": 1, "additionalProperties": SaharaImageValidator.ORDERED_VALIDATORS_SCHEMA, }, }, "additionalProperties": False, "required": ["argument_name", "cases"] } @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds an argument_case validator from a specification. :param spec: A dictionary with two items: "argument_name", containing a string indicating the argument to be checked, and "cases", a dictionary. The key of each item in the dictionary is a value which may or may not match the argument value, and the value is a list of validators to be run in case it does. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A SaharaArgumentCaseValidator. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) argument_name = spec['argument_name'] cases = {key: SaharaAllValidator.from_spec( value, validator_map, resource_roots) for key, value in six.iteritems(spec['cases'])} return cls(argument_name, cases) def __init__(self, argument_name, cases): """Constructor method. :param argument_name: The name of an argument. :param cases: A dictionary of possible argument value to a sub-validator to run in case of a match. """ self.argument_name = argument_name self.cases = cases def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate depending on argument value. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. :raises ImageValidationError: If validation fails. """ arg = self.argument_name if arg not in image_arguments: raise p_ex.ImageValidationError( _("Argument {name} not found.").format(name=arg)) value = image_arguments[arg] if value in self.cases: self.cases[value].validate( remote, test_only=test_only, image_arguments=image_arguments) class SaharaArgumentSetterValidator(SaharaImageValidatorBase): """A validator which sets a specific argument to a specific value.""" SPEC_SCHEMA = { "type": "object", "properties": { "argument_name": { "type": "string", "minLength": 1 }, "value": { "type": "string", "minLength": 1 }, }, "additionalProperties": False, "required": ["argument_name", "value"] } @classmethod def from_spec(cls, spec, validator_map, resource_roots): """Builds an argument_set validator from a specification. :param spec: A dictionary with two items: "argument_name", containing a string indicating the argument to be set, and "value", a value to which to set that argument. :param validator_map: A map of validator name to class. :param resource_roots: The roots from which relative paths to resources (scripts and such) will be referenced. Any resource will be pulled from the first path in the list at which a file exists. :return: A SaharaArgumentSetterValidator. """ jsonschema.validate(spec, cls.SPEC_SCHEMA) argument_name = spec['argument_name'] value = spec['value'] return cls(argument_name, value) def __init__(self, argument_name, value): """Constructor method. :param argument_name: The name of an argument. :param value: A value to which to set that argument. """ self.argument_name = argument_name self.value = value def validate(self, remote, test_only=False, image_arguments=None, **kwargs): """Attempts to validate depending on argument value. :param remote: A remote socket to the instance. :param test_only: If true, all validators will only verify that a desired state is present, and fail if it is not. If false, all validators will attempt to enforce the desired state if possible, and succeed if this enforcement succeeds. :param image_arguments: A dictionary of image argument values keyed by argument name. """ image_arguments[self.argument_name] = self.value def _sudo(remote, cmd, **kwargs): return remote.execute_command(cmd, run_as_root=True, **kwargs)