diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py index 66943ade5..b99dd6eae 100644 --- a/linode_api4/groups/monitor.py +++ b/linode_api4/groups/monitor.py @@ -6,6 +6,7 @@ from linode_api4.objects import ( AlertChannel, AlertDefinition, + AlertDefinitionEntity, MonitorDashboard, MonitorMetricsDefinition, MonitorService, @@ -219,6 +220,8 @@ def create_alert_definition( channel_ids: list[int], rule_criteria: dict, trigger_conditions: dict, + scope: Optional[str] = None, + regions: Optional[list[str]] = None, entity_ids: Optional[list[str]] = None, description: Optional[str] = None, ) -> AlertDefinition: @@ -248,6 +251,10 @@ def create_alert_definition( :param trigger_conditions: Trigger conditions that define when the alert should transition state. :type trigger_conditions: dict + :param scope: (Optional) Alert scope (for example: `account`, `entity`, or `region`). Defaults to `entity`. + :type scope: Optional[str] + :param regions: (Optional) Regions to monitor. + :type regions: Optional[list[str]] :param entity_ids: (Optional) Restrict the alert to a subset of entity IDs. :type entity_ids: Optional[list[str]] :param description: (Optional) Longer description for the alert definition. @@ -267,6 +274,10 @@ def create_alert_definition( "rule_criteria": rule_criteria, "trigger_conditions": trigger_conditions, } + if scope is not None: + params["scope"] = scope + if regions is not None: + params["regions"] = regions if description is not None: params["description"] = description if entity_ids is not None: @@ -284,3 +295,38 @@ def create_alert_definition( ) return AlertDefinition(self.client, result["id"], service_type, result) + + def alert_definition_entities( + self, + service_type: str, + id: int, + *filters, + ) -> PaginatedList: + """ + List entities associated with a specific alert definition. + + This endpoint supports pagination fields (`page`, `page_size`) in the API. + + .. note:: This endpoint is in beta and requires using the v4beta base URL. + + API Documentation: + + :param service_type: Service type for the alert definition (e.g. `dbaas`). + :type service_type: str + :param id: Alert definition identifier. + :type id: int + :param filters: Optional filter expressions to apply to the collection. + See :doc:`Filtering Collections`. + + :returns: A paginated list of entities associated with the alert definition. + :rtype: PaginatedList[AlertDefinitionEntity] + """ + + endpoint = ( + f"/monitor/services/{service_type}/alert-definitions/{id}/entities" + ) + return self.client._get_and_filter( + AlertDefinitionEntity, + *filters, + endpoint=endpoint, + ) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index ca8f83921..69e2599cd 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -10,6 +10,9 @@ "Alert", "AlertChannel", "AlertDefinition", + "AlertDefinitionEntity", + "AlertEntities", + "AlertScope", "AlertType", "Alerts", "MonitorDashboard", @@ -387,6 +390,43 @@ class AlertType(StrEnum): user = "user" +class AlertScope(StrEnum): + """ + Scope values supported for alert definitions. + """ + + entity = "entity" + region = "region" + account = "account" + + +@dataclass +class AlertEntities(JSONObject): + """ + Represents entity metadata for an alert definition. + + For entity scoped alerts, `entities` envelope contains the URL to list entities, + a count, and a has_more_resources flag. + For region/account scoped alerts, the `entities` are returned as an empty object. + """ + + url: str = "" + count: int = 0 + has_more_resources: bool = False + + +@dataclass +class AlertDefinitionEntity(JSONObject): + """ + Represents an entity associated with an alert definition. + """ + + id: str = "" + label: str = "" + url: str = "" + type: str = "" + + class AlertDefinition(DerivedBase): """ Represents an alert definition for a monitor service. @@ -406,17 +446,24 @@ class AlertDefinition(DerivedBase): "severity": Property(mutable=True), "type": Property(mutable=True), "status": Property(mutable=True), - "has_more_resources": Property(mutable=True), + "scope": Property(AlertScope), + "regions": Property(mutable=True), + "has_more_resources": Property( + mutable=True + ), # Deprecated; use entities.has_more_resources + "entities": Property(json_object=AlertEntities), "rule_criteria": Property(mutable=True, json_object=RuleCriteria), "trigger_conditions": Property( mutable=True, json_object=TriggerConditions ), - "alert_channels": Property(mutable=True, json_object=Alerts), + "alert_channels": Property(mutable=True, json_object=Alert), "created": Property(is_datetime=True), "updated": Property(is_datetime=True), "updated_by": Property(), "created_by": Property(), - "entity_ids": Property(mutable=True), + "entity_ids": Property( + mutable=True + ), # Deprecated; use entities.url to fetch associated entities "description": Property(mutable=True), "service_class": Property(alias_of="class"), } diff --git a/test/fixtures/monitor_alert-definitions.json b/test/fixtures/monitor_alert-definitions.json index 92b6e0e4c..e500354d1 100644 --- a/test/fixtures/monitor_alert-definitions.json +++ b/test/fixtures/monitor_alert-definitions.json @@ -7,13 +7,26 @@ "severity": 1, "type": "user", "description": "A test alert for dbaas service", + "scope": "entity", + "regions": [], "entity_ids": ["13217"], - "alert_channels": [], + "entities": { + "url": "/monitor/services/dbaas/alert-definitions/12345/entities", + "count": 1, + "has_more_resources": false + }, + "alert_channels": [ + { + "id": 10000, + "label": "Read-Write Channel", + "type": "email", + "url": "/monitor/alert-channels/10000" + } + ], "has_more_resources": false, "rule_criteria": null, "trigger_conditions": null, "class": "alert", - "notification_groups": [], "status": "active", "created": "2024-01-01T00:00:00", "updated": "2024-01-01T00:00:00", diff --git a/test/fixtures/monitor_services_dbaas_alert-definitions.json b/test/fixtures/monitor_services_dbaas_alert-definitions.json index 0c7067a8a..494b407d4 100644 --- a/test/fixtures/monitor_services_dbaas_alert-definitions.json +++ b/test/fixtures/monitor_services_dbaas_alert-definitions.json @@ -7,10 +7,24 @@ "severity": 1, "type": "user", "description": "A test alert for dbaas service", + "scope": "entity", + "regions": [], "entity_ids": [ "13217" ], - "alert_channels": [], + "entities": { + "url": "/monitor/services/dbaas/alert-definitions/12345/entities", + "count": 1, + "has_more_resources": false + }, + "alert_channels": [ + { + "id": 10000, + "label": "Read-Write Channel", + "type": "email", + "url": "/monitor/alert-channels/10000" + } + ], "has_more_resources": false, "rule_criteria": { "rules": [ @@ -39,7 +53,6 @@ "trigger_occurrences": 3 }, "class": "alert", - "notification_groups": [], "status": "active", "created": "2024-01-01T00:00:00", "updated": "2024-01-01T00:00:00", diff --git a/test/fixtures/monitor_services_dbaas_alert-definitions_12345.json b/test/fixtures/monitor_services_dbaas_alert-definitions_12345.json index 822e18b24..4b6a76272 100644 --- a/test/fixtures/monitor_services_dbaas_alert-definitions_12345.json +++ b/test/fixtures/monitor_services_dbaas_alert-definitions_12345.json @@ -5,10 +5,24 @@ "severity": 1, "type": "user", "description": "A test alert for dbaas service", + "scope": "entity", + "regions": [], "entity_ids": [ "13217" ], - "alert_channels": [], + "entities": { + "url": "/monitor/services/dbaas/alert-definitions/12345/entities", + "count": 1, + "has_more_resources": false + }, + "alert_channels": [ + { + "id": 10000, + "label": "Read-Write Channel", + "type": "email", + "url": "/monitor/alert-channels/10000" + } + ], "has_more_resources": false, "rule_criteria": { "rules": [ diff --git a/test/fixtures/monitor_services_dbaas_alert-definitions_12345_entities.json b/test/fixtures/monitor_services_dbaas_alert-definitions_12345_entities.json new file mode 100644 index 000000000..16dad4b7c --- /dev/null +++ b/test/fixtures/monitor_services_dbaas_alert-definitions_12345_entities.json @@ -0,0 +1,25 @@ +{ + "data": [ + { + "id": "1", + "label": "mydatabase-1", + "url": "/v4/databases/mysql/instances/1", + "type": "dbaas" + }, + { + "id": "2", + "label": "mydatabase-2", + "url": "/v4/databases/mysql/instances/2", + "type": "dbaas" + }, + { + "id": "3", + "label": "mydatabase-3", + "url": "/v4/databases/mysql/instances/3", + "type": "dbaas" + } + ], + "page": 1, + "pages": 1, + "results": 3 +} diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 908ac1a44..a1bf49ca0 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -7,9 +7,10 @@ import pytest -from linode_api4 import LinodeClient +from linode_api4 import LinodeClient, PaginatedList from linode_api4.objects import ( AlertDefinition, + AlertDefinitionEntity, ApiError, MonitorDashboard, MonitorMetricsDefinition, @@ -275,3 +276,35 @@ def wait_for_alert_ready(alert_id, service_type: str): AlertDefinition, created.id, service_type ) delete_alert.delete() + + +def test_alert_definition_entities(test_linode_client): + """Test listing entities associated with an alert definition. + + This test first retrieves alert definitions for a service type, then lists entities for the first alert definition. + It asserts that the returned entities have expected fields. + """ + pytest.skip("reason: list alert_definition_entities API is not yet live") + client = test_linode_client + service_type = "dbaas" + + alert_definitions = client.monitor.alert_definitions( + service_type=service_type + ) + + if len(alert_definitions) == 0: + pytest.skip("No alert definitions available for dbaas service type") + + alert_def = alert_definitions[0] + entities = client.monitor.alert_definition_entities( + service_type, alert_def.id + ) + + assert isinstance(entities, PaginatedList) + if len(entities) > 0: + entity = entities[0] + assert isinstance(entity, AlertDefinitionEntity) + assert entity.id + assert entity.label + assert entity.url + assert entity.type == service_type diff --git a/test/unit/groups/monitor_api_test.py b/test/unit/groups/monitor_api_test.py index 9515895ae..6a2ba5ddf 100644 --- a/test/unit/groups/monitor_api_test.py +++ b/test/unit/groups/monitor_api_test.py @@ -4,6 +4,7 @@ from linode_api4.objects import ( AggregateFunction, AlertDefinition, + AlertDefinitionEntity, EntityMetricOptions, ) @@ -71,6 +72,16 @@ def test_alert_definition(self): # assert collection and element types assert isinstance(alert, PaginatedList) assert isinstance(alert[0], AlertDefinition) + assert alert[0].scope == "entity" + assert alert[0].regions == [] + assert alert[0].entities.url.endswith( + "/alert-definitions/12345/entities" + ) + assert alert[0].entities.count == 1 + assert alert[0].entities.has_more_resources is False + assert len(alert[0].alert_channels) == 1 + assert alert[0].alert_channels[0].id == 10000 + assert alert[0].alert_channels[0]._type == "email" # fetch the raw JSON from the client and assert its fields raw = self.client.get(url) @@ -90,6 +101,11 @@ def test_create_alert_definition(self): "service_type": service_type, "severity": 1, "status": "active", + "entities": { + "url": f"/monitor/services/dbaas/alert-definitions/67890/entities", + "count": 1, + "has_more_resources": False, + }, } with self.mock_post(result) as mock_post: @@ -100,6 +116,8 @@ def test_create_alert_definition(self): channel_ids=[1, 2], rule_criteria={"rules": []}, trigger_conditions={"criteria_condition": "ALL"}, + scope="entity", + regions=[], entity_ids=["13217"], description="created via test", ) @@ -109,10 +127,51 @@ def test_create_alert_definition(self): assert mock_post.call_data["label"] == "Created Alert" assert mock_post.call_data["severity"] == 1 assert "channel_ids" in mock_post.call_data + assert mock_post.call_data["scope"] == "entity" + assert mock_post.call_data["regions"] == [] assert isinstance(alert, AlertDefinition) assert alert.id == 67890 + assert alert.entities.url.endswith( + "/alert-definitions/67890/entities" + ) + assert alert.entities.count == 1 + assert alert.entities.has_more_resources is False # fetch the same response from the client and assert resp = self.client.post(url, data={}) assert resp["label"] == "Created Alert" + + def test_alert_definition_entities(self): + service_type = "dbaas" + id = 12345 + url = ( + f"/monitor/services/{service_type}/alert-definitions/{id}/entities" + ) + + with self.mock_get(url) as mock_get: + entities = self.client.monitor.alert_definition_entities( + service_type, id + ) + + assert mock_get.call_url == url + assert isinstance(entities, PaginatedList) + assert len(entities) == 3 + + assert isinstance(entities[0], AlertDefinitionEntity) + assert entities[0].id == "1" + assert entities[0].label == "mydatabase-1" + assert entities[0].url == "/v4/databases/mysql/instances/1" + assert entities[0].type == "dbaas" + + assert isinstance(entities[1], AlertDefinitionEntity) + assert entities[1].id == "2" + assert entities[1].label == "mydatabase-2" + assert entities[1].url == "/v4/databases/mysql/instances/2" + assert entities[1].type == "dbaas" + + assert isinstance(entities[2], AlertDefinitionEntity) + assert entities[2].id == "3" + assert entities[2].label == "mydatabase-3" + assert entities[2].url == "/v4/databases/mysql/instances/3" + assert entities[2].type == "dbaas"