From 6d978bc285b4313be1655c287045e92a1a3e3f46 Mon Sep 17 00:00:00 2001 From: jctanner Date: Mon, 25 Mar 2019 12:15:31 -0400 Subject: [PATCH] check aws inv plugin (#53435) * Add the constructed config with legacy settings enabled to match the script * Add interesting characters in tags and security group names * add strict to config * Add a stopped instance in inventory * Create symlinks in the test * Add reservation details to mock * run script and plugin with a virtual env * call the script with ansible-inventory * Fix code coverage collection. --- .../targets/inventory_aws_conformance/aliases | 2 + .../targets/inventory_aws_conformance/ec2.sh | 5 + .../inventory_diff.py | 64 ++++ .../inventory_aws_conformance/lib/__init__.py | 0 .../lib/boto/__init__.py | 2 + .../lib/boto/ec2/__init__.py | 45 +++ .../lib/boto/elasticache/__init__.py | 29 ++ .../lib/boto/exception.py | 19 + .../lib/boto/exceptions.py | 19 + .../lib/boto/mocks/__init__.py | 0 .../lib/boto/mocks/instances.py | 345 ++++++++++++++++++ .../inventory_aws_conformance/lib/boto/rds.py | 0 .../lib/boto/route53.py | 0 .../lib/boto/session.py | 73 ++++ .../inventory_aws_conformance/lib/boto/sts.py | 0 .../inventory_aws_conformance/runme.sh | 151 ++++++++ 16 files changed, 754 insertions(+) create mode 100644 test/integration/targets/inventory_aws_conformance/aliases create mode 100755 test/integration/targets/inventory_aws_conformance/ec2.sh create mode 100755 test/integration/targets/inventory_aws_conformance/inventory_diff.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/__init__.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/__init__.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/ec2/__init__.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/elasticache/__init__.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/exception.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/exceptions.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/mocks/__init__.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/mocks/instances.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/rds.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/route53.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/session.py create mode 100644 test/integration/targets/inventory_aws_conformance/lib/boto/sts.py create mode 100755 test/integration/targets/inventory_aws_conformance/runme.sh diff --git a/test/integration/targets/inventory_aws_conformance/aliases b/test/integration/targets/inventory_aws_conformance/aliases new file mode 100644 index 00000000000..092d6ac64b5 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/aliases @@ -0,0 +1,2 @@ +shippable/posix/group2 +needs/file/contrib/inventory/ec2.py diff --git a/test/integration/targets/inventory_aws_conformance/ec2.sh b/test/integration/targets/inventory_aws_conformance/ec2.sh new file mode 100755 index 00000000000..9ae9dee58ab --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/ec2.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Wrapper to use the correct Python interpreter and support code coverage. +ABS_SCRIPT=$(python -c "import os; print(os.path.abspath('../../../../contrib/inventory/ec2.py'))") +cd "${OUTPUT_DIR}" +python.py "${ABS_SCRIPT}" "$@" diff --git a/test/integration/targets/inventory_aws_conformance/inventory_diff.py b/test/integration/targets/inventory_aws_conformance/inventory_diff.py new file mode 100755 index 00000000000..f50df11b867 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/inventory_diff.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +import json +import sys + + +def check_hosts(contrib, plugin): + contrib_hosts = sorted(contrib['_meta']['hostvars'].keys()) + plugin_hosts = sorted(plugin['_meta']['hostvars'].keys()) + assert contrib_hosts == plugin_hosts + return contrib_hosts, plugin_hosts + + +def check_groups(contrib, plugin): + contrib_groups = set(contrib.keys()) + plugin_groups = set(plugin.keys()) + missing_groups = contrib_groups.difference(plugin_groups) + if missing_groups: + print("groups: %s are missing from the plugin" % missing_groups) + assert not missing_groups + return contrib_groups, plugin_groups + + +def check_host_vars(key, value, plugin, host): + # tags are a dict in the plugin + if key.startswith('ec2_tag'): + print('assert tag', key, value) + assert 'tags' in plugin['_meta']['hostvars'][host], 'b file does not have tags in host' + btags = plugin['_meta']['hostvars'][host]['tags'] + tagkey = key.replace('ec2_tag_', '') + assert tagkey in btags, '%s tag not in b file host tags' % tagkey + assert value == btags[tagkey], '%s != %s' % (value, btags[tagkey]) + else: + print('assert var', key, value, key in plugin['_meta']['hostvars'][host], plugin['_meta']['hostvars'][host].get(key)) + assert key in plugin['_meta']['hostvars'][host], "%s not in b's %s hostvars" % (key, host) + assert value == plugin['_meta']['hostvars'][host][key], "%s != %s" % (value, plugin['_meta']['hostvars'][host][key]) + + +def main(): + # a should be the source of truth (the script output) + a = sys.argv[1] + # b should be the thing to check (the plugin output) + b = sys.argv[2] + + with open(a, 'r') as f: + adata = json.loads(f.read()) + with open(b, 'r') as f: + bdata = json.loads(f.read()) + + # all hosts should be present obviously + ahosts, bhosts = check_hosts(adata, bdata) + + # all groups should be present obviously + agroups, bgroups = check_groups(adata, bdata) + + # check host vars can be reconstructed + for ahost in ahosts: + contrib_host_vars = adata['_meta']['hostvars'][ahost] + for key, value in contrib_host_vars.items(): + check_host_vars(key, value, bdata, ahost) + + +if __name__ == "__main__": + main() diff --git a/test/integration/targets/inventory_aws_conformance/lib/__init__.py b/test/integration/targets/inventory_aws_conformance/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/__init__.py b/test/integration/targets/inventory_aws_conformance/lib/boto/__init__.py new file mode 100644 index 00000000000..794d39542ef --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/__init__.py @@ -0,0 +1,2 @@ +import boto.exceptions as exceptions # pylint: disable=useless-import-alias +import boto.session as session # pylint: disable=useless-import-alias diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/ec2/__init__.py b/test/integration/targets/inventory_aws_conformance/lib/boto/ec2/__init__.py new file mode 100644 index 00000000000..a95dfe53186 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/ec2/__init__.py @@ -0,0 +1,45 @@ +# boto2 + +from boto.mocks.instances import BotoInstance, Reservation + + +class Region(object): + name = None + + def __init__(self, name): + self.name = name + + +class Connection(object): + region = None + instances = None + + def __init__(self, **kwargs): + self.reservations = [Reservation( + owner_id='123456789012', + instance_ids=['i-0678e70402c0b434c', 'i-16a83b42f01c082a1'], + region=kwargs['region'] + )] + + def get_all_instances(self, *args, **kwargs): + return self.reservations + + def describe_cache_clusters(self, *args, **kwargs): + return {} + + def get_all_tags(self, *args, **kwargs): + tags = [] + resid = kwargs['filters']['resource-id'][0] + for instance in self.reservations[0].instances: + if instance.id == resid: + tags = instance._tags[:] + break + return tags + + +def connect_to_region(*args, **kwargs): + return Connection(region=args[0]) + + +def regions(): + return [Region('us-east-1')] diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/elasticache/__init__.py b/test/integration/targets/inventory_aws_conformance/lib/boto/elasticache/__init__.py new file mode 100644 index 00000000000..e8060797bce --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/elasticache/__init__.py @@ -0,0 +1,29 @@ +class Connection(object): + def __init__(self): + pass + + def get_all_instances(self, *args, **kwargs): + return [] + + def describe_cache_clusters(self, *args, **kwargs): + return { + 'DescribeCacheClustersResponse': { + 'DescribeCacheClustersResult': { + 'Marker': None, + 'CacheClusters': [] + } + } + } + + def describe_replication_groups(self, *args, **kwargs): + return { + 'DescribeReplicationGroupsResponse': { + 'DescribeReplicationGroupsResult': { + 'ReplicationGroups': [] + } + } + } + + +def connect_to_region(*args, **kwargs): + return Connection() diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/exception.py b/test/integration/targets/inventory_aws_conformance/lib/boto/exception.py new file mode 100644 index 00000000000..4617c2896f9 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/exception.py @@ -0,0 +1,19 @@ + +class BotoServerError(Exception): + pass + + +class ClientError(Exception): + pass + + +class PartialCredentialsError(Exception): + pass + + +class ProfileNotFound(Exception): + pass + + +class BotoCoreError(Exception): + pass diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/exceptions.py b/test/integration/targets/inventory_aws_conformance/lib/boto/exceptions.py new file mode 100644 index 00000000000..4617c2896f9 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/exceptions.py @@ -0,0 +1,19 @@ + +class BotoServerError(Exception): + pass + + +class ClientError(Exception): + pass + + +class PartialCredentialsError(Exception): + pass + + +class ProfileNotFound(Exception): + pass + + +class BotoCoreError(Exception): + pass diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/mocks/__init__.py b/test/integration/targets/inventory_aws_conformance/lib/boto/mocks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/mocks/instances.py b/test/integration/targets/inventory_aws_conformance/lib/boto/mocks/instances.py new file mode 100644 index 00000000000..4d6713d8d14 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/mocks/instances.py @@ -0,0 +1,345 @@ +from ansible.module_utils.common._collections_compat import MutableMapping + +import datetime +from dateutil.tz import tzutc +import sys + +try: + from ansible.parsing.yaml.objects import AnsibleUnicode +except ImportError: + AnsibleUnicode = str + + +if sys.version_info[0] >= 3: + unicode = str + +DNSDOMAIN = "ansible.amazon.com" + + +class Reservation(object): + def __init__(self, owner_id, instance_ids, region): + if len(instance_ids) > 1: + stopped_instance = instance_ids[-1] + self.instances = [] + for instance_id in instance_ids: + stopped = bool(instance_id == stopped_instance) + self.instances.append(BotoInstance(instance_id=instance_id, owner_id=owner_id, region=region, stopped=stopped)) + self.owner_id = owner_id + + +class Tag(object): + res_id = None + name = None + value = None + + def __init__(self, res_id, name, value): + self.res_id = res_id + self.name = name + self.value = value + + +class SecurityGroup(object): + name = 'sg_default' + group_id = 'sg-00000' + id = 'sg-00000' + + def __init__(self, group_id, group_name): + self.name = group_name + self.group_id = group_id + self.id = self.group_id + + def __str__(self): + return self.name + + +class NetworkInterfaceBase(list): + + def __init__(self, owner_id=None, private_ip=None, subnet_id=None, vpc_id=None): + self.description = 'Primary network interface' + self.mac_address = '06:32:7e:30:3a:20' + self.owner_id = owner_id + self.private_ip_address = private_ip + self.status = 'in-use' + self.subnet_id = subnet_id + self.vpc_id = vpc_id + + super(NetworkInterfaceBase, self).__init__([self.to_dict()]) + + def to_dict(self): + + data = {} + for attr in dir(self): + if attr.startswith('__') or attr == 'boto3': + continue + + val = getattr(self, attr) + + if callable(val): + continue + + if self.boto3: + attr = ''.join(x.capitalize() or '_' for x in attr.split('_')) + + data[attr] = val + + return data + + +class Boto3NetworkInterface(NetworkInterfaceBase): + + boto3 = True + + def __init__(self, owner_id=None, public_ip=None, public_dns=None, private_ip=None, security_groups=None, subnet_id=None, vpc_id=None): + self.association = { + 'IpOwnerId': 'amazon', + 'PublicDnsName': public_dns, + 'PublicIp': public_ip + } + self.attachment = { + 'AttachTime': datetime.datetime(2019, 2, 27, 19, 41, 49, tzinfo=tzutc()), + 'AttachmentId': 'eni-attach-008fda539bfd1877d', + 'DeleteOnTermination': True, + 'DeviceIndex': 0, + 'Status': 'attached' + } + self.groups = security_groups + self.ipv6_addresses = [{'Ipv6Address': '2600:1f18:1af:f6a1:2c8d:7cf:3d14:1224'}] + self.network_interface_id = 'eni-00abc58b929197984' + self.private_ip_addresses = [{ + 'Association': { + 'IpOwnerId': 'amazon', + 'PublicDnsName': public_dns, + 'PublicIp': public_ip + }, + 'Primary': True, + 'PrivateIpAddress': private_ip + }] + self.source_dest_check = True + + super(Boto3NetworkInterface, self).__init__( + owner_id=owner_id, + private_ip=private_ip, + subnet_id=subnet_id, + vpc_id=vpc_id + ) + + +class BotoNetworkInterface(NetworkInterfaceBase): + + boto3 = False + + def __init__(self, owner_id=None, public_ip=None, public_dns=None, private_ip=None, subnet_id=None, vpc_id=None): + self.tags = {} + self.id = 'eni-00abc58b929197984' + self.availability_zone = None + self.requester_managed = False + self.publicIp = public_ip + self.publicDnsName = public_dns + self.ipOwnerId = 'amazon' + self.association = '\n ' + self.item = '\n ' + + super(BotoNetworkInterface, self).__init__( + owner_id=owner_id, + private_ip=private_ip, + subnet_id=subnet_id, + vpc_id=vpc_id + ) + + +class Volume(object): + def __init__(self, volume_id): + self.volume_id = volume_id + + +class BlockDeviceMapping(MutableMapping): + devices = {} + + def __init__(self, devices): + for device, volume_id in devices.items(): + self.devices[device] = Volume(volume_id) + + def __getitem__(self, key): + return self.devices[key] + + def __setitem__(self, key, value): + self.devices[key] = Volume(value) + + def __delitem__(self, key): + del self.devices[key] + + def __iter__(self): + return iter(self.devices) + + def __len__(self): + return len(self.devices) + + +class InstanceBase(object): + def __init__(self, stopped=False): + # set common ignored attribute to make sure instances have identical tags and security groups + self._ignore_security_groups = { + 'sg-0e1d2bd02b45b712e': 'sgname-with-hyphens', + 'sg-ae5c262eb5c4d712e': 'name@with?invalid!chars' + } + self._ignore_tags = { + 'tag-with-hyphens': 'value:with:colons', + b'\xec\xaa\xb4'.decode('utf'): 'value1with@invalid:characters', + 'tag;me': 'value@noplez', + 'tag!notit': 'value<=ohwhy?' + } + if not stopped: + self._ignore_state = {'Code': 16, 'Name': 'running'} + else: + self._ignore_state = {'Code': 80, 'Name': 'stopped'} + + # common attributes + self.ami_launch_index = '0' + self.architecture = 'x86_64' + self.client_token = '' + self.ebs_optimized = False + self.hypervisor = 'xen' + self.image_id = 'ami-0ac019f4fcb7cb7e6' + self.instance_type = 't2.micro' + self.key_name = 'k!y:2/-n@me' + self.private_dns_name = 'ip-20-0-0-20.ec2.internal' + self.private_ip_address = '20.0.0.20' + self.product_codes = [] + if not stopped: + self.public_dns_name = 'ec2-12-3-456-78.compute-1.amazonaws.com' + else: + self.public_dns_name = '' + self.root_device_name = '/dev/sda1' + self.root_device_type = 'ebs' + self.subnet_id = 'subnet-09564ba2121bca7bd' + self.virtualization_type = 'hvm' + self.vpc_id = 'vpc-01ae527fabc81dd04' + + def to_dict(self): + + data = {} + for attr in dir(self): + if attr.startswith(('__', '_ignore')) or attr in ['to_dict', 'boto3']: + continue + + val = getattr(self, attr) + + if self.boto3: + attr = ''.join(x.capitalize() or '_' for x in attr.split('_')) + + data[attr] = val + + return data + + +class BotoInstance(InstanceBase): + + boto3 = False + + def __init__(self, instance_id=None, owner_id=None, region=None, stopped=False): + super(BotoInstance, self).__init__(stopped=stopped) + + self._in_monitoring_element = False + self._tags = [Tag(instance_id, k, v) for k, v in self._ignore_tags.items()] + self.block_device_mapping = BlockDeviceMapping({'/dev/sda1': 'vol-044a646a9292c82af'}) + self.dns_name = 'ec2-12-3-456-78.compute-1.amazonaws.com' + self.eventsSet = None + self.group_name = None + self.groups = [SecurityGroup(k, v) for k, v in self._ignore_security_groups.items()] + self.id = instance_id + self.instance_profile = { + 'arn': 'arn:aws:iam::{0}:instance-profile/developer'.format(owner_id), + 'id': 'ABCDE2GHIJKLMN8PQRSTU' + } + if not stopped: + self.ip_address = '12.3.456.7' + else: + self.ip_address = '' # variable is returned as empty by boto if the instance is stopped + self.item = '\n ' + self.kernel = None + self.launch_time = '2019-02-27T19:41:49.000Z' + self.monitored = False + self.monitoring = '\n ' + self.monitoring_state = 'disabled' + self.persistent = False + self.placement = region + 'e' + self.platform = None + self.ramdisk = None + self.reason = '' + self.region = region + self.requester_id = None + self.sourceDestCheck = 'true' + self.spot_instance_request_id = None + self.state = self._ignore_state['Name'] + self.state_code = self._ignore_state['Code'] + if not stopped: + self.state_reason = None + else: + self.state_reason = { + 'code': 'Client.UserInitiatedShutdown', + 'message': 'Client.UserInitiatedShutdown: User initiated shutdown' + } + self.tags = dict(self._ignore_tags) + + self.interfaces = BotoNetworkInterface( + owner_id=owner_id, + public_ip=self.ip_address, + public_dns=self.public_dns_name, + private_ip=self.private_ip_address, + subnet_id=self.subnet_id, + vpc_id=self.vpc_id, + ) + + +class Boto3Instance(InstanceBase): + + boto3 = True + + def __init__(self, instance_id=None, owner_id=None, region=None, stopped=False): + super(Boto3Instance, self).__init__(stopped=stopped) + + self.block_device_mappings = [{ + 'DeviceName': '/dev/sda1', + 'Ebs': { + 'AttachTime': datetime.datetime(2019, 2, 27, 19, 41, 50, tzinfo=tzutc()), + 'DeleteOnTermination': True, + 'Status': 'attached', + 'VolumeId': 'vol-044a646a9292c82af' + } + }] + self.capacity_reservation_specification = {'CapacityReservationPreference': 'open'} + self.cpu_options = {'CoreCount': 1, 'ThreadsPerCore': 1} + self.ena_support = True + self.hibernation_options = {'Configured': False} + self.iam_instance_profile = { + 'Arn': 'arn:aws:iam::{0}:instance-profile/developer'.format(owner_id), + 'Id': 'ABCDE2GHIJKLMN8PQRSTU' + } + self.instance_id = instance_id + self.launch_time = datetime.datetime(2019, 2, 27, 19, 41, 49, tzinfo=tzutc()) + self.monitoring = {'State': 'disabled'} + self.placement = {'AvailabilityZone': region + 'e', 'GroupName': '', 'Tenancy': 'default'} + if not stopped: + self.public_ip_address = '12.3.456.7' # variable is not returned by boto3 if the instance is stopped + self.security_groups = [{'GroupId': key, 'GroupName': value} for key, value in self._ignore_security_groups.items()] + self.source_dest_check = True + self.state = dict(self._ignore_state) + if not stopped: + self.state_transition_reason = '' + else: + self.state_transition_reason = 'User initiated (2019-02-11 12:49:13 GMT)' + self.state_reason = { # this variable is only returned by AWS if the instance is stopped + 'Code': 'Client.UserInitiatedShutdown', + 'Message': 'Client.UserInitiatedShutdown: User initiated shutdown' + } + self.tags = [{'Key': k, 'Value': v} for k, v in self._ignore_tags.items()] + + self.network_interfaces = Boto3NetworkInterface( + owner_id=owner_id, + public_ip=getattr(self, 'public_ip_address', ''), + public_dns=self.public_dns_name, + private_ip=self.private_ip_address, + security_groups=self.security_groups, + subnet_id=self.subnet_id, + vpc_id=self.vpc_id + ) diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/rds.py b/test/integration/targets/inventory_aws_conformance/lib/boto/rds.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/route53.py b/test/integration/targets/inventory_aws_conformance/lib/boto/route53.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/session.py b/test/integration/targets/inventory_aws_conformance/lib/boto/session.py new file mode 100644 index 00000000000..4624c306169 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/lib/boto/session.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# boto3 + +from boto.mocks.instances import Boto3Instance + + +class Paginator(object): + def __init__(self, datalist): + self.datalist = datalist + + def paginate(self, *args, **kwargs): + ''' + {'Filters': [{'Name': 'instance-state-name', + 'Values': ['running', 'pending', 'stopping', 'stopped']}]} + ''' + filters = kwargs.get('Filters', []) + if not (filters or any([True for f in filters if f['Name'] == 'instance-state-name'])): + self.instance_states = ['running', 'pending', 'stopping', 'stopped'] + else: + self.instance_states = [f['Values'] for f in filters if f['Name'] == 'instance-state-name'][0] + return self + + def build_full_result(self): + filtered_states = set([x.state['Name'] for x in self.datalist]).difference(set(self.instance_states)) + return {'Reservations': [{ + 'Instances': [x.to_dict() for x in self.datalist if x.state['Name'] not in filtered_states], + 'OwnerId': '123456789012', + 'RequesterId': 'AIDAIS3MMFPO53D2T3WWE', + 'ReservationId': 'r-07889670a282de964' + }]} + + +class Client(object): + cloud = None + region = None + + def __init__(self, *args, **kwargs): + self.cloud = args[0] + self.region = args[1] + + def get_paginator(self, method): + if method == 'describe_instances': + return Paginator( + [Boto3Instance(instance_id='i-0678e70402c0b434c', owner_id='123456789012', region=self.region), + Boto3Instance(instance_id='i-16a83b42f01c082a1', owner_id='123456789012', region=self.region, stopped=True)] + ) + + +class Session(object): + profile_name = None + region = None + + def __init__(self, *args, **kwargs): + for k, v in kwargs.items(): + if hasattr(self, k): + setattr(self, k, v) + + def client(self, *args, **kwargs): + return Client(*args, **kwargs) + + def get_config_variables(self, key): + if hasattr(self, key): + return getattr(self, key) + + def get_available_regions(self, *args): + return ['us-east-1'] + + def get_credentials(self, *args, **kwargs): + raise Exception('not implemented') + + +def get_session(*args, **kwargs): + return Session(*args, **kwargs) diff --git a/test/integration/targets/inventory_aws_conformance/lib/boto/sts.py b/test/integration/targets/inventory_aws_conformance/lib/boto/sts.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/inventory_aws_conformance/runme.sh b/test/integration/targets/inventory_aws_conformance/runme.sh new file mode 100755 index 00000000000..810bc8c09e6 --- /dev/null +++ b/test/integration/targets/inventory_aws_conformance/runme.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -ex + +virtualenv --system-site-packages --python "${ANSIBLE_TEST_PYTHON_INTERPRETER:-python}" "${OUTPUT_DIR}/aws-ec2-inventory" +source "${OUTPUT_DIR}/aws-ec2-inventory/bin/activate" +pip install "python-dateutil>=2.1,<2.7.0" jmespath "Jinja2>=2.10" PyYaml cryptography paramiko + +# create boto3 symlinks +ln -s "$(pwd)/lib/boto" "$(pwd)/lib/boto3" +ln -s "$(pwd)/lib/boto" "$(pwd)/lib/botocore" + +# override boto's import path(s) +export PYTHONPATH +PYTHONPATH="$(pwd)/lib:$PYTHONPATH" + +################################################# +# RUN THE SCRIPT +################################################# + +# run the script first +cat << EOF > "$OUTPUT_DIR/ec2.ini" +[ec2] +regions = us-east-1 +cache_path = $(pwd)/.cache +cache_max_age = 0 +group_by_tag_none = False + +[credentials] +aws_access_key_id = FOO +aws_secret_acccess_key = BAR +EOF + +ANSIBLE_JINJA2_NATIVE=1 ansible-inventory -vvvv -i ./ec2.sh --list --output="$OUTPUT_DIR/script.out" +RC=$? +if [[ $RC != 0 ]]; then + exit $RC +fi + +################################################# +# RUN THE PLUGIN +################################################# + +# run the plugin second +export ANSIBLE_INVENTORY_ENABLED=aws_ec2 +export ANSIBLE_INVENTORY=test.aws_ec2.yml +export AWS_ACCESS_KEY_ID=FOO +export AWS_SECRET_ACCESS_KEY=BAR +export ANSIBLE_TRANSFORM_INVALID_GROUP_CHARS=never + +cat << EOF > "$OUTPUT_DIR/test.aws_ec2.yml" +plugin: aws_ec2 +cache: False +use_contrib_script_compatible_sanitization: True +strict: True +regions: + - us-east-1 +hostnames: + - network-interface.addresses.association.public-ip + - dns-name +filters: + instance-state-name: running +compose: + # vars that don't exist anymore in any meaningful way + ec2_item: undefined | default("") + ec2_monitoring: undefined | default("") + ec2_previous_state: undefined | default("") + ec2_previous_state_code: undefined | default(0) + ec2__in_monitoring_element: undefined | default(false) + # the following three will be accessible again after #53645 + ec2_requester_id: undefined | default("") + ec2_eventsSet: undefined | default("") + ec2_persistent: undefined | default(false) + + # vars that change + ansible_host: public_ip_address + ec2_block_devices: dict(block_device_mappings | map(attribute='device_name') | map('basename') | list | zip(block_device_mappings | map(attribute='ebs.volume_id') | list)) + ec2_dns_name: public_dns_name + ec2_group_name: placement['group_name'] + ec2_id: instance_id + 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_account_id: owner_id + 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_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 | lower | string # butchered snake_case case not a typo. + + # 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: '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 + ec2_spot_instance_request_id: spot_instance_request_id | default("") + ec2_subnet_id: subnet_id + ec2_virtualization_type: virtualization_type + ec2_vpc_id: vpc_id + tags: dict(tags.keys() | map('regex_replace', '[^A-Za-z0-9\_]', '_') | list | zip(tags.values() | list)) + +keyed_groups: + - key: '"ec2"' + separator: "" + - key: 'instance_id' + separator: "" + - key: tags + prefix: tag + - key: key_name | regex_replace('-', '_') + prefix: key + - key: placement['region'] + separator: "" + - key: placement['availability_zone'] + separator: "" + - key: platform | default('undefined') + prefix: platform + - key: vpc_id | regex_replace('-', '_') + prefix: vpc_id + - key: instance_type + prefix: type + - key: "image_id | regex_replace('-', '_')" + separator: "" + - key: security_groups | map(attribute='group_name') | map("regex_replace", "-", "_") | list + prefix: security_group +EOF + +ANSIBLE_JINJA2_NATIVE=1 ansible-inventory -vvvv -i "$OUTPUT_DIR/test.aws_ec2.yml" --list --output="$OUTPUT_DIR/plugin.out" + +################################################# +# DIFF THE RESULTS +################################################# + +./inventory_diff.py "$OUTPUT_DIR/script.out" "$OUTPUT_DIR/plugin.out"