EC2: Add ec2 fleets basic api (#5367)
This commit is contained in:
parent
2d6f04d01c
commit
f743567789
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
313
moto/ec2/models/fleets.py
Normal 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
|
@ -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
|
||||
|
@ -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,
|
||||
|
417
moto/ec2/responses/fleets.py
Normal file
417
moto/ec2/responses/fleets.py
Normal 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>"""
|
@ -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"])
|
||||
|
||||
|
663
tests/test_ec2/test_fleets.py
Normal file
663
tests/test_ec2/test_fleets.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user