From 036f31bad735410cebfd13f41e90a9f4f4249a28 Mon Sep 17 00:00:00 2001 From: Andy Brody Date: Tue, 28 Nov 2017 00:34:30 -0500 Subject: [PATCH 1/2] Verify that EIP was actually associated. Add functionality to check that an EIP was actually associated with the desired instance. This is important because the EC2 API is eventually consistent and receiving an HTTP 200 response from the association API call is not a guarantee that the association will actually happen. This verification is enabled by default but can be disabled with --skip-verify. By default, it will retry up to 10 times, sleeping 0.1 seconds after each attempt, in order to give the EC2 API plenty of time to achieve a result. This allows aws-ec2-assign-elastic-ip to exit nonzero when there is a race condition that causes EIP association to fail. Fixes: #24 --- aws_ec2_assign_elastic_ip/__init__.py | 58 +++++++++++++++++++ .../command_line_options.py | 4 ++ 2 files changed, 62 insertions(+) diff --git a/aws_ec2_assign_elastic_ip/__init__.py b/aws_ec2_assign_elastic_ip/__init__.py index 3e46357..757422a 100644 --- a/aws_ec2_assign_elastic_ip/__init__.py +++ b/aws_ec2_assign_elastic_ip/__init__.py @@ -2,6 +2,7 @@ import logging import logging.config import sys +import time if sys.platform in ['win32', 'cygwin']: import ntpath as ospath @@ -68,6 +69,9 @@ def main(): logger.info('Would assign IP {0}'.format(address.public_ip)) else: _assign_address(instance_id, address) + if args.verify: + if not _verify_associated(instance_id, address): + sys.exit(1) def _assign_address(instance_id, address): @@ -102,6 +106,60 @@ def _assign_address(instance_id, address): logger.info('Successfully associated Elastic IP {0} with {1}'.format( address.public_ip, instance_id)) +def _verify_associated(instance_id, address, retries=10, retry_interval=0.1): + """ + Check that address is actually associated with instance_id by querying + the EC2 API. + + :type instance_id: str + :param instance_id: Instance ID + :type address: boto.ec2.address + :param address: Elastic IP address + :type retries: int + :param retries: Number of times to retry failures + :type retry_interval: float + :param retry_interval: Sleep interval between retries + :returns: bool -- True if EIP is actually associated + """ + + logger.debug('Verifying that {0} is associated with {1}'.format( + instance_id, address.public_ip)) + + actual = connection.get_all_addresses(addresses=[address.public_ip])[0] + + if actual.instance_id == instance_id: + logger.info( + 'Verified that {0} is really associated with {1} ({2}/{3})'.format( + actual.public_ip, instance_id, actual.network_interface_id, + actual.association_id)) + return True + + if actual.instance_id: + logger.error(('Somehow {0!r} is associated with {1!r},' + + ' not expected instance {2!r}').format(address.public_ip, + actual.instance_id, + instance_id)) + return False + + if actual.association_id: + logger.warning( + 'Elastic IP {0} is already associated with {1!r}'.format( + actual.public_ip, actual.network_interface_id)) + else: + logger.info('Elastic IP {0} is not yet associated'.format( + address.public_ip)) + + if retries > 0: + logger.info('Retrying verification...') + time.sleep(retry_interval) + return _verify_associated(instance_id=instance_id, address=address, + retries=retries - 1, + retry_interval=retry_interval) + + logger.error( + 'Ran out of retries verifying association of {0!r} to {1!r}'.format( + address.public_ip, instance_id)) + return False def _get_unassociated_address(): """ Return the first unassociated EIP we can find diff --git a/aws_ec2_assign_elastic_ip/command_line_options.py b/aws_ec2_assign_elastic_ip/command_line_options.py index a5a16d5..3b22771 100644 --- a/aws_ec2_assign_elastic_ip/command_line_options.py +++ b/aws_ec2_assign_elastic_ip/command_line_options.py @@ -39,6 +39,10 @@ help=( 'Turn on dry run mode. No address will be assigned,\n' 'we will only print which we whould take')) +PARSER.add_argument( + '--skip-verify', + help='Skip verification that the EIP was successfully associated', + action='store_false', default=True, dest='verify') PARSER.add_argument( '--valid-ips', help=( From f628f8bc88196629112dbc2ab574c511f7a093e2 Mon Sep 17 00:00:00 2001 From: Andy Brody Date: Sat, 2 Dec 2017 13:40:52 -0500 Subject: [PATCH 2/2] Remove unnecessary default=True. --- aws_ec2_assign_elastic_ip/command_line_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws_ec2_assign_elastic_ip/command_line_options.py b/aws_ec2_assign_elastic_ip/command_line_options.py index 3b22771..b3a89d0 100644 --- a/aws_ec2_assign_elastic_ip/command_line_options.py +++ b/aws_ec2_assign_elastic_ip/command_line_options.py @@ -42,7 +42,7 @@ PARSER.add_argument( '--skip-verify', help='Skip verification that the EIP was successfully associated', - action='store_false', default=True, dest='verify') + action='store_false', dest='verify') PARSER.add_argument( '--valid-ips', help=(