http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/9841ca4a/aria/modeling/service_template.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service_template.py b/aria/modeling/service_template.py new file mode 100644 index 0000000..5d667e3 --- /dev/null +++ b/aria/modeling/service_template.py @@ -0,0 +1,1701 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# pylint: disable=too-many-lines, no-self-argument, no-member, abstract-method + +from __future__ import absolute_import # so we can import standard 'types' + +from types import FunctionType +from datetime import datetime + +from sqlalchemy import ( + Column, + Text, + Integer, + DateTime +) +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.associationproxy import association_proxy + +from ..parser import validation +from ..parser.consumption import ConsumptionContext +from ..parser.reading import deepcopy_with_locators +from ..utils import collections, formatting, console +from .mixins import TemplateModelMixin +from . import ( + relationship, + utils, + types as modeling_types +) + + +class ServiceTemplateBase(TemplateModelMixin): # pylint: disable=too-many-public-methods + """ + A service template is a source for creating :class:`Service` instances. + + It is usually created by various DSL parsers, such as ARIA's TOSCA extension. However, it can + also be created programmatically. + + :ivar name: Name (unique for this ARIA installation) + :vartype name: basestring + :ivar description: Human-readable description + :vartype description: basestring + :ivar main_file_name: Filename of CSAR or YAML file from which this service template was parsed + :vartype main_file_name: basestring + :ivar meta_data: Custom annotations + :vartype meta_data: {basestring: :class:`Metadata`} + :ivar node_templates: Templates for creating nodes + :vartype node_templates: {basestring: :class:`NodeTemplate`} + :ivar group_templates: Templates for creating groups + :vartype group_templates: {basestring: :class:`GroupTemplate`} + :ivar policy_templates: Templates for creating policies + :vartype policy_templates: {basestring: :class:`PolicyTemplate`} + :ivar substitution_template: The entire service can appear as a node + :vartype substitution_template: :class:`SubstitutionTemplate` + :ivar inputs: Externally provided parameters + :vartype inputs: {basestring: :class:`Parameter`} + :ivar outputs: These parameters are filled in after service installation + :vartype outputs: {basestring: :class:`Parameter`} + :ivar workflow_templates: Custom workflows that can be performed on the service + :vartype workflow_templates: {basestring: :class:`OperationTemplate`} + :ivar plugin_specifications: Plugins required by services + :vartype plugin_specifications: {basestring: :class:`PluginSpecification`} + :ivar node_types: Base for the node type hierarchy + :vartype node_types: :class:`Type` + :ivar group_types: Base for the group type hierarchy + :vartype group_types: :class:`Type` + :ivar policy_types: Base for the policy type hierarchy + :vartype policy_types: :class:`Type` + :ivar relationship_types: Base for the relationship type hierarchy + :vartype relationship_types: :class:`Type` + :ivar capability_types: Base for the capability type hierarchy + :vartype capability_types: :class:`Type` + :ivar interface_types: Base for the interface type hierarchy + :vartype interface_types: :class:`Type` + :ivar artifact_types: Base for the artifact type hierarchy + :vartype artifact_types: :class:`Type` + :ivar plugin_specifications: Plugins required to be installed + :vartype plugin_specifications: {basestring: :class:`PluginSpecification`} + :ivar created_at: Creation timestamp + :vartype created_at: :class:`datetime.datetime` + :ivar updated_at: Update timestamp + :vartype updated_at: :class:`datetime.datetime` + + :ivar services: Instantiated services + :vartype services: [:class:`Service`] + """ + + __tablename__ = 'service_template' + + __private_fields__ = ['substitution_template_fk', + 'node_type_fk', + 'group_type_fk', + 'policy_type_fk', + 'relationship_type_fk', + 'capability_type_fk', + 'interface_type_fk', + 'artifact_type_fk'] + + description = Column(Text) + main_file_name = Column(Text) + + @declared_attr + def meta_data(cls): + # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy! + return relationship.many_to_many(cls, 'metadata', dict_key='name') + + @declared_attr + def node_templates(cls): + return relationship.one_to_many(cls, 'node_template', dict_key='name') + + @declared_attr + def group_templates(cls): + return relationship.one_to_many(cls, 'group_template', dict_key='name') + + @declared_attr + def policy_templates(cls): + return relationship.one_to_many(cls, 'policy_template', dict_key='name') + + @declared_attr + def substitution_template(cls): + return relationship.one_to_one(cls, 'substitution_template') + + @declared_attr + def inputs(cls): + return relationship.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + @declared_attr + def outputs(cls): + return relationship.many_to_many(cls, 'parameter', prefix='outputs', dict_key='name') + + @declared_attr + def workflow_templates(cls): + return relationship.one_to_many(cls, 'operation_template', dict_key='name') + + @declared_attr + def plugin_specifications(cls): + return relationship.one_to_many(cls, 'plugin_specification', dict_key='name') + + @declared_attr + def node_types(cls): + return relationship.one_to_one(cls, 'type', fk='node_type_fk', other_property=False) + + @declared_attr + def group_types(cls): + return relationship.one_to_one(cls, 'type', fk='group_type_fk', other_property=False) + + @declared_attr + def policy_types(cls): + return relationship.one_to_one(cls, 'type', fk='policy_type_fk', other_property=False) + + @declared_attr + def relationship_types(cls): + return relationship.one_to_one(cls, 'type', fk='relationship_type_fk', + other_property=False) + + @declared_attr + def capability_types(cls): + return relationship.one_to_one(cls, 'type', fk='capability_type_fk', other_property=False) + + @declared_attr + def interface_types(cls): + return relationship.one_to_one(cls, 'type', fk='interface_type_fk', other_property=False) + + @declared_attr + def artifact_types(cls): + return relationship.one_to_one(cls, 'type', fk='artifact_type_fk', other_property=False) + + # region orchestration + + created_at = Column(DateTime, nullable=False, index=True) + updated_at = Column(DateTime) + + # endregion + + # region foreign keys + + @declared_attr + def substitution_template_fk(cls): + """For ServiceTemplate one-to-one to SubstitutionTemplate""" + return relationship.foreign_key('substitution_template', nullable=True) + + @declared_attr + def node_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def group_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def policy_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def relationship_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def capability_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def interface_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def artifact_type_fk(cls): + """For ServiceTemplate one-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('description', self.description), + ('metadata', formatting.as_raw_dict(self.meta_data)), + ('node_templates', formatting.as_raw_list(self.node_templates)), + ('group_templates', formatting.as_raw_list(self.group_templates)), + ('policy_templates', formatting.as_raw_list(self.policy_templates)), + ('substitution_template', formatting.as_raw(self.substitution_template)), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('outputs', formatting.as_raw_dict(self.outputs)), + ('workflow_templates', formatting.as_raw_list(self.workflow_templates)))) + + @property + def types_as_raw(self): + return collections.OrderedDict(( + ('node_types', formatting.as_raw(self.node_types)), + ('group_types', formatting.as_raw(self.group_types)), + ('policy_types', formatting.as_raw(self.policy_types)), + ('relationship_types', formatting.as_raw(self.relationship_types)), + ('capability_types', formatting.as_raw(self.capability_types)), + ('interface_types', formatting.as_raw(self.interface_types)), + ('artifact_types', formatting.as_raw(self.artifact_types)))) + + def instantiate(self, container): + from . import models + context = ConsumptionContext.get_thread_local() + now = datetime.now() + service = models.Service(created_at=now, + updated_at=now, + description=deepcopy_with_locators(self.description), + service_template=self) + #service.name = '{0}_{1}'.format(self.name, service.id) + + context.modeling.instance = service + + utils.instantiate_dict(self, service.meta_data, self.meta_data) + + for node_template in self.node_templates.itervalues(): + for _ in range(node_template.default_instances): + node = node_template.instantiate(container) + service.nodes[node.name] = node + + utils.instantiate_dict(self, service.groups, self.group_templates) + utils.instantiate_dict(self, service.policies, self.policy_templates) + utils.instantiate_dict(self, service.workflows, self.workflow_templates) + + if self.substitution_template is not None: + service.substitution = self.substitution_template.instantiate(container) + + utils.instantiate_dict(self, service.inputs, self.inputs) + utils.instantiate_dict(self, service.outputs, self.outputs) + + for name, the_input in context.modeling.inputs.iteritems(): + if name not in service.inputs: + context.validation.report('input "{0}" is not supported'.format(name)) + else: + service.inputs[name].value = the_input + + return service + + def validate(self): + utils.validate_dict_values(self.meta_data) + utils.validate_dict_values(self.node_templates) + utils.validate_dict_values(self.group_templates) + utils.validate_dict_values(self.policy_templates) + if self.substitution_template is not None: + self.substitution_template.validate() + utils.validate_dict_values(self.inputs) + utils.validate_dict_values(self.outputs) + utils.validate_dict_values(self.workflow_templates) + if self.node_types is not None: + self.node_types.validate() + if self.group_types is not None: + self.group_types.validate() + if self.policy_types is not None: + self.policy_types.validate() + if self.relationship_types is not None: + self.relationship_types.validate() + if self.capability_types is not None: + self.capability_types.validate() + if self.interface_types is not None: + self.interface_types.validate() + if self.artifact_types is not None: + self.artifact_types.validate() + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.meta_data, report_issues) + utils.coerce_dict_values(container, self.node_templates, report_issues) + utils.coerce_dict_values(container, self.group_templates, report_issues) + utils.coerce_dict_values(container, self.policy_templates, report_issues) + if self.substitution_template is not None: + self.substitution_template.coerce_values(container, report_issues) + utils.coerce_dict_values(container, self.inputs, report_issues) + utils.coerce_dict_values(container, self.outputs, report_issues) + utils.coerce_dict_values(container, self.workflow_templates, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + if self.description is not None: + console.puts(context.style.meta(self.description)) + utils.dump_dict_values(self.meta_data, 'Metadata') + for node_template in self.node_templates.itervalues(): + node_template.dump() + for group_template in self.group_templates.itervalues(): + group_template.dump() + for policy_template in self.policy_templates.itervalues(): + policy_template.dump() + if self.substitution_template is not None: + self.substitution_template.dump() + utils.dump_dict_values(self.inputs, 'Inputs') + utils.dump_dict_values(self.outputs, 'Outputs') + utils.dump_dict_values(self.workflow_templates, 'Workflow templates') + + def dump_types(self): + if self.node_types.children: + console.puts('Node types:') + self.node_types.dump() + if self.group_types.children: + console.puts('Group types:') + self.group_types.dump() + if self.capability_types.children: + console.puts('Capability types:') + self.capability_types.dump() + if self.relationship_types.children: + console.puts('Relationship types:') + self.relationship_types.dump() + if self.policy_types.children: + console.puts('Policy types:') + self.policy_types.dump() + if self.artifact_types.children: + console.puts('Artifact types:') + self.artifact_types.dump() + if self.interface_types.children: + console.puts('Interface types:') + self.interface_types.dump() + + +class NodeTemplateBase(TemplateModelMixin): + """ + A template for creating zero or more :class:`Node` instances. + + :ivar name: Name (unique for this service template; will usually be used as a prefix for node + names) + :vartype name: basestring + :ivar type: Node type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar default_instances: Default number nodes that will appear in the service + :vartype default_instances: int + :ivar min_instances: Minimum number nodes that will appear in the service + :vartype min_instances: int + :ivar max_instances: Maximum number nodes that will appear in the service + :vartype max_instances: int + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interface_templates: Bundles of operations + :vartype interface_templates: {basestring: :class:`InterfaceTemplate`} + :ivar artifact_templates: Associated files + :vartype artifact_templates: {basestring: :class:`ArtifactTemplate`} + :ivar capability_templates: Exposed capabilities + :vartype capability_templates: {basestring: :class:`CapabilityTemplate`} + :ivar requirement_templates: Potential relationships with other nodes + :vartype requirement_templates: [:class:`RequirementTemplate`] + :ivar target_node_template_constraints: Constraints for filtering relationship targets + :vartype target_node_template_constraints: [:class:`FunctionType`] + :ivar plugin_specifications: Plugins required to be installed on the node's host + :vartype plugin_specifications: {basestring: :class:`PluginSpecification`} + + :ivar service_template: Containing service template + :vartype service_template: :class:`ServiceTemplate` + :ivar group_templates: We are a member of these groups + :vartype group_templates: [:class:`GroupTemplate`] + :ivar policy_templates: Policy templates enacted on this node + :vartype policy_templates: [:class:`PolicyTemplate`] + :ivar substitution_template_mapping: Our contribution to service substitution + :vartype substitution_template_mapping: :class:`SubstitutionTemplateMapping` + :ivar nodes: Instantiated nodes + :vartype nodes: [:class:`Node`] + """ + + __tablename__ = 'node_template' + + __private_fields__ = ['type_fk', + 'service_template_fk', + 'service_template_name'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + default_instances = Column(Integer, default=1) + min_instances = Column(Integer, default=0) + max_instances = Column(Integer, default=None) + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interface_templates(cls): + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + @declared_attr + def artifact_templates(cls): + return relationship.one_to_many(cls, 'artifact_template', dict_key='name') + + @declared_attr + def capability_templates(cls): + return relationship.one_to_many(cls, 'capability_template', dict_key='name') + + @declared_attr + def requirement_templates(cls): + return relationship.one_to_many(cls, 'requirement_template', child_fk='node_template_fk', + child_property='node_template') + + target_node_template_constraints = Column(modeling_types.StrictList(FunctionType)) + + @declared_attr + def plugin_specifications(cls): + return relationship.many_to_many(cls, 'plugin_specification', dict_key='name') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For NodeTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to NodeTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + # region association proxies + + @declared_attr + def service_template_name(cls): + """Required for use by SQLAlchemy queries""" + return association_proxy('service_template', 'name') + + # endregion + + def is_target_node_valid(self, target_node_template): + if self.target_node_template_constraints: + for node_type_constraint in self.target_node_template_constraints: + if not node_type_constraint(target_node_template, self): + return False + return True + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('default_instances', self.default_instances), + ('min_instances', self.min_instances), + ('max_instances', self.max_instances), + ('properties', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)), + ('artifact_templates', formatting.as_raw_list(self.artifact_templates)), + ('capability_templates', formatting.as_raw_list(self.capability_templates)), + ('requirement_templates', formatting.as_raw_list(self.requirement_templates)))) + + def instantiate(self, container): + context = ConsumptionContext.get_thread_local() + from . import models + name = context.modeling.generate_node_id(self.name) + node = models.Node(name=name, + type=self.type, + description=deepcopy_with_locators(self.description), + state='', + node_template=self) + utils.instantiate_dict(node, node.properties, self.properties) + utils.instantiate_dict(node, node.interfaces, self.interface_templates) + utils.instantiate_dict(node, node.artifacts, self.artifact_templates) + utils.instantiate_dict(node, node.capabilities, self.capability_templates) + return node + + def validate(self): + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interface_templates) + utils.validate_dict_values(self.artifact_templates) + utils.validate_dict_values(self.capability_templates) + utils.validate_list_values(self.requirement_templates) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + utils.coerce_dict_values(self, self.interface_templates, report_issues) + utils.coerce_dict_values(self, self.artifact_templates, report_issues) + utils.coerce_dict_values(self, self.capability_templates, report_issues) + utils.coerce_list_values(self, self.requirement_templates, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Node template: {0}'.format(context.style.node(self.name))) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + console.puts('Instances: {0:d} ({1:d}{2})'.format( + self.default_instances, + self.min_instances, + ' to {0:d}'.format(self.max_instances) + if self.max_instances is not None + else ' or more')) + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interface_templates) + utils.dump_dict_values(self.artifact_templates, 'Artifact templates') + utils.dump_dict_values(self.capability_templates, 'Capability templates') + utils.dump_list_values(self.requirement_templates, 'Requirement templates') + + +class GroupTemplateBase(TemplateModelMixin): + """ + A template for creating a :class:`Group` instance. + + Groups are logical containers for zero or more nodes. + + :ivar name: Name (unique for this service template) + :vartype name: basestring + :ivar type: Group type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar node_templates: All nodes instantiated by these templates will be members of the group + :vartype node_templates: [:class:`NodeTemplate`] + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interface_templates: Bundles of operations + :vartype interface_templates: {basestring: :class:`InterfaceTemplate`} + + :ivar service_template: Containing service template + :vartype service_template: :class:`ServiceTemplate` + :ivar policy_templates: Policy templates enacted on this group + :vartype policy_templates: [:class:`PolicyTemplate`] + :ivar groups: Instantiated groups + :vartype groups: [:class:`Group`] + """ + + __tablename__ = 'group_template' + + __private_fields__ = ['type_fk', + 'service_template_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def node_templates(cls): + return relationship.many_to_many(cls, 'node_template') + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interface_templates(cls): + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For GroupTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to GroupTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)))) + + def instantiate(self, container): + from . import models + group = models.Group(name=self.name, + type=self.type, + description=deepcopy_with_locators(self.description), + group_template=self) + utils.instantiate_dict(self, group.properties, self.properties) + utils.instantiate_dict(self, group.interfaces, self.interface_templates) + if self.node_templates: + for node_template in self.node_templates: + group.nodes += node_template.nodes.all() + return group + + def validate(self): + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interface_templates) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + utils.coerce_dict_values(self, self.interface_templates, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Group template: {0}'.format(context.style.node(self.name))) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interface_templates) + if self.node_templates: + console.puts('Member node templates: {0}'.format(', '.join( + (str(context.style.node(v.name)) for v in self.node_templates)))) + + +class PolicyTemplateBase(TemplateModelMixin): + """ + Policies can be applied to zero or more :class:`NodeTemplate` or :class:`GroupTemplate` + instances. + + :ivar name: Name (unique for this service template) + :vartype name: basestring + :ivar type: Policy type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar node_templates: Policy will be enacted on all nodes instantiated by these templates + :vartype node_templates: [:class:`NodeTemplate`] + :ivar group_templates: Policy will be enacted on all nodes in these groups + :vartype group_templates: [:class:`GroupTemplate`] + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar service_template: Containing service template + :vartype service_template: :class:`ServiceTemplate` + :ivar policies: Instantiated policies + :vartype policies: [:class:`Policy`] + """ + + __tablename__ = 'policy_template' + + __private_fields__ = ['type_fk', + 'service_template_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def node_templates(cls): + return relationship.many_to_many(cls, 'node_template') + + @declared_attr + def group_templates(cls): + return relationship.many_to_many(cls, 'group_template') + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For PolicyTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to PolicyTemplate""" + return relationship.foreign_key('service_template') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('properties', formatting.as_raw_dict(self.properties)))) + + def instantiate(self, container): + from . import models + policy = models.Policy(name=self.name, + type=self.type, + description=deepcopy_with_locators(self.description), + policy_template=self) + utils.instantiate_dict(self, policy.properties, self.properties) + if self.node_templates: + for node_template in self.node_templates: + policy.nodes += node_template.nodes.all() + if self.group_templates: + for group_template in self.group_templates: + policy.groups += group_template.groups.all() + return policy + + def validate(self): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Policy template: {0}'.format(context.style.node(self.name))) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.properties, 'Properties') + if self.node_templates: + console.puts('Target node templates: {0}'.format(', '.join( + (str(context.style.node(v.name)) for v in self.node_templates)))) + if self.group_templates: + console.puts('Target group templates: {0}'.format(', '.join( + (str(context.style.node(v.name)) for v in self.group_templates)))) + + +class SubstitutionTemplateBase(TemplateModelMixin): + """ + Used to substitute a single node for the entire deployment. + + :ivar node_type: Exposed node type + :vartype node_type: :class:`Type` + :ivar mappings: Requirement and capability mappings + :vartype mappings: {basestring: :class:`SubstitutionTemplateMapping`} + + :ivar service_template: Containing service template + :vartype service_template: :class:`ServiceTemplate` + :ivar substitutions: Instantiated substitutions + :vartype substitutions: [:class:`Substitution`] + """ + + __tablename__ = 'substitution_template' + + __private_fields__ = ['node_type_fk'] + + @declared_attr + def node_type(cls): + return relationship.many_to_one(cls, 'type') + + @declared_attr + def mappings(cls): + return relationship.one_to_many(cls, 'substitution_template_mapping', dict_key='name') + + # region foreign keys + + @declared_attr + def node_type_fk(cls): + """For SubstitutionTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('node_type_name', self.node_type.name), + ('mappings', formatting.as_raw_dict(self.mappings)))) + + def instantiate(self, container): + from . import models + substitution = models.Substitution(node_type=self.node_type, + substitution_template=self) + utils.instantiate_dict(container, substitution.mappings, self.mappings) + return substitution + + def validate(self): + utils.validate_dict_values(self.mappings) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.mappings, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Substitution template:') + with context.style.indent: + console.puts('Node type: {0}'.format(context.style.type(self.node_type.name))) + utils.dump_dict_values(self.mappings, 'Mappings') + + +class SubstitutionTemplateMappingBase(TemplateModelMixin): + """ + Used by :class:`SubstitutionTemplate` to map a capability or a requirement to a node. + + Only one of `capability_template` and `requirement_template` can be set. + + :ivar name: Exposed capability or requirement name + :vartype name: basestring + :ivar node_template: Node template + :vartype node_template: :class:`NodeTemplate` + :ivar capability_template: Capability template in the node template + :vartype capability_template: :class:`CapabilityTemplate` + :ivar requirement_template: Requirement template in the node template + :vartype requirement_template: :class:`RequirementTemplate` + + :ivar substitution_template: Containing substitution template + :vartype substitution_template: :class:`SubstitutionTemplate` + """ + + __tablename__ = 'substitution_template_mapping' + + __private_fields__ = ['substitution_template_fk', + 'node_template_fk', + 'capability_template_fk', + 'requirement_template_fk'] + + @declared_attr + def node_template(cls): + return relationship.one_to_one(cls, 'node_template') + + @declared_attr + def capability_template(cls): + return relationship.one_to_one(cls, 'capability_template') + + @declared_attr + def requirement_template(cls): + return relationship.one_to_one(cls, 'requirement_template') + + # region foreign keys + + @declared_attr + def substitution_template_fk(cls): + """For SubstitutionTemplate one-to-many to SubstitutionTemplateMapping""" + return relationship.foreign_key('substitution_template') + + @declared_attr + def node_template_fk(cls): + """For SubstitutionTemplate one-to-one to NodeTemplate""" + return relationship.foreign_key('node_template') + + @declared_attr + def capability_template_fk(cls): + """For SubstitutionTemplate one-to-one to CapabilityTemplate""" + return relationship.foreign_key('capability_template', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For SubstitutionTemplate one-to-one to RequirementTemplate""" + return relationship.foreign_key('requirement_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name))) + + def coerce_values(self, container, report_issues): + pass + + def instantiate(self, container): + from . import models + context = ConsumptionContext.get_thread_local() + nodes = self.node_template.nodes.all() + if len(nodes) == 0: + context.validation.report( + 'mapping "{0}" refers to node template "{1}" but there are no ' + 'node instances'.format(self.mapped_name, self.node_template.name), + level=validation.Issue.BETWEEN_INSTANCES) + return None + # The TOSCA spec does not provide a way to choose the node, + # so we will just pick the first one + node = nodes[0] + capability = None + if self.capability_template: + for a_capability in node.capabilities.itervalues(): + if a_capability.capability_template.name == self.capability_template.name: + capability = a_capability + return models.SubstitutionMapping(name=self.name, + node=node, + capability=capability, + requirement_template=self.requirement_template) + + def validate(self): + context = ConsumptionContext.get_thread_local() + if (self.capability_template is None) and (self.requirement_template is None): + context.validation.report('mapping "{0}" refers to neither capability nor a requirement' + ' in node template: {1}'.format( + self.name, + formatting.safe_repr(self.node_template.name)), + level=validation.Issue.BETWEEN_TYPES) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('{0} -> {1}.{2}'.format( + context.style.node(self.name), + context.style.node(self.node_template.name), + context.style.node(self.capability_template.name + if self.capability_template + else self.requirement_template.name))) + + +class RequirementTemplateBase(TemplateModelMixin): + """ + A requirement for a :class:`NodeTemplate`. During instantiation will be matched with a + capability of another node. + + Requirements may optionally contain a :class:`RelationshipTemplate` that will be created between + the nodes. + + :ivar name: Name (a node template can have multiple requirements with the same name) + :vartype name: basestring + :ivar target_node_type: Required node type (optional) + :vartype target_node_type: :class:`Type` + :ivar target_node_template: Required node template (optional) + :vartype target_node_template: :class:`NodeTemplate` + :ivar target_capability_type: Required capability type (optional) + :vartype target_capability_type: :class:`Type` + :ivar target_capability_name: Name of capability in target node (optional) + :vartype target_capability_name: basestring + :ivar target_node_template_constraints: Constraints for filtering relationship targets + :vartype target_node_template_constraints: [:class:`FunctionType`] + :ivar relationship_template: Template for relationships (optional) + :vartype relationship_template: :class:`RelationshipTemplate` + + :ivar node_template: Containing node template + :vartype node_template: :class:`NodeTemplate` + :ivar substitution_template_mapping: Our contribution to service substitution + :vartype substitution_template_mapping: :class:`SubstitutionTemplateMapping` + :ivar substitution_mapping: Our contribution to service substitution + :vartype substitution_mapping: :class:`SubstitutionMapping` + """ + + __tablename__ = 'requirement_template' + + __private_fields__ = ['target_node_type_fk', + 'target_node_template_fk', + 'target_capability_type_fk' + 'node_template_fk', + 'relationship_template_fk'] + + @declared_attr + def target_node_type(cls): + return relationship.many_to_one(cls, 'type', fk='target_node_type_fk', + parent_property=False) + + @declared_attr + def target_node_template(cls): + return relationship.one_to_one(cls, 'node_template', fk='target_node_template_fk', + other_property=False) + + @declared_attr + def target_capability_type(cls): + return relationship.one_to_one(cls, 'type', fk='target_capability_type_fk', + other_property=False) + + target_capability_name = Column(Text) + target_node_template_constraints = Column(modeling_types.StrictList(FunctionType)) + + @declared_attr + def relationship_template(cls): + return relationship.one_to_one(cls, 'relationship_template') + + # region foreign keys + + @declared_attr + def target_node_type_fk(cls): + """For RequirementTemplate many-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def target_node_template_fk(cls): + """For RequirementTemplate one-to-one to NodeTemplate""" + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def target_capability_type_fk(cls): + """For RequirementTemplate one-to-one to NodeTemplate""" + return relationship.foreign_key('type', nullable=True) + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to RequirementTemplate""" + return relationship.foreign_key('node_template') + + @declared_attr + def relationship_template_fk(cls): + """For RequirementTemplate one-to-one to RelationshipTemplate""" + return relationship.foreign_key('relationship_template', nullable=True) + + # endregion + + def find_target(self, source_node_template): + context = ConsumptionContext.get_thread_local() + + # We might already have a specific node template, so we'll just verify it + if self.target_node_template is not None: + if not source_node_template.is_target_node_valid(self.target_node_template): + context.validation.report('requirement "{0}" of node template "{1}" is for node ' + 'template "{2}" but it does not match constraints'.format( + self.name, + self.target_node_template_name, + source_node_template.name), + level=validation.Issue.BETWEEN_TYPES) + if (self.target_capability_type is not None) \ + or (self.target_capability_name is not None): + target_node_capability = self.find_target_capability(source_node_template, + self.target_node_template) + if target_node_capability is None: + return None, None + else: + target_node_capability = None + + return self.target_node_template, target_node_capability + + # Find first node that matches the type + elif self.target_node_type is not None: + for target_node_template in context.modeling.template.node_templates.itervalues(): + if self.target_node_type.get_descendant(target_node_template.type.name) is None: + continue + + if not source_node_template.is_target_node_valid(target_node_template): + continue + + target_node_capability = self.find_target_capability(source_node_template, + target_node_template) + if target_node_capability is None: + continue + + return target_node_template, target_node_capability + + return None, None + + def find_target_capability(self, source_node_template, target_node_template): + for capability_template in target_node_template.capability_templates.itervalues(): + if capability_template.satisfies_requirement(source_node_template, + self, + target_node_template): + return capability_template + return None + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('target_node_type_name', self.target_node_type.name + if self.target_node_type is not None else None), + ('target_node_template_name', self.target_node_template.name + if self.target_node_template is not None else None), + ('target_capability_type_name', self.target_capability_type.name + if self.target_capability_type is not None else None), + ('target_capability_name', self.target_capability_name), + ('relationship_template', formatting.as_raw(self.relationship_template)))) + + def validate(self): + if self.relationship_template: + self.relationship_template.validate() + + def coerce_values(self, container, report_issues): + if self.relationship_template is not None: + self.relationship_template.coerce_values(container, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + if self.name: + console.puts(context.style.node(self.name)) + else: + console.puts('Requirement:') + with context.style.indent: + if self.target_node_type is not None: + console.puts('Target node type: {0}'.format( + context.style.type(self.target_node_type.name))) + elif self.target_node_template is not None: + console.puts('Target node template: {0}'.format( + context.style.node(self.target_node_template.name))) + if self.target_capability_type is not None: + console.puts('Target capability type: {0}'.format( + context.style.type(self.target_capability_type.name))) + elif self.target_capability_name is not None: + console.puts('Target capability name: {0}'.format( + context.style.node(self.target_capability_name))) + if self.target_node_template_constraints: + console.puts('Target node template constraints:') + with context.style.indent: + for constraint in self.target_node_template_constraints: + console.puts(context.style.literal(constraint)) + if self.relationship_template: + console.puts('Relationship:') + with context.style.indent: + self.relationship_template.dump() + + +class RelationshipTemplateBase(TemplateModelMixin): + """ + Optional addition to a :class:`RequirementTemplate` in :class:`NodeTemplate` that can be applied + when the requirement is matched with a capability. + + Note that a relationship template here is not equivalent to a relationship template entity in + TOSCA. For example, a TOSCA requirement specifying a relationship type instead of a template + would still be represented here as a relationship template. + + :ivar name: Name (optional; if present is unique for this service template) + :vartype name: basestring + :ivar type: Relationship type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interface_templates: Bundles of operations + :vartype interface_templates: {basestring: :class:`InterfaceTemplate`} + + :ivar requirement_template: Containing requirement template + :vartype requirement_template: :class:`RequirementTemplate` + :ivar relationships: Instantiated relationships + :vartype relationships: [:class:`Relationship`] + """ + + __tablename__ = 'relationship_template' + + __private_fields__ = ['type_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interface_templates(cls): + return relationship.one_to_many(cls, 'interface_template', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For RelationshipTemplate many-to-one to Type""" + return relationship.foreign_key('type', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('type_name', self.type.name if self.type is not None else None), + ('name', self.name), + ('description', self.description), + ('properties', formatting.as_raw_dict(self.properties)), + ('interface_templates', formatting.as_raw_list(self.interface_templates)))) + + def instantiate(self, container): + from . import models + relationship_model = models.Relationship(name=self.name, + type=self.type, + relationship_template=self) + utils.instantiate_dict(container, relationship_model.properties, self.properties) + utils.instantiate_dict(container, relationship_model.interfaces, self.interface_templates) + return relationship_model + + def validate(self): + # TODO: either type or name must be set + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interface_templates) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + utils.coerce_dict_values(self, self.interface_templates, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + if self.type is not None: + console.puts('Relationship type: {0}'.format(context.style.type(self.type.name))) + else: + console.puts('Relationship template: {0}'.format( + context.style.node(self.name))) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interface_templates, 'Interface templates') + + +class CapabilityTemplateBase(TemplateModelMixin): + """ + A capability of a :class:`NodeTemplate`. Nodes expose zero or more capabilities that can be + matched with :class:`Requirement` instances of other nodes. + + :ivar name: Name (unique for the node template) + :vartype name: basestring + :ivar type: Capability type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar valid_source_node_types: Reject requirements that are not from these node types (optional) + :vartype valid_source_node_types: [:class:`Type`] + :ivar min_occurrences: Minimum number of requirement matches required + :vartype min_occurrences: int + :ivar max_occurrences: Maximum number of requirement matches allowed + :vartype min_occurrences: int + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar node_template: Containing node template + :vartype node_template: :class:`NodeTemplate` + :ivar substitution_template_mapping: Our contribution to service substitution + :vartype substitution_template_mapping: :class:`SubstitutionTemplateMapping` + :ivar capabilities: Instantiated capabilities + :vartype capabilities: [:class:`Capability`] + """ + + __tablename__ = 'capability_template' + + __private_fields__ = ['type_fk', + 'node_template_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + min_occurrences = Column(Integer, default=None) # optional + max_occurrences = Column(Integer, default=None) # optional + + @declared_attr + def valid_source_node_types(cls): + return relationship.many_to_many(cls, 'type', prefix='valid_sources') + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For CapabilityTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to CapabilityTemplate""" + return relationship.foreign_key('node_template') + + # endregion + + def satisfies_requirement(self, + source_node_template, + requirement, + target_node_template): + # Do we match the required capability type? + if requirement.target_capability_type and \ + requirement.target_capability_type.get_descendant(self.type.name) is None: + return False + + # Are we in valid_source_node_types? + if self.valid_source_node_types: + for valid_source_node_type in self.valid_source_node_types: + if valid_source_node_type.get_descendant(source_node_template.type.name) is None: + return False + + # Apply requirement constraints + if requirement.target_node_template_constraints: + for node_type_constraint in requirement.target_node_template_constraints: + if not node_type_constraint(target_node_template, source_node_template): + return False + + return True + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('min_occurrences', self.min_occurrences), + ('max_occurrences', self.max_occurrences), + ('valid_source_node_types', [v.name for v in self.valid_source_node_types]), + ('properties', formatting.as_raw_dict(self.properties)))) + + def instantiate(self, container): + from . import models + capability = models.Capability(name=self.name, + type=self.type, + min_occurrences=self.min_occurrences, + max_occurrences=self.max_occurrences, + occurrences=0, + capability_template=self) + utils.instantiate_dict(container, capability.properties, self.properties) + return capability + + def validate(self): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + console.puts( + 'Occurrences: {0:d}{1}'.format( + self.min_occurrences or 0, + ' to {0:d}'.format(self.max_occurrences) + if self.max_occurrences is not None + else ' or more')) + if self.valid_source_node_types: + console.puts('Valid source node types: {0}'.format( + ', '.join((str(context.style.type(v.name)) + for v in self.valid_source_node_types)))) + utils.dump_dict_values(self.properties, 'Properties') + + +class InterfaceTemplateBase(TemplateModelMixin): + """ + A typed set of :class:`OperationTemplate`. + + :ivar name: Name (unique for the node, group, or relationship template) + :vartype name: basestring + :ivar type: Interface type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar inputs: Parameters that can be used by all operations in the interface + :vartype inputs: {basestring: :class:`Parameter`} + :ivar operation_templates: Operations + :vartype operation_templates: {basestring: :class:`OperationTemplate`} + + :ivar node_template: Containing node template + :vartype node_template: :class:`NodeTemplate` + :ivar group_template: Containing group template + :vartype group_template: :class:`GroupTemplate` + :ivar relationship_template: Containing relationship template + :vartype relationship_template: :class:`RelationshipTemplate` + :ivar interfaces: Instantiated interfaces + :vartype interfaces: [:class:`Interface`] + """ + + __tablename__ = 'interface_template' + + __private_fields__ = ['type_fk', + 'node_template_fk', + 'group_template_fk', + 'relationship_template_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def inputs(cls): + return relationship.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + @declared_attr + def operation_templates(cls): + return relationship.one_to_many(cls, 'operation_template', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For InterfaceTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('node_template', nullable=True) + + @declared_attr + def group_template_fk(cls): + """For GroupTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('group_template', nullable=True) + + @declared_attr + def relationship_template_fk(cls): + """For RelationshipTemplate one-to-many to InterfaceTemplate""" + return relationship.foreign_key('relationship_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('inputs', formatting.as_raw_dict(self.inputs)), # pylint: disable=no-member + # TODO fix self.properties reference + ('operation_templates', formatting.as_raw_list(self.operation_templates)))) + + def instantiate(self, container): + from . import models + interface = models.Interface(name=self.name, + type=self.type, + description=deepcopy_with_locators(self.description), + interface_template=self) + utils.instantiate_dict(container, interface.inputs, self.inputs) + utils.instantiate_dict(container, interface.operations, self.operation_templates) + return interface + + def validate(self): + utils.validate_dict_values(self.inputs) + utils.validate_dict_values(self.operation_templates) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.inputs, report_issues) + utils.coerce_dict_values(container, self.operation_templates, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Interface type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.inputs, 'Inputs') + utils.dump_dict_values(self.operation_templates, 'Operation templates') + + +class OperationTemplateBase(TemplateModelMixin): + """ + An operation in a :class:`InterfaceTemplate`. + + Operations are executed by an associated :class:`PluginSpecification` via an executor. + + :ivar name: Name (unique for the interface or service template) + :vartype name: basestring + :ivar description: Human-readable description + :vartype description: basestring + :ivar plugin_specification: Associated plugin + :vartype plugin_specification: :class:`PluginSpecification` + :ivar implementation: Implementation string (interpreted by the plugin) + :vartype implementation: basestring + :ivar dependencies: Dependency strings (interpreted by the plugin) + :vartype dependencies: [basestring] + :ivar inputs: Parameters that can be used by this operation + :vartype inputs: {basestring: :class:`Parameter`} + :ivar executor: Executor name + :vartype executor: basestring + :ivar max_retries: Maximum number of retries allowed in case of failure + :vartype max_retries: int + :ivar retry_interval: Interval between retries (in seconds) + :vartype retry_interval: int + + :ivar interface_template: Containing interface template + :vartype interface_template: :class:`InterfaceTemplate` + :ivar service_template: Containing service template + :vartype service_template: :class:`ServiceTemplate` + :ivar operations: Instantiated operations + :vartype operations: [:class:`Operation`] + """ + + __tablename__ = 'operation_template' + + __private_fields__ = ['service_template_fk', + 'interface_template_fk', + 'plugin_fk'] + + description = Column(Text) + + @declared_attr + def plugin_specification(cls): + return relationship.one_to_one(cls, 'plugin_specification') + + implementation = Column(Text) + dependencies = Column(modeling_types.StrictList(item_cls=basestring)) + + @declared_attr + def inputs(cls): + return relationship.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + executor = Column(Text) + max_retries = Column(Integer) + retry_interval = Column(Integer) + + # region foreign keys + + @declared_attr + def service_template_fk(cls): + """For ServiceTemplate one-to-many to OperationTemplate""" + return relationship.foreign_key('service_template', nullable=True) + + @declared_attr + def interface_template_fk(cls): + """For InterfaceTemplate one-to-many to OperationTemplate""" + return relationship.foreign_key('interface_template', nullable=True) + + @declared_attr + def plugin_specification_fk(cls): + """For OperationTemplate one-to-one to PluginSpecification""" + return relationship.foreign_key('plugin_specification', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('implementation', self.implementation), + ('dependencies', self.dependencies), + ('executor', self.executor), + ('max_retries', self.max_retries), + ('retry_interval', self.retry_interval), + ('inputs', formatting.as_raw_dict(self.inputs)))) + + def instantiate(self, container): + from . import models + operation = models.Operation(name=self.name, + description=deepcopy_with_locators(self.description), + implementation=self.implementation, + dependencies=self.dependencies, + plugin_specification=self.plugin_specification, + executor=self.executor, + max_retries=self.max_retries, + retry_interval=self.retry_interval, + operation_template=self) + utils.instantiate_dict(container, operation.inputs, self.inputs) + return operation + + def validate(self): + utils.validate_dict_values(self.inputs) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.inputs, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + if self.implementation is not None: + console.puts('Implementation: {0}'.format( + context.style.literal(self.implementation))) + if self.dependencies: + console.puts('Dependencies: {0}'.format( + ', '.join((str(context.style.literal(v)) for v in self.dependencies)))) + if self.executor is not None: + console.puts('Executor: {0}'.format(context.style.literal(self.executor))) + if self.max_retries is not None: + console.puts('Max retries: {0}'.format(context.style.literal(self.max_retries))) + if self.retry_interval is not None: + console.puts('Retry interval: {0}'.format( + context.style.literal(self.retry_interval))) + utils.dump_dict_values(self.inputs, 'Inputs') + + +class ArtifactTemplateBase(TemplateModelMixin): + """ + A file associated with a :class:`NodeTemplate`. + + :ivar name: Name (unique for the node template) + :vartype name: basestring + :ivar type: Artifact type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: basestring + :ivar source_path: Source path (CSAR or repository) + :vartype source_path: basestring + :ivar target_path: Path at destination machine + :vartype target_path: basestring + :ivar repository_url: Repository URL + :vartype repository_path: basestring + :ivar repository_credential: Credentials for accessing the repository + :vartype repository_credential: {basestring: basestring} + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar node_template: Containing node template + :vartype node_template: :class:`NodeTemplate` + :ivar artifacts: Instantiated artifacts + :vartype artifacts: [:class:`Artifact`] + """ + + __tablename__ = 'artifact_template' + + __private_fields__ = ['type_fk', + 'node_template_fk'] + + @declared_attr + def type(cls): + return relationship.many_to_one(cls, 'type') + + description = Column(Text) + source_path = Column(Text) + target_path = Column(Text) + repository_url = Column(Text) + repository_credential = Column(modeling_types.StrictDict(basestring, basestring)) + + @declared_attr + def properties(cls): + return relationship.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For ArtifactTemplate many-to-one to Type""" + return relationship.foreign_key('type') + + @declared_attr + def node_template_fk(cls): + """For NodeTemplate one-to-many to ArtifactTemplate""" + return relationship.foreign_key('node_template') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('source_path', self.source_path), + ('target_path', self.target_path), + ('repository_url', self.repository_url), + ('repository_credential', formatting.as_agnostic(self.repository_credential)), + ('properties', formatting.as_raw_dict(self.properties)))) + + def instantiate(self, container): + from . import models + artifact = models.Artifact(name=self.name, + type=self.type, + description=deepcopy_with_locators(self.description), + source_path=self.source_path, + target_path=self.target_path, + repository_url=self.repository_url, + repository_credential=self.repository_credential, + artifact_template=self) + utils.instantiate_dict(container, artifact.properties, self.properties) + return artifact + + def validate(self): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Artifact type: {0}'.format(context.style.type(self.type.name))) + console.puts('Source path: {0}'.format(context.style.literal(self.source_path))) + if self.target_path is not None: + console.puts('Target path: {0}'.format(context.style.literal(self.target_path))) + if self.repository_url is not None: + console.puts('Repository URL: {0}'.format( + context.style.literal(self.repository_url))) + if self.repository_credential: + console.puts('Repository credential: {0}'.format( + context.style.literal(self.repository_credential))) + utils.dump_dict_values(self.properties, 'Properties')
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/9841ca4a/aria/modeling/types.py ---------------------------------------------------------------------- diff --git a/aria/modeling/types.py b/aria/modeling/types.py new file mode 100644 index 0000000..06f171c --- /dev/null +++ b/aria/modeling/types.py @@ -0,0 +1,304 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 json +from collections import namedtuple + +from sqlalchemy import ( + TypeDecorator, + VARCHAR, + event +) +from sqlalchemy.ext import mutable + +from . import exceptions + + +class _MutableType(TypeDecorator): + """ + Dict representation of type. + """ + @property + def python_type(self): + raise NotImplementedError + + def process_literal_param(self, value, dialect): + pass + + impl = VARCHAR + + def process_bind_param(self, value, dialect): + if value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if value is not None: + value = json.loads(value) + return value + + +class Dict(_MutableType): + @property + def python_type(self): + return dict + + +class List(_MutableType): + @property + def python_type(self): + return list + + +class _StrictDictMixin(object): + + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutableDict." + try: + if not isinstance(value, cls): + if isinstance(value, dict): + for k, v in value.items(): + cls._assert_strict_key(k) + cls._assert_strict_value(v) + return cls(value) + return mutable.MutableDict.coerce(key, value) + else: + return value + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + def __setitem__(self, key, value): + self._assert_strict_key(key) + self._assert_strict_value(value) + super(_StrictDictMixin, self).__setitem__(key, value) + + def setdefault(self, key, value): + self._assert_strict_key(key) + self._assert_strict_value(value) + super(_StrictDictMixin, self).setdefault(key, value) + + def update(self, *args, **kwargs): + for k, v in kwargs.items(): + self._assert_strict_key(k) + self._assert_strict_value(v) + super(_StrictDictMixin, self).update(*args, **kwargs) + + @classmethod + def _assert_strict_key(cls, key): + if cls._key_cls is not None and not isinstance(key, cls._key_cls): + raise exceptions.ValueFormatException('key type was set strictly to {0}, but was {1}' + .format(cls._key_cls, type(key))) + + @classmethod + def _assert_strict_value(cls, value): + if cls._value_cls is not None and not isinstance(value, cls._value_cls): + raise exceptions.ValueFormatException('value type was set strictly to {0}, but was {1}' + .format(cls._value_cls, type(value))) + + +class _MutableDict(mutable.MutableDict): + """ + Enables tracking for dict values. + """ + + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutableDict." + try: + return mutable.MutableDict.coerce(key, value) + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce value', cause=e) + + +class _StrictListMixin(object): + + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutableDict." + try: + if not isinstance(value, cls): + if isinstance(value, list): + for item in value: + cls._assert_item(item) + return cls(value) + return mutable.MutableList.coerce(key, value) + else: + return value + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + def __setitem__(self, index, value): + """Detect list set events and emit change events.""" + self._assert_item(value) + super(_StrictListMixin, self).__setitem__(index, value) + + def append(self, item): + self._assert_item(item) + super(_StrictListMixin, self).append(item) + + def extend(self, item): + self._assert_item(item) + super(_StrictListMixin, self).extend(item) + + def insert(self, index, item): + self._assert_item(item) + super(_StrictListMixin, self).insert(index, item) + + @classmethod + def _assert_item(cls, item): + if cls._item_cls is not None and not isinstance(item, cls._item_cls): + raise exceptions.ValueFormatException('key type was set strictly to {0}, but was {1}' + .format(cls._item_cls, type(item))) + + +class _MutableList(mutable.MutableList): + + @classmethod + def coerce(cls, key, value): + "Convert plain dictionaries to MutableDict." + try: + return mutable.MutableList.coerce(key, value) + except ValueError as e: + raise exceptions.ValueFormatException('could not coerce to MutableDict', cause=e) + + +_StrictDictID = namedtuple('_StrictDictID', 'key_cls, value_cls') +_StrictValue = namedtuple('_StrictValue', 'type_cls, listener_cls') + +class _StrictDict(object): + """ + This entire class functions as a factory for strict dicts and their listeners. + No type class, and no listener type class is created more than once. If a relevant type class + exists it is returned. + """ + _strict_map = {} + + def __call__(self, key_cls=None, value_cls=None): + strict_dict_map_key = _StrictDictID(key_cls=key_cls, value_cls=value_cls) + if strict_dict_map_key not in self._strict_map: + key_cls_name = getattr(key_cls, '__name__', str(key_cls)) + value_cls_name = getattr(value_cls, '__name__', str(value_cls)) + # Creating the type class itself. this class would be returned (used by the SQLAlchemy + # Column). + strict_dict_cls = type( + 'StrictDict_{0}_{1}'.format(key_cls_name, value_cls_name), + (Dict, ), + {} + ) + # Creating the type listening class. + # The new class inherits from both the _MutableDict class and the _StrictDictMixin, + # while setting the necessary _key_cls and _value_cls as class attributes. + listener_cls = type( + 'StrictMutableDict_{0}_{1}'.format(key_cls_name, value_cls_name), + (_StrictDictMixin, _MutableDict), + {'_key_cls': key_cls, '_value_cls': value_cls} + ) + self._strict_map[strict_dict_map_key] = _StrictValue(type_cls=strict_dict_cls, + listener_cls=listener_cls) + + return self._strict_map[strict_dict_map_key].type_cls + + +StrictDict = _StrictDict() + + +class _StrictList(object): + """ + This entire class functions as a factory for strict lists and their listeners. + No type class, and no listener type class is created more than once. If a relevant type class + exists it is returned. + """ + _strict_map = {} + + def __call__(self, item_cls=None): + + if item_cls not in self._strict_map: + item_cls_name = getattr(item_cls, '__name__', str(item_cls)) + # Creating the type class itself. this class would be returned (used by the SQLAlchemy + # Column). + strict_list_cls = type( + 'StrictList_{0}'.format(item_cls_name), + (List, ), + {} + ) + # Creating the type listening class. + # The new class inherits from both the _MutableList class and the _StrictListMixin, + # while setting the necessary _item_cls as class attribute. + listener_cls = type( + 'StrictMutableList_{0}'.format(item_cls_name), + (_StrictListMixin, _MutableList), + {'_item_cls': item_cls} + ) + self._strict_map[item_cls] = _StrictValue(type_cls=strict_list_cls, + listener_cls=listener_cls) + + return self._strict_map[item_cls].type_cls + + +StrictList = _StrictList() + + +def _mutable_association_listener(mapper, cls): + strict_dict_type_to_listener = \ + dict((v.type_cls, v.listener_cls) for v in _StrictDict._strict_map.values()) + + strict_list_type_to_listener = \ + dict((v.type_cls, v.listener_cls) for v in _StrictList._strict_map.values()) + + for prop in mapper.column_attrs: + column_type = prop.columns[0].type + # Dict Listeners + if type(column_type) in strict_dict_type_to_listener: # pylint: disable=unidiomatic-typecheck + strict_dict_type_to_listener[type(column_type)].associate_with_attribute( + getattr(cls, prop.key)) + elif isinstance(column_type, Dict): + _MutableDict.associate_with_attribute(getattr(cls, prop.key)) + + # List Listeners + if type(column_type) in strict_list_type_to_listener: # pylint: disable=unidiomatic-typecheck + strict_list_type_to_listener[type(column_type)].associate_with_attribute( + getattr(cls, prop.key)) + elif isinstance(column_type, List): + _MutableList.associate_with_attribute(getattr(cls, prop.key)) + + +_LISTENER_ARGS = (mutable.mapper, 'mapper_configured', _mutable_association_listener) + + +def _register_mutable_association_listener(): + event.listen(*_LISTENER_ARGS) + + +def remove_mutable_association_listener(): + """ + Remove the event listener that associates ``Dict`` and ``List`` column types with + ``MutableDict`` and ``MutableList``, respectively. + + This call must happen before any model instance is instantiated. + This is because once it does, that would trigger the listener we are trying to remove. + Once it is triggered, many other listeners will then be registered. + At that point, it is too late. + + The reason this function exists is that the association listener, interferes with ARIA change + tracking instrumentation, so a way to disable it is required. + + Note that the event listener this call removes is registered by default. + """ + if event.contains(*_LISTENER_ARGS): + event.remove(*_LISTENER_ARGS) + + +_register_mutable_association_listener() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/9841ca4a/aria/modeling/utils.py ---------------------------------------------------------------------- diff --git a/aria/modeling/utils.py b/aria/modeling/utils.py new file mode 100644 index 0000000..0b4015c --- /dev/null +++ b/aria/modeling/utils.py @@ -0,0 +1,121 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..parser.consumption import ConsumptionContext +from ..parser.exceptions import InvalidValueError +from ..parser.presentation import Value +from ..utils.collections import OrderedDict +from ..utils.console import puts +from .exceptions import CannotEvaluateFunctionException + + +def coerce_value(container, value, report_issues=False): + if isinstance(value, Value): + value = value.value + + if isinstance(value, list): + return [coerce_value(container, v, report_issues) for v in value] + elif isinstance(value, dict): + return OrderedDict((k, coerce_value(container, v, report_issues)) + for k, v in value.iteritems()) + elif hasattr(value, '_evaluate'): + context = ConsumptionContext.get_thread_local() + try: + value = value._evaluate(context, container) + value = coerce_value(container, value, report_issues) + except CannotEvaluateFunctionException: + pass + except InvalidValueError as e: + if report_issues: + context.validation.report(e.issue) + return value + + +def coerce_dict_values(container, the_dict, report_issues=False): + if not the_dict: + return + coerce_list_values(container, the_dict.itervalues(), report_issues) + + +def coerce_list_values(container, the_list, report_issues=False): + if not the_list: + return + for value in the_list: + value.coerce_values(container, report_issues) + + +def validate_dict_values(the_dict): + if not the_dict: + return + validate_list_values(the_dict.itervalues()) + + +def validate_list_values(the_list): + if not the_list: + return + for value in the_list: + value.validate() + + +def instantiate_dict(container, the_dict, from_dict): + if not from_dict: + return + for name, value in from_dict.iteritems(): + value = value.instantiate(container) + if value is not None: + the_dict[name] = value + + +def instantiate_list(container, the_list, from_list): + if not from_list: + return + for value in from_list: + value = value.instantiate(container) + if value is not None: + the_list.append(value) + + +def dump_list_values(the_list, name): + if not the_list: + return + puts('%s:' % name) + context = ConsumptionContext.get_thread_local() + with context.style.indent: + for value in the_list: + value.dump() + + +def dump_dict_values(the_dict, name): + if not the_dict: + return + dump_list_values(the_dict.itervalues(), name) + + +def dump_interfaces(interfaces, name='Interfaces'): + if not interfaces: + return + puts('%s:' % name) + context = ConsumptionContext.get_thread_local() + with context.style.indent: + for interface in interfaces.itervalues(): + interface.dump() + + +class classproperty(object): # pylint: disable=invalid-name + def __init__(self, f): + self._func = f + + def __get__(self, instance, owner): + return self._func(owner) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/9841ca4a/aria/orchestrator/__init__.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/__init__.py b/aria/orchestrator/__init__.py index 097ee1d..0b57e4b 100644 --- a/aria/orchestrator/__init__.py +++ b/aria/orchestrator/__init__.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Aria orchestrator +ARIA orchestrator """ from .decorators import workflow, operation, WORKFLOW_DECORATOR_RESERVED_ARGUMENTS
