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