diff --git a/changelogs/fragments/add_imdsv2.yml b/changelogs/fragments/add_imdsv2.yml new file mode 100644 index 0000000000..517aee4548 --- /dev/null +++ b/changelogs/fragments/add_imdsv2.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - ec2_ami - adds ``imdsv2_enable`` parameter to enable IMDSv2 when creating/updating an image (https://github.com/ansible-collections/amazon.aws/pull/2310). + - ec2_ami_info - add ``imdsv2_enable`` return when ``describe_image_attributes`` is set (https://github.com/ansible-collections/amazon.aws/pull/2310). diff --git a/plugins/module_utils/ec2.py b/plugins/module_utils/ec2.py index 9690e0d5cc..05625c8dbc 100644 --- a/plugins/module_utils/ec2.py +++ b/plugins/module_utils/ec2.py @@ -860,7 +860,7 @@ def deregister_image(client, image_id: str) -> bool: @EC2ImageErrorHandler.common_error_handler("modify image attribute") -@AWSRetry.jittered_backoff() +@AWSRetry.jittered_backoff(catch_extra_error_codes=["InvalidAMIID.Unavailable"]) def modify_image_attribute(client, image_id: str, **params: Dict[str, Any]) -> bool: client.modify_image_attribute(ImageId=image_id, **params) return True diff --git a/plugins/modules/ec2_ami.py b/plugins/modules/ec2_ami.py index 2213e93af5..830172e2de 100644 --- a/plugins/modules/ec2_ami.py +++ b/plugins/modules/ec2_ami.py @@ -186,6 +186,14 @@ - See the AWS documentation for more detail U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/uefi-secure-boot.html). type: str version_added: 5.5.0 + imdsv2_enable: + description: + - Force IMDS v2 on the AMI + - See the AWS documentation for more detail + - U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances-ami-configuration). + type: bool + default: false + version_added: 9.0.0 author: - "Evan Duffield (@scicoin-project) " - "Constantin Bugneac (@Constantin07) " @@ -553,12 +561,15 @@ def get_image_by_id(connection, image_id): result = images[0] try: - image_attribue = describe_image_attribute(connection, attribute="launchPermission", image_id=image_id) - if image_attribue: - result["LaunchPermissions"] = image_attribue["LaunchPermissions"] - image_attribue = describe_image_attribute(connection, attribute="productCodes", image_id=image_id) - if image_attribue: - result["ProductCodes"] = image_attribue["ProductCodes"] + image_attribute = describe_image_attribute(connection, attribute="launchPermission", image_id=image_id) + if image_attribute: + result["LaunchPermissions"] = image_attribute["LaunchPermissions"] + image_attribute = describe_image_attribute(connection, attribute="productCodes", image_id=image_id) + if image_attribute: + result["ProductCodes"] = image_attribute["ProductCodes"] + image_attribute = describe_image_attribute(connection, attribute="imdsSupport", image_id=image_id) + if image_attribute: + result["ImdsSupport"] = image_attribute["ImdsSupport"]["Value"] except AnsibleEC2Error as e: raise Ec2AmiFailure(f"Error retrieving image attributes for image {image_id}", e) return result @@ -766,6 +777,20 @@ def set_description(connection, module, image, description): except AnsibleEC2Error as e: raise Ec2AmiFailure(f"Error setting description for image {image['ImageId']}", e) + @staticmethod + def set_imdsv2(connection, image, imdsv2_enable): + if not imdsv2_enable and image["ImdsSupport"] != "v2.0": + return False + + if image["ImdsSupport"] == "v2.0": + return False + + try: + modify_image_attribute(connection, image_id=image["imageId"], Attribute="imdsSupport", Value="v2.0") + return True + except AnsibleEC2Error as e: + raise Ec2AmiFailure(f"Error setting IMDS Support to v2 for image {image['imageId']}", e) + @classmethod def do(cls, module, connection, image_id): """Entry point to update an image""" @@ -782,6 +807,7 @@ def do(cls, module, connection, image_id): changed |= cls.set_launch_permission(connection, image, launch_permissions, module.check_mode) changed |= cls.set_tags(connection, module, image_id, module.params["tags"], module.params["purge_tags"]) changed |= cls.set_description(connection, module, image, module.params["description"]) + changed |= cls.set_imdsv2(connection, image, module.params["imdsv2_enable"]) if changed and module.check_mode: module.exit_json(changed=True, msg="Would have updated AMI if not in check mode.") @@ -844,6 +870,13 @@ def set_launch_permissions(connection, launch_permissions, image_id): except AnsibleEC2Error as e: raise Ec2AmiFailure(f"Error setting launch permissions for image {image_id}", e) + @staticmethod + def set_imdsv2(connection, image_id): + try: + modify_image_attribute(connection, image_id=image_id, Attribute="imdsSupport", Value="v2.0") + except AnsibleEC2Error as e: + raise Ec2AmiFailure(f"Error setting IMDS Support to v2 for image {image_id}", e) + @staticmethod def create_or_register(create_image_parameters): create_from_instance = "InstanceId" in create_image_parameters @@ -952,6 +985,8 @@ def do(cls, module, connection, _image_id): CreateImage.set_tags(connection, module, module.params.get("tags"), image_id) cls.set_launch_permissions(connection, module.params.get("launch_permissions"), image_id) + if module.params.get("imdsv2_enable"): + cls.set_imdsv2(connection, image_id) module.exit_json( msg="AMI creation operation complete.", changed=True, **get_ami_info(get_image_by_id(connection, image_id)) @@ -1002,6 +1037,7 @@ def main(): tpm_support={"type": "str"}, uefi_data={"type": "str"}, virtualization_type={"default": "hvm"}, + imdsv2_enable={"type": "bool", "default": False}, wait={"type": "bool", "default": False}, wait_timeout={"default": 1200, "type": "int"}, ) diff --git a/plugins/modules/ec2_ami_info.py b/plugins/modules/ec2_ami_info.py index 92e59679a0..99efba5ae1 100644 --- a/plugins/modules/ec2_ami_info.py +++ b/plugins/modules/ec2_ami_info.py @@ -152,6 +152,10 @@ description: An AWS account ID with permissions to launch the AMI. type: str sample: [{"group": "all"}, {"user_id": "123456789012"}] + imdsv2_enabled: + description: Whether the image has IMDSv2 enabled or not. + returned: When O(describe_image_attributes=true). + type: bool name: description: The name of the AMI that was provided during image creation. returned: always @@ -277,6 +281,12 @@ def list_ec2_images(ec2_client, module, request_args): ec2_client, attribute="launchPermission", image_id=image["image_id"] ).get("LaunchPermissions", []) image["launch_permissions"] = [camel_dict_to_snake_dict(perm) for perm in launch_permissions] + imdsv2_enabled = describe_image_attribute( + ec2_client, attribute="imdsSupport", image_id=image["image_id"] + ).get("ImdsSupport", {}) + image["imdsv2_enabled"] = ( + True if "Value" in imdsv2_enabled and imdsv2_enabled["Value"] == "v2.0" else False + ) except is_ansible_aws_error_code("AuthFailure"): # describing launch permissions of images owned by others is not permitted, but shouldn't cause failures pass diff --git a/tests/integration/targets/ec2_ami/tasks/main.yml b/tests/integration/targets/ec2_ami/tasks/main.yml index 267e52abb8..7eaf2f825f 100644 --- a/tests/integration/targets/ec2_ami/tasks/main.yml +++ b/tests/integration/targets/ec2_ami/tasks/main.yml @@ -708,8 +708,12 @@ tags: Name: "{{ ec2_ami_name }}_permissions" launch_permissions: - org_arns: ["arn:aws:organizations::123456789012:organization/o-123ab4cdef"] - org_unit_arns: ["arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5exampld"] + org_arns: + ["arn:aws:organizations::123456789012:organization/o-123ab4cdef"] + org_unit_arns: + [ + "arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5exampld", + ] register: permissions_update_result - name: Get ami info @@ -727,6 +731,27 @@ - "'organizational_unit_arn' in permissions_info_result.images[0].launch_permissions[1]" - permissions_info_result.images[0].launch_permissions[1]['organizational_unit_arn'] == 'arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5exampld' + - name: Create image with imdsv2 enabled + amazon.aws.ec2_ami: + state: present + instance_id: "{{ ec2_instance_id }}" + name: "{{ ec2_ami_name }}_imdsv2" + tags: + Name: "{{ ec2_ami_name }}_imdsv2" + imdsv2_enable: true + register: imdsv2_image + + - name: Get ami info + amazon.aws.ec2_ami_info: + image_ids: "{{ imdsv2_image.image_id }}" + describe_image_attributes: true + register: imdsv2_info + + - name: Assert ami IMDSV2 is set + ansible.builtin.assert: + that: + - imdsv2_info.images[0].imdsv2_enabled + # ============================================================ always: diff --git a/tests/integration/targets/ec2_ami_instance/tasks/main.yml b/tests/integration/targets/ec2_ami_instance/tasks/main.yml index 2b3c44c38c..e9130bd735 100644 --- a/tests/integration/targets/ec2_ami_instance/tasks/main.yml +++ b/tests/integration/targets/ec2_ami_instance/tasks/main.yml @@ -92,10 +92,10 @@ amazon.aws.ec2_ami: instance_id: "{{ ec2_instance_id }}" state: present - name: "{{ ec2_ami_name }}_ami" + name: "{{ ec2_ami_name }}_ami_check" description: "{{ ec2_ami_description }}" tags: - Name: "{{ ec2_ami_name }}_ami" + Name: "{{ ec2_ami_name }}_ami_check" wait: true root_device_name: "{{ ec2_ami_root_disk }}" check_mode: true @@ -110,10 +110,10 @@ amazon.aws.ec2_ami: instance_id: "{{ ec2_instance_id }}" state: present - name: "{{ ec2_ami_name }}_ami" + name: "{{ ec2_ami_name }}_ami_instance" description: "{{ ec2_ami_description }}" tags: - Name: "{{ ec2_ami_name }}_ami" + Name: "{{ ec2_ami_name }}_ami_instance" wait: true root_device_name: "{{ ec2_ami_root_disk }}" register: result @@ -127,7 +127,7 @@ that: - result.changed - result.image_id.startswith('ami-') - - "'Name' in result.tags and result.tags.Name == ec2_ami_name + '_ami'" + - "'Name' in result.tags and result.tags.Name == ec2_ami_name + '_ami_instance'" - name: get related snapshot info and ensure the tags have been propagated amazon.aws.ec2_snapshot_info: @@ -139,7 +139,7 @@ ansible.builtin.assert: that: - "'tags' in snapshot_result.snapshots[0]" - - "'Name' in snapshot_result.snapshots[0].tags and snapshot_result.snapshots[0].tags.Name == ec2_ami_name + '_ami'" + - "'Name' in snapshot_result.snapshots[0].tags and snapshot_result.snapshots[0].tags.Name == ec2_ami_name + '_ami_instance'" # ============================================================ @@ -347,6 +347,34 @@ - result.failed - "result.msg == 'state is absent but all of the following are missing: image_id'" + # ============================================================== + + - name: Create image with imdsv2 enabled + amazon.aws.ec2_ami: + state: present + instance_id: "{{ ec2_instance_id }}" + name: "{{ ec2_ami_name }}_imdsv2" + imdsv2_enable: true + wait: true + tags: + Name: "{{ ec2_ami_name }}_imdsv2" + register: imdsv2_image + + - name: Get ami info + amazon.aws.ec2_ami_info: + image_ids: "{{ imdsv2_image.image_id }}" + describe_image_attributes: true + register: imdsv2_info + + - name: Assert ami IMDSV2 is set + ansible.builtin.assert: + that: + - imdsv2_info.images[0].imdsv2_enabled + + - name: set image id fact for deletion later + ansible.builtin.set_fact: + ec2_ami_image_id_imdsv2: "{{ imdsv2_image.image_id }}" + always: # ============================================================ @@ -379,6 +407,14 @@ wait: true ignore_errors: true + - name: delete imdsv2 ami + amazon.aws.ec2_ami: + state: absent + image_id: "{{ ec2_ami_image_id_imdsv2 }}" + name: "{{ ec2_ami_name }}_imdsv2" + wait: true + ignore_errors: true + - name: delete ami amazon.aws.ec2_ami: state: absent diff --git a/tests/unit/plugins/modules/test_ec2_ami.py b/tests/unit/plugins/modules/test_ec2_ami.py index dfec225e24..bc5e9a65b8 100644 --- a/tests/unit/plugins/modules/test_ec2_ami.py +++ b/tests/unit/plugins/modules/test_ec2_ami.py @@ -63,7 +63,7 @@ def test_get_image_by_id_found(m_describe_images, m_describe_image_attribute): image = ec2_ami.get_image_by_id(connection, "ami-0c7a795306730b288") assert image["ImageId"] == "ami-0c7a795306730b288" assert m_describe_images.call_count == 1 - assert m_describe_image_attribute.call_count == 2 + assert m_describe_image_attribute.call_count == 3 m_describe_images.assert_has_calls( [ call( diff --git a/tests/unit/plugins/modules/test_ec2_ami_info.py b/tests/unit/plugins/modules/test_ec2_ami_info.py index 087c55077c..7482990895 100644 --- a/tests/unit/plugins/modules/test_ec2_ami_info.py +++ b/tests/unit/plugins/modules/test_ec2_ami_info.py @@ -140,6 +140,7 @@ def test_list_ec2_images(m_get_images, m_describe_image_attribute): m_describe_image_attribute.return_value = { "ImageId": "ami-1234567890", "LaunchPermissions": [{"UserId": "1234567890"}, {"UserId": "0987654321"}], + "ImdsSupport": {"Value": "v2.0"}, } images = m_get_images.return_value @@ -160,15 +161,20 @@ def test_list_ec2_images(m_get_images, m_describe_image_attribute): assert m_get_images.call_count == 1 m_get_images.assert_called_with(ec2_client, request_args) - assert m_describe_image_attribute.call_count == 2 + assert m_describe_image_attribute.call_count == 4 m_describe_image_attribute.assert_has_calls( [call(ec2_client, attribute="launchPermission", image_id=images[0]["image_id"])], [call(ec2_client, attribute="launchPermission", image_id=images[1]["image_id"])], ) + m_describe_image_attribute.assert_has_calls( + [call(ec2_client, attribute="imdsSupport", image_id=images[0]["image_id"])], + [call(ec2_client, attribute="imdsSupport", image_id=images[1]["image_id"])], + ) assert len(list_ec2_images_result) == 2 assert list_ec2_images_result[0]["image_id"] == "ami-1234567890" assert list_ec2_images_result[1]["image_id"] == "ami-1523498760" + assert list_ec2_images_result[0]["imdsv2_enabled"] @patch(module_name + ".AnsibleAWSModule")