# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.

"""
Driver for Microsoft Azure Resource Manager (ARM) Virtual Machines provider.

http://azure.microsoft.com/en-us/services/virtual-machines/
"""

import os
import time
import base64
import binascii

from libcloud.utils import iso8601
from libcloud.utils.py3 import basestring
from libcloud.common.types import LibcloudError
from libcloud.compute.base import (
    Node,
    NodeSize,
    NodeImage,
    NodeDriver,
    NodeLocation,
    StorageVolume,
    NodeAuthSSHKey,
    VolumeSnapshot,
    NodeAuthPassword,
)
from libcloud.compute.types import NodeState, StorageVolumeState, VolumeSnapshotState
from libcloud.storage.types import ObjectDoesNotExistError
from libcloud.common.azure_arm import AzureResourceManagementConnection
from libcloud.common.exceptions import BaseHTTPError
from libcloud.compute.providers import Provider
from libcloud.storage.drivers.azure_blobs import AzureBlobsStorageDriver

RESOURCE_API_VERSION = "2016-04-30-preview"
DISK_API_VERSION = "2018-06-01"
IMAGES_API_VERSION = "2015-06-15"
INSTANCE_VIEW_API_VERSION = "2015-06-15"
IP_API_VERSION = "2019-06-01"
LOCATIONS_API_VERSION = "2015-01-01"
NIC_API_VERSION = "2018-06-01"
NSG_API_VERSION = "2016-09-01"
RATECARD_API_VERSION = "2016-08-31-preview"
RESOURCE_GROUP_API_VERSION = "2016-09-01"
SNAPSHOT_API_VERSION = "2016-04-30-preview"
STORAGE_ACCOUNT_API_VERSION = "2015-05-01-preview"
SUBNET_API_VERSION = "2015-06-15"
TAG_API_VERSION = "2018-06-01"
VIRTUAL_NETWORK_API_VERSION = "2018-06-01"
VM_API_VERSION = "2021-11-01"
VM_EXTENSION_API_VERSION = "2015-06-15"
VM_SIZE_API_VERSION = "2015-06-15"  # this API is deprecated


class AzureImage(NodeImage):
    """Represents a Marketplace node image that an Azure VM can boot from."""

    def __init__(self, version, sku, offer, publisher, location, driver):
        self.publisher = publisher
        self.offer = offer
        self.sku = sku
        self.version = version
        self.location = location
        urn = "{}:{}:{}:{}".format(self.publisher, self.offer, self.sku, self.version)
        name = "{} {} {} {}".format(self.publisher, self.offer, self.sku, self.version)
        super().__init__(urn, name, driver)

    def __repr__(self):
        return ("<AzureImage: id=%s, name=%s, location=%s>") % (
            self.id,
            self.name,
            self.location,
        )


class AzureVhdImage(NodeImage):
    """Represents a VHD node image that an Azure VM can boot from."""

    def __init__(self, storage_account, blob_container, name, driver):
        urn = "https://{}.blob{}/{}/{}".format(
            storage_account,
            driver.connection.storage_suffix,
            blob_container,
            name,
        )
        super().__init__(urn, name, driver)

    def __repr__(self):
        return ("<AzureVhdImage: id=%s, name=%s>") % (self.id, self.name)


class AzureComputeGalleryImage(NodeImage):
    """Represents a Compute Gallery image that an Azure VM can boot from."""

    def __init__(self, subscription_id, resource_group, gallery, name, driver):
        id = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Compute/galleries/%s/images/%s"
            % (
                subscription_id,
                resource_group,
                gallery,
                name,
            )
        )
        super().__init__(id, name, driver)

    def __repr__(self):
        return ("<AzureComputeGalleryImage: id=%s, name=%s>") % (self.id, self.name)


class AzureResourceGroup:
    """Represent an Azure resource group."""

    def __init__(self, id, name, location, extra):
        self.id = id
        self.name = name
        self.location = location
        self.extra = extra

    def __repr__(self):
        return ("<AzureResourceGroup: id=%s, name=%s, location=%s ...>") % (
            self.id,
            self.name,
            self.location,
        )


class AzureNetworkSecurityGroup:
    """Represent an Azure network security group."""

    def __init__(self, id, name, location, extra):
        self.id = id
        self.name = name
        self.location = location
        self.extra = extra

    def __repr__(self):
        return ("<AzureNetworkSecurityGroup: id=%s, name=%s, location=%s ...>") % (
            self.id,
            self.name,
            self.location,
        )


class AzureNetwork:
    """Represent an Azure virtual network."""

    def __init__(self, id, name, location, extra):
        self.id = id
        self.name = name
        self.location = location
        self.extra = extra

    def __repr__(self):
        return ("<AzureNetwork: id=%s, name=%s, location=%s ...>") % (
            self.id,
            self.name,
            self.location,
        )


class AzureSubnet:
    """Represents a subnet of an Azure virtual network."""

    def __init__(self, id, name, extra):
        self.id = id
        self.name = name
        self.extra = extra

    def __repr__(self):
        return ("<AzureSubnet: id=%s, name=%s ...>") % (self.id, self.name)


class AzureNic:
    """Represents an Azure virtual network interface controller (NIC)."""

    def __init__(self, id, name, location, extra):
        self.id = id
        self.name = name
        self.location = location
        self.extra = extra

    def __repr__(self):
        return ("<AzureNic: id=%s, name=%s ...>") % (self.id, self.name)


class AzureIPAddress:
    """Represents an Azure public IP address resource."""

    def __init__(self, id, name, extra):
        self.id = id
        self.name = name
        self.extra = extra

    def __repr__(self):
        return ("<AzureIPAddress: id=%s, name=%s ...>") % (self.id, self.name)


class AzureNodeDriver(NodeDriver):
    """Compute node driver for Azure Resource Manager."""

    connectionCls = AzureResourceManagementConnection
    name = "Azure Virtual machines"
    website = "http://azure.microsoft.com/en-us/services/virtual-machines/"
    type = Provider.AZURE_ARM
    features = {"create_node": ["ssh_key", "password"]}

    # The API doesn't provide state or country information, so fill it in.
    # Information from https://azure.microsoft.com/en-us/regions/
    _location_to_country = {
        "centralus": "Iowa, USA",
        "eastus": "Virginia, USA",
        "eastus2": "Virginia, USA",
        "usgoviowa": "Iowa, USA",
        "usgovvirginia": "Virginia, USA",
        "northcentralus": "Illinois, USA",
        "southcentralus": "Texas, USA",
        "westus": "California, USA",
        "northeurope": "Ireland",
        "westeurope": "Netherlands",
        "eastasia": "Hong Kong",
        "southeastasia": "Singapore",
        "japaneast": "Tokyo, Japan",
        "japanwest": "Osaka, Japan",
        "brazilsouth": "Sao Paulo State, Brazil",
        "australiaeast": "New South Wales, Australia",
        "australiasoutheast": "Victoria, Australia",
    }

    SNAPSHOT_STATE_MAP = {
        "creating": VolumeSnapshotState.CREATING,
        "updating": VolumeSnapshotState.UPDATING,
        "succeeded": VolumeSnapshotState.AVAILABLE,
        "failed": VolumeSnapshotState.ERROR,
    }

    def __init__(
        self,
        tenant_id,
        subscription_id,
        key,
        secret,
        secure=True,
        host=None,
        port=None,
        api_version=None,
        region=None,
        **kwargs,
    ):
        self.tenant_id = tenant_id
        self.subscription_id = subscription_id
        self.cloud_environment = kwargs.get("cloud_environment")
        super().__init__(
            key=key,
            secret=secret,
            secure=secure,
            host=host,
            port=port,
            api_version=api_version,
            region=region,
            **kwargs,
        )
        if self.region is not None:
            loc_id = self.region.lower().replace(" ", "")
            country = self._location_to_country.get(loc_id)
            self.default_location = NodeLocation(loc_id, self.region, country, self)
        else:
            self.default_location = None

    def list_locations(self):
        """
        List data centers available with the current subscription.

        :return: list of node location objects
        :rtype: ``list`` of :class:`.NodeLocation`
        """

        action = "/subscriptions/%s/providers/Microsoft.Compute" % (self.subscription_id)
        r = self.connection.request(action, params={"api-version": LOCATIONS_API_VERSION})

        for rt in r.object["resourceTypes"]:
            if rt["resourceType"] == "virtualMachines":
                return [self._to_location(location) for location in rt["locations"]]

        return []

    def list_sizes(self, location=None):
        """
        List available VM sizes.

        :param location: The location at which to list sizes
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :return: list of node size objects
        :rtype: ``list`` of :class:`.NodeSize`
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")
        action = "/subscriptions/%s/providers/Microsoft" ".Compute/locations/%s/vmSizes" % (
            self.subscription_id,
            location.id,
        )
        r = self.connection.request(action, params={"api-version": VM_SIZE_API_VERSION})
        return [self._to_node_size(d) for d in r.object["value"]]

    def list_images(
        self,
        location=None,
        ex_publisher=None,
        ex_offer=None,
        ex_sku=None,
        ex_version=None,
    ):
        """
        List available VM images to boot from.

        :param location: The location at which to list images
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :param ex_publisher: Filter by publisher, or None to list
        all publishers.
        :type ex_publisher: ``str``

        :param ex_offer: Filter by offer, or None to list all offers.
        :type ex_offer: ``str``

        :param ex_sku: Filter by sku, or None to list all skus.
        :type ex_sku: ``str``

        :param ex_version: Filter by version, or None to list all versions.
        :type ex_version: ``str``

        :return: list of node image objects.
        :rtype: ``list`` of :class:`.AzureImage`
        """

        images = []

        if location is None:
            locations = [self.default_location]
        else:
            locations = [location]

        for loc in locations:
            if not ex_publisher:
                publishers = self.ex_list_publishers(loc)
            else:
                publishers = [
                    (
                        "/subscriptions/%s/providers/Microsoft"
                        ".Compute/locations/%s/publishers/%s"
                        % (self.subscription_id, loc.id, ex_publisher),
                        ex_publisher,
                    )
                ]

            for pub in publishers:
                if not ex_offer:
                    offers = self.ex_list_offers(pub[0])
                else:
                    offers = [
                        (
                            "{}/artifacttypes/vmimage/offers/{}".format(pub[0], ex_offer),
                            ex_offer,
                        )
                    ]

                for off in offers:
                    if not ex_sku:
                        skus = self.ex_list_skus(off[0])
                    else:
                        skus = [("{}/skus/{}".format(off[0], ex_sku), ex_sku)]

                    for sku in skus:
                        if not ex_version:
                            versions = self.ex_list_image_versions(sku[0])
                        else:
                            versions = [("{}/versions/{}".format(sku[0], ex_version), ex_version)]

                        for v in versions:
                            images.append(
                                AzureImage(
                                    v[1],
                                    sku[1],
                                    off[1],
                                    pub[1],
                                    loc.id,
                                    self.connection.driver,
                                )
                            )
        return images

    def get_image(self, image_id, location=None):
        """Returns a single node image from a provider.

        :param image_id: Either an image urn in the form
        `Publisher:Offer:Sku:Version` or a Azure blob store URI in the form
        `http://storageaccount.blob.core.windows.net/container/image.vhd`
        pointing to a VHD file.
        :type image_id: ``str``

        :param location: The location at which to search for the image
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :rtype :class:`.AzureImage`: or :class:`.AzureVhdImage`:
        :return: AzureImage or AzureVhdImage instance on success.

        """

        if image_id.startswith("http"):
            (storageAccount, blobContainer, blob) = _split_blob_uri(image_id)
            return AzureVhdImage(storageAccount, blobContainer, blob, self)
        else:
            (ex_publisher, ex_offer, ex_sku, ex_version) = image_id.split(":")
            i = self.list_images(location, ex_publisher, ex_offer, ex_sku, ex_version)
            return i[0] if i else None

    def list_nodes(self, ex_resource_group=None, ex_fetch_nic=True, ex_fetch_power_state=True):
        """
        List all nodes.

        :param ex_resource_group: The resource group to list all nodes from.
        :type ex_resource_group: ``str``

        :param ex_fetch_nic: Fetch NIC resources in order to get
        IP address information for nodes.  If True, requires an extra API
        call for each NIC of each node.  If False, IP addresses will not
        be returned.
        :type ex_fetch_nic: ``bool``

        :param ex_fetch_power_state: Fetch node power state.  If True, requires
        an extra API call for each node.  If False, node state
        will be returned based on provisioning state only.
        :type ex_fetch_power_state: ``bool``

        :return:  list of node objects
        :rtype: ``list`` of :class:`.Node`
        """

        if ex_resource_group:
            action = (
                "/subscriptions/%s/resourceGroups/%s/"
                "providers/Microsoft.Compute/virtualMachines"
                % (self.subscription_id, ex_resource_group)
            )
        else:
            action = "/subscriptions/%s/providers/Microsoft.Compute/" "virtualMachines" % (
                self.subscription_id
            )
        r = self.connection.request(action, params={"api-version": VM_API_VERSION})
        return [
            self._to_node(n, fetch_nic=ex_fetch_nic, fetch_power_state=ex_fetch_power_state)
            for n in r.object["value"]
        ]

    def create_node(
        self,
        name,
        size,
        image,
        auth,
        ex_resource_group,
        ex_storage_account=None,
        ex_blob_container="vhds",
        location=None,
        ex_user_name="azureuser",
        ex_network=None,
        ex_subnet=None,
        ex_nic=None,
        ex_tags={},
        ex_customdata="",
        ex_use_managed_disks=False,
        ex_disk_size=None,
        ex_storage_account_type="Standard_LRS",
        ex_os_disk_delete=False,
    ):
        """Create a new node instance. This instance will be started
        automatically.

        This driver supports the ``ssh_key`` feature flag for ``created_node``
        so you can upload a public key into the new instance::

            >>> from libcloud.compute.drivers.azure_arm import AzureNodeDriver
            >>> driver = AzureNodeDriver(...)
            >>> auth = NodeAuthSSHKey('pubkey data here')
            >>> node = driver.create_node("test_node", auth=auth)

        This driver also supports the ``password`` feature flag for
        ``create_node``
        so you can set a password::

            >>> driver = AzureNodeDriver(...)
            >>> auth = NodeAuthPassword('mysecretpassword')
            >>> node = driver.create_node("test_node", auth=auth, ...)

        If you don't provide the ``auth`` argument libcloud will assign
        a password:

            >>> driver = AzureNodeDriver(...)
            >>> node = driver.create_node("test_node", ...)
            >>> password = node.extra["properties"] \
                           ["osProfile"]["adminPassword"]

        :param name:   String with a name for this new node (required)
        :type name:   ``str``

        :param size:   The size of resources allocated to this node.
                            (required)
        :type size:   :class:`.NodeSize`

        :param image:  OS Image to boot on node (required)
        :type image:  :class:`.AzureImage` or :class:`.AzureVhdImage` or :class:`.AzureComputeGalleryImage`

        :param location: Which data center to create a node in.
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :param auth:   Initial authentication information for the node
                            (optional)
        :type auth:   :class:`.NodeAuthSSHKey` or :class:`NodeAuthPassword`

        :param ex_resource_group:  The resource group in which to create the
        node
        :type ex_resource_group: ``str``

        :param ex_storage_account:  The storage account id in which to store
        the node's disk image.
        Note: when booting from a user image (AzureVhdImage)
        the source image and the node image must use the same storage account.
        :type ex_storage_account: ``str``

        :param ex_blob_container:  The name of the blob container on the
        storage account in which to store the node's disk image (optional,
        default "vhds")
        :type ex_blob_container: ``str``

        :param ex_user_name:  User name for the initial admin user
        (optional, default "azureuser")
        :type ex_user_name: ``str``

        :param ex_network: The virtual network the node will be attached to.
        Must provide either `ex_network` (to create a default NIC for the
        node on the given network) or `ex_nic` (to supply the NIC explicitly).
        :type ex_network: ``str``

        :param ex_subnet: If ex_network is provided, the subnet of the
        virtual network the node will be attached to.  Optional, default
        is the "default" subnet.
        :type ex_subnet: ``str``

        :param ex_nic: A virtual NIC to attach to this Node, from
        `ex_create_network_interface` or `ex_get_nic`.
        Must provide either `ex_nic` (to supply the NIC explicitly) or
        ex_network (to create a default NIC for the node on the
        given network).
        :type ex_nic: :class:`AzureNic`

        :param ex_tags: Optional tags to associate with this node.
        :type ex_tags: ``dict``

        :param ex_customdata: Custom data that will
            be placed in the file /var/lib/waagent/CustomData
            https://azure.microsoft.com/en-us/documentation/ \
            articles/virtual-machines-how-to-inject-custom-data/
        :type ex_customdata: ``str``

        :param ex_use_managed_disks: Enable this feature to have Azure
            automatically manage the availability of disks to provide data
            redundancy and fault tolerance, without creating and managing
            storage accounts on your own. Managed disks may not be available
            in all regions (default False).
        :type ex_use_managed_disks: ``bool``

        :param ex_disk_size: Custom OS disk size in GB
        :type ex_disk_size: ``int``

        :param ex_storage_account_type: The Storage Account type,
            ``Standard_LRS``(HDD disks) or ``Premium_LRS``(SSD disks).
        :type ex_storage_account_type: str

        :param ex_os_disk_delete: Enable this feature to have Azure
            automatically delete the OS disk when you delete the VM.
        :type ex_os_disk_delete: ``bool``

        :return: The newly created node.
        :rtype: :class:`.Node`
        """
        if not ex_use_managed_disks and ex_storage_account is None:
            raise ValueError("ex_use_managed_disks is False, " "must provide ex_storage_account")

        if location is None:
            location = self.default_location
        if ex_nic is None:
            if ex_network is None:
                raise ValueError("Must provide either ex_network or ex_nic")
            if ex_subnet is None:
                ex_subnet = "default"

            subnet_id = (
                "/subscriptions/%s/resourceGroups/%s/providers"
                "/Microsoft.Network/virtualnetworks/%s/subnets/%s"
                % (self.subscription_id, ex_resource_group, ex_network, ex_subnet)
            )
            subnet = AzureSubnet(subnet_id, ex_subnet, {})
            ex_nic = self.ex_create_network_interface(
                name + "-nic", subnet, ex_resource_group, location
            )

        auth = self._get_and_check_auth(auth)

        target = (
            "/subscriptions/%s/resourceGroups/%s/providers"
            "/Microsoft.Compute/virtualMachines/%s"
            % (self.subscription_id, ex_resource_group, name)
        )

        if isinstance(image, AzureVhdImage):
            instance_vhd = self._get_instance_vhd(
                name=name,
                ex_resource_group=ex_resource_group,
                ex_storage_account=ex_storage_account,
                ex_blob_container=ex_blob_container,
            )
            storage_profile = {
                "osDisk": {
                    "name": name,
                    "osType": "linux",
                    "caching": "ReadWrite",
                    "createOption": "FromImage",
                    "image": {"uri": image.id},
                    "vhd": {"uri": instance_vhd},
                }
            }
            if ex_os_disk_delete:
                storage_profile["osDisk"]["deleteOption"] = "Delete"
            if ex_use_managed_disks:
                raise LibcloudError(
                    "Creating managed OS disk from %s image " "type is not supported." % type(image)
                )
        elif isinstance(image, AzureImage) or isinstance(image, AzureComputeGalleryImage):
            if isinstance(image, AzureImage):
                imageReference = {
                    "publisher": image.publisher,
                    "offer": image.offer,
                    "sku": image.sku,
                    "version": image.version,
                }
            else:
                imageReference = {"id": image.id}
            storage_profile = {
                "imageReference": imageReference,
                "osDisk": {
                    "name": name,
                    "osType": "linux",
                    "caching": "ReadWrite",
                    "createOption": "FromImage",
                },
            }
            if ex_os_disk_delete:
                storage_profile["osDisk"]["deleteOption"] = "Delete"
            if ex_use_managed_disks:
                storage_profile["osDisk"]["managedDisk"] = {
                    "storageAccountType": ex_storage_account_type
                }
            else:
                instance_vhd = self._get_instance_vhd(
                    name=name,
                    ex_resource_group=ex_resource_group,
                    ex_storage_account=ex_storage_account,
                    ex_blob_container=ex_blob_container,
                )
                storage_profile["osDisk"]["vhd"] = {"uri": instance_vhd}
        else:
            raise LibcloudError(
                "Unknown image type %s, expected one of AzureImage, "
                "AzureVhdImage, AzureComputeGalleryImage." % type(image)
            )

        data = {
            "id": target,
            "name": name,
            "type": "Microsoft.Compute/virtualMachines",
            "location": location.id,
            "tags": ex_tags,
            "properties": {
                "hardwareProfile": {"vmSize": size.id},
                "storageProfile": storage_profile,
                "osProfile": {"computerName": name},
                "networkProfile": {"networkInterfaces": [{"id": ex_nic.id}]},
            },
        }

        if ex_disk_size:
            data["properties"]["storageProfile"]["osDisk"].update({"diskSizeGB": ex_disk_size})

        if ex_customdata:
            data["properties"]["osProfile"]["customData"] = base64.b64encode(ex_customdata)

        data["properties"]["osProfile"]["adminUsername"] = ex_user_name

        if isinstance(auth, NodeAuthSSHKey):
            data["properties"]["osProfile"]["adminPassword"] = binascii.hexlify(
                os.urandom(20)
            ).decode("utf-8")
            data["properties"]["osProfile"]["linuxConfiguration"] = {
                "disablePasswordAuthentication": "true",
                "ssh": {
                    "publicKeys": [
                        {
                            "path": "/home/%s/.ssh/authorized_keys" % (ex_user_name),
                            "keyData": auth.pubkey,  # pylint: disable=no-member
                        }
                    ]
                },
            }
        elif isinstance(auth, NodeAuthPassword):
            data["properties"]["osProfile"]["linuxConfiguration"] = {
                "disablePasswordAuthentication": "false"
            }
            data["properties"]["osProfile"]["adminPassword"] = auth.password
        else:
            raise ValueError("Must provide NodeAuthSSHKey or NodeAuthPassword in auth")

        r = self.connection.request(
            target,
            params={"api-version": VM_API_VERSION},
            data=data,
            method="PUT",
        )

        node = self._to_node(r.object)
        node.size = size
        node.image = image
        return node

    def reboot_node(self, node):
        """
        Reboot a node.

        :param node: The node to be rebooted
        :type node: :class:`.Node`

        :return: True if the reboot was successful, otherwise False
        :rtype: ``bool``
        """

        target = "%s/restart" % node.id
        try:
            self.connection.request(target, params={"api-version": VM_API_VERSION}, method="POST")
            return True
        except BaseHTTPError as h:
            if h.code == 202:
                return True
            else:
                return False

    def destroy_node(
        self,
        node,
        ex_destroy_nic=True,
        ex_destroy_vhd=True,
        ex_poll_qty=10,
        ex_poll_wait=10,
    ):
        """
        Destroy a node.

        :param node: The node to be destroyed
        :type node: :class:`.Node`

        :param ex_destroy_nic: Destroy the NICs associated with
        this node (default True).
        :type node: ``bool``

        :param ex_destroy_vhd: Destroy the OS disk blob associated with
        this node (default True).
        :type node: ``bool``

        :param ex_poll_qty: Number of retries checking if the node
        is gone, destroying the NIC or destroying the VHD (default 10).
        :type node: ``int``

        :param ex_poll_wait: Delay in seconds between retries (default 10).
        :type node: ``int``

        :return: True if the destroy was successful, raises exception
        otherwise.
        :rtype: ``bool``
        """

        do_node_polling = ex_destroy_nic or ex_destroy_vhd

        # This returns a 202 (Accepted) which means that the delete happens
        # asynchronously.
        # If returns 404, we may be retrying a previous destroy_node call that
        # failed to clean up its related resources, so it isn't taken as a
        # failure.
        try:
            self.connection.request(
                node.id, params={"api-version": VM_API_VERSION}, method="DELETE"
            )
        except BaseHTTPError as h:
            if h.code == 202:
                pass
            elif h.code == 204:
                # Returns 204 if node already deleted.
                do_node_polling = False
            else:
                raise

        # Poll until the node actually goes away (otherwise attempt to delete
        # NIC and VHD will fail with "resource in use" errors).
        retries = ex_poll_qty
        while do_node_polling and retries > 0:
            try:
                time.sleep(ex_poll_wait)
                self.connection.request(node.id, params={"api-version": VM_API_VERSION})
                retries -= 1
            except BaseHTTPError as h:
                if h.code in (204, 404):
                    # Node is gone
                    break
                else:
                    raise

        # Optionally clean up the network
        # interfaces that were attached to this node.
        interfaces = node.extra["properties"]["networkProfile"]["networkInterfaces"]
        if ex_destroy_nic:
            for nic in interfaces:
                retries = ex_poll_qty
                while retries > 0:
                    try:
                        self.ex_destroy_nic(self._to_nic(nic))
                        break
                    except BaseHTTPError as h:
                        retries -= 1
                        if h.code == 400 and h.message.startswith("[NicInUse]") and retries > 0:
                            time.sleep(ex_poll_wait)
                        else:
                            raise

        # Optionally clean up OS disk VHD.
        vhd = node.extra["properties"]["storageProfile"]["osDisk"].get("vhd")
        if ex_destroy_vhd and vhd is not None:
            retries = ex_poll_qty
            resourceGroup = node.id.split("/")[4]
            while retries > 0:
                try:
                    if self._ex_delete_old_vhd(resourceGroup, vhd["uri"]):
                        break
                    # Unfortunately lease errors usually result in it returning
                    # "False" with no more information.  Need to wait and try
                    # again.
                except LibcloudError as e:
                    retries -= 1
                    if "LeaseIdMissing" in str(e) and retries > 0:
                        # Unfortunately lease errors
                        # (which occur if the vhd blob
                        # hasn't yet been released by the VM being destroyed)
                        # get raised as plain
                        # LibcloudError.  Wait a bit and try again.
                        time.sleep(ex_poll_wait)
                    else:
                        raise
                time.sleep(10)

        return True

    def create_volume(
        self,
        size,
        name,
        location=None,
        snapshot=None,
        ex_resource_group=None,
        ex_sku_name=None,
        ex_tags=None,
        ex_zones=None,
        ex_iops=None,
        ex_throughput=None,
    ):
        """
        Create a new managed volume.

        :param size: Size of volume in gigabytes.
        :type size: ``int``

        :param name: Name of the volume to be created.
        :type name: ``str``

        :param location: Which data center to create a volume in. (required)
        :type location: :class:`NodeLocation`

        :param snapshot: Snapshot from which to create the new volume.
        :type snapshot: :class:`VolumeSnapshot`

        :param ex_resource_group: The name of resource group in which to
            create the volume. (required)
        :type ex_resource_group: ``str``

        :param ex_sku_name: The Disk SKU name. Refer to the API reference for
            options.
        :type ex_sku_name: ``str``

        :param ex_tags: Optional tags to associate with this resource.
        :type ex_tags: ``dict``

        :param ex_zones: The list of availability zones to create the volume
            in. Options are any or all of ["1", "2", "3"]. (optional)
        :type ex_zones: ``list`` of ``str``

        :param ex_iops: The max IOPS this volume is capable of.
        :type ex_iops: ``int``

        :param ex_throughput: The max throughput of this volume in MBps.
        :type ex_throughput: ``int``

        :return: The newly created volume.
        :rtype: :class:`StorageVolume`
        """
        if location is None:
            raise ValueError("Must provide `location` value.")

        if ex_resource_group is None:
            raise ValueError("Must provide `ex_resource_group` value.")

        action = (
            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
            "/providers/Microsoft.Compute/disks/{volume_name}"
        ).format(
            subscription_id=self.subscription_id,
            resource_group=ex_resource_group,
            volume_name=name,
        )
        tags = ex_tags if ex_tags is not None else {}

        creation_data = (
            {"createOption": "Empty"}
            if snapshot is None
            else {"createOption": "Copy", "sourceUri": snapshot.id}
        )
        data = {
            "location": location.id,
            "tags": tags,
            "properties": {"creationData": creation_data, "diskSizeGB": size},
        }
        if ex_sku_name is not None:
            data["sku"] = {"name": ex_sku_name}

        if ex_zones is not None:
            data["zones"] = ex_zones

        if ex_iops is not None:
            data["properties"]["diskIopsReadWrite"] = ex_iops

        if ex_throughput is not None:
            data["properties"]["diskMBpsReadWrite"] = ex_throughput

        response = self.connection.request(
            action,
            method="PUT",
            params={"api-version": DISK_API_VERSION},
            data=data,
        )

        return self._to_volume(response.object, name=name, ex_resource_group=ex_resource_group)

    def list_volumes(self, ex_resource_group=None):
        """
        Lists all the disks under a resource group or subscription.

        :param ex_resource_group: The identifier of your subscription
            where the managed disks are located.
        :type ex_resource_group: ``str``

        :rtype: list of :class:`StorageVolume`
        """
        if ex_resource_group:
            action = (
                "/subscriptions/{subscription_id}/resourceGroups"
                "/{resource_group}/providers/Microsoft.Compute/disks"
            )
        else:
            action = "/subscriptions/{subscription_id}" "/providers/Microsoft.Compute/disks"

        action = action.format(
            subscription_id=self.subscription_id, resource_group=ex_resource_group
        )

        response = self.connection.request(
            action, method="GET", params={"api-version": DISK_API_VERSION}
        )
        return [self._to_volume(volume) for volume in response.object["value"]]

    def attach_volume(
        self,
        node,
        volume,
        ex_lun=None,
        ex_vhd_uri=None,
        ex_vhd_create=False,
        **ex_kwargs,
    ):
        """
        Attach a volume to node.

        :param node: A node to attach volume.
        :type node: :class:`Node`

        :param volume: A volume to attach.
        :type volume: :class:`StorageVolume`

        :param ex_lun: Specifies the logical unit number (LUN) location for
            the data drive in the virtual machine. Each data disk must have
            a unique LUN.
        :type ex_lun: ``int``

        :param ex_vhd_uri: Attach old-style unmanaged disk from VHD
            blob. (optional)
        :type ex_vhd_uri: ``str``

        :param ex_vhd_create: Create a new VHD blob for unmanaged disk.
            (optional)
        :type ex_vhd_create: ``bool``

        :rtype: ``bool``
        """
        action = node.extra["id"]
        location = node.extra["location"]
        disks = node.extra["properties"]["storageProfile"]["dataDisks"]

        if ex_lun is None:
            # find the smallest unused logical unit number
            used_luns = [disk["lun"] for disk in disks]
            free_luns = [lun for lun in range(0, 64) if lun not in used_luns]
            if len(free_luns) > 0:
                ex_lun = free_luns[0]
            else:
                raise LibcloudError("No LUN available to attach new disk.")

        if ex_vhd_uri is not None:
            new_disk = {
                "name": volume.name,
                "diskSizeGB": volume.size,
                "lun": ex_lun,
                "createOption": "empty" if ex_vhd_create else "attach",
                "vhd": {"uri": ex_vhd_uri},
            }
        else:
            # attach existing managed disk
            new_disk = {
                "lun": ex_lun,
                "createOption": "attach",
                "managedDisk": {"id": volume.id},
            }

        disks.append(new_disk)
        self.connection.request(
            action,
            method="PUT",
            params={"api-version": VM_API_VERSION},
            data={
                "properties": {"storageProfile": {"dataDisks": disks}},
                "location": location,
            },
        )
        return True

    def ex_resize_volume(self, volume, new_size, resource_group):
        """
        Resize a volume.

        :param volume: A volume to resize.
        :type volume: :class:`StorageVolume`

        :param new_size: The new size to resize the volume to in Gib.
        :type new_size: ``int``

        :param resource_group: The name of the resource group in which to
            create the volume.
        :type resource_group: ``str``

        """
        action = (
            "/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
            "/providers/Microsoft.Compute/disks/{volume_name}"
        ).format(
            subscription_id=self.subscription_id,
            resource_group=resource_group,
            volume_name=volume.name,
        )

        data = {
            "location": volume.extra["location"],
            "properties": {
                "diskSizeGB": new_size,
                "creationData": volume.extra["properties"]["creationData"],
            },
        }

        response = self.connection.request(
            action,
            method="PUT",
            params={"api-version": DISK_API_VERSION},
            data=data,
        )

        return self._to_volume(response.object, name=volume.name, ex_resource_group=resource_group)

    def detach_volume(self, volume, ex_node=None):
        """
        Detach a managed volume from a node.
        """
        if ex_node is None:
            raise ValueError("Must provide `ex_node` value.")

        action = ex_node.extra["id"]
        location = ex_node.extra["location"]
        disks = ex_node.extra["properties"]["storageProfile"]["dataDisks"]

        # remove volume from `properties.storageProfile.dataDisks`
        disks[:] = [
            disk
            for disk in disks
            if disk.get("name") != volume.name
            and disk.get("managedDisk", {}).get("id") != volume.id
        ]

        self.connection.request(
            action,
            method="PUT",
            params={"api-version": VM_API_VERSION},
            data={
                "properties": {"storageProfile": {"dataDisks": disks}},
                "location": location,
            },
        )
        return True

    def destroy_volume(self, volume):
        """
        Delete a volume.
        """
        self.ex_delete_resource(volume)
        return True

    def create_volume_snapshot(
        self, volume, name=None, location=None, ex_resource_group=None, ex_tags=None
    ):
        """
        Create snapshot from volume.

        :param volume: Instance of ``StorageVolume``.
        :type volume: :class`StorageVolume`

        :param name: Name of snapshot. (required)
        :type name: ``str``

        :param location: Which data center to create a volume in. (required)
        :type location: :class:`NodeLocation`

        :param ex_resource_group: The name of resource group in which to
            create the snapshot. (required)
        :type ex_resource_group: ``str``

        :param ex_tags: Optional tags to associate with this resource.
        :type ex_tags: ``dict``

        :rtype: :class:`VolumeSnapshot`
        """
        if name is None:
            raise ValueError("Must provide `name` value")
        if location is None:
            raise ValueError("Must provide `location` value")
        if ex_resource_group is None:
            raise ValueError("Must provide `ex_resource_group` value")

        snapshot_id = (
            "/subscriptions/{subscription_id}"
            "/resourceGroups/{resource_group}"
            "/providers/Microsoft.Compute"
            "/snapshots/{snapshot_name}"
        ).format(
            subscription_id=self.subscription_id,
            resource_group=ex_resource_group,
            snapshot_name=name,
        )
        tags = ex_tags if ex_tags is not None else {}

        data = {
            "location": location.id,
            "tags": tags,
            "properties": {
                "creationData": {"createOption": "Copy", "sourceUri": volume.id},
            },
        }
        response = self.connection.request(
            snapshot_id,
            method="PUT",
            data=data,
            params={"api-version": SNAPSHOT_API_VERSION},
        )

        return self._to_snapshot(response.object, name=name, ex_resource_group=ex_resource_group)

    def list_volume_snapshots(self, volume):
        return [
            snapshot
            for snapshot in self.list_snapshots()
            if snapshot.extra["source_id"] == volume.id
        ]

    def list_snapshots(self, ex_resource_group=None):
        """
        Lists all the snapshots under a resource group or subscription.

        :param ex_resource_group: The identifier of your subscription
            where the managed snapshots are located (optional).
        :type ex_resource_group: ``str``

        :rtype: list of :class:`VolumeSnapshot`
        """
        if ex_resource_group:
            action = (
                "/subscriptions/{subscription_id}/resourceGroups"
                "/{resource_group}/providers/Microsoft.Compute/snapshots"
            )
        else:
            action = "/subscriptions/{subscription_id}" "/providers/Microsoft.Compute/snapshots"

        action = action.format(
            subscription_id=self.subscription_id, resource_group=ex_resource_group
        )

        response = self.connection.request(
            action, method="GET", params={"api-version": SNAPSHOT_API_VERSION}
        )
        return [self._to_snapshot(snap) for snap in response.object["value"]]

    def destroy_volume_snapshot(self, snapshot):
        """
        Delete a snapshot.
        """
        self.ex_delete_resource(snapshot)
        return True

    def _to_volume(self, volume_obj, name=None, ex_resource_group=None):
        """
        Parse the JSON element and return a StorageVolume object.

        :param volume_obj: A volume object from an azure response.
        :type volume_obj: ``dict``

        :param name: An optional name for the volume.
        :type name: ``str``

        :param ex_resource_group: An optional resource group for the volume.
        :type ex_resource_group: ``str``

        :rtype: :class:`StorageVolume`
        """
        volume_id = volume_obj.get("id")
        volume_name = volume_obj.get("name")
        extra = dict(volume_obj)
        properties = extra["properties"]
        size = properties.get("diskSizeGB")
        if size is not None:
            size = int(size)

        provisioning_state = properties.get("provisioningState", "").lower()
        disk_state = properties.get("diskState", "").lower()

        if provisioning_state == "creating":
            state = StorageVolumeState.CREATING
        elif provisioning_state == "updating":
            state = StorageVolumeState.UPDATING
        elif provisioning_state == "succeeded":
            if disk_state in ("attached", "reserved", "activesas"):
                state = StorageVolumeState.INUSE
            elif disk_state == "unattached":
                state = StorageVolumeState.AVAILABLE
            else:
                state = StorageVolumeState.UNKNOWN
        else:
            state = StorageVolumeState.UNKNOWN

        if volume_id is None and ex_resource_group is not None and name is not None:
            volume_id = (
                "/subscriptions/{subscription_id}"
                "/resourceGroups/{resource_group}"
                "/providers/Microsoft.Compute/disks/{volume_name}"
            ).format(
                subscription_id=self.subscription_id,
                resource_group=ex_resource_group,
                volume_name=name,
            )
        if volume_name is None and name is not None:
            volume_name = name

        return StorageVolume(
            id=volume_id,
            name=volume_name,
            size=size,
            driver=self,
            state=state,
            extra=extra,
        )

    def _to_snapshot(self, snapshot_obj, name=None, ex_resource_group=None):
        """
        Parse the JSON element and return a VolumeSnapshot object.

        :param snapshot_obj: A snapshot object from an azure response.
        :type snapshot_obj: ``dict``

        :param name: An optional name for the volume.
        :type name: ``str``

        :param ex_resource_group: An optional resource group for the volume.
        :type ex_resource_group: ``str``

        :rtype: :class:`VolumeSnapshot`
        """
        snapshot_id = snapshot_obj.get("id")
        name = snapshot_obj.get("name", name)
        properties = snapshot_obj["properties"]
        size = properties.get("diskSizeGB")
        if size is not None:
            size = int(size)
        extra = dict(snapshot_obj)
        extra["source_id"] = properties["creationData"]["sourceUri"]
        if "/providers/Microsoft.Compute/disks/" in extra["source_id"]:
            extra["volume_id"] = extra["source_id"]
        state = self.SNAPSHOT_STATE_MAP.get(
            properties.get("provisioningState", "").lower(), VolumeSnapshotState.UNKNOWN
        )
        try:
            created_at = iso8601.parse_date(properties.get("timeCreated"))
        except (TypeError, ValueError, iso8601.ParseError):
            created_at = None

        if snapshot_id is None and ex_resource_group is not None and name is not None:
            snapshot_id = (
                "/subscriptions/{subscription_id}"
                "/resourceGroups/{resource_group}"
                "/providers/Microsoft.Compute/snapshots/{snapshot_name}"
            ).format(
                subscription_id=self.subscription_id,
                resource_group=ex_resource_group,
                snapshot_name=name,
            )

        return VolumeSnapshot(
            snapshot_id,
            name=name,
            size=size,
            driver=self,
            state=state,
            extra=extra,
            created=created_at,
        )

    def ex_delete_resource(self, resource):
        """
        Delete a resource.
        """
        if not isinstance(resource, basestring):
            resource = resource.id
        r = self.connection.request(
            resource,
            method="DELETE",
            params={"api-version": RESOURCE_API_VERSION},
        )
        return r.status in [200, 202, 204]

    def ex_get_ratecard(self, offer_durable_id, currency="USD", locale="en-US", region="US"):
        """
        Get rate card

        :param offer_durable_id: ID of the offer applicable for this
        user account. (e.g. "0026P")
        See http://azure.microsoft.com/en-us/support/legal/offer-details/
        :type offer_durable_id: str

        :param currency: Desired currency for the response (default: "USD")
        :type currency: ``str``

        :param locale: Locale (default: "en-US")
        :type locale: ``str``

        :param region: Region (two-letter code) (default: "US")
        :type region: ``str``

        :return: A dictionary of rates whose ID's correspond to nothing at all
        :rtype: ``dict``
        """

        action = "/subscriptions/%s/providers/Microsoft.Commerce/" "RateCard" % (
            self.subscription_id,
        )
        params = {
            "api-version": RATECARD_API_VERSION,
            "$filter": "OfferDurableId eq 'MS-AZR-%s' and "
            "Currency eq '%s' and "
            "Locale eq '%s' and "
            "RegionInfo eq '%s'" % (offer_durable_id, currency, locale, region),
        }
        r = self.connection.request(action, params=params)
        return r.object

    def ex_list_publishers(self, location=None):
        """
        List node image publishers.

        :param location: The location at which to list publishers
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :return: A list of tuples in the form
        ("publisher id", "publisher name")
        :rtype: ``list``
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        action = "/subscriptions/%s/providers/Microsoft.Compute/" "locations/%s/publishers" % (
            self.subscription_id,
            location.id,
        )
        r = self.connection.request(action, params={"api-version": IMAGES_API_VERSION})
        return [(p["id"], p["name"]) for p in r.object]

    def ex_list_offers(self, publisher):
        """
        List node image offers from a publisher.

        :param publisher: The complete resource path to a publisher
        (as returned by `ex_list_publishers`)
        :type publisher: ``str``

        :return: A list of tuples in the form
        ("offer id", "offer name")
        :rtype: ``list``
        """

        action = "%s/artifacttypes/vmimage/offers" % (publisher)
        r = self.connection.request(action, params={"api-version": IMAGES_API_VERSION})
        return [(p["id"], p["name"]) for p in r.object]

    def ex_list_skus(self, offer):
        """
        List node image skus in an offer.

        :param offer: The complete resource path to an offer (as returned by
        `ex_list_offers`)
        :type offer: ``str``

        :return: A list of tuples in the form
        ("sku id", "sku name")
        :rtype: ``list``
        """

        action = "%s/skus" % offer
        r = self.connection.request(action, params={"api-version": IMAGES_API_VERSION})
        return [(sku["id"], sku["name"]) for sku in r.object]

    def ex_list_image_versions(self, sku):
        """
        List node image versions in a sku.

        :param sku: The complete resource path to a sku (as returned by
        `ex_list_skus`)
        :type publisher: ``str``

        :return: A list of tuples in the form
        ("version id", "version name")
        :rtype: ``list``
        """

        action = "%s/versions" % (sku)
        r = self.connection.request(action, params={"api-version": IMAGES_API_VERSION})
        return [(img["id"], img["name"]) for img in r.object]

    def ex_list_resource_groups(self):
        """
        List resource groups.

        :return: A list of resource groups.
        :rtype: ``list`` of :class:`.AzureResourceGroup`
        """

        action = "/subscriptions/%s/resourceGroups/" % (self.subscription_id)
        r = self.connection.request(action, params={"api-version": RESOURCE_GROUP_API_VERSION})
        return [
            AzureResourceGroup(grp["id"], grp["name"], grp["location"], grp["properties"])
            for grp in r.object["value"]
        ]

    def ex_list_network_security_groups(self, resource_group):
        """
        List network security groups.

        :param resource_group: List security groups in a specific resource
        group.
        :type resource_group: ``str``

        :return: A list of network security groups.
        :rtype: ``list`` of :class:`.AzureNetworkSecurityGroup`
        """

        action = (
            "/subscriptions/%s/resourceGroups/%s/providers/"
            "Microsoft.Network/networkSecurityGroups" % (self.subscription_id, resource_group)
        )
        r = self.connection.request(action, params={"api-version": NSG_API_VERSION})
        return [
            AzureNetworkSecurityGroup(net["id"], net["name"], net["location"], net["properties"])
            for net in r.object["value"]
        ]

    def ex_create_network_security_group(self, name, resource_group, location=None):
        """
        Update tags on any resource supporting tags.

        :param name: Name of the network security group to create
        :type name: ``str``

        :param resource_group: The resource group to create the network
        security group in
        :type resource_group: ``str``

        :param location: The location at which to create the network security
        group (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        target = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Network/networkSecurityGroups/%s"
            % (self.subscription_id, resource_group, name)
        )
        data = {
            "location": location.id,
        }
        self.connection.request(
            target, params={"api-version": NSG_API_VERSION}, data=data, method="PUT"
        )

    def ex_delete_network_security_group(self, name, resource_group, location=None):
        """
        Update tags on any resource supporting tags.

        :param name: Name of the network security group to delete
        :type name: ``str``

        :param resource_group: The resource group to create the network
        security group in
        :type resource_group: ``str``

        :param location: The location at which to create the network security
        group (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        target = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Network/networkSecurityGroups/%s"
            % (self.subscription_id, resource_group, name)
        )
        data = {
            "location": location.id,
        }
        self.connection.request(
            target, params={"api-version": NSG_API_VERSION}, data=data, method="DELETE"
        )

    def ex_list_networks(self):
        """
        List virtual networks.

        :return: A list of virtual networks.
        :rtype: ``list`` of :class:`.AzureNetwork`
        """

        action = "/subscriptions/%s/providers/" "Microsoft.Network/virtualnetworks" % (
            self.subscription_id
        )
        r = self.connection.request(action, params={"api-version": VIRTUAL_NETWORK_API_VERSION})
        return [
            AzureNetwork(net["id"], net["name"], net["location"], net["properties"])
            for net in r.object["value"]
        ]

    def ex_list_subnets(self, network):
        """
        List subnets of a virtual network.

        :param network: The virtual network containing the subnets.
        :type network: :class:`.AzureNetwork`

        :return: A list of subnets.
        :rtype: ``list`` of :class:`.AzureSubnet`
        """

        action = "%s/subnets" % (network.id)
        r = self.connection.request(action, params={"api-version": SUBNET_API_VERSION})
        return [AzureSubnet(net["id"], net["name"], net["properties"]) for net in r.object["value"]]

    def ex_list_nics(self, resource_group=None):
        """
        List available virtual network interface controllers
        in a resource group

        :param resource_group: List NICS in a specific resource group
            containing the NICs(optional).
        :type resource_group: ``str``

        :return: A list of NICs.
        :rtype: ``list`` of :class:`.AzureNic`
        """
        if resource_group is None:
            action = (
                "/subscriptions/%s/providers/Microsoft.Network"
                "/networkInterfaces" % self.subscription_id
            )
        else:
            action = (
                "/subscriptions/%s/resourceGroups/%s/providers"
                "/Microsoft.Network/networkInterfaces" % (self.subscription_id, resource_group)
            )
        r = self.connection.request(action, params={"api-version": NIC_API_VERSION})
        return [self._to_nic(net) for net in r.object["value"]]

    def ex_get_nic(self, id):
        """
        Fetch information about a NIC.

        :param id: The complete resource path to the NIC resource.
        :type id: ``str``

        :return: The NIC object
        :rtype: :class:`.AzureNic`
        """

        r = self.connection.request(id, params={"api-version": NIC_API_VERSION})
        return self._to_nic(r.object)

    def ex_update_nic_properties(self, network_interface, resource_group, properties):
        """
        Update the properties of an already existing virtual network
        interface (NIC).

        :param network_interface: The NIC to update.
        :type network_interface: :class:`.AzureNic`

        :param resource_group: The resource group to check the ip address in.
        :type resource_group: ``str``

        :param properties: The dictionary of the NIC's properties
        :type properties: ``dict``

        :return: The NIC object
        :rtype: :class:`.AzureNic`
        """

        target = (
            "/subscriptions/%s/resourceGroups/%s/providers"
            "/Microsoft.Network/networkInterfaces/%s"
            % (self.subscription_id, resource_group, network_interface.name)
        )

        data = {
            "properties": properties,
            "location": network_interface.location,
        }

        r = self.connection.request(
            target, params={"api-version": NIC_API_VERSION}, data=data, method="PUT"
        )
        return AzureNic(
            r.object["id"],
            r.object["name"],
            r.object["location"],
            r.object["properties"],
        )

    def ex_update_network_profile_of_node(self, node, network_profile):
        """
        Update the network profile of a node. This method can be used to
        attach or detach a NIC to a node.

        :param node: A node to attach the network interface to.
        :type node: :class:`Node`

        :param network_profile: The new network profile to update.
        :type network_profile: ``dict``
        """
        action = node.extra["id"]
        location = node.extra["location"]
        self.connection.request(
            action,
            method="PUT",
            params={"api-version": VM_API_VERSION},
            data={
                "id": node.id,
                "name": node.name,
                "type": "Microsoft.Compute/virtualMachines",
                "location": location,
                "properties": {"networkProfile": network_profile},
            },
        )

    def ex_destroy_nic(self, nic):
        """
        Destroy a NIC.

        :param nic: The NIC to destroy.
        :type nic: ``.AzureNic``

        :return: True on success
        :rtype: ``bool``
        """

        try:
            self.connection.request(
                nic.id, params={"api-version": NIC_API_VERSION}, method="DELETE"
            )
            return True
        except BaseHTTPError as h:
            if h.code in (202, 204):
                # Deletion is accepted (but deferred), or NIC is already
                # deleted
                return True
            else:
                raise

    def ex_check_ip_address_availability(self, resource_group, network, ip_address):
        """
        Checks whether a private IP address is available for use. Also
        returns an object that contains the available IPs in the subnet.

        :param resource_group: The resource group to check the ip address in.
        :type resource_group: ``str``

        :param network: The virtual network.
        :type network: :class:`.AzureNetwork`

        :param ip_address: The private IP address to be verified.
        :type ip_address: ``str``
        """
        params = {"api-version": VIRTUAL_NETWORK_API_VERSION}
        action = (
            "/subscriptions/%s/resourceGroups/%s/providers"
            "/Microsoft.Network/virtualNetworks/%s/"
            "CheckIPAddressAvailability" % (self.subscription_id, resource_group, network.name)
        )
        if ip_address is not None:
            params["ipAddress"] = ip_address
        r = self.connection.request(action, params=params)
        return r.object

    def ex_get_node(self, id):
        """
        Fetch information about a node.

        :param id: The complete resource path to the node resource.
        :type id: ``str``

        :return: The Node object
        :rtype: :class:`.Node`
        """

        r = self.connection.request(id, params={"api-version": VM_API_VERSION})
        return self._to_node(r.object)

    def ex_get_volume(self, id):
        """
        Fetch information about a volume.

        :param id: The complete resource path to the volume resource.
        :type id: ``str``

        :return: The StorageVolume object
        :rtype: :class:`.StorageVolume`
        """

        r = self.connection.request(id, params={"api-version": DISK_API_VERSION})
        return self._to_volume(r.object)

    def ex_get_snapshot(self, id):
        """
        Fetch information about a snapshot.

        :param id: The complete resource path to the snapshot resource.
        :type id: ``str``

        :return: The VolumeSnapshot object
        :rtype: :class:`.VolumeSnapshot`
        """

        r = self.connection.request(id, params={"api-version": SNAPSHOT_API_VERSION})
        return self._to_snapshot(r.object)

    def ex_get_public_ip(self, id):
        """
        Fetch information about a public IP resource.

        :param id: The complete resource path to the public IP resource.
        :type id: ``str`

        :return: The public ip object
        :rtype: :class:`.AzureIPAddress`
        """

        r = self.connection.request(id, params={"api-version": IP_API_VERSION})
        return self._to_ip_address(r.object)

    def ex_list_public_ips(self, resource_group):
        """
        List public IP resources.

        :param resource_group: List public IPs in a specific resource group.
        :type resource_group: ``str``

        :return: List of public ip objects
        :rtype: ``list`` of :class:`.AzureIPAddress`
        """

        action = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Network/publicIPAddresses" % (self.subscription_id, resource_group)
        )
        r = self.connection.request(action, params={"api-version": IP_API_VERSION})
        return [self._to_ip_address(net) for net in r.object["value"]]

    def ex_create_public_ip(
        self, name, resource_group, location=None, public_ip_allocation_method=None
    ):
        """
        Create a public IP resources.

        :param name: Name of the public IP resource
        :type name: ``str``

        :param resource_group: The resource group to create the public IP
        :type resource_group: ``str``

        :param location: The location at which to create the public IP
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :param public_ip_allocation_method: Call ex_create_public_ip with
        public_ip_allocation_method="Static" to create a static public
        IP address
        :type public_ip_allocation_method: ``str``

        :return: The newly created public ip object
        :rtype: :class:`.AzureIPAddress`
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        target = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Network/publicIPAddresses/%s"
            % (self.subscription_id, resource_group, name)
        )
        data = {
            "location": location.id,
            "tags": {},
            "properties": {"publicIPAllocationMethod": "Dynamic"},
        }

        if public_ip_allocation_method == "Static":
            data["properties"]["publicIPAllocationMethod"] = "Static"

        r = self.connection.request(
            target, params={"api-version": IP_API_VERSION}, data=data, method="PUT"
        )
        return self._to_ip_address(r.object)

    def ex_delete_public_ip(self, public_ip):
        """
        Delete a public ip address resource.

        :param public_ip: Public ip address resource to delete
        :type public_ip: `.AzureIPAddress`
        """
        # NOTE: This operation requires API version 2018-11-01 so
        # "ex_delete_resource" won't work for deleting an IP address
        resource = public_ip.id
        r = self.connection.request(
            resource,
            method="DELETE",
            params={"api-version": IP_API_VERSION},
        )

        return r.status in [200, 202, 204]

    def ex_create_network_interface(
        self, name, subnet, resource_group, location=None, public_ip=None
    ):
        """
        Create a virtual network interface (NIC).

        :param name: Name of the NIC resource
        :type name: ``str``

        :param subnet: The subnet to attach the NIC
        :type subnet: :class:`.AzureSubnet`

        :param resource_group: The resource group to create the NIC
        :type resource_group: ``str``

        :param location: The location at which to create the NIC
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :param public_ip: Associate a public IP resource with this NIC
        (optional).
        :type public_ip: :class:`.AzureIPAddress`

        :return: The newly created NIC
        :rtype: :class:`.AzureNic`
        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        target = (
            "/subscriptions/%s/resourceGroups/%s/providers"
            "/Microsoft.Network/networkInterfaces/%s" % (self.subscription_id, resource_group, name)
        )

        data = {
            "location": location.id,
            "tags": {},
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "myip1",
                        "properties": {
                            "subnet": {"id": subnet.id},
                            "privateIPAllocationMethod": "Dynamic",
                        },
                    }
                ]
            },
        }

        if public_ip:
            ip_config = data["properties"]["ipConfigurations"][0]
            ip_config["properties"]["publicIPAddress"] = {"id": public_ip.id}

        r = self.connection.request(
            target, params={"api-version": NIC_API_VERSION}, data=data, method="PUT"
        )
        return AzureNic(
            r.object["id"],
            r.object["name"],
            r.object["location"],
            r.object["properties"],
        )

    def ex_create_tags(self, resource, tags, replace=False):
        """
        Update tags on any resource supporting tags.

        :param resource: The resource to update.
        :type resource: ``str`` or Azure object with an ``id`` attribute.

        :param tags: The tags to set.
        :type tags: ``dict``

        :param replace: If true, replace all tags with the new tags.
        If false (default) add or update tags.
        :type replace: ``bool``
        """

        if not isinstance(resource, basestring):
            resource = resource.id
        r = self.connection.request(resource, params={"api-version": TAG_API_VERSION})
        if replace:
            r.object["tags"] = tags
        else:
            r.object["tags"].update(tags)
        self.connection.request(
            resource,
            data={"tags": r.object["tags"]},
            params={"api-version": TAG_API_VERSION},
            method="PATCH",
        )

    def ex_create_additional_capabilities(
        self,
        node,
        additional_capabilities,
        resource_group,
    ):
        """
        Set the additional capabilities on a stopped node.

        :param node: The node to be updated
        :type node: :class:`.Node`

        :param ex_additional_capabilities: Optional additional capabilities
            allowing Ultra SSD and hibernation on this node.
        :type ex_additional_capabilities: ``dict``

        :param resource_group: The resource group of the node to be updated
        :type resource_group: ``str``

        :return: True if the update was successful, otherwise False
        :rtype: ``bool``
        """

        target = (
            "/subscriptions/%s/resourceGroups/%s/providers"
            "/Microsoft.Compute/virtualMachines/%s"
            % (self.subscription_id, resource_group, node.name)
        )

        data = {
            "location": node.extra["location"],
            "properties": {"additionalCapabilities": additional_capabilities},
        }

        r = self.connection.request(
            target,
            data=data,
            params={"api-version": VM_API_VERSION},
            method="PUT",
        )

        return r.status in [200, 202, 204]

    def start_node(self, node):
        """
        Start a stopped node.

        :param node: The node to be started
        :type node: :class:`.Node`
        """

        target = "%s/start" % node.id
        r = self.connection.request(target, params={"api-version": VM_API_VERSION}, method="POST")
        return r.object

    def stop_node(self, node, ex_deallocate=True):
        """
        Stop a running node.

        :param node: The node to be stopped
        :type node: :class:`.Node`

        :param deallocate: If True (default) stop and then deallocate the node
        (release the hardware allocated to run the node).  If False, stop the
        node but maintain the hardware allocation.  If the node is not
        deallocated, the subscription will continue to be billed as if it
        were running.
        :type deallocate: ``bool``
        """
        if ex_deallocate:
            target = "%s/deallocate" % node.id
        else:
            target = "%s/powerOff" % node.id
        r = self.connection.request(target, params={"api-version": VM_API_VERSION}, method="POST")
        return r.object

    def ex_start_node(self, node):
        # NOTE: This method is here for backward compatibility reasons after
        # this method was promoted to be part of the standard compute API in
        # Libcloud v2.7.0
        return self.start_node(node=node)

    def ex_stop_node(self, node, deallocate=True):
        # NOTE: This method is here for backward compatibility reasons after
        # this method was promoted to be part of the standard compute API in
        # Libcloud v2.7.0
        return self.stop_node(node=node, ex_deallocate=deallocate)

    def ex_get_storage_account_keys(self, resource_group, storage_account):
        """
        Get account keys required to access to a storage account
        (using AzureBlobsStorageDriver).

        :param resource_group: The resource group
            containing the storage account
        :type resource_group: ``str``

        :param storage_account: Storage account to access
        :type storage_account: ``str``

        :return: The account keys, in the form `{"key1": "XXX", "key2": "YYY"}`
        :rtype: ``.dict``
        """

        action = (
            "/subscriptions/%s/resourceGroups/%s/"
            "providers/Microsoft.Storage/storageAccounts/%s/listKeys"
            % (self.subscription_id, resource_group, storage_account)
        )

        r = self.connection.request(
            action, params={"api-version": STORAGE_ACCOUNT_API_VERSION}, method="POST"
        )
        return r.object

    def ex_run_command(
        self,
        node,
        command,
        filerefs=[],
        timestamp=0,
        storage_account_name=None,
        storage_account_key=None,
        location=None,
    ):
        """
        Run a command on the node as root.

        Does not require ssh to log in,
        uses Windows Azure Agent (waagent) running
        on the node.

        :param node: The node on which to run the command.
        :type node: :class:``.Node``

        :param command: The actual command to run.  Note this is parsed
        into separate arguments according to shell quoting rules but is
        executed directly as a subprocess, not a shell command.
        :type command: ``str``

        :param filerefs: Optional files to fetch by URI from Azure blob store
        (must provide storage_account_name and storage_account_key),
        or regular HTTP.
        :type command: ``list`` of ``str``

        :param location: The location of the virtual machine
        (if None, use default location specified as 'region' in __init__)
        :type location: :class:`.NodeLocation`

        :param storage_account_name: The storage account
            from which to fetch files in `filerefs`
        :type storage_account_name: ``str``

        :param storage_account_key: The storage key to
            authorize to the blob store.
        :type storage_account_key: ``str``

        :type: ``list`` of :class:`.NodeLocation`

        """

        if location is None:
            if self.default_location:
                location = self.default_location
            else:
                raise ValueError("location is required.")

        name = "init"

        target = node.id + "/extensions/" + name

        data = {
            "location": location.id,
            "name": name,
            "properties": {
                "publisher": "Microsoft.OSTCExtensions",
                "type": "CustomScriptForLinux",
                "typeHandlerVersion": "1.3",
                "settings": {
                    "fileUris": filerefs,
                    "commandToExecute": command,
                    "timestamp": timestamp,
                },
            },
        }

        if storage_account_name and storage_account_key:
            data["properties"]["protectedSettings"] = {
                "storageAccountName": storage_account_name,
                "storageAccountKey": storage_account_key,
            }

        r = self.connection.request(
            target,
            params={"api-version": VM_EXTENSION_API_VERSION},
            data=data,
            method="PUT",
        )
        return r.object

    def _ex_delete_old_vhd(self, resource_group, uri):
        try:
            (storageAccount, blobContainer, blob) = _split_blob_uri(uri)
            keys = self.ex_get_storage_account_keys(resource_group, storageAccount)
            blobdriver = AzureBlobsStorageDriver(
                storageAccount,
                keys["key1"],
                host="{}.blob{}".format(storageAccount, self.connection.storage_suffix),
            )
            return blobdriver.delete_object(blobdriver.get_object(blobContainer, blob))
        except ObjectDoesNotExistError:
            return True

    def _ex_connection_class_kwargs(self):
        kwargs = super()._ex_connection_class_kwargs()
        kwargs["tenant_id"] = self.tenant_id
        kwargs["subscription_id"] = self.subscription_id
        kwargs["cloud_environment"] = self.cloud_environment
        return kwargs

    def _fetch_power_state(self, data):
        state = NodeState.UNKNOWN
        try:
            action = "%s/InstanceView" % (data["id"])
            r = self.connection.request(action, params={"api-version": INSTANCE_VIEW_API_VERSION})
            for status in r.object["statuses"]:
                if status["code"] in ["ProvisioningState/creating"]:
                    state = NodeState.PENDING
                    break
                elif status["code"] == "ProvisioningState/deleting":
                    state = NodeState.TERMINATED
                    break
                elif status["code"].startswith("ProvisioningState/failed"):
                    state = NodeState.ERROR
                    break
                elif status["code"] == "ProvisioningState/updating":
                    state = NodeState.UPDATING
                    break
                elif status["code"] == "ProvisioningState/succeeded":
                    pass

                if status["code"] == "PowerState/deallocated":
                    state = NodeState.STOPPED
                    break
                elif status["code"] == "PowerState/stopped":
                    state = NodeState.PAUSED
                    break
                elif status["code"] == "PowerState/deallocating":
                    state = NodeState.PENDING
                    break
                elif status["code"] == "PowerState/running":
                    state = NodeState.RUNNING
        except BaseHTTPError:
            pass
        return state

    def _to_node(self, data, fetch_nic=True, fetch_power_state=True):
        private_ips = []
        public_ips = []
        nics = data["properties"]["networkProfile"]["networkInterfaces"]
        if fetch_nic:
            for nic in nics:
                try:
                    n = self.ex_get_nic(nic["id"])
                    priv = n.extra["ipConfigurations"][0]["properties"].get("privateIPAddress")
                    if priv:
                        private_ips.append(priv)
                    pub = n.extra["ipConfigurations"][0]["properties"].get("publicIPAddress")
                    if pub:
                        pub_addr = self.ex_get_public_ip(pub["id"])
                        addr = pub_addr.extra.get("ipAddress")
                        if addr:
                            public_ips.append(addr)
                except BaseHTTPError:
                    pass

        state = NodeState.UNKNOWN
        if fetch_power_state:
            state = self._fetch_power_state(data)
        else:
            ps = data["properties"]["provisioningState"].lower()
            if ps == "creating":
                state = NodeState.PENDING
            elif ps == "deleting":
                state = NodeState.TERMINATED
            elif ps == "failed":
                state = NodeState.ERROR
            elif ps == "updating":
                state = NodeState.UPDATING
            elif ps == "succeeded":
                state = NodeState.RUNNING

        node = Node(
            data["id"],
            data["name"],
            state,
            public_ips,
            private_ips,
            driver=self.connection.driver,
            extra=data,
        )

        return node

    def _to_node_size(self, data):
        return NodeSize(
            id=data["name"],
            name=data["name"],
            ram=data["memoryInMB"],
            # convert to disk from MB to GB
            disk=data["resourceDiskSizeInMB"] / 1024,
            bandwidth=0,
            price=0,
            driver=self.connection.driver,
            extra={
                "numberOfCores": data["numberOfCores"],
                "osDiskSizeInMB": data["osDiskSizeInMB"],
                "maxDataDiskCount": data["maxDataDiskCount"],
            },
        )

    def _to_nic(self, data):
        return AzureNic(data["id"], data.get("name"), data.get("location"), data.get("properties"))

    def _to_ip_address(self, data):
        return AzureIPAddress(data["id"], data["name"], data["properties"])

    def _to_location(self, loc):
        # XXX for some reason the API returns location names like
        # "East US" instead of "eastus" which is what is actually needed
        # for other API calls, so do a name->id fixup.
        loc_id = loc.lower().replace(" ", "")
        return NodeLocation(
            loc_id, loc, self._location_to_country.get(loc_id), self.connection.driver
        )

    def _get_instance_vhd(
        self, name, ex_resource_group, ex_storage_account, ex_blob_container="vhds"
    ):
        n = 0
        errors = []
        while n < 10:
            try:
                instance_vhd = "https://%s.blob%s" "/%s/%s-os_%i.vhd" % (
                    ex_storage_account,
                    self.connection.storage_suffix,
                    ex_blob_container,
                    name,
                    n,
                )
                if self._ex_delete_old_vhd(ex_resource_group, instance_vhd):
                    # We were able to remove it or it doesn't exist,
                    # so we can use it.
                    return instance_vhd
            except LibcloudError as lce:
                errors.append(str(lce))
            n += 1
        raise LibcloudError(
            "Unable to find a name for a VHD to use for "
            "instance in 10 tries, errors were:\n  - %s" % ("\n  - ".join(errors))
        )


def _split_blob_uri(uri):
    uri = uri.split("/")
    storage_account = uri[2].split(".")[0]
    blob_container = uri[3]
    blob_name = "/".join(uri[4:])
    return storage_account, blob_container, blob_name
