azure_rm.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2016 Matt Davis, <mdavis@ansible.com>
  4. # Chris Houseknecht, <house@redhat.com>
  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 <http://www.gnu.org/licenses/>.
  20. #
  21. '''
  22. Azure External Inventory Script
  23. ===============================
  24. Generates dynamic inventory by making API requests to the Azure Resource
  25. Manager using the Azure Python SDK. For instruction on installing the
  26. Azure Python SDK see http://azure-sdk-for-python.readthedocs.org/
  27. Authentication
  28. --------------
  29. The order of precedence is command line arguments, environment variables,
  30. and finally the [default] profile found in ~/.azure/credentials.
  31. If using a credentials file, it should be an ini formatted file with one or
  32. more sections, which we refer to as profiles. The script looks for a
  33. [default] section, if a profile is not specified either on the command line
  34. or with an environment variable. The keys in a profile will match the
  35. list of command line arguments below.
  36. For command line arguments and environment variables specify a profile found
  37. in your ~/.azure/credentials file, or a service principal or Active Directory
  38. user.
  39. Command line arguments:
  40. - profile
  41. - client_id
  42. - secret
  43. - subscription_id
  44. - tenant
  45. - ad_user
  46. - password
  47. - cloud_environment
  48. Environment variables:
  49. - AZURE_PROFILE
  50. - AZURE_CLIENT_ID
  51. - AZURE_SECRET
  52. - AZURE_SUBSCRIPTION_ID
  53. - AZURE_TENANT
  54. - AZURE_AD_USER
  55. - AZURE_PASSWORD
  56. - AZURE_CLOUD_ENVIRONMENT
  57. Run for Specific Host
  58. -----------------------
  59. When run for a specific host using the --host option, a resource group is
  60. required. For a specific host, this script returns the following variables:
  61. {
  62. "ansible_host": "XXX.XXX.XXX.XXX",
  63. "computer_name": "computer_name2",
  64. "fqdn": null,
  65. "id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Compute/virtualMachines/object-name",
  66. "image": {
  67. "offer": "CentOS",
  68. "publisher": "OpenLogic",
  69. "sku": "7.1",
  70. "version": "latest"
  71. },
  72. "location": "westus",
  73. "mac_address": "00-00-5E-00-53-FE",
  74. "name": "object-name",
  75. "network_interface": "interface-name",
  76. "network_interface_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkInterfaces/object-name1",
  77. "network_security_group": null,
  78. "network_security_group_id": null,
  79. "os_disk": {
  80. "name": "object-name",
  81. "operating_system_type": "Linux"
  82. },
  83. "plan": null,
  84. "powerstate": "running",
  85. "private_ip": "172.26.3.6",
  86. "private_ip_alloc_method": "Static",
  87. "provisioning_state": "Succeeded",
  88. "public_ip": "XXX.XXX.XXX.XXX",
  89. "public_ip_alloc_method": "Static",
  90. "public_ip_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/publicIPAddresses/object-name",
  91. "public_ip_name": "object-name",
  92. "resource_group": "galaxy-production",
  93. "security_group": "object-name",
  94. "security_group_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkSecurityGroups/object-name",
  95. "tags": {
  96. "db": "database"
  97. },
  98. "type": "Microsoft.Compute/virtualMachines",
  99. "virtual_machine_size": "Standard_DS4"
  100. }
  101. Groups
  102. ------
  103. When run in --list mode, instances are grouped by the following categories:
  104. - azure
  105. - location
  106. - resource_group
  107. - security_group
  108. - tag key
  109. - tag key_value
  110. Control groups using azure_rm.ini or set environment variables:
  111. AZURE_GROUP_BY_RESOURCE_GROUP=yes
  112. AZURE_GROUP_BY_LOCATION=yes
  113. AZURE_GROUP_BY_SECURITY_GROUP=yes
  114. AZURE_GROUP_BY_TAG=yes
  115. Select hosts within specific resource groups by assigning a comma separated list to:
  116. AZURE_RESOURCE_GROUPS=resource_group_a,resource_group_b
  117. Select hosts for specific tag key by assigning a comma separated list of tag keys to:
  118. AZURE_TAGS=key1,key2,key3
  119. Select hosts for specific locations:
  120. AZURE_LOCATIONS=eastus,westus,eastus2
  121. Or, select hosts for specific tag key:value pairs by assigning a comma separated list key:value pairs to:
  122. AZURE_TAGS=key1:value1,key2:value2
  123. If you don't need the powerstate, you can improve performance by turning off powerstate fetching:
  124. AZURE_INCLUDE_POWERSTATE=no
  125. azure_rm.ini
  126. ------------
  127. As mentioned above, you can control execution using environment variables or a .ini file. A sample
  128. azure_rm.ini is included. The name of the .ini file is the basename of the inventory script (in this case
  129. 'azure_rm') with a .ini extension. It also assumes the .ini file is alongside the script. To specify
  130. a different path for the .ini file, define the AZURE_INI_PATH environment variable:
  131. export AZURE_INI_PATH=/path/to/custom.ini
  132. Powerstate:
  133. -----------
  134. The powerstate attribute indicates whether or not a host is running. If the value is 'running', the machine is
  135. up. If the value is anything other than 'running', the machine is down, and will be unreachable.
  136. Examples:
  137. ---------
  138. Execute /bin/uname on all instances in the galaxy-qa resource group
  139. $ ansible -i azure_rm.py galaxy-qa -m shell -a "/bin/uname -a"
  140. Use the inventory script to print instance specific information
  141. $ contrib/inventory/azure_rm.py --host my_instance_host_name --pretty
  142. Use with a playbook
  143. $ ansible-playbook -i contrib/inventory/azure_rm.py my_playbook.yml --limit galaxy-qa
  144. Insecure Platform Warning
  145. -------------------------
  146. If you receive InsecurePlatformWarning from urllib3, install the
  147. requests security packages:
  148. pip install requests[security]
  149. author:
  150. - Chris Houseknecht (@chouseknecht)
  151. - Matt Davis (@nitzmahone)
  152. Company: Ansible by Red Hat
  153. Version: 1.0.0
  154. '''
  155. import argparse
  156. import ConfigParser
  157. import json
  158. import os
  159. import re
  160. import sys
  161. import inspect
  162. import traceback
  163. from packaging.version import Version
  164. from os.path import expanduser
  165. import ansible.module_utils.six.moves.urllib.parse as urlparse
  166. HAS_AZURE = True
  167. HAS_AZURE_EXC = None
  168. try:
  169. from msrestazure.azure_exceptions import CloudError
  170. from msrestazure import azure_cloud
  171. from azure.mgmt.compute import __version__ as azure_compute_version
  172. from azure.common import AzureMissingResourceHttpError, AzureHttpError
  173. from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials
  174. from azure.mgmt.network import NetworkManagementClient
  175. from azure.mgmt.resource.resources import ResourceManagementClient
  176. from azure.mgmt.compute import ComputeManagementClient
  177. except ImportError as exc:
  178. HAS_AZURE_EXC = exc
  179. HAS_AZURE = False
  180. AZURE_CREDENTIAL_ENV_MAPPING = dict(
  181. profile='AZURE_PROFILE',
  182. subscription_id='AZURE_SUBSCRIPTION_ID',
  183. client_id='AZURE_CLIENT_ID',
  184. secret='AZURE_SECRET',
  185. tenant='AZURE_TENANT',
  186. ad_user='AZURE_AD_USER',
  187. password='AZURE_PASSWORD',
  188. cloud_environment='AZURE_CLOUD_ENVIRONMENT',
  189. )
  190. AZURE_CONFIG_SETTINGS = dict(
  191. resource_groups='AZURE_RESOURCE_GROUPS',
  192. tags='AZURE_TAGS',
  193. locations='AZURE_LOCATIONS',
  194. include_powerstate='AZURE_INCLUDE_POWERSTATE',
  195. group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP',
  196. group_by_location='AZURE_GROUP_BY_LOCATION',
  197. group_by_security_group='AZURE_GROUP_BY_SECURITY_GROUP',
  198. group_by_tag='AZURE_GROUP_BY_TAG'
  199. )
  200. AZURE_MIN_VERSION = "2.0.0"
  201. def azure_id_to_dict(id):
  202. pieces = re.sub(r'^\/', '', id).split('/')
  203. result = {}
  204. index = 0
  205. while index < len(pieces) - 1:
  206. result[pieces[index]] = pieces[index + 1]
  207. index += 1
  208. return result
  209. class AzureRM(object):
  210. def __init__(self, args):
  211. self._args = args
  212. self._cloud_environment = None
  213. self._compute_client = None
  214. self._resource_client = None
  215. self._network_client = None
  216. self.debug = False
  217. if args.debug:
  218. self.debug = True
  219. self.credentials = self._get_credentials(args)
  220. if not self.credentials:
  221. self.fail("Failed to get credentials. Either pass as parameters, set environment variables, "
  222. "or define a profile in ~/.azure/credentials.")
  223. # if cloud_environment specified, look up/build Cloud object
  224. raw_cloud_env = self.credentials.get('cloud_environment')
  225. if not raw_cloud_env:
  226. self._cloud_environment = azure_cloud.AZURE_PUBLIC_CLOUD # SDK default
  227. else:
  228. # try to look up "well-known" values via the name attribute on azure_cloud members
  229. all_clouds = [x[1] for x in inspect.getmembers(azure_cloud) if isinstance(x[1], azure_cloud.Cloud)]
  230. matched_clouds = [x for x in all_clouds if x.name == raw_cloud_env]
  231. if len(matched_clouds) == 1:
  232. self._cloud_environment = matched_clouds[0]
  233. elif len(matched_clouds) > 1:
  234. self.fail("Azure SDK failure: more than one cloud matched for cloud_environment name '{0}'".format(raw_cloud_env))
  235. else:
  236. if not urlparse.urlparse(raw_cloud_env).scheme:
  237. self.fail("cloud_environment must be an endpoint discovery URL or one of {0}".format([x.name for x in all_clouds]))
  238. try:
  239. self._cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(raw_cloud_env)
  240. except Exception as e:
  241. self.fail("cloud_environment {0} could not be resolved: {1}".format(raw_cloud_env, e.message))
  242. if self.credentials.get('subscription_id', None) is None:
  243. self.fail("Credentials did not include a subscription_id value.")
  244. self.log("setting subscription_id")
  245. self.subscription_id = self.credentials['subscription_id']
  246. if self.credentials.get('client_id') is not None and \
  247. self.credentials.get('secret') is not None and \
  248. self.credentials.get('tenant') is not None:
  249. self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
  250. secret=self.credentials['secret'],
  251. tenant=self.credentials['tenant'],
  252. cloud_environment=self._cloud_environment)
  253. elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None:
  254. tenant = self.credentials.get('tenant')
  255. if not tenant:
  256. tenant = 'common'
  257. self.azure_credentials = UserPassCredentials(self.credentials['ad_user'],
  258. self.credentials['password'],
  259. tenant=tenant,
  260. cloud_environment=self._cloud_environment)
  261. else:
  262. self.fail("Failed to authenticate with provided credentials. Some attributes were missing. "
  263. "Credentials must include client_id, secret and tenant or ad_user and password.")
  264. def log(self, msg):
  265. if self.debug:
  266. print(msg + u'\n')
  267. def fail(self, msg):
  268. raise Exception(msg)
  269. def _get_profile(self, profile="default"):
  270. path = expanduser("~")
  271. path += "/.azure/credentials"
  272. try:
  273. config = ConfigParser.ConfigParser()
  274. config.read(path)
  275. except Exception as exc:
  276. self.fail("Failed to access {0}. Check that the file exists and you have read "
  277. "access. {1}".format(path, str(exc)))
  278. credentials = dict()
  279. for key in AZURE_CREDENTIAL_ENV_MAPPING:
  280. try:
  281. credentials[key] = config.get(profile, key, raw=True)
  282. except:
  283. pass
  284. if credentials.get('client_id') is not None or credentials.get('ad_user') is not None:
  285. return credentials
  286. return None
  287. def _get_env_credentials(self):
  288. env_credentials = dict()
  289. for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
  290. env_credentials[attribute] = os.environ.get(env_variable, None)
  291. if env_credentials['profile'] is not None:
  292. credentials = self._get_profile(env_credentials['profile'])
  293. return credentials
  294. if env_credentials['client_id'] is not None or env_credentials['ad_user'] is not None:
  295. return env_credentials
  296. return None
  297. def _get_credentials(self, params):
  298. # Get authentication credentials.
  299. # Precedence: cmd line parameters-> environment variables-> default profile in ~/.azure/credentials.
  300. self.log('Getting credentials')
  301. arg_credentials = dict()
  302. for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
  303. arg_credentials[attribute] = getattr(params, attribute)
  304. # try module params
  305. if arg_credentials['profile'] is not None:
  306. self.log('Retrieving credentials with profile parameter.')
  307. credentials = self._get_profile(arg_credentials['profile'])
  308. return credentials
  309. if arg_credentials['client_id'] is not None:
  310. self.log('Received credentials from parameters.')
  311. return arg_credentials
  312. if arg_credentials['ad_user'] is not None:
  313. self.log('Received credentials from parameters.')
  314. return arg_credentials
  315. # try environment
  316. env_credentials = self._get_env_credentials()
  317. if env_credentials:
  318. self.log('Received credentials from env.')
  319. return env_credentials
  320. # try default profile from ~./azure/credentials
  321. default_credentials = self._get_profile()
  322. if default_credentials:
  323. self.log('Retrieved default profile credentials from ~/.azure/credentials.')
  324. return default_credentials
  325. return None
  326. def _register(self, key):
  327. try:
  328. # We have to perform the one-time registration here. Otherwise, we receive an error the first
  329. # time we attempt to use the requested client.
  330. resource_client = self.rm_client
  331. resource_client.providers.register(key)
  332. except Exception as exc:
  333. self.log("One-time registration of {0} failed - {1}".format(key, str(exc)))
  334. self.log("You might need to register {0} using an admin account".format(key))
  335. self.log(("To register a provider using the Python CLI: "
  336. "https://docs.microsoft.com/azure/azure-resource-manager/"
  337. "resource-manager-common-deployment-errors#noregisteredproviderfound"))
  338. @property
  339. def network_client(self):
  340. self.log('Getting network client')
  341. if not self._network_client:
  342. self._network_client = NetworkManagementClient(
  343. self.azure_credentials,
  344. self.subscription_id,
  345. base_url=self._cloud_environment.endpoints.resource_manager,
  346. api_version='2017-06-01'
  347. )
  348. self._register('Microsoft.Network')
  349. return self._network_client
  350. @property
  351. def rm_client(self):
  352. self.log('Getting resource manager client')
  353. if not self._resource_client:
  354. self._resource_client = ResourceManagementClient(
  355. self.azure_credentials,
  356. self.subscription_id,
  357. base_url=self._cloud_environment.endpoints.resource_manager,
  358. api_version='2017-05-10'
  359. )
  360. return self._resource_client
  361. @property
  362. def compute_client(self):
  363. self.log('Getting compute client')
  364. if not self._compute_client:
  365. self._compute_client = ComputeManagementClient(
  366. self.azure_credentials,
  367. self.subscription_id,
  368. base_url=self._cloud_environment.endpoints.resource_manager,
  369. api_version='2017-03-30'
  370. )
  371. self._register('Microsoft.Compute')
  372. return self._compute_client
  373. class AzureInventory(object):
  374. def __init__(self):
  375. self._args = self._parse_cli_args()
  376. try:
  377. rm = AzureRM(self._args)
  378. except Exception as e:
  379. sys.exit("{0}".format(str(e)))
  380. self._compute_client = rm.compute_client
  381. self._network_client = rm.network_client
  382. self._resource_client = rm.rm_client
  383. self._security_groups = None
  384. self.resource_groups = []
  385. self.tags = None
  386. self.locations = None
  387. self.replace_dash_in_groups = False
  388. self.group_by_resource_group = True
  389. self.group_by_location = True
  390. self.group_by_security_group = True
  391. self.group_by_tag = True
  392. self.include_powerstate = True
  393. self._inventory = dict(
  394. _meta=dict(
  395. hostvars=dict()
  396. ),
  397. azure=[]
  398. )
  399. self._get_settings()
  400. if self._args.resource_groups:
  401. self.resource_groups = self._args.resource_groups.split(',')
  402. if self._args.tags:
  403. self.tags = self._args.tags.split(',')
  404. if self._args.locations:
  405. self.locations = self._args.locations.split(',')
  406. if self._args.no_powerstate:
  407. self.include_powerstate = False
  408. self.get_inventory()
  409. print(self._json_format_dict(pretty=self._args.pretty))
  410. sys.exit(0)
  411. def _parse_cli_args(self):
  412. # Parse command line arguments
  413. parser = argparse.ArgumentParser(
  414. description='Produce an Ansible Inventory file for an Azure subscription')
  415. parser.add_argument('--list', action='store_true', default=True,
  416. help='List instances (default: True)')
  417. parser.add_argument('--debug', action='store_true', default=False,
  418. help='Send debug messages to STDOUT')
  419. parser.add_argument('--host', action='store',
  420. help='Get all information about an instance')
  421. parser.add_argument('--pretty', action='store_true', default=False,
  422. help='Pretty print JSON output(default: False)')
  423. parser.add_argument('--profile', action='store',
  424. help='Azure profile contained in ~/.azure/credentials')
  425. parser.add_argument('--subscription_id', action='store',
  426. help='Azure Subscription Id')
  427. parser.add_argument('--client_id', action='store',
  428. help='Azure Client Id ')
  429. parser.add_argument('--secret', action='store',
  430. help='Azure Client Secret')
  431. parser.add_argument('--tenant', action='store',
  432. help='Azure Tenant Id')
  433. parser.add_argument('--ad_user', action='store',
  434. help='Active Directory User')
  435. parser.add_argument('--password', action='store',
  436. help='password')
  437. parser.add_argument('--cloud_environment', action='store',
  438. help='Azure Cloud Environment name or metadata discovery URL')
  439. parser.add_argument('--resource-groups', action='store',
  440. help='Return inventory for comma separated list of resource group names')
  441. parser.add_argument('--tags', action='store',
  442. help='Return inventory for comma separated list of tag key:value pairs')
  443. parser.add_argument('--locations', action='store',
  444. help='Return inventory for comma separated list of locations')
  445. parser.add_argument('--no-powerstate', action='store_true', default=False,
  446. help='Do not include the power state of each virtual host')
  447. return parser.parse_args()
  448. def get_inventory(self):
  449. if len(self.resource_groups) > 0:
  450. # get VMs for requested resource groups
  451. for resource_group in self.resource_groups:
  452. try:
  453. virtual_machines = self._compute_client.virtual_machines.list(resource_group)
  454. except Exception as exc:
  455. sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group, str(exc)))
  456. if self._args.host or self.tags:
  457. selected_machines = self._selected_machines(virtual_machines)
  458. self._load_machines(selected_machines)
  459. else:
  460. self._load_machines(virtual_machines)
  461. else:
  462. # get all VMs within the subscription
  463. try:
  464. virtual_machines = self._compute_client.virtual_machines.list_all()
  465. except Exception as exc:
  466. sys.exit("Error: fetching virtual machines - {0}".format(str(exc)))
  467. if self._args.host or self.tags or self.locations:
  468. selected_machines = self._selected_machines(virtual_machines)
  469. self._load_machines(selected_machines)
  470. else:
  471. self._load_machines(virtual_machines)
  472. def _load_machines(self, machines):
  473. for machine in machines:
  474. id_dict = azure_id_to_dict(machine.id)
  475. # TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets
  476. # fixed, we should remove the .lower(). Opened Issue
  477. # #574: https://github.com/Azure/azure-sdk-for-python/issues/574
  478. resource_group = id_dict['resourceGroups'].lower()
  479. if self.group_by_security_group:
  480. self._get_security_groups(resource_group)
  481. host_vars = dict(
  482. ansible_host=None,
  483. private_ip=None,
  484. private_ip_alloc_method=None,
  485. public_ip=None,
  486. public_ip_name=None,
  487. public_ip_id=None,
  488. public_ip_alloc_method=None,
  489. fqdn=None,
  490. location=machine.location,
  491. name=machine.name,
  492. type=machine.type,
  493. id=machine.id,
  494. tags=machine.tags,
  495. network_interface_id=None,
  496. network_interface=None,
  497. resource_group=resource_group,
  498. mac_address=None,
  499. plan=(machine.plan.name if machine.plan else None),
  500. virtual_machine_size=machine.hardware_profile.vm_size,
  501. computer_name=(machine.os_profile.computer_name if machine.os_profile else None),
  502. provisioning_state=machine.provisioning_state,
  503. )
  504. host_vars['os_disk'] = dict(
  505. name=machine.storage_profile.os_disk.name,
  506. operating_system_type=machine.storage_profile.os_disk.os_type.value
  507. )
  508. if self.include_powerstate:
  509. host_vars['powerstate'] = self._get_powerstate(resource_group, machine.name)
  510. if machine.storage_profile.image_reference:
  511. host_vars['image'] = dict(
  512. offer=machine.storage_profile.image_reference.offer,
  513. publisher=machine.storage_profile.image_reference.publisher,
  514. sku=machine.storage_profile.image_reference.sku,
  515. version=machine.storage_profile.image_reference.version
  516. )
  517. # Add windows details
  518. if machine.os_profile is not None and machine.os_profile.windows_configuration is not None:
  519. host_vars['windows_auto_updates_enabled'] = \
  520. machine.os_profile.windows_configuration.enable_automatic_updates
  521. host_vars['windows_timezone'] = machine.os_profile.windows_configuration.time_zone
  522. host_vars['windows_rm'] = None
  523. if machine.os_profile.windows_configuration.win_rm is not None:
  524. host_vars['windows_rm'] = dict(listeners=None)
  525. if machine.os_profile.windows_configuration.win_rm.listeners is not None:
  526. host_vars['windows_rm']['listeners'] = []
  527. for listener in machine.os_profile.windows_configuration.win_rm.listeners:
  528. host_vars['windows_rm']['listeners'].append(dict(protocol=listener.protocol,
  529. certificate_url=listener.certificate_url))
  530. for interface in machine.network_profile.network_interfaces:
  531. interface_reference = self._parse_ref_id(interface.id)
  532. network_interface = self._network_client.network_interfaces.get(
  533. interface_reference['resourceGroups'],
  534. interface_reference['networkInterfaces'])
  535. if network_interface.primary:
  536. if self.group_by_security_group and \
  537. self._security_groups[resource_group].get(network_interface.id, None):
  538. host_vars['security_group'] = \
  539. self._security_groups[resource_group][network_interface.id]['name']
  540. host_vars['security_group_id'] = \
  541. self._security_groups[resource_group][network_interface.id]['id']
  542. host_vars['network_interface'] = network_interface.name
  543. host_vars['network_interface_id'] = network_interface.id
  544. host_vars['mac_address'] = network_interface.mac_address
  545. for ip_config in network_interface.ip_configurations:
  546. host_vars['private_ip'] = ip_config.private_ip_address
  547. host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method
  548. if ip_config.public_ip_address:
  549. public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id)
  550. public_ip_address = self._network_client.public_ip_addresses.get(
  551. public_ip_reference['resourceGroups'],
  552. public_ip_reference['publicIPAddresses'])
  553. host_vars['ansible_host'] = public_ip_address.ip_address
  554. host_vars['public_ip'] = public_ip_address.ip_address
  555. host_vars['public_ip_name'] = public_ip_address.name
  556. host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method
  557. host_vars['public_ip_id'] = public_ip_address.id
  558. if public_ip_address.dns_settings:
  559. host_vars['fqdn'] = public_ip_address.dns_settings.fqdn
  560. self._add_host(host_vars)
  561. def _selected_machines(self, virtual_machines):
  562. selected_machines = []
  563. for machine in virtual_machines:
  564. if self._args.host and self._args.host == machine.name:
  565. selected_machines.append(machine)
  566. if self.tags and self._tags_match(machine.tags, self.tags):
  567. selected_machines.append(machine)
  568. if self.locations and machine.location in self.locations:
  569. selected_machines.append(machine)
  570. return selected_machines
  571. def _get_security_groups(self, resource_group):
  572. ''' For a given resource_group build a mapping of network_interface.id to security_group name '''
  573. if not self._security_groups:
  574. self._security_groups = dict()
  575. if not self._security_groups.get(resource_group):
  576. self._security_groups[resource_group] = dict()
  577. for group in self._network_client.network_security_groups.list(resource_group):
  578. if group.network_interfaces:
  579. for interface in group.network_interfaces:
  580. self._security_groups[resource_group][interface.id] = dict(
  581. name=group.name,
  582. id=group.id
  583. )
  584. def _get_powerstate(self, resource_group, name):
  585. try:
  586. vm = self._compute_client.virtual_machines.get(resource_group,
  587. name,
  588. expand='instanceview')
  589. except Exception as exc:
  590. sys.exit("Error: fetching instanceview for host {0} - {1}".format(name, str(exc)))
  591. return next((s.code.replace('PowerState/', '')
  592. for s in vm.instance_view.statuses if s.code.startswith('PowerState')), None)
  593. def _add_host(self, vars):
  594. host_name = self._to_safe(vars['name'])
  595. resource_group = self._to_safe(vars['resource_group'])
  596. security_group = None
  597. if vars.get('security_group'):
  598. security_group = self._to_safe(vars['security_group'])
  599. if self.group_by_resource_group:
  600. if not self._inventory.get(resource_group):
  601. self._inventory[resource_group] = []
  602. self._inventory[resource_group].append(host_name)
  603. if self.group_by_location:
  604. if not self._inventory.get(vars['location']):
  605. self._inventory[vars['location']] = []
  606. self._inventory[vars['location']].append(host_name)
  607. if self.group_by_security_group and security_group:
  608. if not self._inventory.get(security_group):
  609. self._inventory[security_group] = []
  610. self._inventory[security_group].append(host_name)
  611. self._inventory['_meta']['hostvars'][host_name] = vars
  612. self._inventory['azure'].append(host_name)
  613. if self.group_by_tag and vars.get('tags'):
  614. for key, value in vars['tags'].items():
  615. safe_key = self._to_safe(key)
  616. safe_value = safe_key + '_' + self._to_safe(value)
  617. if not self._inventory.get(safe_key):
  618. self._inventory[safe_key] = []
  619. if not self._inventory.get(safe_value):
  620. self._inventory[safe_value] = []
  621. self._inventory[safe_key].append(host_name)
  622. self._inventory[safe_value].append(host_name)
  623. def _json_format_dict(self, pretty=False):
  624. # convert inventory to json
  625. if pretty:
  626. return json.dumps(self._inventory, sort_keys=True, indent=2)
  627. else:
  628. return json.dumps(self._inventory)
  629. def _get_settings(self):
  630. # Load settings from the .ini, if it exists. Otherwise,
  631. # look for environment values.
  632. file_settings = self._load_settings()
  633. if file_settings:
  634. for key in AZURE_CONFIG_SETTINGS:
  635. if key in ('resource_groups', 'tags', 'locations') and file_settings.get(key):
  636. values = file_settings.get(key).split(',')
  637. if len(values) > 0:
  638. setattr(self, key, values)
  639. elif file_settings.get(key):
  640. val = self._to_boolean(file_settings[key])
  641. setattr(self, key, val)
  642. else:
  643. env_settings = self._get_env_settings()
  644. for key in AZURE_CONFIG_SETTINGS:
  645. if key in('resource_groups', 'tags', 'locations') and env_settings.get(key):
  646. values = env_settings.get(key).split(',')
  647. if len(values) > 0:
  648. setattr(self, key, values)
  649. elif env_settings.get(key, None) is not None:
  650. val = self._to_boolean(env_settings[key])
  651. setattr(self, key, val)
  652. def _parse_ref_id(self, reference):
  653. response = {}
  654. keys = reference.strip('/').split('/')
  655. for index in range(len(keys)):
  656. if index < len(keys) - 1 and index % 2 == 0:
  657. response[keys[index]] = keys[index + 1]
  658. return response
  659. def _to_boolean(self, value):
  660. if value in ['Yes', 'yes', 1, 'True', 'true', True]:
  661. result = True
  662. elif value in ['No', 'no', 0, 'False', 'false', False]:
  663. result = False
  664. else:
  665. result = True
  666. return result
  667. def _get_env_settings(self):
  668. env_settings = dict()
  669. for attribute, env_variable in AZURE_CONFIG_SETTINGS.items():
  670. env_settings[attribute] = os.environ.get(env_variable, None)
  671. return env_settings
  672. def _load_settings(self):
  673. basename = os.path.splitext(os.path.basename(__file__))[0]
  674. default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini'))
  675. path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_INI_PATH', default_path)))
  676. config = None
  677. settings = None
  678. try:
  679. config = ConfigParser.ConfigParser()
  680. config.read(path)
  681. except:
  682. pass
  683. if config is not None:
  684. settings = dict()
  685. for key in AZURE_CONFIG_SETTINGS:
  686. try:
  687. settings[key] = config.get('azure', key, raw=True)
  688. except:
  689. pass
  690. return settings
  691. def _tags_match(self, tag_obj, tag_args):
  692. '''
  693. Return True if the tags object from a VM contains the requested tag values.
  694. :param tag_obj: Dictionary of string:string pairs
  695. :param tag_args: List of strings in the form key=value
  696. :return: boolean
  697. '''
  698. if not tag_obj:
  699. return False
  700. matches = 0
  701. for arg in tag_args:
  702. arg_key = arg
  703. arg_value = None
  704. if re.search(r':', arg):
  705. arg_key, arg_value = arg.split(':')
  706. if arg_value and tag_obj.get(arg_key, None) == arg_value:
  707. matches += 1
  708. elif not arg_value and tag_obj.get(arg_key, None) is not None:
  709. matches += 1
  710. if matches == len(tag_args):
  711. return True
  712. return False
  713. def _to_safe(self, word):
  714. ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
  715. regex = "[^A-Za-z0-9\_"
  716. if not self.replace_dash_in_groups:
  717. regex += "\-"
  718. return re.sub(regex + "]", "_", word)
  719. def main():
  720. if not HAS_AZURE:
  721. sys.exit("The Azure python sdk is not installed (try `pip install 'azure>={0}' --upgrade`) - {1}".format(AZURE_MIN_VERSION, HAS_AZURE_EXC))
  722. AzureInventory()
  723. if __name__ == '__main__':
  724. main()