feat: yealink-ymcs skill — YMCS v2 device-management API, pairs with packetdial
New skill to manage ACG's Yealink phone fleets via Yealink Management Cloud Service v2 (us-api.ymcs.yealink.com). RTFM'd the API (token auth via POST /v2/token Basic+bearer, NOT the legacy RPS HMAC; legacy-TLS renegotiation required) + endpoint map from the dszp/n8n-nodes- yealinkymcs community node. Live-verified: token auth, sites (one ACG AccessKey sees ALL client sites — VWP/GuruHQ/Ace Pick Up Parks as children of the ACG parent), devices, accounts, rps-servers (RPS = "WL - ACG" ftp://p.packetdials.net). Gated writes (--confirm): add-devices-by-mac, add-sipaccount (push a NetSapiens SIP cred onto a phone = the PBX glue), reboot, reset, rps add/del; + raw passthrough (auto-recovers the MSYS /v2 path-mangling). Creds vaulted at services/yealink-ymcs.sops.yaml. Pairs with packetdial onboard-domain for new-client phone provisioning; VWP is the live pilot. Honest [V]/[P] verification status in SKILL.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
.claude/skills/yealink-ymcs/SKILL.md
Normal file
80
.claude/skills/yealink-ymcs/SKILL.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: yealink-ymcs
|
||||
description: >
|
||||
Manage ACG's Yealink phone fleets via the Yealink Management Cloud Service (YMCS) v2 open API
|
||||
(us-api.ymcs.yealink.com). List sites/devices/accounts/firmwares/models/alarms, RPS servers +
|
||||
devices; gated writes (add devices by MAC, reboot/factory-reset, RPS add/remove, push a SIP
|
||||
account onto a device). Read-only by default; writes gated --confirm. Pairs with the `packetdial`
|
||||
skill (NetSapiens PBX) for phone provisioning. One ACG AccessKey sees ALL client sites.
|
||||
Triggers: yealink, ymcs, provision phone, register MAC, RPS, device firmware, reboot phone,
|
||||
push sip account to phone, yealink device manager.
|
||||
---
|
||||
|
||||
# yealink-ymcs — Yealink Management Cloud Service (YMCS) v2 API
|
||||
|
||||
Manages the **physical Yealink phones** for ACG-managed clients. Pairs with **`packetdial`**:
|
||||
the PBX (NetSapiens/PacketDial) owns the SIP accounts; **YMCS owns the phones** (provisioning,
|
||||
config, firmware, RPS zero-touch). See [[reference_packetdial_oit_netsapiens]] for the vendor stack.
|
||||
|
||||
## Auth (v2 — token, not the legacy RPS HMAC)
|
||||
`POST /v2/token` with HTTP **Basic** `base64(AccessKeyID:Secret)` (+ `timestamp` ms + `nonce`
|
||||
headers + body `{"grant_type":"client_credentials"}`) → bearer token (~24h). Subsequent calls send
|
||||
`Authorization: Bearer <token>`. Creds from vault **`services/yealink-ymcs.sops.yaml`**
|
||||
(`credentials.access_key_id` / `access_key_secret`); env overrides `YMCS_ACCESS_KEY_ID` /
|
||||
`YMCS_ACCESS_KEY_SECRET`; region `YMCS_REGION` (us|eu|au, default us → `us-api.ymcs.yealink.com`).
|
||||
**Yealink servers require LEGACY TLS renegotiation** — handled in-client via an SSL context with
|
||||
`OP_LEGACY_SERVER_CONNECT`.
|
||||
|
||||
## Account scope (verified 2026-06-22)
|
||||
The ACG AccessKey is the **parent account** — one key sees **all client sites** as children:
|
||||
`Arizona Computer Guru LLC` (parent) → `VWP` (Valley Wide Plastering), `GuruHQ`, `Ace Pick Up
|
||||
Parks`. **No per-client keys needed.** (Per-client YMCS portal logins still exist, e.g. Valleywide
|
||||
in `clients/valleywide/`, but the API key covers everything.)
|
||||
|
||||
## Commands
|
||||
```bash
|
||||
PY="$CLAUDETOOLS_ROOT/.claude/scripts/py.sh"; Y=".claude/skills/yealink-ymcs/scripts/ymcs.py"
|
||||
# reads (live-verified [V] unless noted)
|
||||
bash "$PY" "$Y" sites # [V] site tree (id/name/parentId/level)
|
||||
bash "$PY" "$Y" devices [--site <id>] # [V] devices (keys: mac, modelName, deviceStatus, siteId, sn, programVersion, wanIp...) — --site filter is best-effort [P]
|
||||
bash "$PY" "$Y" accounts # [V] device accounts
|
||||
bash "$PY" "$Y" device-groups | device-configs | firmwares | models | alarms | oplogs
|
||||
bash "$PY" "$Y" official-firmwares # [P] returns a non-standard shape; may need a model param
|
||||
bash "$PY" "$Y" rps-servers # [V] RPS provisioning servers (ACG: "WL - ACG" ftp://p.packetdials.net)
|
||||
bash "$PY" "$Y" rps-devices # [V] devices registered in RPS
|
||||
# writes (ALL gated --confirm)
|
||||
bash "$PY" "$Y" add-devices-by-mac --body '{...}' --confirm # bulk-add phones by MAC to a site
|
||||
bash "$PY" "$Y" add-sipaccount --body '{...}' --confirm # push a SIP account onto a device (PBX glue)
|
||||
bash "$PY" "$Y" reboot --body '{"ids":[...]}' --confirm # reboot device(s)
|
||||
bash "$PY" "$Y" reset --body '{"ids":[...]}' --confirm # FACTORY RESET — careful
|
||||
bash "$PY" "$Y" rps-add --body '{...}' --confirm # add device(s) to RPS (zero-touch)
|
||||
bash "$PY" "$Y" rps-del --body '{...}' --confirm
|
||||
bash "$PY" "$Y" raw POST /v2/dm/listDevices --body '{"skip":0,"limit":50,"autoCount":true}' [--confirm]
|
||||
```
|
||||
List endpoints page automatically (`{skip,limit,autoCount}` → `{total, data:[]}`); the wrappers
|
||||
return `{total, count, data}` with all pages.
|
||||
|
||||
## How it pairs with packetdial (the onboarding pipeline)
|
||||
1. **`packetdial onboard-domain`** → create the PBX domain + users + DIDs (NetSapiens SIP accounts).
|
||||
2. **`yealink-ymcs`** → the phones: `add-devices-by-mac` into the client's site, `add-sipaccount`
|
||||
to push each NetSapiens SIP cred onto its phone, and/or `rps-add` so the phone zero-touch-
|
||||
provisions on first boot (RPS server `WL - ACG` = `ftp://p.packetdials.net`).
|
||||
3. Result: new client → domain live → phones registered to `pbx.packetdial.com`.
|
||||
**VWP is the live pilot** (just onboarded to the PBX; 21 devices in YMCS, fleet partly pending).
|
||||
|
||||
## Gotchas
|
||||
- **Legacy TLS** (handled). If a future Python drops `OP_LEGACY_SERVER_CONNECT`, connections fail.
|
||||
- **MSYS path conversion:** in `raw`, a leading `/v2/...` arg gets rewritten by Git-Bash to
|
||||
`C:/Program Files/Git/v2/...`. The `raw` command auto-recovers (cuts back to `/v2/`); don't use
|
||||
`MSYS_NO_PATHCONV=1` (it breaks the vault subprocess). Named wrappers are unaffected.
|
||||
- `/v2/dm/sipAccounts` is the **create** endpoint (not a list) — wrapped as the gated
|
||||
`add-sipaccount`. There's no list-sip-accounts in the v2 API.
|
||||
- Verification: token + sites/devices/accounts/rps-servers are [V]; writes + official-firmwares +
|
||||
the `--site` filter are [P] (plumbed per the n8n reference + spec, not lifecycle-exercised — no
|
||||
throwaway device to safely test writes on).
|
||||
|
||||
## Reference
|
||||
- Vendor stack: [[reference_packetdial_oit_netsapiens]]. Portal login (web UI):
|
||||
`infrastructure/voip-phones.sops.yaml`. Per-client portal e.g. `clients/valleywide/`.
|
||||
- Endpoint map harvested from the `dszp/n8n-nodes-yealinkymcs` community node + Yealink "Open API
|
||||
for YMCS V4X" doc. Full `/v2/dm/*` + `/v2/rps/*` surface reachable via `raw`.
|
||||
111
.claude/skills/yealink-ymcs/scripts/ymcs.py
Normal file
111
.claude/skills/yealink-ymcs/scripts/ymcs.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ymcs.py — CLI for the Yealink Management Cloud Service (YMCS) v2 API.
|
||||
|
||||
Read-only by default; every write (add/delete/reboot/reset/RPS) is gated behind
|
||||
--confirm. Pairs with the `packetdial` skill for phone provisioning.
|
||||
|
||||
ymcs.py sites|devices|accounts|sipaccounts|device-groups|device-configs
|
||||
firmwares|official-firmwares|models|alarms|oplogs|rps-servers|rps-devices
|
||||
ymcs.py devices --site <siteId> # filter a list by site
|
||||
ymcs.py add-devices-by-mac --body '{...}' --confirm
|
||||
ymcs.py reboot --body '{"ids":[...]}' --confirm
|
||||
ymcs.py rps-add --body '{...}' --confirm
|
||||
ymcs.py raw POST /v2/dm/listDevices --body '{"skip":0,"limit":50,"autoCount":true}'
|
||||
"""
|
||||
import sys, os, json, argparse
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ymcs_client import YmcsClient, YmcsError
|
||||
|
||||
|
||||
def _emit(obj):
|
||||
print(json.dumps(obj, indent=2, default=str))
|
||||
|
||||
|
||||
def _parse_body(s):
|
||||
if not s:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[ERROR] --body is not valid JSON: {e}", file=sys.stderr); sys.exit(2)
|
||||
|
||||
|
||||
def _require_confirm(args, action, detail=""):
|
||||
if not getattr(args, "confirm", False):
|
||||
print(f"[DRY RUN] Would {action}" + (f": {detail}" if detail else ""))
|
||||
print("Refusing to perform a write without --confirm. Re-run with --confirm.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(prog="ymcs.py", description="Yealink YMCS v2 API client")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
# reads
|
||||
for name in ["sites", "accounts", "device-groups", "device-configs",
|
||||
"firmwares", "official-firmwares", "models", "alarms", "oplogs",
|
||||
"rps-servers", "rps-devices"]:
|
||||
sub.add_parser(name)
|
||||
d = sub.add_parser("devices"); d.add_argument("--site")
|
||||
|
||||
# writes (gated)
|
||||
for name in ["add-devices-by-mac", "add-devices", "del-devices", "reboot", "reset",
|
||||
"rps-add", "rps-del", "add-sipaccount"]:
|
||||
w = sub.add_parser(name); w.add_argument("--body", required=True); w.add_argument("--confirm", action="store_true")
|
||||
|
||||
r = sub.add_parser("raw")
|
||||
r.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
r.add_argument("path"); r.add_argument("--body"); r.add_argument("--confirm", action="store_true")
|
||||
|
||||
args = p.parse_args(argv)
|
||||
try:
|
||||
c = YmcsClient()
|
||||
if args.cmd == "sites": _emit(c.list_sites())
|
||||
elif args.cmd == "devices": _emit(c.list_devices(**({"siteId": args.site} if args.site else {})))
|
||||
elif args.cmd == "accounts": _emit(c.list_accounts())
|
||||
elif args.cmd == "device-groups": _emit(c.list_device_groups())
|
||||
elif args.cmd == "device-configs": _emit(c.list_device_configs())
|
||||
elif args.cmd == "firmwares": _emit(c.list_firmwares())
|
||||
elif args.cmd == "official-firmwares": _emit(c.list_official_firmwares())
|
||||
elif args.cmd == "models": _emit(c.models())
|
||||
elif args.cmd == "alarms": _emit(c.list_alarms())
|
||||
elif args.cmd == "oplogs": _emit(c.list_oplogs())
|
||||
elif args.cmd == "rps-servers": _emit(c.rps_list_servers())
|
||||
elif args.cmd == "rps-devices": _emit(c.rps_list_devices())
|
||||
|
||||
elif args.cmd == "add-devices-by-mac":
|
||||
b = _parse_body(args.body); _require_confirm(args, "ADD devices by MAC", json.dumps(b)[:160]); _emit(c.add_devices_by_mac(b))
|
||||
elif args.cmd == "add-devices":
|
||||
b = _parse_body(args.body); _require_confirm(args, "ADD devices", json.dumps(b)[:160]); _emit(c.add_devices(b))
|
||||
elif args.cmd == "del-devices":
|
||||
b = _parse_body(args.body); _require_confirm(args, "DELETE devices", json.dumps(b)[:160]); _emit(c.del_devices(b))
|
||||
elif args.cmd == "reboot":
|
||||
b = _parse_body(args.body); _require_confirm(args, "REBOOT devices", json.dumps(b)[:160]); _emit(c.device_reboot(b))
|
||||
elif args.cmd == "reset":
|
||||
b = _parse_body(args.body); _require_confirm(args, "FACTORY-RESET devices", json.dumps(b)[:160]); _emit(c.device_reset(b))
|
||||
elif args.cmd == "rps-add":
|
||||
b = _parse_body(args.body); _require_confirm(args, "RPS add devices", json.dumps(b)[:160]); _emit(c.rps_add_devices(b))
|
||||
elif args.cmd == "rps-del":
|
||||
b = _parse_body(args.body); _require_confirm(args, "RPS delete devices", json.dumps(b)[:160]); _emit(c.rps_delete_devices(b))
|
||||
elif args.cmd == "add-sipaccount":
|
||||
b = _parse_body(args.body); _require_confirm(args, "ADD SIP account to device", json.dumps(b)[:160]); _emit(c.add_sip_account(b))
|
||||
|
||||
elif args.cmd == "raw":
|
||||
b = _parse_body(args.body) if args.body else None
|
||||
path = args.path
|
||||
# Recover from Git-Bash MSYS path-conversion (a leading /v2/... arg gets
|
||||
# rewritten to C:/Program Files/Git/v2/...). Cut back to the API path.
|
||||
if "/v2/" in path:
|
||||
path = path[path.index("/v2/"):]
|
||||
if args.method != "GET":
|
||||
_require_confirm(args, f"{args.method} {path}", json.dumps(b or {})[:160])
|
||||
_emit(c.request(args.method, path, body=b))
|
||||
else:
|
||||
p.error(f"unknown command {args.cmd}")
|
||||
except YmcsError as exc:
|
||||
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
197
.claude/skills/yealink-ymcs/scripts/ymcs_client.py
Normal file
197
.claude/skills/yealink-ymcs/scripts/ymcs_client.py
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ymcs_client.py — Yealink Management Cloud Service (YMCS) v2 open-API client.
|
||||
|
||||
Manages ACG-managed client Yealink phone fleets. Pairs with the `packetdial`
|
||||
skill (NetSapiens PBX): YMCS owns the physical phones (provisioning, config,
|
||||
firmware); the PBX owns the SIP accounts the phones register to.
|
||||
|
||||
Auth (v2): POST /v2/token with HTTP Basic (AccessKeyID:Secret) -> bearer token
|
||||
(cached ~24h). Subsequent calls send `Authorization: Bearer <token>`. Every
|
||||
request also carries `timestamp` (ms) + `nonce` headers. Yealink's servers
|
||||
require LEGACY TLS renegotiation — handled via an SSL context with
|
||||
OP_LEGACY_SERVER_CONNECT.
|
||||
|
||||
Credentials come from the SOPS vault entry `services/yealink-ymcs.sops.yaml`
|
||||
(credentials.access_key_id / access_key_secret), or env overrides
|
||||
YMCS_ACCESS_KEY_ID / YMCS_ACCESS_KEY_SECRET. Region via YMCS_REGION (default us).
|
||||
|
||||
List endpoints are POST with {skip, limit, autoCount} and return {total, data:[]}.
|
||||
"""
|
||||
import sys, os, json, ssl, base64, uuid, time, subprocess
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
|
||||
REGION_HOSTS = {
|
||||
"us": "https://us-api.ymcs.yealink.com",
|
||||
"eu": "https://eu-api.ymcs.yealink.com",
|
||||
"au": "https://au-api.ymcs.yealink.com",
|
||||
}
|
||||
VAULT_ENTRY = "services/yealink-ymcs.sops.yaml"
|
||||
|
||||
|
||||
def _log_skill_error(msg, context=""):
|
||||
try:
|
||||
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||
)
|
||||
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
|
||||
if not os.path.exists(h):
|
||||
return
|
||||
a = ["bash", h, "yealink-ymcs", msg]
|
||||
if context:
|
||||
a += ["--context", context]
|
||||
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class YmcsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _repo_root():
|
||||
return os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||
)
|
||||
|
||||
|
||||
def _vault_field(field):
|
||||
"""Read a field from the YMCS vault entry via vault.sh (None if absent)."""
|
||||
vault_sh = os.path.join(_repo_root(), ".claude", "scripts", "vault.sh")
|
||||
if not os.path.exists(vault_sh):
|
||||
return None
|
||||
try:
|
||||
r = subprocess.run(["bash", vault_sh, "get-field", VAULT_ENTRY, field],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
v = (r.stdout or "").strip()
|
||||
return v or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _legacy_tls_context():
|
||||
"""SSL context that permits Yealink's legacy TLS renegotiation."""
|
||||
ctx = ssl.create_default_context()
|
||||
legacy = getattr(ssl, "OP_LEGACY_SERVER_CONNECT", 0x4)
|
||||
ctx.options |= legacy
|
||||
return ctx
|
||||
|
||||
|
||||
class YmcsClient:
|
||||
def __init__(self):
|
||||
self.key_id = (os.environ.get("YMCS_ACCESS_KEY_ID")
|
||||
or _vault_field("credentials.access_key_id"))
|
||||
self.key_secret = (os.environ.get("YMCS_ACCESS_KEY_SECRET")
|
||||
or _vault_field("credentials.access_key_secret"))
|
||||
if not self.key_id or not self.key_secret:
|
||||
raise YmcsError(
|
||||
"No YMCS credentials. Expected vault " + VAULT_ENTRY +
|
||||
" (credentials.access_key_id/access_key_secret) or env "
|
||||
"YMCS_ACCESS_KEY_ID/YMCS_ACCESS_KEY_SECRET."
|
||||
)
|
||||
self.region = (os.environ.get("YMCS_REGION") or "us").lower()
|
||||
self.base = REGION_HOSTS.get(self.region, REGION_HOSTS["us"])
|
||||
self._ctx = _legacy_tls_context()
|
||||
self._token = None
|
||||
|
||||
# ---- low-level HTTP ----
|
||||
def _http(self, method, url, headers, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30, context=self._ctx) as r:
|
||||
raw = r.read().decode("utf-8", "replace")
|
||||
return r.status, (json.loads(raw) if raw.strip() else None)
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode("utf-8", "replace")
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except Exception:
|
||||
return e.code, {"error": raw[:400]}
|
||||
except Exception as e:
|
||||
return 0, {"error": str(e)}
|
||||
|
||||
def _stamp_headers(self, extra=None):
|
||||
h = {"Content-Type": "application/json",
|
||||
"timestamp": str(int(time.time() * 1000)),
|
||||
"nonce": uuid.uuid4().hex}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
# ---- auth ----
|
||||
def token(self):
|
||||
if self._token:
|
||||
return self._token
|
||||
basic = base64.b64encode(f"{self.key_id}:{self.key_secret}".encode()).decode()
|
||||
h = self._stamp_headers({"Authorization": "Basic " + basic})
|
||||
st, r = self._http("POST", self.base + "/v2/token", h, {"grant_type": "client_credentials"})
|
||||
if st != 200 or not isinstance(r, dict) or not r.get("access_token"):
|
||||
_log_skill_error(f"token request failed (HTTP {st})", context=f"resp={json.dumps(r)[:120]}")
|
||||
raise YmcsError(f"YMCS token request failed (HTTP {st}): {json.dumps(r)[:300]}")
|
||||
self._token = r["access_token"]
|
||||
return self._token
|
||||
|
||||
# ---- authenticated request ----
|
||||
def request(self, method, endpoint, body=None, params=None):
|
||||
if not endpoint.startswith("/"):
|
||||
endpoint = "/" + endpoint
|
||||
url = self.base + endpoint
|
||||
if params:
|
||||
q = {k: v for k, v in params.items() if v is not None}
|
||||
if q:
|
||||
url += "?" + urllib.parse.urlencode(q)
|
||||
h = self._stamp_headers({"Authorization": "Bearer " + self.token()})
|
||||
st, r = self._http(method, url, h, body)
|
||||
if st == 401: # token may have expired mid-life; refresh once
|
||||
self._token = None
|
||||
h = self._stamp_headers({"Authorization": "Bearer " + self.token()})
|
||||
st, r = self._http(method, url, h, body)
|
||||
if st not in (200, 201, 204):
|
||||
raise YmcsError(f"YMCS API {method} {endpoint} -> HTTP {st}: {json.dumps(r)[:300]}")
|
||||
return r
|
||||
|
||||
def _list(self, endpoint, limit=500, extra=None, fetch_all=True):
|
||||
"""POST a paged list endpoint ({skip,limit,autoCount}); returns the data list.
|
||||
|
||||
Set fetch_all=False (or limit small) for a single page.
|
||||
"""
|
||||
out, skip, total = [], 0, None
|
||||
while True:
|
||||
body = {"skip": skip, "limit": limit, "autoCount": total is None}
|
||||
if extra:
|
||||
body.update(extra)
|
||||
r = self.request("POST", endpoint, body=body)
|
||||
if total is None:
|
||||
total = (r or {}).get("total", 0)
|
||||
items = (r or {}).get("data", []) or []
|
||||
out.extend(items)
|
||||
skip += len(items)
|
||||
if not fetch_all or not items or skip >= (total or 0):
|
||||
break
|
||||
return {"total": total, "count": len(out), "data": out}
|
||||
|
||||
# ================= READ wrappers (safe) =================
|
||||
def list_sites(self, **f): return self._list("/v2/dm/listSites", extra=f or None)
|
||||
def list_devices(self, **f): return self._list("/v2/dm/listDevices", extra=f or None)
|
||||
def list_accounts(self, **f): return self._list("/v2/dm/listAccounts", extra=f or None)
|
||||
def list_device_groups(self, **f): return self._list("/v2/dm/listDeviceGroups", extra=f or None)
|
||||
def list_device_configs(self, **f): return self._list("/v2/dm/listDeviceConfigs", extra=f or None)
|
||||
def list_firmwares(self, **f): return self._list("/v2/dm/listFirmwares", extra=f or None)
|
||||
def list_official_firmwares(self, **f): return self._list("/v2/dm/listOfficalFirmwares", extra=f or None)
|
||||
def list_alarms(self, **f): return self._list("/v2/dm/listAlarms", extra=f or None)
|
||||
def list_oplogs(self, **f): return self._list("/v2/dm/listOpLogs", extra=f or None)
|
||||
def models(self, **f): return self.request("POST", "/v2/dm/models", body=(f or {}))
|
||||
def rps_list_servers(self, **f): return self._list("/v2/rps/listServers", extra=f or None)
|
||||
def rps_list_devices(self, **f): return self._list("/v2/rps/listDevices", extra=f or None)
|
||||
|
||||
# ================= WRITE wrappers (gated by the CLI --confirm) =================
|
||||
def add_devices_by_mac(self, body): return self.request("POST", "/v2/dm/addDevicesByMac", body=body)
|
||||
def add_devices(self, body): return self.request("POST", "/v2/dm/addDevices", body=body)
|
||||
def del_devices(self, body): return self.request("POST", "/v2/dm/delDevices", body=body)
|
||||
def device_reboot(self, body): return self.request("POST", "/v2/dm/device/reboot", body=body)
|
||||
def device_reset(self, body): return self.request("POST", "/v2/dm/device/reset", body=body)
|
||||
def rps_add_devices(self, body): return self.request("POST", "/v2/rps/addDevices", body=body)
|
||||
def rps_delete_devices(self, body): return self.request("POST", "/v2/rps/deleteDevices", body=body)
|
||||
# SIP-account create = the PBX glue: push a NetSapiens SIP cred onto a device.
|
||||
# POST /v2/dm/sipAccounts requires sipServer1, register name/user/password, mac, etc.
|
||||
def add_sip_account(self, body): return self.request("POST", "/v2/dm/sipAccounts", body=body)
|
||||
Reference in New Issue
Block a user