EC2: Support Dedicated Hosts (#5878)

This commit is contained in:
Bert Blommers 2023-01-27 15:27:00 -01:00 committed by GitHub
parent 2f8a356b3f
commit 339309c9af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 414 additions and 8 deletions

View File

@ -1745,7 +1745,7 @@
- [X] accept_vpc_peering_connection - [X] accept_vpc_peering_connection
- [ ] advertise_byoip_cidr - [ ] advertise_byoip_cidr
- [X] allocate_address - [X] allocate_address
- [ ] allocate_hosts - [X] allocate_hosts
- [ ] allocate_ipam_pool_cidr - [ ] allocate_ipam_pool_cidr
- [ ] apply_security_groups_to_client_vpn_target_network - [ ] apply_security_groups_to_client_vpn_target_network
- [X] assign_ipv6_addresses - [X] assign_ipv6_addresses
@ -1982,7 +1982,7 @@
- [ ] describe_fpga_images - [ ] describe_fpga_images
- [ ] describe_host_reservation_offerings - [ ] describe_host_reservation_offerings
- [ ] describe_host_reservations - [ ] describe_host_reservations
- [ ] describe_hosts - [X] describe_hosts
- [X] describe_iam_instance_profile_associations - [X] describe_iam_instance_profile_associations
- [ ] describe_id_format - [ ] describe_id_format
- [ ] describe_identity_id_format - [ ] describe_identity_id_format
@ -2193,7 +2193,7 @@
- [ ] modify_ebs_default_kms_key_id - [ ] modify_ebs_default_kms_key_id
- [ ] modify_fleet - [ ] modify_fleet
- [ ] modify_fpga_image_attribute - [ ] modify_fpga_image_attribute
- [ ] modify_hosts - [X] modify_hosts
- [ ] modify_id_format - [ ] modify_id_format
- [ ] modify_identity_id_format - [ ] modify_identity_id_format
- [ ] modify_image_attribute - [ ] modify_image_attribute
@ -2267,7 +2267,7 @@
- [ ] reject_vpc_endpoint_connections - [ ] reject_vpc_endpoint_connections
- [X] reject_vpc_peering_connection - [X] reject_vpc_peering_connection
- [X] release_address - [X] release_address
- [ ] release_hosts - [X] release_hosts
- [ ] release_ipam_pool_allocation - [ ] release_ipam_pool_allocation
- [X] replace_iam_instance_profile_association - [X] replace_iam_instance_profile_association
- [X] replace_network_acl_association - [X] replace_network_acl_association

View File

@ -36,7 +36,7 @@ ec2
- [X] accept_vpc_peering_connection - [X] accept_vpc_peering_connection
- [ ] advertise_byoip_cidr - [ ] advertise_byoip_cidr
- [X] allocate_address - [X] allocate_address
- [ ] allocate_hosts - [X] allocate_hosts
- [ ] allocate_ipam_pool_cidr - [ ] allocate_ipam_pool_cidr
- [ ] apply_security_groups_to_client_vpn_target_network - [ ] apply_security_groups_to_client_vpn_target_network
- [X] assign_ipv6_addresses - [X] assign_ipv6_addresses
@ -281,7 +281,11 @@ ec2
- [ ] describe_fpga_images - [ ] describe_fpga_images
- [ ] describe_host_reservation_offerings - [ ] describe_host_reservation_offerings
- [ ] describe_host_reservations - [ ] describe_host_reservations
- [ ] describe_hosts - [X] describe_hosts
Pagination is not yet implemented
- [X] describe_iam_instance_profile_associations - [X] describe_iam_instance_profile_associations
- [ ] describe_id_format - [ ] describe_id_format
- [ ] describe_identity_id_format - [ ] describe_identity_id_format
@ -511,7 +515,7 @@ ec2
- [ ] modify_ebs_default_kms_key_id - [ ] modify_ebs_default_kms_key_id
- [ ] modify_fleet - [ ] modify_fleet
- [ ] modify_fpga_image_attribute - [ ] modify_fpga_image_attribute
- [ ] modify_hosts - [X] modify_hosts
- [ ] modify_id_format - [ ] modify_id_format
- [ ] modify_identity_id_format - [ ] modify_identity_id_format
- [ ] modify_image_attribute - [ ] modify_image_attribute
@ -589,7 +593,7 @@ ec2
- [ ] reject_vpc_endpoint_connections - [ ] reject_vpc_endpoint_connections
- [X] reject_vpc_peering_connection - [X] reject_vpc_peering_connection
- [X] release_address - [X] release_address
- [ ] release_hosts - [X] release_hosts
- [ ] release_ipam_pool_allocation - [ ] release_ipam_pool_allocation
- [X] replace_iam_instance_profile_association - [X] replace_iam_instance_profile_association
- [X] replace_network_acl_association - [X] replace_network_acl_association

View File

@ -12,6 +12,7 @@ from .dhcp_options import DHCPOptionsSetBackend
from .elastic_block_store import EBSBackend from .elastic_block_store import EBSBackend
from .elastic_ip_addresses import ElasticAddressBackend from .elastic_ip_addresses import ElasticAddressBackend
from .elastic_network_interfaces import NetworkInterfaceBackend from .elastic_network_interfaces import NetworkInterfaceBackend
from .hosts import HostsBackend
from .fleets import FleetsBackend from .fleets import FleetsBackend
from .flow_logs import FlowLogsBackend from .flow_logs import FlowLogsBackend
from .key_pairs import KeyPairBackend from .key_pairs import KeyPairBackend
@ -124,6 +125,7 @@ class EC2Backend(
CarrierGatewayBackend, CarrierGatewayBackend,
FleetsBackend, FleetsBackend,
WindowsBackend, WindowsBackend,
HostsBackend,
): ):
""" """
moto includes a limited set of AMIs in `moto/ec2/resources/amis.json`. moto includes a limited set of AMIs in `moto/ec2/resources/amis.json`.

102
moto/ec2/models/hosts.py Normal file
View File

@ -0,0 +1,102 @@
from .core import TaggedEC2Resource
from ..utils import generic_filter, random_dedicated_host_id
from typing import Any, Dict, List
class Host(TaggedEC2Resource):
def __init__(
self,
host_recovery: str,
zone: str,
instance_type: str,
instance_family: str,
auto_placement: str,
backend: Any,
):
self.id = random_dedicated_host_id()
self.state = "available"
self.host_recovery = host_recovery or "off"
self.zone = zone
self.instance_type = instance_type
self.instance_family = instance_family
self.auto_placement = auto_placement or "on"
self.ec2_backend = backend
def release(self) -> None:
self.state = "released"
def get_filter_value(self, key):
if key == "availability-zone":
return self.zone
if key == "state":
return self.state
if key == "tag-key":
return [t["key"] for t in self.get_tags()]
if key == "instance-type":
return self.instance_type
return None
class HostsBackend:
def __init__(self):
self.hosts = {}
def allocate_hosts(
self,
quantity: int,
host_recovery: str,
zone: str,
instance_type: str,
instance_family: str,
auto_placement: str,
tags: Dict[str, str],
) -> List[str]:
hosts = [
Host(
host_recovery,
zone,
instance_type,
instance_family,
auto_placement,
self,
)
for _ in range(quantity)
]
for host in hosts:
self.hosts[host.id] = host
if tags:
host.add_tags(tags)
return [host.id for host in hosts]
def describe_hosts(
self, host_ids: List[str], filters: Dict[str, Any]
) -> List[Host]:
"""
Pagination is not yet implemented
"""
results = self.hosts.values()
if host_ids:
results = [r for r in results if r.id in host_ids]
if filters:
results = generic_filter(filters, results)
return results
def modify_hosts(
self, host_ids, auto_placement, host_recovery, instance_type, instance_family
):
for _id in host_ids:
host = self.hosts[_id]
if auto_placement is not None:
host.auto_placement = auto_placement
if host_recovery is not None:
host.host_recovery = host_recovery
if instance_type is not None:
host.instance_type = instance_type
host.instance_family = None
if instance_family is not None:
host.instance_family = instance_family
host.instance_type = None
def release_hosts(self, host_ids: List[str]) -> None:
for host_id in host_ids:
self.hosts[host_id].release()

View File

@ -9,6 +9,7 @@ from .elastic_ip_addresses import ElasticIPAddresses
from .elastic_network_interfaces import ElasticNetworkInterfaces from .elastic_network_interfaces import ElasticNetworkInterfaces
from .fleets import Fleets from .fleets import Fleets
from .general import General from .general import General
from .hosts import HostsResponse
from .instances import InstanceResponse from .instances import InstanceResponse
from .internet_gateways import InternetGateways from .internet_gateways import InternetGateways
from .egress_only_internet_gateways import EgressOnlyInternetGateway from .egress_only_internet_gateways import EgressOnlyInternetGateway
@ -55,6 +56,7 @@ class EC2Response(
ElasticNetworkInterfaces, ElasticNetworkInterfaces,
Fleets, Fleets,
General, General,
HostsResponse,
InstanceResponse, InstanceResponse,
InternetGateways, InternetGateways,
EgressOnlyInternetGateway, EgressOnlyInternetGateway,

118
moto/ec2/responses/hosts.py Normal file
View File

@ -0,0 +1,118 @@
from ._base_response import EC2BaseResponse
class HostsResponse(EC2BaseResponse):
def allocate_hosts(self):
params = self._get_params()
quantity = int(params.get("Quantity"))
host_recovery = params.get("HostRecovery")
zone = params.get("AvailabilityZone")
instance_type = params.get("InstanceType")
instance_family = params.get("InstanceFamily")
auto_placement = params.get("AutoPlacement")
tags = self._parse_tag_specification()
host_tags = tags.get("dedicated-host", {})
host_ids = self.ec2_backend.allocate_hosts(
quantity,
host_recovery,
zone,
instance_type,
instance_family,
auto_placement,
host_tags,
)
template = self.response_template(EC2_ALLOCATE_HOSTS)
return template.render(host_ids=host_ids)
def describe_hosts(self):
host_ids = list(self._get_params().get("HostId", {}).values())
filters = self._filters_from_querystring()
hosts = self.ec2_backend.describe_hosts(host_ids, filters)
template = self.response_template(EC2_DESCRIBE_HOSTS)
return template.render(account_id=self.current_account, hosts=hosts)
def modify_hosts(self):
params = self._get_params()
host_ids = list(self._get_params().get("HostId", {}).values())
auto_placement = params.get("AutoPlacement")
host_recovery = params.get("HostRecovery")
instance_type = params.get("InstanceType")
instance_family = params.get("InstanceFamily")
self.ec2_backend.modify_hosts(
host_ids, auto_placement, host_recovery, instance_type, instance_family
)
template = self.response_template(EC2_MODIFY_HOSTS)
return template.render(host_ids=host_ids)
def release_hosts(self):
host_ids = list(self._get_params().get("HostId", {}).values())
self.ec2_backend.release_hosts(host_ids)
template = self.response_template(EC2_RELEASE_HOSTS)
return template.render(host_ids=host_ids)
EC2_ALLOCATE_HOSTS = """<AllocateHostsResult xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
<hostIdSet>
{% for host_id in host_ids %}
<item>{{ host_id }}</item>
{% endfor %}
</hostIdSet>
</AllocateHostsResult>"""
EC2_DESCRIBE_HOSTS = """<DescribeHostsResult xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
<hostSet>
{% for host in hosts %}
<item>
<autoPlacement>{{ host.auto_placement }}</autoPlacement>
<availabilityZone>{{ host.zone }}</availabilityZone>
<availableCapacity></availableCapacity>
<hostId>{{ host.id }}</hostId>
<state>{{ host.state }}</state>
<hostProperties>
{% if host.instance_type %}
<instanceType>{{ host.instance_type }}</instanceType>
{% endif %}
{% if host.instance_family %}
<instanceFamily>{{ host.instance_family }}</instanceFamily>
{% endif %}
</hostProperties>
<hostReservationId>reserv_id</hostReservationId>
<instances>
</instances>
<ownerId>{{ account_id }}</ownerId>
<hostRecovery>{{ host.host_recovery }}</hostRecovery>
<tagSet>
{% for tag in host.get_tags() %}
<item>
<key>{{ tag.key }}</key>
<value>{{ tag.value }}</value>
</item>
{% endfor %}
</tagSet>
</item>
{% endfor %}
</hostSet>
</DescribeHostsResult>"""
EC2_MODIFY_HOSTS = """<ModifyHostsResult xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
<successful>
{% for host_id in host_ids %}
<item>{{ host_id }}</item>
{% endfor %}
</successful>
</ModifyHostsResult>"""
EC2_RELEASE_HOSTS = """<ReleaseHostsResult xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
<requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
<successful>
{% for host_id in host_ids %}
<item>{{ host_id }}</item>
{% endfor %}
</successful>
</ReleaseHostsResult>"""

View File

@ -17,6 +17,7 @@ EC2_RESOURCE_TO_PREFIX = {
"transit-gateway": "tgw", "transit-gateway": "tgw",
"transit-gateway-route-table": "tgw-rtb", "transit-gateway-route-table": "tgw-rtb",
"transit-gateway-attachment": "tgw-attach", "transit-gateway-attachment": "tgw-attach",
"dedicated_host": "h",
"dhcp-options": "dopt", "dhcp-options": "dopt",
"fleet": "fleet", "fleet": "fleet",
"flow-logs": "fl", "flow-logs": "fl",
@ -238,6 +239,10 @@ def random_public_ip():
return f"54.214.{random.choice(range(255))}.{random.choice(range(255))}" return f"54.214.{random.choice(range(255))}.{random.choice(range(255))}"
def random_dedicated_host_id():
return random_id(prefix=EC2_RESOURCE_TO_PREFIX["dedicated_host"], size=17)
def random_private_ip(cidr=None, ipv6=False): def random_private_ip(cidr=None, ipv6=False):
# prefix - ula.prefixlen : get number of remaing length for the IP. # prefix - ula.prefixlen : get number of remaing length for the IP.
# prefix will be 32 for IPv4 and 128 for IPv6. # prefix will be 32 for IPv4 and 128 for IPv6.

View File

@ -118,6 +118,11 @@ dynamodb:
ec2: ec2:
- TestAccEC2AvailabilityZonesDataSource_ - TestAccEC2AvailabilityZonesDataSource_
- TestAccEC2CarrierGateway_ - TestAccEC2CarrierGateway_
- TestAccEC2HostDataSource_
- TestAccEC2Host_basic
- TestAccEC2Host_disappears
- TestAccEC2Host_instanceFamily
- TestAccEC2Host_tags
- TestAccEC2InstanceTypeOfferingDataSource_ - TestAccEC2InstanceTypeOfferingDataSource_
- TestAccEC2InstanceTypeOfferingsDataSource_ - TestAccEC2InstanceTypeOfferingsDataSource_
- TestAccEC2RouteTableAssociation_ - TestAccEC2RouteTableAssociation_

View File

@ -0,0 +1,168 @@
import boto3
from moto import mock_ec2
from uuid import uuid4
@mock_ec2
def test_allocate_hosts():
client = boto3.client("ec2", "us-west-1")
resp = client.allocate_hosts(
AvailabilityZone="us-west-1a",
InstanceType="a1.small",
HostRecovery="off",
AutoPlacement="on",
Quantity=3,
)
resp["HostIds"].should.have.length_of(3)
@mock_ec2
def test_describe_hosts_with_instancefamily():
client = boto3.client("ec2", "us-west-1")
host_ids = client.allocate_hosts(
AvailabilityZone="us-west-1a", InstanceFamily="c5", Quantity=1
)["HostIds"]
host = client.describe_hosts(HostIds=host_ids)["Hosts"][0]
host.should.have.key("HostProperties").should.have.key("InstanceFamily").equals(
"c5"
)
@mock_ec2
def test_describe_hosts():
client = boto3.client("ec2", "us-west-1")
host_ids = client.allocate_hosts(
AvailabilityZone="us-west-1c",
InstanceType="a1.large",
HostRecovery="on",
AutoPlacement="off",
Quantity=2,
)["HostIds"]
hosts = client.describe_hosts(HostIds=host_ids)["Hosts"]
hosts.should.have.length_of(2)
hosts[0].should.have.key("State").equals("available")
hosts[0].should.have.key("AvailabilityZone").equals("us-west-1c")
hosts[0].should.have.key("HostRecovery").equals("on")
hosts[0].should.have.key("HostProperties").should.have.key("InstanceType").equals(
"a1.large"
)
hosts[0].should.have.key("AutoPlacement").equals("off")
@mock_ec2
def test_describe_hosts_with_tags():
client = boto3.client("ec2", "us-west-1")
tagkey = str(uuid4())
host_ids = client.allocate_hosts(
AvailabilityZone="us-west-1b",
InstanceType="b1.large",
Quantity=1,
TagSpecifications=[
{"ResourceType": "dedicated-host", "Tags": [{"Key": tagkey, "Value": "v1"}]}
],
)["HostIds"]
host = client.describe_hosts(HostIds=host_ids)["Hosts"][0]
host.should.have.key("Tags").equals([{"Key": tagkey, "Value": "v1"}])
client.allocate_hosts(
AvailabilityZone="us-west-1a", InstanceType="b1.large", Quantity=1
)
hosts = client.describe_hosts(Filters=[{"Name": "tag-key", "Values": [tagkey]}])[
"Hosts"
]
hosts.should.have.length_of(1)
@mock_ec2
def test_describe_hosts_using_filters():
client = boto3.client("ec2", "us-west-1")
host_id1 = client.allocate_hosts(
AvailabilityZone="us-west-1a", InstanceType="b1.large", Quantity=1
)["HostIds"][0]
host_id2 = client.allocate_hosts(
AvailabilityZone="us-west-1b", InstanceType="b1.large", Quantity=1
)["HostIds"][0]
hosts = client.describe_hosts(
Filters=[{"Name": "availability-zone", "Values": ["us-west-1b"]}]
)["Hosts"]
[h["HostId"] for h in hosts].should.contain(host_id2)
hosts = client.describe_hosts(
Filters=[{"Name": "availability-zone", "Values": ["us-west-1d"]}]
)["Hosts"]
hosts.should.have.length_of(0)
client.release_hosts(HostIds=[host_id1])
hosts = client.describe_hosts(Filters=[{"Name": "state", "Values": ["released"]}])[
"Hosts"
]
[h["HostId"] for h in hosts].should.contain(host_id1)
hosts = client.describe_hosts(
Filters=[{"Name": "state", "Values": ["under-assessment"]}]
)["Hosts"]
hosts.should.have.length_of(0)
@mock_ec2
def test_modify_hosts():
client = boto3.client("ec2", "us-west-1")
host_ids = client.allocate_hosts(
AvailabilityZone="us-west-1a", InstanceFamily="c5", Quantity=1
)["HostIds"]
client.modify_hosts(
HostIds=host_ids,
AutoPlacement="off",
HostRecovery="on",
InstanceType="c5.medium",
)
host = client.describe_hosts(HostIds=host_ids)["Hosts"][0]
host.should.have.key("AutoPlacement").equals("off")
host.should.have.key("HostRecovery").equals("on")
host.should.have.key("HostProperties").shouldnt.have.key("InstanceFamily")
host.should.have.key("HostProperties").should.have.key("InstanceType").equals(
"c5.medium"
)
@mock_ec2
def test_release_hosts():
client = boto3.client("ec2", "us-west-1")
host_ids = client.allocate_hosts(
AvailabilityZone="us-west-1a",
InstanceType="a1.small",
HostRecovery="off",
AutoPlacement="on",
Quantity=2,
)["HostIds"]
resp = client.release_hosts(HostIds=[host_ids[0]])
resp.should.have.key("Successful").equals([host_ids[0]])
host = client.describe_hosts(HostIds=[host_ids[0]])["Hosts"][0]
host.should.have.key("State").equals("released")
@mock_ec2
def test_add_tags_to_dedicated_hosts():
client = boto3.client("ec2", "us-west-1")
resp = client.allocate_hosts(
AvailabilityZone="us-west-1a", InstanceType="a1.small", Quantity=1
)
host_id = resp["HostIds"][0]
client.create_tags(Resources=[host_id], Tags=[{"Key": "k1", "Value": "v1"}])
host = client.describe_hosts(HostIds=[host_id])["Hosts"][0]
host.should.have.key("Tags").equals([{"Key": "k1", "Value": "v1"}])