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/nova_plugin/__init__.py b/aria/multivim-plugin/nova_plugin/__init__.py
new file mode 100644
index 0000000..bb53327
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/__init__.py
@@ -0,0 +1,16 @@
+#########
+# 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.
+
+__author__ = 'idanmo'
diff --git a/aria/multivim-plugin/nova_plugin/floatingip.py b/aria/multivim-plugin/nova_plugin/floatingip.py
new file mode 100644
index 0000000..e770c54
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/floatingip.py
@@ -0,0 +1,60 @@
+#########
+# 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 openstack_plugin_common import with_nova_client
+from openstack_plugin_common.floatingip import (
+    use_external_floatingip,
+    set_floatingip_runtime_properties,
+    delete_floatingip,
+    floatingip_creation_validation
+)
+
+
+# random note regarding nova floating-ips: floating ips on nova-net have
+# pre-assigned ids, and thus a call "nova.floating_ips.get(<fip_id>)" will
+# return a value even if the floating-ip isn't even allocated.
+# currently all lookups in the code, including by id, use search (i.e.
+# nova.<type>.findall) and lists, which won't return such unallocated
+# resources.
+
+@operation
+@with_nova_client
+def create(nova_client, args, **kwargs):
+
+    if use_external_floatingip(nova_client, 'ip',
+                               lambda ext_fip: ext_fip.ip):
+        return
+
+    floatingip = {
+        'pool': None
+    }
+    floatingip.update(ctx.node.properties['floatingip'], **args)
+
+    fip = nova_client.floating_ips.create(floatingip['pool'])
+    set_floatingip_runtime_properties(fip.id, fip.ip)
+
+
+@operation
+@with_nova_client
+def delete(nova_client, **kwargs):
+    delete_floatingip(nova_client)
+
+
+@operation
+@with_nova_client
+def creation_validation(nova_client, **kwargs):
+    floatingip_creation_validation(nova_client, 'ip')
diff --git a/aria/multivim-plugin/nova_plugin/keypair.py b/aria/multivim-plugin/nova_plugin/keypair.py
new file mode 100644
index 0000000..92281ab
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/keypair.py
@@ -0,0 +1,202 @@
+#########
+# 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 os
+import errno
+from getpass import getuser
+
+from cloudify import ctx
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError
+from openstack_plugin_common import (
+    with_nova_client,
+    validate_resource,
+    use_external_resource,
+    transform_resource_name,
+    is_external_resource,
+    is_external_resource_not_conditionally_created,
+    delete_runtime_properties,
+    get_resource_id,
+    delete_resource_and_runtime_properties,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY,
+    COMMON_RUNTIME_PROPERTIES_KEYS
+)
+
+RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS
+KEYPAIR_OPENSTACK_TYPE = 'keypair'
+
+PRIVATE_KEY_PATH_PROP = 'private_key_path'
+
+
+@operation
+@with_nova_client
+def create(nova_client, args, **kwargs):
+
+    private_key_path = _get_private_key_path()
+    pk_exists = _check_private_key_exists(private_key_path)
+
+    if use_external_resource(ctx, nova_client, KEYPAIR_OPENSTACK_TYPE):
+        if not pk_exists:
+            delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+            raise NonRecoverableError(
+                'Failed to use external keypair (node {0}): the public key {1}'
+                ' is available on Openstack, but the private key could not be '
+                'found at {2}'.format(ctx.node.id,
+                                      ctx.node.properties['resource_id'],
+                                      private_key_path))
+        return
+
+    if pk_exists:
+        raise NonRecoverableError(
+            "Can't create keypair - private key path already exists: {0}"
+            .format(private_key_path))
+
+    keypair = {
+        'name': get_resource_id(ctx, KEYPAIR_OPENSTACK_TYPE),
+    }
+    keypair.update(ctx.node.properties['keypair'], **args)
+    transform_resource_name(ctx, keypair)
+
+    keypair = nova_client.keypairs.create(keypair['name'],
+                                          keypair.get('public_key'))
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = keypair.id
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \
+        KEYPAIR_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = keypair.name
+
+    try:
+        # write private key file
+        _mkdir_p(os.path.dirname(private_key_path))
+        with open(private_key_path, 'w') as f:
+            f.write(keypair.private_key)
+        os.chmod(private_key_path, 0600)
+    except Exception:
+        _delete_private_key_file()
+        delete_resource_and_runtime_properties(ctx, nova_client,
+                                               RUNTIME_PROPERTIES_KEYS)
+        raise
+
+
+@operation
+@with_nova_client
+def delete(nova_client, **kwargs):
+    if not is_external_resource(ctx):
+        ctx.logger.info('deleting keypair')
+
+        _delete_private_key_file()
+
+        nova_client.keypairs.delete(
+            ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY])
+    else:
+        ctx.logger.info('not deleting keypair since an external keypair is '
+                        'being used')
+
+    delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+
+
+@operation
+@with_nova_client
+def creation_validation(nova_client, **kwargs):
+
+    def validate_private_key_permissions(private_key_path):
+        ctx.logger.debug('checking whether private key file {0} has the '
+                         'correct permissions'.format(private_key_path))
+        if not os.access(private_key_path, os.R_OK):
+            err = 'private key file {0} is not readable'\
+                .format(private_key_path)
+            ctx.logger.error('VALIDATION ERROR: ' + err)
+            raise NonRecoverableError(err)
+        ctx.logger.debug('OK: private key file {0} has the correct '
+                         'permissions'.format(private_key_path))
+
+    def validate_path_owner(path):
+        ctx.logger.debug('checking whether directory {0} is owned by the '
+                         'current user'.format(path))
+        from pwd import getpwnam, getpwuid
+
+        user = getuser()
+        owner = getpwuid(os.stat(path).st_uid).pw_name
+        current_user_id = str(getpwnam(user).pw_uid)
+        owner_id = str(os.stat(path).st_uid)
+
+        if not current_user_id == owner_id:
+            err = '{0} is not owned by the current user (it is owned by {1})'\
+                  .format(path, owner)
+            ctx.logger.warning('VALIDATION WARNING: {0}'.format(err))
+            return
+        ctx.logger.debug('OK: {0} is owned by the current user'.format(path))
+
+    validate_resource(ctx, nova_client, KEYPAIR_OPENSTACK_TYPE)
+
+    private_key_path = _get_private_key_path()
+    pk_exists = _check_private_key_exists(private_key_path)
+
+    if is_external_resource_not_conditionally_created(ctx):
+        if pk_exists:
+            if os.name == 'posix':
+                validate_private_key_permissions(private_key_path)
+                validate_path_owner(private_key_path)
+        else:
+            err = "can't use external keypair: the public key {0} is " \
+                  "available on Openstack, but the private key could not be " \
+                  "found at {1}".format(ctx.node.properties['resource_id'],
+                                        private_key_path)
+            ctx.logger.error('VALIDATION ERROR: {0}'.format(err))
+            raise NonRecoverableError(err)
+    else:
+        if pk_exists:
+            err = 'private key path already exists: {0}'.format(
+                private_key_path)
+            ctx.logger.error('VALIDATION ERROR: {0}'.format(err))
+            raise NonRecoverableError(err)
+        else:
+            err = 'private key directory {0} is not writable'
+            while private_key_path:
+                if os.path.isdir(private_key_path):
+                    if not os.access(private_key_path, os.W_OK | os.X_OK):
+                        raise NonRecoverableError(err.format(private_key_path))
+                    else:
+                        break
+                private_key_path, _ = os.path.split(private_key_path)
+
+    ctx.logger.debug('OK: keypair configuration is valid')
+
+
+def _get_private_key_path():
+    return os.path.expanduser(ctx.node.properties[PRIVATE_KEY_PATH_PROP])
+
+
+def _delete_private_key_file():
+    private_key_path = _get_private_key_path()
+    ctx.logger.debug('deleting private key file at {0}'.format(
+        private_key_path))
+    try:
+        os.remove(private_key_path)
+    except OSError as e:
+        if e.errno == errno.ENOENT:
+            # file was already deleted somehow
+            pass
+        raise
+
+
+def _check_private_key_exists(private_key_path):
+    return os.path.isfile(private_key_path)
+
+
+def _mkdir_p(path):
+    if path and not os.path.isdir(path):
+        os.makedirs(path)
diff --git a/aria/multivim-plugin/nova_plugin/security_group.py b/aria/multivim-plugin/nova_plugin/security_group.py
new file mode 100644
index 0000000..283eae8
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/security_group.py
@@ -0,0 +1,81 @@
+#########
+# 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 openstack_plugin_common import (
+    transform_resource_name,
+    with_nova_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
+)
+
+
+@operation
+@with_nova_client
+def create(nova_client, args, **kwargs):
+
+    security_group = build_sg_data(args)
+    security_group['description'] = ctx.node.properties['description']
+
+    sgr_default_values = {
+        'ip_protocol': 'tcp',
+        'from_port': 1,
+        'to_port': 65535,
+        'cidr': '0.0.0.0/0',
+        # 'group_id': None,
+        # 'parent_group_id': None,
+    }
+    sg_rules = process_rules(nova_client, sgr_default_values,
+                             'cidr', 'group_id', 'from_port', 'to_port')
+
+    if use_external_sg(nova_client):
+        return
+
+    transform_resource_name(ctx, security_group)
+
+    sg = nova_client.security_groups.create(
+        security_group['name'], security_group['description'])
+
+    set_sg_runtime_properties(sg, nova_client)
+
+    try:
+        for sgr in sg_rules:
+            sgr['parent_group_id'] = sg.id
+            nova_client.security_group_rules.create(**sgr)
+    except Exception:
+        delete_resource_and_runtime_properties(ctx, nova_client,
+                                               RUNTIME_PROPERTIES_KEYS)
+        raise
+
+
+@operation
+@with_nova_client
+def delete(nova_client, **kwargs):
+    delete_sg(nova_client)
+
+
+@operation
+@with_nova_client
+def creation_validation(nova_client, **kwargs):
+    sg_creation_validation(nova_client, 'cidr')
diff --git a/aria/multivim-plugin/nova_plugin/server.py b/aria/multivim-plugin/nova_plugin/server.py
new file mode 100644
index 0000000..6726f24
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/server.py
@@ -0,0 +1,944 @@
+#########
+# 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 os
+import time
+import copy
+import operator
+
+from novaclient import exceptions as nova_exceptions
+
+from cloudify import ctx
+from cloudify.manager import get_rest_client
+from cloudify.decorators import operation
+from cloudify.exceptions import NonRecoverableError, RecoverableError
+from cinder_plugin import volume
+from openstack_plugin_common import (
+    provider,
+    transform_resource_name,
+    get_resource_id,
+    get_openstack_ids_of_connected_nodes_by_openstack_type,
+    with_nova_client,
+    with_cinder_client,
+    assign_payload_as_runtime_properties,
+    get_openstack_id_of_single_connected_node_by_openstack_type,
+    get_openstack_names_of_connected_nodes_by_openstack_type,
+    get_single_connected_node_by_openstack_type,
+    is_external_resource,
+    is_external_resource_by_properties,
+    is_external_resource_not_conditionally_created,
+    is_external_relationship_not_conditionally_created,
+    use_external_resource,
+    delete_runtime_properties,
+    is_external_relationship,
+    validate_resource,
+    USE_EXTERNAL_RESOURCE_PROPERTY,
+    OPENSTACK_AZ_PROPERTY,
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    OPENSTACK_NAME_PROPERTY,
+    COMMON_RUNTIME_PROPERTIES_KEYS,
+    with_neutron_client)
+from nova_plugin.keypair import KEYPAIR_OPENSTACK_TYPE
+from nova_plugin import userdata
+from openstack_plugin_common.floatingip import (IP_ADDRESS_PROPERTY,
+                                                get_server_floating_ip)
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+from neutron_plugin.port import PORT_OPENSTACK_TYPE
+from cinder_plugin.volume import VOLUME_OPENSTACK_TYPE
+from openstack_plugin_common.security_group import \
+    SECURITY_GROUP_OPENSTACK_TYPE
+from glance_plugin.image import handle_image_from_relationship
+
+SERVER_OPENSTACK_TYPE = 'server'
+
+# server status constants. Full lists here: http://docs.openstack.org/api/openstack-compute/2/content/List_Servers-d1e2078.html  # NOQA
+SERVER_STATUS_ACTIVE = 'ACTIVE'
+SERVER_STATUS_BUILD = 'BUILD'
+SERVER_STATUS_SHUTOFF = 'SHUTOFF'
+
+OS_EXT_STS_TASK_STATE = 'OS-EXT-STS:task_state'
+SERVER_TASK_STATE_POWERING_ON = 'powering-on'
+
+MUST_SPECIFY_NETWORK_EXCEPTION_TEXT = 'More than one possible network found.'
+SERVER_DELETE_CHECK_SLEEP = 2
+
+# Runtime properties
+NETWORKS_PROPERTY = 'networks'  # all of the server's ips
+IP_PROPERTY = 'ip'  # the server's private ip
+ADMIN_PASSWORD_PROPERTY = 'password'  # the server's password
+RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS + \
+    [NETWORKS_PROPERTY, IP_PROPERTY, ADMIN_PASSWORD_PROPERTY]
+
+
+def _get_management_network_id_and_name(neutron_client, ctx):
+    """Examine the context to find the management network id and name."""
+    management_network_id = None
+    management_network_name = None
+    provider_context = provider(ctx)
+
+    if ('management_network_name' in ctx.node.properties) and \
+            ctx.node.properties['management_network_name']:
+        management_network_name = \
+            ctx.node.properties['management_network_name']
+        management_network_name = transform_resource_name(
+            ctx, management_network_name)
+        management_network_id = neutron_client.cosmo_get_named(
+            'network', management_network_name)
+        management_network_id = management_network_id['id']
+    else:
+        int_network = provider_context.int_network
+        if int_network:
+            management_network_id = int_network['id']
+            management_network_name = int_network['name']  # Already transform.
+
+    return management_network_id, management_network_name
+
+
+def _merge_nics(management_network_id, *nics_sources):
+    """Merge nics_sources into a single nics list, insert mgmt network if
+    needed.
+    nics_sources are lists of networks received from several sources
+    (server properties, relationships to networks, relationships to ports).
+    Merge them into a single list, and if the management network isn't present
+    there, prepend it as the first network.
+    """
+    merged = []
+    for nics in nics_sources:
+        merged.extend(nics)
+    if management_network_id is not None and \
+            not any(nic['net-id'] == management_network_id for nic in merged):
+        merged.insert(0, {'net-id': management_network_id})
+    return merged
+
+
+def _normalize_nics(nics):
+    """Transform the NICs passed to the form expected by openstack.
+
+    If both net-id and port-id are provided, remove net-id: it is ignored
+    by openstack anyway.
+    """
+    def _normalize(nic):
+        if 'port-id' in nic and 'net-id' in nic:
+            nic = nic.copy()
+            del nic['net-id']
+        return nic
+    return [_normalize(nic) for nic in nics]
+
+
+def _prepare_server_nics(neutron_client, ctx, server):
+    """Update server['nics'] based on declared relationships.
+
+    server['nics'] should contain the pre-declared nics, then the networks
+    that the server has a declared relationship to, then the networks
+    of the ports the server has a relationship to.
+
+    If that doesn't include the management network, it should be prepended
+    as the first network.
+
+    The management network id and name are stored in the server meta properties
+    """
+    network_ids = get_openstack_ids_of_connected_nodes_by_openstack_type(
+        ctx, NETWORK_OPENSTACK_TYPE)
+    port_ids = get_openstack_ids_of_connected_nodes_by_openstack_type(
+        ctx, PORT_OPENSTACK_TYPE)
+    management_network_id, management_network_name = \
+        _get_management_network_id_and_name(neutron_client, ctx)
+    if management_network_id is None and (network_ids or port_ids):
+        # Known limitation
+        raise NonRecoverableError(
+            "Nova server with NICs requires "
+            "'management_network_name' in properties or id "
+            "from provider context, which was not supplied")
+
+    nics = _merge_nics(
+        management_network_id,
+        server.get('nics', []),
+        [{'net-id': net_id} for net_id in network_ids],
+        get_port_networks(neutron_client, port_ids))
+
+    nics = _normalize_nics(nics)
+
+    server['nics'] = nics
+    if management_network_id is not None:
+        server['meta']['cloudify_management_network_id'] = \
+            management_network_id
+    if management_network_name is not None:
+        server['meta']['cloudify_management_network_name'] = \
+            management_network_name
+
+
+def _get_boot_volume_relationships(type_name, ctx):
+    ctx.logger.debug('Instance relationship target instances: {0}'.format(str([
+        rel.target.instance.runtime_properties
+        for rel in ctx.instance.relationships])))
+    targets = [
+            rel.target.instance
+            for rel in ctx.instance.relationships
+            if rel.target.instance.runtime_properties.get(
+                OPENSTACK_TYPE_PROPERTY) == type_name and
+            rel.target.node.properties.get('boot', False)]
+
+    if not targets:
+        return None
+    elif len(targets) > 1:
+        raise NonRecoverableError("2 boot volumes not supported")
+    return targets[0]
+
+
+def _handle_boot_volume(server, ctx):
+    boot_volume = _get_boot_volume_relationships(VOLUME_OPENSTACK_TYPE, ctx)
+    if boot_volume:
+        boot_volume_id = boot_volume.runtime_properties[OPENSTACK_ID_PROPERTY]
+        ctx.logger.info('boot_volume_id: {0}'.format(boot_volume_id))
+        az = boot_volume.runtime_properties[OPENSTACK_AZ_PROPERTY]
+        # If a block device mapping already exists we shouldn't overwrite it
+        # completely
+        bdm = server.setdefault('block_device_mapping', {})
+        bdm['vda'] = '{0}:::0'.format(boot_volume_id)
+        # Some nova configurations allow cross-az server-volume connections, so
+        # we can't treat that as an error.
+        if not server.get('availability_zone'):
+            server['availability_zone'] = az
+
+
+@operation
+@with_nova_client
+@with_neutron_client
+def create(nova_client, neutron_client, args, **kwargs):
+    """
+    Creates a server. Exposes the parameters mentioned in
+    http://docs.openstack.org/developer/python-novaclient/api/novaclient.v1_1
+    .servers.html#novaclient.v1_1.servers.ServerManager.create
+    """
+
+    external_server = use_external_resource(ctx, nova_client,
+                                            SERVER_OPENSTACK_TYPE)
+
+    if external_server:
+        _set_network_and_ip_runtime_properties(external_server)
+        if ctx._local:
+            return
+        else:
+            network_ids = \
+                get_openstack_ids_of_connected_nodes_by_openstack_type(
+                    ctx, NETWORK_OPENSTACK_TYPE)
+            port_ids = get_openstack_ids_of_connected_nodes_by_openstack_type(
+                ctx, PORT_OPENSTACK_TYPE)
+            try:
+                _validate_external_server_nics(
+                    neutron_client,
+                    network_ids,
+                    port_ids
+                )
+                _validate_external_server_keypair(nova_client)
+                return
+            except Exception:
+                delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+                raise
+
+    provider_context = provider(ctx)
+
+    def rename(name):
+        return transform_resource_name(ctx, name)
+
+    server = {
+        'name': get_resource_id(ctx, SERVER_OPENSTACK_TYPE),
+    }
+    server.update(copy.deepcopy(ctx.node.properties['server']))
+    server.update(copy.deepcopy(args))
+
+    _handle_boot_volume(server, ctx)
+    handle_image_from_relationship(server, 'image', ctx)
+
+    if 'meta' not in server:
+        server['meta'] = dict()
+
+    transform_resource_name(ctx, server)
+
+    ctx.logger.debug(
+        "server.create() server before transformations: {0}".format(server))
+
+    for key in 'block_device_mapping', 'block_device_mapping_v2':
+        if key in server:
+            # if there is a connected boot volume, don't require the `image`
+            # property.
+            # However, python-novaclient requires an `image` input anyway, and
+            # checks it for truthiness when deciding whether to pass it along
+            # to the API
+            if 'image' not in server:
+                server['image'] = ctx.node.properties.get('image')
+            break
+    else:
+        _handle_image_or_flavor(server, nova_client, 'image')
+    _handle_image_or_flavor(server, nova_client, 'flavor')
+
+    if provider_context.agents_security_group:
+        security_groups = server.get('security_groups', [])
+        asg = provider_context.agents_security_group['name']
+        if asg not in security_groups:
+            security_groups.append(asg)
+        server['security_groups'] = security_groups
+    elif not server.get('security_groups', []):
+        # Make sure that if the server is connected to a security group
+        # from CREATE time so that there the user can control
+        # that there is never a time that a running server is not protected.
+        security_group_names = \
+            get_openstack_names_of_connected_nodes_by_openstack_type(
+                ctx,
+                SECURITY_GROUP_OPENSTACK_TYPE)
+        server['security_groups'] = security_group_names
+
+    # server keypair handling
+    keypair_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+        ctx, KEYPAIR_OPENSTACK_TYPE, True)
+
+    if 'key_name' in server:
+        if keypair_id:
+            raise NonRecoverableError("server can't both have the "
+                                      '"key_name" nested property and be '
+                                      'connected to a keypair via a '
+                                      'relationship at the same time')
+        server['key_name'] = rename(server['key_name'])
+    elif keypair_id:
+        server['key_name'] = _get_keypair_name_by_id(nova_client, keypair_id)
+    elif provider_context.agents_keypair:
+        server['key_name'] = provider_context.agents_keypair['name']
+    else:
+        server['key_name'] = None
+        ctx.logger.info(
+            'server must have a keypair, yet no keypair was connected to the '
+            'server node, the "key_name" nested property '
+            "wasn't used, and there is no agent keypair in the provider "
+            "context. Agent installation can have issues.")
+
+    _fail_on_missing_required_parameters(
+        server,
+        ('name', 'flavor'),
+        'server')
+
+    _prepare_server_nics(neutron_client, ctx, server)
+
+    ctx.logger.debug(
+        "server.create() server after transformations: {0}".format(server))
+
+    userdata.handle_userdata(server)
+
+    ctx.logger.info("Creating VM with parameters: {0}".format(str(server)))
+    # Store the server dictionary contents in runtime properties
+    assign_payload_as_runtime_properties(ctx, SERVER_OPENSTACK_TYPE, server)
+    ctx.logger.debug(
+        "Asking Nova to create server. All possible parameters are: {0})"
+        .format(','.join(server.keys())))
+
+    try:
+        s = nova_client.servers.create(**server)
+    except nova_exceptions.BadRequest as e:
+        if 'Block Device Mapping is Invalid' in str(e):
+            return ctx.operation.retry(
+                message='Block Device Mapping is not created yet',
+                retry_after=30)
+        if str(e).startswith(MUST_SPECIFY_NETWORK_EXCEPTION_TEXT):
+            raise NonRecoverableError(
+                "Can not provision server: management_network_name or id"
+                " is not specified but there are several networks that the "
+                "server can be connected to.")
+        raise
+    ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = s.id
+    ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \
+        SERVER_OPENSTACK_TYPE
+    ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = server['name']
+
+
+def get_port_networks(neutron_client, port_ids):
+
+    def get_network(port_id):
+        port = neutron_client.show_port(port_id)
+        return {
+            'net-id': port['port']['network_id'],
+            'port-id': port['port']['id']
+        }
+
+    return map(get_network, port_ids)
+
+
+@operation
+@with_nova_client
+def start(nova_client, start_retry_interval, private_key_path, **kwargs):
+    server = get_server_by_context(nova_client)
+
+    if is_external_resource_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external server is started')
+        if server.status != SERVER_STATUS_ACTIVE:
+            raise NonRecoverableError(
+                'Expected external resource server {0} to be in '
+                '"{1}" status'.format(server.id, SERVER_STATUS_ACTIVE))
+        return
+
+    if server.status == SERVER_STATUS_ACTIVE:
+        ctx.logger.info('Server is {0}'.format(server.status))
+
+        if ctx.node.properties['use_password']:
+            private_key = _get_private_key(private_key_path)
+            ctx.logger.debug('retrieving password for server')
+            password = server.get_password(private_key)
+
+            if not password:
+                return ctx.operation.retry(
+                    message='Waiting for server to post generated password',
+                    retry_after=start_retry_interval)
+
+            ctx.instance.runtime_properties[ADMIN_PASSWORD_PROPERTY] = password
+            ctx.logger.info('Server has been set with a password')
+
+        _set_network_and_ip_runtime_properties(server)
+        return
+
+    server_task_state = getattr(server, OS_EXT_STS_TASK_STATE)
+
+    if server.status == SERVER_STATUS_SHUTOFF and \
+            server_task_state != SERVER_TASK_STATE_POWERING_ON:
+        ctx.logger.info('Server is in {0} status - starting server...'.format(
+            SERVER_STATUS_SHUTOFF))
+        server.start()
+        server_task_state = SERVER_TASK_STATE_POWERING_ON
+
+    if server.status == SERVER_STATUS_BUILD or \
+            server_task_state == SERVER_TASK_STATE_POWERING_ON:
+        return ctx.operation.retry(
+            message='Waiting for server to be in {0} state but is in {1}:{2} '
+                    'state. Retrying...'.format(SERVER_STATUS_ACTIVE,
+                                                server.status,
+                                                server_task_state),
+            retry_after=start_retry_interval)
+
+    raise NonRecoverableError(
+        'Unexpected server state {0}:{1}'.format(server.status,
+                                                 server_task_state))
+
+
+@operation
+@with_nova_client
+def stop(nova_client, **kwargs):
+    """
+    Stop server.
+
+    Depends on OpenStack implementation, server.stop() might not be supported.
+    """
+    if is_external_resource(ctx):
+        ctx.logger.info('Not stopping server since an external server is '
+                        'being used')
+        return
+
+    server = get_server_by_context(nova_client)
+
+    if server.status != SERVER_STATUS_SHUTOFF:
+        nova_client.servers.stop(server)
+    else:
+        ctx.logger.info('Server is already stopped')
+
+
+@operation
+@with_nova_client
+def delete(nova_client, **kwargs):
+    if not is_external_resource(ctx):
+        ctx.logger.info('deleting server')
+        server = get_server_by_context(nova_client)
+        nova_client.servers.delete(server)
+        _wait_for_server_to_be_deleted(nova_client, server)
+    else:
+        ctx.logger.info('not deleting server since an external server is '
+                        'being used')
+
+    delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS)
+
+
+def _wait_for_server_to_be_deleted(nova_client,
+                                   server,
+                                   timeout=120,
+                                   sleep_interval=5):
+    timeout = time.time() + timeout
+    while time.time() < timeout:
+        try:
+            server = nova_client.servers.get(server)
+            ctx.logger.debug('Waiting for server "{}" to be deleted. current'
+                             ' status: {}'.format(server.id, server.status))
+            time.sleep(sleep_interval)
+        except nova_exceptions.NotFound:
+            return
+    # recoverable error
+    raise RuntimeError('Server {} has not been deleted. waited for {} seconds'
+                       .format(server.id, timeout))
+
+
+def get_server_by_context(nova_client):
+    return nova_client.servers.get(
+        ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY])
+
+
+def _set_network_and_ip_runtime_properties(server):
+
+    ips = {}
+
+    if not server.networks:
+        raise NonRecoverableError(
+            'The server was created but not attached to a network. '
+            'Cloudify requires that a server is connected to '
+            'at least one port.'
+        )
+
+    manager_network_ip = None
+    management_network_name = server.metadata.get(
+        'cloudify_management_network_name')
+
+    for network, network_ips in server.networks.items():
+        if (management_network_name and
+                network == management_network_name) or not \
+                manager_network_ip:
+            manager_network_ip = next(iter(network_ips or []), None)
+        ips[network] = network_ips
+    ctx.instance.runtime_properties[NETWORKS_PROPERTY] = ips
+    # The ip of this instance in the management network
+    ctx.instance.runtime_properties[IP_PROPERTY] = manager_network_ip
+
+
+@operation
+@with_nova_client
+def connect_floatingip(nova_client, fixed_ip, **kwargs):
+    server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    floating_ip_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+
+    if is_external_relationship_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external floatingip and server '
+                        'are associated')
+        if nova_client.floating_ips.get(floating_ip_id).instance_id ==\
+                server_id:
+            return
+        raise NonRecoverableError(
+            'Expected external resources server {0} and floating-ip {1} to be '
+            'connected'.format(server_id, floating_ip_id))
+
+    floating_ip_address = ctx.target.instance.runtime_properties[
+        IP_ADDRESS_PROPERTY]
+    server = nova_client.servers.get(server_id)
+    server.add_floating_ip(floating_ip_address, fixed_ip or None)
+
+    server = nova_client.servers.get(server_id)
+    all_server_ips = reduce(operator.add, server.networks.values())
+    if floating_ip_address not in all_server_ips:
+        return ctx.operation.retry(message='Failed to assign floating ip {0}'
+                                           ' to machine {1}.'
+                                   .format(floating_ip_address, server_id))
+
+
+@operation
+@with_nova_client
+@with_neutron_client
+def disconnect_floatingip(nova_client, neutron_client, **kwargs):
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not disassociating floatingip and server since '
+                        'external floatingip and server are being used')
+        return
+
+    server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    ctx.logger.info("Remove floating ip {0}".format(
+        ctx.target.instance.runtime_properties[IP_ADDRESS_PROPERTY]))
+    server_floating_ip = get_server_floating_ip(neutron_client, server_id)
+    if server_floating_ip:
+        server = nova_client.servers.get(server_id)
+        server.remove_floating_ip(server_floating_ip['floating_ip_address'])
+        ctx.logger.info("Floating ip {0} detached from server"
+                        .format(server_floating_ip['floating_ip_address']))
+
+
+@operation
+@with_nova_client
+def connect_security_group(nova_client, **kwargs):
+    server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    security_group_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+    security_group_name = ctx.target.instance.runtime_properties[
+        OPENSTACK_NAME_PROPERTY]
+
+    if is_external_relationship_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external security group and server '
+                        'are associated')
+        server = nova_client.servers.get(server_id)
+        if [sg for sg in server.list_security_group() if sg.id ==
+                security_group_id]:
+            return
+        raise NonRecoverableError(
+            'Expected external resources server {0} and security-group {1} to '
+            'be connected'.format(server_id, security_group_id))
+
+    server = nova_client.servers.get(server_id)
+    for security_group in server.list_security_group():
+        # Since some security groups are already attached in
+        # create this will ensure that they are not attached twice.
+        if security_group_id != security_group.id and \
+                security_group_name != security_group.name:
+            # to support nova security groups as well,
+            # we connect the security group by name
+            # (as connecting by id
+            # doesn't seem to work well for nova SGs)
+            server.add_security_group(security_group_name)
+
+    _validate_security_group_and_server_connection_status(nova_client,
+                                                          server_id,
+                                                          security_group_id,
+                                                          security_group_name,
+                                                          is_connected=True)
+
+
+@operation
+@with_nova_client
+def disconnect_security_group(nova_client, **kwargs):
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not disconnecting security group and server since '
+                        'external security group and server are being used')
+        return
+
+    server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    security_group_id = ctx.target.instance.runtime_properties[
+        OPENSTACK_ID_PROPERTY]
+    security_group_name = ctx.target.instance.runtime_properties[
+        OPENSTACK_NAME_PROPERTY]
+    server = nova_client.servers.get(server_id)
+    # to support nova security groups as well, we disconnect the security group
+    # by name (as disconnecting by id doesn't seem to work well for nova SGs)
+    server.remove_security_group(security_group_name)
+
+    _validate_security_group_and_server_connection_status(nova_client,
+                                                          server_id,
+                                                          security_group_id,
+                                                          security_group_name,
+                                                          is_connected=False)
+
+
+@operation
+@with_nova_client
+@with_cinder_client
+def attach_volume(nova_client, cinder_client, status_attempts,
+                  status_timeout, **kwargs):
+    server_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    volume_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+    if is_external_relationship_not_conditionally_created(ctx):
+        ctx.logger.info('Validating external volume and server '
+                        'are connected')
+        attachment = volume.get_attachment(cinder_client=cinder_client,
+                                           volume_id=volume_id,
+                                           server_id=server_id)
+        if attachment:
+            return
+        else:
+            raise NonRecoverableError(
+                'Expected external resources server {0} and volume {1} to be '
+                'connected'.format(server_id, volume_id))
+
+    # Note: The 'device_name' property should actually be a property of the
+    # relationship between a server and a volume; It'll move to that
+    # relationship type once relationship properties are better supported.
+    device = ctx.source.node.properties[volume.DEVICE_NAME_PROPERTY]
+    nova_client.volumes.create_server_volume(
+        server_id,
+        volume_id,
+        device if device != 'auto' else None)
+    try:
+        vol, wait_succeeded = volume.wait_until_status(
+            cinder_client=cinder_client,
+            volume_id=volume_id,
+            status=volume.VOLUME_STATUS_IN_USE,
+            num_tries=status_attempts,
+            timeout=status_timeout
+        )
+        if not wait_succeeded:
+            raise RecoverableError(
+                'Waiting for volume status {0} failed - detaching volume and '
+                'retrying..'.format(volume.VOLUME_STATUS_IN_USE))
+        if device == 'auto':
+            # The device name was assigned automatically so we
+            # query the actual device name
+            attachment = volume.get_attachment(
+                cinder_client=cinder_client,
+                volume_id=volume_id,
+                server_id=server_id
+            )
+            device_name = attachment['device']
+            ctx.logger.info('Detected device name for attachment of volume '
+                            '{0} to server {1}: {2}'
+                            .format(volume_id, server_id, device_name))
+            ctx.source.instance.runtime_properties[
+                volume.DEVICE_NAME_PROPERTY] = device_name
+    except Exception, e:
+        if not isinstance(e, NonRecoverableError):
+            _prepare_attach_volume_to_be_repeated(
+                nova_client, cinder_client, server_id, volume_id,
+                status_attempts, status_timeout)
+        raise
+
+
+def _prepare_attach_volume_to_be_repeated(
+        nova_client, cinder_client, server_id, volume_id,
+        status_attempts, status_timeout):
+
+    ctx.logger.info('Cleaning after a failed attach_volume() call')
+    try:
+        _detach_volume(nova_client, cinder_client, server_id, volume_id,
+                       status_attempts, status_timeout)
+    except Exception, e:
+        ctx.logger.error('Cleaning after a failed attach_volume() call failed '
+                         'raising a \'{0}\' exception.'.format(e))
+        raise NonRecoverableError(e)
+
+
+def _detach_volume(nova_client, cinder_client, server_id, volume_id,
+                   status_attempts, status_timeout):
+    attachment = volume.get_attachment(cinder_client=cinder_client,
+                                       volume_id=volume_id,
+                                       server_id=server_id)
+    if attachment:
+        nova_client.volumes.delete_server_volume(server_id, attachment['id'])
+        volume.wait_until_status(cinder_client=cinder_client,
+                                 volume_id=volume_id,
+                                 status=volume.VOLUME_STATUS_AVAILABLE,
+                                 num_tries=status_attempts,
+                                 timeout=status_timeout)
+
+
+@operation
+@with_nova_client
+@with_cinder_client
+def detach_volume(nova_client, cinder_client, status_attempts,
+                  status_timeout, **kwargs):
+    if is_external_relationship(ctx):
+        ctx.logger.info('Not detaching volume from server since '
+                        'external volume and server are being used')
+        return
+
+    server_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    volume_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+
+    _detach_volume(nova_client, cinder_client, server_id, volume_id,
+                   status_attempts, status_timeout)
+
+
+def _fail_on_missing_required_parameters(obj, required_parameters, hint_where):
+    for k in required_parameters:
+        if k not in obj:
+            raise NonRecoverableError(
+                "Required parameter '{0}' is missing (under host's "
+                "properties.{1}). Required parameters are: {2}"
+                .format(k, hint_where, required_parameters))
+
+
+def _validate_external_server_keypair(nova_client):
+    keypair_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+        ctx, KEYPAIR_OPENSTACK_TYPE, True)
+    if not keypair_id:
+        return
+
+    keypair_instance_id = \
+        [node_instance_id for node_instance_id, runtime_props in
+         ctx.capabilities.get_all().iteritems() if
+         runtime_props.get(OPENSTACK_ID_PROPERTY) == keypair_id][0]
+    keypair_node_properties = _get_properties_by_node_instance_id(
+        keypair_instance_id)
+    if not is_external_resource_by_properties(keypair_node_properties):
+        raise NonRecoverableError(
+            "Can't connect a new keypair node to a server node "
+            "with '{0}'=True".format(USE_EXTERNAL_RESOURCE_PROPERTY))
+
+    server = get_server_by_context(nova_client)
+    if keypair_id == _get_keypair_name_by_id(nova_client, server.key_name):
+        return
+    raise NonRecoverableError(
+        "Expected external resources server {0} and keypair {1} to be "
+        "connected".format(server.id, keypair_id))
+
+
+def _get_keypair_name_by_id(nova_client, key_name):
+    keypair = nova_client.cosmo_get_named(KEYPAIR_OPENSTACK_TYPE, key_name)
+    return keypair.id
+
+
+def _validate_external_server_nics(neutron_client, network_ids, port_ids):
+    # validate no new nics are being assigned to an existing server (which
+    # isn't possible on Openstack)
+    new_nic_nodes = \
+        [node_instance_id for node_instance_id, runtime_props in
+         ctx.capabilities.get_all().iteritems() if runtime_props.get(
+             OPENSTACK_TYPE_PROPERTY) in (PORT_OPENSTACK_TYPE,
+                                          NETWORK_OPENSTACK_TYPE) and
+         not is_external_resource_by_properties(
+             _get_properties_by_node_instance_id(node_instance_id))]
+    if new_nic_nodes:
+        raise NonRecoverableError(
+            "Can't connect new port and/or network nodes to a server node "
+            "with '{0}'=True".format(USE_EXTERNAL_RESOURCE_PROPERTY))
+
+    # validate all expected connected networks and ports are indeed already
+    # connected to the server. note that additional networks (e.g. the
+    # management network) may be connected as well with no error raised
+    if not network_ids and not port_ids:
+        return
+
+    server_id = ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]
+    connected_ports = neutron_client.list_ports(device_id=server_id)['ports']
+
+    # not counting networks connected by a connected port since allegedly
+    # the connection should be on a separate port
+    connected_ports_networks = {port['network_id'] for port in
+                                connected_ports if port['id'] not in port_ids}
+    connected_ports_ids = {port['id'] for port in
+                           connected_ports}
+    disconnected_networks = [network_id for network_id in network_ids if
+                             network_id not in connected_ports_networks]
+    disconnected_ports = [port_id for port_id in port_ids if port_id not
+                          in connected_ports_ids]
+    if disconnected_networks or disconnected_ports:
+        raise NonRecoverableError(
+            'Expected external resources to be connected to external server {'
+            '0}: Networks - {1}; Ports - {2}'.format(server_id,
+                                                     disconnected_networks,
+                                                     disconnected_ports))
+
+
+def _get_properties_by_node_instance_id(node_instance_id):
+    client = get_rest_client()
+    node_instance = client.node_instances.get(node_instance_id)
+    node = client.nodes.get(ctx.deployment.id, node_instance.node_id)
+    return node.properties
+
+
+@operation
+@with_nova_client
+def creation_validation(nova_client, args, **kwargs):
+
+    def validate_server_property_value_exists(server_props, property_name):
+        ctx.logger.debug(
+            'checking whether {0} exists...'.format(property_name))
+
+        serv_props_copy = server_props.copy()
+        try:
+            handle_image_from_relationship(serv_props_copy, 'image', ctx)
+            _handle_image_or_flavor(serv_props_copy, nova_client,
+                                    property_name)
+        except (NonRecoverableError, nova_exceptions.NotFound) as e:
+            # temporary error - once image/flavor_name get removed, these
+            # errors won't be relevant anymore
+            err = str(e)
+            ctx.logger.error('VALIDATION ERROR: ' + err)
+            raise NonRecoverableError(err)
+
+        prop_value_id = str(serv_props_copy[property_name])
+        prop_values = list(nova_client.cosmo_list(property_name))
+        for f in prop_values:
+            if prop_value_id == f.id:
+                ctx.logger.debug('OK: {0} exists'.format(property_name))
+                return
+        err = '{0} {1} does not exist'.format(property_name, prop_value_id)
+        ctx.logger.error('VALIDATION ERROR: ' + err)
+        if prop_values:
+            ctx.logger.info('list of available {0}s:'.format(property_name))
+            for f in prop_values:
+                ctx.logger.info('    {0:>10} - {1}'.format(f.id, f.name))
+        else:
+            ctx.logger.info('there are no available {0}s'.format(
+                property_name))
+        raise NonRecoverableError(err)
+
+    validate_resource(ctx, nova_client, SERVER_OPENSTACK_TYPE)
+
+    server_props = dict(ctx.node.properties['server'], **args)
+    validate_server_property_value_exists(server_props, 'flavor')
+
+
+def _get_private_key(private_key_path):
+    pk_node_by_rel = \
+        get_single_connected_node_by_openstack_type(
+            ctx, KEYPAIR_OPENSTACK_TYPE, True)
+
+    if private_key_path:
+        if pk_node_by_rel:
+            raise NonRecoverableError("server can't both have a "
+                                      '"private_key_path" input and be '
+                                      'connected to a keypair via a '
+                                      'relationship at the same time')
+        key_path = private_key_path
+    else:
+        if pk_node_by_rel and pk_node_by_rel.properties['private_key_path']:
+            key_path = pk_node_by_rel.properties['private_key_path']
+        else:
+            key_path = ctx.bootstrap_context.cloudify_agent.agent_key_path
+
+    if key_path:
+        key_path = os.path.expanduser(key_path)
+        if os.path.isfile(key_path):
+            return key_path
+
+    err_message = 'Cannot find private key file'
+    if key_path:
+        err_message += '; expected file path was {0}'.format(key_path)
+    raise NonRecoverableError(err_message)
+
+
+def _validate_security_group_and_server_connection_status(
+        nova_client, server_id, sg_id, sg_name, is_connected):
+
+    # verifying the security group got connected or disconnected
+    # successfully - this is due to Openstack concurrency issues that may
+    # take place when attempting to connect/disconnect multiple SGs to the
+    # same server at the same time
+    server = nova_client.servers.get(server_id)
+
+    if is_connected ^ any(sg for sg in server.list_security_group() if
+                          sg.id == sg_id):
+        raise RecoverableError(
+            message='Security group {0} did not get {2} server {1} '
+                    'properly'
+            .format(
+                sg_name,
+                server.name,
+                'connected to' if is_connected else 'disconnected from'))
+
+
+def _handle_image_or_flavor(server, nova_client, prop_name):
+    if prop_name not in server and '{0}_name'.format(prop_name) not in server:
+        # setting image or flavor - looking it up by name; if not found, then
+        # the value is assumed to be the id
+        server[prop_name] = ctx.node.properties[prop_name]
+
+        # temporary error message: once the 'image' and 'flavor' properties
+        # become mandatory, this will become less relevant
+        if not server[prop_name]:
+            raise NonRecoverableError(
+                'must set {0} by either setting a "{0}" property or by setting'
+                ' a "{0}" or "{0}_name" (deprecated) field under the "server" '
+                'property'.format(prop_name))
+
+        image_or_flavor = \
+            nova_client.cosmo_get_if_exists(prop_name, name=server[prop_name])
+        if image_or_flavor:
+            server[prop_name] = image_or_flavor.id
+    else:  # Deprecated sugar
+        if '{0}_name'.format(prop_name) in server:
+            prop_name_plural = nova_client.cosmo_plural(prop_name)
+            server[prop_name] = \
+                getattr(nova_client, prop_name_plural).find(
+                    name=server['{0}_name'.format(prop_name)]).id
+            del server['{0}_name'.format(prop_name)]
diff --git a/aria/multivim-plugin/nova_plugin/tests/__init__.py b/aria/multivim-plugin/nova_plugin/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/__init__.py
diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml
new file mode 100644
index 0000000..22b7fb5
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml
@@ -0,0 +1,23 @@
+tosca_definitions_version: cloudify_dsl_1_3
+
+imports:
+  - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml
+  - plugin.yaml
+
+inputs:
+  private_key: {}
+  is_keypair_external: {}
+
+
+node_templates:
+
+  keypair:
+    type: cloudify.openstack.nodes.KeyPair
+    properties:
+      private_key_path: { get_input: private_key }
+      use_external_resource: { get_input: is_keypair_external }
+      openstack_config:
+        username: aaa
+        password: aaa
+        tenant_name: aaa
+        auth_url: aaa
diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml
new file mode 100644
index 0000000..70b75f6
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml
@@ -0,0 +1,31 @@
+tosca_definitions_version: cloudify_dsl_1_3
+
+imports:
+  - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml
+  - plugin.yaml
+
+inputs:
+  use_password:
+    type: boolean
+    default: false
+
+node_templates:
+
+  security_group:
+    type: cloudify.openstack.nodes.SecurityGroup
+
+  server:
+    type: cloudify.openstack.nodes.Server
+    properties:
+      install_agent: false
+      use_password: { get_input: use_password }
+      openstack_config:
+        username: aaa
+        password: aaa
+        tenant_name: aaa
+        auth_url: aaa
+      server:
+        key_name: 'aa'
+    relationships:
+      - type: cloudify.openstack.server_connected_to_security_group
+        target: security_group
diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml
new file mode 100644
index 0000000..275806c
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml
@@ -0,0 +1,31 @@
+tosca_definitions_version: cloudify_dsl_1_3
+
+imports:
+  - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml
+  - plugin.yaml
+
+inputs:
+  use_password:
+    type: boolean
+    default: false
+
+node_templates:
+  server:
+    type: cloudify.openstack.nodes.Server
+    properties:
+      install_agent: false
+      use_password: { get_input: use_password }
+      server:
+        key_name: key
+        scheduler_hints:
+          group: affinity-group-id
+      openstack_config:
+        username: aaa
+        password: aaa
+        tenant_name: aaa
+        auth_url: aaa
+    interfaces:
+      cloudify.interfaces.lifecycle:
+        start:
+          inputs:
+            start_retry_interval: 1
diff --git a/aria/multivim-plugin/nova_plugin/tests/test_relationships.py b/aria/multivim-plugin/nova_plugin/tests/test_relationships.py
new file mode 100644
index 0000000..2814057
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/test_relationships.py
@@ -0,0 +1,228 @@
+#########
+# 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.
+
+"""Test the functions related to retrieving relationship information.
+
+Functions under test are mostly inside openstack_plugin_common:
+get_relationships_by_openstack_type
+get_connected_nodes_by_openstack_type
+get_openstack_ids_of_connected_nodes_by_openstack_type
+get_single_connected_node_by_openstack_type
+"""
+
+import uuid
+from unittest import TestCase
+
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+
+from cloudify.exceptions import NonRecoverableError
+
+from cloudify.mocks import (
+    MockCloudifyContext,
+    MockNodeContext,
+    MockNodeInstanceContext,
+    MockRelationshipContext,
+    MockRelationshipSubjectContext,
+)
+from openstack_plugin_common import (
+    OPENSTACK_ID_PROPERTY,
+    OPENSTACK_TYPE_PROPERTY,
+    get_openstack_id_of_single_connected_node_by_openstack_type,
+    get_openstack_ids_of_connected_nodes_by_openstack_type,
+    get_relationships_by_openstack_type,
+    get_single_connected_node_by_openstack_type,
+)
+
+
+class RelationshipsTestBase(TestCase):
+    def _make_vm_ctx_with_relationships(self, rel_specs, properties=None):
+        """Prepare a mock CloudifyContext from the given relationship spec.
+
+        rel_specs is an ordered collection of relationship specs - dicts
+        with the keys "node" and "instance" used to construct the
+        MockNodeContext and the MockNodeInstanceContext, and optionally a
+        "type" key.
+        Examples: [
+            {},
+            {"node": {"id": 5}},
+            {
+                "type": "some_type",
+                "instance": {
+                    "id": 3,
+                    "runtime_properties":{}
+                }
+            }
+        ]
+        """
+        if properties is None:
+            properties = {}
+        relationships = []
+        for rel_spec in rel_specs:
+            node = rel_spec.get('node', {})
+            node_id = node.pop('id', uuid.uuid4().hex)
+
+            instance = rel_spec.get('instance', {})
+            instance_id = instance.pop('id', '{0}_{1}'.format(
+                node_id, uuid.uuid4().hex))
+            if 'properties' not in node:
+                node['properties'] = {}
+            node_ctx = MockNodeContext(id=node_id, **node)
+            instance_ctx = MockNodeInstanceContext(id=instance_id, **instance)
+
+            rel_subject_ctx = MockRelationshipSubjectContext(
+                node=node_ctx, instance=instance_ctx)
+            rel_type = rel_spec.get('type')
+            rel_ctx = MockRelationshipContext(target=rel_subject_ctx,
+                                              type=rel_type)
+            relationships.append(rel_ctx)
+        return MockCloudifyContext(node_id='vm', properties=properties,
+                                   relationships=relationships)
+
+
+class TestGettingRelatedResources(RelationshipsTestBase):
+
+    def test_get_relationships_finds_all_by_type(self):
+        """get_relationships_by_openstack_type returns all rels that match."""
+        rel_specs = [{
+            'instance': {
+                'id': instance_id,
+                'runtime_properties': {
+                    OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE
+                }
+            }
+        } for instance_id in range(3)]
+
+        rel_specs.append({
+            'instance': {
+                'runtime_properties': {
+                    OPENSTACK_TYPE_PROPERTY: 'something else'
+                }
+            }
+        })
+
+        ctx = self._make_vm_ctx_with_relationships(rel_specs)
+        filtered = get_relationships_by_openstack_type(ctx,
+                                                       NETWORK_OPENSTACK_TYPE)
+        self.assertEqual(3, len(filtered))
+
+    def test_get_ids_of_nodes_by_type(self):
+
+        rel_spec = {
+            'instance': {
+                'runtime_properties': {
+                    OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE,
+                    OPENSTACK_ID_PROPERTY: 'the node id'
+                }
+            }
+        }
+        ctx = self._make_vm_ctx_with_relationships([rel_spec])
+        ids = get_openstack_ids_of_connected_nodes_by_openstack_type(
+            ctx, NETWORK_OPENSTACK_TYPE)
+        self.assertEqual(['the node id'], ids)
+
+
+class TestGetSingleByID(RelationshipsTestBase):
+    def _make_instances(self, ids):
+        """Mock a context with relationships to instances with given ids."""
+        rel_specs = [{
+            'node': {
+                'id': node_id
+            },
+            'instance': {
+                'runtime_properties': {
+                    OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE,
+                    OPENSTACK_ID_PROPERTY: node_id
+                }
+            }
+        } for node_id in ids]
+        return self._make_vm_ctx_with_relationships(rel_specs)
+
+    def test_get_single_id(self):
+        ctx = self._make_instances(['the node id'])
+        found_id = get_openstack_id_of_single_connected_node_by_openstack_type(
+            ctx, NETWORK_OPENSTACK_TYPE)
+        self.assertEqual('the node id', found_id)
+
+    def test_get_single_id_two_found(self):
+        ctx = self._make_instances([0, 1])
+        self.assertRaises(
+            NonRecoverableError,
+            get_openstack_id_of_single_connected_node_by_openstack_type, ctx,
+            NETWORK_OPENSTACK_TYPE)
+
+    def test_get_single_id_two_found_if_exists_true(self):
+        ctx = self._make_instances([0, 1])
+
+        try:
+            get_openstack_id_of_single_connected_node_by_openstack_type(
+                ctx, NETWORK_OPENSTACK_TYPE, if_exists=True)
+        except NonRecoverableError as e:
+            self.assertIn(NETWORK_OPENSTACK_TYPE, e.message)
+        else:
+            self.fail()
+
+    def test_get_single_id_if_exists_none_found(self):
+        ctx = self._make_instances([])
+        found = get_openstack_id_of_single_connected_node_by_openstack_type(
+            ctx, NETWORK_OPENSTACK_TYPE, if_exists=True)
+        self.assertIsNone(found)
+
+    def test_get_single_id_none_found(self):
+        rel_spec = []
+        ctx = self._make_vm_ctx_with_relationships(rel_spec)
+        self.assertRaises(
+            NonRecoverableError,
+            get_openstack_id_of_single_connected_node_by_openstack_type,
+            ctx,
+            NETWORK_OPENSTACK_TYPE)
+
+    def test_get_single_node(self):
+        ctx = self._make_instances(['the node id'])
+        found_node = get_single_connected_node_by_openstack_type(
+            ctx, NETWORK_OPENSTACK_TYPE)
+        self.assertEqual('the node id', found_node.id)
+
+    def test_get_single_node_two_found(self):
+        ctx = self._make_instances([0, 1])
+        self.assertRaises(
+            NonRecoverableError,
+            get_single_connected_node_by_openstack_type,
+            ctx, NETWORK_OPENSTACK_TYPE)
+
+    def test_get_single_node_two_found_if_exists(self):
+        ctx = self._make_instances([0, 1])
+
+        self.assertRaises(
+            NonRecoverableError,
+            get_single_connected_node_by_openstack_type,
+            ctx,
+            NETWORK_OPENSTACK_TYPE,
+            if_exists=True)
+
+    def test_get_single_node_if_exists_none_found(self):
+        ctx = self._make_instances([])
+
+        found = get_single_connected_node_by_openstack_type(
+            ctx, NETWORK_OPENSTACK_TYPE, if_exists=True)
+        self.assertIsNone(found)
+
+    def test_get_single_node_none_found(self):
+        ctx = self._make_instances([])
+
+        self.assertRaises(
+            NonRecoverableError,
+            get_single_connected_node_by_openstack_type,
+            ctx,
+            NETWORK_OPENSTACK_TYPE)
diff --git a/aria/multivim-plugin/nova_plugin/tests/test_server.py b/aria/multivim-plugin/nova_plugin/tests/test_server.py
new file mode 100644
index 0000000..a509305
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/test_server.py
@@ -0,0 +1,551 @@
+#########
+# 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 os import path
+import tempfile
+
+import unittest
+import mock
+
+import nova_plugin
+from cloudify.test_utils import workflow_test
+
+from openstack_plugin_common import NeutronClientWithSugar, \
+    OPENSTACK_TYPE_PROPERTY, OPENSTACK_ID_PROPERTY
+from neutron_plugin.network import NETWORK_OPENSTACK_TYPE
+from neutron_plugin.port import PORT_OPENSTACK_TYPE
+from nova_plugin.tests.test_relationships import RelationshipsTestBase
+from nova_plugin.server import _prepare_server_nics
+from cinder_plugin.volume import VOLUME_OPENSTACK_TYPE
+from cloudify.exceptions import NonRecoverableError
+from cloudify.state import current_ctx
+
+from cloudify.utils import setup_logger
+
+from cloudify.mocks import (
+    MockNodeContext,
+    MockCloudifyContext,
+    MockNodeInstanceContext,
+    MockRelationshipContext,
+    MockRelationshipSubjectContext
+)
+
+
+class TestServer(unittest.TestCase):
+
+    blueprint_path = path.join('resources',
+                               'test-start-operation-retry-blueprint.yaml')
+
+    @mock.patch('nova_plugin.server.create')
+    @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties')
+    @workflow_test(blueprint_path, copy_plugin_yaml=True)
+    def test_nova_server_lifecycle_start(self, cfy_local, *_):
+
+        test_vars = {
+            'counter': 0,
+            'server': mock.MagicMock()
+        }
+
+        def mock_get_server_by_context(*_):
+            s = test_vars['server']
+            if test_vars['counter'] == 0:
+                s.status = nova_plugin.server.SERVER_STATUS_BUILD
+            else:
+                s.status = nova_plugin.server.SERVER_STATUS_ACTIVE
+            test_vars['counter'] += 1
+            return s
+
+        with mock.patch('nova_plugin.server.get_server_by_context',
+                        new=mock_get_server_by_context):
+            cfy_local.execute('install', task_retries=3)
+
+        self.assertEqual(2, test_vars['counter'])
+        self.assertEqual(0, test_vars['server'].start.call_count)
+
+    @workflow_test(blueprint_path, copy_plugin_yaml=True)
+    @mock.patch('nova_plugin.server.create')
+    @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties')
+    def test_nova_server_lifecycle_start_after_stop(self, cfy_local, *_):
+
+        test_vars = {
+            'counter': 0,
+            'server': mock.MagicMock()
+        }
+
+        def mock_get_server_by_context(_):
+            s = test_vars['server']
+            if test_vars['counter'] == 0:
+                s.status = nova_plugin.server.SERVER_STATUS_SHUTOFF
+            elif test_vars['counter'] == 1:
+                setattr(s,
+                        nova_plugin.server.OS_EXT_STS_TASK_STATE,
+                        nova_plugin.server.SERVER_TASK_STATE_POWERING_ON)
+            else:
+                s.status = nova_plugin.server.SERVER_STATUS_ACTIVE
+            test_vars['counter'] += 1
+            test_vars['server'] = s
+            return s
+
+        with mock.patch('nova_plugin.server.get_server_by_context',
+                        new=mock_get_server_by_context):
+            cfy_local.execute('install', task_retries=3)
+
+        self.assertEqual(1, test_vars['server'].start.call_count)
+        self.assertEqual(3, test_vars['counter'])
+
+    @workflow_test(blueprint_path, copy_plugin_yaml=True)
+    @mock.patch('nova_plugin.server.create')
+    @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties')
+    def test_nova_server_lifecycle_start_unknown_status(self, cfy_local, *_):
+        test_vars = {
+            'counter': 0,
+            'server': mock.MagicMock()
+        }
+
+        def mock_get_server_by_context(_):
+            s = test_vars['server']
+            if test_vars['counter'] == 0:
+                s.status = '### unknown-status ###'
+            test_vars['counter'] += 1
+            test_vars['server'] = s
+            return s
+
+        with mock.patch('nova_plugin.server.get_server_by_context',
+                        new=mock_get_server_by_context):
+            self.assertRaisesRegexp(RuntimeError,
+                                    'Unexpected server state',
+                                    cfy_local.execute,
+                                    'install')
+
+        self.assertEqual(0, test_vars['server'].start.call_count)
+        self.assertEqual(1, test_vars['counter'])
+
+    @workflow_test(blueprint_path, copy_plugin_yaml=True)
+    @mock.patch('nova_plugin.server.start')
+    @mock.patch('nova_plugin.server._handle_image_or_flavor')
+    @mock.patch('nova_plugin.server._fail_on_missing_required_parameters')
+    @mock.patch('openstack_plugin_common.nova_client')
+    def test_nova_server_creation_param_integrity(
+            self, cfy_local, mock_nova, *args):
+        cfy_local.execute('install', task_retries=0)
+        calls = mock_nova.Client.return_value.servers.method_calls
+        self.assertEqual(1, len(calls))
+        kws = calls[0][2]
+        self.assertIn('scheduler_hints', kws)
+        self.assertEqual(kws['scheduler_hints'],
+                         {'group': 'affinity-group-id'},
+                         'expecting \'scheduler_hints\' value to exist')
+
+    @workflow_test(blueprint_path, copy_plugin_yaml=True,
+                   inputs={'use_password': True})
+    @mock.patch('nova_plugin.server.create')
+    @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties')
+    @mock.patch(
+        'nova_plugin.server.get_single_connected_node_by_openstack_type',
+        autospec=True, return_value=None)
+    def test_nova_server_with_use_password(self, cfy_local, *_):
+
+        test_vars = {
+            'counter': 0,
+            'server': mock.MagicMock()
+        }
+
+        tmp_path = tempfile.NamedTemporaryFile(prefix='key_name')
+        key_path = tmp_path.name
+
+        def mock_get_server_by_context(_):
+            s = test_vars['server']
+            if test_vars['counter'] == 0:
+                s.status = nova_plugin.server.SERVER_STATUS_BUILD
+            else:
+                s.status = nova_plugin.server.SERVER_STATUS_ACTIVE
+            test_vars['counter'] += 1
+
+            def check_agent_key_path(private_key):
+                self.assertEqual(private_key, key_path)
+                return private_key
+
+            s.get_password = check_agent_key_path
+            return s
+
+        with mock.patch('nova_plugin.server.get_server_by_context',
+                        mock_get_server_by_context):
+            with mock.patch(
+                    'cloudify.context.BootstrapContext.'
+                    'CloudifyAgent.agent_key_path',
+                    new_callable=mock.PropertyMock, return_value=key_path):
+                cfy_local.execute('install', task_retries=5)
+
+
+class TestMergeNICs(unittest.TestCase):
+    def test_merge_prepends_management_network(self):
+        """When the mgmt network isnt in a relationship, its the 1st nic."""
+        mgmt_network_id = 'management network'
+        nics = [{'net-id': 'other network'}]
+
+        merged = nova_plugin.server._merge_nics(mgmt_network_id, nics)
+
+        self.assertEqual(len(merged), 2)
+        self.assertEqual(merged[0]['net-id'], 'management network')
+
+    def test_management_network_in_relationships(self):
+        """When the mgmt network was in a relationship, it's not prepended."""
+        mgmt_network_id = 'management network'
+        nics = [{'net-id': 'other network'}, {'net-id': 'management network'}]
+
+        merged = nova_plugin.server._merge_nics(mgmt_network_id, nics)
+
+        self.assertEqual(nics, merged)
+
+
+class TestNormalizeNICs(unittest.TestCase):
+    def test_normalize_port_priority(self):
+        """Whe there's both net-id and port-id, port-id is used."""
+        nics = [{'net-id': '1'}, {'port-id': '2'}, {'net-id': 3, 'port-id': 4}]
+        normalized = nova_plugin.server._normalize_nics(nics)
+        expected = [{'net-id': '1'}, {'port-id': '2'}, {'port-id': 4}]
+        self.assertEqual(expected, normalized)
+
+
+class MockNeutronClient(NeutronClientWithSugar):
+    """A fake neutron client with hard-coded test data."""
+
+    @mock.patch('openstack_plugin_common.OpenStackClient.__init__',
+                new=mock.Mock())
+    def __init__(self):
+        super(MockNeutronClient, self).__init__()
+
+    @staticmethod
+    def _search_filter(objs, search_params):
+        """Mock neutron's filtering by attributes in list_* methods.
+
+        list_* methods (list_networks, list_ports)
+        """
+        def _matches(obj, search_params):
+            return all(obj[k] == v for k, v in search_params.items())
+        return [obj for obj in objs if _matches(obj, search_params)]
+
+    def list_networks(self, **search_params):
+        networks = [
+            {'name': 'network1', 'id': '1'},
+            {'name': 'network2', 'id': '2'},
+            {'name': 'network3', 'id': '3'},
+            {'name': 'network4', 'id': '4'},
+            {'name': 'network5', 'id': '5'},
+            {'name': 'network6', 'id': '6'},
+            {'name': 'other', 'id': 'other'}
+        ]
+        return {'networks': self._search_filter(networks, search_params)}
+
+    def list_ports(self, **search_params):
+        ports = [
+            {'name': 'port1', 'id': '1', 'network_id': '1'},
+            {'name': 'port2', 'id': '2', 'network_id': '1'},
+            {'name': 'port3', 'id': '3', 'network_id': '2'},
+            {'name': 'port4', 'id': '4', 'network_id': '2'},
+        ]
+        return {'ports': self._search_filter(ports, search_params)}
+
+    def show_port(self, port_id):
+        ports = self.list_ports(id=port_id)
+        return {'port': ports['ports'][0]}
+
+
+class NICTestBase(RelationshipsTestBase):
+    """Base test class for the NICs tests.
+
+    It comes with helper methods to create a mock cloudify context, with
+    the specified relationships.
+    """
+    mock_neutron = MockNeutronClient()
+
+    def _relationship_spec(self, obj, objtype):
+        return {'node': {'properties': obj},
+                'instance': {
+                    'runtime_properties': {OPENSTACK_TYPE_PROPERTY: objtype,
+                                           OPENSTACK_ID_PROPERTY: obj['id']}}}
+
+    def _make_vm_ctx_with_ports(self, management_network_name, ports):
+        port_specs = [self._relationship_spec(obj, PORT_OPENSTACK_TYPE)
+                      for obj in ports]
+        vm_properties = {'management_network_name': management_network_name}
+        return self._make_vm_ctx_with_relationships(port_specs,
+                                                    vm_properties)
+
+    def _make_vm_ctx_with_networks(self, management_network_name, networks):
+        network_specs = [self._relationship_spec(obj, NETWORK_OPENSTACK_TYPE)
+                         for obj in networks]
+        vm_properties = {'management_network_name': management_network_name}
+        return self._make_vm_ctx_with_relationships(network_specs,
+                                                    vm_properties)
+
+
+class TestServerNICs(NICTestBase):
+    """Test preparing the NICs list from server<->network relationships.
+
+    Each test creates a cloudify context that represents a openstack VM
+    with relationships to networks. Then, examine the NICs list produced from
+    the relationships.
+    """
+    def test_nova_server_creation_nics_ordering(self):
+        """NIC list keeps the order of the relationships.
+
+        The nics= list passed to nova.server.create should be ordered
+        depending on the relationships to the networks (as defined in the
+        blueprint).
+        """
+        ctx = self._make_vm_ctx_with_networks(
+            management_network_name='network1',
+            networks=[
+                {'id': '1'},
+                {'id': '2'},
+                {'id': '3'},
+                {'id': '4'},
+                {'id': '5'},
+                {'id': '6'},
+            ])
+        server = {'meta': {}}
+
+        _prepare_server_nics(
+            self.mock_neutron, ctx, server)
+
+        self.assertEqual(
+            ['1', '2', '3', '4', '5', '6'],
+            [n['net-id'] for n in server['nics']])
+
+    def test_server_creation_prepends_mgmt_network(self):
+        """If the management network isn't in a relation, it's the first NIC.
+
+        Creating the server examines the relationships, and if it doesn't find
+        a relationship to the management network, it adds the management
+        network to the NICs list, as the first element.
+        """
+        ctx = self._make_vm_ctx_with_networks(
+            management_network_name='other',
+            networks=[
+                {'id': '1'},
+                {'id': '2'},
+                {'id': '3'},
+                {'id': '4'},
+                {'id': '5'},
+                {'id': '6'},
+            ])
+        server = {'meta': {}}
+
+        _prepare_server_nics(
+            self.mock_neutron, ctx, server)
+
+        first_nic = server['nics'][0]
+        self.assertEqual('other', first_nic['net-id'])
+        self.assertEqual(7, len(server['nics']))
+
+    def test_server_creation_uses_relation_mgmt_nic(self):
+        """If the management network is in a relation, it isn't prepended.
+
+        If the server has a relationship to the management network,
+        a new NIC isn't prepended to the list.
+        """
+        ctx = self._make_vm_ctx_with_networks(
+            management_network_name='network1',
+            networks=[
+                {'id': '1'},
+                {'id': '2'},
+                {'id': '3'},
+                {'id': '4'},
+                {'id': '5'},
+                {'id': '6'},
+            ])
+        server = {'meta': {}}
+
+        _prepare_server_nics(
+            self.mock_neutron, ctx, server)
+        self.assertEqual(6, len(server['nics']))
+
+
+class TestServerPortNICs(NICTestBase):
+    """Test preparing the NICs list from server<->port relationships.
+
+    Create a cloudify ctx representing a vm with relationships to
+    openstack ports. Then examine the resulting NICs list: check that it
+    contains the networks that the ports were connected to, and that each
+    connection uses the port that was provided.
+    """
+
+    def test_network_with_port(self):
+        """Port on the management network is used to connect to it.
+
+        The NICs list entry for the management network contains the
+        port-id of the port from the relationship, but doesn't contain net-id.
+        """
+        ports = [{'id': '1'}]
+        ctx = self._make_vm_ctx_with_ports('network1', ports)
+        server = {'meta': {}}
+
+        _prepare_server_nics(
+            self.mock_neutron, ctx, server)
+
+        self.assertEqual([{'port-id': '1'}], server['nics'])
+
+    def test_port_not_to_mgmt_network(self):
+        """A NICs list entry is added with the network and the port.
+
+        A relationship to a port must not only add a NIC, but the NIC must
+        also make sure to use that port.
+        """
+        ports = [{'id': '1'}]
+        ctx = self._make_vm_ctx_with_ports('other', ports)
+        server = {'meta': {}}
+
+        _prepare_server_nics(
+            self.mock_neutron, ctx, server)
+        expected = [
+            {'net-id': 'other'},
+            {'port-id': '1'}
+        ]
+        self.assertEqual(expected, server['nics'])
+
+
+class TestBootFromVolume(unittest.TestCase):
+
+    @mock.patch('nova_plugin.server._get_boot_volume_relationships',
+                autospec=True)
+    def test_handle_boot_volume(self, mock_get_rels):
+        mock_get_rels.return_value.runtime_properties = {
+                'external_id': 'test-id',
+                'availability_zone': 'test-az',
+                }
+        server = {}
+        ctx = mock.MagicMock()
+        nova_plugin.server._handle_boot_volume(server, ctx)
+        self.assertEqual({'vda': 'test-id:::0'},
+                         server['block_device_mapping'])
+        self.assertEqual('test-az',
+                         server['availability_zone'])
+
+    @mock.patch('nova_plugin.server._get_boot_volume_relationships',
+                autospec=True, return_value=[])
+    def test_handle_boot_volume_no_boot_volume(self, *_):
+        server = {}
+        ctx = mock.MagicMock()
+        nova_plugin.server._handle_boot_volume(server, ctx)
+        self.assertNotIn('block_device_mapping', server)
+
+
+class TestImageFromRelationships(unittest.TestCase):
+
+    @mock.patch('glance_plugin.image.'
+                'get_openstack_ids_of_connected_nodes_by_openstack_type',
+                autospec=True, return_value=['test-id'])
+    def test_handle_boot_image(self, *_):
+        server = {}
+        ctx = mock.MagicMock()
+        nova_plugin.server.handle_image_from_relationship(server, 'image', ctx)
+        self.assertEqual({'image': 'test-id'}, server)
+
+    @mock.patch('glance_plugin.image.'
+                'get_openstack_ids_of_connected_nodes_by_openstack_type',
+                autospec=True, return_value=[])
+    def test_handle_boot_image_no_image(self, *_):
+        server = {}
+        ctx = mock.MagicMock()
+        nova_plugin.server.handle_image_from_relationship(server, 'image', ctx)
+        self.assertNotIn('image', server)
+
+
+class TestServerRelationships(unittest.TestCase):
+
+    def _get_ctx_mock(self, instance_id, boot):
+        rel_specs = [MockRelationshipContext(
+            target=MockRelationshipSubjectContext(node=MockNodeContext(
+                properties={'boot': boot}), instance=MockNodeInstanceContext(
+                runtime_properties={
+                    OPENSTACK_TYPE_PROPERTY: VOLUME_OPENSTACK_TYPE,
+                    OPENSTACK_ID_PROPERTY: instance_id
+                })))]
+        ctx = mock.MagicMock()
+        ctx.instance = MockNodeInstanceContext(relationships=rel_specs)
+        ctx.logger = setup_logger('mock-logger')
+        return ctx
+
+    def test_boot_volume_relationship(self):
+        instance_id = 'test-id'
+        ctx = self._get_ctx_mock(instance_id, True)
+        result = nova_plugin.server._get_boot_volume_relationships(
+            VOLUME_OPENSTACK_TYPE, ctx)
+        self.assertEqual(
+                instance_id,
+                result.runtime_properties['external_id'])
+
+    def test_no_boot_volume_relationship(self):
+        instance_id = 'test-id'
+        ctx = self._get_ctx_mock(instance_id, False)
+        result = nova_plugin.server._get_boot_volume_relationships(
+            VOLUME_OPENSTACK_TYPE, ctx)
+        self.assertFalse(result)
+
+
+class TestServerNetworkRuntimeProperties(unittest.TestCase):
+
+    @property
+    def mock_ctx(self):
+        return MockCloudifyContext(
+            node_id='test',
+            deployment_id='test',
+            properties={},
+            operation={'retry_number': 0},
+            provider_context={'resources': {}}
+        )
+
+    def test_server_networks_runtime_properties_empty_server(self):
+        ctx = self.mock_ctx
+        current_ctx.set(ctx=ctx)
+        server = mock.MagicMock()
+        setattr(server, 'networks', {})
+        with self.assertRaisesRegexp(
+                NonRecoverableError,
+                'The server was created but not attached to a network.'):
+            nova_plugin.server._set_network_and_ip_runtime_properties(server)
+
+    def test_server_networks_runtime_properties_valid_networks(self):
+        ctx = self.mock_ctx
+        current_ctx.set(ctx=ctx)
+        server = mock.MagicMock()
+        network_id = 'management_network'
+        network_ips = ['good', 'bad1', 'bad2']
+        setattr(server,
+                'networks',
+                {network_id: network_ips})
+        nova_plugin.server._set_network_and_ip_runtime_properties(server)
+        self.assertIn('networks', ctx.instance.runtime_properties.keys())
+        self.assertIn('ip', ctx.instance.runtime_properties.keys())
+        self.assertEquals(ctx.instance.runtime_properties['ip'], 'good')
+        self.assertEquals(ctx.instance.runtime_properties['networks'],
+                          {network_id: network_ips})
+
+    def test_server_networks_runtime_properties_empty_networks(self):
+        ctx = self.mock_ctx
+        current_ctx.set(ctx=ctx)
+        server = mock.MagicMock()
+        network_id = 'management_network'
+        network_ips = []
+        setattr(server,
+                'networks',
+                {network_id: network_ips})
+        nova_plugin.server._set_network_and_ip_runtime_properties(server)
+        self.assertIn('networks', ctx.instance.runtime_properties.keys())
+        self.assertIn('ip', ctx.instance.runtime_properties.keys())
+        self.assertEquals(ctx.instance.runtime_properties['ip'], None)
+        self.assertEquals(ctx.instance.runtime_properties['networks'],
+                          {network_id: network_ips})
diff --git a/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py b/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py
new file mode 100644
index 0000000..2ae4758
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py
@@ -0,0 +1,228 @@
+#########
+# 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
+from novaclient import exceptions as nova_exceptions
+
+import nova_plugin.server as server
+from cloudify.exceptions import NonRecoverableError
+from cloudify.mocks import MockCloudifyContext
+
+
+class TestServerImageAndFlavor(unittest.TestCase):
+
+    def test_no_image_and_no_flavor(self):
+        node_props = {
+            'image': '',
+            'flavor': ''
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            self.assertRaises(NonRecoverableError,
+                              server._handle_image_or_flavor,
+                              serv, nova_client, 'image')
+            self.assertRaises(NonRecoverableError,
+                              server._handle_image_or_flavor,
+                              serv, nova_client, 'flavor')
+
+    def test_image_and_flavor_properties_as_names(self):
+        node_props = {
+            'image': 'some-image-name',
+            'flavor': 'some-flavor-name'
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+
+    def test_image_and_flavor_properties_as_ids(self):
+        node_props = {
+            'image': 'some-image-id',
+            'flavor': 'some-flavor-id'
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+
+    def test_image_id_and_flavor_id(self):
+        node_props = {
+            'image': '',
+            'flavor': ''
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image'] = 'some-image-id'
+            serv['flavor'] = 'some-flavor-id'
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+
+    def test_image_name_and_flavor_name(self):
+        node_props = {
+            'image': '',
+            'flavor': ''
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image_name'] = 'some-image-name'
+            serv['flavor_name'] = 'some-flavor-name'
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertNotIn('image_name', serv)
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+        self.assertNotIn('flavor_name', serv)
+
+    def test_unknown_image_name_and_flavor_name(self):
+        node_props = {
+            'image': '',
+            'flavor': ''
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image_name'] = 'some-unknown-image-name'
+            serv['flavor_name'] = 'some-unknown-flavor-name'
+
+            self.assertRaises(nova_exceptions.NotFound,
+                              server._handle_image_or_flavor,
+                              serv, nova_client, 'image')
+            self.assertRaises(nova_exceptions.NotFound,
+                              server._handle_image_or_flavor,
+                              serv, nova_client, 'flavor')
+
+    def test_image_id_and_flavor_id_override_on_properties(self):
+        node_props = {
+            'image': 'properties-image-id',
+            'flavor': 'properties-flavor-id'
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image'] = 'some-image-id'
+            serv['flavor'] = 'some-flavor-id'
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+
+    def test_image_name_and_flavor_name_override_on_properties(self):
+        node_props = {
+            'image': 'properties-image-id',
+            'flavor': 'properties-flavor-id'
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image_name'] = 'some-image-name'
+            serv['flavor_name'] = 'some-flavor-name'
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertNotIn('image_name', serv)
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+        self.assertNotIn('flavor_name', serv)
+
+    def test_image_name_and_flavor_name_override_on_image_and_flavor_ids(self):
+        node_props = {
+            'image': '',
+            'flavor': ''
+        }
+        with mock.patch('nova_plugin.server.ctx',
+                        self._get_mock_ctx_with_node_properties(node_props)):
+            nova_client = self._get_mocked_nova_client()
+
+            serv = {}
+            serv['image'] = 'some-bad-image-id'
+            serv['image_name'] = 'some-image-name'
+            serv['flavor'] = 'some-bad-flavor-id'
+            serv['flavor_name'] = 'some-flavor-name'
+            server._handle_image_or_flavor(serv, nova_client, 'image')
+            server._handle_image_or_flavor(serv, nova_client, 'flavor')
+
+        self.assertEquals('some-image-id', serv.get('image'))
+        self.assertNotIn('image_name', serv)
+        self.assertEquals('some-flavor-id', serv.get('flavor'))
+        self.assertNotIn('flavor_name', serv)
+
+    @staticmethod
+    def _get_mocked_nova_client():
+        nova_client = mock.MagicMock()
+
+        def mock_get_if_exists(prop_name, **kwargs):
+            is_image = prop_name == 'image'
+            searched_name = kwargs.get('name')
+            if (is_image and searched_name == 'some-image-name') or \
+                    (not is_image and searched_name == 'some-flavor-name'):
+                result = mock.MagicMock()
+                result.id = 'some-image-id' if \
+                    is_image else 'some-flavor-id'
+                return result
+            return []
+
+        def mock_find_generator(prop_name):
+            def mock_find(**kwargs):
+                result = mock_get_if_exists(prop_name, **kwargs)
+                if not result:
+                    raise nova_exceptions.NotFound(404)
+                return result
+            return mock_find
+
+        nova_client.cosmo_plural = lambda x: '{0}s'.format(x)
+        nova_client.cosmo_get_if_exists = mock_get_if_exists
+        nova_client.images.find = mock_find_generator('image')
+        nova_client.flavors.find = mock_find_generator('flavor')
+        return nova_client
+
+    @staticmethod
+    def _get_mock_ctx_with_node_properties(properties):
+        return MockCloudifyContext(node_id='test_node_id',
+                                   properties=properties)
diff --git a/aria/multivim-plugin/nova_plugin/tests/test_userdata.py b/aria/multivim-plugin/nova_plugin/tests/test_userdata.py
new file mode 100644
index 0000000..d7f056d
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/test_userdata.py
@@ -0,0 +1,63 @@
+#########
+# Copyright (c) 2015 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
+
+from cloudify.mocks import MockCloudifyContext
+
+from nova_plugin import userdata
+
+
+def ctx_mock():
+    result = MockCloudifyContext(
+        node_id='d',
+        properties={})
+    result.node.type_hierarchy = ['cloudify.nodes.Compute']
+    return result
+
+
+class TestServerUserdataHandling(unittest.TestCase):
+
+    @mock.patch('nova_plugin.userdata.ctx', ctx_mock())
+    def test_no_userdata(self):
+        server_conf = {}
+        userdata.handle_userdata(server_conf)
+        self.assertEqual(server_conf, {})
+
+    def test_agent_installation_userdata(self):
+        ctx = ctx_mock()
+        ctx.agent.init_script = lambda: 'SCRIPT'
+        with mock.patch('nova_plugin.userdata.ctx', ctx):
+            server_conf = {}
+            userdata.handle_userdata(server_conf)
+            self.assertEqual(server_conf, {'userdata': 'SCRIPT'})
+
+    @mock.patch('nova_plugin.userdata.ctx', ctx_mock())
+    def test_existing_userdata(self):
+        server_conf = {'userdata': 'EXISTING'}
+        server_conf_copy = server_conf.copy()
+        userdata.handle_userdata(server_conf)
+        self.assertEqual(server_conf, server_conf_copy)
+
+    def test_existing_and_agent_installation_userdata(self):
+        ctx = ctx_mock()
+        ctx.agent.init_script = lambda: '#! SCRIPT'
+        with mock.patch('nova_plugin.userdata.ctx', ctx):
+            server_conf = {'userdata': '#! EXISTING'}
+            userdata.handle_userdata(server_conf)
+            self.assertTrue(server_conf['userdata'].startswith(
+                'Content-Type: multi'))
diff --git a/aria/multivim-plugin/nova_plugin/tests/test_validation.py b/aria/multivim-plugin/nova_plugin/tests/test_validation.py
new file mode 100644
index 0000000..aa1dfdd
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/tests/test_validation.py
@@ -0,0 +1,194 @@
+#########
+# 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 os
+from os import path
+import tempfile
+import shutil
+
+import unittest
+import mock
+
+from cloudify.test_utils import workflow_test
+from nova_plugin.keypair import creation_validation
+from cloudify.exceptions import NonRecoverableError
+
+PRIVATE_KEY_NAME = 'private_key'
+
+
+class TestValidation(unittest.TestCase):
+
+    blueprint_path = path.join('resources',
+                               'test-keypair-validation-blueprint.yaml')
+
+    def setUp(self):
+        _, fp = tempfile.mkstemp()
+        self.private_key = fp
+        _, fp = tempfile.mkstemp()
+        self.not_readable_private_key = fp
+        os.chmod(self.not_readable_private_key, 0o200)
+        self.temp_dir = tempfile.mkdtemp()
+        self.not_writable_temp_dir_r = tempfile.mkdtemp()
+        os.chmod(self.not_writable_temp_dir_r, 0o400)
+        self.not_writable_temp_dir_rx = tempfile.mkdtemp()
+        os.chmod(self.not_writable_temp_dir_rx, 0o500)
+        self.not_writable_temp_dir_rw = tempfile.mkdtemp()
+        os.chmod(self.not_writable_temp_dir_rw, 0o600)
+
+    def tearDown(self):
+        if self.private_key:
+            os.remove(self.private_key)
+
+        if self.not_readable_private_key:
+            os.remove(self.not_readable_private_key)
+
+        shutil.rmtree(self.not_writable_temp_dir_r, ignore_errors=True)
+        shutil.rmtree(self.not_writable_temp_dir_rx, ignore_errors=True)
+        shutil.rmtree(self.not_writable_temp_dir_rw, ignore_errors=True)
+        shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+    def new_keypair_create(self, *args, **kwargs):
+        creation_validation(*args, **kwargs)
+
+    def new_keypair_create_with_exception(self, *args, **kwargs):
+        self.assertRaises(NonRecoverableError, creation_validation,
+                          *args, **kwargs)
+
+    def get_keypair_inputs_private_key(self, is_external, **kwargs):
+        return {
+            'private_key': self.private_key,
+            'is_keypair_external': is_external
+        }
+
+    def get_keypair_inputs_not_readable_private_key(self,
+                                                    is_external, **kwargs):
+        return {
+            'private_key': self.not_readable_private_key,
+            'is_keypair_external': is_external
+        }
+
+    def get_keypair_inputs_not_writable_dir_r(self, is_external, **kwargs):
+        return {
+            'private_key': path.join(self.not_writable_temp_dir_r,
+                                     PRIVATE_KEY_NAME),
+            'is_keypair_external': is_external
+        }
+
+    def get_keypair_inputs_not_writable_dir_rx(self, is_external, **kwargs):
+        return {
+            'private_key': path.join(self.not_writable_temp_dir_rx,
+                                     PRIVATE_KEY_NAME),
+            'is_keypair_external': is_external
+        }
+
+    def get_keypair_inputs_not_writable_dir_rw(self, is_external, **kwargs):
+        return {
+            'private_key': path.join(self.not_writable_temp_dir_rw,
+                                     PRIVATE_KEY_NAME),
+            'is_keypair_external': is_external
+        }
+
+    def get_keypair_inputs_temp_dir(self, is_external, **kwargs):
+        return {
+            'private_key': path.join(self.temp_dir, PRIVATE_KEY_NAME),
+            'is_keypair_external': is_external
+        }
+
+    @workflow_test(blueprint_path, inputs={
+        'private_key': '',
+        'is_keypair_external': False
+    })
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_valid_config(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path, inputs='get_keypair_inputs_private_key',
+                   input_func_kwargs={'is_external': True})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_valid_config_external(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path, inputs='get_keypair_inputs_temp_dir',
+                   input_func_kwargs={'is_external': True})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_no_private_key(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path, inputs='get_keypair_inputs_private_key',
+                   input_func_kwargs={'is_external': False})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_local_and_exists(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path, inputs='get_keypair_inputs_temp_dir',
+                   input_func_kwargs={'is_external': False})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_local_temp_dir(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path,
+                   inputs='get_keypair_inputs_not_writable_dir_r',
+                   input_func_kwargs={'is_external': False})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_local_non_writable_dir_r(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path,
+                   inputs='get_keypair_inputs_not_writable_dir_rx',
+                   input_func_kwargs={'is_external': False})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_local_non_writable_dir_rx(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path,
+                   inputs='get_keypair_inputs_not_writable_dir_rw',
+                   input_func_kwargs={'is_external': False})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_local_non_writable_dir_rw(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
+
+    @workflow_test(blueprint_path,
+                   inputs='get_keypair_inputs_not_readable_private_key',
+                   input_func_kwargs={'is_external': True})
+    @mock.patch('nova_plugin.keypair.validate_resource')
+    def test_keypair_not_readable_private_key(self, cfy_local, *args):
+
+        with mock.patch('nova_plugin.keypair.create',
+                        new=self.new_keypair_create_with_exception):
+            cfy_local.execute('install', task_retries=0)
diff --git a/aria/multivim-plugin/nova_plugin/userdata.py b/aria/multivim-plugin/nova_plugin/userdata.py
new file mode 100644
index 0000000..ba63bb5
--- /dev/null
+++ b/aria/multivim-plugin/nova_plugin/userdata.py
@@ -0,0 +1,50 @@
+#########
+# Copyright (c) 2015 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 requests
+
+from cloudify import compute
+from cloudify import exceptions
+from cloudify import ctx
+
+
+def handle_userdata(server):
+
+    existing_userdata = server.get('userdata')
+    install_agent_userdata = ctx.agent.init_script()
+
+    if not (existing_userdata or install_agent_userdata):
+        return
+
+    if isinstance(existing_userdata, dict):
+        ud_type = existing_userdata['type']
+        if ud_type not in userdata_handlers:
+            raise exceptions.NonRecoverableError(
+                "Invalid type '{0}' for server userdata)".format(ud_type))
+        existing_userdata = userdata_handlers[ud_type](existing_userdata)
+
+    if not existing_userdata:
+        final_userdata = install_agent_userdata
+    elif not install_agent_userdata:
+        final_userdata = existing_userdata
+    else:
+        final_userdata = compute.create_multi_mimetype_userdata(
+            [existing_userdata, install_agent_userdata])
+    server['userdata'] = final_userdata
+
+
+userdata_handlers = {
+    'http': lambda params: requests.get(params['url']).text
+}