ansible/cloud/azure/azure_deployment.py

672 lines
26 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
DOCUMENTATION = '''
---
module: azure_deployment
short_description: Create or destroy Azure Resource Manager template deployments
version_added: "2.0"
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)"
'''
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
'''
try:
import time
import yaml
import requests
import azure
from itertools import chain
from azure.mgmt.common import SubscriptionCloudCredentials
from azure.mgmt.resource import ResourceManagementClient
HAS_DEPS = True
except ImportError:
HAS_DEPS = False
AZURE_URL = "https://management.azure.com"
DEPLOY_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}/providers/microsoft.resources/deployments/{}?api-version={}"
RES_GROUP_URL_FORMAT = "/subscriptions/{}/resourcegroups/{}?api-version={}"
ARM_API_VERSION = "2015-01-01"
NETWORK_API_VERSION = "2015-06-15"
def get_token(domain_or_tenant, client_id, client_secret):
"""
Get an Azure Active Directory token for a service principal
:param domain_or_tenant: The domain or tenant id of your Azure Active Directory instance
:param client_id: The client id of your application in Azure Active Directory
:param client_secret: One of the application secrets created in your Azure Active Directory application
:return: an authenticated bearer token to be used with requests to the API
"""
# the client id we can borrow from azure xplat cli
grant_type = 'client_credentials'
token_url = 'https://login.microsoftonline.com/{}/oauth2/token'.format(domain_or_tenant)
payload = {
'grant_type': grant_type,
'client_id': client_id,
'client_secret': client_secret,
'resource': 'https://management.core.windows.net/'
}
res = requests.post(token_url, data=payload)
return res.json()['access_token'] if res.status_code == 200 else None
def get_azure_connection_info(module):
azure_url = module.params.get('azure_url')
tenant_or_domain = module.params.get('tenant_or_domain')
client_id = module.params.get('client_id')
client_secret = module.params.get('client_secret')
security_token = module.params.get('security_token')
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 security_token:
if 'AZURE_SECURITY_TOKEN' in os.environ:
security_token = os.environ['AZURE_SECURITY_TOKEN']
else:
security_token = None
if not tenant_or_domain:
if 'AZURE_TENANT_ID' in os.environ:
tenant_or_domain = os.environ['AZURE_TENANT_ID']
elif 'AZURE_DOMAIN' in os.environ:
tenant_or_domain = os.environ['AZURE_DOMAIN']
else:
tenant_or_domain = 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_or_domain=tenant_or_domain,
client_id=client_id,
client_secret=client_secret,
security_token=security_token,
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 follow_deployment(client, group_name, deployment):
state = deployment.properties.provisioning_state
if state == azure.mgmt.common.OperationStatus.Failed or \
state == azure.mgmt.common.OperationStatus.Succeeded or \
state == "Canceled" or \
state == "Deleted":
return deployment
else:
time.sleep(30)
result = client.deployments.get(group_name, deployment.name)
return follow_deployment(client, group_name, result.deployment)
def follow_delete(client, location):
result = client.get_long_running_operation_status(location)
if result.status == azure.mgmt.common.OperationStatus.Succeeded:
return True
elif result.status == azure.mgmt.common.OperationStatus.Failed:
return False
else:
time.sleep(30)
return follow_delete(client, location)
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 = azure.mgmt.resource.DeploymentProperties()
deploy_parameter.mode = "Complete"
if module.params.get('parameters_link') is None:
deploy_parameter.parameters = json.dumps(module.params.get('parameters'), ensure_ascii=False)
else:
parameters_link = azure.mgmt.resource.ParametersLink()
parameters_link.uri = module.params.get('parameters_link')
deploy_parameter.parameters_link = parameters_link
if module.params.get('template_link') is None:
deploy_parameter.template = json.dumps(module.params.get('template'), ensure_ascii=False)
else:
template_link = azure.mgmt.resource.TemplateLink()
template_link.uri = module.params.get('template_link')
deploy_parameter.template_link = template_link
deployment = azure.mgmt.resource.Deployment(properties=deploy_parameter)
params = azure.mgmt.resource.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, deployment)
return follow_deployment(client, group_name, result.deployment)
except azure.common.AzureHttpError as e:
module.fail_json(msg='Deploy create failed with status code: %s and message: "%s"' % (e.status_code, e.message))
def deploy_url(subscription_id, resource_group_name, deployment_name, api_version=ARM_API_VERSION):
return AZURE_URL + DEPLOY_URL_FORMAT.format(subscription_id, resource_group_name, deployment_name, api_version)
def res_group_url(subscription_id, resource_group_name, api_version=ARM_API_VERSION):
return AZURE_URL + RES_GROUP_URL_FORMAT.format(subscription_id, resource_group_name, api_version)
def default_headers(token, with_content=False):
headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'}
if with_content:
headers['Content-Type'] = 'application/json'
return headers
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:
client.resource_groups.get(conn_info['resource_group_name'])
except azure.common.AzureMissingResourceHttpError:
return False
try:
url = res_group_url(conn_info['subscription_id'], conn_info['resource_group_name'])
res = requests.delete(url, headers=default_headers(conn_info['security_token']))
if res.status_code == 404 or res.status_code == 204:
return False
if res.status_code == 202:
location = res.headers['location']
follow_delete(client, location)
return True
if res.status_code == requests.codes.ok:
return True
else:
module.fail_json(
msg='Delete resource group and deploy failed with status code: %s and message: %s' % (res.status_code, res.text))
except azure.common.AzureHttpError 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(module, 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, azure.mgmt.resource.Dependency) and dep.depends_on is not None and len(dep.depends_on) > 0:
build_hierarchy(module, 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']:
tree[key2]['children'][key1] = tree[key1]
tree.pop(key1)
return tree
class ResourceId:
def __init__(self, **kwargs):
self.resource_name = kwargs.get('resource_name')
self.resource_provider_api_version = kwargs.get('api_version')
self.resource_provider_namespace = kwargs.get('resource_namespace')
self.resource_type = kwargs.get('resource_type')
self.parent_resource_path = kwargs.get('parent_resource_path')
pass
def get_resource_details(client, group, name, namespace, resource_type, api_version):
res_id = ResourceId(resource_name=name, api_version=api_version, resource_namespace=namespace,
resource_type=resource_type)
return client.resources.get(group, res_id).resource
def get_ip_dict(ip):
p = json.loads(ip.properties)
d = p['dnsSettings']
return dict(name=ip.name,
id=ip.id,
public_ip=p['ipAddress'],
public_ip_allocation_method=p['publicIPAllocationMethod'],
dns_settings=d)
def get_instances(module, client, group, deployment):
dep_tree = build_hierarchy(module, deployment.properties.dependencies)
vms = get_dependencies(dep_tree, resource_type="Microsoft.Compute/virtualMachines")
vms_and_ips = [(vm, get_dependencies(vm['children'], "Microsoft.Network/publicIPAddresses")) for vm in vms]
vms_and_ips = [(vm['dep'], [get_resource_details(client,
group,
ip['dep'].resource_name,
"Microsoft.Network",
"publicIPAddresses",
NETWORK_API_VERSION) for ip in ip_list]) for vm, ip_list in vms_and_ips if len(ip_list) > 0]
return [dict(vm_name=vm.resource_name, ips=[get_ip_dict(ip) for ip in ips]) for vm, ips in vms_and_ips]
def main():
argument_spec = dict(
azure_url=dict(default=AZURE_URL),
subscription_id=dict(required=True),
client_secret=dict(no_log=True),
client_id=dict(),
tenant_or_domain=dict(),
security_token=dict(aliases=['access_token'], no_log=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")
)
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)
if conn_info['security_token'] is None and \
(conn_info['client_id'] is None or conn_info['client_secret'] is None or conn_info[
'tenant_or_domain'] is None):
module.fail_json(msg='security token or client_id, client_secret and tenant_or_domain is required')
if conn_info['security_token'] is None:
conn_info['security_token'] = get_token(conn_info['tenant_or_domain'],
conn_info['client_id'],
conn_info['client_secret'])
if conn_info['security_token'] is None:
module.fail_json(msg='failed to retrieve a security token from Azure Active Directory')
credentials = SubscriptionCloudCredentials(module.params.get('subscription_id'), conn_info['security_token'])
resource_client = ResourceManagementClient(credentials)
conn_info['deployment_name'] = 'ansible-arm'
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(module, resource_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()