EC2 - Spot Requests improvements (#5087)

This commit is contained in:
Bert Blommers 2022-05-02 15:00:06 +00:00 committed by GitHub
parent 1d87b90e75
commit cb4cbd1f5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 43 deletions

View File

@ -99,8 +99,8 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
self.launch_time = utc_date_and_time() self.launch_time = utc_date_and_time()
self.ami_launch_index = kwargs.get("ami_launch_index", 0) self.ami_launch_index = kwargs.get("ami_launch_index", 0)
self.disable_api_termination = kwargs.get("disable_api_termination", False) self.disable_api_termination = kwargs.get("disable_api_termination", False)
self.instance_initiated_shutdown_behavior = kwargs.get( self.instance_initiated_shutdown_behavior = (
"instance_initiated_shutdown_behavior", "stop" kwargs.get("instance_initiated_shutdown_behavior") or "stop"
) )
self.sriov_net_support = "simple" self.sriov_net_support = "simple"
self._spot_fleet_id = kwargs.get("spot_fleet_id", None) self._spot_fleet_id = kwargs.get("spot_fleet_id", None)

View File

@ -38,6 +38,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
subnet_id, subnet_id,
tags, tags,
spot_fleet_id, spot_fleet_id,
instance_interruption_behaviour,
**kwargs, **kwargs,
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -46,12 +47,20 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
self.launch_specification = ls self.launch_specification = ls
self.id = spot_request_id self.id = spot_request_id
self.state = "open" self.state = "open"
self.status = "pending-evaluation"
self.status_message = "Your Spot request has been submitted for review, and is pending evaluation."
if price:
price = float(price)
price = "{0:.6f}".format(price) # round up/down to 6 decimals
self.price = price self.price = price
self.type = spot_instance_type self.type = spot_instance_type
self.valid_from = valid_from self.valid_from = valid_from
self.valid_until = valid_until self.valid_until = valid_until
self.launch_group = launch_group self.launch_group = launch_group
self.availability_zone_group = availability_zone_group self.availability_zone_group = availability_zone_group
self.instance_interruption_behaviour = (
instance_interruption_behaviour or "terminate"
)
self.user_data = user_data # NOT self.user_data = user_data # NOT
ls.kernel = kernel_id ls.kernel = kernel_id
ls.ramdisk = ramdisk_id ls.ramdisk = ramdisk_id
@ -62,7 +71,9 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
ls.monitored = monitoring_enabled ls.monitored = monitoring_enabled
ls.subnet_id = subnet_id ls.subnet_id = subnet_id
self.spot_fleet_id = spot_fleet_id self.spot_fleet_id = spot_fleet_id
self.tags = tags tag_map = tags.get("spot-instances-request", {})
self.add_tags(tag_map)
self.all_tags = tags
if security_groups: if security_groups:
for group_name in security_groups: for group_name in security_groups:
@ -75,6 +86,9 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
ls.groups.append(default_group) ls.groups.append(default_group)
self.instance = self.launch_instance() self.instance = self.launch_instance()
self.state = "active"
self.status = "fulfilled"
self.status_message = ""
def get_filter_value(self, filter_name): def get_filter_value(self, filter_name):
if filter_name == "state": if filter_name == "state":
@ -95,7 +109,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
security_group_names=[], security_group_names=[],
security_group_ids=self.launch_specification.groups, security_group_ids=self.launch_specification.groups,
spot_fleet_id=self.spot_fleet_id, spot_fleet_id=self.spot_fleet_id,
tags=self.tags, tags=self.all_tags,
lifecycle="spot", lifecycle="spot",
) )
instance = reservation.instances[0] instance = reservation.instances[0]
@ -128,6 +142,7 @@ class SpotRequestBackend(object, metaclass=Model):
subnet_id, subnet_id,
tags=None, tags=None,
spot_fleet_id=None, spot_fleet_id=None,
instance_interruption_behaviour=None,
): ):
requests = [] requests = []
tags = tags or {} tags = tags or {}
@ -154,6 +169,7 @@ class SpotRequestBackend(object, metaclass=Model):
subnet_id, subnet_id,
tags, tags,
spot_fleet_id, spot_fleet_id,
instance_interruption_behaviour,
) )
self.spot_instance_requests[spot_request_id] = request self.spot_instance_requests[spot_request_id] = request
requests.append(request) requests.append(request)
@ -216,6 +232,7 @@ class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
allocation_strategy, allocation_strategy,
launch_specs, launch_specs,
launch_template_config, launch_template_config,
instance_interruption_behaviour,
): ):
self.ec2_backend = ec2_backend self.ec2_backend = ec2_backend
@ -224,6 +241,9 @@ class SpotFleetRequest(TaggedEC2Resource, CloudFormationModel):
self.target_capacity = int(target_capacity) self.target_capacity = int(target_capacity)
self.iam_fleet_role = iam_fleet_role self.iam_fleet_role = iam_fleet_role
self.allocation_strategy = allocation_strategy self.allocation_strategy = allocation_strategy
self.instance_interruption_behaviour = (
instance_interruption_behaviour or "terminate"
)
self.state = "active" self.state = "active"
self.fulfilled_capacity = 0.0 self.fulfilled_capacity = 0.0
@ -404,6 +424,7 @@ class SpotFleetBackend(object):
allocation_strategy, allocation_strategy,
launch_specs, launch_specs,
launch_template_config=None, launch_template_config=None,
instance_interruption_behaviour=None,
): ):
spot_fleet_request_id = random_spot_fleet_request_id() spot_fleet_request_id = random_spot_fleet_request_id()
@ -416,6 +437,7 @@ class SpotFleetBackend(object):
allocation_strategy, allocation_strategy,
launch_specs, launch_specs,
launch_template_config, launch_template_config,
instance_interruption_behaviour,
) )
self.spot_fleet_requests[spot_fleet_request_id] = request self.spot_fleet_requests[spot_fleet_request_id] = request
return request return request

View File

@ -47,6 +47,9 @@ class SpotFleets(BaseResponse):
target_capacity = spot_config["TargetCapacity"] target_capacity = spot_config["TargetCapacity"]
iam_fleet_role = spot_config["IamFleetRole"] iam_fleet_role = spot_config["IamFleetRole"]
allocation_strategy = spot_config["AllocationStrategy"] allocation_strategy = spot_config["AllocationStrategy"]
instance_interruption_behaviour = spot_config.get(
"InstanceInterruptionBehavior"
)
launch_specs = spot_config.get("LaunchSpecifications") launch_specs = spot_config.get("LaunchSpecifications")
launch_template_config = list( launch_template_config = list(
@ -63,6 +66,7 @@ class SpotFleets(BaseResponse):
allocation_strategy=allocation_strategy, allocation_strategy=allocation_strategy,
launch_specs=launch_specs, launch_specs=launch_specs,
launch_template_config=launch_template_config, launch_template_config=launch_template_config,
instance_interruption_behaviour=instance_interruption_behaviour,
) )
template = self.response_template(REQUEST_SPOT_FLEET_TEMPLATE) template = self.response_template(REQUEST_SPOT_FLEET_TEMPLATE)

View File

@ -62,6 +62,10 @@ class SpotInstances(EC2BaseResponse):
ramdisk_id = self._get_param("LaunchSpecification.RamdiskId") ramdisk_id = self._get_param("LaunchSpecification.RamdiskId")
monitoring_enabled = self._get_param("LaunchSpecification.Monitoring.Enabled") monitoring_enabled = self._get_param("LaunchSpecification.Monitoring.Enabled")
subnet_id = self._get_param("LaunchSpecification.SubnetId") subnet_id = self._get_param("LaunchSpecification.SubnetId")
instance_interruption_behaviour = self._get_param(
"InstanceInterruptionBehavior"
)
tags = self._parse_tag_specification()
if self.is_not_dryrun("RequestSpotInstance"): if self.is_not_dryrun("RequestSpotInstance"):
requests = self.ec2_backend.request_spot_instances( requests = self.ec2_backend.request_spot_instances(
@ -82,6 +86,8 @@ class SpotInstances(EC2BaseResponse):
ramdisk_id=ramdisk_id, ramdisk_id=ramdisk_id,
monitoring_enabled=monitoring_enabled, monitoring_enabled=monitoring_enabled,
subnet_id=subnet_id, subnet_id=subnet_id,
instance_interruption_behaviour=instance_interruption_behaviour,
tags=tags,
) )
template = self.response_template(REQUEST_SPOT_INSTANCES_TEMPLATE) template = self.response_template(REQUEST_SPOT_INSTANCES_TEMPLATE)
@ -98,9 +104,9 @@ REQUEST_SPOT_INSTANCES_TEMPLATE = """<RequestSpotInstancesResponse xmlns="http:/
<type>{{ request.type }}</type> <type>{{ request.type }}</type>
<state>{{ request.state }}</state> <state>{{ request.state }}</state>
<status> <status>
<code>pending-evaluation</code> <code>{{ request.status }}</code>
<updateTime>2015-01-01T00:00:00.000Z</updateTime> <updateTime>2015-01-01T00:00:00.000Z</updateTime>
<message>Your Spot request has been submitted for review, and is pending evaluation.</message> <message>{{ request.status_message }}</message>
</status> </status>
<instanceId>{{ request.instance_id }}</instanceId> <instanceId>{{ request.instance_id }}</instanceId>
<availabilityZoneGroup>{{ request.availability_zone_group }}</availabilityZoneGroup> <availabilityZoneGroup>{{ request.availability_zone_group }}</availabilityZoneGroup>
@ -153,11 +159,11 @@ DESCRIBE_SPOT_INSTANCES_TEMPLATE = """<DescribeSpotInstanceRequestsResponse xmln
<type>{{ request.type }}</type> <type>{{ request.type }}</type>
<state>{{ request.state }}</state> <state>{{ request.state }}</state>
<status> <status>
<code>pending-evaluation</code> <code>{{ request.status }}</code>
<updateTime>2015-01-01T00:00:00.000Z</updateTime> <updateTime>2015-01-01T00:00:00.000Z</updateTime>
<message>Your Spot request has been submitted for review, and is pending evaluation.</message> <message>{{ request.status_message }}</message>
</status> </status>
<instanceId>{{ request.instance_id }}</instanceId> <instanceId>{{ request.instance.id }}</instanceId>
{% if request.availability_zone_group %} {% if request.availability_zone_group %}
<availabilityZoneGroup>{{ request.availability_zone_group }}</availabilityZoneGroup> <availabilityZoneGroup>{{ request.availability_zone_group }}</availabilityZoneGroup>
{% endif %} {% endif %}
@ -217,6 +223,7 @@ DESCRIBE_SPOT_INSTANCES_TEMPLATE = """<DescribeSpotInstanceRequestsResponse xmln
<validUntil>{{ request.valid_until }}</validUntil> <validUntil>{{ request.valid_until }}</validUntil>
{% endif %} {% endif %}
<productDescription>Linux/UNIX</productDescription> <productDescription>Linux/UNIX</productDescription>
<instanceInterruptionBehavior>{{ request.instance_interruption_behaviour }}</instanceInterruptionBehavior>
</item> </item>
{% endfor %} {% endfor %}
</spotInstanceRequestSet> </spotInstanceRequestSet>

View File

@ -342,7 +342,7 @@ index 49e4909b3a..731a37f253 100644
Timeouts: &schema.ResourceTimeout{ Timeouts: &schema.ResourceTimeout{
- Create: schema.DefaultTimeout(10 * time.Minute), - Create: schema.DefaultTimeout(10 * time.Minute),
- Delete: schema.DefaultTimeout(15 * time.Minute), - Delete: schema.DefaultTimeout(15 * time.Minute),
+ Create: schema.DefaultTimeout(10 * time.Second), + Create: schema.DefaultTimeout(20 * time.Second),
+ Delete: schema.DefaultTimeout(15 * time.Second), + Delete: schema.DefaultTimeout(15 * time.Second),
}, },
@ -366,7 +366,7 @@ index e054f82987..08aeb6cf70 100644
Timeouts: &schema.ResourceTimeout{ Timeouts: &schema.ResourceTimeout{
- Create: schema.DefaultTimeout(10 * time.Minute), - Create: schema.DefaultTimeout(10 * time.Minute),
- Delete: schema.DefaultTimeout(20 * time.Minute), - Delete: schema.DefaultTimeout(20 * time.Minute),
+ Create: schema.DefaultTimeout(10 * time.Second), + Create: schema.DefaultTimeout(20 * time.Second),
+ Delete: schema.DefaultTimeout(20 * time.Second), + Delete: schema.DefaultTimeout(20 * time.Second),
}, },

View File

@ -40,6 +40,8 @@ ec2:
- TestAccEC2InternetGateway_ - TestAccEC2InternetGateway_
- TestAccEC2NATGateway_ - TestAccEC2NATGateway_
- TestAccEC2RouteTableAssociation_ - TestAccEC2RouteTableAssociation_
- TestAccEC2SpotInstanceRequest_disappears
- TestAccEC2SpotInstanceRequest_interruptUpdate
- TestAccEC2VPCEndpointService_ - TestAccEC2VPCEndpointService_
- TestAccEC2VPNGateway_ - TestAccEC2VPNGateway_
- TestAccEC2VPNGatewayAttachment_ - TestAccEC2VPNGatewayAttachment_

View File

@ -7,7 +7,6 @@ import pytz
import sure # noqa # pylint: disable=unused-import import sure # noqa # pylint: disable=unused-import
from moto import mock_ec2, settings from moto import mock_ec2, settings
from moto.ec2.models import ec2_backends
from moto.core.utils import iso_8601_datetime_with_milliseconds from moto.core.utils import iso_8601_datetime_with_milliseconds
from tests import EXAMPLE_AMI_ID from tests import EXAMPLE_AMI_ID
from uuid import uuid4 from uuid import uuid4
@ -90,8 +89,8 @@ def test_request_spot_instances():
requests.should.have.length_of(1) requests.should.have.length_of(1)
request = requests[0] request = requests[0]
request["State"].should.equal("open") request["State"].should.equal("active")
request["SpotPrice"].should.equal("0.5") request["SpotPrice"].should.equal("0.500000")
request["Type"].should.equal("one-time") request["Type"].should.equal("one-time")
request["ValidFrom"].should.equal(start_dt) request["ValidFrom"].should.equal(start_dt)
request["ValidUntil"].should.equal(end_dt) request["ValidUntil"].should.equal(end_dt)
@ -129,13 +128,14 @@ def test_request_spot_instances_default_arguments():
requests.should.have.length_of(1) requests.should.have.length_of(1)
request = requests[0] request = requests[0]
request["State"].should.equal("open") request["State"].should.equal("active")
request["SpotPrice"].should.equal("0.5") request["SpotPrice"].should.equal("0.500000")
request["Type"].should.equal("one-time") request["Type"].should.equal("one-time")
request.shouldnt.contain("ValidFrom") request.shouldnt.contain("ValidFrom")
request.shouldnt.contain("ValidUntil") request.shouldnt.contain("ValidUntil")
request.shouldnt.contain("LaunchGroup") request.shouldnt.contain("LaunchGroup")
request.shouldnt.contain("AvailabilityZoneGroup") request.shouldnt.contain("AvailabilityZoneGroup")
request.should.have.key("InstanceInterruptionBehavior").equals("terminate")
launch_spec = request["LaunchSpecification"] launch_spec = request["LaunchSpecification"]
@ -153,7 +153,7 @@ def test_request_spot_instances_default_arguments():
@mock_ec2 @mock_ec2
def test_cancel_spot_instance_request_boto3(): def test_cancel_spot_instance_request():
client = boto3.client("ec2", region_name="us-west-1") client = boto3.client("ec2", region_name="us-west-1")
rsi = client.request_spot_instances( rsi = client.request_spot_instances(
@ -169,7 +169,7 @@ def test_cancel_spot_instance_request_boto3():
request.should.have.key("CreateTime") request.should.have.key("CreateTime")
request.should.have.key("Type").equal("one-time") request.should.have.key("Type").equal("one-time")
request.should.have.key("SpotInstanceRequestId") request.should.have.key("SpotInstanceRequestId")
request.should.have.key("SpotPrice").equal("0.5") request.should.have.key("SpotPrice").equal("0.500000")
request["LaunchSpecification"]["ImageId"].should.equal(EXAMPLE_AMI_ID) request["LaunchSpecification"]["ImageId"].should.equal(EXAMPLE_AMI_ID)
with pytest.raises(ClientError) as ex: with pytest.raises(ClientError) as ex:
@ -193,7 +193,7 @@ def test_cancel_spot_instance_request_boto3():
@mock_ec2 @mock_ec2
def test_request_spot_instances_fulfilled_boto3(): def test_request_spot_instances_fulfilled():
""" """
Test that moto correctly fullfills a spot instance request Test that moto correctly fullfills a spot instance request
""" """
@ -210,22 +210,11 @@ def test_request_spot_instances_fulfilled_boto3():
requests.should.have.length_of(1) requests.should.have.length_of(1)
request = requests[0] request = requests[0]
request["State"].should.equal("open")
if not settings.TEST_SERVER_MODE:
ec2_backends["us-east-1"].spot_instance_requests[request_id].state = "active"
requests = client.describe_spot_instance_requests(
SpotInstanceRequestIds=[request_id]
)["SpotInstanceRequests"]
requests.should.have.length_of(1)
request = requests[0]
request["State"].should.equal("active") request["State"].should.equal("active")
@mock_ec2 @mock_ec2
def test_tag_spot_instance_request_boto3(): def test_tag_spot_instance_request():
""" """
Test that moto correctly tags a spot instance request Test that moto correctly tags a spot instance request
""" """
@ -252,7 +241,7 @@ def test_tag_spot_instance_request_boto3():
@mock_ec2 @mock_ec2
def test_get_all_spot_instance_requests_filtering_boto3(): def test_get_all_spot_instance_requests_filtering():
""" """
Test that moto correctly filters spot instance requests Test that moto correctly filters spot instance requests
""" """
@ -280,14 +269,14 @@ def test_get_all_spot_instance_requests_filtering_boto3():
) )
requests = client.describe_spot_instance_requests( requests = client.describe_spot_instance_requests(
Filters=[{"Name": "state", "Values": ["active"]}] Filters=[{"Name": "state", "Values": ["failed"]}]
)["SpotInstanceRequests"] )["SpotInstanceRequests"]
r_ids = [r["SpotInstanceRequestId"] for r in requests] r_ids = [r["SpotInstanceRequestId"] for r in requests]
r_ids.shouldnt.contain(request1_id) r_ids.shouldnt.contain(request1_id)
r_ids.shouldnt.contain(request2_id) r_ids.shouldnt.contain(request2_id)
requests = client.describe_spot_instance_requests( requests = client.describe_spot_instance_requests(
Filters=[{"Name": "state", "Values": ["open"]}] Filters=[{"Name": "state", "Values": ["active"]}]
)["SpotInstanceRequests"] )["SpotInstanceRequests"]
r_ids = [r["SpotInstanceRequestId"] for r in requests] r_ids = [r["SpotInstanceRequestId"] for r in requests]
r_ids.should.contain(request1_id) r_ids.should.contain(request1_id)
@ -322,6 +311,27 @@ def test_request_spot_instances_instance_lifecycle():
instance["InstanceLifecycle"].should.equal("spot") instance["InstanceLifecycle"].should.equal("spot")
@mock_ec2
def test_request_spot_instances_with_tags():
client = boto3.client("ec2", region_name="us-east-1")
request = client.request_spot_instances(
SpotPrice="0.5",
TagSpecifications=[
{
"ResourceType": "spot-instances-request",
"Tags": [{"Key": "k", "Value": "v"}],
}
],
)
request_id = request["SpotInstanceRequests"][0]["SpotInstanceRequestId"]
request = client.describe_spot_instance_requests(
SpotInstanceRequestIds=[request_id]
)["SpotInstanceRequests"][0]
request.should.have.key("Tags").equals([{"Key": "k", "Value": "v"}])
@mock_ec2 @mock_ec2
def test_launch_spot_instance_instance_lifecycle(): def test_launch_spot_instance_instance_lifecycle():
client = boto3.client("ec2", region_name="us-east-1") client = boto3.client("ec2", region_name="us-east-1")
@ -391,18 +401,20 @@ def test_spot_price_history():
@mock_ec2 @mock_ec2
def test_request_spot_instances_setting_instance_id_boto3(): def test_request_spot_instances__instance_should_exist():
client = boto3.client("ec2", region_name="us-east-1") client = boto3.client("ec2", region_name="us-east-1")
request = client.request_spot_instances( request = client.request_spot_instances(
SpotPrice="0.5", LaunchSpecification={"ImageId": EXAMPLE_AMI_ID} SpotPrice="0.5", LaunchSpecification={"ImageId": EXAMPLE_AMI_ID}
) )
request_id = request["SpotInstanceRequests"][0]["SpotInstanceRequestId"] request_id = request["SpotInstanceRequests"][0]["SpotInstanceRequestId"]
if not settings.TEST_SERVER_MODE: request = client.describe_spot_instance_requests(
req = ec2_backends["us-east-1"].spot_instance_requests[request_id] SpotInstanceRequestIds=[request_id]
req.state = "active" )["SpotInstanceRequests"][0]
req.instance_id = "i-12345678" request.should.have.key("InstanceId")
instance_id = request["InstanceId"]
request = client.describe_spot_instance_requests()["SpotInstanceRequests"][0] response = client.describe_instances(InstanceIds=[instance_id])
assert request["State"] == "active" instance = response["Reservations"][0]["Instances"][0]
assert request["InstanceId"] == "i-12345678" instance.should.have.key("InstanceId").equals(instance_id)
instance.should.have.key("ImageId").equals(EXAMPLE_AMI_ID)