Repository: ansible-collections/amazon.aws

Issue:
Boot mode and TPM support for AMI creation
### Summary

I would like to use ansible to create an AMI with UEFI secure boot and TPM support. In the [ec2_ami module](https://docs.ansible.com/ansible/latest/collections/amazon/aws/ec2_ami_module.html#ansible-collections-amazon-aws-ec2-ami-module), there are currently no parameters to do so. As UEFI and TPM support for Linux AMIs is new for AWS, I think it would be a useful new feature to add parameters for boot mode and tpm support. [Amazon EC2 Now Supports NitroTPM and UEFI Secure Boot](https://aws.amazon.com/blogs/aws/amazon-ec2-now-supports-nitrotpm-and-uefi-secure-boot/)

### Issue Type

Feature Idea

### Component Name

ec2_ami

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
 - name: Create AMI
    amazon.aws.ec2_ami:
         name: test
         state: present
         architecture: x86_64
         virtualization_type: hvm
         root_device_name: /dev/sda1
         device_mapping:
                 - device_name: /dev/sda1
                    snapshot_id: "{{ snapshot_id }}"
          wait: yes
          region: us-east-1
          boot_mode: uefi
          uefi_data: data_file.bin
          tpm_support: v2.0  
                                       
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/ec2_ami.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/ec2_ami.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Constantin07 @gunzy83 @jillr @s-hertel @scicoin-project @tremble @wilvk
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc-ing myself as this is a joint issue submission with @avery-blanchard 
cc @lkatalin 
Just curious, which is the PR for this issue? I see the "has PR" label was added.
@lkatalin #1037 should likely solve the issue.

Resolving patch:
diff --git a/plugins/modules/ec2_ami.py b/plugins/modules/ec2_ami.py
--- a/plugins/modules/ec2_ami.py
+++ b/plugins/modules/ec2_ami.py
@@ -144,6 +144,27 @@
     description:
       - Set to simple to enable enhanced networking with the Intel 82599 Virtual Function interface for the AMI and any instances that you launch from the AMI.
     type: str
+  boot_mode:
+    description:
+      - The boot mode of the AMI.
+      - See the AWS documentation for more detail U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ami-boot.html).
+    type: str
+    choices: ['legacy-bios', 'uefi']
+  tpm_support:
+    description:
+      - Set to v2.0 to enable Trusted Platform Module (TPM) support.
+      - If the image is configured for NitroTPM support, the value is v2.0 .
+      - Requires I(boot_mode) to be set to 'uefi'.
+      - Requires an instance type that is compatible with Nitro.
+      - Requires minimum botocore version 1.26.0.
+      - See the AWS documentation for more detail U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nitrotpm.html).
+    type: str
+  uefi_data:
+    description:
+      - Base64 representation of the non-volatile UEFI variable store.
+      - Requires minimum botocore version 1.26.0.
+      - See the AWS documentation for more detail U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/uefi-secure-boot.html).
+    type: str
 author:
   - "Evan Duffield (@scicoin-project) <eduffield@iacquire.com>"
   - "Constantin Bugneac (@Constantin07) <constantin.bugneac@endava.com>"
@@ -216,6 +237,22 @@
         - device_name: /dev/sdb
           no_device: true
 
+- name: AMI Creation with boot_mode and tpm_support
+  amazon.aws.ec2_ami:
+    name: newtest
+    state: present
+    architecture: x86_64
+    virtualization_type: hvm
+    root_device_name: /dev/sda1
+    device_mapping:
+        - device_name: /dev/sda1
+          snapshot_id: "{{ snapshot_id }}"
+    wait: yes
+    region: us-east-1
+    boot_mode: uefi
+    uefi_data: data_file.bin
+    tpm_support: v2.0
+
 - name: Deregister/Delete AMI (keep associated snapshots)
   amazon.aws.ec2_ami:
     image_id: "{{ instance.image_id }}"
@@ -441,6 +478,12 @@ def create_image(module, connection):
     billing_products = module.params.get('billing_products')
     ramdisk_id = module.params.get('ramdisk_id')
     sriov_net_support = module.params.get('sriov_net_support')
+    boot_mode = module.params.get('boot_mode')
+    tpm_support = module.params.get('tpm_support')
+    uefi_data = module.params.get('uefi_data')
+
+    if tpm_support and boot_mode != 'uefi':
+        module.fail_json(msg="To specify 'tpm_support', 'boot_mode' must be 'uefi'.")
 
     if module.check_mode:
         image = connection.describe_images(Filters=[{'Name': 'name', 'Values': [str(name)]}])
@@ -509,6 +552,12 @@ def create_image(module, connection):
                 params['KernelId'] = kernel_id
             if root_device_name:
                 params['RootDeviceName'] = root_device_name
+            if boot_mode:
+                params['BootMode'] = boot_mode
+            if tpm_support:
+                params['TpmSupport'] = tpm_support
+            if uefi_data:
+                params['UefiData'] = uefi_data
             image_id = connection.register_image(aws_retry=True, **params).get('ImageId')
     except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
         module.fail_json_aws(e, msg="Error registering image")
@@ -731,6 +780,9 @@ def main():
         sriov_net_support=dict(),
         tags=dict(type='dict', aliases=['resource_tags']),
         purge_tags=dict(type='bool', default=True),
+        boot_mode=dict(type='str', choices=['legacy-bios', 'uefi']),
+        tpm_support=dict(type='str'),
+        uefi_data=dict(type='str'),
     )
 
     module = AnsibleAWSModule(
@@ -746,6 +798,9 @@ def main():
     if not any([module.params['image_id'], module.params['name']]):
         module.fail_json(msg="one of the following is required: name, image_id")
 
+    if any([module.params['tpm_support'], module.params['uefi_data']]):
+        module.require_botocore_at_least('1.26.0', reason='required for ec2.register_image with tpm_support or uefi_data')
+
     connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff())
 
     if module.params.get('state') == 'absent':


Test patch:
diff --git a/tests/integration/targets/ec2_ami/meta/main.yml b/tests/integration/targets/ec2_ami/meta/main.yml
--- a/tests/integration/targets/ec2_ami/meta/main.yml
+++ b/tests/integration/targets/ec2_ami/meta/main.yml
@@ -1,2 +1,5 @@
 dependencies:
   - setup_ec2_facts
+  - role: setup_botocore_pip
+    vars:
+      botocore_version: '1.26.0'
\ No newline at end of file
diff --git a/tests/integration/targets/ec2_ami/tasks/main.yml b/tests/integration/targets/ec2_ami/tasks/main.yml
--- a/tests/integration/targets/ec2_ami/tasks/main.yml
+++ b/tests/integration/targets/ec2_ami/tasks/main.yml
@@ -10,6 +10,12 @@
     - amazon.aws
   block:
 
+    # AWS CLI is needed until there's a module to get instance uefi data
+    - name: Install AWS CLI
+      pip:
+        name: awscli==1.25.83
+        state: present
+
     # ============================================================
 
     # SETUP: vpc, ec2 key pair, subnet, security group, ec2 instance, snapshot
@@ -74,6 +80,21 @@
         state: present
       register: setup_snapshot
 
+    # note: the current CI supported instance types (t2, t3, m1) do not support uefi boot mode + tpm_support
+    # disabling the task as aws documentation states that support for t3 will be coming soon
+    # - name: get instance UEFI data
+    #   command: aws ec2 get-instance-uefi-data --instance-id {{ ec2_instance_id }} --region {{ aws_region }}
+    #   environment:
+    #     AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
+    #     AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
+    #     AWS_SESSION_TOKEN: "{{ security_token | default('') }}"
+    #     AWS_DEFAULT_REGION: "{{ aws_region }}"
+    #   register: instance_uefi_data_output
+
+    # - name: Convert it to an object
+    #   set_fact:
+    #     instance_uefi_data: "{{ instance_uefi_data_output.stdout | from_json }}"
+
     # ============================================================
 
     - name: test clean failure if not providing image_id or name with state=present
@@ -629,6 +650,52 @@
           - not result.changed
           - not result.failed
 
+    # ============================================================
+
+    - name: create an image from the snapshot with boot_mode and tpm_support
+      ec2_ami:
+        name: '{{ ec2_ami_name }}_ami-boot-tpm'
+        description: '{{ ec2_ami_description }}'
+        state: present
+        boot_mode: uefi
+        tpm_support: v2.0
+        launch_permissions:
+          user_ids: []
+        tags:
+          Name: '{{ ec2_ami_name }}_ami-boot-tpm'
+        root_device_name: '{{ ec2_ami_root_disk }}'
+        device_mapping:
+          - device_name: '{{ ec2_ami_root_disk }}'
+            volume_type: gp2
+            size: 8
+            delete_on_termination: true
+            snapshot_id: '{{ setup_snapshot.snapshot_id }}'
+      register: result
+      ignore_errors: true
+      vars:
+        ansible_python_interpreter: "{{ botocore_virtualenv_interpreter }}"
+
+    - name: set image id fact for deletion later
+      set_fact:
+        ec2_ami_image_id_boot_tpm: "{{ result.image_id }}"
+        ec2_ami_snapshot_boot_tpm: "{{ result.block_device_mapping[ec2_ami_root_disk].snapshot_id }}"
+
+    - name: gather facts about the image created
+      ec2_ami_info:
+        image_ids: '{{ ec2_ami_image_id_boot_tpm }}'
+      register: ami_facts_result_boot_tpm
+      ignore_errors: true
+      vars:
+        ansible_python_interpreter: "{{ botocore_virtualenv_interpreter }}"
+
+    - name: assert that new ami has been created with desired options
+      assert:
+        that:
+          - "result.changed"
+          - "result.image_id.startswith('ami-')"
+          - ami_facts_result_boot_tpm.images[0].image_id | length != 0
+          - ami_facts_result_boot_tpm.images[0].boot_mode == 'uefi'
+          - ami_facts_result_boot_tpm.images[0].tpm_support == 'v2.0'
 
     # ============================================================
 
@@ -641,6 +708,13 @@
       debug:
         msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****"
 
+    - name: delete ami
+      ec2_ami:
+        state: absent
+        image_id: "{{ ec2_ami_image_id_boot_tpm }}"
+        wait: yes
+      ignore_errors: yes
+
     - name: delete ami
       ec2_ami:
         state: absent
diff --git a/tests/unit/plugins/modules/test_ec2_ami.py b/tests/unit/plugins/modules/test_ec2_ami.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/plugins/modules/test_ec2_ami.py
@@ -0,0 +1,44 @@
+# 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, Mock, patch, call
+
+import pytest
+
+from ansible_collections.amazon.aws.plugins.modules import ec2_ami
+
+module_name = "ansible_collections.amazon.aws.plugins.modules.ec2_ami"
+
+
+@patch(module_name + ".get_image_by_id")
+def test_create_image_uefi_data(m_get_image_by_id):
+    module = MagicMock()
+    connection = MagicMock()
+
+    m_get_image_by_id.return_value = {
+        "ImageId": "ami-0c7a795306730b288",
+        "BootMode": "uefi",
+        "TpmSupport": "v2.0",
+    }
+
+    module.params = {
+        "name": "my-image",
+        "boot_mode": "uefi",
+        "tpm_support": "v2.0",
+        "uefi_data": "QU1aTlVFRkk9xcN0AAAAAHj5a7fZ9+3aT2gcVRgA8Ek3NipiPST0pCiCIlTJtj20FzENCcQa",
+    }
+
+    ec2_ami.create_image(module, connection)
+    assert connection.register_image.call_count == 1
+    connection.register_image.assert_has_calls(
+        [
+            call(
+                aws_retry=True,
+                Description=None,
+                Name="my-image",
+                BootMode="uefi",
+                TpmSupport="v2.0",
+                UefiData="QU1aTlVFRkk9xcN0AAAAAHj5a7fZ9+3aT2gcVRgA8Ek3NipiPST0pCiCIlTJtj20FzENCcQa"
+            )
+        ]
+    )