diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 70b4dcaa9f..e7b5150d19 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -744,6 +744,89 @@ class InventorySourceOptions(BaseModel): ('custom', _('Custom Script')), ] + # Use tools/scripts/get_ec2_filter_names.py to build this list. + INSTANCE_FILTER_NAMES = [ + "architecture", + "association.allocation-id", + "association.association-id", + "association.ip-owner-id", + "association.public-ip", + "availability-zone", + "block-device-mapping.attach-time", + "block-device-mapping.delete-on-termination", + "block-device-mapping.device-name", + "block-device-mapping.status", + "block-device-mapping.volume-id", + "client-token", + "dns-name", + "group-id", + "group-name", + "hypervisor", + "iam-instance-profile.arn", + "image-id", + "instance-id", + "instance-lifecycle", + "instance-state-code", + "instance-state-name", + "instance-type", + "instance.group-id", + "instance.group-name", + "ip-address", + "kernel-id", + "key-name", + "launch-index", + "launch-time", + "monitoring-state", + "network-interface-private-dns-name", + "network-interface.addresses.association.ip-owner-id", + "network-interface.addresses.association.public-ip", + "network-interface.addresses.primary", + "network-interface.addresses.private-ip-address", + "network-interface.attachment.attach-time", + "network-interface.attachment.attachment-id", + "network-interface.attachment.delete-on-termination", + "network-interface.attachment.device-index", + "network-interface.attachment.instance-id", + "network-interface.attachment.instance-owner-id", + "network-interface.attachment.status", + "network-interface.availability-zone", + "network-interface.description", + "network-interface.group-id", + "network-interface.group-name", + "network-interface.mac-address", + "network-interface.network-interface.id", + "network-interface.owner-id", + "network-interface.requester-id", + "network-interface.requester-managed", + "network-interface.source-destination-check", + "network-interface.status", + "network-interface.subnet-id", + "network-interface.vpc-id", + "owner-id", + "placement-group-name", + "platform", + "private-dns-name", + "private-ip-address", + "product-code", + "product-code.type", + "ramdisk-id", + "reason", + "requester-id", + "reservation-id", + "root-device-name", + "root-device-type", + "source-dest-check", + "spot-instance-request-id", + "state-reason-code", + "state-reason-message", + "subnet-id", + "tag-key", + "tag-value", + "tenancy", + "virtualization-type", + "vpc-id" + ] + class Meta: abstract = True @@ -938,6 +1021,10 @@ class InventorySourceOptions(BaseModel): continue if not instance_filter_re.match(instance_filter): invalid_filters.append(instance_filter) + continue + instance_filter_name = instance_filter.split('=', 1)[0] + if instance_filter_name not in self.INSTANCE_FILTER_NAMES: + invalid_filters.append(instance_filter) if invalid_filters: raise ValidationError('Invalid filter expression%s: %s' % ('' if len(invalid_filters) == 1 else 's', diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 93ef6817f2..6022ea8de8 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1272,6 +1272,10 @@ class InventoryUpdatesTest(BaseTransactionTest): self.assertEqual(response['group_by'], '') # Invalid string for instance filters. inv_src_data['instance_filters'] = 'tag-key_123=Name,' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=400) + # Invalid field name for instance filters. + inv_src_data['instance_filters'] = 'foo=bar,' with self.current_user(self.super_django_user): response = self.put(inv_src_url1, inv_src_data, expect=400) # Valid string for instance filters. @@ -1514,11 +1518,13 @@ class InventoryUpdatesTest(BaseTransactionTest): for host in self.inventory.hosts.filter(active=True): self.assertEqual(host.variables_dict['ec2_instance_type'], instance_type) - # Try invalid instance filters: empty, only "=", more than one "=", whitespace + # Try invalid instance filters that should be ignored: + # empty filter, only "=", more than one "=", whitespace, invalid value + # for given filter name. cache_path = tempfile.mkdtemp(prefix='awx_ec2_') self._temp_paths.append(cache_path) key_name = max(key_names.items(), key=lambda x: len(x[1]))[0] - inventory_source.instance_filters = ',=,image-id=ami=12345678,instance-type=%s, key-name=%s' % (instance_type, key_name) + inventory_source.instance_filters = ',=,image-id=ami=12345678,instance-type=%s, key-name=%s, architecture=ppc' % (instance_type, key_name) inventory_source.source_vars = '---\n\nnested_groups: false\ncache_path: %s\n' % cache_path inventory_source.save() self.check_inventory_source(inventory_source, initial=False) diff --git a/tools/scripts/get_ec2_filter_names.py b/tools/scripts/get_ec2_filter_names.py new file mode 100755 index 0000000000..d001b1f8e7 --- /dev/null +++ b/tools/scripts/get_ec2_filter_names.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import json +import sys +import requests +from bs4 import BeautifulSoup + +response = requests.get('http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html') +soup = BeautifulSoup(response.text) + +section_h3 = soup.find(id='query-DescribeInstances-filters') +section_div = section_h3.find_parent('div', attrs={'class': 'section'}) + +filter_names = [] +for term in section_div.select('div.variablelist dt span.term'): + filter_name = term.get_text() + if not filter_name.startswith('tag:'): + filter_names.append(filter_name) +filter_names.sort() + +json.dump(filter_names, sys.stdout, indent=4)