Repository: ansible-collections/amazon.aws

Issue:
ec2_ami: Add support for sharing AMIs on Organizations level or to OUs
### Summary

It would be very beneficial to have the ability to share an AMI across an Organization or OUs since AWS has added that ability.

[A similar issue was introduced to Packer's Amazon Plugin.](https://github.com/hashicorp/packer-plugin-amazon/issues/156)

`org_arns` and `ou_arns` could be added to the `launch_permissions` paramater.

### Issue Type

Feature Idea

### Component Name

ec2_ami

### Additional Information

Possible examples if `org_arns` and `ou_arns` are added to the `launch_permissions` parameter.

```yaml
- name: Allow AMI to be launched across an organization
  amazon.aws.ec2_ami:
    image_id: "{{ instance.image_id }}"
    state: present
    launch_permissions:
      org_arns: ['arn:aws:organizations::123456789012:organization/o-ab12c3ef4']
```

```yaml
- name: Allow AMI to be launched across an OU
  amazon.aws.ec2_ami:
    image_id: "{{ instance.image_id }}"
    state: present
    launch_permissions:
      ou_arns: ['some_ou_arn_here']
```


### 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 --->

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
@@ -115,12 +115,33 @@
     type: bool
   launch_permissions:
     description:
-      - Users and groups that should be able to launch the AMI.
-      - Expects dictionary with a key of C(user_ids) and/or C(group_names).
-      - C(user_ids) should be a list of account IDs.
-      - C(group_name) should be a list of groups, C(all) is the only acceptable value currently.
+      - Launch permissions for the AMI.
       - You must pass all desired launch permissions if you wish to modify existing launch permissions (passing just groups will remove all users).
+    required: false
     type: dict
+    suboptions:
+      user_ids:
+        description: List of account IDs.
+        type: list
+        elements: str
+        required: false
+      group_names:
+        description: List of group names.
+        type: list
+        elements: str
+        required: false
+      org_arns:
+        description: List of The Amazon Resource Name(s) (ARN) of organization(s).
+        type: list
+        elements: str
+        required: false
+        version_added: 6.4.0
+      org_unit_arns:
+        description: List of The Amazon Resource Name(s) (ARN) of an organizational unit(s) (OU).
+        type: list
+        elements: str
+        required: false
+        version_added: 6.4.0
   image_location:
     description:
       - The S3 location of an image to use for the AMI.
@@ -279,6 +300,14 @@
     state: present
     launch_permissions:
       user_ids: ['123456789012']
+
+- name: Update AMI Launch Permissions, share AMI across an Organization and Organizational Units
+  amazon.aws.ec2_ami:
+    image_id: "{{ instance.image_id }}"
+    state: present
+    launch_permissions:
+      org_arns: ['arn:aws:organizations::123456789012:organization/o-123ab4cdef']
+      org_unit_arns: ['arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5example']
 """
 
 RETURN = r"""
@@ -633,18 +662,39 @@ def set_launch_permission(connection, image, launch_permissions, check_mode):
         desired_users = set(str(user_id) for user_id in launch_permissions.get("user_ids", []))
         current_groups = set(permission["Group"] for permission in current_permissions if "Group" in permission)
         desired_groups = set(launch_permissions.get("group_names", []))
+        current_org_arns = set(
+            permission["OrganizationArn"] for permission in current_permissions if "OrganizationArn" in permission
+        )
+        desired_org_arns = set(str(org_arn) for org_arn in launch_permissions.get("org_arns", []))
+        current_org_unit_arns = set(
+            permission["OrganizationalUnitArn"]
+            for permission in current_permissions
+            if "OrganizationalUnitArn" in permission
+        )
+        desired_org_unit_arns = set(str(org_unit_arn) for org_unit_arn in launch_permissions.get("org_unit_arns", []))
 
         to_add_users = desired_users - current_users
         to_remove_users = current_users - desired_users
         to_add_groups = desired_groups - current_groups
         to_remove_groups = current_groups - desired_groups
+        to_add_org_arns = desired_org_arns - current_org_arns
+        to_remove_org_arns = current_org_arns - desired_org_arns
+        to_add_org_unit_arns = desired_org_unit_arns - current_org_unit_arns
+        to_remove_org_unit_arns = current_org_unit_arns - desired_org_unit_arns
+
+        to_add = (
+            [dict(Group=group) for group in sorted(to_add_groups)]
+            + [dict(UserId=user_id) for user_id in sorted(to_add_users)]
+            + [dict(OrganizationArn=org_arn) for org_arn in sorted(to_add_org_arns)]
+            + [dict(OrganizationalUnitArn=org_unit_arn) for org_unit_arn in sorted(to_add_org_unit_arns)]
+        )
 
-        to_add = [dict(Group=group) for group in sorted(to_add_groups)] + [
-            dict(UserId=user_id) for user_id in sorted(to_add_users)
-        ]
-        to_remove = [dict(Group=group) for group in sorted(to_remove_groups)] + [
-            dict(UserId=user_id) for user_id in sorted(to_remove_users)
-        ]
+        to_remove = (
+            [dict(Group=group) for group in sorted(to_remove_groups)]
+            + [dict(UserId=user_id) for user_id in sorted(to_remove_users)]
+            + [dict(OrganizationArn=org_arn) for org_arn in sorted(to_remove_org_arns)]
+            + [dict(OrganizationalUnitArn=org_unit_arn) for org_unit_arn in sorted(to_remove_org_unit_arns)]
+        )
 
         if not (to_add or to_remove):
             return False
@@ -693,6 +743,10 @@ def set_description(connection, module, image, description):
     def do(cls, module, connection, image_id):
         """Entry point to update an image"""
         launch_permissions = module.params.get("launch_permissions")
+        # remove any keys with value=None
+        if launch_permissions:
+            launch_permissions = {k: v for k, v in launch_permissions.items() if v is not None}
+
         image = get_image_by_id(connection, image_id)
         if image is None:
             raise Ec2AmiFailure(f"Image {image_id} does not exist")
@@ -747,13 +801,18 @@ def set_tags(connection, module, tags, image_id):
     def set_launch_permissions(connection, launch_permissions, image_id):
         if not launch_permissions:
             return
-
+        # remove any keys with value=None
+        launch_permissions = {k: v for k, v in launch_permissions.items() if v is not None}
         try:
             params = {"Attribute": "LaunchPermission", "ImageId": image_id, "LaunchPermission": {"Add": []}}
             for group_name in launch_permissions.get("group_names", []):
                 params["LaunchPermission"]["Add"].append(dict(Group=group_name))
             for user_id in launch_permissions.get("user_ids", []):
                 params["LaunchPermission"]["Add"].append(dict(UserId=str(user_id)))
+            for org_arn in launch_permissions.get("org_arns", []):
+                params["LaunchPermission"]["Add"].append(dict(OrganizationArn=org_arn))
+            for org_unit_arn in launch_permissions.get("org_unit_arns", []):
+                params["LaunchPermission"]["Add"].append(dict(OrganizationalUnitArn=org_unit_arn))
             if params["LaunchPermission"]["Add"]:
                 connection.modify_image_attribute(aws_retry=True, **params)
         except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
@@ -897,7 +956,15 @@ def main():
         image_location={},
         instance_id={},
         kernel_id={},
-        launch_permissions={"type": "dict"},
+        launch_permissions=dict(
+            type="dict",
+            options=dict(
+                user_ids=dict(type="list", elements="str"),
+                group_names=dict(type="list", elements="str"),
+                org_arns=dict(type="list", elements="str"),
+                org_unit_arns=dict(type="list", elements="str"),
+            ),
+        ),
         name={},
         no_reboot={"default": False, "type": "bool"},
         purge_tags={"type": "bool", "default": True},


Test patch:
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
@@ -697,6 +697,48 @@
           - ami_facts_result_boot_tpm.images[0].boot_mode == 'uefi'
           - ami_facts_result_boot_tpm.images[0].tpm_support == 'v2.0'
 
+    # === Test modify launch permissions org_arns and org_unit_arns=========================
+
+    - name: create an image from the instance
+      ec2_ami:
+        instance_id: '{{ ec2_instance_id }}'
+        state: present
+        name: '{{ ec2_ami_name }}_permissions'
+        description: '{{ ec2_ami_description }}'
+        tags:
+          Name: '{{ ec2_ami_name }}_permissions'
+        wait: yes
+        root_device_name: '{{ ec2_ami_root_disk }}'
+      register: permissions_create_result
+
+    - name: modify the AMI launch permissions
+      ec2_ami:
+        state: present
+        image_id: '{{ permissions_create_result.image_id }}'
+        name: '{{ ec2_ami_name }}_permissions'
+        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']
+      register: permissions_update_result
+
+    - name: Get ami info
+      amazon.aws.ec2_ami_info:
+        image_ids: '{{ permissions_create_result.image_id }}'
+        describe_image_attributes: true
+      register: permissions_info_result
+
+    - name: assert that launch permissions have changed
+      assert:
+        that:
+          - "permissions_update_result.changed"
+          - "'organization_arn' in permissions_info_result.images[0].launch_permissions[0]"
+          - "permissions_info_result.images[0].launch_permissions[0]['organization_arn'] == 'arn:aws:organizations::123456789012:organization/o-123ab4cdef'"
+          - "'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'"
+
+
     # ============================================================
 
   always:
@@ -737,6 +779,14 @@
         wait: yes
       ignore_errors: yes
 
+    - name: delete ami
+      ec2_ami:
+        state: absent
+        image_id: "{{ ec2_ami_image_id }}"
+        name: '{{ ec2_ami_name }}_permissions'
+        wait: yes
+      ignore_errors: yes
+
     - name: remove setup snapshot of ec2 instance
       ec2_snapshot:
         state: absent