aboutsummaryrefslogtreecommitdiffstats
path: root/bpmn/so-bpmn-building-blocks/src/main/resources/subprocess/BuildingBlock/DeleteVolumeGroupBB.bpmn
blob: 746d1f2c5f69a93d510aa568a90970242cf36a94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="1.4.0">
  <bpmn:process id="DeleteVolumeGroupBB" name="DeleteVolumeGroupBB" isExecutable="true">
    <bpmn:startEvent id="DeleteVolumeGroupBB_Start" name="Start">
      <bpmn:outgoing>SequenceFlow_1wz1rfg</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:sequenceFlow id="SequenceFlow_1wz1rfg" sourceRef="DeleteVolumeGroupBB_Start" targetRef="DeleteVolumeGroupVnfAdapter" />
    <bpmn:endEvent id="DeleteVolumeGroupBB_End">
      <bpmn:incoming>SequenceFlow_0mh0v9h</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:serviceTask id="UpdateVolumeGroupAAI" name="&#10;AAI&#10;Update&#10;(volume grp)&#10;" camunda:expression="${AAIUpdateTasks.updateOrchestrationStatusAssignedVolumeGroup(InjectExecution.execute(execution, execution.getVariable(&#34;gBuildingBlockExecution&#34;)))}">
      <bpmn:incoming>SequenceFlow_0hml0t6</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0mh0v9h</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="SequenceFlow_0mh0v9h" sourceRef="UpdateVolumeGroupAAI" targetRef="DeleteVolumeGroupBB_End" />
    <bpmn:sequenceFlow id="SequenceFlow_13ngwev" sourceRef="DeleteVolumeGroupVnfAdapter" targetRef="VnfAdapter" />
    <bpmn:serviceTask id="DeleteVolumeGroupVnfAdapter" name="Delete Volume Group Vnf Adapter (AIC Delete)" camunda:expression="${VnfAdapterDeleteTasks.deleteVolumeGroup(InjectExecution.execute(execution, execution.getVariable(&#34;gBuildingBlockExecution&#34;)))}">
      <bpmn:incoming>SequenceFlow_1wz1rfg</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_13ngwev</bpmn:outgoing>
    </bpmn:serviceTask>
    <bpmn:sequenceFlow id="SequenceFlow_0fkan8t" sourceRef="VnfAdapter" targetRef="UpdateVolumeGroupHeatStackId" />
    <bpmn:callActivity id="VnfAdapter" name="Vnf Adapter" calledElement="VnfAdapter">
      <bpmn:extensionElements>
        <camunda:in source="gBuildingBlockExecution" target="gBuildingBlockExecution" />
        <camunda:inputOutput>
          <camunda:outputParameter name="Output_3bec9ju" />
        </camunda:inputOutput>
        <camunda:out source="WorkflowException" target="WorkflowException" />
        <camunda:out source="heatStackId" target="heatStackId" />
        <camunda:in source="deleteVolumeGroupRequest" target="deleteVolumeGroupRequest" />
        <camunda:in source="VNFREST_Request" target="VNFREST_Request" />
      </bpmn:extensionElements>
      <bpmn:incoming>SequenceFlow_13ngwev</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0fkan8t</bpmn:outgoing>
    </bpmn:callActivity>
    <bpmn:subProcess id="SubProcess_089t601" name="Sub Process Error" triggeredByEvent="true">
      <bpmn:endEvent id="EndEvent_18lpeyi" name="End">
        <bpmn:incoming>SequenceFlow_07rsz9o</bpmn:incoming>
      </bpmn:endEvent>
      <bpmn:startEvent id="StartEvent_1gun94q" name="Start">
        <bpmn:outgoing>SequenceFlow_07rsz9o</bpmn:outgoing>
        <bpmn:errorEventDefinition />
      </bpmn:startEvent>
      <bpmn:sequenceFlow id="SequenceFlow_07rsz9o" sourceRef="StartEvent_1gun94q" targetRef="EndEvent_18lpeyi" />
    </bpmn:subProcess>
    <bpmn:sequenceFlow id="SequenceFlow_0hml0t6" sourceRef="UpdateVolumeGroupHeatStackId" targetRef="UpdateVolumeGroupAAI" />
    <bpmn:serviceTask id="UpdateVolumeGroupHeatStackId" name="&#10;AAI&#10;Update&#10;(volume grp)&#10;" camunda:expression="${AAIUpdateTasks.updateHeatStackIdVolumeGroup(InjectExecution.execute(execution, execution.getVariable(&#34;gBuildingBlockExecution&#34;)))}">
      <bpmn:incoming>SequenceFlow_0fkan8t</bpmn:incoming>
      <bpmn:outgoing>SequenceFlow_0hml0t6</bpmn:outgoing>
    </bpmn:serviceTask>
  </bpmn:process>
  <bpmn:error id="Error_0pz4sdi" name="gDelegateError" errorCode="7000" />
  <bpmn:escalation id="Escalation_1hjulni" name="Escalation_2cgup2p" />
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="DeleteVolumeGroupBB">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="DeleteVolumeGroupBB_Start">
        <dc:Bounds x="310" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="316" y="138" width="24" height="12" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_1wz1rfg_di" bpmnElement="SequenceFlow_1wz1rfg">
        <di:waypoint xsi:type="dc:Point" x="346" y="120" />
        <di:waypoint xsi:type="dc:Point" x="388" y="120" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="322" y="99" width="90" height="12" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="EndEvent_1k6463v_di" bpmnElement="DeleteVolumeGroupBB_End">
        <dc:Bounds x="978" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="996" y="142" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="ServiceTask_0rytcj0_di" bpmnElement="UpdateVolumeGroupAAI">
        <dc:Bounds x="822" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_0mh0v9h_di" bpmnElement="SequenceFlow_0mh0v9h">
        <di:waypoint xsi:type="dc:Point" x="922" y="120" />
        <di:waypoint xsi:type="dc:Point" x="978" y="120" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="950" y="105" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_13ngwev_di" bpmnElement="SequenceFlow_13ngwev">
        <di:waypoint xsi:type="dc:Point" x="488" y="120" />
        <di:waypoint xsi:type="dc:Point" x="526" y="120" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="507" y="105" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="ServiceTask_1nrp0hb_di" bpmnElement="DeleteVolumeGroupVnfAdapter">
        <dc:Bounds x="388" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_0fkan8t_di" bpmnElement="SequenceFlow_0fkan8t">
        <di:waypoint xsi:type="dc:Point" x="626" y="120" />
        <di:waypoint xsi:type="dc:Point" x="678" y="120" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="652" y="105" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="CallActivity_0li7q97_di" bpmnElement="VnfAdapter">
        <dc:Bounds x="526" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="SubProcess_089t601_di" bpmnElement="SubProcess_089t601" isExpanded="true">
        <dc:Bounds x="459" y="242" width="233" height="135" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="EndEvent_18lpeyi_di" bpmnElement="EndEvent_18lpeyi">
        <dc:Bounds x="613" y="287" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="622" y="327" width="19" height="12" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="StartEvent_1gun94q_di" bpmnElement="StartEvent_1gun94q">
        <dc:Bounds x="498" y="287" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="505" y="327" width="24" height="12" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_07rsz9o_di" bpmnElement="SequenceFlow_07rsz9o">
        <di:waypoint xsi:type="dc:Point" x="534" y="305" />
        <di:waypoint xsi:type="dc:Point" x="573" y="305" />
        <di:waypoint xsi:type="dc:Point" x="573" y="305" />
        <di:waypoint xsi:type="dc:Point" x="613" y="305" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="543" y="305" width="0" height="12" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_0hml0t6_di" bpmnElement="SequenceFlow_0hml0t6">
        <di:waypoint xsi:type="dc:Point" x="778" y="120" />
        <di:waypoint xsi:type="dc:Point" x="822" y="120" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="800" y="105" width="0" height="0" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="ServiceTask_1eatpiw_di" bpmnElement="UpdateVolumeGroupHeatStackId">
        <dc:Bounds x="678" y="80" width="100" height="80" />
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>
93'>693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
#########
# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved
#
# Licensed 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 functools import wraps, partial
import json
import os
import sys

from IPy import IP
from keystoneauth1 import loading, session
import cinderclient.client as cinder_client
import cinderclient.exceptions as cinder_exceptions
import keystoneclient.v3.client as keystone_client
import keystoneclient.exceptions as keystone_exceptions
import neutronclient.v2_0.client as neutron_client
import neutronclient.common.exceptions as neutron_exceptions
import novaclient.client as nova_client
import novaclient.exceptions as nova_exceptions
import glanceclient.client as glance_client
import glanceclient.exc as glance_exceptions

import cloudify
from cloudify import context, ctx
from cloudify.exceptions import NonRecoverableError, RecoverableError

INFINITE_RESOURCE_QUOTA = -1

# properties
USE_EXTERNAL_RESOURCE_PROPERTY = 'use_external_resource'
CREATE_IF_MISSING_PROPERTY = 'create_if_missing'
CONFIG_PROPERTY = 'openstack_config'

# runtime properties
OPENSTACK_AZ_PROPERTY = 'availability_zone'
OPENSTACK_ID_PROPERTY = 'external_id'  # resource's openstack id
OPENSTACK_TYPE_PROPERTY = 'external_type'  # resource's openstack type
OPENSTACK_NAME_PROPERTY = 'external_name'  # resource's openstack name
CONDITIONALLY_CREATED = 'conditionally_created'  # resource was
# conditionally created
CONFIG_RUNTIME_PROPERTY = CONFIG_PROPERTY   # openstack configuration

# operation inputs
CONFIG_INPUT = CONFIG_PROPERTY

# runtime properties which all types use
COMMON_RUNTIME_PROPERTIES_KEYS = [OPENSTACK_ID_PROPERTY,
                                  OPENSTACK_TYPE_PROPERTY,
                                  OPENSTACK_NAME_PROPERTY,
                                  CONDITIONALLY_CREATED]

MISSING_RESOURCE_MESSAGE = "Couldn't find a resource of " \
                           "type {0} with the name or id {1}"


class ProviderContext(object):

    def __init__(self, provider_context):
        self._provider_context = provider_context or {}
        self._resources = self._provider_context.get('resources', {})

    @property
    def agents_keypair(self):
        return self._resources.get('agents_keypair')

    @property
    def agents_security_group(self):
        return self._resources.get('agents_security_group')

    @property
    def ext_network(self):
        return self._resources.get('ext_network')

    @property
    def floating_ip(self):
        return self._resources.get('floating_ip')

    @property
    def int_network(self):
        return self._resources.get('int_network')

    @property
    def management_keypair(self):
        return self._resources.get('management_keypair')

    @property
    def management_security_group(self):
        return self._resources.get('management_security_group')

    @property
    def management_server(self):
        return self._resources.get('management_server')

    @property
    def router(self):
        return self._resources.get('router')

    @property
    def subnet(self):
        return self._resources.get('subnet')

    def __repr__(self):
        info = json.dumps(self._provider_context)
        return '<' + self.__class__.__name__ + ' ' + info + '>'


def provider(ctx):
    return ProviderContext(ctx.provider_context)


def assign_payload_as_runtime_properties(ctx, resource_name, payload={}):
    """
    In general Openstack API objects have create, update, and delete
    functions. Each function normally receives a payload that describes
    the desired configuration of the object.
    This makes sure to store that configuration in the runtime
    properties and cleans any potentially sensitive data.

    :param ctx: The Cloudify NodeInstanceContext
    :param resource_name: A string describing the resource.
    :param payload: The payload.
    :return:
    """

    # Avoid failing if a developer inadvertently passes a
    # non-NodeInstanceContext
    if getattr(ctx, 'instance'):
        if resource_name not in ctx.instance.runtime_properties.keys():
            ctx.instance.runtime_properties[resource_name] = {}
        for key, value in payload.items():
            if key != 'user_data' and key != 'adminPass':
                ctx.instance.runtime_properties[resource_name][key] = value


def get_relationships_by_relationship_type(ctx, type_name):
    """
    Get cloudify relationships by relationship type.
    Follows the inheritance tree.

    :param ctx: Cloudify NodeInstanceContext
    :param type_name: desired relationship type derived
    from cloudify.relationships.depends_on.
    :return: list of RelationshipSubjectContext
    """

    return [rel for rel in ctx.instance.relationships if
            type_name in rel.type_hierarchy]


def get_attribute_of_connected_nodes_by_relationship_type(ctx,
                                                          type_name,
                                                          attribute_name):
    """
    Returns a list of OPENSTACK_ID_PROPERTY from a list of
    Cloudify RelationshipSubjectContext.

    :param ctx: Cloudify NodeInstanceContext
    :param type_name: desired relationship type derived
    from cloudify.relationships.depends_on.
    :param attribute_name: usually either
    OPENSTACK_NAME_PROPERTY or OPENSTACK_ID_PROPERTY
    :return:
    """

    return [rel.target.instance.runtime_properties[attribute_name]
            for rel in get_relationships_by_relationship_type(ctx, type_name)]


def get_relationships_by_openstack_type(ctx, type_name):
    return [rel for rel in ctx.instance.relationships
            if rel.target.instance.runtime_properties.get(
                OPENSTACK_TYPE_PROPERTY) == type_name]


def get_connected_nodes_by_openstack_type(ctx, type_name):
    return [rel.target.node
            for rel in get_relationships_by_openstack_type(ctx, type_name)]


def get_openstack_ids_of_connected_nodes_by_openstack_type(ctx, type_name):
    return [rel.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
            for rel in get_relationships_by_openstack_type(ctx, type_name)
            ]


def get_openstack_names_of_connected_nodes_by_openstack_type(ctx, type_name):
    return [rel.target.instance.runtime_properties[OPENSTACK_NAME_PROPERTY]
            for rel in get_relationships_by_openstack_type(ctx, type_name)
            ]


def get_single_connected_node_by_openstack_type(
        ctx, type_name, if_exists=False):
    nodes = get_connected_nodes_by_openstack_type(ctx, type_name)
    check = len(nodes) > 1 if if_exists else len(nodes) != 1
    if check:
        raise NonRecoverableError(
            'Expected {0} one {1} node. got {2}'.format(
                'at most' if if_exists else 'exactly', type_name, len(nodes)))
    return nodes[0] if nodes else None


def get_openstack_id_of_single_connected_node_by_openstack_type(
        ctx, type_name, if_exists=False):
    ids = get_openstack_ids_of_connected_nodes_by_openstack_type(ctx,
                                                                 type_name)
    check = len(ids) > 1 if if_exists else len(ids) != 1
    if check:
        raise NonRecoverableError(
            'Expected {0} one {1} capability. got {2}'.format(
                'at most' if if_exists else 'exactly', type_name, len(ids)))
    return ids[0] if ids else None


def get_resource_id(ctx, type_name):
    if ctx.node.properties['resource_id']:
        return ctx.node.properties['resource_id']
    return "{0}_{1}_{2}".format(type_name, ctx.deployment.id, ctx.instance.id)


def transform_resource_name(ctx, res):

    if isinstance(res, basestring):
        res = {'name': res}

    if not isinstance(res, dict):
        raise ValueError("transform_resource_name() expects either string or "
                         "dict as the first parameter")

    pfx = ctx.bootstrap_context.resources_prefix

    if not pfx:
        return res['name']

    name = res['name']
    res['name'] = pfx + name

    if name.startswith(pfx):
        ctx.logger.warn("Prefixing resource '{0}' with '{1}' but it "
                        "already has this prefix".format(name, pfx))
    else:
        ctx.logger.info("Transformed resource name '{0}' to '{1}'".format(
                        name, res['name']))

    return res['name']


def _get_resource_by_name_or_id_from_ctx(ctx, name_field_name, openstack_type,
                                         sugared_client):
    resource_id = ctx.node.properties['resource_id']
    if not resource_id:
        raise NonRecoverableError(
            "Can't set '{0}' to True without supplying a value for "
            "'resource_id'".format(USE_EXTERNAL_RESOURCE_PROPERTY))

    return get_resource_by_name_or_id(resource_id, openstack_type,
                                      sugared_client, True, name_field_name)


def get_resource_by_name_or_id(
        resource_id, openstack_type, sugared_client,
        raise_if_not_found=True, name_field_name='name'):

    # search for resource by name (or name-equivalent field)
    search_param = {name_field_name: resource_id}
    resource = sugared_client.cosmo_get_if_exists(openstack_type,
                                                  **search_param)
    if not resource:
        # fallback - search for resource by id
        resource = sugared_client.cosmo_get_if_exists(
            openstack_type, id=resource_id)

    if not resource and raise_if_not_found:
        raise NonRecoverableError(
            MISSING_RESOURCE_MESSAGE.format(openstack_type, resource_id))

    return resource


def use_external_resource(ctx, sugared_client, openstack_type,
                          name_field_name='name'):
    if not is_external_resource(ctx):
        return None
    try:
        resource = _get_resource_by_name_or_id_from_ctx(
            ctx, name_field_name, openstack_type, sugared_client)
    except NonRecoverableError:
        if is_create_if_missing(ctx):
            ctx.instance.runtime_properties[CONDITIONALLY_CREATED] = True
            return None
        else:
            raise

    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = \
        sugared_client.get_id_from_resource(resource)
    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = openstack_type

    from openstack_plugin_common.floatingip import FLOATINGIP_OPENSTACK_TYPE
    # store openstack name runtime property, unless it's a floating IP type,
    # in which case the ip will be stored in the runtime properties instead.
    if openstack_type != FLOATINGIP_OPENSTACK_TYPE:
        ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = \
            sugared_client.get_name_from_resource(resource)

    ctx.logger.info('Using external resource {0}: {1}'.format(
        openstack_type, ctx.node.properties['resource_id']))
    return resource


def validate_resource(ctx, sugared_client, openstack_type,
                      name_field_name='name'):
    ctx.logger.debug('validating resource {0} (node {1})'.format(
        openstack_type, ctx.node.id))

    openstack_type_plural = sugared_client.cosmo_plural(openstack_type)
    resource = None

    if is_external_resource(ctx):

        try:
            # validate the resource truly exists
            resource = _get_resource_by_name_or_id_from_ctx(
                ctx, name_field_name, openstack_type, sugared_client)
            ctx.logger.debug('OK: {0} {1} found in pool'.format(
                openstack_type, ctx.node.properties['resource_id']))
        except NonRecoverableError as e:
            if not is_create_if_missing(ctx):
                ctx.logger.error('VALIDATION ERROR: ' + str(e))
                resource_list = list(sugared_client.cosmo_list(openstack_type))
                if resource_list:
                    ctx.logger.info('list of existing {0}: '.format(
                        openstack_type_plural))
                    for resource in resource_list:
                        ctx.logger.info('    {0:>10} - {1}'.format(
                            sugared_client.get_id_from_resource(resource),
                            sugared_client.get_name_from_resource(resource)))
                else:
                    ctx.logger.info('there are no existing {0}'.format(
                        openstack_type_plural))
                raise
    if not resource:
        if isinstance(sugared_client, NovaClientWithSugar):
            # not checking quota for Nova resources due to a bug in Nova client
            return

        # validate available quota for provisioning the resource
        resource_list = list(sugared_client.cosmo_list(openstack_type))
        resource_amount = len(resource_list)

        resource_quota = sugared_client.get_quota(openstack_type)

        if resource_amount < resource_quota \
                or resource_quota == INFINITE_RESOURCE_QUOTA:
            ctx.logger.debug(
                'OK: {0} (node {1}) can be created. provisioned {2}: {3}, '
                'quota: {4}'
                .format(openstack_type, ctx.node.id, openstack_type_plural,
                        resource_amount, resource_quota))
        else:
            err = ('{0} (node {1}) cannot be created due to quota limitations.'
                   ' provisioned {2}: {3}, quota: {4}'
                   .format(openstack_type, ctx.node.id, openstack_type_plural,
                           resource_amount, resource_quota))
            ctx.logger.error('VALIDATION ERROR:' + err)
            raise NonRecoverableError(err)


def delete_resource_and_runtime_properties(ctx, sugared_client,
                                           runtime_properties_keys):
    node_openstack_type = ctx.instance.runtime_properties[
        OPENSTACK_TYPE_PROPERTY]
    if not is_external_resource(ctx):
        ctx.logger.info('deleting {0}'.format(node_openstack_type))
        sugared_client.cosmo_delete_resource(
            node_openstack_type,
            ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY])
    else:
        ctx.logger.info('not deleting {0} since an external {0} is '
                        'being used'.format(node_openstack_type))

    delete_runtime_properties(ctx, runtime_properties_keys)


def is_external_resource(ctx):
    return is_external_resource_by_properties(ctx.node.properties)


def is_external_resource_not_conditionally_created(ctx):
    return is_external_resource_by_properties(ctx.node.properties) and \
        not ctx.instance.runtime_properties.get(CONDITIONALLY_CREATED)


def is_external_relationship_not_conditionally_created(ctx):
    return is_external_resource_by_properties(ctx.source.node.properties) and \
        is_external_resource_by_properties(ctx.target.node.properties) and \
        not ctx.source.instance.runtime_properties.get(
            CONDITIONALLY_CREATED) and not \
        ctx.target.instance.runtime_properties.get(CONDITIONALLY_CREATED)


def is_create_if_missing(ctx):
    return is_create_if_missing_by_properties(ctx.node.properties)


def is_external_relationship(ctx):
    return is_external_resource_by_properties(ctx.source.node.properties) and \
        is_external_resource_by_properties(ctx.target.node.properties)


def is_external_resource_by_properties(properties):
    return USE_EXTERNAL_RESOURCE_PROPERTY in properties and \
        properties[USE_EXTERNAL_RESOURCE_PROPERTY]


def is_create_if_missing_by_properties(properties):
    return CREATE_IF_MISSING_PROPERTY in properties and \
        properties[CREATE_IF_MISSING_PROPERTY]


def delete_runtime_properties(ctx, runtime_properties_keys):
    for runtime_prop_key in runtime_properties_keys:
        if runtime_prop_key in ctx.instance.runtime_properties:
            del ctx.instance.runtime_properties[runtime_prop_key]


def validate_ip_or_range_syntax(ctx, address, is_range=True):
    range_suffix = ' range' if is_range else ''
    ctx.logger.debug('checking whether {0} is a valid address{1}...'
                     .format(address, range_suffix))
    try:
        IP(address)
        ctx.logger.debug('OK:'
                         '{0} is a valid address{1}.'.format(address,
                                                             range_suffix))
    except ValueError as e:
        err = ('{0} is not a valid address{1}; {2}'.format(
            address, range_suffix, e.message))
        ctx.logger.error('VALIDATION ERROR:' + err)
        raise NonRecoverableError(err)


class Config(object):

    OPENSTACK_CONFIG_PATH_ENV_VAR = 'OPENSTACK_CONFIG_PATH'
    OPENSTACK_CONFIG_PATH_DEFAULT_PATH = '~/openstack_config.json'
    OPENSTACK_ENV_VAR_PREFIX = 'OS_'
    OPENSTACK_SUPPORTED_ENV_VARS = {
        'OS_AUTH_URL', 'OS_USERNAME', 'OS_PASSWORD', 'OS_TENANT_NAME',
        'OS_REGION_NAME', 'OS_PROJECT_ID', 'OS_PROJECT_NAME',
        'OS_USER_DOMAIN_NAME', 'OS_PROJECT_DOMAIN_NAME'
    }

    @classmethod
    def get(cls):
        static_config = cls._build_config_from_env_variables()
        env_name = cls.OPENSTACK_CONFIG_PATH_ENV_VAR
        default_location_tpl = cls.OPENSTACK_CONFIG_PATH_DEFAULT_PATH
        default_location = os.path.expanduser(default_location_tpl)
        config_path = os.getenv(env_name, default_location)
        try:
            with open(config_path) as f:
                cls.update_config(static_config, json.loads(f.read()))
        except IOError:
            pass
        return static_config

    @classmethod
    def _build_config_from_env_variables(cls):
        return {v.lstrip(cls.OPENSTACK_ENV_VAR_PREFIX).lower(): os.environ[v]
                for v in cls.OPENSTACK_SUPPORTED_ENV_VARS if v in os.environ}

    @staticmethod
    def update_config(overridden_cfg, overriding_cfg):
        """ this method is like dict.update() only that it doesn't override
        with (or set new) empty values (e.g. empty string) """
        for k, v in overriding_cfg.iteritems():
            if v:
                overridden_cfg[k] = v


class OpenStackClient(object):

    COMMON = {'username', 'password', 'auth_url'}
    AUTH_SETS = [
        COMMON | {'tenant_name'},
        COMMON | {'project_id', 'user_domain_name'},
        COMMON | {'project_id', 'project_name', 'user_domain_name'},
        COMMON | {'project_name', 'user_domain_name', 'project_domain_name'},
    ]
    OPTIONAL_AUTH_PARAMS = {'insecure'}

    def __init__(self, client_name, client_class, config=None, *args, **kw):
        cfg = Config.get()

        if config:
            Config.update_config(cfg, config)

        v3 = '/v3' in cfg['auth_url']
        # Newer libraries expect the region key to be `region_name`, not
        # `region`.
        region = cfg.pop('region', None)
        if v3 and region:
            cfg['region_name'] = region

        cfg = self._merge_custom_configuration(cfg, client_name)

        auth_params, client_params = OpenStackClient._split_config(cfg)
        OpenStackClient._validate_auth_params(auth_params)

        if v3:
            # keystone v3 complains if these aren't set.
            for key in 'user_domain_name', 'project_domain_name':
                auth_params.setdefault(key, 'default')

        client_params['session'] = self._authenticate(auth_params)
        self._client = client_class(**client_params)

    @classmethod
    def _validate_auth_params(cls, params):
        if set(params.keys()) - cls.OPTIONAL_AUTH_PARAMS in cls.AUTH_SETS:
            return

        def set2str(s):
            return '({})'.format(', '.join(sorted(s)))

        received_params = set2str(params)
        valid_auth_sets = map(set2str, cls.AUTH_SETS)
        raise NonRecoverableError(
            "{} is not valid set of auth params. Expected to find parameters "
            "either as environment variables, in a JSON file (at either a "
            "path which is set under the environment variable {} or at the "
            "default location {}), or as nested properties under an "
            "'{}' property. Valid auth param sets are: {}."
            .format(received_params,
                    Config.OPENSTACK_CONFIG_PATH_ENV_VAR,
                    Config.OPENSTACK_CONFIG_PATH_DEFAULT_PATH,
                    CONFIG_PROPERTY,
                    ', '.join(valid_auth_sets)))

    @staticmethod
    def _merge_custom_configuration(cfg, client_name):
        config = cfg.copy()

        mapping = {
            'nova_url': 'nova_client',
            'neutron_url': 'neutron_client'
        }
        for key in 'nova_url', 'neutron_url':
            val = config.pop(key, None)
            if val is not None:
                ctx.logger.warn(
                    "'{}' property is deprecated. Use `custom_configuration"
                    ".{}.endpoint_override` instead.".format(
                        key, mapping[key]))
                if mapping.get(key, None) == client_name:
                    config['endpoint_override'] = val

        if 'custom_configuration' in cfg:
            del config['custom_configuration']
            config.update(cfg['custom_configuration'].get(client_name, {}))
        return config

    @classmethod
    def _split_config(cls, cfg):
        all = reduce(lambda x, y: x | y, cls.AUTH_SETS)
        all |= cls.OPTIONAL_AUTH_PARAMS

        auth, misc = {}, {}
        for param, value in cfg.items():
            if param in all:
                auth[param] = value
            else:
                misc[param] = value
        return auth, misc

    @staticmethod
    def _authenticate(cfg):
        verify = True
        if 'insecure' in cfg:
            cfg = cfg.copy()
            # NOTE: Next line will evaluate to False only when insecure is set
            # to True. Any other value (string etc.) will force verify to True.
            # This is done on purpose, since we do not wish to use insecure
            # connection by mistake.
            verify = not (cfg['insecure'] is True)
            del cfg['insecure']
        loader = loading.get_plugin_loader("password")
        auth = loader.load_from_options(**cfg)
        sess = session.Session(auth=auth, verify=verify)
        return sess

    # Proxy any unknown call to base client
    def __getattr__(self, attr):
        return getattr(self._client, attr)

    # Sugar, common to all clients
    def cosmo_plural(self, obj_type_single):
        return obj_type_single + 's'

    def cosmo_get_named(self, obj_type_single, name, **kw):
        return self.cosmo_get(obj_type_single, name=name, **kw)

    def cosmo_get(self, obj_type_single, **kw):
        return self._cosmo_get(obj_type_single, False, **kw)

    def cosmo_get_if_exists(self, obj_type_single, **kw):
        return self._cosmo_get(obj_type_single, True, **kw)

    def _cosmo_get(self, obj_type_single, if_exists, **kw):
        ls = list(self.cosmo_list(obj_type_single, **kw))
        check = len(ls) > 1 if if_exists else len(ls) != 1
        if check:
            raise NonRecoverableError(
                "Expected {0} one object of type {1} "
                "with match {2} but there are {3}".format(
                    'at most' if if_exists else 'exactly',
                    obj_type_single, kw, len(ls)))
        return ls[0] if ls else None


class GlanceClient(OpenStackClient):

    # Can't glance_url be figured out from keystone
    REQUIRED_CONFIG_PARAMS = \
        ['username', 'password', 'tenant_name', 'auth_url']

    def connect(self, cfg):
        loader = loading.get_plugin_loader('password')
        auth = loader.load_from_options(
            auth_url=cfg['auth_url'],
            username=cfg['username'],
            password=cfg['password'],
            tenant_name=cfg['tenant_name'])
        sess = session.Session(auth=auth)

        client_kwargs = dict(
            session=sess,
        )
        if cfg.get('glance_url'):
            client_kwargs['endpoint'] = cfg['glance_url']

        return GlanceClientWithSugar(**client_kwargs)


# Decorators
def _find_instanceof_in_kw(cls, kw):
    ret = [v for v in kw.values() if isinstance(v, cls)]
    if not ret:
        return None
    if len(ret) > 1:
        raise NonRecoverableError(
            "Expected to find exactly one instance of {0} in "
            "kwargs but found {1}".format(cls, len(ret)))
    return ret[0]


def _find_context_in_kw(kw):
    return _find_instanceof_in_kw(cloudify.context.CloudifyContext, kw)


def with_neutron_client(f):
    @wraps(f)
    def wrapper(*args, **kw):
        _put_client_in_kw('neutron_client', NeutronClientWithSugar, kw)

        try:
            return f(*args, **kw)
        except neutron_exceptions.NeutronClientException, e:
            if e.status_code in _non_recoverable_error_codes:
                _re_raise(e, recoverable=False, status_code=e.status_code)
            else:
                raise
    return wrapper


def with_nova_client(f):
    @wraps(f)
    def wrapper(*args, **kw):
        _put_client_in_kw('nova_client', NovaClientWithSugar, kw)

        try:
            return f(*args, **kw)
        except nova_exceptions.OverLimit, e:
            _re_raise(e, recoverable=True, retry_after=e.retry_after)
        except nova_exceptions.ClientException, e:
            if e.code in _non_recoverable_error_codes:
                _re_raise(e, recoverable=False, status_code=e.code)
            else:
                raise
    return wrapper


def with_cinder_client(f):
    @wraps(f)
    def wrapper(*args, **kw):
        _put_client_in_kw('cinder_client', CinderClientWithSugar, kw)

        try:
            return f(*args, **kw)
        except cinder_exceptions.ClientException, e:
            if e.code in _non_recoverable_error_codes:
                _re_raise(e, recoverable=False, status_code=e.code)
            else:
                raise
    return wrapper


def with_glance_client(f):
    @wraps(f)
    def wrapper(*args, **kw):
        _put_client_in_kw('glance_client', GlanceClientWithSugar, kw)

        try:
            return f(*args, **kw)
        except glance_exceptions.ClientException, e:
            if e.code in _non_recoverable_error_codes:
                _re_raise(e, recoverable=False, status_code=e.code)
            else:
                raise
    return wrapper


def with_keystone_client(f):
    @wraps(f)
    def wrapper(*args, **kw):
        _put_client_in_kw('keystone_client', KeystoneClientWithSugar, kw)

        try:
            return f(*args, **kw)
        except keystone_exceptions.HTTPError, e:
            if e.http_status in _non_recoverable_error_codes:
                _re_raise(e, recoverable=False, status_code=e.http_status)
            else:
                raise
        except keystone_exceptions.ClientException, e:
            _re_raise(e, recoverable=False)
    return wrapper


def _put_client_in_kw(client_name, client_class, kw):
    if client_name in kw:
        return

    ctx = _find_context_in_kw(kw)
    if ctx.type == context.NODE_INSTANCE:
        config = ctx.node.properties.get(CONFIG_PROPERTY)
        rt_config = ctx.instance.runtime_properties.get(
            CONFIG_RUNTIME_PROPERTY)
    elif ctx.type == context.RELATIONSHIP_INSTANCE:
        config = ctx.source.node.properties.get(CONFIG_PROPERTY)
        rt_config = ctx.source.instance.runtime_properties.get(
            CONFIG_RUNTIME_PROPERTY)
        if not config:
            config = ctx.target.node.properties.get(CONFIG_PROPERTY)
            rt_config = ctx.target.instance.runtime_properties.get(
                CONFIG_RUNTIME_PROPERTY)

    else:
        config = None
        rt_config = None

    # Overlay with configuration from runtime property, if any.
    if rt_config:
        if config:
            config = config.copy()
            config.update(rt_config)
        else:
            config = rt_config

    if CONFIG_INPUT in kw:
        if config:
            config = config.copy()
            config.update(kw[CONFIG_INPUT])
        else:
            config = kw[CONFIG_INPUT]
    kw[client_name] = client_class(config=config)


_non_recoverable_error_codes = [400, 401, 403, 404, 409]


def _re_raise(e, recoverable, retry_after=None, status_code=None):
    exc_type, exc, traceback = sys.exc_info()
    message = e.message
    if status_code is not None:
        message = '{0} [status_code={1}]'.format(message, status_code)
    if recoverable:
        if retry_after == 0:
            retry_after = None
        raise RecoverableError(
            message=message,
            retry_after=retry_after), None, traceback
    else:
        raise NonRecoverableError(message), None, traceback


# Sugar for clients

class NovaClientWithSugar(OpenStackClient):

    def __init__(self, *args, **kw):
        config = kw['config']
        if config.get('nova_url'):
            config['endpoint_override'] = config.pop('nova_url')

        super(NovaClientWithSugar, self).__init__(
            'nova_client', partial(nova_client.Client, '2'), *args, **kw)

    def cosmo_list(self, obj_type_single, **kw):
        """ Sugar for xxx.findall() - not using xxx.list() because findall
        can receive filtering parameters, and it's common for all types"""
        obj_type_plural = self._get_nova_field_name_for_type(obj_type_single)
        for obj in getattr(self, obj_type_plural).findall(**kw):
            yield obj

    def cosmo_delete_resource(self, obj_type_single, obj_id):
        obj_type_plural = self._get_nova_field_name_for_type(obj_type_single)
        getattr(self, obj_type_plural).delete(obj_id)

    def get_id_from_resource(self, resource):
        return resource.id

    def get_name_from_resource(self, resource):
        return resource.name

    def get_quota(self, obj_type_single):
        raise RuntimeError(
            'Retrieving quotas from Nova service is currently unsupported '
            'due to a bug in Nova python client')

        # we're already authenticated, but the following call will make
        # 'service_catalog' available under 'client', through which we can
        # extract the tenant_id (Note that self.client.tenant_id might be
        # None if project_id (AKA tenant_name) was used instead; However the
        # actual tenant_id must be used to retrieve the quotas)
        self.client.authenticate()
        tenant_id = self.client.service_catalog.get_tenant_id()
        quotas = self.quotas.get(tenant_id)
        return getattr(quotas, self.cosmo_plural(obj_type_single))

    def _get_nova_field_name_for_type(self, obj_type_single):
        from openstack_plugin_common.floatingip import \
            FLOATINGIP_OPENSTACK_TYPE
        if obj_type_single == FLOATINGIP_OPENSTACK_TYPE:
            # since we use the same 'openstack type' property value for both
            # neutron and nova floating-ips, this adjustment must be made
            # for nova client, as fields names differ between the two clients
            obj_type_single = 'floating_ip'
        return self.cosmo_plural(obj_type_single)


class NeutronClientWithSugar(OpenStackClient):

    def __init__(self, *args, **kw):
        super(NeutronClientWithSugar, self).__init__(
            'neutron_client', neutron_client.Client, *args, **kw)

    def cosmo_list(self, obj_type_single, **kw):
        """ Sugar for list_XXXs()['XXXs'] """
        obj_type_plural = self.cosmo_plural(obj_type_single)
        for obj in getattr(self, 'list_' + obj_type_plural)(**kw)[
                obj_type_plural]:
            yield obj

    def cosmo_delete_resource(self, obj_type_single, obj_id):
        getattr(self, 'delete_' + obj_type_single)(obj_id)

    def get_id_from_resource(self, resource):
        return resource['id']

    def get_name_from_resource(self, resource):
        return resource['name']

    def get_quota(self, obj_type_single):
        tenant_id = self.get_quotas_tenant()['tenant']['tenant_id']
        quotas = self.show_quota(tenant_id)['quota']
        return quotas[obj_type_single]

    def cosmo_list_prefixed(self, obj_type_single, name_prefix):
        for obj in self.cosmo_list(obj_type_single):
            if obj['name'].startswith(name_prefix):
                yield obj

    def cosmo_delete_prefixed(self, name_prefix):
        # Cleanup all neutron.list_XXX() objects with names starting
        #  with self.name_prefix
        for obj_type_single in 'port', 'router', 'network', 'subnet',\
                               'security_group':
            for obj in self.cosmo_list_prefixed(obj_type_single, name_prefix):
                if obj_type_single == 'router':
                    ports = self.cosmo_list('port', device_id=obj['id'])
                    for port in ports:
                        try:
                            self.remove_interface_router(
                                port['device_id'],
                                {'port_id': port['id']})
                        except neutron_exceptions.NeutronClientException:
                            pass
                getattr(self, 'delete_' + obj_type_single)(obj['id'])

    def cosmo_find_external_net(self):
        """ For tests of floating IP """
        nets = self.list_networks()['networks']
        ls = [net for net in nets if net.get('router:external')]
        if len(ls) != 1:
            raise NonRecoverableError(
                "Expected exactly one external network but found {0}".format(
                    len(ls)))
        return ls[0]


class CinderClientWithSugar(OpenStackClient):

    def __init__(self, *args, **kw):
        super(CinderClientWithSugar, self).__init__(
            'cinder_client', partial(cinder_client.Client, '2'), *args, **kw)

    def cosmo_list(self, obj_type_single, **kw):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        for obj in getattr(self, obj_type_plural).findall(**kw):
            yield obj

    def cosmo_delete_resource(self, obj_type_single, obj_id):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        getattr(self, obj_type_plural).delete(obj_id)

    def get_id_from_resource(self, resource):
        return resource.id

    def get_name_from_resource(self, resource):
        return resource.name

    def get_quota(self, obj_type_single):
        # we're already authenticated, but the following call will make
        # 'service_catalog' available under 'client', through which we can
        # extract the tenant_id (Note that self.client.tenant_id might be
        # None if project_id (AKA tenant_name) was used instead; However the
        # actual tenant_id must be used to retrieve the quotas)
        self.client.authenticate()
        project_id = self.client.session.get_project_id()
        quotas = self.quotas.get(project_id)
        return getattr(quotas, self.cosmo_plural(obj_type_single))


class KeystoneClientWithSugar(OpenStackClient):
    # keystone does not have resource quota
    KEYSTONE_INFINITE_RESOURCE_QUOTA = 10**9

    def __init__(self, *args, **kw):
        super(KeystoneClientWithSugar, self).__init__(
            'keystone_client', keystone_client.Client, *args, **kw)

    def cosmo_list(self, obj_type_single, **kw):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        for obj in getattr(self, obj_type_plural).list(**kw):
            yield obj

    def cosmo_delete_resource(self, obj_type_single, obj_id):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        getattr(self, obj_type_plural).delete(obj_id)

    def get_id_from_resource(self, resource):
        return resource.id

    def get_name_from_resource(self, resource):
        return resource.name

    def get_quota(self, obj_type_single):
        return self.KEYSTONE_INFINITE_RESOURCE_QUOTA


class GlanceClientWithSugar(OpenStackClient):
    GLANCE_INIFINITE_RESOURCE_QUOTA = 10**9

    def __init__(self, *args, **kw):
        super(GlanceClientWithSugar, self).__init__(
            'glance_client', partial(glance_client.Client, '2'), *args, **kw)

    def cosmo_list(self, obj_type_single, **kw):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        return getattr(self, obj_type_plural).list(filters=kw)

    def cosmo_delete_resource(self, obj_type_single, obj_id):
        obj_type_plural = self.cosmo_plural(obj_type_single)
        getattr(self, obj_type_plural).delete(obj_id)

    def get_id_from_resource(self, resource):
        return resource.id

    def get_name_from_resource(self, resource):
        return resource.name

    def get_quota(self, obj_type_single):
        return self.GLANCE_INIFINITE_RESOURCE_QUOTA