diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f625637..077d3697b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ Moto Changelog ============== +3.1.15 +----- +Docker Digest for 3.1.15: + + New Methods: + * Databrew: + * create_profile_job() + * create_recipe_job() + * delete_job() + * describe_job() + * list_jobs() + * update_profile_job() + * update_recipe_job() + * Glue: + * create_registry() + * Greengrass: + * create_group() + * create_group_version() + * delete_group() + * get_group() + * get_group_version() + * list_groups() + * list_group_versions() + * update_group() + * KMS: + * sign() + * verify() + * Route53Resolver: + * associate_resolver_endpoint_ip_address() + * disassociate_resolver_endpoint_ip_address() + 3.1.14 ----- Docker Digest for 3.1.14: _sha256:a8ad7f54d7c469e34454f6774f743251c02093c6b2d7e9d7961a5de366783e11_ diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 7333b46a0..83840c51a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -1289,7 +1289,7 @@ ## ds
-19% implemented +18% implemented - [ ] accept_shared_directory - [ ] add_ip_routes @@ -1320,6 +1320,7 @@ - [ ] describe_event_topics - [ ] describe_ldaps_settings - [ ] describe_regions +- [ ] describe_settings - [ ] describe_shared_directories - [ ] describe_snapshots - [ ] describe_trusts @@ -1352,6 +1353,7 @@ - [ ] update_conditional_forwarder - [ ] update_number_of_domain_controllers - [ ] update_radius +- [ ] update_settings - [ ] update_trust - [ ] verify_trust
@@ -2678,7 +2680,7 @@ ## glue
-12% implemented +13% implemented - [ ] batch_create_partition - [ ] batch_delete_connection @@ -2709,7 +2711,7 @@ - [ ] create_ml_transform - [ ] create_partition - [ ] create_partition_index -- [ ] create_registry +- [X] create_registry - [ ] create_schema - [ ] create_script - [ ] create_security_configuration @@ -2806,6 +2808,7 @@ - [ ] import_catalog_to_glue - [ ] list_blueprints - [X] list_crawlers +- [ ] list_crawls - [ ] list_custom_entity_types - [ ] list_dev_endpoints - [X] list_jobs @@ -2867,7 +2870,7 @@ ## greengrass
-43% implemented +52% implemented - [ ] associate_role_to_group - [ ] associate_service_role_to_account @@ -2880,9 +2883,9 @@ - [X] create_device_definition_version - [X] create_function_definition - [X] create_function_definition_version -- [ ] create_group +- [X] create_group - [ ] create_group_certificate_authority -- [ ] create_group_version +- [X] create_group_version - [ ] create_logger_definition - [ ] create_logger_definition_version - [X] create_resource_definition @@ -2894,7 +2897,7 @@ - [X] delete_core_definition - [X] delete_device_definition - [X] delete_function_definition -- [ ] delete_group +- [X] delete_group - [ ] delete_logger_definition - [X] delete_resource_definition - [X] delete_subscription_definition @@ -2912,10 +2915,10 @@ - [X] get_device_definition_version - [X] get_function_definition - [X] get_function_definition_version -- [ ] get_group +- [X] get_group - [ ] get_group_certificate_authority - [ ] get_group_certificate_configuration -- [ ] get_group_version +- [X] get_group_version - [ ] get_logger_definition - [ ] get_logger_definition_version - [X] get_resource_definition @@ -2936,8 +2939,8 @@ - [X] list_function_definition_versions - [X] list_function_definitions - [ ] list_group_certificate_authorities -- [ ] list_group_versions -- [ ] list_groups +- [X] list_group_versions +- [X] list_groups - [ ] list_logger_definition_versions - [ ] list_logger_definitions - [X] list_resource_definition_versions @@ -2955,7 +2958,7 @@ - [X] update_core_definition - [X] update_device_definition - [X] update_function_definition -- [ ] update_group +- [X] update_group - [ ] update_group_certificate_configuration - [ ] update_logger_definition - [X] update_resource_definition @@ -3529,7 +3532,7 @@ ## kms
-52% implemented +56% implemented - [X] cancel_key_deletion - [ ] connect_custom_key_store @@ -4813,10 +4816,10 @@ ## route53resolver
-26% implemented +30% implemented - [ ] associate_firewall_rule_group -- [ ] associate_resolver_endpoint_ip_address +- [X] associate_resolver_endpoint_ip_address - [ ] associate_resolver_query_log_config - [X] associate_resolver_rule - [ ] create_firewall_domain_list @@ -4832,7 +4835,7 @@ - [ ] delete_resolver_query_log_config - [X] delete_resolver_rule - [ ] disassociate_firewall_rule_group -- [ ] disassociate_resolver_endpoint_ip_address +- [X] disassociate_resolver_endpoint_ip_address - [ ] disassociate_resolver_query_log_config - [X] disassociate_resolver_rule - [ ] get_firewall_config @@ -6115,7 +6118,6 @@ - qldb-session - rbin - rds-data -- redshiftserverless - resiliencehub - robomaker - route53-recovery-cluster @@ -6163,4 +6165,4 @@ - workspaces - workspaces-web - xray -
+
\ No newline at end of file diff --git a/docs/docs/services/ds.rst b/docs/docs/services/ds.rst index ff8720ce6..15b32538e 100644 --- a/docs/docs/services/ds.rst +++ b/docs/docs/services/ds.rst @@ -70,6 +70,7 @@ ds - [ ] describe_event_topics - [ ] describe_ldaps_settings - [ ] describe_regions +- [ ] describe_settings - [ ] describe_shared_directories - [ ] describe_snapshots - [ ] describe_trusts @@ -112,6 +113,7 @@ ds - [ ] update_conditional_forwarder - [ ] update_number_of_domain_controllers - [ ] update_radius +- [ ] update_settings - [ ] update_trust - [ ] verify_trust diff --git a/docs/docs/services/glue.rst b/docs/docs/services/glue.rst index 134671c62..20cc99477 100644 --- a/docs/docs/services/glue.rst +++ b/docs/docs/services/glue.rst @@ -54,7 +54,7 @@ glue - [ ] create_ml_transform - [ ] create_partition - [ ] create_partition_index -- [ ] create_registry +- [X] create_registry - [ ] create_schema - [ ] create_script - [ ] create_security_configuration @@ -151,6 +151,7 @@ glue - [ ] import_catalog_to_glue - [ ] list_blueprints - [X] list_crawlers +- [ ] list_crawls - [ ] list_custom_entity_types - [ ] list_dev_endpoints - [X] list_jobs diff --git a/docs/docs/services/greengrass.rst b/docs/docs/services/greengrass.rst index d27ddab3b..eb049bcd1 100644 --- a/docs/docs/services/greengrass.rst +++ b/docs/docs/services/greengrass.rst @@ -36,9 +36,9 @@ greengrass - [X] create_device_definition_version - [X] create_function_definition - [X] create_function_definition_version -- [ ] create_group +- [X] create_group - [ ] create_group_certificate_authority -- [ ] create_group_version +- [X] create_group_version - [ ] create_logger_definition - [ ] create_logger_definition_version - [X] create_resource_definition @@ -50,7 +50,7 @@ greengrass - [X] delete_core_definition - [X] delete_device_definition - [X] delete_function_definition -- [ ] delete_group +- [X] delete_group - [ ] delete_logger_definition - [X] delete_resource_definition - [X] delete_subscription_definition @@ -68,10 +68,10 @@ greengrass - [X] get_device_definition_version - [X] get_function_definition - [X] get_function_definition_version -- [ ] get_group +- [X] get_group - [ ] get_group_certificate_authority - [ ] get_group_certificate_configuration -- [ ] get_group_version +- [X] get_group_version - [ ] get_logger_definition - [ ] get_logger_definition_version - [X] get_resource_definition @@ -92,8 +92,8 @@ greengrass - [X] list_function_definition_versions - [X] list_function_definitions - [ ] list_group_certificate_authorities -- [ ] list_group_versions -- [ ] list_groups +- [X] list_group_versions +- [X] list_groups - [ ] list_logger_definition_versions - [ ] list_logger_definitions - [X] list_resource_definition_versions @@ -111,7 +111,7 @@ greengrass - [X] update_core_definition - [X] update_device_definition - [X] update_function_definition -- [ ] update_group +- [X] update_group - [ ] update_group_certificate_configuration - [ ] update_logger_definition - [X] update_resource_definition diff --git a/docs/docs/services/kms.rst b/docs/docs/services/kms.rst index 7b878330b..a123a1656 100644 --- a/docs/docs/services/kms.rst +++ b/docs/docs/services/kms.rst @@ -68,13 +68,27 @@ kms - [X] retire_grant - [X] revoke_grant - [X] schedule_key_deletion -- [ ] sign +- [X] sign + Sign message using generated private key. + + - signing_algorithm is ignored and hardcoded to RSASSA_PSS_SHA_256 + + - grant_tokens are not implemented + + - [X] tag_resource - [X] untag_resource - [ ] update_alias - [ ] update_custom_key_store - [X] update_key_description - [ ] update_primary_region -- [ ] verify +- [X] verify + Verify message using public key from generated private key. + + - signing_algorithm is ignored and hardcoded to RSASSA_PSS_SHA_256 + + - grant_tokens are not implemented + + - [ ] verify_mac diff --git a/docs/docs/services/route53resolver.rst b/docs/docs/services/route53resolver.rst index 030f45695..576b4fdb9 100644 --- a/docs/docs/services/route53resolver.rst +++ b/docs/docs/services/route53resolver.rst @@ -28,16 +28,15 @@ route53resolver |start-h3| Implemented features for this service |end-h3| - [ ] associate_firewall_rule_group -- [ ] associate_resolver_endpoint_ip_address +- [X] associate_resolver_endpoint_ip_address - [ ] associate_resolver_query_log_config - [X] associate_resolver_rule - Return description for a newly created resolver rule association. - - [ ] create_firewall_domain_list - [ ] create_firewall_rule - [ ] create_firewall_rule_group - [X] create_resolver_endpoint - Return description for a newly created resolver endpoint. + + Return description for a newly created resolver endpoint. NOTE: IPv6 IPs are currently not being filtered when calculating the create_resolver_endpoint() IpAddresses. @@ -51,18 +50,12 @@ route53resolver - [ ] delete_firewall_rule - [ ] delete_firewall_rule_group - [X] delete_resolver_endpoint - Delete a resolver endpoint. - - [ ] delete_resolver_query_log_config - [X] delete_resolver_rule - Delete a resolver rule. - - [ ] disassociate_firewall_rule_group -- [ ] disassociate_resolver_endpoint_ip_address +- [X] disassociate_resolver_endpoint_ip_address - [ ] disassociate_resolver_query_log_config - [X] disassociate_resolver_rule - Removes association between a resolver rule and a VPC. - - [ ] get_firewall_config - [ ] get_firewall_domain_list - [ ] get_firewall_rule_group @@ -71,8 +64,6 @@ route53resolver - [ ] get_resolver_config - [ ] get_resolver_dnssec_config - [X] get_resolver_endpoint - Return info for specified resolver endpoint. - - [ ] get_resolver_query_log_config - [ ] get_resolver_query_log_config_association - [ ] get_resolver_query_log_config_policy @@ -80,8 +71,6 @@ route53resolver Return info for specified resolver rule. - [X] get_resolver_rule_association - Return info for specified resolver rule association. - - [ ] get_resolver_rule_policy - [ ] import_firewall_domains - [ ] list_firewall_configs @@ -93,31 +82,17 @@ route53resolver - [ ] list_resolver_configs - [ ] list_resolver_dnssec_configs - [X] list_resolver_endpoint_ip_addresses - List IP endresses for specified resolver endpoint. - - [X] list_resolver_endpoints - List all resolver endpoints, using filters if specified. - - [ ] list_resolver_query_log_config_associations - [ ] list_resolver_query_log_configs - [X] list_resolver_rule_associations - List all resolver rule associations, using filters if specified. - - [X] list_resolver_rules - List all resolver rules, using filters if specified. - - [X] list_tags_for_resource - List all tags for the given resource. - - [ ] put_firewall_rule_group_policy - [ ] put_resolver_query_log_config_policy - [ ] put_resolver_rule_policy - [X] tag_resource - Add or overwrite one or more tags for specified resource. - - [X] untag_resource - Removes tags from a resource. - - [ ] update_firewall_config - [ ] update_firewall_domains - [ ] update_firewall_rule @@ -125,7 +100,5 @@ route53resolver - [ ] update_resolver_config - [ ] update_resolver_dnssec_config - [X] update_resolver_endpoint - Update name of Resolver endpoint. - - [ ] update_resolver_rule diff --git a/moto/kms/models.py b/moto/kms/models.py index d14566ca3..fa9e60cda 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -545,8 +545,8 @@ class KmsBackend(BaseBackend): def sign(self, key_id, message, signing_algorithm): """Sign message using generated private key. - NOTES: - signing_algorithm is ignored and hardcoded to RSASSA_PSS_SHA_256 + - grant_tokens are not implemented """ key = self.describe_key(key_id) @@ -568,8 +568,8 @@ class KmsBackend(BaseBackend): def verify(self, key_id, message, signature, signing_algorithm): """Verify message using public key from generated private key. - NOTES: - signing_algorithm is ignored and hardcoded to RSASSA_PSS_SHA_256 + - grant_tokens are not implemented """ key = self.describe_key(key_id) diff --git a/moto/route53resolver/models.py b/moto/route53resolver/models.py index eb6dddcc1..90eb6e66b 100644 --- a/moto/route53resolver/models.py +++ b/moto/route53resolver/models.py @@ -351,7 +351,6 @@ class Route53ResolverBackend(BaseBackend): ) def associate_resolver_rule(self, region, resolver_rule_id, name, vpc_id): - """Return description for a newly created resolver rule association.""" validate_args( [("resolverRuleId", resolver_rule_id), ("name", name), ("vPCId", vpc_id)] ) @@ -460,7 +459,8 @@ class Route53ResolverBackend(BaseBackend): ip_addresses, tags, ): # pylint: disable=too-many-arguments - """Return description for a newly created resolver endpoint. + """ + Return description for a newly created resolver endpoint. NOTE: IPv6 IPs are currently not being filtered when calculating the create_resolver_endpoint() IpAddresses. @@ -624,7 +624,6 @@ class Route53ResolverBackend(BaseBackend): ) def delete_resolver_endpoint(self, resolver_endpoint_id): - """Delete a resolver endpoint.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) # Can't delete an endpoint if there are rules associated with it. @@ -658,7 +657,6 @@ class Route53ResolverBackend(BaseBackend): ) def delete_resolver_rule(self, resolver_rule_id): - """Delete a resolver rule.""" self._validate_resolver_rule_id(resolver_rule_id) # Can't delete an rule unless VPC's are disassociated. @@ -682,7 +680,6 @@ class Route53ResolverBackend(BaseBackend): return resolver_rule def disassociate_resolver_rule(self, resolver_rule_id, vpc_id): - """Removes association between a resolver rule and a VPC.""" validate_args([("resolverRuleId", resolver_rule_id), ("vPCId", vpc_id)]) # Non-existent rule or vpc ids? @@ -712,7 +709,6 @@ class Route53ResolverBackend(BaseBackend): return rule_association def get_resolver_endpoint(self, resolver_endpoint_id): - """Return info for specified resolver endpoint.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) return self.resolver_endpoints[resolver_endpoint_id] @@ -722,7 +718,6 @@ class Route53ResolverBackend(BaseBackend): return self.resolver_rules[resolver_rule_id] def get_resolver_rule_association(self, resolver_rule_association_id): - """Return info for specified resolver rule association.""" validate_args([("resolverRuleAssociationId", resolver_rule_association_id)]) if resolver_rule_association_id not in self.resolver_rule_associations: raise ResourceNotFoundException( @@ -732,7 +727,6 @@ class Route53ResolverBackend(BaseBackend): @paginate(pagination_model=PAGINATION_MODEL) def list_resolver_endpoint_ip_addresses(self, resolver_endpoint_id): - """List IP endresses for specified resolver endpoint.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) endpoint = self.resolver_endpoints[resolver_endpoint_id] return endpoint.ip_descriptions() @@ -792,7 +786,6 @@ class Route53ResolverBackend(BaseBackend): @paginate(pagination_model=PAGINATION_MODEL) def list_resolver_endpoints(self, filters): - """List all resolver endpoints, using filters if specified.""" if not filters: filters = [] @@ -807,7 +800,6 @@ class Route53ResolverBackend(BaseBackend): @paginate(pagination_model=PAGINATION_MODEL) def list_resolver_rules(self, filters): - """List all resolver rules, using filters if specified.""" if not filters: filters = [] @@ -822,7 +814,6 @@ class Route53ResolverBackend(BaseBackend): @paginate(pagination_model=PAGINATION_MODEL) def list_resolver_rule_associations(self, filters): - """List all resolver rule associations, using filters if specified.""" if not filters: filters = [] @@ -851,12 +842,10 @@ class Route53ResolverBackend(BaseBackend): @paginate(pagination_model=PAGINATION_MODEL) def list_tags_for_resource(self, resource_arn): - """List all tags for the given resource.""" self._matched_arn(resource_arn) return self.tagger.list_tags_for_resource(resource_arn).get("Tags") def tag_resource(self, resource_arn, tags): - """Add or overwrite one or more tags for specified resource.""" self._matched_arn(resource_arn) errmsg = self.tagger.validate_tags( tags, limit=ResolverEndpoint.MAX_TAGS_PER_RESOLVER_ENDPOINT @@ -866,12 +855,10 @@ class Route53ResolverBackend(BaseBackend): self.tagger.tag_resource(resource_arn, tags) def untag_resource(self, resource_arn, tag_keys): - """Removes tags from a resource.""" self._matched_arn(resource_arn) self.tagger.untag_resource_using_names(resource_arn, tag_keys) def update_resolver_endpoint(self, resolver_endpoint_id, name): - """Update name of Resolver endpoint.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) validate_args([("name", name)]) resolver_endpoint = self.resolver_endpoints[resolver_endpoint_id] @@ -881,7 +868,6 @@ class Route53ResolverBackend(BaseBackend): def associate_resolver_endpoint_ip_address( self, region, resolver_endpoint_id, ip_address ): - """associate resolver endpoint ip address.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) resolver_endpoint = self.resolver_endpoints[resolver_endpoint_id] @@ -898,7 +884,6 @@ class Route53ResolverBackend(BaseBackend): def disassociate_resolver_endpoint_ip_address( self, resolver_endpoint_id, ip_address ): - """disassociate resolver endpoint ip address.""" self._validate_resolver_endpoint_id(resolver_endpoint_id) resolver_endpoint = self.resolver_endpoints[resolver_endpoint_id] diff --git a/tests/test_route53resolver/test_route53resolver_endpoint.py b/tests/test_route53resolver/test_route53resolver_endpoint.py index 54a1ab193..b85969be6 100644 --- a/tests/test_route53resolver/test_route53resolver_endpoint.py +++ b/tests/test_route53resolver/test_route53resolver_endpoint.py @@ -843,3 +843,134 @@ def test_route53resolver_bad_list_resolver_endpoints(): "Value '250' at 'maxResults' failed to satisfy constraint: Member " "must have length less than or equal to 100" ) in err["Message"] + + +@mock_ec2 +@mock_route53resolver +def test_associate_resolver_endpoint_ip_address(): + client = boto3.client("route53resolver", region_name=TEST_REGION) + ec2_client = boto3.client("ec2", region_name=TEST_REGION) + # create subnet + vpc_id = create_vpc(ec2_client) + subnet = ec2_client.create_subnet( + VpcId=vpc_id, CidrBlock="10.0.2.0/24", AvailabilityZone=f"{TEST_REGION}a" + )["Subnet"] + # create resolver + random_num = get_random_hex(10) + resolver = create_test_endpoint(client, ec2_client, name=f"A-{random_num}") + resolver.should.have.key("IpAddressCount").equals(2) + # associate + resp = client.associate_resolver_endpoint_ip_address( + IpAddress={"Ip": "10.0.2.126", "SubnetId": subnet["SubnetId"]}, + ResolverEndpointId=resolver["Id"], + )["ResolverEndpoint"] + resp.should.have.key("Id").equals(resolver["Id"]) + resp.should.have.key("IpAddressCount").equals(3) + resp.should.have.key("SecurityGroupIds").should.have.length_of(1) + # verify ENI was created + enis = ec2_client.describe_network_interfaces()["NetworkInterfaces"] + + ip_addresses = [eni["PrivateIpAddress"] for eni in enis] + ip_addresses.should.contain("10.0.2.126") + + +@mock_route53resolver +def test_associate_resolver_endpoint_ip_address__invalid_resolver(): + client = boto3.client("route53resolver", region_name=TEST_REGION) + with pytest.raises(ClientError) as exc: + client.associate_resolver_endpoint_ip_address( + IpAddress={"Ip": "notapplicable"}, ResolverEndpointId="unknown" + ) + err = exc.value.response["Error"] + err["Code"].should.equal("ResourceNotFoundException") + err["Message"].should.equal("Resolver endpoint with ID 'unknown' does not exist") + + +@mock_ec2 +@mock_route53resolver +def test_disassociate_resolver_endpoint_ip_address__using_ip(): + client = boto3.client("route53resolver", region_name=TEST_REGION) + ec2_client = boto3.client("ec2", region_name=TEST_REGION) + # create subnet + vpc_id = create_vpc(ec2_client) + subnet_id = ec2_client.create_subnet( + VpcId=vpc_id, CidrBlock="10.0.2.0/24", AvailabilityZone=f"{TEST_REGION}a" + )["Subnet"]["SubnetId"] + # create resolver + random_num = get_random_hex(10) + resolver = create_test_endpoint(client, ec2_client, name=f"A-{random_num}") + # associate + client.associate_resolver_endpoint_ip_address( + IpAddress={"Ip": "10.0.2.126", "SubnetId": subnet_id}, + ResolverEndpointId=resolver["Id"], + ) + enis_before = ec2_client.describe_network_interfaces()["NetworkInterfaces"] + # disassociate + client.disassociate_resolver_endpoint_ip_address( + ResolverEndpointId=resolver["Id"], + IpAddress={"SubnetId": subnet_id, "Ip": "10.0.2.126"}, + ) + # One ENI was deleted + enis_after = ec2_client.describe_network_interfaces()["NetworkInterfaces"] + (len(enis_after) + 1).should.equal(len(enis_before)) + + +@mock_ec2 +@mock_route53resolver +def test_disassociate_resolver_endpoint_ip_address__using_ipid_and_subnet(): + client = boto3.client("route53resolver", region_name=TEST_REGION) + ec2_client = boto3.client("ec2", region_name=TEST_REGION) + # create subnet + vpc_id = create_vpc(ec2_client) + subnet_id = ec2_client.create_subnet( + VpcId=vpc_id, CidrBlock="10.0.2.0/24", AvailabilityZone=f"{TEST_REGION}a" + )["Subnet"]["SubnetId"] + # create resolver + random_num = get_random_hex(10) + resolver = create_test_endpoint(client, ec2_client, name=f"A-{random_num}") + # associate + client.associate_resolver_endpoint_ip_address( + IpAddress={"Ip": "10.0.2.126", "SubnetId": subnet_id}, + ResolverEndpointId=resolver["Id"], + ) + + ip_addresses = client.list_resolver_endpoint_ip_addresses( + ResolverEndpointId=resolver["Id"] + )["IpAddresses"] + ip_id = [ip["IpId"] for ip in ip_addresses if ip["Ip"] == "10.0.2.126"][0] + # disassociate + resp = client.disassociate_resolver_endpoint_ip_address( + ResolverEndpointId=resolver["Id"], + IpAddress={"SubnetId": subnet_id, "IpId": ip_id}, + )["ResolverEndpoint"] + resp.should.have.key("IpAddressCount").equals(2) + + +@mock_ec2 +@mock_route53resolver +def test_disassociate_resolver_endpoint_ip_address__using_subnet_alone(): + client = boto3.client("route53resolver", region_name=TEST_REGION) + ec2_client = boto3.client("ec2", region_name=TEST_REGION) + # create subnet + vpc_id = create_vpc(ec2_client) + subnet_id = ec2_client.create_subnet( + VpcId=vpc_id, CidrBlock="10.0.2.0/24", AvailabilityZone=f"{TEST_REGION}a" + )["Subnet"]["SubnetId"] + # create resolver + random_num = get_random_hex(10) + resolver = create_test_endpoint(client, ec2_client, name=f"A-{random_num}") + # associate + client.associate_resolver_endpoint_ip_address( + IpAddress={"Ip": "10.0.2.126", "SubnetId": subnet_id}, + ResolverEndpointId=resolver["Id"], + ) + # disassociate without specifying IP + with pytest.raises(ClientError) as exc: + client.disassociate_resolver_endpoint_ip_address( + ResolverEndpointId=resolver["Id"], IpAddress={"SubnetId": subnet_id} + ) + err = exc.value.response["Error"] + err["Code"].should.equal("InvalidRequestException") + err["Message"].should.equal( + "[RSLVR-00503] Need to specify either the IP ID or both subnet and IP address in order to remove IP address." + )