saltstack/salt

View on GitHub
salt/cloud/clouds/msazure.py

Summary

Maintainability
F
1 mo
Test Coverage
# -*- coding: utf-8 -*-
'''
Azure Cloud Module
==================

The Azure cloud module is used to control access to Microsoft Azure

:depends:
    * `Microsoft Azure SDK for Python <https://pypi.python.org/pypi/azure/1.0.2>`_ >= 1.0.2
    * python-requests, for Python < 2.7.9
:configuration:
    Required provider parameters:

    * ``apikey``
    * ``certificate_path``
    * ``subscription_id``
    * ``backend``

    A Management Certificate (.pem and .crt files) must be created and the .pem
    file placed on the same machine that salt-cloud is run from. Information on
    creating the pem file to use, and uploading the associated cer file can be
    found at:

    http://www.windowsazure.com/en-us/develop/python/how-to-guides/service-management/

    For users with Python < 2.7.9, ``backend`` must currently be set to ``requests``.

Example ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/azure.conf`` configuration:

.. code-block:: yaml

    my-azure-config:
      driver: azure
      subscription_id: 3287abc8-f98a-c678-3bde-326766fd3617
      certificate_path: /etc/salt/azure.pem
      management_host: management.core.windows.net
'''
# pylint: disable=E0102

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import copy
import logging
import pprint
import time

# Import salt libs
import salt.config as config
from salt.exceptions import SaltCloudSystemExit
import salt.utils.cloud
import salt.utils.stringutils
import salt.utils.yaml

# Import 3rd-party libs
from salt.ext import six
HAS_LIBS = False
try:
    import azure
    import azure.storage
    import azure.servicemanagement
    from azure.common import (AzureConflictHttpError,
                       AzureMissingResourceHttpError,
                       AzureException)
    import salt.utils.msazure
    from salt.utils.msazure import object_to_dict
    HAS_LIBS = True
except ImportError:
    pass

__virtualname__ = 'azure'


# Get logging started
log = logging.getLogger(__name__)


# Only load in this module if the AZURE configurations are in place
def __virtual__():
    '''
    Check for Azure configurations.
    '''
    if get_configured_provider() is False:
        return False

    if get_dependencies() is False:
        return False

    return __virtualname__


def get_configured_provider():
    '''
    Return the first configured instance.
    '''
    return config.is_provider_configured(
        __opts__,
        __active_provider_name__ or __virtualname__,
        ('subscription_id', 'certificate_path')
    )


def get_dependencies():
    '''
    Warn if dependencies aren't met.
    '''
    return config.check_driver_dependencies(
        __virtualname__,
        {'azure': HAS_LIBS}
    )


def get_conn():
    '''
    Return a conn object for the passed VM data
    '''
    certificate_path = config.get_cloud_config_value(
        'certificate_path',
        get_configured_provider(), __opts__, search_global=False
    )
    subscription_id = salt.utils.stringutils.to_str(
        config.get_cloud_config_value(
            'subscription_id',
            get_configured_provider(), __opts__, search_global=False
        )
    )
    management_host = config.get_cloud_config_value(
        'management_host',
        get_configured_provider(),
        __opts__,
        search_global=False,
        default='management.core.windows.net'
    )
    return azure.servicemanagement.ServiceManagementService(
        subscription_id, certificate_path, management_host
    )


def script(vm_):
    '''
    Return the script deployment object
    '''
    return salt.utils.cloud.os_script(
        config.get_cloud_config_value('script', vm_, __opts__),
        vm_,
        __opts__,
        salt.utils.cloud.salt_config_to_yaml(
            salt.utils.cloud.minion_config(__opts__, vm_)
        )
    )


def avail_locations(conn=None, call=None):
    '''
    List available locations for Azure
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_locations function must be called with '
            '-f or --function, or with the --list-locations option'
        )

    if not conn:
        conn = get_conn()

    ret = {}
    locations = conn.list_locations()
    for location in locations:
        ret[location.name] = {
            'name': location.name,
            'display_name': location.display_name,
            'available_services': location.available_services,
        }
    return ret


def avail_images(conn=None, call=None):
    '''
    List available images for Azure
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_images function must be called with '
            '-f or --function, or with the --list-images option'
        )

    if not conn:
        conn = get_conn()

    ret = {}
    for item in conn.list_os_images():
        ret[item.name] = object_to_dict(item)
    for item in conn.list_vm_images():
        ret[item.name] = object_to_dict(item)
    return ret


def avail_sizes(call=None):
    '''
    Return a list of sizes from Azure
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_sizes function must be called with '
            '-f or --function, or with the --list-sizes option'
        )

    conn = get_conn()
    data = conn.list_role_sizes()
    ret = {}
    for item in data.role_sizes:
        ret[item.name] = object_to_dict(item)
    return ret


def list_nodes(conn=None, call=None):
    '''
    List VMs on this Azure account
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_nodes function must be called with -f or --function.'
        )

    ret = {}
    nodes = list_nodes_full(conn, call)
    for node in nodes:
        ret[node] = {'name': node}
        for prop in ('id', 'image', 'size', 'state', 'private_ips', 'public_ips'):
            ret[node][prop] = nodes[node].get(prop)
    return ret


def list_nodes_full(conn=None, call=None):
    '''
    List VMs on this Azure account, with full information
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_nodes_full function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    ret = {}
    services = list_hosted_services(conn=conn, call=call)
    for service in services:
        for deployment in services[service]['deployments']:
            deploy_dict = services[service]['deployments'][deployment]
            deploy_dict_no_role_info = copy.deepcopy(deploy_dict)
            del deploy_dict_no_role_info['role_list']
            del deploy_dict_no_role_info['role_instance_list']
            roles = deploy_dict['role_list']
            for role in roles:
                role_instances = deploy_dict['role_instance_list']
                ret[role] = roles[role]
                ret[role].update(role_instances[role])
                ret[role]['id'] = role
                ret[role]['hosted_service'] = service
                if role_instances[role]['power_state'] == 'Started':
                    ret[role]['state'] = 'running'
                elif role_instances[role]['power_state'] == 'Stopped':
                    ret[role]['state'] = 'stopped'
                else:
                    ret[role]['state'] = 'pending'
                ret[role]['private_ips'] = []
                ret[role]['public_ips'] = []
                ret[role]['deployment'] = deploy_dict_no_role_info
                ret[role]['url'] = deploy_dict['url']
                ip_address = role_instances[role]['ip_address']
                if ip_address:
                    if salt.utils.cloud.is_public_ip(ip_address):
                        ret[role]['public_ips'].append(ip_address)
                    else:
                        ret[role]['private_ips'].append(ip_address)
                ret[role]['size'] = role_instances[role]['instance_size']
                ret[role]['image'] = roles[role]['role_info']['os_virtual_hard_disk']['source_image_name']
    return ret


def list_hosted_services(conn=None, call=None):
    '''
    List VMs on this Azure account, with full information
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_hosted_services function must be called with '
            '-f or --function'
        )

    if not conn:
        conn = get_conn()

    ret = {}
    services = conn.list_hosted_services()
    for service in services:
        props = service.hosted_service_properties
        ret[service.service_name] = {
            'name': service.service_name,
            'url': service.url,
            'affinity_group': props.affinity_group,
            'date_created': props.date_created,
            'date_last_modified': props.date_last_modified,
            'description': props.description,
            'extended_properties': props.extended_properties,
            'label': props.label,
            'location': props.location,
            'status': props.status,
            'deployments': {},
        }
        deployments = conn.get_hosted_service_properties(
            service_name=service.service_name, embed_detail=True
        )
        for deployment in deployments.deployments:
            ret[service.service_name]['deployments'][deployment.name] = {
                'configuration': deployment.configuration,
                'created_time': deployment.created_time,
                'deployment_slot': deployment.deployment_slot,
                'extended_properties': deployment.extended_properties,
                'input_endpoint_list': deployment.input_endpoint_list,
                'label': deployment.label,
                'last_modified_time': deployment.last_modified_time,
                'locked': deployment.locked,
                'name': deployment.name,
                'persistent_vm_downtime_info': deployment.persistent_vm_downtime_info,
                'private_id': deployment.private_id,
                'role_instance_list': {},
                'role_list': {},
                'rollback_allowed': deployment.rollback_allowed,
                'sdk_version': deployment.sdk_version,
                'status': deployment.status,
                'upgrade_domain_count': deployment.upgrade_domain_count,
                'upgrade_status': deployment.upgrade_status,
                'url': deployment.url,
            }
            for role_instance in deployment.role_instance_list:
                ret[service.service_name]['deployments'][deployment.name]['role_instance_list'][role_instance.role_name] = {
                    'fqdn': role_instance.fqdn,
                    'instance_error_code': role_instance.instance_error_code,
                    'instance_fault_domain': role_instance.instance_fault_domain,
                    'instance_name': role_instance.instance_name,
                    'instance_size': role_instance.instance_size,
                    'instance_state_details': role_instance.instance_state_details,
                    'instance_status': role_instance.instance_status,
                    'instance_upgrade_domain': role_instance.instance_upgrade_domain,
                    'ip_address': role_instance.ip_address,
                    'power_state': role_instance.power_state,
                    'role_name': role_instance.role_name,
                }
            for role in deployment.role_list:
                ret[service.service_name]['deployments'][deployment.name]['role_list'][role.role_name] = {
                    'role_name': role.role_name,
                    'os_version': role.os_version,
                }
                role_info = conn.get_role(
                    service_name=service.service_name,
                    deployment_name=deployment.name,
                    role_name=role.role_name,
                )
                ret[service.service_name]['deployments'][deployment.name]['role_list'][role.role_name]['role_info'] = {
                    'availability_set_name': role_info.availability_set_name,
                    'configuration_sets': role_info.configuration_sets,
                    'data_virtual_hard_disks': role_info.data_virtual_hard_disks,
                    'os_version': role_info.os_version,
                    'role_name': role_info.role_name,
                    'role_size': role_info.role_size,
                    'role_type': role_info.role_type,
                }
                ret[service.service_name]['deployments'][deployment.name]['role_list'][role.role_name]['role_info']['os_virtual_hard_disk'] = {
                    'disk_label': role_info.os_virtual_hard_disk.disk_label,
                    'disk_name': role_info.os_virtual_hard_disk.disk_name,
                    'host_caching': role_info.os_virtual_hard_disk.host_caching,
                    'media_link': role_info.os_virtual_hard_disk.media_link,
                    'os': role_info.os_virtual_hard_disk.os,
                    'source_image_name': role_info.os_virtual_hard_disk.source_image_name,
                }
    return ret


def list_nodes_select(conn=None, call=None):
    '''
    Return a list of the VMs that are on the provider, with select fields
    '''
    if not conn:
        conn = get_conn()

    return salt.utils.cloud.list_nodes_select(
        list_nodes_full(conn, 'function'), __opts__['query.selection'], call,
    )


def show_instance(name, call=None):
    '''
    Show the details from the provider concerning an instance
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The show_instance action must be called with -a or --action.'
        )

    nodes = list_nodes_full()
    # Find under which cloud service the name is listed, if any
    if name not in nodes:
        return {}
    if 'name' not in nodes[name]:
        nodes[name]['name'] = nodes[name]['id']
    try:
        __utils__['cloud.cache_node'](nodes[name], __active_provider_name__, __opts__)
    except TypeError:
        log.warning('Unable to show cache node data; this may be because the node has been deleted')
    return nodes[name]


def create(vm_):
    '''
    Create a single VM from a data dict
    '''
    try:
        # Check for required profile parameters before sending any API calls.
        if vm_['profile'] and config.is_profile_configured(__opts__,
                                                           __active_provider_name__ or 'azure',
                                                           vm_['profile'],
                                                           vm_=vm_) is False:
            return False
    except AttributeError:
        pass

    __utils__['cloud.fire_event'](
        'event',
        'starting create',
        'salt/cloud/{0}/creating'.format(vm_['name']),
        args=__utils__['cloud.filter_event']('creating', vm_, ['name', 'profile', 'provider', 'driver']),
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )

    log.info('Creating Cloud VM %s', vm_['name'])
    conn = get_conn()

    label = vm_.get('label', vm_['name'])
    service_name = vm_.get('service_name', vm_['name'])
    service_kwargs = {
        'service_name': service_name,
        'label': label,
        'description': vm_.get('desc', vm_['name']),
    }

    loc_error = False
    if 'location' in vm_:
        if 'affinity_group' in vm_:
            loc_error = True
        else:
            service_kwargs['location'] = vm_['location']
    elif 'affinity_group' in vm_:
        service_kwargs['affinity_group'] = vm_['affinity_group']
    else:
        loc_error = True

    if loc_error:
        raise SaltCloudSystemExit(
            'Either a location or affinity group must be specified, but not both'
        )

    ssh_port = config.get_cloud_config_value('port', vm_, __opts__,
                                             default=22, search_global=True)

    ssh_endpoint = azure.servicemanagement.ConfigurationSetInputEndpoint(
        name='SSH',
        protocol='TCP',
        port=ssh_port,
        local_port=22,
    )

    network_config = azure.servicemanagement.ConfigurationSet()
    network_config.input_endpoints.input_endpoints.append(ssh_endpoint)
    network_config.configuration_set_type = 'NetworkConfiguration'

    if 'win_username' in vm_:
        system_config = azure.servicemanagement.WindowsConfigurationSet(
            computer_name=vm_['name'],
            admin_username=vm_['win_username'],
            admin_password=vm_['win_password'],
        )

        smb_port = '445'
        if 'smb_port' in vm_:
            smb_port = vm_['smb_port']

        smb_endpoint = azure.servicemanagement.ConfigurationSetInputEndpoint(
            name='SMB',
            protocol='TCP',
            port=smb_port,
            local_port=smb_port,
        )

        network_config.input_endpoints.input_endpoints.append(smb_endpoint)

        # Domain and WinRM configuration not yet supported by Salt Cloud
        system_config.domain_join = None
        system_config.win_rm = None

    else:
        system_config = azure.servicemanagement.LinuxConfigurationSet(
            host_name=vm_['name'],
            user_name=vm_['ssh_username'],
            user_password=vm_['ssh_password'],
            disable_ssh_password_authentication=False,
        )

    # TODO: Might need to create a storage account
    media_link = vm_['media_link']
    # TODO: Probably better to use more than just the name in the media_link
    media_link += '/{0}.vhd'.format(vm_['name'])
    os_hd = azure.servicemanagement.OSVirtualHardDisk(vm_['image'], media_link)

    vm_kwargs = {
        'service_name': service_name,
        'deployment_name': service_name,
        'deployment_slot': vm_['slot'],
        'label': label,
        'role_name': vm_['name'],
        'system_config': system_config,
        'os_virtual_hard_disk': os_hd,
        'role_size': vm_['size'],
        'network_config': network_config,
    }

    if 'virtual_network_name' in vm_:
        vm_kwargs['virtual_network_name'] = vm_['virtual_network_name']
        if 'subnet_name' in vm_:
            network_config.subnet_names.append(vm_['subnet_name'])

    log.debug('vm_kwargs: %s', vm_kwargs)

    event_kwargs = {'service_kwargs': service_kwargs.copy(),
                    'vm_kwargs': vm_kwargs.copy()}
    del event_kwargs['vm_kwargs']['system_config']
    del event_kwargs['vm_kwargs']['os_virtual_hard_disk']
    del event_kwargs['vm_kwargs']['network_config']
    __utils__['cloud.fire_event'](
        'event',
        'requesting instance',
        'salt/cloud/{0}/requesting'.format(vm_['name']),
        args=__utils__['cloud.filter_event']('requesting', event_kwargs, list(event_kwargs)),
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )
    log.debug('vm_kwargs: %s', vm_kwargs)

    # Azure lets you open winrm on a new VM
    # Can open up specific ports in Azure; but not on Windows
    try:
        conn.create_hosted_service(**service_kwargs)
    except AzureConflictHttpError:
        log.debug('Cloud service already exists')
    except Exception as exc:
        error = 'The hosted service name is invalid.'
        if error in six.text_type(exc):
            log.error(
                'Error creating %s on Azure.\n\n'
                'The hosted service name is invalid. The name can contain '
                'only letters, numbers, and hyphens. The name must start with '
                'a letter and must end with a letter or a number.',
                vm_['name'],
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG
            )
        else:
            log.error(
                'Error creating %s on Azure\n\n'
                'The following exception was thrown when trying to '
                'run the initial deployment: \n%s',
                vm_['name'], exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG
            )
        return False
    try:
        result = conn.create_virtual_machine_deployment(**vm_kwargs)
        log.debug('Request ID for machine: %s', result.request_id)
        _wait_for_async(conn, result.request_id)
    except AzureConflictHttpError:
        log.debug('Conflict error. The deployment may already exist, trying add_role')
        # Deleting two useless keywords
        del vm_kwargs['deployment_slot']
        del vm_kwargs['label']
        del vm_kwargs['virtual_network_name']
        result = conn.add_role(**vm_kwargs)
        _wait_for_async(conn, result.request_id)
    except Exception as exc:
        error = 'The hosted service name is invalid.'
        if error in six.text_type(exc):
            log.error(
                'Error creating %s on Azure.\n\n'
                'The VM name is invalid. The name can contain '
                'only letters, numbers, and hyphens. The name must start with '
                'a letter and must end with a letter or a number.',
                vm_['name'],
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG
            )
        else:
            log.error(
                'Error creating %s on Azure.\n\n'
                'The Virtual Machine could not be created. If you '
                'are using an already existing Cloud Service, '
                'make sure you set up the `port` variable corresponding '
                'to the SSH port exists and that the port number is not '
                'already in use.\nThe following exception was thrown when trying to '
                'run the initial deployment: \n%s',
                vm_['name'], exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG
            )
        return False

    def wait_for_hostname():
        '''
        Wait for the IP address to become available
        '''
        try:
            conn.get_role(service_name, service_name, vm_['name'])
            data = show_instance(vm_['name'], call='action')
            if 'url' in data and data['url'] != six.text_type(''):
                return data['url']
        except AzureMissingResourceHttpError:
            pass
        time.sleep(1)
        return False

    hostname = salt.utils.cloud.wait_for_fun(
        wait_for_hostname,
        timeout=config.get_cloud_config_value(
            'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
    )

    if not hostname:
        log.error('Failed to get a value for the hostname.')
        return False

    vm_['ssh_host'] = hostname.replace('http://', '').replace('/', '')
    vm_['password'] = config.get_cloud_config_value(
        'ssh_password', vm_, __opts__
    )
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)

    # Attaching volumes
    volumes = config.get_cloud_config_value(
        'volumes', vm_, __opts__, search_global=True
    )
    if volumes:
        __utils__['cloud.fire_event'](
            'event',
            'attaching volumes',
            'salt/cloud/{0}/attaching_volumes'.format(vm_['name']),
            args=__utils__['cloud.filter_event']('attaching_volumes', vm_, ['volumes']),
            sock_dir=__opts__['sock_dir'],
            transport=__opts__['transport']
        )

        log.info('Create and attach volumes to node %s', vm_['name'])
        created = create_attach_volumes(
            vm_['name'],
            {
                'volumes': volumes,
                'service_name': service_name,
                'deployment_name': vm_['name'],
                'media_link': media_link,
                'role_name': vm_['name'],
                'del_all_vols_on_destroy': vm_.get('set_del_all_vols_on_destroy', False)
            },
            call='action'
        )
        ret['Attached Volumes'] = created

    data = show_instance(vm_['name'], call='action')
    log.info('Created Cloud VM \'%s\'', vm_)
    log.debug('\'%s\' VM creation details:\n%s', vm_['name'], pprint.pformat(data))

    ret.update(data)

    __utils__['cloud.fire_event'](
        'event',
        'created instance',
        'salt/cloud/{0}/created'.format(vm_['name']),
        args=__utils__['cloud.filter_event']('created', vm_, ['name', 'profile', 'provider', 'driver']),
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )

    return ret


def create_attach_volumes(name, kwargs, call=None, wait_to_finish=True):
    '''
    Create and attach volumes to created node
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The create_attach_volumes action must be called with '
            '-a or --action.'
        )

    if kwargs is None:
        kwargs = {}

    if isinstance(kwargs['volumes'], six.string_types):
        volumes = salt.utils.yaml.safe_load(kwargs['volumes'])
    else:
        volumes = kwargs['volumes']

    # From the Azure .NET SDK doc
    #
    # The Create Data Disk operation adds a data disk to a virtual
    # machine. There are three ways to create the data disk using the
    # Add Data Disk operation.
    #    Option 1 - Attach an empty data disk to
    # the role by specifying the disk label and location of the disk
    # image. Do not include the DiskName and SourceMediaLink elements in
    # the request body. Include the MediaLink element and reference a
    # blob that is in the same geographical region as the role. You can
    # also omit the MediaLink element. In this usage, Azure will create
    # the data disk in the storage account configured as default for the
    # role.
    #    Option 2 - Attach an existing data disk that is in the image
    # repository. Do not include the DiskName and SourceMediaLink
    # elements in the request body. Specify the data disk to use by
    # including the DiskName element. Note: If included the in the
    # response body, the MediaLink and LogicalDiskSizeInGB elements are
    # ignored.
    #    Option 3 - Specify the location of a blob in your storage
    # account that contain a disk image to use. Include the
    # SourceMediaLink element. Note: If the MediaLink element
    # isincluded, it is ignored.  (see
    # http://msdn.microsoft.com/en-us/library/windowsazure/jj157199.aspx
    # for more information)
    #
    # Here only option 1 is implemented
    conn = get_conn()
    ret = []
    for volume in volumes:
        if "disk_name" in volume:
            log.error("You cannot specify a disk_name. Only new volumes are allowed")
            return False
        # Use the size keyword to set a size, but you can use the
        # azure name too. If neither is set, the disk has size 100GB
        volume.setdefault("logical_disk_size_in_gb", volume.get("size", 100))
        volume.setdefault("host_caching", "ReadOnly")
        volume.setdefault("lun", 0)
        # The media link is vm_name-disk-[0-15].vhd
        volume.setdefault("media_link",
                          kwargs["media_link"][:-4] + "-disk-{0}.vhd".format(volume["lun"]))
        volume.setdefault("disk_label",
                          kwargs["role_name"] + "-disk-{0}".format(volume["lun"]))
        volume_dict = {
            'volume_name': volume["lun"],
            'disk_label': volume["disk_label"]
        }

        # Preparing the volume dict to be passed with **
        kwargs_add_data_disk = ["lun", "host_caching", "media_link",
                                "disk_label", "disk_name",
                                "logical_disk_size_in_gb",
                                "source_media_link"]
        for key in set(volume.keys()) - set(kwargs_add_data_disk):
            del volume[key]

        attach = conn.add_data_disk(kwargs["service_name"], kwargs["deployment_name"], kwargs["role_name"],
                                    **volume)
        log.debug(attach)

        # If attach is None then everything is fine
        if attach:
            msg = (
                '{0} attached to {1} (aka {2})'.format(
                    volume_dict['volume_name'],
                    kwargs['role_name'],
                    name,
                )
            )
            log.info(msg)
            ret.append(msg)
        else:
            log.error('Error attaching %s on Azure', volume_dict)
    return ret


def create_attach_volumes(name, kwargs, call=None, wait_to_finish=True):
    '''
    Create and attach volumes to created node
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The create_attach_volumes action must be called with '
            '-a or --action.'
        )

    if kwargs is None:
        kwargs = {}

    if isinstance(kwargs['volumes'], six.string_types):
        volumes = salt.utils.yaml.safe_load(kwargs['volumes'])
    else:
        volumes = kwargs['volumes']

    # From the Azure .NET SDK doc
    #
    # The Create Data Disk operation adds a data disk to a virtual
    # machine. There are three ways to create the data disk using the
    # Add Data Disk operation.
    #    Option 1 - Attach an empty data disk to
    # the role by specifying the disk label and location of the disk
    # image. Do not include the DiskName and SourceMediaLink elements in
    # the request body. Include the MediaLink element and reference a
    # blob that is in the same geographical region as the role. You can
    # also omit the MediaLink element. In this usage, Azure will create
    # the data disk in the storage account configured as default for the
    # role.
    #    Option 2 - Attach an existing data disk that is in the image
    # repository. Do not include the DiskName and SourceMediaLink
    # elements in the request body. Specify the data disk to use by
    # including the DiskName element. Note: If included the in the
    # response body, the MediaLink and LogicalDiskSizeInGB elements are
    # ignored.
    #    Option 3 - Specify the location of a blob in your storage
    # account that contain a disk image to use. Include the
    # SourceMediaLink element. Note: If the MediaLink element
    # isincluded, it is ignored.  (see
    # http://msdn.microsoft.com/en-us/library/windowsazure/jj157199.aspx
    # for more information)
    #
    # Here only option 1 is implemented
    conn = get_conn()
    ret = []
    for volume in volumes:
        if "disk_name" in volume:
            log.error("You cannot specify a disk_name. Only new volumes are allowed")
            return False
        # Use the size keyword to set a size, but you can use the
        # azure name too. If neither is set, the disk has size 100GB
        volume.setdefault("logical_disk_size_in_gb", volume.get("size", 100))
        volume.setdefault("host_caching", "ReadOnly")
        volume.setdefault("lun", 0)
        # The media link is vm_name-disk-[0-15].vhd
        volume.setdefault("media_link",
                          kwargs["media_link"][:-4] + "-disk-{0}.vhd".format(volume["lun"]))
        volume.setdefault("disk_label",
                          kwargs["role_name"] + "-disk-{0}".format(volume["lun"]))
        volume_dict = {
            'volume_name': volume["lun"],
            'disk_label': volume["disk_label"]
        }

        # Preparing the volume dict to be passed with **
        kwargs_add_data_disk = ["lun", "host_caching", "media_link",
                                "disk_label", "disk_name",
                                "logical_disk_size_in_gb",
                                "source_media_link"]
        for key in set(volume.keys()) - set(kwargs_add_data_disk):
            del volume[key]

        result = conn.add_data_disk(kwargs["service_name"],
                                    kwargs["deployment_name"],
                                    kwargs["role_name"],
                                    **volume)
        _wait_for_async(conn, result.request_id)

        msg = (
                '{0} attached to {1} (aka {2})'.format(
                    volume_dict['volume_name'],
                    kwargs['role_name'],
                    name)
               )
        log.info(msg)
        ret.append(msg)
    return ret


# Helper function for azure tests
def _wait_for_async(conn, request_id):
    '''
    Helper function for azure tests
    '''
    count = 0
    log.debug('Waiting for asynchronous operation to complete')
    result = conn.get_operation_status(request_id)
    while result.status == 'InProgress':
        count = count + 1
        if count > 120:
            raise ValueError('Timed out waiting for asynchronous operation to complete.')
        time.sleep(5)
        result = conn.get_operation_status(request_id)

    if result.status != 'Succeeded':
        raise AzureException('Operation failed. {message} ({code})'
                                .format(message=result.error.message,
                                        code=result.error.code))


def destroy(name, conn=None, call=None, kwargs=None):
    '''
    Destroy a VM

    CLI Examples:

    .. code-block:: bash

        salt-cloud -d myminion
        salt-cloud -a destroy myminion service_name=myservice
    '''
    if call == 'function':
        raise SaltCloudSystemExit(
            'The destroy action must be called with -d, --destroy, '
            '-a or --action.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    instance_data = show_instance(name, call='action')
    service_name = instance_data['deployment']['name']
    disk_name = instance_data['role_info']['os_virtual_hard_disk']['disk_name']

    ret = {}
    # TODO: Add the ability to delete or not delete a hosted service when
    # deleting a VM
    try:
        log.debug('Deleting role')
        result = conn.delete_role(service_name, service_name, name)
        delete_type = 'delete_role'
    except AzureException:
        log.debug('Failed to delete role, deleting deployment')
        try:
            result = conn.delete_deployment(service_name, service_name)
        except AzureConflictHttpError as exc:
            log.error(exc.message)
            raise SaltCloudSystemExit('{0}: {1}'.format(name, exc.message))
        delete_type = 'delete_deployment'
    _wait_for_async(conn, result.request_id)
    ret[name] = {
        delete_type: {'request_id': result.request_id},
    }
    if __opts__.get('update_cachedir', False) is True:
        __utils__['cloud.delete_minion_cachedir'](name, __active_provider_name__.split(':')[0], __opts__)

    cleanup_disks = config.get_cloud_config_value(
        'cleanup_disks',
        get_configured_provider(), __opts__, search_global=False, default=False,
    )
    if cleanup_disks:
        cleanup_vhds = kwargs.get('delete_vhd', config.get_cloud_config_value(
            'cleanup_vhds',
            get_configured_provider(), __opts__, search_global=False, default=False,
        ))
        log.debug('Deleting disk %s', disk_name)
        if cleanup_vhds:
            log.debug('Deleting vhd')

        def wait_for_destroy():
            '''
            Wait for the VM to be deleted
            '''
            try:
                data = delete_disk(kwargs={'name': disk_name, 'delete_vhd': cleanup_vhds}, call='function')
                return data
            except AzureConflictHttpError:
                log.debug('Waiting for VM to be destroyed...')
            time.sleep(5)
            return False

        data = salt.utils.cloud.wait_for_fun(
            wait_for_destroy,
            timeout=config.get_cloud_config_value(
                'wait_for_fun_timeout', {}, __opts__, default=15 * 60),
        )
        ret[name]['delete_disk'] = {
            'name': disk_name,
            'delete_vhd': cleanup_vhds,
            'data': data
        }

        # Services can't be cleaned up unless disks are too
        cleanup_services = config.get_cloud_config_value(
            'cleanup_services',
            get_configured_provider(), __opts__, search_global=False, default=False
        )
        if cleanup_services:
            log.debug('Deleting service %s', service_name)

            def wait_for_disk_delete():
                '''
                Wait for the disk to be deleted
                '''
                try:
                    data = delete_service(kwargs={'name': service_name}, call='function')
                    return data
                except AzureConflictHttpError:
                    log.debug('Waiting for disk to be deleted...')
                time.sleep(5)
                return False

            data = salt.utils.cloud.wait_for_fun(
                wait_for_disk_delete,
                timeout=config.get_cloud_config_value(
                    'wait_for_fun_timeout', {}, __opts__, default=15 * 60),
            )
            ret[name]['delete_services'] = {
                'name': service_name,
                'data': data
            }

    return ret


def list_storage_services(conn=None, call=None):
    '''
    List VMs on this Azure account, with full information
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            ('The list_storage_services function must be called '
             'with -f or --function.')
        )

    if not conn:
        conn = get_conn()

    ret = {}
    accounts = conn.list_storage_accounts()
    for service in accounts.storage_services:
        ret[service.service_name] = {
            'capabilities': service.capabilities,
            'service_name': service.service_name,
            'storage_service_properties': service.storage_service_properties,
            'extended_properties': service.extended_properties,
            'storage_service_keys': service.storage_service_keys,
            'url': service.url,
        }
    return ret


def get_operation_status(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Get Operation Status, based on a request ID

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_operation_status my-azure id=0123456789abcdef0123456789abcdef
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_instance function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'id' not in kwargs:
        raise SaltCloudSystemExit('A request ID must be specified as "id"')

    if not conn:
        conn = get_conn()

    data = conn.get_operation_status(kwargs['id'])
    ret = {
        'http_status_code': data.http_status_code,
        'id': kwargs['id'],
        'status': data.status
    }
    if hasattr(data.error, 'code'):
        ret['error'] = {
            'code': data.error.code,
            'message': data.error.message,
        }

    return ret


def list_storage(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List storage accounts associated with the account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_storage my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_storage function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    data = conn.list_storage_accounts()
    pprint.pprint(dir(data))
    ret = {}
    for item in data.storage_services:
        ret[item.service_name] = object_to_dict(item)
    return ret


def show_storage(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List storage service properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_storage my-azure name=my_storage
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    data = conn.get_storage_account_properties(
        kwargs['name'],
    )
    return object_to_dict(data)


# To reflect the Azure API
get_storage = show_storage


def show_storage_keys(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show storage account keys

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_storage_keys my-azure name=my_storage
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage_keys function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    try:
        data = conn.get_storage_account_keys(
            kwargs['name'],
        )
    except AzureMissingResourceHttpError as exc:
        storage_data = show_storage(kwargs={'name': kwargs['name']}, call='function')
        if storage_data['storage_service_properties']['status'] == 'Creating':
            raise SaltCloudSystemExit('The storage account keys have not yet been created.')
        else:
            raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))
    return object_to_dict(data)


# To reflect the Azure API
get_storage_keys = show_storage_keys


def create_storage(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Create a new storage account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_storage my-azure name=my_storage label=my_storage location='West US'
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if not conn:
        conn = get_conn()

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'description' not in kwargs:
        raise SaltCloudSystemExit('A description must be specified as "description"')

    if 'label' not in kwargs:
        raise SaltCloudSystemExit('A label must be specified as "label"')

    if 'location' not in kwargs and 'affinity_group' not in kwargs:
        raise SaltCloudSystemExit('Either a location or an affinity_group '
                                  'must be specified (but not both)')

    try:
        data = conn.create_storage_account(
            service_name=kwargs['name'],
            label=kwargs['label'],
            description=kwargs.get('description', None),
            location=kwargs.get('location', None),
            affinity_group=kwargs.get('affinity_group', None),
            extended_properties=kwargs.get('extended_properties', None),
            geo_replication_enabled=kwargs.get('geo_replication_enabled', None),
            account_type=kwargs.get('account_type', 'Standard_GRS'),
        )
        return {'Success': 'The storage account was successfully created'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the storage account already exists.')


def update_storage(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Update a storage account's properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f update_storage my-azure name=my_storage label=my_storage
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    data = conn.update_storage_account(
        service_name=kwargs['name'],
        label=kwargs.get('label', None),
        description=kwargs.get('description', None),
        extended_properties=kwargs.get('extended_properties', None),
        geo_replication_enabled=kwargs.get('geo_replication_enabled', None),
        account_type=kwargs.get('account_type', 'Standard_GRS'),
    )
    return show_storage(kwargs={'name': kwargs['name']}, call='function')


def regenerate_storage_keys(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Regenerate storage account keys. Requires a key_type ("primary" or
    "secondary") to be specified.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f regenerate_storage_keys my-azure name=my_storage key_type=primary
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'key_type' not in kwargs or kwargs['key_type'] not in ('primary', 'secondary'):
        raise SaltCloudSystemExit('A key_type must be specified ("primary" or "secondary")')

    try:
        data = conn.regenerate_storage_account_keys(
            service_name=kwargs['name'],
            key_type=kwargs['key_type'],
        )
        return show_storage_keys(kwargs={'name': kwargs['name']}, call='function')
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the storage account already exists.')


def delete_storage(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific storage account

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_storage my-azure name=my_storage
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_storage function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if not conn:
        conn = get_conn()

    try:
        data = conn.delete_storage_account(kwargs['name'])
        return {'Success': 'The storage account was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))


def list_services(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List hosted services associated with the account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_services my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_services function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    data = conn.list_hosted_services()
    ret = {}
    for item in data.hosted_services:
        ret[item.service_name] = object_to_dict(item)
        ret[item.service_name]['name'] = item.service_name
    return ret


def show_service(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List hosted service properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_service my-azure name=my_service
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_service function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    data = conn.get_hosted_service_properties(
        kwargs['name'],
        kwargs.get('details', False)
    )
    ret = object_to_dict(data)
    return ret


def create_service(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Create a new hosted service

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_service my-azure name=my_service label=my_service location='West US'
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The create_service function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'label' not in kwargs:
        raise SaltCloudSystemExit('A label must be specified as "label"')

    if 'location' not in kwargs and 'affinity_group' not in kwargs:
        raise SaltCloudSystemExit('Either a location or an affinity_group '
                                  'must be specified (but not both)')

    try:
        data = conn.create_hosted_service(
            kwargs['name'],
            kwargs['label'],
            kwargs.get('description', None),
            kwargs.get('location', None),
            kwargs.get('affinity_group', None),
            kwargs.get('extended_properties', None),
        )
        return {'Success': 'The service was successfully created'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the service already exists.')


def delete_service(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific service associated with the account

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_service my-azure name=my_service
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_service function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if not conn:
        conn = get_conn()

    try:
        conn.delete_hosted_service(kwargs['name'])
        return {'Success': 'The service was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))


def list_disks(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List disks associated with the account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_disks my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_disks function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    data = conn.list_disks()
    ret = {}
    for item in data.disks:
        ret[item.name] = object_to_dict(item)
    return ret


def show_disk(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Return information about a disk

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_disk my-azure name=my_disk
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The get_disk function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    data = conn.get_disk(kwargs['name'])
    return object_to_dict(data)


# For consistency with Azure SDK
get_disk = show_disk


def cleanup_unattached_disks(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Cleans up all disks associated with the account, which are not attached.
    *** CAUTION *** This is a destructive function with no undo button, and no
    "Are you sure?" confirmation!

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f cleanup_unattached_disks my-azure name=my_disk
        salt-cloud -f cleanup_unattached_disks my-azure name=my_disk delete_vhd=True
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_disk function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    disks = list_disks(kwargs=kwargs, conn=conn, call='function')
    for disk in disks:
        if disks[disk]['attached_to'] is None:
            del_kwargs = {
                'name': disks[disk]['name'],
                'delete_vhd': kwargs.get('delete_vhd', False)
            }
            log.info(
                'Deleting disk %s, deleting VHD: %s',
                del_kwargs['name'], del_kwargs['delete_vhd']
            )
            data = delete_disk(kwargs=del_kwargs, call='function')
    return True


def delete_disk(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific disk associated with the account

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_disk my-azure name=my_disk
        salt-cloud -f delete_disk my-azure name=my_disk delete_vhd=True
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_disk function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if not conn:
        conn = get_conn()

    try:
        data = conn.delete_disk(kwargs['name'], kwargs.get('delete_vhd', False))
        return {'Success': 'The disk was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))


def update_disk(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Update a disk's properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f update_disk my-azure name=my_disk label=my_disk
        salt-cloud -f update_disk my-azure name=my_disk new_name=another_disk
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_disk function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    old_data = show_disk(kwargs={'name': kwargs['name']}, call='function')
    data = conn.update_disk(
        disk_name=kwargs['name'],
        has_operating_system=kwargs.get('has_operating_system', old_data['has_operating_system']),
        label=kwargs.get('label', old_data['label']),
        media_link=kwargs.get('media_link', old_data['media_link']),
        name=kwargs.get('new_name', old_data['name']),
        os=kwargs.get('os', old_data['os']),
    )
    return show_disk(kwargs={'name': kwargs['name']}, call='function')


def list_service_certificates(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List certificates associated with the service

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_service_certificates my-azure name=my_service
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_service_certificates function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A service name must be specified as "name"')

    if not conn:
        conn = get_conn()

    data = conn.list_service_certificates(service_name=kwargs['name'])
    ret = {}
    for item in data.certificates:
        ret[item.thumbprint] = object_to_dict(item)
    return ret


def show_service_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Return information about a service certificate

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_service_certificate my-azure name=my_service_certificate \\
            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The get_service_certificate function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A service name must be specified as "name"')

    if 'thumbalgorithm' not in kwargs:
        raise SaltCloudSystemExit('A thumbalgorithm must be specified as "thumbalgorithm"')

    if 'thumbprint' not in kwargs:
        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')

    data = conn.get_service_certificate(
        kwargs['name'],
        kwargs['thumbalgorithm'],
        kwargs['thumbprint'],
    )
    return object_to_dict(data)


# For consistency with Azure SDK
get_service_certificate = show_service_certificate


def add_service_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Add a new service certificate

    CLI Example:

    .. code-block:: bash

        salt-cloud -f add_service_certificate my-azure name=my_service_certificate \\
            data='...CERT_DATA...' certificate_format=sha1 password=verybadpass
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The add_service_certificate function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'data' not in kwargs:
        raise SaltCloudSystemExit('Certificate data must be specified as "data"')

    if 'certificate_format' not in kwargs:
        raise SaltCloudSystemExit('A certificate_format must be specified as "certificate_format"')

    if 'password' not in kwargs:
        raise SaltCloudSystemExit('A password must be specified as "password"')

    try:
        data = conn.add_service_certificate(
            kwargs['name'],
            kwargs['data'],
            kwargs['certificate_format'],
            kwargs['password'],
        )
        return {'Success': 'The service certificate was successfully added'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the '
                                  'service certificate already exists.')


def delete_service_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific certificate associated with the service

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_service_certificate my-azure name=my_service_certificate \\
            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_service_certificate function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'thumbalgorithm' not in kwargs:
        raise SaltCloudSystemExit('A thumbalgorithm must be specified as "thumbalgorithm"')

    if 'thumbprint' not in kwargs:
        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')

    if not conn:
        conn = get_conn()

    try:
        data = conn.delete_service_certificate(
            kwargs['name'],
            kwargs['thumbalgorithm'],
            kwargs['thumbprint'],
        )
        return {'Success': 'The service certificate was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))


def list_management_certificates(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List management certificates associated with the subscription

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_management_certificates my-azure name=my_management
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_management_certificates function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    data = conn.list_management_certificates()
    ret = {}
    for item in data.subscription_certificates:
        ret[item.subscription_certificate_thumbprint] = object_to_dict(item)
    return ret


def show_management_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Return information about a management_certificate

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_management_certificate my-azure name=my_management_certificate \\
            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The get_management_certificate function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'thumbprint' not in kwargs:
        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')

    data = conn.get_management_certificate(kwargs['thumbprint'])
    return object_to_dict(data)


# For consistency with Azure SDK
get_management_certificate = show_management_certificate


def add_management_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Add a new management certificate

    CLI Example:

    .. code-block:: bash

        salt-cloud -f add_management_certificate my-azure public_key='...PUBKEY...' \\
            thumbprint=0123456789ABCDEF data='...CERT_DATA...'
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The add_management_certificate function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'public_key' not in kwargs:
        raise SaltCloudSystemExit('A public_key must be specified as "public_key"')

    if 'thumbprint' not in kwargs:
        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')

    if 'data' not in kwargs:
        raise SaltCloudSystemExit('Certificate data must be specified as "data"')

    try:
        conn.add_management_certificate(
            kwargs['name'],
            kwargs['thumbprint'],
            kwargs['data'],
        )
        return {'Success': 'The management certificate was successfully added'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. '
                                  'This usually means that the management certificate already exists.')


def delete_management_certificate(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific certificate associated with the management

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_management_certificate my-azure name=my_management_certificate \\
            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_management_certificate function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'thumbprint' not in kwargs:
        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')

    if not conn:
        conn = get_conn()

    try:
        conn.delete_management_certificate(kwargs['thumbprint'])
        return {'Success': 'The management certificate was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['thumbprint'], exc.message))


def list_virtual_networks(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List input endpoints associated with the deployment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_virtual_networks my-azure service=myservice deployment=mydeployment
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_virtual_networks function must be called with -f or --function.'
        )

    path = 'services/networking/virtualnetwork'
    data = query(path)
    return data


def list_input_endpoints(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List input endpoints associated with the deployment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_input_endpoints my-azure service=myservice deployment=mydeployment
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_input_endpoints function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'service' not in kwargs:
        raise SaltCloudSystemExit('A service name must be specified as "service"')

    if 'deployment' not in kwargs:
        raise SaltCloudSystemExit('A deployment name must be specified as "deployment"')

    path = 'services/hostedservices/{0}/deployments/{1}'.format(
        kwargs['service'],
        kwargs['deployment'],
    )

    data = query(path)
    if data is None:
        raise SaltCloudSystemExit(
            'There was an error listing endpoints with the {0} service on the {1} deployment.'.format(
                kwargs['service'],
                kwargs['deployment']
            )
        )

    ret = {}
    for item in data:
        if 'Role' in item:
            role = item['Role']
            if not isinstance(role, dict):
                return ret
            input_endpoint = role['ConfigurationSets']['ConfigurationSet'].get('InputEndpoints', {}).get('InputEndpoint')
            if not input_endpoint:
                continue
            if not isinstance(input_endpoint, list):
                input_endpoint = [input_endpoint]
            for endpoint in input_endpoint:
                ret[endpoint['Name']] = endpoint
            return ret
    return ret


def show_input_endpoint(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show an input endpoint associated with the deployment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_input_endpoint my-azure service=myservice \\
            deployment=mydeployment name=SSH
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_input_endpoint function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An endpoint name must be specified as "name"')

    data = list_input_endpoints(kwargs=kwargs, call='function')
    return data.get(kwargs['name'], None)


# For consistency with Azure SDK
get_input_endpoint = show_input_endpoint


def update_input_endpoint(kwargs=None, conn=None, call=None, activity='update'):
    '''
    .. versionadded:: 2015.8.0

    Update an input endpoint associated with the deployment. Please note that
    there may be a delay before the changes show up.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f update_input_endpoint my-azure service=myservice \\
            deployment=mydeployment role=myrole name=HTTP local_port=80 \\
            port=80 protocol=tcp enable_direct_server_return=False \\
            timeout_for_tcp_idle_connection=4
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The update_input_endpoint function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'service' not in kwargs:
        raise SaltCloudSystemExit('A service name must be specified as "service"')

    if 'deployment' not in kwargs:
        raise SaltCloudSystemExit('A deployment name must be specified as "deployment"')

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An endpoint name must be specified as "name"')

    if 'role' not in kwargs:
        raise SaltCloudSystemExit('An role name must be specified as "role"')

    if activity != 'delete':
        if 'port' not in kwargs:
            raise SaltCloudSystemExit('An endpoint port must be specified as "port"')

        if 'protocol' not in kwargs:
            raise SaltCloudSystemExit('An endpoint protocol (tcp or udp) must be specified as "protocol"')

        if 'local_port' not in kwargs:
            kwargs['local_port'] = kwargs['port']

        if 'enable_direct_server_return' not in kwargs:
            kwargs['enable_direct_server_return'] = False
        kwargs['enable_direct_server_return'] = six.text_type(kwargs['enable_direct_server_return']).lower()

        if 'timeout_for_tcp_idle_connection' not in kwargs:
            kwargs['timeout_for_tcp_idle_connection'] = 4

    old_endpoints = list_input_endpoints(kwargs, call='function')

    endpoints_xml = ''
    endpoint_xml = '''
        <InputEndpoint>
          <LocalPort>{local_port}</LocalPort>
          <Name>{name}</Name>
          <Port>{port}</Port>
          <Protocol>{protocol}</Protocol>
          <EnableDirectServerReturn>{enable_direct_server_return}</EnableDirectServerReturn>
          <IdleTimeoutInMinutes>{timeout_for_tcp_idle_connection}</IdleTimeoutInMinutes>
        </InputEndpoint>'''

    if activity == 'add':
        old_endpoints[kwargs['name']] = kwargs
        old_endpoints[kwargs['name']]['Name'] = kwargs['name']

    for endpoint in old_endpoints:
        if old_endpoints[endpoint]['Name'] == kwargs['name']:
            if activity != 'delete':
                this_endpoint_xml = endpoint_xml.format(**kwargs)
                endpoints_xml += this_endpoint_xml
        else:
            this_endpoint_xml = endpoint_xml.format(
                local_port=old_endpoints[endpoint]['LocalPort'],
                name=old_endpoints[endpoint]['Name'],
                port=old_endpoints[endpoint]['Port'],
                protocol=old_endpoints[endpoint]['Protocol'],
                enable_direct_server_return=old_endpoints[endpoint]['EnableDirectServerReturn'],
                timeout_for_tcp_idle_connection=old_endpoints[endpoint].get('IdleTimeoutInMinutes', 4),
            )
            endpoints_xml += this_endpoint_xml

    request_xml = '''<PersistentVMRole xmlns="http://schemas.microsoft.com/windowsazure"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <ConfigurationSets>
    <ConfigurationSet>
      <ConfigurationSetType>NetworkConfiguration</ConfigurationSetType>
      <InputEndpoints>{0}
      </InputEndpoints>
    </ConfigurationSet>
  </ConfigurationSets>
  <OSVirtualHardDisk>
  </OSVirtualHardDisk>
</PersistentVMRole>'''.format(endpoints_xml)

    path = 'services/hostedservices/{0}/deployments/{1}/roles/{2}'.format(
        kwargs['service'],
        kwargs['deployment'],
        kwargs['role'],
    )
    query(
        path=path,
        method='PUT',
        header_dict={'Content-Type': 'application/xml'},
        data=request_xml,
        decode=False,
    )
    return True


def add_input_endpoint(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Add an input endpoint to the deployment. Please note that
    there may be a delay before the changes show up.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f add_input_endpoint my-azure service=myservice \\
            deployment=mydeployment role=myrole name=HTTP local_port=80 \\
            port=80 protocol=tcp enable_direct_server_return=False \\
            timeout_for_tcp_idle_connection=4
    '''
    return update_input_endpoint(
        kwargs=kwargs,
        conn=conn,
        call='function',
        activity='add',
    )


def delete_input_endpoint(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete an input endpoint from the deployment. Please note that
    there may be a delay before the changes show up.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f delete_input_endpoint my-azure service=myservice \\
            deployment=mydeployment role=myrole name=HTTP
    '''
    return update_input_endpoint(
        kwargs=kwargs,
        conn=conn,
        call='function',
        activity='delete',
    )


def show_deployment(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Return information about a deployment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_deployment my-azure name=my_deployment
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The get_deployment function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'service_name' not in kwargs:
        raise SaltCloudSystemExit('A service name must be specified as "service_name"')

    if 'deployment_name' not in kwargs:
        raise SaltCloudSystemExit('A deployment name must be specified as "deployment_name"')

    data = conn.get_deployment_by_name(
        service_name=kwargs['service_name'],
        deployment_name=kwargs['deployment_name'],
    )
    return object_to_dict(data)


# For consistency with Azure SDK
get_deployment = show_deployment


def list_affinity_groups(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List input endpoints associated with the deployment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_affinity_groups my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_affinity_groups function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    data = conn.list_affinity_groups()
    ret = {}
    for item in data.affinity_groups:
        ret[item.name] = object_to_dict(item)
    return ret


def show_affinity_group(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show an affinity group associated with the account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_affinity_group my-azure service=myservice \\
            deployment=mydeployment name=SSH
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_affinity_group function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An affinity group name must be specified as "name"')

    data = conn.get_affinity_group_properties(affinity_group_name=kwargs['name'])
    return object_to_dict(data)


# For consistency with Azure SDK
get_affinity_group = show_affinity_group


def create_affinity_group(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Create a new affinity group

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_affinity_group my-azure name=my_affinity_group
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The create_affinity_group function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'label' not in kwargs:
        raise SaltCloudSystemExit('A label must be specified as "label"')

    if 'location' not in kwargs:
        raise SaltCloudSystemExit('A location must be specified as "location"')

    try:
        conn.create_affinity_group(
            kwargs['name'],
            kwargs['label'],
            kwargs['location'],
            kwargs.get('description', None),
        )
        return {'Success': 'The affinity group was successfully created'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the affinity group already exists.')


def update_affinity_group(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Update an affinity group's properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f update_affinity_group my-azure name=my_group label=my_group
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The update_affinity_group function must be called with -f or --function.'
        )

    if not conn:
        conn = get_conn()

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if 'label' not in kwargs:
        raise SaltCloudSystemExit('A label must be specified as "label"')

    conn.update_affinity_group(
        affinity_group_name=kwargs['name'],
        label=kwargs['label'],
        description=kwargs.get('description', None),
    )
    return show_affinity_group(kwargs={'name': kwargs['name']}, call='function')


def delete_affinity_group(kwargs=None, conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a specific affinity group associated with the account

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_affinity_group my-azure name=my_affinity_group
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_affinity_group function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('A name must be specified as "name"')

    if not conn:
        conn = get_conn()

    try:
        conn.delete_affinity_group(kwargs['name'])
        return {'Success': 'The affinity group was successfully deleted'}
    except AzureMissingResourceHttpError as exc:
        raise SaltCloudSystemExit('{0}: {1}'.format(kwargs['name'], exc.message))


def get_storage_conn(storage_account=None, storage_key=None, conn_kwargs=None):
    '''
    .. versionadded:: 2015.8.0

    Return a storage_conn object for the storage account
    '''
    if conn_kwargs is None:
        conn_kwargs = {}

    if not storage_account:
        storage_account = config.get_cloud_config_value(
            'storage_account',
            get_configured_provider(), __opts__, search_global=False,
            default=conn_kwargs.get('storage_account', None)
        )
    if not storage_key:
        storage_key = config.get_cloud_config_value(
            'storage_key',
            get_configured_provider(), __opts__, search_global=False,
            default=conn_kwargs.get('storage_key', None)
        )
    return azure.storage.BlobService(storage_account, storage_key)


def make_blob_url(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Creates the URL to access a blob

    CLI Example:

    .. code-block:: bash

        salt-cloud -f make_blob_url my-azure container=mycontainer blob=myblob

    container:
        Name of the container.
    blob:
        Name of the blob.
    account:
        Name of the storage account. If not specified, derives the host base
        from the provider configuration.
    protocol:
        Protocol to use: 'http' or 'https'. If not specified, derives the host
        base from the provider configuration.
    host_base:
        Live host base URL.  If not specified, derives the host base from the
        provider configuration.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The make_blob_url function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('A container name must be specified as "container"')

    if 'blob' not in kwargs:
        raise SaltCloudSystemExit('A blob name must be specified as "blob"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.make_blob_url(
        kwargs['container'],
        kwargs['blob'],
        kwargs.get('account', None),
        kwargs.get('protocol', None),
        kwargs.get('host_base', None),
    )
    ret = {}
    for item in data.containers:
        ret[item.name] = object_to_dict(item)
    return ret


def list_storage_containers(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List containers associated with the storage account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_storage_containers my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_storage_containers function must be called with -f or --function.'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.list_containers()
    ret = {}
    for item in data.containers:
        ret[item.name] = object_to_dict(item)
    return ret


def create_storage_container(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Create a storage container

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_storage_container my-azure name=mycontainer

    name:
        Name of container to create.
    meta_name_values:
        Optional. A dict with name_value pairs to associate with the
        container as metadata. Example:{'Category':'test'}
    blob_public_access:
        Optional. Possible values include: container, blob
    fail_on_exist:
        Specify whether to throw an exception when the container exists.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The create_storage_container function must be called with -f or --function.'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    try:
        storage_conn.create_container(
            container_name=kwargs['name'],
            x_ms_meta_name_values=kwargs.get('meta_name_values', None),
            x_ms_blob_public_access=kwargs.get('blob_public_access', None),
            fail_on_exist=kwargs.get('fail_on_exist', False),
        )
        return {'Success': 'The storage container was successfully created'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict. This usually means that the storage container already exists.')


def show_storage_container(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show a container associated with the storage account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_storage_container my-azure name=myservice

    name:
        Name of container to show.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_container_properties(
        container_name=kwargs['name'],
        x_ms_lease_id=kwargs.get('lease_id', None),
    )
    return data


# For consistency with Azure SDK
get_storage_container = show_storage_container


def show_storage_container_metadata(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show a storage container's metadata

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_storage_container_metadata my-azure name=myservice

    name:
        Name of container to show.
    lease_id:
        If specified, show_storage_container_metadata only succeeds if the
        container's lease is active and matches this ID.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_container_metadata(
        container_name=kwargs['name'],
        x_ms_lease_id=kwargs.get('lease_id', None),
    )
    return data


# For consistency with Azure SDK
get_storage_container_metadata = show_storage_container_metadata


def set_storage_container_metadata(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Set a storage container's metadata

    CLI Example:

    .. code-block:: bash

        salt-cloud -f set_storage_container my-azure name=mycontainer \\
            x_ms_meta_name_values='{"my_name": "my_value"}'

    name:
        Name of existing container.
    meta_name_values:
        A dict containing name, value for metadata.
        Example: {'category':'test'}
    lease_id:
        If specified, set_storage_container_metadata only succeeds if the
        container's lease is active and matches this ID.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The create_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    x_ms_meta_name_values = salt.utils.yaml.safe_load(
        kwargs.get('meta_name_values', '')
    )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    try:
        storage_conn.set_container_metadata(
            container_name=kwargs['name'],
            x_ms_meta_name_values=x_ms_meta_name_values,
            x_ms_lease_id=kwargs.get('lease_id', None),
        )
        return {'Success': 'The storage container was successfully updated'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict.')


def show_storage_container_acl(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show a storage container's acl

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_storage_container_acl my-azure name=myservice

    name:
        Name of existing container.
    lease_id:
        If specified, show_storage_container_acl only succeeds if the
        container's lease is active and matches this ID.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_container_acl(
        container_name=kwargs['name'],
        x_ms_lease_id=kwargs.get('lease_id', None),
    )
    return data


# For consistency with Azure SDK
get_storage_container_acl = show_storage_container_acl


def set_storage_container_acl(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Set a storage container's acl

    CLI Example:

    .. code-block:: bash

        salt-cloud -f set_storage_container my-azure name=mycontainer

    name:
        Name of existing container.
    signed_identifiers:
        SignedIdentifers instance
    blob_public_access:
        Optional. Possible values include: container, blob
    lease_id:
        If specified, set_storage_container_acl only succeeds if the
        container's lease is active and matches this ID.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The create_storage_container function must be called with -f or --function.'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    try:
        data = storage_conn.set_container_acl(
            container_name=kwargs['name'],
            signed_identifiers=kwargs.get('signed_identifiers', None),
            x_ms_blob_public_access=kwargs.get('blob_public_access', None),
            x_ms_lease_id=kwargs.get('lease_id', None),
        )
        return {'Success': 'The storage container was successfully updated'}
    except AzureConflictHttpError:
        raise SaltCloudSystemExit('There was a conflict.')


def delete_storage_container(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Delete a container associated with the storage account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f delete_storage_container my-azure name=mycontainer

    name:
        Name of container to create.
    fail_not_exist:
        Specify whether to throw an exception when the container exists.
    lease_id:
        If specified, delete_storage_container only succeeds if the
        container's lease is active and matches this ID.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The delete_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.delete_container(
        container_name=kwargs['name'],
        fail_not_exist=kwargs.get('fail_not_exist', None),
        x_ms_lease_id=kwargs.get('lease_id', None),
    )
    return data


def lease_storage_container(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Lease a container associated with the storage account

    CLI Example:

    .. code-block:: bash

        salt-cloud -f lease_storage_container my-azure name=mycontainer

    name:
        Name of container to create.
    lease_action:
        Required. Possible values: acquire|renew|release|break|change
    lease_id:
        Required if the container has an active lease.
    lease_duration:
        Specifies the duration of the lease, in seconds, or negative one
        (-1) for a lease that never expires. A non-infinite lease can be
        between 15 and 60 seconds. A lease duration cannot be changed
        using renew or change. For backwards compatibility, the default is
        60, and the value is only used on an acquire operation.
    lease_break_period:
        Optional. For a break operation, this is the proposed duration of
        seconds that the lease should continue before it is broken, between
        0 and 60 seconds. This break period is only used if it is shorter
        than the time remaining on the lease. If longer, the time remaining
        on the lease is used. A new lease will not be available before the
        break period has expired, but the lease may be held for longer than
        the break period. If this header does not appear with a break
        operation, a fixed-duration lease breaks after the remaining lease
        period elapses, and an infinite lease breaks immediately.
    proposed_lease_id:
        Optional for acquire, required for change. Proposed lease ID, in a
        GUID string format.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The lease_storage_container function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "name"')

    lease_actions = ('acquire', 'renew', 'release', 'break', 'change')

    if kwargs.get('lease_action', None) not in lease_actions:
        raise SaltCloudSystemExit(
            'A lease_action must be one of: {0}'.format(
                ', '.join(lease_actions)
            )
        )

    if kwargs['lease_action'] != 'acquire' and 'lease_id' not in kwargs:
        raise SaltCloudSystemExit(
            'A lease ID must be specified for the "{0}" lease action '
            'as "lease_id"'.format(kwargs['lease_action'])
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.lease_container(
        container_name=kwargs['name'],
        x_ms_lease_action=kwargs['lease_action'],
        x_ms_lease_id=kwargs.get('lease_id', None),
        x_ms_lease_duration=kwargs.get('lease_duration', 60),
        x_ms_lease_break_period=kwargs.get('lease_break_period', None),
        x_ms_proposed_lease_id=kwargs.get('proposed_lease_id', None),
    )

    return data


def list_blobs(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    List blobs associated with the container

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_blobs my-azure container=mycontainer

    container:
        The name of the storage container
    prefix:
        Optional. Filters the results to return only blobs whose names
        begin with the specified prefix.
    marker:
        Optional. A string value that identifies the portion of the list
        to be returned with the next list operation. The operation returns
        a marker value within the response body if the list returned was
        not complete. The marker value may then be used in a subsequent
        call to request the next set of list items. The marker value is
        opaque to the client.
    maxresults:
        Optional. Specifies the maximum number of blobs to return,
        including all BlobPrefix elements. If the request does not specify
        maxresults or specifies a value greater than 5,000, the server will
        return up to 5,000 items. Setting maxresults to a value less than
        or equal to zero results in error response code 400 (Bad Request).
    include:
        Optional. Specifies one or more datasets to include in the
        response. To specify more than one of these options on the URI,
        you must separate each option with a comma. Valid values are:

        snapshots:
            Specifies that snapshots should be included in the
            enumeration. Snapshots are listed from oldest to newest in
            the response.
        metadata:
            Specifies that blob metadata be returned in the response.
        uncommittedblobs:
            Specifies that blobs for which blocks have been uploaded,
            but which have not been committed using Put Block List
            (REST API), be included in the response.
        copy:
            Version 2012-02-12 and newer. Specifies that metadata
            related to any current or previous Copy Blob operation
            should be included in the response.
    delimiter:
        Optional. When the request includes this parameter, the operation
        returns a BlobPrefix element in the response body that acts as a
        placeholder for all blobs whose names begin with the same
        substring up to the appearance of the delimiter character. The
        delimiter may be a single character or a string.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_blobs function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('An storage container name must be specified as "container"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    return salt.utils.msazure.list_blobs(storage_conn=storage_conn, **kwargs)


def show_blob_service_properties(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Show a blob's service properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_blob_service_properties my-azure
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_blob_service_properties function must be called with -f or --function.'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_blob_service_properties(
        timeout=kwargs.get('timeout', None),
    )
    return data


# For consistency with Azure SDK
get_blob_service_properties = show_blob_service_properties


def set_blob_service_properties(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Sets the properties of a storage account's Blob service, including
    Windows Azure Storage Analytics. You can also use this operation to
    set the default request version for all incoming requests that do not
    have a version specified.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f set_blob_service_properties my-azure

    properties:
        a StorageServiceProperties object.
    timeout:
        Optional. The timeout parameter is expressed in seconds.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The set_blob_service_properties function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'properties' not in kwargs:
        raise SaltCloudSystemExit('The blob service properties name must be specified as "properties"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_blob_service_properties(
        storage_service_properties=kwargs['properties'],
        timeout=kwargs.get('timeout', None),
    )
    return data


def show_blob_properties(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Returns all user-defined metadata, standard HTTP properties, and
    system properties for the blob.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_blob_properties my-azure container=mycontainer blob=myblob

    container:
        Name of existing container.
    blob:
        Name of existing blob.
    lease_id:
        Required if the blob has an active lease.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_blob_properties function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('The container name must be specified as "container"')

    if 'blob' not in kwargs:
        raise SaltCloudSystemExit('The blob name must be specified as "blob"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    try:
        data = storage_conn.get_blob_properties(
            container_name=kwargs['container'],
            blob_name=kwargs['blob'],
            x_ms_lease_id=kwargs.get('lease_id', None),
        )
    except AzureMissingResourceHttpError:
        raise SaltCloudSystemExit('The specified blob does not exist.')

    return data


# For consistency with Azure SDK
get_blob_properties = show_blob_properties


def set_blob_properties(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Set a blob's properties

    CLI Example:

    .. code-block:: bash

        salt-cloud -f set_blob_properties my-azure

    container:
        Name of existing container.
    blob:
        Name of existing blob.
    blob_cache_control:
        Optional. Modifies the cache control string for the blob.
    blob_content_type:
        Optional. Sets the blob's content type.
    blob_content_md5:
        Optional. Sets the blob's MD5 hash.
    blob_content_encoding:
        Optional. Sets the blob's content encoding.
    blob_content_language:
        Optional. Sets the blob's content language.
    lease_id:
        Required if the blob has an active lease.
    blob_content_disposition:
        Optional. Sets the blob's Content-Disposition header.
        The Content-Disposition response header field conveys additional
        information about how to process the response payload, and also can
        be used to attach additional metadata. For example, if set to
        attachment, it indicates that the user-agent should not display the
        response, but instead show a Save As dialog with a filename other
        than the blob name specified.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The set_blob_properties function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('The blob container name must be specified as "container"')

    if 'blob' not in kwargs:
        raise SaltCloudSystemExit('The blob name must be specified as "blob"')

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    data = storage_conn.get_blob_properties(
        container_name=kwargs['container'],
        blob_name=kwargs['blob'],
        x_ms_blob_cache_control=kwargs.get('blob_cache_control', None),
        x_ms_blob_content_type=kwargs.get('blob_content_type', None),
        x_ms_blob_content_md5=kwargs.get('blob_content_md5', None),
        x_ms_blob_content_encoding=kwargs.get('blob_content_encoding', None),
        x_ms_blob_content_language=kwargs.get('blob_content_language', None),
        x_ms_lease_id=kwargs.get('lease_id', None),
        x_ms_blob_content_disposition=kwargs.get('blob_content_disposition', None),
    )

    return data


def put_blob(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Upload a blob

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f put_blob my-azure container=base name=top.sls blob_path=/srv/salt/top.sls
        salt-cloud -f put_blob my-azure container=base name=content.txt blob_content='Some content'

    container:
        Name of existing container.
    name:
        Name of existing blob.
    blob_path:
        The path on the local machine of the file to upload as a blob. Either
        this or blob_content must be specified.
    blob_content:
        The actual content to be uploaded as a blob. Either this or blob_path
        must me specified.
    cache_control:
        Optional. The Blob service stores this value but does not use or
        modify it.
    content_language:
        Optional. Specifies the natural languages used by this resource.
    content_md5:
        Optional. An MD5 hash of the blob content. This hash is used to
        verify the integrity of the blob during transport. When this header
        is specified, the storage service checks the hash that has arrived
        with the one that was sent. If the two hashes do not match, the
        operation will fail with error code 400 (Bad Request).
    blob_content_type:
        Optional. Set the blob's content type.
    blob_content_encoding:
        Optional. Set the blob's content encoding.
    blob_content_language:
        Optional. Set the blob's content language.
    blob_content_md5:
        Optional. Set the blob's MD5 hash.
    blob_cache_control:
        Optional. Sets the blob's cache control.
    meta_name_values:
        A dict containing name, value for metadata.
    lease_id:
        Required if the blob has an active lease.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The put_blob function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('The blob container name must be specified as "container"')

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('The blob name must be specified as "name"')

    if 'blob_path' not in kwargs and 'blob_content' not in kwargs:
        raise SaltCloudSystemExit(
            'Either a path to a file needs to be passed in as "blob_path" or '
            'the contents of a blob as "blob_content."'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    return salt.utils.msazure.put_blob(storage_conn=storage_conn, **kwargs)


def get_blob(kwargs=None, storage_conn=None, call=None):
    '''
    .. versionadded:: 2015.8.0

    Download a blob

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_blob my-azure container=base name=top.sls local_path=/srv/salt/top.sls
        salt-cloud -f get_blob my-azure container=base name=content.txt return_content=True

    container:
        Name of existing container.
    name:
        Name of existing blob.
    local_path:
        The path on the local machine to download the blob to. Either this or
        return_content must be specified.
    return_content:
        Whether or not to return the content directly from the blob. If
        specified, must be True or False. Either this or the local_path must
        be specified.
    snapshot:
        Optional. The snapshot parameter is an opaque DateTime value that,
        when present, specifies the blob snapshot to retrieve.
    lease_id:
        Required if the blob has an active lease.
    progress_callback:
        callback for progress with signature function(current, total) where
        current is the number of bytes transferred so far, and total is the
        size of the blob.
    max_connections:
        Maximum number of parallel connections to use when the blob size
        exceeds 64MB.
        Set to 1 to download the blob chunks sequentially.
        Set to 2 or more to download the blob chunks in parallel. This uses
        more system resources but will download faster.
    max_retries:
        Number of times to retry download of blob chunk if an error occurs.
    retry_wait:
        Sleep time in secs between retries.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The get_blob function must be called with -f or --function.'
        )

    if kwargs is None:
        kwargs = {}

    if 'container' not in kwargs:
        raise SaltCloudSystemExit('The blob container name must be specified as "container"')

    if 'name' not in kwargs:
        raise SaltCloudSystemExit('The blob name must be specified as "name"')

    if 'local_path' not in kwargs and 'return_content' not in kwargs:
        raise SaltCloudSystemExit(
            'Either a local path needs to be passed in as "local_path" or '
            '"return_content" to return the blob contents directly'
        )

    if not storage_conn:
        storage_conn = get_storage_conn(conn_kwargs=kwargs)

    return salt.utils.msazure.get_blob(storage_conn=storage_conn, **kwargs)


def query(path, method='GET', data=None, params=None, header_dict=None, decode=True):
    '''
    Perform a query directly against the Azure REST API
    '''
    certificate_path = config.get_cloud_config_value(
        'certificate_path',
        get_configured_provider(), __opts__, search_global=False
    )
    subscription_id = salt.utils.stringutils.to_str(
        config.get_cloud_config_value(
            'subscription_id',
            get_configured_provider(), __opts__, search_global=False
        )
    )
    management_host = config.get_cloud_config_value(
        'management_host',
        get_configured_provider(),
        __opts__,
        search_global=False,
        default='management.core.windows.net'
    )
    backend = config.get_cloud_config_value(
        'backend',
        get_configured_provider(), __opts__, search_global=False
    )
    url = 'https://{management_host}/{subscription_id}/{path}'.format(
        management_host=management_host,
        subscription_id=subscription_id,
        path=path,
    )

    if header_dict is None:
        header_dict = {}

    header_dict['x-ms-version'] = '2014-06-01'

    result = salt.utils.http.query(
        url,
        method=method,
        params=params,
        data=data,
        header_dict=header_dict,
        port=443,
        text=True,
        cert=certificate_path,
        backend=backend,
        decode=decode,
        decode_type='xml',
    )
    if 'dict' in result:
        return result['dict']
    return