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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
117
scripts/workers/cdr_presets/netsapiens.py
Normal file
117
scripts/workers/cdr_presets/netsapiens.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"""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),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue