Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 35D79200C49 for ; Fri, 17 Mar 2017 20:38:46 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id 345F6160B70; Fri, 17 Mar 2017 19:38:46 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 44F16160B92 for ; Fri, 17 Mar 2017 20:38:44 +0100 (CET) Received: (qmail 44762 invoked by uid 500); 17 Mar 2017 19:38:43 -0000 Mailing-List: contact dev-help@ariatosca.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@ariatosca.incubator.apache.org Delivered-To: mailing list dev@ariatosca.incubator.apache.org Received: (qmail 44751 invoked by uid 99); 17 Mar 2017 19:38:43 -0000 Received: from pnap-us-west-generic-nat.apache.org (HELO spamd4-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 17 Mar 2017 19:38:43 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd4-us-west.apache.org (ASF Mail Server at spamd4-us-west.apache.org) with ESMTP id 0463FC0118 for ; Fri, 17 Mar 2017 19:38:43 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd4-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: -4.369 X-Spam-Level: X-Spam-Status: No, score=-4.369 tagged_above=-999 required=6.31 tests=[RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-0.001, SPF_NEUTRAL=0.652] autolearn=disabled Received: from mx1-lw-eu.apache.org ([10.40.0.8]) by localhost (spamd4-us-west.apache.org [10.40.0.11]) (amavisd-new, port 10024) with ESMTP id Ni2m2_moERhD for ; Fri, 17 Mar 2017 19:38:34 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-lw-eu.apache.org (ASF Mail Server at mx1-lw-eu.apache.org) with SMTP id 0755360D3E for ; Fri, 17 Mar 2017 19:38:30 +0000 (UTC) Received: (qmail 44068 invoked by uid 99); 17 Mar 2017 19:38:30 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 17 Mar 2017 19:38:30 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 21D0CDFFC2; Fri, 17 Mar 2017 19:38:30 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: emblemparade@apache.org To: dev@ariatosca.incubator.apache.org Date: Fri, 17 Mar 2017 19:38:45 -0000 Message-Id: <2c14c437613340e3997cfefb922d83da@git.apache.org> In-Reply-To: <93d68066e1b845dcad74089224218bb4@git.apache.org> References: <93d68066e1b845dcad74089224218bb4@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [17/18] incubator-ariatosca git commit: ARIA-105 Integrate parser and orchestrator models archived-at: Fri, 17 Mar 2017 19:38:46 -0000 http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/09f826a1/aria/modeling/service_instance.py ---------------------------------------------------------------------- diff --git a/aria/modeling/service_instance.py b/aria/modeling/service_instance.py new file mode 100644 index 0000000..5c2cf54 --- /dev/null +++ b/aria/modeling/service_instance.py @@ -0,0 +1,1553 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=too-many-lines, no-self-argument, no-member, abstract-method + +from sqlalchemy import ( + Column, + Text, + Integer +) +from sqlalchemy import DateTime +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.ext.declarative import declared_attr + +from .mixins import InstanceModelMixin +from ..parser import validation +from ..parser.consumption import ConsumptionContext +from ..utils import collections, formatting, console +from . import ( + relationships, + utils, + types as modeling_types +) + + +class ServiceBase(InstanceModelMixin): # pylint: disable=too-many-public-methods + """ + A service is usually an instance of a :class:`ServiceTemplate`. + + You will usually not create it programmatically, but instead instantiate it from a service + template. + + :ivar name: Name (unique for this ARIA installation) + :vartype name: basestring + :ivar service_template: Template from which this service was instantiated (optional) + :vartype service_template: :class:`ServiceTemplate` + :ivar description: Human-readable description + :vartype description: string + :ivar meta_data: Custom annotations + :vartype meta_data: {basestring: :class:`Metadata`} + :ivar node: Nodes + :vartype node: {basestring: :class:`Node`} + :ivar groups: Groups of nodes + :vartype groups: {basestring: :class:`Group`} + :ivar policies: Policies + :vartype policies: {basestring: :class:`Policy`]} + :ivar substitution: The entire service can appear as a node + :vartype substitution: :class:`Substitution` + :ivar inputs: Externally provided parameters + :vartype inputs: {basestring: :class:`Parameter`} + :ivar outputs: These parameters are filled in after service installation + :vartype outputs: {basestring: :class:`Parameter`} + :ivar workflows: Custom workflows that can be performed on the service + :vartype workflows: {basestring: :class:`Operation`} + :ivar plugin_specifications: Plugins required to be installed + :vartype plugin_specifications: {basestring: :class:`PluginSpecification`} + :ivar created_at: Creation timestamp + :vartype created_at: :class:`datetime.datetime` + :ivar updated_at: Update timestamp + :vartype updated_at: :class:`datetime.datetime` + + :ivar permalink: ?? + :vartype permalink: basestring + :ivar scaling_groups: ?? + :vartype scaling_groups: {} + + :ivar modifications: Modifications of this service + :vartype modifications: [:class:`ServiceModification`] + :ivar updates: Updates of this service + :vartype updates: [:class:`ServiceUpdate`] + :ivar executions: Executions on this service + :vartype executions: [:class:`Execution`] + """ + + __tablename__ = 'service' + + @declared_attr + def service_template(cls): + return relationships.many_to_one(cls, 'service_template') + + description = Column(Text) + + @declared_attr + def meta_data(cls): + # Warning! We cannot use the attr name "metadata" because it's used by SQLAlchemy! + return relationships.many_to_many(cls, 'metadata', dict_key='name') + + @declared_attr + def nodes(cls): + return relationships.one_to_many(cls, 'node', dict_key='name') + + @declared_attr + def groups(cls): + return relationships.one_to_many(cls, 'group', dict_key='name') + + @declared_attr + def policies(cls): + return relationships.one_to_many(cls, 'policy', dict_key='name') + + @declared_attr + def substitution(cls): + return relationships.one_to_one(cls, 'substitution') + + @declared_attr + def inputs(cls): + return relationships.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + @declared_attr + def outputs(cls): + return relationships.many_to_many(cls, 'parameter', prefix='outputs', dict_key='name') + + @declared_attr + def workflows(cls): + return relationships.one_to_many(cls, 'operation', dict_key='name') + + @declared_attr + def plugin_specifications(cls): + return relationships.many_to_many(cls, 'plugin_specification') + + created_at = Column(DateTime, nullable=False, index=True) + updated_at = Column(DateTime) + + # region orchestration + + permalink = Column(Text) + scaling_groups = Column(modeling_types.Dict) + + # endregion + + # region foreign keys + + @declared_attr + def substitution_fk(cls): + """Service one-to-one to Substitution""" + return relationships.fk('substitution', nullable=True) + + @declared_attr + def service_template_fk(cls): + """For Service many-to-one to ServiceTemplate""" + return relationships.fk('service_template', nullable=True) + + # endregion + + # region association proxies + + @declared_attr + def service_template_name(cls): + """Required for use by SQLAlchemy queries""" + return association_proxy('service_template', 'name') + + # endregion + + def satisfy_requirements(self): + satisfied = True + for node in self.nodes.itervalues(): + if not node.satisfy_requirements(): + satisfied = False + return satisfied + + def validate_capabilities(self): + satisfied = True + for node in self.nodes.itervalues(): + if not node.validate_capabilities(): + satisfied = False + return satisfied + + def is_node_a_target(self, target_node): + for node in self.nodes.itervalues(): + if self._is_node_a_target(node, target_node): + return True + return False + + def _is_node_a_target(self, source_node, target_node): + if source_node.outbound_relationships: + for relationship in source_node.outbound_relationships: + if relationship.target_node.name == target_node.name: + return True + else: + node = relationship.target_node + if node is not None: + if self._is_node_a_target(node, target_node): + return True + return False + + @property + def as_raw(self): + return collections.OrderedDict(( + ('description', self.description), + ('metadata', formatting.as_raw_dict(self.meta_data)), + ('nodes', formatting.as_raw_list(self.nodes)), + ('groups', formatting.as_raw_list(self.groups)), + ('policies', formatting.as_raw_list(self.policies)), + ('substitution', formatting.as_raw(self.substitution)), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('outputs', formatting.as_raw_dict(self.outputs)), + ('workflows', formatting.as_raw_list(self.workflows)))) + + def validate(self): + utils.validate_dict_values(self.meta_data) + utils.validate_dict_values(self.nodes) + utils.validate_dict_values(self.groups) + utils.validate_dict_values(self.policies) + if self.substitution is not None: + self.substitution.validate() + utils.validate_dict_values(self.inputs) + utils.validate_dict_values(self.outputs) + utils.validate_dict_values(self.workflows) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.meta_data, report_issues) + utils.coerce_dict_values(container, self.nodes, report_issues) + utils.coerce_dict_values(container, self.groups, report_issues) + utils.coerce_dict_values(container, self.policies, report_issues) + if self.substitution is not None: + self.substitution.coerce_values(container, report_issues) + utils.coerce_dict_values(container, self.inputs, report_issues) + utils.coerce_dict_values(container, self.outputs, report_issues) + utils.coerce_dict_values(container, self.workflows, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + if self.description is not None: + console.puts(context.style.meta(self.description)) + utils.dump_dict_values(self.meta_data, 'Metadata') + for node in self.nodes.itervalues(): + node.dump() + for group in self.groups.itervalues(): + group.dump() + for policy in self.policies.itervalues(): + policy.dump() + if self.substitution is not None: + self.substitution.dump() + utils.dump_dict_values(self.inputs, 'Inputs') + utils.dump_dict_values(self.outputs, 'Outputs') + utils.dump_dict_values(self.workflows, 'Workflows') + + def dump_graph(self): + for node in self.nodes.itervalues(): + if not self.is_node_a_target(node): + self._dump_graph_node(node) + + def _dump_graph_node(self, node): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(node.name)) + if node.outbound_relationships: + with context.style.indent: + for relationship in node.outbound_relationships: + if relationship.relationship_template is not None: + relationship_name = context.style.node( + relationship.relationship_template.name) + elif relationship.type is not None: + relationship_name = context.style.type(relationship.type.name) + else: + relationship_name = '?' + if relationship.target_capability is not None: + capability_name = context.style.node(relationship.target_capability.name) + else: + capability_name = None + if capability_name is not None: + console.puts('-> {0} {1}'.format(relationship_name, capability_name)) + else: + console.puts('-> {0}'.format(relationship_name)) + target_node = relationship.target_node + with console.indent(3): + self._dump_graph_node(target_node) + + __private_fields__ = ['substitution_fk', + 'service_template_fk', + 'service_template_name'] + + +class NodeBase(InstanceModelMixin): # pylint: disable=too-many-public-methods + """ + Usually an instance of a :class:`NodeTemplate`. + + Nodes may have zero or more :class:`Relationship` instances to other nodes. + + :ivar name: Name (unique for this service) + :vartype name: basestring + :ivar node_template: Template from which this node was instantiated (optional) + :vartype node_template: :class:`NodeTemplate` + :ivar type: Node type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: string + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interfaces: Bundles of operations + :vartype interfaces: {basestring: :class:`Interface`} + :ivar artifacts: Associated files + :vartype artifacts: {basestring: :class:`Artifact`} + :ivar capabilities: Exposed capabilities + :vartype capabilities: {basestring: :class:`Capability`} + :ivar outbound_relationships: Relationships to other nodes + :vartype outbound_relationships: [:class:`Relationship`] + :ivar inbound_relationships: Relationships from other nodes + :vartype inbound_relationships: [:class:`Relationship`] + :ivar plugin_specifications: Plugins required to be installed on the node's host + :vartype plugin_specifications: {basestring: :class:`PluginSpecification`} + :ivar host: Host node (can be self) + :vartype host: :class:`Node` + + :ivar runtime_properties: TODO: should be replaced with attributes + :vartype runtime_properties: {} + :ivar scaling_groups: ?? + :vartype scaling_groups: [] + :ivar state: ?? + :vartype state: basestring + :ivar version: Used by `aria.storage.instrumentation` + :vartype version: int + + :ivar service: Containing service + :vartype service: :class:`Service` + :ivar groups: We are a member of these groups + :vartype groups: [:class:`Group`] + :ivar policies: Policies enacted on this node + :vartype policies: [:class:`Policy`] + :ivar substitution_mapping: Our contribution to service substitution + :vartype substitution_mapping: :class:`SubstitutionMapping` + :ivar tasks: Tasks on this node + :vartype tasks: [:class:`Task`] + """ + + __tablename__ = 'node' + + @declared_attr + def node_template(cls): + return relationships.many_to_one(cls, 'node_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interfaces(cls): + return relationships.one_to_many(cls, 'interface', dict_key='name') + + @declared_attr + def artifacts(cls): + return relationships.one_to_many(cls, 'artifact', dict_key='name') + + @declared_attr + def capabilities(cls): + return relationships.one_to_many(cls, 'capability', dict_key='name') + + @declared_attr + def outbound_relationships(cls): + return relationships.one_to_many(cls, 'relationship', child_fk='source_node_fk', + child_property='source_node') + + @declared_attr + def inbound_relationships(cls): + return relationships.one_to_many(cls, 'relationship', child_fk='target_node_fk', + child_property='target_node') + + @declared_attr + def plugin_specifications(cls): + return relationships.many_to_many(cls, 'plugin_specification', dict_key='name') + + @declared_attr + def host(cls): + return relationships.one_to_one_self(cls, 'host_fk') + + # region orchestration + + runtime_properties = Column(modeling_types.Dict) + scaling_groups = Column(modeling_types.List) + state = Column(Text, nullable=False) + version = Column(Integer, default=1) + + __mapper_args__ = {'version_id_col': version} # Enable SQLAlchemy automatic version counting + + @property + def ip(self): + # TODO: totally broken + 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 = host_node.properties.get('ip') + if host_ip_property: + return host_ip_property.value + return None + + # endregion + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Node many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def host_fk(cls): + """For Node one-to-one to Node""" + return relationships.fk('node', nullable=True) + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Node""" + return relationships.fk('service') + + @declared_attr + def node_template_fk(cls): + """For Node many-to-one to NodeTemplate""" + return relationships.fk('node_template', nullable=True) + + # endregion + + # region association proxies + + @declared_attr + def service_name(cls): + """Required for use by SQLAlchemy queries""" + return association_proxy('service', 'name') + + # endregion + + def satisfy_requirements(self): + node_template = self.node_template + satisfied = True + for requirement_template in node_template.requirement_templates: + # Find target template + target_node_template, target_node_capability = \ + requirement_template.find_target(node_template) + if target_node_template is not None: + satisfied = self._satisfy_capability(target_node_capability, + target_node_template, + requirement_template) + else: + context = ConsumptionContext.get_thread_local() + context.validation.report('requirement "{0}" of node "{1}" has no target node ' + 'template'.format(requirement_template.name, self.name), + level=validation.Issue.BETWEEN_INSTANCES) + satisfied = False + return satisfied + + def _satisfy_capability(self, target_node_capability, target_node_template, + requirement_template): + from . import models + context = ConsumptionContext.get_thread_local() + # Find target nodes + target_nodes = target_node_template.nodes.all() + 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: + if requirement_template.relationship_template is not None: + relationship = \ + requirement_template.relationship_template.instantiate(self) + else: + relationship = models.Relationship(target_capability=target_capability) + relationship.name = requirement_template.name + relationship.requirement_template = requirement_template + relationship.target_node = target_node + self.outbound_relationships.append(relationship) + return True + else: + context.validation.report('requirement "{0}" of node "{1}" targets node ' + 'template "{2}" but its instantiated nodes do not ' + 'have enough capacity'.format( + requirement_template.name, + self.name, + target_node_template.name), + level=validation.Issue.BETWEEN_INSTANCES) + return False + else: + context.validation.report('requirement "{0}" of node "{1}" targets node template ' + '"{2}" but it has no instantiated nodes'.format( + requirement_template.name, + self.name, + target_node_template.name), + level=validation.Issue.BETWEEN_INSTANCES) + return False + + def validate_capabilities(self): + context = ConsumptionContext.get_thread_local() + satisfied = False + for capability in self.capabilities.itervalues(): + if not capability.has_enough_relationships: + context.validation.report('capability "{0}" of node "{1}" requires at least {2:d} ' + 'relationships but has {3:d}'.format( + capability.name, + self.name, + capability.min_occurrences, + capability.occurrences), + level=validation.Issue.BETWEEN_INSTANCES) + satisfied = False + return satisfied + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('type_name', self.type.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.outbound_relationships)))) + + def validate(self): + context = ConsumptionContext.get_thread_local() + if len(self.name) > context.modeling.id_max_length: + context.validation.report('"{0}" has an ID longer than the limit of {1:d} characters: ' + '{2:d}'.format( + self.name, + context.modeling.id_max_length, + len(self.name)), + level=validation.Issue.BETWEEN_INSTANCES) + + # TODO: validate that node template is of type? + + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interfaces) + utils.validate_dict_values(self.artifacts) + utils.validate_dict_values(self.capabilities) + utils.validate_list_values(self.outbound_relationships) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(self, self.properties, report_issues) + utils.coerce_dict_values(self, self.interfaces, report_issues) + utils.coerce_dict_values(self, self.artifacts, report_issues) + utils.coerce_dict_values(self, self.capabilities, report_issues) + utils.coerce_list_values(self, self.outbound_relationships, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Node: {0}'.format(context.style.node(self.name))) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + console.puts('Template: {0}'.format(context.style.node(self.node_template.name))) + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interfaces) + utils.dump_dict_values(self.artifacts, 'Artifacts') + utils.dump_dict_values(self.capabilities, 'Capabilities') + utils.dump_list_values(self.outbound_relationships, 'Relationships') + + __private_fields__ = ['type_fk', + 'host_fk', + 'service_fk', + 'node_template_fk', + 'service_name'] + +class GroupBase(InstanceModelMixin): + """ + Usually an instance of a :class:`GroupTemplate`. + + :ivar name: Name (unique for this service) + :vartype name: basestring + :ivar group_template: Template from which this group was instantiated (optional) + :vartype group_template: :class:`GroupTemplate` + :ivar type: Group type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: string + :ivar nodes: Members of this group + :vartype nodes: [:class:`Node`] + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interfaces: Bundles of operations + :vartype interfaces: {basestring: :class:`Interface`} + + :ivar service: Containing service + :vartype service: :class:`Service` + :ivar policies: Policies enacted on this group + :vartype policies: [:class:`Policy`] + """ + + __tablename__ = 'group' + + @declared_attr + def group_template(cls): + return relationships.many_to_one(cls, 'group_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def nodes(cls): + return relationships.many_to_many(cls, 'node') + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interfaces(cls): + return relationships.one_to_many(cls, 'interface', dict_key='name') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Group many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Group""" + return relationships.fk('service') + + @declared_attr + def group_template_fk(cls): + """For Group many-to-one to GroupTemplate""" + return relationships.fk('group_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)))) + + def validate(self): + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interfaces) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + utils.coerce_dict_values(container, self.interfaces, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Group: {0}'.format(context.style.node(self.name))) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interfaces) + if self.nodes: + console.puts('Member nodes:') + with context.style.indent: + for node in self.nodes: + console.puts(context.style.node(node.name)) + + __private_fields__ = ['type_fk', + 'service_fk', + 'group_template_fk'] + + +class PolicyBase(InstanceModelMixin): + """ + Usually an instance of a :class:`PolicyTemplate`. + + :ivar name: Name (unique for this service) + :vartype name: basestring + :ivar policy_template: Template from which this policy was instantiated (optional) + :vartype policy_template: :class:`PolicyTemplate` + :ivar type: Policy type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: string + :ivar nodes: Policy will be enacted on all these nodes + :vartype nodes: [:class:`Node`] + :ivar groups: Policy will be enacted on all nodes in these groups + :vartype groups: [:class:`Group`] + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar service: Containing service + :vartype service: :class:`Service` + """ + + __tablename__ = 'policy' + + @declared_attr + def policy_template(cls): + return relationships.many_to_one(cls, 'policy_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def nodes(cls): + return relationships.many_to_many(cls, 'node') + + @declared_attr + def groups(cls): + return relationships.many_to_many(cls, 'group') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Policy many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Policy""" + return relationships.fk('service') + + @declared_attr + def policy_template_fk(cls): + """For Policy many-to-one to PolicyTemplate""" + return relationships.fk('policy_template', nullable=True) + + # endregion + + @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): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Policy: {0}'.format(context.style.node(self.name))) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.properties, 'Properties') + if self.nodes: + console.puts('Target nodes:') + with context.style.indent: + for node in self.nodes: + console.puts(context.style.node(node.name)) + if self.groups: + console.puts('Target groups:') + with context.style.indent: + for group in self.groups: + console.puts(context.style.node(group.name)) + + __private_fields__ = ['type_fk', + 'service_fk', + 'policy_template_fk'] + + +class SubstitutionBase(InstanceModelMixin): + """ + Used to substitute a single node for the entire deployment. + + Usually an instance of a :class:`SubstitutionTemplate`. + + :ivar substitution_template: Template from which this substitution was instantiated (optional) + :vartype substitution_template: :class:`SubstitutionTemplate` + :ivar node_type: Exposed node type + :vartype node_type: :class:`Type` + :ivar mappings: Requirement and capability mappings + :vartype mappings: {basestring: :class:`SubstitutionTemplate`} + + :ivar service: Containing service + :vartype service: :class:`Service` + """ + + __tablename__ = 'substitution' + + @declared_attr + def substitution_template(cls): + return relationships.many_to_one(cls, 'substitution_template') + + @declared_attr + def node_type(cls): + return relationships.many_to_one(cls, 'type') + + @declared_attr + def mappings(cls): + return relationships.one_to_many(cls, 'substitution_mapping', dict_key='name') + + # region foreign_keys + + @declared_attr + def node_type_fk(cls): + """For Substitution many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def substitution_template_fk(cls): + """For Substitution many-to-one to SubstitutionTemplate""" + return relationships.fk('substitution_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('node_type_name', self.node_type_name), + ('mappings', formatting.as_raw_dict(self.mappings)))) + + def validate(self): + utils.validate_dict_values(self.mappings) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.mappings, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('Substitution:') + with context.style.indent: + console.puts('Node type: {0}'.format(context.style.type(self.node_type.name))) + utils.dump_dict_values(self.mappings, 'Mappings') + + __private_fields__ = ['node_type_fk', + 'substitution_template_fk'] + + +class SubstitutionMappingBase(InstanceModelMixin): + """ + Used by :class:`Substitution` to map a capability or a requirement to a node. + + Only one of `capability_template` and `requirement_template` can be set. + + Usually an instance of a :class:`SubstitutionTemplate`. + + :ivar name: Exposed capability or requirement name + :vartype name: basestring + :ivar node: Node + :vartype node: :class:`Node` + :ivar capability: Capability in the node + :vartype capability: :class:`Capability` + :ivar requirement_template: Requirement template in the node template + :vartype requirement_template: :class:`RequirementTemplate` + + :ivar substitution: Containing substitution + :vartype substitution: :class:`Substitution` + """ + + __tablename__ = 'substitution_mapping' + + @declared_attr + def node(cls): + return relationships.one_to_one(cls, 'node') + + @declared_attr + def capability(cls): + return relationships.one_to_one(cls, 'capability') + + @declared_attr + def requirement_template(cls): + return relationships.one_to_one(cls, 'requirement_template') + + # region foreign keys + + @declared_attr + def substitution_fk(cls): + """For Substitution one-to-many to SubstitutionMapping""" + return relationships.fk('substitution') + + @declared_attr + def node_fk(cls): + """For Substitution one-to-one to NodeTemplate""" + return relationships.fk('node') + + @declared_attr + def capability_fk(cls): + """For Substitution one-to-one to Capability""" + return relationships.fk('capability', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For Substitution one-to-one to RequirementTemplate""" + return relationships.fk('requirement_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name))) + + def validate(self): + context = ConsumptionContext.get_thread_local() + if (self.capability is None) and (self.requirement_template is None): + context.validation.report('mapping "{0}" refers to neither capability nor a requirement' + ' in node: {1}'.format( + self.name, + formatting.safe_repr(self.node.name)), + level=validation.Issue.BETWEEN_TYPES) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts('{0} -> {1}.{2}'.format( + context.style.node(self.name), + context.style.node(self.node.name), + context.style.node(self.capability.name + if self.capability + else self.requirement_template.name))) + + __private_fields__ = ['substitution_fk', + 'node_fk', + 'capability_fk', + 'requirement_template_fk'] + + +class RelationshipBase(InstanceModelMixin): + """ + Connects :class:`Node` to a capability in another node. + + Might be an instance of a :class:`RelationshipTemplate`. + + :ivar name: Name (usually the name of the requirement at the source node template) + :vartype name: basestring + :ivar relationship_template: Template from which this relationship was instantiated (optional) + :vartype relationship_template: :class:`RelationshipTemplate` + :ivar requirement_template: Template from which this relationship was instantiated (optional) + :vartype requirement_template: :class:`RequirementTemplate` + :ivar type: Relationship type + :vartype type: :class:`Type` + :ivar target_capability: Capability at the target node (optional) + :vartype target_capability: :class:`Capability` + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + :ivar interfaces: Bundles of operations + :vartype interfaces: {basestring: :class:`Interfaces`} + + :ivar source_position: ?? + :vartype source_position: int + :ivar target_position: ?? + :vartype target_position: int + + :ivar source_node: Source node + :vartype source_node: :class:`Node` + :ivar target_node: Target node + :vartype target_node: :class:`Node` + :ivar tasks: Tasks on this node + :vartype tasks: [:class:`Task`] + """ + + __tablename__ = 'relationship' + + @declared_attr + def relationship_template(cls): + return relationships.many_to_one(cls, 'relationship_template') + + @declared_attr + def requirement_template(cls): + return relationships.many_to_one(cls, 'requirement_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + @declared_attr + def target_capability(cls): + return relationships.one_to_one(cls, 'capability') + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + @declared_attr + def interfaces(cls): + return relationships.one_to_many(cls, 'interface', dict_key='name') + + # region orchestration + + source_position = Column(Integer) # ??? + target_position = Column(Integer) # ??? + + # endregion + + # region foreign keys + + @declared_attr + def type_fk(cls): + """For Relationship many-to-one to Type""" + return relationships.fk('type', nullable=True) + + @declared_attr + def source_node_fk(cls): + """For Node one-to-many to Relationship""" + return relationships.fk('node') + + @declared_attr + def target_node_fk(cls): + """For Node one-to-many to Relationship""" + return relationships.fk('node') + + @declared_attr + def target_capability_fk(cls): + """For Relationship one-to-one to Capability""" + return relationships.fk('capability', nullable=True) + + @declared_attr + def requirement_template_fk(cls): + """For Relationship many-to-one to RequirementTemplate""" + return relationships.fk('requirement_template', nullable=True) + + @declared_attr + def relationship_template_fk(cls): + """For Relationship many-to-one to RelationshipTemplate""" + return relationships.fk('relationship_template', nullable=True) + + # endregion + + # region association proxies + + @declared_attr + def source_node_name(cls): + """Required for use by SQLAlchemy queries""" + return association_proxy('source_node', 'name') + + @declared_attr + def target_node_name(cls): + """Required for use by SQLAlchemy queries""" + return association_proxy('target_node', 'name') + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('target_node_id', self.target_node.name), + ('type_name', self.type.name + if self.type is not None else None), + ('template_name', self.relationship_template.name + if self.relationship_template is not None else None), + ('properties', formatting.as_raw_dict(self.properties)), + ('interfaces', formatting.as_raw_list(self.interfaces)))) + + def validate(self): + utils.validate_dict_values(self.properties) + utils.validate_dict_values(self.interfaces) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + utils.coerce_dict_values(container, self.interfaces, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + if self.name: + console.puts('{0} ->'.format(context.style.node(self.name))) + else: + console.puts('->') + with context.style.indent: + console.puts('Node: {0}'.format(context.style.node(self.target_node.name))) + if self.target_capability: + console.puts('Capability: {0}'.format(context.style.node( + self.target_capability.name))) + if self.type is not None: + console.puts('Relationship type: {0}'.format(context.style.type(self.type.name))) + if (self.relationship_template is not None) and self.relationship_template.name: + console.puts('Relationship template: {0}'.format( + context.style.node(self.relationship_template.name))) + utils.dump_dict_values(self.properties, 'Properties') + utils.dump_interfaces(self.interfaces, 'Interfaces') + + __private_fields__ = ['type_fk', + 'source_node_fk', + 'target_node_fk', + 'target_capability_fk', + 'requirement_template_fk', + 'relationship_template_fk', + 'source_node_name', + 'target_node_name'] + + +class CapabilityBase(InstanceModelMixin): + """ + A capability of a :class:`Node`. + + Usually an instance of a :class:`CapabilityTemplate`. + + :ivar name: Name (unique for the node) + :vartype name: basestring + :ivar capability_template: Template from which this capability was instantiated (optional) + :vartype capability_template: :class:`capabilityTemplate` + :ivar type: Capability type + :vartype type: :class:`Type` + :ivar min_occurrences: Minimum number of requirement matches required + :vartype min_occurrences: int + :ivar max_occurrences: Maximum number of requirement matches allowed + :vartype min_occurrences: int + :ivar occurrences: Actual number of requirement matches + :vartype occurrences: int + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar node: Containing node + :vartype node: :class:`Node` + :ivar relationship: Available when we are the target of a relationship + :vartype relationship: :class:`Relationship` + :ivar substitution_mapping: Our contribution to service substitution + :vartype substitution_mapping: :class:`SubstitutionMapping` + """ + + __tablename__ = 'capability' + + @declared_attr + def capability_template(cls): + return relationships.many_to_one(cls, 'capability_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + min_occurrences = Column(Integer, default=None) + max_occurrences = Column(Integer, default=None) + occurrences = Column(Integer, default=0) + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Capability many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Capability""" + return relationships.fk('node') + + @declared_attr + def capability_template_fk(cls): + """For Capability many-to-one to CapabilityTemplate""" + return relationships.fk('capability_template', nullable=True) + + # 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): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + with context.style.indent: + console.puts('Type: {0}'.format(context.style.type(self.type.name))) + console.puts('Occurrences: {0:d} ({1:d}{2})'.format( + self.occurrences, + self.min_occurrences or 0, + ' to {0:d}'.format(self.max_occurrences) + if self.max_occurrences is not None + else ' or more')) + utils.dump_dict_values(self.properties, 'Properties') + + __private_fields__ = ['capability_fk', + 'node_fk', + 'capability_template_fk'] + + +class InterfaceBase(InstanceModelMixin): + """ + A typed set of :class:`Operation`. + + Usually an instance of :class:`InterfaceTemplate`. + + :ivar name: Name (unique for the node, group, or relationship) + :vartype name: basestring + :ivar interface_template: Template from which this interface was instantiated (optional) + :vartype interface_template: :class:`InterfaceTemplate` + :ivar type: Interface type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: string + :ivar inputs: Parameters that can be used by all operations in the interface + :vartype inputs: {basestring: :class:`Parameter`} + :ivar operations: Operations + :vartype operations: {basestring: :class:`Operation`} + + :ivar node: Containing node + :vartype node: :class:`Node` + :ivar group: Containing group + :vartype group: :class:`Group` + :ivar relationship: Containing relationship + :vartype relationship: :class:`Relationship` + """ + + __tablename__ = 'interface' + + @declared_attr + def interface_template(cls): + return relationships.many_to_one(cls, 'interface_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + description = Column(Text) + + @declared_attr + def inputs(cls): + return relationships.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + @declared_attr + def operations(cls): + return relationships.one_to_many(cls, 'operation', dict_key='name') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Interface many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Interface""" + return relationships.fk('node', nullable=True) + + @declared_attr + def group_fk(cls): + """For Group one-to-many to Interface""" + return relationships.fk('group', nullable=True) + + @declared_attr + def relationship_fk(cls): + """For Relationship one-to-many to Interface""" + return relationships.fk('relationship', nullable=True) + + @declared_attr + def interface_template_fk(cls): + """For Interface many-to-one to InterfaceTemplate""" + return relationships.fk('interface_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('type_name', self.type.name), + ('inputs', formatting.as_raw_dict(self.inputs)), + ('operations', formatting.as_raw_list(self.operations)))) + + def validate(self): + utils.validate_dict_values(self.inputs) + utils.validate_dict_values(self.operations) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.inputs, report_issues) + utils.coerce_dict_values(container, self.operations, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Interface type: {0}'.format(context.style.type(self.type.name))) + utils.dump_dict_values(self.inputs, 'Inputs') + utils.dump_dict_values(self.operations, 'Operations') + + __private_fields__ = ['type_fk', + 'node_fk', + 'group_fk', + 'relationship_fk', + 'interface_template_fk'] + + +class OperationBase(InstanceModelMixin): + """ + An operation in a :class:`Interface`. + + Might be an instance of :class:`OperationTemplate`. + + :ivar name: Name (unique for the interface or service) + :vartype name: basestring + :ivar operation_template: Template from which this operation was instantiated (optional) + :vartype operation_template: :class:`OperationTemplate` + :ivar description: Human-readable description + :vartype description: string + :ivar plugin_specification: Associated plugin + :vartype plugin_specification: :class:`PluginSpecification` + :ivar implementation: Implementation string (interpreted by the plugin) + :vartype implementation: basestring + :ivar dependencies: Dependency strings (interpreted by the plugin) + :vartype dependencies: [basestring] + :ivar inputs: Parameters that can be used by this operation + :vartype inputs: {basestring: :class:`Parameter`} + :ivar executor: Executor name + :vartype executor: basestring + :ivar max_retries: Maximum number of retries allowed in case of failure + :vartype max_retries: int + :ivar retry_interval: Interval between retries (in seconds) + :vartype retry_interval: int + + :ivar interface: Containing interface + :vartype interface: :class:`Interface` + :ivar service: Containing service + :vartype service: :class:`Service` + """ + + __tablename__ = 'operation' + + @declared_attr + def operation_template(cls): + return relationships.many_to_one(cls, 'operation_template') + + description = Column(Text) + + @declared_attr + def plugin_specification(cls): + return relationships.one_to_one(cls, 'plugin_specification') + + implementation = Column(Text) + dependencies = Column(modeling_types.StrictList(item_cls=basestring)) + + @declared_attr + def inputs(cls): + return relationships.many_to_many(cls, 'parameter', prefix='inputs', dict_key='name') + + executor = Column(Text) + max_retries = Column(Integer) + retry_interval = Column(Integer) + + # region foreign_keys + + @declared_attr + def service_fk(cls): + """For Service one-to-many to Operation""" + return relationships.fk('service', nullable=True) + + @declared_attr + def interface_fk(cls): + """For Interface one-to-many to Operation""" + return relationships.fk('interface', nullable=True) + + @declared_attr + def plugin_specification_fk(cls): + """For Operation one-to-one to PluginSpecification""" + return relationships.fk('plugin_specification', nullable=True) + + @declared_attr + def operation_template_fk(cls): + """For Operation many-to-one to OperationTemplate""" + return relationships.fk('operation_template', nullable=True) + + # endregion + + @property + def as_raw(self): + return collections.OrderedDict(( + ('name', self.name), + ('description', self.description), + ('implementation', self.implementation), + ('dependencies', self.dependencies), + ('executor', self.executor), + ('max_retries', self.max_retries), + ('retry_interval', self.retry_interval), + ('inputs', formatting.as_raw_dict(self.inputs)))) + + def validate(self): + # TODO must be associated with interface or service + utils.validate_dict_values(self.inputs) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.inputs, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + if self.implementation is not None: + console.puts('Implementation: {0}'.format( + context.style.literal(self.implementation))) + if self.dependencies: + console.puts( + 'Dependencies: {0}'.format( + ', '.join((str(context.style.literal(v)) for v in self.dependencies)))) + if self.executor is not None: + console.puts('Executor: {0}'.format(context.style.literal(self.executor))) + if self.max_retries is not None: + console.puts('Max retries: {0}'.format(context.style.literal(self.max_retries))) + if self.retry_interval is not None: + console.puts('Retry interval: {0}'.format( + context.style.literal(self.retry_interval))) + utils.dump_dict_values(self.inputs, 'Inputs') + + __private_fields__ = ['service_fk', + 'interface_fk', + 'plugin_fk', + 'operation_template_fk'] + + +class ArtifactBase(InstanceModelMixin): + """ + A file associated with a :class:`Node`. + + Usually an instance of :class:`ArtifactTemplate`. + + :ivar name: Name (unique for the node) + :vartype name: basestring + :ivar artifact_template: Template from which this artifact was instantiated (optional) + :vartype artifact_template: :class:`ArtifactTemplate` + :ivar type: Artifact type + :vartype type: :class:`Type` + :ivar description: Human-readable description + :vartype description: string + :ivar source_path: Source path (CSAR or repository) + :vartype source_path: basestring + :ivar target_path: Path at destination machine + :vartype target_path: basestring + :ivar repository_url: Repository URL + :vartype repository_path: basestring + :ivar repository_credential: Credentials for accessing the repository + :vartype repository_credential: {basestring: basestring} + :ivar properties: Associated parameters + :vartype properties: {basestring: :class:`Parameter`} + + :ivar node: Containing node + :vartype node: :class:`Node` + """ + + __tablename__ = 'artifact' + + @declared_attr + def artifact_template(cls): + return relationships.many_to_one(cls, 'artifact_template') + + @declared_attr + def type(cls): + return relationships.many_to_one(cls, 'type') + + description = Column(Text) + source_path = Column(Text) + target_path = Column(Text) + repository_url = Column(Text) + repository_credential = Column(modeling_types.StrictDict(basestring, basestring)) + + @declared_attr + def properties(cls): + return relationships.many_to_many(cls, 'parameter', prefix='properties', dict_key='name') + + # region foreign_keys + + @declared_attr + def type_fk(cls): + """For Artifact many-to-one to Type""" + return relationships.fk('type') + + @declared_attr + def node_fk(cls): + """For Node one-to-many to Artifact""" + return relationships.fk('node') + + @declared_attr + def artifact_template_fk(cls): + """For Artifact many-to-one to ArtifactTemplate""" + return relationships.fk('artifact_template', nullable=True) + + # 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): + utils.validate_dict_values(self.properties) + + def coerce_values(self, container, report_issues): + utils.coerce_dict_values(container, self.properties, report_issues) + + def dump(self): + context = ConsumptionContext.get_thread_local() + console.puts(context.style.node(self.name)) + if self.description: + console.puts(context.style.meta(self.description)) + with context.style.indent: + console.puts('Artifact type: {0}'.format(context.style.type(self.type.name))) + console.puts('Source path: {0}'.format(context.style.literal(self.source_path))) + if self.target_path is not None: + console.puts('Target path: {0}'.format(context.style.literal(self.target_path))) + if self.repository_url is not None: + console.puts('Repository URL: {0}'.format( + context.style.literal(self.repository_url))) + if self.repository_credential: + console.puts('Repository credential: {0}'.format( + context.style.literal(self.repository_credential))) + utils.dump_dict_values(self.properties, 'Properties') + + __private_fields__ = ['type_fk', + 'node_fk', + 'artifact_template_fk']