1#!/usr/local/bin/python3.8
2#
3# Copyright (c) Ansible Project
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9
10DOCUMENTATION = '''
11---
12module: azure_rm_deployment
13
14version_added: "0.1.0"
15
16short_description: Create or destroy Azure Resource Manager template deployments
17
18description:
19    - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python.
20    - You can find some quick start templates in GitHub here U(https://github.com/azure/azure-quickstart-templates).
21    - For more information on Azure Resource Manager templates see U(https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/).
22
23options:
24  resource_group:
25    description:
26        - The resource group name to use or create to host the deployed template.
27    required: true
28    aliases:
29        - resource_group_name
30  name:
31    description:
32        - The name of the deployment to be tracked in the resource group deployment history.
33        - Re-using a deployment name will overwrite the previous value in the resource group's deployment history.
34    default: ansible-arm
35    aliases:
36        - deployment_name
37  location:
38    description:
39        - The geo-locations in which the resource group will be located.
40    default: westus
41  deployment_mode:
42    description:
43        - In incremental mode, resources are deployed without deleting existing resources that are not included in the template.
44        - In complete mode resources are deployed and existing resources in the resource group not included in the template are deleted.
45    default: incremental
46    choices:
47        - complete
48        - incremental
49  template:
50    description:
51        - A hash containing the templates inline. This parameter is mutually exclusive with I(template_link).
52        - Either I(template) or I(template_link) is required if I(state=present).
53    type: dict
54  template_link:
55    description:
56        - Uri of file containing the template body. This parameter is mutually exclusive with I(template).
57        - Either I(template) or I(template_link) is required if I(state=present).
58  parameters:
59    description:
60        - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with I(parameters_link).
61        - Either I(parameters_link) or I(parameters) is required if I(state=present).
62    type: dict
63  parameters_link:
64    description:
65        - Uri of file containing the parameters body. This parameter is mutually exclusive with I(parameters).
66        - Either I(parameters_link) or I(parameters) is required if I(state=present).
67  wait_for_deployment_completion:
68    description:
69        - Whether or not to block until the deployment has completed.
70    type: bool
71    default: 'yes'
72  wait_for_deployment_polling_period:
73    description:
74        - Time (in seconds) to wait between polls when waiting for deployment completion.
75    default: 10
76  state:
77    description:
78        - If I(state=present), template will be created.
79        - If I(state=present) and deployment exists, it will be updated.
80        - If I(state=absent), the resource group will be removed.
81    default: present
82    choices:
83        - present
84        - absent
85
86extends_documentation_fragment:
87    - azure.azcollection.azure
88    - azure.azcollection.azure_tags
89
90author:
91    - David Justice (@devigned)
92    - Laurent Mazuel (@lmazuel)
93    - Andre Price (@obsoleted)
94
95'''
96
97EXAMPLES = '''
98# Destroy a template deployment
99- name: Destroy Azure Deploy
100  azure_rm_deployment:
101    resource_group: myResourceGroup
102    name: myDeployment
103    state: absent
104
105# Create or update a template deployment based on uris using parameter and template links
106- name: Create Azure Deploy
107  azure_rm_deployment:
108    resource_group: myResourceGroup
109    name: myDeployment
110    template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.json'
111    parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.parameters.json'
112
113# Create or update a template deployment based on a uri to the template and parameters specified inline.
114# This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then
115# used to create a new host group. This host group is then used to wait for each instance to respond to the public IP SSH.
116
117- name: Create Azure Deploy
118  azure_rm_deployment:
119    resource_group: myResourceGroup
120    name: myDeployment
121    parameters:
122      newStorageAccountName:
123        value: devopsclestorage1
124      adminUsername:
125        value: devopscle
126      dnsNameForPublicIP:
127        value: devopscleazure
128      location:
129        value: West US
130      vmSize:
131        value: Standard_A2
132      vmName:
133        value: ansibleSshVm
134      sshKeyData:
135        value: YOUR_SSH_PUBLIC_KEY
136    template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json'
137  register: azure
138- name: Add new instance to host group
139  add_host:
140    hostname: "{{ item['ips'][0].public_ip }}"
141    groupname: azure_vms
142  loop: "{{ azure.deployment.instances }}"
143
144# Deploy an Azure WebApp running a hello world'ish node app
145- name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js
146  azure_rm_deployment:
147    resource_group: myResourceGroup
148    name: myDeployment
149    parameters:
150      repoURL:
151        value: 'https://github.com/devigned/az-roadshow-oss.git'
152      siteName:
153        value: devopscleweb
154      hostingPlanName:
155        value: someplan
156      siteLocation:
157        value: westus
158      sku:
159        value: Standard
160    template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json'
161
162# Create or update a template deployment based on an inline template and parameters
163- name: Create Azure Deploy
164  azure_rm_deployment:
165    resource_group: myResourceGroup
166    name: myDeployment
167    template:
168      $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"
169      contentVersion: "1.0.0.0"
170      parameters:
171        newStorageAccountName:
172          type: "string"
173          metadata:
174            description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
175        adminUsername:
176          type: "string"
177          metadata:
178            description: "User name for the Virtual Machine."
179        adminPassword:
180          type: "securestring"
181          metadata:
182            description: "Password for the Virtual Machine."
183        dnsNameForPublicIP:
184          type: "string"
185          metadata:
186            description: "Unique DNS Name for the Public IP used to access the Virtual Machine."
187        ubuntuOSVersion:
188          type: "string"
189          defaultValue: "14.04.2-LTS"
190          allowedValues:
191            - "12.04.5-LTS"
192            - "14.04.2-LTS"
193            - "15.04"
194          metadata:
195            description: >
196                         The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version.
197                         Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04."
198      variables:
199        location: "West US"
200        imagePublisher: "Canonical"
201        imageOffer: "UbuntuServer"
202        OSDiskName: "osdiskforlinuxsimple"
203        nicName: "myVMNic"
204        addressPrefix: "192.0.2.0/24"
205        subnetName: "Subnet"
206        subnetPrefix: "10.0.0.0/24"
207        storageAccountType: "Standard_LRS"
208        publicIPAddressName: "myPublicIP"
209        publicIPAddressType: "Dynamic"
210        vmStorageAccountContainerName: "vhds"
211        vmName: "MyUbuntuVM"
212        vmSize: "Standard_D1"
213        virtualNetworkName: "MyVNET"
214        vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]"
215        subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
216      resources:
217        - type: "Microsoft.Storage/storageAccounts"
218          name: "[parameters('newStorageAccountName')]"
219          apiVersion: "2015-05-01-preview"
220          location: "[variables('location')]"
221          properties:
222            accountType: "[variables('storageAccountType')]"
223        - apiVersion: "2015-05-01-preview"
224          type: "Microsoft.Network/publicIPAddresses"
225          name: "[variables('publicIPAddressName')]"
226          location: "[variables('location')]"
227          properties:
228            publicIPAllocationMethod: "[variables('publicIPAddressType')]"
229            dnsSettings:
230              domainNameLabel: "[parameters('dnsNameForPublicIP')]"
231        - type: "Microsoft.Network/virtualNetworks"
232          apiVersion: "2015-05-01-preview"
233          name: "[variables('virtualNetworkName')]"
234          location: "[variables('location')]"
235          properties:
236            addressSpace:
237              addressPrefixes:
238                - "[variables('addressPrefix')]"
239            subnets:
240              -
241                name: "[variables('subnetName')]"
242                properties:
243                  addressPrefix: "[variables('subnetPrefix')]"
244        - type: "Microsoft.Network/networkInterfaces"
245          apiVersion: "2015-05-01-preview"
246          name: "[variables('nicName')]"
247          location: "[variables('location')]"
248          dependsOn:
249            - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
250            - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
251          properties:
252            ipConfigurations:
253              -
254                name: "ipconfig1"
255                properties:
256                  privateIPAllocationMethod: "Dynamic"
257                  publicIPAddress:
258                    id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
259                  subnet:
260                    id: "[variables('subnetRef')]"
261        - type: "Microsoft.Compute/virtualMachines"
262          apiVersion: "2015-06-15"
263          name: "[variables('vmName')]"
264          location: "[variables('location')]"
265          dependsOn:
266            - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]"
267            - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
268          properties:
269            hardwareProfile:
270              vmSize: "[variables('vmSize')]"
271            osProfile:
272              computername: "[variables('vmName')]"
273              adminUsername: "[parameters('adminUsername')]"
274              adminPassword: "[parameters('adminPassword')]"
275            storageProfile:
276              imageReference:
277                publisher: "[variables('imagePublisher')]"
278                offer: "[variables('imageOffer')]"
279                sku: "[parameters('ubuntuOSVersion')]"
280                version: "latest"
281              osDisk:
282                name: "osdisk"
283                vhd:
284                  uri: >
285                       [concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',
286                       variables('OSDiskName'),'.vhd')]
287                caching: "ReadWrite"
288                createOption: "FromImage"
289            networkProfile:
290              networkInterfaces:
291                -
292                  id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
293            diagnosticsProfile:
294              bootDiagnostics:
295                enabled: "true"
296                storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]"
297    parameters:
298      newStorageAccountName:
299        value: devopsclestorage
300      adminUsername:
301        value: devopscle
302      adminPassword:
303        value: Password1!
304      dnsNameForPublicIP:
305        value: devopscleazure
306'''
307
308RETURN = '''
309deployment:
310    description: Deployment details.
311    type: complex
312    returned: always
313    contains:
314        group_name:
315            description:
316                - Name of the resource group.
317            type: str
318            returned: always
319            sample: myResourceGroup
320        id:
321            description:
322                - The Azure ID of the deployment.
323            type: str
324            returned: always
325            sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Resources/deployments/myD
326                     eployment"
327        instances:
328            description:
329                - Provides the public IP addresses for each VM instance.
330            type: list
331            returned: always
332            contains:
333                ips:
334                    description:
335                        - List of Public IP addresses.
336                    type: list
337                    returned: always
338                    contains:
339                        dns_settings:
340                            description:
341                                - DNS Settings.
342                            type: complex
343                            returned: always
344                            contains:
345                                domain_name_label:
346                                    description:
347                                        - Domain Name Label.
348                                    type: str
349                                    returned: always
350                                    sample: myvirtualmachine
351                                fqdn:
352                                    description:
353                                        - Fully Qualified Domain Name.
354                                    type: str
355                                    returned: always
356                                    sample: myvirtualmachine.eastus2.cloudapp.azure.com
357                        id:
358                            description:
359                                - Public IP resource id.
360                            returned: always
361                            type: str
362                            sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/p
363                                     ublicIPAddresses/myPublicIP"
364                        name:
365                            description:
366                                -  Public IP resource name.
367                            returned: always
368                            type: str
369                            sample: myPublicIP
370                        public_ip:
371                            description:
372                                - Public IP address value.
373                            returned: always
374                            type: str
375                            sample: 104.209.244.123
376                        public_ip_allocation_method:
377                            description:
378                                - Public IP allocation method.
379                            returned: always
380                            type: str
381                            sample: Dynamic
382                vm_name:
383                    description:
384                        - Virtual machine name.
385                    returned: always
386                    type: str
387                    sample: myvirtualmachine
388        name:
389          description:
390              - Name of the deployment.
391          type: str
392          returned: always
393          sample: myDeployment
394        outputs:
395          description:
396              - Dictionary of outputs received from the deployment.
397          type: complex
398          returned: always
399          sample: { "hostname": { "type": "String", "value": "myvirtualmachine.eastus2.cloudapp.azure.com" } }
400'''
401
402import time
403
404try:
405    from azure.common.credentials import ServicePrincipalCredentials
406    import time
407    import yaml
408except ImportError as exc:
409    IMPORT_ERROR = "Error importing module prerequisites: %s" % exc
410
411try:
412    from itertools import chain
413    from azure.common.exceptions import CloudError
414    from azure.mgmt.resource.resources import ResourceManagementClient
415    from azure.mgmt.network import NetworkManagementClient
416
417except ImportError:
418    # This is handled in azure_rm_common
419    pass
420
421from ansible_collections.azure.azcollection.plugins.module_utils.azure_rm_common import AzureRMModuleBase
422
423
424class AzureRMDeploymentManager(AzureRMModuleBase):
425
426    def __init__(self):
427
428        self.module_arg_spec = dict(
429            resource_group=dict(type='str', required=True, aliases=['resource_group_name']),
430            name=dict(type='str', default="ansible-arm", aliases=['deployment_name']),
431            state=dict(type='str', default='present', choices=['present', 'absent']),
432            template=dict(type='dict', default=None),
433            parameters=dict(type='dict', default=None),
434            template_link=dict(type='str', default=None),
435            parameters_link=dict(type='str', default=None),
436            location=dict(type='str', default="westus"),
437            deployment_mode=dict(type='str', default='incremental', choices=['complete', 'incremental']),
438            wait_for_deployment_completion=dict(type='bool', default=True),
439            wait_for_deployment_polling_period=dict(type='int', default=10)
440        )
441
442        mutually_exclusive = [('template', 'template_link'),
443                              ('parameters', 'parameters_link')]
444
445        self.resource_group = None
446        self.state = None
447        self.template = None
448        self.parameters = None
449        self.template_link = None
450        self.parameters_link = None
451        self.location = None
452        self.deployment_mode = None
453        self.name = None
454        self.wait_for_deployment_completion = None
455        self.wait_for_deployment_polling_period = None
456        self.tags = None
457        self.append_tags = None
458
459        self.results = dict(
460            deployment=dict(),
461            changed=False,
462            msg=""
463        )
464
465        super(AzureRMDeploymentManager, self).__init__(derived_arg_spec=self.module_arg_spec,
466                                                       mutually_exclusive=mutually_exclusive,
467                                                       supports_check_mode=False)
468
469    def exec_module(self, **kwargs):
470
471        for key in list(self.module_arg_spec.keys()) + ['append_tags', 'tags']:
472            setattr(self, key, kwargs[key])
473
474        if self.state == 'present':
475            deployment = self.deploy_template()
476            if deployment is None:
477                self.results['deployment'] = dict(
478                    name=self.name,
479                    group_name=self.resource_group,
480                    id=None,
481                    outputs=None,
482                    instances=None
483                )
484            else:
485                self.results['deployment'] = dict(
486                    name=deployment.name,
487                    group_name=self.resource_group,
488                    id=deployment.id,
489                    outputs=deployment.properties.outputs,
490                    instances=self._get_instances(deployment)
491                )
492
493            self.results['changed'] = True
494            self.results['msg'] = 'deployment succeeded'
495        else:
496            try:
497                if self.get_resource_group(self.resource_group):
498                    self.destroy_resource_group()
499                    self.results['changed'] = True
500                    self.results['msg'] = "deployment deleted"
501            except CloudError:
502                # resource group does not exist
503                pass
504
505        return self.results
506
507    def deploy_template(self):
508        """
509        Deploy the targeted template and parameters
510        :param module: Ansible module containing the validated configuration for the deployment template
511        :param client: resource management client for azure
512        :param conn_info: connection info needed
513        :return:
514        """
515
516        deploy_parameter = self.rm_models.DeploymentProperties(mode=self.deployment_mode)
517        if not self.parameters_link:
518            deploy_parameter.parameters = self.parameters
519        else:
520            deploy_parameter.parameters_link = self.rm_models.ParametersLink(
521                uri=self.parameters_link
522            )
523        if not self.template_link:
524            deploy_parameter.template = self.template
525        else:
526            deploy_parameter.template_link = self.rm_models.TemplateLink(
527                uri=self.template_link
528            )
529
530        if self.append_tags and self.tags:
531            try:
532                # fetch the RG directly (instead of using the base helper) since we don't want to exit if it's missing
533                rg = self.rm_client.resource_groups.get(self.resource_group)
534                if rg.tags:
535                    self.tags = dict(self.tags, **rg.tags)
536            except CloudError:
537                # resource group does not exist
538                pass
539
540        params = self.rm_models.ResourceGroup(location=self.location, tags=self.tags)
541
542        try:
543            self.rm_client.resource_groups.create_or_update(self.resource_group, params)
544        except CloudError as exc:
545            self.fail("Resource group create_or_update failed with status code: %s and message: %s" %
546                      (exc.status_code, exc.message))
547        try:
548            result = self.rm_client.deployments.create_or_update(self.resource_group,
549                                                                 self.name,
550                                                                 deploy_parameter)
551
552            deployment_result = None
553            if self.wait_for_deployment_completion:
554                deployment_result = self.get_poller_result(result)
555                while deployment_result.properties is None or deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted',
556                                                                                                                      'Succeeded']:
557                    time.sleep(self.wait_for_deployment_polling_period)
558                    deployment_result = self.rm_client.deployments.get(self.resource_group, self.name)
559        except CloudError as exc:
560            failed_deployment_operations = self._get_failed_deployment_operations(self.name)
561            self.log("Deployment failed %s: %s" % (exc.status_code, exc.message))
562            error_msg = self._error_msg_from_cloud_error(exc)
563            self.fail(error_msg, failed_deployment_operations=failed_deployment_operations)
564
565        if self.wait_for_deployment_completion and deployment_result.properties.provisioning_state != 'Succeeded':
566            self.log("provisioning state: %s" % deployment_result.properties.provisioning_state)
567            failed_deployment_operations = self._get_failed_deployment_operations(self.name)
568            self.fail('Deployment failed. Deployment id: %s' % deployment_result.id,
569                      failed_deployment_operations=failed_deployment_operations)
570
571        return deployment_result
572
573    def destroy_resource_group(self):
574        """
575        Destroy the targeted resource group
576        """
577        try:
578            result = self.rm_client.resource_groups.delete(self.resource_group)
579            result.wait()  # Blocking wait till the delete is finished
580        except CloudError as e:
581            if e.status_code == 404 or e.status_code == 204:
582                return
583            else:
584                self.fail("Delete resource group and deploy failed with status code: %s and message: %s" %
585                          (e.status_code, e.message))
586
587    def _get_failed_nested_operations(self, current_operations):
588        new_operations = []
589        for operation in current_operations:
590            if operation.properties.provisioning_state == 'Failed':
591                new_operations.append(operation)
592                if operation.properties.target_resource and \
593                   'Microsoft.Resources/deployments' in operation.properties.target_resource.id:
594                    nested_deployment = operation.properties.target_resource.resource_name
595                    try:
596                        nested_operations = self.rm_client.deployment_operations.list(self.resource_group,
597                                                                                      nested_deployment)
598                    except CloudError as exc:
599                        self.fail("List nested deployment operations failed with status code: %s and message: %s" %
600                                  (exc.status_code, exc.message))
601                    new_nested_operations = self._get_failed_nested_operations(nested_operations)
602                    new_operations += new_nested_operations
603        return new_operations
604
605    def _get_failed_deployment_operations(self, name):
606        results = []
607        # time.sleep(15) # there is a race condition between when we ask for deployment status and when the
608        #               # status is available.
609
610        try:
611            operations = self.rm_client.deployment_operations.list(self.resource_group, name)
612        except CloudError as exc:
613            self.fail("Get deployment failed with status code: %s and message: %s" %
614                      (exc.status_code, exc.message))
615        try:
616            results = [
617                dict(
618                    id=op.id,
619                    operation_id=op.operation_id,
620                    status_code=op.properties.status_code,
621                    status_message=op.properties.status_message,
622                    target_resource=dict(
623                        id=op.properties.target_resource.id,
624                        resource_name=op.properties.target_resource.resource_name,
625                        resource_type=op.properties.target_resource.resource_type
626                    ) if op.properties.target_resource else None,
627                    provisioning_state=op.properties.provisioning_state,
628                )
629                for op in self._get_failed_nested_operations(operations)
630            ]
631        except Exception:
632            # If we fail here, the original error gets lost and user receives wrong error message/stacktrace
633            pass
634        self.log(dict(failed_deployment_operations=results), pretty_print=True)
635        return results
636
637    def _get_instances(self, deployment):
638        dep_tree = self._build_hierarchy(deployment.properties.dependencies)
639        vms = self._get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines")
640        vms_and_nics = [(vm, self._get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces"))
641                        for vm in vms]
642        vms_and_ips = [(vm['dep'], self._nic_to_public_ips_instance(nics))
643                       for vm, nics in vms_and_nics]
644        return [dict(vm_name=vm.resource_name, ips=[self._get_ip_dict(ip)
645                                                    for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0]
646
647    def _get_dependencies(self, dep_tree, resource_type):
648        matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type]
649        for child_tree in [value['children'] for value in dep_tree.values()]:
650            matches += self._get_dependencies(child_tree, resource_type)
651        return matches
652
653    def _build_hierarchy(self, dependencies, tree=None):
654        tree = dict(top=True) if tree is None else tree
655        for dep in dependencies:
656            if dep.resource_name not in tree:
657                tree[dep.resource_name] = dict(dep=dep, children=dict())
658            if isinstance(dep, self.rm_models.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0:
659                self._build_hierarchy(dep.depends_on, tree[dep.resource_name]['children'])
660
661        if 'top' in tree:
662            tree.pop('top', None)
663            keys = list(tree.keys())
664            for key1 in keys:
665                for key2 in keys:
666                    if key2 in tree and key1 in tree[key2]['children'] and key1 in tree:
667                        tree[key2]['children'][key1] = tree[key1]
668                        tree.pop(key1)
669        return tree
670
671    def _get_ip_dict(self, ip):
672        ip_dict = dict(name=ip.name,
673                       id=ip.id,
674                       public_ip=ip.ip_address,
675                       public_ip_allocation_method=str(ip.public_ip_allocation_method)
676                       )
677        if ip.dns_settings:
678            ip_dict['dns_settings'] = {
679                'domain_name_label': ip.dns_settings.domain_name_label,
680                'fqdn': ip.dns_settings.fqdn
681            }
682        return ip_dict
683
684    def _nic_to_public_ips_instance(self, nics):
685        return [self.network_client.public_ip_addresses.get(public_ip_id.split('/')[4], public_ip_id.split('/')[-1])
686                for nic_obj in (self.network_client.network_interfaces.get(self.resource_group,
687                                                                           nic['dep'].resource_name) for nic in nics)
688                for public_ip_id in [ip_conf_instance.public_ip_address.id
689                                     for ip_conf_instance in nic_obj.ip_configurations
690                                     if ip_conf_instance.public_ip_address]]
691
692    def _error_msg_from_cloud_error(self, exc):
693        msg = ''
694        status_code = str(exc.status_code)
695        if status_code.startswith('2'):
696            msg = 'Deployment failed: {0}'.format(exc.message)
697        else:
698            msg = 'Deployment failed with status code: {0} and message: {1}'.format(status_code, exc.message)
699        return msg
700
701
702def main():
703    AzureRMDeploymentManager()
704
705
706if __name__ == '__main__':
707    main()
708