diff --git a/lib/ansible/modules/extras/cloud/azure/azure_deployment.py b/lib/ansible/modules/extras/cloud/azure/azure_deployment.py
deleted file mode 100644
index e28663b55f6..00000000000
--- a/lib/ansible/modules/extras/cloud/azure/azure_deployment.py
+++ /dev/null
@@ -1,620 +0,0 @@
-#!/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()
diff --git a/lib/ansible/modules/extras/cloud/azure/azure_rm_deployment.py b/lib/ansible/modules/extras/cloud/azure/azure_rm_deployment.py
new file mode 100644
index 00000000000..022c240a92c
--- /dev/null
+++ b/lib/ansible/modules/extras/cloud/azure/azure_rm_deployment.py
@@ -0,0 +1,646 @@
+#!/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_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 https://github.com/azure/azure-quickstart-templates.
+ For more information on Azue resource manager templates see https://azure.microsoft.com/en-us/documentation/articles/resource-group-template-deploy/."
+
+options:
+ resource_group_name:
+ description:
+ - The resource group name to use or create to host the deployed template
+ required: true
+ location:
+ description:
+ - The geo-locations in which the resource group will be located.
+ required: false
+ default: westus
+ 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.
+ default: present
+ choices:
+ - present
+ - absent
+ template:
+ description:
+ - A hash containing 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
+
+extends_documentation_fragment:
+ - azure
+
+author:
+ - David Justice (@devigned)
+ - Laurent Mazuel (@lmazuel)
+ - Andre Price (@obsoleted)
+
+'''
+
+EXAMPLES = '''
+# Destroy a template deployment
+- name: Destroy Azure Deploy
+ azure_rm_deployment:
+ state: absent
+ subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+ resource_group_name: dev-ops-cle
+
+# Create or update a template deployment based on uris using parameter and template links
+- name: Create Azure Deploy
+ azure_rm_deployment:
+ state: present
+ resource_group_name: dev-ops-cle
+ 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.
+---
+- hosts: localhost
+ connection: local
+ gather_facts: no
+ tasks:
+ - name: Destroy Azure Deploy
+ azure_rm_deployment:
+ state: absent
+ subscription_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+ resource_group_name: dev-ops-cle
+
+ - name: Create Azure Deploy
+ azure_rm_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.deployment.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_rm_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_rm_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 = '''
+msg:
+ description: String indicating if the deployment was created or deleted
+ returned: always
+ type: string
+ sample: "deployment created"
+deployment:
+ description: Deployment details
+ type: dict
+ returned: always
+ sample:{
+ "group_name": "Test_Deployment",
+ "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Resources/deployments/ansible-arm",
+ "instances": [
+ {
+ "ips": [
+ {
+ "dns_settings": {
+ "domain_name_label": "testvm9910001",
+ "fqdn": "testvm9910001.westus.cloudapp.azure.com"
+ },
+ "id": "/subscriptions/3f7e29ba-24e0-42f6-8d9c-5149a14bda37/resourceGroups/Test_Deployment/providers/Microsoft.Network/publicIPAddresses/myPublicIP",
+ "name": "myPublicIP",
+ "public_ip": "13.91.99.232",
+ "public_ip_allocation_method": "IPAllocationMethod.dynamic"
+ }
+ ],
+ "vm_name": "MyUbuntuVM"
+ }
+ ],
+ "name": "ansible-arm",
+ "outputs": {
+ "hostname": {
+ "type": "String",
+ "value": "testvm9910001.westus.cloudapp.azure.com"
+ },
+ "sshCommand": {
+ "type": "String",
+ "value": "ssh chouseknecht@testvm9910001.westus.cloudapp.azure.com"
+ }
+ }
+ }
+'''
+
+import time
+import yaml
+
+from ansible.module_utils.basic import *
+from ansible.module_utils.azure_rm_common import *
+
+try:
+ 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
+
+except ImportError:
+ # This is handled in azure_rm_common
+ pass
+
+
+class AzureRMDeploymentManager(AzureRMModuleBase):
+
+ def __init__(self):
+
+ self.module_arg_spec = dict(
+ resource_group_name=dict(type='str', required=True, aliases=['resource_group']),
+ 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='complete', choices=['complete', 'incremental']),
+ deployment_name=dict(type='str', default="ansible-arm"),
+ wait_for_deployment_completion=dict(type='bool', default=True),
+ wait_for_deployment_polling_period=dict(type='int', default=30)
+ )
+
+ mutually_exclusive = [('template', 'template_link'),
+ ('parameters', 'parameters_link')]
+
+ self.resource_group_name = 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.deployment_name = None
+ self.wait_for_deployment_completion = None
+ self.wait_for_deployment_polling_period = None
+ self.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 self.module_arg_spec.keys() + ['tags']:
+ setattr(self, key, kwargs[key])
+
+ if self.state == 'present':
+ deployment = self.deploy_template()
+ self.results['deployment'] = dict(
+ name=deployment.name,
+ group_name=self.resource_group_name,
+ id=deployment.id,
+ outputs=deployment.properties.outputs,
+ instances=self._get_instances(deployment)
+ )
+ self.results['changed'] = True
+ self.results['msg'] = 'deployment created'
+ else:
+ if self.resource_group_exists(self.resource_group_name):
+ self.destroy_resource_group()
+ self.results['changed'] = True
+ self.results['msg'] = "deployment deleted"
+
+ 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 = DeploymentProperties()
+ deploy_parameter.mode = self.deployment_mode
+ if not self.parameters_link:
+ deploy_parameter.parameters = self.parameters
+ else:
+ deploy_parameter.parameters_link = ParametersLink(
+ uri=self.parameters_link
+ )
+ if not self.template_link:
+ deploy_parameter.template = self.template
+ else:
+ deploy_parameter.template_link = TemplateLink(
+ uri=self.template_link
+ )
+
+ params = ResourceGroup(location=self.location, tags=self.tags)
+
+ try:
+ self.rm_client.resource_groups.create_or_update(self.resource_group_name, 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_name,
+ self.deployment_name,
+ deploy_parameter)
+
+ deployment_result = self.get_poller_result(result)
+ if self.wait_for_deployment_completion:
+ while 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_name, self.deployment_name)
+ except CloudError as exc:
+ failed_deployment_operations = self._get_failed_deployment_operations(self.deployment_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.deployment_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_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
+ else:
+ self.fail("Delete resource group and deploy failed with status code: %s and message: %s" %
+ (e.status_code, e.message))
+
+ def resource_group_exists(self, resource_group):
+ '''
+ Return True/False based on existence of requested resource group.
+
+ :param resource_group: string. Name of a resource group.
+ :return: boolean
+ '''
+ try:
+ self.rm_client.resource_groups.get(resource_group)
+ except CloudError:
+ return False
+ return True
+
+ 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_name,
+ nested_deployment)
+ except CloudError as exc:
+ self.fail("List nested deployment operations failed with status code: %s and message: %s" %
+ (e.status_code, e.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, deployment_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, deployment_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:
+ # 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, 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(self.resource_group_name, public_ip_id.split('/')[-1])
+ for nic_obj in [self.network_client.network_interfaces.get(self.resource_group_name,
+ 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()
\ No newline at end of file