1"""
2Azure Cloud Module
3==================
4
5The Azure cloud module is used to control access to Microsoft Azure
6
7:depends:
8    * `Microsoft Azure SDK for Python <https://pypi.python.org/pypi/azure/1.0.2>`_ >= 1.0.2
9    * python-requests, for Python < 2.7.9
10:configuration:
11    Required provider parameters:
12
13    * ``apikey``
14    * ``certificate_path``
15    * ``subscription_id``
16    * ``backend``
17
18    A Management Certificate (.pem and .crt files) must be created and the .pem
19    file placed on the same machine that salt-cloud is run from. Information on
20    creating the pem file to use, and uploading the associated cer file can be
21    found at:
22
23    http://www.windowsazure.com/en-us/develop/python/how-to-guides/service-management/
24
25    For users with Python < 2.7.9, ``backend`` must currently be set to ``requests``.
26
27Example ``/etc/salt/cloud.providers`` or
28``/etc/salt/cloud.providers.d/azure.conf`` configuration:
29
30.. code-block:: yaml
31
32    my-azure-config:
33      driver: azure
34      subscription_id: 3287abc8-f98a-c678-3bde-326766fd3617
35      certificate_path: /etc/salt/azure.pem
36      management_host: management.core.windows.net
37"""
38# pylint: disable=function-redefined
39
40import copy
41import logging
42import pprint
43import time
44
45import salt.config as config
46import salt.utils.cloud
47import salt.utils.stringutils
48import salt.utils.yaml
49from salt.exceptions import SaltCloudSystemExit
50
51HAS_LIBS = False
52try:
53    import azure
54    import azure.storage
55    import azure.servicemanagement
56    from azure.common import (
57        AzureConflictHttpError,
58        AzureMissingResourceHttpError,
59        AzureException,
60    )
61    import salt.utils.msazure
62    from salt.utils.msazure import object_to_dict
63
64    HAS_LIBS = True
65except ImportError:
66    pass
67
68__virtualname__ = "azure"
69
70
71# Get logging started
72log = logging.getLogger(__name__)
73
74
75# Only load in this module if the AZURE configurations are in place
76def __virtual__():
77    """
78    Check for Azure configurations.
79    """
80    if get_configured_provider() is False:
81        return False
82
83    if get_dependencies() is False:
84        return False
85
86    return __virtualname__
87
88
89def _get_active_provider_name():
90    try:
91        return __active_provider_name__.value()
92    except AttributeError:
93        return __active_provider_name__
94
95
96def get_configured_provider():
97    """
98    Return the first configured instance.
99    """
100    return config.is_provider_configured(
101        __opts__,
102        _get_active_provider_name() or __virtualname__,
103        ("subscription_id", "certificate_path"),
104    )
105
106
107def get_dependencies():
108    """
109    Warn if dependencies aren't met.
110    """
111    return config.check_driver_dependencies(__virtualname__, {"azure": HAS_LIBS})
112
113
114def get_conn():
115    """
116    Return a conn object for the passed VM data
117    """
118    certificate_path = config.get_cloud_config_value(
119        "certificate_path", get_configured_provider(), __opts__, search_global=False
120    )
121    subscription_id = salt.utils.stringutils.to_str(
122        config.get_cloud_config_value(
123            "subscription_id", get_configured_provider(), __opts__, search_global=False
124        )
125    )
126    management_host = config.get_cloud_config_value(
127        "management_host",
128        get_configured_provider(),
129        __opts__,
130        search_global=False,
131        default="management.core.windows.net",
132    )
133    return azure.servicemanagement.ServiceManagementService(
134        subscription_id, certificate_path, management_host
135    )
136
137
138def script(vm_):
139    """
140    Return the script deployment object
141    """
142    return salt.utils.cloud.os_script(
143        config.get_cloud_config_value("script", vm_, __opts__),
144        vm_,
145        __opts__,
146        salt.utils.cloud.salt_config_to_yaml(
147            salt.utils.cloud.minion_config(__opts__, vm_)
148        ),
149    )
150
151
152def avail_locations(conn=None, call=None):
153    """
154    List available locations for Azure
155    """
156    if call == "action":
157        raise SaltCloudSystemExit(
158            "The avail_locations function must be called with "
159            "-f or --function, or with the --list-locations option"
160        )
161
162    if not conn:
163        conn = get_conn()
164
165    ret = {}
166    locations = conn.list_locations()
167    for location in locations:
168        ret[location.name] = {
169            "name": location.name,
170            "display_name": location.display_name,
171            "available_services": location.available_services,
172        }
173    return ret
174
175
176def avail_images(conn=None, call=None):
177    """
178    List available images for Azure
179    """
180    if call == "action":
181        raise SaltCloudSystemExit(
182            "The avail_images function must be called with "
183            "-f or --function, or with the --list-images option"
184        )
185
186    if not conn:
187        conn = get_conn()
188
189    ret = {}
190    for item in conn.list_os_images():
191        ret[item.name] = object_to_dict(item)
192    for item in conn.list_vm_images():
193        ret[item.name] = object_to_dict(item)
194    return ret
195
196
197def avail_sizes(call=None):
198    """
199    Return a list of sizes from Azure
200    """
201    if call == "action":
202        raise SaltCloudSystemExit(
203            "The avail_sizes function must be called with "
204            "-f or --function, or with the --list-sizes option"
205        )
206
207    conn = get_conn()
208    data = conn.list_role_sizes()
209    ret = {}
210    for item in data.role_sizes:
211        ret[item.name] = object_to_dict(item)
212    return ret
213
214
215def list_nodes(conn=None, call=None):
216    """
217    List VMs on this Azure account
218    """
219    if call == "action":
220        raise SaltCloudSystemExit(
221            "The list_nodes function must be called with -f or --function."
222        )
223
224    ret = {}
225    nodes = list_nodes_full(conn, call)
226    for node in nodes:
227        ret[node] = {"name": node}
228        for prop in ("id", "image", "size", "state", "private_ips", "public_ips"):
229            ret[node][prop] = nodes[node].get(prop)
230    return ret
231
232
233def list_nodes_full(conn=None, call=None):
234    """
235    List VMs on this Azure account, with full information
236    """
237    if call == "action":
238        raise SaltCloudSystemExit(
239            "The list_nodes_full function must be called with -f or --function."
240        )
241
242    if not conn:
243        conn = get_conn()
244
245    ret = {}
246    services = list_hosted_services(conn=conn, call=call)
247    for service in services:
248        for deployment in services[service]["deployments"]:
249            deploy_dict = services[service]["deployments"][deployment]
250            deploy_dict_no_role_info = copy.deepcopy(deploy_dict)
251            del deploy_dict_no_role_info["role_list"]
252            del deploy_dict_no_role_info["role_instance_list"]
253            roles = deploy_dict["role_list"]
254            for role in roles:
255                role_instances = deploy_dict["role_instance_list"]
256                ret[role] = roles[role]
257                ret[role].update(role_instances[role])
258                ret[role]["id"] = role
259                ret[role]["hosted_service"] = service
260                if role_instances[role]["power_state"] == "Started":
261                    ret[role]["state"] = "running"
262                elif role_instances[role]["power_state"] == "Stopped":
263                    ret[role]["state"] = "stopped"
264                else:
265                    ret[role]["state"] = "pending"
266                ret[role]["private_ips"] = []
267                ret[role]["public_ips"] = []
268                ret[role]["deployment"] = deploy_dict_no_role_info
269                ret[role]["url"] = deploy_dict["url"]
270                ip_address = role_instances[role]["ip_address"]
271                if ip_address:
272                    if salt.utils.cloud.is_public_ip(ip_address):
273                        ret[role]["public_ips"].append(ip_address)
274                    else:
275                        ret[role]["private_ips"].append(ip_address)
276                ret[role]["size"] = role_instances[role]["instance_size"]
277                ret[role]["image"] = roles[role]["role_info"]["os_virtual_hard_disk"][
278                    "source_image_name"
279                ]
280    return ret
281
282
283def list_hosted_services(conn=None, call=None):
284    """
285    List VMs on this Azure account, with full information
286    """
287    if call == "action":
288        raise SaltCloudSystemExit(
289            "The list_hosted_services function must be called with -f or --function"
290        )
291
292    if not conn:
293        conn = get_conn()
294
295    ret = {}
296    services = conn.list_hosted_services()
297    for service in services:
298        props = service.hosted_service_properties
299        ret[service.service_name] = {
300            "name": service.service_name,
301            "url": service.url,
302            "affinity_group": props.affinity_group,
303            "date_created": props.date_created,
304            "date_last_modified": props.date_last_modified,
305            "description": props.description,
306            "extended_properties": props.extended_properties,
307            "label": props.label,
308            "location": props.location,
309            "status": props.status,
310            "deployments": {},
311        }
312        deployments = conn.get_hosted_service_properties(
313            service_name=service.service_name, embed_detail=True
314        )
315        for deployment in deployments.deployments:
316            ret[service.service_name]["deployments"][deployment.name] = {
317                "configuration": deployment.configuration,
318                "created_time": deployment.created_time,
319                "deployment_slot": deployment.deployment_slot,
320                "extended_properties": deployment.extended_properties,
321                "input_endpoint_list": deployment.input_endpoint_list,
322                "label": deployment.label,
323                "last_modified_time": deployment.last_modified_time,
324                "locked": deployment.locked,
325                "name": deployment.name,
326                "persistent_vm_downtime_info": deployment.persistent_vm_downtime_info,
327                "private_id": deployment.private_id,
328                "role_instance_list": {},
329                "role_list": {},
330                "rollback_allowed": deployment.rollback_allowed,
331                "sdk_version": deployment.sdk_version,
332                "status": deployment.status,
333                "upgrade_domain_count": deployment.upgrade_domain_count,
334                "upgrade_status": deployment.upgrade_status,
335                "url": deployment.url,
336            }
337            for role_instance in deployment.role_instance_list:
338                ret[service.service_name]["deployments"][deployment.name][
339                    "role_instance_list"
340                ][role_instance.role_name] = {
341                    "fqdn": role_instance.fqdn,
342                    "instance_error_code": role_instance.instance_error_code,
343                    "instance_fault_domain": role_instance.instance_fault_domain,
344                    "instance_name": role_instance.instance_name,
345                    "instance_size": role_instance.instance_size,
346                    "instance_state_details": role_instance.instance_state_details,
347                    "instance_status": role_instance.instance_status,
348                    "instance_upgrade_domain": role_instance.instance_upgrade_domain,
349                    "ip_address": role_instance.ip_address,
350                    "power_state": role_instance.power_state,
351                    "role_name": role_instance.role_name,
352                }
353            for role in deployment.role_list:
354                ret[service.service_name]["deployments"][deployment.name]["role_list"][
355                    role.role_name
356                ] = {
357                    "role_name": role.role_name,
358                    "os_version": role.os_version,
359                }
360                role_info = conn.get_role(
361                    service_name=service.service_name,
362                    deployment_name=deployment.name,
363                    role_name=role.role_name,
364                )
365                ret[service.service_name]["deployments"][deployment.name]["role_list"][
366                    role.role_name
367                ]["role_info"] = {
368                    "availability_set_name": role_info.availability_set_name,
369                    "configuration_sets": role_info.configuration_sets,
370                    "data_virtual_hard_disks": role_info.data_virtual_hard_disks,
371                    "os_version": role_info.os_version,
372                    "role_name": role_info.role_name,
373                    "role_size": role_info.role_size,
374                    "role_type": role_info.role_type,
375                }
376                ret[service.service_name]["deployments"][deployment.name]["role_list"][
377                    role.role_name
378                ]["role_info"]["os_virtual_hard_disk"] = {
379                    "disk_label": role_info.os_virtual_hard_disk.disk_label,
380                    "disk_name": role_info.os_virtual_hard_disk.disk_name,
381                    "host_caching": role_info.os_virtual_hard_disk.host_caching,
382                    "media_link": role_info.os_virtual_hard_disk.media_link,
383                    "os": role_info.os_virtual_hard_disk.os,
384                    "source_image_name": role_info.os_virtual_hard_disk.source_image_name,
385                }
386    return ret
387
388
389def list_nodes_select(conn=None, call=None):
390    """
391    Return a list of the VMs that are on the provider, with select fields
392    """
393    if not conn:
394        conn = get_conn()
395
396    return salt.utils.cloud.list_nodes_select(
397        list_nodes_full(conn, "function"),
398        __opts__["query.selection"],
399        call,
400    )
401
402
403def show_instance(name, call=None):
404    """
405    Show the details from the provider concerning an instance
406    """
407    if call != "action":
408        raise SaltCloudSystemExit(
409            "The show_instance action must be called with -a or --action."
410        )
411
412    nodes = list_nodes_full()
413    # Find under which cloud service the name is listed, if any
414    if name not in nodes:
415        return {}
416    if "name" not in nodes[name]:
417        nodes[name]["name"] = nodes[name]["id"]
418    try:
419        __utils__["cloud.cache_node"](
420            nodes[name], _get_active_provider_name(), __opts__
421        )
422    except TypeError:
423        log.warning(
424            "Unable to show cache node data; this may be because the node has been"
425            " deleted"
426        )
427    return nodes[name]
428
429
430def create(vm_):
431    """
432    Create a single VM from a data dict
433    """
434    try:
435        # Check for required profile parameters before sending any API calls.
436        if (
437            vm_["profile"]
438            and config.is_profile_configured(
439                __opts__,
440                _get_active_provider_name() or "azure",
441                vm_["profile"],
442                vm_=vm_,
443            )
444            is False
445        ):
446            return False
447    except AttributeError:
448        pass
449
450    __utils__["cloud.fire_event"](
451        "event",
452        "starting create",
453        "salt/cloud/{}/creating".format(vm_["name"]),
454        args=__utils__["cloud.filter_event"](
455            "creating", vm_, ["name", "profile", "provider", "driver"]
456        ),
457        sock_dir=__opts__["sock_dir"],
458        transport=__opts__["transport"],
459    )
460
461    log.info("Creating Cloud VM %s", vm_["name"])
462    conn = get_conn()
463
464    label = vm_.get("label", vm_["name"])
465    service_name = vm_.get("service_name", vm_["name"])
466    service_kwargs = {
467        "service_name": service_name,
468        "label": label,
469        "description": vm_.get("desc", vm_["name"]),
470    }
471
472    loc_error = False
473    if "location" in vm_:
474        if "affinity_group" in vm_:
475            loc_error = True
476        else:
477            service_kwargs["location"] = vm_["location"]
478    elif "affinity_group" in vm_:
479        service_kwargs["affinity_group"] = vm_["affinity_group"]
480    else:
481        loc_error = True
482
483    if loc_error:
484        raise SaltCloudSystemExit(
485            "Either a location or affinity group must be specified, but not both"
486        )
487
488    ssh_port = config.get_cloud_config_value(
489        "port", vm_, __opts__, default=22, search_global=True
490    )
491
492    ssh_endpoint = azure.servicemanagement.ConfigurationSetInputEndpoint(
493        name="SSH",
494        protocol="TCP",
495        port=ssh_port,
496        local_port=22,
497    )
498
499    network_config = azure.servicemanagement.ConfigurationSet()
500    network_config.input_endpoints.input_endpoints.append(ssh_endpoint)
501    network_config.configuration_set_type = "NetworkConfiguration"
502
503    if "win_username" in vm_:
504        system_config = azure.servicemanagement.WindowsConfigurationSet(
505            computer_name=vm_["name"],
506            admin_username=vm_["win_username"],
507            admin_password=vm_["win_password"],
508        )
509
510        smb_port = "445"
511        if "smb_port" in vm_:
512            smb_port = vm_["smb_port"]
513
514        smb_endpoint = azure.servicemanagement.ConfigurationSetInputEndpoint(
515            name="SMB",
516            protocol="TCP",
517            port=smb_port,
518            local_port=smb_port,
519        )
520
521        network_config.input_endpoints.input_endpoints.append(smb_endpoint)
522
523        # Domain and WinRM configuration not yet supported by Salt Cloud
524        system_config.domain_join = None
525        system_config.win_rm = None
526
527    else:
528        system_config = azure.servicemanagement.LinuxConfigurationSet(
529            host_name=vm_["name"],
530            user_name=vm_["ssh_username"],
531            user_password=vm_["ssh_password"],
532            disable_ssh_password_authentication=False,
533        )
534
535    # TODO: Might need to create a storage account
536    media_link = vm_["media_link"]
537    # TODO: Probably better to use more than just the name in the media_link
538    media_link += "/{}.vhd".format(vm_["name"])
539    os_hd = azure.servicemanagement.OSVirtualHardDisk(vm_["image"], media_link)
540
541    vm_kwargs = {
542        "service_name": service_name,
543        "deployment_name": service_name,
544        "deployment_slot": vm_["slot"],
545        "label": label,
546        "role_name": vm_["name"],
547        "system_config": system_config,
548        "os_virtual_hard_disk": os_hd,
549        "role_size": vm_["size"],
550        "network_config": network_config,
551    }
552
553    if "virtual_network_name" in vm_:
554        vm_kwargs["virtual_network_name"] = vm_["virtual_network_name"]
555        if "subnet_name" in vm_:
556            network_config.subnet_names.append(vm_["subnet_name"])
557
558    log.debug("vm_kwargs: %s", vm_kwargs)
559
560    event_kwargs = {
561        "service_kwargs": service_kwargs.copy(),
562        "vm_kwargs": vm_kwargs.copy(),
563    }
564    del event_kwargs["vm_kwargs"]["system_config"]
565    del event_kwargs["vm_kwargs"]["os_virtual_hard_disk"]
566    del event_kwargs["vm_kwargs"]["network_config"]
567    __utils__["cloud.fire_event"](
568        "event",
569        "requesting instance",
570        "salt/cloud/{}/requesting".format(vm_["name"]),
571        args=__utils__["cloud.filter_event"](
572            "requesting", event_kwargs, list(event_kwargs)
573        ),
574        sock_dir=__opts__["sock_dir"],
575        transport=__opts__["transport"],
576    )
577    log.debug("vm_kwargs: %s", vm_kwargs)
578
579    # Azure lets you open winrm on a new VM
580    # Can open up specific ports in Azure; but not on Windows
581    try:
582        conn.create_hosted_service(**service_kwargs)
583    except AzureConflictHttpError:
584        log.debug("Cloud service already exists")
585    except Exception as exc:  # pylint: disable=broad-except
586        error = "The hosted service name is invalid."
587        if error in str(exc):
588            log.error(
589                "Error creating %s on Azure.\n\n"
590                "The hosted service name is invalid. The name can contain "
591                "only letters, numbers, and hyphens. The name must start with "
592                "a letter and must end with a letter or a number.",
593                vm_["name"],
594                # Show the traceback if the debug logging level is enabled
595                exc_info_on_loglevel=logging.DEBUG,
596            )
597        else:
598            log.error(
599                "Error creating %s on Azure\n\n"
600                "The following exception was thrown when trying to "
601                "run the initial deployment: \n%s",
602                vm_["name"],
603                exc,
604                # Show the traceback if the debug logging level is enabled
605                exc_info_on_loglevel=logging.DEBUG,
606            )
607        return False
608    try:
609        result = conn.create_virtual_machine_deployment(**vm_kwargs)
610        log.debug("Request ID for machine: %s", result.request_id)
611        _wait_for_async(conn, result.request_id)
612    except AzureConflictHttpError:
613        log.debug("Conflict error. The deployment may already exist, trying add_role")
614        # Deleting two useless keywords
615        del vm_kwargs["deployment_slot"]
616        del vm_kwargs["label"]
617        del vm_kwargs["virtual_network_name"]
618        result = conn.add_role(**vm_kwargs)  # pylint: disable=unexpected-keyword-arg
619        _wait_for_async(conn, result.request_id)
620    except Exception as exc:  # pylint: disable=broad-except
621        error = "The hosted service name is invalid."
622        if error in str(exc):
623            log.error(
624                "Error creating %s on Azure.\n\n"
625                "The VM name is invalid. The name can contain "
626                "only letters, numbers, and hyphens. The name must start with "
627                "a letter and must end with a letter or a number.",
628                vm_["name"],
629                # Show the traceback if the debug logging level is enabled
630                exc_info_on_loglevel=logging.DEBUG,
631            )
632        else:
633            log.error(
634                "Error creating %s on Azure.\n\n"
635                "The Virtual Machine could not be created. If you "
636                "are using an already existing Cloud Service, "
637                "make sure you set up the `port` variable corresponding "
638                "to the SSH port exists and that the port number is not "
639                "already in use.\nThe following exception was thrown when trying to "
640                "run the initial deployment: \n%s",
641                vm_["name"],
642                exc,
643                # Show the traceback if the debug logging level is enabled
644                exc_info_on_loglevel=logging.DEBUG,
645            )
646        return False
647
648    def wait_for_hostname():
649        """
650        Wait for the IP address to become available
651        """
652        try:
653            conn.get_role(service_name, service_name, vm_["name"])
654            data = show_instance(vm_["name"], call="action")
655            if "url" in data and data["url"] != "":
656                return data["url"]
657        except AzureMissingResourceHttpError:
658            pass
659        time.sleep(1)
660        return False
661
662    hostname = salt.utils.cloud.wait_for_fun(
663        wait_for_hostname,
664        timeout=config.get_cloud_config_value(
665            "wait_for_fun_timeout", vm_, __opts__, default=15 * 60
666        ),
667    )
668
669    if not hostname:
670        log.error("Failed to get a value for the hostname.")
671        return False
672
673    vm_["ssh_host"] = hostname.replace("http://", "").replace("/", "")
674    vm_["password"] = config.get_cloud_config_value("ssh_password", vm_, __opts__)
675    ret = __utils__["cloud.bootstrap"](vm_, __opts__)
676
677    # Attaching volumes
678    volumes = config.get_cloud_config_value(
679        "volumes", vm_, __opts__, search_global=True
680    )
681    if volumes:
682        __utils__["cloud.fire_event"](
683            "event",
684            "attaching volumes",
685            "salt/cloud/{}/attaching_volumes".format(vm_["name"]),
686            args=__utils__["cloud.filter_event"]("attaching_volumes", vm_, ["volumes"]),
687            sock_dir=__opts__["sock_dir"],
688            transport=__opts__["transport"],
689        )
690
691        log.info("Create and attach volumes to node %s", vm_["name"])
692        created = create_attach_volumes(
693            vm_["name"],
694            {
695                "volumes": volumes,
696                "service_name": service_name,
697                "deployment_name": vm_["name"],
698                "media_link": media_link,
699                "role_name": vm_["name"],
700                "del_all_vols_on_destroy": vm_.get(
701                    "set_del_all_vols_on_destroy", False
702                ),
703            },
704            call="action",
705        )
706        ret["Attached Volumes"] = created
707
708    data = show_instance(vm_["name"], call="action")
709    log.info("Created Cloud VM '%s'", vm_)
710    log.debug("'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data))
711
712    ret.update(data)
713
714    __utils__["cloud.fire_event"](
715        "event",
716        "created instance",
717        "salt/cloud/{}/created".format(vm_["name"]),
718        args=__utils__["cloud.filter_event"](
719            "created", vm_, ["name", "profile", "provider", "driver"]
720        ),
721        sock_dir=__opts__["sock_dir"],
722        transport=__opts__["transport"],
723    )
724
725    return ret
726
727
728def create_attach_volumes(name, kwargs, call=None, wait_to_finish=True):
729    """
730    Create and attach volumes to created node
731    """
732    if call != "action":
733        raise SaltCloudSystemExit(
734            "The create_attach_volumes action must be called with -a or --action."
735        )
736
737    if kwargs is None:
738        kwargs = {}
739
740    if isinstance(kwargs["volumes"], str):
741        volumes = salt.utils.yaml.safe_load(kwargs["volumes"])
742    else:
743        volumes = kwargs["volumes"]
744
745    # From the Azure .NET SDK doc
746    #
747    # The Create Data Disk operation adds a data disk to a virtual
748    # machine. There are three ways to create the data disk using the
749    # Add Data Disk operation.
750    #    Option 1 - Attach an empty data disk to
751    # the role by specifying the disk label and location of the disk
752    # image. Do not include the DiskName and SourceMediaLink elements in
753    # the request body. Include the MediaLink element and reference a
754    # blob that is in the same geographical region as the role. You can
755    # also omit the MediaLink element. In this usage, Azure will create
756    # the data disk in the storage account configured as default for the
757    # role.
758    #    Option 2 - Attach an existing data disk that is in the image
759    # repository. Do not include the DiskName and SourceMediaLink
760    # elements in the request body. Specify the data disk to use by
761    # including the DiskName element. Note: If included the in the
762    # response body, the MediaLink and LogicalDiskSizeInGB elements are
763    # ignored.
764    #    Option 3 - Specify the location of a blob in your storage
765    # account that contain a disk image to use. Include the
766    # SourceMediaLink element. Note: If the MediaLink element
767    # isincluded, it is ignored.  (see
768    # http://msdn.microsoft.com/en-us/library/windowsazure/jj157199.aspx
769    # for more information)
770    #
771    # Here only option 1 is implemented
772    conn = get_conn()
773    ret = []
774    for volume in volumes:
775        if "disk_name" in volume:
776            log.error("You cannot specify a disk_name. Only new volumes are allowed")
777            return False
778        # Use the size keyword to set a size, but you can use the
779        # azure name too. If neither is set, the disk has size 100GB
780        volume.setdefault("logical_disk_size_in_gb", volume.get("size", 100))
781        volume.setdefault("host_caching", "ReadOnly")
782        volume.setdefault("lun", 0)
783        # The media link is vm_name-disk-[0-15].vhd
784        volume.setdefault(
785            "media_link",
786            kwargs["media_link"][:-4] + "-disk-{}.vhd".format(volume["lun"]),
787        )
788        volume.setdefault(
789            "disk_label", kwargs["role_name"] + "-disk-{}".format(volume["lun"])
790        )
791        volume_dict = {"volume_name": volume["lun"], "disk_label": volume["disk_label"]}
792
793        # Preparing the volume dict to be passed with **
794        kwargs_add_data_disk = [
795            "lun",
796            "host_caching",
797            "media_link",
798            "disk_label",
799            "disk_name",
800            "logical_disk_size_in_gb",
801            "source_media_link",
802        ]
803        for key in set(volume.keys()) - set(kwargs_add_data_disk):
804            del volume[key]
805
806        attach = conn.add_data_disk(
807            kwargs["service_name"],
808            kwargs["deployment_name"],
809            kwargs["role_name"],
810            **volume
811        )
812        log.debug(attach)
813
814        # If attach is None then everything is fine
815        if attach:
816            msg = "{} attached to {} (aka {})".format(
817                volume_dict["volume_name"],
818                kwargs["role_name"],
819                name,
820            )
821            log.info(msg)
822            ret.append(msg)
823        else:
824            log.error("Error attaching %s on Azure", volume_dict)
825    return ret
826
827
828def create_attach_volumes(name, kwargs, call=None, wait_to_finish=True):
829    """
830    Create and attach volumes to created node
831    """
832    if call != "action":
833        raise SaltCloudSystemExit(
834            "The create_attach_volumes action must be called with -a or --action."
835        )
836
837    if kwargs is None:
838        kwargs = {}
839
840    if isinstance(kwargs["volumes"], str):
841        volumes = salt.utils.yaml.safe_load(kwargs["volumes"])
842    else:
843        volumes = kwargs["volumes"]
844
845    # From the Azure .NET SDK doc
846    #
847    # The Create Data Disk operation adds a data disk to a virtual
848    # machine. There are three ways to create the data disk using the
849    # Add Data Disk operation.
850    #    Option 1 - Attach an empty data disk to
851    # the role by specifying the disk label and location of the disk
852    # image. Do not include the DiskName and SourceMediaLink elements in
853    # the request body. Include the MediaLink element and reference a
854    # blob that is in the same geographical region as the role. You can
855    # also omit the MediaLink element. In this usage, Azure will create
856    # the data disk in the storage account configured as default for the
857    # role.
858    #    Option 2 - Attach an existing data disk that is in the image
859    # repository. Do not include the DiskName and SourceMediaLink
860    # elements in the request body. Specify the data disk to use by
861    # including the DiskName element. Note: If included the in the
862    # response body, the MediaLink and LogicalDiskSizeInGB elements are
863    # ignored.
864    #    Option 3 - Specify the location of a blob in your storage
865    # account that contain a disk image to use. Include the
866    # SourceMediaLink element. Note: If the MediaLink element
867    # isincluded, it is ignored.  (see
868    # http://msdn.microsoft.com/en-us/library/windowsazure/jj157199.aspx
869    # for more information)
870    #
871    # Here only option 1 is implemented
872    conn = get_conn()
873    ret = []
874    for volume in volumes:
875        if "disk_name" in volume:
876            log.error("You cannot specify a disk_name. Only new volumes are allowed")
877            return False
878        # Use the size keyword to set a size, but you can use the
879        # azure name too. If neither is set, the disk has size 100GB
880        volume.setdefault("logical_disk_size_in_gb", volume.get("size", 100))
881        volume.setdefault("host_caching", "ReadOnly")
882        volume.setdefault("lun", 0)
883        # The media link is vm_name-disk-[0-15].vhd
884        volume.setdefault(
885            "media_link",
886            kwargs["media_link"][:-4] + "-disk-{}.vhd".format(volume["lun"]),
887        )
888        volume.setdefault(
889            "disk_label", kwargs["role_name"] + "-disk-{}".format(volume["lun"])
890        )
891        volume_dict = {"volume_name": volume["lun"], "disk_label": volume["disk_label"]}
892
893        # Preparing the volume dict to be passed with **
894        kwargs_add_data_disk = [
895            "lun",
896            "host_caching",
897            "media_link",
898            "disk_label",
899            "disk_name",
900            "logical_disk_size_in_gb",
901            "source_media_link",
902        ]
903        for key in set(volume.keys()) - set(kwargs_add_data_disk):
904            del volume[key]
905
906        result = conn.add_data_disk(
907            kwargs["service_name"],
908            kwargs["deployment_name"],
909            kwargs["role_name"],
910            **volume
911        )
912        _wait_for_async(conn, result.request_id)
913
914        msg = "{} attached to {} (aka {})".format(
915            volume_dict["volume_name"], kwargs["role_name"], name
916        )
917        log.info(msg)
918        ret.append(msg)
919    return ret
920
921
922# Helper function for azure tests
923def _wait_for_async(conn, request_id):
924    """
925    Helper function for azure tests
926    """
927    count = 0
928    log.debug("Waiting for asynchronous operation to complete")
929    result = conn.get_operation_status(request_id)
930    while result.status == "InProgress":
931        count = count + 1
932        if count > 120:
933            raise ValueError(
934                "Timed out waiting for asynchronous operation to complete."
935            )
936        time.sleep(5)
937        result = conn.get_operation_status(request_id)
938
939    if result.status != "Succeeded":
940        raise AzureException(
941            "Operation failed. {message} ({code})".format(
942                message=result.error.message, code=result.error.code
943            )
944        )
945
946
947def destroy(name, conn=None, call=None, kwargs=None):
948    """
949    Destroy a VM
950
951    CLI Examples:
952
953    .. code-block:: bash
954
955        salt-cloud -d myminion
956        salt-cloud -a destroy myminion service_name=myservice
957    """
958    if call == "function":
959        raise SaltCloudSystemExit(
960            "The destroy action must be called with -d, --destroy, -a or --action."
961        )
962
963    if not conn:
964        conn = get_conn()
965
966    if kwargs is None:
967        kwargs = {}
968
969    instance_data = show_instance(name, call="action")
970    service_name = instance_data["deployment"]["name"]
971    disk_name = instance_data["role_info"]["os_virtual_hard_disk"]["disk_name"]
972
973    ret = {}
974    # TODO: Add the ability to delete or not delete a hosted service when
975    # deleting a VM
976    try:
977        log.debug("Deleting role")
978        result = conn.delete_role(service_name, service_name, name)
979        delete_type = "delete_role"
980    except AzureException:
981        log.debug("Failed to delete role, deleting deployment")
982        try:
983            result = conn.delete_deployment(service_name, service_name)
984        except AzureConflictHttpError as exc:
985            log.error(exc.message)
986            raise SaltCloudSystemExit("{}: {}".format(name, exc.message))
987        delete_type = "delete_deployment"
988    _wait_for_async(conn, result.request_id)
989    ret[name] = {
990        delete_type: {"request_id": result.request_id},
991    }
992    if __opts__.get("update_cachedir", False) is True:
993        __utils__["cloud.delete_minion_cachedir"](
994            name, _get_active_provider_name().split(":")[0], __opts__
995        )
996
997    cleanup_disks = config.get_cloud_config_value(
998        "cleanup_disks",
999        get_configured_provider(),
1000        __opts__,
1001        search_global=False,
1002        default=False,
1003    )
1004    if cleanup_disks:
1005        cleanup_vhds = kwargs.get(
1006            "delete_vhd",
1007            config.get_cloud_config_value(
1008                "cleanup_vhds",
1009                get_configured_provider(),
1010                __opts__,
1011                search_global=False,
1012                default=False,
1013            ),
1014        )
1015        log.debug("Deleting disk %s", disk_name)
1016        if cleanup_vhds:
1017            log.debug("Deleting vhd")
1018
1019        def wait_for_destroy():
1020            """
1021            Wait for the VM to be deleted
1022            """
1023            try:
1024                data = delete_disk(
1025                    kwargs={"name": disk_name, "delete_vhd": cleanup_vhds},
1026                    call="function",
1027                )
1028                return data
1029            except AzureConflictHttpError:
1030                log.debug("Waiting for VM to be destroyed...")
1031            time.sleep(5)
1032            return False
1033
1034        data = salt.utils.cloud.wait_for_fun(
1035            wait_for_destroy,
1036            timeout=config.get_cloud_config_value(
1037                "wait_for_fun_timeout", {}, __opts__, default=15 * 60
1038            ),
1039        )
1040        ret[name]["delete_disk"] = {
1041            "name": disk_name,
1042            "delete_vhd": cleanup_vhds,
1043            "data": data,
1044        }
1045
1046        # Services can't be cleaned up unless disks are too
1047        cleanup_services = config.get_cloud_config_value(
1048            "cleanup_services",
1049            get_configured_provider(),
1050            __opts__,
1051            search_global=False,
1052            default=False,
1053        )
1054        if cleanup_services:
1055            log.debug("Deleting service %s", service_name)
1056
1057            def wait_for_disk_delete():
1058                """
1059                Wait for the disk to be deleted
1060                """
1061                try:
1062                    data = delete_service(
1063                        kwargs={"name": service_name}, call="function"
1064                    )
1065                    return data
1066                except AzureConflictHttpError:
1067                    log.debug("Waiting for disk to be deleted...")
1068                time.sleep(5)
1069                return False
1070
1071            data = salt.utils.cloud.wait_for_fun(
1072                wait_for_disk_delete,
1073                timeout=config.get_cloud_config_value(
1074                    "wait_for_fun_timeout", {}, __opts__, default=15 * 60
1075                ),
1076            )
1077            ret[name]["delete_services"] = {"name": service_name, "data": data}
1078
1079    return ret
1080
1081
1082def list_storage_services(conn=None, call=None):
1083    """
1084    List VMs on this Azure account, with full information
1085    """
1086    if call != "function":
1087        raise SaltCloudSystemExit(
1088            "The list_storage_services function must be called with -f or --function."
1089        )
1090
1091    if not conn:
1092        conn = get_conn()
1093
1094    ret = {}
1095    accounts = conn.list_storage_accounts()
1096    for service in accounts.storage_services:
1097        ret[service.service_name] = {
1098            "capabilities": service.capabilities,
1099            "service_name": service.service_name,
1100            "storage_service_properties": service.storage_service_properties,
1101            "extended_properties": service.extended_properties,
1102            "storage_service_keys": service.storage_service_keys,
1103            "url": service.url,
1104        }
1105    return ret
1106
1107
1108def get_operation_status(kwargs=None, conn=None, call=None):
1109    """
1110    .. versionadded:: 2015.8.0
1111
1112    Get Operation Status, based on a request ID
1113
1114    CLI Example:
1115
1116    .. code-block:: bash
1117
1118        salt-cloud -f get_operation_status my-azure id=0123456789abcdef0123456789abcdef
1119    """
1120    if call != "function":
1121        raise SaltCloudSystemExit(
1122            "The show_instance function must be called with -f or --function."
1123        )
1124
1125    if kwargs is None:
1126        kwargs = {}
1127
1128    if "id" not in kwargs:
1129        raise SaltCloudSystemExit('A request ID must be specified as "id"')
1130
1131    if not conn:
1132        conn = get_conn()
1133
1134    data = conn.get_operation_status(kwargs["id"])
1135    ret = {
1136        "http_status_code": data.http_status_code,
1137        "id": kwargs["id"],
1138        "status": data.status,
1139    }
1140    if hasattr(data.error, "code"):
1141        ret["error"] = {
1142            "code": data.error.code,
1143            "message": data.error.message,
1144        }
1145
1146    return ret
1147
1148
1149def list_storage(kwargs=None, conn=None, call=None):
1150    """
1151    .. versionadded:: 2015.8.0
1152
1153    List storage accounts associated with the account
1154
1155    CLI Example:
1156
1157    .. code-block:: bash
1158
1159        salt-cloud -f list_storage my-azure
1160    """
1161    if call != "function":
1162        raise SaltCloudSystemExit(
1163            "The list_storage function must be called with -f or --function."
1164        )
1165
1166    if not conn:
1167        conn = get_conn()
1168
1169    data = conn.list_storage_accounts()
1170    pprint.pprint(dir(data))
1171    ret = {}
1172    for item in data.storage_services:
1173        ret[item.service_name] = object_to_dict(item)
1174    return ret
1175
1176
1177def show_storage(kwargs=None, conn=None, call=None):
1178    """
1179    .. versionadded:: 2015.8.0
1180
1181    List storage service properties
1182
1183    CLI Example:
1184
1185    .. code-block:: bash
1186
1187        salt-cloud -f show_storage my-azure name=my_storage
1188    """
1189    if call != "function":
1190        raise SaltCloudSystemExit(
1191            "The show_storage function must be called with -f or --function."
1192        )
1193
1194    if not conn:
1195        conn = get_conn()
1196
1197    if kwargs is None:
1198        kwargs = {}
1199
1200    if "name" not in kwargs:
1201        raise SaltCloudSystemExit('A name must be specified as "name"')
1202
1203    data = conn.get_storage_account_properties(
1204        kwargs["name"],
1205    )
1206    return object_to_dict(data)
1207
1208
1209# To reflect the Azure API
1210get_storage = show_storage
1211
1212
1213def show_storage_keys(kwargs=None, conn=None, call=None):
1214    """
1215    .. versionadded:: 2015.8.0
1216
1217    Show storage account keys
1218
1219    CLI Example:
1220
1221    .. code-block:: bash
1222
1223        salt-cloud -f show_storage_keys my-azure name=my_storage
1224    """
1225    if call != "function":
1226        raise SaltCloudSystemExit(
1227            "The show_storage_keys function must be called with -f or --function."
1228        )
1229
1230    if not conn:
1231        conn = get_conn()
1232
1233    if kwargs is None:
1234        kwargs = {}
1235
1236    if "name" not in kwargs:
1237        raise SaltCloudSystemExit('A name must be specified as "name"')
1238
1239    try:
1240        data = conn.get_storage_account_keys(
1241            kwargs["name"],
1242        )
1243    except AzureMissingResourceHttpError as exc:
1244        storage_data = show_storage(kwargs={"name": kwargs["name"]}, call="function")
1245        if storage_data["storage_service_properties"]["status"] == "Creating":
1246            raise SaltCloudSystemExit(
1247                "The storage account keys have not yet been created."
1248            )
1249        else:
1250            raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
1251    return object_to_dict(data)
1252
1253
1254# To reflect the Azure API
1255get_storage_keys = show_storage_keys
1256
1257
1258def create_storage(kwargs=None, conn=None, call=None):
1259    """
1260    .. versionadded:: 2015.8.0
1261
1262    Create a new storage account
1263
1264    CLI Example:
1265
1266    .. code-block:: bash
1267
1268        salt-cloud -f create_storage my-azure name=my_storage label=my_storage location='West US'
1269    """
1270    if call != "function":
1271        raise SaltCloudSystemExit(
1272            "The show_storage function must be called with -f or --function."
1273        )
1274
1275    if kwargs is None:
1276        kwargs = {}
1277
1278    if not conn:
1279        conn = get_conn()
1280
1281    if "name" not in kwargs:
1282        raise SaltCloudSystemExit('A name must be specified as "name"')
1283
1284    if "description" not in kwargs:
1285        raise SaltCloudSystemExit('A description must be specified as "description"')
1286
1287    if "label" not in kwargs:
1288        raise SaltCloudSystemExit('A label must be specified as "label"')
1289
1290    if "location" not in kwargs and "affinity_group" not in kwargs:
1291        raise SaltCloudSystemExit(
1292            "Either a location or an affinity_group must be specified (but not both)"
1293        )
1294
1295    try:
1296        data = conn.create_storage_account(
1297            service_name=kwargs["name"],
1298            label=kwargs["label"],
1299            description=kwargs.get("description", None),
1300            location=kwargs.get("location", None),
1301            affinity_group=kwargs.get("affinity_group", None),
1302            extended_properties=kwargs.get("extended_properties", None),
1303            geo_replication_enabled=kwargs.get("geo_replication_enabled", None),
1304            account_type=kwargs.get("account_type", "Standard_GRS"),
1305        )
1306        return {"Success": "The storage account was successfully created"}
1307    except AzureConflictHttpError:
1308        raise SaltCloudSystemExit(
1309            "There was a conflict. This usually means that the storage account already"
1310            " exists."
1311        )
1312
1313
1314def update_storage(kwargs=None, conn=None, call=None):
1315    """
1316    .. versionadded:: 2015.8.0
1317
1318    Update a storage account's properties
1319
1320    CLI Example:
1321
1322    .. code-block:: bash
1323
1324        salt-cloud -f update_storage my-azure name=my_storage label=my_storage
1325    """
1326    if call != "function":
1327        raise SaltCloudSystemExit(
1328            "The show_storage function must be called with -f or --function."
1329        )
1330
1331    if not conn:
1332        conn = get_conn()
1333
1334    if kwargs is None:
1335        kwargs = {}
1336
1337    if "name" not in kwargs:
1338        raise SaltCloudSystemExit('A name must be specified as "name"')
1339
1340    data = conn.update_storage_account(
1341        service_name=kwargs["name"],
1342        label=kwargs.get("label", None),
1343        description=kwargs.get("description", None),
1344        extended_properties=kwargs.get("extended_properties", None),
1345        geo_replication_enabled=kwargs.get("geo_replication_enabled", None),
1346        account_type=kwargs.get("account_type", "Standard_GRS"),
1347    )
1348    return show_storage(kwargs={"name": kwargs["name"]}, call="function")
1349
1350
1351def regenerate_storage_keys(kwargs=None, conn=None, call=None):
1352    """
1353    .. versionadded:: 2015.8.0
1354
1355    Regenerate storage account keys. Requires a key_type ("primary" or
1356    "secondary") to be specified.
1357
1358    CLI Example:
1359
1360    .. code-block:: bash
1361
1362        salt-cloud -f regenerate_storage_keys my-azure name=my_storage key_type=primary
1363    """
1364    if call != "function":
1365        raise SaltCloudSystemExit(
1366            "The show_storage function must be called with -f or --function."
1367        )
1368
1369    if not conn:
1370        conn = get_conn()
1371
1372    if kwargs is None:
1373        kwargs = {}
1374
1375    if "name" not in kwargs:
1376        raise SaltCloudSystemExit('A name must be specified as "name"')
1377
1378    if "key_type" not in kwargs or kwargs["key_type"] not in ("primary", "secondary"):
1379        raise SaltCloudSystemExit(
1380            'A key_type must be specified ("primary" or "secondary")'
1381        )
1382
1383    try:
1384        data = conn.regenerate_storage_account_keys(
1385            service_name=kwargs["name"],
1386            key_type=kwargs["key_type"],
1387        )
1388        return show_storage_keys(kwargs={"name": kwargs["name"]}, call="function")
1389    except AzureConflictHttpError:
1390        raise SaltCloudSystemExit(
1391            "There was a conflict. This usually means that the storage account already"
1392            " exists."
1393        )
1394
1395
1396def delete_storage(kwargs=None, conn=None, call=None):
1397    """
1398    .. versionadded:: 2015.8.0
1399
1400    Delete a specific storage account
1401
1402    CLI Examples:
1403
1404    .. code-block:: bash
1405
1406        salt-cloud -f delete_storage my-azure name=my_storage
1407    """
1408    if call != "function":
1409        raise SaltCloudSystemExit(
1410            "The delete_storage function must be called with -f or --function."
1411        )
1412
1413    if kwargs is None:
1414        kwargs = {}
1415
1416    if "name" not in kwargs:
1417        raise SaltCloudSystemExit('A name must be specified as "name"')
1418
1419    if not conn:
1420        conn = get_conn()
1421
1422    try:
1423        data = conn.delete_storage_account(kwargs["name"])
1424        return {"Success": "The storage account was successfully deleted"}
1425    except AzureMissingResourceHttpError as exc:
1426        raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
1427
1428
1429def list_services(kwargs=None, conn=None, call=None):
1430    """
1431    .. versionadded:: 2015.8.0
1432
1433    List hosted services associated with the account
1434
1435    CLI Example:
1436
1437    .. code-block:: bash
1438
1439        salt-cloud -f list_services my-azure
1440    """
1441    if call != "function":
1442        raise SaltCloudSystemExit(
1443            "The list_services function must be called with -f or --function."
1444        )
1445
1446    if not conn:
1447        conn = get_conn()
1448
1449    data = conn.list_hosted_services()
1450    ret = {}
1451    for item in data.hosted_services:
1452        ret[item.service_name] = object_to_dict(item)
1453        ret[item.service_name]["name"] = item.service_name
1454    return ret
1455
1456
1457def show_service(kwargs=None, conn=None, call=None):
1458    """
1459    .. versionadded:: 2015.8.0
1460
1461    List hosted service properties
1462
1463    CLI Example:
1464
1465    .. code-block:: bash
1466
1467        salt-cloud -f show_service my-azure name=my_service
1468    """
1469    if call != "function":
1470        raise SaltCloudSystemExit(
1471            "The show_service function must be called with -f or --function."
1472        )
1473
1474    if not conn:
1475        conn = get_conn()
1476
1477    if kwargs is None:
1478        kwargs = {}
1479
1480    if "name" not in kwargs:
1481        raise SaltCloudSystemExit('A name must be specified as "name"')
1482
1483    data = conn.get_hosted_service_properties(
1484        kwargs["name"], kwargs.get("details", False)
1485    )
1486    ret = object_to_dict(data)
1487    return ret
1488
1489
1490def create_service(kwargs=None, conn=None, call=None):
1491    """
1492    .. versionadded:: 2015.8.0
1493
1494    Create a new hosted service
1495
1496    CLI Example:
1497
1498    .. code-block:: bash
1499
1500        salt-cloud -f create_service my-azure name=my_service label=my_service location='West US'
1501    """
1502    if call != "function":
1503        raise SaltCloudSystemExit(
1504            "The create_service function must be called with -f or --function."
1505        )
1506
1507    if not conn:
1508        conn = get_conn()
1509
1510    if kwargs is None:
1511        kwargs = {}
1512
1513    if "name" not in kwargs:
1514        raise SaltCloudSystemExit('A name must be specified as "name"')
1515
1516    if "label" not in kwargs:
1517        raise SaltCloudSystemExit('A label must be specified as "label"')
1518
1519    if "location" not in kwargs and "affinity_group" not in kwargs:
1520        raise SaltCloudSystemExit(
1521            "Either a location or an affinity_group must be specified (but not both)"
1522        )
1523
1524    try:
1525        data = conn.create_hosted_service(
1526            kwargs["name"],
1527            kwargs["label"],
1528            kwargs.get("description", None),
1529            kwargs.get("location", None),
1530            kwargs.get("affinity_group", None),
1531            kwargs.get("extended_properties", None),
1532        )
1533        return {"Success": "The service was successfully created"}
1534    except AzureConflictHttpError:
1535        raise SaltCloudSystemExit(
1536            "There was a conflict. This usually means that the service already exists."
1537        )
1538
1539
1540def delete_service(kwargs=None, conn=None, call=None):
1541    """
1542    .. versionadded:: 2015.8.0
1543
1544    Delete a specific service associated with the account
1545
1546    CLI Examples:
1547
1548    .. code-block:: bash
1549
1550        salt-cloud -f delete_service my-azure name=my_service
1551    """
1552    if call != "function":
1553        raise SaltCloudSystemExit(
1554            "The delete_service function must be called with -f or --function."
1555        )
1556
1557    if kwargs is None:
1558        kwargs = {}
1559
1560    if "name" not in kwargs:
1561        raise SaltCloudSystemExit('A name must be specified as "name"')
1562
1563    if not conn:
1564        conn = get_conn()
1565
1566    try:
1567        conn.delete_hosted_service(kwargs["name"])
1568        return {"Success": "The service was successfully deleted"}
1569    except AzureMissingResourceHttpError as exc:
1570        raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
1571
1572
1573def list_disks(kwargs=None, conn=None, call=None):
1574    """
1575    .. versionadded:: 2015.8.0
1576
1577    List disks associated with the account
1578
1579    CLI Example:
1580
1581    .. code-block:: bash
1582
1583        salt-cloud -f list_disks my-azure
1584    """
1585    if call != "function":
1586        raise SaltCloudSystemExit(
1587            "The list_disks function must be called with -f or --function."
1588        )
1589
1590    if not conn:
1591        conn = get_conn()
1592
1593    data = conn.list_disks()
1594    ret = {}
1595    for item in data.disks:
1596        ret[item.name] = object_to_dict(item)
1597    return ret
1598
1599
1600def show_disk(kwargs=None, conn=None, call=None):
1601    """
1602    .. versionadded:: 2015.8.0
1603
1604    Return information about a disk
1605
1606    CLI Example:
1607
1608    .. code-block:: bash
1609
1610        salt-cloud -f show_disk my-azure name=my_disk
1611    """
1612    if call != "function":
1613        raise SaltCloudSystemExit(
1614            "The get_disk function must be called with -f or --function."
1615        )
1616
1617    if not conn:
1618        conn = get_conn()
1619
1620    if kwargs is None:
1621        kwargs = {}
1622
1623    if "name" not in kwargs:
1624        raise SaltCloudSystemExit('A name must be specified as "name"')
1625
1626    data = conn.get_disk(kwargs["name"])
1627    return object_to_dict(data)
1628
1629
1630# For consistency with Azure SDK
1631get_disk = show_disk
1632
1633
1634def cleanup_unattached_disks(kwargs=None, conn=None, call=None):
1635    """
1636    .. versionadded:: 2015.8.0
1637
1638    Cleans up all disks associated with the account, which are not attached.
1639    *** CAUTION *** This is a destructive function with no undo button, and no
1640    "Are you sure?" confirmation!
1641
1642    CLI Examples:
1643
1644    .. code-block:: bash
1645
1646        salt-cloud -f cleanup_unattached_disks my-azure name=my_disk
1647        salt-cloud -f cleanup_unattached_disks my-azure name=my_disk delete_vhd=True
1648    """
1649    if call != "function":
1650        raise SaltCloudSystemExit(
1651            "The delete_disk function must be called with -f or --function."
1652        )
1653
1654    if kwargs is None:
1655        kwargs = {}
1656
1657    disks = list_disks(kwargs=kwargs, conn=conn, call="function")
1658    for disk in disks:
1659        if disks[disk]["attached_to"] is None:
1660            del_kwargs = {
1661                "name": disks[disk]["name"],
1662                "delete_vhd": kwargs.get("delete_vhd", False),
1663            }
1664            log.info(
1665                "Deleting disk %s, deleting VHD: %s",
1666                del_kwargs["name"],
1667                del_kwargs["delete_vhd"],
1668            )
1669            data = delete_disk(kwargs=del_kwargs, call="function")
1670    return True
1671
1672
1673def delete_disk(kwargs=None, conn=None, call=None):
1674    """
1675    .. versionadded:: 2015.8.0
1676
1677    Delete a specific disk associated with the account
1678
1679    CLI Examples:
1680
1681    .. code-block:: bash
1682
1683        salt-cloud -f delete_disk my-azure name=my_disk
1684        salt-cloud -f delete_disk my-azure name=my_disk delete_vhd=True
1685    """
1686    if call != "function":
1687        raise SaltCloudSystemExit(
1688            "The delete_disk function must be called with -f or --function."
1689        )
1690
1691    if kwargs is None:
1692        kwargs = {}
1693
1694    if "name" not in kwargs:
1695        raise SaltCloudSystemExit('A name must be specified as "name"')
1696
1697    if not conn:
1698        conn = get_conn()
1699
1700    try:
1701        data = conn.delete_disk(kwargs["name"], kwargs.get("delete_vhd", False))
1702        return {"Success": "The disk was successfully deleted"}
1703    except AzureMissingResourceHttpError as exc:
1704        raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
1705
1706
1707def update_disk(kwargs=None, conn=None, call=None):
1708    """
1709    .. versionadded:: 2015.8.0
1710
1711    Update a disk's properties
1712
1713    CLI Example:
1714
1715    .. code-block:: bash
1716
1717        salt-cloud -f update_disk my-azure name=my_disk label=my_disk
1718        salt-cloud -f update_disk my-azure name=my_disk new_name=another_disk
1719    """
1720    if call != "function":
1721        raise SaltCloudSystemExit(
1722            "The show_disk function must be called with -f or --function."
1723        )
1724
1725    if not conn:
1726        conn = get_conn()
1727
1728    if kwargs is None:
1729        kwargs = {}
1730
1731    if "name" not in kwargs:
1732        raise SaltCloudSystemExit('A name must be specified as "name"')
1733
1734    old_data = show_disk(kwargs={"name": kwargs["name"]}, call="function")
1735    data = conn.update_disk(
1736        disk_name=kwargs["name"],
1737        has_operating_system=kwargs.get(
1738            "has_operating_system", old_data["has_operating_system"]
1739        ),
1740        label=kwargs.get("label", old_data["label"]),
1741        media_link=kwargs.get("media_link", old_data["media_link"]),
1742        name=kwargs.get("new_name", old_data["name"]),
1743        os=kwargs.get("os", old_data["os"]),
1744    )
1745    return show_disk(kwargs={"name": kwargs["name"]}, call="function")
1746
1747
1748def list_service_certificates(kwargs=None, conn=None, call=None):
1749    """
1750    .. versionadded:: 2015.8.0
1751
1752    List certificates associated with the service
1753
1754    CLI Example:
1755
1756    .. code-block:: bash
1757
1758        salt-cloud -f list_service_certificates my-azure name=my_service
1759    """
1760    if call != "function":
1761        raise SaltCloudSystemExit(
1762            "The list_service_certificates function must be called with -f or"
1763            " --function."
1764        )
1765
1766    if kwargs is None:
1767        kwargs = {}
1768
1769    if "name" not in kwargs:
1770        raise SaltCloudSystemExit('A service name must be specified as "name"')
1771
1772    if not conn:
1773        conn = get_conn()
1774
1775    data = conn.list_service_certificates(service_name=kwargs["name"])
1776    ret = {}
1777    for item in data.certificates:
1778        ret[item.thumbprint] = object_to_dict(item)
1779    return ret
1780
1781
1782def show_service_certificate(kwargs=None, conn=None, call=None):
1783    """
1784    .. versionadded:: 2015.8.0
1785
1786    Return information about a service certificate
1787
1788    CLI Example:
1789
1790    .. code-block:: bash
1791
1792        salt-cloud -f show_service_certificate my-azure name=my_service_certificate \\
1793            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
1794    """
1795    if call != "function":
1796        raise SaltCloudSystemExit(
1797            "The get_service_certificate function must be called with -f or --function."
1798        )
1799
1800    if not conn:
1801        conn = get_conn()
1802
1803    if kwargs is None:
1804        kwargs = {}
1805
1806    if "name" not in kwargs:
1807        raise SaltCloudSystemExit('A service name must be specified as "name"')
1808
1809    if "thumbalgorithm" not in kwargs:
1810        raise SaltCloudSystemExit(
1811            'A thumbalgorithm must be specified as "thumbalgorithm"'
1812        )
1813
1814    if "thumbprint" not in kwargs:
1815        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')
1816
1817    data = conn.get_service_certificate(
1818        kwargs["name"],
1819        kwargs["thumbalgorithm"],
1820        kwargs["thumbprint"],
1821    )
1822    return object_to_dict(data)
1823
1824
1825# For consistency with Azure SDK
1826get_service_certificate = show_service_certificate
1827
1828
1829def add_service_certificate(kwargs=None, conn=None, call=None):
1830    """
1831    .. versionadded:: 2015.8.0
1832
1833    Add a new service certificate
1834
1835    CLI Example:
1836
1837    .. code-block:: bash
1838
1839        salt-cloud -f add_service_certificate my-azure name=my_service_certificate \\
1840            data='...CERT_DATA...' certificate_format=sha1 password=verybadpass
1841    """
1842    if call != "function":
1843        raise SaltCloudSystemExit(
1844            "The add_service_certificate function must be called with -f or --function."
1845        )
1846
1847    if not conn:
1848        conn = get_conn()
1849
1850    if kwargs is None:
1851        kwargs = {}
1852
1853    if "name" not in kwargs:
1854        raise SaltCloudSystemExit('A name must be specified as "name"')
1855
1856    if "data" not in kwargs:
1857        raise SaltCloudSystemExit('Certificate data must be specified as "data"')
1858
1859    if "certificate_format" not in kwargs:
1860        raise SaltCloudSystemExit(
1861            'A certificate_format must be specified as "certificate_format"'
1862        )
1863
1864    if "password" not in kwargs:
1865        raise SaltCloudSystemExit('A password must be specified as "password"')
1866
1867    try:
1868        data = conn.add_service_certificate(
1869            kwargs["name"],
1870            kwargs["data"],
1871            kwargs["certificate_format"],
1872            kwargs["password"],
1873        )
1874        return {"Success": "The service certificate was successfully added"}
1875    except AzureConflictHttpError:
1876        raise SaltCloudSystemExit(
1877            "There was a conflict. This usually means that the "
1878            "service certificate already exists."
1879        )
1880
1881
1882def delete_service_certificate(kwargs=None, conn=None, call=None):
1883    """
1884    .. versionadded:: 2015.8.0
1885
1886    Delete a specific certificate associated with the service
1887
1888    CLI Examples:
1889
1890    .. code-block:: bash
1891
1892        salt-cloud -f delete_service_certificate my-azure name=my_service_certificate \\
1893            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
1894    """
1895    if call != "function":
1896        raise SaltCloudSystemExit(
1897            "The delete_service_certificate function must be called with -f or"
1898            " --function."
1899        )
1900
1901    if kwargs is None:
1902        kwargs = {}
1903
1904    if "name" not in kwargs:
1905        raise SaltCloudSystemExit('A name must be specified as "name"')
1906
1907    if "thumbalgorithm" not in kwargs:
1908        raise SaltCloudSystemExit(
1909            'A thumbalgorithm must be specified as "thumbalgorithm"'
1910        )
1911
1912    if "thumbprint" not in kwargs:
1913        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')
1914
1915    if not conn:
1916        conn = get_conn()
1917
1918    try:
1919        data = conn.delete_service_certificate(
1920            kwargs["name"],
1921            kwargs["thumbalgorithm"],
1922            kwargs["thumbprint"],
1923        )
1924        return {"Success": "The service certificate was successfully deleted"}
1925    except AzureMissingResourceHttpError as exc:
1926        raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
1927
1928
1929def list_management_certificates(kwargs=None, conn=None, call=None):
1930    """
1931    .. versionadded:: 2015.8.0
1932
1933    List management certificates associated with the subscription
1934
1935    CLI Example:
1936
1937    .. code-block:: bash
1938
1939        salt-cloud -f list_management_certificates my-azure name=my_management
1940    """
1941    if call != "function":
1942        raise SaltCloudSystemExit(
1943            "The list_management_certificates function must be called with -f or"
1944            " --function."
1945        )
1946
1947    if not conn:
1948        conn = get_conn()
1949
1950    data = conn.list_management_certificates()
1951    ret = {}
1952    for item in data.subscription_certificates:
1953        ret[item.subscription_certificate_thumbprint] = object_to_dict(item)
1954    return ret
1955
1956
1957def show_management_certificate(kwargs=None, conn=None, call=None):
1958    """
1959    .. versionadded:: 2015.8.0
1960
1961    Return information about a management_certificate
1962
1963    CLI Example:
1964
1965    .. code-block:: bash
1966
1967        salt-cloud -f get_management_certificate my-azure name=my_management_certificate \\
1968            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
1969    """
1970    if call != "function":
1971        raise SaltCloudSystemExit(
1972            "The get_management_certificate function must be called with -f or"
1973            " --function."
1974        )
1975
1976    if not conn:
1977        conn = get_conn()
1978
1979    if kwargs is None:
1980        kwargs = {}
1981
1982    if "thumbprint" not in kwargs:
1983        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')
1984
1985    data = conn.get_management_certificate(kwargs["thumbprint"])
1986    return object_to_dict(data)
1987
1988
1989# For consistency with Azure SDK
1990get_management_certificate = show_management_certificate
1991
1992
1993def add_management_certificate(kwargs=None, conn=None, call=None):
1994    """
1995    .. versionadded:: 2015.8.0
1996
1997    Add a new management certificate
1998
1999    CLI Example:
2000
2001    .. code-block:: bash
2002
2003        salt-cloud -f add_management_certificate my-azure public_key='...PUBKEY...' \\
2004            thumbprint=0123456789ABCDEF data='...CERT_DATA...'
2005    """
2006    if call != "function":
2007        raise SaltCloudSystemExit(
2008            "The add_management_certificate function must be called with -f or"
2009            " --function."
2010        )
2011
2012    if not conn:
2013        conn = get_conn()
2014
2015    if kwargs is None:
2016        kwargs = {}
2017
2018    if "public_key" not in kwargs:
2019        raise SaltCloudSystemExit('A public_key must be specified as "public_key"')
2020
2021    if "thumbprint" not in kwargs:
2022        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')
2023
2024    if "data" not in kwargs:
2025        raise SaltCloudSystemExit('Certificate data must be specified as "data"')
2026
2027    try:
2028        conn.add_management_certificate(
2029            kwargs["name"],
2030            kwargs["thumbprint"],
2031            kwargs["data"],
2032        )
2033        return {"Success": "The management certificate was successfully added"}
2034    except AzureConflictHttpError:
2035        raise SaltCloudSystemExit(
2036            "There was a conflict. "
2037            "This usually means that the management certificate already exists."
2038        )
2039
2040
2041def delete_management_certificate(kwargs=None, conn=None, call=None):
2042    """
2043    .. versionadded:: 2015.8.0
2044
2045    Delete a specific certificate associated with the management
2046
2047    CLI Examples:
2048
2049    .. code-block:: bash
2050
2051        salt-cloud -f delete_management_certificate my-azure name=my_management_certificate \\
2052            thumbalgorithm=sha1 thumbprint=0123456789ABCDEF
2053    """
2054    if call != "function":
2055        raise SaltCloudSystemExit(
2056            "The delete_management_certificate function must be called with -f or"
2057            " --function."
2058        )
2059
2060    if kwargs is None:
2061        kwargs = {}
2062
2063    if "thumbprint" not in kwargs:
2064        raise SaltCloudSystemExit('A thumbprint must be specified as "thumbprint"')
2065
2066    if not conn:
2067        conn = get_conn()
2068
2069    try:
2070        conn.delete_management_certificate(kwargs["thumbprint"])
2071        return {"Success": "The management certificate was successfully deleted"}
2072    except AzureMissingResourceHttpError as exc:
2073        raise SaltCloudSystemExit("{}: {}".format(kwargs["thumbprint"], exc.message))
2074
2075
2076def list_virtual_networks(kwargs=None, conn=None, call=None):
2077    """
2078    .. versionadded:: 2015.8.0
2079
2080    List input endpoints associated with the deployment
2081
2082    CLI Example:
2083
2084    .. code-block:: bash
2085
2086        salt-cloud -f list_virtual_networks my-azure service=myservice deployment=mydeployment
2087    """
2088    if call != "function":
2089        raise SaltCloudSystemExit(
2090            "The list_virtual_networks function must be called with -f or --function."
2091        )
2092
2093    path = "services/networking/virtualnetwork"
2094    data = query(path)
2095    return data
2096
2097
2098def list_input_endpoints(kwargs=None, conn=None, call=None):
2099    """
2100    .. versionadded:: 2015.8.0
2101
2102    List input endpoints associated with the deployment
2103
2104    CLI Example:
2105
2106    .. code-block:: bash
2107
2108        salt-cloud -f list_input_endpoints my-azure service=myservice deployment=mydeployment
2109    """
2110    if call != "function":
2111        raise SaltCloudSystemExit(
2112            "The list_input_endpoints function must be called with -f or --function."
2113        )
2114
2115    if kwargs is None:
2116        kwargs = {}
2117
2118    if "service" not in kwargs:
2119        raise SaltCloudSystemExit('A service name must be specified as "service"')
2120
2121    if "deployment" not in kwargs:
2122        raise SaltCloudSystemExit('A deployment name must be specified as "deployment"')
2123
2124    path = "services/hostedservices/{}/deployments/{}".format(
2125        kwargs["service"],
2126        kwargs["deployment"],
2127    )
2128
2129    data = query(path)
2130    if data is None:
2131        raise SaltCloudSystemExit(
2132            "There was an error listing endpoints with the {} service on the {}"
2133            " deployment.".format(kwargs["service"], kwargs["deployment"])
2134        )
2135
2136    ret = {}
2137    for item in data:
2138        if "Role" in item:
2139            role = item["Role"]
2140            if not isinstance(role, dict):
2141                return ret
2142            input_endpoint = (
2143                role["ConfigurationSets"]["ConfigurationSet"]
2144                .get("InputEndpoints", {})
2145                .get("InputEndpoint")
2146            )
2147            if not input_endpoint:
2148                continue
2149            if not isinstance(input_endpoint, list):
2150                input_endpoint = [input_endpoint]
2151            for endpoint in input_endpoint:
2152                ret[endpoint["Name"]] = endpoint
2153            return ret
2154    return ret
2155
2156
2157def show_input_endpoint(kwargs=None, conn=None, call=None):
2158    """
2159    .. versionadded:: 2015.8.0
2160
2161    Show an input endpoint associated with the deployment
2162
2163    CLI Example:
2164
2165    .. code-block:: bash
2166
2167        salt-cloud -f show_input_endpoint my-azure service=myservice \\
2168            deployment=mydeployment name=SSH
2169    """
2170    if call != "function":
2171        raise SaltCloudSystemExit(
2172            "The show_input_endpoint function must be called with -f or --function."
2173        )
2174
2175    if kwargs is None:
2176        kwargs = {}
2177
2178    if "name" not in kwargs:
2179        raise SaltCloudSystemExit('An endpoint name must be specified as "name"')
2180
2181    data = list_input_endpoints(kwargs=kwargs, call="function")
2182    return data.get(kwargs["name"], None)
2183
2184
2185# For consistency with Azure SDK
2186get_input_endpoint = show_input_endpoint
2187
2188
2189def update_input_endpoint(kwargs=None, conn=None, call=None, activity="update"):
2190    """
2191    .. versionadded:: 2015.8.0
2192
2193    Update an input endpoint associated with the deployment. Please note that
2194    there may be a delay before the changes show up.
2195
2196    CLI Example:
2197
2198    .. code-block:: bash
2199
2200        salt-cloud -f update_input_endpoint my-azure service=myservice \\
2201            deployment=mydeployment role=myrole name=HTTP local_port=80 \\
2202            port=80 protocol=tcp enable_direct_server_return=False \\
2203            timeout_for_tcp_idle_connection=4
2204    """
2205    if call != "function":
2206        raise SaltCloudSystemExit(
2207            "The update_input_endpoint function must be called with -f or --function."
2208        )
2209
2210    if kwargs is None:
2211        kwargs = {}
2212
2213    if "service" not in kwargs:
2214        raise SaltCloudSystemExit('A service name must be specified as "service"')
2215
2216    if "deployment" not in kwargs:
2217        raise SaltCloudSystemExit('A deployment name must be specified as "deployment"')
2218
2219    if "name" not in kwargs:
2220        raise SaltCloudSystemExit('An endpoint name must be specified as "name"')
2221
2222    if "role" not in kwargs:
2223        raise SaltCloudSystemExit('An role name must be specified as "role"')
2224
2225    if activity != "delete":
2226        if "port" not in kwargs:
2227            raise SaltCloudSystemExit('An endpoint port must be specified as "port"')
2228
2229        if "protocol" not in kwargs:
2230            raise SaltCloudSystemExit(
2231                'An endpoint protocol (tcp or udp) must be specified as "protocol"'
2232            )
2233
2234        if "local_port" not in kwargs:
2235            kwargs["local_port"] = kwargs["port"]
2236
2237        if "enable_direct_server_return" not in kwargs:
2238            kwargs["enable_direct_server_return"] = False
2239        kwargs["enable_direct_server_return"] = str(
2240            kwargs["enable_direct_server_return"]
2241        ).lower()
2242
2243        if "timeout_for_tcp_idle_connection" not in kwargs:
2244            kwargs["timeout_for_tcp_idle_connection"] = 4
2245
2246    old_endpoints = list_input_endpoints(kwargs, call="function")
2247
2248    endpoints_xml = ""
2249    endpoint_xml = """
2250        <InputEndpoint>
2251          <LocalPort>{local_port}</LocalPort>
2252          <Name>{name}</Name>
2253          <Port>{port}</Port>
2254          <Protocol>{protocol}</Protocol>
2255          <EnableDirectServerReturn>{enable_direct_server_return}</EnableDirectServerReturn>
2256          <IdleTimeoutInMinutes>{timeout_for_tcp_idle_connection}</IdleTimeoutInMinutes>
2257        </InputEndpoint>"""
2258
2259    if activity == "add":
2260        old_endpoints[kwargs["name"]] = kwargs
2261        old_endpoints[kwargs["name"]]["Name"] = kwargs["name"]
2262
2263    for endpoint in old_endpoints:
2264        if old_endpoints[endpoint]["Name"] == kwargs["name"]:
2265            if activity != "delete":
2266                this_endpoint_xml = endpoint_xml.format(**kwargs)
2267                endpoints_xml += this_endpoint_xml
2268        else:
2269            this_endpoint_xml = endpoint_xml.format(
2270                local_port=old_endpoints[endpoint]["LocalPort"],
2271                name=old_endpoints[endpoint]["Name"],
2272                port=old_endpoints[endpoint]["Port"],
2273                protocol=old_endpoints[endpoint]["Protocol"],
2274                enable_direct_server_return=old_endpoints[endpoint][
2275                    "EnableDirectServerReturn"
2276                ],
2277                timeout_for_tcp_idle_connection=old_endpoints[endpoint].get(
2278                    "IdleTimeoutInMinutes", 4
2279                ),
2280            )
2281            endpoints_xml += this_endpoint_xml
2282
2283    request_xml = """<PersistentVMRole xmlns="http://schemas.microsoft.com/windowsazure"
2284xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
2285  <ConfigurationSets>
2286    <ConfigurationSet>
2287      <ConfigurationSetType>NetworkConfiguration</ConfigurationSetType>
2288      <InputEndpoints>{}
2289      </InputEndpoints>
2290    </ConfigurationSet>
2291  </ConfigurationSets>
2292  <OSVirtualHardDisk>
2293  </OSVirtualHardDisk>
2294</PersistentVMRole>""".format(
2295        endpoints_xml
2296    )
2297
2298    path = "services/hostedservices/{}/deployments/{}/roles/{}".format(
2299        kwargs["service"],
2300        kwargs["deployment"],
2301        kwargs["role"],
2302    )
2303    query(
2304        path=path,
2305        method="PUT",
2306        header_dict={"Content-Type": "application/xml"},
2307        data=request_xml,
2308        decode=False,
2309    )
2310    return True
2311
2312
2313def add_input_endpoint(kwargs=None, conn=None, call=None):
2314    """
2315    .. versionadded:: 2015.8.0
2316
2317    Add an input endpoint to the deployment. Please note that
2318    there may be a delay before the changes show up.
2319
2320    CLI Example:
2321
2322    .. code-block:: bash
2323
2324        salt-cloud -f add_input_endpoint my-azure service=myservice \\
2325            deployment=mydeployment role=myrole name=HTTP local_port=80 \\
2326            port=80 protocol=tcp enable_direct_server_return=False \\
2327            timeout_for_tcp_idle_connection=4
2328    """
2329    return update_input_endpoint(
2330        kwargs=kwargs,
2331        conn=conn,
2332        call="function",
2333        activity="add",
2334    )
2335
2336
2337def delete_input_endpoint(kwargs=None, conn=None, call=None):
2338    """
2339    .. versionadded:: 2015.8.0
2340
2341    Delete an input endpoint from the deployment. Please note that
2342    there may be a delay before the changes show up.
2343
2344    CLI Example:
2345
2346    .. code-block:: bash
2347
2348        salt-cloud -f delete_input_endpoint my-azure service=myservice \\
2349            deployment=mydeployment role=myrole name=HTTP
2350    """
2351    return update_input_endpoint(
2352        kwargs=kwargs,
2353        conn=conn,
2354        call="function",
2355        activity="delete",
2356    )
2357
2358
2359def show_deployment(kwargs=None, conn=None, call=None):
2360    """
2361    .. versionadded:: 2015.8.0
2362
2363    Return information about a deployment
2364
2365    CLI Example:
2366
2367    .. code-block:: bash
2368
2369        salt-cloud -f show_deployment my-azure name=my_deployment
2370    """
2371    if call != "function":
2372        raise SaltCloudSystemExit(
2373            "The get_deployment function must be called with -f or --function."
2374        )
2375
2376    if not conn:
2377        conn = get_conn()
2378
2379    if kwargs is None:
2380        kwargs = {}
2381
2382    if "service_name" not in kwargs:
2383        raise SaltCloudSystemExit('A service name must be specified as "service_name"')
2384
2385    if "deployment_name" not in kwargs:
2386        raise SaltCloudSystemExit(
2387            'A deployment name must be specified as "deployment_name"'
2388        )
2389
2390    data = conn.get_deployment_by_name(
2391        service_name=kwargs["service_name"],
2392        deployment_name=kwargs["deployment_name"],
2393    )
2394    return object_to_dict(data)
2395
2396
2397# For consistency with Azure SDK
2398get_deployment = show_deployment
2399
2400
2401def list_affinity_groups(kwargs=None, conn=None, call=None):
2402    """
2403    .. versionadded:: 2015.8.0
2404
2405    List input endpoints associated with the deployment
2406
2407    CLI Example:
2408
2409    .. code-block:: bash
2410
2411        salt-cloud -f list_affinity_groups my-azure
2412    """
2413    if call != "function":
2414        raise SaltCloudSystemExit(
2415            "The list_affinity_groups function must be called with -f or --function."
2416        )
2417
2418    if not conn:
2419        conn = get_conn()
2420
2421    data = conn.list_affinity_groups()
2422    ret = {}
2423    for item in data.affinity_groups:
2424        ret[item.name] = object_to_dict(item)
2425    return ret
2426
2427
2428def show_affinity_group(kwargs=None, conn=None, call=None):
2429    """
2430    .. versionadded:: 2015.8.0
2431
2432    Show an affinity group associated with the account
2433
2434    CLI Example:
2435
2436    .. code-block:: bash
2437
2438        salt-cloud -f show_affinity_group my-azure service=myservice \\
2439            deployment=mydeployment name=SSH
2440    """
2441    if call != "function":
2442        raise SaltCloudSystemExit(
2443            "The show_affinity_group function must be called with -f or --function."
2444        )
2445
2446    if not conn:
2447        conn = get_conn()
2448
2449    if kwargs is None:
2450        kwargs = {}
2451
2452    if "name" not in kwargs:
2453        raise SaltCloudSystemExit('An affinity group name must be specified as "name"')
2454
2455    data = conn.get_affinity_group_properties(affinity_group_name=kwargs["name"])
2456    return object_to_dict(data)
2457
2458
2459# For consistency with Azure SDK
2460get_affinity_group = show_affinity_group
2461
2462
2463def create_affinity_group(kwargs=None, conn=None, call=None):
2464    """
2465    .. versionadded:: 2015.8.0
2466
2467    Create a new affinity group
2468
2469    CLI Example:
2470
2471    .. code-block:: bash
2472
2473        salt-cloud -f create_affinity_group my-azure name=my_affinity_group
2474    """
2475    if call != "function":
2476        raise SaltCloudSystemExit(
2477            "The create_affinity_group function must be called with -f or --function."
2478        )
2479
2480    if not conn:
2481        conn = get_conn()
2482
2483    if kwargs is None:
2484        kwargs = {}
2485
2486    if "name" not in kwargs:
2487        raise SaltCloudSystemExit('A name must be specified as "name"')
2488
2489    if "label" not in kwargs:
2490        raise SaltCloudSystemExit('A label must be specified as "label"')
2491
2492    if "location" not in kwargs:
2493        raise SaltCloudSystemExit('A location must be specified as "location"')
2494
2495    try:
2496        conn.create_affinity_group(
2497            kwargs["name"],
2498            kwargs["label"],
2499            kwargs["location"],
2500            kwargs.get("description", None),
2501        )
2502        return {"Success": "The affinity group was successfully created"}
2503    except AzureConflictHttpError:
2504        raise SaltCloudSystemExit(
2505            "There was a conflict. This usually means that the affinity group already"
2506            " exists."
2507        )
2508
2509
2510def update_affinity_group(kwargs=None, conn=None, call=None):
2511    """
2512    .. versionadded:: 2015.8.0
2513
2514    Update an affinity group's properties
2515
2516    CLI Example:
2517
2518    .. code-block:: bash
2519
2520        salt-cloud -f update_affinity_group my-azure name=my_group label=my_group
2521    """
2522    if call != "function":
2523        raise SaltCloudSystemExit(
2524            "The update_affinity_group function must be called with -f or --function."
2525        )
2526
2527    if not conn:
2528        conn = get_conn()
2529
2530    if kwargs is None:
2531        kwargs = {}
2532
2533    if "name" not in kwargs:
2534        raise SaltCloudSystemExit('A name must be specified as "name"')
2535
2536    if "label" not in kwargs:
2537        raise SaltCloudSystemExit('A label must be specified as "label"')
2538
2539    conn.update_affinity_group(
2540        affinity_group_name=kwargs["name"],
2541        label=kwargs["label"],
2542        description=kwargs.get("description", None),
2543    )
2544    return show_affinity_group(kwargs={"name": kwargs["name"]}, call="function")
2545
2546
2547def delete_affinity_group(kwargs=None, conn=None, call=None):
2548    """
2549    .. versionadded:: 2015.8.0
2550
2551    Delete a specific affinity group associated with the account
2552
2553    CLI Examples:
2554
2555    .. code-block:: bash
2556
2557        salt-cloud -f delete_affinity_group my-azure name=my_affinity_group
2558    """
2559    if call != "function":
2560        raise SaltCloudSystemExit(
2561            "The delete_affinity_group function must be called with -f or --function."
2562        )
2563
2564    if kwargs is None:
2565        kwargs = {}
2566
2567    if "name" not in kwargs:
2568        raise SaltCloudSystemExit('A name must be specified as "name"')
2569
2570    if not conn:
2571        conn = get_conn()
2572
2573    try:
2574        conn.delete_affinity_group(kwargs["name"])
2575        return {"Success": "The affinity group was successfully deleted"}
2576    except AzureMissingResourceHttpError as exc:
2577        raise SaltCloudSystemExit("{}: {}".format(kwargs["name"], exc.message))
2578
2579
2580def get_storage_conn(storage_account=None, storage_key=None, conn_kwargs=None):
2581    """
2582    .. versionadded:: 2015.8.0
2583
2584    Return a storage_conn object for the storage account
2585    """
2586    if conn_kwargs is None:
2587        conn_kwargs = {}
2588
2589    if not storage_account:
2590        storage_account = config.get_cloud_config_value(
2591            "storage_account",
2592            get_configured_provider(),
2593            __opts__,
2594            search_global=False,
2595            default=conn_kwargs.get("storage_account", None),
2596        )
2597    if not storage_key:
2598        storage_key = config.get_cloud_config_value(
2599            "storage_key",
2600            get_configured_provider(),
2601            __opts__,
2602            search_global=False,
2603            default=conn_kwargs.get("storage_key", None),
2604        )
2605    return azure.storage.BlobService(storage_account, storage_key)
2606
2607
2608def make_blob_url(kwargs=None, storage_conn=None, call=None):
2609    """
2610    .. versionadded:: 2015.8.0
2611
2612    Creates the URL to access a blob
2613
2614    CLI Example:
2615
2616    .. code-block:: bash
2617
2618        salt-cloud -f make_blob_url my-azure container=mycontainer blob=myblob
2619
2620    container:
2621        Name of the container.
2622    blob:
2623        Name of the blob.
2624    account:
2625        Name of the storage account. If not specified, derives the host base
2626        from the provider configuration.
2627    protocol:
2628        Protocol to use: 'http' or 'https'. If not specified, derives the host
2629        base from the provider configuration.
2630    host_base:
2631        Live host base URL.  If not specified, derives the host base from the
2632        provider configuration.
2633    """
2634    if call != "function":
2635        raise SaltCloudSystemExit(
2636            "The make_blob_url function must be called with -f or --function."
2637        )
2638
2639    if kwargs is None:
2640        kwargs = {}
2641
2642    if "container" not in kwargs:
2643        raise SaltCloudSystemExit('A container name must be specified as "container"')
2644
2645    if "blob" not in kwargs:
2646        raise SaltCloudSystemExit('A blob name must be specified as "blob"')
2647
2648    if not storage_conn:
2649        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2650
2651    data = storage_conn.make_blob_url(
2652        kwargs["container"],
2653        kwargs["blob"],
2654        kwargs.get("account", None),
2655        kwargs.get("protocol", None),
2656        kwargs.get("host_base", None),
2657    )
2658    ret = {}
2659    for item in data.containers:
2660        ret[item.name] = object_to_dict(item)
2661    return ret
2662
2663
2664def list_storage_containers(kwargs=None, storage_conn=None, call=None):
2665    """
2666    .. versionadded:: 2015.8.0
2667
2668    List containers associated with the storage account
2669
2670    CLI Example:
2671
2672    .. code-block:: bash
2673
2674        salt-cloud -f list_storage_containers my-azure
2675    """
2676    if call != "function":
2677        raise SaltCloudSystemExit(
2678            "The list_storage_containers function must be called with -f or --function."
2679        )
2680
2681    if not storage_conn:
2682        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2683
2684    data = storage_conn.list_containers()
2685    ret = {}
2686    for item in data.containers:
2687        ret[item.name] = object_to_dict(item)
2688    return ret
2689
2690
2691def create_storage_container(kwargs=None, storage_conn=None, call=None):
2692    """
2693    .. versionadded:: 2015.8.0
2694
2695    Create a storage container
2696
2697    CLI Example:
2698
2699    .. code-block:: bash
2700
2701        salt-cloud -f create_storage_container my-azure name=mycontainer
2702
2703    name:
2704        Name of container to create.
2705    meta_name_values:
2706        Optional. A dict with name_value pairs to associate with the
2707        container as metadata. Example:{'Category':'test'}
2708    blob_public_access:
2709        Optional. Possible values include: container, blob
2710    fail_on_exist:
2711        Specify whether to throw an exception when the container exists.
2712    """
2713    if call != "function":
2714        raise SaltCloudSystemExit(
2715            "The create_storage_container function must be called with -f or"
2716            " --function."
2717        )
2718
2719    if not storage_conn:
2720        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2721
2722    try:
2723        storage_conn.create_container(
2724            container_name=kwargs["name"],
2725            x_ms_meta_name_values=kwargs.get("meta_name_values", None),
2726            x_ms_blob_public_access=kwargs.get("blob_public_access", None),
2727            fail_on_exist=kwargs.get("fail_on_exist", False),
2728        )
2729        return {"Success": "The storage container was successfully created"}
2730    except AzureConflictHttpError:
2731        raise SaltCloudSystemExit(
2732            "There was a conflict. This usually means that the storage container"
2733            " already exists."
2734        )
2735
2736
2737def show_storage_container(kwargs=None, storage_conn=None, call=None):
2738    """
2739    .. versionadded:: 2015.8.0
2740
2741    Show a container associated with the storage account
2742
2743    CLI Example:
2744
2745    .. code-block:: bash
2746
2747        salt-cloud -f show_storage_container my-azure name=myservice
2748
2749    name:
2750        Name of container to show.
2751    """
2752    if call != "function":
2753        raise SaltCloudSystemExit(
2754            "The show_storage_container function must be called with -f or --function."
2755        )
2756
2757    if kwargs is None:
2758        kwargs = {}
2759
2760    if "name" not in kwargs:
2761        raise SaltCloudSystemExit(
2762            'An storage container name must be specified as "name"'
2763        )
2764
2765    if not storage_conn:
2766        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2767
2768    data = storage_conn.get_container_properties(
2769        container_name=kwargs["name"],
2770        x_ms_lease_id=kwargs.get("lease_id", None),
2771    )
2772    return data
2773
2774
2775# For consistency with Azure SDK
2776get_storage_container = show_storage_container
2777
2778
2779def show_storage_container_metadata(kwargs=None, storage_conn=None, call=None):
2780    """
2781    .. versionadded:: 2015.8.0
2782
2783    Show a storage container's metadata
2784
2785    CLI Example:
2786
2787    .. code-block:: bash
2788
2789        salt-cloud -f show_storage_container_metadata my-azure name=myservice
2790
2791    name:
2792        Name of container to show.
2793    lease_id:
2794        If specified, show_storage_container_metadata only succeeds if the
2795        container's lease is active and matches this ID.
2796    """
2797    if call != "function":
2798        raise SaltCloudSystemExit(
2799            "The show_storage_container function must be called with -f or --function."
2800        )
2801
2802    if kwargs is None:
2803        kwargs = {}
2804
2805    if "name" not in kwargs:
2806        raise SaltCloudSystemExit(
2807            'An storage container name must be specified as "name"'
2808        )
2809
2810    if not storage_conn:
2811        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2812
2813    data = storage_conn.get_container_metadata(
2814        container_name=kwargs["name"],
2815        x_ms_lease_id=kwargs.get("lease_id", None),
2816    )
2817    return data
2818
2819
2820# For consistency with Azure SDK
2821get_storage_container_metadata = show_storage_container_metadata
2822
2823
2824def set_storage_container_metadata(kwargs=None, storage_conn=None, call=None):
2825    """
2826    .. versionadded:: 2015.8.0
2827
2828    Set a storage container's metadata
2829
2830    CLI Example:
2831
2832    .. code-block:: bash
2833
2834        salt-cloud -f set_storage_container my-azure name=mycontainer \\
2835            x_ms_meta_name_values='{"my_name": "my_value"}'
2836
2837    name:
2838        Name of existing container.
2839    meta_name_values:
2840        A dict containing name, value for metadata.
2841        Example: {'category':'test'}
2842    lease_id:
2843        If specified, set_storage_container_metadata only succeeds if the
2844        container's lease is active and matches this ID.
2845    """
2846    if call != "function":
2847        raise SaltCloudSystemExit(
2848            "The create_storage_container function must be called with -f or"
2849            " --function."
2850        )
2851
2852    if kwargs is None:
2853        kwargs = {}
2854
2855    if "name" not in kwargs:
2856        raise SaltCloudSystemExit(
2857            'An storage container name must be specified as "name"'
2858        )
2859
2860    x_ms_meta_name_values = salt.utils.yaml.safe_load(
2861        kwargs.get("meta_name_values", "")
2862    )
2863
2864    if not storage_conn:
2865        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2866
2867    try:
2868        storage_conn.set_container_metadata(
2869            container_name=kwargs["name"],
2870            x_ms_meta_name_values=x_ms_meta_name_values,
2871            x_ms_lease_id=kwargs.get("lease_id", None),
2872        )
2873        return {"Success": "The storage container was successfully updated"}
2874    except AzureConflictHttpError:
2875        raise SaltCloudSystemExit("There was a conflict.")
2876
2877
2878def show_storage_container_acl(kwargs=None, storage_conn=None, call=None):
2879    """
2880    .. versionadded:: 2015.8.0
2881
2882    Show a storage container's acl
2883
2884    CLI Example:
2885
2886    .. code-block:: bash
2887
2888        salt-cloud -f show_storage_container_acl my-azure name=myservice
2889
2890    name:
2891        Name of existing container.
2892    lease_id:
2893        If specified, show_storage_container_acl only succeeds if the
2894        container's lease is active and matches this ID.
2895    """
2896    if call != "function":
2897        raise SaltCloudSystemExit(
2898            "The show_storage_container function must be called with -f or --function."
2899        )
2900
2901    if kwargs is None:
2902        kwargs = {}
2903
2904    if "name" not in kwargs:
2905        raise SaltCloudSystemExit(
2906            'An storage container name must be specified as "name"'
2907        )
2908
2909    if not storage_conn:
2910        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2911
2912    data = storage_conn.get_container_acl(
2913        container_name=kwargs["name"],
2914        x_ms_lease_id=kwargs.get("lease_id", None),
2915    )
2916    return data
2917
2918
2919# For consistency with Azure SDK
2920get_storage_container_acl = show_storage_container_acl
2921
2922
2923def set_storage_container_acl(kwargs=None, storage_conn=None, call=None):
2924    """
2925    .. versionadded:: 2015.8.0
2926
2927    Set a storage container's acl
2928
2929    CLI Example:
2930
2931    .. code-block:: bash
2932
2933        salt-cloud -f set_storage_container my-azure name=mycontainer
2934
2935    name:
2936        Name of existing container.
2937    signed_identifiers:
2938        SignedIdentifers instance
2939    blob_public_access:
2940        Optional. Possible values include: container, blob
2941    lease_id:
2942        If specified, set_storage_container_acl only succeeds if the
2943        container's lease is active and matches this ID.
2944    """
2945    if call != "function":
2946        raise SaltCloudSystemExit(
2947            "The create_storage_container function must be called with -f or"
2948            " --function."
2949        )
2950
2951    if not storage_conn:
2952        storage_conn = get_storage_conn(conn_kwargs=kwargs)
2953
2954    try:
2955        data = storage_conn.set_container_acl(
2956            container_name=kwargs["name"],
2957            signed_identifiers=kwargs.get("signed_identifiers", None),
2958            x_ms_blob_public_access=kwargs.get("blob_public_access", None),
2959            x_ms_lease_id=kwargs.get("lease_id", None),
2960        )
2961        return {"Success": "The storage container was successfully updated"}
2962    except AzureConflictHttpError:
2963        raise SaltCloudSystemExit("There was a conflict.")
2964
2965
2966def delete_storage_container(kwargs=None, storage_conn=None, call=None):
2967    """
2968    .. versionadded:: 2015.8.0
2969
2970    Delete a container associated with the storage account
2971
2972    CLI Example:
2973
2974    .. code-block:: bash
2975
2976        salt-cloud -f delete_storage_container my-azure name=mycontainer
2977
2978    name:
2979        Name of container to create.
2980    fail_not_exist:
2981        Specify whether to throw an exception when the container exists.
2982    lease_id:
2983        If specified, delete_storage_container only succeeds if the
2984        container's lease is active and matches this ID.
2985    """
2986    if call != "function":
2987        raise SaltCloudSystemExit(
2988            "The delete_storage_container function must be called with -f or"
2989            " --function."
2990        )
2991
2992    if kwargs is None:
2993        kwargs = {}
2994
2995    if "name" not in kwargs:
2996        raise SaltCloudSystemExit(
2997            'An storage container name must be specified as "name"'
2998        )
2999
3000    if not storage_conn:
3001        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3002
3003    data = storage_conn.delete_container(
3004        container_name=kwargs["name"],
3005        fail_not_exist=kwargs.get("fail_not_exist", None),
3006        x_ms_lease_id=kwargs.get("lease_id", None),
3007    )
3008    return data
3009
3010
3011def lease_storage_container(kwargs=None, storage_conn=None, call=None):
3012    """
3013    .. versionadded:: 2015.8.0
3014
3015    Lease a container associated with the storage account
3016
3017    CLI Example:
3018
3019    .. code-block:: bash
3020
3021        salt-cloud -f lease_storage_container my-azure name=mycontainer
3022
3023    name:
3024        Name of container to create.
3025    lease_action:
3026        Required. Possible values: acquire|renew|release|break|change
3027    lease_id:
3028        Required if the container has an active lease.
3029    lease_duration:
3030        Specifies the duration of the lease, in seconds, or negative one
3031        (-1) for a lease that never expires. A non-infinite lease can be
3032        between 15 and 60 seconds. A lease duration cannot be changed
3033        using renew or change. For backwards compatibility, the default is
3034        60, and the value is only used on an acquire operation.
3035    lease_break_period:
3036        Optional. For a break operation, this is the proposed duration of
3037        seconds that the lease should continue before it is broken, between
3038        0 and 60 seconds. This break period is only used if it is shorter
3039        than the time remaining on the lease. If longer, the time remaining
3040        on the lease is used. A new lease will not be available before the
3041        break period has expired, but the lease may be held for longer than
3042        the break period. If this header does not appear with a break
3043        operation, a fixed-duration lease breaks after the remaining lease
3044        period elapses, and an infinite lease breaks immediately.
3045    proposed_lease_id:
3046        Optional for acquire, required for change. Proposed lease ID, in a
3047        GUID string format.
3048    """
3049    if call != "function":
3050        raise SaltCloudSystemExit(
3051            "The lease_storage_container function must be called with -f or --function."
3052        )
3053
3054    if kwargs is None:
3055        kwargs = {}
3056
3057    if "name" not in kwargs:
3058        raise SaltCloudSystemExit(
3059            'An storage container name must be specified as "name"'
3060        )
3061
3062    lease_actions = ("acquire", "renew", "release", "break", "change")
3063
3064    if kwargs.get("lease_action", None) not in lease_actions:
3065        raise SaltCloudSystemExit(
3066            "A lease_action must be one of: {}".format(", ".join(lease_actions))
3067        )
3068
3069    if kwargs["lease_action"] != "acquire" and "lease_id" not in kwargs:
3070        raise SaltCloudSystemExit(
3071            'A lease ID must be specified for the "{}" lease action '
3072            'as "lease_id"'.format(kwargs["lease_action"])
3073        )
3074
3075    if not storage_conn:
3076        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3077
3078    data = storage_conn.lease_container(
3079        container_name=kwargs["name"],
3080        x_ms_lease_action=kwargs["lease_action"],
3081        x_ms_lease_id=kwargs.get("lease_id", None),
3082        x_ms_lease_duration=kwargs.get("lease_duration", 60),
3083        x_ms_lease_break_period=kwargs.get("lease_break_period", None),
3084        x_ms_proposed_lease_id=kwargs.get("proposed_lease_id", None),
3085    )
3086
3087    return data
3088
3089
3090def list_blobs(kwargs=None, storage_conn=None, call=None):
3091    """
3092    .. versionadded:: 2015.8.0
3093
3094    List blobs associated with the container
3095
3096    CLI Example:
3097
3098    .. code-block:: bash
3099
3100        salt-cloud -f list_blobs my-azure container=mycontainer
3101
3102    container:
3103        The name of the storage container
3104    prefix:
3105        Optional. Filters the results to return only blobs whose names
3106        begin with the specified prefix.
3107    marker:
3108        Optional. A string value that identifies the portion of the list
3109        to be returned with the next list operation. The operation returns
3110        a marker value within the response body if the list returned was
3111        not complete. The marker value may then be used in a subsequent
3112        call to request the next set of list items. The marker value is
3113        opaque to the client.
3114    maxresults:
3115        Optional. Specifies the maximum number of blobs to return,
3116        including all BlobPrefix elements. If the request does not specify
3117        maxresults or specifies a value greater than 5,000, the server will
3118        return up to 5,000 items. Setting maxresults to a value less than
3119        or equal to zero results in error response code 400 (Bad Request).
3120    include:
3121        Optional. Specifies one or more datasets to include in the
3122        response. To specify more than one of these options on the URI,
3123        you must separate each option with a comma. Valid values are:
3124
3125        snapshots:
3126            Specifies that snapshots should be included in the
3127            enumeration. Snapshots are listed from oldest to newest in
3128            the response.
3129        metadata:
3130            Specifies that blob metadata be returned in the response.
3131        uncommittedblobs:
3132            Specifies that blobs for which blocks have been uploaded,
3133            but which have not been committed using Put Block List
3134            (REST API), be included in the response.
3135        copy:
3136            Version 2012-02-12 and newer. Specifies that metadata
3137            related to any current or previous Copy Blob operation
3138            should be included in the response.
3139    delimiter:
3140        Optional. When the request includes this parameter, the operation
3141        returns a BlobPrefix element in the response body that acts as a
3142        placeholder for all blobs whose names begin with the same
3143        substring up to the appearance of the delimiter character. The
3144        delimiter may be a single character or a string.
3145    """
3146    if call != "function":
3147        raise SaltCloudSystemExit(
3148            "The list_blobs function must be called with -f or --function."
3149        )
3150
3151    if kwargs is None:
3152        kwargs = {}
3153
3154    if "container" not in kwargs:
3155        raise SaltCloudSystemExit(
3156            'An storage container name must be specified as "container"'
3157        )
3158
3159    if not storage_conn:
3160        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3161
3162    return salt.utils.msazure.list_blobs(storage_conn=storage_conn, **kwargs)
3163
3164
3165def show_blob_service_properties(kwargs=None, storage_conn=None, call=None):
3166    """
3167    .. versionadded:: 2015.8.0
3168
3169    Show a blob's service properties
3170
3171    CLI Example:
3172
3173    .. code-block:: bash
3174
3175        salt-cloud -f show_blob_service_properties my-azure
3176    """
3177    if call != "function":
3178        raise SaltCloudSystemExit(
3179            "The show_blob_service_properties function must be called with -f or"
3180            " --function."
3181        )
3182
3183    if not storage_conn:
3184        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3185
3186    data = storage_conn.get_blob_service_properties(
3187        timeout=kwargs.get("timeout", None),
3188    )
3189    return data
3190
3191
3192# For consistency with Azure SDK
3193get_blob_service_properties = show_blob_service_properties
3194
3195
3196def set_blob_service_properties(kwargs=None, storage_conn=None, call=None):
3197    """
3198    .. versionadded:: 2015.8.0
3199
3200    Sets the properties of a storage account's Blob service, including
3201    Windows Azure Storage Analytics. You can also use this operation to
3202    set the default request version for all incoming requests that do not
3203    have a version specified.
3204
3205    CLI Example:
3206
3207    .. code-block:: bash
3208
3209        salt-cloud -f set_blob_service_properties my-azure
3210
3211    properties:
3212        a StorageServiceProperties object.
3213    timeout:
3214        Optional. The timeout parameter is expressed in seconds.
3215    """
3216    if call != "function":
3217        raise SaltCloudSystemExit(
3218            "The set_blob_service_properties function must be called with -f or"
3219            " --function."
3220        )
3221
3222    if kwargs is None:
3223        kwargs = {}
3224
3225    if "properties" not in kwargs:
3226        raise SaltCloudSystemExit(
3227            'The blob service properties name must be specified as "properties"'
3228        )
3229
3230    if not storage_conn:
3231        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3232
3233    data = storage_conn.get_blob_service_properties(
3234        storage_service_properties=kwargs["properties"],
3235        timeout=kwargs.get("timeout", None),
3236    )
3237    return data
3238
3239
3240def show_blob_properties(kwargs=None, storage_conn=None, call=None):
3241    """
3242    .. versionadded:: 2015.8.0
3243
3244    Returns all user-defined metadata, standard HTTP properties, and
3245    system properties for the blob.
3246
3247    CLI Example:
3248
3249    .. code-block:: bash
3250
3251        salt-cloud -f show_blob_properties my-azure container=mycontainer blob=myblob
3252
3253    container:
3254        Name of existing container.
3255    blob:
3256        Name of existing blob.
3257    lease_id:
3258        Required if the blob has an active lease.
3259    """
3260    if call != "function":
3261        raise SaltCloudSystemExit(
3262            "The show_blob_properties function must be called with -f or --function."
3263        )
3264
3265    if kwargs is None:
3266        kwargs = {}
3267
3268    if "container" not in kwargs:
3269        raise SaltCloudSystemExit('The container name must be specified as "container"')
3270
3271    if "blob" not in kwargs:
3272        raise SaltCloudSystemExit('The blob name must be specified as "blob"')
3273
3274    if not storage_conn:
3275        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3276
3277    try:
3278        data = storage_conn.get_blob_properties(
3279            container_name=kwargs["container"],
3280            blob_name=kwargs["blob"],
3281            x_ms_lease_id=kwargs.get("lease_id", None),
3282        )
3283    except AzureMissingResourceHttpError:
3284        raise SaltCloudSystemExit("The specified blob does not exist.")
3285
3286    return data
3287
3288
3289# For consistency with Azure SDK
3290get_blob_properties = show_blob_properties
3291
3292
3293def set_blob_properties(kwargs=None, storage_conn=None, call=None):
3294    """
3295    .. versionadded:: 2015.8.0
3296
3297    Set a blob's properties
3298
3299    CLI Example:
3300
3301    .. code-block:: bash
3302
3303        salt-cloud -f set_blob_properties my-azure
3304
3305    container:
3306        Name of existing container.
3307    blob:
3308        Name of existing blob.
3309    blob_cache_control:
3310        Optional. Modifies the cache control string for the blob.
3311    blob_content_type:
3312        Optional. Sets the blob's content type.
3313    blob_content_md5:
3314        Optional. Sets the blob's MD5 hash.
3315    blob_content_encoding:
3316        Optional. Sets the blob's content encoding.
3317    blob_content_language:
3318        Optional. Sets the blob's content language.
3319    lease_id:
3320        Required if the blob has an active lease.
3321    blob_content_disposition:
3322        Optional. Sets the blob's Content-Disposition header.
3323        The Content-Disposition response header field conveys additional
3324        information about how to process the response payload, and also can
3325        be used to attach additional metadata. For example, if set to
3326        attachment, it indicates that the user-agent should not display the
3327        response, but instead show a Save As dialog with a filename other
3328        than the blob name specified.
3329    """
3330    if call != "function":
3331        raise SaltCloudSystemExit(
3332            "The set_blob_properties function must be called with -f or --function."
3333        )
3334
3335    if kwargs is None:
3336        kwargs = {}
3337
3338    if "container" not in kwargs:
3339        raise SaltCloudSystemExit(
3340            'The blob container name must be specified as "container"'
3341        )
3342
3343    if "blob" not in kwargs:
3344        raise SaltCloudSystemExit('The blob name must be specified as "blob"')
3345
3346    if not storage_conn:
3347        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3348
3349    data = storage_conn.get_blob_properties(
3350        container_name=kwargs["container"],
3351        blob_name=kwargs["blob"],
3352        x_ms_blob_cache_control=kwargs.get("blob_cache_control", None),
3353        x_ms_blob_content_type=kwargs.get("blob_content_type", None),
3354        x_ms_blob_content_md5=kwargs.get("blob_content_md5", None),
3355        x_ms_blob_content_encoding=kwargs.get("blob_content_encoding", None),
3356        x_ms_blob_content_language=kwargs.get("blob_content_language", None),
3357        x_ms_lease_id=kwargs.get("lease_id", None),
3358        x_ms_blob_content_disposition=kwargs.get("blob_content_disposition", None),
3359    )
3360
3361    return data
3362
3363
3364def put_blob(kwargs=None, storage_conn=None, call=None):
3365    """
3366    .. versionadded:: 2015.8.0
3367
3368    Upload a blob
3369
3370    CLI Examples:
3371
3372    .. code-block:: bash
3373
3374        salt-cloud -f put_blob my-azure container=base name=top.sls blob_path=/srv/salt/top.sls
3375        salt-cloud -f put_blob my-azure container=base name=content.txt blob_content='Some content'
3376
3377    container:
3378        Name of existing container.
3379    name:
3380        Name of existing blob.
3381    blob_path:
3382        The path on the local machine of the file to upload as a blob. Either
3383        this or blob_content must be specified.
3384    blob_content:
3385        The actual content to be uploaded as a blob. Either this or blob_path
3386        must me specified.
3387    cache_control:
3388        Optional. The Blob service stores this value but does not use or
3389        modify it.
3390    content_language:
3391        Optional. Specifies the natural languages used by this resource.
3392    content_md5:
3393        Optional. An MD5 hash of the blob content. This hash is used to
3394        verify the integrity of the blob during transport. When this header
3395        is specified, the storage service checks the hash that has arrived
3396        with the one that was sent. If the two hashes do not match, the
3397        operation will fail with error code 400 (Bad Request).
3398    blob_content_type:
3399        Optional. Set the blob's content type.
3400    blob_content_encoding:
3401        Optional. Set the blob's content encoding.
3402    blob_content_language:
3403        Optional. Set the blob's content language.
3404    blob_content_md5:
3405        Optional. Set the blob's MD5 hash.
3406    blob_cache_control:
3407        Optional. Sets the blob's cache control.
3408    meta_name_values:
3409        A dict containing name, value for metadata.
3410    lease_id:
3411        Required if the blob has an active lease.
3412    """
3413    if call != "function":
3414        raise SaltCloudSystemExit(
3415            "The put_blob function must be called with -f or --function."
3416        )
3417
3418    if kwargs is None:
3419        kwargs = {}
3420
3421    if "container" not in kwargs:
3422        raise SaltCloudSystemExit(
3423            'The blob container name must be specified as "container"'
3424        )
3425
3426    if "name" not in kwargs:
3427        raise SaltCloudSystemExit('The blob name must be specified as "name"')
3428
3429    if "blob_path" not in kwargs and "blob_content" not in kwargs:
3430        raise SaltCloudSystemExit(
3431            'Either a path to a file needs to be passed in as "blob_path" or '
3432            'the contents of a blob as "blob_content."'
3433        )
3434
3435    if not storage_conn:
3436        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3437
3438    return salt.utils.msazure.put_blob(storage_conn=storage_conn, **kwargs)
3439
3440
3441def get_blob(kwargs=None, storage_conn=None, call=None):
3442    """
3443    .. versionadded:: 2015.8.0
3444
3445    Download a blob
3446
3447    CLI Example:
3448
3449    .. code-block:: bash
3450
3451        salt-cloud -f get_blob my-azure container=base name=top.sls local_path=/srv/salt/top.sls
3452        salt-cloud -f get_blob my-azure container=base name=content.txt return_content=True
3453
3454    container:
3455        Name of existing container.
3456    name:
3457        Name of existing blob.
3458    local_path:
3459        The path on the local machine to download the blob to. Either this or
3460        return_content must be specified.
3461    return_content:
3462        Whether or not to return the content directly from the blob. If
3463        specified, must be True or False. Either this or the local_path must
3464        be specified.
3465    snapshot:
3466        Optional. The snapshot parameter is an opaque DateTime value that,
3467        when present, specifies the blob snapshot to retrieve.
3468    lease_id:
3469        Required if the blob has an active lease.
3470    progress_callback:
3471        callback for progress with signature function(current, total) where
3472        current is the number of bytes transferred so far, and total is the
3473        size of the blob.
3474    max_connections:
3475        Maximum number of parallel connections to use when the blob size
3476        exceeds 64MB.
3477        Set to 1 to download the blob chunks sequentially.
3478        Set to 2 or more to download the blob chunks in parallel. This uses
3479        more system resources but will download faster.
3480    max_retries:
3481        Number of times to retry download of blob chunk if an error occurs.
3482    retry_wait:
3483        Sleep time in secs between retries.
3484    """
3485    if call != "function":
3486        raise SaltCloudSystemExit(
3487            "The get_blob function must be called with -f or --function."
3488        )
3489
3490    if kwargs is None:
3491        kwargs = {}
3492
3493    if "container" not in kwargs:
3494        raise SaltCloudSystemExit(
3495            'The blob container name must be specified as "container"'
3496        )
3497
3498    if "name" not in kwargs:
3499        raise SaltCloudSystemExit('The blob name must be specified as "name"')
3500
3501    if "local_path" not in kwargs and "return_content" not in kwargs:
3502        raise SaltCloudSystemExit(
3503            'Either a local path needs to be passed in as "local_path" or '
3504            '"return_content" to return the blob contents directly'
3505        )
3506
3507    if not storage_conn:
3508        storage_conn = get_storage_conn(conn_kwargs=kwargs)
3509
3510    return salt.utils.msazure.get_blob(storage_conn=storage_conn, **kwargs)
3511
3512
3513def query(path, method="GET", data=None, params=None, header_dict=None, decode=True):
3514    """
3515    Perform a query directly against the Azure REST API
3516    """
3517    certificate_path = config.get_cloud_config_value(
3518        "certificate_path", get_configured_provider(), __opts__, search_global=False
3519    )
3520    subscription_id = salt.utils.stringutils.to_str(
3521        config.get_cloud_config_value(
3522            "subscription_id", get_configured_provider(), __opts__, search_global=False
3523        )
3524    )
3525    management_host = config.get_cloud_config_value(
3526        "management_host",
3527        get_configured_provider(),
3528        __opts__,
3529        search_global=False,
3530        default="management.core.windows.net",
3531    )
3532    backend = config.get_cloud_config_value(
3533        "backend", get_configured_provider(), __opts__, search_global=False
3534    )
3535    url = "https://{management_host}/{subscription_id}/{path}".format(
3536        management_host=management_host,
3537        subscription_id=subscription_id,
3538        path=path,
3539    )
3540
3541    if header_dict is None:
3542        header_dict = {}
3543
3544    header_dict["x-ms-version"] = "2014-06-01"
3545
3546    result = salt.utils.http.query(
3547        url,
3548        method=method,
3549        params=params,
3550        data=data,
3551        header_dict=header_dict,
3552        port=443,
3553        text=True,
3554        cert=certificate_path,
3555        backend=backend,
3556        decode=decode,
3557        decode_type="xml",
3558    )
3559    if "dict" in result:
3560        return result["dict"]
3561    return
3562