# 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 base64
import copy

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils

from senlin.common import constraints
from senlin.common import consts
from senlin.common import context
from senlin.common import exception as exc
from senlin.common.i18n import _
from senlin.common import schema
from senlin.objects import node as node_obj
from senlin.profiles import base

LOG = logging.getLogger(__name__)


class ServerProfile(base.Profile):
    """Profile for an OpenStack Nova server."""

    VERSIONS = {
        '1.0': [
            {'status': consts.SUPPORTED, 'since': '2016.04'}
        ]
    }

    KEYS = (
        CONTEXT, ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE,
        BLOCK_DEVICE_MAPPING_V2,
        CONFIG_DRIVE, FLAVOR, IMAGE, KEY_NAME, METADATA,
        NAME, NETWORKS, PERSONALITY, SECURITY_GROUPS,
        USER_DATA, SCHEDULER_HINTS,
    ) = (
        'context', 'admin_pass', 'auto_disk_config', 'availability_zone',
        'block_device_mapping_v2',
        'config_drive', 'flavor', 'image', 'key_name', 'metadata',
        'name', 'networks', 'personality', 'security_groups',
        'user_data', 'scheduler_hints',
    )

    BDM2_KEYS = (
        BDM2_UUID, BDM2_SOURCE_TYPE, BDM2_DESTINATION_TYPE,
        BDM2_DISK_BUS, BDM2_DEVICE_NAME, BDM2_VOLUME_SIZE,
        BDM2_GUEST_FORMAT, BDM2_BOOT_INDEX, BDM2_DEVICE_TYPE,
        BDM2_DELETE_ON_TERMINATION,
    ) = (
        'uuid', 'source_type', 'destination_type', 'disk_bus',
        'device_name', 'volume_size', 'guest_format', 'boot_index',
        'device_type', 'delete_on_termination',
    )

    NETWORK_KEYS = (
        PORT, VNIC_TYPE, FIXED_IP, NETWORK, PORT_SECURITY_GROUPS,
        FLOATING_NETWORK, FLOATING_IP,
    ) = (
        'port', 'vnic_type', 'fixed_ip', 'network', 'security_groups',
        'floating_network', 'floating_ip',
    )

    PERSONALITY_KEYS = (
        PATH, CONTENTS,
    ) = (
        'path', 'contents',
    )

    SCHEDULER_HINTS_KEYS = (
        GROUP,
    ) = (
        'group',
    )

    properties_schema = {
        CONTEXT: schema.Map(
            _('Customized security context for operating servers.'),
        ),
        ADMIN_PASS: schema.String(
            _('Password for the administrator account.'),
        ),
        AUTO_DISK_CONFIG: schema.Boolean(
            _('Whether the disk partition is done automatically.'),
            default=True,
        ),
        AVAILABILITY_ZONE: schema.String(
            _('Name of availability zone for running the server.'),
        ),
        BLOCK_DEVICE_MAPPING_V2: schema.List(
            _('A list specifying the properties of block devices to be used '
              'for this server.'),
            schema=schema.Map(
                _('A map specifying the properties of a block device to be '
                  'used by the server.'),
                schema={
                    BDM2_UUID: schema.String(
                        _('ID of the source image, snapshot or volume'),
                    ),
                    BDM2_SOURCE_TYPE: schema.String(
                        _("Volume source type, must be one of 'image', "
                          "'snapshot', 'volume' or 'blank'"),
                        required=True,
                    ),
                    BDM2_DESTINATION_TYPE: schema.String(
                        _("Volume destination type, must be 'volume' or "
                          "'local'"),
                        required=True,
                    ),
                    BDM2_DISK_BUS: schema.String(
                        _('Bus of the device.'),
                    ),
                    BDM2_DEVICE_NAME: schema.String(
                        _('Name of the device(e.g. vda, xda, ....).'),
                    ),
                    BDM2_VOLUME_SIZE: schema.Integer(
                        _('Size of the block device in MB(for swap) and '
                          'in GB(for other formats)'),
                        required=True,
                    ),
                    BDM2_GUEST_FORMAT: schema.String(
                        _('Specifies the disk file system format(e.g. swap, '
                          'ephemeral, ...).'),
                    ),
                    BDM2_BOOT_INDEX: schema.Integer(
                        _('Define the boot order of the device'),
                    ),
                    BDM2_DEVICE_TYPE: schema.String(
                        _('Type of the device(e.g. disk, cdrom, ...).'),
                    ),
                    BDM2_DELETE_ON_TERMINATION: schema.Boolean(
                        _('Whether to delete the volume when the server '
                          'stops.'),
                    ),
                }
            ),
        ),
        CONFIG_DRIVE: schema.Boolean(
            _('Whether config drive should be enabled for the server.'),
        ),
        FLAVOR: schema.String(
            _('ID of flavor used for the server.'),
            required=True,
            updatable=True,
        ),
        IMAGE: schema.String(
            # IMAGE is not required, because there could be BDM or BDMv2
            # support and the corresponding settings effective
            _('ID of image to be used for the new server.'),
            updatable=True,
        ),
        KEY_NAME: schema.String(
            _('Name of Nova keypair to be injected to server.'),
        ),
        METADATA: schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'server created. Both key and value must be <=255 chars.'),
            updatable=True,
        ),
        NAME: schema.String(
            _('Name of the server. When omitted, the node name will be used.'),
            updatable=True,
        ),
        NETWORKS: schema.List(
            _('List of networks for the server.'),
            schema=schema.Map(
                _('A map specifying the properties of a network for uses.'),
                schema={
                    NETWORK: schema.String(
                        _('Name or ID of network to create a port on.'),
                    ),
                    PORT: schema.String(
                        _('Port ID to be used by the network.'),
                    ),
                    VNIC_TYPE: schema.String(
                        _('Define vnic_type to be used by port'),
                    ),
                    FIXED_IP: schema.String(
                        _('Fixed IP to be used by the network.'),
                    ),
                    PORT_SECURITY_GROUPS: schema.List(
                        _('A list of security groups to be attached to '
                          'this port.'),
                        schema=schema.String(
                            _('Name of a security group'),
                            required=True,
                        ),
                    ),
                    FLOATING_NETWORK: schema.String(
                        _('The network on which to create a floating IP'),
                    ),
                    FLOATING_IP: schema.String(
                        _('The floating IP address to be associated with '
                          'this port.'),
                    ),
                },
            ),
            updatable=True,
        ),
        PERSONALITY: schema.List(
            _('List of files to be injected into the server, where each.'),
            schema=schema.Map(
                _('A map specifying the path & contents for an injected '
                  'file.'),
                schema={
                    PATH: schema.String(
                        _('In-instance path for the file to be injected.'),
                        required=True,
                    ),
                    CONTENTS: schema.String(
                        _('Contents of the file to be injected.'),
                        required=True,
                    ),
                },
            ),
        ),
        SCHEDULER_HINTS: schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'Scheduler hints. Both key and value must be <=255 chars.'),
        ),

        SECURITY_GROUPS: schema.List(
            _('List of security groups.'),
            schema=schema.String(
                _('Name of a security group'),
                required=True,
            ),
        ),
        USER_DATA: schema.String(
            _('User data to be exposed by the metadata server.'),
        ),
    }

    OP_NAMES = (
        OP_REBOOT, OP_REBUILD, OP_CHANGE_PASSWORD, OP_PAUSE, OP_UNPAUSE,
        OP_SUSPEND, OP_RESUME, OP_LOCK, OP_UNLOCK, OP_START, OP_STOP,
        OP_RESCUE, OP_UNRESCUE, OP_EVACUATE, OP_MIGRATE,
    ) = (
        'reboot', 'rebuild', 'change_password', 'pause', 'unpause',
        'suspend', 'resume', 'lock', 'unlock', 'start', 'stop',
        'rescue', 'unrescue', 'evacuate', 'migrate',
    )

    ADMIN_PASSWORD = 'admin_pass'
    RESCUE_IMAGE = 'image_ref'
    EVACUATE_OPTIONS = (
        EVACUATE_HOST, EVACUATE_FORCE
    ) = (
        'host', 'force'
    )

    OPERATIONS = {
        OP_REBOOT: schema.Operation(
            _("Reboot the nova server."),
            schema={
                consts.REBOOT_TYPE: schema.StringParam(
                    _("Type of reboot which can be 'SOFT' or 'HARD'."),
                    default=consts.REBOOT_SOFT,
                    constraints=[
                        constraints.AllowedValues(consts.REBOOT_TYPES),
                    ]
                )
            }
        ),
        OP_REBUILD: schema.Operation(
            _("Rebuild the server using current image and admin password."),
        ),
        OP_CHANGE_PASSWORD: schema.Operation(
            _("Change the administrator password."),
            schema={
                ADMIN_PASSWORD: schema.StringParam(
                    _("New password for the administrator.")
                )
            }
        ),
        OP_PAUSE: schema.Operation(
            _("Pause the server from running."),
        ),
        OP_UNPAUSE: schema.Operation(
            _("Unpause the server to running state."),
        ),
        OP_SUSPEND: schema.Operation(
            _("Suspend the running of the server."),
        ),
        OP_RESUME: schema.Operation(
            _("Resume the running of the server."),
        ),
        OP_LOCK: schema.Operation(
            _("Lock the server."),
        ),
        OP_UNLOCK: schema.Operation(
            _("Unlock the server."),
        ),
        OP_START: schema.Operation(
            _("Start the server."),
        ),
        OP_STOP: schema.Operation(
            _("Stop the server."),
        ),
        OP_RESCUE: schema.Operation(
            _("Rescue the server."),
            schema={
                RESCUE_IMAGE: schema.StringParam(
                    _("A string referencing the image to use."),
                ),
            }
        ),
        OP_UNRESCUE: schema.Operation(
            _("Unrescue the server."),
        ),
        OP_EVACUATE: schema.Operation(
            _("Evacuate the server to a different host."),
            schema={
                EVACUATE_HOST: schema.StringParam(
                    _("The target host to evacuate the server."),
                ),
                EVACUATE_FORCE: schema.StringParam(
                    _("Whether the evacuation should be a forced one.")
                )
            }
        )
    }

    def __init__(self, type_name, name, **kwargs):
        super(ServerProfile, self).__init__(type_name, name, **kwargs)
        self.server_id = None
        self.stop_timeout = cfg.CONF.default_nova_timeout

    def _validate_az(self, obj, az_name, reason=None):
        try:
            res = self.compute(obj).validate_azs([az_name])
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=str(ex))
            else:
                raise

        if not res:
            msg = _("The specified %(key)s '%(value)s' could not be found"
                    ) % {'key': self.AVAILABILITY_ZONE, 'value': az_name}
            if reason == 'create':
                raise exc.EResourceCreation(type='server', message=msg)
            else:
                raise exc.InvalidSpec(message=msg)

        return az_name

    def _validate_flavor(self, obj, name_or_id, reason=None):
        flavor = None
        msg = ''
        try:
            flavor = self.compute(obj).flavor_find(name_or_id, False)
        except exc.InternalError as ex:
            msg = str(ex)
            if reason is None:  # reason is 'validate'
                if ex.code == 404:
                    msg = _("The specified %(k)s '%(v)s' could not be found."
                            ) % {'k': self.FLAVOR, 'v': name_or_id}
                    raise exc.InvalidSpec(message=msg)
                else:
                    raise

        if flavor is not None:
            if not flavor.is_disabled:
                return flavor
            msg = _("The specified %(k)s '%(v)s' is disabled"
                    ) % {'k': self.FLAVOR, 'v': name_or_id}

        if reason == 'create':
            raise exc.EResourceCreation(type='server', message=msg)
        elif reason == 'update':
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=msg)
        else:
            raise exc.InvalidSpec(message=msg)

    def _validate_image(self, obj, name_or_id, reason=None):
        try:
            return self.glance(obj).image_find(name_or_id, False)
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=str(ex))
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))
            elif ex.code == 404:
                msg = _("The specified %(k)s '%(v)s' could not be found."
                        ) % {'k': self.IMAGE, 'v': name_or_id}
                raise exc.InvalidSpec(message=msg)
            else:
                raise

    def _validate_keypair(self, obj, name_or_id, reason=None):
        try:
            return self.compute(obj).keypair_find(name_or_id, False)
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=str(ex))
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))
            elif ex.code == 404:
                msg = _("The specified %(k)s '%(v)s' could not be found."
                        ) % {'k': self.KEY_NAME, 'v': name_or_id}
                raise exc.InvalidSpec(message=msg)
            else:
                raise

    def _validate_volume(self, obj, name_or_id, reason=None):
        try:
            volume = self.block_storage(obj).volume_get(name_or_id)
            if volume.status == 'available':
                return volume

            msg = _("The volume %(k)s should be in 'available' status "
                    "but is in '%(v)s' status."
                    ) % {'k': name_or_id, 'v': volume.status}
            raise exc.InvalidSpec(message=msg)
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=str(ex))
            elif ex.code == 404:
                msg = _("The specified volume '%(k)s' could not be found."
                        ) % {'k': name_or_id}
                raise exc.InvalidSpec(message=msg)
            else:
                raise

    def do_validate(self, obj):
        """Validate if the spec has provided valid info for server creation.

        :param obj: The node object.
        """
        # validate availability_zone
        az_name = self.properties[self.AVAILABILITY_ZONE]
        if az_name is not None:
            self._validate_az(obj, az_name)

        # validate flavor
        flavor = self.properties[self.FLAVOR]
        self._validate_flavor(obj, flavor)

        # validate image
        image = self.properties[self.IMAGE]
        if image is not None:
            self._validate_image(obj, image)

        # validate key_name
        keypair = self.properties[self.KEY_NAME]
        if keypair is not None:
            self._validate_keypair(obj, keypair)

        # validate networks
        networks = self.properties[self.NETWORKS]
        for net in networks:
            self._validate_network(obj, net)

        return True

    def _resolve_bdm(self, obj, bdm, reason=None):
        for bd in bdm:
            for key in self.BDM2_KEYS:
                if bd[key] is None:
                    del bd[key]
            if 'uuid' in bd and 'source_type' in bd:
                if bd['source_type'] == 'image':
                    self._validate_image(obj, bd['uuid'], reason)
                elif bd['source_type'] == 'volume':
                    self._validate_volume(obj, bd['uuid'], reason)

        return bdm

    def _check_security_groups(self, nc, net_spec, result):
        """Check security groups.

        :param nc: network driver connection.
        :param net_spec: the specification to check.
        :param result: the result that is used as return value.
        :returns: None if succeeded or an error message if things go wrong.
        """
        sgs = net_spec.get(self.PORT_SECURITY_GROUPS)
        if not sgs:
            return

        res = []
        try:
            for sg in sgs:
                try:
                    # try to find sg scoped by project first
                    sg_obj = nc.security_group_find(
                        sg, project_id=self.project)
                except exc.InternalError:
                    # if it fails to find sg, try without project scope
                    sg_obj = nc.security_group_find(sg)
                res.append(sg_obj.id)
        except exc.InternalError as ex:
            return str(ex)

        result[self.PORT_SECURITY_GROUPS] = res
        return

    def _check_network(self, nc, net, result):
        """Check the specified network.

        :param nc: network driver connection.
        :param net: the name or ID of network to check.
        :param result: the result that is used as return value.
        :returns: None if succeeded or an error message if things go wrong.
        """
        if net is None:
            return
        try:
            net_obj = nc.network_get(net)
            if net_obj is None:
                return _("The specified network %s could not be found.") % net
            result[self.NETWORK] = net_obj.id
        except exc.InternalError as ex:
            return str(ex)

    def _check_port(self, nc, port, result):
        """Check the specified port.

        :param nc: network driver connection.
        :param port: the name or ID of port to check.
        :param result: the result that is used as return value.
        :returns: None if succeeded or an error message if things go wrong.
        """
        if port is None:
            return

        try:
            port_obj = nc.port_find(port)
            if port_obj.status != 'DOWN':
                return _("The status of the port %(p)s must be DOWN"
                         ) % {'p': port}
            result[self.PORT] = port_obj.id
            return
        except exc.InternalError as ex:
            return str(ex)

    def _check_floating_ip(self, nc, net_spec, result):
        """Check floating IP and network, if specified.

        :param nc: network driver connection.
        :param net_spec: the specification to check.
        :param result: the result that is used as return value.
        :returns: None if succeeded or an error message if things go wrong.
        """
        net = net_spec.get(self.FLOATING_NETWORK)
        if net:
            try:
                net_obj = nc.network_get(net)
                if net_obj is None:
                    return _("The floating network %s could not be found."
                             ) % net
                result[self.FLOATING_NETWORK] = net_obj.id
            except exc.InternalError as ex:
                return str(ex)

        flt_ip = net_spec.get(self.FLOATING_IP)
        if not flt_ip:
            return

        try:
            # Find floating ip with this address
            fip = nc.floatingip_find(flt_ip)
            if fip:
                if fip.status == 'ACTIVE':
                    return _('the floating IP %s has been used.') % flt_ip
                result['floating_ip_id'] = fip.id

            # Create a floating IP with address if floating ip unspecified
            if not net:
                return _('Must specify a network to create floating IP')

            result[self.FLOATING_IP] = flt_ip
            return
        except exc.InternalError as ex:
            return str(ex)

    def _validate_network(self, obj, net_spec, reason=None):

        def _verify(error):
            if error is None:
                return

            if reason == 'create':
                raise exc.EResourceCreation(type='server', message=error)
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=error)
            else:
                raise exc.InvalidSpec(message=error)

        nc = self.network(obj)
        result = {}

        # check network
        net = net_spec.get(self.NETWORK)
        error = self._check_network(nc, net, result)
        _verify(error)

        # check port
        port = net_spec.get(self.PORT)
        error = self._check_port(nc, port, result)
        _verify(error)

        if port is None and net is None:
            _verify(_("One of '%(p)s' and '%(n)s' must be provided"
                      ) % {'p': self.PORT, 'n': self.NETWORK})

        fixed_ip = net_spec.get(self.FIXED_IP)
        if fixed_ip:
            if port is not None:
                _verify(_("The '%(p)s' property and the '%(fip)s' property "
                          "cannot be specified at the same time"
                          ) % {'p': self.PORT, 'fip': self.FIXED_IP})
            result[self.FIXED_IP] = fixed_ip

        # Validate vnic_type input
        vnic_type = net_spec.get(self.VNIC_TYPE, None)
        if vnic_type is not None:
            if vnic_type not in ['normal', 'direct', 'macvtap']:
                _verify(_("vnic_type: '%(v)s' is not supported."
                          "(supported types are: normal, direct, macvtap)"
                          ) % {'v': vnic_type})
            result[self.VNIC_TYPE] = vnic_type

        # Check security_groups
        error = self._check_security_groups(nc, net_spec, result)
        _verify(error)

        # Check floating IP
        error = self._check_floating_ip(nc, net_spec, result)
        _verify(error)

        return result

    def _get_port(self, obj, net_spec):
        """Fetch or create a port.

        :param obj: The node object.
        :param net_spec: The parameters to create a port.
        :returns: Created port object and error message.
        """
        port_id = net_spec.get(self.PORT, None)
        if port_id:
            try:
                port = self.network(obj).port_find(port_id)
                return port, None
            except exc.InternalError as ex:
                return None, ex
        port_attr = {
            'network_id': net_spec.get(self.NETWORK),
        }
        fixed_ip = net_spec.get(self.FIXED_IP, None)
        if fixed_ip:
            port_attr['fixed_ips'] = [fixed_ip]
        security_groups = net_spec.get(self.PORT_SECURITY_GROUPS, [])
        if security_groups:
            port_attr['security_groups'] = security_groups
        vnic_type = net_spec.get(self.VNIC_TYPE, None)
        if vnic_type:
            port_attr['binding_vnic_type'] = vnic_type
        try:
            port = self.network(obj).port_create(**port_attr)
            LOG.debug('Network port_attr : %s', port)
            return port, None
        except exc.InternalError as ex:
            return None, ex

    def _delete_ports(self, obj, ports):
        """Delete ports.

        :param obj: The node object
        :param ports: A list of internal ports.
        :returns: None for succeed or error for failure.
        """
        pp = copy.deepcopy(ports)
        for port in pp:
            # remove port created by senlin
            if port.get('remove', False):
                try:
                    # remove floating IP created by senlin
                    if port.get('floating', None) and port[
                            'floating'].get('remove', False):
                        self.network(obj).floatingip_delete(
                            port['floating']['id'])
                    self.network(obj).port_delete(port['id'])
                except exc.InternalError as ex:
                    return ex
                ports.remove(port)
        node_data = obj.data
        node_data['internal_ports'] = ports
        node_obj.Node.update(self.context, obj.id, {'data': node_data})

    def _get_floating_ip(self, obj, fip_spec, port_id):
        """Find or Create a floating IP.

        :param obj: The node object.
        :param fip_spec: The parameters to create a floating ip
        :param port_id: The port ID to associate with
        :returns: A floating IP object and error message.
        """
        floating_ip_id = fip_spec.get('floating_ip_id', None)
        if floating_ip_id:
            try:
                fip = self.network(obj).floatingip_find(floating_ip_id)
                if fip.port_id is None:
                    attr = {'port_id': port_id}
                    fip = self.network(obj).floatingip_update(fip, **attr)
                return fip, None
            except exc.InternalError as ex:
                return None, ex
        net_id = fip_spec.get(self.FLOATING_NETWORK)
        fip_addr = fip_spec.get(self.FLOATING_IP)
        attr = {
            'port_id': port_id,
            'floating_network_id': net_id,
        }
        if fip_addr:
            attr.update({'floating_ip_address': fip_addr})
        try:
            fip = self.network(obj).floatingip_create(**attr)
            return fip, None
        except exc.InternalError as ex:
            return None, ex

    def _create_ports_from_properties(self, obj, networks, action_type):
        """Create or find ports based on networks property.

        :param obj: The node object.
        :param networks: The networks property used for node.
        :param action_type: Either 'create' or 'update'.

        :returns: A list of created port's attributes.
        """
        internal_ports = obj.data.get('internal_ports', [])
        if not networks:
            return []

        for net_spec in networks:
            net = self._validate_network(obj, net_spec, action_type)
            # Create port
            port, ex = self._get_port(obj, net)
            # Delete created ports before raise error
            if ex:
                d_ex = self._delete_ports(obj, internal_ports)
                if d_ex:
                    raise d_ex
                else:
                    raise ex
            port_attrs = {
                'id': port.id,
                'network_id': port.network_id,
                'security_group_ids': port.security_group_ids,
                'fixed_ips': port.fixed_ips
            }
            if self.PORT not in net:
                port_attrs.update({'remove': True})
            # Create floating ip
            if 'floating_ip_id' in net or self.FLOATING_NETWORK in net:
                fip, ex = self._get_floating_ip(obj, net, port_attrs['id'])
                if ex:
                    d_ex = self._delete_ports(obj, internal_ports)
                    if d_ex:
                        raise d_ex
                    else:
                        raise ex
                port_attrs['floating'] = {
                    'id': fip.id,
                    'floating_ip_address': fip.floating_ip_address,
                    'floating_network_id': fip.floating_network_id,
                }
                if self.FLOATING_NETWORK in net:
                    port_attrs['floating'].update({'remove': True})
            internal_ports.append(port_attrs)
        if internal_ports:
            try:
                node_data = obj.data
                node_data.update(internal_ports=internal_ports)
                node_obj.Node.update(self.context, obj.id, {'data': node_data})
            except exc.ResourceNotFound:
                self._rollback_ports(obj, internal_ports)
                raise
        return internal_ports

    def _build_metadata(self, obj, usermeta):
        """Build custom metadata for server.

        :param obj: The node object to operate on.
        :return: A dictionary containing the new metadata.
        """
        metadata = usermeta or {}
        metadata['cluster_node_id'] = obj.id
        if obj.cluster_id:
            metadata['cluster_id'] = obj.cluster_id
            metadata['cluster_node_index'] = str(obj.index)

        return metadata

    def _update_zone_info(self, obj, server):
        """Update the actual zone placement data.

        :param obj: The node object associated with this server.
        :param server: The server object returned from creation.
        """
        if server.availability_zone:
            placement = obj.data.get('placement', None)
            if not placement:
                obj.data['placement'] = {'zone': server.availability_zone}
            else:
                obj.data['placement'].setdefault('zone',
                                                 server.availability_zone)
            # It is safe to use admin context here
            ctx = context.get_admin_context()
            node_obj.Node.update(ctx, obj.id, {'data': obj.data})

    def do_create(self, obj):
        """Create a server for the node object.

        :param obj: The node object for which a server will be created.
        """
        kwargs = self._generate_kwargs()

        admin_pass = self.properties[self.ADMIN_PASS]
        if admin_pass:
            kwargs.pop(self.ADMIN_PASS)
            kwargs['adminPass'] = admin_pass

        auto_disk_config = self.properties[self.AUTO_DISK_CONFIG]
        kwargs.pop(self.AUTO_DISK_CONFIG)
        kwargs['OS-DCF:diskConfig'] = 'AUTO' if auto_disk_config else 'MANUAL'

        image_ident = self.properties[self.IMAGE]
        if image_ident is not None:
            image = self._validate_image(obj, image_ident, 'create')
            kwargs.pop(self.IMAGE)
            kwargs['imageRef'] = image.id

        flavor_ident = self.properties[self.FLAVOR]
        flavor = self._validate_flavor(obj, flavor_ident, 'create')
        kwargs.pop(self.FLAVOR)
        kwargs['flavorRef'] = flavor.id

        keypair_name = self.properties[self.KEY_NAME]
        if keypair_name:
            keypair = self._validate_keypair(obj, keypair_name, 'create')
            kwargs['key_name'] = keypair.name

        kwargs['name'] = self.properties[self.NAME] or obj.name

        metadata = self._build_metadata(obj, self.properties[self.METADATA])
        kwargs['metadata'] = metadata

        block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2]
        if block_device_mapping_v2 is not None:
            kwargs['block_device_mapping_v2'] = self._resolve_bdm(
                obj, block_device_mapping_v2, 'create')

        user_data = self.properties[self.USER_DATA]
        if user_data is not None:
            ud = encodeutils.safe_encode(user_data)
            kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud))

        networks = self.properties[self.NETWORKS]
        ports = None
        if networks is not None:
            ports = self._create_ports_from_properties(
                obj, networks, 'create')
            kwargs['networks'] = [
                {'port': port['id']} for port in ports]

        secgroups = self.properties[self.SECURITY_GROUPS]
        if secgroups:
            kwargs['security_groups'] = [{'name': sg} for sg in secgroups]

        if 'placement' in obj.data:
            if 'zone' in obj.data['placement']:
                kwargs['availability_zone'] = obj.data['placement']['zone']

            if 'servergroup' in obj.data['placement']:
                group_id = obj.data['placement']['servergroup']
                hints = self.properties.get(self.SCHEDULER_HINTS) or {}
                hints.update({'group': group_id})
                kwargs['scheduler_hints'] = hints

        server = None
        resource_id = None
        try:
            server = self.compute(obj).server_create(**kwargs)
            self.compute(obj).wait_for_server(
                server.id, timeout=cfg.CONF.default_nova_timeout)
            server = self.compute(obj).server_get(server.id)
            # Update zone placement info if available
            self._update_zone_info(obj, server)
            return server.id
        except exc.ResourceNotFound:
            self._rollback_ports(obj, ports)
            self._rollback_instance(obj, server)
            raise
        except exc.InternalError as ex:
            if server and server.id:
                resource_id = server.id
                LOG.debug('Deleting server %s that is ERROR state after'
                          ' create.', server.id)

                try:
                    obj.physical_id = server.id
                    self.do_delete(obj, internal_ports=ports)
                except Exception:
                    LOG.error('Failed to delete server %s', server.id)
                    pass
            elif ports:
                self._delete_ports(obj, ports)
            raise exc.EResourceCreation(type='server',
                                        message=str(ex),
                                        resource_id=resource_id)

    def _generate_kwargs(self):
        """Generate the base kwargs for a server.

        :return:
        """
        kwargs = {}
        for key in self.KEYS:
            # context is treated as connection parameters
            if key == self.CONTEXT:
                continue

            if self.properties[key] is not None:
                kwargs[key] = self.properties[key]
        return kwargs

    def _rollback_ports(self, obj, ports):
        """Rollback any ports created after a ResourceNotFound exception.

        :param obj: The node object.
        :param ports: A list of ports which attached to this server.
        :return:
        """
        if not ports:
            return

        LOG.warning(
            'Rolling back ports for Node %s.',
            obj.id
        )

        for port in ports:
            if not port.get('remove', False):
                continue
            try:
                if (port.get('floating') and
                        port['floating'].get('remove', False)):
                    self.network(obj).floatingip_delete(
                        port['floating']['id']
                    )
                self.network(obj).port_delete(port['id'])
            except exc.InternalError as ex:
                LOG.debug(
                    'Failed to delete port %s during rollback for Node %s: %s',
                    port['id'], obj.id, ex
                )

    def _rollback_instance(self, obj, server):
        """Rollback an instance created after a ResourceNotFound exception.

        :param obj: The node object.
        :param server: A server.
        :return:
        """
        if not server or not server.id:
            return

        LOG.warning(
            'Rolling back instance %s for Node %s.',
            server.id, obj.id
        )

        try:
            self.compute(obj).server_force_delete(server.id, True)
        except exc.InternalError as ex:
            LOG.debug(
                'Failed to delete instance %s during rollback for Node %s: %s',
                server.id, obj.id, ex
            )

    def do_delete(self, obj, **params):
        """Delete the physical resource associated with the specified node.

        :param obj: The node object to operate on.
        :param kwargs params: Optional keyword arguments for the delete
                              operation.
        :returns: This operation always return True unless exception is
                  caught.
        :raises: `EResourceDeletion` if interaction with compute service fails.
        """
        server_id = obj.physical_id
        ignore_missing = params.get('ignore_missing', True)
        internal_ports = obj.data.get('internal_ports', [])
        force = params.get('force', False)
        timeout = params.get('timeout', cfg.CONF.default_nova_timeout)

        try:
            if server_id:
                driver = self.compute(obj)
                if force:
                    driver.server_force_delete(server_id, ignore_missing)
                else:
                    driver.server_delete(server_id, ignore_missing)

                driver.wait_for_server_delete(server_id, timeout=timeout)
        except exc.InternalError as ex:
            raise exc.EResourceDeletion(type='server', id=server_id,
                                        message=str(ex))
        finally:
            if internal_ports:
                ex = self._delete_ports(obj, internal_ports)
                if ex:
                    raise exc.EResourceDeletion(type='server', d=server_id,
                                                message=str(ex))
        return True

    def _check_server_name(self, obj, profile):
        """Check if there is a new name to be assigned to the server.

        :param obj: The node object to operate on.
        :param new_profile: The new profile which may contain a name for
                            the server instance.
        :return: A tuple consisting a boolean indicating whether the name
                 needs change and the server name determined.
        """
        old_name = self.properties[self.NAME] or obj.name
        new_name = profile.properties[self.NAME] or obj.name
        if old_name == new_name:
            return False, new_name
        return True, new_name

    def _update_name(self, obj, new_name):
        """Update the name of the server.

        :param obj: The node object to operate.
        :param new_name: The new name for the server instance.
        :return: ``None``.
        :raises: ``EResourceUpdate``.
        """
        try:
            self.compute(obj).server_update(obj.physical_id, name=new_name)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

    def _check_password(self, obj, new_profile):
        """Check if the admin password has been changed in the new profile.

        :param obj: The server node to operate, not used currently.
        :param new_profile: The new profile which may contain a new password
                            for the server instance.
        :return: A tuple consisting a boolean indicating whether the password
                 needs a change and the password determined which could be
                 '' if new password is not set.
        """
        old_passwd = self.properties.get(self.ADMIN_PASS) or ''
        new_passwd = new_profile.properties[self.ADMIN_PASS] or ''
        if old_passwd == new_passwd:
            return False, new_passwd
        return True, new_passwd

    def _update_password(self, obj, new_password):
        """Update the admin password for the server.

        :param obj: The node object to operate.
        :param new_password: The new password for the server instance.
        :return: ``None``.
        :raises: ``EResourceUpdate``.
        """
        try:
            self.compute(obj).server_change_password(obj.physical_id,
                                                     new_password)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

    def _update_metadata(self, obj, new_profile):
        """Update the server metadata.

        :param obj: The node object to operate on.
        :param new_profile: The new profile that may contain some changes to
                            the metadata.
        :returns: ``None``
        :raises: `EResourceUpdate`.
        """
        old_meta = self._build_metadata(obj, self.properties[self.METADATA])
        new_meta = self._build_metadata(obj,
                                        new_profile.properties[self.METADATA])
        if new_meta == old_meta:
            return

        try:
            self.compute(obj).server_metadata_update(obj.physical_id, new_meta)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

    def _update_flavor(self, obj, new_profile):
        """Update server flavor.

        :param obj: The node object to operate on.
        :param old_flavor: The identity of the current flavor.
        :param new_flavor: The identity of the new flavor.
        :returns: Returns true if the flavor was updated or false otherwise.
        :raises: `EResourceUpdate` when operation was a failure.
        """
        old_flavor = self.properties[self.FLAVOR]
        new_flavor = new_profile.properties[self.FLAVOR]
        cc = self.compute(obj)
        oldflavor = self._validate_flavor(obj, old_flavor, 'update')
        newflavor = self._validate_flavor(obj, new_flavor, 'update')
        if oldflavor.id == newflavor.id:
            return False

        try:
            # server has to be active or stopped in order to resize
            # stop server if it is active
            server = cc.server_get(obj.physical_id)
            if server.status == consts.VS_ACTIVE:
                cc.server_stop(obj.physical_id)
                cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
                                   timeout=self.stop_timeout)
            elif server.status != consts.VS_SHUTOFF:
                raise exc.InternalError(
                    message='Server needs to be ACTIVE or STOPPED in order to'
                            ' update flavor.')
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

        try:
            cc.server_resize(obj.physical_id, newflavor.id)
            cc.wait_for_server(obj.physical_id, 'VERIFY_RESIZE')
        except exc.InternalError as ex:
            msg = str(ex)
            try:
                server = cc.server_get(obj.physical_id)
                if server.status == 'RESIZE':
                    cc.server_resize_revert(obj.physical_id)
                    cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)

                # start server back up in case of exception during resize
                cc.server_start(obj.physical_id)
                cc.wait_for_server(obj.physical_id, consts.VS_ACTIVE)
            except exc.InternalError as ex1:
                msg = str(ex1)
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=msg)

        try:
            cc.server_resize_confirm(obj.physical_id)
            cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

        return True

    def _update_image(self, obj, new_profile, new_name, new_password):
        """Update image used by server node.

        :param obj: The node object to operate on.
        :param new_profile: The profile which may contain a new image name or
                            ID to use.
        :param new_name: The name for the server node.
        :param newn_password: The new password for the administrative account
                              if provided.
        :returns: A boolean indicating whether the image needs an update.
        :raises: ``InternalError`` if operation was a failure.
        """
        new_image = new_profile.properties[self.IMAGE]
        if not new_image:
            msg = _("Updating Nova server with image set to None is not "
                    "supported by Nova")
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=msg)
        # check the new image first
        img_new = self._validate_image(obj, new_image, reason='update')
        new_image_id = img_new.id

        driver = self.compute(obj)

        try:
            server = driver.server_get(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))
        old_image_id = self._get_image_id(obj, server, 'updating')

        if new_image_id == old_image_id:
            return False

        try:
            # server has to be active or stopped in order to resize
            # stop server if it is active
            if server.status == consts.VS_ACTIVE:
                driver.server_stop(obj.physical_id)
                driver.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
                                       timeout=self.stop_timeout)
            elif server.status != consts.VS_SHUTOFF:
                raise exc.InternalError(
                    message='Server needs to be ACTIVE or STOPPED in order to'
                            ' update image.')

            driver.server_rebuild(obj.physical_id, new_image_id,
                                  new_name, new_password)
            driver.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))
        return True

    def _update_network_add_port(self, obj, networks):
        """Create new interfaces for the server node.

        :param obj: The node object to operate.
        :param networks: A list containing information about new network
                         interfaces to be created.
        :returns: ``None``.
        :raises: ``EResourceUpdate`` if interaction with drivers failed.
        """
        cc = self.compute(obj)
        try:
            server = cc.server_get(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))

        ports = self._create_ports_from_properties(
            obj, networks, 'update')
        for port in ports:
            params = {'port_id': port['id']}
            try:
                cc.server_interface_create(server, **params)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server',
                                          id=obj.physical_id,
                                          message=str(ex))

    def _find_port_by_net_spec(self, obj, net_spec, ports):
        """Find existing ports match with specific network properties.

        :param obj: The node object.
        :param net_spec: Network property of this profile.
        :param ports: A list of ports which attached to this server.
        :returns: A list of candidate ports matching this network spec.
        """
        # TODO(anyone): handle security_groups
        net = self._validate_network(obj, net_spec, 'update')
        selected_ports = []
        for p in ports:
            floating = p.get('floating', {})
            floating_network = net.get(self.FLOATING_NETWORK, None)
            if floating_network and floating.get(
                    'floating_network_id') != floating_network:
                continue
            floating_ip_address = net.get(self.FLOATING_IP, None)
            if floating_ip_address and floating.get(
                    'floating_ip_address') != floating_ip_address:
                continue
            # If network properties didn't contain floating ip,
            # then we should better not make a port with floating ip
            # as candidate.
            if (floating and not floating_network and not floating_ip_address):
                continue
            port_id = net.get(self.PORT, None)
            if port_id and p['id'] != port_id:
                continue
            fixed_ip = net.get(self.FIXED_IP, None)
            if fixed_ip:
                fixed_ips = [ff['ip_address'] for ff in p['fixed_ips']]
                if fixed_ip not in fixed_ips:
                    continue
            network = net.get(self.NETWORK, None)
            if network:
                net_id = self.network(obj).network_get(network).id
                if p['network_id'] != net_id:
                    continue
            selected_ports.append(p)
        return selected_ports

    def _update_network_remove_port(self, obj, networks):
        """Delete existing interfaces from the node.

        :param obj: The node object to operate.
        :param networks: A list containing information about network
                         interfaces to be created.
        :returns: ``None``
        :raises: ``EResourceUpdate``
        """
        cc = self.compute(obj)
        nc = self.network(obj)
        internal_ports = obj.data.get('internal_ports', [])

        if networks:
            try:
                # stop server if it is active
                server = cc.server_get(obj.physical_id)
                if server.status == consts.VS_ACTIVE:
                    cc.server_stop(obj.physical_id)
                    cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
                                       timeout=self.stop_timeout)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))

        for n in networks:
            candidate_ports = self._find_port_by_net_spec(
                obj, n, internal_ports)
            port = candidate_ports[0]
            try:
                # Detach port from server
                cc.server_interface_delete(port['id'], obj.physical_id)
                # delete port if created by senlin
                if port.get('remove', False):
                    nc.port_delete(port['id'], ignore_missing=True)
                # delete floating IP if created by senlin
                if (port.get('floating', None) and
                        port['floating'].get('remove', False)):
                    nc.floatingip_delete(port['floating']['id'],
                                         ignore_missing=True)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))
            internal_ports.remove(port)
        obj.data['internal_ports'] = internal_ports
        node_obj.Node.update(self.context, obj.id, {'data': obj.data})

    def _nw_compare(self, n1, n2, property):
        return n1.get(property, None) == n2.get(property, None)

    def _update_network_update_port(self, obj, networks):
        """Update existing port in network from the node.

           Currently only update to security group is supported.

        :param obj: The node object to operate.
        :param networks: A list networks that contain updated security groups.
        :returns: ``None``
        :raises: ``EResourceUpdate``
        """
        nc = self.network(obj)
        internal_ports = obj.data.get('internal_ports', [])

        # process each network to be updated
        for n in networks:
            # verify network properties and resolve names into ids
            net = self._validate_network(obj, n, 'update')

            # find existing port that matches network
            candidate_ports = self._find_port_by_net_spec(
                obj, net, internal_ports)
            port = candidate_ports[0]
            try:
                # set updated security groups for port
                port_attr = {
                    'security_groups': net.get(self.PORT_SECURITY_GROUPS, []),
                }
                LOG.debug("Setting security groups %s for port %s",
                          port_attr, port['id'])
                nc.port_update(port['id'], **port_attr)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))

    def _update_network(self, obj, new_profile):
        """Updating server network interfaces.

        :param obj: The node object to operate.
        :param new_profile: The new profile which may contain new network
                            settings.
        :return: Returns a tuple of booleans if network was created or deleted.
        :raises: ``EResourceUpdate`` if there are driver failures.
        """
        networks_current = self.properties[self.NETWORKS]
        networks_create = new_profile.properties[self.NETWORKS]
        networks_delete = copy.deepcopy(networks_current)
        networks_update = []

        for nw in networks_current:
            if nw in networks_create:
                # network already exist.  no need to create or delete it.
                LOG.debug("Network %s already exists, skip create/delete", nw)
                networks_create.remove(nw)
                networks_delete.remove(nw)

        # find networks for which only security group changed
        for nw in networks_current:
            # networks to be created with only sg changes
            sg_create_nw = [n for n in networks_create
                            if (self._nw_compare(n, nw, 'network') and
                                self._nw_compare(n, nw, 'port') and
                                self._nw_compare(n, nw, 'fixed_ip') and
                                self._nw_compare(n, nw, 'floating_network') and
                                self._nw_compare(n, nw, 'floating_ip'))]
            for n in sg_create_nw:
                # don't create networks with only security group changes
                LOG.debug("Network %s only has security group changes, "
                          "don't create/delete it.  Only update it.", n)
                networks_create.remove(n)
                networks_update.append(n)
                if nw in networks_delete:
                    networks_delete.remove(nw)

        # update network
        if networks_update:
            self._update_network_update_port(obj, networks_update)

        # Detach some existing interfaces
        if networks_delete:
            self._update_network_remove_port(obj, networks_delete)

        # Attach new interfaces
        if networks_create:
            self._update_network_add_port(obj, networks_create)

        return networks_create, networks_delete

    def do_update(self, obj, new_profile=None, **params):
        """Perform update on the server.

        :param obj: the server to operate on
        :param new_profile: the new profile for the server.
        :param params: a dictionary of optional parameters.
        :returns: True if update was successful or False otherwise.
        :raises: `EResourceUpdate` if operation fails.
        """
        if not obj.physical_id:
            return False

        if not new_profile:
            return False

        if not self.validate_for_update(new_profile):
            return False

        self.stop_timeout = params.get('cluster.stop_timeout_before_update',
                                       cfg.CONF.default_nova_timeout)

        if not isinstance(self.stop_timeout, int):
            raise exc.EResourceUpdate(
                type='server', id=obj.physical_id,
                message='cluster.stop_timeout_before_update value must be of '
                        'type int.')

        name_changed, new_name = self._check_server_name(obj, new_profile)
        passwd_changed, new_passwd = self._check_password(obj, new_profile)
        # Update server image: may have side effect of changing server name
        # and/or admin password
        image_changed = self._update_image(obj, new_profile, new_name,
                                           new_passwd)
        if not image_changed:
            # we do this separately only when rebuild wasn't performed
            if name_changed:
                self._update_name(obj, new_name)
            if passwd_changed:
                self._update_password(obj, new_passwd)

        # Update server flavor: note that flavor is a required property
        flavor_changed = self._update_flavor(obj, new_profile)
        network_created, network_deleted = self._update_network(
            obj, new_profile)

        # TODO(Yanyan Hu): Update block_device properties
        # Update server metadata
        self._update_metadata(obj, new_profile)

        # start server if it was stopped as part of this update operation
        if image_changed or flavor_changed or network_deleted:
            cc = self.compute(obj)
            try:
                server = cc.server_get(obj.physical_id)
                if server.status == consts.VS_SHUTOFF:
                    cc.server_start(obj.physical_id)
                    cc.wait_for_server(obj.physical_id, consts.VS_ACTIVE)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                          message=str(ex))

        return True

    def do_get_details(self, obj):
        known_keys = {
            'OS-DCF:diskConfig',
            'OS-EXT-AZ:availability_zone',
            'OS-EXT-STS:power_state',
            'OS-EXT-STS:vm_state',
            'accessIPv4',
            'accessIPv6',
            'config_drive',
            'created',
            'hostId',
            'id',
            'key_name',
            'locked',
            'metadata',
            'name',
            'os-extended-volumes:volumes_attached',
            'progress',
            'status',
            'updated'
        }
        if obj.physical_id is None or obj.physical_id == '':
            return {}

        driver = self.compute(obj)
        try:
            server = driver.server_get(obj.physical_id)
        except exc.InternalError as ex:
            return {
                'Error': {
                    'code': ex.code,
                    'message': str(ex)
                }
            }

        if server is None:
            return {}
        server_data = server.to_dict()
        if 'id' in server_data['image']:
            image_id = server_data['image']['id']
        else:
            image_id = server_data['image']
        attached_volumes = []
        if ('attached_volumes' in server_data and
                len(server_data['attached_volumes']) > 0):
            for volume in server_data['attached_volumes']:
                attached_volumes.append(volume['id'])
        details = {
            'image': image_id,
            'attached_volumes': attached_volumes,
            'flavor': self._get_flavor_id(obj, server_data),
        }
        for key in known_keys:
            if key in server_data:
                details[key] = server_data[key]

        # process special keys like 'OS-EXT-STS:task_state': these keys have
        # a default value '-' when not existing
        special_keys = [
            'OS-EXT-STS:task_state',
            'OS-SRV-USG:launched_at',
            'OS-SRV-USG:terminated_at',
        ]
        for key in special_keys:
            if key in server_data:
                val = server_data[key]
                details[key] = val if val else '-'

        # process network addresses
        details['addresses'] = copy.deepcopy(server_data['addresses'])

        # process security groups
        sgroups = []
        if 'security_groups' in server_data:
            if server_data['security_groups'] is not None:
                for sg in server_data['security_groups']:
                    sgroups.append(sg['name'])
        # when we have multiple nics the info will include the
        # security groups N times where N == number of nics. Be nice
        # and only display it once.
        sgroups = list(set(sgroups))
        if len(sgroups) == 0:
            details['security_groups'] = ''
        elif len(sgroups) == 1:
            details['security_groups'] = sgroups[0]
        else:
            details['security_groups'] = sgroups

        return dict((k, details[k]) for k in sorted(details))

    def _get_flavor_id(self, obj, server):
        """Get flavor id.

        :param obj: The node object.
        :param dict server: The server object.
        :return: The flavor_id for the server.
        """
        flavor = server['flavor']

        if 'id' in flavor:
            return flavor['id']

        return self.compute(obj).flavor_find(flavor['original_name'], False).id

    def _get_image_id(self, obj, server, op):
        """Get image id.

        :param obj: The node object.
        :param server: The server object.
        :param op: The operate on the node.
        :return: The image_id for the server.
        """
        image_id = None

        if server.image:
            image_id = server.image['id'] or server.image
        # when booting a nova server from volume, the image property
        # can be ignored.
        # we try to find a volume which is bootable and use its image_id
        # for the server.
        elif server.attached_volumes:
            cinder_driver = self.block_storage(obj)
            for volume_ids in server.attached_volumes:
                try:
                    vs = cinder_driver.volume_get(volume_ids['id'])
                    if vs.is_bootable:
                        image_id = vs.volume_image_metadata['image_id']
                except exc.InternalError as ex:
                    raise exc.EResourceOperation(op=op, type='server',
                                                 id=obj.physical_id,
                                                 message=str(ex))
        else:
            msg = _("server doesn't have an image and it has no "
                    "bootable volume")
            raise exc.EResourceOperation(op=op, type="server",
                                         id=obj.physical_id,
                                         message=msg)
        return image_id

    def _handle_generic_op(self, obj, driver_func_name,
                           op_name, expected_server_status=None,
                           **kwargs):
        """Generic handler for standard server operations."""
        if not obj.physical_id:
            return False
        server_id = obj.physical_id
        nova_driver = self.compute(obj)

        try:
            driver_func = getattr(nova_driver, driver_func_name)
            driver_func(server_id, **kwargs)
            if expected_server_status:
                nova_driver.wait_for_server(server_id, expected_server_status)
            return True
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op=op_name, type='server',
                                         id=server_id,
                                         message=str(ex))

    def do_adopt(self, obj, overrides=None, snapshot=False):
        """Adopt an existing server node for management.

        :param obj: A node object for this operation. It could be a puppet
            node that provides only 'user', 'project' and 'physical_id'
            properties when doing a preview. It can be a real Node object for
            node adoption.
        :param overrides: A dict containing the properties that will be
            overridden when generating a profile for the server.
        :param snapshot: A boolean flag indicating whether the profile should
            attempt a snapshot operation before adopting the server. If set to
            True, the ID of the snapshot will be used as the image ID.

        :returns: A dict containing the spec created from the server object or
            a dict containing error information if failure occurred.
        """
        driver = self.compute(obj)

        # TODO(Qiming): Add snapshot support
        # snapshot = driver.snapshot_create(...)

        error = {}
        try:
            server = driver.server_get(obj.physical_id)
        except exc.InternalError as ex:
            error = {'code': ex.code, 'message': str(ex)}

        if error:
            return {'Error': error}

        spec = {}
        # Context?
        # TODO(Qiming): Need to fetch admin password from a different API
        spec[self.AUTO_DISK_CONFIG] = server.disk_config == 'AUTO'

        spec[self.AVAILABILITY_ZONE] = server.availability_zone

        # TODO(Anyone): verify if this needs a format conversion
        bdm = server.block_device_mapping or []
        spec[self.BLOCK_DEVICE_MAPPING_V2] = bdm

        spec[self.CONFIG_DRIVE] = server.has_config_drive or False
        spec[self.FLAVOR] = server.flavor['id']
        spec[self.IMAGE] = self._get_image_id(obj, server, 'adopting')
        spec[self.KEY_NAME] = server.key_name

        # metadata
        metadata = server.metadata or {}
        metadata.pop('cluster_id', None)
        metadata.pop('cluster_node_id', None)
        metadata.pop('cluster_node_index', None)
        spec[self.METADATA] = metadata

        # name
        spec[self.NAME] = server.name

        networks = server.addresses
        net_list = []
        for network, interfaces in networks.items():
            for intf in interfaces:
                ip_type = intf.get('OS-EXT-IPS:type')
                net = {self.NETWORK: network}
                if ip_type == 'fixed' and net not in net_list:
                    net_list.append({self.NETWORK: network})

        spec[self.NETWORKS] = net_list
        # NOTE: the personality attribute is missing for ever.
        spec[self.SECURITY_GROUPS] = [
            sg['name'] for sg in server.security_groups
        ]
        # TODO(Qiming): get server user_data and parse it.
        # Note: user_data is returned in 2.3 microversion API, in a different
        # property name.
        # spec[self.USER_DATA] = server.user_data

        if overrides:
            spec.update(overrides)

        return spec

    def do_join(self, obj, cluster_id):
        if not obj.physical_id:
            return False

        driver = self.compute(obj)
        try:
            metadata = {}
            metadata['cluster_id'] = cluster_id
            metadata['cluster_node_index'] = str(obj.index)
            driver.server_metadata_update(obj.physical_id, metadata)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server', id=obj.physical_id,
                                      message=str(ex))
        return super(ServerProfile, self).do_join(obj, cluster_id)

    def do_leave(self, obj):
        if not obj.physical_id:
            return False

        keys = ['cluster_id', 'cluster_node_index']
        try:
            self.compute(obj).server_metadata_delete(obj.physical_id, keys)
        except exc.InternalError as ex:
            raise exc.EResourceDeletion(type='server', id=obj.physical_id,
                                        message=str(ex))
        return super(ServerProfile, self).do_leave(obj)

    def do_check(self, obj):
        if not obj.physical_id:
            return False

        try:
            server = self.compute(obj).server_get(obj.physical_id)
        except exc.InternalError as ex:
            if ex.code == 404:
                raise exc.EServerNotFound(type='server',
                                          id=obj.physical_id,
                                          message=str(ex))
            else:
                raise exc.EResourceOperation(op='checking', type='server',
                                             id=obj.physical_id,
                                             message=str(ex))

        if (server is None or server.status != consts.VS_ACTIVE):
            return False

        return True

    def do_healthcheck(self, obj, health_check_type):
        """Healthcheck operation.

        This method checks if a node is healthy.  If health check type is
        NODE_STATUS_POLLING it will check the server status.  If health check
        type is HYPERVISOR_STATUS_POLLING it will check the hypervisor state
        and status.

        :param obj: The node object to operate on.
        :param health_check_type: The type of health check.  Either
        NODE_STATUS_POLLING or HYPERVISOR_STATUS_POLLING.
        :return status: True indicates node is healthy, False indicates
            it is unhealthy.
        """

        if not obj.physical_id:
            if obj.status == 'BUILD' or obj.status == 'CREATING':
                return True

            LOG.info('%s for %s: server has no physical ID.',
                     consts.POLL_STATUS_FAIL, obj.name)
            return False

        try:
            server = self.compute(obj).server_get(obj.physical_id)
        except Exception as ex:
            if isinstance(ex, exc.InternalError) and ex.code == 404:
                # treat resource not found exception as unhealthy
                LOG.info('%s for %s: server was not found.',
                         consts.POLL_STATUS_FAIL, obj.name)
                return False
            else:
                # treat all other exceptions as healthy
                LOG.info(
                    '%s for %s: Exception when trying to get server info but '
                    'ignoring this error: %s.',
                    consts.POLL_STATUS_PASS, obj.name, ex.message)
                return True

        if server is None:
            # no server information is available, treat the node as healthy
            LOG.info(
                '%s for %s: No server information was returned but ignoring '
                'this error.',
                consts.POLL_STATUS_PASS, obj.name)
            return True

        if health_check_type == consts.NODE_STATUS_POLLING:
            return self._do_healthcheck_server(obj, server)
        elif health_check_type == consts.HYPERVISOR_STATUS_POLLING:
            return self._do_healthcheck_hypervisor(obj, server)
        else:
            LOG.info('%s for %s: ignoring invalid health check type %s',
                     consts.POLL_STATUS_PASS, obj.name, health_check_type)
            return True

    def _do_healthcheck_server(self, obj, server):
        """Healthcheck operation based on server.

        This method checks if a server node is healthy by getting the server
        status from nova.  A server is considered unhealthy if it does not
        exist or its status is one of the following:
        - ERROR
        - SHUTOFF
        - DELETED

        :param obj: The node object to operate on.
        :param server: The server object associated with the node.
        :return status: True indicates node is healthy, False indicates
            it is unhealthy.
        """

        unhealthy_server_status = [consts.VS_ERROR, consts.VS_SHUTOFF,
                                   consts.VS_DELETED]

        if server.status in unhealthy_server_status:
            LOG.info('%s for %s: server status is unhealthy.',
                     consts.POLL_STATUS_FAIL, obj.name)
            return False

        LOG.info('%s for %s', consts.POLL_STATUS_PASS, obj.name)
        return True

    def _do_healthcheck_hypervisor(self, obj, server):
        """Healthcheck operation based on hypervisor.

        This method checks if a server node is healthy by getting the
        hypervisor state and status from nova.  A server is considered
        unhealthy if the hypervisor it is running on has a state that is down
        or a status that is disabled.

        :param obj: The node object to operate on.
        :param server: The server object associated with the node.
        :return status: True indicates node is healthy, False indicates
            it is unhealthy.
        """

        if server.hypervisor_hostname != "":
            try:
                hv = self.compute(obj).hypervisor_find(
                    server.hypervisor_hostname)
            except Exception as ex:
                if isinstance(ex, exc.InternalError) and ex.code == 404:
                    # treat resource not found exception as unhealthy
                    LOG.info('%s for %s: hypervisor %s was not found.',
                             consts.POLL_STATUS_FAIL, obj.name,
                             server.hypervisor_hostname)
                    return False
                else:
                    # treat all other exceptions as healthy
                    LOG.info(
                        '%s for %s: Exception when trying to get hypervisor '
                        'info for %s, but ignoring this error: %s.',
                        consts.POLL_STATUS_PASS, obj.name,
                        server.hypervisor_hostname, ex.message)
                    return True

            if hv is None:
                # no hypervisor information is available, treat the node as
                # healthy
                LOG.info(
                    '%s for %s: No hypervisor information was returned but '
                    'ignoring this error.',
                    consts.POLL_STATUS_PASS, obj.name)
                return True

            if hv.state == 'down':
                LOG.info('%s for %s: server status is unhealthy because '
                         'hypervisor %s state is down',
                         consts.POLL_STATUS_FAIL, obj.name,
                         server.hypervisor_hostname)
                return False

            if hv.status == 'disabled':
                LOG.info('%s for %s: server status is unhealthy because '
                         'hypervisor %s status is disabled',
                         consts.POLL_STATUS_FAIL, obj.name,
                         server.hypervisor_hostname)
                return False

        LOG.info('%s for %s', consts.POLL_STATUS_PASS, obj.name)
        return True

    def do_recover(self, obj, **options):
        """Handler for recover operation.

        :param obj: The node object.
        :param dict options: A list for operations each of which has a name
            and optionally a map from parameter to values.
        :return id: New id of the recovered resource or None if recovery
            failed.
        :return status: True indicates successful recovery, False indicates
            failure.
        """

        # default is recreate if not specified
        if 'operation' not in options or not options['operation']:
            options['operation'] = consts.RECOVER_RECREATE

        operation = options.get('operation')

        if operation.upper() not in consts.RECOVERY_ACTIONS:
            LOG.error("The operation '%s' is not supported",
                      operation)
            return obj.physical_id, False

        op_params = options.get('operation_params', {})
        if operation.upper() == consts.RECOVER_REBOOT:
            # default to hard reboot if operation_params was not specified
            if not isinstance(op_params, dict):
                op_params = {}
            if consts.REBOOT_TYPE not in op_params.keys():
                op_params[consts.REBOOT_TYPE] = consts.REBOOT_HARD

        if operation.upper() == consts.RECOVER_RECREATE:
            # recreate is implemented in base class
            return super(ServerProfile, self).do_recover(obj, **options)
        else:
            method = getattr(self, "handle_" + operation.lower())
            return method(obj, **op_params)

    def handle_reboot(self, obj, **options):
        """Handler for the reboot operation."""
        if not obj.physical_id:
            return None, False

        server_id = obj.physical_id
        reboot_type = options.get(consts.REBOOT_TYPE, consts.REBOOT_SOFT)
        if (not isinstance(reboot_type, str) or
                reboot_type.upper() not in consts.REBOOT_TYPES):
            return server_id, False

        nova_driver = self.compute(obj)
        try:
            server = nova_driver.server_get(server_id)
            if server is None:
                return None, False
            nova_driver.server_reboot(server_id, reboot_type)
            nova_driver.wait_for_server(obj.physical_id,
                                        consts.VS_ACTIVE)
            return server_id, True
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='rebooting', type='server',
                                         id=server_id,
                                         message=str(ex))

    def handle_rebuild(self, obj, **options):
        """Handler for the rebuild operation.

        :param obj: The node object.
        :param dict options: A list for operations each of which has a name
            and optionally a map from parameter to values.
        :return id: New id of the recovered resource or None if recovery
            failed.
        :return status: True indicates successful recovery, False indicates
            failure.
        """
        if not obj.physical_id:
            return None, False

        server_id = obj.physical_id
        nova_driver = self.compute(obj)
        try:
            server = nova_driver.server_get(server_id)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='rebuilding', type='server',
                                         id=server_id,
                                         message=str(ex))

        if server is None:
            return None, False
        image_id = options.get(self.IMAGE, None)
        if not image_id:
            image_id = self._get_image_id(obj, server, 'rebuilding')

        admin_pass = self.properties.get(self.ADMIN_PASS)
        name = self.properties[self.NAME] or obj.name
        try:
            nova_driver.server_rebuild(server_id, image_id,
                                       name, admin_pass)
            nova_driver.wait_for_server(server_id, consts.VS_ACTIVE)
            return server_id, True
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='rebuilding', type='server',
                                         id=server_id,
                                         message=str(ex))

    def handle_change_password(self, obj, **options):
        """Handler for the change_password operation."""
        password = options.get(self.ADMIN_PASSWORD, None)
        if (password is None or not isinstance(password, str)):
            return False
        return self._handle_generic_op(obj, 'server_change_password',
                                       'change_password',
                                       new_password=password)

    def handle_suspend(self, obj):
        """Handler for the suspend operation."""
        return self._handle_generic_op(obj, 'server_suspend',
                                       'suspend', consts.VS_SUSPENDED)

    def handle_resume(self, obj):
        """Handler for the resume operation."""
        return self._handle_generic_op(obj, 'server_resume',
                                       'resume', consts.VS_ACTIVE)

    def handle_start(self, obj):
        """Handler for the start operation."""
        return self._handle_generic_op(obj, 'server_start',
                                       'start', consts.VS_ACTIVE)

    def handle_stop(self, obj):
        """Handler for the stop operation."""
        return self._handle_generic_op(obj, 'server_stop',
                                       'stop', consts.VS_SHUTOFF)

    def handle_lock(self, obj):
        """Handler for the lock operation."""
        return self._handle_generic_op(obj, 'server_lock',
                                       'lock')

    def handle_unlock(self, obj):
        """Handler for the unlock operation."""
        return self._handle_generic_op(obj, 'server_unlock',
                                       'unlock')

    def handle_pause(self, obj):
        """Handler for the pause operation."""
        return self._handle_generic_op(obj, 'server_pause',
                                       'pause', consts.VS_PAUSED)

    def handle_unpause(self, obj):
        """Handler for the unpause operation."""
        return self._handle_generic_op(obj, 'server_unpause',
                                       'unpause', consts.VS_ACTIVE)

    def handle_rescue(self, obj, **options):
        """Handler for the rescue operation."""
        password = options.get(self.ADMIN_PASSWORD, None)
        image = options.get(self.IMAGE, None)
        if not image:
            return False

        self._validate_image(obj, image)
        return self._handle_generic_op(obj, 'server_rescue',
                                       'rescue', consts.VS_RESCUE,
                                       admin_pass=password,
                                       image_ref=image)

    def handle_unrescue(self, obj):
        """Handler for the unrescue operation."""
        return self._handle_generic_op(obj, 'server_unrescue',
                                       'unrescue', consts.VS_ACTIVE)

    def handle_migrate(self, obj):
        """Handler for the migrate operation."""
        return self._handle_generic_op(obj, 'server_migrate',
                                       'migrate', consts.VS_ACTIVE)

    def handle_snapshot(self, obj):
        """Handler for the snapshot operation."""
        return self._handle_generic_op(obj, 'server_create_image',
                                       'snapshot', consts.VS_ACTIVE,
                                       name=obj.name)

    def handle_restore(self, obj, **options):
        """Handler for the restore operation."""
        image = options.get(self.IMAGE, None)
        if not image:
            return False
        return self.handle_rebuild(obj, **options)
