import json import re import logging from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import iri_to_uri FrozenInjectors = dict() logger = logging.getLogger('awx.main.migrations') class PluginFileInjector(object): plugin_name = None # Ansible core name used to reference plugin # every source should have collection, these are for the collection name namespace = None collection = None def inventory_as_dict(self, inventory_source, private_data_dir): """Default implementation of inventory plugin file contents. There are some valid cases when all parameters can be obtained from the environment variables, example "plugin: linode" is valid ideally, however, some options should be filled from the inventory source data """ if self.plugin_name is None: raise NotImplementedError('At minimum the plugin name is needed for inventory plugin use.') proper_name = f'{self.namespace}.{self.collection}.{self.plugin_name}' return {'plugin': proper_name} class azure_rm(PluginFileInjector): plugin_name = 'azure_rm' namespace = 'azure' collection = 'azcollection' def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(azure_rm, self).inventory_as_dict(inventory_source, private_data_dir) source_vars = inventory_source.source_vars_dict ret['fail_on_template_errors'] = False group_by_hostvar = { 'location': {'prefix': '', 'separator': '', 'key': 'location'}, 'tag': {'prefix': '', 'separator': '', 'key': 'tags.keys() | list if tags else []'}, # Introduced with https://github.com/ansible/ansible/pull/53046 'security_group': {'prefix': '', 'separator': '', 'key': 'security_group'}, 'resource_group': {'prefix': '', 'separator': '', 'key': 'resource_group'}, # Note, os_family was not documented correctly in script, but defaulted to grouping by it 'os_family': {'prefix': '', 'separator': '', 'key': 'os_disk.operating_system_type'} } # by default group by everything # always respect user setting, if they gave it group_by = [ grouping_name for grouping_name in group_by_hostvar if source_vars.get('group_by_{}'.format(grouping_name), True) ] ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by] if 'tag' in group_by: # Nasty syntax to reproduce "key_value" group names in addition to "key" ret['keyed_groups'].append({ 'prefix': '', 'separator': '', 'key': r'dict(tags.keys() | map("regex_replace", "^(.*)$", "\1_") | list | zip(tags.values() | list)) if tags else []' }) # Compatibility content # TODO: add proper support for instance_filters non-specific to compatibility # TODO: add proper support for group_by non-specific to compatibility # Dashes were not configurable in azure_rm.py script, we do not want unicode, so always use this ret['use_contrib_script_compatible_sanitization'] = True # use same host names as script ret['plain_host_names'] = True # By default the script did not filter hosts ret['default_host_filters'] = [] # User-given host filters user_filters = [] old_filterables = [ ('resource_groups', 'resource_group'), ('tags', 'tags') # locations / location would be an entry # but this would conflict with source_regions ] for key, loc in old_filterables: value = source_vars.get(key, None) if value and isinstance(value, str): # tags can be list of key:value pairs # e.g. 'Creator:jmarshall, peanutbutter:jelly' # or tags can be a list of keys # e.g. 'Creator, peanutbutter' if key == "tags": # grab each key value pair for kvpair in value.split(','): # split into key and value kv = kvpair.split(':') # filter out any host that does not have key # in their tags.keys() variable user_filters.append('"{}" not in tags.keys()'.format(kv[0].strip())) # if a value is provided, check that the key:value pair matches if len(kv) > 1: user_filters.append('tags["{}"] != "{}"'.format(kv[0].strip(), kv[1].strip())) else: user_filters.append('{} not in {}'.format( loc, value.split(',') )) if user_filters: ret.setdefault('exclude_host_filters', []) ret['exclude_host_filters'].extend(user_filters) ret['conditional_groups'] = {'azure': True} ret['hostvar_expressions'] = { 'provisioning_state': 'provisioning_state | title', 'computer_name': 'name', 'type': 'resource_type', 'private_ip': 'private_ipv4_addresses[0] if private_ipv4_addresses else None', 'public_ip': 'public_ipv4_addresses[0] if public_ipv4_addresses else None', 'public_ip_name': 'public_ip_name if public_ip_name is defined else None', 'public_ip_id': 'public_ip_id if public_ip_id is defined else None', 'tags': 'tags if tags else None' } # Special functionality from script if source_vars.get('use_private_ip', False): ret['hostvar_expressions']['ansible_host'] = 'private_ipv4_addresses[0]' # end compatibility content if inventory_source.source_regions and 'all' not in inventory_source.source_regions: # initialize a list for this section in inventory file ret.setdefault('exclude_host_filters', []) # make a python list of the regions we will use python_regions = [x.strip() for x in inventory_source.source_regions.split(',')] # convert that list in memory to python syntax in a string # now put that in jinja2 syntax operating on hostvar key "location" # and put that as an entry in the exclusions list ret['exclude_host_filters'].append("location not in {}".format(repr(python_regions))) return ret class ec2(PluginFileInjector): plugin_name = 'aws_ec2' namespace = 'amazon' collection = 'aws' def _get_ec2_group_by_choices(self): return [ ('ami_id', _('Image ID')), ('availability_zone', _('Availability Zone')), ('aws_account', _('Account')), ('instance_id', _('Instance ID')), ('instance_state', _('Instance State')), ('platform', _('Platform')), ('instance_type', _('Instance Type')), ('key_pair', _('Key Name')), ('region', _('Region')), ('security_group', _('Security Group')), ('tag_keys', _('Tags')), ('tag_none', _('Tag None')), ('vpc_id', _('VPC ID')), ] def _compat_compose_vars(self): return { # vars that change 'ec2_block_devices': ( "dict(block_device_mappings | map(attribute='device_name') | list | zip(block_device_mappings " "| map(attribute='ebs.volume_id') | list))" ), 'ec2_dns_name': 'public_dns_name', 'ec2_group_name': 'placement.group_name', 'ec2_instance_profile': 'iam_instance_profile | default("")', 'ec2_ip_address': 'public_ip_address', 'ec2_kernel': 'kernel_id | default("")', 'ec2_monitored': "monitoring.state in ['enabled', 'pending']", 'ec2_monitoring_state': 'monitoring.state', 'ec2_placement': 'placement.availability_zone', 'ec2_ramdisk': 'ramdisk_id | default("")', 'ec2_reason': 'state_transition_reason', 'ec2_security_group_ids': "security_groups | map(attribute='group_id') | list | join(',')", 'ec2_security_group_names': "security_groups | map(attribute='group_name') | list | join(',')", 'ec2_tag_Name': 'tags.Name', 'ec2_state': 'state.name', 'ec2_state_code': 'state.code', 'ec2_state_reason': 'state_reason.message if state_reason is defined else ""', 'ec2_sourceDestCheck': 'source_dest_check | default(false) | lower | string', # snake_case syntax intended 'ec2_account_id': 'owner_id', # vars that just need ec2_ prefix 'ec2_ami_launch_index': 'ami_launch_index | string', 'ec2_architecture': 'architecture', 'ec2_client_token': 'client_token', 'ec2_ebs_optimized': 'ebs_optimized', 'ec2_hypervisor': 'hypervisor', 'ec2_image_id': 'image_id', 'ec2_instance_type': 'instance_type', 'ec2_key_name': 'key_name', 'ec2_launch_time': r'launch_time | regex_replace(" ", "T") | regex_replace("(\+)(\d\d):(\d)(\d)$", ".\g<2>\g<3>Z")', 'ec2_platform': 'platform | default("")', 'ec2_private_dns_name': 'private_dns_name', 'ec2_private_ip_address': 'private_ip_address', 'ec2_public_dns_name': 'public_dns_name', 'ec2_region': 'placement.region', 'ec2_root_device_name': 'root_device_name', 'ec2_root_device_type': 'root_device_type', # many items need blank defaults because the script tended to keep a common schema 'ec2_spot_instance_request_id': 'spot_instance_request_id | default("")', 'ec2_subnet_id': 'subnet_id | default("")', 'ec2_virtualization_type': 'virtualization_type', 'ec2_vpc_id': 'vpc_id | default("")', # same as ec2_ip_address, the script provided this 'ansible_host': 'public_ip_address', # new with https://github.com/ansible/ansible/pull/53645 'ec2_eventsSet': 'events | default("")', 'ec2_persistent': 'persistent | default(false)', 'ec2_requester_id': 'requester_id | default("")' } def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(ec2, self).inventory_as_dict(inventory_source, private_data_dir) keyed_groups = [] group_by_hostvar = { 'ami_id': {'prefix': '', 'separator': '', 'key': 'image_id', 'parent_group': 'images'}, # 2 entries for zones for same groups to establish 2 parentage trees 'availability_zone': {'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': 'zones'}, 'aws_account': {'prefix': '', 'separator': '', 'key': 'ec2_account_id', 'parent_group': 'accounts'}, # composed var 'instance_id': {'prefix': '', 'separator': '', 'key': 'instance_id', 'parent_group': 'instances'}, # normally turned off 'instance_state': {'prefix': 'instance_state', 'key': 'ec2_state', 'parent_group': 'instance_states'}, # composed var # ec2_platform is a composed var, but group names do not match up to hostvar exactly 'platform': {'prefix': 'platform', 'key': 'platform | default("undefined")', 'parent_group': 'platforms'}, 'instance_type': {'prefix': 'type', 'key': 'instance_type', 'parent_group': 'types'}, 'key_pair': {'prefix': 'key', 'key': 'key_name', 'parent_group': 'keys'}, 'region': {'prefix': '', 'separator': '', 'key': 'placement.region', 'parent_group': 'regions'}, # Security requires some ninja jinja2 syntax, credit to s-hertel 'security_group': {'prefix': 'security_group', 'key': 'security_groups | map(attribute="group_name")', 'parent_group': 'security_groups'}, # tags cannot be parented in exactly the same way as the script due to # https://github.com/ansible/ansible/pull/53812 'tag_keys': [ {'prefix': 'tag', 'key': 'tags', 'parent_group': 'tags'}, {'prefix': 'tag', 'key': 'tags.keys()', 'parent_group': 'tags'} ], # 'tag_none': None, # grouping by no tags isn't a different thing with plugin # naming is redundant, like vpc_id_vpc_8c412cea, but intended 'vpc_id': {'prefix': 'vpc_id', 'key': 'vpc_id', 'parent_group': 'vpcs'}, } # -- same-ish as script here -- group_by = [x.strip().lower() for x in inventory_source.group_by.split(',') if x.strip()] for choice in self._get_ec2_group_by_choices(): value = bool((group_by and choice[0] in group_by) or (not group_by and choice[0] != 'instance_id')) # -- end sameness to script -- if value: this_keyed_group = group_by_hostvar.get(choice[0], None) # If a keyed group syntax does not exist, there is nothing we can do to get this group if this_keyed_group is not None: if isinstance(this_keyed_group, list): keyed_groups.extend(this_keyed_group) else: keyed_groups.append(this_keyed_group) # special case, this parentage is only added if both zones and regions are present if not group_by or ('region' in group_by and 'availability_zone' in group_by): keyed_groups.append({'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': '{{ placement.region }}'}) source_vars = inventory_source.source_vars_dict # This is a setting from the script, hopefully no one used it # if true, it replaces dashes, but not in region / loc names replace_dash = bool(source_vars.get('replace_dash_in_groups', True)) # Compatibility content legacy_regex = { True: r"[^A-Za-z0-9\_]", False: r"[^A-Za-z0-9\_\-]" # do not replace dash, dash is allowed }[replace_dash] list_replacer = 'map("regex_replace", "{rx}", "_") | list'.format(rx=legacy_regex) # this option, a plugin option, will allow dashes, but not unicode # when set to False, unicode will be allowed, but it was not allowed by script # thus, we always have to use this option, and always use our custom regex ret['use_contrib_script_compatible_sanitization'] = True for grouping_data in keyed_groups: if grouping_data['key'] in ('placement.region', 'placement.availability_zone'): # us-east-2 is always us-east-2 according to ec2.py # no sanitization in region-ish groups for the script standards, ever ever continue if grouping_data['key'] == 'tags': # dict jinja2 transformation grouping_data['key'] = 'dict(tags.keys() | {replacer} | zip(tags.values() | {replacer}))'.format( replacer=list_replacer ) elif grouping_data['key'] == 'tags.keys()' or grouping_data['prefix'] == 'security_group': # list jinja2 transformation grouping_data['key'] += ' | {replacer}'.format(replacer=list_replacer) else: # string transformation grouping_data['key'] += ' | regex_replace("{rx}", "_")'.format(rx=legacy_regex) # end compatibility content if source_vars.get('iam_role_arn', None): ret['iam_role_arn'] = source_vars['iam_role_arn'] # This was an allowed ec2.ini option, also plugin option, so pass through if source_vars.get('boto_profile', None): ret['boto_profile'] = source_vars['boto_profile'] elif not replace_dash: # Using the plugin, but still want dashes allowed ret['use_contrib_script_compatible_sanitization'] = True if source_vars.get('nested_groups') is False: for this_keyed_group in keyed_groups: this_keyed_group.pop('parent_group', None) if keyed_groups: ret['keyed_groups'] = keyed_groups # Instance ID not part of compat vars, because of settings.EC2_INSTANCE_ID_VAR compose_dict = {'ec2_id': 'instance_id'} inst_filters = {} # Compatibility content compose_dict.update(self._compat_compose_vars()) # plugin provides "aws_ec2", but not this which the script gave ret['groups'] = {'ec2': True} if source_vars.get('hostname_variable') is not None: hnames = [] for expr in source_vars.get('hostname_variable').split(','): if expr == 'public_dns_name': hnames.append('dns-name') elif not expr.startswith('tag:') and '_' in expr: hnames.append(expr.replace('_', '-')) else: hnames.append(expr) ret['hostnames'] = hnames else: # public_ip as hostname is non-default plugin behavior, script behavior ret['hostnames'] = [ 'network-interface.addresses.association.public-ip', 'dns-name', 'private-dns-name' ] # The script returned only running state by default, the plugin does not # https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options # options: pending | running | shutting-down | terminated | stopping | stopped inst_filters['instance-state-name'] = ['running'] # end compatibility content if source_vars.get('destination_variable') or source_vars.get('vpc_destination_variable'): for fd in ('destination_variable', 'vpc_destination_variable'): if source_vars.get(fd): compose_dict['ansible_host'] = source_vars.get(fd) break if compose_dict: ret['compose'] = compose_dict if inventory_source.instance_filters: # logic used to live in ec2.py, now it belongs to us. Yay more code? filter_sets = [f for f in inventory_source.instance_filters.split(',') if f] for instance_filter in filter_sets: # AND logic not supported, unclear how to... instance_filter = instance_filter.strip() if not instance_filter or '=' not in instance_filter: continue filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)] if not filter_key: continue inst_filters[filter_key] = filter_value if inst_filters: ret['filters'] = inst_filters if inventory_source.source_regions and 'all' not in inventory_source.source_regions: ret['regions'] = inventory_source.source_regions.split(',') return ret class gce(PluginFileInjector): plugin_name = 'gcp_compute' namespace = 'google' collection = 'cloud' def _compat_compose_vars(self): # missing: gce_image, gce_uuid # https://github.com/ansible/ansible/issues/51884 return { 'gce_description': 'description if description else None', 'gce_machine_type': 'machineType', 'gce_name': 'name', 'gce_network': 'networkInterfaces[0].network.name', 'gce_private_ip': 'networkInterfaces[0].networkIP', 'gce_public_ip': 'networkInterfaces[0].accessConfigs[0].natIP | default(None)', 'gce_status': 'status', 'gce_subnetwork': 'networkInterfaces[0].subnetwork.name', 'gce_tags': 'tags.get("items", [])', 'gce_zone': 'zone', 'gce_metadata': 'metadata.get("items", []) | items2dict(key_name="key", value_name="value")', # NOTE: image hostvar is enabled via retrieve_image_info option 'gce_image': 'image', # We need this as long as hostnames is non-default, otherwise hosts # will not be addressed correctly, was returned in script 'ansible_ssh_host': 'networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP)' } def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(gce, self).inventory_as_dict(inventory_source, private_data_dir) # auth related items ret['auth_kind'] = "serviceaccount" filters = [] # TODO: implement gce group_by options # gce never processed the group_by field, if it had, we would selectively # apply those options here, but it did not, so all groups are added here keyed_groups = [ # the jinja2 syntax is duplicated with compose # https://github.com/ansible/ansible/issues/51883 {'prefix': 'network', 'key': 'gce_subnetwork'}, # composed var {'prefix': '', 'separator': '', 'key': 'gce_private_ip'}, # composed var {'prefix': '', 'separator': '', 'key': 'gce_public_ip'}, # composed var {'prefix': '', 'separator': '', 'key': 'machineType'}, {'prefix': '', 'separator': '', 'key': 'zone'}, {'prefix': 'tag', 'key': 'gce_tags'}, # composed var {'prefix': 'status', 'key': 'status | lower'}, # NOTE: image hostvar is enabled via retrieve_image_info option {'prefix': '', 'separator': '', 'key': 'image'}, ] # This will be used as the gce instance_id, must be universal, non-compat compose_dict = {'gce_id': 'id'} # Compatibility content # TODO: proper group_by and instance_filters support, irrelevant of compat mode # The gce.py script never sanitized any names in any way ret['use_contrib_script_compatible_sanitization'] = True # Perform extra API query to get the image hostvar ret['retrieve_image_info'] = True # Add in old hostvars aliases compose_dict.update(self._compat_compose_vars()) # Non-default names to match script ret['hostnames'] = ['name', 'public_ip', 'private_ip'] # end compatibility content if keyed_groups: ret['keyed_groups'] = keyed_groups if filters: ret['filters'] = filters if compose_dict: ret['compose'] = compose_dict if inventory_source.source_regions and 'all' not in inventory_source.source_regions: ret['zones'] = inventory_source.source_regions.split(',') return ret class vmware(PluginFileInjector): plugin_name = 'vmware_vm_inventory' namespace = 'community' collection = 'vmware' def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(vmware, self).inventory_as_dict(inventory_source, private_data_dir) ret['strict'] = False # Documentation of props, see # https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/scenario_guides/vmware_scenarios/vmware_inventory_vm_attributes.rst UPPERCASE_PROPS = [ "availableField", "configIssue", "configStatus", "customValue", # optional "datastore", "effectiveRole", "guestHeartbeatStatus", # optional "layout", # optional "layoutEx", # optional "name", "network", "overallStatus", "parentVApp", # optional "permission", "recentTask", "resourcePool", "rootSnapshot", "snapshot", # optional "triggeredAlarmState", "value" ] NESTED_PROPS = [ "capability", "config", "guest", "runtime", "storage", "summary", # repeat of other properties ] ret['properties'] = UPPERCASE_PROPS + NESTED_PROPS ret['compose'] = {'ansible_host': 'guest.ipAddress'} # default value ret['compose']['ansible_ssh_host'] = ret['compose']['ansible_host'] # the ansible_uuid was unique every host, every import, from the script ret['compose']['ansible_uuid'] = '99999999 | random | to_uuid' for prop in UPPERCASE_PROPS: if prop == prop.lower(): continue ret['compose'][prop.lower()] = prop ret['with_nested_properties'] = True # ret['property_name_format'] = 'lower_case' # only dacrystal/topic/vmware-inventory-plugin-property-format # process custom options vmware_opts = dict(inventory_source.source_vars_dict.items()) if inventory_source.instance_filters: vmware_opts.setdefault('host_filters', inventory_source.instance_filters) if inventory_source.group_by: vmware_opts.setdefault('groupby_patterns', inventory_source.group_by) alias_pattern = vmware_opts.get('alias_pattern') if alias_pattern: ret.setdefault('hostnames', []) for alias in alias_pattern.split(','): # make best effort striped_alias = alias.replace('{', '').replace('}', '').strip() # make best effort if not striped_alias: continue ret['hostnames'].append(striped_alias) host_pattern = vmware_opts.get('host_pattern') # not working in script if host_pattern: stripped_hp = host_pattern.replace('{', '').replace('}', '').strip() # make best effort ret['compose']['ansible_host'] = stripped_hp ret['compose']['ansible_ssh_host'] = stripped_hp host_filters = vmware_opts.get('host_filters') if host_filters: ret.setdefault('filters', []) for hf in host_filters.split(','): striped_hf = hf.replace('{', '').replace('}', '').strip() # make best effort if not striped_hf: continue ret['filters'].append(striped_hf) else: # default behavior filters by power state ret['filters'] = ['runtime.powerState == "poweredOn"'] groupby_patterns = vmware_opts.get('groupby_patterns') ret.setdefault('keyed_groups', []) if groupby_patterns: for pattern in groupby_patterns.split(','): stripped_pattern = pattern.replace('{', '').replace('}', '').strip() # make best effort ret['keyed_groups'].append({ 'prefix': '', 'separator': '', 'key': stripped_pattern }) else: # default groups from script for entry in ('config.guestId', '"templates" if config.template else "guests"'): ret['keyed_groups'].append({ 'prefix': '', 'separator': '', 'key': entry }) return ret class openstack(PluginFileInjector): plugin_name = 'openstack' namespace = 'openstack' collection = 'cloud' def inventory_as_dict(self, inventory_source, private_data_dir): def use_host_name_for_name(a_bool_maybe): if not isinstance(a_bool_maybe, bool): # Could be specified by user via "host" or "uuid" return a_bool_maybe elif a_bool_maybe: return 'name' # plugin default else: return 'uuid' ret = super(openstack, self).inventory_as_dict(inventory_source, private_data_dir) ret['fail_on_errors'] = True ret['expand_hostvars'] = True ret['inventory_hostname'] = use_host_name_for_name(False) # Note: mucking with defaults will break import integrity # For the plugin, we need to use the same defaults as the old script # or else imports will conflict. To find script defaults you have # to read source code of the script. # # Script Defaults Plugin Defaults # 'use_hostnames': False, 'name' (True) # 'expand_hostvars': True, 'no' (False) # 'fail_on_errors': True, 'no' (False) # # These are, yet again, different from ansible_variables in script logic # but those are applied inconsistently source_vars = inventory_source.source_vars_dict for var_name in ['expand_hostvars', 'fail_on_errors']: if var_name in source_vars: ret[var_name] = source_vars[var_name] if 'use_hostnames' in source_vars: ret['inventory_hostname'] = use_host_name_for_name(source_vars['use_hostnames']) return ret class rhv(PluginFileInjector): """ovirt uses the custom credential templating, and that is all """ plugin_name = 'ovirt' initial_version = '2.9' namespace = 'ovirt' collection = 'ovirt' def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(rhv, self).inventory_as_dict(inventory_source, private_data_dir) ret['ovirt_insecure'] = False # Default changed from script # TODO: process strict option upstream ret['compose'] = { 'ansible_host': '(devices.values() | list)[0][0] if devices else None' } ret['keyed_groups'] = [] for key in ('cluster', 'status'): ret['keyed_groups'].append({'prefix': key, 'separator': '_', 'key': key}) ret['keyed_groups'].append({'prefix': 'tag', 'separator': '_', 'key': 'tags'}) ret['ovirt_hostname_preference'] = ['name', 'fqdn'] source_vars = inventory_source.source_vars_dict for key, value in source_vars.items(): if key == 'plugin': continue ret[key] = value return ret class satellite6(PluginFileInjector): plugin_name = 'foreman' namespace = 'theforeman' collection = 'foreman' def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(satellite6, self).inventory_as_dict(inventory_source, private_data_dir) ret['validate_certs'] = False group_patterns = '[]' group_prefix = 'foreman_' want_hostcollections = False want_ansible_ssh_host = False want_facts = True foreman_opts = inventory_source.source_vars_dict.copy() for k, v in foreman_opts.items(): if k == 'satellite6_group_patterns' and isinstance(v, str): group_patterns = v elif k == 'satellite6_group_prefix' and isinstance(v, str): group_prefix = v elif k == 'satellite6_want_hostcollections' and isinstance(v, bool): want_hostcollections = v elif k == 'satellite6_want_ansible_ssh_host' and isinstance(v, bool): want_ansible_ssh_host = v elif k == 'satellite6_want_facts' and isinstance(v, bool): want_facts = v # add backwards support for ssl_verify # plugin uses new option, validate_certs, instead elif k == 'ssl_verify' and isinstance(v, bool): ret['validate_certs'] = v else: ret[k] = str(v) # Compatibility content group_by_hostvar = { "environment": {"prefix": "{}environment_".format(group_prefix), "separator": "", "key": "foreman['environment_name'] | lower | regex_replace(' ', '') | " "regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '')"}, "location": {"prefix": "{}location_".format(group_prefix), "separator": "", "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "organization": {"prefix": "{}organization_".format(group_prefix), "separator": "", "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "lifecycle_environment": {"prefix": "{}lifecycle_environment_".format(group_prefix), "separator": "", "key": "foreman['content_facet_attributes']['lifecycle_environment_name'] | " "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, "content_view": {"prefix": "{}content_view_".format(group_prefix), "separator": "", "key": "foreman['content_facet_attributes']['content_view_name'] | " "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"} } ret['legacy_hostvars'] = True # convert hostvar structure to the form used by the script ret['want_params'] = True ret['group_prefix'] = group_prefix ret['want_hostcollections'] = want_hostcollections ret['want_facts'] = want_facts if want_ansible_ssh_host: ret['compose'] = {'ansible_ssh_host': "foreman['ip6'] | default(foreman['ip'], true)"} ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by_hostvar] def form_keyed_group(group_pattern): """ Converts foreman group_pattern to inventory plugin keyed_group e.g. {app_param}-{tier_param}-{dc_param} becomes "%s-%s-%s" | format(app_param, tier_param, dc_param) """ if type(group_pattern) is not str: return None params = re.findall('{[^}]*}', group_pattern) if len(params) == 0: return None param_names = [] for p in params: param_names.append(p[1:-1].strip()) # strip braces and space # form keyed_group key by # replacing curly braces with '%s' # (for use with jinja's format filter) key = group_pattern for p in params: key = key.replace(p, '%s', 1) # apply jinja filter to key key = '"{}" | format({})'.format(key, ', '.join(param_names)) keyed_group = {'key': key, 'separator': ''} return keyed_group try: group_patterns = json.loads(group_patterns) if type(group_patterns) is list: for group_pattern in group_patterns: keyed_group = form_keyed_group(group_pattern) if keyed_group: ret['keyed_groups'].append(keyed_group) except json.JSONDecodeError: logger.warning('Could not parse group_patterns. Expected JSON-formatted string, found: {}' .format(group_patterns)) return ret class tower(PluginFileInjector): plugin_name = 'tower' namespace = 'awx' collection = 'awx' def inventory_as_dict(self, inventory_source, private_data_dir): ret = super(tower, self).inventory_as_dict(inventory_source, private_data_dir) # Credentials injected as env vars, same as script try: # plugin can take an actual int type identifier = int(inventory_source.instance_filters) except ValueError: # inventory_id could be a named URL identifier = iri_to_uri(inventory_source.instance_filters) ret['inventory_id'] = identifier ret['include_metadata'] = True # used for license check return ret for cls in PluginFileInjector.__subclasses__(): FrozenInjectors[cls.__name__] = cls