13fcb05d13
- Added argument declaration to image specification - Added validation of image arguments - Removed target_variable property. Variables are now placed into variables equal to the argument name. This makes the code cleaner and avoids confusing re-mapping of argument to variable name for users. Change-Id: I974ed6b659b83e16c6406811afee3a4b08f4d3ee Partially-implements: image-generation-cli
993 lines
39 KiB
Python
993 lines
39 KiB
Python
# 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.
|
|
|
|
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, reconcile=True, **kwargs):
|
|
"""Runs all validators against the specified instance.
|
|
|
|
:param instance: An instance to validate.
|
|
:param validators: A sequence of ImageValidators.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile, **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', 'reconcile']
|
|
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, reconcile=True, **kwargs):
|
|
"""Validates the image.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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'
|
|
RECONCILE_KEY = 'reconcile'
|
|
|
|
ORDERED_VALIDATORS_SCHEMA = {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"minProperties": 1,
|
|
"maxProperties": 1
|
|
}
|
|
}
|
|
|
|
_DISTRO_FAMILES = {
|
|
'centos': 'redhat',
|
|
'centos7': 'redhat',
|
|
'fedora': '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,
|
|
'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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate, but returns rather than raising on failure.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile,
|
|
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, reconcile=True,
|
|
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 reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile,
|
|
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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate package installation on the image.
|
|
|
|
Even if reconcile=True, 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 reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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 reconcile:
|
|
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.RECONCILE_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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate by running a script on the image.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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_RECONCILE will be set to 1 if the script
|
|
should reconcile 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.RECONCILE_KEY] = 1 if reconcile else 0
|
|
script = "\n".join(["%(env_vars)s",
|
|
"bash <<_SIV_",
|
|
"%(script)s",
|
|
"_SIV_"])
|
|
env_vars = "\n".join("export %s=%s" % (key, value) for (key, value)
|
|
in six.iteritems(image_arguments)
|
|
if key in self.env_vars)
|
|
script = script % {"env_vars": env_vars,
|
|
"script": self.script_contents}
|
|
code, stdout = _sudo(remote, script)
|
|
if self.output_var:
|
|
image_arguments[self.output_var] = stdout
|
|
|
|
|
|
@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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
results = []
|
|
for validator in self.validators:
|
|
result = validator.try_validate(remote, reconcile=reconcile,
|
|
image_arguments=image_arguments,
|
|
**kwargs)
|
|
results.append(result)
|
|
if result:
|
|
break
|
|
return results
|
|
|
|
def validate(self, remote, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate any of the contained validators.
|
|
|
|
Note that if reconcile=True, this validator will first run all
|
|
contained validators using reconcile=False, and succeed immediately
|
|
should any pass validation. If all fail, it will only then run them
|
|
using reconcile=True, and again succeed immediately should any pass.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=False,
|
|
image_arguments=image_arguments)
|
|
if reconcile and not any(results):
|
|
results = self._try_all(remote, reconcile=True,
|
|
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, reconcile=True, image_arguments=None, **kwargs):
|
|
"""Attempts to validate all of the contained validators.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile,
|
|
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, reconcile=True,
|
|
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 reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile,
|
|
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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate depending on argument value.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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, reconcile=reconcile,
|
|
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, reconcile=True,
|
|
image_arguments=None, **kwargs):
|
|
"""Attempts to validate depending on argument value.
|
|
|
|
:param remote: A remote socket to the instance.
|
|
:param reconcile: If false, all validators will only verify that a
|
|
desired state is present, and fail if it is not. If true, 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)
|