<filename>aria/multivim-plugin/src/main/python/multivim-plugin/nova_plugin/server.py
#########
# 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)]
