http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/55556793/aria/storage/modeling/elements.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/elements.py b/aria/storage/modeling/elements.py new file mode 100644 index 0000000..699a511 --- /dev/null +++ b/aria/storage/modeling/elements.py @@ -0,0 +1,102 @@ +# 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 sqlalchemy import ( + Column, + Text +) + +from ...parser.modeling import utils +from ...utils.collections import OrderedDict +from ...utils.console import puts + +from . import structure +from . import type + +# pylint: disable=no-self-argument, no-member, abstract-method + + +class ParameterBase(structure.ModelMixin): + """ + Represents a typed value. + + This class is used by both service model and service instance elements. + """ + __tablename__ = 'parameter' + name = Column(Text, nullable=False) + type = Column(Text, nullable=False) + + # Check: value type + value = Column(Text) + description = Column(Text) + + @property + def as_raw(self): + return OrderedDict(( + ('name', self.name), + ('type_name', self.type), + ('value', self._cast_value()), + ('description', self.description))) + + # TODO: change name + def _cast_value(self): + if self.type is None: + return + + if self.type.lower() == 'str': + return str(self.value) + elif self.type.lower() == 'int': + return int(self.value) + elif self.type.lower() == 'bool': + return bool(self.value) + elif self.type.lower() == 'float': + return float(self.value) + else: + raise Exception('No supported type_name was provided') + + def instantiate(self, context, container): + return ParameterBase(self.type, self.value, self.description) + + def coerce_values(self, context, container, report_issues): + if self.value is not None: + self.value = utils.coerce_value(context, container, self.value, report_issues) + + +class MetadataBase(structure.ModelMixin): + """ + Custom values associated with the deployment template and its plans. + + This class is used by both service model and service instance elements. + + Properties: + + * :code:`values`: Dict of custom values + """ + values = Column(type.StrictDict(key_cls=basestring)) + + @property + def as_raw(self): + return self.values + + def instantiate(self, context, container): + metadata = MetadataBase() + metadata.values.update(self.values) + return metadata + + def dump(self, context): + puts('Metadata:') + with context.style.indent: + for name, value in self.values.iteritems(): + puts('%s: %s' % (name, context.style.meta(value)))
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/55556793/aria/storage/modeling/instance_elements.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/instance_elements.py b/aria/storage/modeling/instance_elements.py new file mode 100644 index 0000000..e3dedc8 --- /dev/null +++ b/aria/storage/modeling/instance_elements.py @@ -0,0 +1,1257 @@ +# 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 sqlalchemy import ( + Column, + Text, + Integer, + Boolean, +) +from sqlalchemy import DateTime +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.orderinglist import ordering_list + +from aria.parser import validation +from aria.utils import collections, formatting, console + +from . import ( + utils, + structure, + type as aria_types +) + +# pylint: disable=no-self-argument, no-member, abstract-method + +# region Element instances + + +class ServiceInstanceBase(structure.ModelMixin): + __tablename__ = 'service_instance' + + description = Column(Text) + _metadata = Column(Text) + + # region orchestrator required columns + + created_at = Column(DateTime, nullable=False, index=True) + permalink = Column(Text) + policy_triggers = Column(aria_types.Dict) + policy_types = Column(aria_types.Dict) + scaling_groups = Column(aria_types.Dict) + updated_at = Column(DateTime) + workflows = Column(aria_types.Dict) + + @declared_attr + def service_template_name(cls): + return association_proxy('service_template', 'name') + + # endregion + + # region foreign keys + @declared_attr + def substitution_fk(cls): + return cls.foreign_key('substitution', nullable=True) + + @declared_attr + def service_template_fk(cls): + return cls.foreign_key('service_template') + + # endregion + + # region one-to-one relationships + @declared_attr + def substitution(cls): + return cls.one_to_one_relationship('substitution') + # endregion + + # region many-to-one relationships + @declared_attr + def service_template(cls): + return cls.many_to_one_relationship('service_template') + + # endregion + + # region many-to-many relationships + @declared_attr + def inputs(cls): + return cls.many_to_many_relationship('parameter', table_prefix='inputs') + + @declared_attr + def outputs(cls): + return cls.many_to_many_relationship('parameter', table_prefix='outputs') + + # endregion + + # association proxies + + def satisfy_requirements(self, context): + satisfied = True + for node in self.nodes.all(): + if not node.satisfy_requirements(context): + satisfied = False + return satisfied + + def validate_capabilities(self, context): + satisfied = True + for node in self.nodes.all(): + if not node.validate_capabilities(context): + satisfied = False + return satisfied + + def find_nodes(self, node_template_name): + nodes = [] + for node in self.nodes.all(): + if node.template_name == node_template_name: + nodes.append(node) + return collections.FrozenList(nodes) + + def get_node_ids(self, node_template_name): + return collections.FrozenList((node.id for node in self.find_nodes(node_template_name))) + + def find_groups(self, group_template_name): + groups = [] + for group in self.groups.all(): + if group.template_name == group_template_name: + groups.append(group) + return collections.FrozenList(groups) + + def get_group_ids(self, group_template_name): + return collections.FrozenList((group.id for group in self.find_groups(group_template_name))) + + def is_node_a_target(self, context, target_node): + for node in self.nodes.all(): + if self._is_node_a_target(context, node, target_node): + return True + return False + + def _is_node_a_target(self, context, source_node, target_node): + if source_node.relationships: + for relationship in source_node.relationships: + if relationship.target_node_id == target_node.id: + return True + else: + node = context.modeling.instance.nodes.get(relationship.target_node_id) + if node is not None: + if self._is_node_a_target(context, node, target_node): + return True + return False + + +class OperationBase(structure.ModelMixin): + """ + An operation in a :class:`Interface`. + + Properties: + + * :code:`name`: Name + * :code:`description`: Description + * :code:`implementation`: Implementation string (interpreted by the orchestrator) + * :code:`dependencies`: List of strings (interpreted by the orchestrator) + * :code:`executor`: Executor string (interpreted by the orchestrator) + * :code:`max_retries`: Maximum number of retries allowed in case of failure + * :code:`retry_interval`: Interval between retries + * :code:`inputs`: Dict of :class:`Parameter` + """ + __tablename__ = 'operation' + # region foreign_keys + + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance', nullable=True) + + @declared_attr + def interface_instance_fk(cls): + return cls.foreign_key('interface', nullable=True) + + # endregion + description = Column(Text) + implementation = Column(Text) + dependencies = Column(aria_types.StrictList(item_cls=basestring)) + + executor = Column(Text) + max_retries = Column(Integer, default=None) + retry_interval = Column(Integer, default=None) + plugin = Column(Text) + operation = Column(Boolean) + + # region many-to-one relationships + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance') + + @declared_attr + def interfaces(cls): + return cls.many_to_one_relationship('interface') + # region many-to-many relationships + + @declared_attr + def inputs(cls): + return cls.many_to_many_relationship('parameter', table_prefix='inputs') + + # 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 validate(self, context): + utils.validate_dict_values(context, self.inputs) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.inputs, report_issues) + + def dump(self, context): + 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: %s' % context.style.literal(self.implementation)) + if self.dependencies: + console.puts( + 'Dependencies: %s' + % ', '.join((str(context.style.literal(v)) for v in self.dependencies))) + if self.executor is not None: + console.puts('Executor: %s' % context.style.literal(self.executor)) + if self.max_retries is not None: + console.puts('Max retries: %s' % context.style.literal(self.max_retries)) + if self.retry_interval is not None: + console.puts('Retry interval: %s' % context.style.literal(self.retry_interval)) + utils.dump_parameters(context, self.inputs, 'Inputs') + + +class InterfaceBase(structure.ModelMixin): + """ + A typed set of :class:`Operation`. + + Properties: + + * :code:`name`: Name + * :code:`description`: Description + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`inputs`: Dict of :class:`Parameter` + * :code:`operations`: Dict of :class:`Operation` + """ + __tablename__ = 'interface' + # region foreign_keys + @declared_attr + def group_fk(cls): + return cls.foreign_key('group', nullable=True) + + @declared_attr + def node_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def relationship_fk(cls): + return cls.foreign_key('relationship', nullable=True) + + # endregion + + description = Column(Text) + type_name = Column(Text) + + # region many-to-one relationships + + @declared_attr + def node(cls): + return cls.many_to_one_relationship('node') + + @declared_attr + def relationship(cls): + return cls.many_to_one_relationship('relationship') + + @declared_attr + def group(cls): + return cls.many_to_one_relationship('group') + + # endregion + + + # endregion + + + + # region many-to-many relationships + + @declared_attr + def inputs(cls): + return cls.many_to_many_relationship('parameter', table_prefix='inputs') + + # 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)), + ('operations', formatting.as_raw_list(self.operations)))) + + def validate(self, context): + if self.type_name: + if context.modeling.interface_types.get_descendant(self.type_name) is None: + context.validation.report('interface "%s" has an unknown type: %s' + % (self.name, + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.inputs) + utils.validate_dict_values(context, self.operations) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.inputs, report_issues) + utils.coerce_dict_values(context, container, self.operations, report_issues) + + def dump(self, context): + 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: %s' % context.style.type(self.type_name)) + utils.dump_parameters(context, self.inputs, 'Inputs') + utils.dump_dict_values(context, self.operations, 'Operations') + + +class CapabilityBase(structure.ModelMixin): + """ + A capability of a :class:`Node`. + + An instance of a :class:`CapabilityTemplate`. + + Properties: + + * :code:`name`: Name + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`min_occurrences`: Minimum number of requirement matches required + * :code:`max_occurrences`: Maximum number of requirement matches allowed + * :code:`properties`: Dict of :class:`Parameter` + """ + __tablename__ = 'capability' + # region foreign_keys + @declared_attr + def node_fk(cls): + return cls.foreign_key('node') + + # endregion + type_name = Column(Text) + + min_occurrences = Column(Integer, default=None) # optional + max_occurrences = Column(Integer, default=None) # optional + occurrences = Column(Integer, default=0) + + + # region many-to-one relationships + @declared_attr + def node(cls): + return cls.many_to_one_relationship('node') + + # endregion + + + # region many-to-many relationships + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # endregion + + @property + def has_enough_relationships(self): + if self.min_occurrences is not None: + return self.occurrences >= self.min_occurrences + return True + + def relate(self): + if self.max_occurrences is not None: + if self.occurrences == self.max_occurrences: + return False + self.occurrences += 1 + return True + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type_name), + ('properties', formatting.as_raw_dict(self.properties)))) + + def validate(self, context): + if context.modeling.capability_types.get_descendant(self.type_name) is None: + context.validation.report('capability "%s" has an unknown type: %s' + % (self.name, + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.properties) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + + def dump(self, context): + console.puts(context.style.node(self.name)) + with context.style.indent: + console.puts('Type: %s' % context.style.type(self.type_name)) + console.puts('Occurrences: %s (%s%s)' + % (self.occurrences, + self.min_occurrences or 0, + (' to %d' % self.max_occurrences) + if self.max_occurrences is not None + else ' or more')) + utils.dump_parameters(context, self.properties) + + +class ArtifactBase(structure.ModelMixin): + """ + A file associated with a :class:`Node`. + + Properties: + + * :code:`name`: Name + * :code:`description`: Description + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`source_path`: Source path (CSAR or repository) + * :code:`target_path`: Path at destination machine + * :code:`repository_url`: Repository URL + * :code:`repository_credential`: Dict of string + * :code:`properties`: Dict of :class:`Parameter` + """ + __tablename__ = 'artifact' + # region foreign_keys + + @declared_attr + def node_fk(cls): + return cls.foreign_key('node') + + # endregion + + description = Column(Text) + type_name = Column(Text) + source_path = Column(Text) + target_path = Column(Text) + repository_url = Column(Text) + repository_credential = Column(aria_types.StrictDict(basestring, basestring)) + + # region many-to-one relationships + @declared_attr + def node(cls): + return cls.many_to_one_relationship('node') + + # endregion + + + # region many-to-many relationships + + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # 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 validate(self, context): + if context.modeling.artifact_types.get_descendant(self.type_name) is None: + context.validation.report('artifact "%s" has an unknown type: %s' + % (self.name, + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + utils.validate_dict_values(context, self.properties) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + + def dump(self, context): + 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: %s' % context.style.type(self.type_name)) + console.puts('Source path: %s' % context.style.literal(self.source_path)) + if self.target_path is not None: + console.puts('Target path: %s' % context.style.literal(self.target_path)) + if self.repository_url is not None: + console.puts('Repository URL: %s' % context.style.literal(self.repository_url)) + if self.repository_credential: + console.puts('Repository credential: %s' + % context.style.literal(self.repository_credential)) + utils.dump_parameters(context, self.properties) + + +class PolicyBase(structure.ModelMixin): + """ + An instance of a :class:`PolicyTemplate`. + + Properties: + + * :code:`name`: Name + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`properties`: Dict of :class:`Parameter` + * :code:`target_node_ids`: Must be represented in the :class:`ServiceInstance` + * :code:`target_group_ids`: Must be represented in the :class:`ServiceInstance` + """ + __tablename__ = 'policy' + # region foreign_keys + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + # endregion + type_name = Column(Text) + target_node_ids = Column(aria_types.StrictList(basestring)) + target_group_ids = Column(aria_types.StrictList(basestring)) + + # region many-to-one relationships + @declared_attr + def service_instnce(cls): + return cls.many_to_one_relationship('service_instance') + + # region many-to-many relationships + + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type_name), + ('properties', formatting.as_raw_dict(self.properties)), + ('target_node_ids', self.target_node_ids), + ('target_group_ids', self.target_group_ids))) + + def validate(self, context): + if context.modeling.policy_types.get_descendant(self.type_name) is None: + context.validation.report('policy "%s" has an unknown type: %s' + % (self.name, utils.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.properties) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + + def dump(self, context): + console.puts('Policy: %s' % context.style.node(self.name)) + with context.style.indent: + console.puts('Type: %s' % context.style.type(self.type_name)) + utils.dump_parameters(context, self.properties) + if self.target_node_ids: + console.puts('Target nodes:') + with context.style.indent: + for node_id in self.target_node_ids: + console.puts(context.style.node(node_id)) + if self.target_group_ids: + console.puts('Target groups:') + with context.style.indent: + for group_id in self.target_group_ids: + console.puts(context.style.node(group_id)) + + +class GroupPolicyBase(structure.ModelMixin): + """ + Policies applied to groups. + + Properties: + + * :code:`name`: Name + * :code:`description`: Description + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`properties`: Dict of :class:`Parameter` + * :code:`triggers`: Dict of :class:`GroupPolicyTrigger` + """ + __tablename__ = 'group_policy' + # region foreign_keys + @declared_attr + def group_fk(cls): + return cls.foreign_key('group') + + # endregion + + description = Column(Text) + type_name = Column(Text) + + # region many-to-one relationships + @declared_attr + def group(cls): + return cls.many_to_one_relationship('group') + + # end region + + # region many-to-many relationships + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # 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)), + ('triggers', formatting.as_raw_list(self.triggers)))) + + def validate(self, context): + if context.modeling.policy_types.get_descendant(self.type_name) is None: + context.validation.report( + 'group policy "%s" has an unknown type: %s' + % (self.name, + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.properties) + utils.validate_dict_values(context, self.triggers) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + utils.coerce_dict_values(context, container, self.triggers, report_issues) + + def dump(self, context): + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Group policy type: %s' % context.style.type(self.type_name)) + utils.dump_parameters(context, self.properties) + utils.dump_dict_values(context, self.triggers, 'Triggers') + + +class GroupPolicyTriggerBase(structure.ModelMixin): + """ + Triggers for :class:`GroupPolicy`. + + Properties: + + * :code:`name`: Name + * :code:`description`: Description + * :code:`implementation`: Implementation string (interpreted by the orchestrator) + * :code:`properties`: Dict of :class:`Parameter` + """ + __tablename__ = 'group_policy_trigger' + # region foreign keys + + @declared_attr + def group_policy_fk(cls): + return cls.foreign_key('group_policy') + + # endregion + + description = Column(Text) + implementation = Column(Text) + + # region many-to-one relationships + + @declared_attr + def group_policy(cls): + return cls.many_to_one_relationship('group_policy') + + # endregion + + # region many-to-many relationships + + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('implementation', self.implementation), + ('properties', formatting.as_raw_dict(self.properties)))) + + def validate(self, context): + utils.validate_dict_values(context, self.properties) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + + def dump(self, context): + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Implementation: %s' % context.style.literal(self.implementation)) + utils.dump_parameters(context, self.properties) + + +class MappingBase(structure.ModelMixin): + """ + An instance of a :class:`MappingTemplate`. + + Properties: + + * :code:`mapped_name`: Exposed capability or requirement name + * :code:`node_id`: Must be represented in the :class:`ServiceInstance` + * :code:`name`: Name of capability or requirement at the node + """ + __tablename__ = 'mapping' + + mapped_name = Column(Text) + node_id = Column(Text) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('mapped_name', self.mapped_name), + ('node_id', self.node_id), + ('name', self.name))) + + def dump(self, context): + console.puts('%s -> %s.%s' + % (context.style.node(self.mapped_name), + context.style.node(self.node_id), + context.style.node(self.name))) + + +class SubstitutionBase(structure.ModelMixin): + """ + An instance of a :class:`SubstitutionTemplate`. + + Properties: + + * :code:`node_type_name`: Must be represented in the :class:`ModelingContext` + * :code:`capabilities`: Dict of :class:`Mapping` + * :code:`requirements`: Dict of :class:`Mapping` + """ + __tablename__ = 'substitution' + node_type_name = Column(Text) + + # region many-to-many relationships + + @declared_attr + def capabilities(cls): + return cls.many_to_many_relationship('mapping', table_prefix='capabilities') + + @declared_attr + def requirements(cls): + return cls.many_to_many_relationship('mapping', + table_prefix='requirements', + relationship_kwargs=dict(lazy='dynamic')) + + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('node_type_name', self.node_type_name), + ('capabilities', formatting.as_raw_list(self.capabilities)), + ('requirements', formatting.as_raw_list(self.requirements)))) + + def validate(self, context): + if context.modeling.node_types.get_descendant(self.node_type_name) is None: + context.validation.report('substitution "%s" has an unknown type: %s' + % (self.name, # pylint: disable=no-member + # TODO fix self.name reference + formatting.safe_repr(self.node_type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.capabilities) + utils.validate_dict_values(context, self.requirements) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.capabilities, report_issues) + utils.coerce_dict_values(context, container, self.requirements, report_issues) + + def dump(self, context): + console.puts('Substitution:') + with context.style.indent: + console.puts('Node type: %s' % context.style.type(self.node_type_name)) + utils.dump_dict_values(context, self.capabilities, 'Capability mappings') + utils.dump_dict_values(context, self.requirements, 'Requirement mappings') + + +# endregion + +# region Node instances + +class NodeBase(structure.ModelMixin): + """ + An instance of a :class:`NodeTemplate`. + + Nodes may have zero or more :class:`Relationship` instances to other nodes. + + Properties: + + * :code:`id`: Unique ID (prefixed with the template name) + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`template_name`: Must be represented in the :class:`ServiceModel` + * :code:`properties`: Dict of :class:`Parameter` + * :code:`interfaces`: Dict of :class:`Interface` + * :code:`artifacts`: Dict of :class:`Artifact` + * :code:`capabilities`: Dict of :class:`CapabilityTemplate` + * :code:`relationship`: List of :class:`Relationship` + """ + __tablename__ = 'node' + + # region foreign_keys + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + @declared_attr + def host_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def node_template_fk(cls): + return cls.foreign_key('node_template') + + # endregion + + type_name = Column(Text) + template_name = Column(Text) + + # region orchestrator required columns + runtime_properties = Column(aria_types.Dict) + scaling_groups = Column(aria_types.List) + state = Column(Text, nullable=False) + version = Column(Integer, default=1) + + @declared_attr + def plugins(cls): + return association_proxy('node_template', 'plugins') + + @declared_attr + def host(cls): + return cls.relationship_to_self('host_fk') + + @property + def ip(self): + if not self.host_fk: + return None + host_node_instance = self.host + if 'ip' in host_node_instance.runtime_properties: # pylint: disable=no-member + return host_node_instance.runtime_properties['ip'] # pylint: disable=no-member + host_node = host_node_instance.node # pylint: disable=no-member + if 'ip' in host_node.properties: + return host_node.properties['ip'] + return None + + @declared_attr + def node_template(cls): + return cls.many_to_one_relationship('node_template') + + @declared_attr + def service_template(cls): + return association_proxy('service_instance', 'service_template') + # endregion + + # region many-to-one relationships + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance') + + # endregion + + # region many-to-many relationships + + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # endregion + + def satisfy_requirements(self, context): + node_template = context.modeling.model.node_templates.get(self.template_name) + satisfied = True + for i in range(len(node_template.requirement_templates)): + requirement_template = node_template.requirement_templates[i] + + # Find target template + target_node_template, target_node_capability = \ + requirement_template.find_target(context, node_template) + if target_node_template is not None: + satisfied = self._satisfy_capability(context, + target_node_capability, + target_node_template, + requirement_template, + requirement_template_index=i) + else: + context.validation.report('requirement "%s" of node "%s" has no target node ' + 'template' % (requirement_template.name, + self.id), + level=validation.Issue.BETWEEN_INSTANCES) + satisfied = False + return satisfied + + def _satisfy_capability(self, context, target_node_capability, target_node_template, + requirement_template, requirement_template_index): + # Find target nodes + target_nodes = context.modeling.instance.find_nodes(target_node_template.name) + if target_nodes: + target_node = None + target_capability = None + + if target_node_capability is not None: + # Relate to the first target node that has capacity + for node in target_nodes: + target_capability = node.capabilities.get(target_node_capability.name) + if target_capability.relate(): + target_node = node + break + else: + # Use first target node + target_node = target_nodes[0] + + if target_node is not None: + relationship = RelationshipBase( + name=requirement_template.name, + source_requirement_index=requirement_template_index, + target_node_id=target_node.id, + target_capability_name=target_capability.name + ) + self.relationships.append(relationship) + else: + context.validation.report('requirement "%s" of node "%s" targets node ' + 'template "%s" but its instantiated nodes do not ' + 'have enough capacity' + % (requirement_template.name, + self.id, + target_node_template.name), + level=validation.Issue.BETWEEN_INSTANCES) + return False + else: + context.validation.report('requirement "%s" of node "%s" targets node template ' + '"%s" but it has no instantiated nodes' + % (requirement_template.name, + self.id, + target_node_template.name), + level=validation.Issue.BETWEEN_INSTANCES) + return False + + def validate_capabilities(self, context): + satisfied = False + for capability in self.capabilities.itervalues(): + if not capability.has_enough_relationships: + context.validation.report('capability "%s" of node "%s" requires at least %d ' + 'relationships but has %d' + % (capability.name, + self.id, + capability.min_occurrences, + capability.occurrences), + level=validation.Issue.BETWEEN_INSTANCES) + satisfied = False + return satisfied + + @property + def as_raw(self): + return collections.OrderedDict(( + ('id', self.id), + ('type_name', self.type_name), + ('template_name', self.template_name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)), + ('artifacts', formatting.as_raw_list(self.artifacts)), + ('capabilities', formatting.as_raw_list(self.capabilities)), + ('relationships', formatting.as_raw_list(self.relationships)))) + + def validate(self, context): + if len(self.id) > context.modeling.id_max_length: + context.validation.report('"%s" has an ID longer than the limit of %d characters: %d' + % (self.id, + context.modeling.id_max_length, + len(self.id)), + level=validation.Issue.BETWEEN_INSTANCES) + + # TODO: validate that node template is of type? + + utils.validate_dict_values(context, self.properties) + utils.validate_dict_values(context, self.interfaces) + utils.validate_dict_values(context, self.artifacts) + utils.validate_dict_values(context, self.capabilities) + utils.validate_list_values(context, self.relationships) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, self, self.properties, report_issues) + utils.coerce_dict_values(context, self, self.interfaces, report_issues) + utils.coerce_dict_values(context, self, self.artifacts, report_issues) + utils.coerce_dict_values(context, self, self.capabilities, report_issues) + utils.coerce_list_values(context, self, self.relationships, report_issues) + + def dump(self, context): + console.puts('Node: %s' % context.style.node(self.id)) + with context.style.indent: + console.puts('Template: %s' % context.style.node(self.template_name)) + console.puts('Type: %s' % context.style.type(self.type_name)) + utils.dump_parameters(context, self.properties) + utils.dump_interfaces(context, self.interfaces) + utils.dump_dict_values(context, self.artifacts, 'Artifacts') + utils.dump_dict_values(context, self.capabilities, 'Capabilities') + utils.dump_list_values(context, self.relationships, 'Relationships') + + +class GroupBase(structure.ModelMixin): + """ + An instance of a :class:`GroupTemplate`. + + Properties: + + * :code:`id`: Unique ID (prefixed with the template name) + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`template_name`: Must be represented in the :class:`ServiceModel` + * :code:`properties`: Dict of :class:`Parameter` + * :code:`interfaces`: Dict of :class:`Interface` + * :code:`policies`: Dict of :class:`GroupPolicy` + * :code:`member_node_ids`: Must be represented in the :class:`ServiceInstance` + * :code:`member_group_ids`: Must be represented in the :class:`ServiceInstance` + """ + __tablename__ = 'group' + # region foreign_keys + + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + # endregion + + type_name = Column(Text) + template_name = Column(Text) + member_node_ids = Column(aria_types.StrictList(basestring)) + member_group_ids = Column(aria_types.StrictList(basestring)) + + # region many-to-one relationships + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance') + + # region many-to-many relationships + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('id', self.id), + ('type_name', self.type_name), + ('template_name', self.template_name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)), + ('policies', formatting.as_raw_list(self.policies)), + ('member_node_ids', self.member_node_ids), + ('member_group_ids', self.member_group_ids))) + + def validate(self, context): + if context.modeling.group_types.get_descendant(self.type_name) is None: + context.validation.report('group "%s" has an unknown type: %s' + % (self.name, # pylint: disable=no-member + # TODO fix self.name reference + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + + utils.validate_dict_values(context, self.properties) + utils.validate_dict_values(context, self.interfaces) + utils.validate_dict_values(context, self.policies) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + utils.coerce_dict_values(context, container, self.interfaces, report_issues) + utils.coerce_dict_values(context, container, self.policies, report_issues) + + def dump(self, context): + console.puts('Group: %s' % context.style.node(self.id)) + with context.style.indent: + console.puts('Type: %s' % context.style.type(self.type_name)) + console.puts('Template: %s' % context.style.type(self.template_name)) + utils.dump_parameters(context, self.properties) + utils.dump_interfaces(context, self.interfaces) + utils.dump_dict_values(context, self.policies, 'Policies') + if self.member_node_ids: + console.puts('Member nodes:') + with context.style.indent: + for node_id in self.member_node_ids: + console.puts(context.style.node(node_id)) + +# endregion + +# region Relationship instances + + +class RelationshipBase(structure.ModelMixin): + """ + Connects :class:`Node` to another node. + + An instance of a :class:`RelationshipTemplate`. + + Properties: + + * :code:`name`: Name (usually the name of the requirement at the source node template) + * :code:`source_requirement_index`: Must be represented in the source node template + * :code:`target_node_id`: Must be represented in the :class:`ServiceInstance` + * :code:`target_capability_name`: Matches the capability at the target node + * :code:`type_name`: Must be represented in the :class:`ModelingContext` + * :code:`template_name`: Must be represented in the :class:`ServiceModel` + * :code:`properties`: Dict of :class:`Parameter` + * :code:`source_interfaces`: Dict of :class:`Interface` + * :code:`target_interfaces`: Dict of :class:`Interface` + """ + __tablename__ = 'relationship' + + source_requirement_index = Column(Integer) + target_node_id = Column(Text) + target_capability_name = Column(Text) + type_name = Column(Text) + template_name = Column(Text) + + # # region orchestrator required columns + source_position = Column(Integer) + target_position = Column(Integer) + + @declared_attr + def source_node_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def source_node(cls): + return cls.many_to_one_relationship( + 'node', + 'source_node_fk', + backreference='outbound_relationships', + backref_kwargs=dict( + order_by=cls.source_position, + collection_class=ordering_list('source_position', count_from=0), + lazy='select' + ) + ) + + @declared_attr + def source_node_name(cls): + return association_proxy('source_node', cls.name_column_name()) + + @declared_attr + def target_node_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def target_node(cls): + return cls.many_to_one_relationship( + 'node', + 'target_node_fk', + backreference='inbound_relationships', + backref_kwargs=dict( + order_by=cls.target_position, + collection_class=ordering_list('target_position', count_from=0), + lazy='select' + ) + ) + + @declared_attr + def target_node_name(cls): + return association_proxy('target_node', cls.name_column_name()) + # endregion + + # region many-to-many relationship + + @declared_attr + def properties(cls): + return cls.many_to_many_relationship('parameter', table_prefix='properties') + + @declared_attr + def source_interfaces(cls): + return cls.many_to_many_relationship('interface', + table_prefix='source_interfaces', + relationship_kwargs=dict(lazy='dynamic')) + + @declared_attr + def target_interfaces(cls): + return cls.many_to_many_relationship('interface', + table_prefix='target_interfaces', + relationship_kwargs=dict(lazy='dynamic')) + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('source_requirement_index', self.source_requirement_index), + ('target_node_id', self.target_node_id), + ('target_capability_name', self.target_capability_name), + ('type_name', self.type_name), + ('template_name', self.template_name), + ('properties', formatting.as_raw_dict(self.properties)), + ('source_interfaces', formatting.as_raw_list(self.source_interfaces)), + ('target_interfaces', formatting.as_raw_list(self.target_interfaces)))) + + def validate(self, context): + if self.type_name: + if context.modeling.relationship_types.get_descendant(self.type_name) is None: + context.validation.report('relationship "%s" has an unknown type: %s' + % (self.name, + formatting.safe_repr(self.type_name)), + level=validation.Issue.BETWEEN_TYPES) + utils.validate_dict_values(context, self.properties) + utils.validate_dict_values(context, self.source_interfaces) + utils.validate_dict_values(context, self.target_interfaces) + + def coerce_values(self, context, container, report_issues): + utils.coerce_dict_values(context, container, self.properties, report_issues) + utils.coerce_dict_values(context, container, self.source_interfaces, report_issues) + utils.coerce_dict_values(context, container, self.target_interfaces, report_issues) + + def dump(self, context): + if self.name: + if self.source_requirement_index is not None: + console.puts('%s (%d) ->' % ( + context.style.node(self.name), + self.source_requirement_index)) + else: + console.puts('%s ->' % context.style.node(self.name)) + else: + console.puts('->') + with context.style.indent: + console.puts('Node: %s' % context.style.node(self.target_node_id)) + if self.target_capability_name is not None: + console.puts('Capability: %s' % context.style.node(self.target_capability_name)) + if self.type_name is not None: + console.puts('Relationship type: %s' % context.style.type(self.type_name)) + if self.template_name is not None: + console.puts('Relationship template: %s' % context.style.node(self.template_name)) + utils.dump_parameters(context, self.properties) + utils.dump_interfaces(context, self.source_interfaces, 'Source interfaces') + utils.dump_interfaces(context, self.target_interfaces, 'Target interfaces') + +# endregion http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/55556793/aria/storage/modeling/model.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/model.py b/aria/storage/modeling/model.py new file mode 100644 index 0000000..fc45a8e --- /dev/null +++ b/aria/storage/modeling/model.py @@ -0,0 +1,175 @@ +# 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 sqlalchemy.ext.declarative import declarative_base + +from . import ( + template_elements, + instance_elements, + orchestrator_elements, + elements, + structure, +) + +DB = declarative_base(cls=structure.ModelIDMixin) + +# pylint: disable=abstract-method + +# region elements + + +class Parameter(elements.ParameterBase, DB): + pass + +# endregion + +# region template models + + +class MappingTemplate(DB, template_elements.MappingTemplateBase): + pass + + +class SubstitutionTemplate(DB, template_elements.SubstitutionTemplateBase): + pass + + +class InterfaceTemplate(DB, template_elements.InterfaceTemplateBase): + pass + + +class OperationTemplate(DB, template_elements.OperationTemplateBase): + pass + + +class ServiceTemplate(DB, template_elements.ServiceTemplateBase): + pass + + +class NodeTemplate(DB, template_elements.NodeTemplateBase): + pass + + +class GroupTemplate(DB, template_elements.GroupTemplateBase): + pass + + +class ArtifactTemplate(DB, template_elements.ArtifactTemplateBase): + pass + + +class PolicyTemplate(DB, template_elements.PolicyTemplateBase): + pass + + +class GroupPolicyTemplate(DB, template_elements.GroupPolicyTemplateBase): + pass + + +class GroupPolicyTriggerTemplate(DB, template_elements.GroupPolicyTriggerTemplateBase): + pass + + +class RequirementTemplate(DB, template_elements.RequirementTemplateBase): + pass + + +class CapabilityTemplate(DB, template_elements.CapabilityTemplateBase): + pass + + +# endregion + +# region instance models + +class Mapping(DB, instance_elements.MappingBase): + pass + + +class Substitution(DB, instance_elements.SubstitutionBase): + pass + + +class ServiceInstance(DB, instance_elements.ServiceInstanceBase): + pass + + +class Node(DB, instance_elements.NodeBase): + pass + + +class Relationship(DB, instance_elements.RelationshipBase): + pass + + +class Artifact(DB, instance_elements.ArtifactBase): + pass + + +class Group(DB, instance_elements.GroupBase): + pass + + +class Interface(DB, instance_elements.InterfaceBase): + pass + + +class Operation(DB, instance_elements.OperationBase): + pass + + +class Capability(DB, instance_elements.CapabilityBase): + pass + + +class Policy(DB, instance_elements.PolicyBase): + pass + + +class GroupPolicy(DB, instance_elements.GroupPolicyBase): + pass + + +class GroupPolicyTrigger(DB, instance_elements.GroupPolicyTriggerBase): + pass + + +# endregion + +# region orchestrator models + +class Execution(DB, orchestrator_elements.Execution): + pass + + +class ServiceInstanceUpdate(DB, orchestrator_elements.ServiceInstanceUpdateBase): + pass + + +class ServiceInstanceUpdateStep(DB, orchestrator_elements.ServiceInstanceUpdateStepBase): + pass + + +class ServiceInstanceModification(DB, orchestrator_elements.ServiceInstanceModificationBase): + pass + + +class Plugin(DB, orchestrator_elements.PluginBase): + pass + + +class Task(DB, orchestrator_elements.TaskBase): + pass +# endregion http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/55556793/aria/storage/modeling/orchestrator_elements.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/orchestrator_elements.py b/aria/storage/modeling/orchestrator_elements.py new file mode 100644 index 0000000..a7ed5e9 --- /dev/null +++ b/aria/storage/modeling/orchestrator_elements.py @@ -0,0 +1,461 @@ +# 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. + +""" +Aria's storage.models module +Path: aria.storage.models + +models module holds aria's models. + +classes: + * Field - represents a single field. + * IterField - represents an iterable field. + * Model - abstract model implementation. + * Snapshot - snapshots implementation model. + * Deployment - deployment implementation model. + * DeploymentUpdateStep - deployment update step implementation model. + * DeploymentUpdate - deployment update implementation model. + * DeploymentModification - deployment modification implementation model. + * Execution - execution implementation model. + * Node - node implementation model. + * Relationship - relationship implementation model. + * NodeInstance - node instance implementation model. + * RelationshipInstance - relationship instance implementation model. + * Plugin - plugin implementation model. +""" +from collections import namedtuple +from datetime import datetime + +from sqlalchemy import ( + Column, + Integer, + Text, + DateTime, + Boolean, + Enum, + String, + Float, + orm, +) +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.declarative import declared_attr + +from aria.orchestrator.exceptions import TaskAbortException, TaskRetryException + +from .type import List, Dict +from .structure import ModelMixin + +__all__ = ( + 'ServiceInstanceUpdateStepBase', + 'ServiceInstanceUpdateBase', + 'ServiceInstanceModificationBase', + 'Execution', + 'PluginBase', + 'TaskBase' +) + +# pylint: disable=no-self-argument, no-member, abstract-method + + +class Execution(ModelMixin): + """ + Execution model representation. + """ + # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. + __tablename__ = 'execution' + + TERMINATED = 'terminated' + FAILED = 'failed' + CANCELLED = 'cancelled' + PENDING = 'pending' + STARTED = 'started' + CANCELLING = 'cancelling' + FORCE_CANCELLING = 'force_cancelling' + + STATES = [TERMINATED, FAILED, CANCELLED, PENDING, STARTED, CANCELLING, FORCE_CANCELLING] + END_STATES = [TERMINATED, FAILED, CANCELLED] + ACTIVE_STATES = [state for state in STATES if state not in END_STATES] + + VALID_TRANSITIONS = { + PENDING: [STARTED, CANCELLED], + STARTED: END_STATES + [CANCELLING], + CANCELLING: END_STATES + [FORCE_CANCELLING] + } + + @orm.validates('status') + def validate_status(self, key, value): + """Validation function that verifies execution status transitions are OK""" + try: + current_status = getattr(self, key) + except AttributeError: + return + valid_transitions = self.VALID_TRANSITIONS.get(current_status, []) + if all([current_status is not None, + current_status != value, + value not in valid_transitions]): + raise ValueError('Cannot change execution status from {current} to {new}'.format( + current=current_status, + new=value)) + return value + + created_at = Column(DateTime, index=True) + started_at = Column(DateTime, nullable=True, index=True) + ended_at = Column(DateTime, nullable=True, index=True) + error = Column(Text, nullable=True) + is_system_workflow = Column(Boolean, nullable=False, default=False) + parameters = Column(Dict) + status = Column(Enum(*STATES, name='execution_status'), default=PENDING) + workflow_name = Column(Text) + + @declared_attr + def service_template(cls): + return association_proxy('service_instance', 'service_template') + + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance') + + @declared_attr + def service_instance_name(cls): + return association_proxy('service_instance', cls.name_column_name()) + + @declared_attr + def service_template_name(cls): + return association_proxy('service_instance', 'service_template_name') + + def __str__(self): + return '<{0} id=`{1}` (status={2})>'.format( + self.__class__.__name__, + getattr(self, self.name_column_name()), + self.status + ) + + +class ServiceInstanceUpdateBase(ModelMixin): + """ + Deployment update model representation. + """ + # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. + steps = None + + __tablename__ = 'service_instance_update' + + _private_fields = ['execution_fk', 'deployment_fk'] + + created_at = Column(DateTime, nullable=False, index=True) + deployment_plan = Column(Dict, nullable=False) + deployment_update_node_instances = Column(Dict) + deployment_update_deployment = Column(Dict) + deployment_update_nodes = Column(List) + modified_entity_ids = Column(Dict) + state = Column(Text) + + @declared_attr + def execution_fk(cls): + return cls.foreign_key('execution', nullable=True) + + @declared_attr + def execution(cls): + return cls.many_to_one_relationship('execution') + + @declared_attr + def execution_name(cls): + return association_proxy('execution', cls.name_column_name()) + + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance') + + @declared_attr + def service_instance_name(cls): + return association_proxy('service_instance', cls.name_column_name()) + + def to_dict(self, suppress_error=False, **kwargs): + dep_update_dict = super(ServiceInstanceUpdateBase, self).to_dict(suppress_error) #pylint: disable=no-member + # Taking care of the fact the DeploymentSteps are _BaseModels + dep_update_dict['steps'] = [step.to_dict() for step in self.steps] + return dep_update_dict + + +class ServiceInstanceUpdateStepBase(ModelMixin): + """ + Deployment update step model representation. + """ + # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. + __tablename__ = 'service_instance_update_step' + _private_fields = ['deployment_update_fk'] + + _action_types = namedtuple('ACTION_TYPES', 'ADD, REMOVE, MODIFY') + ACTION_TYPES = _action_types(ADD='add', REMOVE='remove', MODIFY='modify') + _entity_types = namedtuple( + 'ENTITY_TYPES', + 'NODE, RELATIONSHIP, PROPERTY, OPERATION, WORKFLOW, OUTPUT, DESCRIPTION, GROUP, ' + 'POLICY_TYPE, POLICY_TRIGGER, PLUGIN') + ENTITY_TYPES = _entity_types( + NODE='node', + RELATIONSHIP='relationship', + PROPERTY='property', + OPERATION='operation', + WORKFLOW='workflow', + OUTPUT='output', + DESCRIPTION='description', + GROUP='group', + POLICY_TYPE='policy_type', + POLICY_TRIGGER='policy_trigger', + PLUGIN='plugin' + ) + + action = Column(Enum(*ACTION_TYPES, name='action_type'), nullable=False) + entity_id = Column(Text, nullable=False) + entity_type = Column(Enum(*ENTITY_TYPES, name='entity_type'), nullable=False) + + @declared_attr + def service_instance_update_fk(cls): + return cls.foreign_key('service_instance_update') + + @declared_attr + def deployment_update(cls): + return cls.many_to_one_relationship('service_instance_update', + backreference='steps') + + @declared_attr + def deployment_update_name(cls): + return association_proxy('deployment_update', cls.name_column_name()) + + def __hash__(self): + return hash((getattr(self, self.id_column_name()), self.entity_id)) + + def __lt__(self, other): + """ + the order is 'remove' < 'modify' < 'add' + :param other: + :return: + """ + if not isinstance(other, self.__class__): + return not self >= other + + if self.action != other.action: + if self.action == 'remove': + return_value = True + elif self.action == 'add': + return_value = False + else: + return_value = other.action == 'add' + return return_value + + if self.action == 'add': + return self.entity_type == 'node' and other.entity_type == 'relationship' + if self.action == 'remove': + return self.entity_type == 'relationship' and other.entity_type == 'node' + return False + + +class ServiceInstanceModificationBase(ModelMixin): + """ + Deployment modification model representation. + """ + __tablename__ = 'service_instance_modification' + _private_fields = ['deployment_fk'] + + STARTED = 'started' + FINISHED = 'finished' + ROLLEDBACK = 'rolledback' + + STATES = [STARTED, FINISHED, ROLLEDBACK] + END_STATES = [FINISHED, ROLLEDBACK] + + context = Column(Dict) + created_at = Column(DateTime, nullable=False, index=True) + ended_at = Column(DateTime, index=True) + modified_nodes = Column(Dict) + node_instances = Column(Dict) + status = Column(Enum(*STATES, name='deployment_modification_status')) + + @declared_attr + def service_instance_fk(cls): + return cls.foreign_key('service_instance') + + @declared_attr + def service_instance(cls): + return cls.many_to_one_relationship('service_instance', + backreference='modifications') + + @declared_attr + def deployment_name(cls): + return association_proxy('service_instance', cls.name_column_name()) + + +class PluginBase(ModelMixin): + """ + Plugin model representation. + """ + __tablename__ = 'plugin' + + archive_name = Column(Text, nullable=False, index=True) + distribution = Column(Text) + distribution_release = Column(Text) + distribution_version = Column(Text) + package_name = Column(Text, nullable=False, index=True) + package_source = Column(Text) + package_version = Column(Text) + supported_platform = Column(Text) + supported_py_versions = Column(List) + uploaded_at = Column(DateTime, nullable=False, index=True) + wheels = Column(List, nullable=False) + + +class TaskBase(ModelMixin): + """ + A Model which represents an task + """ + __tablename__ = 'task' + _private_fields = ['node_instance_fk', 'relationship_instance_fk', 'execution_fk'] + + @declared_attr + def node_fk(cls): + return cls.foreign_key('node', nullable=True) + + @declared_attr + def node_name(cls): + return association_proxy('node', cls.name_column_name()) + + @declared_attr + def node(cls): + return cls.many_to_one_relationship('node') + + @declared_attr + def relationship_fk(cls): + return cls.foreign_key('relationship', nullable=True) + + @declared_attr + def relationship_name(cls): + return association_proxy('relationships', cls.name_column_name()) + + @declared_attr + def relationship(cls): + return cls.many_to_one_relationship('relationship') + + @declared_attr + def plugin_fk(cls): + return cls.foreign_key('plugin', nullable=True) + + @declared_attr + def plugin(cls): + return cls.many_to_one_relationship('plugin') + + @declared_attr + def execution_fk(cls): + return cls.foreign_key('execution', nullable=True) + + @declared_attr + def execution(cls): + return cls.many_to_one_relationship('execution') + + @declared_attr + def execution_name(cls): + return association_proxy('execution', cls.name_column_name()) + + PENDING = 'pending' + RETRYING = 'retrying' + SENT = 'sent' + STARTED = 'started' + SUCCESS = 'success' + FAILED = 'failed' + STATES = ( + PENDING, + RETRYING, + SENT, + STARTED, + SUCCESS, + FAILED, + ) + + WAIT_STATES = [PENDING, RETRYING] + END_STATES = [SUCCESS, FAILED] + + RUNS_ON_SOURCE = 'source' + RUNS_ON_TARGET = 'target' + RUNS_ON_NODE_INSTANCE = 'node_instance' + RUNS_ON = (RUNS_ON_NODE_INSTANCE, RUNS_ON_SOURCE, RUNS_ON_TARGET) + + @orm.validates('max_attempts') + def validate_max_attempts(self, _, value): # pylint: disable=no-self-use + """Validates that max attempts is either -1 or a positive number""" + if value < 1 and value != TaskBase.INFINITE_RETRIES: + raise ValueError('Max attempts can be either -1 (infinite) or any positive number. ' + 'Got {value}'.format(value=value)) + return value + + INFINITE_RETRIES = -1 + + status = Column(Enum(*STATES, name='status'), default=PENDING) + + due_at = Column(DateTime, default=datetime.utcnow) + started_at = Column(DateTime, default=None) + ended_at = Column(DateTime, default=None) + max_attempts = Column(Integer, default=1) + retry_count = Column(Integer, default=0) + retry_interval = Column(Float, default=0) + ignore_failure = Column(Boolean, default=False) + + # Operation specific fields + implementation = Column(String) + inputs = Column(Dict) + # This is unrelated to the plugin of the task. This field is related to the plugin name + # received from the blueprint. + plugin_name = Column(String) + _runs_on = Column(Enum(*RUNS_ON, name='runs_on'), name='runs_on') + + @property + def runs_on(self): + if self._runs_on == self.RUNS_ON_NODE_INSTANCE: + return self.node + elif self._runs_on == self.RUNS_ON_SOURCE: + return self.relationship.source_node # pylint: disable=no-member + elif self._runs_on == self.RUNS_ON_TARGET: + return self.relationship.target_node # pylint: disable=no-member + return None + + @property + def actor(self): + """ + Return the actor of the task + :return: + """ + return self.node or self.relationship + + @classmethod + def as_node_instance(cls, instance, runs_on, **kwargs): + return cls(node=instance, _runs_on=runs_on, **kwargs) + + @classmethod + def as_relationship_instance(cls, instance, runs_on, **kwargs): + return cls(relationship=instance, _runs_on=runs_on, **kwargs) + + @staticmethod + def abort(message=None): + raise TaskAbortException(message) + + @staticmethod + def retry(message=None, retry_interval=None): + raise TaskRetryException(message, retry_interval=retry_interval) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/55556793/aria/storage/modeling/structure.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/structure.py b/aria/storage/modeling/structure.py new file mode 100644 index 0000000..386887e --- /dev/null +++ b/aria/storage/modeling/structure.py @@ -0,0 +1,320 @@ +# 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. + +""" +Aria's storage.structures module +Path: aria.storage.structures + +models module holds aria's models. + +classes: + * Field - represents a single field. + * IterField - represents an iterable field. + * PointerField - represents a single pointer field. + * IterPointerField - represents an iterable pointers field. + * Model - abstract model implementation. +""" + +from sqlalchemy.orm import relationship, backref +from sqlalchemy.ext import associationproxy +from sqlalchemy import ( + Column, + ForeignKey, + Integer, + Text, + Table, +) + +from . import utils + + +class Function(object): + """ + An intrinsic function. + + Serves as a placeholder for a value that should eventually be derived + by calling the function. + """ + + @property + def as_raw(self): + raise NotImplementedError + + def _evaluate(self, context, container): + raise NotImplementedError + + def __deepcopy__(self, memo): + # Circumvent cloning in order to maintain our state + return self + + +class ElementBase(object): + """ + Base class for :class:`ServiceInstance` elements. + + All elements support validation, diagnostic dumping, and representation as + raw data (which can be translated into JSON or YAML) via :code:`as_raw`. + """ + + @property + def as_raw(self): + raise NotImplementedError + + def validate(self, context): + pass + + def coerce_values(self, context, container, report_issues): + pass + + def dump(self, context): + pass + + +class ModelElementBase(ElementBase): + """ + Base class for :class:`ServiceModel` elements. + + All model elements can be instantiated into :class:`ServiceInstance` elements. + """ + + def instantiate(self, context, container): + raise NotImplementedError + + +class ModelMixin(ModelElementBase): + + @utils.classproperty + def __modelname__(cls): # pylint: disable=no-self-argument + return getattr(cls, '__mapiname__', cls.__tablename__) + + @classmethod + def id_column_name(cls): + raise NotImplementedError + + @classmethod + def name_column_name(cls): + raise NotImplementedError + + @classmethod + def _get_cls_by_tablename(cls, tablename): + """Return class reference mapped to table. + + :param tablename: String with name of table. + :return: Class reference or None. + """ + if tablename in (cls.__name__, cls.__tablename__): + return cls + + for table_cls in cls._decl_class_registry.values(): + if tablename == getattr(table_cls, '__tablename__', None): + return table_cls + + @classmethod + def foreign_key(cls, table_name, nullable=False): + """Return a ForeignKey object with the relevant + + :param table: Unique id column in the parent table + :param nullable: Should the column be allowed to remain empty + """ + return Column(Integer, + ForeignKey('{tablename}.id'.format(tablename=table_name), ondelete='CASCADE'), + nullable=nullable) + + @classmethod + def one_to_one_relationship(cls, table_name, backreference=None): + return relationship(lambda: cls._get_cls_by_tablename(table_name), + backref=backref(backreference or cls.__tablename__, uselist=False)) + + @classmethod + def many_to_one_relationship(cls, + parent_table_name, + foreign_key_column=None, + backreference=None, + backref_kwargs=None, + **kwargs): + """Return a one-to-many SQL relationship object + Meant to be used from inside the *child* object + + :param parent_class: Class of the parent table + :param cls: Class of the child table + :param foreign_key_column: The column of the foreign key (from the child table) + :param backreference: The name to give to the reference to the child (on the parent table) + """ + relationship_kwargs = kwargs + if foreign_key_column: + relationship_kwargs.setdefault('foreign_keys', getattr(cls, foreign_key_column)) + + backref_kwargs = backref_kwargs or {} + backref_kwargs.setdefault('lazy', 'dynamic') + # The following line make sure that when the *parent* is + # deleted, all its connected children are deleted as well + backref_kwargs.setdefault('cascade', 'all') + + return relationship(lambda: cls._get_cls_by_tablename(parent_table_name), + backref=backref(backreference or utils.pluralize(cls.__tablename__), + **backref_kwargs or {}), + **relationship_kwargs) + + @classmethod + def relationship_to_self(cls, local_column): + + remote_side_str = '{cls.__name__}.{remote_column}'.format( + cls=cls, + remote_column=cls.id_column_name() + ) + primaryjoin_str = '{remote_side_str} == {cls.__name__}.{local_column}'.format( + remote_side_str=remote_side_str, + cls=cls, + local_column=local_column) + return relationship(cls._get_cls_by_tablename(cls.__tablename__).__name__, + primaryjoin=primaryjoin_str, + remote_side=remote_side_str, + post_update=True) + + @classmethod + def many_to_many_relationship(cls, other_table_name, table_prefix, relationship_kwargs=None): + """Return a many-to-many SQL relationship object + + Notes: + 1. The backreference name is the current table's table name + 2. This method creates a new helper table in the DB + + :param cls: The class of the table we're connecting from + :param other_table_name: The class of the table we're connecting to + :param table_prefix: Custom prefix for the helper table name and the + backreference name + """ + current_table_name = cls.__tablename__ + current_column_name = '{0}_id'.format(current_table_name) + current_foreign_key = '{0}.id'.format(current_table_name) + + other_column_name = '{0}_id'.format(other_table_name) + other_foreign_key = '{0}.id'.format(other_table_name) + + helper_table_name = '{0}_{1}'.format(current_table_name, other_table_name) + + backref_name = current_table_name + if table_prefix: + helper_table_name = '{0}_{1}'.format(table_prefix, helper_table_name) + backref_name = '{0}_{1}'.format(table_prefix, backref_name) + + secondary_table = cls.get_secondary_table( + cls.metadata, + helper_table_name, + current_column_name, + other_column_name, + current_foreign_key, + other_foreign_key + ) + + return relationship( + lambda: cls._get_cls_by_tablename(other_table_name), + secondary=secondary_table, + backref=backref(backref_name), + **(relationship_kwargs or {}) + ) + + @staticmethod + def get_secondary_table(metadata, + helper_table_name, + first_column_name, + second_column_name, + first_foreign_key, + second_foreign_key): + """Create a helper table for a many-to-many relationship + + :param helper_table_name: The name of the table + :param first_column_name: The name of the first column in the table + :param second_column_name: The name of the second column in the table + :param first_foreign_key: The string representing the first foreign key, + for example `blueprint.storage_id`, or `tenants.id` + :param second_foreign_key: The string representing the second foreign key + :return: A Table object + """ + return Table( + helper_table_name, + metadata, + Column( + first_column_name, + Integer, + ForeignKey(first_foreign_key) + ), + Column( + second_column_name, + Integer, + ForeignKey(second_foreign_key) + ) + ) + + def to_dict(self, fields=None, suppress_error=False): + """Return a dict representation of the model + + :param suppress_error: If set to True, sets `None` to attributes that + it's unable to retrieve (e.g., if a relationship wasn't established + yet, and so it's impossible to access a property through it) + """ + res = dict() + fields = fields or self.fields() + for field in fields: + try: + field_value = getattr(self, field) + except AttributeError: + if suppress_error: + field_value = None + else: + raise + if isinstance(field_value, list): + field_value = list(field_value) + elif isinstance(field_value, dict): + field_value = dict(field_value) + elif isinstance(field_value, ModelMixin): + field_value = field_value.to_dict() + res[field] = field_value + + return res + + @classmethod + def _association_proxies(cls): + for col, value in vars(cls).items(): + if isinstance(value, associationproxy.AssociationProxy): + yield col + + @classmethod + def fields(cls): + """Return the list of field names for this table + + Mostly for backwards compatibility in the code (that uses `fields`) + """ + fields = set(cls._association_proxies()) + fields.update(cls.__table__.columns.keys()) + return fields - set(getattr(cls, '_private_fields', [])) + + def __repr__(self): + return '<{__class__.__name__} id=`{id}`>'.format( + __class__=self.__class__, + id=getattr(self, self.name_column_name())) + + +class ModelIDMixin(object): + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(Text, nullable=True, index=True) + + @classmethod + def id_column_name(cls): + return 'id' + + @classmethod + def name_column_name(cls): + return 'name'
