EC2: Add ec2 fleets basic api (#5367)

This commit is contained in:
Felipe Marinho 2022-08-12 06:33:39 -03:00 committed by GitHub
parent 2d6f04d01c
commit f743567789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1428 additions and 14 deletions

View File

@ -1597,7 +1597,7 @@
- [X] create_default_vpc
- [X] create_dhcp_options
- [X] create_egress_only_internet_gateway
- [ ] create_fleet
- [X] create_fleet
- [X] create_flow_logs
- [ ] create_fpga_image
- [X] create_image
@ -1665,7 +1665,7 @@
- [X] delete_customer_gateway
- [ ] delete_dhcp_options
- [X] delete_egress_only_internet_gateway
- [ ] delete_fleets
- [X] delete_fleets
- [X] delete_flow_logs
- [ ] delete_fpga_image
- [ ] delete_instance_event_window
@ -1757,8 +1757,8 @@
- [ ] describe_fast_launch_images
- [ ] describe_fast_snapshot_restores
- [ ] describe_fleet_history
- [ ] describe_fleet_instances
- [ ] describe_fleets
- [X] describe_fleet_instances
- [X] describe_fleets
- [X] describe_flow_logs
- [ ] describe_fpga_image_attribute
- [ ] describe_fpga_images

View File

@ -85,7 +85,7 @@ ec2
- [X] create_default_vpc
- [X] create_dhcp_options
- [X] create_egress_only_internet_gateway
- [ ] create_fleet
- [X] create_fleet
- [X] create_flow_logs
- [ ] create_fpga_image
- [X] create_image
@ -157,7 +157,7 @@ ec2
- [X] delete_customer_gateway
- [ ] delete_dhcp_options
- [X] delete_egress_only_internet_gateway
- [ ] delete_fleets
- [X] delete_fleets
- [X] delete_flow_logs
- [ ] delete_fpga_image
- [ ] delete_instance_event_window
@ -253,8 +253,8 @@ ec2
- [ ] describe_fast_launch_images
- [ ] describe_fast_snapshot_restores
- [ ] describe_fleet_history
- [ ] describe_fleet_instances
- [ ] describe_fleets
- [X] describe_fleet_instances
- [X] describe_fleets
- [X] describe_flow_logs
- [ ] describe_fpga_image_attribute
- [ ] describe_fpga_images

View File

@ -14,6 +14,7 @@ from .dhcp_options import DHCPOptionsSetBackend
from .elastic_block_store import EBSBackend
from .elastic_ip_addresses import ElasticAddressBackend
from .elastic_network_interfaces import NetworkInterfaceBackend
from .fleets import FleetsBackend
from .flow_logs import FlowLogsBackend
from .key_pairs import KeyPairBackend
from .launch_templates import LaunchTemplateBackend
@ -124,6 +125,7 @@ class EC2Backend(
LaunchTemplateBackend,
IamInstanceProfileAssociationBackend,
CarrierGatewayBackend,
FleetsBackend,
):
"""
Implementation of the AWS EC2 endpoint.

313
moto/ec2/models/fleets.py Normal file
View File

@ -0,0 +1,313 @@
from collections import defaultdict
from moto.ec2.models.spot_requests import SpotFleetLaunchSpec
from .core import TaggedEC2Resource
from ..utils import (
random_fleet_id,
convert_tag_spec,
)
class Fleet(TaggedEC2Resource):
def __init__(
self,
ec2_backend,
fleet_id,
on_demand_options,
spot_options,
target_capacity_specification,
launch_template_configs,
excess_capacity_termination_policy,
replace_unhealthy_instances,
terminate_instances_with_expiration,
fleet_type,
valid_from,
valid_until,
tag_specifications,
):
self.ec2_backend = ec2_backend
self.id = fleet_id
self.spot_options = spot_options
self.on_demand_options = on_demand_options
self.target_capacity_specification = target_capacity_specification
self.launch_template_configs = launch_template_configs
self.excess_capacity_termination_policy = (
excess_capacity_termination_policy or "termination"
)
self.replace_unhealthy_instances = replace_unhealthy_instances
self.terminate_instances_with_expiration = terminate_instances_with_expiration
self.fleet_type = fleet_type
self.valid_from = valid_from
self.valid_until = valid_until
tag_map = convert_tag_spec(tag_specifications).get("fleet", {})
self.add_tags(tag_map)
self.tags = self.get_tags()
self.state = "active"
self.fulfilled_capacity = 0.0
self.fulfilled_on_demand_capacity = 0.0
self.fulfilled_spot_capacity = 0.0
self.launch_specs = []
launch_specs_from_config = []
for config in launch_template_configs or []:
spec = config["LaunchTemplateSpecification"]
if "LaunchTemplateId" in spec:
launch_template = self.ec2_backend.get_launch_template(
template_id=spec["LaunchTemplateId"]
)
elif "LaunchTemplateName" in spec:
launch_template = self.ec2_backend.get_launch_template_by_name(
name=spec["LaunchTemplateName"]
)
else:
continue
launch_template_data = launch_template.latest_version().data
new_launch_template = launch_template_data.copy()
if config.get("Overrides"):
for override in config["Overrides"]:
new_launch_template.update(override)
launch_specs_from_config.append(new_launch_template)
for spec in launch_specs_from_config:
tag_spec_set = spec.get("TagSpecification", [])
tags = convert_tag_spec(tag_spec_set)
self.launch_specs.append(
SpotFleetLaunchSpec(
ebs_optimized=spec.get("EbsOptimized"),
group_set=spec.get("GroupSet", []),
iam_instance_profile=spec.get("IamInstanceProfile"),
image_id=spec["ImageId"],
instance_type=spec["InstanceType"],
key_name=spec.get("KeyName"),
monitoring=spec.get("Monitoring"),
spot_price=spec.get("SpotPrice"),
subnet_id=spec.get("SubnetId"),
tag_specifications=tags,
user_data=spec.get("UserData"),
weighted_capacity=spec.get("WeightedCapacity", 1),
)
)
self.spot_requests = []
self.on_demand_instances = []
default_capacity = (
target_capacity_specification.get("DefaultTargetCapacityType")
or "on-demand"
)
self.target_capacity = int(
target_capacity_specification.get("TotalTargetCapacity")
)
self.spot_target_capacity = int(
target_capacity_specification.get("SpotTargetCapacity")
)
if self.spot_target_capacity > 0:
self.create_spot_requests(self.spot_target_capacity)
self.on_demand_target_capacity = int(
target_capacity_specification.get("OnDemandTargetCapacity")
)
if self.on_demand_target_capacity > 0:
self.create_on_demand_requests(self.on_demand_target_capacity)
remaining_capacity = self.target_capacity - self.fulfilled_capacity
if remaining_capacity > 0:
if default_capacity == "on-demand":
self.create_on_demand_requests(remaining_capacity)
elif default_capacity == "spot":
self.create_spot_requests(remaining_capacity)
@property
def physical_resource_id(self):
return self.id
def create_spot_requests(self, weight_to_add):
weight_map, added_weight = self.get_launch_spec_counts(weight_to_add)
for launch_spec, count in weight_map.items():
requests = self.ec2_backend.request_spot_instances(
price=launch_spec.spot_price,
image_id=launch_spec.image_id,
count=count,
spot_instance_type="persistent",
valid_from=None,
valid_until=None,
launch_group=None,
availability_zone_group=None,
key_name=launch_spec.key_name,
security_groups=launch_spec.group_set,
user_data=launch_spec.user_data,
instance_type=launch_spec.instance_type,
placement=None,
kernel_id=None,
ramdisk_id=None,
monitoring_enabled=launch_spec.monitoring,
subnet_id=launch_spec.subnet_id,
spot_fleet_id=self.id,
tags=launch_spec.tag_specifications,
)
self.spot_requests.extend(requests)
self.fulfilled_capacity += added_weight
return self.spot_requests
def create_on_demand_requests(self, weight_to_add):
weight_map, added_weight = self.get_launch_spec_counts(weight_to_add)
for launch_spec, count in weight_map.items():
reservation = self.ec2_backend.add_instances(
image_id=launch_spec.image_id,
count=count,
instance_type=launch_spec.instance_type,
is_instance_type_default=False,
key_name=launch_spec.key_name,
security_group_names=launch_spec.group_set,
user_data=launch_spec.user_data,
placement=None,
kernel_id=None,
ramdisk_id=None,
monitoring_enabled=launch_spec.monitoring,
subnet_id=launch_spec.subnet_id,
fleet_id=self.id,
tags=launch_spec.tag_specifications,
)
# get the instance from the reservation
instance = reservation.instances[0]
self.on_demand_instances.append(
{
"id": reservation.id,
"instance": instance,
}
)
self.fulfilled_capacity += added_weight
return self.on_demand_instances
def get_launch_spec_counts(self, weight_to_add):
weight_map = defaultdict(int)
weight_so_far = 0
if (
self.spot_options
and self.spot_options["AllocationStrategy"] == "diversified"
):
launch_spec_index = 0
while True:
launch_spec = self.launch_specs[
launch_spec_index % len(self.launch_specs)
]
weight_map[launch_spec] += 1
weight_so_far += launch_spec.weighted_capacity
if weight_so_far >= weight_to_add:
break
launch_spec_index += 1
else: # lowestPrice
cheapest_spec = sorted(
# FIXME: change `+inf` to the on demand price scaled to weighted capacity when it's not present
self.launch_specs,
key=lambda spec: float(spec.spot_price or "+inf"),
)[0]
weight_so_far = weight_to_add + (
weight_to_add % cheapest_spec.weighted_capacity
)
weight_map[cheapest_spec] = int(
weight_so_far // cheapest_spec.weighted_capacity
)
return weight_map, weight_so_far
def terminate_instances(self):
instance_ids = []
new_fulfilled_capacity = self.fulfilled_capacity
for req in self.spot_requests + self.on_demand_instances:
instance = None
try:
instance = req.instance
except AttributeError:
instance = req["instance"]
if instance.state == "terminated":
continue
# stop when we hit the target capacity
if new_fulfilled_capacity <= self.target_capacity:
break
instance_ids.append(instance.id)
new_fulfilled_capacity -= 1
self.spot_requests = [
req for req in self.spot_requests if req.instance.id not in instance_ids
]
self.on_demand_instances = [
req
for req in self.on_demand_instances
if req["instance"].id not in instance_ids
]
self.ec2_backend.terminate_instances(instance_ids)
class FleetsBackend:
def __init__(self):
self.fleets = {}
def create_fleet(
self,
on_demand_options,
spot_options,
target_capacity_specification,
launch_template_configs,
excess_capacity_termination_policy,
replace_unhealthy_instances,
terminate_instances_with_expiration,
fleet_type,
valid_from,
valid_until,
tag_specifications,
):
fleet_id = random_fleet_id()
fleet = Fleet(
self,
fleet_id,
on_demand_options,
spot_options,
target_capacity_specification,
launch_template_configs,
excess_capacity_termination_policy,
replace_unhealthy_instances,
terminate_instances_with_expiration,
fleet_type,
valid_from,
valid_until,
tag_specifications,
)
self.fleets[fleet_id] = fleet
return fleet
def get_fleet(self, fleet_id):
return self.fleets.get(fleet_id)
def describe_fleet_instances(self, fleet_id):
fleet = self.get_fleet(fleet_id)
if not fleet:
return []
return fleet.spot_requests + fleet.on_demand_instances
def describe_fleets(self, fleet_ids):
fleets = self.fleets.values()
if fleet_ids:
fleets = [fleet for fleet in fleets if fleet.id in fleet_ids]
return fleets
def delete_fleets(self, fleet_ids, terminate_instances):
fleets = []
for fleet_id in fleet_ids:
fleet = self.fleets[fleet_id]
if terminate_instances:
fleet.target_capacity = 0
fleet.terminate_instances()
fleets.append(fleet)
fleet.state = "deleted"
return fleets

View File

@ -7,6 +7,7 @@ from moto import settings
from moto.core import get_account_id
from moto.core import CloudFormationModel
from moto.core.utils import camelcase_to_underscores
from moto.ec2.models.fleets import Fleet
from moto.ec2.models.instance_types import (
INSTANCE_TYPE_OFFERINGS,
InstanceTypeOfferingBackend,
@ -114,6 +115,7 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
)
self.sriov_net_support = "simple"
self._spot_fleet_id = kwargs.get("spot_fleet_id", None)
self._fleet_id = kwargs.get("fleet_id", None)
self.associate_public_ip = kwargs.get("associate_public_ip", False)
if in_ec2_classic:
# If we are in EC2-Classic, autoassign a public IP
@ -367,18 +369,28 @@ class Instance(TaggedEC2Resource, BotoInstance, CloudFormationModel):
self.teardown_defaults()
if self._spot_fleet_id:
spot_fleet = self.ec2_backend.get_spot_fleet_request(self._spot_fleet_id)
for spec in spot_fleet.launch_specs:
if self._spot_fleet_id or self._fleet_id:
fleet = self.ec2_backend.get_spot_fleet_request(self._spot_fleet_id)
if not fleet:
fleet = self.ec2_backend.get_fleet(
self._spot_fleet_id
) or self.ec2_backend.get_fleet(self._fleet_id)
for spec in fleet.launch_specs:
if (
spec.instance_type == self.instance_type
and spec.subnet_id == self.subnet_id
):
break
spot_fleet.fulfilled_capacity -= spec.weighted_capacity
spot_fleet.spot_requests = [
req for req in spot_fleet.spot_requests if req.instance != self
fleet.fulfilled_capacity -= spec.weighted_capacity
fleet.spot_requests = [
req for req in fleet.spot_requests if req.instance != self
]
if isinstance(fleet, Fleet):
fleet.on_demand_instances = [
inst
for inst in fleet.on_demand_instances
if inst["instance"] != self
]
self._state.name = "terminated"
self._state.code = 48

View File

@ -7,6 +7,7 @@ from .dhcp_options import DHCPOptions
from .elastic_block_store import ElasticBlockStore
from .elastic_ip_addresses import ElasticIPAddresses
from .elastic_network_interfaces import ElasticNetworkInterfaces
from .fleets import Fleets
from .general import General
from .instances import InstanceResponse
from .internet_gateways import InternetGateways
@ -52,6 +53,7 @@ class EC2Response(
ElasticBlockStore,
ElasticIPAddresses,
ElasticNetworkInterfaces,
Fleets,
General,
InstanceResponse,
InternetGateways,

View File

@ -0,0 +1,417 @@
from moto.core.responses import BaseResponse
class Fleets(BaseResponse):
def delete_fleets(self):
fleet_ids = self._get_multi_param("FleetId.")
terminate_instances = self._get_param("TerminateInstances")
fleets = self.ec2_backend.delete_fleets(fleet_ids, terminate_instances)
template = self.response_template(DELETE_FLEETS_TEMPLATE)
return template.render(fleets=fleets)
def describe_fleet_instances(self):
fleet_id = self._get_param("FleetId")
instances = self.ec2_backend.describe_fleet_instances(fleet_id)
template = self.response_template(DESCRIBE_FLEET_INSTANCES_TEMPLATE)
return template.render(fleet_id=fleet_id, instances=instances)
def describe_fleets(self):
fleet_ids = self._get_multi_param("FleetId.")
requests = self.ec2_backend.describe_fleets(fleet_ids)
template = self.response_template(DESCRIBE_FLEETS_TEMPLATE)
rend = template.render(requests=requests)
return rend
def create_fleet(self):
on_demand_options = self._get_multi_param_dict("OnDemandOptions")
spot_options = self._get_multi_param_dict("SpotOptions")
target_capacity_specification = self._get_multi_param_dict(
"TargetCapacitySpecification"
)
launch_template_configs = self._get_multi_param("LaunchTemplateConfigs")
excess_capacity_termination_policy = self._get_param(
"ExcessCapacityTerminationPolicy"
)
replace_unhealthy_instances = self._get_param("ReplaceUnhealthyInstances")
terminate_instances_with_expiration = self._get_param(
"TerminateInstancesWithExpiration", if_none=True
)
fleet_type = self._get_param("Type", if_none="maintain")
valid_from = self._get_param("ValidFrom")
valid_until = self._get_param("ValidUntil")
tag_specifications = self._get_multi_param("TagSpecification")
request = self.ec2_backend.create_fleet(
on_demand_options=on_demand_options,
spot_options=spot_options,
target_capacity_specification=target_capacity_specification,
launch_template_configs=launch_template_configs,
excess_capacity_termination_policy=excess_capacity_termination_policy,
replace_unhealthy_instances=replace_unhealthy_instances,
terminate_instances_with_expiration=terminate_instances_with_expiration,
fleet_type=fleet_type,
valid_from=valid_from,
valid_until=valid_until,
tag_specifications=tag_specifications,
)
template = self.response_template(CREATE_FLEET_TEMPLATE)
return template.render(request=request)
CREATE_FLEET_TEMPLATE = """<CreateFleetResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>60262cc5-2bd4-4c8d-98ed-example</requestId>
<fleetId>{{ request.id }}</fleetId>
{% if request.fleet_type == "instant" %}
<fleetInstanceSet>
{% for instance in request.on_demand_instances %}
<item>
<instanceType>{{ instance["instance"].instance_type }}</instanceType>
<lifecycle>on-demand</lifecycle>
<instanceIds>
<item>{{ instance["instance"].id }}</item>
</instanceIds>
</item>
{% endfor %}
{% for instance in request.spot_requests %}
<item>
<instanceType>{{ instance.instance.instance_type }}</instanceType>
<lifecycle>spot</lifecycle>
<instanceIds>
<item>{{ instance.instance.id }}</item>
</instanceIds>
</item>
{% endfor %}
</fleetInstanceSet>
{% endif %}
</CreateFleetResponse>"""
DESCRIBE_FLEETS_TEMPLATE = """<DescribeFleetsResponse xmlns="http://ec2.amazonaws.com/doc/2016-09-15/">
<requestId>4d68a6cc-8f2e-4be1-b425-example</requestId>
<fleetSet>
{% for request in requests %}
<item>
<fleetId>{{ request.id }}</fleetId>
<fleetState>{{ request.state }}</fleetState>
<excessCapacityTerminationPolicy>{{ request.excess_capacity_termination_policy }}</excessCapacityTerminationPolicy>
<fulfilledCapacity>{{ request.fulfilled_capacity }}</fulfilledCapacity>
<fulfilledOnDemandCapacity>{{ request.fulfilled_on_demand_capacity }}</fulfilledOnDemandCapacity>
<launchTemplateConfigs>
{% for config in request.launch_template_configs %}
<item>
<launchTemplateSpecification>
<launchTemplateId>{{ config.LaunchTemplateSpecification.LaunchTemplateId }}</launchTemplateId>
<version>{{ config.LaunchTemplateSpecification.Version }}</version>
</launchTemplateSpecification>
{% if config.Overrides %}
<overrides>
{% for override in config.Overrides %}
<item>
{% if override.AvailabilityZone %}
<availabilityZone>{{ override.AvailabilityZone }}</availabilityZone>
{% endif %}
{% if override.InstanceType %}
<instanceType>{{ override.InstanceType }}</instanceType>
{% endif %}
{% if override.InstanceRequirements %}
<instanceRequirements>
{% if override.InstanceRequirements.AcceleratorCount %}
<acceleratorCount>
{% if override.InstanceRequirements.AcceleratorCount.Max %}
<max>{{ override.InstanceRequirements.AcceleratorCount.Max }}</max>
{% endif %}
{% if override.InstanceRequirements.AcceleratorCount.Min %}
<min>{{ override.InstanceRequirements.AcceleratorCount.Min }}</min>
{% endif %}
</acceleratorCount>
{% endif %}
{% if override.InstanceRequirements.AcceleratorManufacturer %}
<acceleratorManufacturerSet>
{% for manufacturer in override.InstanceRequirements.AcceleratorManufacturer %}
<item>{{ manufacturer }}</item>
{% endfor %}
</acceleratorManufacturerSet>
{% endif %}
{% if override.InstanceRequirements.AcceleratorName %}
<acceleratorNameSet>
{% for name in override.InstanceRequirements.AcceleratorName %}
<item>{{ name }}</item>
{% endfor %}
</acceleratorNameSet>
{% endif %}
{% if override.InstanceRequirements.AcceleratorTotalMemoryMiB %}
<acceleratorTotalMemoryMiB>
{% if override.InstanceRequirements.AcceleratorTotalMemoryMiB.Max %}
<max>{{ override.InstanceRequirements.AcceleratorTotalMemoryMiB.Max }}</max>
{% endif %}
{% if override.InstanceRequirements.AcceleratorTotalMemoryMiB.Min %}
<min>{{ override.InstanceRequirements.AcceleratorTotalMemoryMiB.Min }}</min>
{% endif %}
</acceleratorTotalMemoryMiB>
{% endif %}
{% if override.InstanceRequirements.AcceleratorType %}
<acceleratorTypeSet>
{% for type in override.InstanceRequirements.AcceleratorType %}
<item>{{ type }}</item>
{% endfor %}
</acceleratorTypeSet>
{% endif %}
{% if override.InstanceRequirements.BareMetal %}
<bareMetal>{{ override.InstanceRequirements.BareMetal }}</bareMetal>
{% endif %}
{% if override.InstanceRequirements.BaselineEbsBandwidthMbps %}
<baselineEbsBandwidthMbps>
{% if override.InstanceRequirements.BaselineEbsBandwidthMbps.Min %}
<min>{{ override.InstanceRequirements.BaselineEbsBandwidthMbps.Min }}</min>
{% endif %}
{% if override.InstanceRequirements.BaselineEbsBandwidthMbps.Max %}
<max>{{ override.InstanceRequirements.BaselineEbsBandwidthMbps.Max }}</max>
{% endif %}
</baselineEbsBandwidthMbps>
{% endif %}
{% if override.InstanceRequirements.BurstablePerformance %}
<burstablePerformance>{{ override.InstanceRequirements.BurstablePerformance }}</burstablePerformance>
{% endif %}
{% if override.InstanceRequirements.CpuManufacturer %}
<cpuManufacturerSet>
{% for manufacturer in override.InstanceRequirements.CpuManufacturer %}
<item>{{ manufacturer }}</item>
{% endfor %}
</cpuManufacturerSet>
{% endif %}
{% if override.InstanceRequirements.ExcludedInstanceType %}
<excludedInstanceTypeSet>
{% for type in override.InstanceRequirements.ExcludedInstanceType %}
<item>{{ type }}</item>
{% endfor %}
</excludedInstanceTypeSet>
{% endif %}
{% if override.InstanceRequirements.InstanceGeneration %}
<instanceGenerationSet>
{% for generation in override.InstanceRequirements.InstanceGeneration %}
<item>{{ generation }}</item>
{% endfor %}
</instanceGenerationSet>
{% endif %}
{% if override.InstanceRequirements.LocalStorage %}
<localStorage>{{ override.InstanceRequirements.LocalStorage }}</localStorage>
{% endif %}
{% if override.InstanceRequirements.LocalStorageType %}
<localStorageTypeSet>
{% for type in override.InstanceRequirements.LocalStorageType %}
<item>{{ type }}</item>
{% endfor %}
</localStorageTypeSet>
{% endif %}
{% if override.InstanceRequirements.MemoryGiBPerVCpu %}
<memoryGiBPerVCpu>
<min>{{ override.InstanceRequirements.MemoryGiBPerVCpu.Min }}</min>
<max>{{ override.InstanceRequirements.MemoryGiBPerVCpu.Max }}</max>
</memoryGiBPerVCpu>
{% endif %}
{% if override.InstanceRequirements.MemoryMiB %}
<memoryMiB>
{% if override.InstanceRequirements.MemoryMiB.Min %}
<min>{{ override.InstanceRequirements.MemoryMiB.Min }}</min>
{% endif %}
{% if override.InstanceRequirements.MemoryMiB.Max %}
<max>{{ override.InstanceRequirements.MemoryMiB.Max }}</max>
{% endif %}
</memoryMiB>
{% endif %}
{% if override.InstanceRequirements.NetworkInterfaceCount %}
<networkInterfaceCount>
{% if override.InstanceRequirements.NetworkInterfaceCount.Max %}
<max>{{ override.InstanceRequirements.NetworkInterfaceCount.Max }}</max>
{% endif %}
{% if override.InstanceRequirements.NetworkInterfaceCount.Min %}
<min>{{ override.InstanceRequirements.NetworkInterfaceCount.Min }}</min>
{% endif %}
</networkInterfaceCount>
{% endif %}
{% if override.InstanceRequirements.OnDemandMaxPricePercentageOverLowestPrice %}
<onDemandMaxPricePercentageOverLowestPrice>{{ override.InstanceRequirements.OnDemandMaxPricePercentageOverLowestPrice }}</onDemandMaxPricePercentageOverLowestPrice>
{% endif %}
{% if override.InstanceRequirements.RequireHibernateSupport %}
<requireHibernateSupport>{{ override.InstanceRequirements.RequireHibernateSupport }}</requireHibernateSupport>
{% endif %}
{% if override.InstanceRequirements.SpotMaxPricePercentageOverLowestPrice %}
<spotMaxPricePercentageOverLowestPrice>{{ override.InstanceRequirements.SpotMaxPricePercentageOverLowestPrice }}</spotMaxPricePercentageOverLowestPrice>
{% endif %}
{% if override.InstanceRequirements.TotalLocalStorageGB %}
<totalLocalStorageGB>
{% if override.InstanceRequirements.TotalLocalStorageGB.Min %}
<min>{{ override.InstanceRequirements.TotalLocalStorageGB.Min }}</min>
{% endif %}
{% if override.InstanceRequirements.TotalLocalStorageGB.Max %}
<max>{{ override.InstanceRequirements.TotalLocalStorageGB.Max }}</max>
{% endif %}
</totalLocalStorageGB>
{% endif %}
{% if override.InstanceRequirements.VCpuCount %}
<vCpuCount>
{% if override.InstanceRequirements.VCpuCount.Min %}
<min>{{ override.InstanceRequirements.VCpuCount.Min }}</min>
{% endif %}
{% if override.InstanceRequirements.VCpuCount.Max %}
<max>{{ override.InstanceRequirements.VCpuCount.Max }}</max>
{% endif %}
</vCpuCount>
{% endif %}
</instanceRequirements>
{% endif %}
{% if override.MaxPrice %}
<maxPrice>{{ override.MaxPrice }}</maxPrice>
{% endif %}
{% if override.Placement %}
<placement>
{% if override.Placement.GroupName %}
<groupName>{{ override.Placement.GroupName }}</groupName>
{% endif %}
</placement>
{% endif %}
{% if override.Priority %}
<priority>{{ override.Priority }}</priority>
{% endif %}
{% if override.SubnetId %}
<subnetId>{{ override.SubnetId }}</subnetId>
{% endif %}
{% if override.WeightedCapacity %}
<weightedCapacity>{{ override.WeightedCapacity }}</weightedCapacity>
{% endif %}
</item>
{% endfor %}
</overrides>
{% endif %}
</item>
{% endfor %}
</launchTemplateConfigs>
<targetCapacitySpecification>
<totalTargetCapacity>{{ request.target_capacity }}</totalTargetCapacity>
{% if request.on_demand_target_capacity %}
<onDemandTargetCapacity>{{ request.on_demand_target_capacity }}</onDemandTargetCapacity>
{% endif %}
{% if request.spot_target_capacity %}
<spotTargetCapacity>{{ request.spot_target_capacity }}</spotTargetCapacity>
{% endif %}
<defaultTargetCapacityType>{{ request.target_capacity_specification.DefaultTargetCapacityType }}</defaultTargetCapacityType>
</targetCapacitySpecification>
{% if request.spot_options %}
<spotOptions>
{% if request.spot_options.AllocationStrategy %}
<allocationStrategy>{{ request.spot_options.AllocationStrategy }}</allocationStrategy>
{% endif %}
{% if request.spot_options.InstanceInterruptionBehavior %}
<instanceInterruptionBehavior>{{ request.spot_options.InstanceInterruptionBehavior }}</instanceInterruptionBehavior>
{% endif %}
{% if request.spot_options.InstancePoolsToUseCount %}
<instancePoolsToUseCount>{{ request.spot_options.InstancePoolsToUseCount }}</instancePoolsToUseCount>
{% endif %}
{% if request.spot_options.MaintenanceStrategies %}
<maintenanceStrategies>
{% if request.spot_options.MaintenanceStrategies.CapacityRebalance %}
<capacityRebalance>
{% if request.spot_options.MaintenanceStrategies.CapacityRebalance.ReplacementStrategy %}
<replacementStrategy>{{ request.spot_options.MaintenanceStrategies.CapacityRebalance.ReplacementStrategy }}</replacementStrategy>
{% endif %}
{% if request.spot_options.MaintenanceStrategies.CapacityRebalance.TerminationDelay %}
<terminationDelay>{{ request.spot_options.MaintenanceStrategies.CapacityRebalance.TerminationDelay }}</terminationDelay>
{% endif %}
</capacityRebalance>
{% endif %}
</maintenanceStrategies>
{% endif %}
{% if request.spot_options.MaxTotalPrice %}
<maxTotalPrice>{{ request.spot_options.MaxTotalPrice }}</maxTotalPrice>
{% endif %}
{% if request.spot_options.MinTargetCapacity %}
<minTargetCapacity>{{ request.spot_options.MinTargetCapacity }}</minTargetCapacity>
{% endif %}
{% if request.spot_options.SingleAvailabilityZone %}
<singleAvailabilityZone>{{ request.spot_options.SingleAvailabilityZone }}</singleAvailabilityZone>
{% endif %}
{% if request.spot_options.SingleInstanceType %}
<singleInstanceType>{{ request.spot_options.SingleInstanceType }}</singleInstanceType>
{% endif %}
</spotOptions>
{% endif %}
<!-- {'AllocationStrategy': 'lowest-price', 'MaxTotalPrice': '50', 'MinTargetCapacity': 1, 'SingleAvailabilityZone': True, 'SingleInstanceType': True} -->
{% if request.on_demand_options %}
<onDemandOptions>
{% if request.on_demand_options.AllocationStrategy %}
<allocationStrategy>{{ request.on_demand_options.AllocationStrategy }}</allocationStrategy>
{% endif %}
{% if request.on_demand_options.MaxTotalPrice %}
<maxTotalPrice>{{ request.on_demand_options.MaxTotalPrice }}</maxTotalPrice>
{% endif %}
{% if request.on_demand_options.MinTargetCapacity %}
<minTargetCapacity>{{ request.on_demand_options.MinTargetCapacity }}</minTargetCapacity>
{% endif %}
{% if request.on_demand_options.SingleAvailabilityZone %}
<singleAvailabilityZone>{{ request.on_demand_options.SingleAvailabilityZone }}</singleAvailabilityZone>
{% endif %}
{% if request.on_demand_options.SingleInstanceType %}
<singleInstanceType>{{ request.on_demand_options.SingleInstanceType }}</singleInstanceType>
{% endif %}
{% if request.on_demand_options.CapacityReservationOptions %}
<capacityReservationOptions>
{% if request.on_demand_options.CapacityReservationOptions.UsageStrategy %}
<usageStrategy>{{ request.on_demand_options.CapacityReservationOptions.UsageStrategy }}</usageStrategy>
{% endif %}
</capacityReservationOptions>
{% endif %}
</onDemandOptions>
{% endif %}
<terminateInstancesWithExpiration>{{ request.terminate_instances_with_expiration }}</terminateInstancesWithExpiration>
<type>{{ request.fleet_type }}</type>
<validFrom>{{ request.valid_from }}</validFrom>
<validUntil>{{ request.valid_until }}</validUntil>
<replaceUnhealthyInstances>{{ request.replace_unhealthy_instances }}</replaceUnhealthyInstances>
<tagSet>
{% for tag in request.tags %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
</item>
{% endfor %}
</fleetSet>
</DescribeFleetsResponse>"""
DESCRIBE_FLEET_INSTANCES_TEMPLATE = """<DescribeFleetInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2016-09-15/">
<requestId>cfb09950-45e2-472d-a6a9-example</requestId>
<fleetId>{{ fleet_id }}</fleetId>
<activeInstanceSet>
{% for i in instances %}
<item>
<instanceId>{{ i.instance.id }}</instanceId>
{% if i.id %}
<spotInstanceRequestId>{{ i.id }}</spotInstanceRequestId>
{% endif %}
<instanceType>{{ i.instance.instance_type }}</instanceType>
<instanceHealth>healthy</instanceHealth>
</item>
{% endfor %}
</activeInstanceSet>
</DescribeFleetInstancesResponse>
"""
DELETE_FLEETS_TEMPLATE = """<DeleteFleetResponse xmlns="http://ec2.amazonaws.com/doc/2016-09-15/">
<requestId>e12d2fe5-6503-4b4b-911c-example</requestId>
<unsuccessfulFleetDeletionSet/>
<successfulFleetDeletionSet>
{% for fleet in fleets %}
<item>
<fleetId>{{ fleet.id }}</fleetId>
<currentFleetState>{{ fleet.state }}</currentFleetState>
<previousFleetState>active</previousFleetState>
</item>
{% endfor %}
</successfulFleetDeletionSet>
</DeleteFleetResponse>"""

View File

@ -19,6 +19,7 @@ EC2_RESOURCE_TO_PREFIX = {
"transit-gateway-route-table": "tgw-rtb",
"transit-gateway-attachment": "tgw-attach",
"dhcp-options": "dopt",
"fleet": "fleet",
"flow-logs": "fl",
"image": "ami",
"instance": "i",
@ -90,6 +91,10 @@ def random_security_group_rule_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX["security-group-rule"], size=17)
def random_fleet_id():
return f"fleet-{random_resource_id(size=8)}-{random_resource_id(size=4)}-{random_resource_id(size=4)}-{random_resource_id(size=4)}-{random_resource_id(size=12)}"
def random_flow_log_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX["flow-logs"])

View File

@ -0,0 +1,663 @@
import boto3
import sure # noqa # pylint: disable=unused-import
import pytest
from moto import mock_ec2
from tests import EXAMPLE_AMI_ID
from uuid import uuid4
def get_subnet_id(conn):
vpc = conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
subnet = conn.create_subnet(
VpcId=vpc["VpcId"], CidrBlock="10.0.0.0/16", AvailabilityZone="us-east-1a"
)["Subnet"]
subnet_id = subnet["SubnetId"]
return subnet_id
def get_launch_template(conn, instance_type="t2.micro"):
launch_template = conn.create_launch_template(
LaunchTemplateName="test" + str(uuid4()),
LaunchTemplateData={
"ImageId": EXAMPLE_AMI_ID,
"InstanceType": instance_type,
"KeyName": "test",
"SecurityGroups": ["sg-123456"],
"DisableApiTermination": False,
"TagSpecifications": [
{
"ResourceType": "instance",
"Tags": [
{"Key": "test", "Value": "value"},
{"Key": "Name", "Value": "test"},
],
}
],
},
)["LaunchTemplate"]
launch_template_id = launch_template["LaunchTemplateId"]
launch_template_name = launch_template["LaunchTemplateName"]
return launch_template_id, launch_template_name
@mock_ec2
def test_create_spot_fleet_with_lowest_price():
conn = boto3.client("ec2", region_name="us-west-2")
launch_template_id, _ = get_launch_template(conn)
fleet_res = conn.create_fleet(
ExcessCapacityTerminationPolicy="terminate",
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 0,
"SpotTargetCapacity": 1,
"TotalTargetCapacity": 1,
},
SpotOptions={
"AllocationStrategy": "lowest-price",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
fleet_id.should.be.a(str)
fleet_id.should.have.length_of(42)
fleets_res = conn.describe_fleets(FleetIds=[fleet_id])
fleets_res.should.have.key("Fleets")
fleets = fleets_res["Fleets"]
fleet = fleets[0]
fleet["FleetState"].should.equal("active")
fleet_config = fleet["LaunchTemplateConfigs"][0]
launch_template_spec = fleet_config["LaunchTemplateSpecification"]
launch_template_spec["LaunchTemplateId"].should.equal(launch_template_id)
launch_template_spec["Version"].should.equal("1")
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
len(instances).should.equal(1)
@mock_ec2
def test_create_on_demand_fleet():
conn = boto3.client("ec2", region_name="us-west-2")
launch_template_id, _ = get_launch_template(conn)
fleet_res = conn.create_fleet(
ExcessCapacityTerminationPolicy="terminate",
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "on-demand",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 0,
"TotalTargetCapacity": 1,
},
OnDemandOptions={
"AllocationStrategy": "lowestPrice",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
fleet_id.should.be.a(str)
fleet_id.should.have.length_of(42)
fleets_res = conn.describe_fleets(FleetIds=[fleet_id])
fleets_res.should.have.key("Fleets")
fleets = fleets_res["Fleets"]
fleet = fleets[0]
fleet["FleetState"].should.equal("active")
fleet_config = fleet["LaunchTemplateConfigs"][0]
launch_template_spec = fleet_config["LaunchTemplateSpecification"]
launch_template_spec["LaunchTemplateId"].should.equal(launch_template_id)
launch_template_spec["Version"].should.equal("1")
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
len(instances).should.equal(1)
@mock_ec2
def test_create_diversified_spot_fleet():
conn = boto3.client("ec2", region_name="us-west-2")
launch_template_id_1, _ = get_launch_template(conn, instance_type="t2.small")
launch_template_id_2, _ = get_launch_template(conn, instance_type="t2.large")
fleet_res = conn.create_fleet(
ExcessCapacityTerminationPolicy="terminate",
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id_1,
"Version": "1",
},
},
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id_2,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 0,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 2,
},
SpotOptions={
"AllocationStrategy": "diversified",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
len(instances).should.equal(2)
instance_types = set([instance["InstanceType"] for instance in instances])
instance_types.should.equal(set(["t2.small", "t2.large"]))
instances[0]["InstanceId"].should.contain("i-")
@mock_ec2
@pytest.mark.parametrize(
"spot_allocation_strategy",
[
"diversified",
"lowest-price",
"capacity-optimized",
"capacity-optimized-prioritized",
],
)
@pytest.mark.parametrize(
"on_demand_allocation_strategy",
["lowestPrice", "prioritized"],
)
def test_request_fleet_using_launch_template_config__name(
spot_allocation_strategy, on_demand_allocation_strategy
):
conn = boto3.client("ec2", region_name="us-east-2")
_, launch_template_name = get_launch_template(conn, instance_type="t2.medium")
fleet_res = conn.create_fleet(
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateName": launch_template_name,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 3,
},
SpotOptions={
"AllocationStrategy": spot_allocation_strategy,
},
OnDemandOptions={
"AllocationStrategy": on_demand_allocation_strategy,
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
len(instances).should.equal(3)
instance_types = set([instance["InstanceType"] for instance in instances])
instance_types.should.equal(set(["t2.medium"]))
instances[0]["InstanceId"].should.contain("i-")
@mock_ec2
def test_create_fleet_request_with_tags():
conn = boto3.client("ec2", region_name="us-west-2")
launch_template_id, _ = get_launch_template(conn)
tags = [
{"Key": "Name", "Value": "test-fleet"},
{"Key": "Another", "Value": "tag"},
]
tags_instance = [
{"Key": "test", "Value": "value"},
{"Key": "Name", "Value": "test"},
]
fleet_res = conn.create_fleet(
DryRun=False,
SpotOptions={
"AllocationStrategy": "lowestPrice",
"InstanceInterruptionBehavior": "terminate",
},
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 3,
},
Type="request",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
TagSpecifications=[
{
"ResourceType": "fleet",
"Tags": tags,
},
],
)
fleet_id = fleet_res["FleetId"]
fleets = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"]
fleets[0]["Tags"].should.equal(tags)
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = conn.describe_instances(
InstanceIds=[i["InstanceId"] for i in instance_res["ActiveInstances"]]
)
for instance in instances["Reservations"][0]["Instances"]:
for tag in tags_instance:
instance["Tags"].should.contain(tag)
@mock_ec2
def test_create_fleet_using_launch_template_config__overrides():
conn = boto3.client("ec2", region_name="us-east-2")
subnet_id = get_subnet_id(conn)
template_id, _ = get_launch_template(conn, instance_type="t2.medium")
template_config_overrides = [
{
"InstanceType": "t2.nano",
"SubnetId": subnet_id,
"AvailabilityZone": "us-west-1",
"WeightedCapacity": 2,
}
]
fleet_res = conn.create_fleet(
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": template_id,
"Version": "1",
},
"Overrides": template_config_overrides,
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 0,
"TotalTargetCapacity": 1,
},
SpotOptions={
"AllocationStrategy": "lowest-price",
"InstanceInterruptionBehavior": "terminate",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
instances.should.have.length_of(1)
instances[0].should.have.key("InstanceType").equals("t2.nano")
instance = conn.describe_instances(
InstanceIds=[i["InstanceId"] for i in instances]
)["Reservations"][0]["Instances"][0]
instance.should.have.key("SubnetId").equals(subnet_id)
@mock_ec2
def test_delete_fleet():
conn = boto3.client("ec2", region_name="us-west-2")
launch_template_id, _ = get_launch_template(conn)
fleet_res = conn.create_fleet(
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 3,
},
SpotOptions={
"AllocationStrategy": "lowestPrice",
"InstanceInterruptionBehavior": "terminate",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
delete_fleet_out = conn.delete_fleets(FleetIds=[fleet_id], TerminateInstances=True)
delete_fleet_out["SuccessfulFleetDeletions"].should.have.length_of(1)
delete_fleet_out["SuccessfulFleetDeletions"][0]["FleetId"].should.equal(fleet_id)
delete_fleet_out["SuccessfulFleetDeletions"][0]["CurrentFleetState"].should.equal(
"deleted"
)
fleets = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"]
len(fleets).should.equal(1)
target_capacity_specification = fleets[0]["TargetCapacitySpecification"]
target_capacity_specification.should.have.key("TotalTargetCapacity").equals(0)
fleets[0]["FleetState"].should.equal("deleted")
# Instances should be terminated
instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
instances = instance_res["ActiveInstances"]
len(instances).should.equal(0)
@mock_ec2
def test_describe_fleet_instences_api():
conn = boto3.client("ec2", region_name="us-west-1")
launch_template_id, _ = get_launch_template(conn)
fleet_res = conn.create_fleet(
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 3,
},
SpotOptions={
"AllocationStrategy": "lowestPrice",
"InstanceInterruptionBehavior": "terminate",
},
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
fleet_res = conn.describe_fleet_instances(FleetId=fleet_id)
fleet_res["FleetId"].should.equal(fleet_id)
fleet_res["ActiveInstances"].should.have.length_of(3)
instance_ids = [i["InstanceId"] for i in fleet_res["ActiveInstances"]]
for instance_id in instance_ids:
instance_id.startswith("i-").should.be.true
instance_types = [i["InstanceType"] for i in fleet_res["ActiveInstances"]]
instance_types.should.equal(["t2.micro", "t2.micro", "t2.micro"])
instance_healths = [i["InstanceHealth"] for i in fleet_res["ActiveInstances"]]
instance_healths.should.equal(["healthy", "healthy", "healthy"])
@mock_ec2
def test_create_fleet_api():
conn = boto3.client("ec2", region_name="us-west-1")
launch_template_id, _ = get_launch_template(conn)
fleet_res = conn.create_fleet(
LaunchTemplateConfigs=[
{
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
},
],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "spot",
"OnDemandTargetCapacity": 1,
"SpotTargetCapacity": 2,
"TotalTargetCapacity": 3,
},
SpotOptions={
"AllocationStrategy": "lowestPrice",
"InstanceInterruptionBehavior": "terminate",
},
Type="instant",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_res.should.have.key("FleetId")
fleet_res["FleetId"].startswith("fleet-").should.be.true
fleet_res.should.have.key("Instances")
fleet_res["Instances"].should.have.length_of(3)
instance_ids = [i["InstanceIds"] for i in fleet_res["Instances"]]
for instance_id in instance_ids:
instance_id[0].startswith("i-").should.be.true
instance_types = [i["InstanceType"] for i in fleet_res["Instances"]]
instance_types.should.equal(["t2.micro", "t2.micro", "t2.micro"])
lifecycle = [i["Lifecycle"] for i in fleet_res["Instances"]]
lifecycle.should.contain("spot")
lifecycle.should.contain("on-demand")
@mock_ec2
def test_create_fleet_api_response():
conn = boto3.client("ec2", region_name="us-west-2")
subnet_id = get_subnet_id(conn)
launch_template_id, _ = get_launch_template(conn)
lt_config = {
"LaunchTemplateSpecification": {
"LaunchTemplateId": launch_template_id,
"Version": "1",
},
"Overrides": [
{
"AvailabilityZone": "us-west-1a",
"InstanceRequirements": {
"AcceleratorCount": {
"Max": 10,
"Min": 1,
},
"AcceleratorManufacturers": ["nvidia"],
"AcceleratorNames": ["t4"],
"AcceleratorTotalMemoryMiB": {
"Max": 20972,
"Min": 1,
},
"AcceleratorTypes": ["gpu"],
"BareMetal": "included",
"BaselineEbsBandwidthMbps": {
"Max": 10000,
"Min": 125,
},
"BurstablePerformance": "included",
"CpuManufacturers": ["amd", "intel"],
"ExcludedInstanceTypes": ["m5.8xlarge"],
"InstanceGenerations": ["current"],
"LocalStorage": "included",
"LocalStorageTypes": ["ssd"],
"MemoryGiBPerVCpu": {
"Min": 1,
"Max": 160,
},
"MemoryMiB": {
"Min": 2048,
"Max": 40960,
},
"NetworkInterfaceCount": {
"Max": 1,
"Min": 1,
},
"OnDemandMaxPricePercentageOverLowestPrice": 99999,
"RequireHibernateSupport": True,
"SpotMaxPricePercentageOverLowestPrice": 99999,
"TotalLocalStorageGB": {
"Min": 100,
"Max": 10000,
},
"VCpuCount": {
"Min": 2,
"Max": 160,
},
},
"MaxPrice": "0.5",
"Priority": 2,
"SubnetId": subnet_id,
"WeightedCapacity": 1,
},
],
}
fleet_res = conn.create_fleet(
ExcessCapacityTerminationPolicy="no-termination",
LaunchTemplateConfigs=[lt_config],
TargetCapacitySpecification={
"DefaultTargetCapacityType": "on-demand",
"OnDemandTargetCapacity": 10,
"SpotTargetCapacity": 10,
"TotalTargetCapacity": 30,
},
SpotOptions={
"AllocationStrategy": "lowest-price",
"InstanceInterruptionBehavior": "terminate",
"InstancePoolsToUseCount": 1,
"MaintenanceStrategies": {
"CapacityRebalance": {
"ReplacementStrategy": "launch-before-terminate",
"TerminationDelay": 120,
},
},
"MaxTotalPrice": "50",
"MinTargetCapacity": 1,
"SingleAvailabilityZone": True,
"SingleInstanceType": True,
},
OnDemandOptions={
"AllocationStrategy": "lowest-price",
"MaxTotalPrice": "50",
"MinTargetCapacity": 1,
"SingleAvailabilityZone": True,
"SingleInstanceType": True,
},
ReplaceUnhealthyInstances=True,
TerminateInstancesWithExpiration=True,
Type="maintain",
ValidFrom="2020-01-01T00:00:00Z",
ValidUntil="2020-12-31T00:00:00Z",
)
fleet_id = fleet_res["FleetId"]
fleet_res = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"]
fleet_res.should.have.length_of(1)
fleet_res[0].should.have.key("FleetId").equals(fleet_id)
fleet_res[0].should.have.key("ExcessCapacityTerminationPolicy").equals(
"no-termination"
)
fleet_res[0].should.have.key("LaunchTemplateConfigs").equals([lt_config])
fleet_res[0].should.have.key("TargetCapacitySpecification").equals(
{
"DefaultTargetCapacityType": "on-demand",
"OnDemandTargetCapacity": 10,
"SpotTargetCapacity": 10,
"TotalTargetCapacity": 30,
}
)
fleet_res[0].should.have.key("SpotOptions").equals(
{
"AllocationStrategy": "lowest-price",
"InstanceInterruptionBehavior": "terminate",
"InstancePoolsToUseCount": 1,
"MaintenanceStrategies": {
"CapacityRebalance": {
"ReplacementStrategy": "launch-before-terminate",
"TerminationDelay": 120,
},
},
"MaxTotalPrice": "50",
"MinTargetCapacity": 1,
"SingleAvailabilityZone": True,
"SingleInstanceType": True,
}
)
fleet_res[0].should.have.key("OnDemandOptions").equals(
{
"AllocationStrategy": "lowest-price",
"MaxTotalPrice": "50",
"MinTargetCapacity": 1,
"SingleAvailabilityZone": True,
"SingleInstanceType": True,
}
)
fleet_res[0].should.have.key("ReplaceUnhealthyInstances").equals(True)
fleet_res[0].should.have.key("TerminateInstancesWithExpiration").equals(True)
fleet_res[0].should.have.key("Type").equals("maintain")
fleet_res[0].should.have.key("ValidFrom")
fleet_res[0]["ValidFrom"].isoformat().should.equal("2020-01-01T00:00:00+00:00")
fleet_res[0].should.have.key("ValidUntil")
fleet_res[0]["ValidUntil"].isoformat().should.equal("2020-12-31T00:00:00+00:00")