diff --git a/changelogs/fragments/2292-ec2_eip-add-support-to-update-reverse-dns-record.yml b/changelogs/fragments/2292-ec2_eip-add-support-to-update-reverse-dns-record.yml new file mode 100644 index 0000000000..01961e06ec --- /dev/null +++ b/changelogs/fragments/2292-ec2_eip-add-support-to-update-reverse-dns-record.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - ec2_eip - Add support to update reverse DNS record of an EIP (https://github.com/ansible-collections/amazon.aws/pull/2292). diff --git a/plugins/modules/ec2_eip.py b/plugins/modules/ec2_eip.py index 10a0ef463d..40e83d69d3 100644 --- a/plugins/modules/ec2_eip.py +++ b/plugins/modules/ec2_eip.py @@ -76,6 +76,11 @@ - Allocates the new Elastic IP from the provided public IPv4 pool (BYOIP) only applies to newly allocated Elastic IPs, isn't validated when O(reuse_existing_ip_allowed=true). type: str + domain_name: + description: The domain name to attach to the IP address. + required: false + type: str + version_added: 8.3.0 extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules @@ -201,6 +206,23 @@ tag_name: reserved_for tag_value: "{{ inventory_hostname }}" public_ipv4_pool: ipv4pool-ec2-0588c9b75a25d1a02 + +- name: create new IP and modify it's reverse DNS record + amazon.aws.ec2_eip: + state: present + domain_name: test-domain.xyz + +- name: Modify reverse DNS record of an existing EIP + amazon.aws.ec2_eip: + public_ip: 44.224.84.105 + domain_name: test-domain.xyz + state: present + +- name: Remove reverse DNS record of an existing EIP + amazon.aws.ec2_eip: + public_ip: 44.224.84.105 + domain_name: "" + state: present """ RETURN = r""" @@ -214,6 +236,46 @@ returned: on success type: str sample: 52.88.159.209 +update_reverse_dns_record_result: + description: Information about result of update reverse dns record operation. + returned: When O(domain_name) is specified. + type: dict + contains: + address: + description: Information about the Elastic IP address. + returned: always + type: dict + contains: + allocation_id: + description: The allocation ID. + returned: always + type: str + sample: "eipalloc-00a11aa111aaa1a11" + ptr_record: + description: The pointer (PTR) record for the IP address. + returned: always + type: str + sample: "ec2-11-22-33-44.us-east-2.compute.amazonaws.com." + ptr_record_update: + description: The updated PTR record for the IP address. + returned: always + type: dict + contains: + status: + description: The status of the PTR record update. + returned: always + type: str + sample: "PENDING" + value: + description: The value for the PTR record update. + returned: always + type: str + sample: "example.com" + public_ip: + description: The public IP address. + returned: always + type: str + sample: "11.22.33.44" """ from typing import Any @@ -223,6 +285,8 @@ from typing import Tuple from typing import Union +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error from ansible_collections.amazon.aws.plugins.module_utils.ec2 import allocate_address as allocate_ip_address from ansible_collections.amazon.aws.plugins.module_utils.ec2 import associate_address @@ -410,6 +474,7 @@ def ensure_present( public_ipv4_pool = module.params.get("public_ipv4_pool") tags = module.params.get("tags") purge_tags = module.params.get("purge_tags") + domain_name = module.params.get("domain_name") # Tags for *searching* for an EIP. search_tags = generate_tag_dict(module) @@ -422,6 +487,12 @@ def ensure_present( client, module.check_mode, search_tags, domain, reuse_existing_ip_allowed, tags, public_ipv4_pool ) + if domain_name is not None: + changed, update_reverse_dns_record_result = update_reverse_dns_record_of_eip( + client, module, address, domain_name + ) + result.update({"update_reverse_dns_record_result": update_reverse_dns_record_result}) + # Associate address to instance if device_id: # Find instance @@ -462,13 +533,55 @@ def ensure_present( client, module, address["AllocationId"], resource_type="elastic-ip", tags=tags, purge_tags=purge_tags ) result.update({"public_ip": address["PublicIp"], "allocation_id": address["AllocationId"]}) + result["changed"] = changed return result +def update_reverse_dns_record_of_eip(client, module: AnsibleAWSModule, address, domain_name): + if module.check_mode: + return True, {} + + current_ptr_record_domain = client.describe_addresses_attribute( + AllocationIds=[address["AllocationId"]], Attribute="domain-name" + ) + + if ( + current_ptr_record_domain["Addresses"] + and current_ptr_record_domain["Addresses"][0]["PtrRecord"] == domain_name + "." + ): + return False, {"ptr_record": domain_name + "."} + + if len(domain_name) == 0: + try: + update_reverse_dns_record_result = client.reset_address_attribute( + AllocationId=address["AllocationId"], Attribute="domain-name" + ) + changed = True + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + if "ResponseMetadata" in update_reverse_dns_record_result: + del update_reverse_dns_record_result["ResponseMetadata"] + else: + try: + update_reverse_dns_record_result = client.modify_address_attribute( + AllocationId=address["AllocationId"], DomainName=domain_name + ) + changed = True + except AnsibleEC2Error as e: + module.fail_json_aws_error(e) + + if "ResponseMetadata" in update_reverse_dns_record_result: + del update_reverse_dns_record_result["ResponseMetadata"] + + return changed, camel_dict_to_snake_dict(update_reverse_dns_record_result) + + def main(): argument_spec = dict( device_id=dict(required=False), + domain_name=dict(required=False, type="str"), public_ip=dict(required=False, aliases=["ip"]), state=dict(required=False, default="present", choices=["present", "absent"]), in_vpc=dict(required=False, type="bool", default=False), diff --git a/tests/integration/targets/ec2_eip/defaults/main.yml b/tests/integration/targets/ec2_eip/defaults/main.yml index 82c09d949b..736ebbcb76 100644 --- a/tests/integration/targets/ec2_eip/defaults/main.yml +++ b/tests/integration/targets/ec2_eip/defaults/main.yml @@ -8,3 +8,5 @@ eip_test_tags: AnsibleEIPTestPrefix: "{{ resource_prefix }}" eip_info_filters: tag:AnsibleEIPTestPrefix: "{{ resource_prefix }}" +test_domain: "{{ resource_prefix }}.example.xyz" +test_hosted_zone: "{{ resource_prefix }}.example.xyz" diff --git a/tests/integration/targets/ec2_eip/tasks/main.yml b/tests/integration/targets/ec2_eip/tasks/main.yml index 9523fd1ac3..667d77b476 100644 --- a/tests/integration/targets/ec2_eip/tasks/main.yml +++ b/tests/integration/targets/ec2_eip/tasks/main.yml @@ -18,5 +18,8 @@ - ansible.builtin.include_tasks: tasks/attach_detach_to_eni.yml - ansible.builtin.include_tasks: tasks/attach_detach_to_instance.yml + # Disabled as it requires a registered domain, and corresponding hosted zone + # - ansible.builtin.include_tasks: tasks/update_reverse_dns_record.yml + always: - ansible.builtin.include_tasks: tasks/teardown.yml diff --git a/tests/integration/targets/ec2_eip/tasks/update_reverse_dns_record.yml b/tests/integration/targets/ec2_eip/tasks/update_reverse_dns_record.yml new file mode 100644 index 0000000000..4fc63b55ed --- /dev/null +++ b/tests/integration/targets/ec2_eip/tasks/update_reverse_dns_record.yml @@ -0,0 +1,117 @@ +- name: Test EIP allocation and reverse DNS record operations + block: + # ------------------------------------------------------------------------------------------ + # Allocate EIP with reverse DNS record - check mode + # ------------------------------------------------------------------------------------------ + - name: Allocate a new EIP and modify it's reverse DNS record - check_mode + amazon.aws.ec2_eip: + state: present + domain_name: "{{ test_domain }}" + tags: "{{ eip_test_tags }}" + register: eip + check_mode: true + + - name: Assert that task result was as expected + ansible.builtin.assert: + that: + - eip is changed + + - name: Ensure no new EIP was created + ansible.builtin.include_tasks: tasks/common.yml + vars: + has_no_new_eip: true + + # ------------------------------------------------------------------------------------------ + # Allocate EIP with reverse DNS record + # ------------------------------------------------------------------------------------------ + - name: Allocate a new EIP and modify it's reverse DNS record + amazon.aws.ec2_eip: + state: present + domain_name: "{{ test_domain }}" + tags: "{{ eip_test_tags }}" + register: eip + + - name: Add EIP IP address an A record + amazon.aws.route53: + state: present + zone: "{{ test_hosted_zone }}" + record: "{{ test_domain }}" + type: A + ttl: 7200 + value: "{{ eip.public_ip}}" + identifier: "{{ resource_prefix }}" + wait: true + + - name: Wait for reverse DNS record update to complete + pause: + minutes: 3 + + - name: Assert that task result was as expected + ansible.builtin.assert: + that: + - eip is changed + - eip.public_ip is defined and ( eip.public_ip | ansible.utils.ipaddr ) + - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") + - eip.update_reverse_dns_record_result is defined + - eip.update_reverse_dns_record_result.address.ptr_record_update is defined + - eip.update_reverse_dns_record_result.address.ptr_record_update.value == "{{ test_domain }}." + + # ------------------------------------------------------------------------------------------ + # Allocate EIP with reverse DNS record - idempotence + # ------------------------------------------------------------------------------------------ + - name: Try modifying reverse DNS record of EIP to same domain as current - Idempotent + amazon.aws.ec2_eip: + state: present + public_ip: "{{ eip.public_ip }}" + domain_name: "{{ test_domain }}" + tags: "{{ eip_test_tags }}" + register: eip + + - name: Assert that task result was as expected + ansible.builtin.assert: + that: + - eip is not changed + - eip.public_ip is defined and ( eip.public_ip | ansible.utils.ipaddr ) + - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") + - eip.update_reverse_dns_record_result is defined + - eip.update_reverse_dns_record_result.ptr_record == "{{ test_domain }}." + + # ------------------------------------------------------------------------------------------ + # Update reverse DNS record of existing EIP - remove reverse DNS record + # ------------------------------------------------------------------------------------------ + - name: Try modifying reverse DNS record of EIP to different domain than current + amazon.aws.ec2_eip: + state: present + public_ip: "{{ eip.public_ip }}" + domain_name: "" + tags: "{{ eip_test_tags }}" + register: eip + + - name: Assert that changes were applied + ansible.builtin.assert: + that: + - eip is changed + - eip.public_ip is defined and ( eip.public_ip | ansible.utils.ipaddr ) + - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") + + - name: Wait for reverse DNS record update to complete + pause: + minutes: 3 + + always: + + - name: Delete EIP IP address an A record + amazon.aws.route53: + state: present + zone: "{{ test_hosted_zone }}" + record: "{{ test_domain }}" + type: A + ttl: 7200 + value: "{{ eip.public_ip}}" + identifier: "{{ resource_prefix }}" + wait: true + + - name: Delete EIP + ansible.builtin.include_tasks: tasks/common.yml + vars: + delete_eips: true diff --git a/tests/unit/plugins/modules/ec2_eip/test_reverse_dns_record_updates.py b/tests/unit/plugins/modules/ec2_eip/test_reverse_dns_record_updates.py new file mode 100644 index 0000000000..143db84572 --- /dev/null +++ b/tests/unit/plugins/modules/ec2_eip/test_reverse_dns_record_updates.py @@ -0,0 +1,188 @@ +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from unittest.mock import MagicMock + +import pytest + +from ansible_collections.amazon.aws.plugins.modules import ec2_eip + +module_name = "ansible_collections.amazon.aws.plugins.modules.ec2_eip" + + +class FailJsonException(Exception): + def __init__(self): + pass + + +@pytest.fixture +def ansible_module(): + module = MagicMock() + module.fail_json.side_effect = FailJsonException() + module.fail_json_aws.side_effect = FailJsonException() + module.check_mode = False + return module + + +def mock_address(): + return { + "InstanceId": "i-12345678", + "PublicIp": "1.2.3.4", + "AllocationId": "eipalloc-12345678", + "AssociationId": "eipassoc-12345678", + "Domain": "vpc", + "NetworkInterfaceId": "eni-12345678", + "NetworkInterfaceOwnerId": "123456789012", + "PrivateIpAddress": "10.0.0.1", + "Tags": [{"Key": "Name", "Value": "MyElasticIP"}], + "PublicIpv4Pool": "my-ipv4-pool", + "NetworkBorderGroup": "my-border-group", + "CustomerOwnedIp": "192.0.2.0", + "CustomerOwnedIpv4Pool": "my-customer-owned-pool", + "CarrierIp": "203.0.113.0", + } + + +def mock_return_value_describe_addresses_attribute(): + return { + "Addresses": [ + { + "PublicIp": "1.2.3.4", + "AllocationId": "eipalloc-12345678", + "PtrRecord": "current.example.com.", + "PtrRecordUpdate": { + "Value": "current.example.com.", + "Status": "successful", + "Reason": "updated", + }, + } + ] + } + + +def test_update_reverse_dns_record_of_eip_no_change_in_dns_record(ansible_module): + client = MagicMock() + + address = mock_address() + mock_domain_name = "current.example.com" + + client.describe_addresses_attribute.return_value = mock_return_value_describe_addresses_attribute() + + client.modify_address_attribute.return_value = None + client.reset_address_attribute.return_value = None + + assert ec2_eip.update_reverse_dns_record_of_eip(client, ansible_module, address, mock_domain_name) == ( + False, + {"ptr_record": mock_domain_name + "."}, + ) + + assert client.describe_addresses_attribute.call_count == 1 + assert client.modify_address_attribute.call_count == 0 + assert client.reset_address_attribute.call_count == 0 + + client.describe_addresses_attribute.assert_called_once_with( + AllocationIds=["eipalloc-12345678"], Attribute="domain-name" + ) + + +def test_update_reverse_dns_record_of_eip_reset_dns_record(ansible_module): + client = MagicMock() + + address = mock_address() + mock_domain_name = "" + + client.describe_addresses_attribute.return_value = mock_return_value_describe_addresses_attribute() + + client.modify_address_attribute.return_value = None + client.reset_address_attribute.return_value = { + "Addresses": [ + { + "PublicIp": "1.2.3.4", + "AllocationId": "eipalloc-12345678", + "PtrRecord": "current.example.com.", + "PtrRecordUpdate": { + "Value": "", + "Status": "PENDING", + "Reason": "update in progress", + }, + } + ] + } + + assert ec2_eip.update_reverse_dns_record_of_eip(client, ansible_module, address, mock_domain_name) == ( + True, + { + "addresses": [ + { + "public_ip": "1.2.3.4", + "allocation_id": "eipalloc-12345678", + "ptr_record": "current.example.com.", + "ptr_record_update": {"value": "", "status": "PENDING", "reason": "update in progress"}, + } + ] + }, + ) + + assert client.describe_addresses_attribute.call_count == 1 + assert client.modify_address_attribute.call_count == 0 + assert client.reset_address_attribute.call_count == 1 + + client.describe_addresses_attribute.assert_called_once_with( + AllocationIds=["eipalloc-12345678"], Attribute="domain-name" + ) + client.reset_address_attribute.assert_called_once_with(AllocationId="eipalloc-12345678", Attribute="domain-name") + + +def test_update_reverse_dns_record_of_eip_modify_dns_record(ansible_module): + client = MagicMock() + + address = mock_address() + + mock_domain_name = "new.example.com" + + client.describe_addresses_attribute.return_value = mock_return_value_describe_addresses_attribute() + + client.modify_address_attribute.return_value = { + "Addresses": [ + { + "PublicIp": "1.2.3.4", + "AllocationId": "eipalloc-12345678", + "PtrRecord": "current.example.com.", + "PtrRecordUpdate": { + "Value": "new.example.com", + "Status": "PENDING", + "Reason": "update in progress", + }, + } + ] + } + client.reset_address_attribute.return_value = None + + assert ec2_eip.update_reverse_dns_record_of_eip(client, ansible_module, address, mock_domain_name) == ( + True, + { + "addresses": [ + { + "public_ip": "1.2.3.4", + "allocation_id": "eipalloc-12345678", + "ptr_record": "current.example.com.", + "ptr_record_update": { + "value": "new.example.com", + "status": "PENDING", + "reason": "update in progress", + }, + } + ] + }, + ) + + assert client.describe_addresses_attribute.call_count == 1 + assert client.modify_address_attribute.call_count == 1 + assert client.reset_address_attribute.call_count == 0 + + client.describe_addresses_attribute.assert_called_once_with( + AllocationIds=["eipalloc-12345678"], Attribute="domain-name" + ) + client.modify_address_attribute.assert_called_once_with( + AllocationId="eipalloc-12345678", DomainName=mock_domain_name + )