diff --git a/awx/plugins/inventory/azure_rm.py b/awx/plugins/inventory/azure_rm.py index 513e06f30b..691c196f11 100755 --- a/awx/plugins/inventory/azure_rm.py +++ b/awx/plugins/inventory/azure_rm.py @@ -24,7 +24,7 @@ Azure External Inventory Script =============================== Generates dynamic inventory by making API requests to the Azure Resource Manager using the Azure Python SDK. For instruction on installing the -Azure Python SDK see http://azure-sdk-for-python.readthedocs.org/ +Azure Python SDK see https://azure-sdk-for-python.readthedocs.io/ Authentication -------------- @@ -50,6 +50,7 @@ Command line arguments: - ad_user - password - cloud_environment + - adfs_authority_url Environment variables: - AZURE_PROFILE @@ -60,6 +61,7 @@ Environment variables: - AZURE_AD_USER - AZURE_PASSWORD - AZURE_CLOUD_ENVIRONMENT + - AZURE_ADFS_AUTHORITY_URL Run for Specific Host ----------------------- @@ -200,27 +202,43 @@ except ImportError: # python3 import configparser as cp -from packaging.version import Version - from os.path import expanduser import ansible.module_utils.six.moves.urllib.parse as urlparse HAS_AZURE = True HAS_AZURE_EXC = None +HAS_AZURE_CLI_CORE = True +CLIError = None try: + from msrestazure.azure_active_directory import AADTokenCredentials from msrestazure.azure_exceptions import CloudError + from msrestazure.azure_active_directory import MSIAuthentication from msrestazure import azure_cloud from azure.mgmt.compute import __version__ as azure_compute_version from azure.common import AzureMissingResourceHttpError, AzureHttpError from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials from azure.mgmt.network import NetworkManagementClient from azure.mgmt.resource.resources import ResourceManagementClient + from azure.mgmt.resource.subscriptions import SubscriptionClient from azure.mgmt.compute import ComputeManagementClient + from adal.authentication_context import AuthenticationContext except ImportError as exc: HAS_AZURE_EXC = exc HAS_AZURE = False +try: + from azure.cli.core.util import CLIError + from azure.common.credentials import get_azure_cli_credentials, get_cli_profile + from azure.common.cloud import get_cli_active_cloud +except ImportError: + HAS_AZURE_CLI_CORE = False + CLIError = Exception + +try: + from ansible.release import __version__ as ansible_version +except ImportError: + ansible_version = 'unknown' AZURE_CREDENTIAL_ENV_MAPPING = dict( profile='AZURE_PROFILE', @@ -231,6 +249,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict( ad_user='AZURE_AD_USER', password='AZURE_PASSWORD', cloud_environment='AZURE_CLOUD_ENVIRONMENT', + adfs_authority_url='AZURE_ADFS_AUTHORITY_URL' ) AZURE_CONFIG_SETTINGS = dict( @@ -241,10 +260,13 @@ AZURE_CONFIG_SETTINGS = dict( group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP', group_by_location='AZURE_GROUP_BY_LOCATION', group_by_security_group='AZURE_GROUP_BY_SECURITY_GROUP', - group_by_tag='AZURE_GROUP_BY_TAG' + group_by_tag='AZURE_GROUP_BY_TAG', + group_by_os_family='AZURE_GROUP_BY_OS_FAMILY', + use_private_ip='AZURE_USE_PRIVATE_IP' ) AZURE_MIN_VERSION = "2.0.0" +ANSIBLE_USER_AGENT = 'Ansible/{0}'.format(ansible_version) def azure_id_to_dict(id): @@ -265,6 +287,8 @@ class AzureRM(object): self._compute_client = None self._resource_client = None self._network_client = None + self._adfs_authority_url = None + self._resource = None self.debug = False if args.debug: @@ -300,13 +324,38 @@ class AzureRM(object): self.log("setting subscription_id") self.subscription_id = self.credentials['subscription_id'] - if self.credentials.get('client_id') is not None and \ - self.credentials.get('secret') is not None and \ - self.credentials.get('tenant') is not None: + # get authentication authority + # for adfs, user could pass in authority or not. + # for others, use default authority from cloud environment + if self.credentials.get('adfs_authority_url'): + self._adfs_authority_url = self.credentials.get('adfs_authority_url') + else: + self._adfs_authority_url = self._cloud_environment.endpoints.active_directory + + # get resource from cloud environment + self._resource = self._cloud_environment.endpoints.active_directory_resource_id + + if self.credentials.get('credentials'): + self.azure_credentials = self.credentials.get('credentials') + elif self.credentials.get('client_id') and self.credentials.get('secret') and self.credentials.get('tenant'): self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'], secret=self.credentials['secret'], tenant=self.credentials['tenant'], cloud_environment=self._cloud_environment) + + elif self.credentials.get('ad_user') is not None and \ + self.credentials.get('password') is not None and \ + self.credentials.get('client_id') is not None and \ + self.credentials.get('tenant') is not None: + + self.azure_credentials = self.acquire_token_with_username_password( + self._adfs_authority_url, + self._resource, + self.credentials['ad_user'], + self.credentials['password'], + self.credentials['client_id'], + self.credentials['tenant']) + elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None: tenant = self.credentials.get('tenant') if not tenant: @@ -315,9 +364,12 @@ class AzureRM(object): self.credentials['password'], tenant=tenant, cloud_environment=self._cloud_environment) + else: self.fail("Failed to authenticate with provided credentials. Some attributes were missing. " - "Credentials must include client_id, secret and tenant or ad_user and password.") + "Credentials must include client_id, secret and tenant or ad_user and password, or " + "ad_user, password, client_id, tenant and adfs_authority_url(optional) for ADFS authentication, or " + "be logged in using AzureCLI.") def log(self, msg): if self.debug: @@ -361,6 +413,32 @@ class AzureRM(object): return None + def _get_azure_cli_credentials(self): + credentials, subscription_id = get_azure_cli_credentials() + cloud_environment = get_cli_active_cloud() + + cli_credentials = { + 'credentials': credentials, + 'subscription_id': subscription_id, + 'cloud_environment': cloud_environment + } + return cli_credentials + + def _get_msi_credentials(self, subscription_id_param=None): + credentials = MSIAuthentication() + subscription_id_param = subscription_id_param or os.environ.get(AZURE_CREDENTIAL_ENV_MAPPING['subscription_id'], None) + try: + # try to get the subscription in MSI to test whether MSI is enabled + subscription_client = SubscriptionClient(credentials) + subscription = next(subscription_client.subscriptions.list()) + subscription_id = str(subscription.subscription_id) + return { + 'credentials': credentials, + 'subscription_id': subscription_id_param or subscription_id + } + except Exception as exc: + return None + def _get_credentials(self, params): # Get authentication credentials. # Precedence: cmd line parameters-> environment variables-> default profile in ~/.azure/credentials. @@ -397,8 +475,31 @@ class AzureRM(object): self.log('Retrieved default profile credentials from ~/.azure/credentials.') return default_credentials + msi_credentials = self._get_msi_credentials(arg_credentials.get('subscription_id')) + if msi_credentials: + self.log('Retrieved credentials from MSI.') + return msi_credentials + + try: + if HAS_AZURE_CLI_CORE: + self.log('Retrieving credentials from AzureCLI profile') + cli_credentials = self._get_azure_cli_credentials() + return cli_credentials + except CLIError as ce: + self.log('Error getting AzureCLI profile credentials - {0}'.format(ce)) + return None + def acquire_token_with_username_password(self, authority, resource, username, password, client_id, tenant): + authority_uri = authority + + if tenant is not None: + authority_uri = authority + '/' + tenant + + context = AuthenticationContext(authority_uri) + token_response = context.acquire_token_with_username_password(resource, username, password, client_id) + return AADTokenCredentials(token_response) + def _register(self, key): try: # We have to perform the one-time registration here. Otherwise, we receive an error the first @@ -412,16 +513,21 @@ class AzureRM(object): "https://docs.microsoft.com/azure/azure-resource-manager/" "resource-manager-common-deployment-errors#noregisteredproviderfound")) + def get_mgmt_svc_client(self, client_type, base_url, api_version): + client = client_type(self.azure_credentials, + self.subscription_id, + base_url=base_url, + api_version=api_version) + client.config.add_user_agent(ANSIBLE_USER_AGENT) + return client + @property def network_client(self): self.log('Getting network client') if not self._network_client: - self._network_client = NetworkManagementClient( - self.azure_credentials, - self.subscription_id, - base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2017-06-01' - ) + self._network_client = self.get_mgmt_svc_client(NetworkManagementClient, + self._cloud_environment.endpoints.resource_manager, + '2017-06-01') self._register('Microsoft.Network') return self._network_client @@ -429,24 +535,18 @@ class AzureRM(object): def rm_client(self): self.log('Getting resource manager client') if not self._resource_client: - self._resource_client = ResourceManagementClient( - self.azure_credentials, - self.subscription_id, - base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2017-05-10' - ) + self._resource_client = self.get_mgmt_svc_client(ResourceManagementClient, + self._cloud_environment.endpoints.resource_manager, + '2017-05-10') return self._resource_client @property def compute_client(self): self.log('Getting compute client') if not self._compute_client: - self._compute_client = ComputeManagementClient( - self.azure_credentials, - self.subscription_id, - base_url=self._cloud_environment.endpoints.resource_manager, - api_version='2017-03-30' - ) + self._compute_client = self.get_mgmt_svc_client(ComputeManagementClient, + self._cloud_environment.endpoints.resource_manager, + '2017-03-30') self._register('Microsoft.Compute') return self._compute_client @@ -473,9 +573,11 @@ class AzureInventory(object): self.replace_dash_in_groups = False self.group_by_resource_group = True self.group_by_location = True + self.group_by_os_family = True self.group_by_security_group = True self.group_by_tag = True self.include_powerstate = True + self.use_private_ip = False self._inventory = dict( _meta=dict( @@ -528,6 +630,8 @@ class AzureInventory(object): help='Active Directory User') parser.add_argument('--password', action='store', help='password') + parser.add_argument('--adfs_authority_url', action='store', + help='Azure ADFS authority url') parser.add_argument('--cloud_environment', action='store', help='Azure Cloud Environment name or metadata discovery URL') parser.add_argument('--resource-groups', action='store', @@ -545,7 +649,7 @@ class AzureInventory(object): # get VMs for requested resource groups for resource_group in self.resource_groups: try: - virtual_machines = self._compute_client.virtual_machines.list(resource_group) + virtual_machines = self._compute_client.virtual_machines.list(resource_group.lower()) except Exception as exc: sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group, str(exc))) if self._args.host or self.tags: @@ -604,7 +708,7 @@ class AzureInventory(object): host_vars['os_disk'] = dict( name=machine.storage_profile.os_disk.name, - operating_system_type=machine.storage_profile.os_disk.os_type.value + operating_system_type=machine.storage_profile.os_disk.os_type.value.lower() ) if self.include_powerstate: @@ -651,12 +755,15 @@ class AzureInventory(object): for ip_config in network_interface.ip_configurations: host_vars['private_ip'] = ip_config.private_ip_address host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method + if self.use_private_ip: + host_vars['ansible_host'] = ip_config.private_ip_address if ip_config.public_ip_address: public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id) public_ip_address = self._network_client.public_ip_addresses.get( public_ip_reference['resourceGroups'], public_ip_reference['publicIPAddresses']) - host_vars['ansible_host'] = public_ip_address.ip_address + if not self.use_private_ip: + host_vars['ansible_host'] = public_ip_address.ip_address host_vars['public_ip'] = public_ip_address.ip_address host_vars['public_ip_name'] = public_ip_address.name host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method @@ -706,10 +813,16 @@ class AzureInventory(object): host_name = self._to_safe(vars['name']) resource_group = self._to_safe(vars['resource_group']) + operating_system_type = self._to_safe(vars['os_disk']['operating_system_type'].lower()) security_group = None if vars.get('security_group'): security_group = self._to_safe(vars['security_group']) + if self.group_by_os_family: + if not self._inventory.get(operating_system_type): + self._inventory[operating_system_type] = [] + self._inventory[operating_system_type].append(host_name) + if self.group_by_resource_group: if not self._inventory.get(resource_group): self._inventory[resource_group] = []