From 9c21a52b498210d3fcb47fe56233d435cf5caa10 Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Wed, 14 Aug 2024 14:08:18 +0200
Subject: [PATCH] Rework Kentik client

---
 gso/services/kentik_client.py | 61 ++++++++++++++++++++++++-----------
 1 file changed, 43 insertions(+), 18 deletions(-)

diff --git a/gso/services/kentik_client.py b/gso/services/kentik_client.py
index bb990288..e00bd6f8 100644
--- a/gso/services/kentik_client.py
+++ b/gso/services/kentik_client.py
@@ -4,6 +4,7 @@ import logging
 from typing import Any, Literal
 
 import requests
+from orchestrator.utils.errors import ProcessFailureError
 from pydantic import BaseModel
 from requests import Response
 
@@ -39,22 +40,37 @@ class KentikClient:
         })
 
     def _send_request(
-        self, method: Literal["GET", "POST", "PUT", "DELETE"], endpoint: str, data: dict[str, Any] | None = None
-    ) -> Response:
+        self, method: Literal["GET", "POST", "PUT"], endpoint: str, data: dict[str, Any] | None = None
+    ) -> dict[str, Any]:
         url = self.config.api_base + endpoint
-        logger.debug("Kentik - Sending request", extra={"method": method, "endpoint": url, "form_data": data})
-        result = self.session.request(method, url, json=data)
-        logger.debug("Kentik - Received response", extra=result.__dict__)
+        logger.debug("Kentik - Sending %s request to %s with headers %s", method, url, data)
+        result = self.session.request(method, url, json=data).json()
 
+        if "error" in result or "kentik_error" in result:
+            msg = "Failed to process request in Kentik"
+            raise ProcessFailureError(msg, details=result)
+
+        logger.debug("Kentik - Received response %s", result)
         return result
 
+    def _send_delete(self, endpoint: str, data: dict[str, Any] | None = None) -> Response:
+        url = self.config.api_base + endpoint
+        logger.debug("Kentik - Sending delete request to %s with headers %s", url, data)
+        return self.session.delete(url, json=data)
+
     def get_devices(self) -> list[dict[str, Any]]:
-        """List all devices in Kentik."""
-        return [self._send_request("GET", "v5/devices").json()]
+        """List all devices in Kentik.
+
+        Returns a list of shape ``[{**device_1}, {**device_2}, ..., {**device_n}]}``.
+        """
+        return self._send_request("GET", "v5/devices")["devices"]
 
     def get_device(self, device_id: str) -> dict[str, Any]:
         """Get a device by ID."""
-        return self._send_request("GET", f"v5/device/{device_id}").json()
+        device = self._send_request("GET", f"v5/device/{device_id}")
+        device.pop("custom_column_data", None)
+        device.pop("custom_columns", None)
+        return device
 
     def get_device_by_name(self, device_name: str) -> dict[str, Any]:
         """Fetch a device in Kentik by its :term:`FQDN`.
@@ -65,18 +81,18 @@ class KentikClient:
         """
         devices = self.get_devices()
         for device in devices:
-            if device["name"] == device_name:
+            if device["device_name"] == device_name:
                 return device
 
         return {}
 
     def get_sites(self) -> list[dict[str, Any]]:
         """Get a list of all available sites in Kentik."""
-        return self._send_request("GET", "v5/sites").json()["sites"]
+        return self._send_request("GET", "v5/sites")["sites"]
 
     def get_site(self, site_id: str) -> dict[str, Any]:
         """Get a site by ID."""
-        return self._send_request("GET", f"v5/site/{site_id}").json()
+        return self._send_request("GET", f"v5/site/{site_id}")
 
     def get_site_by_name(self, site_slug: str) -> dict[str, Any]:
         """Get a Kentik site by its name.
@@ -94,7 +110,11 @@ class KentikClient:
 
     def get_plans(self) -> list[dict[str, Any]]:
         """Get all Kentik plans available."""
-        return self._send_request("GET", "v5/plans").json()["plans"]
+        return self._send_request("GET", "v5/plans")["plans"]
+
+    def get_plan(self, plan_id: int) -> dict[str, Any]:
+        """Get a Kentik plan by ID."""
+        return self._send_request("GET", f"v5/plan/{plan_id}")
 
     def get_plan_by_name(self, plan_name: str) -> dict[str, Any]:
         """Get a Kentik plan by its name.
@@ -130,18 +150,23 @@ class KentikClient:
             }
         }
 
-        new_device = self._send_request("POST", "v5/device", request_body).json()
+        new_device = self._send_request("POST", "v5/device", request_body)["device"]
 
         # The name of the device has to be updated from the subscription ID to its FQDN.
         # This is a limitation of the Kentik API that disallows settings device names containing a . symbol.
-        self.update_device(new_device["device"]["id"], {"device": {"device_name": device.device_name}})
-        new_device["device"]["device_name"] = device.device_name
+        self.update_device(new_device["id"], {"device": {"device_name": device.device_name}})
+        new_device["device_name"] = device.device_name
+        new_device.pop("custom_column_data", None)
+        new_device.pop("custom_columns", None)
 
         return new_device
 
     def update_device(self, device_id: str, updated_device: dict[str, Any]) -> dict[str, Any]:
         """Update an existing device in Kentik."""
-        return self._send_request("PUT", f"v5/device/{device_id}", updated_device).json()
+        device = self._send_request("PUT", f"v5/device/{device_id}", updated_device)["device"]
+        device.pop("custom_column_data", None)
+        device.pop("custom_columns", None)
+        return device
 
     def remove_device(self, device_id: str, *, archive: bool) -> None:
         """Remove a device from Kentik.
@@ -150,9 +175,9 @@ class KentikClient:
         :param bool archive: Archive the device instead of completely deleting it.
         """
         if not archive:
-            self._send_request("DELETE", f"v5/device/{device_id}")
+            self._send_delete(f"v5/device/{device_id}")
 
-        self._send_request("DELETE", f"v5/device/{device_id}")
+        self._send_delete(f"v5/device/{device_id}")
 
     def remove_device_by_fqdn(self, fqdn: str, *, archive: bool = True) -> None:
         """Remove a device from Kentik, by its :term:`FQDN`."""
-- 
GitLab