├── 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 |
--------------------------------------------------------------------------------