From 786e83b73075901813593c61f2ff7823b654cbb6 Mon Sep 17 00:00:00 2001 From: Karel van Klink <karel.vanklink@geant.org> Date: Tue, 23 Jul 2024 16:08:20 +0200 Subject: [PATCH] Add Kentik service --- gso/oss-params-example.json | 18 +++++ gso/services/kentik_client.py | 121 ++++++++++++++++++++++++++++++++++ gso/settings.py | 19 ++++++ 3 files changed, 158 insertions(+) create mode 100644 gso/services/kentik_client.py diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 2a402693..595c22f9 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -102,5 +102,23 @@ "p_router": "UUID" }, "scopes": ["https://graph.microsoft.com/.default"] + }, + "KENTIK": { + "api_base": "https://api.kentik.com/api/", + "user_email": "robot-user@geant.org", + "api_key": "kentik_api_key", + "device_type": "router", + "billing_plans": { + "1": "XL license", + "2": "L license", + "3": "M license", + "4": "S license" + }, + "sample_rate": 100, + "bgp_type": "device", + "ASN": 137, + "route_selection": "both directions", + "snmp_community": "secret community string", + "md5_password": "snmp password" } } diff --git a/gso/services/kentik_client.py b/gso/services/kentik_client.py new file mode 100644 index 00000000..1a6f7cc0 --- /dev/null +++ b/gso/services/kentik_client.py @@ -0,0 +1,121 @@ +"""The Kentik service is used for external interactions with Kentik.""" + +import logging +from typing import Any + +import requests + +from gso.products.product_types.router import Router +from gso.settings import load_oss_params + +logger = logging.getLogger(__name__) + + +class KentikClient: + """The client for Kentik that interacts with an external instance.""" + + def __init__(self) -> None: + """Instantiate a new Kentik Client.""" + self.config = load_oss_params().KENTIK + self.headers = { + "X-CH-Auth-Email": self.config.user_email, + "X-CH-Auth-API-Token": self.config.api_key, + "Content-Type": "application/json", + } + + def _send_request(self, method, endpoint: str, data: dict[str, Any] | None = None) -> dict[str, Any]: + url = self.config.api_base + endpoint + result = requests.request(method, url, json=data, headers=self.headers) + + return result.json() + + def get_devices(self) -> list[dict[str, Any]]: + """List all devices in Kentik.""" + return [self._send_request("GET", "v5/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}") + + def get_device_by_name(self, device_name: str) -> dict[str, Any]: + """Fetch a device in Kentik by its :term:`FQDN`. + + If the device is not found, returns an empty dict. + + :param str device_name: The :term:`FQDN` of the sought device. + """ + devices = self.get_devices() + for device in devices: + if 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")["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}") + + def get_site_by_name(self, site_slug: str) -> dict[str, Any]: + """Get a Kentik site by its name. + + If the site is not found, return an empty dict. + + :param str site_slug: The name of the site, should be a three-letter slug like COR or POZ. + """ + sites = self.get_sites() + for site in sites: + if site["site_name"] == site_slug: + return site + + return {} + + def get_plans(self) -> list[dict[str, Any]]: + """Get all Kentik plans available.""" + return self._send_request("GET", "v5/plans")["plans"] + + def get_plan_by_name(self, plan_name: str) -> dict[str, Any]: + """Get a Kentik plan by its name. + + If the plan is not found, returns an empty dict. + + :param str plan_name: The name of the plan. + """ + plans = self.get_plans() + for plan in plans: + if plan["name"] == plan_name: + return plan + + return {} + + def create_device(self, router: Router) -> dict[str, Any]: + """Add a new device to Kentik.""" + site_id = self.get_site_by_name(router.router.router_site.site_name)["id"] + plan_id = self.get_plan_by_name(self.config.billing_plans[router.router.router_site.site_tier]) + request_body = { + "device": { + # Route selection missing + "deviceName": router.router.router_fqdn, + "deviceSubtype": self.config.device_type, + "sendingIps": [str(router.router.router_lo_ipv4_address)], + "deviceSampleRate": self.config.sample_rate, + "planId": plan_id, + "siteId": site_id, + "deviceSnmpIp": str(router.router.router_lo_ipv4_address), + "deviceSnmpCommunity": self.config.snmp_community, + "deviceBgpType": self.config.bgp_type, + "deviceBgpFlowspec": False, + "deviceBgpNeighborIp": str(router.router.router_lo_ipv4_address), + "deviceBgpNeighborIp6": str(router.router.router_lo_ipv6_address), + "deviceBgpNeighborAsn": str(self.config.ASN), + "deviceBgpPassword": self.config.md5_password, + } + } + + return self._send_request("POST", "v5/device", request_body) + + def remove_device(self, hostname: str) -> None: + """Remove a device from Kentik.""" diff --git a/gso/settings.py b/gso/settings.py index 21f517bc..a8fa96d7 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -16,6 +16,8 @@ from pydantic_forms.types import UUIDstr from pydantic_settings import BaseSettings from typing_extensions import Doc +from gso.products.product_blocks.site import SiteTier + logger = logging.getLogger(__name__) @@ -171,6 +173,22 @@ class SharepointParams(BaseSettings): scopes: list[str] +class KentikParams(BaseSettings): + """Settings for accessing Kentik's API.""" + + api_base: str + user_email: str + api_key: str + device_type: str + billing_plans: dict[SiteTier, str] + sample_rate: int + bgp_type: str + ASN: int + route_selection: str + snmp_community: str + md5_password: str + + class OSSParams(BaseSettings): """The set of parameters required for running :term:`GSO`.""" @@ -183,6 +201,7 @@ class OSSParams(BaseSettings): THIRD_PARTY_API_KEYS: dict[str, str] EMAIL: EmailParams SHAREPOINT: SharepointParams + KENTIK: KentikParams def load_oss_params() -> OSSParams: -- GitLab