From 9d149bac1da0c2dd0cda5e2f0fc82052a8c9d944 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Fri, 4 Jul 2025 17:04:42 +0530 Subject: [PATCH 1/3] updating services endpoint --- linode_api4/groups/monitor.py | 18 ++++++-------- linode_api4/objects/monitor.py | 11 +++++---- test/fixtures/monitor_services_dbaas.json | 24 +++++++++++-------- .../models/monitor/test_monitor.py | 8 +++---- test/unit/objects/monitor_test.py | 8 +++---- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py index 908b4e819..14b4065f5 100644 --- a/linode_api4/groups/monitor.py +++ b/linode_api4/groups/monitor.py @@ -62,32 +62,28 @@ def dashboards( ) def services( - self, *filters, service_type: Optional[str] = None - ) -> list[MonitorService]: + self, + *filters, + ) -> PaginatedList: """ Lists services supported by ACLP. supported_services = client.monitor.services() - service_details = client.monitor.services(service_type="dbaas") + service_details = client.monitor.load(MonitorService, "dbaas") .. note:: This endpoint is in beta. This will only function if base_url is set to `https://api.linode.com/v4beta`. API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-services API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-services-for-service-type - :param service_type: The service type to get details for. - :type service_type: Optional[str] :param filters: Any number of filters to apply to this query. See :doc:`Filtering Collections` for more details on filtering. - :returns: Lists monitor services by a given service_type + :returns: Lists monitor services :rtype: PaginatedList of the Services """ - endpoint = ( - f"/monitor/services/{service_type}" - if service_type - else "/monitor/services" - ) + endpoint = "/monitor/services" + return self.client._get_and_filter( MonitorService, *filters, diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index f518e641d..ae3936ee7 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -157,16 +157,19 @@ class MonitorDashboard(Base): } -@dataclass -class MonitorService(JSONObject): +class MonitorService(Base): """ Represents a single service type. API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-services """ - service_type: ServiceType = "" - label: str = "" + api_endpoint = "/monitor/services/{service_type}" + id_attribute = "service_type" + properties = { + "service_type": Property(ServiceType), + "label": Property(), + } @dataclass diff --git a/test/fixtures/monitor_services_dbaas.json b/test/fixtures/monitor_services_dbaas.json index 7a568866c..211833847 100644 --- a/test/fixtures/monitor_services_dbaas.json +++ b/test/fixtures/monitor_services_dbaas.json @@ -1,11 +1,15 @@ { - "data": [ - { - "label": "Databases", - "service_type": "dbaas" - } - ], - "page": 1, - "pages": 1, - "results": 1 - } \ No newline at end of file + "service_type": "dbaas", + "label": "Databases", + "alert": { + "polling_interval_seconds": [ + 300 + ], + "evaluation_period_seconds": [ + 300 + ], + "scope": [ + "entity" + ] + } +} \ No newline at end of file diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 5fb9626b3..7c9249f42 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -44,11 +44,9 @@ def test_get_supported_services(test_linode_client): get_supported_service = supported_services[0].service_type # Get details for a particular service - service_details = client.monitor.services( - service_type=get_supported_service - ) - assert isinstance(service_details[0], MonitorService) - assert service_details[0].service_type == get_supported_service + service_details = client.load(MonitorService, get_supported_service) + assert isinstance(service_details, MonitorService) + assert service_details.service_type == get_supported_service # Get Metric definition details for that particular service metric_definitions = client.monitor.metric_definitions( diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index 385eaf462..a010514c2 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -1,7 +1,7 @@ import datetime from test.unit.base import ClientBaseCase -from linode_api4.objects import MonitorDashboard +from linode_api4.objects import MonitorDashboard, MonitorService class MonitorTest(ClientBaseCase): @@ -85,9 +85,9 @@ def test_get_all_dashboards(self): self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage") def test_specific_service_details(self): - data = self.client.monitor.services(service_type="dbaas") - self.assertEqual(data[0].label, "Databases") - self.assertEqual(data[0].service_type, "dbaas") + data = self.client.load(MonitorService, "dbaas") + self.assertEqual(data.label, "Databases") + self.assertEqual(data.service_type, "dbaas") def test_metric_definitions(self): From f1ffed8836bd931af7a7762569bd2c84c808565f Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Mon, 7 Jul 2025 20:00:22 +0530 Subject: [PATCH 2/3] resolving lint errors --- linode_api4/groups/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py index 14b4065f5..14b5617c4 100644 --- a/linode_api4/groups/monitor.py +++ b/linode_api4/groups/monitor.py @@ -79,7 +79,7 @@ def services( See :doc:`Filtering Collections` for more details on filtering. - :returns: Lists monitor services + :returns: Lists monitor services :rtype: PaginatedList of the Services """ endpoint = "/monitor/services" From 1c17c7456e1524e7eb04a25f6ea18c6c50725f21 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Wed, 20 Aug 2025 13:49:28 +0530 Subject: [PATCH 3/3] adding monitors in region endpoint --- linode_api4/objects/monitor.py | 42 ++++++++++++++++--- linode_api4/objects/region.py | 25 +++++++++-- test/fixtures/monitor_dashboards.json | 8 +++- test/fixtures/monitor_dashboards_1.json | 8 +++- .../monitor_services_dbaas_dashboards.json | 15 ++++++- test/fixtures/regions.json | 8 ++++ test/unit/objects/monitor_test.py | 33 +++++++++++++-- test/unit/objects/region_test.py | 5 +++ 8 files changed, 125 insertions(+), 19 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index ed6ce79a5..1888fa7ce 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -49,6 +49,7 @@ class ServiceType(StrEnum): firewall = "firewall" object_storage = "object_storage" aclb = "aclb" + netloadbalancer = "netloadbalancer" class MetricType(StrEnum): @@ -82,6 +83,10 @@ class MetricUnit(StrEnum): RATIO = "ratio" OPS_PER_SECOND = "ops_per_second" IOPS = "iops" + KILO_BYTES_PER_SECOND = "kilo_bytes_per_second" + SESSIONS_PER_SECOND = "sessions_per_second" + PACKETS_PER_SECOND = "packets_per_second" + KILO_BITS_PER_SECOND = "kilo_bits_per_second" class DashboardType(StrEnum): @@ -93,6 +98,17 @@ class DashboardType(StrEnum): custom = "custom" +@dataclass +class Filter(JSONObject): + """ + Represents a filter in the filters list of a dashboard widget. + """ + + dimension_label: str = "" + operator: str = "" + value: str = "" + + @dataclass class DashboardWidget(JSONObject): """ @@ -107,6 +123,19 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" + group_by: list[str] = field(default_factory=list) + filters: list[Filter] | None = None + + +@dataclass +class ServiceAlert(JSONObject): + """ + Represents alert configuration options for a monitor service. + """ + + polling_interval_seconds: list[int] = field(default_factory=list) + evaluation_period_seconds: list[int] = field(default_factory=list) + scope: list[str] = field(default_factory=list) @dataclass @@ -115,9 +144,9 @@ class Dimension(JSONObject): Represents a single dimension in the dimensions list. """ - dimension_label: Optional[str] = None - label: Optional[str] = None - values: Optional[List[str]] = None + dimension_label: str | None = None + label: str | None = None + values: list[str] | None = None @dataclass @@ -134,8 +163,8 @@ class MonitorMetricsDefinition(JSONObject): unit: MetricUnit = "" scrape_interval: int = 0 is_alertable: bool = False - dimensions: Optional[List[Dimension]] = None - available_aggregate_functions: List[AggregateFunction] = field( + dimensions: list[Dimension] | None = None + available_aggregate_functions: list[AggregateFunction] = field( default_factory=list ) @@ -154,7 +183,7 @@ class MonitorDashboard(Base): "label": Property(), "service_type": Property(ServiceType), "type": Property(DashboardType), - "widgets": Property(List[DashboardWidget]), + "widgets": Property(list[DashboardWidget]), "updated": Property(is_datetime=True), } @@ -171,6 +200,7 @@ class MonitorService(Base): properties = { "service_type": Property(ServiceType), "label": Property(), + "alert": Property(json_object=ServiceAlert), } diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index 34577c336..c7ba16b70 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -16,6 +16,24 @@ class RegionPlacementGroupLimits(JSONObject): maximum_linodes_per_pg: int = 0 +@dataclass +class RegionMonitors(JSONObject): + """ + Represents the monitor services available in a region. + Lists the services in this region that support metrics and alerts + use with Akamai Cloud Pulse (ACLP). + """ + + alerts: list[str] | None = None + metrics: list[str] | None = None + + def __post_init__(self): + if self.alerts is None: + self.alerts = [] + if self.metrics is None: + self.metrics = [] + + class Region(Base): """ A Region. Regions correspond to individual data centers, each located in a different geographical area. @@ -35,10 +53,11 @@ class Region(Base): "placement_group_limits": Property( json_object=RegionPlacementGroupLimits ), + "monitors": Property(json_object=RegionMonitors), } @property - def availability(self) -> List["RegionAvailabilityEntry"]: + def availability(self) -> list["RegionAvailabilityEntry"]: result = self._client.get( f"{self.api_endpoint}/availability", model=self ) @@ -59,6 +78,6 @@ class RegionAvailabilityEntry(JSONObject): API Documentation: https://techdocs.akamai.com/linode-api/reference/get-region-availability """ - region: Optional[str] = None - plan: Optional[str] = None + region: str | None = None + plan: str | None = None available: bool = False diff --git a/test/fixtures/monitor_dashboards.json b/test/fixtures/monitor_dashboards.json index 42de92b55..5e56923a1 100644 --- a/test/fixtures/monitor_dashboards.json +++ b/test/fixtures/monitor_dashboards.json @@ -16,7 +16,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -26,7 +28,9 @@ "metric": "write_iops", "size": 6, "unit": "IOPS", - "y_label": "write_iops" + "y_label": "write_iops", + "group_by": ["entity_id"], + "filters": null } ] } diff --git a/test/fixtures/monitor_dashboards_1.json b/test/fixtures/monitor_dashboards_1.json index b78bf3447..afb5d71ee 100644 --- a/test/fixtures/monitor_dashboards_1.json +++ b/test/fixtures/monitor_dashboards_1.json @@ -14,7 +14,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -24,7 +26,9 @@ "metric": "available_memory", "size": 6, "unit": "GB", - "y_label": "available_memory" + "y_label": "available_memory", + "group_by": ["entity_id"], + "filters": null } ] } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_dashboards.json b/test/fixtures/monitor_services_dbaas_dashboards.json index 5fbb7e9db..e39a231b2 100644 --- a/test/fixtures/monitor_services_dbaas_dashboards.json +++ b/test/fixtures/monitor_services_dbaas_dashboards.json @@ -16,7 +16,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -26,7 +28,16 @@ "metric": "memory_usage", "size": 6, "unit": "%", - "y_label": "memory_usage" + "y_label": "memory_usage", + "group_by": ["entity_id"], + "filters": [ + { + "dimension_label": "pattern", + "operator": "in", + "value": "publicout,privateout" + } + ] + } ] } diff --git a/test/fixtures/regions.json b/test/fixtures/regions.json index 5fe55e200..c3f192d47 100644 --- a/test/fixtures/regions.json +++ b/test/fixtures/regions.json @@ -125,6 +125,14 @@ "Block Storage", "Object Storage" ], + "monitors": { + "alerts": [ + "Managed Databases" + ], + "metrics": [ + "Managed Databases" + ] + }, "status": "ok", "resolvers": { "ipv4": "66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5", diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index a010514c2..86f39a6ca 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -41,6 +41,8 @@ def test_dashboard_by_ID(self): self.assertEqual(dashboard.widgets[0].size, 12) self.assertEqual(dashboard.widgets[0].unit, "%") self.assertEqual(dashboard.widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboard.widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboard.widgets[0].filters) def test_dashboard_by_service_type(self): dashboards = self.client.monitor.dashboards(service_type="dbaas") @@ -62,6 +64,21 @@ def test_dashboard_by_service_type(self): self.assertEqual(dashboards[0].widgets[0].size, 12) self.assertEqual(dashboards[0].widgets[0].unit, "%") self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboards[0].widgets[0].filters) + + # Test the second widget which has filters + self.assertEqual(dashboards[0].widgets[1].label, "Memory Usage") + self.assertEqual(dashboards[0].widgets[1].group_by, ["entity_id"]) + self.assertIsNotNone(dashboards[0].widgets[1].filters) + self.assertEqual(len(dashboards[0].widgets[1].filters), 1) + self.assertEqual( + dashboards[0].widgets[1].filters[0].dimension_label, "pattern" + ) + self.assertEqual(dashboards[0].widgets[1].filters[0].operator, "in") + self.assertEqual( + dashboards[0].widgets[1].filters[0].value, "publicout,privateout" + ) def test_get_all_dashboards(self): dashboards = self.client.monitor.dashboards() @@ -83,12 +100,20 @@ def test_get_all_dashboards(self): self.assertEqual(dashboards[0].widgets[0].size, 12) self.assertEqual(dashboards[0].widgets[0].unit, "%") self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboards[0].widgets[0].filters) def test_specific_service_details(self): data = self.client.load(MonitorService, "dbaas") self.assertEqual(data.label, "Databases") self.assertEqual(data.service_type, "dbaas") + # Test alert configuration + self.assertIsNotNone(data.alert) + self.assertEqual(data.alert.polling_interval_seconds, [300]) + self.assertEqual(data.alert.evaluation_period_seconds, [300]) + self.assertEqual(data.alert.scope, ["entity"]) + def test_metric_definitions(self): metrics = self.client.monitor.metric_definitions(service_type="dbaas") @@ -96,16 +121,16 @@ def test_metric_definitions(self): metrics[0].available_aggregate_functions, ["max", "avg", "min", "sum"], ) - self.assertEqual(metrics[0].is_alertable, True) + self.assertTrue(metrics[0].is_alertable) self.assertEqual(metrics[0].label, "CPU Usage") self.assertEqual(metrics[0].metric, "cpu_usage") self.assertEqual(metrics[0].metric_type, "gauge") self.assertEqual(metrics[0].scrape_interval, "60s") self.assertEqual(metrics[0].unit, "percent") - self.assertEqual(metrics[0].dimensions[0].dimension_label, "node_type") - self.assertEqual(metrics[0].dimensions[0].label, "Node Type") + self.assertEqual(metrics[0].dimensions[0]["dimension_label"], "node_type") + self.assertEqual(metrics[0].dimensions[0]["label"], "Node Type") self.assertEqual( - metrics[0].dimensions[0].values, ["primary", "secondary"] + metrics[0].dimensions[0]["values"], ["primary", "secondary"] ) def test_create_token(self): diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 0bc1afa9e..f49bf2cc2 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -28,6 +28,11 @@ def test_get_region(self): region.placement_group_limits.maximum_linodes_per_pg, 5 ) + # Test monitors section + self.assertIsNotNone(region.monitors) + self.assertEqual(region.monitors.alerts, ["Managed Databases"]) + self.assertEqual(region.monitors.metrics, ["Managed Databases"]) + def test_region_availability(self): """ Tests that availability for a specific region can be listed and filtered on.