blob: 971c2152bb4bac22d5539112d85d44bc06b0ee6f [file] [log] [blame]
# 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