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

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

from heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine.clients import progress
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources.openstack.neutron import port as neutron_port
from heat.engine.resources.openstack.neutron import subnet
from heat.engine.resources.openstack.nova import server_network_mixin
from heat.engine.resources import scheduler_hints as sh
from heat.engine.resources import server_base
from heat.engine import support
from heat.engine import translation
from heat.rpc import api as rpc_api

cfg.CONF.import_opt('default_software_config_transport', 'heat.common.config')
cfg.CONF.import_opt('default_user_data_format', 'heat.common.config')

LOG = logging.getLogger(__name__)

NOVA_MICROVERSIONS = (MICROVERSION_TAGS, MICROVERSION_STR_NETWORK,
                      MICROVERSION_NIC_TAGS, MICROVERSION_PERSONALITY_REMOVED
                      ) = ('2.26', '2.37', '2.42', '2.57')


class Server(server_base.BaseServer, sh.SchedulerHintsMixin,
             server_network_mixin.ServerNetworkMixin):
    """A resource for managing Nova instances.

    A Server resource manages the running virtual machine instance within an
    OpenStack cloud.
    """

    PROPERTIES = (
        NAME, IMAGE, BLOCK_DEVICE_MAPPING, BLOCK_DEVICE_MAPPING_V2,
        FLAVOR, FLAVOR_UPDATE_POLICY, IMAGE_UPDATE_POLICY, KEY_NAME,
        ADMIN_USER, AVAILABILITY_ZONE, SECURITY_GROUPS, NETWORKS,
        SCHEDULER_HINTS, METADATA, USER_DATA_FORMAT, USER_DATA,
        RESERVATION_ID, CONFIG_DRIVE, DISK_CONFIG, PERSONALITY,
        ADMIN_PASS, SOFTWARE_CONFIG_TRANSPORT, USER_DATA_UPDATE_POLICY,
        TAGS, DEPLOYMENT_SWIFT_DATA
    ) = (
        'name', 'image', 'block_device_mapping', 'block_device_mapping_v2',
        'flavor', 'flavor_update_policy', 'image_update_policy', 'key_name',
        'admin_user', 'availability_zone', 'security_groups', 'networks',
        'scheduler_hints', 'metadata', 'user_data_format', 'user_data',
        'reservation_id', 'config_drive', 'diskConfig', 'personality',
        'admin_pass', 'software_config_transport', 'user_data_update_policy',
        'tags', 'deployment_swift_data'
    )

    _BLOCK_DEVICE_MAPPING_KEYS = (
        BLOCK_DEVICE_MAPPING_DEVICE_NAME, BLOCK_DEVICE_MAPPING_VOLUME_ID,
        BLOCK_DEVICE_MAPPING_SNAPSHOT_ID,
        BLOCK_DEVICE_MAPPING_VOLUME_SIZE,
        BLOCK_DEVICE_MAPPING_DELETE_ON_TERM,
    ) = (
        'device_name', 'volume_id',
        'snapshot_id',
        'volume_size',
        'delete_on_termination',
    )

    _BLOCK_DEVICE_MAPPING_V2_KEYS = (
        BLOCK_DEVICE_MAPPING_DEVICE_NAME,
        BLOCK_DEVICE_MAPPING_VOLUME_ID,
        BLOCK_DEVICE_MAPPING_IMAGE_ID,
        BLOCK_DEVICE_MAPPING_IMAGE,
        BLOCK_DEVICE_MAPPING_SNAPSHOT_ID,
        BLOCK_DEVICE_MAPPING_SWAP_SIZE,
        BLOCK_DEVICE_MAPPING_DEVICE_TYPE,
        BLOCK_DEVICE_MAPPING_DISK_BUS,
        BLOCK_DEVICE_MAPPING_BOOT_INDEX,
        BLOCK_DEVICE_MAPPING_VOLUME_SIZE,
        BLOCK_DEVICE_MAPPING_DELETE_ON_TERM,
        BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE,
        BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT,
    ) = (
        'device_name',
        'volume_id',
        'image_id',
        'image',
        'snapshot_id',
        'swap_size',
        'device_type',
        'disk_bus',
        'boot_index',
        'volume_size',
        'delete_on_termination',
        'ephemeral_size',
        'ephemeral_format'
    )

    _NETWORK_KEYS = (
        NETWORK_UUID, NETWORK_ID, NETWORK_FIXED_IP, NETWORK_PORT,
        NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP,
        ALLOCATE_NETWORK, NIC_TAG,
    ) = (
        'uuid', 'network', 'fixed_ip', 'port',
        'subnet', 'port_extra_properties', 'floating_ip',
        'allocate_network', 'tag',
    )

    _IFACE_MANAGED_KEYS = (NETWORK_PORT, NETWORK_ID,
                           NETWORK_FIXED_IP, NETWORK_SUBNET)

    _SOFTWARE_CONFIG_FORMATS = (
        HEAT_CFNTOOLS, RAW, SOFTWARE_CONFIG
    ) = (
        'HEAT_CFNTOOLS', 'RAW', 'SOFTWARE_CONFIG'
    )

    _SOFTWARE_CONFIG_TRANSPORTS = (
        POLL_SERVER_CFN, POLL_SERVER_HEAT, POLL_TEMP_URL, ZAQAR_MESSAGE
    ) = (
        'POLL_SERVER_CFN', 'POLL_SERVER_HEAT', 'POLL_TEMP_URL', 'ZAQAR_MESSAGE'
    )

    _ALLOCATE_TYPES = (
        NETWORK_NONE, NETWORK_AUTO,
    ) = (
        'none', 'auto',
    )

    _DEPLOYMENT_SWIFT_DATA_KEYS = (
        CONTAINER, OBJECT
    ) = (
        'container', 'object',
    )

    ATTRIBUTES = (
        NAME_ATTR, ADDRESSES, NETWORKS_ATTR, FIRST_ADDRESS,
        INSTANCE_NAME, ACCESSIPV4, ACCESSIPV6, CONSOLE_URLS, TAGS_ATTR,
        OS_COLLECT_CONFIG
    ) = (
        'name', 'addresses', 'networks', 'first_address',
        'instance_name', 'accessIPv4', 'accessIPv6', 'console_urls', 'tags',
        'os_collect_config'
    )

    # Image Statuses
    IMAGE_STATUSES = (IMAGE_ACTIVE, IMAGE_ERROR,
                      IMAGE_DELETED) = ('active', 'error', 'deleted')

    properties_schema = {
        NAME: properties.Schema(
            properties.Schema.STRING,
            _('Server name.'),
            update_allowed=True
        ),
        IMAGE: properties.Schema(
            properties.Schema.STRING,
            _('The ID or name of the image to boot with.'),
            constraints=[
                constraints.CustomConstraint('glance.image')
            ],
            update_allowed=True
        ),
        BLOCK_DEVICE_MAPPING: properties.Schema(
            properties.Schema.LIST,
            _('Block device mappings for this server.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    BLOCK_DEVICE_MAPPING_DEVICE_NAME: properties.Schema(
                        properties.Schema.STRING,
                        _('A device name where the volume will be '
                          'attached in the system at /dev/device_name. '
                          'This value is typically vda.'),
                        required=True
                    ),
                    BLOCK_DEVICE_MAPPING_VOLUME_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID of the volume to boot from. Only one '
                          'of volume_id or snapshot_id should be '
                          'provided.'),
                        constraints=[
                            constraints.CustomConstraint('cinder.volume')
                        ]
                    ),
                    BLOCK_DEVICE_MAPPING_SNAPSHOT_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID of the snapshot to create a volume '
                          'from.'),
                        constraints=[
                            constraints.CustomConstraint('cinder.snapshot')
                        ]
                    ),
                    BLOCK_DEVICE_MAPPING_VOLUME_SIZE: properties.Schema(
                        properties.Schema.INTEGER,
                        _('The size of the volume, in GB. It is safe to '
                          'leave this blank and have the Compute service '
                          'infer the size.')
                    ),
                    BLOCK_DEVICE_MAPPING_DELETE_ON_TERM: properties.Schema(
                        properties.Schema.BOOLEAN,
                        _('Indicate whether the volume should be deleted '
                          'when the server is terminated.')
                    ),
                },
            )
        ),
        BLOCK_DEVICE_MAPPING_V2: properties.Schema(
            properties.Schema.LIST,
            _('Block device mappings v2 for this server.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    BLOCK_DEVICE_MAPPING_DEVICE_NAME: properties.Schema(
                        properties.Schema.STRING,
                        _('A device name where the volume will be '
                          'attached in the system at /dev/device_name. '
                          'This value is typically vda.'),
                    ),
                    BLOCK_DEVICE_MAPPING_VOLUME_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The volume_id can be boot or non-boot device '
                          'to the server.'),
                        constraints=[
                            constraints.CustomConstraint('cinder.volume')
                        ]
                    ),
                    BLOCK_DEVICE_MAPPING_IMAGE_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID of the image to create a volume from.'),
                        support_status=support.SupportStatus(
                            status=support.HIDDEN,
                            version='9.0.0',
                            message=_('Use property %s.') %
                                    BLOCK_DEVICE_MAPPING_IMAGE,
                            previous_status=support.SupportStatus(
                                status=support.DEPRECATED,
                                version='7.0.0',
                                previous_status=support.SupportStatus(
                                    version='5.0.0')
                            )
                        ),
                        constraints=[
                            constraints.CustomConstraint('glance.image')
                        ],
                    ),
                    BLOCK_DEVICE_MAPPING_IMAGE: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID or name of the image '
                          'to create a volume from.'),
                        support_status=support.SupportStatus(version='7.0.0'),
                        constraints=[
                            constraints.CustomConstraint('glance.image')
                        ],
                    ),
                    BLOCK_DEVICE_MAPPING_SNAPSHOT_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID of the snapshot to create a volume '
                          'from.'),
                        constraints=[
                            constraints.CustomConstraint('cinder.snapshot')
                        ]
                    ),
                    BLOCK_DEVICE_MAPPING_SWAP_SIZE: properties.Schema(
                        properties.Schema.INTEGER,
                        _('The size of the swap, in MB.')
                    ),
                    BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE: properties.Schema(
                        properties.Schema.INTEGER,
                        _('The size of the local ephemeral block device, '
                          'in GB.'),
                        support_status=support.SupportStatus(version='8.0.0'),
                        constraints=[constraints.Range(min=1)]
                    ),
                    BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT: properties.Schema(
                        properties.Schema.STRING,
                        _('The format of the local ephemeral block device. '
                          'If no format is specified, uses default value, '
                          'defined in nova configuration file.'),
                        constraints=[
                            constraints.AllowedValues(['ext2', 'ext3', 'ext4',
                                                       'xfs', 'ntfs'])
                        ],
                        support_status=support.SupportStatus(version='8.0.0')
                    ),
                    BLOCK_DEVICE_MAPPING_DEVICE_TYPE: properties.Schema(
                        properties.Schema.STRING,
                        _('Device type: at the moment we can make distinction '
                          'only between disk and cdrom.'),
                        constraints=[
                            constraints.AllowedValues(['cdrom', 'disk']),
                        ],
                    ),
                    BLOCK_DEVICE_MAPPING_DISK_BUS: properties.Schema(
                        properties.Schema.STRING,
                        _('Bus of the device: hypervisor driver chooses a '
                          'suitable default if omitted.'),
                        constraints=[
                            constraints.AllowedValues(['ide', 'lame_bus',
                                                       'scsi', 'usb',
                                                       'virtio']),
                        ],
                    ),
                    BLOCK_DEVICE_MAPPING_BOOT_INDEX: properties.Schema(
                        properties.Schema.INTEGER,
                        _('Integer used for ordering the boot disks. If '
                          'it is not specified, value "0" will be set '
                          'for bootable sources (volume, snapshot, image); '
                          'value "-1" will be set for non-bootable sources.'),
                    ),
                    BLOCK_DEVICE_MAPPING_VOLUME_SIZE: properties.Schema(
                        properties.Schema.INTEGER,
                        _('Size of the block device in GB. If it is omitted, '
                          'hypervisor driver calculates size.'),
                    ),
                    BLOCK_DEVICE_MAPPING_DELETE_ON_TERM: properties.Schema(
                        properties.Schema.BOOLEAN,
                        _('Indicate whether the volume should be deleted '
                          'when the server is terminated. '
                          'Defaults to "False" in case of a volume, snapshot '
                          'or image and to "True" in case of swap or '
                          'ephemeral.')
                    ),
                },
            ),
            support_status=support.SupportStatus(version='2015.1')
        ),
        FLAVOR: properties.Schema(
            properties.Schema.STRING,
            _('The ID or name of the flavor to boot onto.'),
            required=True,
            update_allowed=True,
            constraints=[
                constraints.CustomConstraint('nova.flavor')
            ]
        ),
        FLAVOR_UPDATE_POLICY: properties.Schema(
            properties.Schema.STRING,
            _('Policy on how to apply a flavor update; either by requesting '
              'a server resize or by replacing the entire server.'),
            default='RESIZE',
            constraints=[
                constraints.AllowedValues(['RESIZE', 'REPLACE']),
            ],
            update_allowed=True
        ),
        IMAGE_UPDATE_POLICY: properties.Schema(
            properties.Schema.STRING,
            _('Policy on how to apply an image-id update; either by '
              'requesting a server rebuild or by replacing '
              'the entire server.'),
            default='REBUILD',
            constraints=[
                constraints.AllowedValues(['REBUILD', 'REPLACE',
                                           'REBUILD_PRESERVE_EPHEMERAL']),
            ],
            update_allowed=True
        ),
        KEY_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Name of keypair to inject into the server.'),
            constraints=[
                constraints.CustomConstraint('nova.keypair')
            ]
        ),
        ADMIN_USER: properties.Schema(
            properties.Schema.STRING,
            _('Name of the administrative user to use on the server.'),
            support_status=support.SupportStatus(
                status=support.HIDDEN,
                version='5.0.0',
                message=_('The default cloud-init user set up for each image '
                          '(e.g. "ubuntu" for Ubuntu 12.04+, "fedora" for '
                          'Fedora 19+ and "cloud-user" for CentOS/RHEL 6.5).'),
                previous_status=support.SupportStatus(
                    status=support.DEPRECATED,
                    version='2014.1',
                    previous_status=support.SupportStatus(version='2013.2')
                )
            )
        ),
        AVAILABILITY_ZONE: properties.Schema(
            properties.Schema.STRING,
            _('Name of the availability zone for server placement.')
        ),
        SECURITY_GROUPS: properties.Schema(
            properties.Schema.LIST,
            _('List of security group names or IDs. Cannot be used if '
              'neutron ports are associated with this server; assign '
              'security groups to the ports instead.'),
            default=[]
        ),
        NETWORKS: properties.Schema(
            properties.Schema.LIST,
            _('An ordered list of nics to be added to this server, with '
              'information about connected networks, fixed ips, port etc.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    NETWORK_UUID: properties.Schema(
                        properties.Schema.STRING,
                        _('ID of network to create a port on.'),
                        support_status=support.SupportStatus(
                            status=support.HIDDEN,
                            version='5.0.0',
                            previous_status=support.SupportStatus(
                                status=support.DEPRECATED,
                                message=_('Use property %s.') % NETWORK_ID,
                                version='2014.1'
                            )
                        ),
                        constraints=[
                            constraints.CustomConstraint('neutron.network')
                        ]
                    ),
                    NETWORK_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('Name or ID of network to create a port on.'),
                        constraints=[
                            constraints.CustomConstraint('neutron.network')
                        ]
                    ),
                    ALLOCATE_NETWORK: properties.Schema(
                        properties.Schema.STRING,
                        _('The special string values of network, '
                          'auto: means either a network that is already '
                          'available to the project will be used, or if one '
                          'does not exist, will be automatically created for '
                          'the project; none: means no networking will be '
                          'allocated for the created server. Supported by '
                          'Nova API since version "2.37". This property can '
                          'not be used with other network keys.'),
                        support_status=support.SupportStatus(version='9.0.0'),
                        constraints=[
                            constraints.AllowedValues(
                                [NETWORK_NONE, NETWORK_AUTO])
                        ],
                        update_allowed=True,
                    ),
                    NETWORK_FIXED_IP: properties.Schema(
                        properties.Schema.STRING,
                        _('Fixed IP address to specify for the port '
                          'created on the requested network.'),
                        constraints=[
                            constraints.CustomConstraint('ip_addr')
                        ]
                    ),
                    NETWORK_PORT: properties.Schema(
                        properties.Schema.STRING,
                        _('ID of an existing port to associate with this '
                          'server.'),
                        constraints=[
                            constraints.CustomConstraint('neutron.port')
                        ]
                    ),
                    NETWORK_PORT_EXTRA: properties.Schema(
                        properties.Schema.MAP,
                        _('Dict, which has expand properties for port. '
                          'Used only if port property is not specified '
                          'for creating port.'),
                        schema=neutron_port.Port.extra_properties_schema,
                        support_status=support.SupportStatus(version='6.0.0')
                    ),
                    NETWORK_SUBNET: properties.Schema(
                        properties.Schema.STRING,
                        _('Subnet in which to allocate the IP address for '
                          'port. Used for creating port, based on derived '
                          'properties. If subnet is specified, network '
                          'property becomes optional.'),
                        support_status=support.SupportStatus(version='5.0.0')
                    ),
                    NETWORK_FLOATING_IP: properties.Schema(
                        properties.Schema.STRING,
                        _('ID of the floating IP to associate.'),
                        support_status=support.SupportStatus(version='6.0.0')
                    ),
                    NIC_TAG: properties.Schema(
                        properties.Schema.STRING,
                        _('Port tag. Heat ignores any update on this property '
                          'as nova does not support it.'),
                        support_status=support.SupportStatus(version='9.0.0')
                    )
                },
            ),
            update_allowed=True
        ),
        SCHEDULER_HINTS: properties.Schema(
            properties.Schema.MAP,
            _('Arbitrary key-value pairs specified by the client to help '
              'boot a server.')
        ),
        METADATA: properties.Schema(
            properties.Schema.MAP,
            _('Arbitrary key/value metadata to store for this server. Both '
              'keys and values must be 255 characters or less. Non-string '
              'values will be serialized to JSON (and the serialized '
              'string must be 255 characters or less).'),
            update_allowed=True,
            default={}
        ),
        USER_DATA_FORMAT: properties.Schema(
            properties.Schema.STRING,
            _('How the user_data should be formatted for the server. For '
              'HEAT_CFNTOOLS, the user_data is bundled as part of the '
              'heat-cfntools cloud-init boot configuration data. For RAW '
              'the user_data is passed to Nova unmodified. '
              'For SOFTWARE_CONFIG user_data is bundled as part of the '
              'software config data, and metadata is derived from any '
              'associated SoftwareDeployment resources. And if the '
              'user_data is in CoreOS ignition(json) format, the metadata '
              'will be injected into the user_data automatically by Heat.'),
            default=cfg.CONF.default_user_data_format,
            constraints=[
                constraints.AllowedValues(_SOFTWARE_CONFIG_FORMATS),
            ]
        ),
        SOFTWARE_CONFIG_TRANSPORT: properties.Schema(
            properties.Schema.STRING,
            _('How the server should receive the metadata required for '
              'software configuration. POLL_SERVER_CFN will allow calls to '
              'the cfn API action DescribeStackResource authenticated with '
              'the provided keypair. POLL_SERVER_HEAT will allow calls to '
              'the Heat API resource-show using the provided keystone '
              'credentials. POLL_TEMP_URL will create and populate a '
              'Swift TempURL with metadata for polling. ZAQAR_MESSAGE will '
              'create a dedicated zaqar queue and post the metadata '
              'for polling.'),
            default=cfg.CONF.default_software_config_transport,
            update_allowed=True,
            constraints=[
                constraints.AllowedValues(_SOFTWARE_CONFIG_TRANSPORTS),
            ]
        ),
        USER_DATA_UPDATE_POLICY: properties.Schema(
            properties.Schema.STRING,
            _('Policy on how to apply a user_data update; by '
              'ignoring it, by replacing the entire server, '
              'or rebuild the server.'),
            default='REPLACE',
            constraints=[
                constraints.AllowedValues(['REPLACE', 'IGNORE', 'REBUILD']),
            ],
            support_status=support.SupportStatus(version='6.0.0'),
            update_allowed=True
        ),
        USER_DATA: properties.Schema(
            properties.Schema.STRING,
            _('User data script to be executed by cloud-init or CoreOS '
              'ignition. Changes cause replacement of the resource '
              'by default, but can be ignored altogether by setting the '
              '`user_data_update_policy` property.'),
            default='',
            update_allowed=True
        ),
        RESERVATION_ID: properties.Schema(
            properties.Schema.STRING,
            _('A UUID for the set of servers being requested.')
        ),
        CONFIG_DRIVE: properties.Schema(
            properties.Schema.BOOLEAN,
            _('If True, enable config drive on the server.')
        ),
        DISK_CONFIG: properties.Schema(
            properties.Schema.STRING,
            _('Control how the disk is partitioned when the server is '
              'created.'),
            constraints=[
                constraints.AllowedValues(['AUTO', 'MANUAL']),
            ]
        ),
        PERSONALITY: properties.Schema(
            properties.Schema.MAP,
            _('A map of files to create/overwrite on the server upon boot. '
              'Keys are file names and values are the file contents.'),
            support_status=support.SupportStatus(
                status=support.DEPRECATED,
                version='12.0.0',
                message=_('This is not supported with nova api '
                          'microversion 2.57 and above. '
                          'OS::Nova::Server resource will not support '
                          'it in the future. Please use user_data or metadata '
                          'instead. However, you can set heat config option '
                          'max_nova_api_microversion < 2.57 to use '
                          'this property in the meantime.')
            ),
            default={}
        ),
        ADMIN_PASS: properties.Schema(
            properties.Schema.STRING,
            _('The administrator password for the server.'),
            update_allowed=True
        ),
        TAGS: properties.Schema(
            properties.Schema.LIST,
            _('Server tags. Supported since client version 2.26.'),
            support_status=support.SupportStatus(version='8.0.0'),
            schema=properties.Schema(properties.Schema.STRING),
            update_allowed=True
        ),
        DEPLOYMENT_SWIFT_DATA: properties.Schema(
            properties.Schema.MAP,
            _('Swift container and object to use for storing deployment data '
              'for the server resource. The parameter is a map value '
              'with the keys "container" and "object", and the values '
              'are the corresponding container and object names. The '
              'software_config_transport parameter must be set to '
              'POLL_TEMP_URL for swift to be used. If not specified, '
              'and software_config_transport is set to POLL_TEMP_URL, a '
              'container will be automatically created from the resource '
              'name, and the object name will be a generated uuid.'),
            support_status=support.SupportStatus(version='9.0.0'),
            default={},
            update_allowed=True,
            schema={
                CONTAINER: properties.Schema(
                    properties.Schema.STRING,
                    _('Name of the container.'),
                    constraints=[
                        constraints.Length(min=1)
                    ]
                ),
                OBJECT: properties.Schema(
                    properties.Schema.STRING,
                    _('Name of the object.'),
                    constraints=[
                        constraints.Length(min=1)
                    ]
                )
            }
        )
    }

    attributes_schema = {
        NAME_ATTR: attributes.Schema(
            _('Name of the server.'),
            type=attributes.Schema.STRING
        ),
        ADDRESSES: attributes.Schema(
            _('A dict of all network addresses with corresponding port_id and '
              'subnets. Each network will have two keys in dict, they are '
              'network name and network id. The port ID may be obtained '
              'through the following expression: ``{get_attr: [<server>, '
              'addresses, <network name_or_id>, 0, port]}``. The subnets may '
              'be obtained trough the following expression: ``{get_attr: '
              '[<server>, addresses, <network name_or_id>, 0, subnets]}``. '
              'The network may be obtained through the following expression: '
              '``{get_attr: [<server>, addresses, <network name_or_id>, 0, '
              'network]}``.'),
            type=attributes.Schema.MAP,
            support_status=support.SupportStatus(
                version='11.0.0',
                status=support.SUPPORTED,
                message=_('The attribute was extended to include subnets and '
                          'network with version 11.0.0.'),
                previous_status=support.SupportStatus(
                    status=support.SUPPORTED
                )
            )
        ),
        NETWORKS_ATTR: attributes.Schema(
            _('A dict of assigned network addresses of the form: '
              '{"public": [ip1, ip2...], "private": [ip3, ip4], '
              '"public_uuid": [ip1, ip2...], "private_uuid": [ip3, ip4]}. '
              'Each network will have two keys in dict, they are network '
              'name and network id.'),
            type=attributes.Schema.MAP
        ),
        FIRST_ADDRESS: attributes.Schema(
            _('Convenience attribute to fetch the first assigned network '
              'address, or an empty string if nothing has been assigned at '
              'this time. Result may not be predictable if the server has '
              'addresses from more than one network.'),
            support_status=support.SupportStatus(
                status=support.HIDDEN,
                version='5.0.0',
                message=_('Use the networks attribute instead of '
                          'first_address. For example: "{get_attr: '
                          '[<server name>, networks, <network name>, 0]}"'),
                previous_status=support.SupportStatus(
                    status=support.DEPRECATED,
                    version='2014.2',
                    previous_status=support.SupportStatus(version='2013.2')
                )
            )
        ),
        INSTANCE_NAME: attributes.Schema(
            _('AWS compatible instance name.'),
            type=attributes.Schema.STRING
        ),
        ACCESSIPV4: attributes.Schema(
            _('The manually assigned alternative public IPv4 address '
              'of the server.'),
            type=attributes.Schema.STRING,
            support_status=support.SupportStatus(
                status=support.DEPRECATED,
                version='14.0.0',
                previous_status=support.SupportStatus(version='2015.1')),
        ),
        ACCESSIPV6: attributes.Schema(
            _('The manually assigned alternative public IPv6 address '
              'of the server.'),
            type=attributes.Schema.STRING,
            support_status=support.SupportStatus(
                status=support.DEPRECATED,
                version='14.0.0',
                previous_status=support.SupportStatus(version='2015.1'))
        ),
        CONSOLE_URLS: attributes.Schema(
            _("URLs of server's consoles. "
              "To get a specific console type, the requested type "
              "can be specified as parameter to the get_attr function, "
              "e.g. get_attr: [ <server>, console_urls, novnc ]. "
              "Currently supported types are "
              "novnc, xvpvnc, spice-html5, rdp-html5, serial and webmks."),
            support_status=support.SupportStatus(version='2015.1'),
            type=attributes.Schema.MAP
        ),
        TAGS_ATTR: attributes.Schema(
            _('Tags from the server. Supported since client version 2.26.'),
            support_status=support.SupportStatus(version='8.0.0'),
            type=attributes.Schema.LIST
        ),
        OS_COLLECT_CONFIG: attributes.Schema(
            _('The os-collect-config configuration for the server\'s local '
              'agent to be configured to connect to Heat to retrieve '
              'deployment data.'),
            support_status=support.SupportStatus(version='9.0.0'),
            type=attributes.Schema.MAP,
            cache_mode=attributes.Schema.CACHE_NONE
        ),
    }

    default_client_name = 'nova'

    def translation_rules(self, props):
        neutron_client_plugin = self.client_plugin('neutron')
        glance_client_plugin = self.client_plugin('glance')
        rules = [
            translation.TranslationRule(
                props,
                translation.TranslationRule.REPLACE,
                translation_path=[self.NETWORKS, self.NETWORK_ID],
                value_name=self.NETWORK_UUID),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.FLAVOR],
                client_plugin=self.client_plugin('nova'),
                finder='find_flavor_by_name_or_id'),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.IMAGE],
                client_plugin=glance_client_plugin,
                finder='find_image_by_name_or_id'),
            translation.TranslationRule(
                props,
                translation.TranslationRule.REPLACE,
                translation_path=[self.BLOCK_DEVICE_MAPPING_V2,
                                  self.BLOCK_DEVICE_MAPPING_IMAGE],
                value_name=self.BLOCK_DEVICE_MAPPING_IMAGE_ID),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.BLOCK_DEVICE_MAPPING_V2,
                                  self.BLOCK_DEVICE_MAPPING_IMAGE],
                client_plugin=glance_client_plugin,
                finder='find_image_by_name_or_id'),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.NETWORKS, self.NETWORK_ID],
                client_plugin=neutron_client_plugin,
                finder='find_resourceid_by_name_or_id',
                entity=neutron_client_plugin.RES_TYPE_NETWORK),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.NETWORKS, self.NETWORK_SUBNET],
                client_plugin=neutron_client_plugin,
                finder='find_resourceid_by_name_or_id',
                entity=neutron_client_plugin.RES_TYPE_SUBNET),
            translation.TranslationRule(
                props,
                translation.TranslationRule.RESOLVE,
                translation_path=[self.NETWORKS, self.NETWORK_PORT],
                client_plugin=neutron_client_plugin,
                finder='find_resourceid_by_name_or_id',
                entity=neutron_client_plugin.RES_TYPE_PORT)
        ]
        return rules

    def __init__(self, name, json_snippet, stack):
        super(Server, self).__init__(name, json_snippet, stack)
        if self.user_data_software_config():
            self._register_access_key()
        self.default_collectors = ['ec2']

    def _config_drive(self):
        # This method is overridden by the derived CloudServer resource
        return self.properties[self.CONFIG_DRIVE]

    def user_data_raw(self):
        return self.properties[self.USER_DATA_FORMAT] == self.RAW

    def user_data_software_config(self):
        return self.properties[
            self.USER_DATA_FORMAT] == self.SOFTWARE_CONFIG

    def get_software_config(self, ud_content):
        with self.rpc_client().ignore_error_by_name('NotFound'):
            sc = self.rpc_client().show_software_config(
                self.context, ud_content)
            return sc[rpc_api.SOFTWARE_CONFIG_CONFIG]
        return ud_content

    def handle_create(self):
        security_groups = self.properties[self.SECURITY_GROUPS]

        user_data_format = self.properties[self.USER_DATA_FORMAT]
        ud_content = self.properties[self.USER_DATA]
        if self.user_data_software_config() or self.user_data_raw():
            if uuidutils.is_uuid_like(ud_content):
                # attempt to load the userdata from software config
                ud_content = self.get_software_config(ud_content)

        metadata = self.metadata_get(True) or {}

        if self.user_data_software_config():
            self._create_transport_credentials(self.properties)
            self._populate_deployments_metadata(metadata, self.properties)

        userdata = self.client_plugin().build_userdata(
            metadata,
            ud_content,
            instance_user=None,
            user_data_format=user_data_format)

        availability_zone = self.properties[self.AVAILABILITY_ZONE]
        instance_meta = self.properties[self.METADATA]
        if instance_meta:
            instance_meta = self.client_plugin().meta_serialize(
                instance_meta)

        scheduler_hints = self._scheduler_hints(
            self.properties[self.SCHEDULER_HINTS])

        nics = self._build_nics(self.properties[self.NETWORKS],
                                security_groups=security_groups)
        block_device_mapping = self._build_block_device_mapping(
            self.properties[self.BLOCK_DEVICE_MAPPING])
        block_device_mapping_v2 = self._build_block_device_mapping_v2(
            self.properties[self.BLOCK_DEVICE_MAPPING_V2])
        reservation_id = self.properties[self.RESERVATION_ID]
        disk_config = self.properties[self.DISK_CONFIG]
        admin_pass = self.properties[self.ADMIN_PASS] or None
        personality_files = self.properties[self.PERSONALITY]
        key_name = self.properties[self.KEY_NAME]
        flavor = self.properties[self.FLAVOR]
        image = self.properties[self.IMAGE]

        server = None
        try:
            server = self.client().servers.create(
                name=self._server_name(),
                image=image,
                flavor=flavor,
                key_name=key_name,
                security_groups=security_groups,
                userdata=userdata,
                meta=instance_meta,
                scheduler_hints=scheduler_hints,
                nics=nics,
                availability_zone=availability_zone,
                block_device_mapping=block_device_mapping,
                block_device_mapping_v2=block_device_mapping_v2,
                reservation_id=reservation_id,
                config_drive=self._config_drive(),
                disk_config=disk_config,
                files=personality_files,
                admin_pass=admin_pass)
        finally:
            # Avoid a race condition where the thread could be canceled
            # before the ID is stored
            if server is not None:
                self.resource_id_set(server.id)

        assert server is not None
        return server.id

    def check_create_complete(self, server_id):
        check = self.client_plugin()._check_active(server_id)
        if check:
            if self.properties[self.TAGS]:
                self._update_server_tags(self.properties[self.TAGS])
            self.store_external_ports()
        return check

    def _update_server_tags(self, tags):
        server = self.client().servers.get(self.resource_id)
        self.client().servers.set_tags(server, tags)

    def handle_check(self):
        server = self.client().servers.get(self.resource_id)
        status = self.client_plugin().get_status(server)
        checks = [{'attr': 'status', 'expected': 'ACTIVE', 'current': status}]
        self._verify_check_conditions(checks)

    def get_live_resource_data(self):
        try:
            server = self.client().servers.get(self.resource_id)
            server_data = server.to_dict()
            active = self.client_plugin()._check_active(server)
            if not active:
                # There is no difference what error raised, because update
                # method of resource just silently log it as warning.
                raise exception.Error(_('Server %s is not '
                                        'in ACTIVE state') % self.name)
        except Exception as ex:
            if self.client_plugin().is_not_found(ex):
                raise exception.EntityNotFound(entity='Resource',
                                               name=self.name)
            raise

        if self.client_plugin().is_version_supported(MICROVERSION_TAGS):
            tag_server = self.client().servers.get(self.resource_id)
            server_data['tags'] = tag_server.tag_list()
        return server, server_data

    def parse_live_resource_data(self, resource_properties, resource_data):
        server, server_data = resource_data
        result = {
            # there's a risk that flavor id will be int type, so cast to str
            self.FLAVOR: str(server_data.get(self.FLAVOR)['id']),
            self.IMAGE: str(server_data.get(self.IMAGE)['id']),
            self.NAME: server_data.get(self.NAME),
            self.METADATA: server_data.get(self.METADATA),
            self.NETWORKS: self._get_live_networks(server, resource_properties)
        }
        if 'tags' in server_data:
            result.update({self.TAGS: server_data['tags']})
        return result

    def _get_live_networks(self, server, props):
        reality_nets = self._get_server_addresses(server,
                                                  extend_networks=False)
        reality_net_ids = {}
        client_plugin = self.client_plugin('neutron')
        for net_key in reality_nets:
            try:
                net_id = client_plugin.find_resourceid_by_name_or_id(
                    client_plugin.RES_TYPE_NETWORK,
                    net_key)
            except Exception as ex:
                if (client_plugin.is_not_found(ex) or
                        client_plugin.is_no_unique(ex)):
                    net_id = None
                else:
                    raise
            if net_id:
                reality_net_ids[net_id] = reality_nets.get(net_key)

        resource_nets = props.get(self.NETWORKS)

        result_nets = []
        for net in resource_nets or []:
            net_id = self._get_network_id(net)
            if reality_net_ids.get(net_id):
                for idx, address in enumerate(reality_net_ids.get(net_id)):
                    if address['addr'] == net[self.NETWORK_FIXED_IP]:
                        result_nets.append(net)
                        reality_net_ids.get(net_id).pop(idx)
                        break

        for key, value in reality_nets.items():
            for address in reality_nets[key]:
                new_net = {self.NETWORK_ID: key,
                           self.NETWORK_FIXED_IP: address['addr']}
                if address['port'] not in [port['id']
                                           for port in self._data_get_ports()]:
                    new_net.update({self.NETWORK_PORT: address['port']})
                result_nets.append(new_net)
        return result_nets

    @classmethod
    def _build_block_device_mapping(cls, bdm):
        if not bdm:
            return None
        bdm_dict = {}
        for mapping in bdm:
            mapping_parts = []
            snapshot_id = mapping.get(cls.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
            if snapshot_id:
                mapping_parts.append(snapshot_id)
                mapping_parts.append('snap')
            else:
                volume_id = mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_ID)
                mapping_parts.append(volume_id)
                mapping_parts.append('')

            volume_size = mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_SIZE)
            delete = mapping.get(cls.BLOCK_DEVICE_MAPPING_DELETE_ON_TERM)
            if volume_size:
                mapping_parts.append(str(volume_size))
            else:
                mapping_parts.append('')
            if delete:
                mapping_parts.append(str(delete))

            device_name = mapping.get(cls.BLOCK_DEVICE_MAPPING_DEVICE_NAME)
            bdm_dict[device_name] = ':'.join(mapping_parts)

        return bdm_dict

    @classmethod
    def _build_block_device_mapping_v2(cls, bdm_v2):
        if not bdm_v2:
            return None

        bdm_v2_list = []
        for mapping in bdm_v2:
            bmd_dict = None
            if mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_ID):
                bmd_dict = {
                    'uuid': mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_ID),
                    'source_type': 'volume',
                    'destination_type': 'volume',
                    'boot_index': 0,
                    'delete_on_termination': False,
                }
            elif mapping.get(cls.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID):
                bmd_dict = {
                    'uuid': mapping.get(cls.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID),
                    'source_type': 'snapshot',
                    'destination_type': 'volume',
                    'boot_index': 0,
                    'delete_on_termination': False,
                }
            elif mapping.get(cls.BLOCK_DEVICE_MAPPING_IMAGE):
                bmd_dict = {
                    'uuid': mapping.get(cls.BLOCK_DEVICE_MAPPING_IMAGE),
                    'source_type': 'image',
                    'destination_type': 'volume',
                    'boot_index': 0,
                    'delete_on_termination': False,
                }
            elif mapping.get(cls.BLOCK_DEVICE_MAPPING_SWAP_SIZE):
                bmd_dict = {
                    'source_type': 'blank',
                    'destination_type': 'local',
                    'boot_index': -1,
                    'delete_on_termination': True,
                    'guest_format': 'swap',
                    'volume_size': mapping.get(
                        cls.BLOCK_DEVICE_MAPPING_SWAP_SIZE),
                }
            elif (mapping.get(cls.BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE) or
                  mapping.get(cls.BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT)):
                bmd_dict = {
                    'source_type': 'blank',
                    'destination_type': 'local',
                    'boot_index': -1,
                    'delete_on_termination': True
                }
                ephemeral_size = mapping.get(
                    cls.BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE)
                if ephemeral_size:
                    bmd_dict.update({'volume_size': ephemeral_size})
                ephemeral_format = mapping.get(
                    cls.BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT)
                if ephemeral_format:
                    bmd_dict.update({'guest_format': ephemeral_format})

            # NOTE(prazumovsky): In case of server doesn't take empty value of
            # device name, need to escape from such situation.
            device_name = mapping.get(cls.BLOCK_DEVICE_MAPPING_DEVICE_NAME)
            if device_name:
                bmd_dict[cls.BLOCK_DEVICE_MAPPING_DEVICE_NAME] = device_name

            update_props = (cls.BLOCK_DEVICE_MAPPING_DEVICE_TYPE,
                            cls.BLOCK_DEVICE_MAPPING_DISK_BUS,
                            cls.BLOCK_DEVICE_MAPPING_BOOT_INDEX,
                            cls.BLOCK_DEVICE_MAPPING_VOLUME_SIZE,
                            cls.BLOCK_DEVICE_MAPPING_DELETE_ON_TERM)

            for update_prop in update_props:
                if mapping.get(update_prop) is not None:
                    bmd_dict[update_prop] = mapping.get(update_prop)

            if bmd_dict:
                bdm_v2_list.append(bmd_dict)

        return bdm_v2_list

    def _get_subnets_attr(self, fixed_ips):
        subnets = []
        try:
            for fixed_ip in fixed_ips:
                if fixed_ip.get('subnet_id'):
                    subnets.append(self.client('neutron').show_subnet(
                        fixed_ip['subnet_id'])['subnet'])
        except Exception as ex:
            LOG.warning("Failed to fetch resource attributes: %s", ex)
            return
        return subnets

    def _get_network_attr(self, network_id):
        try:
            return self.client('neutron').show_network(network_id)['network']
        except Exception as ex:
            LOG.warning("Failed to fetch resource attributes: %s", ex)
            return

    def _get_server_addresses(self, server, extend_networks=True):
        """Adds port id, subnets and network attributes to addresses list.

        This method is used only for resolving attributes.
        :param server: The server resource
        :param extend_networks: When False the network is not extended, i.e
                                the net is returned without replacing name on
                                id.
        """
        nets = {}
        ifaces = self.client('neutron').list_ports(device_id=server.id)
        for port in ifaces['ports']:
            net_label = self.client('neutron').list_networks(
                id=port['network_id'])['networks'][0]['name']
            net = nets.setdefault(net_label, [])
            for fixed_ip in port['fixed_ips']:
                addr = {'addr': fixed_ip.get('ip_address'),
                        'OS-EXT-IPS-MAC:mac_addr': port['mac_address'],
                        'OS-EXT-IPS:type': 'fixed',
                        'port': port['id']}

                try:
                    addr['version'] = ipaddress.ip_address(
                        addr['addr']).version,
                except ValueError:
                    addr['version'] = None

                if addr['addr']:
                    fips = self.client('neutron').list_floatingips(
                        fixed_ip_address=addr['addr'])
                    for fip in fips['floatingips']:
                        net.append({
                            'addr': fip['floating_ip_address'],
                            'version': addr['version'],
                            'OS-EXT-IPS-MAC:mac_addr': port['mac_address'],
                            'OS-EXT-IPS:type': 'floating',
                            'port': None})

                # _get_live_networks() uses this method to get reality_nets.
                # We don't need to get subnets and network in that case. Only
                # do the external calls if extend_networks is true, i.e called
                # from _resolve_attribute()
                if not extend_networks:
                    net.append(addr)
                    continue

                addr['subnets'] = self._get_subnets_attr(port['fixed_ips'])
                addr['network'] = self._get_network_attr(port['network_id'])

                net.append(addr)

        if extend_networks:
            return self._extend_networks(nets)
        else:
            return nets

    def _extend_networks(self, networks):
        """Method adds same networks with replaced name on network id.

        This method is used only for resolving attributes.
        """
        nets = copy.deepcopy(networks)
        client_plugin = self.client_plugin('neutron')
        for key in list(nets.keys()):
            try:
                net_id = client_plugin.find_resourceid_by_name_or_id(
                    client_plugin.RES_TYPE_NETWORK,
                    key)
            except Exception as ex:
                if (client_plugin.is_not_found(ex) or
                        client_plugin.is_no_unique(ex)):
                    net_id = None
                else:
                    raise
            if net_id:
                nets[net_id] = nets[key]
        return nets

    def _resolve_attribute(self, name):
        if self.resource_id is None:
            return
        if name == self.FIRST_ADDRESS:
            return self.client_plugin().server_to_ipaddress(
                self.resource_id) or ''
        if name == self.OS_COLLECT_CONFIG:
            return self.metadata_get().get('os-collect-config', {})
        if name == self.NAME_ATTR:
            return self._server_name()
        try:
            server = self.client().servers.get(self.resource_id)
        except Exception as e:
            self.client_plugin().ignore_not_found(e)
            return ''
        if name == self.ADDRESSES:
            return self._get_server_addresses(server)
        if name == self.NETWORKS_ATTR:
            return self._extend_networks(server.networks)
        if name == self.INSTANCE_NAME:
            return getattr(server, 'OS-EXT-SRV-ATTR:instance_name', None)
        if name == self.ACCESSIPV4:
            return server.accessIPv4
        if name == self.ACCESSIPV6:
            return server.accessIPv6
        if name == self.CONSOLE_URLS:
            return self.client_plugin('nova').get_console_urls(server)
        if name == self.TAGS_ATTR:
            if self.client_plugin().is_version_supported(MICROVERSION_TAGS):
                return self.client().servers.tag_list(server)
            return None

    def add_dependencies(self, deps):
        super(Server, self).add_dependencies(deps)
        # Depend on any Subnet in this template with the same
        # network_id as the networks attached to this server.
        # It is not known which subnet a server might be assigned
        # to so all subnets in a network should be created before
        # the servers in that network.
        try:
            nets = self.properties[self.NETWORKS]
        except (ValueError, TypeError):
            # Properties errors will be caught later in validation,
            # where we can report them in their proper context.
            return
        if not nets:
            return
        for res in self.stack.values():
            if res.has_interface('OS::Neutron::Subnet'):
                try:
                    subnet_net = res.properties.get(subnet.Subnet.NETWORK)
                except (ValueError, TypeError):
                    # Properties errors will be caught later in validation,
                    # where we can report them in their proper context.
                    continue
                # Be wary of the case where we do not know a subnet's
                # network. If that's the case, be safe and add it as a
                # dependency.
                if not subnet_net:
                    deps += (self, res)
                    continue
                for net in nets:
                    # worry about network_id because that could be the match
                    # assigned to the subnet as well and could have been
                    # created by this stack. Regardless, the server should
                    # still wait on the subnet.
                    net_id = net.get(self.NETWORK_ID)
                    if net_id and net_id == subnet_net:
                        deps += (self, res)
                        break
                    # If we don't know a given net_id right now, it's
                    # plausible this subnet depends on it.
                    if not net_id:
                        deps += (self, res)
                        break

    def _update_flavor(self, after_props):
        flavor = after_props[self.FLAVOR]
        handler_args = checker_args = {'args': (flavor,)}
        prg_resize = progress.ServerUpdateProgress(self.resource_id,
                                                   'resize',
                                                   handler_extra=handler_args,
                                                   checker_extra=checker_args)
        prg_verify = progress.ServerUpdateProgress(self.resource_id,
                                                   'verify_resize')
        return prg_resize, prg_verify

    def _update_image(self, after_props):
        image_update_policy = after_props[self.IMAGE_UPDATE_POLICY]

        instance_meta = after_props[self.METADATA]
        if instance_meta is not None:
            instance_meta = self.client_plugin().meta_serialize(
                instance_meta)
        personality_files = after_props[self.PERSONALITY]

        image = after_props[self.IMAGE]
        preserve_ephemeral = (
            image_update_policy == 'REBUILD_PRESERVE_EPHEMERAL')
        password = after_props[self.ADMIN_PASS]
        kwargs = {'password': password,
                  'preserve_ephemeral': preserve_ephemeral,
                  'meta': instance_meta,
                  'files': personality_files}
        prg = progress.ServerUpdateProgress(self.resource_id,
                                            'rebuild',
                                            handler_extra={'args': (image,),
                                                           'kwargs': kwargs})
        return prg

    def _update_user_data_rebuild(self, after_props):
        user_data = after_props[self.USER_DATA]
        prg = progress.ServerUpdateProgress(
            self.resource_id,
            'rebuild',
            handler_extra={'args': (user_data,)})
        return prg

    def _update_networks(self, server, after_props):
        updaters = []
        new_networks = after_props[self.NETWORKS]
        old_networks = self.properties[self.NETWORKS]
        security_groups = after_props[self.SECURITY_GROUPS]

        if not server:
            server = self.client().servers.get(self.resource_id)
        interfaces = server.interface_list()
        remove_ports, add_nets = self.calculate_networks(
            old_networks, new_networks, interfaces, security_groups)

        for port in remove_ports:
            updaters.append(
                progress.ServerUpdateProgress(
                    self.resource_id, 'interface_detach',
                    handler_extra={'args': (port,)},
                    checker_extra={'args': (port,)})
            )

        for args in add_nets:
            updaters.append(
                progress.ServerUpdateProgress(
                    self.resource_id, 'interface_attach',
                    handler_extra={'kwargs': args},
                    checker_extra={'args': (args['port_id'],)})
            )

        return updaters

    def needs_replace_with_prop_diff(self, changed_properties_set,
                                     after_props, before_props):
        """Needs replace based on prop_diff."""
        if self.FLAVOR in changed_properties_set:
            flavor_update_policy = (
                after_props.get(self.FLAVOR_UPDATE_POLICY) or
                before_props.get(self.FLAVOR_UPDATE_POLICY))
            if flavor_update_policy == 'REPLACE':
                return True

        if self.IMAGE in changed_properties_set:
            image_update_policy = (
                after_props.get(self.IMAGE_UPDATE_POLICY) or
                before_props.get(self.IMAGE_UPDATE_POLICY))
            if image_update_policy == 'REPLACE':
                return True

        if self.USER_DATA in changed_properties_set:
            ud_update_policy = (
                after_props.get(self.USER_DATA_UPDATE_POLICY) or
                before_props.get(self.USER_DATA_UPDATE_POLICY))
            return ud_update_policy == 'REPLACE'

    def needs_replace_failed(self):
        if not self.resource_id:
            return True

        with self.client_plugin().ignore_not_found:
            server = self.client().servers.get(self.resource_id)
            return server.status in ('ERROR', 'DELETED', 'SOFT_DELETED')

        return True

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        updaters = super(Server, self).handle_update(
            json_snippet,
            tmpl_diff,
            prop_diff)
        server = None

        after_props = json_snippet.properties(self.properties_schema,
                                              self.context)
        if self.METADATA in prop_diff:
            server = self.client_plugin().get_server(self.resource_id)
            self.client_plugin().meta_update(server,
                                             after_props[self.METADATA])

        if self.TAGS in prop_diff:
            self._update_server_tags(after_props[self.TAGS] or [])

        if self.NAME in prop_diff:
            if not server:
                server = self.client_plugin().get_server(self.resource_id)
            self.client_plugin().rename(server, after_props[self.NAME])

        if self.NETWORKS in prop_diff:
            updaters.extend(self._update_networks(server, after_props))

        if self.FLAVOR in prop_diff:
            updaters.extend(self._update_flavor(after_props))

        if self.USER_DATA in prop_diff:
            # We only care about rebuild here. The standard replace is
            # dealt elsewere
            if after_props[self.USER_DATA_UPDATE_POLICY] == 'REBUILD':
                updaters.append(self._update_user_data_rebuild(after_props))

        if self.IMAGE in prop_diff:
            updaters.append(self._update_image(after_props))
        elif self.ADMIN_PASS in prop_diff:
            if not server:
                server = self.client_plugin().get_server(self.resource_id)
            server.change_password(after_props[self.ADMIN_PASS])

        # NOTE(pas-ha) optimization is possible (starting first task
        # right away), but we'd rather not, as this method already might
        # have called several APIs
        return updaters

    def check_update_complete(self, updaters):
        """Push all updaters to completion in list order."""
        for prg in updaters:
            if not prg.called:
                handler = getattr(self.client_plugin(), prg.handler)
                prg.called = handler(*prg.handler_args,
                                     **prg.handler_kwargs)
                return False
            if not prg.complete:
                check_complete = getattr(self.client_plugin(), prg.checker)
                prg.complete = check_complete(*prg.checker_args,
                                              **prg.checker_kwargs)
                break
        status = all(prg.complete for prg in updaters)
        if status:
            self.store_external_ports()
        return status

    def _validate_block_device_mapping(self):

        # either volume_id or snapshot_id needs to be specified, but not both
        # for block device mapping.
        bdm = self.properties[self.BLOCK_DEVICE_MAPPING] or []
        bdm_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2] or []
        image = self.properties[self.IMAGE]
        if bdm and bdm_v2:
            raise exception.ResourcePropertyConflict(
                self.BLOCK_DEVICE_MAPPING, self.BLOCK_DEVICE_MAPPING_V2)
        bootable = image is not None
        for mapping in bdm:
            device_name = mapping[self.BLOCK_DEVICE_MAPPING_DEVICE_NAME]
            if device_name == 'vda':
                bootable = True

            volume_id = mapping.get(self.BLOCK_DEVICE_MAPPING_VOLUME_ID)
            snapshot_id = mapping.get(self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
            if volume_id is not None and snapshot_id is not None:
                raise exception.ResourcePropertyConflict(
                    self.BLOCK_DEVICE_MAPPING_VOLUME_ID,
                    self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
            if volume_id is None and snapshot_id is None:
                msg = _('Either volume_id or snapshot_id must be specified for'
                        ' device mapping %s') % device_name
                raise exception.StackValidationFailed(message=msg)

        bootable_devs = [image]
        for mapping in bdm_v2:
            volume_id = mapping.get(self.BLOCK_DEVICE_MAPPING_VOLUME_ID)
            snapshot_id = mapping.get(self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
            image_id = mapping.get(self.BLOCK_DEVICE_MAPPING_IMAGE)
            boot_index = mapping.get(self.BLOCK_DEVICE_MAPPING_BOOT_INDEX)
            swap_size = mapping.get(self.BLOCK_DEVICE_MAPPING_SWAP_SIZE)
            ephemeral = (mapping.get(
                self.BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE) or mapping.get(
                self.BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT))

            property_tuple = (volume_id, snapshot_id, image_id, swap_size,
                              ephemeral)

            if property_tuple.count(None) < 4:
                raise exception.ResourcePropertyConflict(
                    self.BLOCK_DEVICE_MAPPING_VOLUME_ID,
                    self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID,
                    self.BLOCK_DEVICE_MAPPING_IMAGE,
                    self.BLOCK_DEVICE_MAPPING_SWAP_SIZE,
                    self.BLOCK_DEVICE_MAPPING_EPHEMERAL_SIZE,
                    self.BLOCK_DEVICE_MAPPING_EPHEMERAL_FORMAT
                )

            if property_tuple.count(None) == 5:
                msg = _('Either volume_id, snapshot_id, image_id, swap_size, '
                        'ephemeral_size or ephemeral_format must be '
                        'specified.')
                raise exception.StackValidationFailed(message=msg)

            if any((volume_id is not None, snapshot_id is not None,
                    image_id is not None)):
                # boot_index is not specified, set boot_index=0 when
                # build_block_device_mapping for volume, snapshot, image
                if boot_index is None or boot_index == 0:
                    bootable = True
                    bootable_devs.append(volume_id)
                    bootable_devs.append(snapshot_id)
                    bootable_devs.append(image_id)
        if not bootable:
            msg = _('Neither image nor bootable volume is specified for '
                    'instance %s') % self.name
            raise exception.StackValidationFailed(message=msg)
        if bdm_v2 and len(list(
                dev for dev in bootable_devs if dev is not None)) != 1:
            msg = _('Multiple bootable sources for instance %s.') % self.name
            raise exception.StackValidationFailed(message=msg)

    def _validate_image_flavor(self, image, flavor):
        try:
            image_obj = self.client_plugin('glance').get_image(image)
            flavor_obj = self.client_plugin().get_flavor(flavor)
        except Exception as ex:
            # Flavor or image may not have been created in the backend
            # yet when they are part of the same stack/template.
            if (self.client_plugin().is_not_found(ex) or
                    self.client_plugin('glance').is_not_found(ex)):
                return
            raise
        else:
            if image_obj.status.lower() != self.IMAGE_ACTIVE:
                msg = _('Image status is required to be %(cstatus)s not '
                        '%(wstatus)s.') % {
                    'cstatus': self.IMAGE_ACTIVE,
                    'wstatus': image_obj.status}
                raise exception.StackValidationFailed(message=msg)

            # validate image/flavor combination
            if flavor_obj.ram < image_obj.min_ram:
                msg = _('Image %(image)s requires %(imram)s minimum ram. '
                        'Flavor %(flavor)s has only %(flram)s.') % {
                    'image': image, 'imram': image_obj.min_ram,
                    'flavor': flavor, 'flram': flavor_obj.ram}
                raise exception.StackValidationFailed(message=msg)

            # validate image/flavor disk compatibility
            if flavor_obj.disk < image_obj.min_disk:
                msg = _('Image %(image)s requires %(imsz)s GB minimum '
                        'disk space. Flavor %(flavor)s has only '
                        '%(flsz)s GB.') % {
                    'image': image, 'imsz': image_obj.min_disk,
                    'flavor': flavor, 'flsz': flavor_obj.disk}
                raise exception.StackValidationFailed(message=msg)

    def validate(self):
        """Validate any of the provided params."""
        super(Server, self).validate()

        if self.user_data_software_config():
            if 'deployments' in self.t.metadata():
                msg = _('deployments key not allowed in resource metadata '
                        'with user_data_format of SOFTWARE_CONFIG')
                raise exception.StackValidationFailed(message=msg)

        self._validate_block_device_mapping()

        # make sure the image exists if specified.
        image = self.properties[self.IMAGE]
        flavor = self.properties[self.FLAVOR]
        if image:
            self._validate_image_flavor(image, flavor)

        networks = self.properties[self.NETWORKS] or []
        for network in networks:
            self._validate_network(network)

        has_str_net = self._str_network(networks) is not None
        if has_str_net:
            if len(networks) != 1:
                msg = _('Property "%s" can not be specified if '
                        'multiple network interfaces set for '
                        'server.') % self.ALLOCATE_NETWORK
                raise exception.StackValidationFailed(message=msg)
            # Check if str_network is allowed to use
            if not self.client_plugin().is_version_supported(
                    MICROVERSION_STR_NETWORK):
                msg = (_('Cannot use "%s" property - compute service '
                         'does not support the required api '
                         'microversion.') % self.ALLOCATE_NETWORK)
                raise exception.StackValidationFailed(message=msg)

        # record if any networks include explicit ports
        has_port = any(n[self.NETWORK_PORT] is not None for n in networks)
        # if 'security_groups' present for the server and explicit 'port'
        # in one or more entries in 'networks', raise validation error
        if has_port and self.properties[self.SECURITY_GROUPS]:
            raise exception.ResourcePropertyConflict(
                self.SECURITY_GROUPS,
                "/".join([self.NETWORKS, self.NETWORK_PORT]))

        # Check if nic tag is allowed to use
        if self._is_nic_tagged(networks=networks):
            if not self.client_plugin().is_version_supported(
                    MICROVERSION_NIC_TAGS):
                msg = (_('Cannot use "%s" property in networks - '
                         'nova does not support required '
                         'api microversion.'), self.NIC_TAG)
                raise exception.StackValidationFailed(message=msg)

        # Check if tags is allowed to use
        if self.properties[self.TAGS]:
            if not self.client_plugin().is_version_supported(
                    MICROVERSION_TAGS):
                msg = (_('Cannot use "%s" property - nova does not support '
                         'required api microversion.') % self.TAGS)
                raise exception.StackValidationFailed(message=msg)

        # retrieve provider's absolute limits if it will be needed
        metadata = self.properties[self.METADATA]
        personality = self.properties[self.PERSONALITY]

        if personality:
            if self.client_plugin().is_version_supported(
                    MICROVERSION_PERSONALITY_REMOVED):
                msg = (_('Cannot use the personality parameter as nova no '
                         'longer supports it. Use user_data instead.'))
                raise exception.StackValidationFailed(message=msg)

        if metadata or personality:
            limits = self.client_plugin().absolute_limits()

        # verify that the number of metadata entries is not greater
        # than the maximum number allowed in the provider's absolute
        # limits
        if metadata:
            msg = _('Instance metadata must not contain greater than %s '
                    'entries.  This is the maximum number allowed by your '
                    'service provider') % limits['maxServerMeta']
            self._check_maximum(len(metadata),
                                limits['maxServerMeta'], msg)

        # verify the number of personality files and the size of each
        # personality file against the provider's absolute limits
        if personality:
            msg = _("The personality property may not contain "
                    "greater than %s entries.") % limits['maxPersonality']
            self._check_maximum(len(personality),
                                limits['maxPersonality'], msg)

            for path, contents in personality.items():
                msg = (_("The contents of personality file \"%(path)s\" "
                         "is larger than the maximum allowed personality "
                         "file size (%(max_size)s bytes).") %
                       {'path': path,
                        'max_size': limits['maxPersonalitySize']})
                self._check_maximum(len(bytes(contents.encode('utf-8'))
                                        ) if contents is not None else 0,
                                    limits['maxPersonalitySize'], msg)

    def _delete(self):
        if self.user_data_software_config():
            self._delete_queue()
            self._delete_user()
            self._delete_temp_url()

        # remove internal and external ports
        self._delete_internal_ports()
        self.data_delete('external_ports')

        if self.resource_id is None:
            return

        try:
            self.client().servers.delete(self.resource_id)
        except Exception as e:
            self.client_plugin().ignore_not_found(e)
            return
        return progress.ServerDeleteProgress(self.resource_id)

    def handle_snapshot_delete(self, state):

        if state[1] != self.FAILED and self.resource_id:
            image_id = self.client().servers.create_image(
                self.resource_id, self.physical_resource_name())
            return progress.ServerDeleteProgress(
                self.resource_id, image_id, False)
        return self._delete()

    def handle_delete(self):

        return self._delete()

    def check_delete_complete(self, prg):
        if not prg:
            return True
        if not prg.image_complete:
            image = self.client_plugin('glance').get_image(prg.image_id)
            if image.status.lower() in (self.IMAGE_ERROR,
                                        self.IMAGE_DELETED):
                raise exception.Error(image.status)
            elif image.status.lower() == self.IMAGE_ACTIVE:
                prg.image_complete = True
                if not self._delete():
                    return True
            return False

        return self.client_plugin().check_delete_server_complete(
            prg.server_id)

    def handle_suspend(self):
        """Suspend a server.

        Note we do not wait for the SUSPENDED state, this is polled for by
        check_suspend_complete in a similar way to the create logic so we can
        take advantage of coroutines.
        """
        if self.resource_id is None:
            raise exception.Error(_('Cannot suspend %s, resource_id not set') %
                                  self.name)

        try:
            server = self.client().servers.get(self.resource_id)
        except Exception as e:
            if self.client_plugin().is_not_found(e):
                raise exception.NotFound(_('Failed to find server %s') %
                                         self.resource_id)
            else:
                raise
        else:
            # if the server has been suspended successful,
            # no need to suspend again
            if self.client_plugin().get_status(server) != 'SUSPENDED':
                LOG.debug('suspending server %s', self.resource_id)
                server.suspend()
            return server.id

    def check_suspend_complete(self, server_id):
        cp = self.client_plugin()
        server = cp.fetch_server(server_id)
        if not server:
            return False
        status = cp.get_status(server)
        LOG.debug('%(name)s check_suspend_complete status = %(status)s',
                  {'name': self.name, 'status': status})
        if status in (cp.deferred_server_statuses | {'ACTIVE'}):
            return status == 'SUSPENDED'
        else:
            exc = exception.ResourceUnknownStatus(
                result=_('Suspend of server %s failed') % server.name,
                resource_status=status)
            raise exc

    def handle_resume(self):
        """Resume a server.

        Note we do not wait for the ACTIVE state, this is polled for by
        check_resume_complete in a similar way to the create logic so we can
        take advantage of coroutines.
        """
        if self.resource_id is None:
            raise exception.Error(_('Cannot resume %s, resource_id not set') %
                                  self.name)

        try:
            server = self.client().servers.get(self.resource_id)
        except Exception as e:
            if self.client_plugin().is_not_found(e):
                raise exception.NotFound(_('Failed to find server %s') %
                                         self.resource_id)
            else:
                raise
        else:
            # if the server has been resumed successful,
            # no need to resume again
            if self.client_plugin().get_status(server) != 'ACTIVE':
                LOG.debug('resuming server %s', self.resource_id)
                server.resume()
            return server.id

    def check_resume_complete(self, server_id):
        return self.client_plugin()._check_active(server_id)

    def handle_snapshot(self):
        image_id = self.client().servers.create_image(
            self.resource_id, self.physical_resource_name())
        self.data_set('snapshot_image_id', image_id)
        return image_id

    def check_snapshot_complete(self, image_id):
        image = self.client_plugin('glance').get_image(image_id)
        if image.status.lower() == self.IMAGE_ACTIVE:
            server = self.client_plugin().get_server(self.resource_id)
            task_state = getattr(server, 'OS-EXT-STS:task_state', '')
            if task_state not in {'image_uploading', 'image_snapshot_pending',
                                  'image_snapshot', 'image_pending_upload'}:
                return True
        elif image.status.lower() in (self.IMAGE_ERROR, self.IMAGE_DELETED):
            raise exception.Error(image.status)

        return False

    def handle_delete_snapshot(self, snapshot):
        image_id = snapshot['resource_data'].get('snapshot_image_id')
        with self.client_plugin('glance').ignore_not_found:
            self.client('glance').images.delete(image_id)

    def handle_restore(self, defn, restore_data):
        image_id = restore_data['resource_data']['snapshot_image_id']
        props = dict((k, v) for k, v in self.properties.data.items()
                     if v is not None)
        for key in [self.BLOCK_DEVICE_MAPPING, self.BLOCK_DEVICE_MAPPING_V2,
                    self.NETWORKS]:
            if props.get(key) is not None:
                props[key] = list(dict((k, v) for k, v in prop.items()
                                       if v is not None)
                                  for prop in props[key])
        props[self.IMAGE] = image_id
        return defn.freeze(properties=props)

    def prepare_for_replace(self):
        # if the server has not been created yet, do nothing
        if self.resource_id is None:
            return

        self.prepare_ports_for_replace()

    def restore_prev_rsrc(self, convergence=False):
        self.restore_ports_after_rollback(convergence=convergence)


def resource_mapping():
    return {
        'OS::Nova::Server': Server,
    }
