├── delete_vm.sh ├── azure-playbooks ├── test_var.yml ├── test.yml ├── httpd.yml ├── delete_vm.yml ├── new_vm.yml └── new_vm_web.yml ├── create_vm.sh ├── index.html ├── .gitattributes ├── azure ├── .gitignore └── azure_rm.py /delete_vm.sh: -------------------------------------------------------------------------------- 1 | ansible-playbook ./azure_playbooks/delete_vm.yml --extra-vars "vmname=$1" 2 | -------------------------------------------------------------------------------- /azure-playbooks/test_var.yml: -------------------------------------------------------------------------------- 1 | - name: Test the inventory script 2 | hosts: '{{ vmname }}' 3 | gather_facts: no 4 | tasks: 5 | - debug: msg="{{ inventory_hostname }} has powerstate {{ powerstate }}" 6 | -------------------------------------------------------------------------------- /azure-playbooks/test.yml: -------------------------------------------------------------------------------- 1 | - name: Test the inventory script 2 | hosts: azure 3 | connection: local 4 | gather_facts: no 5 | tasks: 6 | - debug: msg="{{ inventory_hostname }} has powerstate {{ powerstate }}" 7 | -------------------------------------------------------------------------------- /create_vm.sh: -------------------------------------------------------------------------------- 1 | ansible-playbook ./azure_playbooks/new_vm_web.yml --extra-vars "vmname=$1 rscgrp=$2" 2 | # At this point the VM is new, we need to skip the known_hosts check 3 | export ANSIBLE_HOST_KEY_CHECKING=False 4 | ansible-playbook -i ./azure_rm.py ./azure_playbooks/httpd.yml --extra-vars "vmname=$1" 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 |

Hello World

9 |

10 | This is a test page 11 | This is a test page 12 | This is a test page 13 |

14 | 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /azure-playbooks/httpd.yml: -------------------------------------------------------------------------------- 1 | - name: Install Apache Web Server 2 | hosts: '{{ vmname }}' 3 | gather_facts: no 4 | tasks: 5 | - name: Ensure apache is at the latest version 6 | yum: name=httpd state=latest 7 | become: true 8 | - name: Change permissions of /var/www/html 9 | file: path=/var/www/html mode=0777 10 | become: true 11 | - name: Download index.html 12 | get_url: 13 | url: https://raw.githubusercontent.com/erjosito/Azure-Ansible-Examples/master/index.html 14 | dest: /var/www/html/index.html 15 | mode: 0644 16 | - name: Ensure apache is running (and enable it at boot) 17 | service: name=httpd state=started enabled=yes 18 | become: true 19 | 20 | 21 | -------------------------------------------------------------------------------- /azure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // 4 | // Copyright (c) Microsoft and contributors. All rights reserved. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | require('./azure.js'); 20 | -------------------------------------------------------------------------------- /azure-playbooks/delete_vm.yml: -------------------------------------------------------------------------------- 1 | # The following variables must be specified: 2 | # - vmname 3 | # - resgrp 4 | - name: Remove Virtual Machine and associated objects 5 | hosts: localhost 6 | connection: local 7 | gather_facts: no 8 | tasks: 9 | - name: Remove VM and all resources 10 | azure_rm_virtualmachine: 11 | resource_group: '{{ resgrp }}' 12 | name: '{{ vmname }}' 13 | state: absent 14 | # ignore_errors: yes 15 | remove_on_absent: 16 | - network_interfaces 17 | - virtual_storage 18 | - public_ips 19 | - name: Remove storage account 20 | azure_rm_storageaccount: 21 | resource_group: '{{ resgrp }}' 22 | name: '{{ vmname }}' 23 | state: absent 24 | # ignore_errors: yes 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /azure-playbooks/new_vm.yml: -------------------------------------------------------------------------------- 1 | - name: CREATE VM PLAYBOOK 2 | hosts: localhost 3 | connection: local 4 | gather_facts: False 5 | vars: 6 | vmname: web01 7 | resgrp: ansiblelabVMs 8 | tasks: 9 | - name: Create storage account 10 | azure_rm_storageaccount: 11 | resource_group: '{{ resgrp }}' 12 | name: '{{ vmname }}' 13 | account_type: Standard_LRS 14 | - name: Create VM 15 | azure_rm_virtualmachine: 16 | resource_group: '{{ resgrp }}' 17 | name: '{{ vmname }}' 18 | storage_account: '{{ vmname }}' 19 | storage_container: '{{ vmname }}' 20 | vm_size: Standard_A0 21 | admin_username: jose 22 | ssh_password_enabled: False 23 | ssh_public_keys: 24 | - path: /home/jose/.ssh/authorized_keys 25 | key_data: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDn4i0NkH4uFL7B87MJBW0TyQcsk99vQNlHyZLJyRielhU2kxy73K4ecOEcYQPu0B58KbQfHM2EooaHbZIDAaZK8K62yaYz5eV7YBMr5TdN9Tw5u1GGT5LrWsOYoHcQtcSnTRbBSWZDFIx5eJWebBxdDh61LbEftyOLg16xsLRIqp6SeAtJANTWNSMCEH96qn4+12eoW8bYQ7flVyR7uyE+7NDKmMaHk0zWUQe0wluHyUnfj15g1tfRvwXyUEMLMagyFrhRh0n/wNBnV8XrX74OjqCseJfh3YnuLxhy4hAmw0di699Q3jTB3xJ8b7yg2NvoAF+lzSkQtiArTjEBVKuv jose@jose-centos-01 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDn4i0NkH4uFL7B87MJBW0TyQcsk99vQNlHyZLJyRielhU2kxy73K4ecOEcYQPu0B58KbQfHM2EooaHbZIDAaZK8K62yaYz5eV7YBMr5TdN9Tw5u1GGT5LrWsOYoHcQtcSnTRbBSWZDFIx5eJWebBxdDh61LbEftyOLg16xsLRIqp6SeAtJANTWNSMCEH96qn4+12eoW8bYQ7flVyR7uyE+7NDKmMaHk0zWUQe0wluHyUnfj15g1tfRvwXyUEMLMagyFrhRh0n/wNBnV8XrX74OjqCseJfh3YnuLxhy4hAmw0di699Q3jTB3xJ8b7yg2NvoAF+lzSkQtiArTjEBVKuv jose@10.0.0.4' 26 | image: 27 | offer: CentOS 28 | publisher: OpenLogic 29 | sku: '7.2' 30 | version: latest 31 | -------------------------------------------------------------------------------- /azure-playbooks/new_vm_web.yml: -------------------------------------------------------------------------------- 1 | - name: CREATE VM PLAYBOOK 2 | hosts: localhost 3 | connection: local 4 | gather_facts: False 5 | vars: 6 | # vmname: josetestansible03 7 | # resgrp: PermanentLab 8 | # vnet: PermanentLab-vnet 9 | # Variables 'vnet', 'subnet', 'vmname' and 'resgrp' need to be provided at command line with arg --extra-vars 10 | dnsname: '{{ vmname }}.westeurope.cloudapp.azure.com' 11 | # The DNS name might not be right depending on your region!! 12 | ip: "{{ lookup ('dig', '{{ dnsname }}') }}" 13 | 14 | tasks: 15 | - debug: msg="Public DNS name {{ dnsname }} resolved to IP {{ ip }}. " 16 | - name: Check if DNS is taken 17 | fail: msg="That DNS name seems to be already taken" 18 | when: ip != 'NXDOMAIN' 19 | - name: Create storage account 20 | azure_rm_storageaccount: 21 | resource_group: '{{ resgrp }}' 22 | name: '{{ vmname }}' 23 | account_type: Standard_LRS 24 | - name: Create security group that allows SSH and HTTP 25 | azure_rm_securitygroup: 26 | resource_group: '{{ resgrp }}' 27 | name: '{{ vmname }}' 28 | rules: 29 | - name: SSH 30 | protocol: Tcp 31 | destination_port_range: 22 32 | access: Allow 33 | priority: 101 34 | direction: Inbound 35 | - name: WEB 36 | protocol: Tcp 37 | destination_port_range: 80 38 | access: Allow 39 | priority: 102 40 | direction: Inbound 41 | - name: Create public IP address 42 | azure_rm_publicipaddress: 43 | resource_group: '{{ resgrp }}' 44 | allocation_method: Static 45 | name: '{{ vmname }}' 46 | domain_name_label: '{{ vmname }}' 47 | - name: Create NIC 48 | azure_rm_networkinterface: 49 | resource_group: '{{ resgrp }}' 50 | name: '{{ vmname }}' 51 | virtual_network: '{{ vnet }}' 52 | subnet: '{{ subnet }}' 53 | public_ip_name: '{{ vmname }}' 54 | security_group: '{{ vmname }}' 55 | - name: Create VM 56 | azure_rm_virtualmachine: 57 | resource_group: '{{ resgrp }}' 58 | name: '{{ vmname }}' 59 | storage_account: '{{ vmname }}' 60 | storage_container: '{{ vmname }}' 61 | storage_blob: '{{ vmname }}.vhd' 62 | network_interfaces: '{{ vmname }}' 63 | vm_size: Standard_A0 64 | admin_username: jose 65 | ssh_password_enabled: False 66 | ssh_public_keys: 67 | - path: /home/jose/.ssh/authorized_keys 68 | key_data: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDn4i0NkH4uFL7B87MJBW0TyQcsk99vQNlHyZLJyRielhU2kxy73K4ecOEcYQPu0B58KbQfHM2EooaHbZIDAaZK8K62yaYz5eV7YBMr5TdN9Tw5u1GGT5LrWsOYoHcQtcSnTRbBSWZDFIx5eJWebBxdDh61LbEftyOLg16xsLRIqp6SeAtJANTWNSMCEH96qn4+12eoW8bYQ7flVyR7uyE+7NDKmMaHk0zWUQe0wluHyUnfj15g1tfRvwXyUEMLMagyFrhRh0n/wNBnV8XrX74OjqCseJfh3YnuLxhy4hAmw0di699Q3jTB3xJ8b7yg2NvoAF+lzSkQtiArTjEBVKuv jose@jose-centos-01 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDn4i0NkH4uFL7B87MJBW0TyQcsk99vQNlHyZLJyRielhU2kxy73K4ecOEcYQPu0B58KbQfHM2EooaHbZIDAaZK8K62yaYz5eV7YBMr5TdN9Tw5u1GGT5LrWsOYoHcQtcSnTRbBSWZDFIx5eJWebBxdDh61LbEftyOLg16xsLRIqp6SeAtJANTWNSMCEH96qn4+12eoW8bYQ7flVyR7uyE+7NDKmMaHk0zWUQe0wluHyUnfj15g1tfRvwXyUEMLMagyFrhRh0n/wNBnV8XrX74OjqCseJfh3YnuLxhy4hAmw0di699Q3jTB3xJ8b7yg2NvoAF+lzSkQtiArTjEBVKuv jose@10.0.0.4' 69 | image: 70 | offer: CentOS 71 | publisher: OpenLogic 72 | sku: '7.2' 73 | version: latest 74 | -------------------------------------------------------------------------------- /azure_rm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2016 Matt Davis, 4 | # Chris Houseknecht, 5 | # 6 | # This file is part of Ansible 7 | # 8 | # Ansible is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # Ansible is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with Ansible. If not, see . 20 | # 21 | 22 | ''' 23 | Azure External Inventory Script 24 | =============================== 25 | Generates dynamic inventory by making API requests to the Azure Resource 26 | Manager using the AAzure Python SDK. For instruction on installing the 27 | Azure Python SDK see http://azure-sdk-for-python.readthedocs.org/ 28 | 29 | Authentication 30 | -------------- 31 | The order of precedence is command line arguments, environment variables, 32 | and finally the [default] profile found in ~/.azure/credentials. 33 | 34 | If using a credentials file, it should be an ini formatted file with one or 35 | more sections, which we refer to as profiles. The script looks for a 36 | [default] section, if a profile is not specified either on the command line 37 | or with an environment variable. The keys in a profile will match the 38 | list of command line arguments below. 39 | 40 | For command line arguments and environment variables specify a profile found 41 | in your ~/.azure/credentials file, or a service principal or Active Directory 42 | user. 43 | 44 | Command line arguments: 45 | - profile 46 | - client_id 47 | - secret 48 | - subscription_id 49 | - tenant 50 | - ad_user 51 | - password 52 | 53 | Environment variables: 54 | - AZURE_PROFILE 55 | - AZURE_CLIENT_ID 56 | - AZURE_SECRET 57 | - AZURE_SUBSCRIPTION_ID 58 | - AZURE_TENANT 59 | - AZURE_AD_USER 60 | - AZURE_PASSWORD 61 | 62 | Run for Specific Host 63 | ----------------------- 64 | When run for a specific host using the --host option, a resource group is 65 | required. For a specific host, this script returns the following variables: 66 | 67 | { 68 | "ansible_host": "XXX.XXX.XXX.XXX", 69 | "computer_name": "computer_name2", 70 | "fqdn": null, 71 | "id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Compute/virtualMachines/object-name", 72 | "image": { 73 | "offer": "CentOS", 74 | "publisher": "OpenLogic", 75 | "sku": "7.1", 76 | "version": "latest" 77 | }, 78 | "location": "westus", 79 | "mac_address": "00-00-5E-00-53-FE", 80 | "name": "object-name", 81 | "network_interface": "interface-name", 82 | "network_interface_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkInterfaces/object-name1", 83 | "network_security_group": null, 84 | "network_security_group_id": null, 85 | "os_disk": { 86 | "name": "object-name", 87 | "operating_system_type": "Linux" 88 | }, 89 | "plan": null, 90 | "powerstate": "running", 91 | "private_ip": "172.26.3.6", 92 | "private_ip_alloc_method": "Static", 93 | "provisioning_state": "Succeeded", 94 | "public_ip": "XXX.XXX.XXX.XXX", 95 | "public_ip_alloc_method": "Static", 96 | "public_ip_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/publicIPAddresses/object-name", 97 | "public_ip_name": "object-name", 98 | "resource_group": "galaxy-production", 99 | "security_group": "object-name", 100 | "security_group_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkSecurityGroups/object-name", 101 | "tags": { 102 | "db": "database" 103 | }, 104 | "type": "Microsoft.Compute/virtualMachines", 105 | "virtual_machine_size": "Standard_DS4" 106 | } 107 | 108 | Groups 109 | ------ 110 | When run in --list mode, instances are grouped by the following categories: 111 | - azure 112 | - location 113 | - resource_group 114 | - security_group 115 | - tag key 116 | - tag key_value 117 | 118 | Control groups using azure_rm.ini or set environment variables: 119 | 120 | AZURE_GROUP_BY_RESOURCE_GROUP=yes 121 | AZURE_GROUP_BY_LOCATION=yes 122 | AZURE_GROUP_BY_SECURITY_GROUP=yes 123 | AZURE_GROUP_BY_TAG=yes 124 | 125 | Select hosts within specific resource groups by assigning a comma separated list to: 126 | 127 | AZURE_RESOURCE_GROUPS=resource_group_a,resource_group_b 128 | 129 | Select hosts for specific tag key by assigning a comma separated list of tag keys to: 130 | 131 | AZURE_TAGS=key1,key2,key3 132 | 133 | Select hosts for specific locations: 134 | 135 | AZURE_LOCATIONS=eastus,westus,eastus2 136 | 137 | Or, select hosts for specific tag key:value pairs by assigning a comma separated list key:value pairs to: 138 | 139 | AZURE_TAGS=key1:value1,key2:value2 140 | 141 | If you don't need the powerstate, you can improve performance by turning off powerstate fetching: 142 | AZURE_INCLUDE_POWERSTATE=no 143 | 144 | azure_rm.ini 145 | ------------ 146 | As mentioned above, you can control execution using environment variables or a .ini file. A sample 147 | azure_rm.ini is included. The name of the .ini file is the basename of the inventory script (in this case 148 | 'azure_rm') with a .ini extension. It also assumes the .ini file is alongside the script. To specify 149 | a different path for the .ini file, define the AZURE_INI_PATH environment variable: 150 | 151 | export AZURE_INI_PATH=/path/to/custom.ini 152 | 153 | Powerstate: 154 | ----------- 155 | The powerstate attribute indicates whether or not a host is running. If the value is 'running', the machine is 156 | up. If the value is anything other than 'running', the machine is down, and will be unreachable. 157 | 158 | Examples: 159 | --------- 160 | Execute /bin/uname on all instances in the galaxy-qa resource group 161 | $ ansible -i azure_rm.py galaxy-qa -m shell -a "/bin/uname -a" 162 | 163 | Use the inventory script to print instance specific information 164 | $ contrib/inventory/azure_rm.py --host my_instance_host_name --pretty 165 | 166 | Use with a playbook 167 | $ ansible-playbook -i contrib/inventory/azure_rm.py my_playbook.yml --limit galaxy-qa 168 | 169 | 170 | Insecure Platform Warning 171 | ------------------------- 172 | If you receive InsecurePlatformWarning from urllib3, install the 173 | requests security packages: 174 | 175 | pip install requests[security] 176 | 177 | 178 | author: 179 | - Chris Houseknecht (@chouseknecht) 180 | - Matt Davis (@nitzmahone) 181 | 182 | Company: Ansible by Red Hat 183 | 184 | Version: 1.0.0 185 | ''' 186 | 187 | import argparse 188 | import ConfigParser 189 | import json 190 | import os 191 | import re 192 | import sys 193 | 194 | from distutils.version import LooseVersion 195 | 196 | from os.path import expanduser 197 | 198 | HAS_AZURE = True 199 | HAS_AZURE_EXC = None 200 | 201 | try: 202 | from msrestazure.azure_exceptions import CloudError 203 | from azure.mgmt.compute import __version__ as azure_compute_version 204 | from azure.common import AzureMissingResourceHttpError, AzureHttpError 205 | from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials 206 | from azure.mgmt.network.network_management_client import NetworkManagementClient 207 | from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient 208 | from azure.mgmt.compute.compute_management_client import ComputeManagementClient 209 | except ImportError as exc: 210 | HAS_AZURE_EXC = exc 211 | HAS_AZURE = False 212 | 213 | 214 | AZURE_CREDENTIAL_ENV_MAPPING = dict( 215 | profile='AZURE_PROFILE', 216 | subscription_id='AZURE_SUBSCRIPTION_ID', 217 | client_id='AZURE_CLIENT_ID', 218 | secret='AZURE_SECRET', 219 | tenant='AZURE_TENANT', 220 | ad_user='AZURE_AD_USER', 221 | password='AZURE_PASSWORD' 222 | ) 223 | 224 | AZURE_CONFIG_SETTINGS = dict( 225 | resource_groups='AZURE_RESOURCE_GROUPS', 226 | tags='AZURE_TAGS', 227 | locations='AZURE_LOCATIONS', 228 | include_powerstate='AZURE_INCLUDE_POWERSTATE', 229 | group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP', 230 | group_by_location='AZURE_GROUP_BY_LOCATION', 231 | group_by_security_group='AZURE_GROUP_BY_SECURITY_GROUP', 232 | group_by_tag='AZURE_GROUP_BY_TAG' 233 | ) 234 | 235 | AZURE_MIN_VERSION = "0.30.0rc5" 236 | 237 | 238 | def azure_id_to_dict(id): 239 | pieces = re.sub(r'^\/', '', id).split('/') 240 | result = {} 241 | index = 0 242 | while index < len(pieces) - 1: 243 | result[pieces[index]] = pieces[index + 1] 244 | index += 1 245 | return result 246 | 247 | 248 | class AzureRM(object): 249 | 250 | def __init__(self, args): 251 | self._args = args 252 | self._compute_client = None 253 | self._resource_client = None 254 | self._network_client = None 255 | 256 | self.debug = False 257 | if args.debug: 258 | self.debug = True 259 | 260 | self.credentials = self._get_credentials(args) 261 | if not self.credentials: 262 | self.fail("Failed to get credentials. Either pass as parameters, set environment variables, " 263 | "or define a profile in ~/.azure/credentials.") 264 | 265 | if self.credentials.get('subscription_id', None) is None: 266 | self.fail("Credentials did not include a subscription_id value.") 267 | self.log("setting subscription_id") 268 | self.subscription_id = self.credentials['subscription_id'] 269 | 270 | if self.credentials.get('client_id') is not None and \ 271 | self.credentials.get('secret') is not None and \ 272 | self.credentials.get('tenant') is not None: 273 | self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'], 274 | secret=self.credentials['secret'], 275 | tenant=self.credentials['tenant']) 276 | elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None: 277 | self.azure_credentials = UserPassCredentials(self.credentials['ad_user'], self.credentials['password']) 278 | else: 279 | self.fail("Failed to authenticate with provided credentials. Some attributes were missing. " 280 | "Credentials must include client_id, secret and tenant or ad_user and password.") 281 | 282 | def log(self, msg): 283 | if self.debug: 284 | print (msg + u'\n') 285 | 286 | def fail(self, msg): 287 | raise Exception(msg) 288 | 289 | def _get_profile(self, profile="default"): 290 | path = expanduser("~") 291 | path += "/.azure/credentials" 292 | try: 293 | config = ConfigParser.ConfigParser() 294 | config.read(path) 295 | except Exception as exc: 296 | self.fail("Failed to access {0}. Check that the file exists and you have read " 297 | "access. {1}".format(path, str(exc))) 298 | credentials = dict() 299 | for key in AZURE_CREDENTIAL_ENV_MAPPING: 300 | try: 301 | credentials[key] = config.get(profile, key, raw=True) 302 | except: 303 | pass 304 | 305 | if credentials.get('client_id') is not None or credentials.get('ad_user') is not None: 306 | return credentials 307 | 308 | return None 309 | 310 | def _get_env_credentials(self): 311 | env_credentials = dict() 312 | for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.iteritems(): 313 | env_credentials[attribute] = os.environ.get(env_variable, None) 314 | 315 | if env_credentials['profile'] is not None: 316 | credentials = self._get_profile(env_credentials['profile']) 317 | return credentials 318 | 319 | if env_credentials['client_id'] is not None or env_credentials['ad_user'] is not None: 320 | return env_credentials 321 | 322 | return None 323 | 324 | def _get_credentials(self, params): 325 | # Get authentication credentials. 326 | # Precedence: cmd line parameters-> environment variables-> default profile in ~/.azure/credentials. 327 | 328 | self.log('Getting credentials') 329 | 330 | arg_credentials = dict() 331 | for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.iteritems(): 332 | arg_credentials[attribute] = getattr(params, attribute) 333 | 334 | # try module params 335 | if arg_credentials['profile'] is not None: 336 | self.log('Retrieving credentials with profile parameter.') 337 | credentials = self._get_profile(arg_credentials['profile']) 338 | return credentials 339 | 340 | if arg_credentials['client_id'] is not None: 341 | self.log('Received credentials from parameters.') 342 | return arg_credentials 343 | 344 | # try environment 345 | env_credentials = self._get_env_credentials() 346 | if env_credentials: 347 | self.log('Received credentials from env.') 348 | return env_credentials 349 | 350 | # try default profile from ~./azure/credentials 351 | default_credentials = self._get_profile() 352 | if default_credentials: 353 | self.log('Retrieved default profile credentials from ~/.azure/credentials.') 354 | return default_credentials 355 | 356 | return None 357 | 358 | def _register(self, key): 359 | try: 360 | # We have to perform the one-time registration here. Otherwise, we receive an error the first 361 | # time we attempt to use the requested client. 362 | resource_client = self.rm_client 363 | resource_client.providers.register(key) 364 | except Exception as exc: 365 | self.fail("One-time registration of {0} failed - {1}".format(key, str(exc))) 366 | 367 | @property 368 | def network_client(self): 369 | self.log('Getting network client') 370 | if not self._network_client: 371 | self._network_client = NetworkManagementClient(self.azure_credentials, self.subscription_id) 372 | self._register('Microsoft.Network') 373 | return self._network_client 374 | 375 | @property 376 | def rm_client(self): 377 | self.log('Getting resource manager client') 378 | if not self._resource_client: 379 | self._resource_client = ResourceManagementClient(self.azure_credentials, self.subscription_id) 380 | return self._resource_client 381 | 382 | @property 383 | def compute_client(self): 384 | self.log('Getting compute client') 385 | if not self._compute_client: 386 | self._compute_client = ComputeManagementClient(self.azure_credentials, self.subscription_id) 387 | self._register('Microsoft.Compute') 388 | return self._compute_client 389 | 390 | 391 | class AzureInventory(object): 392 | 393 | def __init__(self): 394 | 395 | self._args = self._parse_cli_args() 396 | 397 | try: 398 | rm = AzureRM(self._args) 399 | except Exception as e: 400 | sys.exit("{0}".format(str(e))) 401 | 402 | self._compute_client = rm.compute_client 403 | self._network_client = rm.network_client 404 | self._resource_client = rm.rm_client 405 | self._security_groups = None 406 | 407 | self.resource_groups = [] 408 | self.tags = None 409 | self.locations = None 410 | self.replace_dash_in_groups = False 411 | self.group_by_resource_group = True 412 | self.group_by_location = True 413 | self.group_by_security_group = True 414 | self.group_by_tag = True 415 | self.include_powerstate = True 416 | 417 | self._inventory = dict( 418 | _meta=dict( 419 | hostvars=dict() 420 | ), 421 | azure=[] 422 | ) 423 | 424 | self._get_settings() 425 | 426 | if self._args.resource_groups: 427 | self.resource_groups = self._args.resource_groups.split(',') 428 | 429 | if self._args.tags: 430 | self.tags = self._args.tags.split(',') 431 | 432 | if self._args.locations: 433 | self.locations = self._args.locations.split(',') 434 | 435 | if self._args.no_powerstate: 436 | self.include_powerstate = False 437 | 438 | self.get_inventory() 439 | print (self._json_format_dict(pretty=self._args.pretty)) 440 | sys.exit(0) 441 | 442 | def _parse_cli_args(self): 443 | # Parse command line arguments 444 | parser = argparse.ArgumentParser( 445 | description='Produce an Ansible Inventory file for an Azure subscription') 446 | parser.add_argument('--list', action='store_true', default=True, 447 | help='List instances (default: True)') 448 | parser.add_argument('--debug', action='store_true', default=False, 449 | help='Send debug messages to STDOUT') 450 | parser.add_argument('--host', action='store', 451 | help='Get all information about an instance') 452 | parser.add_argument('--pretty', action='store_true', default=False, 453 | help='Pretty print JSON output(default: False)') 454 | parser.add_argument('--profile', action='store', 455 | help='Azure profile contained in ~/.azure/credentials') 456 | parser.add_argument('--subscription_id', action='store', 457 | help='Azure Subscription Id') 458 | parser.add_argument('--client_id', action='store', 459 | help='Azure Client Id ') 460 | parser.add_argument('--secret', action='store', 461 | help='Azure Client Secret') 462 | parser.add_argument('--tenant', action='store', 463 | help='Azure Tenant Id') 464 | parser.add_argument('--ad-user', action='store', 465 | help='Active Directory User') 466 | parser.add_argument('--password', action='store', 467 | help='password') 468 | parser.add_argument('--resource-groups', action='store', 469 | help='Return inventory for comma separated list of resource group names') 470 | parser.add_argument('--tags', action='store', 471 | help='Return inventory for comma separated list of tag key:value pairs') 472 | parser.add_argument('--locations', action='store', 473 | help='Return inventory for comma separated list of locations') 474 | parser.add_argument('--no-powerstate', action='store_true', default=False, 475 | help='Do not include the power state of each virtual host') 476 | return parser.parse_args() 477 | 478 | def get_inventory(self): 479 | if len(self.resource_groups) > 0: 480 | # get VMs for requested resource groups 481 | for resource_group in self.resource_groups: 482 | try: 483 | virtual_machines = self._compute_client.virtual_machines.list(resource_group) 484 | except Exception as exc: 485 | sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group, 486 | str(exc))) 487 | if self._args.host or self.tags: 488 | selected_machines = self._selected_machines(virtual_machines) 489 | self._load_machines(selected_machines) 490 | else: 491 | self._load_machines(virtual_machines) 492 | else: 493 | # get all VMs within the subscription 494 | try: 495 | virtual_machines = self._compute_client.virtual_machines.list_all() 496 | except Exception as exc: 497 | sys.exit("Error: fetching virtual machines - {0}".format(str(exc))) 498 | 499 | if self._args.host or self.tags or self.locations: 500 | selected_machines = self._selected_machines(virtual_machines) 501 | self._load_machines(selected_machines) 502 | else: 503 | self._load_machines(virtual_machines) 504 | 505 | def _load_machines(self, machines): 506 | for machine in machines: 507 | id_dict = azure_id_to_dict(machine.id) 508 | 509 | #TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets 510 | # fixed, we should remove the .lower(). Opened Issue 511 | # #574: https://github.com/Azure/azure-sdk-for-python/issues/574 512 | resource_group = id_dict['resourceGroups'].lower() 513 | 514 | if self.group_by_security_group: 515 | self._get_security_groups(resource_group) 516 | 517 | host_vars = dict( 518 | ansible_host=None, 519 | private_ip=None, 520 | private_ip_alloc_method=None, 521 | public_ip=None, 522 | public_ip_name=None, 523 | public_ip_id=None, 524 | public_ip_alloc_method=None, 525 | fqdn=None, 526 | location=machine.location, 527 | name=machine.name, 528 | type=machine.type, 529 | id=machine.id, 530 | tags=machine.tags, 531 | network_interface_id=None, 532 | network_interface=None, 533 | resource_group=resource_group, 534 | mac_address=None, 535 | plan=(machine.plan.name if machine.plan else None), 536 | virtual_machine_size=machine.hardware_profile.vm_size, 537 | computer_name=machine.os_profile.computer_name, 538 | provisioning_state=machine.provisioning_state, 539 | ) 540 | 541 | host_vars['os_disk'] = dict( 542 | name=machine.storage_profile.os_disk.name, 543 | operating_system_type=machine.storage_profile.os_disk.os_type.value 544 | ) 545 | 546 | if self.include_powerstate: 547 | host_vars['powerstate'] = self._get_powerstate(resource_group, machine.name) 548 | 549 | if machine.storage_profile.image_reference: 550 | host_vars['image'] = dict( 551 | offer=machine.storage_profile.image_reference.offer, 552 | publisher=machine.storage_profile.image_reference.publisher, 553 | sku=machine.storage_profile.image_reference.sku, 554 | version=machine.storage_profile.image_reference.version 555 | ) 556 | 557 | # Add windows details 558 | if machine.os_profile.windows_configuration is not None: 559 | host_vars['windows_auto_updates_enabled'] = \ 560 | machine.os_profile.windows_configuration.enable_automatic_updates 561 | host_vars['windows_timezone'] = machine.os_profile.windows_configuration.time_zone 562 | host_vars['windows_rm'] = None 563 | if machine.os_profile.windows_configuration.win_rm is not None: 564 | host_vars['windows_rm'] = dict(listeners=None) 565 | if machine.os_profile.windows_configuration.win_rm.listeners is not None: 566 | host_vars['windows_rm']['listeners'] = [] 567 | for listener in machine.os_profile.windows_configuration.win_rm.listeners: 568 | host_vars['windows_rm']['listeners'].append(dict(protocol=listener.protocol, 569 | certificate_url=listener.certificate_url)) 570 | 571 | for interface in machine.network_profile.network_interfaces: 572 | interface_reference = self._parse_ref_id(interface.id) 573 | network_interface = self._network_client.network_interfaces.get( 574 | interface_reference['resourceGroups'], 575 | interface_reference['networkInterfaces']) 576 | if network_interface.primary: 577 | if self.group_by_security_group and \ 578 | self._security_groups[resource_group].get(network_interface.id, None): 579 | host_vars['security_group'] = \ 580 | self._security_groups[resource_group][network_interface.id]['name'] 581 | host_vars['security_group_id'] = \ 582 | self._security_groups[resource_group][network_interface.id]['id'] 583 | host_vars['network_interface'] = network_interface.name 584 | host_vars['network_interface_id'] = network_interface.id 585 | host_vars['mac_address'] = network_interface.mac_address 586 | for ip_config in network_interface.ip_configurations: 587 | host_vars['private_ip'] = ip_config.private_ip_address 588 | host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method 589 | if ip_config.public_ip_address: 590 | public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id) 591 | public_ip_address = self._network_client.public_ip_addresses.get( 592 | public_ip_reference['resourceGroups'], 593 | public_ip_reference['publicIPAddresses']) 594 | host_vars['ansible_host'] = public_ip_address.ip_address 595 | host_vars['public_ip'] = public_ip_address.ip_address 596 | host_vars['public_ip_name'] = public_ip_address.name 597 | host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method 598 | host_vars['public_ip_id'] = public_ip_address.id 599 | if public_ip_address.dns_settings: 600 | host_vars['fqdn'] = public_ip_address.dns_settings.fqdn 601 | 602 | self._add_host(host_vars) 603 | 604 | def _selected_machines(self, virtual_machines): 605 | selected_machines = [] 606 | for machine in virtual_machines: 607 | if self._args.host and self._args.host == machine.name: 608 | selected_machines.append(machine) 609 | if self.tags and self._tags_match(machine.tags, self.tags): 610 | selected_machines.append(machine) 611 | if self.locations and machine.location in self.locations: 612 | selected_machines.append(machine) 613 | return selected_machines 614 | 615 | def _get_security_groups(self, resource_group): 616 | ''' For a given resource_group build a mapping of network_interface.id to security_group name ''' 617 | if not self._security_groups: 618 | self._security_groups = dict() 619 | if not self._security_groups.get(resource_group): 620 | self._security_groups[resource_group] = dict() 621 | for group in self._network_client.network_security_groups.list(resource_group): 622 | if group.network_interfaces: 623 | for interface in group.network_interfaces: 624 | self._security_groups[resource_group][interface.id] = dict( 625 | name=group.name, 626 | id=group.id 627 | ) 628 | 629 | def _get_powerstate(self, resource_group, name): 630 | try: 631 | vm = self._compute_client.virtual_machines.get(resource_group, 632 | name, 633 | expand='instanceview') 634 | except Exception as exc: 635 | sys.exit("Error: fetching instanceview for host {0} - {1}".format(name, str(exc))) 636 | 637 | return next((s.code.replace('PowerState/', '') 638 | for s in vm.instance_view.statuses if s.code.startswith('PowerState')), None) 639 | 640 | def _add_host(self, vars): 641 | 642 | host_name = self._to_safe(vars['name']) 643 | resource_group = self._to_safe(vars['resource_group']) 644 | security_group = None 645 | if vars.get('security_group'): 646 | security_group = self._to_safe(vars['security_group']) 647 | 648 | if self.group_by_resource_group: 649 | if not self._inventory.get(resource_group): 650 | self._inventory[resource_group] = [] 651 | self._inventory[resource_group].append(host_name) 652 | 653 | if self.group_by_location: 654 | if not self._inventory.get(vars['location']): 655 | self._inventory[vars['location']] = [] 656 | self._inventory[vars['location']].append(host_name) 657 | 658 | if self.group_by_security_group and security_group: 659 | if not self._inventory.get(security_group): 660 | self._inventory[security_group] = [] 661 | self._inventory[security_group].append(host_name) 662 | 663 | self._inventory['_meta']['hostvars'][host_name] = vars 664 | self._inventory['azure'].append(host_name) 665 | 666 | if self.group_by_tag and vars.get('tags'): 667 | for key, value in vars['tags'].iteritems(): 668 | safe_key = self._to_safe(key) 669 | safe_value = safe_key + '_' + self._to_safe(value) 670 | if not self._inventory.get(safe_key): 671 | self._inventory[safe_key] = [] 672 | if not self._inventory.get(safe_value): 673 | self._inventory[safe_value] = [] 674 | self._inventory[safe_key].append(host_name) 675 | self._inventory[safe_value].append(host_name) 676 | 677 | def _json_format_dict(self, pretty=False): 678 | # convert inventory to json 679 | if pretty: 680 | return json.dumps(self._inventory, sort_keys=True, indent=2) 681 | else: 682 | return json.dumps(self._inventory) 683 | 684 | def _get_settings(self): 685 | # Load settings from the .ini, if it exists. Otherwise, 686 | # look for environment values. 687 | file_settings = self._load_settings() 688 | if file_settings: 689 | for key in AZURE_CONFIG_SETTINGS: 690 | if key in ('resource_groups', 'tags', 'locations') and file_settings.get(key): 691 | values = file_settings.get(key).split(',') 692 | if len(values) > 0: 693 | setattr(self, key, values) 694 | elif file_settings.get(key): 695 | val = self._to_boolean(file_settings[key]) 696 | setattr(self, key, val) 697 | else: 698 | env_settings = self._get_env_settings() 699 | for key in AZURE_CONFIG_SETTINGS: 700 | if key in('resource_groups', 'tags', 'locations') and env_settings.get(key): 701 | values = env_settings.get(key).split(',') 702 | if len(values) > 0: 703 | setattr(self, key, values) 704 | elif env_settings.get(key, None) is not None: 705 | val = self._to_boolean(env_settings[key]) 706 | setattr(self, key, val) 707 | 708 | def _parse_ref_id(self, reference): 709 | response = {} 710 | keys = reference.strip('/').split('/') 711 | for index in range(len(keys)): 712 | if index < len(keys) - 1 and index % 2 == 0: 713 | response[keys[index]] = keys[index + 1] 714 | return response 715 | 716 | def _to_boolean(self, value): 717 | if value in ['Yes', 'yes', 1, 'True', 'true', True]: 718 | result = True 719 | elif value in ['No', 'no', 0, 'False', 'false', False]: 720 | result = False 721 | else: 722 | result = True 723 | return result 724 | 725 | def _get_env_settings(self): 726 | env_settings = dict() 727 | for attribute, env_variable in AZURE_CONFIG_SETTINGS.iteritems(): 728 | env_settings[attribute] = os.environ.get(env_variable, None) 729 | return env_settings 730 | 731 | def _load_settings(self): 732 | basename = os.path.splitext(os.path.basename(__file__))[0] 733 | default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini')) 734 | path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_INI_PATH', default_path))) 735 | config = None 736 | settings = None 737 | try: 738 | config = ConfigParser.ConfigParser() 739 | config.read(path) 740 | except: 741 | pass 742 | 743 | if config is not None: 744 | settings = dict() 745 | for key in AZURE_CONFIG_SETTINGS: 746 | try: 747 | settings[key] = config.get('azure', key, raw=True) 748 | except: 749 | pass 750 | 751 | return settings 752 | 753 | def _tags_match(self, tag_obj, tag_args): 754 | ''' 755 | Return True if the tags object from a VM contains the requested tag values. 756 | 757 | :param tag_obj: Dictionary of string:string pairs 758 | :param tag_args: List of strings in the form key=value 759 | :return: boolean 760 | ''' 761 | 762 | if not tag_obj: 763 | return False 764 | 765 | matches = 0 766 | for arg in tag_args: 767 | arg_key = arg 768 | arg_value = None 769 | if re.search(r':', arg): 770 | arg_key, arg_value = arg.split(':') 771 | if arg_value and tag_obj.get(arg_key, None) == arg_value: 772 | matches += 1 773 | elif not arg_value and tag_obj.get(arg_key, None) is not None: 774 | matches += 1 775 | if matches == len(tag_args): 776 | return True 777 | return False 778 | 779 | def _to_safe(self, word): 780 | ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' 781 | regex = "[^A-Za-z0-9\_" 782 | if not self.replace_dash_in_groups: 783 | regex += "\-" 784 | return re.sub(regex + "]", "_", word) 785 | 786 | 787 | def main(): 788 | if not HAS_AZURE: 789 | sys.exit("The Azure python sdk is not installed (try 'pip install azure>=2.0.0rc5') - {0}".format(HAS_AZURE_EXC)) 790 | 791 | if LooseVersion(azure_compute_version) < LooseVersion(AZURE_MIN_VERSION): 792 | sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} " 793 | "Do you have Azure >= 2.0.0rc5 installed?".format(AZURE_MIN_VERSION, azure_compute_version)) 794 | 795 | AzureInventory() 796 | 797 | if __name__ == '__main__': 798 | main() 799 | --------------------------------------------------------------------------------