1#!/usr/local/bin/python3.8
2# Copyright: Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8
9DOCUMENTATION = '''
10---
11module: ec2_ami
12version_added: 1.0.0
13short_description: Create or destroy an image (AMI) in ec2
14description:
15     - Registers or deregisters ec2 images.
16options:
17  instance_id:
18    description:
19      - Instance ID to create the AMI from.
20    type: str
21  name:
22    description:
23      - The name of the new AMI.
24    type: str
25  architecture:
26    description:
27      - The target architecture of the image to register
28    default: "x86_64"
29    type: str
30  kernel_id:
31    description:
32      - The target kernel id of the image to register.
33    type: str
34  virtualization_type:
35    description:
36      - The virtualization type of the image to register.
37    default: "hvm"
38    type: str
39  root_device_name:
40    description:
41      - The root device name of the image to register.
42    type: str
43  wait:
44    description:
45      - Wait for the AMI to be in state 'available' before returning.
46    default: false
47    type: bool
48  wait_timeout:
49    description:
50      - How long before wait gives up, in seconds.
51    default: 1200
52    type: int
53  state:
54    description:
55      - Register or deregister an AMI.
56    default: 'present'
57    choices: [ "absent", "present" ]
58    type: str
59  description:
60    description:
61      - Human-readable string describing the contents and purpose of the AMI.
62    type: str
63  no_reboot:
64    description:
65      - Flag indicating that the bundling process should not attempt to shutdown the instance before bundling. If this flag is True, the
66        responsibility of maintaining file system integrity is left to the owner of the instance.
67    default: false
68    type: bool
69  image_id:
70    description:
71      - Image ID to be deregistered.
72    type: str
73  device_mapping:
74    description:
75      - List of device hashes/dictionaries with custom configurations (same block-device-mapping parameters).
76    type: list
77    elements: dict
78    suboptions:
79        device_name:
80          type: str
81          description:
82          - The device name. For example C(/dev/sda).
83          required: yes
84          aliases: ['DeviceName']
85        virtual_name:
86          type: str
87          description:
88          - The virtual name for the device.
89          - See the AWS documentation for more detail U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html).
90          - Alias C(VirtualName) has been deprecated and will be removed after 2022-06-01.
91          aliases: ['VirtualName']
92        no_device:
93          type: bool
94          description:
95          - Suppresses the specified device included in the block device mapping of the AMI.
96          - Alias C(NoDevice) has been deprecated and will be removed after 2022-06-01.
97          aliases: ['NoDevice']
98        volume_type:
99          type: str
100          description: The volume type.  Defaults to C(gp2) when not set.
101        delete_on_termination:
102          type: bool
103          description: Whether the device should be automatically deleted when the Instance is terminated.
104        snapshot_id:
105          type: str
106          description: The ID of the Snapshot.
107        iops:
108          type: int
109          description: When using an C(io1) I(volume_type) this sets the number of IOPS provisioned for the volume
110        encrypted:
111          type: bool
112          description: Whether the volume should be encrypted.
113        volume_size:
114          aliases: ['size']
115          type: int
116          description: The size of the volume (in GiB)
117  delete_snapshot:
118    description:
119      - Delete snapshots when deregistering the AMI.
120    default: false
121    type: bool
122  tags:
123    description:
124      - A dictionary of tags to add to the new image; '{"key":"value"}' and '{"key":"value","key":"value"}'
125    type: dict
126  purge_tags:
127    description: Whether to remove existing tags that aren't passed in the C(tags) parameter
128    default: false
129    type: bool
130  launch_permissions:
131    description:
132      - Users and groups that should be able to launch the AMI. Expects dictionary with a key of user_ids and/or group_names. user_ids should
133        be a list of account ids. group_name should be a list of groups, "all" is the only acceptable value currently.
134      - You must pass all desired launch permissions if you wish to modify existing launch permissions (passing just groups will remove all users)
135    type: dict
136  image_location:
137    description:
138      - The s3 location of an image to use for the AMI.
139    type: str
140  enhanced_networking:
141    description:
142      - A boolean representing whether enhanced networking with ENA is enabled or not.
143    type: bool
144  billing_products:
145    description:
146      - A list of valid billing codes. To be used with valid accounts by aws marketplace vendors.
147    type: list
148    elements: str
149  ramdisk_id:
150    description:
151      - The ID of the RAM disk.
152    type: str
153  sriov_net_support:
154    description:
155      - 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.
156    type: str
157author:
158    - "Evan Duffield (@scicoin-project) <eduffield@iacquire.com>"
159    - "Constantin Bugneac (@Constantin07) <constantin.bugneac@endava.com>"
160    - "Ross Williams (@gunzy83) <gunzy83au@gmail.com>"
161    - "Willem van Ketwich (@wilvk) <willvk@gmail.com>"
162extends_documentation_fragment:
163- amazon.aws.aws
164- amazon.aws.ec2
165
166'''
167
168# Thank you to iAcquire for sponsoring development of this module.
169
170EXAMPLES = '''
171# Note: These examples do not set authentication details, see the AWS Guide for details.
172
173- name: Basic AMI Creation
174  amazon.aws.ec2_ami:
175    instance_id: i-xxxxxx
176    wait: yes
177    name: newtest
178    tags:
179      Name: newtest
180      Service: TestService
181
182- name: Basic AMI Creation, without waiting
183  amazon.aws.ec2_ami:
184    instance_id: i-xxxxxx
185    wait: no
186    name: newtest
187
188- name: AMI Registration from EBS Snapshot
189  amazon.aws.ec2_ami:
190    name: newtest
191    state: present
192    architecture: x86_64
193    virtualization_type: hvm
194    root_device_name: /dev/xvda
195    device_mapping:
196      - device_name: /dev/xvda
197        volume_size: 8
198        snapshot_id: snap-xxxxxxxx
199        delete_on_termination: true
200        volume_type: gp2
201
202- name: AMI Creation, with a custom root-device size and another EBS attached
203  amazon.aws.ec2_ami:
204    instance_id: i-xxxxxx
205    name: newtest
206    device_mapping:
207        - device_name: /dev/sda1
208          size: XXX
209          delete_on_termination: true
210          volume_type: gp2
211        - device_name: /dev/sdb
212          size: YYY
213          delete_on_termination: false
214          volume_type: gp2
215
216- name: AMI Creation, excluding a volume attached at /dev/sdb
217  amazon.aws.ec2_ami:
218    instance_id: i-xxxxxx
219    name: newtest
220    device_mapping:
221        - device_name: /dev/sda1
222          size: XXX
223          delete_on_termination: true
224          volume_type: gp2
225        - device_name: /dev/sdb
226          no_device: yes
227
228- name: Deregister/Delete AMI (keep associated snapshots)
229  amazon.aws.ec2_ami:
230    image_id: "{{ instance.image_id }}"
231    delete_snapshot: False
232    state: absent
233
234- name: Deregister AMI (delete associated snapshots too)
235  amazon.aws.ec2_ami:
236    image_id: "{{ instance.image_id }}"
237    delete_snapshot: True
238    state: absent
239
240- name: Update AMI Launch Permissions, making it public
241  amazon.aws.ec2_ami:
242    image_id: "{{ instance.image_id }}"
243    state: present
244    launch_permissions:
245      group_names: ['all']
246
247- name: Allow AMI to be launched by another account
248  amazon.aws.ec2_ami:
249    image_id: "{{ instance.image_id }}"
250    state: present
251    launch_permissions:
252      user_ids: ['123456789012']
253'''
254
255RETURN = '''
256architecture:
257    description: Architecture of image.
258    returned: when AMI is created or already exists
259    type: str
260    sample: "x86_64"
261block_device_mapping:
262    description: Block device mapping associated with image.
263    returned: when AMI is created or already exists
264    type: dict
265    sample: {
266        "/dev/sda1": {
267            "delete_on_termination": true,
268            "encrypted": false,
269            "size": 10,
270            "snapshot_id": "snap-1a03b80e7",
271            "volume_type": "standard"
272        }
273    }
274creationDate:
275    description: Creation date of image.
276    returned: when AMI is created or already exists
277    type: str
278    sample: "2015-10-15T22:43:44.000Z"
279description:
280    description: Description of image.
281    returned: when AMI is created or already exists
282    type: str
283    sample: "nat-server"
284hypervisor:
285    description: Type of hypervisor.
286    returned: when AMI is created or already exists
287    type: str
288    sample: "xen"
289image_id:
290    description: ID of the image.
291    returned: when AMI is created or already exists
292    type: str
293    sample: "ami-1234abcd"
294is_public:
295    description: Whether image is public.
296    returned: when AMI is created or already exists
297    type: bool
298    sample: false
299launch_permission:
300    description: Permissions allowing other accounts to access the AMI.
301    returned: when AMI is created or already exists
302    type: list
303    sample:
304      - group: "all"
305location:
306    description: Location of image.
307    returned: when AMI is created or already exists
308    type: str
309    sample: "315210894379/nat-server"
310name:
311    description: AMI name of image.
312    returned: when AMI is created or already exists
313    type: str
314    sample: "nat-server"
315ownerId:
316    description: Owner of image.
317    returned: when AMI is created or already exists
318    type: str
319    sample: "435210894375"
320platform:
321    description: Platform of image.
322    returned: when AMI is created or already exists
323    type: str
324    sample: null
325root_device_name:
326    description: Root device name of image.
327    returned: when AMI is created or already exists
328    type: str
329    sample: "/dev/sda1"
330root_device_type:
331    description: Root device type of image.
332    returned: when AMI is created or already exists
333    type: str
334    sample: "ebs"
335state:
336    description: State of image.
337    returned: when AMI is created or already exists
338    type: str
339    sample: "available"
340tags:
341    description: A dictionary of tags assigned to image.
342    returned: when AMI is created or already exists
343    type: dict
344    sample: {
345        "Env": "devel",
346        "Name": "nat-server"
347    }
348virtualization_type:
349    description: Image virtualization type.
350    returned: when AMI is created or already exists
351    type: str
352    sample: "hvm"
353snapshots_deleted:
354    description: A list of snapshot ids deleted after deregistering image.
355    returned: after AMI is deregistered, if I(delete_snapshot=true)
356    type: list
357    sample: [
358        "snap-fbcccb8f",
359        "snap-cfe7cdb4"
360    ]
361'''
362
363import time
364
365try:
366    import botocore
367except ImportError:
368    pass  # Handled by AnsibleAWSModule
369
370from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
371
372from ..module_utils.core import AnsibleAWSModule
373from ..module_utils.core import is_boto3_error_code
374from ..module_utils.ec2 import AWSRetry
375from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list
376from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict
377from ..module_utils.ec2 import compare_aws_tags
378from ..module_utils.waiters import get_waiter
379
380
381def get_block_device_mapping(image):
382    bdm_dict = dict()
383    if image is not None and image.get('block_device_mappings') is not None:
384        bdm = image.get('block_device_mappings')
385        for device in bdm:
386            device_name = device.get('device_name')
387            if 'ebs' in device:
388                ebs = device.get("ebs")
389                bdm_dict_item = {
390                    'size': ebs.get("volume_size"),
391                    'snapshot_id': ebs.get("snapshot_id"),
392                    'volume_type': ebs.get("volume_type"),
393                    'encrypted': ebs.get("encrypted"),
394                    'delete_on_termination': ebs.get("delete_on_termination")
395                }
396            elif 'virtual_name' in device:
397                bdm_dict_item = dict(virtual_name=device['virtual_name'])
398            bdm_dict[device_name] = bdm_dict_item
399    return bdm_dict
400
401
402def get_ami_info(camel_image):
403    image = camel_dict_to_snake_dict(camel_image)
404    return dict(
405        image_id=image.get("image_id"),
406        state=image.get("state"),
407        architecture=image.get("architecture"),
408        block_device_mapping=get_block_device_mapping(image),
409        creationDate=image.get("creation_date"),
410        description=image.get("description"),
411        hypervisor=image.get("hypervisor"),
412        is_public=image.get("public"),
413        location=image.get("image_location"),
414        ownerId=image.get("owner_id"),
415        root_device_name=image.get("root_device_name"),
416        root_device_type=image.get("root_device_type"),
417        virtualization_type=image.get("virtualization_type"),
418        name=image.get("name"),
419        tags=boto3_tag_list_to_ansible_dict(image.get('tags')),
420        platform=image.get("platform"),
421        enhanced_networking=image.get("ena_support"),
422        image_owner_alias=image.get("image_owner_alias"),
423        image_type=image.get("image_type"),
424        kernel_id=image.get("kernel_id"),
425        product_codes=image.get("product_codes"),
426        ramdisk_id=image.get("ramdisk_id"),
427        sriov_net_support=image.get("sriov_net_support"),
428        state_reason=image.get("state_reason"),
429        launch_permissions=image.get('launch_permissions')
430    )
431
432
433def create_image(module, connection):
434    instance_id = module.params.get('instance_id')
435    name = module.params.get('name')
436    wait = module.params.get('wait')
437    wait_timeout = module.params.get('wait_timeout')
438    description = module.params.get('description')
439    architecture = module.params.get('architecture')
440    kernel_id = module.params.get('kernel_id')
441    root_device_name = module.params.get('root_device_name')
442    virtualization_type = module.params.get('virtualization_type')
443    no_reboot = module.params.get('no_reboot')
444    device_mapping = module.params.get('device_mapping')
445    tags = module.params.get('tags')
446    launch_permissions = module.params.get('launch_permissions')
447    image_location = module.params.get('image_location')
448    enhanced_networking = module.params.get('enhanced_networking')
449    billing_products = module.params.get('billing_products')
450    ramdisk_id = module.params.get('ramdisk_id')
451    sriov_net_support = module.params.get('sriov_net_support')
452
453    try:
454        params = {
455            'Name': name,
456            'Description': description
457        }
458
459        block_device_mapping = None
460
461        # Remove empty values injected by using options
462        if device_mapping:
463            block_device_mapping = []
464            for device in device_mapping:
465                device = dict((k, v) for k, v in device.items() if v is not None)
466                device['Ebs'] = {}
467                device = rename_item_if_exists(device, 'device_name', 'DeviceName')
468                device = rename_item_if_exists(device, 'virtual_name', 'VirtualName')
469                device = rename_item_if_exists(device, 'no_device', 'NoDevice')
470                device = rename_item_if_exists(device, 'volume_type', 'VolumeType', 'Ebs')
471                device = rename_item_if_exists(device, 'snapshot_id', 'SnapshotId', 'Ebs')
472                device = rename_item_if_exists(device, 'delete_on_termination', 'DeleteOnTermination', 'Ebs')
473                device = rename_item_if_exists(device, 'size', 'VolumeSize', 'Ebs', attribute_type=int)
474                device = rename_item_if_exists(device, 'volume_size', 'VolumeSize', 'Ebs', attribute_type=int)
475                device = rename_item_if_exists(device, 'iops', 'Iops', 'Ebs')
476                device = rename_item_if_exists(device, 'encrypted', 'Encrypted', 'Ebs')
477                block_device_mapping.append(device)
478        if block_device_mapping:
479            params['BlockDeviceMappings'] = block_device_mapping
480        if instance_id:
481            params['InstanceId'] = instance_id
482            params['NoReboot'] = no_reboot
483            image_id = connection.create_image(aws_retry=True, **params).get('ImageId')
484        else:
485            if architecture:
486                params['Architecture'] = architecture
487            if virtualization_type:
488                params['VirtualizationType'] = virtualization_type
489            if image_location:
490                params['ImageLocation'] = image_location
491            if enhanced_networking:
492                params['EnaSupport'] = enhanced_networking
493            if billing_products:
494                params['BillingProducts'] = billing_products
495            if ramdisk_id:
496                params['RamdiskId'] = ramdisk_id
497            if sriov_net_support:
498                params['SriovNetSupport'] = sriov_net_support
499            if kernel_id:
500                params['KernelId'] = kernel_id
501            if root_device_name:
502                params['RootDeviceName'] = root_device_name
503            image_id = connection.register_image(aws_retry=True, **params).get('ImageId')
504    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
505        module.fail_json_aws(e, msg="Error registering image")
506
507    if wait:
508        delay = 15
509        max_attempts = wait_timeout // delay
510        waiter = get_waiter(connection, 'image_available')
511        waiter.wait(ImageIds=[image_id], WaiterConfig=dict(Delay=delay, MaxAttempts=max_attempts))
512
513    if tags:
514        try:
515            connection.create_tags(aws_retry=True, Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags))
516        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
517            module.fail_json_aws(e, msg="Error tagging image")
518
519    if launch_permissions:
520        try:
521            params = dict(Attribute='LaunchPermission', ImageId=image_id, LaunchPermission=dict(Add=list()))
522            for group_name in launch_permissions.get('group_names', []):
523                params['LaunchPermission']['Add'].append(dict(Group=group_name))
524            for user_id in launch_permissions.get('user_ids', []):
525                params['LaunchPermission']['Add'].append(dict(UserId=str(user_id)))
526            if params['LaunchPermission']['Add']:
527                connection.modify_image_attribute(aws_retry=True, **params)
528        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
529            module.fail_json_aws(e, msg="Error setting launch permissions for image %s" % image_id)
530
531    module.exit_json(msg="AMI creation operation complete.", changed=True,
532                     **get_ami_info(get_image_by_id(module, connection, image_id)))
533
534
535def deregister_image(module, connection):
536    image_id = module.params.get('image_id')
537    delete_snapshot = module.params.get('delete_snapshot')
538    wait = module.params.get('wait')
539    wait_timeout = module.params.get('wait_timeout')
540    image = get_image_by_id(module, connection, image_id)
541
542    if image is None:
543        module.exit_json(changed=False)
544
545    # Get all associated snapshot ids before deregistering image otherwise this information becomes unavailable.
546    snapshots = []
547    if 'BlockDeviceMappings' in image:
548        for mapping in image.get('BlockDeviceMappings'):
549            snapshot_id = mapping.get('Ebs', {}).get('SnapshotId')
550            if snapshot_id is not None:
551                snapshots.append(snapshot_id)
552
553    # When trying to re-deregister an already deregistered image it doesn't raise an exception, it just returns an object without image attributes.
554    if 'ImageId' in image:
555        try:
556            connection.deregister_image(aws_retry=True, ImageId=image_id)
557        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
558            module.fail_json_aws(e, msg="Error deregistering image")
559    else:
560        module.exit_json(msg="Image %s has already been deregistered." % image_id, changed=False)
561
562    image = get_image_by_id(module, connection, image_id)
563    wait_timeout = time.time() + wait_timeout
564
565    while wait and wait_timeout > time.time() and image is not None:
566        image = get_image_by_id(module, connection, image_id)
567        time.sleep(3)
568
569    if wait and wait_timeout <= time.time():
570        module.fail_json(msg="Timed out waiting for image to be deregistered.")
571
572    exit_params = {'msg': "AMI deregister operation complete.", 'changed': True}
573
574    if delete_snapshot:
575        for snapshot_id in snapshots:
576            try:
577                connection.delete_snapshot(aws_retry=True, SnapshotId=snapshot_id)
578            # Don't error out if root volume snapshot was already deregistered as part of deregister_image
579            except is_boto3_error_code('InvalidSnapshot.NotFound'):
580                pass
581            except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
582                module.fail_json_aws(e, msg='Failed to delete snapshot.')
583        exit_params['snapshots_deleted'] = snapshots
584
585    module.exit_json(**exit_params)
586
587
588def update_image(module, connection, image_id):
589    launch_permissions = module.params.get('launch_permissions')
590    image = get_image_by_id(module, connection, image_id)
591    if image is None:
592        module.fail_json(msg="Image %s does not exist" % image_id, changed=False)
593    changed = False
594
595    if launch_permissions is not None:
596        current_permissions = image['LaunchPermissions']
597
598        current_users = set(permission['UserId'] for permission in current_permissions if 'UserId' in permission)
599        desired_users = set(str(user_id) for user_id in launch_permissions.get('user_ids', []))
600        current_groups = set(permission['Group'] for permission in current_permissions if 'Group' in permission)
601        desired_groups = set(launch_permissions.get('group_names', []))
602
603        to_add_users = desired_users - current_users
604        to_remove_users = current_users - desired_users
605        to_add_groups = desired_groups - current_groups
606        to_remove_groups = current_groups - desired_groups
607
608        to_add = [dict(Group=group) for group in to_add_groups] + [dict(UserId=user_id) for user_id in to_add_users]
609        to_remove = [dict(Group=group) for group in to_remove_groups] + [dict(UserId=user_id) for user_id in to_remove_users]
610
611        if to_add or to_remove:
612            try:
613                connection.modify_image_attribute(aws_retry=True,
614                                                  ImageId=image_id, Attribute='launchPermission',
615                                                  LaunchPermission=dict(Add=to_add, Remove=to_remove))
616                changed = True
617            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
618                module.fail_json_aws(e, msg="Error updating launch permissions of image %s" % image_id)
619
620    desired_tags = module.params.get('tags')
621    if desired_tags is not None:
622        current_tags = boto3_tag_list_to_ansible_dict(image.get('Tags'))
623        tags_to_add, tags_to_remove = compare_aws_tags(current_tags, desired_tags, purge_tags=module.params.get('purge_tags'))
624
625        if tags_to_remove:
626            try:
627                connection.delete_tags(aws_retry=True, Resources=[image_id], Tags=[dict(Key=tagkey) for tagkey in tags_to_remove])
628                changed = True
629            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
630                module.fail_json_aws(e, msg="Error updating tags")
631
632        if tags_to_add:
633            try:
634                connection.create_tags(aws_retry=True, Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags_to_add))
635                changed = True
636            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
637                module.fail_json_aws(e, msg="Error updating tags")
638
639    description = module.params.get('description')
640    if description and description != image['Description']:
641        try:
642            connection.modify_image_attribute(aws_retry=True, Attribute='Description ', ImageId=image_id, Description=dict(Value=description))
643            changed = True
644        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
645            module.fail_json_aws(e, msg="Error setting description for image %s" % image_id)
646
647    if changed:
648        module.exit_json(msg="AMI updated.", changed=True,
649                         **get_ami_info(get_image_by_id(module, connection, image_id)))
650    else:
651        module.exit_json(msg="AMI not updated.", changed=False,
652                         **get_ami_info(get_image_by_id(module, connection, image_id)))
653
654
655def get_image_by_id(module, connection, image_id):
656    try:
657        try:
658            images_response = connection.describe_images(aws_retry=True, ImageIds=[image_id])
659        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
660            module.fail_json_aws(e, msg="Error retrieving image %s" % image_id)
661        images = images_response.get('Images')
662        no_images = len(images)
663        if no_images == 0:
664            return None
665        if no_images == 1:
666            result = images[0]
667            try:
668                result['LaunchPermissions'] = connection.describe_image_attribute(aws_retry=True, Attribute='launchPermission',
669                                                                                  ImageId=image_id)['LaunchPermissions']
670                result['ProductCodes'] = connection.describe_image_attribute(aws_retry=True, Attribute='productCodes',
671                                                                             ImageId=image_id)['ProductCodes']
672            except is_boto3_error_code('InvalidAMIID.Unavailable'):
673                pass
674            except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:  # pylint: disable=duplicate-except
675                module.fail_json_aws(e, msg="Error retrieving image attributes for image %s" % image_id)
676            return result
677        module.fail_json(msg="Invalid number of instances (%s) found for image_id: %s." % (str(len(images)), image_id))
678    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
679        module.fail_json_aws(e, msg="Error retrieving image by image_id")
680
681
682def rename_item_if_exists(dict_object, attribute, new_attribute, child_node=None, attribute_type=None):
683    new_item = dict_object.get(attribute)
684    if new_item is not None:
685        if attribute_type is not None:
686            new_item = attribute_type(new_item)
687        if child_node is None:
688            dict_object[new_attribute] = new_item
689        else:
690            dict_object[child_node][new_attribute] = new_item
691        dict_object.pop(attribute)
692    return dict_object
693
694
695def main():
696    mapping_options = dict(
697        device_name=dict(type='str', aliases=['DeviceName'], required=True),
698        virtual_name=dict(
699            type='str', aliases=['VirtualName'],
700            deprecated_aliases=[dict(name='VirtualName', date='2022-06-01', collection_name='amazon.aws')]),
701        no_device=dict(
702            type='bool', aliases=['NoDevice'],
703            deprecated_aliases=[dict(name='NoDevice', date='2022-06-01', collection_name='amazon.aws')]),
704        volume_type=dict(type='str'),
705        delete_on_termination=dict(type='bool'),
706        snapshot_id=dict(type='str'),
707        iops=dict(type='int'),
708        encrypted=dict(type='bool'),
709        volume_size=dict(type='int', aliases=['size']),
710    )
711    argument_spec = dict(
712        instance_id=dict(),
713        image_id=dict(),
714        architecture=dict(default='x86_64'),
715        kernel_id=dict(),
716        virtualization_type=dict(default='hvm'),
717        root_device_name=dict(),
718        delete_snapshot=dict(default=False, type='bool'),
719        name=dict(),
720        wait=dict(type='bool', default=False),
721        wait_timeout=dict(default=1200, type='int'),
722        description=dict(default=''),
723        no_reboot=dict(default=False, type='bool'),
724        state=dict(default='present', choices=['present', 'absent']),
725        device_mapping=dict(type='list', elements='dict', options=mapping_options),
726        tags=dict(type='dict'),
727        launch_permissions=dict(type='dict'),
728        image_location=dict(),
729        enhanced_networking=dict(type='bool'),
730        billing_products=dict(type='list', elements='str',),
731        ramdisk_id=dict(),
732        sriov_net_support=dict(),
733        purge_tags=dict(type='bool', default=False)
734    )
735
736    module = AnsibleAWSModule(
737        argument_spec=argument_spec,
738        required_if=[
739            ['state', 'absent', ['image_id']],
740        ]
741    )
742
743    # Using a required_one_of=[['name', 'image_id']] overrides the message that should be provided by
744    # the required_if for state=absent, so check manually instead
745    if not any([module.params['image_id'], module.params['name']]):
746        module.fail_json(msg="one of the following is required: name, image_id")
747
748    connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff())
749
750    if module.params.get('state') == 'absent':
751        deregister_image(module, connection)
752    elif module.params.get('state') == 'present':
753        if module.params.get('image_id'):
754            update_image(module, connection, module.params.get('image_id'))
755        if not module.params.get('instance_id') and not module.params.get('device_mapping'):
756            module.fail_json(msg="The parameters instance_id or device_mapping (register from EBS snapshot) are required for a new image.")
757        create_image(module, connection)
758
759
760if __name__ == '__main__':
761    main()
762