From a0e2cb3d9880026d962d7ed8b25746a482f3cbf5 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 17 Aug 2013 18:11:29 -0400 Subject: [PATCH] Add EC2 spot instances --- moto/ec2/models.py | 85 ++++++++++- moto/ec2/responses/spot_instances.py | 197 +++++++++++++++++++++++++- moto/ec2/utils.py | 28 ++-- tests/test_ec2/test_spot_instances.py | 94 +++++++++++- 4 files changed, 381 insertions(+), 23 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 0ea10555c..2150f2567 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -11,6 +11,7 @@ from .utils import ( random_reservation_id, random_security_group_id, random_snapshot_id, + random_spot_request_id, random_subnet_id, random_volume_id, random_vpc_id, @@ -306,11 +307,12 @@ class SecurityGroupBackend(object): self.groups = {} super(SecurityGroupBackend, self).__init__() - def create_security_group(self, name, description): + def create_security_group(self, name, description, force=False): group_id = random_security_group_id() - existing_group = self.get_security_group_from_name(name) - if existing_group: - return None + if not force: + existing_group = self.get_security_group_from_name(name) + if existing_group: + return None group = SecurityGroup(group_id, name, description) self.groups[group_id] = group return group @@ -333,6 +335,11 @@ class SecurityGroupBackend(object): if group.name == name: return group + if name == 'default': + # If the request is for the default group and it does not exist, create it + default_group = ec2_backend.create_security_group("default", "The default security group", force=True) + return default_group + def authorize_security_group_ingress(self, group_name, ip_protocol, from_port, to_port, ip_ranges=None, source_group_names=None): group = self.get_security_group_from_name(group_name) source_groups = [] @@ -496,9 +503,77 @@ class SubnetBackend(object): return self.subnets.pop(subnet_id, None) +class SpotInstanceRequest(object): + def __init__(self, spot_request_id, price, image_id, type, valid_from, + valid_until, launch_group, availability_zone_group, key_name, + security_groups, user_data, instance_type, placement, kernel_id, + ramdisk_id, monitoring_enabled, subnet_id): + self.id = spot_request_id + self.state = "open" + self.price = price + self.image_id = image_id + self.type = type + self.valid_from = valid_from + self.valid_until = valid_until + self.launch_group = launch_group + self.availability_zone_group = availability_zone_group + self.key_name = key_name + self.user_data = user_data + self.instance_type = instance_type + self.placement = placement + self.kernel_id = kernel_id + self.ramdisk_id = ramdisk_id + self.monitoring_enabled = monitoring_enabled + self.subnet_id = subnet_id + + self.security_groups = [] + if security_groups: + for group_name in security_groups: + group = ec2_backend.get_security_group_from_name(group_name) + if group: + self.security_groups.append(group) + else: + # If not security groups, add the default + default_group = ec2_backend.get_security_group_from_name("default") + self.security_groups.append(default_group) + + +class SpotRequestBackend(object): + def __init__(self): + self.spot_instance_requests = {} + super(SpotRequestBackend, self).__init__() + + def request_spot_instances(self, price, image_id, count, type, valid_from, + valid_until, launch_group, availability_zone_group, + key_name, security_groups, user_data, + instance_type, placement, kernel_id, ramdisk_id, + monitoring_enabled, subnet_id): + requests = [] + for index in range(count): + spot_request_id = random_spot_request_id() + request = SpotInstanceRequest( + spot_request_id, price, image_id, type, valid_from, valid_until, + launch_group, availability_zone_group, key_name, security_groups, + user_data, instance_type, placement, kernel_id, ramdisk_id, + monitoring_enabled, subnet_id + ) + self.spot_instance_requests[spot_request_id] = request + requests.append(request) + return requests + + def describe_spot_instance_requests(self): + return self.spot_instance_requests.values() + + def cancel_spot_instance_requests(self, request_ids): + requests = [] + for request_id in request_ids: + requests.append(self.spot_instance_requests.pop(request_id)) + return requests + + class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, - VPCBackend, SubnetBackend): + VPCBackend, SubnetBackend, SpotRequestBackend): pass diff --git a/moto/ec2/responses/spot_instances.py b/moto/ec2/responses/spot_instances.py index 0ace72dfd..759914989 100644 --- a/moto/ec2/responses/spot_instances.py +++ b/moto/ec2/responses/spot_instances.py @@ -1,12 +1,25 @@ from jinja2 import Template from moto.ec2.models import ec2_backend -from moto.ec2.utils import resource_ids_from_querystring class SpotInstances(object): + def _get_param(self, param_name): + return self.querystring.get(param_name, [None])[0] + + def _get_int_param(self, param_name): + value = self._get_param(param_name) + if value is not None: + return int(value) + + def _get_multi_param(self, param_prefix): + return [value[0] for key, value in self.querystring.items() if key.startswith(param_prefix)] + def cancel_spot_instance_requests(self): - raise NotImplementedError('SpotInstances.cancel_spot_instance_requests is not yet implemented') + request_ids = self._get_multi_param('SpotInstanceRequestId') + requests = ec2_backend.cancel_spot_instance_requests(request_ids) + template = Template(CANCEL_SPOT_INSTANCES_TEMPLATE) + return template.render(requests=requests) def create_spot_datafeed_subscription(self): raise NotImplementedError('SpotInstances.create_spot_datafeed_subscription is not yet implemented') @@ -18,10 +31,186 @@ class SpotInstances(object): raise NotImplementedError('SpotInstances.describe_spot_datafeed_subscription is not yet implemented') def describe_spot_instance_requests(self): - raise NotImplementedError('SpotInstances.describe_spot_instance_requests is not yet implemented') + requests = ec2_backend.describe_spot_instance_requests() + template = Template(DESCRIBE_SPOT_INSTANCES_TEMPLATE) + return template.render(requests=requests) def describe_spot_price_history(self): raise NotImplementedError('SpotInstances.describe_spot_price_history is not yet implemented') def request_spot_instances(self): - raise NotImplementedError('SpotInstances.request_spot_instances is not yet implemented') + price = self._get_param('SpotPrice') + image_id = self._get_param('LaunchSpecification.ImageId') + count = self._get_int_param('InstanceCount') + type = self._get_param('Type') + valid_from = self._get_param('ValidFrom') + valid_until = self._get_param('ValidUntil') + launch_group = self._get_param('LaunchGroup') + availability_zone_group = self._get_param('AvailabilityZoneGroup') + key_name = self._get_param('LaunchSpecification.KeyName') + security_groups = self._get_multi_param('LaunchSpecification.SecurityGroup.') + user_data = self._get_param('LaunchSpecification.UserData') + instance_type = self._get_param('LaunchSpecification.InstanceType') + placement = self._get_param('LaunchSpecification.Placement.AvailabilityZone') + kernel_id = self._get_param('LaunchSpecification.KernelId') + ramdisk_id = self._get_param('LaunchSpecification.RamdiskId') + monitoring_enabled = self._get_param('LaunchSpecification.Monitoring.Enabled') + subnet_id = self._get_param('LaunchSpecification.SubnetId') + + requests = ec2_backend.request_spot_instances( + price=price, + image_id=image_id, + count=count, + type=type, + valid_from=valid_from, + valid_until=valid_until, + launch_group=launch_group, + availability_zone_group=availability_zone_group, + key_name=key_name, + security_groups=security_groups, + user_data=user_data, + instance_type=instance_type, + placement=placement, + kernel_id=kernel_id, + ramdisk_id=ramdisk_id, + monitoring_enabled=monitoring_enabled, + subnet_id=subnet_id, + ) + + template = Template(REQUEST_SPOT_INSTANCES_TEMPLATE) + return template.render(requests=requests) + + +REQUEST_SPOT_INSTANCES_TEMPLATE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for request in requests %} + + {{ request.price }} + {{ request.price }} + {{ request.type }} + {{ request.state }} + + pending-evaluation + YYYY-MM-DDTHH:MM:SS.000Z + Your Spot request has been submitted for review, and is pending evaluation. + + {{ request.availability_zone_group }} + + {{ request.image_id }} + {{ request.key_name }} + + {% for group in request.security_groups %} + + {{ group.id }} + {{ group.name }} + + {% endfor %} + + {{ request.kernel_id }} + {{ request.ramdisk_id }} + {{ request.subnet_id }} + {{ request.instance_type }} + + + {{ request.monitoring_enabled }} + + {{ request.ebs_optimized }} + + {{ request.placement }} + + + + {{ request.launch_group }} + YYYY-MM-DDTHH:MM:SS.000Z + {% if request.valid_from %} + {{ request.valid_from }} + {% endif %} + {% if request.valid_until %} + {{ request.valid_until }} + {% endif %} + Linux/UNIX + + {% endfor %} + +""" + +DESCRIBE_SPOT_INSTANCES_TEMPLATE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for request in requests %} + + {{ request.id }} + {{ request.price }} + {{ request.type }} + {{ request.state }} + + pending-evaluation + YYYY-MM-DDTHH:MM:SS.000Z + Your Spot request has been submitted for review, and is pending evaluation. + + {% if request.availability_zone_group %} + {{ request.availability_zone_group }} + {% endif %} + + {{ request.image_id }} + {% if request.key_name %} + {{ request.key_name }} + {% endif %} + + {% for group in request.security_groups %} + + {{ group.id }} + {{ group.name }} + + {% endfor %} + + {% if request.kernel_id %} + {{ request.kernel_id }} + {% endif %} + {% if request.ramdisk_id %} + {{ request.ramdisk_id }} + {% endif %} + {% if request.subnet_id %} + {{ request.subnet_id }} + {% endif %} + {{ request.instance_type }} + + + {{ request.monitoring_enabled }} + + {{ request.ebs_optimized }} + {% if request.placement %} + + {{ request.placement }} + + + {% endif %} + + {% if request.launch_group %} + {{ request.launch_group }} + {% endif %} + YYYY-MM-DDTHH:MM:SS.000Z + {% if request.valid_from %} + {{ request.valid_from }} + {% endif %} + {% if request.valid_until %} + {{ request.valid_until }} + {% endif %} + Linux/UNIX + + {% endfor %} + +""" + +CANCEL_SPOT_INSTANCES_TEMPLATE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for request in requests %} + + {{ request.id }} + cancelled + + {% endfor %} + +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 7dfa3ea03..a86ed64c5 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -10,6 +10,10 @@ def random_id(prefix=''): return '{}-{}'.format(prefix, instance_tag) +def random_ami_id(): + return random_id(prefix='ami') + + def random_instance_id(): return random_id(prefix='i') @@ -18,14 +22,22 @@ def random_reservation_id(): return random_id(prefix='r') -def random_ami_id(): - return random_id(prefix='ami') - - def random_security_group_id(): return random_id(prefix='sg') +def random_snapshot_id(): + return random_id(prefix='snap') + + +def random_spot_request_id(): + return random_id(prefix='sir') + + +def random_subnet_id(): + return random_id(prefix='subnet') + + def random_volume_id(): return random_id(prefix='vol') @@ -34,14 +46,6 @@ def random_vpc_id(): return random_id(prefix='vpc') -def random_subnet_id(): - return random_id(prefix='subnet') - - -def random_snapshot_id(): - return random_id(prefix='snap') - - def instance_ids_from_querystring(querystring_dict): instance_ids = [] for key, value in querystring_dict.iteritems(): diff --git a/tests/test_ec2/test_spot_instances.py b/tests/test_ec2/test_spot_instances.py index 0a08e243d..91a3158eb 100644 --- a/tests/test_ec2/test_spot_instances.py +++ b/tests/test_ec2/test_spot_instances.py @@ -1,9 +1,99 @@ +import datetime + import boto import sure # noqa from moto import mock_ec2 +from moto.core.utils import iso_8601_datetime @mock_ec2 -def test_spot_instances(): - pass +def test_request_spot_instances(): + conn = boto.connect_ec2() + + conn.create_security_group('group1', 'description') + conn.create_security_group('group2', 'description') + + start = iso_8601_datetime(datetime.datetime(2013, 1, 1)) + end = iso_8601_datetime(datetime.datetime(2013, 1, 2)) + + request = conn.request_spot_instances( + price=0.5, image_id='ami-abcd1234', count=1, type='one-time', + valid_from=start, valid_until=end, launch_group="the-group", + availability_zone_group='my-group', key_name="test", + security_groups=['group1', 'group2'], user_data="some test data", + instance_type='m1.small', placement='us-east-1c', + kernel_id="test-kernel", ramdisk_id="test-ramdisk", + monitoring_enabled=True, subnet_id="subnet123", + ) + + requests = conn.get_all_spot_instance_requests() + requests.should.have.length_of(1) + request = requests[0] + + request.state.should.equal("open") + request.price.should.equal(0.5) + request.launch_specification.image_id.should.equal('ami-abcd1234') + request.type.should.equal('one-time') + request.valid_from.should.equal(start) + request.valid_until.should.equal(end) + request.launch_group.should.equal("the-group") + request.availability_zone_group.should.equal('my-group') + request.launch_specification.key_name.should.equal("test") + security_group_names = [group.name for group in request.launch_specification.groups] + set(security_group_names).should.equal(set(['group1', 'group2'])) + request.launch_specification.instance_type.should.equal('m1.small') + request.launch_specification.placement.should.equal('us-east-1c') + request.launch_specification.kernel.should.equal("test-kernel") + request.launch_specification.ramdisk.should.equal("test-ramdisk") + request.launch_specification.subnet_id.should.equal("subnet123") + + +@mock_ec2 +def test_request_spot_instances_default_arguments(): + """ + Test that moto set the correct default arguments + """ + conn = boto.connect_ec2() + + request = conn.request_spot_instances( + price=0.5, image_id='ami-abcd1234', + ) + + requests = conn.get_all_spot_instance_requests() + requests.should.have.length_of(1) + request = requests[0] + + request.state.should.equal("open") + request.price.should.equal(0.5) + request.launch_specification.image_id.should.equal('ami-abcd1234') + request.type.should.equal('one-time') + request.valid_from.should.equal(None) + request.valid_until.should.equal(None) + request.launch_group.should.equal(None) + request.availability_zone_group.should.equal(None) + request.launch_specification.key_name.should.equal(None) + security_group_names = [group.name for group in request.launch_specification.groups] + security_group_names.should.equal(["default"]) + request.launch_specification.instance_type.should.equal('m1.small') + request.launch_specification.placement.should.equal(None) + request.launch_specification.kernel.should.equal(None) + request.launch_specification.ramdisk.should.equal(None) + request.launch_specification.subnet_id.should.equal(None) + + +@mock_ec2 +def test_cancel_spot_instance_request(): + conn = boto.connect_ec2() + + conn.request_spot_instances( + price=0.5, image_id='ami-abcd1234', + ) + + requests = conn.get_all_spot_instance_requests() + requests.should.have.length_of(1) + + conn.cancel_spot_instance_requests([requests[0].id]) + + requests = conn.get_all_spot_instance_requests() + requests.should.have.length_of(0)