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)