Directory Service, Route53Resolver: Delete network interfaces created during initialization (#4629)

This commit is contained in:
kbalk 2021-11-24 13:22:38 -05:00 committed by GitHub
parent 9a8be5ce28
commit dd0441d36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 99 additions and 41 deletions

View File

@ -79,26 +79,31 @@ class Directory(BaseModel): # pylint: disable=too-many-instance-attributes
self.stage = "Active"
self.launch_time = datetime.now(timezone.utc).isoformat()
self.stage_last_updated_date_time = datetime.now(timezone.utc).isoformat()
# Create a security group and ENI, returning the IPs for the ENI.
subnet_ips = self.create_eni()
if self.directory_type != "ADConnector":
self.dns_ip_addrs = subnet_ips
else:
if self.directory_type == "ADConnector":
self.security_group_id = self.create_security_group(
self.connect_settings["VpcId"]
)
self.eni_ids, self.subnet_ips = self.create_eni(
self.security_group_id, self.connect_settings["SubnetIds"]
)
self.connect_settings["SecurityGroupId"] = self.security_group_id
self.connect_settings["ConnectIps"] = self.subnet_ips
self.dns_ip_addrs = self.connect_settings["CustomerDnsIps"]
self.connect_settings["ConnectIps"] = subnet_ips
def create_eni(self):
"""Return IP addrs after creating an ENI for each subnet."""
if self.vpc_settings:
vpc_id = self.vpc_settings["VpcId"]
subnet_ids = self.vpc_settings["SubnetIds"]
else:
vpc_id = self.connect_settings["VpcId"]
subnet_ids = self.connect_settings["SubnetIds"]
self.security_group_id = self.create_security_group(
self.vpc_settings["VpcId"]
)
self.eni_ids, self.subnet_ips = self.create_eni(
self.security_group_id, self.vpc_settings["SubnetIds"]
)
self.vpc_settings["SecurityGroupId"] = self.security_group_id
self.dns_ip_addrs = self.subnet_ips
# Need a security group for the ENI.
security_group = ec2_backends[self.region].create_security_group(
def create_security_group(self, vpc_id):
"""Create security group for the network interface."""
security_group_info = ec2_backends[self.region].create_security_group(
name=f"{self.directory_id}_controllers",
description=(
f"AWS created security group for {self.directory_id} "
@ -106,17 +111,31 @@ class Directory(BaseModel): # pylint: disable=too-many-instance-attributes
),
vpc_id=vpc_id,
)
return security_group_info.id
ip_addrs = []
def delete_security_group(self):
"""Delete the given security group."""
ec2_backends[self.region].delete_security_group(group_id=self.security_group_id)
def create_eni(self, security_group_id, subnet_ids):
"""Return ENI ids and primary addresses created for each subnet."""
eni_ids = []
subnet_ips = []
for subnet_id in subnet_ids:
eni_info = ec2_backends[self.region].create_network_interface(
subnet=subnet_id,
private_ip_address=None,
group_ids=[security_group.id],
group_ids=[security_group_id],
description=f"AWS created network interface for {self.directory_id}",
)
ip_addrs.append(eni_info.private_ip_address)
return ip_addrs
eni_ids.append(eni_info.id)
subnet_ips.append(eni_info.private_ip_address)
return eni_ids, subnet_ips
def delete_eni(self):
"""Delete ENI for each subnet and the security group."""
for eni_id in self.eni_ids:
ec2_backends[self.region].delete_network_interface(eni_id)
def update_alias(self, alias):
"""Change default alias to given alias."""
@ -127,26 +146,37 @@ class Directory(BaseModel): # pylint: disable=too-many-instance-attributes
"""Enable/disable sso based on whether new_state is True or False."""
self.sso_enabled = new_state
def to_json(self):
"""Convert the attributes into json with CamelCase tags."""
replacement_keys = {"directory_type": "Type"}
exclude_items = ["password"]
def to_dict(self):
"""Create a dictionary of attributes for Directory."""
attributes = {
"AccessUrl": self.access_url,
"Alias": self.alias,
"DirectoryId": self.directory_id,
"DesiredNumberOfDomainControllers": self.desired_number_of_domain_controllers,
"DnsIpAddrs": self.dns_ip_addrs,
"LaunchTime": self.launch_time,
"Name": self.name,
"SsoEnabled": self.sso_enabled,
"Stage": self.stage,
"StageLastUpdatedDateTime": self.stage_last_updated_date_time,
"Type": self.directory_type,
}
json_result = {}
for item, value in self.__dict__.items():
# Discard empty strings, but allow values set to False or zero.
if value == "" or item in exclude_items:
continue
if self.edition:
attributes["Edition"] = self.edition
if self.size:
attributes["Size"] = self.size
if self.short_name:
attributes["ShortName"] = self.short_name
if self.description:
attributes["Description"] = self.description
if item in replacement_keys:
json_result[replacement_keys[item]] = value
else:
new_tag = "".join(x.title() for x in item.split("_"))
json_result[new_tag] = value
if json_result["ConnectSettings"]:
json_result["ConnectSettings"]["CustomerDnsIps"] = None
return json_result
if self.vpc_settings:
attributes["VpcSettings"] = self.vpc_settings
else:
attributes["ConnectSettings"] = self.connect_settings
attributes["ConnectSettings"]["CustomerDnsIps"] = None
return attributes
class DirectoryServiceBackend(BaseBackend):
@ -394,6 +424,8 @@ class DirectoryServiceBackend(BaseBackend):
def delete_directory(self, directory_id):
"""Delete directory with the matching ID."""
self._validate_directory_id(directory_id)
self.directories[directory_id].delete_eni()
self.directories[directory_id].delete_security_group()
self.tagger.delete_all_tags_for_resource(directory_id)
self.directories.pop(directory_id)
return directory_id

View File

@ -97,13 +97,13 @@ class DirectoryServiceResponse(BaseResponse):
next_token = self._get_param("NextToken")
limit = self._get_int_param("Limit")
try:
(descriptions, next_token) = self.ds_backend.describe_directories(
(directories, next_token) = self.ds_backend.describe_directories(
directory_ids, next_token=next_token, limit=limit
)
except InvalidToken as exc:
raise InvalidNextTokenException() from exc
response = {"DirectoryDescriptions": [x.to_json() for x in descriptions]}
response = {"DirectoryDescriptions": [x.to_dict() for x in directories]}
if next_token:
response["NextToken"] = next_token
return json.dumps(response)

View File

@ -187,6 +187,7 @@ class ResolverEndpoint(BaseModel): # pylint: disable=too-many-instance-attribut
# NOTE; This currently doesn't reflect IPv6 addresses.
self.subnets = self._build_subnet_info()
self.eni_ids = self.create_eni()
self.ip_address_count = len(ip_addresses)
self.host_vpc_id = self._vpc_id_from_subnet()
@ -232,9 +233,10 @@ class ResolverEndpoint(BaseModel): # pylint: disable=too-many-instance-attribut
def create_eni(self):
"""Create a VPC ENI for each combo of AZ, subnet and IP."""
eni_ids = []
for subnet, ip_info in self.subnets.items():
for ip_addr, eni_id in ip_info.items():
ec2_backends[self.region].create_network_interface(
eni_info = ec2_backends[self.region].create_network_interface(
description=f"Route 53 Resolver: {self.id}:{eni_id}",
group_ids=self.security_group_ids,
interface_type="interface",
@ -244,6 +246,13 @@ class ResolverEndpoint(BaseModel): # pylint: disable=too-many-instance-attribut
],
subnet=subnet,
)
eni_ids.append(eni_info.id)
return eni_ids
def delete_eni(self):
"""Delete the VPC ENI created for the subnet and IP combos."""
for eni_id in self.eni_ids:
ec2_backends[self.region].delete_network_interface(eni_id)
def description(self):
"""Return a dictionary of relevant info for this resolver endpoint."""
@ -466,7 +475,6 @@ class Route53ResolverBackend(BaseBackend):
ip_addresses,
name,
)
resolver_endpoint.create_eni()
self.resolver_endpoints[endpoint_id] = resolver_endpoint
self.tagger.tag_resource(resolver_endpoint.arn, tags or [])
@ -593,6 +601,7 @@ class Route53ResolverBackend(BaseBackend):
self.tagger.delete_all_tags_for_resource(resolver_endpoint_id)
resolver_endpoint = self.resolver_endpoints.pop(resolver_endpoint_id)
resolver_endpoint.delete_eni()
resolver_endpoint.status = "DELETING"
resolver_endpoint.status_message = resolver_endpoint.status_message.replace(
"Successfully created", "Deleting"

View File

@ -36,6 +36,16 @@ def test_ds_delete_directory():
result = client.delete_directory(DirectoryId=directory_id)
assert result["DirectoryId"] == directory_id
# Verify there are no dictionaries, network interfaces or associated
# security groups.
result = client.describe_directories()
assert len(result["DirectoryDescriptions"]) == 0
result = ec2_client.describe_network_interfaces()
assert len(result["NetworkInterfaces"]) == 0
result = ec2_client.describe_security_groups()
for group in result["SecurityGroups"]:
assert "directory controllers" not in group["Description"]
# Attempt to delete a non-existent directory.
nonexistent_id = f"d-{get_random_hex(10)}"
with pytest.raises(ClientError) as exc:
@ -116,6 +126,7 @@ def test_ds_describe_directories():
assert dir_info["Type"] == "SimpleAD"
assert dir_info["VpcSettings"]["VpcId"].startswith("vpc-")
assert len(dir_info["VpcSettings"]["SubnetIds"]) == 2
assert dir_info["VpcSettings"]["SecurityGroupId"].startswith("sg-")
assert len(dir_info["DnsIpAddrs"]) == 2
assert "NextToken" not in result

View File

@ -415,6 +415,12 @@ def test_route53resolver_delete_resolver_endpoint():
assert "Deleting" in endpoint["StatusMessage"]
assert endpoint["CreationTime"] == created_endpoint["CreationTime"]
# Verify there are no endpoints or no network interfaces.
response = client.list_resolver_endpoints()
assert len(response["ResolverEndpoints"]) == 0
result = ec2_client.describe_network_interfaces()
assert len(result["NetworkInterfaces"]) == 0
@mock_ec2
@mock_route53resolver