ARIA multivim plugin initial checkin

Change-Id: I3a24ab6fc5ba54466bfecaf596a13b8907248ae8
Issue-id: SO-77
Signed-off-by: DeWayne Filppi <dewayne@gigaspaces.com>
diff --git a/aria/multivim-plugin/neutron_plugin/__init__.py b/aria/multivim-plugin/neutron_plugin/__init__.py
new file mode 100644
index 0000000..04cb21f
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/__init__.py
@@ -0,0 +1 @@
+__author__ = 'idanmo'
diff --git a/aria/multivim-plugin/neutron_plugin/floatingip.py b/aria/multivim-plugin/neutron_plugin/floatingip.py
new file mode 100644
index 0000000..1a9d044
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/floatingip.py
@@ -0,0 +1,104 @@
+#########
+# 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 cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+from openstack_plugin_common import (
+    with_neutron_client,
+    provider,
+    is_external_relationship,
+    is_external_relationship_not_conditionally_created,
+    OPENSTACK_ID_PROPERTY
+)
+from openstack_plugin_common.floatingip import (
+    use_external_floatingip,
+    set_floatingip_runtime_properties,
+    delete_floatingip,
+    floatingip_creation_validation
+)
+
+
+@operation
+@with_neutron_client
+def create(neutron_client, args, **kwargs):
+
+    if use_external_floatingip(neutron_client, 'floating_ip_address',
+                               lambda ext_fip: ext_fip['floating_ip_address']):
+        return
+
+    floatingip = {
+        # No defaults
+    }
+    floatingip.update(ctx.node.properties['floatingip'], **args)
+
+    # Sugar: floating_network_name -> (resolve) -> floating_network_id
+    if 'floating_network_name' in floatingip:
+        floatingip['floating_network_id'] = neutron_client.cosmo_get_named(
+            'network', floatingip['floating_network_name'])['id']
+        del floatingip['floating_network_name']
+    elif 'floating_network_id' not in floatingip:
+        provider_context = provider(ctx)
+        ext_network = provider_context.ext_network
+        if ext_network:
+            floatingip['floating_network_id'] = ext_network['id']
+        else:
+            raise NonRecoverableError(
+                'Missing floating network id, name or external network')
+
+    fip = neutron_client.create_floatingip(
+        {'floatingip': floatingip})['floatingip']
+    set_floatingip_runtime_properties(fip['id'], fip['floating_ip_address'])
+
+    ctx.logger.info('Floating IP creation response: {0}'.format(fip))
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    delete_floatingip(neutron_client)
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, **kwargs):
+    floatingip_creation_validation(neutron_client, 'floating_ip_address')
+
+
+@operation
+@with_neutron_client
+def connect_port(neutron_client, **kwargs):
+    if is_external_relationship_not_conditionally_created(ctx):
+        return
+
+    port_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    floating_ip_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+    fip = {'port_id': port_id}
+    neutron_client.update_floatingip(floating_ip_id, {'floatingip': fip})
+
+
+@operation
+@with_neutron_client
+def disconnect_port(neutron_client, **kwargs):
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not disassociating floatingip and port since '
+                        'external floatingip and port are being used')
+        return
+
+    floating_ip_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+    fip = {'port_id': None}
+    neutron_client.update_floatingip(floating_ip_id, {'floatingip': fip})
diff --git a/aria/multivim-plugin/neutron_plugin/network.py b/aria/multivim-plugin/neutron_plugin/network.py
new file mode 100644
index 0000000..eadcc3b
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/network.py
@@ -0,0 +1,109 @@
+#########
+# 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 cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+from openstack_plugin_common import (
+    transform_resource_name,
+    with_neutron_client,
+    get_resource_id,
+    is_external_resource,
+    is_external_resource_not_conditionally_created,
+    delete_resource_and_runtime_properties,
+    use_external_resource,
+    validate_resource,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY,
+    COMMON_RUNTIME_PROPERTIES_KEYS
+)
+
+NETWORK_OPENSTACK_TYPE = 'network'
+
+# Runtime properties
+RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS
+
+
+@operation
+@with_neutron_client
+def create(neutron_client, args, **kwargs):
+
+    if use_external_resource(ctx, neutron_client, NETWORK_OPENSTACK_TYPE):
+        return
+
+    network = {
+        'admin_state_up': True,
+        'name': get_resource_id(ctx, NETWORK_OPENSTACK_TYPE),
+    }
+    network.update(ctx.node.properties['network'], **args)
+    transform_resource_name(ctx, network)
+
+    net = neutron_client.create_network({'network': network})['network']
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = net['id']
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] =\
+        NETWORK_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = net['name']
+
+
+@operation
+@with_neutron_client
+def start(neutron_client, **kwargs):
+    network_id = ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+    if is_external_resource_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external network is started')
+        if not neutron_client.show_network(
+                network_id)['network']['admin_state_up']:
+            raise NonRecoverableError(
+                'Expected external resource network {0} to be in '
+                '"admin_state_up"=True'.format(network_id))
+        return
+
+    neutron_client.update_network(
+        network_id, {
+            'network': {
+                'admin_state_up': True
+            }
+        })
+
+
+@operation
+@with_neutron_client
+def stop(neutron_client, **kwargs):
+    if is_external_resource(ctx):
+        ctx.logger.info('Not stopping network since an external network is '
+                        'being used')
+        return
+
+    neutron_client.update_network(
+        ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY], {
+            'network': {
+                'admin_state_up': False
+            }
+        })
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    delete_resource_and_runtime_properties(ctx, neutron_client,
+                                           RUNTIME_PROPERTIES_KEYS)
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, **kwargs):
+    validate_resource(ctx, neutron_client, NETWORK_OPENSTACK_TYPE)
diff --git a/aria/multivim-plugin/neutron_plugin/port.py b/aria/multivim-plugin/neutron_plugin/port.py
new file mode 100644
index 0000000..4db4c44
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/port.py
@@ -0,0 +1,222 @@
+#########
+# 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 cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+
+import neutronclient.common.exceptions as neutron_exceptions
+
+from openstack_plugin_common import (
+    transform_resource_name,
+    with_neutron_client,
+    with_nova_client,
+    get_resource_id,
+    get_openstack_id_of_single_connected_node_by_openstack_type,
+    delete_resource_and_runtime_properties,
+    delete_runtime_properties,
+    use_external_resource,
+    is_external_relationship,
+    validate_resource,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY,
+    COMMON_RUNTIME_PROPERTIES_KEYS,
+    is_external_relationship_not_conditionally_created)
+
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+from neutron_plugin.subnet import SUBNET_OPENSTACK_TYPE
+from openstack_plugin_common.floatingip import get_server_floating_ip
+
+PORT_OPENSTACK_TYPE = 'port'
+
+# Runtime properties
+FIXED_IP_ADDRESS_PROPERTY = 'fixed_ip_address'  # the fixed ip address
+MAC_ADDRESS_PROPERTY = 'mac_address'  # the mac address
+RUNTIME_PROPERTIES_KEYS = \
+    COMMON_RUNTIME_PROPERTIES_KEYS + [FIXED_IP_ADDRESS_PROPERTY,
+                                      MAC_ADDRESS_PROPERTY]
+
+NO_SG_PORT_CONNECTION_RETRY_INTERVAL = 3
+
+
+@operation
+@with_neutron_client
+def create(neutron_client, args, **kwargs):
+
+    ext_port = use_external_resource(ctx, neutron_client, PORT_OPENSTACK_TYPE)
+    if ext_port:
+        try:
+            net_id = \
+                get_openstack_id_of_single_connected_node_by_openstack_type(
+                    ctx, NETWORK_OPENSTACK_TYPE, True)
+
+            if net_id:
+                port_id = ctx.instance.runtime_properties[
+                    OPENSTACK_ID_PROPERTY]
+
+                if neutron_client.show_port(
+                        port_id)['port']['network_id'] != net_id:
+                    raise NonRecoverableError(
+                        'Expected external resources port {0} and network {1} '
+                        'to be connected'.format(port_id, net_id))
+
+            ctx.instance.runtime_properties[FIXED_IP_ADDRESS_PROPERTY] = \
+                _get_fixed_ip(ext_port)
+            ctx.instance.runtime_properties[MAC_ADDRESS_PROPERTY] = \
+                ext_port['mac_address']
+            return
+        except Exception:
+            delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+            raise
+
+    net_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+        ctx, NETWORK_OPENSTACK_TYPE)
+
+    port = {
+        'name': get_resource_id(ctx, PORT_OPENSTACK_TYPE),
+        'network_id': net_id,
+        'security_groups': [],
+    }
+
+    _handle_fixed_ips(port)
+    port.update(ctx.node.properties['port'], **args)
+    transform_resource_name(ctx, port)
+
+    p = neutron_client.create_port({'port': port})['port']
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = p['id']
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] =\
+        PORT_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = p['name']
+    ctx.instance.runtime_properties[FIXED_IP_ADDRESS_PROPERTY] = \
+        _get_fixed_ip(p)
+    ctx.instance.runtime_properties[MAC_ADDRESS_PROPERTY] = p['mac_address']
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    try:
+        delete_resource_and_runtime_properties(ctx, neutron_client,
+                                               RUNTIME_PROPERTIES_KEYS)
+    except neutron_exceptions.NeutronClientException, e:
+        if e.status_code == 404:
+            # port was probably deleted when an attached device was deleted
+            delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+        else:
+            raise
+
+
+@operation
+@with_nova_client
+@with_neutron_client
+def detach(nova_client, neutron_client, **kwargs):
+
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not detaching port from server since '
+                        'external port and server are being used')
+        return
+
+    port_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+    server_floating_ip = get_server_floating_ip(neutron_client, server_id)
+    if server_floating_ip:
+        ctx.logger.info('We have floating ip {0} attached to server'
+                        .format(server_floating_ip['floating_ip_address']))
+        server = nova_client.servers.get(server_id)
+        server.remove_floating_ip(server_floating_ip['floating_ip_address'])
+        return ctx.operation.retry(
+            message='Waiting for the floating ip {0} to '
+                    'detach from server {1}..'
+                    .format(server_floating_ip['floating_ip_address'],
+                            server_id),
+            retry_after=10)
+    change = {
+        'port': {
+            'device_id': '',
+            'device_owner': ''
+        }
+    }
+    ctx.logger.info('Detaching port {0}...'.format(port_id))
+    neutron_client.update_port(port_id, change)
+    ctx.logger.info('Successfully detached port {0}'.format(port_id))
+
+
+@operation
+@with_neutron_client
+def connect_security_group(neutron_client, **kwargs):
+    port_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    security_group_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+
+    if is_external_relationship_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external port and security-group are '
+                        'connected')
+        if any(sg for sg in neutron_client.show_port(port_id)['port'].get(
+                'security_groups', []) if sg == security_group_id):
+            return
+        raise NonRecoverableError(
+            'Expected external resources port {0} and security-group {1} to '
+            'be connected'.format(port_id, security_group_id))
+
+    # WARNING: non-atomic operation
+    port = neutron_client.cosmo_get('port', id=port_id)
+    ctx.logger.info(
+        "connect_security_group(): source_id={0} target={1}".format(
+            port_id, ctx.target.instance.runtime_properties))
+    sgs = port['security_groups'] + [security_group_id]
+    neutron_client.update_port(port_id, {'port': {'security_groups': sgs}})
+
+    # Double check if SG has been actually updated (a race-condition
+    # in OpenStack):
+    port_info = neutron_client.show_port(port_id)['port']
+    port_security_groups = port_info.get('security_groups', [])
+    if security_group_id not in port_security_groups:
+        return ctx.operation.retry(
+            message='Security group connection (`{0}\' -> `{1}\')'
+                    ' has not been established!'.format(port_id,
+                                                        security_group_id),
+            retry_after=NO_SG_PORT_CONNECTION_RETRY_INTERVAL
+        )
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, **kwargs):
+    validate_resource(ctx, neutron_client, PORT_OPENSTACK_TYPE)
+
+
+def _get_fixed_ip(port):
+    # a port may have no fixed IP if it's set on a network without subnets
+    return port['fixed_ips'][0]['ip_address'] if port['fixed_ips'] else None
+
+
+def _handle_fixed_ips(port):
+    fixed_ips_element = {}
+
+    # checking for fixed ip property
+    if ctx.node.properties['fixed_ip']:
+        fixed_ips_element['ip_address'] = ctx.node.properties['fixed_ip']
+
+    # checking for a connected subnet
+    subnet_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+        ctx, SUBNET_OPENSTACK_TYPE, if_exists=True)
+    if subnet_id:
+        fixed_ips_element['subnet_id'] = subnet_id
+
+    # applying fixed ip parameter, if available
+    if fixed_ips_element:
+        port['fixed_ips'] = [fixed_ips_element]
diff --git a/aria/multivim-plugin/neutron_plugin/router.py b/aria/multivim-plugin/neutron_plugin/router.py
new file mode 100644
index 0000000..1a2851e
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/router.py
@@ -0,0 +1,215 @@
+#########
+# 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.
+
+import warnings
+
+from cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+
+from openstack_plugin_common import (
+    provider,
+    transform_resource_name,
+    get_resource_id,
+    with_neutron_client,
+    use_external_resource,
+    is_external_relationship,
+    is_external_relationship_not_conditionally_created,
+    delete_runtime_properties,
+    get_openstack_ids_of_connected_nodes_by_openstack_type,
+    delete_resource_and_runtime_properties,
+    get_resource_by_name_or_id,
+    validate_resource,
+    COMMON_RUNTIME_PROPERTIES_KEYS,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY
+)
+
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+
+
+ROUTER_OPENSTACK_TYPE = 'router'
+
+# Runtime properties
+RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS
+
+
+@operation
+@with_neutron_client
+def create(neutron_client, args, **kwargs):
+
+    if use_external_resource(ctx, neutron_client, ROUTER_OPENSTACK_TYPE):
+        try:
+            ext_net_id_by_rel = _get_connected_ext_net_id(neutron_client)
+
+            if ext_net_id_by_rel:
+                router_id = \
+                    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+                router = neutron_client.show_router(router_id)['router']
+                if not (router['external_gateway_info'] and 'network_id' in
+                        router['external_gateway_info'] and
+                        router['external_gateway_info']['network_id'] ==
+                        ext_net_id_by_rel):
+                    raise NonRecoverableError(
+                        'Expected external resources router {0} and '
+                        'external network {1} to be connected'.format(
+                            router_id, ext_net_id_by_rel))
+            return
+        except Exception:
+            delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+            raise
+
+    router = {
+        'name': get_resource_id(ctx, ROUTER_OPENSTACK_TYPE),
+    }
+    router.update(ctx.node.properties['router'], **args)
+    transform_resource_name(ctx, router)
+
+    _handle_external_network_config(router, neutron_client)
+
+    r = neutron_client.create_router({'router': router})['router']
+
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = r['id']
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] =\
+        ROUTER_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = r['name']
+
+
+@operation
+@with_neutron_client
+def connect_subnet(neutron_client, **kwargs):
+    router_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    subnet_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+    if is_external_relationship_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external subnet and router '
+                        'are associated')
+        for port in neutron_client.list_ports(device_id=router_id)['ports']:
+            for fixed_ip in port.get('fixed_ips', []):
+                if fixed_ip.get('subnet_id') == subnet_id:
+                    return
+        raise NonRecoverableError(
+            'Expected external resources router {0} and subnet {1} to be '
+            'connected'.format(router_id, subnet_id))
+
+    neutron_client.add_interface_router(router_id, {'subnet_id': subnet_id})
+
+
+@operation
+@with_neutron_client
+def disconnect_subnet(neutron_client, **kwargs):
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not connecting subnet and router since external '
+                        'subnet and router are being used')
+        return
+
+    neutron_client.remove_interface_router(
+        ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY], {
+            'subnet_id': ctx.source.instance.runtime_properties[
+                OPENSTACK_ID_PROPERTY]
+        }
+    )
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    delete_resource_and_runtime_properties(ctx, neutron_client,
+                                           RUNTIME_PROPERTIES_KEYS)
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, **kwargs):
+    validate_resource(ctx, neutron_client, ROUTER_OPENSTACK_TYPE)
+
+
+def _insert_ext_net_id_to_router_config(ext_net_id, router):
+    router['external_gateway_info'] = router.get(
+        'external_gateway_info', {})
+    router['external_gateway_info']['network_id'] = ext_net_id
+
+
+def _handle_external_network_config(router, neutron_client):
+    # attempting to find an external network for the router to connect to -
+    # first by either a network name or id passed in explicitly; then by a
+    # network connected by a relationship; with a final optional fallback to an
+    # external network set in the Provider-context. Otherwise the router will
+    # simply not get connected to an external network
+
+    provider_context = provider(ctx)
+
+    ext_net_id_by_rel = _get_connected_ext_net_id(neutron_client)
+    ext_net_by_property = ctx.node.properties['external_network']
+
+    # the following is meant for backwards compatibility with the
+    # 'network_name' sugaring
+    if 'external_gateway_info' in router and 'network_name' in \
+            router['external_gateway_info']:
+        warnings.warn(
+            'Passing external "network_name" inside the '
+            'external_gateway_info key of the "router" property is now '
+            'deprecated; Use the "external_network" property instead',
+            DeprecationWarning)
+
+        ext_net_by_property = router['external_gateway_info']['network_name']
+        del (router['external_gateway_info']['network_name'])
+
+    # need to check if the user explicitly passed network_id in the external
+    # gateway configuration as it affects external network behavior by
+    # relationship and/or provider context
+    if 'external_gateway_info' in router and 'network_id' in \
+            router['external_gateway_info']:
+        ext_net_by_property = router['external_gateway_info']['network_name']
+
+    if ext_net_by_property and ext_net_id_by_rel:
+        raise RuntimeError(
+            "Router can't have an external network connected by both a "
+            'relationship and by a network name/id')
+
+    if ext_net_by_property:
+        ext_net_id = get_resource_by_name_or_id(
+            ext_net_by_property, NETWORK_OPENSTACK_TYPE, neutron_client)['id']
+        _insert_ext_net_id_to_router_config(ext_net_id, router)
+    elif ext_net_id_by_rel:
+        _insert_ext_net_id_to_router_config(ext_net_id_by_rel, router)
+    elif ctx.node.properties['default_to_managers_external_network'] and \
+            provider_context.ext_network:
+        _insert_ext_net_id_to_router_config(provider_context.ext_network['id'],
+                                            router)
+
+
+def _check_if_network_is_external(neutron_client, network_id):
+    return neutron_client.show_network(
+        network_id)['network']['router:external']
+
+
+def _get_connected_ext_net_id(neutron_client):
+    ext_net_ids = \
+        [net_id
+            for net_id in
+            get_openstack_ids_of_connected_nodes_by_openstack_type(
+                ctx, NETWORK_OPENSTACK_TYPE) if
+            _check_if_network_is_external(neutron_client, net_id)]
+
+    if len(ext_net_ids) > 1:
+        raise NonRecoverableError(
+            'More than one external network is connected to router {0}'
+            ' by a relationship; External network IDs: {0}'.format(
+                ext_net_ids))
+
+    return ext_net_ids[0] if ext_net_ids else None
diff --git a/aria/multivim-plugin/neutron_plugin/security_group.py b/aria/multivim-plugin/neutron_plugin/security_group.py
new file mode 100644
index 0000000..5f335f4
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/security_group.py
@@ -0,0 +1,130 @@
+#########
+# 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 time import sleep
+
+from requests.exceptions import RequestException
+
+from cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+from openstack_plugin_common import (
+    transform_resource_name,
+    with_neutron_client,
+    delete_resource_and_runtime_properties,
+)
+from openstack_plugin_common.security_group import (
+    build_sg_data,
+    process_rules,
+    use_external_sg,
+    set_sg_runtime_properties,
+    delete_sg,
+    sg_creation_validation,
+    RUNTIME_PROPERTIES_KEYS
+)
+
+DEFAULT_RULE_VALUES = {
+    'direction': 'ingress',
+    'ethertype': 'IPv4',
+    'port_range_min': 1,
+    'port_range_max': 65535,
+    'protocol': 'tcp',
+    'remote_group_id': None,
+    'remote_ip_prefix': '0.0.0.0/0',
+}
+
+
+@operation
+@with_neutron_client
+def create(
+    neutron_client, args,
+    status_attempts=10, status_timeout=2, **kwargs
+):
+
+    security_group = build_sg_data(args)
+    if not security_group['description']:
+        security_group['description'] = ctx.node.properties['description']
+
+    sg_rules = process_rules(neutron_client, DEFAULT_RULE_VALUES,
+                             'remote_ip_prefix', 'remote_group_id',
+                             'port_range_min', 'port_range_max')
+
+    disable_default_egress_rules = ctx.node.properties.get(
+        'disable_default_egress_rules')
+
+    if use_external_sg(neutron_client):
+        return
+
+    transform_resource_name(ctx, security_group)
+
+    sg = neutron_client.create_security_group(
+        {'security_group': security_group})['security_group']
+
+    for attempt in range(max(status_attempts, 1)):
+        sleep(status_timeout)
+        try:
+            neutron_client.show_security_group(sg['id'])
+        except RequestException as e:
+            ctx.logger.debug("Waiting for SG to be visible. Attempt {}".format(
+                attempt))
+        else:
+            break
+    else:
+        raise NonRecoverableError(
+            "Timed out waiting for security_group to exist", e)
+
+    set_sg_runtime_properties(sg, neutron_client)
+
+    try:
+        if disable_default_egress_rules:
+            for er in _egress_rules(_rules_for_sg_id(neutron_client,
+                                                     sg['id'])):
+                neutron_client.delete_security_group_rule(er['id'])
+
+        for sgr in sg_rules:
+            sgr['security_group_id'] = sg['id']
+            neutron_client.create_security_group_rule(
+                {'security_group_rule': sgr})
+    except Exception:
+        try:
+            delete_resource_and_runtime_properties(
+                ctx, neutron_client,
+                RUNTIME_PROPERTIES_KEYS)
+        except Exception as e:
+            raise NonRecoverableError(
+                'Exception while tearing down for retry', e)
+        raise
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    delete_sg(neutron_client)
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, **kwargs):
+    sg_creation_validation(neutron_client, 'remote_ip_prefix')
+
+
+def _egress_rules(rules):
+    return [rule for rule in rules if rule.get('direction') == 'egress']
+
+
+def _rules_for_sg_id(neutron_client, id):
+    rules = neutron_client.list_security_group_rules()['security_group_rules']
+    rules = [rule for rule in rules if rule['security_group_id'] == id]
+    return rules
diff --git a/aria/multivim-plugin/neutron_plugin/subnet.py b/aria/multivim-plugin/neutron_plugin/subnet.py
new file mode 100644
index 0000000..6e97c96
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/subnet.py
@@ -0,0 +1,101 @@
+#########
+# 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 cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+from openstack_plugin_common import (
+    with_neutron_client,
+    transform_resource_name,
+    get_resource_id,
+    get_openstack_id_of_single_connected_node_by_openstack_type,
+    delete_resource_and_runtime_properties,
+    delete_runtime_properties,
+    use_external_resource,
+    validate_resource,
+    validate_ip_or_range_syntax,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY,
+    COMMON_RUNTIME_PROPERTIES_KEYS
+)
+
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+
+SUBNET_OPENSTACK_TYPE = 'subnet'
+
+# Runtime properties
+RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS
+
+
+@operation
+@with_neutron_client
+def create(neutron_client, args, **kwargs):
+
+    if use_external_resource(ctx, neutron_client, SUBNET_OPENSTACK_TYPE):
+        try:
+            net_id = \
+                get_openstack_id_of_single_connected_node_by_openstack_type(
+                    ctx, NETWORK_OPENSTACK_TYPE, True)
+
+            if net_id:
+                subnet_id = \
+                    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+                if neutron_client.show_subnet(
+                        subnet_id)['subnet']['network_id'] != net_id:
+                    raise NonRecoverableError(
+                        'Expected external resources subnet {0} and network'
+                        ' {1} to be connected'.format(subnet_id, net_id))
+            return
+        except Exception:
+            delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+            raise
+
+    net_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+        ctx, NETWORK_OPENSTACK_TYPE)
+    subnet = {
+        'name': get_resource_id(ctx, SUBNET_OPENSTACK_TYPE),
+        'network_id': net_id,
+    }
+    subnet.update(ctx.node.properties['subnet'], **args)
+    transform_resource_name(ctx, subnet)
+
+    s = neutron_client.create_subnet({'subnet': subnet})['subnet']
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = s['id']
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \
+        SUBNET_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = subnet['name']
+
+
+@operation
+@with_neutron_client
+def delete(neutron_client, **kwargs):
+    delete_resource_and_runtime_properties(ctx, neutron_client,
+                                           RUNTIME_PROPERTIES_KEYS)
+
+
+@operation
+@with_neutron_client
+def creation_validation(neutron_client, args, **kwargs):
+    validate_resource(ctx, neutron_client, SUBNET_OPENSTACK_TYPE)
+    subnet = dict(ctx.node.properties['subnet'], **args)
+
+    if 'cidr' not in subnet:
+        err = '"cidr" property must appear under the "subnet" property of a ' \
+              'subnet node'
+        ctx.logger.error('VALIDATION ERROR: ' + err)
+        raise NonRecoverableError(err)
+    validate_ip_or_range_syntax(ctx, subnet['cidr'])
diff --git a/aria/multivim-plugin/neutron_plugin/tests/__init__.py b/aria/multivim-plugin/neutron_plugin/tests/__init__.py
new file mode 100644
index 0000000..04cb21f
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/tests/__init__.py
@@ -0,0 +1 @@
+__author__ = 'idanmo'
diff --git a/aria/multivim-plugin/neutron_plugin/tests/test.py b/aria/multivim-plugin/neutron_plugin/tests/test.py
new file mode 100644
index 0000000..459c23a
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/tests/test.py
@@ -0,0 +1,220 @@
+#########
+# 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.
+
+import mock
+import random
+import string
+import unittest
+
+from cloudify.exceptions import NonRecoverableError
+from cloudify.context import BootstrapContext
+
+from cloudify.mocks import MockCloudifyContext
+
+import openstack_plugin_common as common
+import openstack_plugin_common.tests.test as common_test
+
+import neutron_plugin
+import neutron_plugin.network
+import neutron_plugin.port
+import neutron_plugin.router
+import neutron_plugin.security_group
+
+
+class ResourcesRenamingTest(unittest.TestCase):
+    def setUp(self):
+        neutron_plugin.port._find_network_in_related_nodes = mock.Mock()
+        # *** Configs from files ********************
+        common.Config.get = mock.Mock()
+        common.Config.get.return_value = {}
+        # *** Neutron ********************
+        self.neutron_mock = mock.Mock()
+
+        def neutron_mock_connect(unused_self, unused_cfg):
+            return self.neutron_mock
+        common.NeutronClient.connect = neutron_mock_connect
+
+        self.neutron_mock.cosmo_list = mock.Mock()
+        self.neutron_mock.cosmo_list.return_value = []
+
+    def _setup_ctx(self, obj_type):
+        ctx = common_test.create_mock_ctx_with_provider_info(
+            node_id='__cloudify_id_something_001',
+            properties={
+                obj_type: {
+                    'name': obj_type + '_name',
+                },
+                'rules': []  # For security_group
+            }
+        )
+        return ctx
+
+    def _test(self, obj_type):
+        ctx = self._setup_ctx(obj_type)
+        attr = getattr(self.neutron_mock, 'create_' + obj_type)
+        attr.return_value = {
+            obj_type: {
+                'id': obj_type + '_id',
+            }
+        }
+        getattr(neutron_plugin, obj_type).create(ctx)
+        calls = attr.mock_calls
+        self.assertEquals(len(calls), 1)  # Exactly one object created
+        # Indexes into call[]:
+        # 0 - the only call
+        # 1 - regular arguments
+        # 0 - first argument
+        arg = calls[0][1][0]
+        self.assertEquals(arg[obj_type]['name'], 'p2_' + obj_type + '_name')
+
+    def test_network(self):
+        self._test('network')
+
+    def test_port(self):
+        self._test('port')
+
+    def test_router(self):
+        self._test('router')
+
+    def test_security_group(self):
+        self._test('security_group')
+
+    # Network chosen arbitrary for this test.
+    # Just testing something without prefix.
+    def test_network_no_prefix(self):
+        ctx = self._setup_ctx('network')
+        for pctx in common_test.BOOTSTRAP_CONTEXTS_WITHOUT_PREFIX:
+            ctx._bootstrap_context = BootstrapContext(pctx)
+            self.neutron_mock.create_network.reset_mock()
+            self.neutron_mock.create_network.return_value = {
+                'network': {
+                    'id': 'network_id',
+                }
+            }
+            neutron_plugin.network.create(ctx)
+            calls = self.neutron_mock.create_network.mock_calls
+            self.assertEquals(len(calls), 1)  # Exactly one network created
+            # Indexes into call[]:
+            # 0 - the only call
+            # 1 - regular arguments
+            # 0 - first argument
+            arg = calls[0][1][0]
+            self.assertEquals(arg['network']['name'], 'network_name',
+                              "Failed with context: " + str(pctx))
+
+
+def _rand_str(n):
+    chars = string.ascii_uppercase + string.digits
+    return ''.join(random.choice(chars) for _ in range(n))
+
+
+class SecurityGroupTest(unittest.TestCase):
+    def setUp(self):
+        # *** Configs from files ********************
+        common.Config.get = mock.Mock()
+        common.Config.get.return_value = {}
+        # *** Neutron ********************
+        self.neutron_mock = mock.Mock()
+
+        def neutron_mock_connect(unused_self, unused_cfg):
+            return self.neutron_mock
+        common.NeutronClient.connect = neutron_mock_connect
+        neutron_plugin.security_group._rules_for_sg_id = mock.Mock()
+        neutron_plugin.security_group._rules_for_sg_id.return_value = []
+
+    def _setup_ctx(self):
+        sg_name = _rand_str(6) + '_new'
+        ctx = MockCloudifyContext(properties={
+            'security_group': {
+                'name': sg_name,
+                'description': 'blah'
+            },
+            'rules': [{'port': 80}],
+            'disable_default_egress_rules': True,
+        })
+        return ctx
+
+    def test_sg_new(self):
+        ctx = self._setup_ctx()
+        self.neutron_mock.cosmo_list = mock.Mock()
+        self.neutron_mock.cosmo_list.return_value = []
+        self.neutron_mock.create_security_group = mock.Mock()
+        self.neutron_mock.create_security_group.return_value = {
+            'security_group': {
+                'description': 'blah',
+                'id': ctx['security_group']['name'] + '_id',
+            }
+        }
+        neutron_plugin.security_group.create(ctx)
+        self.assertTrue(self.neutron_mock.create_security_group.mock_calls)
+
+    def test_sg_use_existing(self):
+        ctx = self._setup_ctx()
+        self.neutron_mock.cosmo_list = mock.Mock()
+        self.neutron_mock.cosmo_list.return_value = [{
+            'id': ctx['security_group']['name'] + '_existing_id',
+            'description': 'blah',
+            'security_group_rules': [{
+                'remote_group_id': None,
+                'direction': 'ingress',
+                'protocol': 'tcp',
+                'ethertype': 'IPv4',
+                'port_range_max': 80,
+                'port_range_min': 80,
+                'remote_ip_prefix': '0.0.0.0/0',
+            }]
+        }]
+        self.neutron_mock.create_security_group = mock.Mock()
+        self.neutron_mock.create_security_group.return_value = {
+            'security_group': {
+                'description': 'blah',
+                'id': ctx['security_group']['name'] + '_id',
+            }
+        }
+        neutron_plugin.security_group.create(ctx)
+        self.assertFalse(self.neutron_mock.create_security_group.mock_calls)
+
+    def test_sg_use_existing_with_other_rules(self):
+        ctx = self._setup_ctx()
+        self.neutron_mock.cosmo_list = mock.Mock()
+        self.neutron_mock.cosmo_list.return_value = [{
+            'id': ctx['security_group']['name'] + '_existing_id',
+            'description': 'blah',
+            'security_group_rules': [{
+                'remote_group_id': None,
+                'direction': 'ingress',
+                'protocol': 'tcp',
+                'ethertype': 'IPv4',
+                'port_range_max': 81,  # Note the different port!
+                'port_range_min': 81,  # Note the different port!
+                'remote_ip_prefix': '0.0.0.0/0',
+            }]
+        }]
+        self.neutron_mock.create_security_group = mock.Mock()
+        self.neutron_mock.create_security_group.return_value = {
+            'security_group': {
+                'description': 'blah',
+                'id': ctx['security_group']['name'] + '_id',
+            }
+        }
+        self.assertRaises(
+            NonRecoverableError,
+            neutron_plugin.security_group.create,
+            ctx
+        )
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/aria/multivim-plugin/neutron_plugin/tests/test_port.py b/aria/multivim-plugin/neutron_plugin/tests/test_port.py
new file mode 100644
index 0000000..1acee3d
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/tests/test_port.py
@@ -0,0 +1,156 @@
+########
+# 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.
+
+import unittest
+
+import mock
+
+import neutron_plugin.port
+from cloudify.mocks import (MockCloudifyContext,
+                            MockNodeInstanceContext,
+                            MockRelationshipSubjectContext)
+from openstack_plugin_common import (NeutronClientWithSugar,
+                                     OPENSTACK_ID_PROPERTY)
+from cloudify.exceptions import OperationRetry
+
+
+class TestPort(unittest.TestCase):
+
+    def test_fixed_ips_no_fixed_ips(self):
+        node_props = {'fixed_ip': ''}
+
+        with mock.patch(
+                'neutron_plugin.port.'
+                'get_openstack_id_of_single_connected_node_by_openstack_type',
+                self._get_connected_subnet_mock(return_empty=True)):
+            with mock.patch(
+                    'neutron_plugin.port.ctx',
+                    self._get_mock_ctx_with_node_properties(node_props)):
+
+                port = {}
+                neutron_plugin.port._handle_fixed_ips(port)
+
+        self.assertNotIn('fixed_ips', port)
+
+    def test_fixed_ips_subnet_only(self):
+        node_props = {'fixed_ip': ''}
+
+        with mock.patch(
+                'neutron_plugin.port.'
+                'get_openstack_id_of_single_connected_node_by_openstack_type',
+                self._get_connected_subnet_mock(return_empty=False)):
+            with mock.patch(
+                    'neutron_plugin.port.ctx',
+                    self._get_mock_ctx_with_node_properties(node_props)):
+
+                port = {}
+                neutron_plugin.port._handle_fixed_ips(port)
+
+        self.assertEquals([{'subnet_id': 'some-subnet-id'}],
+                          port.get('fixed_ips'))
+
+    def test_fixed_ips_ip_address_only(self):
+        node_props = {'fixed_ip': '1.2.3.4'}
+
+        with mock.patch(
+                'neutron_plugin.port.'
+                'get_openstack_id_of_single_connected_node_by_openstack_type',
+                self._get_connected_subnet_mock(return_empty=True)):
+            with mock.patch(
+                    'neutron_plugin.port.ctx',
+                    self._get_mock_ctx_with_node_properties(node_props)):
+
+                port = {}
+                neutron_plugin.port._handle_fixed_ips(port)
+
+        self.assertEquals([{'ip_address': '1.2.3.4'}],
+                          port.get('fixed_ips'))
+
+    def test_fixed_ips_subnet_and_ip_address(self):
+        node_props = {'fixed_ip': '1.2.3.4'}
+
+        with mock.patch(
+                'neutron_plugin.port.'
+                'get_openstack_id_of_single_connected_node_by_openstack_type',
+                self._get_connected_subnet_mock(return_empty=False)):
+            with mock.patch(
+                    'neutron_plugin.port.ctx',
+                    self._get_mock_ctx_with_node_properties(node_props)):
+
+                port = {}
+                neutron_plugin.port._handle_fixed_ips(port)
+
+        self.assertEquals([{'ip_address': '1.2.3.4',
+                            'subnet_id': 'some-subnet-id'}],
+                          port.get('fixed_ips'))
+
+    @staticmethod
+    def _get_connected_subnet_mock(return_empty=True):
+        return lambda *args, **kw: None if return_empty else 'some-subnet-id'
+
+    @staticmethod
+    def _get_mock_ctx_with_node_properties(properties):
+        return MockCloudifyContext(node_id='test_node_id',
+                                   properties=properties)
+
+
+class MockNeutronClient(NeutronClientWithSugar):
+    """A fake neutron client with hard-coded test data."""
+    def __init__(self, update):
+        self.update = update
+        self.body = {'port': {'id': 'test-id', 'security_groups': []}}
+
+    def show_port(self, *_):
+        return self.body
+
+    def update_port(self, _, b, **__):
+        if self.update:
+            self.body.update(b)
+        return
+
+    def cosmo_get(self, *_, **__):
+        return self.body['port']
+
+
+class TestPortSG(unittest.TestCase):
+    @mock.patch('openstack_plugin_common._put_client_in_kw')
+    def test_connect_sg_to_port(self, *_):
+        mock_neutron = MockNeutronClient(update=True)
+        ctx = MockCloudifyContext(
+            source=MockRelationshipSubjectContext(node=mock.MagicMock(),
+                                                  instance=mock.MagicMock()),
+            target=MockRelationshipSubjectContext(node=mock.MagicMock(),
+                                                  instance=mock.MagicMock()))
+
+        with mock.patch('neutron_plugin.port.ctx', ctx):
+            neutron_plugin.port.connect_security_group(mock_neutron)
+            self.assertIsNone(ctx.operation._operation_retry)
+
+    @mock.patch('openstack_plugin_common._put_client_in_kw')
+    def test_connect_sg_to_port_race_condition(self, *_):
+        mock_neutron = MockNeutronClient(update=False)
+
+        ctx = MockCloudifyContext(
+            source=MockRelationshipSubjectContext(node=mock.MagicMock(),
+                                                  instance=mock.MagicMock()),
+            target=MockRelationshipSubjectContext(
+                node=mock.MagicMock(),
+                instance=MockNodeInstanceContext(
+                    runtime_properties={
+                        OPENSTACK_ID_PROPERTY: 'test-sg-id'})))
+        with mock.patch('neutron_plugin.port.ctx', ctx):
+            neutron_plugin.port.connect_security_group(mock_neutron, ctx=ctx)
+            self.assertIsInstance(ctx.operation._operation_retry,
+                                  OperationRetry)
diff --git a/aria/multivim-plugin/neutron_plugin/tests/test_security_group.py b/aria/multivim-plugin/neutron_plugin/tests/test_security_group.py
new file mode 100644
index 0000000..e958cdd
--- /dev/null
+++ b/aria/multivim-plugin/neutron_plugin/tests/test_security_group.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+#########
+# Copyright (c) 2016 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.
+
+import unittest
+
+from mock import Mock, patch
+from requests.exceptions import RequestException
+
+from neutron_plugin import security_group
+
+from cloudify.exceptions import NonRecoverableError
+from cloudify.state import current_ctx
+
+from cloudify.mocks import MockCloudifyContext
+
+
+class FakeException(Exception):
+    pass
+
+
+@patch('openstack_plugin_common.OpenStackClient._validate_auth_params')
+@patch('openstack_plugin_common.NeutronClientWithSugar')
+class TestSecurityGroup(unittest.TestCase):
+
+    def setUp(self):
+        super(TestSecurityGroup, self).setUp()
+        self.nova_client = Mock()
+
+        self.ctx = MockCloudifyContext(
+            node_id='test',
+            deployment_id='test',
+            properties={
+                'description': 'The best Security Group. Great',
+                'rules': [],
+                'resource_id': 'mock_sg',
+                'security_group': {
+                },
+                'server': {},
+                'openstack_config': {
+                    'auth_url': 'things/v3',
+                },
+            },
+            operation={'retry_number': 0},
+            provider_context={'resources': {}}
+        )
+        current_ctx.set(self.ctx)
+        self.addCleanup(current_ctx.clear)
+
+        findctx = patch(
+            'openstack_plugin_common._find_context_in_kw',
+            return_value=self.ctx,
+        )
+        findctx.start()
+        self.addCleanup(findctx.stop)
+
+    def test_set_sg_runtime_properties(self, mock_nc, *_):
+        security_group.create(
+            nova_client=self.nova_client,
+            ctx=self.ctx,
+            args={},
+            )
+
+        self.assertEqual(
+            {
+                'external_type': 'security_group',
+                'external_id': mock_nc().get_id_from_resource(),
+                'external_name': mock_nc().get_name_from_resource(),
+            },
+            self.ctx.instance.runtime_properties
+        )
+
+    def test_create_sg_wait_timeout(self, mock_nc, *_):
+        mock_nc().show_security_group.side_effect = RequestException
+
+        with self.assertRaises(NonRecoverableError):
+            security_group.create(
+                nova_client=self.nova_client,
+                ctx=self.ctx,
+                args={},
+                status_attempts=3,
+                status_timeout=0.001,
+                )
+
+    @patch(
+        'neutron_plugin.security_group.delete_resource_and_runtime_properties')
+    def test_dont_duplicate_if_failed_rule(self, mock_del_res, mock_nc, *_):
+        self.ctx.node.properties['rules'] = [
+            {
+                'port': '🍷',
+            },
+        ]
+        mock_nc().create_security_group_rule.side_effect = FakeException
+        mock_del_res.side_effect = FakeException('the 2nd')
+
+        with self.assertRaises(NonRecoverableError) as e:
+            security_group.create(
+                nova_client=self.nova_client,
+                ctx=self.ctx,
+                args={},
+                )
+
+        self.assertIn('the 2nd', str(e.exception))