diff --git a/linode_api4/groups/__init__.py b/linode_api4/groups/__init__.py index e50eeab6..175a88c5 100644 --- a/linode_api4/groups/__init__.py +++ b/linode_api4/groups/__init__.py @@ -21,3 +21,4 @@ from .tag import * from .volume import * from .vpc import * +from .monitor import * diff --git a/linode_api4/groups/monitor.py b/linode_api4/groups/monitor.py new file mode 100644 index 00000000..5565fac2 --- /dev/null +++ b/linode_api4/groups/monitor.py @@ -0,0 +1,182 @@ +from linode_api4.errors import UnexpectedResponseError +from linode_api4.groups import Group +from linode_api4.objects import ( + CreateToken, + DashboardByService, + ServiceDetails, + Dashboard, + MonitorServiceSupported, + MetricDefinition, + DashboardsByID, +) + +class MonitorGroup(Group): + """ + Encapsulates Monitor-related methods of the :any:`LinodeClient`. This + should not be instantiated on its own, but should instead be used through + an instance of :any:`LinodeClient`:: + + client = LinodeClient(token) + instances = client.monitor.dashboards() # use the LKEGroup + + This group contains all features beneath the `/monitor` group in the API v4. + """ + + def dashboards(self, *filters): + """ + Returns a list of dashboards on your account. + + .. 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(Dashboard, *filters) + + def dashboard_by_ID(self, dashboard_id: int, *filters): + """ + Returns a dashboards on your account based on the ID passed. + + .. 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-by-id + + :param filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A Dashboards. + :rtype: PaginatedList of the Dashboard + """ + result = self.client.get(f"/monitor/dashboards/{dashboard_id}") + + if not "id" in result: + raise UnexpectedResponseError( + "Unexpected response when getting Dashboard!", json=result + ) + return DashboardsByID(self.client, result["id"], result) + + + def dashboard_by_service(self, service_type: str,*filters): + """ + Returns a dashboards on your account based on the service passed. + + .. 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 filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: A Dashboards filtered by Service Type. + :rtype: PaginatedList of the Dashboards + """ + + return self.client._get_and_filter( + DashboardByService, + *filters, + endpoint=f"/monitor/services/{service_type}/dashboards", + ) + + + def supported_services(self, *filters): + """ + Returns a list of services supported by ACLP. + + .. 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 the Dashboards + """ + + return self.client._get_and_filter(MonitorServiceSupported, *filters) + + def details_by_service(self, service_type: str,*filters): + """ + Returns a details about a particular service. + + .. 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 filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :returns: Details about a Supported Services + :rtype: PaginatedList of the Service + """ + return self.client._get_and_filter( + ServiceDetails, + *filters, + endpoint=f"/monitor/services/{service_type}", + ) + + def metric_definitions(self, service_type: str,*filters): + """ + Returns metrics for a specific service type. + + .. 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 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( + MetricDefinition, + *filters, + endpoint=f"/monitor/services/{service_type}/metric-definitions", + ) + + def create_token(self, service_type: str, entity_ids: list, *filters): + """ + Returns a JWE Token for a specific service type. + + .. 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 filters: Any number of filters to apply to this query. + See :doc:`Filtering Collections` + for more details on filtering. + + :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 CreateToken(self.client, result["token"], result) + + + + + + + + + + \ No newline at end of file diff --git a/linode_api4/linode_client.py b/linode_api4/linode_client.py index 19e6f390..ff641e6d 100644 --- a/linode_api4/linode_client.py +++ b/linode_api4/linode_client.py @@ -29,6 +29,7 @@ TagGroup, VolumeGroup, VPCGroup, + MonitorGroup, ) from linode_api4.objects import Image, and_ @@ -201,6 +202,8 @@ def __init__( #: Access methods related to VM placement - See :any:`PlacementAPIGroup` for more information. self.placement = PlacementAPIGroup(self) + self.monitor = 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..7f1542d2 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 import * diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py new file mode 100644 index 00000000..ad2c8124 --- /dev/null +++ b/linode_api4/objects/monitor.py @@ -0,0 +1,106 @@ +from linode_api4.objects import ( + Base, + Property, +) + +class Dashboard(Base): + """ + List dashboards: https://techdocs.akamai.com/linode-api/get-dashboards-all + """ + + api_endpoint = "/monitor/dashboards/" + properties = { + "id": Property(identifier=True), + "created": Property(is_datetime=True), + "label": Property(), + "service_type": Property(), + "type": Property(), + "widgets": Property(mutable=True), + "updated": Property(is_datetime=True), + + } + +class DashboardsByID(Base): + """ + Get a dashboard: https://techdocs.akamai.com/linode-api/reference/get-dashboards-by-id + """ + + + properties = { + "id": Property(identifier=True), + "created": Property(is_datetime=True), + "label": Property(), + "service_type": Property(), + "type": Property(), + "widgets": Property(mutable=True), + "updated": Property(is_datetime=True), + + } + +class DashboardByService(Base): + """ + Get a dashboard: https://techdocs.akamai.com/linode-api/reference/get-dashboards + """ + + properties = { + "id": Property(identifier=True), + "created": Property(is_datetime=True), + "label": Property(), + "service_type": Property(), + "type": Property(), + "widgets": Property(mutable=True), + "updated": Property(is_datetime=True), + + } + + +class MonitorServiceSupported(Base): + + api_endpoint = "/monitor/services/" + id_attribute = "service_type" + properties = { + "service_type": Property(), + "label": Property(mutable=True), + + } + +class ServiceDetails(Base): + """ + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-services-for-service-type + """ + id_attribute = "service_type" + properties = { + "label": Property(), + "service_type": Property(), + } + + + +class MetricDefinition(Base): + """ + API Documentation: https://techdocs.akamai.com/linode-api/reference/get-monitor-information + """ + + id_attribute = "metric" + properties = { + "available_aggregate_functions": Property(), + "dimensions": Property(mutable=True), + "label": Property(), + "is_alertable": Property(), + "metric": Property(), + "metric_type": Property(), + "scrape_interval": Property(), + "unit": Property(), + } + +class CreateToken(Base): + """ + API Documentation: https://techdocs.akamai.com/linode-api/reference/post-get-token + """ + + properties = { + "token": Property(mutable=True) + } + + + 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..ce1f94df --- /dev/null +++ b/test/integration/models/monitor/test_monitor.py @@ -0,0 +1,122 @@ +import json + +from linode_api4.objects import ( + MonitorServiceSupported, + Dashboard, + DashboardsByID, + DashboardByService, + ServiceDetails, + MetricDefinition, + CreateToken, +) +from linode_api4 import LinodeClient,MySQLDatabase +import re +import time +from test.integration.helpers import ( + get_test_label, + send_request_when_resource_available, + wait_for_condition, +) + +import pytest + + + +# List all dashboards +def test_get_all_dashboards(test_linode_client): + client = test_linode_client + dashboards = client.monitor.dashboards() + assert isinstance(dashboards[0],Dashboard) + + dashboard_get = dashboards[0] + dashboard_id = dashboard_get.id + get_service_type = dashboard_get.service_type + print(f"printing svc {get_service_type}") + + #Fetch Dashboard by ID + dashboard_by_id = client.monitor.dashboard_by_ID(dashboard_id=dashboard_id) + assert isinstance(dashboard_by_id, DashboardsByID) + assert dashboard_by_id.id == dashboard_id + + #Fetch Dashboard by service_type + dashboards_by_svc = client.monitor.dashboard_by_service(service_type=get_service_type) + assert isinstance(dashboards_by_svc[0], DashboardByService) + 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.supported_services() + assert isinstance(supported_services[0], MonitorServiceSupported) + + get_supported_service = supported_services[0].service_type + + #Get details for a particular service + service_details = client.monitor.details_by_service(service_type=get_supported_service) + assert isinstance(service_details[0],ServiceDetails) + assert service_details[0].service_type == get_supported_service + + #Get Metric definition details for that particular service + metric_definitions = client.monitor.metric_definitions(service_type=get_supported_service) + assert isinstance(metric_definitions[0],MetricDefinition) + +# 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.create_token(service_type="dbaas", entity_ids=[entity_id]) + assert isinstance(token, CreateToken) + 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_test.py b/test/unit/objects/monitor_test.py new file mode 100644 index 00000000..54775794 --- /dev/null +++ b/test/unit/objects/monitor_test.py @@ -0,0 +1,101 @@ +from test.unit.base import ClientBaseCase +import datetime + +from linode_api4.objects import CreateToken + +class MonitorSupportedServicesTest(ClientBaseCase): + """ + Tests the methods of MonitorServiceSupported class + """ + + def test_supported_services(self): + """ + Test the services supported by monitor + """ + service = self.client.monitor.supported_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.monitor.dashboard_by_ID(dashboard_id=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.dashboard_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.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.details_by_service(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.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 create_token(self): + + with self.mock_post("/monitor/services/dbaas/token") as m: + self.client.monitor.create_token(service_type="dbaas", entity_ids=[189690,188020]) + self.assertEqual(m.return_dct["token"], "abcdefhjigkfghh") + + +