SSM - Maintenance Windows (#4549)
This commit is contained in:
parent
c62a34528e
commit
db0738025a
@ -4196,7 +4196,7 @@
|
|||||||
|
|
||||||
## ssm
|
## ssm
|
||||||
<details>
|
<details>
|
||||||
<summary>17% implemented</summary>
|
<summary>20% implemented</summary>
|
||||||
|
|
||||||
- [X] add_tags_to_resource
|
- [X] add_tags_to_resource
|
||||||
- [ ] associate_ops_item_related_item
|
- [ ] associate_ops_item_related_item
|
||||||
@ -4206,7 +4206,7 @@
|
|||||||
- [ ] create_association
|
- [ ] create_association
|
||||||
- [ ] create_association_batch
|
- [ ] create_association_batch
|
||||||
- [X] create_document
|
- [X] create_document
|
||||||
- [ ] create_maintenance_window
|
- [X] create_maintenance_window
|
||||||
- [ ] create_ops_item
|
- [ ] create_ops_item
|
||||||
- [ ] create_ops_metadata
|
- [ ] create_ops_metadata
|
||||||
- [ ] create_patch_baseline
|
- [ ] create_patch_baseline
|
||||||
@ -4215,7 +4215,7 @@
|
|||||||
- [ ] delete_association
|
- [ ] delete_association
|
||||||
- [X] delete_document
|
- [X] delete_document
|
||||||
- [ ] delete_inventory
|
- [ ] delete_inventory
|
||||||
- [ ] delete_maintenance_window
|
- [X] delete_maintenance_window
|
||||||
- [ ] delete_ops_metadata
|
- [ ] delete_ops_metadata
|
||||||
- [X] delete_parameter
|
- [X] delete_parameter
|
||||||
- [X] delete_parameters
|
- [X] delete_parameters
|
||||||
@ -4248,7 +4248,7 @@
|
|||||||
- [ ] describe_maintenance_window_schedule
|
- [ ] describe_maintenance_window_schedule
|
||||||
- [ ] describe_maintenance_window_targets
|
- [ ] describe_maintenance_window_targets
|
||||||
- [ ] describe_maintenance_window_tasks
|
- [ ] describe_maintenance_window_tasks
|
||||||
- [ ] describe_maintenance_windows
|
- [X] describe_maintenance_windows
|
||||||
- [ ] describe_maintenance_windows_for_target
|
- [ ] describe_maintenance_windows_for_target
|
||||||
- [ ] describe_ops_items
|
- [ ] describe_ops_items
|
||||||
- [X] describe_parameters
|
- [X] describe_parameters
|
||||||
@ -4267,7 +4267,7 @@
|
|||||||
- [X] get_document
|
- [X] get_document
|
||||||
- [ ] get_inventory
|
- [ ] get_inventory
|
||||||
- [ ] get_inventory_schema
|
- [ ] get_inventory_schema
|
||||||
- [ ] get_maintenance_window
|
- [X] get_maintenance_window
|
||||||
- [ ] get_maintenance_window_execution
|
- [ ] get_maintenance_window_execution
|
||||||
- [ ] get_maintenance_window_execution_task
|
- [ ] get_maintenance_window_execution_task
|
||||||
- [ ] get_maintenance_window_execution_task_invocation
|
- [ ] get_maintenance_window_execution_task_invocation
|
||||||
|
@ -35,7 +35,11 @@ ssm
|
|||||||
- [ ] create_association
|
- [ ] create_association
|
||||||
- [ ] create_association_batch
|
- [ ] create_association_batch
|
||||||
- [X] create_document
|
- [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_item
|
||||||
- [ ] create_ops_metadata
|
- [ ] create_ops_metadata
|
||||||
- [ ] create_patch_baseline
|
- [ ] create_patch_baseline
|
||||||
@ -44,7 +48,11 @@ ssm
|
|||||||
- [ ] delete_association
|
- [ ] delete_association
|
||||||
- [X] delete_document
|
- [X] delete_document
|
||||||
- [ ] delete_inventory
|
- [ ] delete_inventory
|
||||||
- [ ] delete_maintenance_window
|
- [X] delete_maintenance_window
|
||||||
|
|
||||||
|
Assumes the provided WindowId exists. No error handling has been implemented yet.
|
||||||
|
|
||||||
|
|
||||||
- [ ] delete_ops_metadata
|
- [ ] delete_ops_metadata
|
||||||
- [X] delete_parameter
|
- [X] delete_parameter
|
||||||
- [X] delete_parameters
|
- [X] delete_parameters
|
||||||
@ -77,7 +85,13 @@ ssm
|
|||||||
- [ ] describe_maintenance_window_schedule
|
- [ ] describe_maintenance_window_schedule
|
||||||
- [ ] describe_maintenance_window_targets
|
- [ ] describe_maintenance_window_targets
|
||||||
- [ ] describe_maintenance_window_tasks
|
- [ ] 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_maintenance_windows_for_target
|
||||||
- [ ] describe_ops_items
|
- [ ] describe_ops_items
|
||||||
- [X] describe_parameters
|
- [X] describe_parameters
|
||||||
@ -100,7 +114,12 @@ ssm
|
|||||||
- [X] get_document
|
- [X] get_document
|
||||||
- [ ] get_inventory
|
- [ ] get_inventory
|
||||||
- [ ] get_inventory_schema
|
- [ ] 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
|
||||||
- [ ] get_maintenance_window_execution_task
|
- [ ] get_maintenance_window_execution_task
|
||||||
- [ ] get_maintenance_window_execution_task_invocation
|
- [ ] get_maintenance_window_execution_task_invocation
|
||||||
|
@ -15,6 +15,7 @@ import uuid
|
|||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
|
|
||||||
from .utils import parameter_arn
|
from .utils import parameter_arn
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
@ -669,6 +670,53 @@ def _valid_parameter_data_type(data_type):
|
|||||||
return data_type in ("text", "aws:ec2:image")
|
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):
|
class SimpleSystemManagerBackend(BaseBackend):
|
||||||
def __init__(self, region_name=None):
|
def __init__(self, region_name=None):
|
||||||
super(SimpleSystemManagerBackend, self).__init__()
|
super(SimpleSystemManagerBackend, self).__init__()
|
||||||
@ -681,6 +729,8 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||||||
self._errors = []
|
self._errors = []
|
||||||
self._documents: Dict[str, Documents] = {}
|
self._documents: Dict[str, Documents] = {}
|
||||||
|
|
||||||
|
self.windows: Dict[str, FakeMaintenanceWindow] = dict()
|
||||||
|
|
||||||
self._region = region_name
|
self._region = region_name
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
@ -1735,6 +1785,63 @@ class SimpleSystemManagerBackend(BaseBackend):
|
|||||||
command = self.get_command_by_id(command_id)
|
command = self.get_command_by_id(command_id)
|
||||||
return command.get_invocation(instance_id, plugin_name)
|
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 = {}
|
ssm_backends = {}
|
||||||
for region in Session().get_available_regions("ssm"):
|
for region in Session().get_available_regions("ssm"):
|
||||||
|
@ -379,3 +379,46 @@ class SimpleSystemManagerResponse(BaseResponse):
|
|||||||
return json.dumps(
|
return json.dumps(
|
||||||
self.ssm_backend.get_command_invocation(**self.request_params)
|
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 "{}"
|
||||||
|
155
tests/test_ssm/test_ssm_maintenance_windows.py
Normal file
155
tests/test_ssm/test_ssm_maintenance_windows.py
Normal 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([])
|
Loading…
Reference in New Issue
Block a user