#!/usr/bin/python # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . DOCUMENTATION = ''' --- module: azure_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 https://github.com/azure/azure-quickstart-templates. If you would like to find out more information about Azure Resource Manager templates, see https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/. options: subscription_id: description: - The Azure subscription to deploy the template into. required: true resource_group_name: description: - The resource group name to use or create to host the deployed template required: true state: description: - If state is "present", template will be created. If state is "present" and if deployment exists, it will be updated. If state is "absent", stack will be removed. required: true template: description: - A hash containg the templates inline. This parameter is mutually exclusive with 'template_link'. Either one of them is required if "state" parameter is "present". required: false default: None template_link: description: - Uri of file containing the template body. This parameter is mutually exclusive with 'template'. Either one of them is required if "state" parameter is "present". required: false default: None parameters: description: - A hash of all the required template variables for the deployment template. This parameter is mutually exclusive with 'parameters_link'. Either one of them is required if "state" parameter is "present". required: false default: None parameters_link: description: - Uri of file containing the parameters body. This parameter is mutually exclusive with 'parameters'. Either one of them is required if "state" parameter is "present". required: false default: None location: description: - The geo-locations in which the resource group will be located. require: false default: West US author: "David Justice (@devigned) / Laurent Mazuel (@lmazuel) / Andre Price (@obsoleted)" ''' EXAMPLES = ''' # Destroy a template deployment - name: Destroy Azure Deploy azure_deploy: state: absent subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle # Create or update a template deployment based on uris to paramters and a template - name: Create Azure Deploy azure_deploy: state: present subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle parameters_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.parameters.json' template_link: 'https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/101-simple-linux-vm/azuredeploy.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. --- - hosts: localhost tasks: - name: Destroy Azure Deploy azure_deployment: state: absent subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle - name: Create Azure Deploy azure_deployment: state: present subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle 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 with_items: azure.instances - hosts: azure_vms user: devopscle tasks: - name: Wait for SSH to come up wait_for: port=22 timeout=2000 state=started - name: echo the hostname of the vm shell: hostname # Deploy an Azure WebApp running a hello world'ish node app - name: Create Azure WebApp Deployment at http://devopscleweb.azurewebsites.net/hello.js azure_deployment: state: present subscription_id: cbbdaed0-fea9-4693-bf0c-d446ac93c030 resource_group_name: dev-ops-cle-webapp 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_deploy: state: present subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx resource_group_name: dev-ops-cle 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: "10.0.0.0/16" 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 = ''' ''' try: import time import yaml from itertools import chain from azure.common.credentials import ServicePrincipalCredentials from azure.common.exceptions import CloudError from azure.mgmt.resource.resources.models import ( DeploymentProperties, ParametersLink, TemplateLink, Deployment, ResourceGroup, Dependency ) from azure.mgmt.resource.resources import ResourceManagementClient, ResourceManagementClientConfiguration from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration HAS_DEPS = True except ImportError: HAS_DEPS = False AZURE_URL = "https://management.azure.com" def get_azure_connection_info(module): azure_url = module.params.get('azure_url') tenant_id = module.params.get('tenant_id') client_id = module.params.get('client_id') client_secret = module.params.get('client_secret') resource_group_name = module.params.get('resource_group_name') subscription_id = module.params.get('subscription_id') if not azure_url: if 'AZURE_URL' in os.environ: azure_url = os.environ['AZURE_URL'] else: azure_url = None if not subscription_id: if 'AZURE_SUBSCRIPTION_ID' in os.environ: subscription_id = os.environ['AZURE_SUBSCRIPTION_ID'] else: subscription_id = None if not resource_group_name: if 'AZURE_RESOURCE_GROUP_NAME' in os.environ: resource_group_name = os.environ['AZURE_RESOURCE_GROUP_NAME'] else: resource_group_name = None if not tenant_id: if 'AZURE_TENANT_ID' in os.environ: tenant_id = os.environ['AZURE_TENANT_ID'] elif 'AZURE_DOMAIN' in os.environ: tenant_id = os.environ['AZURE_DOMAIN'] else: tenant_id = None if not client_id: if 'AZURE_CLIENT_ID' in os.environ: client_id = os.environ['AZURE_CLIENT_ID'] else: client_id = None if not client_secret: if 'AZURE_CLIENT_SECRET' in os.environ: client_secret = os.environ['AZURE_CLIENT_SECRET'] else: client_secret = None return dict(azure_url=azure_url, tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, resource_group_name=resource_group_name, subscription_id=subscription_id) def build_deployment_body(module): """ Build the deployment body from the module parameters :param module: Ansible module containing the validated configuration for the deployment template :return: body as dict """ properties = dict(mode='Incremental') properties['templateLink'] = \ dict(uri=module.params.get('template_link'), contentVersion=module.params.get('content_version')) properties['parametersLink'] = \ dict(uri=module.params.get('parameters_link'), contentVersion=module.params.get('content_version')) return dict(properties=properties) def get_failed_nested_operations(client, resource_group, 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 nested_operations = client.deployment_operations.list(resource_group, nested_deployment) new_nested_operations = get_failed_nested_operations(client, resource_group, nested_operations) new_operations += new_nested_operations return new_operations def get_failed_deployment_operations(module, client, resource_group, deployment_name): operations = client.deployment_operations.list(resource_group, deployment_name) return [ 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 get_failed_nested_operations(client, resource_group, operations) ] def deploy_template(module, client, conn_info): """ 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: """ deployment_name = conn_info["deployment_name"] group_name = conn_info["resource_group_name"] deploy_parameter = DeploymentProperties() deploy_parameter.mode = module.params.get('deployment_mode') if module.params.get('parameters_link') is None: deploy_parameter.parameters = module.params.get('parameters') else: parameters_link = ParametersLink( uri = module.params.get('parameters_link') ) deploy_parameter.parameters_link = parameters_link if module.params.get('template_link') is None: deploy_parameter.template = module.params.get('template') else: template_link = TemplateLink( uri = module.params.get('template_link') ) deploy_parameter.template_link = template_link params = ResourceGroup(location=module.params.get('location'), tags=module.params.get('tags')) try: client.resource_groups.create_or_update(group_name, params) result = client.deployments.create_or_update(group_name, deployment_name, deploy_parameter) deployment_result = result.result() # Blocking wait, return the Deployment object if module.params.get('wait_for_deployment_completion'): while not deployment_result.properties.provisioning_state in ['Canceled', 'Failed', 'Deleted', 'Succeeded']: deployment_result = client.deployments.get(group_name, deployment_name) time.sleep(module.params.get('wait_for_deployment_polling_period')) if deployment_result.properties.provisioning_state == 'Succeeded': return deployment_result failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) module.fail_json(msg='Deployment failed. Deployment id: %s' % (deployment_result.id), failed_deployment_operations=failed_deployment_operations) except CloudError as e: failed_deployment_operations = get_failed_deployment_operations(module, client, group_name, deployment_name) module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message),failed_deployment_operations=failed_deployment_operations) def destroy_resource_group(module, client, conn_info): """ Destroy the targeted resource group :param module: ansible module :param client: resource management client for azure :param conn_info: connection info needed :return: if the result caused a change in the deployment """ try: result = client.resource_groups.delete(conn_info['resource_group_name']) result.wait() # Blocking wait till the delete is finished except CloudError as e: if e.status_code == 404 or e.status_code == 204: return True else: module.fail_json( msg='Delete resource group and deploy failed with status code: %s and message: %s' % (e.status_code, e.message)) def get_dependencies(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 += get_dependencies(child_tree, resource_type) return matches def build_hierarchy(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, Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0: 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(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(client, group, nics): return [client.public_ip_addresses.get(group, public_ip_id.split('/')[-1]) for nic_obj in [client.network_interfaces.get(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 get_instances(client, group, deployment): dep_tree = build_hierarchy(deployment.properties.dependencies) vms = get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines") vms_and_nics = [(vm, get_dependencies(vm['children'], "Microsoft.Network/networkInterfaces")) for vm in vms] vms_and_ips = [(vm['dep'], nic_to_public_ips_instance(client, group, nics)) for vm, nics in vms_and_nics] return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips if len(ips) > 0] def main(): argument_spec = dict( azure_url=dict(default=AZURE_URL), subscription_id=dict(), client_secret=dict(no_log=True), client_id=dict(required=True), tenant_id=dict(required=True), resource_group_name=dict(required=True), state=dict(default='present', choices=['present', 'absent']), template=dict(default=None, type='dict'), parameters=dict(default=None, type='dict'), template_link=dict(default=None), parameters_link=dict(default=None), location=dict(default="West US"), deployment_mode=dict(default='Complete', choices=['Complete', 'Incremental']), deployment_name=dict(default="ansible-arm"), wait_for_deployment_completion=dict(default=True), wait_for_deployment_polling_period=dict(default=30) ) module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=[['template_link', 'template'], ['parameters_link', 'parameters']], ) if not HAS_DEPS: module.fail_json(msg='requests and azure are required for this module') conn_info = get_azure_connection_info(module) credentials = ServicePrincipalCredentials(client_id=conn_info['client_id'], secret=conn_info['client_secret'], tenant=conn_info['tenant_id']) subscription_id = conn_info['subscription_id'] resource_configuration = ResourceManagementClientConfiguration(credentials, subscription_id) resource_configuration.add_user_agent('Ansible-Deploy') resource_client = ResourceManagementClient(resource_configuration) network_configuration = NetworkManagementClientConfiguration(credentials, subscription_id) network_configuration.add_user_agent('Ansible-Deploy') network_client = NetworkManagementClient(network_configuration) conn_info['deployment_name'] = module.params.get('deployment_name') if module.params.get('state') == 'present': deployment = deploy_template(module, resource_client, conn_info) data = dict(name=deployment.name, group_name=conn_info['resource_group_name'], id=deployment.id, outputs=deployment.properties.outputs, instances=get_instances(network_client, conn_info['resource_group_name'], deployment), changed=True, msg='deployment created') module.exit_json(**data) else: destroy_resource_group(module, resource_client, conn_info) module.exit_json(changed=True, msg='deployment deleted') # import module snippets from ansible.module_utils.basic import * if __name__ == '__main__': main()