SSM - Maintenance Windows (#4549)

This commit is contained in:
Bert Blommers 2021-11-09 21:28:24 -01:00 committed by GitHub
parent c62a34528e
commit db0738025a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 333 additions and 9 deletions

View File

@ -4196,7 +4196,7 @@
## ssm
<details>
<summary>17% implemented</summary>
<summary>20% implemented</summary>
- [X] add_tags_to_resource
- [ ] associate_ops_item_related_item
@ -4206,7 +4206,7 @@
- [ ] create_association
- [ ] create_association_batch
- [X] create_document
- [ ] create_maintenance_window
- [X] create_maintenance_window
- [ ] create_ops_item
- [ ] create_ops_metadata
- [ ] create_patch_baseline
@ -4215,7 +4215,7 @@
- [ ] delete_association
- [X] delete_document
- [ ] delete_inventory
- [ ] delete_maintenance_window
- [X] delete_maintenance_window
- [ ] delete_ops_metadata
- [X] delete_parameter
- [X] delete_parameters
@ -4248,7 +4248,7 @@
- [ ] describe_maintenance_window_schedule
- [ ] describe_maintenance_window_targets
- [ ] describe_maintenance_window_tasks
- [ ] describe_maintenance_windows
- [X] describe_maintenance_windows
- [ ] describe_maintenance_windows_for_target
- [ ] describe_ops_items
- [X] describe_parameters
@ -4267,7 +4267,7 @@
- [X] get_document
- [ ] get_inventory
- [ ] get_inventory_schema
- [ ] get_maintenance_window
- [X] get_maintenance_window
- [ ] get_maintenance_window_execution
- [ ] get_maintenance_window_execution_task
- [ ] get_maintenance_window_execution_task_invocation

View File

@ -35,7 +35,11 @@ ssm
- [ ] create_association
- [ ] create_association_batch
- [X] create_document
- [ ] create_maintenance_window
- [X] create_maintenance_window
Creates a maintenance window. No error handling or input validation has been implemented yet.
- [ ] create_ops_item
- [ ] create_ops_metadata
- [ ] create_patch_baseline
@ -44,7 +48,11 @@ ssm
- [ ] delete_association
- [X] delete_document
- [ ] delete_inventory
- [ ] delete_maintenance_window
- [X] delete_maintenance_window
Assumes the provided WindowId exists. No error handling has been implemented yet.
- [ ] delete_ops_metadata
- [X] delete_parameter
- [X] delete_parameters
@ -77,7 +85,13 @@ ssm
- [ ] describe_maintenance_window_schedule
- [ ] describe_maintenance_window_targets
- [ ] describe_maintenance_window_tasks
- [ ] describe_maintenance_windows
- [X] describe_maintenance_windows
Returns all windows. No pagination has been implemented yet. Only filtering for Name is supported.
The NextExecutionTime-field is not returned.
- [ ] describe_maintenance_windows_for_target
- [ ] describe_ops_items
- [X] describe_parameters
@ -100,7 +114,12 @@ ssm
- [X] get_document
- [ ] get_inventory
- [ ] get_inventory_schema
- [ ] get_maintenance_window
- [X] get_maintenance_window
The window is assumed to exist - no error handling has been implemented yet.
The NextExecutionTime-field is not returned.
- [ ] get_maintenance_window_execution
- [ ] get_maintenance_window_execution_task
- [ ] get_maintenance_window_execution_task_invocation

View File

@ -15,6 +15,7 @@ import uuid
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 @@ class SimpleSystemManagerBackend(BaseBackend):
self._errors = []
self._documents: Dict[str, Documents] = {}
self.windows: Dict[str, FakeMaintenanceWindow] = dict()
self._region = region_name
def reset(self):
@ -1735,6 +1785,63 @@ class SimpleSystemManagerBackend(BaseBackend):
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"):

View File

@ -379,3 +379,46 @@ class SimpleSystemManagerResponse(BaseResponse):
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 "{}"

View File

@ -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([])