|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
import errno
|
|
|
|
import os
|
|
|
|
import time
|
|
|
|
import mock
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
boto3 = pytest.importorskip("boto3")
|
|
|
|
botocore = pytest.importorskip("botocore")
|
|
|
|
placebo = pytest.importorskip("placebo")
|
|
|
|
|
|
|
|
"""
|
|
|
|
Using Placebo to test modules using boto3:
|
|
|
|
|
|
|
|
This is an example test, using the placeboify fixture to test that a module
|
|
|
|
will fail if resources it depends on don't exist.
|
|
|
|
|
|
|
|
> from placebo_fixtures import placeboify, scratch_vpc
|
|
|
|
>
|
|
|
|
> def test_create_with_nonexistent_launch_config(placeboify):
|
|
|
|
> connection = placeboify.client('autoscaling')
|
|
|
|
> module = FakeModule('test-asg-created', None, min_size=0, max_size=0, desired_capacity=0)
|
|
|
|
> with pytest.raises(FailJSON) as excinfo:
|
|
|
|
> asg_module.create_autoscaling_group(connection, module)
|
|
|
|
> .... asserts based on module state/exceptions ....
|
|
|
|
|
|
|
|
In more advanced cases, use unrecorded resource fixtures to fill in ARNs/IDs of
|
|
|
|
things modules depend on, such as:
|
|
|
|
|
|
|
|
> def test_create_in_vpc(placeboify, scratch_vpc):
|
|
|
|
> connection = placeboify.client('autoscaling')
|
|
|
|
> module = FakeModule(name='test-asg-created',
|
|
|
|
> min_size=0, max_size=0, desired_capacity=0,
|
|
|
|
> availability_zones=[s['az'] for s in scratch_vpc['subnets']],
|
|
|
|
> vpc_zone_identifier=[s['id'] for s in scratch_vpc['subnets']],
|
|
|
|
> )
|
|
|
|
> ..... so on and so forth ....
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
def placeboify(request, monkeypatch):
|
|
|
|
"""This fixture puts a recording/replaying harness around `boto3_conn`
|
|
|
|
|
|
|
|
Placeboify patches the `boto3_conn` function in ec2 module_utils to return
|
|
|
|
a boto3 session that in recording or replaying mode, depending on the
|
|
|
|
PLACEBO_RECORD environment variable. Unset PLACEBO_RECORD (the common case
|
|
|
|
for just running tests) will put placebo in replay mode, set PLACEBO_RECORD
|
|
|
|
to any value to turn off replay & operate on real AWS resources.
|
|
|
|
|
|
|
|
The recorded sessions are stored in the test file's directory, under the
|
|
|
|
namespace `placebo_recordings/{testfile name}/{test function name}` to
|
|
|
|
distinguish them.
|
|
|
|
"""
|
|
|
|
session = boto3.Session(region_name='us-west-2')
|
|
|
|
|
|
|
|
recordings_path = os.path.join(
|
|
|
|
request.fspath.dirname,
|
|
|
|
'placebo_recordings',
|
|
|
|
request.fspath.basename.replace('.py', ''),
|
|
|
|
request.function.__name__
|
|
|
|
# remove the test_ prefix from the function & file name
|
|
|
|
).replace('test_', '')
|
|
|
|
|
|
|
|
if not os.getenv('PLACEBO_RECORD'):
|
|
|
|
if not os.path.isdir(recordings_path):
|
|
|
|
raise NotImplementedError('Missing Placebo recordings in directory: %s' % recordings_path)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
# make sure the directory for placebo test recordings is available
|
|
|
|
os.makedirs(recordings_path)
|
|
|
|
except OSError as e:
|
|
|
|
if e.errno != errno.EEXIST:
|
|
|
|
raise
|
|
|
|
|
|
|
|
pill = placebo.attach(session, data_path=recordings_path)
|
|
|
|
if os.getenv('PLACEBO_RECORD'):
|
|
|
|
pill.record()
|
|
|
|
else:
|
|
|
|
pill.playback()
|
|
|
|
|
|
|
|
def boto3_middleman_connection(module, conn_type, resource, region='us-west-2', **kwargs):
|
|
|
|
if conn_type != 'client':
|
|
|
|
# TODO support resource-based connections
|
|
|
|
raise ValueError('Mocker only supports client, not %s' % conn_type)
|
|
|
|
return session.client(resource, region_name=region)
|
|
|
|
|
|
|
|
import ansible.module_utils.ec2
|
|
|
|
monkeypatch.setattr(
|
|
|
|
ansible.module_utils.ec2,
|
|
|
|
'boto3_conn',
|
|
|
|
boto3_middleman_connection,
|
|
|
|
)
|
|
|
|
yield session
|
|
|
|
|
|
|
|
# tear down
|
|
|
|
pill.stop()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
|
|
def basic_launch_config():
|
|
|
|
"""Create an EC2 launch config whose creation *is not* recorded and return its name
|
|
|
|
|
|
|
|
This fixture is module-scoped, since launch configs are immutable and this
|
|
|
|
can be reused for many tests.
|
|
|
|
"""
|
|
|
|
if not os.getenv('PLACEBO_RECORD'):
|
|
|
|
yield 'pytest_basic_lc'
|
|
|
|
return
|
|
|
|
|
|
|
|
# use a *non recording* session to make the launch config
|
|
|
|
# since that's a prereq of the ec2_asg module, and isn't what
|
|
|
|
# we're testing.
|
|
|
|
asg = boto3.client('autoscaling')
|
|
|
|
asg.create_launch_configuration(
|
|
|
|
LaunchConfigurationName='pytest_basic_lc',
|
|
|
|
ImageId='ami-9be6f38c', # Amazon Linux 2016.09 us-east-1 AMI, can be any valid AMI
|
|
|
|
SecurityGroups=[],
|
|
|
|
UserData='#!/bin/bash\necho hello world',
|
|
|
|
InstanceType='t2.micro',
|
|
|
|
InstanceMonitoring={'Enabled': False},
|
|
|
|
AssociatePublicIpAddress=True
|
|
|
|
)
|
|
|
|
|
|
|
|
yield 'pytest_basic_lc'
|
|
|
|
|
|
|
|
try:
|
|
|
|
asg.delete_launch_configuration(LaunchConfigurationName='pytest_basic_lc')
|
|
|
|
except botocore.exceptions.ClientError as e:
|
|
|
|
if 'not found' in e.message:
|
|
|
|
return
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
|
|
def scratch_vpc():
|
|
|
|
if not os.getenv('PLACEBO_RECORD'):
|
|
|
|
yield {
|
|
|
|
'vpc_id': 'vpc-123456',
|
|
|
|
'cidr_range': '10.0.0.0/16',
|
|
|
|
'subnets': [
|
|
|
|
{
|
|
|
|
'id': 'subnet-123456',
|
|
|
|
'az': 'us-east-1d',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'id': 'subnet-654321',
|
|
|
|
'az': 'us-east-1e',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
return
|
|
|
|
|
|
|
|
# use a *non recording* session to make the base VPC and subnets
|
|
|
|
ec2 = boto3.client('ec2')
|
|
|
|
vpc_resp = ec2.create_vpc(
|
|
|
|
CidrBlock='10.0.0.0/16',
|
|
|
|
AmazonProvidedIpv6CidrBlock=False,
|
|
|
|
)
|
|
|
|
subnets = (
|
|
|
|
ec2.create_subnet(
|
|
|
|
VpcId=vpc_resp['Vpc']['VpcId'],
|
|
|
|
CidrBlock='10.0.0.0/24',
|
|
|
|
),
|
|
|
|
ec2.create_subnet(
|
|
|
|
VpcId=vpc_resp['Vpc']['VpcId'],
|
|
|
|
CidrBlock='10.0.1.0/24',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
time.sleep(3)
|
|
|
|
|
|
|
|
yield {
|
|
|
|
'vpc_id': vpc_resp['Vpc']['VpcId'],
|
|
|
|
'cidr_range': '10.0.0.0/16',
|
|
|
|
'subnets': [
|
|
|
|
{
|
|
|
|
'id': s['Subnet']['SubnetId'],
|
|
|
|
'az': s['Subnet']['AvailabilityZone'],
|
|
|
|
} for s in subnets
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
for s in subnets:
|
|
|
|
try:
|
|
|
|
ec2.delete_subnet(SubnetId=s['Subnet']['SubnetId'])
|
|
|
|
except botocore.exceptions.ClientError as e:
|
|
|
|
if 'not found' in e.message:
|
|
|
|
continue
|
|
|
|
raise
|
|
|
|
ec2.delete_vpc(VpcId=vpc_resp['Vpc']['VpcId'])
|
|
|
|
except botocore.exceptions.ClientError as e:
|
|
|
|
if 'not found' in e.message:
|
|
|
|
return
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope='module')
|
|
|
|
def maybe_sleep():
|
|
|
|
"""If placebo is reading saved sessions, make sleep always take 0 seconds.
|
|
|
|
|
|
|
|
AWS modules often perform polling or retries, but when using recorded
|
|
|
|
sessions there's no reason to wait. We can still exercise retry and other
|
|
|
|
code paths without waiting for wall-clock time to pass."""
|
|
|
|
if not os.getenv('PLACEBO_RECORD'):
|
|
|
|
p = mock.patch('time.sleep', return_value=None)
|
|
|
|
p.start()
|
|
|
|
yield
|
|
|
|
p.stop()
|
|
|
|
else:
|
|
|
|
yield
|