http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/aria/storage/base_model.py ---------------------------------------------------------------------- diff --git a/aria/storage/base_model.py b/aria/storage/base_model.py deleted file mode 100644 index f7d0e5b..0000000 --- a/aria/storage/base_model.py +++ /dev/null @@ -1,757 +0,0 @@ -# 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.ext.associationproxy import association_proxy -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy import ( - Column, - Integer, - Text, - DateTime, - Boolean, - Enum, - String, - Float, - orm, -) -from sqlalchemy.ext.orderinglist import ordering_list - -from ..orchestrator.exceptions import TaskAbortException, TaskRetryException -from .structure import ModelMixin -from .type import ( - List, - Dict -) - -__all__ = ( - 'BlueprintBase', - 'DeploymentBase', - 'DeploymentUpdateStepBase', - 'DeploymentUpdateBase', - 'DeploymentModificationBase', - 'ExecutionBase', - 'NodeBase', - 'RelationshipBase', - 'NodeInstanceBase', - 'RelationshipInstanceBase', - 'PluginBase', - 'TaskBase' -) - -#pylint: disable=no-self-argument, abstract-method - - -class BlueprintBase(ModelMixin): - """ - Blueprint model representation. - """ - __tablename__ = 'blueprints' - - created_at = Column(DateTime, nullable=False, index=True) - main_file_name = Column(Text, nullable=False) - plan = Column(Dict, nullable=False) - updated_at = Column(DateTime) - description = Column(Text) - - -class DeploymentBase(ModelMixin): - """ - Deployment model representation. - """ - __tablename__ = 'deployments' - - _private_fields = ['blueprint_fk'] - - created_at = Column(DateTime, nullable=False, index=True) - description = Column(Text) - inputs = Column(Dict) - groups = Column(Dict) - permalink = Column(Text) - policy_triggers = Column(Dict) - policy_types = Column(Dict) - outputs = Column(Dict) - scaling_groups = Column(Dict) - updated_at = Column(DateTime) - workflows = Column(Dict) - - @declared_attr - def blueprint_fk(cls): - return cls.foreign_key(BlueprintBase, nullable=False) - - @declared_attr - def blueprint(cls): - return cls.one_to_many_relationship('blueprint_fk') - - @declared_attr - def blueprint_name(cls): - return association_proxy('blueprint', cls.name_column_name()) - - -class ExecutionBase(ModelMixin): - """ - Execution model representation. - """ - # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. - __tablename__ = 'executions' - _private_fields = ['deployment_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 blueprint(cls): - return association_proxy('deployment', 'blueprint') - - @declared_attr - def deployment_fk(cls): - return cls.foreign_key(DeploymentBase, nullable=True) - - @declared_attr - def deployment(cls): - return cls.one_to_many_relationship('deployment_fk') - - @declared_attr - def deployment_name(cls): - return association_proxy('deployment', cls.name_column_name()) - - @declared_attr - def blueprint_name(cls): - return association_proxy('deployment', 'blueprint_name') - - def __str__(self): - return '<{0} id=`{1}` (status={2})>'.format( - self.__class__.__name__, - getattr(self, self.name_column_name()), - self.status - ) - - -class DeploymentUpdateBase(ModelMixin): - """ - Deployment update model representation. - """ - # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. - steps = None - - __tablename__ = 'deployment_updates' - - _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(ExecutionBase, nullable=True) - - @declared_attr - def execution(cls): - return cls.one_to_many_relationship('execution_fk') - - @declared_attr - def execution_name(cls): - return association_proxy('execution', cls.name_column_name()) - - @declared_attr - def deployment_fk(cls): - return cls.foreign_key(DeploymentBase) - - @declared_attr - def deployment(cls): - return cls.one_to_many_relationship('deployment_fk') - - @declared_attr - def deployment_name(cls): - return association_proxy('deployment', cls.name_column_name()) - - def to_dict(self, suppress_error=False, **kwargs): - dep_update_dict = super(DeploymentUpdateBase, 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 DeploymentUpdateStepBase(ModelMixin): - """ - Deployment update step model representation. - """ - # Needed only for pylint. the id will be populated by sqlalcehmy and the proper column. - __tablename__ = 'deployment_update_steps' - _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 deployment_update_fk(cls): - return cls.foreign_key(DeploymentUpdateBase) - - @declared_attr - def deployment_update(cls): - return cls.one_to_many_relationship('deployment_update_fk', 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 DeploymentModificationBase(ModelMixin): - """ - Deployment modification model representation. - """ - __tablename__ = 'deployment_modifications' - _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 deployment_fk(cls): - return cls.foreign_key(DeploymentBase) - - @declared_attr - def deployment(cls): - return cls.one_to_many_relationship('deployment_fk', backreference='modifications') - - @declared_attr - def deployment_name(cls): - return association_proxy('deployment', cls.name_column_name()) - - -class NodeBase(ModelMixin): - """ - Node model representation. - """ - __tablename__ = 'nodes' - - # See base class for an explanation on these properties - is_id_unique = False - - _private_fields = ['blueprint_fk', 'host_fk'] - - @declared_attr - def host_fk(cls): - return cls.foreign_key(NodeBase, nullable=True) - - @declared_attr - def host(cls): - return cls.relationship_to_self('host_fk') - - @declared_attr - def host_name(cls): - return association_proxy('host', cls.name_column_name()) - - @declared_attr - def deployment_fk(cls): - return cls.foreign_key(DeploymentBase) - - @declared_attr - def deployment(cls): - return cls.one_to_many_relationship('deployment_fk') - - @declared_attr - def deployment_name(cls): - return association_proxy('deployment', cls.name_column_name()) - - @declared_attr - def blueprint_name(cls): - return association_proxy('deployment', 'blueprint_{0}'.format(cls.name_column_name())) - - deploy_number_of_instances = Column(Integer, nullable=False) - max_number_of_instances = Column(Integer, nullable=False) - min_number_of_instances = Column(Integer, nullable=False) - number_of_instances = Column(Integer, nullable=False) - planned_number_of_instances = Column(Integer, nullable=False) - plugins = Column(List) - properties = Column(Dict) - operations = Column(Dict) - type = Column(Text, nullable=False, index=True) - type_hierarchy = Column(List) - - -class RelationshipBase(ModelMixin): - """ - Relationship model representation. - """ - __tablename__ = 'relationships' - - _private_fields = ['source_node_fk', 'target_node_fk', 'source_position', 'target_position'] - - source_position = Column(Integer) - target_position = Column(Integer) - - @declared_attr - def deployment_id(self): - return association_proxy('source_node', 'deployment_id') - - @declared_attr - def source_node_fk(cls): - return cls.foreign_key(NodeBase) - - @declared_attr - def source_node(cls): - return cls.one_to_many_relationship( - 'source_node_fk', - backreference='outbound_relationships', - backref_kwargs=dict( - order_by=cls.source_position, - collection_class=ordering_list('source_position', count_from=0) - ) - ) - - @declared_attr - def source_name(cls): - return association_proxy('source_node', cls.name_column_name()) - - @declared_attr - def target_node_fk(cls): - return cls.foreign_key(NodeBase, nullable=True) - - @declared_attr - def target_node(cls): - return cls.one_to_many_relationship( - 'target_node_fk', - backreference='inbound_relationships', - backref_kwargs=dict( - order_by=cls.target_position, - collection_class=ordering_list('target_position', count_from=0) - ) - ) - - @declared_attr - def target_name(cls): - return association_proxy('target_node', cls.name_column_name()) - - source_interfaces = Column(Dict) - source_operations = Column(Dict, nullable=False) - target_interfaces = Column(Dict) - target_operations = Column(Dict, nullable=False) - type = Column(String, nullable=False) - type_hierarchy = Column(List) - properties = Column(Dict) - - -class NodeInstanceBase(ModelMixin): - """ - Node instance model representation. - """ - __tablename__ = 'node_instances' - _private_fields = ['node_fk', 'host_fk'] - - runtime_properties = Column(Dict) - scaling_groups = Column(List) - state = Column(Text, nullable=False) - version = Column(Integer, default=1) - - @declared_attr - def host_fk(cls): - return cls.foreign_key(NodeInstanceBase, nullable=True) - - @declared_attr - def host(cls): - return cls.relationship_to_self('host_fk') - - @declared_attr - def host_name(cls): - return association_proxy('host', cls.name_column_name()) - - @declared_attr - def deployment(cls): - return association_proxy('node', 'deployment') - - @declared_attr - def deployment_name(cls): - return association_proxy('node', 'deployment_name') - - @declared_attr - def node_fk(cls): - return cls.foreign_key(NodeBase, nullable=True) - - @declared_attr - def node(cls): - return cls.one_to_many_relationship('node_fk') - - @declared_attr - def node_name(cls): - return association_proxy('node', cls.name_column_name()) - - @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 - - -class RelationshipInstanceBase(ModelMixin): - """ - Relationship instance model representation. - """ - __tablename__ = 'relationship_instances' - _private_fields = ['relationship_storage_fk', - 'source_node_instance_fk', - 'target_node_instance_fk', - 'source_position', - 'target_position'] - - source_position = Column(Integer) - target_position = Column(Integer) - - @declared_attr - def source_node_instance_fk(cls): - return cls.foreign_key(NodeInstanceBase, nullable=True) - - @declared_attr - def source_node_instance(cls): - return cls.one_to_many_relationship( - 'source_node_instance_fk', - backreference='outbound_relationship_instances', - backref_kwargs=dict( - order_by=cls.source_position, - collection_class=ordering_list('source_position', count_from=0) - ) - ) - - @declared_attr - def source_node_instance_name(cls): - return association_proxy('source_node_instance', 'node_{0}'.format(cls.name_column_name())) - - @declared_attr - def source_node_name(cls): - return association_proxy('source_node_instance', cls.name_column_name()) - - @declared_attr - def target_node_instance_fk(cls): - return cls.foreign_key(NodeInstanceBase, nullable=True) - - @declared_attr - def target_node_instance(cls): - return cls.one_to_many_relationship( - 'target_node_instance_fk', - backreference='inbound_relationship_instances', - backref_kwargs=dict( - order_by=cls.target_position, - collection_class=ordering_list('target_position', count_from=0) - ) - ) - - @declared_attr - def target_node_instance_name(cls): - return association_proxy('target_node_instance', cls.name_column_name()) - - @declared_attr - def target_node_name(cls): - return association_proxy('target_node_instance', 'node_{0}'.format(cls.name_column_name())) - - @declared_attr - def relationship_fk(cls): - return cls.foreign_key(RelationshipBase) - - @declared_attr - def relationship(cls): - return cls.one_to_many_relationship('relationship_fk') - - @declared_attr - def relationship_name(cls): - return association_proxy('relationship', cls.name_column_name()) - - - -class PluginBase(ModelMixin): - """ - Plugin model representation. - """ - __tablename__ = 'plugins' - - 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__ = 'tasks' - _private_fields = ['node_instance_fk', 'relationship_instance_fk', 'execution_fk'] - - @declared_attr - def node_instance_fk(cls): - return cls.foreign_key(NodeInstanceBase, nullable=True) - - @declared_attr - def node_instance_name(cls): - return association_proxy('node_instance', cls.name_column_name()) - - @declared_attr - def node_instance(cls): - return cls.one_to_many_relationship('node_instance_fk') - - @declared_attr - def relationship_instance_fk(cls): - return cls.foreign_key(RelationshipInstanceBase, nullable=True) - - @declared_attr - def relationship_instance_name(cls): - return association_proxy('relationship_instance', cls.name_column_name()) - - @declared_attr - def relationship_instance(cls): - return cls.one_to_many_relationship('relationship_instance_fk') - - @declared_attr - def plugin_fk(cls): - return cls.foreign_key(PluginBase, nullable=True) - - @declared_attr - def plugin(cls): - return cls.one_to_many_relationship('plugin_fk') - - @declared_attr - def execution_fk(cls): - return cls.foreign_key(ExecutionBase, nullable=True) - - @declared_attr - def execution(cls): - return cls.one_to_many_relationship('execution_fk') - - @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 - operation_mapping = Column(String) - inputs = Column(Dict) - plugin_name = Column(String) - _runs_on = Column(Enum(*RUNS_ON, name='runs_on'), name='runs_on') - - @property - def actor(self): - """ - Return the actor of the task - :return: - """ - return self.node_instance or self.relationship_instance - - @property - def runs_on(self): - if self._runs_on == self.RUNS_ON_NODE_INSTANCE: - return self.node_instance - elif self._runs_on == self.RUNS_ON_SOURCE: - return self.relationship_instance.source_node_instance # pylint: disable=no-member - elif self._runs_on == self.RUNS_ON_TARGET: - return self.relationship_instance.target_node_instance # pylint: disable=no-member - return None - - @classmethod - def as_node_instance(cls, instance, runs_on, **kwargs): - return cls(node_instance=instance, _runs_on=runs_on, **kwargs) - - @classmethod - def as_relationship_instance(cls, instance, runs_on, **kwargs): - return cls(relationship_instance=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/core.py ---------------------------------------------------------------------- diff --git a/aria/storage/core.py b/aria/storage/core.py index 3878dca..0e189e6 100644 --- a/aria/storage/core.py +++ b/aria/storage/core.py @@ -39,10 +39,7 @@ API: """ from aria.logger import LoggerMixin -from . import ( - api as storage_api, - sql_mapi -) +from . import sql_mapi __all__ = ( 'Storage', @@ -146,7 +143,7 @@ class ModelStorage(Storage): :param model_cls: the model to register. :return: """ - model_name = storage_api.generate_lower_name(model_cls) + model_name = model_cls.__modelname__ if model_name in self.registered: self.logger.debug('{name} in already storage {self!r}'.format(name=model_name, self=self)) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/aria/storage/instrumentation.py ---------------------------------------------------------------------- diff --git a/aria/storage/instrumentation.py b/aria/storage/instrumentation.py index 537dbb5..57fe9bd 100644 --- a/aria/storage/instrumentation.py +++ b/aria/storage/instrumentation.py @@ -17,12 +17,11 @@ import copy import sqlalchemy.event -from . import api -from . import model as _model +from .modeling import model as _model _STUB = object() _INSTRUMENTED = { - _model.NodeInstance.runtime_properties: dict + _model.Node.runtime_properties: dict } @@ -75,7 +74,7 @@ class _Instrumentation(object): def _register_set_attribute_listener(self, instrumented_attribute, attribute_type): def listener(target, value, *_): - mapi_name = self._mapi_name(target.__class__) + mapi_name = target.__modelname__ tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) tracked_attributes = tracked_instances.setdefault(target.id, {}) if value is None: @@ -90,7 +89,7 @@ class _Instrumentation(object): def _register_instance_listeners(self, instrumented_class, instrumented_attributes): def listener(target, *_): - mapi_name = self._mapi_name(instrumented_class) + mapi_name = instrumented_class.__modelname__ tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) tracked_attributes = tracked_instances.setdefault(target.id, {}) for attribute_name, attribute_type in instrumented_attributes.items(): @@ -110,7 +109,7 @@ class _Instrumentation(object): def clear(self, target=None): if target: - mapi_name = self._mapi_name(target.__class__) + mapi_name = target.__modelname__ tracked_instances = self.tracked_changes.setdefault(mapi_name, {}) tracked_instances.pop(target.id, None) else: @@ -128,10 +127,6 @@ class _Instrumentation(object): def __exit__(self, exc_type, exc_val, exc_tb): self.restore() - @staticmethod - def _mapi_name(instrumented_class): - return api.generate_lower_name(instrumented_class) - class _Value(object): # You may wonder why is this a full blown class and not a named tuple. The reason is that http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/aria/storage/model.py ---------------------------------------------------------------------- diff --git a/aria/storage/model.py b/aria/storage/model.py deleted file mode 100644 index afca3e4..0000000 --- a/aria/storage/model.py +++ /dev/null @@ -1,110 +0,0 @@ -# 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. - * ProviderContext - provider context implementation model. - * Plugin - plugin implementation model. -""" -from sqlalchemy.ext.declarative import declarative_base - -from . import structure -from . import base_model as base - -__all__ = ( - 'Blueprint', - 'Deployment', - 'DeploymentUpdateStep', - 'DeploymentUpdate', - 'DeploymentModification', - 'Execution', - 'Node', - 'Relationship', - 'NodeInstance', - 'RelationshipInstance', - 'Plugin', -) - - -#pylint: disable=abstract-method -# The required abstract method implementation are implemented in the ModelIDMixin, which is used as -# a base to the DeclerativeBase. -DeclarativeBase = declarative_base(cls=structure.ModelIDMixin) - - -class Blueprint(DeclarativeBase, base.BlueprintBase): - pass - - -class Deployment(DeclarativeBase, base.DeploymentBase): - pass - - -class Execution(DeclarativeBase, base.ExecutionBase): - pass - - -class DeploymentUpdate(DeclarativeBase, base.DeploymentUpdateBase): - pass - - -class DeploymentUpdateStep(DeclarativeBase, base.DeploymentUpdateStepBase): - pass - - -class DeploymentModification(DeclarativeBase, base.DeploymentModificationBase): - pass - - -class Node(DeclarativeBase, base.NodeBase): - pass - - -class Relationship(DeclarativeBase, base.RelationshipBase): - pass - - -class NodeInstance(DeclarativeBase, base.NodeInstanceBase): - pass - - -class RelationshipInstance(DeclarativeBase, base.RelationshipInstanceBase): - pass - - -class Plugin(DeclarativeBase, base.PluginBase): - pass - - -class Task(DeclarativeBase, base.TaskBase): - pass http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/aria/storage/modeling/__init__.py ---------------------------------------------------------------------- diff --git a/aria/storage/modeling/__init__.py b/aria/storage/modeling/__init__.py new file mode 100644 index 0000000..697ed09 --- /dev/null +++ b/aria/storage/modeling/__init__.py @@ -0,0 +1,35 @@ +# 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 collections import namedtuple + +from . import ( + model, + instance_elements as _instance_base, + orchestrator_elements as _orchestrator_base, + template_elements as _template_base, +) + +_ModelBaseCls = namedtuple('ModelBase', 'instance_elements,' + 'orchestrator_elements,' + 'template_elements') +model_base = _ModelBaseCls(instance_elements=_instance_base, + orchestrator_elements=_orchestrator_base, + template_elements=_template_base) + +__all__ = ( + 'model', + 'model_base', +) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/b6193359/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..8c720b9 --- /dev/null +++ b/aria/storage/modeling/elements.py @@ -0,0 +1,106 @@ +# 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 exceptions + +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 + str_value = Column(Text) + description = Column(Text) + + @property + def as_raw(self): + return OrderedDict(( + ('name', self.name), + ('type_name', self.type), + ('value', self.value), + ('description', self.description))) + + @property + def value(self): + if self.type is None: + return + try: + if self.type.lower() in ['str', 'unicode']: + return self.str_value.decode('utf-8') + elif self.type.lower() == 'int': + return int(self.str_value) + elif self.type.lower() == 'bool': + return bool(self.str_value) + elif self.type.lower() == 'float': + return float(self.str_value) + else: + raise exceptions.StorageError('No supported type_name was provided') + except ValueError: + raise exceptions.StorageError('Trying to cast {0} to {1} failed'.format(self.str_value, + self.type)) + + def instantiate(self, context, container): + return ParameterBase(self.type, self.str_value, self.description) + + def coerce_values(self, context, container, report_issues): + if self.str_value is not None: + self.str_value = utils.coerce_value(context, container, self.str_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/b6193359/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..0666c8a --- /dev/null +++ b/aria/storage/modeling/instance_elements.py @@ -0,0 +1,1286 @@ +# 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' + + __private_fields__ = ['substituion_fk', + 'service_template_fk'] + + 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' + + __private_fields__ = ['service_template_fk', + 'interface_instance_fk'] + + # 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 interface(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' + + __private_fields__ = ['group_fk', + 'node_fk', + 'relationship_fk'] + + + # 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) + edge = 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 + + # 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' + + __private_fields__ = ['node_fk'] + + # 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' + + __private_fields__ = ['node_fk'] + + # 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' + + __private_fields__ = ['service_instance_fk'] + + # 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' + + __private_fields__ = ['group_fk'] + + # 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' + + __private_fields__ = ['group_policy_fk'] + + # 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:`relationships`: List of :class:`Relationship` + """ + __tablename__ = 'node' + + __private_fields__ = ['service_instance_fk', + 'host_fk', + 'node_template_fk'] + + # 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') + + @declared_attr + def service_instance_name(cls): + return association_proxy('service_instance', 'name') + + @property + def ip(self): + if not self.host_fk: + return None + host_node = self.host + if 'ip' in host_node.runtime_properties: # pylint: disable=no-member + return host_node.runtime_properties['ip'] # pylint: disable=no-member + host_node = host_node.node_template # pylint: disable=no-member + host_ip_property = [prop for prop in host_node.properties if prop.name == 'ip'] + if host_ip_property: + return host_ip_property[0].value + 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' + + __private_fields__ = ['service_instance_fk'] + + # 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' + + __private_fields__ = ['source_node_fk', + 'target_node_fk'] + + 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), + ) + ) + + @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), + ) + ) + + @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') + + # endregion + + @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