"""NetSapiens preset — REST API pull (CDRv2 endpoint). NetSapiens exposes ``/ns-api/`` with OAuth2 client-credentials. CDRs are paginated at ``/cdrs`` with ``start_datetime`` / ``end_datetime`` filters; we paginate and stream the full response as NDJSON into MinIO for the netsapiens adapter to consume. """ from __future__ import annotations import json import logging import urllib.parse import urllib.request from datetime import datetime, timedelta from typing import Iterable, Optional from .base import BasePreset, CredentialField, FetchedFile logger = logging.getLogger(__name__) class NetSapiensPreset(BasePreset): PRESET_SLUG = "netsapiens" LABEL = "NetSapiens" CDR_FORMAT = "netsapiens" TRANSPORT_METHOD = "api" DEFAULT_CRON = "0 2 * * *" CREDENTIAL_FIELDS = ( CredentialField("api_host", "NetSapiens API host", "text", help="e.g. https://core1.example.com"), CredentialField("client_id", "OAuth client ID", "text"), CredentialField("client_secret", "OAuth client secret", "password", sensitive=True), CredentialField("domain", "NetSapiens domain", "text", help="Your tenant/domain within the switch."), ) def _request(self, url: str, headers: dict, method: str = "GET", data: Optional[bytes] = None, timeout: int = 30) -> bytes: req = urllib.request.Request(url, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.read() def _oauth_token(self, cfg: dict, secrets: dict) -> str: body = urllib.parse.urlencode({ "grant_type": "client_credentials", "client_id": cfg["client_id"], "client_secret": secrets["client_secret"], }).encode("utf-8") token_url = cfg["api_host"].rstrip("/") + "/ns-api/oauth2/token/" resp = self._request( token_url, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST", data=body, ) payload = json.loads(resp) token = payload.get("access_token") if not token: raise RuntimeError(f"NetSapiens OAuth failed: {payload}") return token def validate(self, profile_config: dict, secrets: dict) -> tuple[bool, str]: try: self._oauth_token(profile_config, secrets) return True, "OAuth token acquired" except Exception as exc: return False, f"NetSapiens validate failed: {exc}" def fetch( self, profile_config: dict, secrets: dict, since: Optional[datetime], ) -> Iterable[FetchedFile]: token = self._oauth_token(profile_config, secrets) since = since or (datetime.utcnow() - timedelta(days=1)) end = datetime.utcnow() base = profile_config["api_host"].rstrip("/") headers = { "Authorization": f"Bearer {token}", "Accept": "application/json", } records: list[dict] = [] page = 1 while True: qs = urllib.parse.urlencode({ "domain": profile_config["domain"], "start_datetime": since.strftime("%Y-%m-%d %H:%M:%S"), "end_datetime": end.strftime("%Y-%m-%d %H:%M:%S"), "page": page, "per_page": 500, }) url = f"{base}/ns-api/v2/cdrs?{qs}" try: body = self._request(url, headers=headers) except Exception as exc: logger.warning("NetSapiens fetch page %s failed: %s", page, exc) break payload = json.loads(body) items = payload if isinstance(payload, list) else payload.get("data", []) if not items: break records.extend(items) if len(items) < 500: break page += 1 if not records: return # Stream as NDJSON — the netsapiens adapter reads both array + NDJSON ndjson_bytes = "\n".join(json.dumps(r) for r in records).encode("utf-8") yield FetchedFile( remote_path=f"netsapiens_cdrs_{end:%Y%m%dT%H%M%SZ}.ndjson", mtime=end, content=ndjson_bytes, size_bytes=len(ndjson_bytes), )