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