diff --git a/tuskar/templates/__init__.py b/tuskar/templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tuskar/templates/composer.py b/tuskar/templates/composer.py new file mode 100644 index 00000000..da9b6f2d --- /dev/null +++ b/tuskar/templates/composer.py @@ -0,0 +1,162 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Functionality for converting Tuskar domain models into their Heat-acceptable +formats. + +These functions are written against the HOT specification found at: +http://docs.openstack.org/developer/heat/template_guide/hot_spec.html +""" + +import yaml + + +def compose_template(template): + """Converts a template object into its HOT template format. + + :param template: template object to convert + :type template: tuskar.templates.heat.Template + + :return: HOT template + :rtype: str + """ + parameters = _compose_parameters(template) + resources = _compose_resources(template) + outputs = _compose_outputs(template) + + template_dict = { + 'heat_template_version': template.version, + 'parameters': parameters, + 'resources': resources, + 'outputs': outputs, + } + + if template.description is not None: + template_dict['description'] = template.description + + content = yaml.dump(template_dict, default_flow_style=False) + return content + + +def compose_environment(environment): + """Converts an environment object into its HOT template format. + + :param environment: environment object to convert + :type environment: tuskar.templates.heat.Environment + + :return: HOT template + :rtype: str + """ + parameters = _compose_environment_parameters(environment) + registry = _compose_resource_registry(environment) + + env_dict = { + 'parameters': parameters, + 'resource_registry': registry + } + + content = yaml.dump(env_dict, default_flow_style=False) + return content + + +def _compose_parameters(template): + parameters = {} + for p in template.parameters: + details = { + 'type': p.param_type, + 'description': p.description, + 'default': p.default, + 'label': p.label, + 'hidden': p.hidden, + } + + details = _strip_missing(details) + + if len(p.constraints) > 0: + details['constraints'] = [] + + for constraint in p.constraints: + constraint_value = { + constraint.constraint_type: constraint.definition + } + if constraint.description is not None: + constraint_value['description'] = constraint.description + details['constraints'].append(constraint_value) + + parameters[p.name] = details + + return parameters + + +def _compose_resources(template): + resources = {} + for r in template.resources: + details = { + 'type': r.resource_type, + 'metadata': r.metadata, + 'depends_on': r.depends_on, + 'update_policy': r.update_policy, + 'deletion_policy': r.deletion_policy, + } + + details = _strip_missing(details) + + # Properties + if len(r.properties) > 0: + details['properties'] = {} + for p in r.properties: + details['properties'][p.name] = p.value + + resources[r.resource_id] = details + + return resources + + +def _compose_outputs(template): + outputs = {} + for o in template.outputs: + details = { + 'description': o.description, + 'value': o.value, + } + + details = _strip_missing(details) + + outputs[o.name] = details + + return outputs + + +def _compose_environment_parameters(environment): + params = dict((p.name, p.value) for p in environment.parameters) + return params + + +def _compose_resource_registry(environment): + reg = dict((e.alias, e.filename) for e in environment.registry_entries) + return reg + + +def _strip_missing(details): + """Removes all entries from a dictionary whose value is None. This is used + in this context to remove optional attributes that were added to the + template creation. + + :type details: dict + + :return: new dictionary with the empty attributes removed + :rtype: dict + """ + return dict((k, v) for k, v in details.items() if v is not None) diff --git a/tuskar/templates/heat.py b/tuskar/templates/heat.py new file mode 100644 index 00000000..1de6c1e7 --- /dev/null +++ b/tuskar/templates/heat.py @@ -0,0 +1,448 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Object representations of the elements of a HOT template. + +These objects were created against the HOT specification found at: +http://docs.openstack.org/developer/heat/template_guide/hot_spec.html +""" + +from tuskar.templates import namespace as ns_utils + + +DEFAULT_VERSION = '2013-05-23' + + +class Template(object): + + def __init__(self, version=DEFAULT_VERSION, description=None): + super(Template, self).__init__() + self.version = version + self.description = description + self._parameter_groups = [] # list of ParameterGroup + self._parameters = [] # list of Parameter + self._resources = [] # list of Resource + self._outputs = [] # list of Output + + def __str__(self): + msg = 'Template: version=%(ver)s, description=%(desc)s, ' \ + 'parameter_count=%(param)s, output_count=%(out)s' + data = { + 'ver': self.version, + 'desc': _safe_strip(self.description), + 'param': len(self.parameters), + 'out': len(self.outputs) + } + + return msg % data + + @property + def parameter_groups(self): + return tuple(self._parameter_groups) + + @property + def parameters(self): + return tuple(self._parameters) + + @property + def resources(self): + return tuple(self._resources) + + @property + def outputs(self): + return tuple(self._outputs) + + def add_parameter(self, parameter): + """Adds a parameter to the template. + + :type parameter: tuskar.templates.heat.Parameter + """ + self._parameters.append(parameter) + + def remove_parameter(self, parameter): + """Removes a parameter from the template. + + :type parameter: tuskar.templates.heat.Parameter + :raise ValueError: if the parameter is not in the template + """ + self._parameters.remove(parameter) + + def remove_parameters_by_namespace(self, namespace): + """Removes all parameters in the given namespace. + + :type namespace: str + """ + self._parameters = \ + [p for p in self.parameters + if not ns_utils.matches_template_namespace(namespace, p.name)] + + def add_parameter_group(self, parameter_group): + """Adds a parameter group to the template. + + :type parameter_group: tuskar.templates.heat.ParameterGroup + """ + self._parameter_groups.append(parameter_group) + + def remove_parameter_group(self, parameter_group): + """Removes a parameter group from the template. + + :type parameter_group: tuskar.templates.heat.ParameterGroup + :raise ValueError: if the parameter group is not in the template + """ + self._parameter_groups.remove(parameter_group) + + def add_resource(self, resource): + """Adds a resource to the template. + + :type resource: tuskar.templates.heat.Resource + """ + self._resources.append(resource) + + def remove_resource(self, resource): + """Removes a resource from the template. + + :type resource: tuskar.templates.heat.Resource + :raise ValueError: if the resource is not in the template + """ + self._resources.remove(resource) + + def remove_resource_by_id(self, resource_id): + """Removes a resource from the template if found. + + :type resource_id: str + """ + self._resources = [r for r in self._resources + if r.resource_id != resource_id] + + def add_output(self, output): + """Adds an output to the template. + + :type output: tuskar.templates.heat.Output + """ + self._outputs.append(output) + + def remove_output(self, output): + """Removes an output from the template. + + :type output: tuskar.templates.heat.Output + :raise ValueError: if the output is not in the template + """ + self._outputs.remove(output) + + def remove_outputs_by_namespace(self, namespace): + """Removes all outputs in the given namespace from the template. + + :type namespace: str + """ + self._outputs =\ + [o for o in self.outputs + if not ns_utils.matches_template_namespace(namespace, o.name)] + + +class ParameterGroup(object): + + def __init__(self, label, description): + super(ParameterGroup, self).__init__() + self.label = label + self.description = description + self._parameter_names = set() + + def __str__(self): + msg = 'ParameterGroup: label=%(label)s, description=%(desc)s ' \ + 'parameter_names=%(names)s' + data = { + 'label': self.label, + 'desc': self.description, + 'names': ','.join(self.parameter_names), + } + return msg % data + + @property + def parameter_names(self): + return tuple(self._parameter_names) + + def add_parameter_name(self, name): + """Adds a parameter to the group. + + :type name: str + """ + self._parameter_names.add(name) + + def remove_parameter_name(self, name): + """Removes a parameter from the group if it is present. + + :type name: str + """ + self._parameter_names.discard(name) + + +class Parameter(object): + + def __init__(self, name, param_type, + description=None, label=None, default=None, hidden=None): + super(Parameter, self).__init__() + self.name = name + self.param_type = param_type + self.description = description + self.label = label + self.default = default + self.hidden = hidden + self._constraints = [] + + def __str__(self): + msg = 'Parameter: name=%(name)s, type=%(type)s, ' \ + 'description=%(desc)s, label=%(label)s, ' \ + 'default=%(def)s, hidden=%(hidden)s' + data = { + 'name': self.name, + 'type': self.param_type, + 'desc': self.description, + 'label': self.label, + 'def': self.default, + 'hidden': self.hidden, + } + return msg % data + + @property + def constraints(self): + return tuple(self._constraints) + + def add_constraint(self, constraint): + """Adds a constraint to the parameter. + + :type constraint: tuskar.templates.heat.ParameterConstraint + """ + self._constraints.append(constraint) + + def remove_constraint(self, constraint): + """Removes a constraint from the template. + + :type constraint: tuskar.templates.heat.ParameterConstraint + :raise ValueError: if the given constraint isn't in the parameter + """ + self._constraints.remove(constraint) + + +class ParameterConstraint(object): + + def __init__(self, constraint_type, definition, description=None): + super(ParameterConstraint, self).__init__() + self.constraint_type = constraint_type + self.definition = definition + self.description = description + + def __str__(self): + msg = 'Constraint: type=%(type)s, definition=%(def)s, ' \ + 'description=%(desc)s' + data = { + 'type': self.constraint_type, + 'def': self.definition, + 'desc': self.description, + } + return msg % data + + +class Resource(object): + + def __init__(self, resource_id, resource_type, + metadata=None, depends_on=None, + update_policy=None, deletion_policy=None): + super(Resource, self).__init__() + self.resource_id = resource_id + self.resource_type = resource_type + self.metadata = metadata + self.depends_on = depends_on + self.update_policy = update_policy + self.deletion_policy = deletion_policy + self._properties = [] + + def __str__(self): + msg = 'Resource: id=%(id)s, resource_type=%(type)s' + data = { + 'id': self.resource_id, + 'type': self.resource_type, + } + return msg % data + + @property + def properties(self): + return tuple(self._properties) + + def add_property(self, resource_property): + """Adds a property to the resource. + + :type resource_property: tuskar.templates.heat.ResourceProperty + """ + self._properties.append(resource_property) + + def remove_property(self, resource_property): + """Removes a property from the template. + + :type resource_property: tuskar.templates.heat.ResourceProperty + :raise ValueError: if the property isn't in the resource + """ + self._properties.remove(resource_property) + + +class ResourceProperty(object): + + def __init__(self, name, value): + super(ResourceProperty, self).__init__() + self.name = name + self.value = value + + def __str__(self): + msg = 'ResourceProperty: name=%(name)s, value=%(value)s' + data = { + 'name': self.name, + 'value': self.value, + } + return msg % data + + +class Output(object): + + def __init__(self, name, value, description=None): + super(Output, self).__init__() + self.name = name + self.value = value + self.description = description + + def __str__(self): + msg = 'Output: name=%(name)s, value=%(value)s, description=%(desc)s' + data = { + 'name': self.name, + 'value': self.value, + 'desc': _safe_strip(self.description) + } + return msg % data + + +class Environment(object): + + def __init__(self): + super(Environment, self).__init__() + self._parameters = [] + self._registry_entries = [] + + def __str__(self): + msg = 'Environment: parameter_count=%(p_count)s, ' \ + 'registry_count=%(r_count)s' + data = { + 'p_count': len(self.parameters), + 'r_count': len(self.registry_entries), + } + return msg % data + + @property + def parameters(self): + return tuple(self._parameters) + + @property + def registry_entries(self): + return tuple(self._registry_entries) + + def add_parameter(self, parameter): + """Adds a property to the environment. + + :type parameter: tuskar.templates.heat.EnvironmentParameter + """ + self._parameters.append(parameter) + + def remove_parameter(self, parameter): + """Removes a parameter from the environment. + + :type parameter: tuskar.templates.heat.EnvironmentParameter + :raise ValueError: if the parameter is not in the environment + """ + self._parameters.remove(parameter) + + def remove_parameters_by_namespace(self, namespace): + """Removes all parameters that match the given namespace. + + :type namespace: str + """ + self._parameters =\ + [p for p in self._parameters + if not ns_utils.matches_template_namespace(namespace, p.name)] + + def add_registry_entry(self, entry): + """Adds a registry entry to the environment. + + :type entry: tuskar.templates.heat.RegistryEntry + """ + self._registry_entries.append(entry) + + def remove_registry_entry(self, entry): + """Removes a registry entry from the environment. + + :type entry: tuskar.templates.heat.RegistryEntry + :raise ValueError: if the entry is not in the environment + """ + self._registry_entries.remove(entry) + + def remove_registry_entry_by_alias(self, alias): + """Removes a registry entry from the environment if it is found. + + :type alias: str + """ + self._registry_entries = [e for e in self._registry_entries + if e.alias != alias] + + +class EnvironmentParameter(object): + + def __init__(self, name, value): + super(EnvironmentParameter, self).__init__() + self.name = name + self.value = value + + def __str__(self): + msg = 'EnvironmentParameter: name=%(name)s, value=%(value)s' + data = { + 'name': self.name, + 'value': self.value, + } + return msg % data + + +class RegistryEntry(object): + + def __init__(self, alias, filename): + super(RegistryEntry, self).__init__() + self.alias = alias + self.filename = filename + + def __str__(self): + msg = 'RegistryEntry: alias=%(alias)s, filename=%(f)s' + data = { + 'alias': self.alias, + 'f': self.filename, + } + return msg % data + + +def _safe_strip(value): + """Strips the value if it is not None. + + :param value: text to be cleaned up + :type value: str or None + + :return: clean value if one was specified; None otherwise + :rtype: str or None + """ + if value is not None: + return value.strip() + return None diff --git a/tuskar/templates/namespace.py b/tuskar/templates/namespace.py new file mode 100644 index 00000000..2c977e8e --- /dev/null +++ b/tuskar/templates/namespace.py @@ -0,0 +1,66 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Methods for manipulating Heat template pieces (parameters, outputs, etc.) +and Heat environment pieces (resource alias) to scope them to a particular +namespace to prevent conflicts when combining templates. This module contains +methods for applying, removing, and testing if a name is part of a +particular namespace. +""" + +DELIMITER = '::' + +ALIAS_PREFIX = 'Tuskar::' + + +def apply_template_namespace(namespace, original_name): + """Applies a namespace to a template component, such as a parameter + or output. + + :rtype: str + """ + return namespace + DELIMITER + original_name + + +def remove_template_namespace(name): + """Strips any namespace off the given value and returns the original name. + + :rtype: str + """ + return name[name.index(DELIMITER) + len(DELIMITER):] + + +def matches_template_namespace(namespace, name): + """Returns whether or not the given name is in the specified namespace. + + :rtype: bool + """ + return name.startswith(namespace + DELIMITER) + + +def apply_resource_alias_namespace(alias): + """Creates a Heat environment resource alias under the Tuskar namespace. + + :rtype: str + """ + return ALIAS_PREFIX + alias + + +def remove_resource_alias_namespace(alias): + """Returns the original resource alias without the Tuskar namespace. + + :rtype: str + """ + return alias[len(ALIAS_PREFIX) + 1:] diff --git a/tuskar/templates/parser.py b/tuskar/templates/parser.py new file mode 100644 index 00000000..f807d1bc --- /dev/null +++ b/tuskar/templates/parser.py @@ -0,0 +1,169 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Functionality for parsing Heat files (templates and environment files) into +their object model representations. + +The parsing was written against the HOT specification found at: +http://docs.openstack.org/developer/heat/template_guide/hot_spec.html +""" + +import yaml + +from tuskar.templates.heat import Environment +from tuskar.templates.heat import EnvironmentParameter +from tuskar.templates.heat import Output +from tuskar.templates.heat import Parameter +from tuskar.templates.heat import ParameterConstraint +from tuskar.templates.heat import RegistryEntry +from tuskar.templates.heat import Resource +from tuskar.templates.heat import ResourceProperty +from tuskar.templates.heat import Template + + +def parse_template(content): + """Parses a Heat template into the Tuskar object model. + + :param content: string representation of the template + :type content: str + + :return: Tuskar representation of the template + :rtype: tuskar.templates.heat.Template + """ + yaml_parsed = yaml.load(content) + template = Template() + + _parse_version(template, yaml_parsed) + _parse_description(template, yaml_parsed) + _parse_template_parameters(template, yaml_parsed) + _parse_parameter_group(template, yaml_parsed) + _parse_resources(template, yaml_parsed) + _parse_outputs(template, yaml_parsed) + + return template + + +def parse_environment(content): + """Parses a Heat environment file into the Tuskar object model. + + :param content: string representation of the environment file + :type content: str + + :return: Tuskar representation of the environment file + :rtype: tuskar.templates.heat.Environment + """ + yaml_parsed = yaml.load(content) + environment = Environment() + + _parse_environment_parameters(environment, yaml_parsed) + _parse_resource_registry(environment, yaml_parsed) + + return environment + + +def _parse_version(template, yaml_parsed): + template.version = \ + yaml_parsed.get('heat_template_version', None) or template.version + + +def _parse_description(template, yaml_parsed): + template.description = \ + yaml_parsed.get('description', None) or template.description + + +def _parse_template_parameters(template, yaml_parsed): + yaml_parameters = yaml_parsed.get('parameters', {}) + for name, details in yaml_parameters.items(): + + # Basic parameter data + param_type = details['type'] # required + description = details.get('description', None) + label = details.get('label', None) + default = details.get('default', None) + hidden = details.get('hidden', None) + + parameter = Parameter(name, param_type, description=description, + label=label, default=default, hidden=hidden) + template.add_parameter(parameter) + + # Parse constraints if present + constraints = details.get('constraints', None) + if constraints is not None: + for constraint_details in constraints: + + # The type of constraint is a key in the constraint data, so + # rather than know all of the possible values, pop out the + # description (if present) and assume the remaining key/value + # pair is the type and definition. + + description = constraint_details.pop('description', None) + constraint_type = constraint_details.keys()[0] + definition = constraint_details[constraint_type] + + constraint = ParameterConstraint(constraint_type, definition, + description=description) + parameter.add_constraint(constraint) + + +def _parse_parameter_group(template, yaml_parsed): + # There are no plans in Tuskar to use the role template groups, so + # we can hold off implementing this until they will be present. + pass + + +def _parse_resources(template, yaml_parsed): + yaml_resources = yaml_parsed.get('resources', {}) + for resource_id, details in yaml_resources.items(): + resource_type = details['type'] # required + metadata = details.get('metadata', None) + depends_on = details.get('depends_on', None) + update_policy = details.get('update_policy', None) + deletion_policy = details.get('deletion_policy', None) + + resource = Resource(resource_id, resource_type, metadata=metadata, + depends_on=depends_on, update_policy=update_policy, + deletion_policy=deletion_policy) + template.add_resource(resource) + + for key, value in details.get('properties', {}).items(): + prop = ResourceProperty(key, value) + resource.add_property(prop) + + +def _parse_outputs(template, yaml_parsed): + yaml_outputs = yaml_parsed.get('outputs', {}) + for name, details in yaml_outputs.items(): + value = details['value'] # required + + # HOT spec doesn't list this as optional, but most descriptions are, + # so assume it is here too + description = details.get('description', None) + + output = Output(name, value, description=description) + template.add_output(output) + + +def _parse_environment_parameters(environment, yaml_parsed): + yaml_parameters = yaml_parsed.get('parameters', {}) + for name, value in yaml_parameters.items(): + parameter = EnvironmentParameter(name, value) + environment.add_parameter(parameter) + + +def _parse_resource_registry(environment, yaml_parsed): + yaml_entries = yaml_parsed.get('resource_registry', {}) + for namespace, filename in yaml_entries.items(): + entry = RegistryEntry(namespace, filename) + environment.add_registry_entry(entry) diff --git a/tuskar/templates/plan.py b/tuskar/templates/plan.py new file mode 100644 index 00000000..ef842edc --- /dev/null +++ b/tuskar/templates/plan.py @@ -0,0 +1,144 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Object representations of the Tuskar-specific domain concepts. These objects +are used to build up a deployment plan by adding templates (roles, in +Tuskar terminology). The composer module can then be used to translate +these models into the corresponding Heat format. +""" + +import copy + +from tuskar.templates.heat import Environment +from tuskar.templates.heat import EnvironmentParameter +from tuskar.templates.heat import Output +from tuskar.templates.heat import RegistryEntry +from tuskar.templates.heat import Resource +from tuskar.templates.heat import ResourceProperty +from tuskar.templates.heat import Template + +import tuskar.templates.namespace as ns_utils + + +class DeploymentPlan(object): + + def __init__(self, master_template=None, environment=None, + description=None): + super(DeploymentPlan, self).__init__() + self.master_template = \ + master_template or Template(description=description) + self.environment = environment or Environment() + + def add_template(self, namespace, template, filename): + """Adds a new template to the plan. The pieces of the template will + be prefixed with the given namespace in the plan's master template. + + :param namespace: prefix to use to prevent parameter and output + naming conflicts + :type namespace: str + :param template: template being added to the plan + :type template: tuskar.templates.heat.Template + :param filename: name of the file where the template is stored, used + when mapping the template in the environment + :type filename: str + """ + resource_alias = ns_utils.apply_resource_alias_namespace(namespace) + + self._add_to_master_template(namespace, template, resource_alias) + self._add_to_environment(namespace, template, filename, resource_alias) + + def remove_template(self, namespace): + """Removes all references to the template added under the given + namespace. This call does not error if a template with the given + namespace hasn't been added. + + :type namespace: str + """ + self._remove_from_master_template(namespace) + self._remove_from_environment(namespace) + + def _add_to_master_template(self, namespace, template, resource_alias): + # Add Parameters + for add_me in template.parameters: + cloned = copy.deepcopy(add_me) + cloned.name = ns_utils.apply_template_namespace(namespace, + add_me.name) + self.master_template.add_parameter(cloned) + + # Create Resource + resource = Resource(_generate_resource_id(namespace), resource_alias) + self.master_template.add_resource(resource) + + for map_me in template.parameters: + name = map_me.name + master_name = ns_utils.apply_template_namespace(namespace, + map_me.name) + value = {'get_param': [master_name]} + resource_property = ResourceProperty(name, value) + resource.add_property(resource_property) + + # Add Outputs + for add_me in template.outputs: + # The output creation is a bit trickier than simply copying the + # original. The master output is namespaced like the other pieces, + # and it's value is retrieved from the resource that's created in + # the master template, but will be present in that resource + # under it's original name. + output_name = ns_utils.apply_template_namespace(namespace, + add_me.name) + output_value = {'get_attr': [resource.resource_id, add_me.name]} + master_out = Output(output_name, output_value) + self.master_template.add_output(master_out) + + def _add_to_environment(self, namespace, template, + filename, resource_alias): + # Add Parameters + for add_me in template.parameters: + name = ns_utils.apply_template_namespace(namespace, add_me.name) + env_parameter = EnvironmentParameter(name, '') + self.environment.add_parameter(env_parameter) + + # Add Resource Registry Entry + registry_entry = RegistryEntry(resource_alias, filename) + self.environment.add_registry_entry(registry_entry) + + def _remove_from_master_template(self, namespace): + # Remove Parameters + self.master_template.remove_parameters_by_namespace(namespace) + + # Remove Outputs + self.master_template.remove_outputs_by_namespace(namespace) + + # Remove Resource + resource_id = _generate_resource_id(namespace) + self.master_template.remove_resource_by_id(resource_id) + + def _remove_from_environment(self, namespace): + # Remove Parameters + self.environment.remove_parameters_by_namespace(namespace) + + # Remove Resource Registry Entry + resource_alias = ns_utils.apply_resource_alias_namespace(namespace) + self.environment.remove_registry_entry_by_alias(resource_alias) + + +def _generate_resource_id(namespace): + """Generates the ID of the resource to be added to the plan's master + template when a new template is added. + + :type namespace: str + :rtype: str + """ + return namespace + '-resource' diff --git a/tuskar/tests/templates/__init__.py b/tuskar/tests/templates/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tuskar/tests/templates/test_composer.py b/tuskar/tests/templates/test_composer.py new file mode 100644 index 00000000..531c60b9 --- /dev/null +++ b/tuskar/tests/templates/test_composer.py @@ -0,0 +1,169 @@ +# -*- encoding: utf-8 -*- +# +# 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 unittest +import yaml + +from tuskar.templates import composer +from tuskar.templates import heat + + +class ComposerTests(unittest.TestCase): + + def test_compose_template(self): + # Test + sample = self._sample_template() + composed = composer.compose_template(sample) + + # Verify + self.assertTrue(isinstance(composed, str)) + + # Check that it can both be parsed back as YAML and use the resulting + # dict in the assertions + template = yaml.safe_load(composed) + + # Verify Overall Structure + self.assertEqual(5, len(template)) + self.assertTrue('heat_template_version' in template) + self.assertTrue('description' in template) + self.assertTrue('parameters' in template) + self.assertTrue('resources' in template) + self.assertTrue('outputs' in template) + + # Verify Top-Level Attributes + self.assertEqual('2013-05-23', template['heat_template_version']) + self.assertEqual('template-desc', template['description']) + + # Verify Parameters + self.assertEqual(2, len(template['parameters'])) + + self.assertTrue('p1' in template['parameters']) + self.assertEqual('t1', template['parameters']['p1']['type']) + self.assertEqual('desc-1', template['parameters']['p1']['description']) + self.assertEqual('l1', template['parameters']['p1']['label']) + self.assertEqual('def-1', template['parameters']['p1']['default']) + self.assertEqual(True, template['parameters']['p1']['hidden']) + + self.assertTrue('p2' in template['parameters']) + self.assertEqual('t2', template['parameters']['p2']['type']) + self.assertTrue('description' not in template['parameters']['p2']) + self.assertTrue('label' not in template['parameters']['p2']) + self.assertTrue('default' not in template['parameters']['p2']) + self.assertTrue('hidden' not in template['parameters']['p2']) + + # Verify Resources + self.assertEqual(2, len(template['resources'])) + + self.assertTrue('r1' in template['resources']) + self.assertEqual('t1', template['resources']['r1']['type']) + self.assertEqual('m1', template['resources']['r1']['metadata']) + self.assertEqual('r2', template['resources']['r1']['depends_on']) + self.assertEqual({'u1': 'u2'}, + template['resources']['r1']['update_policy']) + self.assertEqual({'d1': 'd2'}, + template['resources']['r1']['deletion_policy']) + + self.assertTrue('r2' in template['resources']) + self.assertEqual('t2', template['resources']['r2']['type']) + self.assertTrue('metadata' not in template['resources']['r2']) + self.assertTrue('depends_on' not in template['resources']['r2']) + self.assertTrue('update_policy' not in template['resources']['r2']) + self.assertTrue('deletion_policy' not in template['resources']['r2']) + + # Verify Outputs + self.assertEqual(2, len(template['outputs'])) + + self.assertTrue('n1' in template['outputs']) + self.assertEqual('v1', template['outputs']['n1']['value']) + self.assertEqual('desc-1', template['outputs']['n1']['description']) + + self.assertTrue('n2' in template['outputs']) + self.assertEqual('v2', template['outputs']['n2']['value']) + self.assertTrue('description' not in template['outputs']['n2']) + + def test_compose_environment(self): + # Test + sample = self._sample_environment() + composed = composer.compose_environment(sample) + + # Verify + self.assertTrue(isinstance(composed, str)) + + # Check that it can both be parsed back as YAML and use the resulting + # dict in the assertions + template = yaml.safe_load(composed) + + # Verify Overall Structure + self.assertEqual(2, len(template)) + self.assertTrue('parameters' in template) + self.assertTrue('resource_registry' in template) + + # Verify Parameters + self.assertEqual(2, len(template['parameters'])) + + self.assertTrue('n1' in template['parameters']) + self.assertEqual('v1', template['parameters']['n1']) + + self.assertTrue('n2' in template['parameters']) + self.assertEqual('v2', template['parameters']['n2']) + + # Verify Resource Registry + self.assertEqual(2, len(template['resource_registry'])) + + self.assertTrue('a1' in template['resource_registry']) + self.assertEqual('f1', template['resource_registry']['a1']) + + self.assertTrue('a2' in template['resource_registry']) + self.assertEqual('f2', template['resource_registry']['a2']) + + def _sample_template(self): + t = heat.Template(description='template-desc') + + # Complex Parameter + param = heat.Parameter('p1', 't1', description='desc-1', label='l1', + default='def-1', hidden=True) + param.add_constraint(heat.ParameterConstraint('t1', 'def-1', + description='desc-1')) + t.add_parameter(param) + + # Simple Parameter + t.add_parameter(heat.Parameter('p2', 't2')) + + # Complex Resource + resource = heat.Resource('r1', 't1', metadata='m1', depends_on='r2', + update_policy={'u1': 'u2'}, + deletion_policy={'d1': 'd2'}) + t.add_resource(resource) + + # Simple Resource + t.add_resource(heat.Resource('r2', 't2')) + + # Complex Output + t.add_output(heat.Output('n1', 'v1', description='desc-1')) + + # Simple Output + t.add_output(heat.Output('n2', 'v2')) + + return t + + def _sample_environment(self): + e = heat.Environment() + + e.add_parameter(heat.EnvironmentParameter('n1', 'v1')) + e.add_parameter(heat.EnvironmentParameter('n2', 'v2')) + + e.add_registry_entry(heat.RegistryEntry('a1', 'f1')) + e.add_registry_entry(heat.RegistryEntry('a2', 'f2')) + + return e diff --git a/tuskar/tests/templates/test_heat.py b/tuskar/tests/templates/test_heat.py new file mode 100644 index 00000000..15560cb4 --- /dev/null +++ b/tuskar/tests/templates/test_heat.py @@ -0,0 +1,386 @@ +# -*- encoding: utf-8 -*- +# +# 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 unittest + +from tuskar.templates import heat +from tuskar.templates import namespace as ns + + +class TemplateTests(unittest.TestCase): + + def test_default_version(self): + self.assertEqual(heat.DEFAULT_VERSION, '2013-05-23') + + def test_init(self): + # Test + t = heat.Template(description='test template') + str(t) # should not raise an exception + + # Verify + self.assertEqual(t.version, heat.DEFAULT_VERSION) + self.assertEqual(t.description, 'test template') + self.assertEqual(0, len(t.parameters)) + self.assertEqual(0, len(t.parameter_groups)) + self.assertEqual(0, len(t.resources)) + self.assertEqual(0, len(t.outputs)) + + def test_add_remove_parameter(self): + t = heat.Template() + p = heat.Parameter('test-param', 'test-type') + + # Test Add + t.add_parameter(p) + self.assertEqual(1, len(t.parameters)) + self.assertEqual(p, t.parameters[0]) + + # Test Remove + t.remove_parameter(p) + self.assertEqual(0, len(t.parameters)) + + def test_remove_parameters_by_namespace(self): + # Setup + t = heat.Template() + p1 = heat.Parameter(ns.apply_template_namespace('ns1', 'foo'), 't') + p2 = heat.Parameter(ns.apply_template_namespace('ns2', 'bar'), 't') + p3 = heat.Parameter(ns.apply_template_namespace('ns1', 'baz'), 't') + + t.add_parameter(p1) + t.add_parameter(p2) + t.add_parameter(p3) + + # Test + t.remove_parameters_by_namespace('ns1') + + # Verify + self.assertEqual(1, len(t.parameters)) + self.assertEqual(p2, t.parameters[0]) + + def test_remove_parameter_not_found(self): + t = heat.Template() + self.assertRaises(ValueError, t.remove_parameter, + heat.Parameter('n', 't')) + + def test_add_remove_parameter_group(self): + t = heat.Template() + pg = heat.ParameterGroup('test-label', 'test-desc') + + # Test Add + t.add_parameter_group(pg) + self.assertEqual(1, len(t.parameter_groups)) + self.assertEqual(pg, t.parameter_groups[0]) + + # Test Remove + t.remove_parameter_group(pg) + self.assertEqual(0, len(t.parameter_groups)) + + def test_add_remove_resource(self): + t = heat.Template() + r = heat.Resource('id', 't') + + # Test Add + t.add_resource(r) + self.assertEqual(1, len(t.resources)) + self.assertEqual(r, t.resources[0]) + + # Test Remove + t.remove_resource(r) + self.assertEqual(0, len(t.resources)) + + def test_remove_resource_by_id(self): + # Test + t = heat.Template() + t.add_resource(heat.Resource('id1', 't1')) + t.add_resource(heat.Resource('id2', 't2')) + + t.remove_resource_by_id('id1') + + # Verify + self.assertEqual(1, len(t.resources)) + self.assertEqual(t.resources[0].resource_type, 't2') + + def test_add_remove_output(self): + t = heat.Template() + o = heat.Output('n', 'v') + + # Test Add + t.add_output(o) + self.assertEqual(1, len(t.outputs)) + self.assertEqual(o, t.outputs[0]) + + def test_remove_outputs_by_namespace(self): + # Setup + t = heat.Template() + + o1 = heat.Output(ns.apply_template_namespace('ns1', 'foo'), 'v') + o2 = heat.Output(ns.apply_template_namespace('ns2', 'bar'), 'v') + o3 = heat.Output(ns.apply_template_namespace('ns1', 'foo'), 'v') + + t.add_output(o1) + t.add_output(o2) + t.add_output(o3) + + # Test + t.remove_outputs_by_namespace('ns1') + + # Verify + self.assertEqual(1, len(t.outputs)) + self.assertEqual(o2, t.outputs[0]) + + def test_remove_output_not_found(self): + t = heat.Template() + self.assertRaises(ValueError, t.remove_output, heat.Output('n', 'v')) + + +class ParameterGroupTests(unittest.TestCase): + + def test_init(self): + # Test + g = heat.ParameterGroup('test-label', 'test-desc') + str(g) # should not raise an exception + + # Verify + self.assertEqual(g.label, 'test-label') + self.assertEqual(g.description, 'test-desc') + self.assertEqual(0, len(g.parameter_names)) + + def test_add_remove_property_name(self): + g = heat.ParameterGroup('l', 'd') + + # Test Add + g.add_parameter_name('p1') + self.assertEqual(1, len(g.parameter_names)) + self.assertEqual('p1', g.parameter_names[0]) + + # Test Remove + g.remove_parameter_name('p1') + self.assertEqual(0, len(g.parameter_names)) + + def test_remove_name_not_found(self): + g = heat.ParameterGroup('l', 'd') + g.remove_parameter_name('n1') # should not error + + +class ParameterTests(unittest.TestCase): + + def test_init(self): + # Test + p = heat.Parameter('test-name', 'test-type', description='test-desc', + label='test-label', default='test-default', + hidden='test-hidden') + str(p) # should not error + + # Verify + self.assertEqual('test-name', p.name) + self.assertEqual('test-type', p.param_type) + self.assertEqual('test-desc', p.description) + self.assertEqual('test-label', p.label) + self.assertEqual('test-default', p.default) + self.assertEqual('test-hidden', p.hidden) + + def test_add_remove_constraint(self): + p = heat.Parameter('n', 't') + c = heat.ParameterConstraint('t', 'd') + + # Test Add + p.add_constraint(c) + self.assertEqual(1, len(p.constraints)) + self.assertEqual(c, p.constraints[0]) + + # Test Remove + p.remove_constraint(c) + self.assertEqual(0, len(p.constraints)) + + def test_remove_constraint_not_found(self): + p = heat.Parameter('n', 't') + self.assertRaises(ValueError, p.remove_constraint, + heat.ParameterConstraint('t', 'd')) + + +class ParameterConstraintTests(unittest.TestCase): + + def test_init(self): + # Test + c = heat.ParameterConstraint('test-type', 'test-def', + description='test-desc') + str(c) # should not error + + # Verify + self.assertEqual('test-type', c.constraint_type) + self.assertEqual('test-def', c.definition) + self.assertEqual('test-desc', c.description) + + +class ResourceTests(unittest.TestCase): + + def test_init(self): + # Test + r = heat.Resource('test-id', + 'test-type', + metadata='test-meta', + depends_on='test-depends', + update_policy='test-update', + deletion_policy='test-delete') + str(r) # should not error + + # Verify + self.assertEqual('test-id', r.resource_id) + self.assertEqual('test-type', r.resource_type) + self.assertEqual('test-meta', r.metadata) + self.assertEqual('test-depends', r.depends_on) + self.assertEqual('test-update', r.update_policy) + self.assertEqual('test-delete', r.deletion_policy) + + def test_add_remove_property(self): + r = heat.Resource('i', 't') + p = heat.ResourceProperty('n', 'v') + + # Test Add + r.add_property(p) + self.assertEqual(1, len(r.properties)) + self.assertEqual(p, r.properties[0]) + + # Test Remove + r.remove_property(p) + self.assertEqual(0, len(r.properties)) + + def test_remove_property_not_found(self): + r = heat.Resource('i', 't') + self.assertRaises(ValueError, r.remove_property, + heat.ResourceProperty('n', 'v')) + + +class ResourcePropertyTests(unittest.TestCase): + + def test_init(self): + # Test + p = heat.ResourceProperty('test-name', 'test-value') + str(p) # should not error + + # Verify + self.assertEqual('test-name', p.name) + self.assertEqual('test-value', p.value) + + +class OutputTests(unittest.TestCase): + + def test_init(self): + # Test + o = heat.Output('test-name', 'test-value', description='test-desc') + str(o) # should not error + + # Verify + self.assertEqual('test-name', o.name) + self.assertEqual('test-value', o.value) + self.assertEqual('test-desc', o.description) + + +class EnvironmentTests(unittest.TestCase): + + def test_init(self): + # Test + e = heat.Environment() + str(e) # should not error + + def test_add_remove_parameter(self): + e = heat.Environment() + p = heat.EnvironmentParameter('n', 'v') + + # Test Add + e.add_parameter(p) + self.assertEqual(1, len(e.parameters)) + self.assertEqual(p, e.parameters[0]) + + # Test Remove + e.remove_parameter(p) + self.assertEqual(0, len(e.parameters)) + + def test_remove_parameter_not_found(self): + e = heat.Environment() + self.assertRaises(ValueError, e.remove_parameter, + heat.EnvironmentParameter('n', 'v')) + + def test_remove_parameters_by_namespace(self): + # Setup + e = heat.Environment() + + p1 = heat.EnvironmentParameter( + ns.apply_template_namespace('ns1', 'n1'), 'v') + p2 = heat.EnvironmentParameter( + ns.apply_template_namespace('ns2', 'n2'), 'v') + p3 = heat.EnvironmentParameter( + ns.apply_template_namespace('ns1', 'n3'), 'v') + + e.add_parameter(p1) + e.add_parameter(p2) + e.add_parameter(p3) + + # Test + e.remove_parameters_by_namespace('ns1') + + # Verify + self.assertEqual(1, len(e.parameters)) + self.assertEqual(p2, e.parameters[0]) + + def test_add_remove_registry_entry(self): + e = heat.Environment() + re = heat.RegistryEntry('a', 'f') + + # Test Add + e.add_registry_entry(re) + self.assertEqual(1, len(e.registry_entries)) + self.assertEqual(re, e.registry_entries[0]) + + # Test Remove + e.remove_registry_entry(re) + self.assertEqual(0, len(e.registry_entries)) + + def test_remove_registry_entry_not_found(self): + e = heat.Environment() + self.assertRaises(ValueError, e.remove_registry_entry, + heat.RegistryEntry('a', 'f')) + + def test_remove_registry_entry_by_namespace(self): + # Setup + e = heat.Environment() + + e.add_registry_entry(heat.RegistryEntry('a1', 'f1')) + e.add_registry_entry(heat.RegistryEntry('a2', 'f2')) + e.add_registry_entry(heat.RegistryEntry('a1', 'f3')) + + # Test + e.remove_registry_entry_by_alias('a1') + + # Verify + self.assertEqual(1, len(e.registry_entries)) + self.assertEqual(e.registry_entries[0].filename, 'f2') + + +class EnvironmentParameterTests(unittest.TestCase): + + def test_init(self): + # Test + p = heat.EnvironmentParameter('test-name', 'test-value') + str(p) # should not error + + # Verify + self.assertEqual('test-name', p.name) + self.assertEqual('test-value', p.value) + + +class ModuleMethodTests(unittest.TestCase): + + def test_safe_strip(self): + self.assertEqual('foo', heat._safe_strip(' foo ')) + self.assertEqual(None, heat._safe_strip(None)) diff --git a/tuskar/tests/templates/test_namespace.py b/tuskar/tests/templates/test_namespace.py new file mode 100644 index 00000000..ac3cb8e1 --- /dev/null +++ b/tuskar/tests/templates/test_namespace.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +# +# 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 unittest + +from tuskar.templates import namespace + + +class NamespaceTests(unittest.TestCase): + + def test_apply_template_namespace(self): + namespaced = namespace.apply_template_namespace('test-ns', 'test-name') + self.assertEqual(namespaced, 'test-ns::test-name') + self.assertTrue(namespace.matches_template_namespace('test-ns', + namespaced)) + + def test_remove_template_namespace(self): + stripped = namespace.remove_template_namespace('test-ns::test-name') + self.assertEqual(stripped, 'test-name') + + def test_matches_template_namespace(self): + value = 'test-ns::test-name' + self.assertTrue(namespace.matches_template_namespace('test-ns', value)) + self.assertFalse(namespace.matches_template_namespace('fake', value)) + + def test_apply_resource_alias_namespace(self): + namespaced = namespace.apply_resource_alias_namespace('compute') + self.assertEqual(namespaced, 'Tuskar::compute') + + def test_remove_resource_alias_namespace(self): + stripped = namespace.remove_template_namespace('Tuskar::controller') + self.assertEqual(stripped, 'controller') diff --git a/tuskar/tests/templates/test_parser.py b/tuskar/tests/templates/test_parser.py new file mode 100644 index 00000000..3a735fa0 --- /dev/null +++ b/tuskar/tests/templates/test_parser.py @@ -0,0 +1,170 @@ +# -*- encoding: utf-8 -*- +# +# 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 datetime +import unittest + +from tuskar.templates import heat +from tuskar.templates import parser + + +TEST_TEMPLATE = """ +heat_template_version: 2013-05-23 + +description: Test provider resource foo + +parameters: + + key_name: + type: string + description : Name of a KeyPair + hidden: true + label: Key + + instance_type: + type: string + description: Instance type + default: m1.small + constraints: + - allowed_values: [m1.small, m1.medium, m1.large] + description: instance_type must be one of m1.small or m1.medium + + image_id: + type: string + description: ID of the image to use + default: 3e6270da-fbf7-4aef-bc78-6d0cfc3ad11b + +resources: + foo_instance: + type: OS::Nova::Server + properties: + image: { get_param: image_id } + flavor: { get_param: instance_type } + key_name: { get_param: key_name } + +outputs: + foo_ip: + description: IP of the created foo instance + value: { get_attr: [foo_instance, first_address] } +""" + +TEST_ENVIRONMENT = """ +parameters: + key_name: heat_key + instance_type: m1.small + image_id: 3e6270da-fbf7-4aef-bc78-6d0cfc3ad11b + +resource_registry: + Tuskar::Foo: provider-foo.yaml + Tuskar::Bar: provider-bar.yaml +""" + + +class ParserTests(unittest.TestCase): + + def test_parse_template(self): + # Test + t = parser.parse_template(TEST_TEMPLATE) + + # Verify + self.assertTrue(isinstance(t, heat.Template)) + self.assertEqual(t.version, datetime.date(2013, 5, 23)) + self.assertEqual(t.description, 'Test provider resource foo') + + self.assertEqual(3, len(t.parameters)) + ordered_params = sorted(t.parameters, key=lambda x: x.name) + + # Image ID Parameter + self.assertEqual('image_id', ordered_params[0].name) + self.assertEqual('string', ordered_params[0].param_type) + self.assertEqual('ID of the image to use', + ordered_params[0].description) + self.assertEqual('3e6270da-fbf7-4aef-bc78-6d0cfc3ad11b', + ordered_params[0].default) + self.assertEqual(None, ordered_params[0].hidden) + self.assertEqual(None, ordered_params[0].label) + self.assertEqual(0, len(ordered_params[0].constraints)) + + # Instance Type Parameter + self.assertEqual('instance_type', ordered_params[1].name) + self.assertEqual('string', ordered_params[1].param_type) + self.assertEqual('Instance type', ordered_params[1].description) + self.assertEqual('m1.small', ordered_params[1].default) + self.assertEqual(None, ordered_params[1].hidden) + self.assertEqual(None, ordered_params[1].label) + self.assertEqual(1, len(ordered_params[1].constraints)) + c = ordered_params[1].constraints[0] + self.assertEqual('instance_type must be one of m1.small or m1.medium', + c.description) + self.assertEqual('allowed_values', c.constraint_type) + self.assertEqual(['m1.small', 'm1.medium', 'm1.large'], c.definition) + + # Key Name Parameter + self.assertEqual('key_name', ordered_params[2].name) + self.assertEqual('string', ordered_params[2].param_type) + self.assertEqual('Name of a KeyPair', ordered_params[2].description) + self.assertEqual(None, ordered_params[2].default) + self.assertEqual(True, ordered_params[2].hidden) + self.assertEqual('Key', ordered_params[2].label) + self.assertEqual(0, len(ordered_params[2].constraints)) + + # Resources + self.assertEqual(1, len(t.resources)) + self.assertEqual('foo_instance', t.resources[0].resource_id) + self.assertEqual('OS::Nova::Server', t.resources[0].resource_type) + self.assertEqual(3, len(t.resources[0].properties)) + + resource_props = sorted(t.resources[0].properties, + key=lambda x: x.name) + self.assertEqual('flavor', resource_props[0].name) + self.assertEqual({'get_param': 'instance_type'}, + resource_props[0].value) + self.assertEqual('image', resource_props[1].name) + self.assertEqual({'get_param': 'image_id'}, + resource_props[1].value) + self.assertEqual('key_name', resource_props[2].name) + self.assertEqual({'get_param': 'key_name'}, + resource_props[2].value) + + # Outputs + self.assertEqual(1, len(t.outputs)) + self.assertEqual('foo_ip', t.outputs[0].name) + self.assertEqual({'get_attr': ['foo_instance', 'first_address']}, + t.outputs[0].value) + + def test_parse_environment(self): + # Test + e = parser.parse_environment(TEST_ENVIRONMENT) + + # Verify + self.assertTrue(isinstance(e, heat.Environment)) + + # Parameters + self.assertEqual(3, len(e.parameters)) + ordered_params = sorted(e.parameters, key=lambda x: x.name) + self.assertEqual('image_id', ordered_params[0].name) + self.assertEqual('3e6270da-fbf7-4aef-bc78-6d0cfc3ad11b', + ordered_params[0].value) + self.assertEqual('instance_type', ordered_params[1].name) + self.assertEqual('m1.small', ordered_params[1].value) + self.assertEqual('key_name', ordered_params[2].name) + self.assertEqual('heat_key', ordered_params[2].value) + + # Resource Registry + self.assertEqual(2, len(e.registry_entries)) + ordered_entries = sorted(e.registry_entries, key=lambda x: x.alias) + self.assertEqual('Tuskar::Bar', ordered_entries[0].alias) + self.assertEqual('provider-bar.yaml', ordered_entries[0].filename) + self.assertEqual('Tuskar::Foo', ordered_entries[1].alias) + self.assertEqual('provider-foo.yaml', ordered_entries[1].filename) diff --git a/tuskar/tests/templates/test_plan.py b/tuskar/tests/templates/test_plan.py new file mode 100644 index 00000000..eebc7adc --- /dev/null +++ b/tuskar/tests/templates/test_plan.py @@ -0,0 +1,135 @@ +# -*- encoding: utf-8 -*- +# +# 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 unittest + +from tuskar.templates import heat +from tuskar.templates import namespace as ns_utils +from tuskar.templates import plan + + +class DeploymentPlanTests(unittest.TestCase): + + def test_empty(self): + # Test + p = plan.DeploymentPlan(description='test-desc') + str(p) # should not error + + # Verify + self.assertTrue(isinstance(p.master_template, heat.Template)) + self.assertTrue(isinstance(p.environment, heat.Environment)) + self.assertEqual('test-desc', p.master_template.description) + + def test_existing_pieces(self): + # Test + t = heat.Template() + e = heat.Environment() + p = plan.DeploymentPlan(master_template=t, environment=e) + + # Verify + self.assertTrue(p.master_template is t) + self.assertTrue(p.environment is e) + + def test_add_template(self): + # Test + p = plan.DeploymentPlan() + t = self._generate_template() + p.add_template('ns1', t, 'template-1.yaml') + + # Verify Master Template Parameters + self.assertEqual(2, len(p.master_template.parameters)) + for original, added in zip(t.parameters, p.master_template.parameters): + self.assertTrue(added is not original) + + expected_name = ns_utils.apply_template_namespace('ns1', + original.name) + self.assertEqual(added.name, expected_name) + self.assertEqual(added.param_type, original.param_type) + + # Verify Resource + self.assertEqual(1, len(p.master_template.resources)) + added = p.master_template.resources[0] + + expected_id = plan._generate_resource_id('ns1') + self.assertEqual(added.resource_id, expected_id) + expected_type = ns_utils.apply_resource_alias_namespace('ns1') + self.assertEqual(added.resource_type, expected_type) + + for param, prop in zip(t.parameters, added.properties): + v = ns_utils.apply_template_namespace('ns1', param.name) + expected_value = {'get_param': [v]} + self.assertEqual(prop.value, expected_value) + + # Verify Outputs + self.assertEqual(2, len(p.master_template.outputs)) + for original, added in zip(t.outputs, p.master_template.outputs): + self.assertTrue(added is not original) + + expected_name = ns_utils.apply_template_namespace('ns1', + original.name) + expected_value = {'get_attr': [expected_id, original.name]} + self.assertEqual(added.name, expected_name) + self.assertEqual(added.value, expected_value) + + # Verify Environment Parameters + self.assertEqual(2, len(p.environment.parameters)) + for env_param, template_param in zip(p.environment.parameters, + t.parameters): + expected_name =\ + ns_utils.apply_template_namespace('ns1', template_param.name) + self.assertEqual(env_param.name, expected_name) + self.assertEqual(env_param.value, '') + + # Verify Resource Registry Entry + self.assertEqual(1, len(p.environment.registry_entries)) + added = p.environment.registry_entries[0] + expected_alias = ns_utils.apply_resource_alias_namespace('ns1') + self.assertEqual(added.alias, expected_alias) + self.assertEqual(added.filename, 'template-1.yaml') + + def test_remove_template(self): + # Setup & Sanity Check + p = plan.DeploymentPlan() + t = self._generate_template() + p.add_template('ns1', t, 'template-1.yaml') + p.add_template('ns2', t, 'template-2.yaml') + + self.assertEqual(4, len(p.master_template.parameters)) + self.assertEqual(4, len(p.master_template.outputs)) + self.assertEqual(2, len(p.master_template.resources)) + + self.assertEqual(4, len(p.environment.parameters)) + self.assertEqual(2, len(p.environment.registry_entries)) + + # Test + p.remove_template('ns1') + + # Verify + self.assertEqual(2, len(p.master_template.parameters)) + self.assertEqual(2, len(p.master_template.outputs)) + self.assertEqual(1, len(p.master_template.resources)) + + self.assertEqual(2, len(p.environment.parameters)) + self.assertEqual(1, len(p.environment.registry_entries)) + + def _generate_template(self): + t = heat.Template() + + t.add_parameter(heat.Parameter('param-1', 'type-1')) + t.add_parameter(heat.Parameter('param-2', 'type-2')) + + t.add_output(heat.Output('out-1', 'value-1')) + t.add_output(heat.Output('out-2', 'value-2')) + + return t