Repository: getmoto/moto (python)

Issue:
EC2: Add support for hibernation APIs in `describe_instances`
This is feature request expands on https://github.com/getmoto/moto/issues/5336. While `HibernationOptions` are supported in many AWS APIs, the other one where it would be valuable to add support is [describe_instances](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/describe_instances.html#describe-instances).

Pull request:
EC2: Streamline output of run_instances()/describe_instances()
The duplicate copy/pasta nature before meant that  `run_instances()` and `describe_instances()` were missing different fields in the response. 

The output of both methods is now the same.

Fixes #6903 

Patch:
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
index 30785bf1c..4bded43bb 100644
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -47,6 +47,7 @@ class InstanceResponse(EC2BaseResponse):
                 account_id=self.current_account,
                 reservations=reservations_resp,
                 next_token=next_token,
+                run_instances=False,
             )
             .replace("True", "true")
             .replace("False", "false")
@@ -128,7 +129,9 @@ class InstanceResponse(EC2BaseResponse):
 
         template = self.response_template(EC2_RUN_INSTANCES)
         return template.render(
-            account_id=self.current_account, reservation=new_reservation
+            account_id=self.current_account,
+            reservation=new_reservation,
+            run_instances=True,
         )
 
     def terminate_instances(self) -> str:
@@ -443,25 +446,20 @@ BLOCK_DEVICE_MAPPING_TEMPLATE = {
     },
 }
 
-EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
-  <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
-  <reservationId>{{ reservation.id }}</reservationId>
-  <ownerId>{{ account_id }}</ownerId>
-  <groupSet>
-    <item>
-      <groupId>sg-245f6a01</groupId>
-      <groupName>default</groupName>
-    </item>
-  </groupSet>
-  <instancesSet>
-    {% for instance in reservation.instances %}
-        <item>
+INSTANCE_TEMPLATE = """<item>
           <instanceId>{{ instance.id }}</instanceId>
           <imageId>{{ instance.image_id }}</imageId>
+          {% if run_instances %}
           <instanceState>
             <code>0</code>
             <name>pending</name>
          </instanceState>
+          {% else %}
+          <instanceState>
+            <code>{{ instance._state.code }}</code>
+            <name>{{ instance._state.name }}</name>
+         </instanceState>
+         {% endif %}
           <privateDnsName>{{ instance.private_dns }}</privateDnsName>
           <publicDnsName>{{ instance.public_dns }}</publicDnsName>
           <dnsName>{{ instance.public_dns }}</dnsName>
@@ -509,8 +507,12 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
           <groupSet>
              {% for group in instance.dynamic_group_list %}
              <item>
+               {% if group.id %}
                 <groupId>{{ group.id }}</groupId>
                 <groupName>{{ group.name }}</groupName>
+                {% else %}
+                  <groupId>{{ group }}</groupId>
+                {% endif %}
              </item>
              {% endfor %}
           </groupSet>
@@ -518,24 +520,47 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
           <platform>{{ instance.platform }}</platform>
           {% endif %}
           <virtualizationType>{{ instance.virtualization_type }}</virtualizationType>
+          <stateReason>
+              <code>{{ instance._state_reason.code }}</code>
+              <message>{{ instance._state_reason.message }}</message>
+          </stateReason>
           <architecture>{{ instance.architecture }}</architecture>
           <kernelId>{{ instance.kernel }}</kernelId>
-          <clientToken/>
+          <rootDeviceType>ebs</rootDeviceType>
+          <rootDeviceName>/dev/sda1</rootDeviceName>
+          <blockDeviceMapping>
+              {% for device_name,deviceobject in instance.get_block_device_mapping %}
+              <item>
+                 <deviceName>{{ device_name }}</deviceName>
+                  <ebs>
+                     <volumeId>{{ deviceobject.volume_id }}</volumeId>
+                     <status>{{ deviceobject.status }}</status>
+                     <attachTime>{{ deviceobject.attach_time }}</attachTime>
+                     <deleteOnTermination>{{ deviceobject.delete_on_termination }}</deleteOnTermination>
+                     <size>{{deviceobject.size}}</size>
+                </ebs>
+              </item>
+             {% endfor %}
+          </blockDeviceMapping>
+          <clientToken>ABCDE{{ account_id }}3</clientToken>
           <hypervisor>xen</hypervisor>
-          <ebsOptimized>false</ebsOptimized>
           {% if instance.hibernation_options %}
           <hibernationOptions>
             <configured>{{ instance.hibernation_options.get("Configured") }}</configured>
           </hibernationOptions>
           {% endif %}
+          {% if instance.get_tags() %}
           <tagSet>
             {% for tag in instance.get_tags() %}
               <item>
+                <resourceId>{{ tag.resource_id }}</resourceId>
+                <resourceType>{{ tag.resource_type }}</resourceType>
                 <key>{{ tag.key }}</key>
                 <value>{{ tag.value }}</value>
               </item>
             {% endfor %}
           </tagSet>
+          {% endif %}
           <networkInterfaceSet>
             {% for nic in instance.nics.values() %}
               <item>
@@ -553,8 +578,12 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
                 <groupSet>
                   {% for group in nic.group_set %}
                   <item>
-                    <groupId>{{ group.id }}</groupId>
-                    <groupName>{{ group.name }}</groupName>
+                    {% if group.id %}
+                      <groupId>{{ group.id }}</groupId>
+                      <groupName>{{ group.name }}</groupName>
+                    {% else %}
+                      <groupId>{{ group }}</groupId>
+                    {% endif %}
                   </item>
                   {% endfor %}
                 </groupSet>
@@ -586,12 +615,31 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
               </item>
             {% endfor %}
           </networkInterfaceSet>
-        </item>
+        </item>"""
+
+EC2_RUN_INSTANCES = (
+    """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
+  <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
+  <reservationId>{{ reservation.id }}</reservationId>
+  <ownerId>{{ account_id }}</ownerId>
+  <groupSet>
+    <item>
+      <groupId>sg-245f6a01</groupId>
+      <groupName>default</groupName>
+    </item>
+  </groupSet>
+  <instancesSet>
+    {% for instance in reservation.instances %}
+        """
+    + INSTANCE_TEMPLATE
+    + """
     {% endfor %}
   </instancesSet>
   </RunInstancesResponse>"""
+)
 
-EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
+EC2_DESCRIBE_INSTANCES = (
+    """<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
   <requestId>fdcdcab1-ae5c-489e-9c33-4637c5dda355</requestId>
       <reservationSet>
         {% for reservation in reservations %}
@@ -612,165 +660,9 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns="http://ec2.amazona
             </groupSet>
             <instancesSet>
                 {% for instance in reservation.instances %}
-                  <item>
-                    <instanceId>{{ instance.id }}</instanceId>
-                    <imageId>{{ instance.image_id }}</imageId>
-                    <instanceState>
-                      <code>{{ instance._state.code }}</code>
-                      <name>{{ instance._state.name }}</name>
-                    </instanceState>
-                    <privateDnsName>{{ instance.private_dns }}</privateDnsName>
-                    <publicDnsName>{{ instance.public_dns }}</publicDnsName>
-                    <dnsName>{{ instance.public_dns }}</dnsName>
-                    <reason>{{ instance._reason }}</reason>
-                    {% if instance.key_name is not none %}
-                        <keyName>{{ instance.key_name }}</keyName>
-                    {% endif %}
-                    <ebsOptimized>{{ instance.ebs_optimized }}</ebsOptimized>
-                    <amiLaunchIndex>{{ instance.ami_launch_index }}</amiLaunchIndex>
-                    <productCodes/>
-                    <instanceType>{{ instance.instance_type }}</instanceType>
-                    {% if instance.iam_instance_profile %}
-                    <iamInstanceProfile>
-                        <arn>{{ instance.iam_instance_profile['Arn'] }}</arn>
-                        <id>{{ instance.iam_instance_profile['Id'] }}</id>
-                    </iamInstanceProfile>
-                    {% endif %}
-                    <launchTime>{{ instance.launch_time }}</launchTime>
-                    {% if instance.lifecycle %}
-                    <instanceLifecycle>{{ instance.lifecycle }}</instanceLifecycle>
-                    {% endif %}
-                    <placement>
-                      {% if instance.placement_hostid %}<hostId>{{ instance.placement_hostid }}</hostId>{% endif %}
-                      <availabilityZone>{{ instance.placement }}</availabilityZone>
-                      <groupName/>
-                      <tenancy>default</tenancy>
-                    </placement>
-                    {% if instance.platform %}
-                    <platform>{{ instance.platform }}</platform>
-                    {% endif %}
-                    <monitoring>
-                      <state>{{ instance.monitoring_state }}</state>
-                    </monitoring>
-                    {% if instance.subnet_id %}
-                      <subnetId>{{ instance.subnet_id }}</subnetId>
-                    {% elif instance.nics[0].subnet.id %}
-                      <subnetId>{{ instance.nics[0].subnet.id }}</subnetId>
-                    {% endif %}
-                    {% if instance.vpc_id %}
-                      <vpcId>{{ instance.vpc_id }}</vpcId>
-                    {% elif instance.nics[0].subnet.vpc_id %}
-                      <vpcId>{{ instance.nics[0].subnet.vpc_id }}</vpcId>
-                    {% endif %}
-                    <privateIpAddress>{{ instance.private_ip }}</privateIpAddress>
-                    {% if instance.nics[0].public_ip %}
-                        <ipAddress>{{ instance.nics[0].public_ip }}</ipAddress>
-                    {% endif %}
-                    <sourceDestCheck>{{ instance.source_dest_check }}</sourceDestCheck>
-                    <groupSet>
-                      {% for group in instance.dynamic_group_list %}
-                      <item>
-                      {% if group.id %}
-                      <groupId>{{ group.id }}</groupId>
-                      <groupName>{{ group.name }}</groupName>
-                      {% else %}
-                      <groupId>{{ group }}</groupId>
-                      {% endif %}
-                      </item>
-                      {% endfor %}
-                    </groupSet>
-                    <stateReason>
-                      <code>{{ instance._state_reason.code }}</code>
-                      <message>{{ instance._state_reason.message }}</message>
-                    </stateReason>
-                    <architecture>{{ instance.architecture }}</architecture>
-                    <kernelId>{{ instance.kernel }}</kernelId>
-                    <rootDeviceType>ebs</rootDeviceType>
-                    <rootDeviceName>/dev/sda1</rootDeviceName>
-                    <blockDeviceMapping>
-                        {% for device_name,deviceobject in instance.get_block_device_mapping %}
-                      <item>
-                         <deviceName>{{ device_name }}</deviceName>
-                          <ebs>
-                             <volumeId>{{ deviceobject.volume_id }}</volumeId>
-                             <status>{{ deviceobject.status }}</status>
-                             <attachTime>{{ deviceobject.attach_time }}</attachTime>
-                             <deleteOnTermination>{{ deviceobject.delete_on_termination }}</deleteOnTermination>
-                             <size>{{deviceobject.size}}</size>
-                        </ebs>
-                      </item>
-                     {% endfor %}
-                    </blockDeviceMapping>
-                    <virtualizationType>{{ instance.virtualization_type }}</virtualizationType>
-                    <clientToken>ABCDE{{ account_id }}3</clientToken>
-                    {% if instance.get_tags() %}
-                    <tagSet>
-                      {% for tag in instance.get_tags() %}
-                        <item>
-                          <resourceId>{{ tag.resource_id }}</resourceId>
-                          <resourceType>{{ tag.resource_type }}</resourceType>
-                          <key>{{ tag.key }}</key>
-                          <value>{{ tag.value }}</value>
-                        </item>
-                      {% endfor %}
-                    </tagSet>
-                    {% endif %}
-                    <hypervisor>xen</hypervisor>
-                    <networkInterfaceSet>
-                      {% for nic in instance.nics.values() %}
-                        <item>
-                          <networkInterfaceId>{{ nic.id }}</networkInterfaceId>
-                          {% if nic.subnet %}
-                            <subnetId>{{ nic.subnet.id }}</subnetId>
-                            <vpcId>{{ nic.subnet.vpc_id }}</vpcId>
-                          {% endif %}
-                          <description>Primary network interface</description>
-                          <ownerId>{{ account_id }}</ownerId>
-                          <status>in-use</status>
-                          <macAddress>1b:2b:3c:4d:5e:6f</macAddress>
-                          <privateIpAddress>{{ nic.private_ip_address }}</privateIpAddress>
-                          <sourceDestCheck>{{ instance.source_dest_check }}</sourceDestCheck>
-                          <groupSet>
-                            {% for group in nic.group_set %}
-                            <item>
-               {% if group.id %}
-               <groupId>{{ group.id }}</groupId>
-               <groupName>{{ group.name }}</groupName>
-               {% else %}
-               <groupId>{{ group }}</groupId>
-               {% endif %}
-                            </item>
-                            {% endfor %}
-                          </groupSet>
-                          <attachment>
-                            <attachmentId>{{ nic.attachment_id }}</attachmentId>
-                            <deviceIndex>{{ nic.device_index }}</deviceIndex>
-                            <status>attached</status>
-                            <attachTime>2015-01-01T00:00:00Z</attachTime>
-                            <deleteOnTermination>true</deleteOnTermination>
-                          </attachment>
-                          {% if nic.public_ip %}
-                            <association>
-                              <publicIp>{{ nic.public_ip }}</publicIp>
-                              <ipOwnerId>{{ account_id }}</ipOwnerId>
-                            </association>
-                          {% endif %}
-                          <privateIpAddressesSet>
-                            <item>
-                              <privateIpAddress>{{ nic.private_ip_address }}</privateIpAddress>
-                              <primary>true</primary>
-                              {% if nic.public_ip %}
-                                <association>
-                                  <publicIp>{{ nic.public_ip }}</publicIp>
-                                  <ipOwnerId>{{ account_id }}</ipOwnerId>
-                                </association>
-                              {% endif %}
-                            </item>
-                          </privateIpAddressesSet>
-                        </item>
-                      {% endfor %}
-                    </networkInterfaceSet>
-                  </item>
+                  """
+    + INSTANCE_TEMPLATE
+    + """
                 {% endfor %}
             </instancesSet>
           </item>
@@ -780,6 +672,7 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns="http://ec2.amazona
       <nextToken>{{ next_token }}</nextToken>
       {% endif %}
 </DescribeInstancesResponse>"""
+)
 
 EC2_TERMINATE_INSTANCES = """
 <TerminateInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">


Test patch:
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
index d57fcfef5..e9e4fe469 100644
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -1192,6 +1192,7 @@ def test_run_instance_with_security_group_id():
 @mock_ec2
 @pytest.mark.parametrize("hibernate", [True, False])
 def test_run_instance_with_additional_args(hibernate):
+    client = boto3.client("ec2", region_name="us-east-1")
     ec2 = boto3.resource("ec2", region_name="us-east-1")
     instance = ec2.create_instances(
         ImageId=EXAMPLE_AMI_ID,
@@ -1206,6 +1207,10 @@ def test_run_instance_with_additional_args(hibernate):
     assert instance.placement["AvailabilityZone"] == "us-east-1b"
     assert instance.hibernation_options == {"Configured": hibernate}
 
+    reservations = client.describe_instances(InstanceIds=[instance.id])["Reservations"]
+    instance = reservations[0]["Instances"][0]
+    assert instance["HibernationOptions"] == {"Configured": hibernate}
+
 
 @mock_ec2
 def test_run_instance_with_default_placement():