new-site/scripts/workers/cdr_presets/netsapiens.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

117 lines
4.3 KiB
Python

"""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),
)