Repository: getmoto/moto

Issue:
Mock of boto3.client("ec2").describe_instance_status Filters Not Working As Expected
## Summary
When I mock out an AWS account with mock ec2 instances in different states (e.g. 2 in "running" and 1 in "stopped), I do not get the expected results when I run `describe_instance_status` using the `Filters` attribute. 

I am using `moto` version '1.0.1', `boto3` version '1.7.9', `botocore` version '1.10.9', `boto` version '2.49.0', and python 3.6.8. Everything was installed via `pip`.

## How to Reproduce the Issue
```
import moto

mock_aws_account = moto.mock_ec2()
mock_aws_account.start()

import boto3

# Set up mock "AWS Account"
c = boto3.client("ec2", region_name="us-west-2")
c.run_instances(ImageId="ami-faab1234", MinCount=3, MaxCount=3)

# stop the first one
first_instance_id = c.describe_instances()["Reservations"][0]["Instances"][0][
    "InstanceId"
]
c.stop_instances(InstanceIds=[first_instance_id])


# Count the number of instances (all, running, stopped)
# via boto3.client().describe_instances()

all_instances_via_describe_instances = c.describe_instances()["Reservations"][0][
    "Instances"
]

all_states_via_describe_instances = [
    i["State"]["Name"] for i in all_instances_via_describe_instances
]

count_describe_instances = {
    "all": len(all_states_via_describe_instances),
    "running": all_states_via_describe_instances.count("running"),
    "stopped": all_states_via_describe_instances.count("stopped"),
}


# Count the number of instances (all, running, stopped)
# via boto3.client().describe_instance_status()

# Count not using `Filters`

all_instances_via_describe_instance_status_no_filter = c.describe_instance_status(
    IncludeAllInstances=True
)["InstanceStatuses"]

all_states_via_describe_instance_status_no_filter = [
    i["InstanceState"]["Name"]
    for i in all_instances_via_describe_instance_status_no_filter
]

count_describe_instance_status_no_filter = {
    "all": len(all_states_via_describe_instance_status_no_filter),
    "running": all_states_via_describe_instance_status_no_filter.count("running"),
    "stopped": all_states_via_describe_instance_status_no_filter.count("stopped"),
}

# Count using `Filters`
filter_for = {
    "running_and_stopped": [
        {"Name": "instance-state-name", "Values": ["running", "stopped"]}
    ],
    "running": [{"Name": "instance-state-name", "Values": ["running"]}],
    "stopped": [{"Name": "instance-state-name", "Values": ["stopped"]}],
}

all_states_via_describe_instance_status_with_filter = c.describe_instance_status(
    IncludeAllInstances=True, Filters=filter_for["running_and_stopped"]
)["InstanceStatuses"]


all_instances_via_describe_instance_status_with_running_filter = c.describe_instance_status(
    IncludeAllInstances=True, Filters=filter_for["running"]
)[
    "InstanceStatuses"
]

all_instances_via_describe_instance_status_with_stopped_filter = c.describe_instance_status(
    IncludeAllInstances=True, Filters=filter_for["stopped"]
)[
    "InstanceStatuses"
]

# No need to count by running state, because each set should have the desired state(s) in it
count_describe_instance_status_with_filter = {
    "all": len(all_states_via_describe_instance_status_with_filter),
    "running": len(all_instances_via_describe_instance_status_with_running_filter),
    "stopped": len(all_instances_via_describe_instance_status_with_stopped_filter),
}


# We expect the number of instances to match when not using a filter
state = "all"
assert (
    count_describe_instances[state] == count_describe_instance_status_no_filter[state]
)

state = "running"
assert (
    count_describe_instances[state] == count_describe_instance_status_no_filter[state]
)

state = "stopped"
assert (
    count_describe_instances[state] == count_describe_instance_status_no_filter[state]
)


# We expect the number of instances to match when using a filter
state = "all"
assert (
    count_describe_instances[state] == count_describe_instance_status_with_filter[state]
)

# This fails
state = "running"
assert (
    count_describe_instances[state] == count_describe_instance_status_with_filter[state]
)

# This fails
state = "stopped"
assert (
    count_describe_instances[state] == count_describe_instance_status_with_filter[state]
)
```

The last two `assert`ions fail because it appears moto has not implemented the `Filters` option of `describe_instance_status`. I have not looked at the `moto` codebase to verify this is true.

## Expected Result

The number of instances returned by `describe_instance_status` when using `Filter` should match the number of instances in the same filtered state in the dictionary returned by `describe_instances`.





Discussion:

Resolving patch:
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -822,6 +822,21 @@ def get_cfn_attribute(self, attribute_name):
             return self.public_ip
         raise UnformattedGetAttTemplateException()
 
+    def applies(self, filters):
+        if filters:
+            applicable = False
+            for f in filters:
+                acceptable_values = f["values"]
+                if f["name"] == "instance-state-name":
+                    if self._state.name in acceptable_values:
+                        applicable = True
+                if f["name"] == "instance-state-code":
+                    if str(self._state.code) in acceptable_values:
+                        applicable = True
+            return applicable
+        # If there are no filters, all instances are valid
+        return True
+
 
 class InstanceBackend(object):
     def __init__(self):
@@ -921,22 +936,23 @@ def describe_instance_attribute(self, instance_id, attribute):
         value = getattr(instance, key)
         return instance, value
 
-    def all_instances(self):
+    def all_instances(self, filters=None):
         instances = []
         for reservation in self.all_reservations():
             for instance in reservation.instances:
-                instances.append(instance)
+                if instance.applies(filters):
+                    instances.append(instance)
         return instances
 
-    def all_running_instances(self):
+    def all_running_instances(self, filters=None):
         instances = []
         for reservation in self.all_reservations():
             for instance in reservation.instances:
-                if instance.state_code == 16:
+                if instance.state_code == 16 and instance.applies(filters):
                     instances.append(instance)
         return instances
 
-    def get_multi_instances_by_id(self, instance_ids):
+    def get_multi_instances_by_id(self, instance_ids, filters=None):
         """
         :param instance_ids: A string list with instance ids
         :return: A list with instance objects
@@ -946,7 +962,8 @@ def get_multi_instances_by_id(self, instance_ids):
         for reservation in self.all_reservations():
             for instance in reservation.instances:
                 if instance.id in instance_ids:
-                    result.append(instance)
+                    if instance.applies(filters):
+                        result.append(instance)
 
         # TODO: Trim error message down to specific invalid id.
         if instance_ids and len(instance_ids) > len(result):
diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py
--- a/moto/ec2/responses/instances.py
+++ b/moto/ec2/responses/instances.py
@@ -113,16 +113,34 @@ def start_instances(self):
             template = self.response_template(EC2_START_INSTANCES)
             return template.render(instances=instances)
 
+    def _get_list_of_dict_params(self, param_prefix, _dct):
+        """
+        Simplified version of _get_dict_param
+        Allows you to pass in a custom dict instead of using self.querystring by default
+        """
+        params = []
+        for key, value in _dct.items():
+            if key.startswith(param_prefix):
+                params.append(value)
+        return params
+
     def describe_instance_status(self):
         instance_ids = self._get_multi_param("InstanceId")
         include_all_instances = self._get_param("IncludeAllInstances") == "true"
+        filters = self._get_list_prefix("Filter")
+        filters = [
+            {"name": f["name"], "values": self._get_list_of_dict_params("value.", f)}
+            for f in filters
+        ]
 
         if instance_ids:
-            instances = self.ec2_backend.get_multi_instances_by_id(instance_ids)
+            instances = self.ec2_backend.get_multi_instances_by_id(
+                instance_ids, filters
+            )
         elif include_all_instances:
-            instances = self.ec2_backend.all_instances()
+            instances = self.ec2_backend.all_instances(filters)
         else:
-            instances = self.ec2_backend.all_running_instances()
+            instances = self.ec2_backend.all_running_instances(filters)
 
         template = self.response_template(EC2_INSTANCE_STATUS)
         return template.render(instances=instances)


Test patch:
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -1144,7 +1144,7 @@ def test_describe_instance_status_with_instances():
 
 
 @mock_ec2_deprecated
-def test_describe_instance_status_with_instance_filter():
+def test_describe_instance_status_with_instance_filter_deprecated():
     conn = boto.connect_ec2("the_key", "the_secret")
 
     # We want to filter based on this one
@@ -1166,6 +1166,75 @@ def test_describe_instance_status_with_instance_filter():
     cm.exception.request_id.should_not.be.none
 
 
+@mock_ec2
+def test_describe_instance_status_with_instance_filter():
+    conn = boto3.client("ec2", region_name="us-west-1")
+
+    # We want to filter based on this one
+    reservation = conn.run_instances(ImageId="ami-1234abcd", MinCount=3, MaxCount=3)
+    instance1 = reservation["Instances"][0]
+    instance2 = reservation["Instances"][1]
+    instance3 = reservation["Instances"][2]
+    conn.stop_instances(InstanceIds=[instance1["InstanceId"]])
+    stopped_instance_ids = [instance1["InstanceId"]]
+    running_instance_ids = sorted([instance2["InstanceId"], instance3["InstanceId"]])
+    all_instance_ids = sorted(stopped_instance_ids + running_instance_ids)
+
+    # Filter instance using the state name
+    state_name_filter = {
+        "running_and_stopped": [
+            {"Name": "instance-state-name", "Values": ["running", "stopped"]}
+        ],
+        "running": [{"Name": "instance-state-name", "Values": ["running"]}],
+        "stopped": [{"Name": "instance-state-name", "Values": ["stopped"]}],
+    }
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_name_filter["running_and_stopped"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(all_instance_ids)
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_name_filter["running"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(running_instance_ids)
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_name_filter["stopped"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(stopped_instance_ids)
+
+    # Filter instance using the state code
+    state_code_filter = {
+        "running_and_stopped": [
+            {"Name": "instance-state-code", "Values": ["16", "80"]}
+        ],
+        "running": [{"Name": "instance-state-code", "Values": ["16"]}],
+        "stopped": [{"Name": "instance-state-code", "Values": ["80"]}],
+    }
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_code_filter["running_and_stopped"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(all_instance_ids)
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_code_filter["running"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(running_instance_ids)
+
+    found_statuses = conn.describe_instance_status(
+        IncludeAllInstances=True, Filters=state_code_filter["stopped"]
+    )["InstanceStatuses"]
+    found_instance_ids = [status["InstanceId"] for status in found_statuses]
+    sorted(found_instance_ids).should.equal(stopped_instance_ids)
+
+
 @requires_boto_gte("2.32.0")
 @mock_ec2_deprecated
 def test_describe_instance_status_with_non_running_instances():