Repository: getmoto/moto

Issue:
`describe_maintenance_windows` action has not been implemented
## Reporting Bugs

`moto` version `1.3.14` installed with `pip` in a virtualenv on `python3` / ubuntu. Using `python3` mocks.

The following simple test fails 

```
from moto import mock_autoscaling, mock_ec2, mock_ssm
import unittest


class EC2HelperTests(unittest.TestCase):

    def setUp(self):
        for vv in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']:
            os.environ[vv] = 'testing'

    def tearDown(self):
        for vv in ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SECURITY_TOKEN', 'AWS_SESSION_TOKEN']:
            os.environ.pop(vv)

    @mock_ssm
    def test_get_maintenance_window(self):
        region = 'us-east-1'

        patch_region.return_value = region
        patch_instance_id.return_value = 'fake-instance'

        ssm_client = boto3.client('ssm', region_name=region)

        response = ssm_client.describe_maintenance_windows(
            Filters=[
                {
                    'Key': 'Name',
                    'Values': [
                        "fake-maintenance-window-name",
                    ]
                },
            ],
        )
```

with the following exception

```
Traceback (most recent call last):
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/moto/core/models.py", line 88, in wrapper
    result = func(*args, **kwargs)
  File "/usr/lib/python3.6/unittest/mock.py", line 1183, in patched
    return func(*args, **keywargs)
  File "/data/code/pix4d/berlin-infra/fields_cloud_data_sync/couchbase-ami/pix4d-scripts/couchbase_tools/pix4d_couchbase_tools/tests/test_cb_node.py", line 102, in test_get_maintenance_window
    "fake-maintenance-window-name",
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/client.py", line 316, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/client.py", line 622, in _make_api_call
    operation_model, request_dict, request_context)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/client.py", line 641, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/endpoint.py", line 137, in _send_request
    success_response, exception):
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/endpoint.py", line 256, in _needs_retry
    caught_exception=caught_exception, request_dict=request_dict)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 183, in __call__
    if self._checker(attempts, response, caught_exception):
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 251, in __call__
    caught_exception)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 269, in _should_retry
    return self._checker(attempt_number, response, caught_exception)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 317, in __call__
    caught_exception)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 223, in __call__
    attempt_number, caught_exception)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/retryhandler.py", line 359, in _check_caught_exception
    raise caught_exception
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/endpoint.py", line 197, in _do_get_response
    responses = self._event_emitter.emit(event_name, request=request)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/moto/core/models.py", line 272, in __call__
    request, request.url, request.headers
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/moto/core/responses.py", line 197, in dispatch
    return cls()._dispatch(*args, **kwargs)
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/moto/core/responses.py", line 295, in _dispatch
    return self.call_action()
  File "/data/code/pix4d/sandbox/venv_enterprise/lib/python3.6/site-packages/moto/core/responses.py", line 393, in call_action
    "The {0} action has not been implemented".format(action)
NotImplementedError: The describe_maintenance_windows action has not been implemented

```

## Actual result

Exception raised with a `NotImplementedError` message.

## Expected result

1. successful call to `ssm_client.describe_maintenance_windows` without exception raised
1. filter working on the set of maintenance windows: 
    1. first call returns an empty set of maintenance windows
    1. second call (after having manually created the maintenance windows) has an active filter with a behaviour close to the one of aws-cli/boto



Discussion:
Thanks for raising this @raffienficiaud - marking this as an enhancement

Resolving patch:
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -15,6 +15,7 @@
 import json
 import yaml
 import hashlib
+import random
 
 from .utils import parameter_arn
 from .exceptions import (
@@ -669,6 +670,53 @@ def _valid_parameter_data_type(data_type):
     return data_type in ("text", "aws:ec2:image")
 
 
+class FakeMaintenanceWindow:
+    def __init__(
+        self,
+        name,
+        description,
+        enabled,
+        duration,
+        cutoff,
+        schedule,
+        schedule_timezone,
+        schedule_offset,
+        start_date,
+        end_date,
+    ):
+        self.id = FakeMaintenanceWindow.generate_id()
+        self.name = name
+        self.description = description
+        self.enabled = enabled
+        self.duration = duration
+        self.cutoff = cutoff
+        self.schedule = schedule
+        self.schedule_timezone = schedule_timezone
+        self.schedule_offset = schedule_offset
+        self.start_date = start_date
+        self.end_date = end_date
+
+    def to_json(self):
+        return {
+            "WindowId": self.id,
+            "Name": self.name,
+            "Description": self.description,
+            "Enabled": self.enabled,
+            "Duration": self.duration,
+            "Cutoff": self.cutoff,
+            "Schedule": self.schedule,
+            "ScheduleTimezone": self.schedule_timezone,
+            "ScheduleOffset": self.schedule_offset,
+            "StartDate": self.start_date,
+            "EndDate": self.end_date,
+        }
+
+    @staticmethod
+    def generate_id():
+        chars = list(range(10)) + ["a", "b", "c", "d", "e", "f"]
+        return "mw-" + "".join(str(random.choice(chars)) for _ in range(17))
+
+
 class SimpleSystemManagerBackend(BaseBackend):
     def __init__(self, region_name=None):
         super(SimpleSystemManagerBackend, self).__init__()
@@ -681,6 +729,8 @@ def __init__(self, region_name=None):
         self._errors = []
         self._documents: Dict[str, Documents] = {}
 
+        self.windows: Dict[str, FakeMaintenanceWindow] = dict()
+
         self._region = region_name
 
     def reset(self):
@@ -1735,6 +1785,63 @@ def get_command_invocation(self, **kwargs):
         command = self.get_command_by_id(command_id)
         return command.get_invocation(instance_id, plugin_name)
 
+    def create_maintenance_window(
+        self,
+        name,
+        description,
+        enabled,
+        duration,
+        cutoff,
+        schedule,
+        schedule_timezone,
+        schedule_offset,
+        start_date,
+        end_date,
+    ):
+        """
+        Creates a maintenance window. No error handling or input validation has been implemented yet.
+        """
+        window = FakeMaintenanceWindow(
+            name,
+            description,
+            enabled,
+            duration,
+            cutoff,
+            schedule,
+            schedule_timezone,
+            schedule_offset,
+            start_date,
+            end_date,
+        )
+        self.windows[window.id] = window
+        return window.id
+
+    def get_maintenance_window(self, window_id):
+        """
+        The window is assumed to exist - no error handling has been implemented yet.
+        The NextExecutionTime-field is not returned.
+        """
+        return self.windows[window_id]
+
+    def describe_maintenance_windows(self, filters):
+        """
+        Returns all windows. No pagination has been implemented yet. Only filtering for Name is supported.
+        The NextExecutionTime-field is not returned.
+
+        """
+        res = [window for window in self.windows.values()]
+        if filters:
+            for f in filters:
+                if f["Key"] == "Name":
+                    res = [w for w in res if w.name in f["Values"]]
+        return res
+
+    def delete_maintenance_window(self, window_id):
+        """
+        Assumes the provided WindowId exists. No error handling has been implemented yet.
+        """
+        del self.windows[window_id]
+
 
 ssm_backends = {}
 for region in Session().get_available_regions("ssm"):
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -379,3 +379,46 @@ def get_command_invocation(self):
         return json.dumps(
             self.ssm_backend.get_command_invocation(**self.request_params)
         )
+
+    def create_maintenance_window(self):
+        name = self._get_param("Name")
+        desc = self._get_param("Description", None)
+        enabled = self._get_bool_param("Enabled", True)
+        duration = self._get_int_param("Duration")
+        cutoff = self._get_int_param("Cutoff")
+        schedule = self._get_param("Schedule")
+        schedule_timezone = self._get_param("ScheduleTimezone")
+        schedule_offset = self._get_int_param("ScheduleOffset")
+        start_date = self._get_param("StartDate")
+        end_date = self._get_param("EndDate")
+        window_id = self.ssm_backend.create_maintenance_window(
+            name=name,
+            description=desc,
+            enabled=enabled,
+            duration=duration,
+            cutoff=cutoff,
+            schedule=schedule,
+            schedule_timezone=schedule_timezone,
+            schedule_offset=schedule_offset,
+            start_date=start_date,
+            end_date=end_date,
+        )
+        return json.dumps({"WindowId": window_id})
+
+    def get_maintenance_window(self):
+        window_id = self._get_param("WindowId")
+        window = self.ssm_backend.get_maintenance_window(window_id)
+        return json.dumps(window.to_json())
+
+    def describe_maintenance_windows(self):
+        filters = self._get_param("Filters", None)
+        windows = [
+            window.to_json()
+            for window in self.ssm_backend.describe_maintenance_windows(filters)
+        ]
+        return json.dumps({"WindowIdentities": windows})
+
+    def delete_maintenance_window(self):
+        window_id = self._get_param("WindowId")
+        self.ssm_backend.delete_maintenance_window(window_id)
+        return "{}"


Test patch:
diff --git a/tests/test_ssm/test_ssm_maintenance_windows.py b/tests/test_ssm/test_ssm_maintenance_windows.py
new file mode 100644
--- /dev/null
+++ b/tests/test_ssm/test_ssm_maintenance_windows.py
@@ -0,0 +1,155 @@
+import boto3
+import sure  # noqa # pylint: disable=unused-import
+
+from moto import mock_ssm
+
+
+@mock_ssm
+def test_describe_maintenance_window():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.describe_maintenance_windows()
+    resp.should.have.key("WindowIdentities").equals([])
+
+    resp = ssm.describe_maintenance_windows(
+        Filters=[{"Key": "Name", "Values": ["fake-maintenance-window-name",]},],
+    )
+    resp.should.have.key("WindowIdentities").equals([])
+
+
+@mock_ssm
+def test_create_maintenance_windows_simple():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    resp.should.have.key("WindowId")
+    _id = resp["WindowId"]  # mw-01d6bbfdf6af2c39a
+
+    resp = ssm.describe_maintenance_windows()
+    resp.should.have.key("WindowIdentities").have.length_of(1)
+
+    my_window = resp["WindowIdentities"][0]
+    my_window.should.have.key("WindowId").equal(_id)
+    my_window.should.have.key("Name").equal("simple-window")
+    my_window.should.have.key("Enabled").equal(True)
+    my_window.should.have.key("Duration").equal(2)
+    my_window.should.have.key("Cutoff").equal(1)
+    my_window.should.have.key("Schedule").equal("cron(15 12 * * ? *)")
+    # my_window.should.have.key("NextExecutionTime")
+    my_window.shouldnt.have.key("Description")
+    my_window.shouldnt.have.key("ScheduleTimezone")
+    my_window.shouldnt.have.key("ScheduleOffset")
+    my_window.shouldnt.have.key("EndDate")
+    my_window.shouldnt.have.key("StartDate")
+
+
+@mock_ssm
+def test_create_maintenance_windows_advanced():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Description="French windows are just too fancy",
+        Schedule="cron(15 12 * * ? *)",
+        ScheduleTimezone="Europe/London",
+        ScheduleOffset=1,
+        Duration=5,
+        Cutoff=4,
+        AllowUnassociatedTargets=False,
+        StartDate="2021-11-01",
+        EndDate="2021-12-31",
+    )
+    resp.should.have.key("WindowId")
+    _id = resp["WindowId"]  # mw-01d6bbfdf6af2c39a
+
+    resp = ssm.describe_maintenance_windows()
+    resp.should.have.key("WindowIdentities").have.length_of(1)
+
+    my_window = resp["WindowIdentities"][0]
+    my_window.should.have.key("WindowId").equal(_id)
+    my_window.should.have.key("Name").equal("simple-window")
+    my_window.should.have.key("Enabled").equal(True)
+    my_window.should.have.key("Duration").equal(5)
+    my_window.should.have.key("Cutoff").equal(4)
+    my_window.should.have.key("Schedule").equal("cron(15 12 * * ? *)")
+    # my_window.should.have.key("NextExecutionTime")
+    my_window.should.have.key("Description").equals("French windows are just too fancy")
+    my_window.should.have.key("ScheduleTimezone").equals("Europe/London")
+    my_window.should.have.key("ScheduleOffset").equals(1)
+    my_window.should.have.key("StartDate").equals("2021-11-01")
+    my_window.should.have.key("EndDate").equals("2021-12-31")
+
+
+@mock_ssm
+def test_get_maintenance_windows():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="my-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    resp.should.have.key("WindowId")
+    _id = resp["WindowId"]  # mw-01d6bbfdf6af2c39a
+
+    my_window = ssm.get_maintenance_window(WindowId=_id)
+    my_window.should.have.key("WindowId").equal(_id)
+    my_window.should.have.key("Name").equal("my-window")
+    my_window.should.have.key("Enabled").equal(True)
+    my_window.should.have.key("Duration").equal(2)
+    my_window.should.have.key("Cutoff").equal(1)
+    my_window.should.have.key("Schedule").equal("cron(15 12 * * ? *)")
+    # my_window.should.have.key("NextExecutionTime")
+    my_window.shouldnt.have.key("Description")
+    my_window.shouldnt.have.key("ScheduleTimezone")
+    my_window.shouldnt.have.key("ScheduleOffset")
+    my_window.shouldnt.have.key("EndDate")
+    my_window.shouldnt.have.key("StartDate")
+
+
+@mock_ssm
+def test_describe_maintenance_windows():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    for idx in range(0, 4):
+        ssm.create_maintenance_window(
+            Name=f"window_{idx}",
+            Schedule="cron(15 12 * * ? *)",
+            Duration=2,
+            Cutoff=1,
+            AllowUnassociatedTargets=False,
+        )
+
+    resp = ssm.describe_maintenance_windows()
+    resp.should.have.key("WindowIdentities").have.length_of(4)
+
+    resp = ssm.describe_maintenance_windows(
+        Filters=[{"Key": "Name", "Values": ["window_0", "window_2",]},],
+    )
+    resp.should.have.key("WindowIdentities").have.length_of(2)
+
+
+@mock_ssm
+def test_delete_maintenance_windows():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+
+    ssm.delete_maintenance_window(WindowId=(resp["WindowId"]))
+
+    resp = ssm.describe_maintenance_windows()
+    resp.should.have.key("WindowIdentities").equals([])