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