diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 6bfe877b1..06a73bc4f 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -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
diff --git a/docs/docs/services/ec2.rst b/docs/docs/services/ec2.rst
index 89052cfc7..65c5a6412 100644
--- a/docs/docs/services/ec2.rst
+++ b/docs/docs/services/ec2.rst
@@ -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
diff --git a/moto/ec2/models/__init__.py b/moto/ec2/models/__init__.py
index bca0dc1b5..37e480ed9 100644
--- a/moto/ec2/models/__init__.py
+++ b/moto/ec2/models/__init__.py
@@ -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.
diff --git a/moto/ec2/models/fleets.py b/moto/ec2/models/fleets.py
new file mode 100644
index 000000000..656506f7e
--- /dev/null
+++ b/moto/ec2/models/fleets.py
@@ -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
diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py
index 9424699b2..c66fec62a 100644
--- a/moto/ec2/models/instances.py
+++ b/moto/ec2/models/instances.py
@@ -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
diff --git a/moto/ec2/responses/__init__.py b/moto/ec2/responses/__init__.py
index 24be4b8f8..548d97188 100644
--- a/moto/ec2/responses/__init__.py
+++ b/moto/ec2/responses/__init__.py
@@ -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,
diff --git a/moto/ec2/responses/fleets.py b/moto/ec2/responses/fleets.py
new file mode 100644
index 000000000..dc8b31f56
--- /dev/null
+++ b/moto/ec2/responses/fleets.py
@@ -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 = """
+ 60262cc5-2bd4-4c8d-98ed-example
+ {{ request.id }}
+ {% if request.fleet_type == "instant" %}
+
+ {% for instance in request.on_demand_instances %}
+ -
+ {{ instance["instance"].instance_type }}
+ on-demand
+
+
- {{ instance["instance"].id }}
+
+
+ {% endfor %}
+ {% for instance in request.spot_requests %}
+ -
+ {{ instance.instance.instance_type }}
+ spot
+
+
- {{ instance.instance.id }}
+
+
+ {% endfor %}
+
+ {% endif %}
+"""
+
+DESCRIBE_FLEETS_TEMPLATE = """
+ 4d68a6cc-8f2e-4be1-b425-example
+
+ {% for request in requests %}
+ -
+ {{ request.id }}
+ {{ request.state }}
+ {{ request.excess_capacity_termination_policy }}
+ {{ request.fulfilled_capacity }}
+ {{ request.fulfilled_on_demand_capacity }}
+
+ {% for config in request.launch_template_configs %}
+
-
+
+ {{ config.LaunchTemplateSpecification.LaunchTemplateId }}
+ {{ config.LaunchTemplateSpecification.Version }}
+
+ {% if config.Overrides %}
+
+ {% for override in config.Overrides %}
+
-
+ {% if override.AvailabilityZone %}
+ {{ override.AvailabilityZone }}
+ {% endif %}
+ {% if override.InstanceType %}
+ {{ override.InstanceType }}
+ {% endif %}
+ {% if override.InstanceRequirements %}
+
+ {% if override.InstanceRequirements.AcceleratorCount %}
+
+ {% if override.InstanceRequirements.AcceleratorCount.Max %}
+ {{ override.InstanceRequirements.AcceleratorCount.Max }}
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorCount.Min %}
+ {{ override.InstanceRequirements.AcceleratorCount.Min }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorManufacturer %}
+
+ {% for manufacturer in override.InstanceRequirements.AcceleratorManufacturer %}
+
- {{ manufacturer }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorName %}
+
+ {% for name in override.InstanceRequirements.AcceleratorName %}
+ - {{ name }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorTotalMemoryMiB %}
+
+ {% if override.InstanceRequirements.AcceleratorTotalMemoryMiB.Max %}
+ {{ override.InstanceRequirements.AcceleratorTotalMemoryMiB.Max }}
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorTotalMemoryMiB.Min %}
+ {{ override.InstanceRequirements.AcceleratorTotalMemoryMiB.Min }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.AcceleratorType %}
+
+ {% for type in override.InstanceRequirements.AcceleratorType %}
+ - {{ type }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.BareMetal %}
+ {{ override.InstanceRequirements.BareMetal }}
+ {% endif %}
+ {% if override.InstanceRequirements.BaselineEbsBandwidthMbps %}
+
+ {% if override.InstanceRequirements.BaselineEbsBandwidthMbps.Min %}
+ {{ override.InstanceRequirements.BaselineEbsBandwidthMbps.Min }}
+ {% endif %}
+ {% if override.InstanceRequirements.BaselineEbsBandwidthMbps.Max %}
+ {{ override.InstanceRequirements.BaselineEbsBandwidthMbps.Max }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.BurstablePerformance %}
+ {{ override.InstanceRequirements.BurstablePerformance }}
+ {% endif %}
+ {% if override.InstanceRequirements.CpuManufacturer %}
+
+ {% for manufacturer in override.InstanceRequirements.CpuManufacturer %}
+ - {{ manufacturer }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.ExcludedInstanceType %}
+
+ {% for type in override.InstanceRequirements.ExcludedInstanceType %}
+ - {{ type }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.InstanceGeneration %}
+
+ {% for generation in override.InstanceRequirements.InstanceGeneration %}
+ - {{ generation }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.LocalStorage %}
+ {{ override.InstanceRequirements.LocalStorage }}
+ {% endif %}
+ {% if override.InstanceRequirements.LocalStorageType %}
+
+ {% for type in override.InstanceRequirements.LocalStorageType %}
+ - {{ type }}
+ {% endfor %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.MemoryGiBPerVCpu %}
+
+ {{ override.InstanceRequirements.MemoryGiBPerVCpu.Min }}
+ {{ override.InstanceRequirements.MemoryGiBPerVCpu.Max }}
+
+ {% endif %}
+ {% if override.InstanceRequirements.MemoryMiB %}
+
+ {% if override.InstanceRequirements.MemoryMiB.Min %}
+ {{ override.InstanceRequirements.MemoryMiB.Min }}
+ {% endif %}
+ {% if override.InstanceRequirements.MemoryMiB.Max %}
+ {{ override.InstanceRequirements.MemoryMiB.Max }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.NetworkInterfaceCount %}
+
+ {% if override.InstanceRequirements.NetworkInterfaceCount.Max %}
+ {{ override.InstanceRequirements.NetworkInterfaceCount.Max }}
+ {% endif %}
+ {% if override.InstanceRequirements.NetworkInterfaceCount.Min %}
+ {{ override.InstanceRequirements.NetworkInterfaceCount.Min }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.OnDemandMaxPricePercentageOverLowestPrice %}
+ {{ override.InstanceRequirements.OnDemandMaxPricePercentageOverLowestPrice }}
+ {% endif %}
+ {% if override.InstanceRequirements.RequireHibernateSupport %}
+ {{ override.InstanceRequirements.RequireHibernateSupport }}
+ {% endif %}
+ {% if override.InstanceRequirements.SpotMaxPricePercentageOverLowestPrice %}
+ {{ override.InstanceRequirements.SpotMaxPricePercentageOverLowestPrice }}
+ {% endif %}
+ {% if override.InstanceRequirements.TotalLocalStorageGB %}
+
+ {% if override.InstanceRequirements.TotalLocalStorageGB.Min %}
+ {{ override.InstanceRequirements.TotalLocalStorageGB.Min }}
+ {% endif %}
+ {% if override.InstanceRequirements.TotalLocalStorageGB.Max %}
+ {{ override.InstanceRequirements.TotalLocalStorageGB.Max }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.InstanceRequirements.VCpuCount %}
+
+ {% if override.InstanceRequirements.VCpuCount.Min %}
+ {{ override.InstanceRequirements.VCpuCount.Min }}
+ {% endif %}
+ {% if override.InstanceRequirements.VCpuCount.Max %}
+ {{ override.InstanceRequirements.VCpuCount.Max }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endif %}
+ {% if override.MaxPrice %}
+ {{ override.MaxPrice }}
+ {% endif %}
+ {% if override.Placement %}
+
+ {% if override.Placement.GroupName %}
+ {{ override.Placement.GroupName }}
+ {% endif %}
+
+ {% endif %}
+ {% if override.Priority %}
+ {{ override.Priority }}
+ {% endif %}
+ {% if override.SubnetId %}
+ {{ override.SubnetId }}
+ {% endif %}
+ {% if override.WeightedCapacity %}
+ {{ override.WeightedCapacity }}
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+ {{ request.target_capacity }}
+ {% if request.on_demand_target_capacity %}
+ {{ request.on_demand_target_capacity }}
+ {% endif %}
+ {% if request.spot_target_capacity %}
+ {{ request.spot_target_capacity }}
+ {% endif %}
+ {{ request.target_capacity_specification.DefaultTargetCapacityType }}
+
+ {% if request.spot_options %}
+
+ {% if request.spot_options.AllocationStrategy %}
+ {{ request.spot_options.AllocationStrategy }}
+ {% endif %}
+ {% if request.spot_options.InstanceInterruptionBehavior %}
+ {{ request.spot_options.InstanceInterruptionBehavior }}
+ {% endif %}
+ {% if request.spot_options.InstancePoolsToUseCount %}
+ {{ request.spot_options.InstancePoolsToUseCount }}
+ {% endif %}
+ {% if request.spot_options.MaintenanceStrategies %}
+
+ {% if request.spot_options.MaintenanceStrategies.CapacityRebalance %}
+
+ {% if request.spot_options.MaintenanceStrategies.CapacityRebalance.ReplacementStrategy %}
+ {{ request.spot_options.MaintenanceStrategies.CapacityRebalance.ReplacementStrategy }}
+ {% endif %}
+ {% if request.spot_options.MaintenanceStrategies.CapacityRebalance.TerminationDelay %}
+ {{ request.spot_options.MaintenanceStrategies.CapacityRebalance.TerminationDelay }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endif %}
+ {% if request.spot_options.MaxTotalPrice %}
+ {{ request.spot_options.MaxTotalPrice }}
+ {% endif %}
+ {% if request.spot_options.MinTargetCapacity %}
+ {{ request.spot_options.MinTargetCapacity }}
+ {% endif %}
+ {% if request.spot_options.SingleAvailabilityZone %}
+ {{ request.spot_options.SingleAvailabilityZone }}
+ {% endif %}
+ {% if request.spot_options.SingleInstanceType %}
+ {{ request.spot_options.SingleInstanceType }}
+ {% endif %}
+
+ {% endif %}
+
+ {% if request.on_demand_options %}
+
+ {% if request.on_demand_options.AllocationStrategy %}
+ {{ request.on_demand_options.AllocationStrategy }}
+ {% endif %}
+ {% if request.on_demand_options.MaxTotalPrice %}
+ {{ request.on_demand_options.MaxTotalPrice }}
+ {% endif %}
+ {% if request.on_demand_options.MinTargetCapacity %}
+ {{ request.on_demand_options.MinTargetCapacity }}
+ {% endif %}
+ {% if request.on_demand_options.SingleAvailabilityZone %}
+ {{ request.on_demand_options.SingleAvailabilityZone }}
+ {% endif %}
+ {% if request.on_demand_options.SingleInstanceType %}
+ {{ request.on_demand_options.SingleInstanceType }}
+ {% endif %}
+ {% if request.on_demand_options.CapacityReservationOptions %}
+
+ {% if request.on_demand_options.CapacityReservationOptions.UsageStrategy %}
+ {{ request.on_demand_options.CapacityReservationOptions.UsageStrategy }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endif %}
+ {{ request.terminate_instances_with_expiration }}
+ {{ request.fleet_type }}
+ {{ request.valid_from }}
+ {{ request.valid_until }}
+ {{ request.replace_unhealthy_instances }}
+
+ {% for tag in request.tags %}
+ -
+ {{ tag.key }}
+ {{ tag.value }}
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+"""
+
+DESCRIBE_FLEET_INSTANCES_TEMPLATE = """
+ cfb09950-45e2-472d-a6a9-example
+ {{ fleet_id }}
+
+ {% for i in instances %}
+ -
+ {{ i.instance.id }}
+ {% if i.id %}
+ {{ i.id }}
+ {% endif %}
+ {{ i.instance.instance_type }}
+ healthy
+
+ {% endfor %}
+
+
+"""
+
+DELETE_FLEETS_TEMPLATE = """
+ e12d2fe5-6503-4b4b-911c-example
+
+
+ {% for fleet in fleets %}
+ -
+ {{ fleet.id }}
+ {{ fleet.state }}
+ active
+
+ {% endfor %}
+
+"""
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
index 1a992586c..367c183dd 100644
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -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"])
diff --git a/tests/test_ec2/test_fleets.py b/tests/test_ec2/test_fleets.py
new file mode 100644
index 000000000..c97dffb65
--- /dev/null
+++ b/tests/test_ec2/test_fleets.py
@@ -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")