Repository: getmoto/moto

Issue:
Expose logic of get_amis.py in moto as a test.utils.something
Would you guys be opposed to making this code

https://github.com/spulec/moto/blob/871591f7f17458c10ce039d6e2f36abe3430fec5/scripts/get_amis.py#L47-L65

a utility function somewhere in moto so that users who want/need to create their own MOTO_AMIS_PATH can reuse it?  I'm tempted to just copy/paste this tiny bit of code but that makes me 🤮 

Also, are you opposed to documenting MOTO_AMIS_PATH?  I was able to find it looking through the code but I'd be happy to document it as a part of a PR for this if y'all think it is a good idea. 


Discussion:
Hi @timsnyder, that is a very good suggestion, both the utility and documenting it.

The utility method would IMO make most sense in `moto/ec2/utils.py`.

Documenting the `MOTO_AMIS_PATH` would make most sense as a docstring of the `EC2Backend`-class here: https://github.com/spulec/moto/blob/master/moto/ec2/models.py#L8603
On release, any docstring will be automatically converted and added to the RTD for that specific service: http://docs.getmoto.org/en/latest/docs/services/ec2.html

A good example of what the RTD docs could look like is AWSLambda: http://docs.getmoto.org/en/latest/docs/services/lambda.html
All that documentation is defined in the `LambdaBackend`-class.
Thanks @bblommers.  I'll submit a PR shortly.

Patch:
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -8642,6 +8642,20 @@ class EC2Backend(
     IamInstanceProfileAssociationBackend,
     CarrierGatewayBackend,
 ):
+    """
+    Implementation of the AWS EC2 endpoint.
+
+    moto includes a limited set of AMIs in `moto/ec2/resources/amis.json`.  If you require specific
+    AMIs to be available during your tests, you can provide your own AMI definitions by setting the
+    environment variable `MOTO_AMIS_PATH` to point to a JSON file containing definitions of the
+    required AMIs.
+
+    To create such a file, refer to `scripts/get_amis.py`
+
+    .. note:: You must set `MOTO_AMIS_PATH` before importing moto.
+
+    """
+
     def __init__(self, region_name):
         self.region_name = region_name
         super().__init__()
diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py
--- a/moto/ec2/utils.py
+++ b/moto/ec2/utils.py
@@ -786,3 +786,55 @@ def describe_tag_filter(filters, instances):
                     if need_delete:
                         result.remove(instance)
     return result
+
+
+def gen_moto_amis(described_images, drop_images_missing_keys=True):
+    """Convert `boto3.EC2.Client.describe_images` output to form acceptable to `MOTO_AMIS_PATH`
+
+    Parameters
+    ==========
+    described_images : list of dicts
+        as returned by :ref:`boto3:EC2.Client.describe_images` in "Images" key
+    drop_images_missing_keys : bool, default=True
+        When `True` any entry in `images` that is missing a required key will silently
+        be excluded from the returned list
+
+    Throws
+    ======
+    `KeyError` when `drop_images_missing_keys` is `False` and a required key is missing
+    from an element of `images`
+
+    Returns
+    =======
+    list of dicts suitable to be serialized into JSON as a target for `MOTO_AMIS_PATH` environment
+    variable.
+
+    See Also
+    ========
+    * :ref:`moto.ec2.models.EC2Backend`
+    """
+    result = []
+    for image in described_images:
+        try:
+            tmp = {
+                "ami_id": image["ImageId"],
+                "name": image["Name"],
+                "description": image["Description"],
+                "owner_id": image["OwnerId"],
+                "public": image["Public"],
+                "virtualization_type": image["VirtualizationType"],
+                "architecture": image["Architecture"],
+                "state": image["State"],
+                "platform": image.get("Platform"),
+                "image_type": image["ImageType"],
+                "hypervisor": image["Hypervisor"],
+                "root_device_name": image["RootDeviceName"],
+                "root_device_type": image["RootDeviceType"],
+                "sriov": image.get("SriovNetSupport", "simple"),
+            }
+            result.append(tmp)
+        except Exception as err:
+            if not drop_images_missing_keys:
+                raise err
+
+    return result
diff --git a/scripts/get_amis.py b/scripts/get_amis.py
--- a/scripts/get_amis.py
+++ b/scripts/get_amis.py
@@ -1,6 +1,8 @@
 import boto3
 import json
 
+from moto.ec2.utils import gen_moto_amis
+
 # Taken from free tier list when creating an instance
 instances = [
     "ami-760aaa0f",
@@ -43,27 +45,6 @@
 
 test = client.describe_images(ImageIds=instances)
 
-result = []
-for image in test["Images"]:
-    try:
-        tmp = {
-            "ami_id": image["ImageId"],
-            "name": image["Name"],
-            "description": image["Description"],
-            "owner_id": image["OwnerId"],
-            "public": image["Public"],
-            "virtualization_type": image["VirtualizationType"],
-            "architecture": image["Architecture"],
-            "state": image["State"],
-            "platform": image.get("Platform"),
-            "image_type": image["ImageType"],
-            "hypervisor": image["Hypervisor"],
-            "root_device_name": image["RootDeviceName"],
-            "root_device_type": image["RootDeviceType"],
-            "sriov": image.get("SriovNetSupport", "simple"),
-        }
-        result.append(tmp)
-    except Exception as err:
-        pass
+result = gen_moto_amis(test["Images"])
 
 print(json.dumps(result, indent=2))


Test patch:
diff --git a/tests/test_ec2/test_utils.py b/tests/test_ec2/test_utils.py
--- a/tests/test_ec2/test_utils.py
+++ b/tests/test_ec2/test_utils.py
@@ -1,5 +1,8 @@
+from copy import deepcopy
 import ipaddress
+import sure  # noqa # pylint: disable=unused-import
 from unittest.mock import patch
+from pytest import raises
 
 from moto.ec2 import utils
 
@@ -23,3 +26,36 @@ def mocked_random_resource_id(chars: int):
         cidr_address = utils.random_ipv6_cidr()
         # this will throw value error if host bits are set
         ipaddress.ip_network(cidr_address)
+
+
+def test_gen_moto_amis():
+    image_with_all_reqd_keys = {
+        "ImageId": "ami-03cf127a",
+        "State": "available",
+        "Public": True,
+        "OwnerId": "801119661308",
+        "RootDeviceType": "ebs",
+        "RootDeviceName": "/dev/sda1",
+        "Description": "Microsoft Windows Server 2016 Nano Locale English AMI provided by Amazon",
+        "ImageType": "machine",
+        "Architecture": "x86_64",
+        "Name": "Windows_Server-2016-English-Nano-Base-2017.10.13",
+        "VirtualizationType": "hvm",
+        "Hypervisor": "xen",
+    }
+
+    images = []
+    images.append(deepcopy(image_with_all_reqd_keys))
+    images.append(deepcopy(image_with_all_reqd_keys))
+
+    # make one of the copies of the image miss a key
+    images[1].pop("Public")
+
+    # with drop=True, it shouldn't throw but will give us only one AMI in the result
+    images.should.have.length_of(2)
+    amis = utils.gen_moto_amis(images, drop_images_missing_keys=True)
+    amis.should.have.length_of(1)
+
+    # with drop=False, it should raise KeyError because of the missing key
+    with raises(KeyError, match="'Public'"):
+        utils.gen_moto_amis(images, drop_images_missing_keys=False)