summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbuildscripts/aws_ec2.py288
-rwxr-xr-xbuildscripts/tests/test_aws_ec2.py276
2 files changed, 564 insertions, 0 deletions
diff --git a/buildscripts/aws_ec2.py b/buildscripts/aws_ec2.py
new file mode 100755
index 00000000000..788d5de65b8
--- /dev/null
+++ b/buildscripts/aws_ec2.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python
+
+"""AWS EC2 instance launcher and controller."""
+
+from __future__ import print_function
+
+import collections
+import datetime
+import optparse
+import sys
+import time
+
+import boto3
+import botocore
+import yaml
+
+_MODES = ("status", "create", "start", "stop", "force-stop", "reboot", "terminate")
+
+
+class AwsEc2(object):
+ """Class to support controlling AWS EC2 istances."""
+
+ InstanceStatus = collections.namedtuple(
+ "InstanceStatus",
+ "instance_id image_id instance_type state private_ip_address public_ip_address tags")
+
+ def __init__(self):
+ try:
+ self.connection = boto3.resource("ec2")
+ except botocore.exceptions.BotoCoreError:
+ print("Please configure your Boto environment variables or files, see"
+ " http://boto3.readthedocs.io/en/latest/guide/configuration.html"
+ " for the variable names, file names and precedence order.")
+ raise
+
+ def control_instance(self, mode, image_id):
+ """Controls an AMI instance. Returns 0 & status information, if successful."""
+ if mode not in _MODES:
+ raise ValueError(
+ "Invalid mode '{}' specified, choose from {}.".format(mode, _MODES))
+
+ instance = self.connection.Instance(image_id)
+ try:
+ if mode == "start":
+ instance.start()
+ elif mode == "stop":
+ instance.stop()
+ elif mode == "force-stop":
+ instance.stop(Force=True)
+ elif mode == "terminate":
+ instance.terminate()
+ elif mode == "reboot":
+ instance.reboot()
+ except botocore.exceptions.ClientError as e:
+ return 1, e.message
+
+ try:
+ # Always provide status after executing command.
+ status = self.InstanceStatus(
+ getattr(instance, "instance_id", None),
+ getattr(instance, "image_id", None),
+ getattr(instance, "instance_type", None),
+ getattr(instance, "state", None),
+ getattr(instance, "private_ip_address", None),
+ getattr(instance, "public_ip_address", None),
+ getattr(instance, "tags", None))
+ except botocore.exceptions.ClientError as e:
+ return 1, e.message
+
+ return 0, status
+
+ def tag_instance(self, image_id, tags):
+ """Tags an AMI instance. """
+ if tags:
+ # It's possible that ClientError code InvalidInstanceID.NotFound could be returned,
+ # even if the 'image_id' exists. We will retry up to 5 times, with increasing wait,
+ # if this occurs.
+ # http://docs.aws.amazon.com/AWSEC2/latest/APIReference/query-api-troubleshooting.html
+ for i in range(5):
+ try:
+ instance = self.connection.Instance(image_id)
+ break
+ except botocore.exceptions.ClientError as e:
+ if e.response["Error"]["Code"] != "InvalidInstanceID.NotFound":
+ raise
+ time.sleep(i + 1)
+ instance.create_tags(Tags=tags)
+
+ def launch_instance(self,
+ ami,
+ instance_type,
+ block_devices=None,
+ key_name=None,
+ security_groups=None,
+ tags=None,
+ wait_time_secs=0,
+ show_progress=False,
+ **kwargs):
+ """Launches and tags an AMI instance.
+
+ Returns the tuple (0, status_information), if successful."""
+
+ bdms = []
+ if block_devices is None:
+ block_devices = {}
+ for block_device in block_devices:
+ bdm = {}
+ bdm["DeviceName"] = block_device
+ bdm["Ebs"] = {"DeleteOnTermination": True, "VolumeSize": block_devices[block_device]}
+ bdms.append(bdm)
+ if bdms:
+ kwargs["BlockDeviceMappings"] = bdms
+ if security_groups:
+ kwargs["SecurityGroups"] = security_groups
+ if key_name:
+ kwargs["KeyName"] = key_name
+
+ try:
+ instances = self.connection.create_instances(
+ ImageId=ami,
+ InstanceType=instance_type,
+ MaxCount=1,
+ MinCount=1,
+ **kwargs)
+ except (botocore.exceptions.ClientError, botocore.exceptions.ParamValidationError) as e:
+ return 1, e.message
+
+ instance = instances[0]
+
+ if wait_time_secs:
+ # Wait up to 'wait_time_secs' for instance to be 'running'.
+ end_time = time.time() + wait_time_secs
+ if show_progress:
+ print("Waiting for instance {} ".format(instance), end="", file=sys.stdout)
+ while time.time() < end_time:
+ if show_progress:
+ print(".", end="", file=sys.stdout)
+ sys.stdout.flush()
+ time.sleep(5)
+ instance.load()
+ if instance.state["Name"] == "running":
+ if show_progress:
+ print(" Instance running!", file=sys.stdout)
+ sys.stdout.flush()
+ break
+
+ self.tag_instance(instance.instance_id, tags)
+
+ return self.control_instance("status", instance.instance_id)
+
+
+def main():
+
+ required_create_options = ["ami", "key_name"]
+
+ parser = optparse.OptionParser(description=__doc__)
+ control_options = optparse.OptionGroup(parser, "Control options")
+ create_options = optparse.OptionGroup(parser, "Create options")
+
+ parser.add_option("--mode",
+ dest="mode",
+ choices=_MODES,
+ default="status",
+ help="Operations to perform on an EC2 instance, choose one of"
+ " '{}', defaults to '%default'.".format(", ".join(_MODES)))
+
+ control_options.add_option("--imageId",
+ dest="image_id",
+ default=None,
+ help="EC2 image_id to perform operation on [REQUIRED for control].")
+
+ create_options.add_option("--ami",
+ dest="ami",
+ default=None,
+ help="EC2 AMI to launch [REQUIRED for create].")
+
+ create_options.add_option("--blockDevice",
+ dest="block_devices",
+ metavar="DEVICE-NAME DEVICE-SIZE-GB",
+ action="append",
+ default=[],
+ nargs=2,
+ help="EBS device name and volume size in GiB."
+ " More than one device can be attached, by specifying"
+ " this option more than once."
+ " The device will be deleted on termination of the instance.")
+
+ create_options.add_option("--instanceType",
+ dest="instance_type",
+ default="t1.micro",
+ help="EC2 instance type to launch, defaults to '%default'.")
+
+ create_options.add_option("--keyName",
+ dest="key_name",
+ default=None,
+ help="EC2 key name [REQUIRED for create].")
+
+ create_options.add_option("--securityGroup",
+ dest="security_groups",
+ action="append",
+ default=[],
+ help="EC2 security group. More than one security group can be added,"
+ " by specifying this option more than once.")
+
+ create_options.add_option("--tagExpireHours",
+ dest="tag_expire_hours",
+ type=int,
+ default=2,
+ help="EC2 tag expire time in hours, defaults to '%default'.")
+
+ create_options.add_option("--tagName",
+ dest="tag_name",
+ default="",
+ help="EC2 tag and instance name.")
+
+ create_options.add_option("--tagOwner",
+ dest="tag_owner",
+ default="",
+ help="EC2 tag owner.")
+
+ create_options.add_option("--extraArgs",
+ dest="extra_args",
+ metavar="{key1: value1, key2: value2, ..., keyN: valueN}",
+ default=None,
+ help="EC2 create instance keyword args. The argument is specified as"
+ " bracketed YAML - i.e. JSON with support for single quoted"
+ " and unquoted keys. Example, '{DryRun: True}'")
+
+ parser.add_option_group(control_options)
+ parser.add_option_group(create_options)
+
+ (options, args) = parser.parse_args()
+
+ aws_ec2 = AwsEc2()
+
+ if options.mode == "create":
+ for option in required_create_options:
+ if not getattr(options, option, None):
+ parser.print_help()
+ parser.error("Missing required create option")
+
+ block_devices = {}
+ for (device_name, device_size) in options.block_devices:
+ try:
+ device_size = int(device_size)
+ except ValueError:
+ parser.print_help()
+ parser.error("Block size must be an integer")
+ block_devices[device_name] = device_size
+
+ # The 'expire-on' key is a UTC time.
+ expire_dt = datetime.datetime.utcnow() + datetime.timedelta(hours=options.tag_expire_hours)
+ tags = [{"Key": "expire-on", "Value": expire_dt.strftime("%Y-%m-%d %H:%M:%S")},
+ {"Key": "Name", "Value": options.tag_name},
+ {"Key": "owner", "Value": options.tag_owner}]
+
+ my_kwargs = {}
+ if options.extra_args is not None:
+ my_kwargs = yaml.safe_load(options.extra_args)
+
+ (ret_code, instance_status) = aws_ec2.launch_instance(
+ ami=options.ami,
+ instance_type=options.instance_type,
+ block_devices=block_devices,
+ key_name=options.key_name,
+ security_groups=options.security_groups,
+ tags=tags,
+ wait_time_secs=60,
+ show_progress=True,
+ **my_kwargs)
+ else:
+ if not getattr(options, "image_id", None):
+ parser.print_help()
+ parser.error("Missing required control option")
+
+ (ret_code, instance_status) = aws_ec2.control_instance(options.mode, options.image_id)
+
+ print("Return code: {}, Instance status:".format(ret_code))
+ if ret_code:
+ print(instance_status)
+ else:
+ for field in instance_status._fields:
+ print("\t{}: {}".format(field, getattr(instance_status, field)))
+
+ sys.exit(ret_code)
+
+if __name__ == "__main__":
+ main()
diff --git a/buildscripts/tests/test_aws_ec2.py b/buildscripts/tests/test_aws_ec2.py
new file mode 100755
index 00000000000..40f433392f2
--- /dev/null
+++ b/buildscripts/tests/test_aws_ec2.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+
+"""Unit test for buildscripts/aws_ec2.py."""
+
+import datetime
+import os
+import sys
+import unittest
+
+if __name__ == "__main__" and __package__ is None:
+ sys.path.append(os.getcwd())
+from buildscripts import aws_ec2
+
+_AMI = "ami-ed6bec86"
+_INSTANCE_TYPE = "t1.micro"
+
+
+class AwsEc2TestCase(unittest.TestCase):
+ def setUp(self):
+ self.aws_ec2 = aws_ec2.AwsEc2()
+ self.launched_instances = []
+ self.ami = _AMI
+ self.instance_type = _INSTANCE_TYPE
+ self.key_name = None
+ self.security_groups = None
+ self.expire_dt = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
+ self.tags = [{"Key": "expire-on", "Value": self.expire_dt.strftime("%Y-%m-%d %H:%M:%S")},
+ {"Key": "Name", "Value": "Unittest AWS EC2 Launcher"},
+ {"Key": "owner", "Value": ""}]
+
+ def tearDown(self):
+ for instance in self.launched_instances:
+ self.aws_ec2.control_instance(mode="terminate", image_id=instance)
+
+
+class AwsEc2Connect(AwsEc2TestCase):
+ def runTest(self):
+ self.assertIsNotNone(self.aws_ec2)
+
+
+class AwsEc2Launch(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ block_devices={"xvde": 5, "xvdf": 10},
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags,
+ DryRun=True)
+ self.assertEqual(1, code, ret)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags,
+ InvalidParam=True)
+ self.assertEqual(1, code, ret)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami="ami-bad_ami",
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertNotEqual(0, code, ret)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type="bad_instance_type",
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertNotEqual(0, code, ret)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name="bad_key_name",
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertNotEqual(0, code, ret)
+
+
+class AwsEc2LaunchStatus(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+ self.assertEqual(self.ami, ret.image_id, ret)
+ self.assertEqual(self.instance_type, ret.instance_type, ret)
+ self.assertTrue(ret.state["Name"] in ["pending", "running"], ret)
+ for tag in ret.tags:
+ self.assertTrue(tag in self.tags, ret)
+ for tag in self.tags:
+ self.assertTrue(tag in ret.tags, ret)
+
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags,
+ wait_time_secs=300,
+ show_progress=True)
+ self.assertEqual("running", ret.state["Name"], ret)
+ self.assertIsNotNone(ret.public_ip_address, ret)
+ self.assertIsNotNone(ret.private_ip_address, ret)
+ self.launched_instances.append(ret.instance_id)
+
+
+class AwsEc2ControlStatus(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(
+ mode="status",
+ image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertEqual(self.ami, ret.image_id, ret)
+ self.assertEqual(self.instance_type, ret.instance_type, ret)
+ self.assertTrue(ret.state["Name"] in ["pending", "running"], ret)
+ for tag in ret.tags:
+ self.assertTrue(tag in self.tags, ret)
+ for tag in self.tags:
+ self.assertTrue(tag in ret.tags, ret)
+ self.assertIsNotNone(ret.instance_id, ret)
+ self.assertIsNotNone(ret.image_id, ret)
+ self.assertIsNotNone(ret.instance_type, ret)
+ self.assertIsNotNone(ret.state["Name"], ret)
+ self.assertIsNotNone(ret.tags, ret)
+
+ self.assertRaises(ValueError,
+ self.aws_ec2.control_instance,
+ mode="bad_mode",
+ image_id=ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(mode="status", image_id="bad_id")
+ self.assertNotEqual(0, code, ret)
+ self.assertRegexpMatches(ret, "Invalid", ret)
+
+
+class AwsEc2ControlStart(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(mode="start", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["starting", "running"], ret)
+
+
+class AwsEc2ControlStartReboot(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(mode="start", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["running"], ret)
+
+ code, ret = self.aws_ec2.control_instance(mode="reboot", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["rebooting", "running"], ret)
+
+
+class AwsEc2ControlStop(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags,
+ wait_time_secs=60)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(mode="stop", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["stopping", "stopped"], ret)
+
+ code, ret = self.aws_ec2.control_instance(mode="start", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["pending", "running"], ret)
+
+ code, ret = self.aws_ec2.control_instance(mode="force-stop", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["stopping", "stopped"], ret)
+
+
+class AwsEc2ControlTerminate(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=self.tags)
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ code, ret = self.aws_ec2.control_instance(mode="terminate", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ self.assertTrue(ret.state["Name"] in ["shutting-down", "terminated"], ret)
+
+ code, ret = self.aws_ec2.control_instance(mode="start", image_id=ret.instance_id)
+ self.assertNotEqual(0, code, ret)
+
+
+class AwsEc2TagInstance(AwsEc2TestCase):
+ def runTest(self):
+ code, ret = self.aws_ec2.launch_instance(
+ ami=self.ami,
+ instance_type=self.instance_type,
+ key_name=self.key_name,
+ security_groups=self.security_groups,
+ tags=[])
+ self.assertEqual(0, code, ret)
+ self.launched_instances.append(ret.instance_id)
+
+ self.aws_ec2.tag_instance(image_id=ret.instance_id, tags=self.tags)
+ code, ret = self.aws_ec2.control_instance(mode="status", image_id=ret.instance_id)
+ self.assertEqual(0, code, ret)
+ for tag in ret.tags:
+ self.assertTrue(tag in self.tags, ret)
+ for tag in self.tags:
+ self.assertTrue(tag in ret.tags, ret)
+
+
+if __name__ == "__main__":
+ unittest.main()