#!/usr/bin/env python3 """CLI for the packetdial skill — NetSapiens SNAPsolution API v2. Read subcommands run freely. Write subcommands (create-*, update-*, delete-*, raw with a non-GET method) refuse to run unless --confirm is passed; without it they print what they WOULD do and exit non-zero. Output: --json emits raw JSON (default for most commands); a few commands render a readable table when --json is omitted. Read examples: python ns.py status python ns.py whoami python ns.py domains python ns.py domain python ns.py users python ns.py user python ns.py phones python ns.py dids python ns.py devices python ns.py cdrs --domain --start 2026-06-01 --end 2026-06-02 python ns.py resellers Write examples (all require --confirm): python ns.py create-domain --body '{"domain":"acme","description":"Acme Inc"}' --confirm python ns.py create-user --body '{"user":"101","name-first-name":"Jane"}' --confirm python ns.py delete-user --confirm Escape hatch (raw request against any of the 239 v2 paths): python ns.py raw GET domains/acme/users python ns.py raw POST domains/acme/users --body '{...}' --confirm """ from __future__ import annotations import argparse import json import sys from ns_client import NetSapiensClient, PacketDialError def _emit(obj) -> None: print(json.dumps(obj, indent=2, ensure_ascii=False, default=str)) def _parse_body(raw: str | None) -> dict | None: if raw is None: return None try: parsed = json.loads(raw) except json.JSONDecodeError as exc: raise SystemExit(f"--body is not valid JSON: {exc}") if not isinstance(parsed, dict): raise SystemExit("--body must be a JSON object") return parsed def _require_confirm(args, action: str, detail: str) -> None: if not getattr(args, "confirm", False): print(f"[DRY RUN] Would {action}: {detail}") print("Refusing to perform a write without --confirm. Re-run with --confirm.") raise SystemExit(2) def main(argv=None) -> int: p = argparse.ArgumentParser(prog="ns.py", description="PacketDial / NetSapiens v2 CLI") p.add_argument("--json", action="store_true", help="emit raw JSON (default for most)") sub = p.add_subparsers(dest="cmd", required=True) # --- read --- sub.add_parser("status", help="API version + authenticated key identity") sub.add_parser("whoami", help="details of the authenticated API key") sub.add_parser("domains", help="list all domains") sp = sub.add_parser("domain", help="one domain"); sp.add_argument("domain") sp = sub.add_parser("users", help="users in a domain"); sp.add_argument("domain") sp = sub.add_parser("user", help="one user"); sp.add_argument("domain"); sp.add_argument("user") sp = sub.add_parser("phones", help="phones (devices) in a domain"); sp.add_argument("domain") sp = sub.add_parser("dids", help="phone numbers in a domain"); sp.add_argument("domain") sp = sub.add_parser("devices", help="devices for a user"); sp.add_argument("domain"); sp.add_argument("user") sub.add_parser("resellers", help="list resellers") sp = sub.add_parser("cdrs", help="call detail records") sp.add_argument("--domain"); sp.add_argument("--start"); sp.add_argument("--end") sp.add_argument("--limit", type=int, default=100) # --- write (gated) --- sp = sub.add_parser("create-domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("create-user"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("update-user"); sp.add_argument("domain"); sp.add_argument("user"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("delete-user"); sp.add_argument("domain"); sp.add_argument("user"); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("create-phone"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") sp = sub.add_parser("create-did"); sp.add_argument("domain"); sp.add_argument("--body", required=True); sp.add_argument("--confirm", action="store_true") # --- raw escape hatch --- sp = sub.add_parser("raw", help="raw request against any v2 path") sp.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"]) sp.add_argument("path", help="relative path, e.g. domains/acme/users") sp.add_argument("--body"); sp.add_argument("--confirm", action="store_true") args = p.parse_args(argv) client = NetSapiensClient() try: if args.cmd == "status": _emit({"version": client.version(), "apiKey": client.whoami()}) elif args.cmd == "whoami": _emit(client.whoami()) elif args.cmd == "domains": _emit(client.domains()) elif args.cmd == "domain": _emit(client.domain(args.domain)) elif args.cmd == "users": _emit(client.users(args.domain)) elif args.cmd == "user": _emit(client.user(args.domain, args.user)) elif args.cmd == "phones": _emit(client.phones(args.domain)) elif args.cmd == "dids": _emit(client.phonenumbers(args.domain)) elif args.cmd == "devices": _emit(client.devices(args.domain, args.user)) elif args.cmd == "resellers": _emit(client.resellers()) elif args.cmd == "cdrs": filters = {"limit": args.limit} if args.start: filters["start-date"] = args.start if args.end: filters["end-date"] = args.end _emit(client.cdrs(domain=args.domain, **filters)) elif args.cmd == "create-domain": body = _parse_body(args.body) _require_confirm(args, "create domain", json.dumps(body)) _emit(client.create_domain(body)) elif args.cmd == "create-user": body = _parse_body(args.body) _require_confirm(args, "create user", f"{args.domain}: {json.dumps(body)}") _emit(client.create_user(args.domain, body)) elif args.cmd == "update-user": body = _parse_body(args.body) _require_confirm(args, "update user", f"{args.domain}/{args.user}: {json.dumps(body)}") _emit(client.update_user(args.domain, args.user, body)) elif args.cmd == "delete-user": _require_confirm(args, "DELETE user", f"{args.domain}/{args.user}") _emit(client.delete_user(args.domain, args.user)) elif args.cmd == "create-phone": body = _parse_body(args.body) _require_confirm(args, "create phone", f"{args.domain}: {json.dumps(body)}") _emit(client.create_phone(args.domain, body)) elif args.cmd == "create-did": body = _parse_body(args.body) _require_confirm(args, "create phone number", f"{args.domain}: {json.dumps(body)}") _emit(client.create_phonenumber(args.domain, body)) elif args.cmd == "raw": body = _parse_body(args.body) if args.method != "GET": _require_confirm(args, f"{args.method} {args.path}", json.dumps(body)) _emit(client.request(args.method, args.path, json_body=body)) else: p.error(f"unknown command {args.cmd}") except PacketDialError as exc: print(f"[ERROR] {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())