sync: auto-sync from HOWARD-HOME at 2026-07-05 19:59:52
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-05 19:59:52
This commit is contained in:
430
.claude/skills/msp360/scripts/msp360.py
Normal file
430
.claude/skills/msp360/scripts/msp360.py
Normal file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MSP360 Managed Backup Service (MBS) CLI.
|
||||
|
||||
Read the fleet backup state and manage users / companies / licenses via the MBS
|
||||
provider API. Reads run freely; every WRITE requires --confirm (a dry-run preview is
|
||||
printed without it). See SKILL.md for the trigger list and examples.
|
||||
|
||||
Reads: status | monitoring | companies | users | computers | licenses | billing
|
||||
Writes: create-company | delete-company | create-user | delete-user
|
||||
grant-license | release-license | revoke-license | raw
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from msp360_client import Msp360Client, Msp360Error, STATUS_TEXT, BAD_STATUS # noqa: E402
|
||||
import backup_common as bc # noqa: E402 (msp360_client put .claude/scripts on sys.path)
|
||||
|
||||
|
||||
# --- helpers ------------------------------------------------------------------
|
||||
def _root() -> Path:
|
||||
# honor CLAUDETOOLS_ROOT / identity.json, same as the vault/HTTP plumbing.
|
||||
return bc.resolve_root()
|
||||
|
||||
|
||||
def _as_list(data, what: str) -> list:
|
||||
"""A read endpoint promised a JSON array; a non-list 2xx body is anomalous."""
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
raise Msp360Error(f"expected a list from {what}, got {type(data).__name__}: {str(data)[:160]}")
|
||||
|
||||
|
||||
def log_error(brief: str, context: str = "") -> None:
|
||||
"""Best-effort fleet error log; never breaks the caller (per CLAUDE.md)."""
|
||||
try:
|
||||
script = _root() / ".claude" / "scripts" / "log-skill-error.sh"
|
||||
if not script.exists():
|
||||
return
|
||||
cmd = ["bash", str(script), "msp360", brief]
|
||||
if context:
|
||||
cmd += ["--context", context]
|
||||
subprocess.run(cmd, capture_output=True, text=True, timeout=20)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def out(obj, as_json: bool) -> None:
|
||||
if as_json:
|
||||
print(json.dumps(obj, indent=2, default=str))
|
||||
|
||||
|
||||
def human(n) -> str:
|
||||
try:
|
||||
n = float(n)
|
||||
except (TypeError, ValueError):
|
||||
return str(n)
|
||||
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||
if abs(n) < 1024:
|
||||
return f"{n:.1f}{unit}"
|
||||
n /= 1024
|
||||
return f"{n:.1f}PB"
|
||||
|
||||
|
||||
def _days_since(ts: str):
|
||||
"""Whole days since an ISO timestamp. None if empty OR unparseable (callers must
|
||||
distinguish 'never ran' from 'could not parse' via the raw value themselves)."""
|
||||
if not ts:
|
||||
return None
|
||||
try:
|
||||
dt = _dt.datetime.fromisoformat(ts.strip().replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=_dt.timezone.utc)
|
||||
return (_dt.datetime.now(_dt.timezone.utc) - dt).days
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def require_confirm(args, action: str) -> bool:
|
||||
"""Return True to proceed; if --confirm absent, print the dry-run and return False."""
|
||||
if getattr(args, "confirm", False):
|
||||
return True
|
||||
print(f"[DRY-RUN] Would: {action}")
|
||||
print("[DRY-RUN] Re-run with --confirm to execute. Nothing was changed.")
|
||||
return False
|
||||
|
||||
|
||||
# --- read commands ------------------------------------------------------------
|
||||
def cmd_status(c: Msp360Client, args) -> int:
|
||||
comps = c.companies()
|
||||
users = c.users()
|
||||
mon = c.monitoring()
|
||||
if args.json:
|
||||
out({"companies": len(comps), "users": len(users), "plans": len(mon)}, True)
|
||||
return 0
|
||||
bad = sum(1 for m in mon if m.get("Status") in BAD_STATUS)
|
||||
print(f"[OK] MSP360 MBS reachable at {c.base}")
|
||||
print(f"[INFO] companies={len(comps)} users={len(users)} backup plans={len(mon)} "
|
||||
f"plans needing attention={bad}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_monitoring(c: Msp360Client, args) -> int:
|
||||
mon = _as_list(c.monitoring(), "monitoring")
|
||||
rows = []
|
||||
for m in mon:
|
||||
if args.company and args.company.lower() not in (m.get("CompanyName") or "").lower():
|
||||
continue
|
||||
status = m.get("Status")
|
||||
last_raw = m.get("LastStart") or ""
|
||||
never = not last_raw
|
||||
age = _days_since(last_raw)
|
||||
# --failed = triage: anything that is NOT a clean completed run and is not
|
||||
# in-progress. Includes never-run, Unknown(5) and null status (dead plans).
|
||||
if args.failed:
|
||||
clean_success = status in (0, 1) and not never
|
||||
if clean_success or status == 4: # 4 = Running
|
||||
continue
|
||||
# --stale = last run older than N days, OR never ran. An unparseable-but-present
|
||||
# timestamp is NOT claimed stale (avoid a false "not backing up").
|
||||
if args.stale is not None:
|
||||
if never:
|
||||
pass
|
||||
elif age is not None and age >= args.stale:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
rows.append((m, age))
|
||||
if args.json:
|
||||
out([r[0] for r in rows], True)
|
||||
return 0
|
||||
if not rows:
|
||||
print("[INFO] no plans match the filter.")
|
||||
return 0
|
||||
print(f"{'COMPANY':<26} {'COMPUTER':<20} {'PLAN':<26} {'STATUS':<12} {'LAST START':<19} DATA")
|
||||
for m, age in sorted(rows, key=lambda r: ((r[0].get('CompanyName') or '').lower(),
|
||||
r[0].get('ComputerName') or '')):
|
||||
st = STATUS_TEXT.get(m.get("Status"), str(m.get("Status")))
|
||||
last = m.get("LastStart") or "never"
|
||||
agestr = f" ({age}d)" if age is not None and age >= 30 else ""
|
||||
print(f"{(m.get('CompanyName') or '')[:25]:<26} {(m.get('ComputerName') or '')[:19]:<20} "
|
||||
f"{(m.get('PlanName') or '')[:25]:<26} {st:<12} {last[:19]:<19}{agestr} "
|
||||
f"{human(m.get('DataCopied', 0))}")
|
||||
print(f"\n[INFO] {len(rows)} plan(s). Filters: "
|
||||
f"company={args.company or '-'} failed={args.failed} stale>={args.stale}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_companies(c: Msp360Client, args) -> int:
|
||||
comps = _as_list(c.companies(), "companies")
|
||||
if args.json:
|
||||
out(comps, True)
|
||||
return 0
|
||||
for co in sorted(comps, key=lambda x: (x.get("Name") or "").lower()):
|
||||
lim = co.get("StorageLimit")
|
||||
limstr = "unlimited" if lim in (-1, None) else human(lim)
|
||||
print(f" {(co.get('Name') or ''):<34} id={co.get('Id')} storage_limit={limstr}")
|
||||
print(f"\n[INFO] {len(comps)} companies")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_users(c: Msp360Client, args) -> int:
|
||||
users = _as_list(c.users(), "users")
|
||||
if args.company:
|
||||
users = [u for u in users if args.company.lower() in (u.get("Company") or "").lower()]
|
||||
if args.json:
|
||||
out(users, True)
|
||||
return 0
|
||||
for u in sorted(users, key=lambda x: (x.get("Company") or "", x.get("Email") or "")):
|
||||
en = "enabled" if u.get("Enabled") else "DISABLED"
|
||||
print(f" {(u.get('Email') or ''):<34} {(u.get('Company') or ''):<26} "
|
||||
f"{en:<9} used={human(u.get('SpaceUsed', 0))} id={u.get('ID')}")
|
||||
print(f"\n[INFO] {len(users)} users" + (f" in '{args.company}'" if args.company else ""))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_computers(c: Msp360Client, args) -> int:
|
||||
try:
|
||||
comps = _as_list(c.computers(), "computers")
|
||||
except Msp360Error as exc:
|
||||
msg = str(exc).lower()
|
||||
if "remote management" in msg or "not enabled" in msg or "not available" in msg:
|
||||
print("[INFO] /api/Computers is gated behind the Remote Management API feature, "
|
||||
"which is not enabled on this MSP360 account. Use `users` + `monitoring` "
|
||||
"for endpoint/backup data instead.")
|
||||
return 0
|
||||
raise
|
||||
if args.json:
|
||||
out(comps, True)
|
||||
return 0
|
||||
for m in comps:
|
||||
print(f" {(m.get('Name') or m.get('ComputerName') or ''):<24} "
|
||||
f"user={m.get('User') or m.get('UserEmail') or ''} hid={m.get('Hid') or m.get('ID')}")
|
||||
print(f"\n[INFO] {len(comps) if isinstance(comps, list) else 0} computers")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_licenses(c: Msp360Client, args) -> int:
|
||||
lic = _as_list(c.licenses(available=True if args.available else None), "licenses")
|
||||
if args.json:
|
||||
out(lic, True)
|
||||
return 0
|
||||
taken = sum(1 for x in lic if x.get("IsTaken"))
|
||||
for x in lic:
|
||||
state = "TAKEN" if x.get("IsTaken") else "free"
|
||||
num = x.get("Number")
|
||||
num = "" if num is None else num
|
||||
print(f" #{str(num):<5} {(x.get('LicenseType') or ''):<22} {state:<6} "
|
||||
f"exp={(x.get('DateExpired') or '')[:10]} user={x.get('User') or '-'}")
|
||||
print(f"\n[INFO] {len(lic)} licenses ({taken} taken, {len(lic) - taken} free)")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_billing(c: Msp360Client, args) -> int:
|
||||
out(c.billing(), True) # billing has no table form; always JSON
|
||||
return 0
|
||||
|
||||
|
||||
# --- write commands -----------------------------------------------------------
|
||||
def cmd_create_company(c: Msp360Client, args) -> int:
|
||||
if not require_confirm(args, f"create company '{args.name}'"):
|
||||
return 0
|
||||
res = c.create_company(args.name)
|
||||
print(f"[OK] created company '{args.name}'")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_delete_company(c: Msp360Client, args) -> int:
|
||||
print(f"[WARNING] Deleting company id={args.id} removes the company record.")
|
||||
if not require_confirm(args, f"DELETE company id={args.id}"):
|
||||
return 0
|
||||
res = c.delete_company(args.id)
|
||||
print(f"[OK] deleted company id={args.id}")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_create_user(c: Msp360Client, args) -> int:
|
||||
payload = {
|
||||
"Email": args.email,
|
||||
"FirstName": args.first or "",
|
||||
"LastName": args.last or "",
|
||||
"Company": args.company,
|
||||
"Enabled": True,
|
||||
"NotificationEmails": [args.notify] if args.notify else [],
|
||||
}
|
||||
if args.password:
|
||||
payload["Password"] = args.password
|
||||
if not require_confirm(args, f"create user '{args.email}' in company '{args.company}'"):
|
||||
preview = dict(payload)
|
||||
if "Password" in preview:
|
||||
preview["Password"] = "***redacted***" # never echo a secret to the transcript
|
||||
out(preview, True)
|
||||
return 0
|
||||
res = c.create_user(payload)
|
||||
print(f"[OK] created user '{args.email}'")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_delete_user(c: Msp360Client, args) -> int:
|
||||
scope = "metadata only (backup data KEPT)" if args.keep_data else "metadata AND all backup data"
|
||||
print(f"[WARNING] Deleting user id={args.id}: {scope}.")
|
||||
if not require_confirm(args, f"DELETE user id={args.id} ({scope})"):
|
||||
return 0
|
||||
res = c.delete_user(args.id, keep_data=args.keep_data)
|
||||
print(f"[OK] deleted user id={args.id}")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_grant_license(c: Msp360Client, args) -> int:
|
||||
payload = {"UserID": args.user_id}
|
||||
if args.license_id:
|
||||
payload["LicenseID"] = args.license_id
|
||||
if not require_confirm(args, f"grant license {args.license_id or '(auto)'} to user {args.user_id}"):
|
||||
out(payload, True)
|
||||
return 0
|
||||
res = c.grant_license(payload)
|
||||
print("[OK] license granted")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_release_license(c: Msp360Client, args) -> int:
|
||||
payload = {"UserID": args.user_id}
|
||||
if args.license_id:
|
||||
payload["LicenseID"] = args.license_id
|
||||
if not require_confirm(args, f"release license from user {args.user_id}"):
|
||||
return 0
|
||||
res = c.release_license(payload)
|
||||
print("[OK] license released")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_revoke_license(c: Msp360Client, args) -> int:
|
||||
payload = {"UserID": args.user_id}
|
||||
if args.license_id:
|
||||
payload["LicenseID"] = args.license_id
|
||||
if not require_confirm(args, f"revoke license from user {args.user_id}"):
|
||||
return 0
|
||||
res = c.revoke_license(payload)
|
||||
print("[OK] license revoked")
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_raw(c: Msp360Client, args) -> int:
|
||||
method = args.method.upper()
|
||||
body = json.loads(args.data) if args.data else None
|
||||
# Git-Bash rewrites a leading-slash arg into a Windows path (e.g.
|
||||
# '/api/Companies' -> 'C:/Program Files/Git/api/Companies'). Recover the /api/... tail.
|
||||
path = args.path
|
||||
if "/api/" in path and not path.startswith("/api"):
|
||||
path = path[path.index("/api/"):]
|
||||
elif not path.startswith("/"):
|
||||
path = "/" + path
|
||||
if method != "GET" and not require_confirm(args, f"{method} {path} {args.data or ''}"):
|
||||
return 0
|
||||
res = c.request(method, path, body=body)
|
||||
out(res, True)
|
||||
return 0
|
||||
|
||||
|
||||
# --- argparse -----------------------------------------------------------------
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(prog="msp360", description="MSP360 Managed Backup Service CLI")
|
||||
p.add_argument("--base", help="override API base URL")
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
def add_json(sp):
|
||||
sp.add_argument("--json", action="store_true", help="machine-readable JSON output")
|
||||
return sp
|
||||
|
||||
add_json(sub.add_parser("status", help="reachability + fleet counts")).set_defaults(func=cmd_status)
|
||||
|
||||
sp = sub.add_parser("monitoring", help="fleet backup status (the primary read)")
|
||||
sp.add_argument("--company", help="filter by company name substring")
|
||||
sp.add_argument("--failed", action="store_true", help="only plans not in a clean state")
|
||||
sp.add_argument("--stale", type=int, metavar="DAYS",
|
||||
help="only plans whose last run is >= DAYS old (or never)")
|
||||
add_json(sp)
|
||||
sp.set_defaults(func=cmd_monitoring)
|
||||
|
||||
add_json(sub.add_parser("companies", help="list companies")).set_defaults(func=cmd_companies)
|
||||
|
||||
sp = sub.add_parser("users", help="list backup users")
|
||||
sp.add_argument("--company", help="filter by company name substring")
|
||||
add_json(sp)
|
||||
sp.set_defaults(func=cmd_users)
|
||||
|
||||
add_json(sub.add_parser("computers", help="list managed endpoints")).set_defaults(func=cmd_computers)
|
||||
|
||||
sp = sub.add_parser("licenses", help="list licenses")
|
||||
sp.add_argument("--available", action="store_true", help="only free/available licenses")
|
||||
add_json(sp)
|
||||
sp.set_defaults(func=cmd_licenses)
|
||||
|
||||
sub.add_parser("billing", help="billing info (JSON)").set_defaults(func=cmd_billing)
|
||||
|
||||
# writes
|
||||
sp = sub.add_parser("create-company", help="create a company [--confirm]")
|
||||
sp.add_argument("--name", required=True)
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=cmd_create_company)
|
||||
|
||||
sp = sub.add_parser("delete-company", help="delete a company by id [--confirm]")
|
||||
sp.add_argument("--id", required=True)
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=cmd_delete_company)
|
||||
|
||||
sp = sub.add_parser("create-user", help="create a backup user [--confirm]")
|
||||
sp.add_argument("--email", required=True)
|
||||
sp.add_argument("--company", required=True)
|
||||
sp.add_argument("--first")
|
||||
sp.add_argument("--last")
|
||||
sp.add_argument("--password")
|
||||
sp.add_argument("--notify", help="notification email")
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=cmd_create_user)
|
||||
|
||||
sp = sub.add_parser("delete-user", help="delete a user by id [--confirm]")
|
||||
sp.add_argument("--id", required=True)
|
||||
sp.add_argument("--keep-data", action="store_true", help="keep backup data (metadata only)")
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=cmd_delete_user)
|
||||
|
||||
for name, fn, verb in (("grant-license", cmd_grant_license, "grant"),
|
||||
("release-license", cmd_release_license, "release"),
|
||||
("revoke-license", cmd_revoke_license, "revoke")):
|
||||
sp = sub.add_parser(name, help=f"{verb} a license [--confirm]")
|
||||
sp.add_argument("--user-id", required=True)
|
||||
sp.add_argument("--license-id")
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=fn)
|
||||
|
||||
sp = sub.add_parser("raw", help="raw API call (writes need --confirm)")
|
||||
sp.add_argument("method")
|
||||
sp.add_argument("path", help="e.g. /api/Users/{id}")
|
||||
sp.add_argument("--data", help="JSON request body")
|
||||
sp.add_argument("--confirm", action="store_true")
|
||||
sp.set_defaults(func=cmd_raw)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
client = Msp360Client(base=args.base)
|
||||
try:
|
||||
return args.func(client, args)
|
||||
except bc.BackupError as exc: # includes Msp360Error + vault/HTTP transport errors
|
||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||
log_error(str(exc)[:200], context=f"command={args.command}")
|
||||
return 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[ERROR] unexpected: {exc}", file=sys.stderr)
|
||||
log_error(f"unexpected: {exc}"[:200], context=f"command={args.command}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
119
.claude/skills/msp360/scripts/msp360_client.py
Normal file
119
.claude/skills/msp360/scripts/msp360_client.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MSP360 Managed Backup Service (MBS) API client.
|
||||
|
||||
Full CRUD over the provider REST API at https://api.mspbackups.com. Auth is a Bearer
|
||||
token from POST /api/Provider/Login (login+password generated in MSP360 console
|
||||
Settings > General, vaulted at msp-tools/msp360-api.sops.yaml).
|
||||
|
||||
Reuses `.claude/scripts/backup_common.py` for repo-root resolution, the canonical SOPS
|
||||
vault read, and stdlib JSON HTTP -- same plumbing as the seafile/owncloud/datto-workplace
|
||||
sibling skills. Standalone: stdlib-only transport.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
# backup_common lives at <root>/.claude/scripts/ ; this file is
|
||||
# <root>/.claude/skills/msp360/scripts/msp360_client.py -> parents[3] == <root>/.claude
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "scripts"))
|
||||
import backup_common as bc # noqa: E402
|
||||
|
||||
VAULT_ENTRY = "msp-tools/msp360-api.sops.yaml"
|
||||
DEFAULT_BASE = "https://api.mspbackups.com"
|
||||
|
||||
# Monitoring plan Status codes (observed + MBS convention).
|
||||
STATUS_TEXT = {
|
||||
0: "Success", 1: "Success", 2: "Warning", 3: "Failed",
|
||||
4: "Running", 5: "Unknown", 6: "Interrupted", 7: "Completed/warnings",
|
||||
}
|
||||
# Statuses that mean "this run did NOT produce a clean backup".
|
||||
BAD_STATUS = {2, 3, 6, 7}
|
||||
|
||||
|
||||
class Msp360Error(bc.BackupError):
|
||||
"""MSP360 transport / auth / API error (carries optional HTTP status)."""
|
||||
|
||||
|
||||
class Msp360Client:
|
||||
"""Thin MSP360 MBS API client: login once, then typed helpers over the endpoints."""
|
||||
|
||||
def __init__(self, base: Optional[str] = None):
|
||||
self.base = (base or DEFAULT_BASE).rstrip("/")
|
||||
self._token: Optional[str] = None
|
||||
|
||||
# --- auth -----------------------------------------------------------------
|
||||
def login(self) -> None:
|
||||
login = bc.vault_get_field(VAULT_ENTRY, "credentials.login")
|
||||
pw = bc.vault_get_field(VAULT_ENTRY, "credentials.password")
|
||||
st, body = bc.http_json(
|
||||
"POST", f"{self.base}/api/Provider/Login",
|
||||
body={"UserName": login, "Password": pw})
|
||||
if st != 200 or not isinstance(body, dict) or not body.get("access_token"):
|
||||
raise Msp360Error(f"MSP360 login failed (HTTP {st}): {body}", status=st)
|
||||
self._token = body["access_token"]
|
||||
|
||||
def _auth(self) -> dict:
|
||||
if not self._token:
|
||||
self.login()
|
||||
return {"Authorization": "Bearer " + str(self._token)}
|
||||
|
||||
def request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
|
||||
"""One API call. Raises Msp360Error on any non-2xx (with the API's error body)."""
|
||||
st, data = bc.http_json(method, f"{self.base}{path}", headers=self._auth(), body=body)
|
||||
if st < 200 or st >= 300:
|
||||
raise Msp360Error(f"{method} {path} -> HTTP {st}: {data}", status=st)
|
||||
return data
|
||||
|
||||
# --- reads ----------------------------------------------------------------
|
||||
def monitoring(self, user_id: Optional[str] = None) -> list:
|
||||
return self.request("GET", f"/api/Monitoring/{user_id}" if user_id else "/api/Monitoring")
|
||||
|
||||
def companies(self) -> list:
|
||||
return self.request("GET", "/api/Companies")
|
||||
|
||||
def users(self) -> list:
|
||||
return self.request("GET", "/api/Users")
|
||||
|
||||
def computers(self, offset: int = 0, count: int = 1000) -> list:
|
||||
return self.request("GET", f"/api/Computers/{offset}/{count}")
|
||||
|
||||
def licenses(self, available: Optional[bool] = None) -> list:
|
||||
path = "/api/Licenses"
|
||||
if available is not None:
|
||||
path += f"?isAvailable={'true' if available else 'false'}"
|
||||
return self.request("GET", path)
|
||||
|
||||
def billing(self) -> Any:
|
||||
return self.request("GET", "/api/Billing")
|
||||
|
||||
# --- writes (companies) ---------------------------------------------------
|
||||
def create_company(self, name: str, **extra: Any) -> Any:
|
||||
payload = {"Name": name, "StorageLimit": -1}
|
||||
payload.update(extra)
|
||||
return self.request("POST", "/api/Companies", body=payload)
|
||||
|
||||
def delete_company(self, company_id: str) -> Any:
|
||||
return self.request("DELETE", f"/api/Companies/{company_id}")
|
||||
|
||||
# --- writes (users) -------------------------------------------------------
|
||||
# Property edits (PUT /api/Users, /api/Companies) are reachable via the CLI's
|
||||
# `raw PUT ...` passthrough; not wrapped here until a typed edit command exists.
|
||||
def create_user(self, payload: dict) -> Any:
|
||||
return self.request("POST", "/api/Users", body=payload)
|
||||
|
||||
def delete_user(self, user_id: str, keep_data: bool = False) -> Any:
|
||||
# /{id}/Account = drop metadata but KEEP backup data; /{id} = drop everything.
|
||||
suffix = f"/api/Users/{user_id}/Account" if keep_data else f"/api/Users/{user_id}"
|
||||
return self.request("DELETE", suffix)
|
||||
|
||||
# --- writes (licenses) ----------------------------------------------------
|
||||
def grant_license(self, payload: dict) -> Any:
|
||||
return self.request("POST", "/api/Licenses/Grant", body=payload)
|
||||
|
||||
def release_license(self, payload: dict) -> Any:
|
||||
return self.request("POST", "/api/Licenses/Release", body=payload)
|
||||
|
||||
def revoke_license(self, payload: dict) -> Any:
|
||||
return self.request("POST", "/api/Licenses/Revoke", body=payload)
|
||||
Reference in New Issue
Block a user