diff --git a/linode_api4/groups/__init__.py b/linode_api4/groups/__init__.py index e50eeab6..29a8f0c8 100644 --- a/linode_api4/groups/__init__.py +++ b/linode_api4/groups/__init__.py @@ -10,6 +10,7 @@ from .lke import * from .lke_tier import * from .longview import * +from .monitor_service import * from .networking import * from .nodebalancer import * from .object_storage import * diff --git a/linode_api4/groups/monitor_service.py b/linode_api4/groups/monitor_service.py new file mode 100644 index 00000000..b446034e --- /dev/null +++ b/linode_api4/groups/monitor_service.py @@ -0,0 +1,170 @@ +from linode_api4.errors import UnexpectedResponseError +from linode_api4.groups import Group +from linode_api4.objects import ( + MonitorDashboard, + MonitorMetricsDefinition, + MonitorService, + MonitorServiceToken, +) + + +class MonitorGroup(Group): + """ + Encapsulates Monitor-related methods of the :any:`LinodeClient`. + + This group contains all features beneath the `/monitor` group in the API v4. + """ + + def dashboards(self, *filters) -> list[MonitorDashboard]: + """ + Returns a list of dashboards. + + dashboards = client.monitor_service.list_monitor_dashboards() + dashboard = client.load(MonitorDashboard, 1) + + .. 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-dashboards-all + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A list of Dashboards. + :rtype: PaginatedList of Dashboard + """ + + return self.client._get_and_filter(MonitorDashboard, *filters) + + def dashboards_by_service( + self, service_type: str, *filters + ) -> list[MonitorDashboard]: + """ + Returns a list of dashboards for a particular service. + + dashboard_by_service = client.monitor_service.list_dashboards_by_service(service_type="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-dashboards + + :param service_type: The service type to get dashboards for. + :type service_type: str + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: Dashboards filtered by Service Type. + :rtype: PaginatedList of the Dashboards + """ + + return self.client._get_and_filter( + MonitorDashboard, + *filters, + endpoint=f"/monitor/services/{service_type}/dashboards", + ) + + def services(self, *filters) -> list[MonitorService]: + """ + Returns a list of services supported by ACLP. + + supported_services = client.monitor_service.list_supported_services() + + .. 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 + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A list of Supported Services + :rtype: PaginatedList of Services + """ + + return self.client._get_and_filter(MonitorService, *filters) + + def service_by_type( + self, service_type: str, *filters + ) -> list[MonitorService]: + """ + Lists monitor services by a given service_type + + service_details = client.monitor_service.list_service_by_type(service_type="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-for-service-type + + :param service_type: The service type to get details for. + :type service_type: 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 + :rtype: PaginatedList of the Services + """ + return self.client._get_and_filter( + MonitorService, + *filters, + endpoint=f"/monitor/services/{service_type}", + ) + + def metric_definitions( + self, service_type: str, *filters + ) -> list[MonitorMetricsDefinition]: + """ + Returns metrics for a specific service type. + + metrics = client.monitor_service.list_metric_definitions(service_type="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-information + + :param service_type: The service type to get metrics for. + :type service_type: str + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: Returns a List of metrics for a service + :rtype: PaginatedList of metrics + """ + return self.client._get_and_filter( + MonitorMetricsDefinition, + *filters, + endpoint=f"/monitor/services/{service_type}/metric-definitions", + ) + + def create_token( + self, service_type: str, entity_ids: list + ) -> MonitorServiceToken: + """ + Returns a JWE Token for a specific service type. + token = client.monitor_service.create_token(service_type="dbaas", entity_ids=[1234]) + + .. 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/post-get-token + + :param service_type: The service type to create token for. + :type service_type: str + :param entity_ids: The list of entity IDs for which the token is valid. + :type entity_ids: list of int + + :returns: Returns a token for a service + :rtype: str + """ + + params = {"entity_ids": entity_ids} + + result = self.client.post( + f"/monitor/services/{service_type}/token", data=params + ) + + if "token" not in result: + raise UnexpectedResponseError( + "Unexpected response when creating token!", json=result + ) + return MonitorServiceToken(token=result["token"]) diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py index 19e6f390..b0431eb5 100644 --- a/linode_api4/linode_client.py +++ b/linode_api4/linode_client.py @@ -19,6 +19,7 @@ LinodeGroup, LKEGroup, LongviewGroup, + MonitorGroup, NetworkingGroup, NodeBalancerGroup, ObjectStorageGroup, @@ -201,6 +202,8 @@ def __init__( #: Access methods related to VM placement - See :any:`PlacementAPIGroup` for more information. self.placement = PlacementAPIGroup(self) + self.monitor_service = MonitorGroup(self) + @property def _user_agent(self): return "{}python-linode_api4/{} {}".format( diff --git a/linode_api4/objects/__init__.py b/linode_api4/objects/__init__.py index b13fac51..3d684b59 100644 --- a/linode_api4/objects/__init__.py +++ b/linode_api4/objects/__init__.py @@ -21,3 +21,4 @@ from .vpc import * from .beta import * from .placement import * +from .monitor_service import * diff --git a/linode_api4/objects/monitor_service.py b/linode_api4/objects/monitor_service.py new file mode 100644 index 00000000..14599022 --- /dev/null +++ b/linode_api4/objects/monitor_service.py @@ -0,0 +1,177 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +from linode_api4.objects import Base, JSONObject, Property, StrEnum + + +class AggregateFunction(StrEnum): + """ + Enum for supported aggregate functions. + """ + + min = "min" + max = "max" + avg = "avg" + sum = "sum" + count = "count" + rate = "rate" + increase = "increase" + last = "last" + + +class ChartType(StrEnum): + """ + Enum for supported chart types. + """ + + line = "line" + area = "area" + + +class ServiceType(StrEnum): + """ + Enum for supported service types. + """ + + dbaas = "dbaas" + linode = "linode" + lke = "lke" + vpc = "vpc" + nodebalancer = "nodebalancer" + firewall = "firewall" + object_storage = "object_storage" + aclb = "aclb" + + +class MetricType(StrEnum): + """ + Enum for supported metric type + """ + + gauge = "gauge" + counter = "counter" + histogram = "histogram" + summary = "summary" + + +class MetricUnit(StrEnum): + """ + Enum for supported metric units. + """ + + COUNT = "count" + PERCENT = "percent" + BYTE = "byte" + SECOND = "second" + BITS_PER_SECOND = "bits_per_second" + MILLISECOND = "millisecond" + KB = "KB" + MB = "MB" + GB = "GB" + RATE = "rate" + BYTES_PER_SECOND = "bytes_per_second" + PERCENTILE = "percentile" + RATIO = "ratio" + OPS_PER_SECOND = "ops_per_second" + IOPS = "iops" + + +class DashboardType(StrEnum): + """ + Enum for supported dashboard types. + """ + + standard = "standard" + custom = "custom" + + +@dataclass +class DashboardWidget(JSONObject): + """ + Represents a single widget in the widgets list. + """ + + metric: str = "" + unit: MetricUnit = "" + label: str = "" + color: str = "" + size: int = 0 + chart_type: ChartType = "" + y_label: str = "" + aggregate_function: AggregateFunction = "" + + +@dataclass +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 + + +@dataclass +class MonitorMetricsDefinition(JSONObject): + """ + Represents a single metric definition in the metrics definition list. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-information + """ + + metric: str = "" + label: str = "" + metric_type: MetricType = "" + unit: MetricUnit = "" + scrape_interval: int = 0 + is_alertable: bool = False + dimensions: Optional[List[Dimension]] = None + available_aggregate_functions: List[AggregateFunction] = field( + default_factory=list + ) + + +class MonitorDashboard(Base): + """ + Dashboard details. + + List dashboards: https://techdocs.akamai.com/linode-api/get-dashboards-all + """ + + api_endpoint = "/monitor/dashboards/{id}" + properties = { + "id": Property(identifier=True), + "created": Property(is_datetime=True), + "label": Property(), + "service_type": Property(ServiceType), + "type": Property(DashboardType), + "widgets": Property(List[DashboardWidget]), + "updated": Property(is_datetime=True), + } + + +class MonitorService(Base): + """ + Represents a single service type. + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-services + + """ + + api_endpoint = "/monitor/services/{service_type}" + id_attribute = "service_type" + properties = { + "service_type": Property(ServiceType, identifier=True), + "label": Property(), + } + + +@dataclass +class MonitorServiceToken(JSONObject): + """ + A token for the requested service_type. + + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-get-token + """ + + token: str = "" diff --git a/test/fixtures/monitor_dashboards.json b/test/fixtures/monitor_dashboards.json new file mode 100644 index 00000000..42de92b5 --- /dev/null +++ b/test/fixtures/monitor_dashboards.json @@ -0,0 +1,37 @@ +{ + "data": [ + { + "created": "2024-10-10T05:01:58", + "id": 1, + "label": "Resource Usage", + "service_type": "dbaas", + "type": "standard", + "updated": "2024-10-10T05:01:58", + "widgets": [ + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "CPU Usage", + "metric": "cpu_usage", + "size": 12, + "unit": "%", + "y_label": "cpu_usage" + }, + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "Disk I/O Write", + "metric": "write_iops", + "size": 6, + "unit": "IOPS", + "y_label": "write_iops" + } + ] + } + ], + "page": 1, + "pages": 1, + "results": 1 + } \ No newline at end of file diff --git a/test/fixtures/monitor_dashboards_1.json b/test/fixtures/monitor_dashboards_1.json new file mode 100644 index 00000000..b78bf344 --- /dev/null +++ b/test/fixtures/monitor_dashboards_1.json @@ -0,0 +1,30 @@ +{ + "created": "2024-10-10T05:01:58", + "id": 1, + "label": "Resource Usage", + "service_type": "dbaas", + "type": "standard", + "updated": "2024-10-10T05:01:58", + "widgets": [ + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "CPU Usage", + "metric": "cpu_usage", + "size": 12, + "unit": "%", + "y_label": "cpu_usage" + }, + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "Available Memory", + "metric": "available_memory", + "size": 6, + "unit": "GB", + "y_label": "available_memory" + } + ] + } \ No newline at end of file diff --git a/test/fixtures/monitor_services.json b/test/fixtures/monitor_services.json new file mode 100644 index 00000000..7a568866 --- /dev/null +++ b/test/fixtures/monitor_services.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "label": "Databases", + "service_type": "dbaas" + } + ], + "page": 1, + "pages": 1, + "results": 1 + } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas.json b/test/fixtures/monitor_services_dbaas.json new file mode 100644 index 00000000..7a568866 --- /dev/null +++ b/test/fixtures/monitor_services_dbaas.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "label": "Databases", + "service_type": "dbaas" + } + ], + "page": 1, + "pages": 1, + "results": 1 + } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_dashboards.json b/test/fixtures/monitor_services_dbaas_dashboards.json new file mode 100644 index 00000000..5fbb7e9d --- /dev/null +++ b/test/fixtures/monitor_services_dbaas_dashboards.json @@ -0,0 +1,37 @@ +{ + "data": [ + { + "created": "2024-10-10T05:01:58", + "id": 1, + "label": "Resource Usage", + "service_type": "dbaas", + "type": "standard", + "updated": "2024-10-10T05:01:58", + "widgets": [ + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "CPU Usage", + "metric": "cpu_usage", + "size": 12, + "unit": "%", + "y_label": "cpu_usage" + }, + { + "aggregate_function": "sum", + "chart_type": "area", + "color": "default", + "label": "Memory Usage", + "metric": "memory_usage", + "size": 6, + "unit": "%", + "y_label": "memory_usage" + } + ] + } + ], + "page": 1, + "pages": 1, + "results": 1 + } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_metric-definitions.json b/test/fixtures/monitor_services_dbaas_metric-definitions.json new file mode 100644 index 00000000..c493b23a --- /dev/null +++ b/test/fixtures/monitor_services_dbaas_metric-definitions.json @@ -0,0 +1,55 @@ +{ + "data": [ + { + "available_aggregate_functions": [ + "max", + "avg", + "min", + "sum" + ], + "dimensions": [ + { + "dimension_label": "node_type", + "label": "Node Type", + "values": [ + "primary", + "secondary" + ] + } + ], + "is_alertable": true, + "label": "CPU Usage", + "metric": "cpu_usage", + "metric_type": "gauge", + "scrape_interval": "60s", + "unit": "percent" + }, + { + "available_aggregate_functions": [ + "max", + "avg", + "min", + "sum" + ], + "dimensions": [ + { + "dimension_label": "node_type", + "label": "Node Type", + "values": [ + "primary", + "secondary" + ] + } + ], + "is_alertable": true, + "label": "Disk I/O Read", + "metric": "read_iops", + "metric_type": "gauge", + "scrape_interval": "60s", + "unit": "iops" + } + ], + "page": 1, + "pages": 1, + "results": 2 + } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_token.json b/test/fixtures/monitor_services_dbaas_token.json new file mode 100644 index 00000000..b1aa0d78 --- /dev/null +++ b/test/fixtures/monitor_services_dbaas_token.json @@ -0,0 +1,3 @@ +{ + "token": "abcdefhjigkfghh" +} \ No newline at end of file diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py new file mode 100644 index 00000000..064d1134 --- /dev/null +++ b/test/integration/models/monitor/test_monitor.py @@ -0,0 +1,111 @@ +from test.integration.helpers import ( + get_test_label, + send_request_when_resource_available, + wait_for_condition, +) + +import pytest + +from linode_api4 import LinodeClient +from linode_api4.objects import ( + MonitorDashboard, + MonitorMetricsDefinition, + MonitorService, + MonitorServiceToken, +) + + +# List all dashboards +def test_get_all_dashboards(test_linode_client): + client = test_linode_client + dashboards = client.monitor_service.dashboards() + assert isinstance(dashboards[0], MonitorDashboard) + + dashboard_get = dashboards[0] + get_service_type = dashboard_get.service_type + + # Fetch Dashboard by ID + dashboard_by_id = client.load(MonitorDashboard, 1) + assert isinstance(dashboard_by_id, MonitorDashboard) + assert dashboard_by_id.id == 1 + + # #Fetch Dashboard by service_type + dashboards_by_svc = client.monitor_service.dashboards_by_service( + service_type=get_service_type + ) + assert isinstance(dashboards_by_svc[0], MonitorDashboard) + assert dashboards_by_svc[0].service_type == get_service_type + + +# List supported services +def test_get_supported_services(test_linode_client): + client = test_linode_client + supported_services = client.monitor_service.services() + assert isinstance(supported_services[0], MonitorService) + + get_supported_service = supported_services[0].service_type + + # Get details for a particular service + service_details = client.monitor_service.service_by_type( + service_type=get_supported_service + ) + assert isinstance(service_details[0], MonitorService) + assert service_details[0].service_type == get_supported_service + + # Get Metric definition details for that particular service + metric_definitions = client.monitor_service.metric_definitions( + service_type=get_supported_service + ) + assert isinstance(metric_definitions[0], MonitorMetricsDefinition) + + +# Test Helpers +def get_db_engine_id(client: LinodeClient, engine: str): + engines = client.database.engines() + engine_id = "" + for e in engines: + if e.engine == engine: + engine_id = e.id + + return str(engine_id) + + +@pytest.fixture(scope="session") +def test_create_and_test_db(test_linode_client): + client = test_linode_client + label = get_test_label() + "-sqldb" + region = "us-ord" + engine_id = get_db_engine_id(client, "mysql") + dbtype = "g6-standard-1" + + db = client.database.mysql_create( + label=label, + region=region, + engine=engine_id, + ltype=dbtype, + cluster_size=None, + ) + + def get_db_status(): + return db.status == "active" + + # TAKES 15-30 MINUTES TO FULLY PROVISION DB + wait_for_condition(60, 2000, get_db_status) + + yield db + send_request_when_resource_available(300, db.delete) + + +def test_my_db_functionality(test_linode_client, test_create_and_test_db): + client = test_linode_client + assert test_create_and_test_db.status == "active" + + entity_id = test_create_and_test_db.id + + # create token for the particular service + token = client.monitor_service.create_token( + service_type="dbaas", entity_ids=[entity_id] + ) + assert isinstance(token, MonitorServiceToken) + assert len(token.token) > 0, "Token should not be empty" + assert hasattr(token, "token"), "Response object has no 'token' attribute" diff --git a/test/unit/objects/monitor_service_test.py b/test/unit/objects/monitor_service_test.py new file mode 100644 index 00000000..4d18cbc0 --- /dev/null +++ b/test/unit/objects/monitor_service_test.py @@ -0,0 +1,123 @@ +import datetime +from test.unit.base import ClientBaseCase + +from linode_api4.objects import MonitorDashboard + + +class MonitorTest(ClientBaseCase): + """ + Tests the methods of MonitorServiceSupported class + """ + + def test_supported_services(self): + """ + Test the services supported by monitor + """ + service = self.client.monitor_service.services() + self.assertEqual(len(service), 1) + self.assertEqual(service[0].label, "Databases") + self.assertEqual(service[0].service_type, "dbaas") + + def test_dashboard_by_ID(self): + """ + Test the dashboard by ID API + """ + dashboard = self.client.load(MonitorDashboard, 1) + self.assertEqual(dashboard.type, "standard") + self.assertEqual( + dashboard.created, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboard.id, 1) + self.assertEqual(dashboard.label, "Resource Usage") + self.assertEqual(dashboard.service_type, "dbaas") + self.assertEqual( + dashboard.updated, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboard.widgets[0].aggregate_function, "sum") + self.assertEqual(dashboard.widgets[0].chart_type, "area") + self.assertEqual(dashboard.widgets[0].color, "default") + self.assertEqual(dashboard.widgets[0].label, "CPU Usage") + self.assertEqual(dashboard.widgets[0].metric, "cpu_usage") + self.assertEqual(dashboard.widgets[0].size, 12) + self.assertEqual(dashboard.widgets[0].unit, "%") + self.assertEqual(dashboard.widgets[0].y_label, "cpu_usage") + + def test_dashboard_by_service_type(self): + dashboards = self.client.monitor_service.dashboards_by_service( + service_type="dbaas" + ) + self.assertEqual(dashboards[0].type, "standard") + self.assertEqual( + dashboards[0].created, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboards[0].id, 1) + self.assertEqual(dashboards[0].label, "Resource Usage") + self.assertEqual(dashboards[0].service_type, "dbaas") + self.assertEqual( + dashboards[0].updated, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboards[0].widgets[0].aggregate_function, "sum") + self.assertEqual(dashboards[0].widgets[0].chart_type, "area") + self.assertEqual(dashboards[0].widgets[0].color, "default") + self.assertEqual(dashboards[0].widgets[0].label, "CPU Usage") + self.assertEqual(dashboards[0].widgets[0].metric, "cpu_usage") + 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") + + def test_get_all_dashboards(self): + dashboards = self.client.monitor_service.dashboards() + self.assertEqual(dashboards[0].type, "standard") + self.assertEqual( + dashboards[0].created, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboards[0].id, 1) + self.assertEqual(dashboards[0].label, "Resource Usage") + self.assertEqual(dashboards[0].service_type, "dbaas") + self.assertEqual( + dashboards[0].updated, datetime.datetime(2024, 10, 10, 5, 1, 58) + ) + self.assertEqual(dashboards[0].widgets[0].aggregate_function, "sum") + self.assertEqual(dashboards[0].widgets[0].chart_type, "area") + self.assertEqual(dashboards[0].widgets[0].color, "default") + self.assertEqual(dashboards[0].widgets[0].label, "CPU Usage") + self.assertEqual(dashboards[0].widgets[0].metric, "cpu_usage") + 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") + + def test_specific_service_details(self): + data = self.client.monitor_service.service_by_type( + service_type="dbaas" + ) + self.assertEqual(data[0].label, "Databases") + self.assertEqual(data[0].service_type, "dbaas") + + def test_metric_definitions(self): + + metrics = self.client.monitor_service.metric_definitions( + service_type="dbaas" + ) + self.assertEqual( + metrics[0].available_aggregate_functions, + ["max", "avg", "min", "sum"], + ) + self.assertEqual(metrics[0].is_alertable, True) + 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].values, ["primary", "secondary"] + ) + + def test_create_token(self): + + with self.mock_post("/monitor/services/dbaas/token") as m: + self.client.monitor_service.create_token( + service_type="dbaas", entity_ids=[189690, 188020] + ) + self.assertEqual(m.return_dct["token"], "abcdefhjigkfghh")