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