SPI Method to Validate Images

Adds SPI method and utility module to Sahara. Does not yet include any
yaml files or plugin implementations in this change set; early
implementations are in the works for testing but not yet ready for
review, and will come in separate commits for modularity.

Partially-implements: blueprint validate-image-spi
Change-Id: I9f59219a801325176a70fd8da1ca3b38efc5b109
This commit is contained in:
Ethan Gafford 2015-12-10 19:36:54 -05:00 committed by Vitaly Gridnev
parent 2e1e6e55d4
commit bde808fe2a
6 changed files with 1169 additions and 2 deletions

View File

@ -142,3 +142,39 @@ class ResourceManagerHAConfigurationError(e.SaharaException):
self.message = self.base_message % message
super(ResourceManagerHAConfigurationError, self).__init__()
class ImageValidationSpecificationError(e.SaharaException):
"""Exception indicating that an image validation spec is in error."""
base_message = _("Image validation spec is in error: %s")
def __init__(self, message, *args):
self.code = "IMAGE_SPECIFICATION_ERROR"
self.message = self.base_message % message
super(ImageValidationSpecificationError, self).__init__()
class ImageValidationError(e.SaharaException):
"""Exception indicating that an image has failed validation."""
base_message = _("Image has failed validation: %s")
def __init__(self, message):
self.code = "IMAGE_VALIDATION_FAILED"
self.message = self.base_message % message
super(ImageValidationError, self).__init__()
class AllValidationsFailedError(ImageValidationError):
"""Exception indicating that all validations in an any block failed."""
sub_message = _("All validations have failed: %s")
def __init__(self, exceptions):
data = ";".join(ex.message for ex in exceptions)
message = self.sub_message % data
super(AllValidationsFailedError, self).__init__(message)

727
sahara/plugins/images.py Normal file
View File

@ -0,0 +1,727 @@
# 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)
@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 = 'SIV_DISTRO'
RECONCILE_KEY = 'SIV_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,
}
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, env_map=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 env_map: A map of environment variables to pass to scripts.
:return True if successful, ValidationAttemptFailed object if failed.
"""
try:
self.validate(
remote, reconcile=reconcile, env_map=env_map, **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"]
}
@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)
specs = spec['validators']
validator = SaharaAllValidator.from_spec(
specs, validator_map, resource_roots)
return cls(validator)
def __init__(self, validator):
"""Constructor method.
:param validator: A SaharaAllValidator containing the specified
validators.
"""
self.validator = validator
self.validators = validator.validators
@transform_exception(ex.RemoteCommandException, p_ex.ImageValidationError)
def validate(self, remote, reconcile=True, env_map=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 env_map: A map of environment variables to pass to scripts.
:raises ImageValidationError: If validation fails.
"""
env_map = copy.deepcopy(env_map) if env_map else {}
env_map[self.RECONCILE_KEY] = 1 if reconcile else 0
raw_distro = remote.execute_command('lsb_release -is')
distro = raw_distro[1].strip().lower()
env_map[self.DISTRO_KEY] = distro
self.validator.validate(remote, reconcile=reconcile, env_map=env_map)
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, env_map=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 env_map: A map of environment variables to pass to scripts.
:raises ImageValidationError: If validation fails.
"""
env_distro = env_map[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):
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
}
},
}
},
{
"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
env_map 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)
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 = None
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 env_map 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, env_map=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 env_map: A map of environment variables to pass to scripts.
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.
"""
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(env_map)
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:
env_map[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, env_map=None, **kwargs):
results = []
for validator in self.validators:
result = validator.try_validate(remote, reconcile=reconcile,
env_map=env_map, **kwargs)
results.append(result)
if result:
break
return results
def validate(self, remote, reconcile=True, env_map=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 env_map: A map of environment variables to pass to scripts.
:raises ImageValidationError: If validation fails.
"""
results = self._try_all(remote, reconcile=False, env_map=env_map)
if reconcile and not any(results):
results = self._try_all(remote, reconcile=True, env_map=env_map)
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, env_map=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 env_map: A map of environment variables to pass to scripts.
:raises ImageValidationError: If validation fails.
"""
for validator in self.validators:
validator.validate(remote, reconcile=reconcile, env_map=env_map)
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, env_map=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 env_map: A map of environment variables to pass to scripts.
:raises ImageValidationError: If validation fails.
"""
env_distro = env_map[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, env_map=env_map)
break
def _sudo(remote, cmd, **kwargs):
return remote.execute_command(cmd, run_as_root=True, **kwargs)

View File

@ -81,6 +81,10 @@ class ProvisioningPluginBase(plugins_base.PluginInterface):
def decommission_nodes(self, cluster, instances):
pass
@plugins_base.optional
def validate_images(self, cluster, reconcile=True):
pass
@plugins_base.optional
def convert(self, config, plugin_name, version, template_name,
cluster_template_create):

View File

@ -287,9 +287,10 @@ def _provision_cluster(cluster_id):
# configure cluster
cluster = c_u.change_cluster_status(
cluster, c_u.CLUSTER_STATUS_CONFIGURING)
shares.mount_shares(cluster)
context.set_step_type(_("Plugin: configure cluster"))
if hasattr(plugin, 'validate_images'):
plugin.validate_images(cluster, reconcile=True)
shares.mount_shares(cluster)
plugin.configure_cluster(cluster)
# starting prepared and configured cluster

View File

@ -0,0 +1,389 @@
# 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 mock
import yaml
from sahara import exceptions as ex
from sahara.plugins import exceptions as p_ex
from sahara.plugins import images
from sahara.tests.unit import base as b
class TestImages(b.SaharaTestCase):
def test_package_spec(self):
cls = images.SaharaPackageValidator
validator = cls.from_spec("java", {}, [])
self.assertIsInstance(validator, cls)
self.assertEqual(str(validator.packages[0]), "java")
validator = cls.from_spec({"java": {"version": "8"}}, {}, [])
self.assertIsInstance(validator, cls)
self.assertEqual(str(validator.packages[0]), "java-8")
validator = cls.from_spec(
[{"java": {"version": "8"}}, "hadoop"], {}, [])
self.assertIsInstance(validator, cls)
self.assertEqual(str(validator.packages[0]), "java-8")
self.assertEqual(str(validator.packages[1]), "hadoop")
def test_script_spec(self):
cls = images.SaharaScriptValidator
resource_roots = ['tests/unit/plugins']
validator = cls.from_spec('test_images.py', {}, resource_roots)
self.assertIsInstance(validator, cls)
self.assertEqual(validator.env_vars, ['SIV_RECONCILE', 'SIV_DISTRO'])
validator = cls.from_spec(
{'test_images.py': {'env_vars': ['EXTRA_FILE', 'USER']}},
{}, resource_roots)
self.assertIsInstance(validator, cls)
self.assertEqual(validator.env_vars,
['SIV_RECONCILE', 'SIV_DISTRO',
'EXTRA_FILE', 'USER'])
def test_all_spec(self):
cls = images.SaharaAllValidator
validator_map = images.SaharaImageValidatorBase.get_validator_map()
validator = cls.from_spec(
[{'package': {'java': {'version': '8'}}}, {'package': 'hadoop'}],
validator_map, [])
self.assertIsInstance(validator, cls)
self.assertEqual(len(validator.validators), 2)
self.assertEqual(validator.validators[0].packages[0].name, 'java')
self.assertEqual(validator.validators[1].packages[0].name, 'hadoop')
def test_any_spec(self):
cls = images.SaharaAnyValidator
validator_map = images.SaharaImageValidatorBase.get_validator_map()
validator = cls.from_spec(
[{'package': {'java': {'version': '8'}}}, {'package': 'hadoop'}],
validator_map, [])
self.assertIsInstance(validator, cls)
self.assertEqual(len(validator.validators), 2)
self.assertEqual(validator.validators[0].packages[0].name, 'java')
self.assertEqual(validator.validators[1].packages[0].name, 'hadoop')
def test_os_case_spec(self):
cls = images.SaharaOSCaseValidator
validator_map = images.SaharaImageValidatorBase.get_validator_map()
spec = [
{'redhat': [{'package': 'nfs-utils'}]},
{'debian': [{'package': 'nfs-common'}]}
]
validator = cls.from_spec(spec, validator_map, [])
self.assertIsInstance(validator, cls)
self.assertEqual(len(validator.distros), 2)
self.assertEqual(validator.distros[0].distro, 'redhat')
self.assertEqual(validator.distros[1].distro, 'debian')
redhat, debian = (
validator.distros[os].validator.validators[0].packages[0].name
for os in range(2))
self.assertEqual(redhat, 'nfs-utils')
self.assertEqual(debian, 'nfs-common')
def test_sahara_image_validator_spec(self):
cls = images.SaharaImageValidator
validator_map = images.SaharaImageValidatorBase.get_validator_map()
resource_roots = ['tests/unit/plugins']
spec = """
validators:
- os_case:
- redhat:
- package: nfs-utils
- debian:
- package: nfs-common
- any:
- package: java-1.8.0-openjdk-devel
- package: java-1.7.0-openjdk-devel
- script: test_images.py
- package:
- hadoop
- hadoop-libhdfs
- hadoop-native
- hadoop-pipes
- hadoop-sbin
- hadoop-lzo
- lzo
- lzo-devel
- hadoop-lzo-native
"""
spec = yaml.safe_load(spec)
validator = cls.from_spec(spec, validator_map, resource_roots)
validators = validator.validators
self.assertIsInstance(validator, cls)
self.assertEqual(len(validators), 4)
self.assertIsInstance(validators[0], images.SaharaOSCaseValidator)
self.assertIsInstance(validators[1], images.SaharaAnyValidator)
self.assertIsInstance(validators[2], images.SaharaScriptValidator)
self.assertIsInstance(validators[3], images.SaharaPackageValidator)
def test_package_validator_redhat(self):
cls = images.SaharaPackageValidator
env_map = {"SIV_DISTRO": 'centos'}
packages = [cls.Package("java", "8")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
validator.validate(remote, reconcile=False, env_map=env_map)
remote.execute_command.assert_called_with(
"rpm -q java-8", run_as_root=True)
env_map = {"SIV_DISTRO": 'fedora'}
packages = [cls.Package("java", "8"), cls.Package("hadoop")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
remote.execute_command.side_effect = (
ex.RemoteCommandException("So bad!"))
try:
validator.validate(remote, reconcile=False, env_map=env_map)
except p_ex.ImageValidationError as e:
self.assertIn("So bad!", e.message)
remote.execute_command.assert_called_with(
"rpm -q java-8 hadoop", run_as_root=True)
self.assertEqual(remote.execute_command.call_count, 1)
env_map = {"SIV_DISTRO": 'redhatenterpriseserver'}
packages = [cls.Package("java", "8"), cls.Package("hadoop")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
def side_effect(call, run_as_root=False):
if "rpm" in call:
raise ex.RemoteCommandException("So bad!")
remote.execute_command.side_effect = side_effect
try:
validator.validate(remote, reconcile=True, env_map=env_map)
except p_ex.ImageValidationError as e:
self.assertIn("So bad!", e.message)
self.assertEqual(remote.execute_command.call_count, 3)
calls = [mock.call("rpm -q java-8 hadoop", run_as_root=True),
mock.call("yum install -y java-8 hadoop", run_as_root=True),
mock.call("rpm -q java-8 hadoop", run_as_root=True)]
remote.execute_command.assert_has_calls(calls)
def test_package_validator_debian(self):
cls = images.SaharaPackageValidator
env_map = {"SIV_DISTRO": 'ubuntu'}
packages = [cls.Package("java", "8")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
validator.validate(remote, reconcile=False, env_map=env_map)
remote.execute_command.assert_called_with(
"dpkg -s java-8", run_as_root=True)
env_map = {"SIV_DISTRO": 'ubuntu'}
packages = [cls.Package("java", "8"), cls.Package("hadoop")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
remote.execute_command.side_effect = (
ex.RemoteCommandException("So bad!"))
try:
validator.validate(remote, reconcile=False, env_map=env_map)
except p_ex.ImageValidationError as e:
self.assertIn("So bad!", e.message)
remote.execute_command.assert_called_with(
"dpkg -s java-8 hadoop", run_as_root=True)
self.assertEqual(remote.execute_command.call_count, 1)
env_map = {"SIV_DISTRO": 'ubuntu'}
packages = [cls.Package("java", "8"), cls.Package("hadoop")]
validator = images.SaharaPackageValidator(packages)
remote = mock.Mock()
remote.execute_command.side_effect = (
ex.RemoteCommandException("So bad!"))
try:
validator.validate(remote, reconcile=True, env_map=env_map)
except p_ex.ImageValidationError as e:
self.assertIn("So bad!", e.message)
self.assertEqual(remote.execute_command.call_count, 2)
calls = [mock.call("dpkg -s java-8 hadoop",
run_as_root=True),
mock.call("apt-get -y install java-8 hadoop",
run_as_root=True)]
remote.execute_command.assert_has_calls(calls)
def test_script_validator(self):
cls = images.SaharaScriptValidator
env_map = {"SIV_DISTRO": 'centos'}
map_rep = "export SIV_DISTRO=centos\n"
cmd = "It's dangerous to go alone. Run this."
expected_cmd = "bash <<_SIV_\n%s\n_SIV_" % cmd
validator = cls(cmd, env_vars=env_map.keys(), output_var="SIV_DISTRO")
remote = mock.Mock(
execute_command=mock.Mock(
return_value=(0, 'fedora')))
validator.validate(remote, reconcile=True, env_map=env_map)
call = [mock.call(map_rep + expected_cmd, run_as_root=True)]
remote.execute_command.assert_has_calls(call)
self.assertEqual(env_map['SIV_DISTRO'], 'fedora')
def test_any_validator(self):
cls = images.SaharaAnyValidator
class FakeValidator(images.SaharaImageValidatorBase):
def __init__(self, mock_validate):
self.mock_validate = mock_validate
def validate(self, remote, reconcile=True, **kwargs):
self.mock_validate(remote, reconcile=reconcile, **kwargs)
# One success short circuits validation
always_tells_the_truth = FakeValidator(mock.Mock())
validator = cls([always_tells_the_truth, always_tells_the_truth])
validator.validate(None, reconcile=True)
self.assertEqual(always_tells_the_truth.mock_validate.call_count, 1)
# All failures fails, and calls with reconcile=False on all first
always_lies = FakeValidator(
mock.Mock(side_effect=p_ex.ImageValidationError("Oh no!")))
validator = cls([always_lies, always_lies])
try:
validator.validate(None, reconcile=True)
except p_ex.ImageValidationError:
pass
self.assertEqual(always_lies.mock_validate.call_count, 4)
# But it fails after a first pass if reconcile=False.
always_lies = FakeValidator(
mock.Mock(side_effect=p_ex.ImageValidationError("Oh no!")))
validator = cls([always_lies, always_lies])
try:
validator.validate(None, reconcile=False)
except p_ex.ImageValidationError:
pass
self.assertEqual(always_lies.mock_validate.call_count, 2)
# One failure doesn't end iteration.
always_tells_the_truth = FakeValidator(mock.Mock())
always_lies = FakeValidator(
mock.Mock(side_effect=p_ex.ImageValidationError("Oh no!")))
validator = cls([always_lies, always_tells_the_truth])
validator.validate(None, reconcile=True)
self.assertEqual(always_lies.mock_validate.call_count, 1)
self.assertEqual(always_tells_the_truth.mock_validate.call_count, 1)
def test_all_validator(self):
cls = images.SaharaAllValidator
# All pass
always_tells_the_truth = mock.Mock()
validator = cls([always_tells_the_truth, always_tells_the_truth])
validator.validate(None, reconcile=True)
self.assertEqual(always_tells_the_truth.validate.call_count, 2)
always_tells_the_truth.validate.assert_called_with(
None, reconcile=True, env_map=None)
# Second fails
always_tells_the_truth = mock.Mock()
always_lies = mock.Mock(validate=mock.Mock(
side_effect=p_ex.ImageValidationError("Boom!")))
validator = cls([always_tells_the_truth, always_lies])
try:
validator.validate(None, reconcile=False)
except p_ex.ImageValidationError:
pass
self.assertEqual(always_tells_the_truth.validate.call_count, 1)
self.assertEqual(always_lies.validate.call_count, 1)
always_tells_the_truth.validate.assert_called_with(
None, reconcile=False, env_map=None)
always_lies.validate.assert_called_with(
None, reconcile=False, env_map=None)
# First fails
always_tells_the_truth = mock.Mock()
always_lies = mock.Mock(validate=mock.Mock(
side_effect=p_ex.ImageValidationError("Boom!")))
validator = cls([always_lies, always_tells_the_truth])
try:
validator.validate(None, reconcile=False, env_map={})
except p_ex.ImageValidationError:
pass
self.assertEqual(always_lies.validate.call_count, 1)
always_lies.validate.assert_called_with(
None, reconcile=False, env_map={})
self.assertEqual(always_tells_the_truth.validate.call_count, 0)
def test_os_case_validator(self):
cls = images.SaharaOSCaseValidator
Distro = images.SaharaOSCaseValidator._distro_tuple
# First match wins and short circuits iteration
centos = Distro("centos", mock.Mock())
redhat = Distro("redhat", mock.Mock())
distros = [centos, redhat]
env_map = {images.SaharaImageValidator.DISTRO_KEY: "centos"}
validator = cls(distros)
validator.validate(None, reconcile=True, env_map=env_map)
self.assertEqual(centos.validator.validate.call_count, 1)
self.assertEqual(redhat.validator.validate.call_count, 0)
centos.validator.validate.assert_called_with(
None, reconcile=True, env_map=env_map)
# Familes match
centos = Distro("centos", mock.Mock())
redhat = Distro("redhat", mock.Mock())
distros = [centos, redhat]
env_map = {images.SaharaImageValidator.DISTRO_KEY: "fedora"}
validator = cls(distros)
validator.validate(None, reconcile=True, env_map=env_map)
self.assertEqual(centos.validator.validate.call_count, 0)
self.assertEqual(redhat.validator.validate.call_count, 1)
redhat.validator.validate.assert_called_with(
None, reconcile=True, env_map=env_map)
# Non-matches do nothing
centos = Distro("centos", mock.Mock())
redhat = Distro("redhat", mock.Mock())
distros = [centos, redhat]
env_map = {images.SaharaImageValidator.DISTRO_KEY: "ubuntu"}
validator = cls(distros)
validator.validate(None, reconcile=True, env_map=env_map)
self.assertEqual(centos.validator.validate.call_count, 0)
self.assertEqual(redhat.validator.validate.call_count, 0)
def test_sahara_image_validator(self):
cls = images.SaharaImageValidator
sub_validator = mock.Mock(validate=mock.Mock())
remote = mock.Mock(execute_command=mock.Mock(
return_value=(None, "CENTOS ")))
validator = cls(sub_validator)
validator.validate(remote, reconcile=True, env_map={})
expected_map = {images.SaharaImageValidatorBase.DISTRO_KEY: "centos",
images.SaharaImageValidatorBase.RECONCILE_KEY: 1}
remote.execute_command.assert_called_with('lsb_release -is')
sub_validator.validate.assert_called_with(
remote, reconcile=True, env_map=expected_map)
expected_map = {images.SaharaImageValidatorBase.DISTRO_KEY: "centos",
images.SaharaImageValidatorBase.RECONCILE_KEY: 0}
validator.validate(remote, reconcile=False, env_map={})
sub_validator.validate.assert_called_with(
remote, reconcile=False, env_map=expected_map)

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from os import path
import pkg_resources as pkg
from sahara import version
@ -28,3 +30,11 @@ def get_file_binary(file_name):
full_name = pkg.resource_filename(
version.version_info.package, file_name)
return open(full_name, "rb").read()
def try_get_file_text(file_name):
full_name = pkg.resource_filename(
version.version_info.package, file_name)
return (
open(full_name, "rb").read()
if path.isfile(full_name) else False)