You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by mx...@apache.org on 2017/02/16 14:33:20 UTC
[08/13] incubator-ariatosca git commit: ARIA-44 Merge parser and
storage model
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/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..62b90b3
--- /dev/null
+++ b/aria/storage/modeling/model.py
@@ -0,0 +1,219 @@
+# 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,
+)
+
+__all__ = (
+ 'aria_declarative_base',
+
+ 'Parameter',
+
+ 'MappingTemplate',
+ 'InterfaceTemplate',
+ 'OperationTemplate',
+ 'ServiceTemplate',
+ 'NodeTemplate',
+ 'GroupTemplate',
+ 'ArtifactTemplate',
+ 'PolicyTemplate',
+ 'GroupPolicyTemplate',
+ 'GroupPolicyTriggerTemplate',
+ 'RequirementTemplate',
+ 'CapabilityTemplate',
+
+ 'Mapping',
+ 'Substitution',
+ 'ServiceInstance',
+ 'Node',
+ 'Relationship',
+ 'Artifact',
+ 'Group',
+ 'Interface',
+ 'Operation',
+ 'Capability',
+ 'Policy',
+ 'GroupPolicy',
+ 'GroupPolicyTrigger',
+
+ 'Execution',
+ 'ServiceInstanceUpdate',
+ 'ServiceInstanceUpdateStep',
+ 'ServiceInstanceModification',
+ 'Plugin',
+ 'Task'
+)
+
+aria_declarative_base = declarative_base(cls=structure.ModelIDMixin)
+
+# pylint: disable=abstract-method
+
+# region elements
+
+
+class Parameter(aria_declarative_base, elements.ParameterBase):
+ pass
+
+# endregion
+
+# region template models
+
+
+class MappingTemplate(aria_declarative_base, template_elements.MappingTemplateBase):
+ pass
+
+
+class SubstitutionTemplate(aria_declarative_base, template_elements.SubstitutionTemplateBase):
+ pass
+
+
+class InterfaceTemplate(aria_declarative_base, template_elements.InterfaceTemplateBase):
+ pass
+
+
+class OperationTemplate(aria_declarative_base, template_elements.OperationTemplateBase):
+ pass
+
+
+class ServiceTemplate(aria_declarative_base, template_elements.ServiceTemplateBase):
+ pass
+
+
+class NodeTemplate(aria_declarative_base, template_elements.NodeTemplateBase):
+ pass
+
+
+class GroupTemplate(aria_declarative_base, template_elements.GroupTemplateBase):
+ pass
+
+
+class ArtifactTemplate(aria_declarative_base, template_elements.ArtifactTemplateBase):
+ pass
+
+
+class PolicyTemplate(aria_declarative_base, template_elements.PolicyTemplateBase):
+ pass
+
+
+class GroupPolicyTemplate(aria_declarative_base, template_elements.GroupPolicyTemplateBase):
+ pass
+
+
+class GroupPolicyTriggerTemplate(aria_declarative_base,
+ template_elements.GroupPolicyTriggerTemplateBase):
+ pass
+
+
+class RequirementTemplate(aria_declarative_base, template_elements.RequirementTemplateBase):
+ pass
+
+
+class CapabilityTemplate(aria_declarative_base, template_elements.CapabilityTemplateBase):
+ pass
+
+
+# endregion
+
+# region instance models
+
+class Mapping(aria_declarative_base, instance_elements.MappingBase):
+ pass
+
+
+class Substitution(aria_declarative_base, instance_elements.SubstitutionBase):
+ pass
+
+
+class ServiceInstance(aria_declarative_base, instance_elements.ServiceInstanceBase):
+ pass
+
+
+class Node(aria_declarative_base, instance_elements.NodeBase):
+ pass
+
+
+class Relationship(aria_declarative_base, instance_elements.RelationshipBase):
+ pass
+
+
+class Artifact(aria_declarative_base, instance_elements.ArtifactBase):
+ pass
+
+
+class Group(aria_declarative_base, instance_elements.GroupBase):
+ pass
+
+
+class Interface(aria_declarative_base, instance_elements.InterfaceBase):
+ pass
+
+
+class Operation(aria_declarative_base, instance_elements.OperationBase):
+ pass
+
+
+class Capability(aria_declarative_base, instance_elements.CapabilityBase):
+ pass
+
+
+class Policy(aria_declarative_base, instance_elements.PolicyBase):
+ pass
+
+
+class GroupPolicy(aria_declarative_base, instance_elements.GroupPolicyBase):
+ pass
+
+
+class GroupPolicyTrigger(aria_declarative_base, instance_elements.GroupPolicyTriggerBase):
+ pass
+
+
+# endregion
+
+# region orchestrator models
+
+class Execution(aria_declarative_base, orchestrator_elements.Execution):
+ pass
+
+
+class ServiceInstanceUpdate(aria_declarative_base,
+ orchestrator_elements.ServiceInstanceUpdateBase):
+ pass
+
+
+class ServiceInstanceUpdateStep(aria_declarative_base,
+ orchestrator_elements.ServiceInstanceUpdateStepBase):
+ pass
+
+
+class ServiceInstanceModification(aria_declarative_base,
+ orchestrator_elements.ServiceInstanceModificationBase):
+ pass
+
+
+class Plugin(aria_declarative_base, orchestrator_elements.PluginBase):
+ pass
+
+
+class Task(aria_declarative_base, orchestrator_elements.TaskBase):
+ pass
+# endregion
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/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..5f7a3f2
--- /dev/null
+++ b/aria/storage/modeling/orchestrator_elements.py
@@ -0,0 +1,468 @@
+# 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'
+
+ __private_fields__ = ['service_instance_fk']
+
+ 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__ = ['service_instance_fk',
+ 'execution_fk']
+
+ _private_fields = ['execution_fk', 'deployment_fk']
+
+ created_at = Column(DateTime, nullable=False, index=True)
+ service_instance_plan = Column(Dict, nullable=False)
+ service_instance_update_node_instances = Column(Dict)
+ service_instance_update_service_instance = Column(Dict)
+ service_instance_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__ = ['service_instance_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 service_instance_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__ = ['service_instance_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 service_instance_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_fk',
+ 'relationship_fk',
+ 'execution_fk',
+ 'plugin_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/b6193359/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..eacdb44
--- /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'