#!/usr/bin/python # # Copyright (c) Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: azure_rm_deployment short_description: Create or destroy Azure Resource Manager template deployments version_added: "2.1" description: - Create or destroy Azure Resource Manager template deployments via the Azure SDK for Python. - You can find some quick start templates in GitHub here U(https://github.com/azure/azure-quickstart-templates). - For more information on Azure Resource Manager templates see U(https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/). options: resource_group: description: - The resource group name to use or create to host the deployed template. required: true aliases: - resource_group_name name: description: - The name of the deployment to be tracked in the resource group deployment history. - Re-using a deployment name will overwrite the previous value in the resource group's deployment history. default: ansible-arm aliases: - deployment_name location: description: - The geo-locations in which the resource group will be located. default: westus deployment_mode: description: - In incremental mode, resources are deployed without deleting existing resources that are not included in the template. - In complete mode resources are deployed and existing resources in the resource group not included in the template are deleted. default: incremental choices: - complete - incremental template: description: - A hash containing the templates inline. This parameter is mutually exclusive with I(template_link). - Either I(template) or I(template_link) is required if I(state=present). type: dict template_link: description: - Uri of file containing the template body. This parameter is mutually exclusive with I(template). - Either I(template) or I(template_link) is required if I(state=present). parameters: description: - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with I(parameters_link). - Either I(parameters_link) or I(parameters) is required if I(state=present). type: dict parameters_link: description: - Uri of file containing the parameters body. This parameter is mutually exclusive with I(parameters). - Either I(parameters_link) or I(parameters) is required if I(state=present). wait_for_deployment_completion: description: - Whether or not to block until the deployment has completed. type: bool default: 'yes' wait_for_deployment_polling_period: description: - Time (in seconds) to wait between polls when waiting for deployment completion. default: 10 state: description: - If I(state=present), template will be created. - If I(state=present) and deployment exists, it will be updated. - If I(state=absent), stack will be removed. default: present choices: - present - absent extends_documentation_fragment: - azure - azure_tags author: - David Justice (@devigned) - Laurent Mazuel (@lmazuel) - Andre Price (@obsoleted) ''' EXAMPLES = ''' # Destroy a template deployment - name: Destroy Azure Deploy azure_rm_deployment: resource_group: myResourceGroup name: myDeployment state: absent # Create or update a template deployment based on uris using parameter and template links - name: Create Azure Deploy azure_rm_deployment: resource_group: myResourceGroup name: myDeployment template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.json' parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-simple-linux/azuredeploy.parameters.json' # Create or update a template deployment based on a uri to the template and parameters specified inline. # This deploys a VM with SSH support for a given public key, then stores the result in 'azure_vms'. The result is then # 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. --- - name: Create Azure Deploy azure_rm_deployment: resource_group: myResourceGroup name: myDeployment parameters: newStorageAccountName: value: devopsclestorage1 adminUsername: value: devopscle dnsNameForPublicIP: value: devopscleazure location: value: West US vmSize: value: Standard_A2 vmName: value: ansibleSshVm sshKeyData: value: YOUR_SSH_PUBLIC_KEY template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-vm-sshkey/azuredeploy.json' register: azure - name: Add new instance to host group add_host: hostname: "{{ item['ips'][0].public_ip }}" groupname: azure_vms loop: "{{ azure.deployment.instances }}" # Deploy an Azure WebApp running a hello world'ish node app - name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js azure_rm_deployment: resource_group: myResourceGroup name: myDeployment parameters: repoURL: value: 'https://github.com/devigned/az-roadshow-oss.git' siteName: value: devopscleweb hostingPlanName: value: someplan siteLocation: value: westus sku: value: Standard template_link: 'https://raw.githubusercontent.com/azure/azure-quickstart-templates/master/201-web-app-github-deploy/azuredeploy.json' # Create or update a template deployment based on an inline template and parameters - name: Create Azure Deploy azure_rm_deployment: resource_group: myResourceGroup name: myDeployment template: $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" contentVersion: "1.0.0.0" parameters: newStorageAccountName: type: "string" metadata: description: "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed." adminUsername: type: "string" metadata: description: "User name for the Virtual Machine." adminPassword: type: "securestring" metadata: description: "Password for the Virtual Machine." dnsNameForPublicIP: type: "string" metadata: description: "Unique DNS Name for the Public IP used to access the Virtual Machine." ubuntuOSVersion: type: "string" defaultValue: "14.04.2-LTS" allowedValues: - "12.04.5-LTS" - "14.04.2-LTS" - "15.04" metadata: description: > The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04." variables: location: "West US" imagePublisher: "Canonical" imageOffer: "UbuntuServer" OSDiskName: "osdiskforlinuxsimple" nicName: "myVMNic" addressPrefix: "192.0.2.0/24" subnetName: "Subnet" subnetPrefix: "10.0.0.0/24" storageAccountType: "Standard_LRS" publicIPAddressName: "myPublicIP" publicIPAddressType: "Dynamic" vmStorageAccountContainerName: "vhds" vmName: "MyUbuntuVM" vmSize: "Standard_D1" virtualNetworkName: "MyVNET" vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]" subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]" resources: - type: "Microsoft.Storage/storageAccounts" name: "[parameters('newStorageAccountName')]" apiVersion: "2015-05-01-preview" location: "[variables('location')]" properties: accountType: "[variables('storageAccountType')]" - apiVersion: "2015-05-01-preview" type: "Microsoft.Network/publicIPAddresses" name: "[variables('publicIPAddressName')]" location: "[variables('location')]" properties: publicIPAllocationMethod: "[variables('publicIPAddressType')]" dnsSettings: domainNameLabel: "[parameters('dnsNameForPublicIP')]" - type: "Microsoft.Network/virtualNetworks" apiVersion: "2015-05-01-preview" name: "[variables('virtualNetworkName')]" location: "[variables('location')]" properties: addressSpace: addressPrefixes: - "[variables('addressPrefix')]" subnets: - name: "[variables('subnetName')]" properties: addressPrefix: "[variables('subnetPrefix')]" - type: "Microsoft.Network/networkInterfaces" apiVersion: "2015-05-01-preview" name: "[variables('nicName')]" location: "[variables('location')]" dependsOn: - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]" - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]" properties: ipConfigurations: - name: "ipconfig1" properties: privateIPAllocationMethod: "Dynamic" publicIPAddress: id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" subnet: id: "[variables('subnetRef')]" - type: "Microsoft.Compute/virtualMachines" apiVersion: "2015-06-15" name: "[variables('vmName')]" location: "[variables('location')]" dependsOn: - "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]" - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]" properties: hardwareProfile: vmSize: "[variables('vmSize')]" osProfile: computername: "[variables('vmName')]" adminUsername: "[parameters('adminUsername')]" adminPassword: "[parameters('adminPassword')]" storageProfile: imageReference: publisher: "[variables('imagePublisher')]" offer: "[variables('imageOffer')]" sku: "[parameters('ubuntuOSVersion')]" version: "latest" osDisk: name: "osdisk" vhd: uri: > [concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/', variables('OSDiskName'),'.vhd')] caching: "ReadWrite" createOption: "FromImage" networkProfile: networkInterfaces: - id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" diagnosticsProfile: bootDiagnostics: enabled: "true" storageUri: "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]" parameters: newStorageAccountName: value: devopsclestorage adminUsername: value: devopscle adminPassword: value: Password1! dnsNameForPublicIP: value: devopscleazure ''' RETURN = ''' deployment: description: Deployment details. type: complex returned: always contains: group_name: description: - Name of the resource group. type: str returned: always sample: myResourceGroup id: description: - The Azure ID of the deployment. type: str returned: always sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Resources/deployments/myD eployment" instances: description: - Provides the public IP addresses for each VM instance. type: list returned: always contains: ips: description: - List of Public IP addresses. type: list returned: always contains: dns_settings: description: - DNS Settings. type: complex returned: always contains: domain_name_label: description: - Domain Name Label. type: str returned: always sample: myvirtualmachine fqdn: description: - Fully Qualified Domain Name. type: str returned: always sample: myvirtualmachine.eastus2.cloudapp.azure.com id: description: - Public IP resource id. returned: always type: str sample: "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.Network/p ublicIPAddresses/myPublicIP" name: description: - Public IP resource name. returned: always type: str sample: myPublicIP public_ip: description: - Public IP address value. returned: always type: str sample: 104.209.244.123 public_ip_allocation_method: description: - Public IP allocation method. returned: always type: str sample: Dynamic vm_name: description: - Virtual machine name. returned: always type: str sample: myvirtualmachine name: description: - Name of the deployment. type: str returned: always sample: myDeployment outputs: description: - Dictionary of outputs received from the deployment. type: complex returned: always sample: { "hostname": { "type": "String", "value": "myvirtualmachine.eastus2.cloudapp.azure.com" } } ''' import time try: from azure.common.credentials import ServicePrincipalCredentials import time import yaml except ImportError as exc: IMPORT_ERROR = "Error importing module prerequisites: %s" % exc try: from itertools import chain from azure.common.exceptions import CloudError from azure.mgmt.resource.resources import ResourceManagementClient from azure.mgmt.network import NetworkManagementClient except ImportError: # This is handled in azure_rm_common pass from ansible.module_utils.azure_rm_common import AzureRMModuleBase class AzureRMDeploymentManager(AzureRMModuleBase): def __init__(self): self.module_arg_spec = dict( resource_group=dict(type='str', required=True, aliases=['resource_group_name']), name=dict(type='str', default="ansible-arm", aliases=['deployment_name']), state=dict(type='str', default='present', choices=['present', 'absent']), template=dict(type='dict', default=None), parameters=dict(type='dict', default=None), template_link=dict(type='str', default=None), parameters_link=dict(type='str', default=None), location=dict(type='str', default="westus"), deployment_mode=dict(type='str', default='incremental', choices=['complete', 'incremental']), wait_for_deployment_completion=dict(type='bool', default=True), wait_for_deployment_polling_period=dict(type='int', default=10) ) mutually_exclusive = [('template', 'template_link'), ('parameters', 'parameters_link')] self.resource_group = None self.state = None self.template = None self.parameters = None self.template_link = None self.parameters_link = None self.location = None self.deployment_mode = None self.name = None self.wait_for_deployment_completion = None self.wait_for_deployment_polling_period = None self.tags = None self.append_tags = None self.results = dict( deployment=dict(), changed=False, msg="" ) super(AzureRMDeploymentManager, self).__init__(derived_arg_spec=self.module_arg_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=False) def exec_module(self, **kwargs): for key in list(self.module_arg_spec.keys()) + ['append_tags', 'tags']: setattr(self, key, kwargs[key]) if self.state == 'present': deployment = self.deploy_template() if deployment is None: self.results['deployment'] = dict( name=self.name, group_name=self.resource_group, id=None, outputs=None, instances=None ) else: self.results['deployment'] = dict( name=deployment.name, group_name=self.resource_group, id=deployment.id, outputs=deployment.properties.outputs, instances=self._get_instances(deployment) ) self.results['changed'] = True self.results['msg'] = 'deployment succeeded' else: try: if self.get_resource_group(self.resource_group): self.destroy_resource_group() self.results['changed'] = True self.results['msg'] = "deployment deleted" except CloudError: # resource group does not exist pass return self.results def deploy_template(self): """ Deploy the targeted template and parameters :param module: Ansible module containing the validated configuration for the deployment template :param client: resource management client for azure :param conn_info: connection info needed :return: """ deploy_parameter = self.rm_models.DeploymentProperties(mode=self.deployment_mode) if not self.parameters_link: deploy_parameter.parameters = self.parameters else: deploy_parameter.parameters_link = self.rm_models.ParametersLink( uri=self.parameters_link ) if not self.template_link: deploy_parameter.template = self.template else: deploy_parameter.template_link = self.rm_models.TemplateLink( uri=self.template_link ) if self.append_tags and self.tags: try: # fetch the RG directly (instead of using the base helper) since we don't want to exit if it's missing rg = self.rm_client.resource_groups.get(self.resource_group) if rg.tags: self.tags = dict(self.tags, **rg.tags) except CloudError: # resource group does not exist pass params = self.rm_models.ResourceGroup(location=self.location, tags=self.tags) try: self.rm_client.resource_groups.create_or_update(self.resource_group, params) except CloudError as exc: self.fail("Resource group create_or_update failed with status code: %s and message: %s" % (exc.status_code, exc.message)) try: result = self.rm_client.deployments.create_or_update(self.resource_group, self.name, deploy_parameter) deployment_result = None if self.wait_for_deployment_completion: deployment_result = self.get_poller_result(result) while deployment_result.properties is None or deployment_result.properties.provisioning_state not in ['Canceled', 'Failed', 'Deleted', 'Succeeded']: time.sleep(self.wait_for_deployment_polling_period) deployment_result = self.rm_client.deployments.get(self.resource_group, self.name) except CloudError as exc: failed_deployment_operations = self._get_failed_deployment_operations(self.name) self.log("Deployment failed %s: %s" % (exc.status_code, exc.message)) self.fail("Deployment failed with status code: %s and message: %s" % (exc.status_code, exc.message), failed_deployment_operations=failed_deployment_operations) if self.wait_for_deployment_completion and deployment_result.properties.provisioning_state != 'Succeeded': self.log("provisioning state: %s" % deployment_result.properties.provisioning_state) failed_deployment_operations = self._get_failed_deployment_operations(self.name) self.fail('Deployment failed. Deployment id: %s' % deployment_result.id, failed_deployment_operations=failed_deployment_operations) return deployment_result def destroy_resource_group(self): """ Destroy the targeted resource group """ try: result = self.rm_client.resource_groups.delete(self.resource_group) result.wait() # Blocking wait till the delete is finished except CloudError as e: if e.status_code == 404 or e.status_code == 204: return else: self.fail("Delete resource group and deploy failed with status code: %s and message: %s" % (e.status_code, e.message)) def _get_failed_nested_operations(self, current_operations): new_operations = [] for operation in current_operations: if operation.properties.provisioning_state == 'Failed': new_operations.append(operation) if operation.properties.target_resource and \ 'Microsoft.Resources/deployments' in operation.properties.target_resource.id: nested_deployment = operation.properties.target_resource.resource_name try: nested_operations = self.rm_client.deployment_operations.list(self.resource_group, nested_deployment) except CloudError as exc: self.fail("List nested deployment operations failed with status code: %s and message: %s" % (exc.status_code, exc.message)) new_nested_operations = self._get_failed_nested_operations(nested_operations) new_operations += new_nested_operations return new_operations def _get_failed_deployment_operations(self, name): results = [] # time.sleep(15) # there is a race condition between when we ask for deployment status and when the # # status is available. try: operations = self.rm_client.deployment_operations.list(self.resource_group, name) except CloudError as exc: self.fail("Get deployment failed with status code: %s and message: %s" % (exc.status_code, exc.message)) try: results = [ dict( id=op.id, operation_id=op.operation_id, status_code=op.properties.status_code, status_message=op.properties.status_message, target_resource=dict( id=op.properties.target_resource.id, resource_name=op.properties.target_resource.resource_name, resource_type=op.properties.target_resource.resource_type ) if op.properties.target_resource else None, provisioning_state=op.properties.provisioning_state, ) for op in self._get_failed_nested_operations(operations) ] except Exception: # If we fail here, the original error gets lost and user receives wrong error message/stacktrace pass self.log(dict(failed_deployment_operations=results), pretty_print=True) return results def _get_instances(self, deployment): dep_tree = self._build_hierarchy(deployment.properties.dependencies) vms = self._get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") vms_and_nics = [(vm, self._get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces")) for vm in vms] vms_and_ips = [(vm['dep'], self._nic_to_public_ips_instance(nics)) for vm, nics in vms_and_nics] return [dict(vm_name=vm.resource_name, ips=[self._get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] def _get_dependencies(self, dep_tree, resource_type): matches = [value for value in dep_tree.values() if value['dep'].resource_type == resource_type] for child_tree in [value['children'] for value in dep_tree.values()]: matches += self._get_dependencies(child_tree, resource_type) return matches def _build_hierarchy(self, dependencies, tree=None): tree = dict(top=True) if tree is None else tree for dep in dependencies: if dep.resource_name not in tree: tree[dep.resource_name] = dict(dep=dep, children=dict()) if isinstance(dep, self.rm_models.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: self._build_hierarchy(dep.depends_on, tree[dep.resource_name]['children']) if 'top' in tree: tree.pop('top', None) keys = list(tree.keys()) for key1 in keys: for key2 in keys: if key2 in tree and key1 in tree[key2]['children'] and key1 in tree: tree[key2]['children'][key1] = tree[key1] tree.pop(key1) return tree def _get_ip_dict(self, ip): ip_dict = dict(name=ip.name, id=ip.id, public_ip=ip.ip_address, public_ip_allocation_method=str(ip.public_ip_allocation_method) ) if ip.dns_settings: ip_dict['dns_settings'] = { 'domain_name_label': ip.dns_settings.domain_name_label, 'fqdn': ip.dns_settings.fqdn } return ip_dict def _nic_to_public_ips_instance(self, nics): return [self.network_client.public_ip_addresses.get(public_ip_id.split('/')[4], public_ip_id.split('/')[-1]) for nic_obj in (self.network_client.network_interfaces.get(self.resource_group, nic['dep'].resource_name) for nic in nics) for public_ip_id in [ip_conf_instance.public_ip_address.id for ip_conf_instance in nic_obj.ip_configurations if ip_conf_instance.public_ip_address]] def main(): AzureRMDeploymentManager() if __name__ == '__main__': main()