Skip to content

Commit

Permalink
SSM: add support for maintenance window targets (#6429)
Browse files Browse the repository at this point in the history
  • Loading branch information
pinzon authored Jun 23, 2023
1 parent 883d01d commit 9a247d5
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 3 deletions.
6 changes: 3 additions & 3 deletions IMPLEMENTATION_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6744,7 +6744,7 @@
- [ ] delete_resource_policy
- [ ] deregister_managed_instance
- [ ] deregister_patch_baseline_for_patch_group
- [ ] deregister_target_from_maintenance_window
- [X] deregister_target_from_maintenance_window
- [ ] deregister_task_from_maintenance_window
- [ ] describe_activations
- [ ] describe_association
Expand All @@ -6767,7 +6767,7 @@
- [ ] describe_maintenance_window_execution_tasks
- [ ] describe_maintenance_window_executions
- [ ] describe_maintenance_window_schedule
- [ ] describe_maintenance_window_targets
- [X] describe_maintenance_window_targets
- [ ] describe_maintenance_window_tasks
- [X] describe_maintenance_windows
- [ ] describe_maintenance_windows_for_target
Expand Down Expand Up @@ -6828,7 +6828,7 @@
- [ ] put_resource_policy
- [ ] register_default_patch_baseline
- [ ] register_patch_baseline_for_patch_group
- [ ] register_target_with_maintenance_window
- [X] register_target_with_maintenance_window
- [ ] register_task_with_maintenance_window
- [X] remove_tags_from_resource
- [ ] reset_service_setting
Expand Down
91 changes: 91 additions & 0 deletions moto/ssm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,48 @@ def _valid_parameter_data_type(data_type: str) -> bool:
return data_type in ("text", "aws:ec2:image")


class FakeMaintenanceWindowTarget:
def __init__(
self,
window_id: str,
resource_type: str,
targets: List[Dict[str, Any]],
owner_information: Optional[str],
name: Optional[str],
description: Optional[str],
):
self.window_id = window_id
self.window_target_id = self.generate_id()
self.resource_type = resource_type
self.targets = targets
self.name = name
self.description = description
self.owner_information = owner_information

def to_json(self) -> Dict[str, Any]:
return {
"WindowId": self.window_id,
"WindowTargetId": self.window_target_id,
"ResourceType": self.resource_type,
"Targets": self.targets,
"OwnerInformation": "",
"Name": self.name,
"Description": self.description,
}

@staticmethod
def generate_id() -> str:
return str(random.uuid4())


def _maintenance_window_target_filter_match(
filters: Optional[List[Dict[str, Any]]], target: FakeMaintenanceWindowTarget
) -> bool:
if not filters and target:
return True
return False


class FakeMaintenanceWindow:
def __init__(
self,
Expand All @@ -964,6 +1006,7 @@ def __init__(
self.schedule_offset = schedule_offset
self.start_date = start_date
self.end_date = end_date
self.targets: Dict[str, FakeMaintenanceWindowTarget] = {}

def to_json(self) -> Dict[str, Any]:
return {
Expand Down Expand Up @@ -2254,5 +2297,53 @@ def delete_patch_baseline(self, baseline_id: str) -> None:
"""
del self.baselines[baseline_id]

def register_target_with_maintenance_window(
self,
window_id: str,
resource_type: str,
targets: List[Dict[str, Any]],
owner_information: Optional[str],
name: Optional[str],
description: Optional[str],
) -> str:
"""
Registers a target with a maintenance window. No error handling has been implemented yet.
"""
window = self.get_maintenance_window(window_id)

target = FakeMaintenanceWindowTarget(
window_id,
resource_type,
targets,
owner_information=owner_information,
name=name,
description=description,
)
window.targets[target.window_target_id] = target
return target.window_target_id

def deregister_target_from_maintenance_window(
self, window_id: str, window_target_id: str
) -> None:
"""
Deregisters a target from a maintenance window. No error handling has been implemented yet.
"""
window = self.get_maintenance_window(window_id)
del window.targets[window_target_id]

def describe_maintenance_window_targets(
self, window_id: str, filters: Optional[List[Dict[str, Any]]]
) -> List[FakeMaintenanceWindowTarget]:
"""
Describes all targets for a maintenance window. No error handling has been implemented yet.
"""
window = self.get_maintenance_window(window_id)
targets = [
target
for target in window.targets.values()
if _maintenance_window_target_filter_match(filters, target)
]
return targets


ssm_backends = BackendDict(SimpleSystemManagerBackend, "ssm")
30 changes: 30 additions & 0 deletions moto/ssm/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,36 @@ def get_maintenance_window(self) -> str:
window = self.ssm_backend.get_maintenance_window(window_id)
return json.dumps(window.to_json())

def register_target_with_maintenance_window(self) -> str:
window_target_id = self.ssm_backend.register_target_with_maintenance_window(
window_id=self._get_param("WindowId"),
resource_type=self._get_param("ResourceType"),
targets=self._get_param("Targets"),
owner_information=self._get_param("OwnerInformation"),
name=self._get_param("Name"),
description=self._get_param("Description"),
)
return json.dumps({"WindowTargetId": window_target_id})

def describe_maintenance_window_targets(self) -> str:
window_id = self._get_param("WindowId")
filters = self._get_param("Filters", [])
targets = [
target.to_json()
for target in self.ssm_backend.describe_maintenance_window_targets(
window_id, filters
)
]
return json.dumps({"Targets": targets})

def deregister_target_from_maintenance_window(self) -> str:
window_id = self._get_param("WindowId")
window_target_id = self._get_param("WindowTargetId")
self.ssm_backend.deregister_target_from_maintenance_window(
window_id, window_target_id
)
return "{}"

def describe_maintenance_windows(self) -> str:
filters = self._get_param("Filters", None)
windows = [
Expand Down
62 changes: 62 additions & 0 deletions tests/test_ssm/test_ssm_maintenance_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,65 @@ def test_tags():
ResourceType="MaintenanceWindow", ResourceId=mw_id
)["TagList"]
assert tags == [{"Key": "k2", "Value": "v2"}]


@mock_ssm
def test_register_maintenance_window_target():
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,
)
window_id = resp["WindowId"]

resp = ssm.register_target_with_maintenance_window(
WindowId=window_id,
ResourceType="INSTANCE",
Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
)
resp.should.have.key("WindowTargetId")
_id = resp["WindowTargetId"]

resp = ssm.describe_maintenance_window_targets(
WindowId=window_id,
)
resp.should.have.key("Targets").should.have.length_of(1)
resp["Targets"][0].should.have.key("ResourceType").equal("INSTANCE")
resp["Targets"][0].should.have.key("WindowTargetId").equal(_id)
resp["Targets"][0]["Targets"][0].should.have.key("Key").equal("tag:Name")
resp["Targets"][0]["Targets"][0].should.have.key("Values").equal(["my-instance"])


@mock_ssm
def test_deregister_target_from_maintenance_window():
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,
)
window_id = resp["WindowId"]

resp = ssm.register_target_with_maintenance_window(
WindowId=window_id,
ResourceType="INSTANCE",
Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
)
_id = resp["WindowTargetId"]

ssm.deregister_target_from_maintenance_window(
WindowId=window_id,
WindowTargetId=_id,
)

resp = ssm.describe_maintenance_window_targets(
WindowId=window_id,
)
resp.should.have.key("Targets").should.have.length_of(0)

0 comments on commit 9a247d5

Please sign in to comment.