# -*- coding: utf-8 -*-
'''
Azure Cloud Module
==================

.. versionadded:: 2016.11.0

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

:depends:
    * `Microsoft Azure SDK for Python <https://pypi.python.org/pypi/azure>`_ >= 2.0rc5
    * `Microsoft Azure Storage SDK for Python <https://pypi.python.org/pypi/azure-storage>`_ >= 0.32
:configuration:
    Required provider parameters:

    * ``subscription_id``
    * ``username``
    * ``password``

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
      username: larry
      password: 123pass
'''
# pylint: disable=E0102

# pylint: disable=wrong-import-position,wrong-import-order
from __future__ import absolute_import
import os
import os.path
import time
import logging
import pprint
import base64
import yaml
import salt.cache
import salt.config as config
import salt.utils
import salt.utils.cloud
import salt.ext.six as six
import salt.version
from salt.exceptions import (
    SaltCloudSystemExit,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout,
)

# Import 3rd-party libs
HAS_LIBS = False
try:
    import salt.utils.msazure
    from salt.utils.msazure import object_to_dict
    import azure.storage
    from azure.common.credentials import UserPassCredentials
    from azure.mgmt.compute import ComputeManagementClient
    from azure.mgmt.compute.models import (
        CachingTypes,
        DataDisk,
        DiskCreateOptionTypes,
        HardwareProfile,
        ImageReference,
        NetworkInterfaceReference,
        NetworkProfile,
        OSDisk,
        OSProfile,
        StorageProfile,
        VirtualHardDisk,
        VirtualMachine,
        VirtualMachineSizeTypes,
    )
    from azure.mgmt.network import NetworkManagementClient
    from azure.mgmt.network.models import (
        IPAllocationMethod,
        NetworkInterface,
        NetworkInterfaceIPConfiguration,
        NetworkSecurityGroup,
        PublicIPAddress,
        SecurityRule,
    )
    from azure.mgmt.resource.resources import ResourceManagementClient
    from azure.mgmt.storage import StorageManagementClient
    from azure.mgmt.web import WebSiteManagementClient
    from msrestazure.azure_exceptions import CloudError
    HAS_LIBS = True
except ImportError:
    pass
# pylint: enable=wrong-import-position,wrong-import-order

__virtualname__ = 'azurearm'
# pylint: disable=invalid-name
cache = None
storconn = None
compconn = None
netconn = None
webconn = None
resconn = None
# pylint: enable=invalid-name


# 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

    global cache  # pylint: disable=global-statement,invalid-name
    cache = salt.cache.Cache(__opts__)

    return __virtualname__


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


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


def get_conn(Client=None):
    '''
    Return a conn object for the passed VM data
    '''
    if Client is None:
        Client = ComputeManagementClient

    subscription_id = config.get_cloud_config_value(
        'subscription_id',
        get_configured_provider(), __opts__, search_global=False
    )

    username = config.get_cloud_config_value(
        'username',
        get_configured_provider(), __opts__, search_global=False
    )

    password = config.get_cloud_config_value(
        'password',
        get_configured_provider(), __opts__, search_global=False
    )

    credentials = UserPassCredentials(username, password)
    client = Client(
        credentials=credentials,
        subscription_id=subscription_id,
    )
    client.config.add_user_agent('SaltCloud/{0}'.format(salt.version.__version__))
    return client


def get_location():
    '''
    Return the location that is configured for this provider
    '''
    return config.get_cloud_config_value(
        'location',
        get_configured_provider(), __opts__, search_global=False
    )


def avail_locations(conn=None, call=None):  # pylint: disable=unused-argument
    '''
    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'
        )

    global webconn  # pylint: disable=global-statement,invalid-name
    if not webconn:
        webconn = get_conn(WebSiteManagementClient)

    ret = {}
    regions = webconn.global_model.get_subscription_geo_regions()
    for location in regions.value:  # pylint: disable=no-member
        lowername = str(location.name).lower().replace(' ', '')
        ret[lowername] = object_to_dict(location)
    return ret


def _cache(bank, key, fun, **kwargs):
    '''
    Cache an Azure ARM object
    '''
    items = cache.fetch(bank, key)
    if items is None:
        items = {}
        try:
            item_list = fun(**kwargs)
        except CloudError as exc:
            log.warning('There was a cloud error calling {0} with kwargs {1}: {2}'.format(fun, kwargs, exc))
        for item in item_list:
            items[item.name] = object_to_dict(item)
        cache.store(bank, key, items)
    return items


def avail_images(conn=None, call=None):  # pylint: disable=unused-argument
    '''
    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'
        )

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}'.format(region)
    publishers = _cache(
        bank,
        'publishers',
        compconn.virtual_machine_images.list_publishers,
        location=region,
    )

    ret = {}
    for publisher in publishers:
        pub_bank = os.path.join(bank, 'publishers', publisher)
        offers = _cache(
            pub_bank,
            'offers',
            compconn.virtual_machine_images.list_offers,
            location=region,
            publisher_name=publishers[publisher]['name'],
        )

        for offer in offers:
            offer_bank = os.path.join(pub_bank, 'offers', offer)
            skus = _cache(
                offer_bank,
                'skus',
                compconn.virtual_machine_images.list_skus,
                location=region,
                publisher_name=publishers[publisher]['name'],
                offer=offers[offer]['name'],
            )

            for sku in skus:
                sku_bank = os.path.join(offer_bank, 'skus', sku)
                results = _cache(
                    sku_bank,
                    'results',
                    compconn.virtual_machine_images.list,
                    location=region,
                    publisher_name=publishers[publisher]['name'],
                    offer=offers[offer]['name'],
                    skus=skus[sku]['name'],
                )

                for version in results:
                    name = '|'.join((
                        publishers[publisher]['name'],
                        offers[offer]['name'],
                        skus[sku]['name'],
                        results[version]['name'],
                    ))
                    ret[name] = {
                        'publisher': publishers[publisher]['name'],
                        'offer': offers[offer]['name'],
                        'sku': skus[sku]['name'],
                        'version': results[version]['name'],
                    }
    return ret


def avail_sizes(call=None):  # pylint: disable=unused-argument
    '''
    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'
        )

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    ret = {}
    location = get_location()
    sizes = compconn.virtual_machine_sizes.list(location)
    for size in sizes:
        ret[size.name] = object_to_dict(size)
    return ret


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

    ret = {}
    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    nodes = list_nodes_full(compconn, 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):  # pylint: disable=unused-argument
    '''
    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.'
        )

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    ret = {}
    for group in list_resource_groups():
        nodes = compconn.virtual_machines.list(group)
        for node in nodes:
            ret[node.name] = object_to_dict(node)
            ret[node.name]['id'] = node.id
            ret[node.name]['name'] = node.name
            ret[node.name]['size'] = node.hardware_profile.vm_size
            ret[node.name]['state'] = node.provisioning_state
            ret[node.name]['private_ips'] = node.network_profile.network_interfaces
            ret[node.name]['public_ips'] = node.network_profile.network_interfaces
            ret[node.name]['storage_profile']['data_disks'] = []
            for disk in node.storage_profile.data_disks:
                ret[node.name]['storage_profile']['data_disks'].append(make_safe(disk))
            try:
                ret[node.name]['image'] = '|'.join((
                    ret[node.name]['storage_profile']['image_reference']['publisher'],
                    ret[node.name]['storage_profile']['image_reference']['offer'],
                    ret[node.name]['storage_profile']['image_reference']['sku'],
                    ret[node.name]['storage_profile']['image_reference']['version'],
                ))
            except TypeError:
                try:
                    ret[node.name]['image'] = ret[node.name]['storage_profile']['os_disk']['image']['uri']
                except TypeError:
                    ret[node.name]['image'] = None
    return ret


def list_resource_groups(conn=None, call=None):  # pylint: disable=unused-argument
    '''
    List resource groups associated with the account
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_hosted_services function must be called with '
            '-f or --function'
        )

    global resconn  # pylint: disable=global-statement,invalid-name
    if not resconn:
        resconn = get_conn(ResourceManagementClient)

    ret = {}

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}'.format(region)
    groups = cache.cache(
        bank,
        'resource_groups',
        resconn.resource_groups.list,
        loop_fun=object_to_dict,
        expire=config.get_cloud_config_value(
            'expire_group_cache', get_configured_provider(),
            __opts__, search_global=False, default=86400,
        )
    )

    for group in groups:
        ret[group['name']] = group
    return ret


def list_nodes_select(conn=None, call=None):  # pylint: disable=unused-argument
    '''
    Return a list of the VMs that are on the provider, with select fields
    '''
    return salt.utils.cloud.list_nodes_select(
        list_nodes_full(conn, 'function'), __opts__['query.selection'], call,
    )


def show_instance(name, resource_group=None, call=None):  # pylint: disable=unused-argument
    '''
    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.'
        )

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    data = None
    if resource_group is None:
        for group in list_resource_groups():
            try:
                instance = compconn.virtual_machines.get(group, name)
                data = object_to_dict(instance)
                resource_group = group
            except CloudError:
                continue

    # Find under which cloud service the name is listed, if any
    if data is None:
        return {}

    ifaces = {}
    if 'network_profile' not in data:
        data['network_profile'] = {}

    if 'network_interfaces' not in data['network_profile']:
        data['network_profile']['network_interfaces'] = []

    for iface in data['network_profile']['network_interfaces']:
        iface_name = iface.id.split('/')[-1]
        iface_data = show_interface(kwargs={
            'resource_group': resource_group,
            'iface_name': iface_name,
            'name': name,
        })
        ifaces[iface_name] = iface_data

    data['network_profile']['network_interfaces'] = ifaces
    data['resource_group'] = resource_group

    __utils__['cloud.cache_node'](
        salt.utils.simple_types_filter(data),
        __active_provider_name__,
        __opts__
    )
    return data


def list_networks(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List virtual networks
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_sizes function must be called with '
            '-f or --function, or with the --list-sizes option'
        )

    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}/virtual_networks'.format(region)

    if kwargs is None:
        kwargs = {}

    if 'group' in kwargs:
        groups = [kwargs['group']]
    else:
        groups = list_resource_groups()

    ret = {}
    for group in groups:
        try:
            networks = cache.cache(
                bank,
                group,
                netconn.virtual_networks.list,
                loop_fun=make_safe,
                expire=config.get_cloud_config_value(
                    'expire_network_cache', get_configured_provider(),
                    __opts__, search_global=False, default=86400,
                ),
                resource_group_name=group,
            )
        except CloudError:
            networks = {}
        for vnet in networks:
            ret[vnet['name']] = make_safe(vnet)
            ret[vnet['name']]['subnets'] = list_subnets(
                kwargs={'group': group, 'network': vnet['name']}
            )

    return ret


def list_subnets(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List subnets in a virtual network
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_sizes function must be called with '
            '-f or --function, or with the --list-sizes option'
        )

    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if 'group' in kwargs and 'resource_group' not in kwargs:
        kwargs['resource_group'] = kwargs['group']

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

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

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}/{1}'.format(region, kwargs['network'])

    ret = {}
    subnets = netconn.subnets.list(kwargs['resource_group'], kwargs['network'])
    for subnet in subnets:
        ret[subnet.name] = make_safe(subnet)
        ret[subnet.name]['ip_configurations'] = {}
        for ip_ in subnet.ip_configurations:
            comps = ip_.id.split('/')
            name = comps[-1]
            ret[subnet.name]['ip_configurations'][name] = make_safe(ip_)
            ret[subnet.name]['ip_configurations'][name]['subnet'] = subnet.name
        ret[subnet.name]['resource_group'] = kwargs['resource_group']
    return ret


def delete_interface(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network interface
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    ips = []
    iface = netconn.network_interfaces.get(
        kwargs['resource_group'],
        kwargs['iface_name'],
    )
    iface_name = iface.name
    for ip_ in iface.ip_configurations:
        ips.append(ip_.name)

    poller = netconn.network_interfaces.delete(
        kwargs['resource_group'],
        kwargs['iface_name'],
    )
    poller.wait()

    for ip_ in ips:
        poller = netconn.public_ip_addresses.delete(kwargs['resource_group'], ip_)
        poller.wait()

    return {iface_name: ips}


def delete_ip(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network interface
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    return netconn.public_ip_addresses.delete(
        kwargs['resource_group'],
        kwargs['ip_name'],
    )


def show_interface(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network interface
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('group'):
        kwargs['resource_group'] = kwargs['group']

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    iface_name = kwargs.get('iface_name', kwargs.get('name'))
    iface = netconn.network_interfaces.get(
        kwargs['resource_group'],
        iface_name,
    )
    data = object_to_dict(iface)
    data['resource_group'] = kwargs['resource_group']
    data['ip_configurations'] = {}
    for ip_ in iface.ip_configurations:
        data['ip_configurations'][ip_.name] = make_safe(ip_)
        try:
            pubip = netconn.public_ip_addresses.get(
                kwargs['resource_group'],
                ip_.name,
            )
            data['ip_configurations'][ip_.name]['public_ip_address']['ip_address'] = pubip.ip_address
        except Exception as exc:
            log.warning('There was a cloud error: {0}'.format(exc))
            log.warning('{0}'.format(type(exc)))
            continue

    return data


def list_ip_configurations(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List IP configurations
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if 'group' not in kwargs:
        if 'resource_group' in kwargs:
            kwargs['group'] = kwargs['resource_group']
        else:
            raise SaltCloudSystemExit(
                'A resource_group must be specified as "group" or "resource_group"'
            )

    ip_conf = {}
    for ip_ in kwargs.get('ip_configurations', []):
        ip_data = object_to_dict(ip_)
        ip_conf[ip_data['name']] = ip_data
        try:
            pub_ip = netconn.public_ip_addresses.get(  # pylint: disable=no-member
                kwargs['resource_group'], ip_data['name']
            ).ip_address
            ip_conf[ip_data['name']]['public_ip_address'] = pub_ip
        except CloudError:
            # There is no public IP on this interface
            pass
    return ip_conf


def list_interfaces(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network interface
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True,
            default=config.get_cloud_config_value(
                'group', {}, __opts__, search_global=True
            )
        )

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}'.format(region)
    interfaces = cache.cache(
        bank,
        'network_interfaces',
        netconn.network_interfaces.list,
        loop_fun=make_safe,
        expire=config.get_cloud_config_value(
            'expire_interface_cache', get_configured_provider(),
            __opts__, search_global=False, default=86400,
        ),
        resource_group_name=kwargs['resource_group']
    )
    ret = {}
    for interface in interfaces:
        ret[interface['name']] = interface
    return ret


def create_interface(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network interface
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}
    vm_ = kwargs

    if kwargs.get('location') is None:
        kwargs['location'] = get_location()

    if kwargs.get('network') is None:
        kwargs['network'] = config.get_cloud_config_value(
            'network', vm_, __opts__, search_global=True
        )

    if kwargs.get('subnet') is None:
        kwargs['subnet'] = config.get_cloud_config_value(
            'subnet', vm_, __opts__, default='default', search_global=True
        )

    if kwargs.get('iface_name') is None:
        kwargs['iface_name'] = '{0}-iface0'.format(vm_['name'])

    subnet_obj = netconn.subnets.get(
        resource_group_name=kwargs['network_resource_group'],
        virtual_network_name=kwargs['network'],
        subnet_name=kwargs['subnet'],
    )

    ip_kwargs = {}
    ip_configurations = None

    if 'private_ip_address' in kwargs.keys():
        ip_kwargs['private_ip_address'] = kwargs['private_ip_address']
        ip_kwargs['private_ip_allocation_method'] = IPAllocationMethod.static
    else:
        ip_kwargs['private_ip_allocation_method'] = IPAllocationMethod.dynamic

    if bool(kwargs.get('public_ip')) is True:
        pub_ip_name = '{0}-ip'.format(kwargs['iface_name'])
        poller = netconn.public_ip_addresses.create_or_update(
            resource_group_name=kwargs['resource_group'],
            public_ip_address_name=pub_ip_name,
            parameters=PublicIPAddress(
                location=kwargs['location'],
                public_ip_allocation_method=IPAllocationMethod.static,
            ),
        )
        count = 0
        poller.wait()
        while True:
            try:
                pub_ip_data = netconn.public_ip_addresses.get(
                    kwargs['resource_group'],
                    pub_ip_name,
                )
                if pub_ip_data.ip_address:  # pylint: disable=no-member
                    ip_kwargs['public_ip_address'] = PublicIPAddress(
                        str(pub_ip_data.id),  # pylint: disable=no-member
                    )
                    ip_configurations = [
                        NetworkInterfaceIPConfiguration(
                            name='{0}-ip'.format(kwargs['iface_name']),
                            subnet=subnet_obj,
                            **ip_kwargs
                        )
                    ]
                    break
            except CloudError as exc:
                log.error('There was a cloud error: {0}'.format(exc))
            count += 1
            if count > 120:
                raise ValueError('Timed out waiting for public IP Address.')
            time.sleep(5)
    else:
        priv_ip_name = '{0}-ip'.format(kwargs['iface_name'])
        ip_configurations = [
            NetworkInterfaceIPConfiguration(
                name='{0}-ip'.format(kwargs['iface_name']),
                subnet=subnet_obj,
                **ip_kwargs
            )
        ]

    iface_params = NetworkInterface(
        location=kwargs['location'],
        ip_configurations=ip_configurations,
    )

    poller = netconn.network_interfaces.create_or_update(
        kwargs['resource_group'], kwargs['iface_name'], iface_params
    )
    poller.wait()
    count = 0
    while True:
        try:
            return show_interface(kwargs=kwargs)
        except CloudError:
            count += 1
            if count > 120:
                raise ValueError('Timed out waiting for operation to complete.')
            time.sleep(5)


def request_instance(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Request that Azure spin up a new instance
    '''
    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    vm_ = kwargs

    if vm_.get('driver') is None:
        vm_['driver'] = 'azurearm'

    if vm_.get('location') is None:
        vm_['location'] = get_location()

    if vm_.get('resource_group') is None:
        vm_['resource_group'] = config.get_cloud_config_value(
            'resource_group', vm_, __opts__, search_global=True
        )

    if vm_.get('name') is None:
        vm_['name'] = config.get_cloud_config_value(
            'name', vm_, __opts__, search_global=True
        )

    os_kwargs = {}
    userdata = None
    userdata_file = config.get_cloud_config_value(
        'userdata_file', vm_, __opts__, search_global=False, default=None
    )
    if userdata_file is None:
        userdata = config.get_cloud_config_value(
            'userdata', vm_, __opts__, search_global=False, default=None
        )
    else:
        if os.path.exists(userdata_file):
            with salt.utils.fopen(userdata_file, 'r') as fh_:
                userdata = fh_.read()

    userdata = salt.utils.cloud.userdata_template(__opts__, vm_, userdata)

    if userdata is not None:
        try:
            os_kwargs['custom_data'] = base64.b64encode(userdata)
        except Exception as exc:
            log.exception('Failed to encode userdata: %s', exc)

    iface_data = create_interface(kwargs=vm_)
    vm_['iface_id'] = iface_data['id']

    disk_name = '{0}-vol0'.format(vm_['name'])

    vm_username = config.get_cloud_config_value(
        'ssh_username', vm_, __opts__, search_global=True,
        default=config.get_cloud_config_value(
            'win_username', vm_, __opts__, search_global=True
        )
    )

    vm_password = config.get_cloud_config_value(
        'ssh_password', vm_, __opts__, search_global=True,
        default=config.get_cloud_config_value(
            'win_password', vm_, __opts__, search_global=True
        )
    )

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

    data_disks = None
    if isinstance(volumes, list):
        data_disks = []
    else:
        volumes = []

    lun = 0
    luns = []
    for volume in volumes:
        if isinstance(volume, six.string_types):
            volume = {'name': volume}
        # Use the size keyword to set a size, but you can use either the new
        # azure name (disk_size_gb) or the old (logical_disk_size_in_gb)
        # instead. If none are set, the disk has size 100GB.
        volume.setdefault(
            'disk_size_gb', volume.get(
                'logical_disk_size_in_gb', volume.get('size', 100)
            )
        )
        # Old kwarg was host_caching, new name is caching
        volume.setdefault('caching', volume.get('host_caching', 'ReadOnly'))
        while lun in luns:
            lun += 1
            if lun > 15:
                log.error('Maximum lun count has been reached')
                break
        volume.setdefault('lun', lun)
        lun += 1
        # The default vhd is {vm_name}-datadisk{lun}.vhd
        if 'media_link' in volume:
            volume['vhd'] = VirtualHardDisk(volume['media_link'])
            del volume['media_link']
        elif 'vhd' in volume:
            volume['vhd'] = VirtualHardDisk(volume['vhd'])
        else:
            volume['vhd'] = VirtualHardDisk(
                'https://{0}.blob.core.windows.net/vhds/{1}-datadisk{2}.vhd'.format(
                    vm_['storage_account'],
                    vm_['name'],
                    volume['lun'],
                ),
            )
        if 'image' in volume:
            volume['create_option'] = DiskCreateOptionTypes.from_image
        elif 'attach' in volume:
            volume['create_option'] = DiskCreateOptionTypes.attach
        else:
            volume['create_option'] = DiskCreateOptionTypes.empty
        data_disks.append(DataDisk(**volume))

    win_installer = config.get_cloud_config_value(
        'win_installer', vm_, __opts__, search_global=True
    )
    if vm_['image'].startswith('http'):
        # https://{storage_account}.blob.core.windows.net/{path}/{vhd}
        source_image = VirtualHardDisk(vm_['image'])
        img_ref = None
        if win_installer:
            os_type = 'Windows'
        else:
            os_type = 'Linux'
    else:
        img_pub, img_off, img_sku, img_ver = vm_['image'].split('|')
        source_image = None
        os_type = None
        img_ref = ImageReference(
            publisher=img_pub,
            offer=img_off,
            sku=img_sku,
            version=img_ver,
        )

    params = VirtualMachine(
        location=vm_['location'],
        plan=None,
        hardware_profile=HardwareProfile(
            vm_size=getattr(
                VirtualMachineSizeTypes, vm_['size'].lower()
            ),
        ),
        storage_profile=StorageProfile(
            os_disk=OSDisk(
                caching=CachingTypes.none,
                create_option=DiskCreateOptionTypes.from_image,
                name=disk_name,
                vhd=VirtualHardDisk(
                    'https://{0}.blob.core.windows.net/vhds/{1}.vhd'.format(
                        vm_['storage_account'],
                        disk_name,
                    ),
                ),
                os_type=os_type,
                image=source_image,
                disk_size_gb=vm_.get('os_disk_size_gb', 30)
            ),
            data_disks=data_disks,
            image_reference=img_ref,
        ),
        os_profile=OSProfile(
            admin_username=vm_username,
            admin_password=vm_password,
            computer_name=vm_['name'],
            **os_kwargs
        ),
        network_profile=NetworkProfile(
            network_interfaces=[
                NetworkInterfaceReference(vm_['iface_id']),
            ],
        ),
    )

    poller = compconn.virtual_machines.create_or_update(
        vm_['resource_group'], vm_['name'], params
    )
    try:
        poller.wait()
    except CloudError as exc:
        log.warning('There was a cloud error: {0}'.format(exc))
        log.warning('This may or may not indicate an actual problem')

    try:
        return show_instance(vm_['name'], call='action')
    except CloudError:
        return {}


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

    # Since using "provider: <provider-engine>" is deprecated, alias provider
    # to use driver: "driver: <provider-engine>"
    if 'provider' in vm_:
        vm_['driver'] = vm_.pop('provider')

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

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    def _query_ip_address():
        data = request_instance(kwargs=vm_)
        ifaces = data['network_profile']['network_interfaces']
        iface = ifaces.keys()[0]
        ip_name = ifaces[iface]['ip_configurations'].keys()[0]

        if vm_.get('public_ip') is True:
            hostname = ifaces[iface]['ip_configurations'][ip_name]['public_ip_address']
        else:
            hostname = ifaces[iface]['ip_configurations'][ip_name]['private_ip_address']

        if isinstance(hostname, dict):
            hostname = hostname.get('ip_address')

        if not isinstance(hostname, six.string_types):
            return None
        return hostname

    try:
        data = salt.utils.cloud.wait_for_ip(
            _query_ip_address,
            timeout=config.get_cloud_config_value(
                'wait_for_ip_timeout', vm_, __opts__, default=10 * 60),
            interval=config.get_cloud_config_value(
                'wait_for_ip_interval', vm_, __opts__, default=10),
            interval_multiplier=config.get_cloud_config_value(
                'wait_for_ip_interval_multiplier', vm_, __opts__, default=1),
        )
    except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure, SaltCloudSystemExit) as exc:
        try:
            log.warning(exc)
        finally:
            raise SaltCloudSystemExit(str(exc))

    # calling _query_ip_address() causes Salt to attempt to build the VM again.
    #hostname = _query_ip_address()
    hostname = data

    if not hostname or not isinstance(hostname, six.string_types):
        log.error('Failed to get a value for the hostname.')
        return False

    vm_['ssh_host'] = hostname
    if not vm_.get('ssh_username'):
        vm_['ssh_username'] = config.get_cloud_config_value(
            'ssh_username', vm_, __opts__
        )
    vm_['password'] = config.get_cloud_config_value(
        'ssh_password', vm_, __opts__
    )
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)

    data = show_instance(vm_['name'], call='action')
    log.info('Created Cloud VM \'{0[name]}\''.format(vm_))
    log.debug(
        '\'{0[name]}\' VM creation details:\n{1}'.format(
            vm_, pprint.pformat(data)
        )
    )

    ret.update(data)

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

    return ret


def destroy(name, conn=None, call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    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.'
        )

    global compconn  # pylint: disable=global-statement,invalid-name
    if not compconn:
        compconn = get_conn()

    if kwargs is None:
        kwargs = {}

    node_data = show_instance(name, call='action')
    vhd = node_data['storage_profile']['os_disk']['vhd']['uri']

    ret = {name: {}}
    log.debug('Deleting VM')
    result = compconn.virtual_machines.delete(node_data['resource_group'], name)
    result.wait()

    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,
        ))
        if cleanup_vhds:
            log.debug('Deleting vhd')

            comps = vhd.split('/')
            container = comps[-2]
            blob = comps[-1]

            ret[name]['delete_disk'] = {
                'delete_disks': cleanup_disks,
                'delete_vhd': cleanup_vhds,
                'container': container,
                'blob': blob,
            }
            ret[name]['data'] = delete_blob(
                kwargs={'container': container, 'blob': blob},
                call='function'
            )

        cleanup_data_disks = kwargs.get('delete_data_disks', config.get_cloud_config_value(
            'cleanup_data_disks',
            get_configured_provider(), __opts__, search_global=False, default=False,
        ))
        if cleanup_data_disks:
            log.debug('Deleting data_disks')
            ret[name]['data_disks'] = {}

            for disk in node_data['storage_profile']['data_disks']:
                datavhd = disk.vhd.uri
                comps = datavhd.split('/')
                container = comps[-2]
                blob = comps[-1]

                ret[name]['data_disks'][disk.name] = {
                    'delete_disks': cleanup_disks,
                    'delete_vhd': cleanup_vhds,
                    'container': container,
                    'blob': blob,
                }
                ret[name]['data'] = delete_blob(
                    kwargs={'container': container, 'blob': blob},
                    call='function'
                )

    cleanup_interfaces = config.get_cloud_config_value(
        'cleanup_interfaces',
        get_configured_provider(), __opts__, search_global=False, default=False,
    )
    if cleanup_interfaces:
        ret[name]['cleanup_network'] = {
            'cleanup_interfaces': cleanup_interfaces,
            'resource_group': cleanup_interfaces,
            'iface_name': cleanup_interfaces,
            'data': [],
        }
        ifaces = node_data['network_profile']['network_interfaces']
        for iface in ifaces:
            ret[name]['cleanup_network']['data'].append(
                delete_interface(
                    kwargs={
                        'resource_group': ifaces[iface]['resource_group'],
                        'iface_name': iface,
                    },
                    call='function',
                )
            )

    return ret


def make_safe(data):
    '''
    Turn object data into something serializable
    '''
    return salt.utils.simple_types_filter(object_to_dict(data))


def create_security_group(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a security group
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('location') is None:
        kwargs['location'] = get_location()

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    if kwargs.get('name') is None:
        kwargs['name'] = config.get_cloud_config_value(
            'name', {}, __opts__, search_global=True
        )

    group_params = NetworkSecurityGroup(
        location=kwargs['location'],
    )

    netconn.network_security_group.create_or_update(  # pylint: disable=no-member
        rource_group_name=kwargs['resource_group'],
        network_security_group_name=kwargs['name'],
        parameters=group_params,
    )
    count = 0
    while True:
        try:
            return show_security_group(kwargs=kwargs)
        except CloudError:
            count += 1
            if count > 120:
                raise ValueError('Timed out waiting for operation to complete.')
            time.sleep(5)


def show_security_group(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network security_group
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    group = netconn.network_security_groups.get(
        resource_group_name=kwargs['resource_group'],
        network_security_group_name=kwargs['security_group'],
    )
    group_dict = make_safe(group)
    def_rules = {}
    for rule in group.default_security_rules:  # pylint: disable=no-member
        def_rules[rule.name] = make_safe(rule)
    group_dict['default_security_rules'] = def_rules
    sec_rules = {}
    for rule in group.security_rules:  # pylint: disable=no-member
        sec_rules[rule.name] = make_safe(rule)
    group_dict['security_rules'] = sec_rules
    return group_dict


def list_security_groups(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network security_group
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}'.format(region)
    security_groups = cache.cache(
        bank,
        'network_security_groups',
        netconn.network_security_groups.list,
        loop_fun=make_safe,
        expire=config.get_cloud_config_value(
            'expire_security_group_cache', get_configured_provider(),
            __opts__, search_global=False, default=86400,
        ),
        resource_group_name=kwargs['resource_group']
    )
    ret = {}
    for group in security_groups:
        ret[group['name']] = group
    return ret


def create_security_rule(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a security rule (aka, firewall rule)
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    if kwargs.get('security_group') is None:
        kwargs['security_group'] = config.get_cloud_config_value(
            'security_group', {}, __opts__, search_global=True
        )

    if kwargs.get('name') is None:
        kwargs['name'] = config.get_cloud_config_value(
            'name', {}, __opts__, default='default', search_global=True
        )

    rule_params = SecurityRule(
        protocol=kwargs['protocol'],  # Can be 'Tcp', 'Udp', or '*'
        source_address_prefix=kwargs['source_address'],  # '*', 'VirtualNetwork', 'AzureLoadBalancer', 'Internet', '0.0.0.0/24', etc pylint: disable=line-too-long
        source_port_range=kwargs['source_ports'],  # '*', int, or range (0-65535)
        destination_address_prefix=kwargs['destination_address'],  # '*', 'VirtualNetwork', 'AzureLoadBalancer', 'Internet', '0.0.0.0/24', etc pylint: disable=line-too-long
        destination_port_range=kwargs['destination_ports'],  # '*', int, or range (0-65535)
        access=kwargs['access'],  # 'Allow' or 'Deny'
        direction=kwargs['direction'],  # 'Inbound' or 'Outbound'
        priority=kwargs['priority'],  # Unique number between and 100-4096
    )

    netconn.security_rules.create_or_update(
        resource_group_name=kwargs['resource_group'],
        network_security_group_name=kwargs['security_group'],
        security_rule_name=kwargs['name'],
        security_rule_parameters=rule_params,
    )
    count = 0
    while True:
        try:
            return show_security_rule(kwargs=kwargs)
        except CloudError:
            count += 1
            if count > 120:
                raise ValueError('Timed out waiting for operation to complete.')
            time.sleep(5)


def show_security_rule(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Create a network security_rule
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    rule = netconn.security_rules.get(
        resource_group_name=kwargs['resource_group'],
        network_security_group_name=kwargs['security_group'],
        security_rule_name=kwargs['name'],
    )
    return make_safe(rule)


def list_security_rules(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Lits network security rules
    '''
    global netconn  # pylint: disable=global-statement,invalid-name
    if not netconn:
        netconn = get_conn(NetworkManagementClient)

    if kwargs is None:
        kwargs = {}

    if kwargs.get('resource_group') is None:
        kwargs['resource_group'] = config.get_cloud_config_value(
            'resource_group', {}, __opts__, search_global=True
        )

    if kwargs.get('security_group') is None:
        kwargs['security_group'] = config.get_cloud_config_value(
            'security_group', {}, __opts__, search_global=True
        )

    region = get_location()
    bank = 'cloud/metadata/azurearm/{0}'.format(region)
    security_rules = cache.cache(
        bank,
        'security_rules',
        netconn.security_rules.list,
        loop_fun=make_safe,
        expire=config.get_cloud_config_value(
            'expire_security_rule_cache', get_configured_provider(),
            __opts__, search_global=False, default=86400,
        ),
        resource_group_name=kwargs['resource_group'],
        network_security_group_name=kwargs['security_group'],
    )
    ret = {}
    for group in security_rules:
        ret[group['name']] = group
    return ret


def pages_to_list(items):
    '''
    Convert a set of links from a group of pages to a list
    '''
    objs = []
    while True:
        try:
            page = items.next()  # pylint: disable=incompatible-py3-code
            for item in page:
                objs.append(item)
        except GeneratorExit:
            break
    return objs


def list_storage_accounts(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List storage accounts
    '''
    global storconn  # pylint: disable=global-statement,invalid-name
    if not storconn:
        storconn = get_conn(StorageManagementClient)

    if kwargs is None:
        kwargs = {}

    ret = {}
    for acct in pages_to_list(storconn.storage_accounts.list()):
        ret[acct.name] = object_to_dict(acct)

    return ret


def list_containers(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List containers
    '''
    global storconn  # pylint: disable=global-statement,invalid-name
    if not storconn:
        storconn = get_conn(StorageManagementClient)

    storageaccount = azure.storage.CloudStorageAccount(
        config.get_cloud_config_value(
            'storage_account',
            get_configured_provider(), __opts__, search_global=False
        ),
        config.get_cloud_config_value(
            'storage_key',
            get_configured_provider(), __opts__, search_global=False
        ),
    )
    storageservice = storageaccount.create_block_blob_service()

    if kwargs is None:
        kwargs = {}

    ret = {}
    for cont in storageservice.list_containers().items:
        ret[cont.name] = object_to_dict(cont)

    return ret


list_storage_containers = list_containers


def list_blobs(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    List blobs
    '''
    global storconn  # pylint: disable=global-statement,invalid-name
    if not storconn:
        storconn = get_conn(StorageManagementClient)

    if kwargs is None:
        kwargs = {}

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

    storageaccount = azure.storage.CloudStorageAccount(
        config.get_cloud_config_value(
            'storage_account',
            get_configured_provider(), __opts__, search_global=False
        ),
        config.get_cloud_config_value(
            'storage_key',
            get_configured_provider(), __opts__, search_global=False
        ),
    )
    storageservice = storageaccount.create_block_blob_service()

    ret = {}
    for blob in storageservice.list_blobs(kwargs['container']).items:
        ret[blob.name] = object_to_dict(blob)

    return ret


def delete_blob(call=None, kwargs=None):  # pylint: disable=unused-argument
    '''
    Delete a blob from a container
    '''
    global storconn  # pylint: disable=global-statement,invalid-name
    if not storconn:
        storconn = get_conn(StorageManagementClient)

    if kwargs is None:
        kwargs = {}

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

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

    storageaccount = azure.storage.CloudStorageAccount(
        config.get_cloud_config_value(
            'storage_account',
            get_configured_provider(), __opts__, search_global=False
        ),
        config.get_cloud_config_value(
            'storage_key',
            get_configured_provider(), __opts__, search_global=False
        ),
    )
    storageservice = storageaccount.create_block_blob_service()

    storageservice.delete_blob(kwargs['container'], kwargs['blob'])
    return True
