Compare commits
194 Commits
85c8149495
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 996041406c | |||
| 1f56098652 | |||
| 46d2ae7a5b | |||
| 830a86ea95 | |||
| a0690793bc | |||
| 0fdeabde3d | |||
|
|
df46f56e59 | ||
|
|
299c053795 | ||
|
|
8350a4aeaf | ||
| 1041db8587 | |||
| ebeb0ec1f6 | |||
| ecc0c8cd82 | |||
|
|
07404759f3 | ||
|
|
3286a47090 | ||
| f95809cd9d | |||
| e78152a706 | |||
| 017dcdf04c | |||
| 2f35163866 | |||
| c34cdb3a39 | |||
| ec1cb40b62 | |||
| f9b44cfb68 | |||
| 1ca456b03f | |||
| 250eb8ecfe | |||
| 5641532429 | |||
| 1b0eeb5595 | |||
| b4972497a7 | |||
| a70ec28bdc | |||
| 7f8fb5d9c5 | |||
| d477970ab8 | |||
| 522594dedb | |||
| 06ed9ba562 | |||
| 25af453f61 | |||
| 736d43f482 | |||
| 033019df18 | |||
| bf95a10685 | |||
| 2fba62b7df | |||
| db10206aea | |||
| eb04287396 | |||
| ab987bf9e1 | |||
| d9f877661e | |||
| 5a463ae133 | |||
| 6942827391 | |||
|
|
07268cd741 | ||
| 57c23fc8d7 | |||
| 2e92a2d38d | |||
| 9926e5f2cc | |||
|
|
252d22048b | ||
| b3ea44a8ff | |||
| e765c9115d | |||
| cac5eadaee | |||
| 9c998a95d0 | |||
| 447b18d62b | |||
| a7b2fae468 | |||
| cc336243e3 | |||
| fa97bcc49b | |||
| d037d82f5e | |||
| c0d304485a | |||
| 25ca9306e1 | |||
| fcf43044d4 | |||
| 2a1ea6c9f7 | |||
| fc12c596f1 | |||
| eee469ce86 | |||
| 4e148b6dc5 | |||
| 19edfc45c0 | |||
| e548f63605 | |||
| 840b0ce9d6 | |||
| 09ab3de106 | |||
| 361478dd66 | |||
| 0a438929d0 | |||
|
|
f379a828e9 | ||
| a2a277ab67 | |||
| 9340cdec17 | |||
| 8def189a17 | |||
| 29725a3851 | |||
| 81398663ac | |||
| 2fd646218d | |||
|
|
30c20df2b9 | ||
| 5edab36df6 | |||
| 1952c36675 | |||
| 8b016edb9d | |||
| 5dfaaaa33b | |||
| 2259f4c172 | |||
| a507678254 | |||
|
|
1e1bae772d | ||
| 3d28f9ea5d | |||
|
|
a0a86742bc | ||
| 27595d6475 | |||
| 766c7fa54d | |||
| ff6546bee8 | |||
| 33b6140236 | |||
| efca405669 | |||
| f30413cffc | |||
| 5d53452c6d | |||
| 03d02128d7 | |||
| 2e262c619e | |||
| 8efc351e25 | |||
| ead30520e5 | |||
| 0daf06263f | |||
| 8854a584f3 | |||
| 5968ee9231 | |||
| da2aa60990 | |||
| b9635cafd9 | |||
| 45e1072dcb | |||
| 7264840da6 | |||
| 43101f4597 | |||
| e790d7c7e8 | |||
| 12c3d83f75 | |||
|
|
419de64af5 | ||
| ba2403b798 | |||
| 7dc519827c | |||
| 8f4984c5a6 | |||
| 0201e7422c | |||
| 0b11a400ed | |||
| 4b535a1a81 | |||
| c9ea7e8f01 | |||
| 27dc83f927 | |||
| b10f527cda | |||
| bd247191e6 | |||
| b20985d241 | |||
| 3cc9b22594 | |||
| 49d5a535a4 | |||
| 37a6598ef7 | |||
| a2a9356a02 | |||
| c3ad79d9fe | |||
| dfc237e6d4 | |||
| 14ac542a00 | |||
| c1706393b9 | |||
| e08f6c534e | |||
| f88c7d1739 | |||
| d5f7eadf82 | |||
| c289ba3370 | |||
| 60b307316c | |||
| 355c5f1e3f | |||
| d70cdd88e4 | |||
| faf796386a | |||
| 8b08ec64c2 | |||
| da6567ffe0 | |||
| 3d8aa2be59 | |||
| a8e76ba215 | |||
| 976bfe7957 | |||
| b0ebd44c9d | |||
| f274eed8fa | |||
| 5e682c9293 | |||
| bee140c020 | |||
| a737c4ab24 | |||
| fa4867580f | |||
| a5b8f6e7b1 | |||
| 6ad608c3bf | |||
| ea84b7bcd2 | |||
| b835f350d6 | |||
| 5e1ebf4789 | |||
| 656101a7b4 | |||
| 7c0f467cdf | |||
| 26b1cf400f | |||
| e359702837 | |||
| 67822d5ca8 | |||
| 346832b469 | |||
| a7174a894e | |||
| 3600df76cf | |||
| 9efa6eaf4f | |||
| 9542de2b0a | |||
| a0998fa255 | |||
| c8036d7e67 | |||
| 567fd8dd8c | |||
| 66211a5652 | |||
| 46705d48ff | |||
| e6dae3dba2 | |||
| 46d4900229 | |||
| 65b6043cde | |||
| 7a6fbcfc29 | |||
| 371fce1104 | |||
| 42d43a5bcc | |||
| a270f405a6 | |||
| a6a6a477d5 | |||
| b8bba3cd8f | |||
| 011ee7350d | |||
| 21b198f1ee | |||
| 6d36f7151c | |||
| edd4bd39e7 | |||
| ddd6bb06c2 | |||
| b97223f69e | |||
| f64418d5e1 | |||
| 2aed9d93c9 | |||
| cb99daa6cb | |||
| d7bc2b34ac | |||
| 734a7b201d | |||
| d4a1bbbc5b | |||
| a733db1029 | |||
| d5020a6415 | |||
| 0da31cbb65 | |||
| ff7025d912 | |||
| ad6a529ba4 | |||
| 60a0a5728f | |||
| 10b1ea7528 |
36
.analyze-glaztech.py
Normal file
36
.analyze-glaztech.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
with open('/Users/azcomputerguru/ClaudeTools/.glaztech-sessions.json') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
print(f'Total machines: {len(data)}')
|
||||||
|
|
||||||
|
# Extract IPs and current sites
|
||||||
|
subnet_info = defaultdict(lambda: {'count': 0, 'machines': [], 'current_sites': set()})
|
||||||
|
|
||||||
|
for session in data:
|
||||||
|
ip = session.get('GuestInfo', {}).get('PrivateNetworkAddress', '')
|
||||||
|
name = session.get('Name', 'Unknown')
|
||||||
|
current_site = session.get('CustomProperties', {}).get('CustomProperty2', '')
|
||||||
|
|
||||||
|
if ip and ip != '':
|
||||||
|
# Extract /24 subnet
|
||||||
|
subnet = '.'.join(ip.split('.')[:3]) + '.0/24'
|
||||||
|
subnet_info[subnet]['count'] += 1
|
||||||
|
subnet_info[subnet]['machines'].append({'name': name, 'ip': ip})
|
||||||
|
if current_site:
|
||||||
|
subnet_info[subnet]['current_sites'].add(current_site)
|
||||||
|
|
||||||
|
# Sort by count descending
|
||||||
|
sorted_subnets = sorted(subnet_info.items(), key=lambda x: x[1]['count'], reverse=True)
|
||||||
|
|
||||||
|
print('\n=== Subnet Distribution ===')
|
||||||
|
for subnet, info in sorted_subnets:
|
||||||
|
sites_str = ', '.join(sorted(info['current_sites'])) if info['current_sites'] else 'No site tag'
|
||||||
|
print(f'{subnet:20s} {info["count"]:3d} machines Current tags: {sites_str}')
|
||||||
|
# Show first 3 machines as examples
|
||||||
|
for m in info['machines'][:3]:
|
||||||
|
print(f' - {m["name"]:30s} {m["ip"]}')
|
||||||
|
if len(info['machines']) > 3:
|
||||||
|
print(f' ... and {len(info["machines"]) - 3} more')
|
||||||
10
.bk.ps1
Normal file
10
.bk.ps1
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
$b='C:\ProgramData\acg-backup'
|
||||||
|
'--- COMPLETE.txt ---'
|
||||||
|
if (Test-Path (Join-Path $b 'COMPLETE.txt')) { Get-Content (Join-Path $b 'COMPLETE.txt') } else { 'absent' }
|
||||||
|
'--- dir listing ---'
|
||||||
|
Get-ChildItem $b | Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize | Out-String
|
||||||
|
'--- rclone.log tail ---'
|
||||||
|
Get-Content (Join-Path $b 'rclone.log') -Tail 25
|
||||||
|
'--- rclone process ---'
|
||||||
|
$p = Get-Process rclone -ErrorAction SilentlyContinue
|
||||||
|
if ($p) { 'RUNNING pid=' + $p.Id } else { 'not running' }
|
||||||
7
.chk.ps1
Normal file
7
.chk.ps1
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Import-Module BitsTransfer
|
||||||
|
$j = Get-BitsTransfer -Name 'Win11ARM64ISO' -AllUsers -ErrorAction SilentlyContinue
|
||||||
|
if ($j) { 'state=' + $j.JobState + ' bytes=' + $j.BytesTransferred + '/' + $j.BytesTotal }
|
||||||
|
else { 'NO JOB' }
|
||||||
|
$f = Get-Item 'C:\Users\howar\Desktop\Win11_25H2_English_Arm64_v2.iso' -ErrorAction SilentlyContinue
|
||||||
|
if ($f) { 'file present size=' + $f.Length }
|
||||||
|
'uptime-min: ' + [math]::Round(((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalMinutes,0)
|
||||||
8
.chk2.ps1
Normal file
8
.chk2.ps1
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Import-Module BitsTransfer
|
||||||
|
'--- all BITS jobs (AllUsers) ---'
|
||||||
|
Get-BitsTransfer -AllUsers -ErrorAction SilentlyContinue | Select-Object DisplayName,JobState,BytesTransferred,BytesTotal,OwnerAccount | Format-Table -AutoSize | Out-String
|
||||||
|
'--- desktop files ---'
|
||||||
|
Get-ChildItem 'C:\Users\howar\Desktop' -Filter '*.iso*' -Force -ErrorAction SilentlyContinue | Select-Object Name,Length,LastWriteTime | Format-Table -AutoSize | Out-String
|
||||||
|
'--- tmp BITS file? ---'
|
||||||
|
Get-ChildItem 'C:\Users\howar\Desktop' -Filter 'BIT*' -Force -Hidden -ErrorAction SilentlyContinue | Select-Object Name,Length | Format-Table -AutoSize | Out-String
|
||||||
|
'uptime-min: ' + [math]::Round(((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalMinutes,0)
|
||||||
@@ -30,6 +30,12 @@ failures, production risk). Bump one tier for: security, auth, credential, migra
|
|||||||
production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
|
production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
|
||||||
|
|
||||||
## Key rules (always)
|
## Key rules (always)
|
||||||
|
- **Simplest solution FIRST.** Start with the least complex thing that could answer the
|
||||||
|
problem, confirm it works, and only THEN graduate to more advanced/complicated approaches
|
||||||
|
as the simple one proves insufficient. Don't reach for SSH/plink/multi-hop tooling when a
|
||||||
|
one-shot query (a single DNS lookup, one API call, one command) would answer it. E.g. "what
|
||||||
|
IP does the UDM think X is?" = `nslookup X <udm-ip>`, not shelling into the device. Escalate
|
||||||
|
deliberately, not by default.
|
||||||
- **NO EMOJIS.** Use ASCII markers: `[OK]` `[ERROR]` `[WARNING]` `[INFO]` `[CRITICAL]`.
|
- **NO EMOJIS.** Use ASCII markers: `[OK]` `[ERROR]` `[WARNING]` `[INFO]` `[CRITICAL]`.
|
||||||
- **Skill-first — if a skill/command covers the task, USE IT; never hand-roll the API.**
|
- **Skill-first — if a skill/command covers the task, USE IT; never hand-roll the API.**
|
||||||
When a request maps to an installed skill or slash-command, INVOKE THAT SKILL instead of
|
When a request maps to an installed skill or slash-command, INVOKE THAT SKILL instead of
|
||||||
@@ -38,7 +44,9 @@ production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
|
|||||||
records (e.g. Syncro tickets/invoices) reach a human for cleanup. **Syncro billing/invoicing
|
records (e.g. Syncro tickets/invoices) reach a human for cleanup. **Syncro billing/invoicing
|
||||||
ALWAYS runs through `/syncro` (or `/syncro-emergency-billing`) — no exceptions.** Same for
|
ALWAYS runs through `/syncro` (or `/syncro-emergency-billing`) — no exceptions.** Same for
|
||||||
other covered domains: credentials → `vault`, RMM actions → `/rmm` (+ `rmm-search` to find a
|
other covered domains: credentials → `vault`, RMM actions → `/rmm` (+ `rmm-search` to find a
|
||||||
host), M365 → `remediation-tool`, etc. Knowing the API is NOT a reason to bypass the skill —
|
host), M365 → `remediation-tool`, asking a human a question/decision in Discord (get their
|
||||||
|
reply back in-session) → `ask-forum` (never hand-roll the Discord API — that footgun cost us
|
||||||
|
a broken background wait), etc. Knowing the API is NOT a reason to bypass the skill —
|
||||||
the memory rules (e.g. [[feedback_syncro_billing]]) describe what the SKILL does, not a license
|
the memory rules (e.g. [[feedback_syncro_billing]]) describe what the SKILL does, not a license
|
||||||
to free-hand it. Reach for raw API ONLY when no skill fits or the skill genuinely cannot do it
|
to free-hand it. Reach for raw API ONLY when no skill fits or the skill genuinely cannot do it
|
||||||
— and say so explicitly when you do. Mistakes here go to `errorlog.md` (`--correction`).
|
— and say so explicitly when you do. Mistakes here go to `errorlog.md` (`--correction`).
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
| Run a command / investigate / script on an RMM agent | `/rmm` (find the host first with `rmm-search`) |
|
| Run a command / investigate / script on an RMM agent | `/rmm` (find the host first with `rmm-search`) |
|
||||||
| M365 investigation/remediation, breach check, mailbox/inbox-rule/forwarding audit, tenant sweep | `remediation-tool` |
|
| M365 investigation/remediation, breach check, mailbox/inbox-rule/forwarding audit, tenant sweep | `remediation-tool` |
|
||||||
| Onboard a new M365 tenant to the remediation app suite | `onboard365` |
|
| Onboard a new M365 tenant to the remediation app suite | `onboard365` |
|
||||||
| Backblaze / B2 storage, buckets, backup storage cost | `b2` |
|
| Anything about a customer's **BACKUP** — is X backing up, who backs up where, backup status/failures, or set up / provision a backup | **Backups → wiki-first** (see the "Backups" block right below this table) |
|
||||||
| Bitdefender / GravityZone AV — endpoints, sweeps, policies, quarantine, EDR | `bitdefender` |
|
| Bitdefender / GravityZone AV — endpoints, sweeps, policies, quarantine, EDR | `bitdefender` |
|
||||||
| Datto EDR / Datto AV — detections, isolate, scan, agent deploy | `datto-edr` |
|
| Datto EDR / Datto AV — detections, isolate, scan, agent deploy | `datto-edr` |
|
||||||
| VoIP — PacketDial / OITVOIP / NetSapiens domains, users, DIDs, queues, CDRs | `packetdial` |
|
| VoIP — PacketDial / OITVOIP / NetSapiens domains, users, DIDs, queues, CDRs | `packetdial` |
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
| UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` |
|
| UniFi WiFi tuning / RF / channel analysis | `unifi-wifi` |
|
||||||
| Map/repoint a Windows network drive on a remote endpoint | `drive-map` |
|
| Map/repoint a Windows network drive on a remote endpoint | `drive-map` |
|
||||||
| Send someone a Discord DM / copy-paste-friendly link or command | `discord-dm` |
|
| Send someone a Discord DM / copy-paste-friendly link or command | `discord-dm` |
|
||||||
|
| Ask a teammate a question/decision/sign-off and get their human answer back in-session | `ask-forum` (run the `--wait` as a proper background task — NO shell `&`) |
|
||||||
| Inter-session messaging, fleet todos, resource locks, coord status | `coord` |
|
| Inter-session messaging, fleet todos, resource locks, coord status | `coord` |
|
||||||
| Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) |
|
| Git / Gitea / submodules / sync health | `gitea` (or `/sync`, `/scc`, `/save` for session ops) |
|
||||||
| Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` |
|
| Build/verify GuruRMM agent/server/dashboard, pre-merge check | `gururmm-build` |
|
||||||
@@ -46,6 +47,31 @@
|
|||||||
|
|
||||||
> Not listed? If no skill fits, do it directly — and say so. New recurring need → `skill-creator`.
|
> Not listed? If no skill fits, do it directly — and say so. New recurring need → `skill-creator`.
|
||||||
|
|
||||||
|
### Backups (wiki-first routing)
|
||||||
|
|
||||||
|
A customer can back up through **any** of several backends, so a backup request is a
|
||||||
|
two-step route: **first find out which backend THIS customer uses, then use that skill.**
|
||||||
|
Never assume B2/MSP360 — several clients are on Seafile, ownCloud, or Datto Workplace.
|
||||||
|
|
||||||
|
1. **Identify the backend for this customer (don't guess).** Read the client wiki
|
||||||
|
(`wiki/clients/<slug>.md`) and the backup map (`projects/gps-rmm-audit/backup-map.md`) —
|
||||||
|
both record each customer's real destination. If it isn't recorded, confirm with
|
||||||
|
`msp360 monitoring --company <name>` plus the per-backend inventory skills, then write the
|
||||||
|
answer back to the client wiki.
|
||||||
|
2. **Route to that backend's skill:**
|
||||||
|
|
||||||
|
| Need | Skill |
|
||||||
|
|---|---|
|
||||||
|
| Is X backing up? status / last run / failures / stale plans (MSP360 + B2 Cloud plans) | `msp360` — **authoritative**, check here not bucket contents |
|
||||||
|
| Who backs up to Seafile / ownCloud / Datto Workplace (inventory + endpoint detection) | `seafile` / `owncloud` / `datto-workplace` |
|
||||||
|
| Storage / bucket / cost on the destination side | `b2` (Backblaze) |
|
||||||
|
| **Set up / provision a backup** — create account/user, quota, library, license, destination | the **target backend's** skill: `msp360` (MBS user/company/license), `seafile`, `owncloud` (all writes gated `--confirm`); `b2` for a new bucket/key |
|
||||||
|
|
||||||
|
- **Which machine gets backed up is a per-client decision** — report mismatches (billed vs
|
||||||
|
running), don't provision jobs off a billing count. See [[feedback_backup_targets_never_guessed]].
|
||||||
|
- Full per-customer destination + health map: `projects/gps-rmm-audit/backup-map.md`;
|
||||||
|
how backup-status works: memory `[[reference_msp360_backup_monitoring]]`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Check-skills (work-type → the gate run BEFORE "done")
|
## Check-skills (work-type → the gate run BEFORE "done")
|
||||||
|
|||||||
46
.claude/commands/ask-forum.md
Normal file
46
.claude/commands/ask-forum.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: ask-forum
|
||||||
|
description: Ask a teammate (e.g. Mike) a question in the private #ct-forum Discord forum and BLOCK until a human answers, then act on the reply — a live three-way between the user, this same Claude session, and the teammate. No second bot, no DMs, one tool call (the wait runs server-side in bash).
|
||||||
|
---
|
||||||
|
|
||||||
|
# /ask-forum — human-in-the-loop question via #ct-forum
|
||||||
|
|
||||||
|
Entry point to the `ask-forum` skill — full contract in `.claude/skills/ask-forum/SKILL.md`.
|
||||||
|
Engine: `.claude/scripts/ask-forum.sh`. Lets THIS session put a question
|
||||||
|
to a human in the private #ct-forum forum and get their answer back in-session, so it
|
||||||
|
can keep going. The forum post's thread id is the correlation key — the session reads
|
||||||
|
only the thread it created, so an answer is never confused with another question's.
|
||||||
|
|
||||||
|
Forum-only by design (Mike's call, 2026-07-08): DM replies are intentionally not used,
|
||||||
|
so answers stay visible to the team. The BEAST bot ignores #ct-forum, so the asking
|
||||||
|
session is the only agent that consumes replies there.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ask and block until a human replies (default timeout 600s, poll 8s):
|
||||||
|
bash .claude/scripts/ask-forum.sh "your question" --tag @264814939619721216
|
||||||
|
|
||||||
|
# Re-read a thread you already asked in (one-shot, no waiting):
|
||||||
|
bash .claude/scripts/ask-forum.sh --read <thread_id>
|
||||||
|
|
||||||
|
# Resume blocking on a thread already posted (e.g. after a timeout):
|
||||||
|
bash .claude/scripts/ask-forum.sh --wait <thread_id> --timeout 1800 --poll 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Flags: `--title "short title"` (forum post name; defaults to first 60 chars of the
|
||||||
|
question), `--timeout SEC`, `--poll SEC`, `--tag @<discord_id>` (repeatable — pings
|
||||||
|
that person; ids in `.claude/users.json`: mike 264814939619721216,
|
||||||
|
howard 624667664501178379, winter 624666486362996755, rob 261978810713505792).
|
||||||
|
|
||||||
|
## How to run it well
|
||||||
|
|
||||||
|
- **Long waits → background it** with `run_in_background: true` (NO shell `&` — that
|
||||||
|
forks the wait off and exits early). You get the answer when the human replies.
|
||||||
|
- Only **non-bot** replies count as answers, so bot chatter can never be mistaken for
|
||||||
|
the human's answer.
|
||||||
|
- Exit codes: 0 answered · 2 no token · 3 Discord API error · 4 timeout (thread stays
|
||||||
|
open — resume with `--wait <thread_id>`).
|
||||||
|
|
||||||
|
Channel: #ct-forum (`1522960388432465950`), private — @everyone denied; Howard, Mike,
|
||||||
|
and the bot allowed. Add someone via a channel permission overwrite if they need access.
|
||||||
@@ -23,7 +23,7 @@ Follow these steps exactly when /inject-standards is invoked:
|
|||||||
|
|
||||||
### Step 2 — Auto-select relevant standards
|
### Step 2 — Auto-select relevant standards
|
||||||
|
|
||||||
1. Read `D:/claudetools/.claude/standards/index.yml`.
|
1. Read `.claude/standards/index.yml`.
|
||||||
2. Review the descriptions for all entries.
|
2. Review the descriptions for all entries.
|
||||||
3. Select the 2–5 standards most relevant to either:
|
3. Select the 2–5 standards most relevant to either:
|
||||||
- The task description in $ARGUMENTS, or
|
- The task description in $ARGUMENTS, or
|
||||||
@@ -35,7 +35,7 @@ Follow these steps exactly when /inject-standards is invoked:
|
|||||||
|
|
||||||
For each selected standard (in order of relevance):
|
For each selected standard (in order of relevance):
|
||||||
|
|
||||||
1. Read the file at `D:/claudetools/.claude/standards/<slug>.md`.
|
1. Read the file at `.claude/standards/<slug>.md`.
|
||||||
2. Display a header:
|
2. Display a header:
|
||||||
```
|
```
|
||||||
=== STANDARD: <slug> ===
|
=== STANDARD: <slug> ===
|
||||||
@@ -87,8 +87,8 @@ Reads the recent conversation, infers the task type, selects 2–5 most relevant
|
|||||||
|
|
||||||
## Standards index location
|
## Standards index location
|
||||||
|
|
||||||
`D:/claudetools/.claude/standards/index.yml`
|
`.claude/standards/index.yml`
|
||||||
|
|
||||||
## Standards files location
|
## Standards files location
|
||||||
|
|
||||||
`D:/claudetools/.claude/standards/<folder>/<name>.md`
|
`.claude/standards/<folder>/<name>.md`
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
|
|||||||
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
|
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
|
||||||
/syncro schedules <customer> List recurring invoice schedules for a customer
|
/syncro schedules <customer> List recurring invoice schedules for a customer
|
||||||
/syncro schedule <id> View a schedule's template and line items
|
/syncro schedule <id> View a schedule's template and line items
|
||||||
|
/syncro assets <customer> List a customer's RMM assets (read)
|
||||||
|
/syncro asset <id> Asset detail + patches + installed applications
|
||||||
|
/syncro asset-update <id> Rename / fix serial / edit an asset (preview-gated)
|
||||||
|
/syncro move-asset <id> <folder> Move asset to a policy folder (needs token scope - see below)
|
||||||
|
/syncro rmm-alerts [<customer>] List live RMM alerts; mute/clear by id
|
||||||
|
/syncro deploy-agent <customer> <host> Push the Syncro RMM agent to a machine via GuruRMM
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Configuration
|
## API Configuration
|
||||||
@@ -26,6 +32,10 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
|
|||||||
**API Key:** per-user tokens in SOPS vault — see "Get API key" below
|
**API Key:** per-user tokens in SOPS vault — see "Get API key" below
|
||||||
**Rate limit:** 180 requests/minute per IP
|
**Rate limit:** 180 requests/minute per IP
|
||||||
**Docs:** https://api-docs.syncromsp.com/
|
**Docs:** https://api-docs.syncromsp.com/
|
||||||
|
**Full endpoint catalog:** `.claude/standards/syncro/api-reference.md` — every Syncro call
|
||||||
|
(~180 endpoints, 38 resource types), verified against the live spec + tenant 2026-07-10.
|
||||||
|
This file documents the core PSA + asset/RMM/agent-deploy workflows; the reference covers
|
||||||
|
the rest (contacts, contracts, leads, vendors, POs, products, payments, wiki, policy folders).
|
||||||
|
|
||||||
## Hard Rules (violations have occurred — no exceptions)
|
## Hard Rules (violations have occurred — no exceptions)
|
||||||
|
|
||||||
@@ -96,53 +106,39 @@ Every Syncro API call is attributed to the **owner of the API key**. Comments, l
|
|||||||
- **Correcting mis-billed labor** (a debug action) must keep the ORIGINAL tech's `user_id` (their commission). `update_line_item` preserves the existing `user_id`; a remove+add defaults the new line to the API-key owner — so set `user_id` to the original tech on `add_line_item`, or PUT it afterward. Determine the original tech from `.ticket.user_id` and the line's `.user_id`. Don't take a tech's commission just because the math was fixed by someone else.
|
- **Correcting mis-billed labor** (a debug action) must keep the ORIGINAL tech's `user_id` (their commission). `update_line_item` preserves the existing `user_id`; a remove+add defaults the new line to the API-key owner — so set `user_id` to the original tech on `add_line_item`, or PUT it afterward. Determine the original tech from `.ticket.user_id` and the line's `.user_id`. Don't take a tech's commission just because the math was fixed by someone else.
|
||||||
- **Ticket ownership:** adding notes/labor or changing status does NOT change the ticket owner. Multiple techs routinely work one ticket. Only PUT a ticket's `user_id` (reassign owner) when explicitly asked; status PUTs send only `status`.
|
- **Ticket ownership:** adding notes/labor or changing status does NOT change the ticket owner. Multiple techs routinely work one ticket. Only PUT a ticket's `user_id` (reassign owner) when explicitly asked; status PUTs send only `status`.
|
||||||
|
|
||||||
| identity.json user | Syncro user | user_id |
|
| identity.json user | Syncro user | user_id | vault path |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `mike` | Michael Swanson | 1735 |
|
| `mike` | Michael Swanson | 1735 | `msp-tools/syncro` |
|
||||||
| `howard` | Howard Enos | 1750 |
|
| `howard` | Howard Enos | 1750 | `msp-tools/syncro-howard` |
|
||||||
|
| `winter` | Winter Williams | 1737 | `msp-tools/syncro-winter` |
|
||||||
|
|
||||||
Keys are baked into the skill below. To add a new user: generate a token in Syncro → Admin → API Tokens, add a case to the key-select block, and store a backup copy in the vault at `msp-tools/syncro-<user>.sops.yaml`.
|
Keys live in the SOPS vault and are resolved by `.claude/scripts/syncro-env.sh` (source it — never hardcode a key). When `CLAUDETOOLS_REQUESTER_USER` is set (Discord bot threads), syncro-env.sh prefers the REQUESTER's key if one is vaulted, so bot-driven Syncro actions attribute to the person who asked; it falls back to the identity.json user otherwise. To add a new user: generate a token in Syncro → Admin → API Tokens, vault it at `msp-tools/syncro-<user>.sops.yaml`, and add a case to `_syncro_key_path()` in syncro-env.sh.
|
||||||
|
|
||||||
### Get API key
|
### Get API key
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
# Resolves CLAUDETOOLS_ROOT + VAULT_ROOT from identity.json and reads the caller's
|
||||||
|
# per-user Syncro key from the SOPS vault. Never hardcode a repo path or an API key.
|
||||||
|
#
|
||||||
|
# The key is per-user on purpose: every action in Syncro is attributed to the key
|
||||||
|
# owner, so using someone else's key misattributes tickets, time, and invoices.
|
||||||
|
#
|
||||||
|
# NOTE: identity.json lives in the REPO (.claude/identity.json), not ~/.claude/.
|
||||||
|
# Reading ~/.claude/identity.json first is a known bug — it silently picks up a
|
||||||
|
# stale or absent file. syncro-env.sh resolves the repo copy from its own location.
|
||||||
|
source "$(git rev-parse --show-toplevel)/.claude/scripts/syncro-env.sh" || {
|
||||||
|
echo "[ERROR] Cannot resolve Syncro credentials — run onboarding / check the vault" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# Get repo root from identity.json (set during machine onboarding)
|
BASE="$SYNCRO_BASE"
|
||||||
# Fallback to dynamic detection for legacy machines that haven't updated identity.json yet
|
API_KEY="$SYNCRO_API_KEY"
|
||||||
IDENTITY_PATH="${HOME}/.claude/identity.json"
|
REPO_ROOT="$CLAUDETOOLS_ROOT"
|
||||||
if [ ! -f "$IDENTITY_PATH" ]; then
|
|
||||||
# Try in-repo identity.json (gitignored, machine-specific)
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
||||||
if [ -n "$REPO_ROOT" ]; then
|
|
||||||
IDENTITY_PATH="$REPO_ROOT/.claude/identity.json"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$IDENTITY_PATH" ]; then
|
if [ -z "$API_KEY" ]; then
|
||||||
echo "[ERROR] Cannot locate identity.json - run onboarding first" >&2
|
echo "[ERROR] No Syncro API key for user '${SYNCRO_USER:-unknown}'" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPO_ROOT=$(jq -r '.claudetools_root // empty' "$IDENTITY_PATH")
|
|
||||||
if [ -z "$REPO_ROOT" ]; then
|
|
||||||
# Legacy fallback for machines without claudetools_root in identity.json
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
||||||
if [ -z "$REPO_ROOT" ]; then
|
|
||||||
echo "[ERROR] claudetools_root not set in identity.json and not in a git directory" >&2
|
|
||||||
echo "[ERROR] Add 'claudetools_root' field to $IDENTITY_PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "[WARNING] Using git-detected repo root. Add 'claudetools_root' to identity.json to avoid this." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Per-user keys — actions in Syncro are attributed to the key owner
|
|
||||||
USER_ID=$(jq -r '.user // empty' "$IDENTITY_PATH")
|
|
||||||
case "$USER_ID" in
|
|
||||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
|
||||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
|
||||||
*) echo "[ERROR] Unknown user '$USER_ID' in identity.json — cannot select Syncro API key" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ollama drafting
|
### Ollama drafting
|
||||||
@@ -613,6 +609,157 @@ COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id')
|
|||||||
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
|
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
|
||||||
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
|
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
|
||||||
|
|
||||||
|
#### Customer Assets (RMM) — read intelligence + write management
|
||||||
|
|
||||||
|
Assets are Syncro's RMM device records. Reads are free; writes follow the standard
|
||||||
|
preview+confirm gate. Response shape is `{"asset": {...}}` (single) / `{"assets": [...]}`
|
||||||
|
(list). RMM telemetry lives under `.asset.properties.kabuto_information`.
|
||||||
|
|
||||||
|
**Reads (verified with per-user token 2026-07-10):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List / search a customer's assets (100/page, .assets[])
|
||||||
|
curl -s "${BASE}/customer_assets?customer_id=${CUST_ID}&per_page=100&api_key=${API_KEY}" | \
|
||||||
|
tr -d '\000-\037' | jq -r '.assets[] | "\(.id)\t\(.asset_type_name)\t\(.name)"'
|
||||||
|
|
||||||
|
# Asset detail
|
||||||
|
curl -s "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" | tr -d '\000-\037' | \
|
||||||
|
jq '{id:.asset.id, name:.asset.name, customer_id:.asset.customer_id, policy_folder_id:.asset.policy_folder_id, serial:.asset.asset_serial}'
|
||||||
|
|
||||||
|
# Windows patch posture -> {available_patches, available_patches_meta, installed_patches}
|
||||||
|
curl -s "${BASE}/customer_assets/${ASSET_ID}/patches?api_key=${API_KEY}" | tr -d '\000-\037' | \
|
||||||
|
jq '{available: (.available_patches|length), installed: (.installed_patches|length)}'
|
||||||
|
|
||||||
|
# Installed software inventory (paginated) -> {installed_applications, meta}
|
||||||
|
# Use this to VERIFY an install (e.g. after /syncro deploy-agent) — see Agent Deployment.
|
||||||
|
curl -s "${BASE}/customer_assets/${ASSET_ID}/installed_applications?per_page=100&api_key=${API_KEY}" | \
|
||||||
|
tr -d '\000-\037' | jq -r '.installed_applications[]?.name' | sort -u
|
||||||
|
```
|
||||||
|
|
||||||
|
`updated_at` is NOT a reliable "last online" (bulk account touches move it) — use the
|
||||||
|
ScreenConnect `GuestInfoUpdateTime` or kabuto data for real last-seen.
|
||||||
|
|
||||||
|
**Writes — `POST /customer_assets` (create), `PUT /customer_assets/{id}` (update).**
|
||||||
|
`name` is **required** on both (422 without it). Accepted body fields: `name`,
|
||||||
|
`asset_serial`, `asset_type_id`, `asset_type_name`, `customer_id`, `properties`. Preview the
|
||||||
|
full payload and confirm before writing; post a bot alert after. Response `{"asset": {...}}`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update (rename / fix serial). Always resend name (required) even when only changing serial.
|
||||||
|
curl -s -X PUT "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" \
|
||||||
|
-H "Content-Type: application/json" --data-binary @- <<JSON
|
||||||
|
{"name": "${NEW_NAME}", "asset_serial": "${NEW_SERIAL}"}
|
||||||
|
JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
**Move to a policy folder — `PUT /customer_assets/{id}` with `policy_folder_id` — WORKS, but
|
||||||
|
is TOKEN-SCOPE-GATED and MUST be capability-checked + verified.** Live-proven 2026-07-10 via
|
||||||
|
flip-and-restore on ACG-internal asset `12335235` (`692253 → 692278 → 692253`) with Howard's
|
||||||
|
full-scope token. The spec: *"Updating only `policy_folder_id` requires **Assets - Policy
|
||||||
|
Change**; with other fields requires **Assets - Edit** AND **Assets - Policy Change**. Nil,
|
||||||
|
nonexistent, or cross-customer folder IDs return 422."* `name` is required on the PUT, so a
|
||||||
|
move always sends other fields → needs BOTH scopes.
|
||||||
|
|
||||||
|
**The failure mode is SILENT, so never trust the 200.** A token WITHOUT Policy-Change returns
|
||||||
|
`HTTP 200` and simply does not change the folder (this is the symptom once misread as "RMM is
|
||||||
|
GUI-only"). `/me` reports coarse `asset:{write:true}` even without Policy-Change, so it cannot
|
||||||
|
detect this — you must probe and verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
move_asset() { # $1=asset_id $2=target_folder_id (needs $BASE $API_KEY)
|
||||||
|
local AID="$1" TARGET="$2" NAME CUST CUR
|
||||||
|
local A; A=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037')
|
||||||
|
NAME=$(echo "$A" | jq -r '.asset.name'); CUST=$(echo "$A" | jq -r '.asset.customer_id')
|
||||||
|
CUR=$(echo "$A" | jq -r '.asset.policy_folder_id')
|
||||||
|
# 1) CAPABILITY PREFLIGHT — 200 = token can move; 401 = lacks Assets - Policy Change
|
||||||
|
local PC; PC=$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/policy_folders?customer_id=${CUST}&api_key=${API_KEY}")
|
||||||
|
if [ "$PC" = "401" ]; then
|
||||||
|
echo "[ERROR] This token lacks 'Assets - Policy Change' — cannot move policy folders."
|
||||||
|
echo " Options: move it in the Syncro web console, OR have a user whose token has"
|
||||||
|
echo " the scope run it (do NOT silently swap keys — writes attribute to the owner)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# 2) PUT (name required), then 3) VERIFY the value actually changed — a 200 no-op = failure
|
||||||
|
curl -s -X PUT "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" -H "Content-Type: application/json" \
|
||||||
|
--data-binary "$(jq -nc --arg n "$NAME" --argjson pf "$TARGET" '{name:$n, policy_folder_id:$pf}')" >/dev/null
|
||||||
|
local NEW; NEW=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037' | jq -r '.asset.policy_folder_id')
|
||||||
|
if [ "$NEW" = "$TARGET" ]; then echo "[OK] asset ${AID} moved ${CUR} -> ${NEW}"; else
|
||||||
|
echo "[ERROR] move NOT applied (still ${NEW}) — token likely under-scoped; do it in the console."; return 1; fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Policy-folder route quirks (verified 2026-07-10):** `GET /policy_folders` (bare) → **404**;
|
||||||
|
`GET /policy_folders?customer_id=N` → **200** (list a customer's folders — how you enumerate
|
||||||
|
folder IDs); `GET /policy_folders/{id}` → **401** even with full scope. Find valid same-customer
|
||||||
|
folder IDs from the `?customer_id=N` list or from other assets' `policy_folder_id` values.
|
||||||
|
|
||||||
|
**Fleet posture:** Howard's per-user token now has full scope (verified). Other per-user tokens
|
||||||
|
may not — the preflight above handles that per-run. Recommend granting every per-user Syncro
|
||||||
|
token the Policy-Change scope so `move-asset` behaves the same regardless of who runs it (Mike's
|
||||||
|
call — least-privilege alternative is to leave the preflight to degrade gracefully). Corrects
|
||||||
|
memory `reference-syncro-rmm-api-gui-only`; full detail in `.claude/standards/syncro/api-reference.md`.
|
||||||
|
|
||||||
|
**Archive (retire) is still GUI-only — no REST API.** Verified 2026-07-08: archiving is web
|
||||||
|
UI only (asset Details → **Actions → Archive**, or Assets table → **Bulk Actions →
|
||||||
|
Archive**). There is no archive endpoint and no `archived` field on the PUT. Do NOT substitute
|
||||||
|
`DELETE /customer_assets/{id}` — delete is destructive/irreversible, loses history, and breaks
|
||||||
|
ticket linkages (Archive keeps the asset on its tickets/alerts). To retire: hand the user the
|
||||||
|
asset list + the Bulk-Actions-→-Archive steps.
|
||||||
|
|
||||||
|
#### RMM Alerts
|
||||||
|
|
||||||
|
Live RMM alert feed (`GET /rmm_alerts` -> `{rmm_alerts, meta}`, verified 2026-07-10). Reads
|
||||||
|
free; `POST /rmm_alerts/{id}/mute` and `DELETE /rmm_alerts/{id}` (clear) are gated writes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "${BASE}/rmm_alerts?per_page=50&api_key=${API_KEY}" | tr -d '\000-\037' | \
|
||||||
|
jq -r '.rmm_alerts[] | "\(.id)\t\(.created_at)\t\(.description // .kind)"'
|
||||||
|
# Mute: POST /rmm_alerts/${ALERT_ID}/mute | Clear: DELETE /rmm_alerts/${ALERT_ID}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Agent Deployment (`/syncro deploy-agent`) — hybrid via GuruRMM
|
||||||
|
|
||||||
|
Push the Syncro RMM agent to a machine. Syncro's API has **no** installer-download endpoint,
|
||||||
|
so this is a **hybrid**: the per-policy-folder installer (which embeds the customer enrollment
|
||||||
|
token) is obtained once from the Syncro GUI and vaulted; **GuruRMM (`/rmm`)** does the actual
|
||||||
|
push (it runs PowerShell as SYSTEM on the endpoint). `/rmm diagnose` already *detects* a
|
||||||
|
Syncro agent as a competitor-RMM check — this is the deploy half.
|
||||||
|
|
||||||
|
**Prerequisites (one-time per customer):**
|
||||||
|
1. In Syncro: **Assets & RMM → Downloads** (or the target Policy Folder) → copy the
|
||||||
|
`SyncroSetup-<company>-<token>.exe` **download URL**. It is per-policy-folder — the token
|
||||||
|
binds the installed agent to that customer + policy. Do NOT reuse one customer's URL for
|
||||||
|
another.
|
||||||
|
2. Vault it via the `vault` skill at `clients/<slug>/syncro-agent-installer.sops.yaml`
|
||||||
|
(`credentials.installer_url`). Never hardcode the token URL.
|
||||||
|
|
||||||
|
**Deploy workflow:**
|
||||||
|
1. Resolve the target host in GuruRMM (`/rmm` — resolve hostname → UUID; confirm `os_type`
|
||||||
|
is `windows` and it is `is_connected`). Confirm the customer↔host mapping with the user.
|
||||||
|
2. Preview the exact install command + target, get explicit confirmation.
|
||||||
|
3. Dispatch via GuruRMM (`command_type: powershell`, SYSTEM context). The agent installs
|
||||||
|
**silently by default**; `--console` runs it non-interactively and `--allow-force-reboot`
|
||||||
|
permits the reboot Syncro triggers if it must update .NET first:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$url = '<vaulted SyncroSetup URL>'
|
||||||
|
$exe = "$env:TEMP\SyncroSetup.exe"
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing
|
||||||
|
Start-Process -FilePath $exe -ArgumentList '--console','--allow-force-reboot' -Wait
|
||||||
|
```
|
||||||
|
(Refs: Syncro deploy docs `docs.syncrosecure.com/agents-alerts-automations/deploy-an-endpoint`;
|
||||||
|
community "Command Line install".) `--allow-force-reboot` CAN reboot the endpoint — treat
|
||||||
|
deploy-agent as an outward-facing action: confirm reboot tolerance with the user first.
|
||||||
|
4. **Verify** the agent enrolled: after ~2-3 min, either re-query GuruRMM output, or confirm
|
||||||
|
the new asset appears under the customer via `GET /customer_assets?customer_id=N&query=<host>`
|
||||||
|
and/or that "Syncro" shows in the endpoint's `installed_applications`.
|
||||||
|
5. Bot alert (RMM write → `[RMM]`/`[DEPLOY]` routes to #dev-alerts; Syncro-side note to
|
||||||
|
#bot-alerts if an asset record was created).
|
||||||
|
|
||||||
|
**[VERIFY before first production use]** Confirm `--console --allow-force-reboot` against the
|
||||||
|
current Syncro installer on one ACG-internal test machine before rolling to a customer — the
|
||||||
|
switches are stable historically but the installer is versioned.
|
||||||
|
|
||||||
#### Customers
|
#### Customers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
55
.claude/commands/tailscale.md
Normal file
55
.claude/commands/tailscale.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: tailscale
|
||||||
|
description: Manage an ACG-administered Tailscale tailnet via the REST API v2 - list/inspect devices, delete a node, authorize a device, create/list/revoke tagged pre-auth keys. Reads free; writes gated --confirm.
|
||||||
|
---
|
||||||
|
|
||||||
|
# /tailscale — Tailscale tailnet management (REST API v2)
|
||||||
|
|
||||||
|
Thin entry point to the `tailscale` skill. Engine:
|
||||||
|
`.claude/skills/tailscale/scripts/tailscale-api.sh`. Full reference:
|
||||||
|
`.claude/skills/tailscale/SKILL.md`. Doctrine (per-client tailnets, tagged pre-auth
|
||||||
|
keys, offboarding): `wiki/patterns/tailscale-client-management.md`.
|
||||||
|
|
||||||
|
Read-only by default; every device delete/authorize and auth-key create/delete is
|
||||||
|
**gated behind `--confirm`** and previews first. ADMIN rights: add + delete.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TS="bash .claude/skills/tailscale/scripts/tailscale-api.sh"
|
||||||
|
|
||||||
|
# Reads (no confirm)
|
||||||
|
$TS status # auth check + tailnet + device count
|
||||||
|
$TS devices [--json] # list devices
|
||||||
|
$TS device <name|id|100.x> [--json] # device detail
|
||||||
|
$TS keys [--json] # list auth keys
|
||||||
|
|
||||||
|
# Writes (GATED — preview without --confirm, act with it)
|
||||||
|
$TS create-key --tag tag:<client> --reusable --preauth [--ephemeral] \
|
||||||
|
[--expiry-days N] [--desc "..."] --confirm
|
||||||
|
$TS delete-key <keyId> --confirm # revoke an auth key
|
||||||
|
$TS authorize <name|id|100.x> --confirm # approve a device
|
||||||
|
$TS delete-device <name|id|100.x> --confirm # remove a node
|
||||||
|
|
||||||
|
# Point at a specific per-client tailnet's vault entry
|
||||||
|
$TS devices --vault tailscale/roberts.sops.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Vault-first auth.** Credentials come from `tailscale/api-access.sops.yaml` (OAuth
|
||||||
|
client preferred, `credentials.api_key` token fallback); tailnet defaults to `-`. Never
|
||||||
|
hardcode a token. Provisioning commands are in the skill doc.
|
||||||
|
2. **Read before write.** Device ops resolve a name/100.x to the stable id first; an
|
||||||
|
**ambiguous match STOPS** (lists candidates, does nothing) — pass a specific id.
|
||||||
|
3. **Gated writes.** No `--confirm` = preview + exit, no change. Deleting a node drops it
|
||||||
|
off the tailnet; revoking a key blocks future enrollments with it.
|
||||||
|
4. **Per-client isolation.** One tailnet per client, one vault entry per tailnet. Confirm
|
||||||
|
the `--vault`/tailnet before any write.
|
||||||
|
5. **Key secrets → vault, never chat/commit.** `create-key` prints the secret once; store
|
||||||
|
it via `/vault` immediately.
|
||||||
|
6. **Bot alert after every write.** Successful writes auto-post a `[TAILSCALE] ...` line to
|
||||||
|
Discord (soft-fail); reads do not.
|
||||||
|
|
||||||
|
Use this to *drive* the tailnet; use `/rmm` to push the tagged pre-auth key onto client
|
||||||
|
machines (`tailscale-client-enroll.ps1`) once a key is minted.
|
||||||
@@ -36,17 +36,18 @@ want a periodic clean rebuild. For "I just did some work, capture it," use plain
|
|||||||
## Phase 0 — Setup
|
## Phase 0 — Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CLAUDETOOLS_ROOT="D:/claudetools"
|
# Resolves CLAUDETOOLS_ROOT + VAULT_ROOT from identity.json and reads the caller's
|
||||||
VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh"
|
# per-user Syncro key from the SOPS vault. Never hardcode a repo path or an API key.
|
||||||
|
# Exports: CLAUDETOOLS_ROOT, VAULT_ROOT, SYNCRO_USER, SYNCRO_BASE, SYNCRO_API_KEY.
|
||||||
|
source "$(git rev-parse --show-toplevel)/.claude/scripts/syncro-env.sh" || true
|
||||||
|
|
||||||
# Syncro auth (read-only operations only — GET requests ONLY in this skill)
|
VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh"
|
||||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
BASE="$SYNCRO_BASE" # read-only: GET requests ONLY in this skill
|
||||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
API_KEY="$SYNCRO_API_KEY" # empty if no key vaulted for this user -> skip enrichment
|
||||||
case "$USER_ID" in
|
|
||||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
if [ -z "$API_KEY" ]; then
|
||||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
echo "[WARNING] No Syncro key for user '${SYNCRO_USER:-unknown}' — Syncro enrichment skipped"
|
||||||
*) echo "[WARNING] Unknown user — Syncro enrichment skipped" ; API_KEY="" ;;
|
fi
|
||||||
esac
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Synthesis engine:** seed/full article drafting is done by a **Sonnet subagent** (Agent tool, `model: "sonnet"`), not Ollama. The main agent gathers sources + Syncro data, delegates the draft, then reviews it before writing. No Ollama dependency.
|
**Synthesis engine:** seed/full article drafting is done by a **Sonnet subagent** (Agent tool, `model: "sonnet"`), not Ollama. The main agent gathers sources + Syncro data, delegates the draft, then reviews it before writing. No Ollama dependency.
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ Scan for clients and projects that have session logs but no wiki article.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all client slugs that have session-logs but no wiki article
|
# List all client slugs that have session-logs but no wiki article
|
||||||
cd D:/claudetools
|
# Repo root is machine-specific (C:/ on some, D:/ on others) — never hardcode it.
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
for dir in clients/*/session-logs; do
|
for dir in clients/*/session-logs; do
|
||||||
slug=$(echo "$dir" | sed 's|clients/||;s|/session-logs||')
|
slug=$(echo "$dir" | sed 's|clients/||;s|/session-logs||')
|
||||||
wiki="wiki/clients/$slug.md"
|
wiki="wiki/clients/$slug.md"
|
||||||
@@ -96,13 +97,17 @@ For every client wiki article that contains a `Syncro customer ID` line, pull li
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
BASE="https://computerguru.syncromsp.com/api/v1"
|
# Resolves CLAUDETOOLS_ROOT + VAULT_ROOT from identity.json and reads the caller's
|
||||||
USER_ID=$(jq -r '.user // empty' "$CLAUDETOOLS_ROOT/.claude/identity.json")
|
# per-user Syncro key from the SOPS vault. Never hardcode a repo path or an API key.
|
||||||
case "$USER_ID" in
|
source "$(git rev-parse --show-toplevel)/.claude/scripts/syncro-env.sh" || true
|
||||||
mike) API_KEY="T259810e5c9917386b-52c2aeea7cdb5ff41c6685a73cebbeb3" ;;
|
|
||||||
howard) API_KEY="Tde5174a6e9e312d14-02fd5bfe0f0ee40c87d027507c680e18" ;;
|
BASE="$SYNCRO_BASE"
|
||||||
*) echo "[SYNCRO] No API key for user '$USER_ID' — skipping Step 6" ; exit 0 ;;
|
API_KEY="$SYNCRO_API_KEY"
|
||||||
esac
|
|
||||||
|
if [ -z "$API_KEY" ]; then
|
||||||
|
echo "[SYNCRO] No API key for user '${SYNCRO_USER:-unknown}' — skipping Step 6"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
### For Each Client Article
|
### For Each Client Article
|
||||||
|
|||||||
@@ -1,213 +1,230 @@
|
|||||||
# Memory Index
|
# Memory Index
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
- [ACG resource map](reference_resource_map.md) — **READ THIS FIRST** when a task references a server/service/tenant/API. What we have access to, how to connect from this machine, per-machine exceptions, gotchas. Points at the detail files below.
|
- [ACG resource map](reference_resource_map.md) — **READ THIS FIRST** when a task references a server/service/tenant/API. What we have access to, how to connect from this machine, per-machine exceptions, gotchas. Points at the detail files below.
|
||||||
- [ALIS (Medtelligent)](reference_alis_medtelligent.md) — Cascades assisted-living EHR. API host api.alisonline.com, community 622; username must be tenant-qualified (howard.enos@cascadestucson). Staff are READ-ONLY via API — create/change staff via web-UI Staff Import .xls. Use the `alis` skill.
|
- [MSP360 backup monitoring](reference_msp360_backup_monitoring.md) — Authoritative "is client X actually backing up / last run" source for the whole fleet (NOT B2 bucket contents). MSP360 MBS API `/api/Monitoring`, vault `msp-tools/msp360-api.sops.yaml`. CompanyName also IDs B2 buckets (ACG-PST=Peaceful Spirit, ACG-TCA=Tucson Coin).
|
||||||
- [GuruRMM User Manager](reference_gururmm_user_manager.md) — GuruRMM has a built-in per-agent User Manager tab (reset_password/enable/disable/groups for local+domain+AAD endpoint users; domain users only on a DC via `is_dc`). Use it, NOT raw Set-ADAccountPassword via /rmm. Endpoints: /api/agents/{id}/users + /users/action.
|
- [Backup checks only for billed clients](feedback_backup_check_only_billed.md) — Don't scan backup status for clients with no billed Data Backup line (Syncro schedule qty=0); verify billed clients only. Non-billed-but-backing-up = revenue-leak question for Mike/Winter, not a scan target.
|
||||||
- [RMM map network drive (err67 double-hop)](reference_rmm_map_network_drive.md) — Pushing a persistent mapped drive to a remote share via /rmm user_session fails with err67/1702 (impersonated token = no network cred/double-hop). Plant HKCU:\Network\<drv> keys + cmdkey; mounts at next interactive logon. Immediate visibility needs the live session (ScreenConnect).
|
- [ALIS (Medtelligent)](reference_alis_medtelligent.md) — Cascades assisted-living EHR. API host api.alisonline.com, community 622; username must be tenant-qualified (howard.enos@cascadestucson). Staff are READ-ONLY via API — create/change staff via web-UI Staff Import .xls. Use the `alis` skill.
|
||||||
- [exchange-op = all-access Exchange tier](feedback_exchange_op_all_access.md) — STOP claiming "no tier can write mail." Exchange Operator app = Exchange Admin role + full_access_as_app + Exchange.ManageAsApp = full all-access (move mail, rules, config, EWS). Default to `exchange-op` for any Exchange write.
|
- [GuruRMM User Manager](reference_gururmm_user_manager.md) — GuruRMM has a built-in per-agent User Manager tab (reset_password/enable/disable/groups for local+domain+AAD endpoint users; domain users only on a DC via `is_dc`). Use it, NOT raw Set-ADAccountPassword via /rmm. Endpoints: /api/agents/{id}/users + /users/action.
|
||||||
- [Tedards tenant facts](reference_tedards_tenant_facts.md) — Bill Tedards law office; tenant `4fcbb1f4…`; bt@/y226@ mailboxes; matter-number filing; UAL ingestion OFF; 9 synced devices; botched-import DUPLICATE folder.
|
- [RMM map network drive (err67 double-hop)](reference_rmm_map_network_drive.md) — Pushing a persistent mapped drive to a remote share via /rmm user_session fails with err67/1702 (impersonated token = no network cred/double-hop). Plant HKCU:\Network\<drv> keys + cmdkey; mounts at next interactive logon. Immediate visibility needs the live session (ScreenConnect).
|
||||||
- [Investigator EXO ManageAsApp gap](reference_investigator_exo_manageasapp_gap.md) — Security Investigator app lacks `Exchange.ManageAsApp` (only `full_access_as_app`) so `investigator-exo` 401s on EXO adminapi; use `exchange-op` tier for InvokeCommand.
|
- [exchange-op = all-access Exchange tier](feedback_exchange_op_all_access.md) — STOP claiming "no tier can write mail." Exchange Operator app = Exchange Admin role + full_access_as_app + Exchange.ManageAsApp = full all-access (move mail, rules, config, EWS). Default to `exchange-op` for any Exchange write.
|
||||||
- [Tailscale subnet-route key expiry](reference_tailscale_subnet_key_expiry.md) — "internet OK but all of 172.16.3.x (Gitea .20, RMM/coord .30) dead" = Tailscale infra-node KEY EXPIRY (pfSense subnet router advertises 172.16.0.0/22), NOT a LAN outage; expiry now disabled on infra nodes (2026-06-25). Fallback: gururmm-server direct at tailnet 100.86.12.15:3001.
|
- [Tedards tenant facts](reference_tedards_tenant_facts.md) — Bill Tedards law office; tenant `4fcbb1f4…`; bt@/y226@ mailboxes; matter-number filing; UAL ingestion OFF; 9 synced devices; botched-import DUPLICATE folder.
|
||||||
- [GravityZone support center](reference_gravityzone_support.md) — Authoritative Bitdefender GravityZone product + Public API docs; use to confirm UNVERIFIED `bitdefender` skill methods/param shapes (push setPushEventSettings, assignPolicy, report/account writes, maintenancewindows/integrations names).
|
- [Investigator EXO ManageAsApp gap](reference_investigator_exo_manageasapp_gap.md) — Security Investigator app lacks `Exchange.ManageAsApp` (only `full_access_as_app`) so `investigator-exo` 401s on EXO adminapi; use `exchange-op` tier for InvokeCommand.
|
||||||
- [GURU-5070 Rust toolchain](reference_guru5070_rust_toolchain.md) — GURU-5070 now has cargo + MSVC + protoc; build/clippy/test guru-connect LOCALLY (set PROTOC to the winget path) instead of the build host. CI only clippy-checks the Linux server, not the Windows agent.
|
- [Windows edition upgrade via RMM](reference_windows_edition_upgrade_rmm.md) — Verified Home->Pro/Pro-for-Workstations fleet procedure: `changepk.exe /ProductKey <generic Pro>` (NOT DISM Set-Edition — Error 50) -> reboot with `Restart-Computer -Force` (`shutdown /r /t N` is silently dropped in SYSTEM session) -> `slmgr /ipk <MAK>` + /ato. The vault MAK is a Pro-for-WORKSTATIONS key (auto-upgrades edition past plain Pro).
|
||||||
- [ACG Office Network Infrastructure](infra_office_network.md) — IPs/hosts/roles for pfSense/Jupiter/VMs/Docker. Check before assuming; .21 (Uranus) is storage.
|
- [Tailscale subnet-route key expiry](reference_tailscale_subnet_key_expiry.md) — "internet OK but all of 172.16.3.x (Gitea .20, RMM/coord .30) dead" = Tailscale infra-node KEY EXPIRY (pfSense subnet router advertises 172.16.0.0/22), NOT a LAN outage; expiry now disabled on infra nodes (2026-06-25). Fallback: gururmm-server direct at tailnet 100.86.12.15:3001.
|
||||||
- [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS.
|
- [GravityZone support center](reference_gravityzone_support.md) — Authoritative Bitdefender GravityZone product + Public API docs; use to confirm UNVERIFIED `bitdefender` skill methods/param shapes (push setPushEventSettings, assignPolicy, report/account writes, maintenancewindows/integrations names).
|
||||||
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number.
|
- [GURU-5070 Rust toolchain](reference_guru5070_rust_toolchain.md) — toolchain installed BUT a WDAC/Smart App Control policy (since 2026-07-04) BLOCKS running rustc/cargo/sops/openssl locally (os error 4551) — build the Windows agent on Beast, not here. Toolchain paths + CI gates still valid for reference.
|
||||||
- [Syncro RMM policies = API-impossible](reference_syncro_rmm_api_gui_only.md) — policy create/assign/folder-move is GUI-ONLY; `policy_folder_id` is read-only on PUT (live-proven), policy endpoints 404, /policy_folders 401 scope-gated. Don't build /syncro move-asset; use `bitdefender` for API policy work.
|
- [ACG Office Network Infrastructure](infra_office_network.md) — IPs/hosts/roles for pfSense/Jupiter/VMs/Docker. Check before assuming; .21 (Uranus) is storage.
|
||||||
- [Datto EDR detection behavior](reference_datto_edr_detection_behavior.md) — alert `sourceType`: `av`=Datto AV signature, `rule`=EDR reputation detection (both via `edr.py detections`). EDR is reputation-based not structural (wire known-bad file as autostart exe to trip it; loose files aren't surveyed). AV is tamper-protected (console-only disable); disabling Datto AV uninstalls it + Defender auto-reactivates (AMSI blocks scripts with literal EICAR → build from char codes). Verified live on RMM-TEST-MACHINE.
|
- [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS.
|
||||||
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval for architecture/features; Howard can handle merges/deploys himself (2026-06-21); bugs→bug list.
|
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number.
|
||||||
- [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style.
|
- [Syncro asset/RMM IS API-drivable](reference_syncro_rmm_api_gui_only.md) — CORRECTED 2026-07-10: asset read/create/update, patches, installed_apps, rmm_alerts all work via API; ONLY policy folders + `policy_folder_id` move need the `Assets - Policy Change` token scope (401 today). Archive stays GUI-only. Full catalog: `.claude/standards/syncro/api-reference.md`.
|
||||||
- [Firefox driver (ff.py)](reference_ff_firefox_driver.md) — PREFERRED browser driver. Drive Firefox via Playwright (.claude/scripts/ff.py): daemon on :9333, persistent profile, nav/shot/click/type/eval/console/network. Mike dislikes Chrome; claude-in-chrome connector disabled 2026-06-06.
|
- [Datto EDR detection behavior](reference_datto_edr_detection_behavior.md) — alert `sourceType`: `av`=Datto AV signature, `rule`=EDR reputation detection (both via `edr.py detections`). EDR is reputation-based not structural (wire known-bad file as autostart exe to trip it; loose files aren't surveyed). AV is tamper-protected (console-only disable); disabling Datto AV uninstalls it + Defender auto-reactivates (AMSI blocks scripts with literal EICAR → build from char codes). Verified live on RMM-TEST-MACHINE.
|
||||||
- [Community Forum (Flarum)](reference_community_forum.md) — Flarum forum at community.azcomputerguru.com, API access, database, posting workflow.
|
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval for architecture/features; Howard can handle merges/deploys himself (2026-06-21); bugs→bug list.
|
||||||
- [Radio Show Website](reference_radio_website.md) — Astro static site at radio.azcomputerguru.com on IX server.
|
- [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style.
|
||||||
- [IX Server Access](reference_ix_server_access.md) — `ix.azcomputerguru.com` / 172.16.3.10. Reachable when Tailscale is on (no VPN). SSH currently uses sshpass with root password; key auth from GURU-5070 not configured yet (was CachyOS, now Win11 — verify).
|
- [Firefox driver (ff.py)](reference_ff_firefox_driver.md) — PREFERRED browser driver. Drive Firefox via Playwright (.claude/scripts/ff.py): daemon on :9333, persistent profile, nav/shot/click/type/eval/console/network. Mike dislikes Chrome; claude-in-chrome connector disabled 2026-06-06.
|
||||||
- [Cloudflare access](reference_cloudflare_access.md) — Cloudflare API creds in SOPS `services/cloudflare.sops.yaml` (full DNS + account tokens; azcomputerguru zone_id 1beb9917...). azcomputerguru.com DNS is on Cloudflare (not IX) — edit via Cloudflare API, not whmapi1.
|
- [Community Forum (Flarum)](reference_community_forum.md) — Flarum forum at community.azcomputerguru.com, API access, database, posting workflow.
|
||||||
- [Matomo Analytics](reference_matomo_analytics.md) — Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites.
|
- [Radio Show Website](reference_radio_website.md) — Astro static site at radio.azcomputerguru.com on IX server.
|
||||||
- [TickTick Integration](reference_ticktick_integration.md) — OAuth API integration, MCP server, SOPS vault creds, project/task CRUD.
|
- [IX Server Access](reference_ix_server_access.md) — `ix.azcomputerguru.com` / 172.16.3.10. Reachable when Tailscale is on (no VPN). SSH currently uses sshpass with root password; key auth from GURU-5070 not configured yet (was CachyOS, now Win11 — verify).
|
||||||
- [Client Docs Structure](reference_client_docs_structure.md) — clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm). Template: clients/_client_template/.
|
- [Cloudflare access](reference_cloudflare_access.md) — Cloudflare API creds in SOPS `services/cloudflare.sops.yaml` (full DNS + account tokens; azcomputerguru zone_id 1beb9917...). azcomputerguru.com DNS is on Cloudflare (not IX) — edit via Cloudflare API, not whmapi1.
|
||||||
- [MSP Audit Scripts](reference_msp_audit_scripts.md) — server_audit.ps1 / workstation_audit.ps1 at projects/msp-tools/msp-audit-scripts/.
|
- [Matomo Analytics](reference_matomo_analytics.md) — Self-hosted analytics at analytics.azcomputerguru.com, site IDs, tracking for all 3 sites.
|
||||||
- [Pluto Build Server](reference_pluto_build_server.md) — Windows build VM: hostname PLUTO = Unraid VM "Claude-Builder" = 172.16.3.36 (all the same box). MSVC + WiX + Azure Trusted Signing. Drive via /rmm (agent enrolls as PLUTO) when SSH key isn't authorized.
|
- [TickTick Integration](reference_ticktick_integration.md) — OAuth API integration, MCP server, SOPS vault creds, project/task CRUD.
|
||||||
- [Coord /messages API shape](reference_coord_messages_api_shape.md) — GET /api/coord/messages returns {total,skip,limit,messages[]} NOT a bare array; parse .messages[], strip control chars, read flag may be null.
|
- [Client Docs Structure](reference_client_docs_structure.md) — clients/<name>/docs/ layout (overview, network, servers, cloud, security, rmm). Template: clients/_client_template/.
|
||||||
- [Gitea API credential](reference_gitea_api_credential.md) — Gitea API (PRs/merges) as howard uses services/gitea-howard.sops.yaml password on internal http://172.16.3.20:3000; NOT the gururmm-server SSH password.
|
- [Client Wi-Fi Inventory](reference_client_wifi_inventory.md) — Building a fleet Wi-Fi list to connect onsite without asking. Passwords → vault `clients/<slug>/wifi.sops.yaml` (`credentials.<key>_ssid/_password`); readable index → `wiki/reference/client-wifi.md`. `/wifi` importer deferred (structure-only 2026-07-06).
|
||||||
- [Gitea Internal API Access](reference_gitea_internal.md) — git.azcomputerguru.com is NOT behind Cloudflare — it's the office Cox IP NAT'd to NPM (openresty) on Jupiter. Prefer internal 172.16.3.20:3000 for reliability (bypasses NPM SSL-renewal reload blips).
|
- [Git Bash TZ ignored](feedback_gitbash_tz_ignored.md) — `TZ=America/Phoenix date` in Git Bash silently prints UTC (no tzdata) — confidently-wrong "current time". Use PowerShell `Get-Date` (machine already AZ) or plain `date` with no TZ override for now-time on Windows.
|
||||||
- [Gitea git-op latency](reference_gitea_git_op_latency.md) — SSH (.20:2222) is SLOWEST (~1.5s); internal HTTP+token ~0.55s; SOPS lookup only ~0.33s. Don't switch to SSH for speed. Gitea SSH is .20:2222 (API ssh_url .21 is wrong).
|
- [Discord read replies](feedback_discord_read_replies.md) — Discord DM replies don't come through coord/repo sync. Use `discord-dm.sh read <user>` to see if mike/winter/rob actually answered before assuming silence.
|
||||||
- [GuruRMM technical reference](reference_gururmm.md) — Server (172.16.3.30) layout + downloads dir `/var/www/gururmm/downloads` + `.channel` sidecar rollout control (stable/beta) + privileged server access via the server's OWN root RMM agent (hostname `gururmm`, no SSH needed; plink fallback) + API + `context=user_session` (WTS impersonation) + build-pipeline vendoring at `deploy/build-pipeline/` + Linux agent systemd sandbox trap.
|
- [MSP Audit Scripts](reference_msp_audit_scripts.md) — server_audit.ps1 / workstation_audit.ps1 at projects/msp-tools/msp-audit-scripts/.
|
||||||
- [GuruRMM command timeout_seconds](reference_gururmm_command_timeout_seconds.md) — agent command dispatch honors `timeout_seconds`, NOT `timeout`; long jobs die ~300s / go zombie (`running`, empty stdout) otherwise. Cost Birth Biologic a full day.
|
- [Pluto Build Server](reference_pluto_build_server.md) — Windows build VM: hostname PLUTO = Unraid VM "Claude-Builder" = 172.16.3.36 (all the same box). MSVC + WiX + Azure Trusted Signing. Drive via /rmm (agent enrolls as PLUTO) when SSH key isn't authorized.
|
||||||
- [SharePoint Graph large-file upload](reference_sharepoint_graph_large_file_upload.md) — <4MB simple PUT, >=4MB MUST use chunked upload session (Content-Range); `\\?\` long paths; idempotent size-check; verify counts via /root/delta; single stream ~40Mbps (SPO throttle).
|
- [Coord /messages API shape](reference_coord_messages_api_shape.md) — GET /api/coord/messages returns {total,skip,limit,messages[]} NOT a bare array; parse .messages[], strip control chars, read flag may be null.
|
||||||
- [RMM-spawn headless Claude](reference_rmm_spawn_headless_claude.md) — run `claude -p` on any RMM-managed Windows box with Claude Code (reaches coord-isolated sites like AD2); use `context:user_session`, UNSET the stale machine `ANTHROPIC_API_KEY` (shadows OAuth → "Invalid API key"), detach + poll a DONE marker. Validated on AD2 2026-07-01.
|
- [Gitea API credential](reference_gitea_api_credential.md) — Gitea API (PRs/merges) as howard uses services/gitea-howard.sops.yaml password on internal http://172.16.3.20:3000; NOT the gururmm-server SSH password.
|
||||||
- [RMM agent update model](rmm-agent-update-model.md) — Agent updates are server-PUSH on heartbeat (no self-poll); available versions = filesystem scan needing a `.sha256`; promote flips `.channel` sidecars beta→stable globally. Two stranders: beta-first freezes stable until an explicit promote; agents older than ~0.6.50 re-enroll with a NEW device_id/agent row when updated.
|
- [Gitea Internal API Access](reference_gitea_internal.md) — git.azcomputerguru.com is NOT behind Cloudflare — it's the office Cox IP NAT'd to NPM (openresty) on Jupiter. Prefer internal 172.16.3.20:3000 for reliability (bypasses NPM SSL-renewal reload blips).
|
||||||
- [GuruRMM physical server storage](gururmm-physical-server-storage.md) — New box 172.16.1.231 (temp IP→will be .30), Ubuntu 26.04, ssh key `gururmm-physical`/alias `gururmm-new`. SSD (915G root) = HOT (PG default tablespace + WAL + builds); HDD ext4 at `/data` = COLD (`gururmm_cold` PG tablespace for aged `agent_logs` partitions + downloads + backups + archive). The #3 retention answer.
|
- [Gitea git-op latency](reference_gitea_git_op_latency.md) — SSH (.20:2222) is SLOWEST (~1.5s); internal HTTP+token ~0.55s; SOPS lookup only ~0.33s. Don't switch to SSH for speed. Gitea SSH is .20:2222 (API ssh_url .21 is wrong).
|
||||||
- [Trebesch DESKTOP-QNP3ON5 shell replacement](reference_trebesch_qnp3on5.md) — AT Trebesch box runs an Explorer shell replacement; explorer.exe owner check returns blank — use Win32_ComputerSystem.UserName. GuruRMM SWIFT-LION-2892.
|
- [GuruRMM technical reference](reference_gururmm.md) — Server (172.16.3.30) layout + downloads dir `/var/www/gururmm/downloads` + `.channel` sidecar rollout control (stable/beta) + privileged server access via the server's OWN root RMM agent (hostname `gururmm`, no SSH needed; plink fallback) + API + `context=user_session` (WTS impersonation) + build-pipeline vendoring at `deploy/build-pipeline/` + Linux agent systemd sandbox trap.
|
||||||
- [reference_backblaze_storage_rate](reference_backblaze_storage_rate.md) -- ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation
|
- [GuruRMM command timeout_seconds](reference_gururmm_command_timeout_seconds.md) — agent command dispatch honors `timeout_seconds`, NOT `timeout`; long jobs die ~300s / go zombie (`running`, empty stdout) otherwise. Cost Birth Biologic a full day.
|
||||||
- [Unraid VM no-IP causes](unraid-windows-vm-virtio-no-ip.md) — PRIMARY (general "new VMs stopped getting IPs lately"): Docker sets bridge-nf-call-iptables=1, so br0 VM DHCP OFFERs hit DOCKER-FORWARD (no br0 ACCEPT) and get dropped; new VMs can't complete DORA (existing renew via ESTABLISHED). Fix `=0` runtime (needs persistent post-Docker hook; not yet persisted on Jupiter). SECONDARY (Windows VM): virtio-net has no in-box driver -> use e1000 or virtio-win. Diagnose: tcpdump DHCP on pfSense; /sys vnetN rx_packets.
|
- [SharePoint Graph large-file upload](reference_sharepoint_graph_large_file_upload.md) — <4MB simple PUT, >=4MB MUST use chunked upload session (Content-Range); `\\?\` long paths; idempotent size-check; verify counts via /root/delta; single stream ~40Mbps (SPO throttle).
|
||||||
- [Starr Pass mail routing](reference_starrpass_mail_routing.md) — starrpass.com is DIRECT to MS (EOP/Defender, tenant 222450dd…); only devconllc.com is on Mailprotector (MP acct 16170). Check @starrpass.com quarantine/rejects via remediation-tool, not Mailprotector.
|
- [RMM-spawn headless Claude](reference_rmm_spawn_headless_claude.md) — run `claude -p` on any RMM-managed Windows box with Claude Code (reaches coord-isolated sites like AD2); use `context:user_session`, UNSET the stale machine `ANTHROPIC_API_KEY` (shadows OAuth → "Invalid API key"), detach + poll a DONE marker. Validated on AD2 2026-07-01.
|
||||||
- [INKY outbound breaks DMARC](reference_inky_outbound_breaks_dmarc.md) — Reverse-resolve DMARC rua failing IPs before blaming a sender: ipw-outbound.inkyphishfence.com / us.cloud-sec-av.com = INKY re-injection breaking DKIM+SPF. INKY is in-M365 (connectors+transport rules) per enrolled tenant, but hosting-level (IX/cPanel website) outbound also routes through it independent of M365 enrollment. Fix is INKY-side (outbound DKIM/SPF/ARC), not cPanel DNS.
|
- [RMM agent update model](rmm-agent-update-model.md) — Agent updates are server-PUSH on heartbeat (no self-poll); available versions = filesystem scan needing a `.sha256`; promote flips `.channel` sidecars beta→stable globally. Two stranders: beta-first freezes stable until an explicit promote; agents older than ~0.6.50 re-enroll with a NEW device_id/agent row when updated.
|
||||||
- [Syncro prepay: full-GET only](feedback_syncro_prepay_full_get_only.md) — read prepay_hours ONLY from GET /customers/{id}; the customer search/list endpoint returns null/stale prepay. Never assert "no block" in a billing preview from search data.
|
- [GuruRMM physical server storage](gururmm-physical-server-storage.md) — New box 172.16.1.231 (temp IP→will be .30), Ubuntu 26.04, ssh key `gururmm-physical`/alias `gururmm-new`. SSD (915G root) = HOT (PG default tablespace + WAL + builds); HDD ext4 at `/data` = COLD (`gururmm_cold` PG tablespace for aged `agent_logs` partitions + downloads + backups + archive). The #3 retention answer.
|
||||||
- [Syncro priority/type format](feedback_syncro_priority_type_format.md) — every ticket create needs a number-prefixed priority ("2 Normal", not bare "Normal" which renders blank) AND a valid problem_type. Winter flagged #32193/#32194. Use the syncro skill's create flow.
|
- [Trebesch DESKTOP-QNP3ON5 shell replacement](reference_trebesch_qnp3on5.md) — AT Trebesch box runs an Explorer shell replacement; explorer.exe owner check returns blank — use Win32_ComputerSystem.UserName. GuruRMM SWIFT-LION-2892.
|
||||||
- [Syncro line-item category](feedback_syncro_line_item_category.md) — product_category is a controlled vocab: never invent one, never invoice a line whose product has product_category:null (blank CATEGORY breaks QB mapping). Pre-flight GET /products/<id>. Winter fixed Cascades 67887/67890 "Windows Pro Upgrade".
|
- [reference_backblaze_storage_rate](reference_backblaze_storage_rate.md) -- ACG's Backblaze B2 storage cost rate ($0.00695/GB) for the GuruRMM mspbackups storage-cost calculation
|
||||||
- [RMM drive-map Explorer refresh](reference_rmm_drive_map_explorer_refresh.md) — drive mapped via RMM user_session works but the user's running Explorer won't show it until SHChangeNotify(DRIVEADD); also UNC \\ gets eaten in heredoc+jq, build it from [char]92.
|
- [Unraid VM no-IP causes](unraid-windows-vm-virtio-no-ip.md) — PRIMARY (general "new VMs stopped getting IPs lately"): Docker sets bridge-nf-call-iptables=1, so br0 VM DHCP OFFERs hit DOCKER-FORWARD (no br0 ACCEPT) and get dropped; new VMs can't complete DORA (existing renew via ESTABLISHED). Fix `=0` runtime (needs persistent post-Docker hook; not yet persisted on Jupiter). SECONDARY (Windows VM): virtio-net has no in-box driver -> use e1000 or virtio-win. Diagnose: tcpdump DHCP on pfSense; /sys vnetN rx_packets.
|
||||||
- [Verify live before acting](feedback_verify_live_before_acting.md) — pull LIVE data (OMSA/iDRAC/live API) before acting on a hardware/infra flag; wiki/logs go stale. Cascades CS-SERVER "degraded RAID" was 9-day-stale (mirror self-recovered, SSDs bought needlessly). Windows can't see RAID member health.
|
- [Starr Pass mail routing](reference_starrpass_mail_routing.md) — starrpass.com is DIRECT to MS (EOP/Defender, tenant 222450dd…); only devconllc.com is on Mailprotector (MP acct 16170). Check @starrpass.com quarantine/rejects via remediation-tool, not Mailprotector.
|
||||||
- [Windows Pro upgrade billing](feedback_windows_pro_upgrade_billing.md) — ACG MAK key (vault infrastructure/windows-pro-mak, Mike's ACG key) for Home->Pro upgrades; RULE: invoice $99 PER machine activated, name the machine on the line item, bill after success. Each use consumes a MAK count.
|
- [INKY outbound breaks DMARC](reference_inky_outbound_breaks_dmarc.md) — Reverse-resolve DMARC rua failing IPs before blaming a sender: ipw-outbound.inkyphishfence.com / us.cloud-sec-av.com = INKY re-injection breaking DKIM+SPF. INKY is in-M365 (connectors+transport rules) per enrolled tenant, but hosting-level (IX/cPanel website) outbound also routes through it independent of M365 enrollment. Fix is INKY-side (outbound DKIM/SPF/ARC), not cPanel DNS.
|
||||||
- [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp.
|
- [Syncro prepay: full-GET only](feedback_syncro_prepay_full_get_only.md) — read prepay_hours ONLY from GET /customers/{id}; the customer search/list endpoint returns null/stale prepay. Never assert "no block" in a billing preview from search data.
|
||||||
- [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients/<slug>/udm-ssh`.
|
- [Syncro priority/type format](feedback_syncro_priority_type_format.md) — every ticket create needs a number-prefixed priority ("2 Normal", not bare "Normal" which renders blank) AND a valid problem_type. Winter flagged #32193/#32194. Use the syncro skill's create flow.
|
||||||
- [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration.
|
- [Syncro line-item category](feedback_syncro_line_item_category.md) — product_category is a controlled vocab: never invent one, never invoice a line whose product has product_category:null (blank CATEGORY breaks QB mapping). Pre-flight GET /products/<id>. Winter fixed Cascades 67887/67890 "Windows Pro Upgrade".
|
||||||
- [AD2 SSH MTU blackhole](ad2-ssh-mtu-blackhole.md) — AD2 SSH "lockouts"/mid-session read-errors over the Dataforth OpenVPN were a PMTU blackhole (tunnel PMTU ~1424 vs adapter MTU 1500), NOT a ban/account-lockout/flaky tunnel. Fix: pin the OpenVPN adapter MTU to 1400 (done on GURU-5070 via its SYSTEM RMM agent); permanent = `mssfix 1360` on the OpenVPN server. Diagnose over RMM, not SSH.
|
- [RMM drive-map Explorer refresh](reference_rmm_drive_map_explorer_refresh.md) — drive mapped via RMM user_session works but the user's running Explorer won't show it until SHChangeNotify(DRIVEADD); also UNC \\ gets eaten in heredoc+jq, build it from [char]92.
|
||||||
- [DSCA33/45 resolved via Hoffman](project_dsca33_45_resolved_via_hoffman.md) — The "lost" DSCA33/45 spec files are recoverable from the Hoffman API (original certs survived the wipe); do NOT ask John. 56/58 models mined into projects/dataforth-dos/dsca33-45-templates.json; only DSCA33-1948 + DSCA45-1746 (24 units) lack an original. AD2 handoff: DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md.
|
- [Verify live before acting](feedback_verify_live_before_acting.md) — pull LIVE data (OMSA/iDRAC/live API) before acting on a hardware/infra flag; wiki/logs go stale. Cascades CS-SERVER "degraded RAID" was 9-day-stale (mirror self-recovered, SSDs bought needlessly). Windows can't see RAID member health.
|
||||||
- [AD2 comms via sync only](ad2-comms-via-sync-only.md) — The AD2 Dataforth-box Claude session is coord-API-isolated (Gitea only); coord msg/lock/todo never reach it. Coordinate with AD2 ONLY via git /sync (committed docs + ## Note blocks).
|
- [Windows Pro upgrade billing](feedback_windows_pro_upgrade_billing.md) — ACG MAK key (vault infrastructure/windows-pro-mak, Mike's ACG key) for Home->Pro upgrades; RULE: invoice $99 PER machine activated, name the machine on the line item, bill after success. Each use consumes a MAK count.
|
||||||
- [reference_syncro_agent_handle_leak](reference_syncro_agent_handle_leak.md) -- RDS "no available computers in the pool" (0x3/0x408) can really be a SyncroLive.Agent.Runner handle leak starving the box. How to spot + fix.
|
- [AAD Connect msDS-KeyCredentialLink writeback](reference_aadconnect_keycredlink_writeback.md) — "completed-export-errors" + 8344 INSUFF_ACCESS_RIGHTS on a protected admin account = WHfB key writeback blocked by AdminSDHolder. Diagnose with csexport /f:x; fix with dsacls WP;msDS-KeyCredentialLink on AdminSDHolder + SDProp.
|
||||||
|
- [UniFi Site Manager cloud API](reference_unifi_site_manager_api.md) — `api.ui.com` + `X-API-KEY` (vault `services/unifi-site-manager`) = remote access to the WHOLE ACG UniFi fleet (~36 consoles) outside UOS. Tier1 `/v1/hosts|sites|devices|isp-metrics` = inventory+health+WAN. Tier2 CONNECTOR `/v1/connector/consoles/{id}/proxy/network/api/s/default/stat/{device,sta}` = **full UOS parity** (per-radio cu_total airtime + per-client RSSI) for ANY console, remote. Backend `unifi-wifi/scripts/gw-sitemanager.sh` (`fleet|devices|sites|isp|net`). Standalone UDM WAN SSH usually firewalled; per-console SSH pw at `clients/<slug>/udm-ssh`.
|
||||||
## Users
|
- [reference_sqlx_migrations_immutable](reference_sqlx_migrations_immutable.md) -- NEVER edit an already-applied sqlx migration file — even a comment. sqlx::migrate! checksums each file at compile time and validates against _sqlx_migrations at startup; a changed checksum crash-loops the server with "migration N was previously applied but has been modified". Code review MUST flag any edit to an applied migration.
|
||||||
- [Howard Enos](user_howard.md) — Mike's brother, technician, full access. Machines: ACG-TECH03L, Howard-Home (authoritative in users.json).
|
- [AD2 SSH MTU blackhole](ad2-ssh-mtu-blackhole.md) — AD2 SSH "lockouts"/mid-session read-errors over the Dataforth OpenVPN were a PMTU blackhole (tunnel PMTU ~1424 vs adapter MTU 1500), NOT a ban/account-lockout/flaky tunnel. Fix: pin the OpenVPN adapter MTU to 1400 (done on GURU-5070 via its SYSTEM RMM agent); permanent = `mssfix 1360` on the OpenVPN server. Diagnose over RMM, not SSH.
|
||||||
- [Mike — font preference](user_font_preference.md) — Mike prefers Lucida Console for monospace UI.
|
- [DSCA33/45 resolved via Hoffman](project_dsca33_45_resolved_via_hoffman.md) — The "lost" DSCA33/45 spec files are recoverable from the Hoffman API (original certs survived the wipe); do NOT ask John. 56/58 models mined into projects/dataforth-dos/dsca33-45-templates.json; only DSCA33-1948 + DSCA45-1746 (24 units) lack an original. AD2 handoff: DSCA33-45-HOFFMAN-RECOVERY-2026-06-18.md.
|
||||||
|
- [AD2 comms via sync only](ad2-comms-via-sync-only.md) — The AD2 Dataforth-box Claude session is coord-API-isolated (Gitea only); coord msg/lock/todo never reach it. Coordinate with AD2 ONLY via git /sync (committed docs + ## Note blocks).
|
||||||
## Feedback
|
- [reference_syncro_agent_handle_leak](reference_syncro_agent_handle_leak.md) -- RDS "no available computers in the pool" (0x3/0x408) can really be a SyncroLive.Agent.Runner handle leak starving the box. How to spot + fix.
|
||||||
- [Report times in Arizona time](feedback_timezone_arizona_reporting.md) — All user-facing times in America/Phoenix (MST, no DST), labeled AZ; convert from UTC. Set by Winter 2026-07-02.
|
|
||||||
- [RMM dashboard: beta before main](rmm-dashboard-beta-before-main.md) — GuruRMM website/dashboard changes land on beta (rmm-beta.azcomputerguru.com) and get tested BEFORE pushing/merging to main, unless told otherwise. Pipeline auto-builds beta FROM main, so don't merge to get on beta — build the feature branch's dashboard and rsync to /var/www/gururmm/dashboard-beta via a git worktree. Promote beta→prod with promote-dashboard.sh --confirm.
|
## Users
|
||||||
- [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — When a target has SSH (key auth) and the task is easier over it, default to `scp script + ssh run` (system OpenSSH); RMM runs as SYSTEM + hits the server-side timeout reaper. Reserve RMM as fallback when SSH/VPN is down.
|
- [Howard Enos](user_howard.md) — Mike's brother, technician, full access. Machines: ACG-TECH03L, Howard-Home (authoritative in users.json).
|
||||||
- [Re-clone submodule creds](reclone-submodule-creds.md) — Re-cloning the restructured claudetools (projects now submodules): set `credential.helper=store` GLOBALLY before `git submodule update --init --recursive` or every Gitea submodule fails "could not read Username". Steps in RECLONE.md.
|
- [Mike — font preference](user_font_preference.md) — Mike prefers Lucida Console for monospace UI.
|
||||||
- [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/<internal_id> (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message.
|
|
||||||
- [RMM Set-Acl timeout loses stdout](feedback_rmm_setacl_timeout_password_loss.md) — NTFS ACL propagation (Set-Acl/icacls) on a large folder tree exceeds the RMM command timeout and stdout is dropped, so a password printed in that script is lost. Generate secrets LOCALLY (placeholder-inject) so they survive; isolate the ACL grant into its own long-timeout command.
|
## Feedback
|
||||||
- [Mac RMM authentication fixed](feedback_mac_rmm_auth_fixed.md) — Use `.claude/scripts/rmm-auth.sh` helper instead of heredoc pattern. Heredoc with `--data-binary @-` fails on macOS. Helper uses `jq -n --arg` to build JSON safely. Usage: `eval "$(bash .claude/scripts/rmm-auth.sh)"` sets $TOKEN, $RMM, $REPO_ROOT. Updated in /rmm Phase 0.
|
- [Simplest solution first](feedback_simplest_solution_first.md) — Start with the least-complex thing that could answer the problem (one DNS lookup / one API call / one command), confirm it works, THEN graduate to advanced/complicated as needed. Don't reach for SSH/plink/multi-hop when a one-shot query answers it. Now a CLAUDE.md core rule.
|
||||||
- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory.
|
- [Report times in Arizona time](feedback_timezone_arizona_reporting.md) — All user-facing times in America/Phoenix (MST, no DST), labeled AZ; convert from UTC. Set by Winter 2026-07-02.
|
||||||
- [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks.
|
- [RMM dashboard: beta before main](rmm-dashboard-beta-before-main.md) — GuruRMM website/dashboard changes land on beta (rmm-beta.azcomputerguru.com) and get tested BEFORE pushing/merging to main, unless told otherwise. Pipeline auto-builds beta FROM main, so don't merge to get on beta — build the feature branch's dashboard and rsync to /var/www/gururmm/dashboard-beta via a git worktree. Promote beta→prod with promote-dashboard.sh --confirm.
|
||||||
- [DMARC rua INKY only when onboarded](feedback_dmarc_rua_inky_onboarded_only.md) — Don't point a client's DMARC rua at reports-sg.inkydmarc.com unless that client is onboarded to INKY (most aren't). Use plain `p=none` with no rua otherwise.
|
- [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — When a target has SSH (key auth) and the task is easier over it, default to `scp script + ssh run` (system OpenSSH); RMM runs as SYSTEM + hits the server-side timeout reaper. Reserve RMM as fallback when SSH/VPN is down.
|
||||||
- [Use rmm-search to find machines](feedback_rmm_search_skill.md) — Find GuruRMM agents via the `rmm-search` skill (`rmm-search.sh <words> [-c client]`), never hand-grep /api/agents (it bleeds across clients). Then hand hostname/id to `/rmm`.
|
- [Re-clone submodule creds](reclone-submodule-creds.md) — Re-cloning the restructured claudetools (projects now submodules): set `credential.helper=store` GLOBALLY before `git submodule update --init --recursive` or every Gitea submodule fails "could not read Username". Steps in RECLONE.md.
|
||||||
- [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh.
|
- [Bot alerts need a ticket link](feedback_bot_alert_ticket_link.md) — Syncro ticket bot-alerts MUST include a clickable link: https://computerguru.syncromsp.com/tickets/<internal_id> (internal id, not ticket number). post-bot-alert.sh posts raw text; put the URL in the message.
|
||||||
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin.
|
- [RMM Set-Acl timeout loses stdout](feedback_rmm_setacl_timeout_password_loss.md) — NTFS ACL propagation (Set-Acl/icacls) on a large folder tree exceeds the RMM command timeout and stdout is dropped, so a password printed in that script is lost. Generate secrets LOCALLY (placeholder-inject) so they survive; isolate the ACL grant into its own long-timeout command.
|
||||||
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) — Set permissions.defaultMode to bypassPermissions in settings.json on all machines.
|
- [Mac RMM authentication fixed](feedback_mac_rmm_auth_fixed.md) — Use `.claude/scripts/rmm-auth.sh` helper instead of heredoc pattern. Heredoc with `--data-binary @-` fails on macOS. Helper uses `jq -n --arg` to build JSON safely. Usage: `eval "$(bash .claude/scripts/rmm-auth.sh)"` sets $TOKEN, $RMM, $REPO_ROOT. Updated in /rmm Phase 0.
|
||||||
- [365 Remediation Tool](feedback_365_remediation_tool.md) — "remediation tool" = tiered ComputerGuru app suite via /remediation-tool; NOT CIPP, NOT the deprecated fabb3421.
|
- [Verify committed state before push](feedback_verify_committed_state_before_push.md) — webhook builds from origin/main: verify the COMMITTED build (git stash + build), not the working tree; bad git-add pathspec silently aborts staging. Stage by directory.
|
||||||
- [CA managed programmatically (with discipline)](feedback_ca_programmatic_management.md) — Conditional Access CAN be written via Tenant Admin app; ALWAYS report-only first + exclude break-glass + confirm before enforcing. Overrides old "CA manual" rule.
|
- [Scheduling = coord todo, not schedulers](feedback_scheduling_via_coord_todo.md) — Defer future work as a coord todo (POST /api/coord/todos; needs text + created_by_user + created_by_machine) for a later session to pick up. NOT /schedule remote CCR agents (no vault/creds there) or local scheduled tasks.
|
||||||
- [Ollama Tier-0 Routing](feedback_ollama_tier0_routing.md) — Route drafts/summaries/classifications through Ollama (qwen3:14b). Mike designed ClaudeTools this way — not optional.
|
- [DMARC rua INKY only when onboarded](feedback_dmarc_rua_inky_onboarded_only.md) — Don't point a client's DMARC rua at reports-sg.inkydmarc.com unless that client is onboarded to INKY (most aren't). Use plain `p=none` with no rua otherwise.
|
||||||
- [/save writes narrative directly](feedback_save_no_ollama.md) — No Ollama for /save; write all sections inline — too slow.
|
- [Use rmm-search to find machines](feedback_rmm_search_skill.md) — Find GuruRMM agents via the `rmm-search` skill (`rmm-search.sh <words> [-c client]`), never hand-grep /api/agents (it bleeds across clients). Then hand hostname/id to `/rmm`.
|
||||||
- [Identity precedence](feedback_identity_precedence.md) — Trust `.claude/identity.json` over the system-reminder `userEmail` hint when they disagree (shared-login machines).
|
- [Attribution is read, never inferred](feedback_attribution_from_identity.md) — Who-did-what (user+machine) comes ONLY from identity.json + users.json + git authorship. Never infer from hostname patterns, the userEmail hint, or memory. The "5070" box is Mike's. sync.sh reconciles git config to identity.json; /save renders the User block via whoami-block.sh.
|
||||||
- [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows.
|
- [D2TESTNAS SSH Access](feedback_d2testnas_ssh.md) — Use root@192.168.0.9 with Paper123!@#, not sysadmin.
|
||||||
- [Point vault-access teammates at SOPS path](feedback_vault_pointer_for_teammates.md) — When relaying infra/credential info to Howard or other vault-access teammates, hand over the SOPS path + key anchors; don't transcribe the entry's fields into the message.
|
- [Bypass Permissions Setting](feedback_bypass_permissions_setting.md) — Set permissions.defaultMode to bypassPermissions in settings.json on all machines.
|
||||||
- [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl.
|
- [365 Remediation Tool](feedback_365_remediation_tool.md) — "remediation tool" = tiered ComputerGuru app suite via /remediation-tool; NOT CIPP, NOT the deprecated fabb3421.
|
||||||
- [Windows strips embedded double-quotes](feedback_windows_quote_stripping.md) — Embedded `"` in an arg gets eaten twice over: PowerShell->curl.exe (CommandLineToArgvW) AND RMM->cmd.exe. MECHANICAL FIX: deliver PS scripts via `.claude/scripts/ps-encoded.sh` (-EncodedCommand, byte-exact; author the file with the Write tool). Inline one-offs: single-quoted heredoc `<<'JSON'` + `--data-binary @-`; build `"` from `[char]34`.
|
- [CA managed programmatically (with discipline)](feedback_ca_programmatic_management.md) — Conditional Access CAN be written via Tenant Admin app; ALWAYS report-only first + exclude break-glass + confirm before enforcing. Overrides old "CA manual" rule.
|
||||||
- [Interview the AI / read its docs before probing](feedback_interview_ai_read_docs.md) — To learn an external AI/CLI's syntax or capabilities, READ its bundled docs (Grok: `~/.grok/docs/user-guide/`, `README.md`, `grok inspect`/`models`/`--help`) or interview the model; don't guess flags or run slow trial-and-error. One run to confirm a doc-derived hypothesis, not a dozen to discover.
|
- [Ollama Tier-0 Routing](feedback_ollama_tier0_routing.md) — Route drafts/summaries/classifications through Ollama (qwen3:14b). Mike designed ClaudeTools this way — not optional.
|
||||||
- [Web search over blind probing](feedback_web_search_over_probing.md) — For external API/capability discovery, LEAD with web search (grok/gemini) + vendor docs; live endpoint-probing only CONFIRMS a hypothesis, never the primary discovery method (it mostly 404s, "highly suspect"). Reading a system's OWN config is fine; guessing unknown PATHS is not. Web-search bots being flaky is a must-fix (CT_THOUGHTS Thought 2).
|
- [/save writes narrative directly](feedback_save_no_ollama.md) — No Ollama for /save; write all sections inline — too slow.
|
||||||
- [Windows bash command mapping](feedback_windows_bash_mapping.md) — `bash` often resolves to WSL stub instead of Git/MSYS bash required by the harness. Fix by prepending `C:\Program Files\Git\bin` (and usr\bin) to PATH, or source `.claude/scripts/ensure-git-bash.ps1`. Profile has the logic; use plain `bash .claude/scripts/...` after remap. See the helper and this memory file for details.
|
- [Identity precedence](feedback_identity_precedence.md) — Trust `.claude/identity.json` over the system-reminder `userEmail` hint when they disagree (shared-login machines).
|
||||||
- [Git must authenticate non-interactively](feedback_git_noninteractive_auth.md) — Mike's gripe with Git for Windows is the constant password prompts (GCM) that hang automation, NOT the tool itself. D:\ClaudeTools is set to `credential.helper=store` primed with the azcomputerguru Gitea API token (host 172.16.3.20:3000); always set `GIT_TERMINAL_PROMPT=0`. Any never-prompts solution is acceptable.
|
- [1Password — always use service token](feedback_1password_service_token.md) — Source OP_SERVICE_ACCOUNT_TOKEN from SOPS for every `op` call. Desktop-app integration prompts are unacceptable in agent flows.
|
||||||
- [Vault git auth — GCM shadows store token](feedback_vault_gcm_shadow_auth.md) — vault sync "Failed to authenticate user" on git.azcomputerguru.com: GCM is first in the helper chain and shadows the valid store token. Fix (machine-local): store-only credential.helper reset + pin `azcomputerguru@` in the vault remote URL so store returns the durable PAT (not the volatile OAUTH_USER JWT). Applied GURU-5070 2026-06-07.
|
- [Point vault-access teammates at SOPS path](feedback_vault_pointer_for_teammates.md) — When relaying infra/credential info to Howard or other vault-access teammates, hand over the SOPS path + key anchors; don't transcribe the entry's fields into the message.
|
||||||
- [agy.exe NOW works headless (2026-07-03)](reference_antigravity_agy_not_headless.md) — Antigravity `agy.exe` v1.0.6+ has a working `-p/--print` headless mode (clean stdout, `--add-dir` to read files); use it as the Gemini/Antigravity second-model path, esp. when gemini-cli OAuth is ineligible. Call by full path until PATH refresh. (Supersedes the old "agy is DOA headless" note.)
|
- [/tmp path mismatch on Windows](feedback_tmp_path_windows.md) — Write tool and Git Bash resolve `/tmp` to DIFFERENT real dirs. Use heredoc or workspace path for JSON payloads handed to curl.
|
||||||
- [SQL instance role — verify by connections, not name](feedback_sql_instance_role_by_connection.md) — Standard installed under default `SQLEXPRESS` instance name is real. Prove role with `sys.dm_exec_sessions` + `Get-NetTCPConnection -OwningProcess` before recommending stop/uninstall.
|
- [Windows strips embedded double-quotes](feedback_windows_quote_stripping.md) — Embedded `"` in an arg gets eaten twice over: PowerShell->curl.exe (CommandLineToArgvW) AND RMM->cmd.exe. MECHANICAL FIX: deliver PS scripts via `.claude/scripts/ps-encoded.sh` (-EncodedCommand, byte-exact; author the file with the Write tool). Inline one-offs: single-quoted heredoc `<<'JSON'` + `--data-binary @-`; build `"` from `[char]34`.
|
||||||
- [RMM password setting limitation](feedback_rmm_password_limitation.md) — `net user <user> <password>` via GuruRMM fails silently (exit 0 but password doesn't set). Tested PowerShell AND CMD - both fail. ScreenConnect CMD works (also as SYSTEM). GuruRMM agent bug in process spawning. Use ScreenConnect for password ops. HIGH priority to fix.
|
- [Interview the AI / read its docs before probing](feedback_interview_ai_read_docs.md) — To learn an external AI/CLI's syntax or capabilities, READ its bundled docs (Grok: `~/.grok/docs/user-guide/`, `README.md`, `grok inspect`/`models`/`--help`) or interview the model; don't guess flags or run slow trial-and-error. One run to confirm a doc-derived hypothesis, not a dozen to discover.
|
||||||
- [Clear-RecycleBin fails silently as SYSTEM](feedback_clear_recyclebin_system_context.md) — RMM-dispatched cleanup scripts cannot use `Clear-RecycleBin -Force`; the cmdlet uses Shell COM and silently no-ops without an interactive desktop. Enumerate `C:\$Recycle.Bin\<SID>\*` directly.
|
- [Web search over blind probing](feedback_web_search_over_probing.md) — For external API/capability discovery, LEAD with web search (grok/gemini) + vendor docs; live endpoint-probing only CONFIRMS a hypothesis, never the primary discovery method (it mostly 404s, "highly suspect"). Reading a system's OWN config is fine; guessing unknown PATHS is not. Web-search bots being flaky is a must-fix (CT_THOUGHTS Thought 2).
|
||||||
- [Graph CA policy reads are eventually consistent](feedback_graph_ca_policy_eventual_consistency.md) — After PATCHing a CA policy (204), wait ~5s before GET-verifying; immediate reads can be stale.
|
- [Windows bash command mapping](feedback_windows_bash_mapping.md) — `bash` often resolves to WSL stub instead of Git/MSYS bash required by the harness. Fix by prepending `C:\Program Files\Git\bin` (and usr\bin) to PATH, or source `.claude/scripts/ensure-git-bash.ps1`. Profile has the logic; use plain `bash .claude/scripts/...` after remap. See the helper and this memory file for details.
|
||||||
- [Graph password reset needs a privileged role](feedback_graph_password_reset_requires_role.md) — PATCH passwordProfile on an existing user 403s without a directory role; User.ReadWrite.All alone only sets a password at CREATE.
|
- [Git must authenticate non-interactively](feedback_git_noninteractive_auth.md) — Mike's gripe with Git for Windows is the constant password prompts (GCM) that hang automation, NOT the tool itself. D:\ClaudeTools is set to `credential.helper=store` primed with the azcomputerguru Gitea API token (host 172.16.3.20:3000); always set `GIT_TERMINAL_PROMPT=0`. Any never-prompts solution is acceptable.
|
||||||
- [Vault writes — do the full sequence yourself](feedback_complete_vault_operations_end_to_end.md) — A vault entry = write plaintext → sops -e -i → git add/commit/push, all of it; don't stop at "encrypted on disk."
|
- [Vault git auth — GCM shadows store token](feedback_vault_gcm_shadow_auth.md) — vault sync "Failed to authenticate user" on git.azcomputerguru.com: GCM is first in the helper chain and shadows the valid store token. Fix (machine-local): store-only credential.helper reset + pin `azcomputerguru@` in the vault remote URL so store returns the durable PAT (not the volatile OAUTH_USER JWT). Applied GURU-5070 2026-06-07.
|
||||||
- [Exchange role recurring gap — backfill, don't promise](feedback_exchange_role_recurring_gap.md) — EXO email-cleanup 401/403 = Exchange Operator SP missing the Exchange Admin directory role (consent never grants it). Fix: `assign-exchange-role.sh <domain|--all>` (idempotent); audit with `--all --verify`. Fleet backfilled 2026-06-08. Verify membership via roleManagement/directory/roleAssignments (not the laggy directoryRoles/members list); EXO propagation 15-60min.
|
- [agy.exe NOW works headless (2026-07-03)](reference_antigravity_agy_not_headless.md) — Antigravity `agy.exe` v1.0.6+ has a working `-p/--print` headless mode (clean stdout, `--add-dir` to read files); use it as the Gemini/Antigravity second-model path, esp. when gemini-cli OAuth is ineligible. Call by full path until PATH refresh. (Supersedes the old "agy is DOA headless" note.)
|
||||||
- [Syncro is the default PSA; Autotask is opt-in](feedback_psa_default_syncro.md) — Ticketing/billing/customers default to Syncro (/syncro). Only use /autotask on an explicit "in Autotask" request. /autotask kept local/undistributed.
|
- [SQL instance role — verify by connections, not name](feedback_sql_instance_role_by_connection.md) — Standard installed under default `SQLEXPRESS` instance name is real. Prove role with `sys.dm_exec_sessions` + `Get-NetTCPConnection -OwningProcess` before recommending stop/uninstall.
|
||||||
- [Paste-safe command formatting (Howard)](feedback_command_formatting.md) — Two clauses, one root cause: (a) multi-line scripts not semicolon one-liners (wrap breaks paste), (b) all code at column 0 inside fences (indentation breaks PowerShell paste).
|
- [RMM password setting limitation](feedback_rmm_password_limitation.md) — `net user <user> <password>` via GuruRMM fails silently (exit 0 but password doesn't set). Tested PowerShell AND CMD - both fail. ScreenConnect CMD works (also as SYSTEM). GuruRMM agent bug in process spawning. Use ScreenConnect for password ops. HIGH priority to fix.
|
||||||
- [Autonomous infra/build setup](feedback_autonomous_infra_setup.md) — During infra/build/CI/dev setup, just install prerequisites and push through routine steps; reserve check-ins for genuine decisions (forks, destructive/outward, client/prod).
|
- [Clear-RecycleBin fails silently as SYSTEM](feedback_clear_recyclebin_system_context.md) — RMM-dispatched cleanup scripts cannot use `Clear-RecycleBin -Force`; the cmdlet uses Shell COM and silently no-ops without an interactive desktop. Enumerate `C:\$Recycle.Bin\<SID>\*` directly.
|
||||||
- [Check patterns before asking](feedback_check_patterns_before_asking.md) — Before asking how to do something repeat-style (sync, save, sweep, billing), study existing artifacts and workflow docs first; reach for similar past artifacts as the template.
|
- [Graph CA policy reads are eventually consistent](feedback_graph_ca_policy_eventual_consistency.md) — After PATCHing a CA policy (204), wait ~5s before GET-verifying; immediate reads can be stale.
|
||||||
- [Cascades scan-to-folder uses svc-scan](feedback_cascades_scan_account.md) — Every scanner->network-folder setup at Cascades reuses the one `svc-scan` AD service account (NTLMv2, vaulted); never make a per-printer scan account.
|
- [Graph password reset needs a privileged role](feedback_graph_password_reset_requires_role.md) — PATCH passwordProfile on an existing user 403s without a directory role; User.ReadWrite.All alone only sets a password at CREATE.
|
||||||
- [Drive-letter mapping convention](feedback_drive_letter_mapping.md) — Pick the MAIN/primary drive letter first (consistent across users for the principal share), then assign smaller/secondary maps. Don't retroactively renumber existing maps unless asked.
|
- [Vault writes — do the full sequence yourself](feedback_complete_vault_operations_end_to_end.md) — A vault entry = write plaintext → sops -e -i → git add/commit/push, all of it; don't stop at "encrypted on disk."
|
||||||
- [Calibrate effort to stakes](feedback_calibrate_effort_to_stakes.md) — Don't over-verify or over-engineer low-consequence details; confirm the happy path, note the limitation, and take the simplest path (e.g. put the instruction in the prompt) instead of building robust mechanisms.
|
- [Exchange role recurring gap — backfill, don't promise](feedback_exchange_role_recurring_gap.md) — EXO email-cleanup 401/403 = Exchange Operator SP missing the Exchange Admin directory role (consent never grants it). Fix: `assign-exchange-role.sh <domain|--all>` (idempotent); audit with `--all --verify`. Fleet backfilled 2026-06-08. Verify membership via roleManagement/directory/roleAssignments (not the laggy directoryRoles/members list); EXO propagation 15-60min.
|
||||||
- [Pricing verification — no guessing](policy_pricing_verification.md) — ANY cost presented to the team or a client MUST be verified via live web lookup (WebFetch/WebSearch, fallback to headless Chrome). Never estimate from training data. Cite source + date inline. If unreachable, say so — do NOT substitute a guess.
|
- [Syncro is the default PSA; Autotask is opt-in](feedback_psa_default_syncro.md) — Ticketing/billing/customers default to Syncro (/syncro). Only use /autotask on an explicit "in Autotask" request. /autotask kept local/undistributed.
|
||||||
- [Client communication tone](feedback_client_tone.md) — How to write client-facing Syncro comments — expert partner, not intake questionnaire.
|
- [Paste-safe command formatting (Howard)](feedback_command_formatting.md) — Two clauses, one root cause: (a) multi-line scripts not semicolon one-liners (wrap breaks paste), (b) all code at column 0 inside fences (indentation breaks PowerShell paste).
|
||||||
- [Impeccable on outbound](feedback_impeccable_on_outbound.md) — Run the `impeccable` skill on anything sent to a client or vendor before delivery; internal drafts exempt.
|
- [Autonomous infra/build setup](feedback_autonomous_infra_setup.md) — During infra/build/CI/dev setup, just install prerequisites and push through routine steps; reserve check-ins for genuine decisions (forks, destructive/outward, client/prod).
|
||||||
- [Default to inline links](feedback_inline_links.md) — Use `[text](url)` inline markdown links (clickable, wrap-safe) not bare URLs in code fences; exception = raw URL the user must copy/paste.
|
- [Check patterns before asking](feedback_check_patterns_before_asking.md) — Before asking how to do something repeat-style (sync, save, sweep, billing), study existing artifacts and workflow docs first; reach for similar past artifacts as the template.
|
||||||
- [Add Mike as owner on all Entra apps](feedback_entra_app_owner.md) — Apps created via management SP have no user owner — must add Mike manually or publisher verification fails.
|
- [Cascades scan-to-folder uses svc-scan](feedback_cascades_scan_account.md) — Every scanner->network-folder setup at Cascades reuses the one `svc-scan` AD service account (NTLMv2, vaulted); never make a per-printer scan account.
|
||||||
- [No TOML/config file approach for endpoints](feedback_no_toml_config_endpoints.md) — User explicitly prohibits TOML or config-file-based endpoint configuration — this will never be approved.
|
- [Drive-letter mapping convention](feedback_drive_letter_mapping.md) — Pick the MAIN/primary drive letter first (consistent across users for the principal share), then assign smaller/secondary maps. Don't retroactively renumber existing maps unless asked.
|
||||||
- [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL.
|
- [Calibrate effort to stakes](feedback_calibrate_effort_to_stakes.md) — Don't over-verify or over-engineer low-consequence details; confirm the happy path, note the limitation, and take the simplest path (e.g. put the instruction in the prompt) instead of building robust mechanisms.
|
||||||
- [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending.
|
- [Pricing verification — no guessing](policy_pricing_verification.md) — ANY cost presented to the team or a client MUST be verified via live web lookup (WebFetch/WebSearch, fallback to headless Chrome). Never estimate from training data. Cite source + date inline. If unreachable, say so — do NOT substitute a guess.
|
||||||
- [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects/<slug>/*.jsonl`; the detector auto-recovers orphans, `/recover <uuid>` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`.
|
- [Client communication tone](feedback_client_tone.md) — How to write client-facing Syncro comments — expert partner, not intake questionnaire.
|
||||||
- [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work.
|
- [Impeccable on outbound](feedback_impeccable_on_outbound.md) — Run the `impeccable` skill on anything sent to a client or vendor before delivery; internal drafts exempt.
|
||||||
- [Don't present inferred topology as fact](feedback_no_inferred_topology_as_fact.md) — Private-IP overlap (172.16.x on both sides) is NOT proof of a site-to-site link; I fabricated a VWP<->office VPN. State observations vs inferences; a failed reachability test disproves a link, don't explain it away; test "can reach RMM" against the EXTERNAL endpoint, not internal 172.16.3.30.
|
- [Default to inline links](feedback_inline_links.md) — Use `[text](url)` inline markdown links (clickable, wrap-safe) not bare URLs in code fences; exception = raw URL the user must copy/paste.
|
||||||
|
- [Add Mike as owner on all Entra apps](feedback_entra_app_owner.md) — Apps created via management SP have no user owner — must add Mike manually or publisher verification fails.
|
||||||
### Syncro
|
- [No TOML/config file approach for endpoints](feedback_no_toml_config_endpoints.md) — User explicitly prohibits TOML or config-file-based endpoint configuration — this will never be approved.
|
||||||
- [Skill-first routing + Definition of Done](feedback_skill_first_routing.md) — If a skill covers the task, INVOKE IT (Syncro billing ALWAYS via `/syncro`, never ad-hoc curl), AND gate the result with the matching check-skill before "done" (code→/code-review+/simplify+/security-review; outbound→impeccable/stop-slop; wiki/memory→wiki-lint/memory-dream) — inferred from the request, not user-named. CORE rule; full map in `.claude/SKILL_ROUTING.md`; calibrate to stakes, Ollama Tier-0 for cheap passes.
|
- [Python on Windows — use py launcher](feedback_python_windows.md) — Windows Store python/python3 aliases disabled; always use py or jq on DESKTOP-0O8A1RL.
|
||||||
- [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `<br>` not `<ul>/<li>`; timer_entry response is FLAT but SUPERSEDED (use add_line_item).
|
- [Memory tooling may delete now — additive-only constraint dropped](feedback_memory_sync_destructive_ok.md) — As of 2026-06-02, memory-dream and sync-memory.sh are sanctioned to perform destructive ops (apply proposed merges/dedups, propagate repo deletions back to harness profile stores). Onboarding-phase safety net now fights deliberate consolidation (e.g. 2026-06-01's 39 deletions resurrected on the next sync). Script updates pending.
|
||||||
- [Syncro billing rules](feedback_syncro_billing.md) — Bill with `add_line_item` directly (not timers); fetch rates LIVE; never invent labor names (real product names only); match labor type to delivery channel (never "Prepaid project labor"); labor `taxable:false` (AZ); warranty `1049360` (never patch price); emergency `26184` ×1.5 once, branch by `prepay_hours`; corrections preserve original tech's user_id; estimate hardware `32252`.
|
- [Unsaved sessions are recoverable from transcripts](feedback_session_recovery.md) — Crashed/closed-before-save sessions live in `~/.claude/projects/<slug>/*.jsonl`; the detector auto-recovers orphans, `/recover <uuid>` does it manually. Ollama prose + Python verbatim. See `.claude/RECOVERY.md`.
|
||||||
- [Syncro workflow rules](feedback_syncro_workflow.md) — ALWAYS preview comments before posting (no exceptions); verify appointment day-of-week ("Saturday 2026-05-23") before creating; ASK who the appointment owner is; leave `contact_id` BLANK by default for ALL customers (ignore Syncro's contact-picker auto-default).
|
- [Scheduled tasks must not flash a console](feedback_scheduled_task_no_console_flash.md) — A task action running bash.exe/py.exe/python.exe with Hidden=False draws a console window every fire (the recurring "command prompt opening/closing"). Wrap bash via a wscript VBS (style 0), use pythonw.exe for python, always add `-Hidden`. Fixed installers: register-edr-watcher.ps1, register-orphan-detector.ps1.
|
||||||
- [Syncro lessons / incident archive](feedback_syncro_history.md) — Detail behind the three rule files: tickets (#32332, #32312, #32225, #32253, #32203, #32185, #32142, #32304, #32333), verbatim Mike/Howard/Winter quotes, dates, tech user_id table (Mike 1735 / Howard 1750 / Winter 1737 / Rob 1760), labor product table, and superseded-rule history.
|
- [agy review is not read-only](feedback_agy_review_not_readonly.md) — agy review/review-files CAN write files + run npm despite docs claiming plan-mode; always git diff after and treat Gemini's output as a proposal to validate, not trusted/finished work.
|
||||||
|
- [Don't present inferred topology as fact](feedback_no_inferred_topology_as_fact.md) — Private-IP overlap (172.16.x on both sides) is NOT proof of a site-to-site link; I fabricated a VWP<->office VPN. State observations vs inferences; a failed reachability test disproves a link, don't explain it away; test "can reach RMM" against the EXTERNAL endpoint, not internal 172.16.3.30.
|
||||||
### GuruRMM
|
|
||||||
- [GuruRMM build verification (read before touching the pipeline)](feedback_gururmm_build_verification.md) — Merge-to-main IS the build+deploy; verify locally FIRST. Canonical refs: guru-rmm `docs/BUILD.md` + the `gururmm-build` skill (`verify.sh server|agent|dashboard|migrations`) + `deploy/build-pipeline/README.md`. Compile-gate trap: Windows cargo can't verify Linux-gated agent code (openssl-sys); Linux build on .30 is the real gate. Server needs SQLX_OFFLINE + fresh server/.sqlx; check migration-number collisions.
|
### Syncro
|
||||||
- [Submodule auto-sync discipline](feedback_submodule_autosync_discipline.md) — In auto-synced submodules (guru-rmm/guru-connect) local branch refs/HEAD don't survive across calls (background sync resets to the lagging gitlink; sessions share the tree). Use a git worktree or commit+push-by-explicit-SHA + `ls-remote` verify; assert HEAD==origin/main (or read `origin/main:<file>`) before audits; never `checkout --` shared files. Recurring fleet friction.
|
- [Skill-first routing + Definition of Done](feedback_skill_first_routing.md) — If a skill covers the task, INVOKE IT (Syncro billing ALWAYS via `/syncro`, never ad-hoc curl), AND gate the result with the matching check-skill before "done" (code→/code-review+/simplify+/security-review; outbound→impeccable/stop-slop; wiki/memory→wiki-lint/memory-dream) — inferred from the request, not user-named. CORE rule; full map in `.claude/SKILL_ROUTING.md`; calibrate to stakes, Ollama Tier-0 for cheap passes.
|
||||||
- [GuruRMM operational rules](feedback_gururmm.md) — Six rules: (1) RMM dev = Mike, never Howard (368/0 commits); GuruScan is Howard's. (2) Agent parity Win+Linux+macOS in same change. (3) Builds via Gitea webhook pipeline only, never SSH. (4) #bot-alerts only for client/ticket impact, skip internal infra/dev. (5) Identify agents by IP, not by reconning candidates. (6) UNC paths in user_session need [char]92 — literals get halved.
|
- [Syncro API plumbing](feedback_syncro_api.md) — Content-Type required on all POST/PUT; NO idempotency anywhere — always GET before retrying; response wrappers (`.ticket.id`, `.comment.id`); add_line_item shape (internal ID, flat response, required fields); HTML uses `<br>` not `<ul>/<li>`; timer_entry response is FLAT but SUPERSEDED (use add_line_item).
|
||||||
- [Build channel default = beta](feedback_gururmm_build_channel_default.md) — New agent builds must be tagged BETA by default (stable = explicit promote re-tag); distinct from agents defaulting to the stable CHANNEL (correct). Fixed build-windows/linux.sh 2026-06-01; macOS already correct. Enables beta-first canary.
|
- [Syncro billing rules](feedback_syncro_billing.md) — Bill with `add_line_item` directly (not timers); fetch rates LIVE; never invent labor names (real product names only); match labor type to delivery channel (never "Prepaid project labor"); labor `taxable:false` (AZ); warranty `1049360` (never patch price); emergency `26184` ×1.5 once, branch by `prepay_hours`; corrections preserve original tech's user_id; estimate hardware `32252`.
|
||||||
- [Dashboard beta-first deploy](feedback_dashboard_beta_first.md) — Dashboard auto-builds to rmm-beta.azcomputerguru.com on push; prod (rmm.azcomputerguru.com) is explicit promote-only via promote-dashboard.sh --confirm. Never hand-rsync prod. One artifact, nginx sub_filter BETA banner. Stood up 2026-06-02.
|
- [Syncro workflow rules](feedback_syncro_workflow.md) — ALWAYS preview comments before posting (no exceptions); verify appointment day-of-week ("Saturday 2026-05-23") before creating; ASK who the appointment owner is; leave `contact_id` BLANK by default for ALL customers (ignore Syncro's contact-picker auto-default).
|
||||||
|
- [Syncro lessons / incident archive](feedback_syncro_history.md) — Detail behind the three rule files: tickets (#32332, #32312, #32225, #32253, #32203, #32185, #32142, #32304, #32333), verbatim Mike/Howard/Winter quotes, dates, tech user_id table (Mike 1735 / Howard 1750 / Winter 1737 / Rob 1760), labor product table, and superseded-rule history.
|
||||||
### Cascades
|
|
||||||
- [Cascades operational rules](feedback_cascades.md) — Active rules: (1) folder redirection (fdeploy) needs subfolders PRE-CREATED before first logon or it caches a failure forever; recovery via fix-shell-redirect.ps1. (2) ALWAYS ask which security group(s) a new user goes into — never auto-derive from OU. (3) Do NOT lock down the legacy Main\Company Web Docs\Accounting (Everyone:Full) folder — still in active use. (4) NEVER change Cascades production infra (pfSense/UniFi/switches/DHCP) without discussing it + explicit per-change go — read-only/dry-run until then.
|
### GuruRMM
|
||||||
- [Cascades FR GPO fix](reference_cascades_fr_gpo_fix.md) — Native Folder Redirection was DOA on every machine: redirect targets were in a misnamed `fdeploy1.ini` (Windows reads `fdeploy.ini`) → empty target path → silent no-op → per-user registry workaround every time. Fixed 2026-06-08 (correct fdeploy.ini + version bump). Also: CS-SERVER live RMM agent is `c39f1de7...` (old `6766e973` stale).
|
- [GuruRMM build verification (read before touching the pipeline)](feedback_gururmm_build_verification.md) — Merge-to-main IS the build+deploy; verify locally FIRST. Canonical refs: guru-rmm `docs/BUILD.md` + the `gururmm-build` skill (`verify.sh server|agent|dashboard|migrations`) + `deploy/build-pipeline/README.md`. Compile-gate trap: Windows cargo can't verify Linux-gated agent code (openssl-sys); Linux build on .30 is the real gate. Server needs SQLX_OFFLINE + fresh server/.sqlx; check migration-number collisions.
|
||||||
- [pfSense 25.07 ops quirks](reference_pfsense_25_07_ops.md) — Cascades pfSense Plus 25.07: logs are PLAIN TEXT (use tail/grep, NOT clog → clog returns empty); clean dhcpd restart = `services_dhcpd_configure()` via slow pfSsh.php (needs 50s+ timeout); dirty boot can leave 2 dhcpd → DISCOVER/OFFER but no ACK; reboot the Cox modem after a config restore; ZFS survives power loss. From the 2026-06-17 power-outage incident.
|
- [Submodule auto-sync discipline](feedback_submodule_autosync_discipline.md) — In auto-synced submodules (guru-rmm/guru-connect) local branch refs/HEAD don't survive across calls (background sync resets to the lagging gitlink; sessions share the tree). Use a git worktree or commit+push-by-explicit-SHA + `ls-remote` verify; assert HEAD==origin/main (or read `origin/main:<file>`) before audits; never `checkout --` shared files. Recurring fleet friction.
|
||||||
- [feedback_ascii_only_api_payloads](feedback_ascii_only_api_payloads.md) -- On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc.
|
- [GuruRMM operational rules](feedback_gururmm.md) — Six rules: (1) RMM dev = Mike, never Howard (368/0 commits); GuruScan is Howard's. (2) Agent parity Win+Linux+macOS in same change. (3) Builds via Gitea webhook pipeline only, never SSH. (4) #bot-alerts only for client/ticket impact, skip internal infra/dev. (5) Identify agents by IP, not by reconning candidates. (6) UNC paths in user_session need [char]92 — literals get halved.
|
||||||
- [feedback_bitdefender_unattended_install](feedback_bitdefender_unattended_install.md) -- Bitdefender unattended RMM install must use the FULL KIT as SYSTEM (silent, no UAC) — the downloader stub fails headless and triggers UAC
|
- [Build channel default = beta](feedback_gururmm_build_channel_default.md) — New agent builds must be tagged BETA by default (stable = explicit promote re-tag); distinct from agents defaulting to the stable CHANNEL (correct). Fixed build-windows/linux.sh 2026-06-01; macOS already correct. Enables beta-first canary.
|
||||||
- [feedback_rmm_longops_fire_and_forget](feedback_rmm_longops_fire_and_forget.md) -- Long-running RMM endpoint ops (software installs, big downloads) must be fire-and-forget, not live-monitored
|
- [Dashboard beta-first deploy](feedback_dashboard_beta_first.md) — Dashboard auto-builds to rmm-beta.azcomputerguru.com on push; prod (rmm.azcomputerguru.com) is explicit promote-only via promote-dashboard.sh --confirm. Never hand-rsync prod. One artifact, nginx sub_filter BETA banner. Stood up 2026-06-02.
|
||||||
|
|
||||||
## Machine
|
### Cascades
|
||||||
- [GURU-5070 Workstation Setup](reference_workstation_setup.md) — Mike's primary (owner confirmed 2026-05-26). Windows 11 Pro. Renamed from OC-5070 → ACG-5070/acg-guru-5070 → GURU-5070; all the same box, all Mike's.
|
- [Cascades operational rules](feedback_cascades.md) — Active rules: (1) folder redirection (fdeploy) needs subfolders PRE-CREATED before first logon or it caches a failure forever; recovery via fix-shell-redirect.ps1. (2) ALWAYS ask which security group(s) a new user goes into — never auto-derive from OU. (3) Do NOT lock down the legacy Main\Company Web Docs\Accounting (Everyone:Full) folder — still in active use. (4) NEVER change Cascades production infra (pfSense/UniFi/switches/DHCP) without discussing it + explicit per-change go — read-only/dry-run until then.
|
||||||
- [GURU-BEAST-ROG Setup Status](machine_windows_guru_setup_status.md) — Windows workstation fully configured except SSH key deployment to servers.
|
- [Cascades FR GPO fix](reference_cascades_fr_gpo_fix.md) — Native Folder Redirection was DOA on every machine: redirect targets were in a misnamed `fdeploy1.ini` (Windows reads `fdeploy.ini`) → empty target path → silent no-op → per-user registry workaround every time. Fixed 2026-06-08 (correct fdeploy.ini + version bump). Also: CS-SERVER live RMM agent is `c39f1de7...` (old `6766e973` stale).
|
||||||
|
- [pfSense 25.07 ops quirks](reference_pfsense_25_07_ops.md) — Cascades pfSense Plus 25.07: logs are PLAIN TEXT (use tail/grep, NOT clog → clog returns empty); clean dhcpd restart = `services_dhcpd_configure()` via slow pfSsh.php (needs 50s+ timeout); dirty boot can leave 2 dhcpd → DISCOVER/OFFER but no ACK; reboot the Cox modem after a config restore; ZFS survives power loss. From the 2026-06-17 power-outage incident.
|
||||||
## Project
|
- [feedback_ascii_only_api_payloads](feedback_ascii_only_api_payloads.md) -- On Windows/Git-bash, non-ASCII chars (em-dash, arrow, smart quotes) in JSON payload TEXT passed to curl get mangled and rejected — Discord bot-alert returns 400, the coord API returns "error parsing the body". Use ASCII-only in API payload text, or a single-quoted heredoc.
|
||||||
- [Cascades network + CS-SERVER SMB instability](project_cascades_network_segments.md) — NAS->CS-SERVER migration. NOT a CSC-ENT-vs-CSCNet issue (corrected): fresh SMB to `\\cs-server\*` fails err 67 from BOTH Wi-Fis; only persistent admin-mapped drives work, intermittently → CS-SERVER-side SMB instability (multichannel advertises .248/.254/IPv6 ULAs; Get-SmbServerConfiguration errors). Blockers: CSCNet=WPA3 (old adapters can't join); workstations store domain-admin cred. Repoint tool [[drive-map]].
|
- [feedback_bitdefender_unattended_install](feedback_bitdefender_unattended_install.md) -- Bitdefender unattended RMM install must use the FULL KIT as SYSTEM (silent, no UAC) — the downloader stub fails headless and triggers UAC
|
||||||
- [CyndyOffice physical HP lockups](cyndyoffice-physical-hp-lockups.md) — RMM "Howard-VM" site agent CyndyOffice is a PHYSICAL HP Pavilion TP01 (not a VM); ~20 hard freezes/6wk = Kernel-Power 41 bugcheck-0, no dump/WHEA = hardware (RAM/PSU/BIOS), SSD healthy. UUID re-enrolls.
|
- [feedback_rmm_longops_fire_and_forget](feedback_rmm_longops_fire_and_forget.md) -- Long-running RMM endpoint ops (software installs, big downloads) must be fire-and-forget, not live-monitored
|
||||||
- [Automate memory consolidation/lint (phased)](project_memory_consolidation_automation.md) — Eventually auto-run /memory-dream; lint+additive fixes can automate early, merges/deletes stay human-approved. Engine: .claude/skills/memory-dream/ + .claude/scripts/sync-memory.sh.
|
|
||||||
- [Trebesch PST consolidation (staged)](project_trebesch_pst_consolidation.md) — Address-book CSV from 24 PSTs on DESKTOP-QNP3ON5; scripts staged at .claude/tmp/treb-*.ps1, WAITING for Howard's 6pm-MST 2026-06-01 go signal (attended run). See [[reference_trebesch_qnp3on5]].
|
## Machine
|
||||||
- [GuruRMM security scope — integrate AV, don't replace it](project_gururmm_security_scope.md) — No native virus/malware removal in the RMM; AV products do that. RMM monitors AV reports + sends commands to AV products, and its built-in value is helping techs FIND issues. Program removal is a separate feature.
|
- [GURU-5070 Workstation Setup](reference_workstation_setup.md) — Mike's primary (owner confirmed 2026-05-26). Windows 11 Pro. Renamed from OC-5070 → ACG-5070/acg-guru-5070 → GURU-5070; all the same box, all Mike's.
|
||||||
- [GuruRMM project state](project_gururmm.md) — Dev principles (every feature full-stack: backend+API+UI+docs+scalability; product works without AI; FEATURE_ROADMAP update is part of definition-of-done; mirrors guru-rmm/docs/DESIGN.md). Webhook docs-only build guard (SPEC-020 Phase 0; webhook-handler.py repo copy is STALE — don't redeploy). Mac install-hooks.sh setup STILL PENDING on Mikes-MacBook-Air.
|
- [GURU-BEAST-ROG Setup Status](machine_windows_guru_setup_status.md) — Windows workstation fully configured except SSH key deployment to servers.
|
||||||
- [GuruConnect](project_guruconnect.md) — v2 direction (native-first full key fidelity Win+R/Ctrl+Alt+Del + bidirectional file cut/paste/drag; WebRTC fallback only; standalone-first + RMM contract; tenancy-ready schema; Mike willing to scrap v1). Manual deploy procedure to 172.16.3.30 (build-on-server in login shell; sqlx runtime queries; NPM `CONNECT_TRUSTED_PROXIES=172.16.3.20` gotcha). v2 live since 2026-05-30.
|
|
||||||
- [Apple MDM + Developer certs (GuruRMM mobile)](project_apple_mdm_certs.md) — ACG holds Apple Developer+signing and Apple MDM Push certs (acquired 2026-05-29) for SPEC-017. MDM push cert RENEWS ANNUALLY on the same Apple ID or all enrolled iOS devices break.
|
## Project
|
||||||
- [Only RMM & GC are versionable products](project_versionable_products.md) — GuruRMM + GuruConnect are the only products with own repos/submodules; everything else stays in the claudetools monorepo. Split only for independent pipeline OR versioned external consumer.
|
- [GuruRMM software-removal PR #58](project_gururmm_software_removal_pr58.md) — SPEC-030 hardening (driver exclusion in removal UI, lingering-uninstaller sweep, job list + stale reaper) on PR #58, UNMERGED; merge = beta deploy; live testing planned 2026-07-06 on DESKTOP-MS42HNC.
|
||||||
- [Quantum GoDaddy M365 tenant](project_quantum_godaddy_m365_tenant.md) — quantumwms.com parked in a GoDaddy-provisioned M365 tenant (id ddf3d2c9-b76c-40d9-a216-9f11a1a26f97, netorg18235235.onmicrosoft.com); blocks Pax8 migration until GoDaddy removed.
|
- [Cascades network + CS-SERVER SMB instability](project_cascades_network_segments.md) — NAS->CS-SERVER migration. NOT a CSC-ENT-vs-CSCNet issue (corrected): fresh SMB to `\\cs-server\*` fails err 67 from BOTH Wi-Fis; only persistent admin-mapped drives work, intermittently → CS-SERVER-side SMB instability (multichannel advertises .248/.254/IPv6 ULAs; Get-SmbServerConfiguration errors). Blockers: CSCNet=WPA3 (old adapters can't join); workstations store domain-admin cred. Repoint tool [[drive-map]].
|
||||||
- [Howard-Home LAN shadow (RESOLVED)](howard-home-lan-shadow.md) — Howard-Home renumbered 2026-06-16 to **10.137.42.0/24** (gw 10.137.42.1, UniFi — NOT pfSense), off the old 192.168.0.0/24 that shadowed Cascades pfSense .0.x over the VPN. Cascades .0.x should now route via the tunnel; this machine is 10.137.42.x now (not 192.168.0.x).
|
- [CyndyOffice physical HP lockups](cyndyoffice-physical-hp-lockups.md) — RMM "Howard-VM" site agent CyndyOffice is a PHYSICAL HP Pavilion TP01 (not a VM); ~20 hard freezes/6wk = Kernel-Power 41 bugcheck-0, no dump/WHEA = hardware (RAM/PSU/BIOS), SSD healthy. UUID re-enrolls.
|
||||||
- [Cascades CARF tech plan](project_cascades_carf_tech_plan.md) — Ashley's "technology plan" is a CARF accreditation deliverable (Aging Services Technology and System Plan standard); must use CARF action-plan structure (owner/cost/target+completion dates per area), persons-served assistive-tech lens, annual-review sign-off; it's Cascades' leadership-adopted plan, ACG supplies content.
|
- [Automate memory consolidation/lint (phased)](project_memory_consolidation_automation.md) — Eventually auto-run /memory-dream; lint+additive fixes can automate early, merges/deletes stay human-approved. Engine: .claude/skills/memory-dream/ + .claude/scripts/sync-memory.sh.
|
||||||
- [Cascades](project_cascades.md) — Active state: Syncro ticket #110680053 + plan file (machine-specific path on Howard's box), admin accounts (sysadmin@=Howard, admin@=Mike — daily-driver, NOT break-glass), Phase-B caregiver CA pilot (SG-Caregivers-Pilot, group-scoped never tenant-wide), prepaid block ~37.5h (rate TBD), pilot cleanup checklist.
|
- [Trebesch PST consolidation (staged)](project_trebesch_pst_consolidation.md) — Address-book CSV from 24 PSTs on DESKTOP-QNP3ON5; scripts staged at .claude/tmp/treb-*.ps1, WAITING for Howard's 6pm-MST 2026-06-01 go signal (attended run). See [[reference_trebesch_qnp3on5]].
|
||||||
- [Cascades history](project_cascades_history.md) — fdeploy 502/ACL root cause (Flags=1211→187 fix), 2026-04-29 CA-rescoping decision (Howard pulled the brakes on tenant-wide), 2026-05-14 per-user-security-group decision rationale.
|
- [GuruRMM security scope — integrate AV, don't replace it](project_gururmm_security_scope.md) — No native virus/malware removal in the RMM; AV products do that. RMM monitors AV reports + sends commands to AV products, and its built-in value is helping techs FIND issues. Program removal is a separate feature.
|
||||||
- [Cascades isolated-VLAN pattern](project_cascades_isolated_vlan_pattern.md) — pfSense: the GUEST VLAN (VLAN50/igc1.50) is the isolation template (4 any-proto quick rules: block 192.168.0.0/22 + 10.0.0.0/8 + 172.16.0.0/12, then pass any; public DNS via DHCP). VLAN20 is NOT isolated. Verify with `pfctl -sr`, not config.xml. Protocol MUST be Any (TCP-only leaks UDP). VOICE VLAN30 built to this 2026-06-17.
|
- [GuruRMM project state](project_gururmm.md) — Dev principles (every feature full-stack: backend+API+UI+docs+scalability; product works without AI; FEATURE_ROADMAP update is part of definition-of-done; mirrors guru-rmm/docs/DESIGN.md). Webhook docs-only build guard (SPEC-020 Phase 0; webhook-handler.py repo copy is STALE — don't redeploy). Mac install-hooks.sh setup STILL PENDING on Mikes-MacBook-Air.
|
||||||
- [Cascades VLAN20 migration + routing](project_cascades_vlan20_migration_routing.md) — Staff machines/printers moving to VLAN20 (10.0.20.0/24). CS-SERVER couldn't reach VLAN20 printers because the LAN "allow LAN to any" rule policy-routes via WAN_Group → add a top LAN pass rule (src CS-SERVER 192.168.2.248, dst 10.0.20.0/24, gw=default) to bypass. pfSense SSH from VPN is blocked (do firewall in GUI). Printer client-map via GPO or SYSTEM `printui /ga` to dodge the 0x800702e4 PrintNightmare prompt; build UNC with [char]92.
|
- [GuruConnect](project_guruconnect.md) — v2 direction (native-first full key fidelity Win+R/Ctrl+Alt+Del + bidirectional file cut/paste/drag; WebRTC fallback only; standalone-first + RMM contract; tenancy-ready schema; Mike willing to scrap v1). Manual deploy procedure to 172.16.3.30 (build-on-server in login shell; sqlx runtime queries; NPM `CONNECT_TRUSTED_PROXIES=172.16.3.20` gotcha). v2 live since 2026-05-30.
|
||||||
- [Cascades KPI dashboard (parked)](project_cascades_kpi_dashboard.md) — Ashley Jensen wants one dashboard across their reporting SaaS (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Power BI Gateway is the WRONG frame (on-prem only). Recommended Tier1→Tier2: scheduled exports → SharePoint → Power BI Pro, automate API-capable systems (Bill.com/QBO) via Power Automate later. Full notes: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`. Next: draft client one-pager.
|
- [Apple MDM + Developer certs (GuruRMM mobile)](project_apple_mdm_certs.md) — ACG holds Apple Developer+signing and Apple MDM Push certs (acquired 2026-05-29) for SPEC-017. MDM push cert RENEWS ANNUALLY on the same Apple ID or all enrolled iOS devices break.
|
||||||
- [Sync script bug — untracked files (RESOLVED)](project_sync_script_bug.md) — FIXED 2026-05-21: sync.sh now uses `git status --porcelain` for change detection (repo + vault).
|
- [Only RMM & GC are versionable products](project_versionable_products.md) — GuruRMM + GuruConnect are the only products with own repos/submodules; everything else stays in the claudetools monorepo. Split only for independent pipeline OR versioned external consumer.
|
||||||
- [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/.
|
- [GuruRMM legacy 1.77 build disabled](project_gururmm_legacy_disabled.md) — legacy (2008R2/Win7) Rust 1.77 variant DISABLED 2026-07-04 (`LEGACY_ENABLED=false`) after an edition2024 dep (chacha20/rand_core 0.10.1) broke the unpinned 1.77 re-resolve and fail-closed the whole Windows build; existing legacy artifacts preserved at 0.6.76. Do NOT re-enable blindly — 2008R2 support returns via a C++/.NET or clean-rewrite legacy agent (Mike, required).
|
||||||
- [AMPIPIT toolkit](project_ampipit.md) — Howard's ACTIVE Rust+Slint Windows deploy/recovery toolkit (not just a reference program), migrated into the fleet as a private Gitea submodule at projects/msp-tools/ampipit (azcomputerguru/ampipit). Active feature: recovery partition with GuruRMM agent + built-in repair scripts for offline-Windows repair (spike GO-WITH-WORK). Preserve all work; PRIVATE Gitea only, no public GitHub.
|
- [Quantum GoDaddy M365 tenant](project_quantum_godaddy_m365_tenant.md) — quantumwms.com parked in a GoDaddy-provisioned M365 tenant (id ddf3d2c9-b76c-40d9-a216-9f11a1a26f97, netorg18235235.onmicrosoft.com); blocks Pax8 migration until GoDaddy removed.
|
||||||
- [Audio Processor Architecture](project_audio_processor_architecture.md) — Segment-first pipeline: detect breaks before transcription for complete content capture.
|
- [Howard-Home LAN shadow (RESOLVED)](howard-home-lan-shadow.md) — Howard-Home renumbered 2026-06-16 to **10.137.42.0/24** (gw 10.137.42.1, UniFi — NOT pfSense), off the old 192.168.0.0/24 that shadowed Cascades pfSense .0.x over the VPN. Cascades .0.x should now route via the tunnel; this machine is 10.137.42.x now (not 192.168.0.x).
|
||||||
- [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) — Full SBR routing chain, config file locations, MailProtector integration, access methods. Treat routing breakage as systemic (devcon, Sorensen/rieussetcorp), not per-client.
|
- [Cascades CARF tech plan](project_cascades_carf_tech_plan.md) — Ashley's "technology plan" is a CARF accreditation deliverable (Aging Services Technology and System Plan standard); must use CARF action-plan structure (owner/cost/target+completion dates per area), persons-served assistive-tech lens, annual-review sign-off; it's Cascades' leadership-adopted plan, ACG supplies content.
|
||||||
- [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) — Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken.
|
- [Cascades](project_cascades.md) — Active state: Syncro ticket #110680053 + plan file (machine-specific path on Howard's box), admin accounts (sysadmin@=Howard, admin@=Mike — daily-driver, NOT break-glass), Phase-B caregiver CA pilot (SG-Caregivers-Pilot, group-scoped never tenant-wide), prepaid block ~37.5h (rate TBD), pilot cleanup checklist.
|
||||||
- [Dataforth](project_dataforth.md) — M365 email (Graph API; tenant in vault at clients/dataforth/m365.sops.yaml); neptune.acghosting.com is ACG's, NOT Dataforth's. MFA enforced 2026-04-04 (3 CA policies). AJ needs dataforthgit@ forwarding.
|
- [Cascades history](project_cascades_history.md) — fdeploy 502/ACL root cause (Flags=1211→187 fix), 2026-04-29 CA-rescoping decision (Howard pulled the brakes on tenant-wide), 2026-05-14 per-user-security-group decision rationale.
|
||||||
- [Dataforth history (2026-03-27 incident)](project_dataforth_history.md) — DF-JOEL2 compromise via ScreenConnect social-engineering, attacker C2 IPs + IC3 case + remediation log + MFA rollout origin story + Joel Lohr retirement. RESOLVED 2026-04-04.
|
- [Cascades isolated-VLAN pattern](project_cascades_isolated_vlan_pattern.md) — pfSense: the GUEST VLAN (VLAN50/igc1.50) is the isolation template (4 any-proto quick rules: block 192.168.0.0/22 + 10.0.0.0/8 + 172.16.0.0/12, then pass any; public DNS via DHCP). VLAN20 is NOT isolated. Verify with `pfctl -sr`, not config.xml. Protocol MUST be Any (TCP-only leaks UDP). VOICE VLAN30 built to this 2026-06-17.
|
||||||
- [Radio show co-host — Tara, not Tom](radio_show_no_cohost_named_tom.md) — Co-host in 2014-s6e19 and 2016-s8e43 is Tara. "Tom" was hallucinated; rename complete.
|
- [Cascades VLAN20 migration + routing](project_cascades_vlan20_migration_routing.md) — Staff machines/printers moving to VLAN20 (10.0.20.0/24). CS-SERVER couldn't reach VLAN20 printers because the LAN "allow LAN to any" rule policy-routes via WAN_Group → add a top LAN pass rule (src CS-SERVER 192.168.2.248, dst 10.0.20.0/24, gw=default) to bypass. pfSense SSH from VPN is blocked (do firewall in GUI). Printer client-map via GPO or SYSTEM `printui /ga` to dodge the 0x800702e4 PrintNightmare prompt; build UNC with [char]92.
|
||||||
- [Proposal: centralize config in identity.json](proposal_identity_centralization.md) — Rationale for the identity.json machine-config centralization (claudetools_root, ollama/python); now implemented.
|
- [Cascades KPI dashboard (parked)](project_cascades_kpi_dashboard.md) — Ashley Jensen wants one dashboard across their reporting SaaS (ALIS/QuickBooks/Bill.com/Relias/You've Got Leads/TELS/Focus HR/Helpany/POS). Power BI Gateway is the WRONG frame (on-prem only). Recommended Tier1→Tier2: scheduled exports → SharePoint → Power BI Pro, automate API-capable systems (Bill.com/QBO) via Power Automate later. Full notes: `clients/cascades-tucson/docs/proposals/kpi-dashboard.md`. Next: draft client one-pager.
|
||||||
- [Defender: exempt all Claude activity](feedback_defender_claude_exclusions.md) — Mike wants NOTHING Claude/ClaudeTools issues flagged by Defender; ClickFix threat IDs 2147939088/2147945138 (RMM curl dispatch AMSI FPs) set to Allow + broad process/path exclusions. ThreatID-Allow is the lever; exclusions alone don't stop AMSI CmdLine detections.
|
- [Sync script bug — untracked files (RESOLVED)](project_sync_script_bug.md) — FIXED 2026-05-21: sync.sh now uses `git status --porcelain` for change detection (repo + vault).
|
||||||
- [ACG MSP tool stack](reference_acg_msp_stack.md) — ScreenConnect/CW Control, Splashtop, Syncro, Datto RMM, Datto EDR/AV, GuruRMM are ACG's OWN tools; do not flag as foreign/threat on managed machines (Defender-off is expected when Datto AV is active).
|
- [MasterBooter Side Project](project_masterbooter.md) — Howard's Rust+Slint Windows deployment toolkit at C:\MasterBooter, separate from client work. Do not log to clients/.
|
||||||
- [VoIP vendor stack: PacketDial / OIT / NetSapiens / YMCS](reference_packetdial_oit_netsapiens.md) — PacketDial = ACG's VoIP-dept brand (pbx.packetdial.com, the `packetdial` skill); NetSapiens = the PBX platform (API v2); OIT/OITVOIP = white-label wholesaler running NetSapiens (api.ucaasnetwork.com); reseller `91912.service`. YMCS (Yealink) = phone device-mgmt, pairs with the PBX.
|
- [AMPIPIT toolkit](project_ampipit.md) — Howard's ACTIVE Rust+Slint Windows deploy/recovery toolkit (not just a reference program), migrated into the fleet as a private Gitea submodule at projects/msp-tools/ampipit (azcomputerguru/ampipit). Active feature: recovery partition with GuruRMM agent + built-in repair scripts for offline-Windows repair (spike GO-WITH-WORK). Preserve all work; PRIVATE Gitea only, no public GitHub.
|
||||||
- [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel.
|
- [Audio Processor Architecture](project_audio_processor_architecture.md) — Segment-first pipeline: detect breaks before transcription for complete content capture.
|
||||||
- [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux).
|
- [Neptune SBR Email Routing Setup](project_neptune_sbr_email_routing.md) — Full SBR routing chain, config file locations, MailProtector integration, access methods. Treat routing breakage as systemic (devcon, Sorensen/rieussetcorp), not per-client.
|
||||||
- [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; now wrapped by the /screenconnect skill. Verified surface: GetSessionsByName/GetSessionDetails + writes SendCommand/SendMessage/UpdateCustomProperties + parameterized self-tagging installer. Still NO full-fleet inventory method (GetSessions missing).
|
- [Dataforth Test Datasheet Pipeline](project_datasheet_pipeline.md) — Full pipeline rebuilt 2026-03-27. Server-side generation replaces DFWDS/Uploader. Website upload still broken.
|
||||||
- [No manufactured guardrails on our products](feedback_no_manufactured_guardrails.md) — At Mikes request on GuruRMM/GuruConnect/ClaudeTools, just execute; stop only for genuinely irreversible/destructive ops (with a heads-up). Read the actual code/state before claiming something is disallowed or a security hole.
|
- [Dataforth](project_dataforth.md) — M365 email (Graph API; tenant in vault at clients/dataforth/m365.sops.yaml); neptune.acghosting.com is ACG's, NOT Dataforth's. MFA enforced 2026-04-04 (3 CA policies). AJ needs dataforthgit@ forwarding.
|
||||||
- [Stream-of-thought design convos](feedback_stream_of_thought_design.md) — Mike brainstorms features free-form, adding requirements iteratively; Claude validates/sharpens as a design partner but does NOT build until an explicit go, then captures parked threads durably (PARKED_*.md + todos) for a later /shape-spec.
|
- [Dataforth history (2026-03-27 incident)](project_dataforth_history.md) — DF-JOEL2 compromise via ScreenConnect social-engineering, attacker C2 IPs + IC3 case + remediation log + MFA rollout origin story + Joel Lohr retirement. RESOLVED 2026-04-04.
|
||||||
- [RMM Thoughts backlog](feedback_rmm_thoughts_backlog.md) — GuruRMM ideas from Mike & Howard go in projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md (Status: Raw); pipeline thought -> discuss -> spec (/shape-spec) -> roadmap. Don't build until an explicit go.
|
- [Radio show co-host — Tara, not Tom](radio_show_no_cohost_named_tom.md) — Co-host in 2014-s6e19 and 2016-s8e43 is Tara. "Tom" was hallucinated; rename complete.
|
||||||
- [Syncro preview mandatory](feedback_syncro_preview_mandatory.md) — preview+confirm every Syncro write, including internal notes
|
- [Proposal: centralize config in identity.json](proposal_identity_centralization.md) — Rationale for the identity.json machine-config centralization (claudetools_root, ollama/python); now implemented.
|
||||||
- [Refresh session history first](feedback_refresh_session_history_first.md) — read prior incident logs before acting; do not re-remediate already-handled accounts
|
- [Defender: exempt all Claude activity](feedback_defender_claude_exclusions.md) — Mike wants NOTHING Claude/ClaudeTools issues flagged by Defender; ClickFix threat IDs 2147939088/2147945138 (RMM curl dispatch AMSI FPs) set to Allow + broad process/path exclusions. ThreatID-Allow is the lever; exclusions alone don't stop AMSI CmdLine detections.
|
||||||
- [Autonomy scope](feedback_autonomy_scope.md) — confirm only for client-affecting actions; internal docs/wiki/ClaudeTools = act autonomously
|
- [ACG MSP tool stack](reference_acg_msp_stack.md) — ScreenConnect/CW Control, Splashtop, Syncro, Datto RMM, Datto EDR/AV, GuruRMM are ACG's OWN tools; do not flag as foreign/threat on managed machines (Defender-off is expected when Datto AV is active).
|
||||||
- [Check for client-slug fragmentation](feedback_client_slug_fragmentation.md) — Before concluding a client has no records, grep broadly (company/owner/initials/hostname/"Last, First") across clients/, wiki/, session-logs/, vault — one client gets split across slug variants (Wolkin was 4: wolkin/wolkin-law/rswolkin/robert-wolkin). Consolidate to one canonical slug; action prior logs' Pending items.
|
- [VoIP vendor stack: PacketDial / OIT / NetSapiens / YMCS](reference_packetdial_oit_netsapiens.md) — PacketDial = ACG's VoIP-dept brand (pbx.packetdial.com, the `packetdial` skill); NetSapiens = the PBX platform (API v2); OIT/OITVOIP = white-label wholesaler running NetSapiens (api.ucaasnetwork.com); reseller `91912.service`. YMCS (Yealink) = phone device-mgmt, pairs with the PBX.
|
||||||
- [RMM user_session = false SMB failures](feedback_rmm_user_session_smb_false_negative.md) — GuruRMM net use/net view/Add-Printer to a remote \HOST fail with error 67 / RPC 1702 (even with valid creds) because user_session is a WTS-impersonated non-interactive token that can't do authenticated SMB. The share/printer may work fine interactively. Treat RMM SMB results as "can't tell"; verify via ScreenConnect.
|
- [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel.
|
||||||
- [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — when a target has SSH (key auth) and it's easier, drive it via system OpenSSH (scp+ssh) instead of the GuruRMM agent; RMM runs as SYSTEM + is bound by the server-side timeout reaper + forces base64/quoting gymnastics. Reserve RMM as the fallback when SSH/VPN is down.
|
- [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux).
|
||||||
- [Broken [[backlinks]] = write-me-later markers](feedback_broken_backlinks_are_writeme_markers.md) — A [[name]] with no matching file is an intentional "worth writing" marker, not breakage. Flesh the missing memory out from session history/logs and index it; never strip the link to silence the warning. memory-dream reports these as INFO candidates, not errors.
|
- [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; now wrapped by the /screenconnect skill. Verified surface: GetSessionsByName/GetSessionDetails + writes SendCommand/SendMessage/UpdateCustomProperties + parameterized self-tagging installer. Still NO full-fleet inventory method (GetSessions missing).
|
||||||
- [gururmm session-logs are in a submodule](gururmm-session-logs-submodule-save.md) — commit in the submodule + `git push origin HEAD:main` (GURU-5070 CAN push over HTTP now); then advance the parent gitlink
|
- [No manufactured guardrails on our products](feedback_no_manufactured_guardrails.md) — At Mikes request on GuruRMM/GuruConnect/ClaudeTools, just execute; stop only for genuinely irreversible/destructive ops (with a heads-up). Read the actual code/state before claiming something is disallowed or a security hole.
|
||||||
- [Use `python` not `python3` on GURU-5070](python3-shim-use-python.md) — `python3` in Git bash hits the flaky MS Store shim; real interpreters are `python` (3.12) / `py` (3.14). coord.py + wiki-compile work via `python`; the coord lock IS claimable here
|
- [Stream-of-thought design convos](feedback_stream_of_thought_design.md) — Mike brainstorms features free-form, adding requirements iteratively; Claude validates/sharpens as a design partner but does NOT build until an explicit go, then captures parked threads durably (PARKED_*.md + todos) for a later /shape-spec.
|
||||||
- [Beast = primary GuruRMM Windows build host](gururmm-beast-windows-build-host.md) — GURU-BEAST-ROG (i9), reached from .30 via Tailscale-on-.30 at 100.101.122.4 as guru; Pluto is the fallback (`attempt_build beast || attempt_build pluto`). WiX must be 4.x (v6+ = OSMF); Beast NuGet needed nuget.org added
|
- [RMM Thoughts backlog](feedback_rmm_thoughts_backlog.md) — GuruRMM ideas from Mike & Howard go in projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md (Status: Raw); pipeline thought -> discuss -> spec (/shape-spec) -> roadmap. Don't build until an explicit go.
|
||||||
- [GuruRMM command_type gotcha](reference_gururmm_command_type.md) — only shell/powershell/python/script/claude_task (+cmd alias); unknown type silently dropped, looks like a black-hole
|
- [Syncro preview mandatory](feedback_syncro_preview_mandatory.md) — preview+confirm every Syncro write, including internal notes
|
||||||
- [GuruRMM log analysis -> Claude Haiku](gururmm-log-analysis-claude-cutover.md) — cut over from Ollama-on-Beast (timed out on fleet-sized prompts; "unreachable" was a mislabeled 120s timeout) to Anthropic API Haiku 4.5 w/ structured outputs; key at vault `projects/gururmm/anthropic-api`; ZDR pending; deploy needs root on .30 (.env + restart)
|
- [Refresh session history first](feedback_refresh_session_history_first.md) — read prior incident logs before acting; do not re-remediate already-handled accounts
|
||||||
- [IX WHM API access = 'ClaudeTools' token, not password](ix-whm-dns-api-access.md) — IX cPanel/WHM (ix.azcomputerguru.com:2087) DNS + all API work uses the FULL-ACCESS-root WHM API token at vault `infrastructure/ix-server` `credentials.whm-api-token` via header `Authorization: whm root:<token>` (force curl -4). Password basic-auth on legacy json-api now 403s. Public NS ns1/ns2.acghosting.com = 52.52.94.202.
|
- [Autonomy scope](feedback_autonomy_scope.md) — confirm only for client-affecting actions; internal docs/wiki/ClaudeTools = act autonomously
|
||||||
- [Vault EVERY credential surfaced in-session](feedback-vault-every-credential.md) — any cred (pasted/created/discovered) -> store via the vault skill + document purpose & exact usage immediately; it's a standing job rule (reinforced in CORE CLAUDE.md). Lost IX creds wasted ~1h on 2026-06-12.
|
- [Check for client-slug fragmentation](feedback_client_slug_fragmentation.md) — Before concluding a client has no records, grep broadly (company/owner/initials/hostname/"Last, First") across clients/, wiki/, session-logs/, vault — one client gets split across slug variants (Wolkin was 4: wolkin/wolkin-law/rswolkin/robert-wolkin). Consolidate to one canonical slug; action prior logs' Pending items.
|
||||||
- [GuruRMM install-report v1: reuse endpoint + failed-install agent](gururmm-install-report-failed-agent-v1.md) — legacy NSIS installer reuses /api/install-report (machine info + logs, success+fail); server upserts a visible "failed-install" device on failure reports (Mike: in v1); verify-connect-before-success; trend/near-fail analytics. Server side is a separate sequential SPEC after the legacy-agent branch lands.
|
- [RMM user_session = false SMB failures](feedback_rmm_user_session_smb_false_negative.md) — GuruRMM net use/net view/Add-Printer to a remote \HOST fail with error 67 / RPC 1702 (even with valid creds) because user_session is a WTS-impersonated non-interactive token that can't do authenticated SMB. The share/printer may work fine interactively. Treat RMM SMB results as "can't tell"; verify via ScreenConnect.
|
||||||
- [DM wrapping commands to Mike in Discord](feedback_dm_wrapping_commands_to_mike.md) — long/wrapping output (commands, consent links, URLs) goes via Discord DM (copies clean), not just chat; use the `discord-dm` skill (`discord-dm.sh mike "<x>"`). Gotchas: bot token vault projects/discord-bot/bot-token, Mike uid 264814939619721216, MUST set User-Agent or Cloudflare 403 errcode 1010, build JSON via jq + printf|--data-binary (direct -d → 50109).
|
- [Prefer SSH over RMM](feedback_prefer_ssh_over_rmm.md) — when a target has SSH (key auth) and it's easier, drive it via system OpenSSH (scp+ssh) instead of the GuruRMM agent; RMM runs as SYSTEM + is bound by the server-side timeout reaper + forces base64/quoting gymnastics. Reserve RMM as the fallback when SSH/VPN is down.
|
||||||
- [Physical access codes -> vault + wiki pointer](feedback_physical_access_codes.md) — alarm/lockbox/door codes go in vault clients/<slug>/physical-access-<location>.sops.yaml (kind: physical-access) + a `## Physical Access` pointer section in the client wiki; never plaintext. First entry: Peaceful Spirit NW.
|
- [Broken [[backlinks]] = write-me-later markers](feedback_broken_backlinks_are_writeme_markers.md) — A [[name]] with no matching file is an intentional "worth writing" marker, not breakage. Flesh the missing memory out from session history/logs and index it; never strip the link to silence the warning. memory-dream reports these as INFO candidates, not errors.
|
||||||
- [CT Thoughts backlog](feedback_ct_thoughts_backlog.md) — ClaudeTools harness ideas go in docs/CT_THOUGHTS.md (trigger "ct thought:"); CT analogue of RMM_THOUGHTS. Don't build until explicit go. First entry = ClaudeTools 3.0 web co-work vision.
|
- [gururmm session-logs are in a submodule](gururmm-session-logs-submodule-save.md) — commit in the submodule + `git push origin HEAD:main` (GURU-5070 CAN push over HTTP now); then advance the parent gitlink
|
||||||
- [AI-auth product boundary](project_ai_auth_product_boundary.md) — ClaudeTools/ClaudeTools 3.0 = internal-only, per-person subscription OAuth ok; GuruRMM = sellable, customer brings own API key (never ACG's subscription); backend dev = internal. Anthropic ToS bans subscription auth in third-party products.
|
- [Use `python` not `python3` on GURU-5070](python3-shim-use-python.md) — `python3` in Git bash hits the flaky MS Store shim; real interpreters are `python` (3.12) / `py` (3.14). coord.py + wiki-compile work via `python`; the coord lock IS claimable here
|
||||||
- [RMM SYSTEM context can't see user mapped drives](feedback_rmm_system_context_mapped_drives.md) — RMM runs as SYSTEM; `Test-Path F:\` etc. is False even when the user's mapped/redirected drive exists. Diagnose mapped-drive/redirect issues in `context:user_session`. Elevated apps (e.g. QB DB Server Manager "unable to retrieve root folder") need `EnableLinkedConnections=1` + reboot.
|
- [Beast = primary GuruRMM Windows build host](gururmm-beast-windows-build-host.md) — GURU-BEAST-ROG (i9), reached from .30 via Tailscale-on-.30 at 100.101.122.4 as guru; Pluto is the fallback (`attempt_build beast || attempt_build pluto`). WiX must be 4.x (v6+ = OSMF); Beast NuGet needed nuget.org added
|
||||||
- [AD2 = Dataforth-ops fork](project_ad2_dataforth_fork.md) — branch ad2 = main + thin Dataforth layer; keep fork edits ADDITIVE (Dataforth context in clients/dataforth/CLAUDE.dataforth.md, NOT .claude/CLAUDE.md); rebase onto main directly when sync.sh self-lock hits; no vault/jq/sops/age on this box.
|
- [GuruRMM command_type gotcha](reference_gururmm_command_type.md) — only shell/powershell/python/script/claude_task (+cmd alias); unknown type silently dropped, looks like a black-hole
|
||||||
- [GuruScan verification IN TEST / paused](project_guruscan_in_test_paused.md) — multi-engine scanner verify on DESKTOP-MS42HNC paused 2026-06-22 (VM rebooted mid-Emsisoft run); HitmanPro done (36 removed), Emsisoft full-scan unverified; resume `guruscan-agent-test.sh DESKTOP-MS42HNC scan-one Emsisoft`; Defender RTP/Tamper still off on VM
|
- [GuruRMM log analysis -> Claude Haiku](gururmm-log-analysis-claude-cutover.md) — cut over from Ollama-on-Beast (timed out on fleet-sized prompts; "unreachable" was a mislabeled 120s timeout) to Anthropic API Haiku 4.5 w/ structured outputs; key at vault `projects/gururmm/anthropic-api`; ZDR pending; deploy needs root on .30 (.env + restart)
|
||||||
- [GuruRMM fleet dispatch-hang fix](project_gururmm_dispatch_hang_fix.md) — blocking send_to on a full bounded channel to one black-holed agent wedged ALL command dispatch; fixed with try_send (9dae20c, deployed); proper black-hole eviction still missing (was reverted in 80df458) — finish it if it recurs
|
- [IX WHM API access = 'ClaudeTools' token, not password](ix-whm-dns-api-access.md) — IX cPanel/WHM (ix.azcomputerguru.com:2087) DNS + all API work uses the FULL-ACCESS-root WHM API token at vault `infrastructure/ix-server` `credentials.whm-api-token` via header `Authorization: whm root:<token>` (force curl -4). Password basic-auth on legacy json-api now 403s. Public NS ns1/ns2.acghosting.com = 52.52.94.202.
|
||||||
- [Windows won't-boot / offline DISM repair playbook](windows-offline-dism-repair-gotchas.md) — Automatic Repair loop = boot-critical fault (disk/registry/wedged update), NOT shell/appx store corruption (that's a symptom); `FaultyPackageInProgress` + 100s of Install/Uninstall-Pending packages = wedged CU -> RevertPendingActions or clean install. Offline DISM rejects `wim:` source (0x800f082e) -> MOUNT the wim, source `\Windows`. Ventoy breaks WIM mount (0xc1420134) -> use Rufus. 25H2(26200)=24H2(26100)+enablement, so match 26100 media. First hit: Four Paws AvImark #32447.
|
- [Vault EVERY credential surfaced in-session](feedback-vault-every-credential.md) — any cred (pasted/created/discovered) -> store via the vault skill + document purpose & exact usage immediately; it's a standing job rule (reinforced in CORE CLAUDE.md). Lost IX creds wasted ~1h on 2026-06-12.
|
||||||
- [365 app suite — authoritative map + consent-drift fix](reference_365_app_suite.md) — full map in `.claude/skills/remediation-tool/references/app-suite.md`; per-tenant consent is NOT uniform (VWP had the app but no SharePoint role). Run `consent-audit.sh <tenant|--all>` to detect gaps; fix via adminconsent URL or direct appRoleAssignment grant.
|
- [GuruRMM install-report v1: reuse endpoint + failed-install agent](gururmm-install-report-failed-agent-v1.md) — legacy NSIS installer reuses /api/install-report (machine info + logs, success+fail); server upserts a visible "failed-install" device on failure reports (Mike: in v1); verify-connect-before-success; trend/near-fail analytics. Server side is a separate sequential SPEC after the legacy-agent branch lands.
|
||||||
- [Remediation-tool has full M365 access (incl. SharePoint)](reference_remediation_tool_365_access.md) — the app suite covers Graph/EXO/Defender/SharePoint; don't declare "no access" on an accessDenied. SharePoint app-only needs a CERT (secret = "Unsupported app only token"); use get-token.sh `sharepoint`/`sharepoint-admin` tiers + CSOM admin API (Graph /admin/sharepoint/settings scope not held). Full map: skill references/app-permissions-and-sharepoint.md.
|
- [DM wrapping commands to Mike in Discord](feedback_dm_wrapping_commands_to_mike.md) — long/wrapping output (commands, consent links, URLs) goes via Discord DM (copies clean), not just chat; use the `discord-dm` skill (`discord-dm.sh mike "<x>"`). Gotchas: bot token vault projects/discord-bot/bot-token, Mike uid 264814939619721216, MUST set User-Agent or Cloudflare 403 errcode 1010, build JSON via jq + printf|--data-binary (direct -d → 50109).
|
||||||
- [AV migration: Bitdefender -> Datto EDR](project_av_migration_bitdefender_to_edr.md) — retire Bitdefender fleet-wide except Dataforth; end-state per machine = GuruRMM + Datto EDR
|
- [Physical access codes -> vault + wiki pointer](feedback_physical_access_codes.md) — alarm/lockbox/door codes go in vault clients/<slug>/physical-access-<location>.sops.yaml (kind: physical-access) + a `## Physical Access` pointer section in the client wiki; never plaintext. First entry: Peaceful Spirit NW.
|
||||||
- [RMM deploy via ScreenConnect](reference_rmm_deploy_via_screenconnect.md) — push GuruRMM agent to client workstations via SC send-command (SYSTEM), not DC remote-exec (DCOM/schtasks blocked on Win11 clients)
|
- [CT Thoughts backlog](feedback_ct_thoughts_backlog.md) — ClaudeTools harness ideas go in docs/CT_THOUGHTS.md (trigger "ct thought:"); CT analogue of RMM_THOUGHTS. Don't build until explicit go. First entry = ClaudeTools 3.0 web co-work vision.
|
||||||
- [ScreenConnect custom-property slots](reference_screenconnect_custom_property_slots.md) — CP1=Company CP2=Site CP3=Department CP4=Device Type CP8=Tag (API hides labels; UpdateSessionCustomProperties replaces the whole array)
|
- [AI-auth product boundary](project_ai_auth_product_boundary.md) — ClaudeTools/ClaudeTools 3.0 = internal-only, per-person subscription OAuth ok; GuruRMM = sellable, customer brings own API key (never ACG's subscription); backend dev = internal. Anthropic ToS bans subscription auth in third-party products.
|
||||||
- [ScreenConnect cleanup uses wiki as source](feedback_screenconnect_cleanup_wiki_source.md) — per-client SC/RMM metadata cleanup pulls machine->dept/location from the client wiki; enrich the wiki when missing
|
- [RMM SYSTEM context can't see user mapped drives](feedback_rmm_system_context_mapped_drives.md) — RMM runs as SYSTEM; `Test-Path F:\` etc. is False even when the user's mapped/redirected drive exists. Diagnose mapped-drive/redirect issues in `context:user_session`. Elevated apps (e.g. QB DB Server Manager "unable to retrieve root folder") need `EnableLinkedConnections=1` + reboot.
|
||||||
- [TECH03L systemprofile shortcut corruption](project_tech03l_systemprofile_shortcut_corruption.md) — Auto-Claude "opens then closes" = .lnk pointing at nonexistent systemprofile path; repoint, do not debug the app
|
- [AD2 = Dataforth-ops fork](project_ad2_dataforth_fork.md) — branch ad2 = main + thin Dataforth layer; keep fork edits ADDITIVE (Dataforth context in clients/dataforth/CLAUDE.dataforth.md, NOT .claude/CLAUDE.md); rebase onto main directly when sync.sh self-lock hits; no vault/jq/sops/age on this box.
|
||||||
|
- [GuruScan verification IN TEST / paused](project_guruscan_in_test_paused.md) — multi-engine scanner verify on DESKTOP-MS42HNC paused 2026-06-22 (VM rebooted mid-Emsisoft run); HitmanPro done (36 removed), Emsisoft full-scan unverified; resume `guruscan-agent-test.sh DESKTOP-MS42HNC scan-one Emsisoft`; Defender RTP/Tamper still off on VM
|
||||||
|
- [GuruRMM fleet dispatch-hang fix](project_gururmm_dispatch_hang_fix.md) — blocking send_to on a full bounded channel to one black-holed agent wedged ALL command dispatch; fixed with try_send (9dae20c, deployed); proper black-hole eviction still missing (was reverted in 80df458) — finish it if it recurs
|
||||||
|
- [Windows won't-boot / offline DISM repair playbook](windows-offline-dism-repair-gotchas.md) — Automatic Repair loop = boot-critical fault (disk/registry/wedged update), NOT shell/appx store corruption (that's a symptom); `FaultyPackageInProgress` + 100s of Install/Uninstall-Pending packages = wedged CU -> RevertPendingActions or clean install. Offline DISM rejects `wim:` source (0x800f082e) -> MOUNT the wim, source `\Windows`. Ventoy breaks WIM mount (0xc1420134) -> use Rufus. 25H2(26200)=24H2(26100)+enablement, so match 26100 media. First hit: Four Paws AvImark #32447.
|
||||||
|
- [365 app suite — authoritative map + consent-drift fix](reference_365_app_suite.md) — full map in `.claude/skills/remediation-tool/references/app-suite.md`; per-tenant consent is NOT uniform (VWP had the app but no SharePoint role). Run `consent-audit.sh <tenant|--all>` to detect gaps; fix via adminconsent URL or direct appRoleAssignment grant.
|
||||||
|
- [Remediation-tool has full M365 access (incl. SharePoint)](reference_remediation_tool_365_access.md) — the app suite covers Graph/EXO/Defender/SharePoint; don't declare "no access" on an accessDenied. SharePoint app-only needs a CERT (secret = "Unsupported app only token"); use get-token.sh `sharepoint`/`sharepoint-admin` tiers + CSOM admin API (Graph /admin/sharepoint/settings scope not held). Full map: skill references/app-permissions-and-sharepoint.md.
|
||||||
|
- [AV migration: Bitdefender -> Datto EDR](project_av_migration_bitdefender_to_edr.md) — retire Bitdefender fleet-wide, ONLY exception Glaztech (Dataforth migrates); end-state per machine = GuruRMM + Datto EDR
|
||||||
|
- [RMM deploy via ScreenConnect](reference_rmm_deploy_via_screenconnect.md) — push GuruRMM agent to client workstations via SC send-command (SYSTEM), not DC remote-exec (DCOM/schtasks blocked on Win11 clients)
|
||||||
|
- [ScreenConnect custom-property slots](reference_screenconnect_custom_property_slots.md) — CP1=Company CP2=Site CP3=Department CP4=Device Type CP8=Tag (API hides labels; UpdateSessionCustomProperties replaces the whole array)
|
||||||
|
- [ScreenConnect cleanup uses wiki as source](feedback_screenconnect_cleanup_wiki_source.md) — per-client SC/RMM metadata cleanup pulls machine->dept/location from the client wiki; enrich the wiki when missing
|
||||||
|
- [TECH03L systemprofile shortcut corruption](project_tech03l_systemprofile_shortcut_corruption.md) — Auto-Claude "opens then closes" = .lnk pointing at nonexistent systemprofile path; repoint, do not debug the app
|
||||||
|
- [Claude tui fullscreen crash](feedback_claude_tui_fullscreen_crash.md) — "flicker-free" prompt writes tui=fullscreen; instant-exit crash loop on some Windows terminals; set tui=default (reinstall does NOT fix)
|
||||||
|
- [Backup targets never guessed](feedback_backup_targets_never_guessed.md) — billed-vs-running mismatches are billing questions for Mike/Winter; never infer/deploy backup targets
|
||||||
|
- [Fire #dev-alerts for client/RMM work](feedback_fire_dev_alerts_for_client_work.md) — Howard+Mike watch #dev-alerts Discord for live visibility into what techs/sessions touch. Fire post-bot-alert.sh ([DEV]/[RMM]/[DEPLOY]/[GURURMM] prefix -> #dev-alerts) at the START of any RMM/Dev or active-client-machine action. No CLAUDE.md rule yet — this memory is the rule.
|
||||||
|
- [M365 app: SharePoint needs CERT](reference_m365_app_sharepoint_rest_vs_graph.md) — SP app-only works via the CERT (get-token.sh <tenant> sharepoint); the secret gives "Unsupported app only token". Graph app-only uses the secret.
|
||||||
|
- [EDR auto-isolates GuruRMM PowerShell](project_edr_rmm_autoisolation_fp.md) — Datto "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM scripts fleet-wide (vwp-qbs 4h outage); watcher + narrow suppression deployed, policy fix pending Mike
|
||||||
|
- [Background tasks — no shell `&`](feedback_background_task_no_ampersand.md) — with run_in_background:true, run the command in the foreground of the shell; adding `&`/`disown` forks + exits 0 instantly, orphaning a blocking wait so it never delivers (hit twice building ask-forum)
|
||||||
|
- [ask-forum — human-in-the-loop via #ct-forum](../skills/ask-forum/SKILL.md) — ask a teammate a question in the private ct-forum Discord forum, get their human answer back in-session; forum-only, any human can answer, run the --wait as a proper background task
|
||||||
|
|||||||
26
.claude/memory/feedback_background_task_no_ampersand.md
Normal file
26
.claude/memory/feedback_background_task_no_ampersand.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: Background tasks — run_in_background only, never add a shell `&`
|
||||||
|
description: When launching a long-running command as a background task (run_in_background:true), do NOT also append a shell `&`/`disown`/`nohup`. The shell forks the command and exits 0 immediately, orphaning it — a blocking wait then never delivers its result. Run it in the FOREGROUND of that shell; the harness does the backgrounding.
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
When you want a long-running command (e.g. `ask-forum.sh --wait`, a poll loop, a blocking
|
||||||
|
watcher) to run in the background, use the Bash tool's **`run_in_background: true`** and run
|
||||||
|
the command in the **foreground of that shell** — a bare `bash script.sh ...` with **no
|
||||||
|
trailing `&`, no `disown`, no `nohup`**.
|
||||||
|
|
||||||
|
**Why:** appending `&` forks the command off inside the shell; the shell then reaches the
|
||||||
|
end and exits **0 immediately**. The harness sees exit 0 and reports the task "completed"
|
||||||
|
right away, while the forked process is orphaned (and often killed). A blocking `--wait`
|
||||||
|
that was supposed to sit for minutes and deliver an answer instead returns nothing — its
|
||||||
|
result is never captured or notified.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
- Background wait, CORRECT: `Bash(command="bash .claude/scripts/ask-forum.sh --wait <id> --timeout 1800", run_in_background=true)` — the wait truly blocks server-side, costs zero tokens while pending, and the harness pings you the instant it completes.
|
||||||
|
- WRONG: `Bash(command="bash ... --wait ... &", run_in_background=true)` and `... & disown` — orphans the wait, "completes" instantly, no answer.
|
||||||
|
- The two mechanisms are redundant: `run_in_background` IS the backgrounding. Never stack a shell `&` on top of it.
|
||||||
|
|
||||||
|
**Incident:** hit TWICE in one session (2026-07-08) building the [[ask-forum]] flow — each
|
||||||
|
time the "background wait" for a Discord reply exited 0 instantly and never delivered the
|
||||||
|
answer, which looked like the reply was lost and forced the user to manually prompt "did
|
||||||
|
they answer?". Logged as `--friction` in `errorlog.md`. Related: [[ask-forum]].
|
||||||
20
.claude/memory/feedback_backup_check_only_billed.md
Normal file
20
.claude/memory/feedback_backup_check_only_billed.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: feedback_backup_check_only_billed
|
||||||
|
description: Only verify backup status for clients that actually bill a backup line - don't scan non-billed clients
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Backup verification is scoped to clients **marked for backups** — i.e. those with a billed
|
||||||
|
Data Backup line (Syncro recurring-schedule line qty > 0). If a client is NOT billed for
|
||||||
|
backup, do NOT scan their machines / MSP360 / backends for backup status. (Howard,
|
||||||
|
2026-07-07, GPS audit — Marty Ryan has Svc = A E only, no backup line, so stop checking.)
|
||||||
|
|
||||||
|
**Why:** "is X backing up" only matters where the client pays for it; scanning non-billed
|
||||||
|
clients burns time/endpoint calls and produces findings nobody asked for.
|
||||||
|
|
||||||
|
**How to apply:** determine billed-backup status first (Syncro backup line qty — see the
|
||||||
|
`/syncro schedules`/`schedule` pull, authoritative), then only verify the ones with a seat.
|
||||||
|
Non-billed clients that happen to be backing up are a separate "revenue leak" question for
|
||||||
|
Mike/Winter, not a per-machine scan target. Related: [[feedback_backup_targets_never_guessed]],
|
||||||
|
[[reference_msp360_backup_monitoring]].
|
||||||
21
.claude/memory/feedback_backup_targets_never_guessed.md
Normal file
21
.claude/memory/feedback_backup_targets_never_guessed.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: feedback_backup_targets_never_guessed
|
||||||
|
description: Backup targets are a deliberate per-client decision - never infer which machine needs backup or deploy backup jobs from billing counts
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
When reconciling billed backup lines (e.g. Syncro "Service - Data Backup" qty) against
|
||||||
|
machines actually backing up (MSP360/B2), a mismatch is a BILLING question for Mike/Winter,
|
||||||
|
never a deployment instruction. (Howard, 2026-07-05, GPS->RMM audit backup verification.)
|
||||||
|
|
||||||
|
**Why:** which machine gets backed up is a deliberate choice made per client (usually the
|
||||||
|
server or a specific data machine) — billed qty does not mean "N machines each need a
|
||||||
|
backup job." Guessing targets and adding backup jobs would burn storage and misrepresent
|
||||||
|
what the client agreed to.
|
||||||
|
|
||||||
|
**How to apply:** report mismatches (billed > running, running > billed, dead buckets) as
|
||||||
|
findings with evidence; route the decision to Mike/Winter/client. Only configure a backup
|
||||||
|
job when the target machine is explicitly named by the user/client. Related:
|
||||||
|
[[project_av_migration_bitdefender_to_edr]] (the AV migration IS deploy-everywhere, unlike
|
||||||
|
backup — don't conflate the two postures).
|
||||||
22
.claude/memory/feedback_claude_tui_fullscreen_crash.md
Normal file
22
.claude/memory/feedback_claude_tui_fullscreen_crash.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: claude-tui-fullscreen-crash
|
||||||
|
description: Claude Code "flicker-free rendering" prompt writes tui=fullscreen to settings.json and can crash-loop the CLI on Windows; fix = delete the tui key
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
On ACG-TECH03L (2026-07-04, Claude Code 2.1.201), accepting the startup prompt asking to
|
||||||
|
enable **flicker-free rendering** wrote `"tui": "fullscreen"` into
|
||||||
|
`~/.claude/settings.json`. On that machine's terminal the fullscreen/alternate-screen
|
||||||
|
mode crashed the CLI instantly on every subsequent launch ("opens and then closes") —
|
||||||
|
from any launch path, since all read the same settings file.
|
||||||
|
|
||||||
|
**Why:** the setting persists, so one bad answer to the prompt turns into a permanent
|
||||||
|
crash loop; reinstalling Claude Code does NOT fix it because settings survive reinstall.
|
||||||
|
|
||||||
|
**How to apply:** if a fleet machine's Claude Code exits immediately at launch, check
|
||||||
|
`~/.claude/settings.json` for `"tui": "fullscreen"` and set it to `"tui": "default"`
|
||||||
|
(surgically — keep hooks/plugins; explicit "default" also suppresses re-prompting,
|
||||||
|
which deleting the key does not). Emergency override without touching config:
|
||||||
|
`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`. Decline the flicker-free prompt on machines
|
||||||
|
with older terminals/conhost. Related: [[tech03l-systemprofile-shortcut-corruption]].
|
||||||
26
.claude/memory/feedback_discord_read_replies.md
Normal file
26
.claude/memory/feedback_discord_read_replies.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: feedback_discord_read_replies
|
||||||
|
description: Discord DM replies are invisible to coord/repo sync — use `discord-dm.sh read <user>` to see if mike/winter/rob actually answered.
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
When we DM someone via the `discord-dm` skill and wait on an answer, their reply does NOT
|
||||||
|
come through coord messages or repo sync — those channels never carry Discord replies. Before
|
||||||
|
telling the user "no reply yet / they didn't answer," READ the DM channel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .claude/scripts/discord-dm.sh read mike 10 # last 10 in the mike DM
|
||||||
|
bash .claude/scripts/discord-dm.sh read #dev-alerts # a channel
|
||||||
|
```
|
||||||
|
|
||||||
|
Output is oldest-first; lines marked `>>>` are from THEM (replies), indented lines are us.
|
||||||
|
|
||||||
|
**Why:** the bot participates in every DM channel it opened, so `GET /channels/<id>/messages`
|
||||||
|
returns full content — the Message Content privileged intent only gates gateway events, not this
|
||||||
|
REST fetch. Before adding `read` (2026-07-07, Howard flagged it) the wrapper was send-only, so
|
||||||
|
replies from mike/winter were silently missed.
|
||||||
|
|
||||||
|
**How to apply:** after DMing a question to mike/winter/rob, `read` that user to check for their
|
||||||
|
answer instead of assuming silence. Related: [[reference_community_forum]] is a different channel;
|
||||||
|
this is Discord DMs/channels via [[discord-dm]] (`.claude/scripts/discord-dm.sh`).
|
||||||
29
.claude/memory/feedback_fire_dev_alerts_for_client_work.md
Normal file
29
.claude/memory/feedback_fire_dev_alerts_for_client_work.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: feedback_fire_dev_alerts_for_client_work
|
||||||
|
description: Fire a #dev-alerts Discord post (post-bot-alert.sh with an [RMM]/[DEV]/[DEPLOY]/[BUILD]/[GURURMM] prefix) at the START of any RMM/Dev or active-client-machine action — Howard+Mike watch that channel for live visibility into what techs/sessions are touching
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Howard and Mike watch the **#dev-alerts** Discord channel to see, in real time, what the other techs
|
||||||
|
(and Claude sessions) are working on. Any substantive action on **RMM/Dev** or an **ACTIVE CLIENT
|
||||||
|
machine** MUST post a #dev-alerts heads-up — ideally at the START of the work, not just after.
|
||||||
|
|
||||||
|
**What counts:** SSH into a client server, restarting/reconfiguring services, editing boot/config on a
|
||||||
|
live box, RMM remote commands, agent/server/dashboard deploys, channel promotions, anything with real
|
||||||
|
blast radius on production/client infra.
|
||||||
|
|
||||||
|
**Why:** without it a session can be hand-editing a live client box (e.g. Lonestar Electrical's Unraid
|
||||||
|
`/boot/config/go`, cycling libvirt) with ZERO visibility to the team — exactly what happened
|
||||||
|
2026-07-06, which Mike flagged: "all of the actions you've taken today should have fired the discord
|
||||||
|
alerts."
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
`bash .claude/scripts/post-bot-alert.sh "[DEV] <client/box> — <what + why>. — Claude @ <machine> (<user>)"`
|
||||||
|
Prefixes `[RMM] [DEPLOY] [DEV] [BUILD] [GURURMM] [SMARTBADGE-WATCH]` auto-route to the PRIVATE
|
||||||
|
#dev-alerts (Howard+Mike only, channel 1509998508198068484); everything else → #bot-alerts (whole
|
||||||
|
team). It soft-fails (best-effort), so it never blocks the work. Channel IDs + routing live in
|
||||||
|
`.claude/scripts/post-bot-alert.sh`.
|
||||||
|
|
||||||
|
As of 2026-07-06 there is NO CLAUDE.md rule enforcing this — treat this memory AS the rule until a core
|
||||||
|
rule/hook lands (worth proposing one). Related: [[discord-dm]] skill for direct DMs.
|
||||||
22
.claude/memory/feedback_gitbash_tz_ignored.md
Normal file
22
.claude/memory/feedback_gitbash_tz_ignored.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Git Bash `TZ=` is silently ignored on Windows — never use it for current-time reporting
|
||||||
|
|
||||||
|
**Date:** 2026-07-09
|
||||||
|
**Source:** Winter correction, Len's Auto browser-hijack thread (Discord bot session)
|
||||||
|
|
||||||
|
## What happened
|
||||||
|
Reported "current time" to Winter as `4:40 PM AZ` using
|
||||||
|
`TZ="America/Phoenix" date '+... AZ'` in Git Bash on BEAST. MSYS/Git-Bash has no
|
||||||
|
tzdata for that zone name, so `TZ` was silently ignored and the command printed
|
||||||
|
**UTC** (16:40) — which the format string then labeled "AZ". Real local time was
|
||||||
|
9:40 AM AZ. Winter caught it; caused a confusing side-quest about whether BEAST's
|
||||||
|
clock was a month off (it wasn't — clock + NTP + timezone all correct).
|
||||||
|
|
||||||
|
## Rule
|
||||||
|
- For current date/time on a Windows machine: use **PowerShell `Get-Date`**
|
||||||
|
(system local time — BEAST is already set to Arizona), or `date` in bash with
|
||||||
|
NO TZ override (MSYS date honors the Windows local zone), or an external HTTP
|
||||||
|
`Date:` header for authoritative UTC.
|
||||||
|
- Never trust `TZ=<IANA zone>` in Git Bash — unknown zones fall back to UTC with
|
||||||
|
no error, and a hardcoded zone label in the format string turns that into a
|
||||||
|
confidently-wrong answer.
|
||||||
|
- Calendar math (`date -d 2026-06-05 +%A`) is unaffected — that stays fine.
|
||||||
29
.claude/memory/feedback_scheduled_task_no_console_flash.md
Normal file
29
.claude/memory/feedback_scheduled_task_no_console_flash.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: feedback_scheduled_task_no_console_flash
|
||||||
|
description: Windows scheduled tasks must launch console apps windowless (wscript VBS for bash, pythonw + -Hidden for python) or they flash a console window on the desktop every run
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
A Windows scheduled task whose action runs a **console-subsystem** program — `bash.exe`,
|
||||||
|
`py.exe`, `python.exe`, `cmd.exe` — with `LogonType=Interactive` and `Settings.Hidden=False`
|
||||||
|
draws a visible console window on the desktop **every time it fires**. On short repetition
|
||||||
|
intervals (the EDR watcher fires every 10 min) this reads to the user as a command prompt
|
||||||
|
"opening and closing" constantly. It is the single most reported harness annoyance and it
|
||||||
|
keeps recurring because the *installer scripts* recreate the flashing task.
|
||||||
|
|
||||||
|
**Why:** wscript.exe is a GUI-subsystem host and pythonw.exe is the windowless Python host;
|
||||||
|
neither allocates a console. bash.exe / py.exe / python.exe do.
|
||||||
|
|
||||||
|
**How to apply:** whenever you register (or find) a ClaudeTools scheduled task:
|
||||||
|
- **bash target** -> point the action at `C:\Windows\System32\wscript.exe` with a one-line VBS
|
||||||
|
wrapper that does `CreateObject("WScript.Shell").Run "...bash.exe -lc ...", 0, False`
|
||||||
|
(window style `0` = hidden). Pattern files: `gps-rmm-progress-hidden.vbs`,
|
||||||
|
`edr-isolation-watch-hidden.vbs`.
|
||||||
|
- **python target** -> use `pythonw.exe` (next to the active interpreter), never `py.exe`/`python.exe`.
|
||||||
|
- **Always** add `-Hidden` to `New-ScheduledTaskSettingsSet` as belt-and-suspenders.
|
||||||
|
- Verify: `Start-ScheduledTask`, then confirm no visible bash/wscript/conhost MainWindow.
|
||||||
|
|
||||||
|
Fixed installers: `register-edr-watcher.ps1`, `register-orphan-detector.ps1`. The scheduled
|
||||||
|
tasks themselves are per-machine (not in the repo) — re-run the fixed installer, or repoint
|
||||||
|
the existing task's action, on every box that runs it. Related: [[feedback_session_recovery]].
|
||||||
20
.claude/memory/feedback_simplest_solution_first.md
Normal file
20
.claude/memory/feedback_simplest_solution_first.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: feedback_simplest_solution_first
|
||||||
|
description: Always try the simplest solution to a problem first; escalate to complex only as needed
|
||||||
|
metadata:
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Always START with the simplest solution that could solve the problem. Confirm it works, then
|
||||||
|
graduate to more advanced/complicated approaches only as the simple one proves insufficient.
|
||||||
|
|
||||||
|
**Why:** Mike watched a "ask the UDM what IP it thinks VWP-QBS is" turn balloon into vault
|
||||||
|
reads, plink host-key pinning, and password sanity checks — when the clean answer was a single
|
||||||
|
`nslookup vwp-qbs.vwp.us 172.16.9.1` from a machine already on the VPN. The heavy path burned
|
||||||
|
tokens and his patience ("incapable of doing things cleanly like a week ago"). Now a standing
|
||||||
|
CLAUDE.md core rule.
|
||||||
|
|
||||||
|
**How to apply:** Before picking tooling, ask "what is the least-complex thing that answers
|
||||||
|
this?" — one DNS lookup, one API call, one command. Do that first and observe the result.
|
||||||
|
Only reach for SSH/plink/double-hops/multi-step orchestration when the simple probe genuinely
|
||||||
|
can't answer. Escalate deliberately, not by default. See [[feedback_verify_live_before_acting]].
|
||||||
@@ -1,24 +1,32 @@
|
|||||||
---
|
---
|
||||||
name: project_av_migration_bitdefender_to_edr
|
name: project_av_migration_bitdefender_to_edr
|
||||||
description: AV strategy — migrate all clients from Bitdefender to Datto EDR, except Glaztech and Dataforth
|
description: AV strategy — migrate all clients from Bitdefender to Datto EDR; ONLY exception is Glaztech
|
||||||
metadata:
|
metadata:
|
||||||
type: project
|
type: project
|
||||||
---
|
---
|
||||||
|
|
||||||
Standing AV direction (set by Howard 2026-07-03): ACG is moving endpoint AV/security
|
Standing AV direction (Howard 2026-07-03, scope narrowed 2026-07-04): ACG is moving
|
||||||
from **Bitdefender GravityZone -> Datto EDR** for **all clients EXCEPT Glaztech Industries
|
endpoint AV/security from **Bitdefender GravityZone -> Datto EDR** for **all clients
|
||||||
and Dataforth Corp** (those two stay on Bitdefender / handled separately).
|
EXCEPT Glaz-Tech Industries (glaztech)** — the ONLY client staying on Bitdefender.
|
||||||
|
**Dataforth migrates fully** (originally excepted; Howard removed the exception
|
||||||
|
2026-07-04 — Dataforth already runs 51 EDR agents with only 5 BD endpoints left:
|
||||||
|
D1-ENGI-006, DESKTOP-L2LE31M, DATAFORTH-PC, SURFACEOPS, MING-HP).
|
||||||
|
|
||||||
**Why:** consolidate on Datto EDR as the security plane; Bitdefender is being retired
|
**Why:** consolidate on Datto EDR as the security plane; Bitdefender is being retired
|
||||||
fleet-wide (Glaztech + Dataforth are the two exceptions — both have large established
|
fleet-wide. Glaztech keeps its large established Bitdefender footprint (~242 BD
|
||||||
Bitdefender footprints: Glaztech ~242 endpoints, Dataforth managed separately).
|
endpoint records, vs 159 GPS-billed — count includes stale ghosts).
|
||||||
|
|
||||||
**How to apply:** whenever setting up or reconciling a client's endpoints (e.g. the
|
**How to apply:** whenever setting up or reconciling a client's endpoints (e.g. the
|
||||||
GPS->GuruRMM coverage audit), the target end-state per machine is: GuruRMM agent +
|
GPS->GuruRMM coverage audit), the target end-state per machine is: GuruRMM agent +
|
||||||
Datto EDR agent, and Bitdefender **removed**. Do NOT deploy new Bitdefender coverage.
|
Datto EDR agent (AV on), and Bitdefender **removed**. Do NOT deploy new Bitdefender
|
||||||
Use existing Bitdefender inventory only as a discovery source for which machines exist
|
coverage. Use existing Bitdefender inventory only as a discovery source for which
|
||||||
(its company names carry the Syncro CID `_NNNNN`, handy for mapping). Deploy Datto EDR
|
machines exist (its company names carry the Syncro CID `_NNNNN` suffix — exact join
|
||||||
via `[[datto-edr]]` (create-group -> mint-key -> deploy-cmd, pushed through `/rmm`).
|
key to Syncro customers). Deploy Datto EDR via `[[datto-edr]]` (create-group ->
|
||||||
|
mint-key -> deploy-cmd, pushed through `/rmm`).
|
||||||
|
|
||||||
Related: GPS->RMM audit tracker `projects/gps-rmm-audit/tracker.md`. Exceptions = Glaztech +
|
Migration scope quantified 2026-07-04 (tracker Phase 4): 27 clients / 141 BD
|
||||||
Dataforth (leave their existing AV alone; do not migrate them to EDR in this effort).
|
endpoints + Dataforth's 5. Datto EDR "Default RMM Org" holds ~35 unassigned agents
|
||||||
|
that belong to real clients (IMC x7, Russo x2, Len's, Rednour, Reliant, etc.) —
|
||||||
|
attribute them to proper orgs as part of the migration.
|
||||||
|
|
||||||
|
Related: GPS->RMM audit tracker `projects/gps-rmm-audit/tracker.md`.
|
||||||
|
|||||||
28
.claude/memory/project_edr_rmm_autoisolation_fp.md
Normal file
28
.claude/memory/project_edr_rmm_autoisolation_fp.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: project_edr_rmm_autoisolation_fp
|
||||||
|
description: Datto EDR "Exfiltration Over HTTP" rule auto-isolates benign GuruRMM PowerShell fleet-wide; watcher + narrow suppression deployed, policy fix pending Mike
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
Incident 2026-07-07: **vwp-qbs** (Valley Wide Plastering QuickBooks server) was auto-isolated by Datto EDR — Mike lost ~4h before realizing EDR did it. No notification fired (EDR alert delivery was never wired to any channel).
|
||||||
|
|
||||||
|
**Root cause:** Datto EDR rule **"Exfiltration Over HTTP Protocol" (MITRE T1041, HIGH)** carries an automated response of **`kill-process` + `isolate-host`**. It fires on *signed* PowerShell doing an outbound HTTPS POST — which our **GuruRMM automation** does routinely. Process tree on the alerts: `gururmm-agent.exe -> cmd.exe -> powershell.exe`. Confirmed benign both times (vwp-qbs = a vSphere VM-power-on script; tps-tina = a network/connectivity diagnostic).
|
||||||
|
|
||||||
|
**This is SYSTEMIC, not a one-off.** The same rule fired + attempted auto-isolate on **tps-tina** (The Prairie Schooner) the same day — different GuruRMM script, different client. Per-command-line suppression is whack-a-mole. Note: the isolate-host response only actually cuts a host off where its endpoint policy has isolation enabled (vwp-qbs got isolated; tps-tina fired the same alert but `extensionSuccess=None`, stayed online).
|
||||||
|
|
||||||
|
**FLEET-WIDE FIX LIVE (2026-07-07):** Datto EDR suppression rule **`3365e79a`** "GuruRMM origin - Exfil-over-HTTP (fleet-wide FP)" — matches Rule Name = Exfiltration Over HTTP Protocol + Grand Parent = `gururmm-agent.exe`, org/loc empty = ALL clients. Suppresses this one rule for any GuruRMM-launched process fleet-wide (present + future scripts); every other rule still fires on RMM-origin. Created in the CONSOLE (by Howard) after the API create path proved unsafe. The two earlier narrow per-command-line rules (`e4dd55bf`, `7ea2a577`) are now redundant subsets — harmless, left in place.
|
||||||
|
|
||||||
|
**API FOOTGUN (do NOT create suppressions via `raw POST` on this tenant):** `POST /SuppressionRules` forces `active:true` and auto-creates an EMPTY match-everything version → a live "suppress all" window (I hit this with `7c36b89f`, deleted within ~2 min). `GET`/`PATCH /SuppressionRules/{id}` 400 ("undefined is not valid JSON"); `DELETE /{id}` works. Create suppressions in the CONSOLE until an atomic API create is verified. Details in `.claude/skills/datto-edr/references/api-reference.md`.
|
||||||
|
|
||||||
|
**What's deployed (done):**
|
||||||
|
- Narrow suppression rule (Datto EDR, id `e4dd55bf`) for VWP's exact script: matches Process Command Line + Grand Parent = `gururmm-agent.exe`. Does NOT blind other PowerShell. API schema for suppression is now in `.claude/skills/datto-edr/references/api-reference.md`.
|
||||||
|
- **EDR isolation watcher**: `.claude/scripts/edr-isolation-watch.sh` + `register-edr-watcher.ps1`, registered as Windows Scheduled Task **"ClaudeTools - EDR Isolation Watcher"** on Howard-Home (runs every 10 min, posts new auto-isolations to #dev-alerts — see [[feedback_fire_dev_alerts_for_client_work]]). Plain script, zero tokens. State file `.edr-watch-state.json` (gitignored) dedupes; seeded with the vwp-qbs + tps-tina event ids so it stays silent for the already-triaged ones. Retire when GuruRMM EDR webhook (Feature 6) lands.
|
||||||
|
|
||||||
|
**Pending Mike decisions (do NOT act without him):**
|
||||||
|
1. Auto-isolate vs **alert-only** on behavioral rules — the real fix (the isolation, not the alert, caused the outage).
|
||||||
|
2. Whether to allow a small curated "GuruRMM-origin + this-rule OK" exception set — but NOT a blanket `gururmm-agent.exe` whitelist (RMM = top MSP attack path; blinding EDR there is dangerous).
|
||||||
|
3. **Fix the RMM scripts** to stop looking like exfil (drop `-EncodedCommand`, least-priv accounts).
|
||||||
|
4. Hygiene: VWP's script had **ESXi root creds hardcoded** (base64, cert-validation disabled) — already in vault `clients/vwp/esxi.sops.yaml`; move to a least-priv service account + vault injection.
|
||||||
|
|
||||||
|
See [[reference_datto_edr_detection_behavior]] (behavioral rules DO fire on signed PowerShell, unlike reputation rules).
|
||||||
27
.claude/memory/project_gururmm_legacy_disabled.md
Normal file
27
.claude/memory/project_gururmm_legacy_disabled.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: project_gururmm_legacy_disabled
|
||||||
|
description: GuruRMM's legacy Rust 1.77 (Server 2008R2 / Win7) build variant is DISABLED as of 2026-07-04 — do not just re-enable it; 2008R2 support returns via a separate legacy agent (C++/.NET or clean rewrite)
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
The GuruRMM Windows build's **legacy Rust 1.77 variant is disabled** (2026-07-04) via
|
||||||
|
`LEGACY_ENABLED=false` in `deploy/build-pipeline/build-windows.sh`.
|
||||||
|
|
||||||
|
**Why:** the legacy build moves `Cargo.lock` aside and re-resolves deps unpinned; crates.io now
|
||||||
|
serves edition2024 versions of transitive deps (`chacha20`/`rand_core` 0.10.1) that Rust 1.77
|
||||||
|
cannot parse ("feature `edition2024` is required"). This fail-closed the ENTIRE Windows build and
|
||||||
|
blocked modern (amd64/x86/tray) deploys. It is a **BUG-021 recurrence** and is NOT caused by any
|
||||||
|
agent code change — it reproduces at any base commit right now. This is at least the 3rd edition2024
|
||||||
|
break of the 1.77 build (the `agent/Cargo.toml` transitive-pin list is a monument to it).
|
||||||
|
|
||||||
|
**What the disable does:** skips the legacy wave/copy/deploy/symlink and EXCLUDES legacy from the
|
||||||
|
artifact cleanup, so the last-built legacy binaries (**0.6.76**) are PRESERVED and existing Server
|
||||||
|
2008R2 / Win7 endpoints stay supported *frozen at 0.6.76*. Modern agents advance normally.
|
||||||
|
|
||||||
|
**Mike's directive (2026-07-04):** "Kill the legacy version for now, but we will come back to it" —
|
||||||
|
2008R2 support is REQUIRED (a number are still in the wild) and will return via a **different
|
||||||
|
architecture (C++/.NET) or a clean rewrite for legacy**, NOT by flipping `LEGACY_ENABLED` back on
|
||||||
|
without first solving the 1.77 edition2024 pin problem. Do NOT re-enable it blindly. Tracked in
|
||||||
|
`projects/msp-tools/guru-rmm/docs/RMM_THOUGHTS.md`. Related: the policy-controlled tray feature that
|
||||||
|
was shipping when this surfaced ([[project_guruconnect]] is a different project).
|
||||||
31
.claude/memory/project_gururmm_software_removal_pr58.md
Normal file
31
.claude/memory/project_gururmm_software_removal_pr58.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: project_gururmm_software_removal_pr58
|
||||||
|
description: SPEC-030 software-removal hardening (driver exclusion, lingering-process sweep, job list/reaper) is on PR #58 awaiting merge; live testing planned 2026-07-06 on DESKTOP-MS42HNC.
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
|
||||||
|
GuruRMM SPEC-030 software removal (Assets -> Inventory tab): the 2026-06-24 session's open
|
||||||
|
requests were implemented 2026-07-05 and sit on branch `feat/software-removal-hardening`,
|
||||||
|
**PR #58** (https://git.azcomputerguru.com/azcomputerguru/gururmm/pulls/58), NOT merged —
|
||||||
|
merge deploys to beta. Live testing planned 2026-07-06 on the test box DESKTOP-MS42HNC
|
||||||
|
(agent 0de89b88-b21d-4647-ab64-96157ba87cc5).
|
||||||
|
|
||||||
|
What PR #58 contains:
|
||||||
|
- Engine `-List` emits `is_driver` (heuristic: driver/chipset/firmware name words, or hw-vendor
|
||||||
|
publisher + device words; junkware like "Driver Booster" deliberately excluded). Dashboard
|
||||||
|
hides drivers by default, keeps them out of select-all, deselects on re-hide.
|
||||||
|
- End-of-run lingering-uninstaller sweep in uninstall-engine.ps1: 45s budget-aware grace, then
|
||||||
|
taskkill of started trees (StartTime PID-reuse guard, same-session only, null CreationDate
|
||||||
|
excluded) + late re-verify that flips async-finisher false-failures to success. This is the
|
||||||
|
chosen resolution of the Launchy/AIMP "failed / no usable result" open decision.
|
||||||
|
- `GET /agents/:id/software/uninstall/jobs` (list, results omitted), stale-running-job reaper
|
||||||
|
(900s no-progress -> failed), `finish_job` guarded WHERE status='running', UI 100-target cap.
|
||||||
|
|
||||||
|
Verified pre-merge: engine live tests on Howard-Home (driver flags, sweep kill/no-kill,
|
||||||
|
orphaned/refused paths), heuristic unit cases, `verify.sh server --check` + `dashboard` PASS,
|
||||||
|
high-effort workflow code review with all confirmed findings fixed.
|
||||||
|
|
||||||
|
Remaining after this: recipe catalog (P3, [[project_gururmm_av_removal_recipes]] if created),
|
||||||
|
dashboard UI for the new job-list endpoint (client binding `softwareApi.uninstallJobs` exists,
|
||||||
|
unused), and TeamViewer retry-verify still not re-validated live.
|
||||||
32
.claude/memory/reference_client_wifi_inventory.md
Normal file
32
.claude/memory/reference_client_wifi_inventory.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: reference_client_wifi_inventory
|
||||||
|
description: Where client Wi-Fi networks are recorded so techs connect onsite without asking — vault holds passwords, wiki holds the readable index.
|
||||||
|
metadata:
|
||||||
|
type: reference
|
||||||
|
---
|
||||||
|
|
||||||
|
We are building a fleet-wide client Wi-Fi list so anyone onsite can connect without asking
|
||||||
|
for the SSID/password again. Established 2026-07-06 (Howard).
|
||||||
|
|
||||||
|
**Split (security-driven):**
|
||||||
|
- **Passwords/PSKs → SOPS vault**, `clients/<slug>/wifi.sops.yaml`, one file per client.
|
||||||
|
Per network use `credentials.<key>_ssid`, `credentials.<key>_password`, optional
|
||||||
|
`credentials.<key>_auth` (`<key>` = `staff`/`guest`/`voice`/`warehouse`...). Everything under
|
||||||
|
`credentials:` is encrypted at rest, so the entry is self-contained for an importer. Write it
|
||||||
|
with the `vault` skill's `vault-helper.sh new`/`set` (NEVER paste a Wi-Fi password into chat,
|
||||||
|
a ticket, a session log, or the wiki).
|
||||||
|
- **Readable index → wiki**, `wiki/reference/client-wifi.md` (registered in `wiki/index.md`
|
||||||
|
under a new **Reference** section). Client / SSID / band / location / vault-field — NO passwords.
|
||||||
|
It lives under `wiki/reference/` so `/wiki-compile` never clobbers it (compile only touches
|
||||||
|
clients/projects/systems slugs).
|
||||||
|
|
||||||
|
**Capture flow onsite:** ask "Wi-Fi name + password? staff vs guest? which should our machines
|
||||||
|
use?" → store in vault via vault-helper → add a row to the wiki inventory → `/sync`.
|
||||||
|
|
||||||
|
**Importer is DEFERRED** (Howard chose "structure only for now" 2026-07-06). Planned end state:
|
||||||
|
a `/wifi import <client>` that reads the vault file, builds a Windows WLAN profile per network
|
||||||
|
(`netsh wlan add profile`), and auto-connects. Interim: read the password with
|
||||||
|
`vault.sh get-field clients/<slug>/wifi credentials.<key>_password` and connect manually.
|
||||||
|
Build the importer/`/wifi` skill once a few real networks exist.
|
||||||
|
|
||||||
|
Keep `<slug>` identical to the client's `wiki/clients/<slug>.md` slug so vault + wiki + article line up.
|
||||||
@@ -14,6 +14,8 @@ Verified live 2026-06-25/26 on RMM-TEST-MACHINE (EDR agent `b98b3ba0-...`, group
|
|||||||
|
|
||||||
**Datto EDR is reputation-based, NOT structural.** A synthetic "looks suspicious" artifact (Run-key/scheduled task launching hidden `-EncodedCommand` powershell) is collected by the forensic scan but scored BENIGN → no alert (powershell.exe is signed/clean). To get an EDR detection you need a real reputation hit: wire a known-bad file as the **executable of an autostart** (Run-key/scheduled task) so the survey collects + hashes it. EICAR-as-autostart works → high-sev `rule` alert. A loose file on disk is NOT scanned by the EDR forensic survey (it only walks execution/persistence artifacts).
|
**Datto EDR is reputation-based, NOT structural.** A synthetic "looks suspicious" artifact (Run-key/scheduled task launching hidden `-EncodedCommand` powershell) is collected by the forensic scan but scored BENIGN → no alert (powershell.exe is signed/clean). To get an EDR detection you need a real reputation hit: wire a known-bad file as the **executable of an autostart** (Run-key/scheduled task) so the survey collects + hashes it. EICAR-as-autostart works → high-sev `rule` alert. A loose file on disk is NOT scanned by the EDR forensic survey (it only walks execution/persistence artifacts).
|
||||||
|
|
||||||
|
**BUT behavioral rules DO fire on signed PowerShell** (unlike the reputation scoring above). Rules like **"Exfiltration Over HTTP Protocol" (T1041)** key on *runtime behavior* (outbound HTTP from powershell), not file reputation, so they alert HIGH on clean signed `powershell.exe` — and can carry an automated `isolate-host`/`kill-process` response. This routinely false-positives on GuruRMM automation (`gururmm-agent.exe -> cmd.exe -> powershell`). See [[project_edr_rmm_autoisolation_fp]].
|
||||||
|
|
||||||
**AV-suppression gotchas (to isolate EDR on an endpoint):**
|
**AV-suppression gotchas (to isolate EDR on an endpoint):**
|
||||||
- Datto AV is **tamper-protected**: `Stop-Service EndpointProtectionService2 -Force` is refused ("cannot be stopped"); can't disable from the endpoint. Supported path = console policy (AV disabled / path-exclusion) — console-only, like policy assignment.
|
- Datto AV is **tamper-protected**: `Stop-Service EndpointProtectionService2 -Force` is refused ("cannot be stopped"); can't disable from the endpoint. Supported path = console policy (AV disabled / path-exclusion) — console-only, like policy assignment.
|
||||||
- Disabling Datto AV in the policy **uninstalls** the AV component on the box (services `EndpointProtectionService`/`...2` go absent; `HUNTAgent` EDR stays). Platform `dattoAvEnabled` flips to False at the console first; the on-box apply lags a few minutes.
|
- Disabling Datto AV in the policy **uninstalls** the AV component on the box (services `EndpointProtectionService`/`...2` go absent; `HUNTAgent` EDR stays). Platform `dattoAvEnabled` flips to False at the console first; the on-box apply lags a few minutes.
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
---
|
---
|
||||||
name: reference_guru5070_rust_toolchain
|
name: reference_guru5070_rust_toolchain
|
||||||
description: GURU-5070 has the full local Rust toolchain (cargo + MSVC + protoc) — build/clippy/test the guru-connect workspace LOCALLY instead of the build host; set PROTOC first
|
description: GURU-5070 has a full Rust toolchain installed BUT a WDAC/Smart App Control policy now blocks running rustc/cargo locally (os error 4551) — build the Windows agent on Beast, not here; toolchain paths + CI gates still valid for reference
|
||||||
metadata:
|
metadata:
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
As of 2026-05-30, GURU-5070 has the full Rust dev toolchain installed, so GuruConnect can be
|
**BLOCKED AS OF 2026-07-04 — do NOT rely on local Rust builds on GURU-5070.** A Windows
|
||||||
built/linted/tested locally — **no more build-host (172.16.3.30) round-trips just for `cargo fmt`/clippy.**
|
Application Control (WDAC / Smart App Control) policy now blocks execution of `rustc.exe`/`cargo.exe`
|
||||||
|
(and `sops.exe`, `openssl`, `gawk`, Python `cryptography`'s native DLL) — attempts fail with
|
||||||
|
`An Application Control policy has blocked this file. (os error 4551)`. This is the same policy behind
|
||||||
|
the "Windows can't confirm who published …" blocks on unsigned binaries. Until the policy is adjusted
|
||||||
|
to trust the toolchain, **compile Windows agent/GuruConnect changes on Beast** (`guru@100.101.122.4`,
|
||||||
|
Tailscale; sccache at `C:\sccache`) via a throwaway `git worktree` + `cargo check` — see
|
||||||
|
[[gururmm-beast-windows-build-host]]. The paths/gates below remain accurate for reference and for when
|
||||||
|
the policy is relaxed. WDAC impact also breaks the `vault`/`sops` path here (use `age`+Node fallback per
|
||||||
|
[[feedback_vault_gcm_shadow_auth]]) and silently swallowed error logging (`CLAUDETOOLS_ROOT=D:/claudetools`
|
||||||
|
wrong-case → `log-skill-error.sh` can't write; real repo is `D:/ClaudeTools`).
|
||||||
|
|
||||||
|
As of 2026-05-30, GURU-5070 has the full Rust dev toolchain installed (able to build locally *until the
|
||||||
|
2026-07-04 WDAC block above*):
|
||||||
|
|
||||||
- **cargo/rustc/clippy/rustfmt:** `C:\Users\guru\.cargo\bin\` (rustup; cargo 1.96, rustfmt 1.9, clippy 0.1.96).
|
- **cargo/rustc/clippy/rustfmt:** `C:\Users\guru\.cargo\bin\` (rustup; cargo 1.96, rustfmt 1.9, clippy 0.1.96).
|
||||||
- **MSVC C++ Build Tools:** VS2022 BuildTools (VCTools workload) — provides the `x86_64-pc-windows-msvc` linker.
|
- **MSVC C++ Build Tools:** VS2022 BuildTools (VCTools workload) — provides the `x86_64-pc-windows-msvc` linker.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: reference_m365_app_sharepoint_rest_vs_graph
|
||||||
|
description: ComputerGuru M365 app suite — SharePoint app-only WORKS but requires the CERT (client_assertion), not the secret. Secret => "Unsupported app only token". Use get-token.sh <tenant> sharepoint.
|
||||||
|
metadata:
|
||||||
|
type: reference
|
||||||
|
---
|
||||||
|
|
||||||
|
The ACG **Tenant Admin app** (`709e6eed-0711-4875-9c44-2d3518c47063`) has **full SharePoint app-only
|
||||||
|
access** — `Sites.FullControl.All` on the SharePoint resource — but it is gated by an auth-method rule
|
||||||
|
that keeps biting:
|
||||||
|
|
||||||
|
- **SharePoint app-only REQUIRES a CERTIFICATE (client_assertion). A `client_secret` token is rejected**
|
||||||
|
with **`Unsupported app only token`** on EVERY SharePoint endpoint (REST `/_api/...` and CSOM
|
||||||
|
`/_vti_bin/client.svc/ProcessQuery`). This is a SharePoint platform rule, not a missing grant.
|
||||||
|
- **Graph** app-only accepts the secret fine — so Graph SharePoint calls (driveItem `createdBy`/
|
||||||
|
`lastModifiedBy`/`retentionLabel`, item `/permissions` + inheritance, `/groups` + members) work with
|
||||||
|
the secret and are the right tool for *investigation*.
|
||||||
|
|
||||||
|
**Do NOT hand-roll a SharePoint token from the secret. Use the remediation-tool cert tiers:**
|
||||||
|
```bash
|
||||||
|
cd .claude/skills/remediation-tool
|
||||||
|
bash scripts/get-token.sh <tenant> sharepoint # content resource <name>.sharepoint.com (cert)
|
||||||
|
bash scripts/get-token.sh <tenant> sharepoint-admin # admin resource <name>-admin.sharepoint.com (cert)
|
||||||
|
```
|
||||||
|
`get-token.sh` forces cert for the `sharepoint*` tiers automatically; the minted token's `roles` =
|
||||||
|
`["Sites.FullControl.All"]`. The cert is vaulted in `msp-tools/computerguru-tenant-admin.sops.yaml`
|
||||||
|
(`cert_thumbprint_b64url` + `cert_private_key_pem_b64`). Tenant arg = the domain (`birthbiologic.com`).
|
||||||
|
|
||||||
|
**Use the CERT (SP REST/CSOM) for:** list settings (`ForceCheckout`, `EnableModeration`/content approval,
|
||||||
|
versioning), per-item `CheckoutUserId`/checkout state, re-stamping `Author`/`Editor` (Created By /
|
||||||
|
Modified By), site lock, tenant settings. Graph can't do these.
|
||||||
|
|
||||||
|
Reference (authoritative): `.claude/skills/remediation-tool/references/app-permissions-and-sharepoint.md`
|
||||||
|
and `app-suite.md`. **Before ever telling the user "the tool can't do X" on SharePoint, use the cert
|
||||||
|
tier and verify** — the "Unsupported app only token" wall means *wrong auth method (secret)*, NOT no access.
|
||||||
|
See [[birth-biologic]] (migrated content owned by "SharePoint App" => greyed-out Move; fix = re-stamp
|
||||||
|
Author/Editor via cert CSOM `SystemUpdate`). Open question (Mike, 2026-07-06): standardize ALL M365
|
||||||
|
app-only auth on cert (cert is resource-agnostic + more secure) to kill this secret-vs-cert friction.
|
||||||
30
.claude/memory/reference_msp360_backup_monitoring.md
Normal file
30
.claude/memory/reference_msp360_backup_monitoring.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: reference_msp360_backup_monitoring
|
||||||
|
description: MSP360 MBS /api/Monitoring is the authoritative "is this client backing up" source for the whole ACG fleet
|
||||||
|
metadata:
|
||||||
|
type: reference
|
||||||
|
---
|
||||||
|
|
||||||
|
For any "is client X actually backing up / when did it last run" question, the authoritative
|
||||||
|
source is the **MSP360 Managed Backup Service API**, not B2 bucket contents (buckets are just
|
||||||
|
where Cloud plans land; a bucket with files does not prove a recent backup ran).
|
||||||
|
|
||||||
|
- Creds: vault `msp-tools/msp360-api.sops.yaml` (API login+password, generated in MSP360
|
||||||
|
console Settings>General — not the console login).
|
||||||
|
- Auth: `POST https://api.mspbackups.com/api/Provider/Login` `{UserName, Password}` -> Bearer
|
||||||
|
`access_token`.
|
||||||
|
- `GET /api/Monitoring` -> every backup plan across all companies: `CompanyName`,
|
||||||
|
`ComputerName`, `PlanName`, `StorageType`, `LastStart`, `Status` (0/1=Success, 2=Warning,
|
||||||
|
3=Failed, 4=Running, 6=Interrupted, 7=completed-with-warnings), `DataCopied`, `TotalData`,
|
||||||
|
`ErrorMessage`. Also `GET /api/Companies` (37, with Destinations) and `GET /api/Users` (59).
|
||||||
|
`/api/Computers` is 404.
|
||||||
|
- Used 2026-07-05 for the GPS backup map (`projects/gps-rmm-audit/backup-map.md`): resolved 11
|
||||||
|
of 12 "no destination found" clients, surfaced 4 not-backing-up findings (AMT + T&C Sorensen
|
||||||
|
= 0 plans, Grabb GND-SERVER failing, Bill Tedards ~12mo stale). MSP360 CompanyName also IDs
|
||||||
|
B2 buckets: `ACG-PST`=Peaceful Spirit, `ACG-TCA`=Tucson Coin and Autograph.
|
||||||
|
- Backup targets are a per-client decision — report mismatches, don't deploy jobs. See
|
||||||
|
[[feedback_backup_targets_never_guessed]]. Related: [[reference_backblaze_storage_rate]].
|
||||||
|
- **Use the `msp360` skill** (`.claude/skills/msp360/`) — built 2026-07-05, full (not
|
||||||
|
read-only): reads (`status`/`monitoring --failed|--company|--stale`/`companies`/`users`/
|
||||||
|
`licenses`/`billing`) plus `--confirm`-gated writes (create/delete user+company,
|
||||||
|
grant/release/revoke license) and a `raw` passthrough for the rest of the MBS API.
|
||||||
@@ -1,15 +1,41 @@
|
|||||||
---
|
---
|
||||||
name: reference-syncro-rmm-api-gui-only
|
name: reference-syncro-rmm-api-gui-only
|
||||||
description: Syncro's public API cannot manage RMM policies/folders — creation, assignment, and asset moves are GUI-only (live-verified 2026-06-25)
|
description: Syncro asset/RMM IS API-drivable (reads, create/update, patches, installed_apps, rmm_alerts) — only policy-folder ops need a token scope our per-user keys currently lack (re-verified 2026-07-10)
|
||||||
metadata:
|
metadata:
|
||||||
type: reference
|
type: reference
|
||||||
---
|
---
|
||||||
|
|
||||||
**Syncro RMM policy management is GUI-only — the public REST API does NOT expose it.** Live-verified against the ACG production tenant (computerguru.syncromsp.com) on 2026-06-25:
|
**CORRECTION (2026-07-10): most of Syncro's asset/RMM surface IS API-drivable — the earlier
|
||||||
|
"RMM is GUI-only" conclusion was a token-scope symptom, not an API limitation.** Live-probed
|
||||||
|
the ACG production tenant with a per-user token (howard) on 2026-07-10 and confirmed against
|
||||||
|
the OpenAPI 3.0 spec (`api-docs.syncromsp.com/swagger.json`):
|
||||||
|
|
||||||
- `GET /customer_assets` objects carry a read-only **`policy_folder_id`** field (which policy folder the machine sits in). **`PUT /customer_assets/{id}` with `policy_folder_id` is silently ignored** — returns HTTP 200 but the value never changes. Proven by a flip-and-restore test on ACG-internal asset 12335235 (DESKTOP-0O8A1RL): value stayed at folder 692253. **You CANNOT move a machine between policy folders via the API.**
|
- **Works with our current per-user token (HTTP 200):** `GET/POST /customer_assets`,
|
||||||
- `/policies`, `/policy_builders`, `/rmm_policies`, `/asset_policies` all return **404** — no policy-CRUD endpoints exist. Policy Builder (the `/policy_builders` GUI page) is web-console only.
|
`PUT /customer_assets/{id}` (name/serial/type/customer/properties — `name` required),
|
||||||
- `/policy_folders` (collection and specific-ID) returns **401** — the route exists but our API token lacks RMM/policy scope. A re-issued token *might* read folders, but since assets can't be moved anyway, it's moot for the move use case.
|
`GET /customer_assets/{id}/patches`, `.../installed_applications`, `/rmm_alerts` (list +
|
||||||
- Syncro docs (docs.syncrosecure.com / docs.syncromsp.com "Work with Policies") confirm: policies are created in Policy Builder, assigned via an Organization's "Assets & Policies" subtab "Update Assigned Policy" dropdown, or "Bulk Assign Top-Level Policy" — all GUI, **no API mention**.
|
`{id}/mute` + DELETE clear), plus contacts, contracts, leads, vendors, purchase_orders,
|
||||||
|
products, payments, wiki_pages, `/me`, `/search`. The `/syncro` skill now documents the
|
||||||
|
asset-read/asset-write/rmm-alert/agent-deploy workflows; the full inventory is in
|
||||||
|
`.claude/standards/syncro/api-reference.md`.
|
||||||
|
- **Policy-folder move: VERIFIED WORKING with a full-scope token (2026-07-10).** Howard's
|
||||||
|
token was upgraded to full scope; flip-and-restore on ACG-internal asset `12335235`
|
||||||
|
(`692253 → 692278 → 692253`) succeeded, each confirmed by GET. `PUT /customer_assets/{id}`
|
||||||
|
with `policy_folder_id` (+ required `name`) moves the asset. The spec: *"only
|
||||||
|
policy_folder_id requires **Assets - Policy Change**; with other fields requires **Assets -
|
||||||
|
Edit** AND **Assets - Policy Change**; nil/nonexistent/cross-customer folder → 422."*
|
||||||
|
- **Route quirks:** `GET /policy_folders` (bare) → 404; `GET /policy_folders?customer_id=N` →
|
||||||
|
200 (enumerate a customer's folders); `GET /policy_folders/{id}` → 401 even at full scope.
|
||||||
|
- **Per-user scope differs — capability-check every scope-gated write.** A token WITHOUT
|
||||||
|
Policy-Change returns HTTP 200 and silently no-ops the move (the exact 2026-06-25 symptom
|
||||||
|
that produced the original "GUI-only" belief); `/me` shows coarse `asset:{write:true}` and
|
||||||
|
cannot detect it. The `/syncro move-asset` helper preflights `GET /policy_folders?customer_id=N`
|
||||||
|
(401 = no scope) and verifies the folder actually changed after the PUT. Only Howard's token
|
||||||
|
is confirmed full-scope so far; grant the scope to each per-user token for consistent behavior.
|
||||||
|
- **Archive (retire) IS genuinely GUI-only** — no archive endpoint, no `archived` field, and
|
||||||
|
DELETE is destructive. That part of the old note stands.
|
||||||
|
|
||||||
**How to apply:** Do NOT attempt to build `/syncro move-asset` or any Syncro RMM policy/folder/group capability — it's not buildable on the public API. Don't re-probe these endpoints. The only API-drivable policy surface in the fleet is the `bitdefender` skill (GravityZone: create/assign policies, custom groups, move endpoints). For Syncro RMM policy work, direct the user to the Syncro web console. The `/syncro` skill stays PSA-only (tickets/billing/customers/scheduling/estimates + read-only asset lookup). See [[feedback-psa-default-syncro]].
|
**How to apply:** Build/asset-manage freely via API for everything EXCEPT policy folders. To
|
||||||
|
unlock `/syncro move-asset` + policy CRUD, add **Assets - Policy Change** + policy-folder
|
||||||
|
read/manage to the Syncro API token (Admin → API Tokens), re-vault, then flip-and-restore
|
||||||
|
verify on ACG-internal asset `12335235` (folder `692253`). Do NOT re-assert "can't do assets
|
||||||
|
via API" — that was wrong. See [[feedback-psa-default-syncro]] and the api-reference doc.
|
||||||
|
|||||||
54
.claude/memory/reference_windows_edition_upgrade_rmm.md
Normal file
54
.claude/memory/reference_windows_edition_upgrade_rmm.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: reference_windows_edition_upgrade_rmm
|
||||||
|
description: Verified fleet procedure to upgrade a Windows machine Home->Pro/Pro-for-Workstations via RMM, with the three gotchas (DISM error 50, shutdown /t drop, PfW MAK auto-upgrade).
|
||||||
|
metadata:
|
||||||
|
type: reference
|
||||||
|
---
|
||||||
|
|
||||||
|
Verified on ACG-Tech03L 2026-07-06 (Home 26200 -> Win 11 Pro for Workstations, permanently activated).
|
||||||
|
|
||||||
|
**Procedure (all steps as SYSTEM via `/rmm`, byte-exact via -EncodedCommand):**
|
||||||
|
1. Probe: EditionID/LicenseStatus (`.rmm-key-probe.ps1` pattern), confirm no logged-in user
|
||||||
|
(`query user`) so a reboot is safe.
|
||||||
|
2. Flip edition: `changepk.exe /ProductKey VK7JG-NPHTM-C97JM-9MPGT-3V66T` (generic Pro key).
|
||||||
|
Flips EditionID Core->Professional **live** (registry) + sets CBS RebootPending. exit 0.
|
||||||
|
3. Reboot to finalize (see gotcha #2).
|
||||||
|
4. Install MAK + activate: `slmgr /ipk <MAK>` then `slmgr /ato`; verify `slmgr /xpr` =
|
||||||
|
"permanently activated" and WMI SoftwareLicensingProduct LicenseStatus=1.
|
||||||
|
|
||||||
|
**Gotcha 1 — DISM is a dead end for online edition change.** `DISM /online /Set-Edition:Professional`
|
||||||
|
returns **Error 50 "Setting an edition is not supported with online images"** even though
|
||||||
|
`/Get-TargetEditions` lists Professional. Use `changepk.exe`, not DISM, for online Home->Pro.
|
||||||
|
|
||||||
|
**Gotcha 2 — `shutdown /r /t N` is silently DROPPED in the SYSTEM service session.** The command
|
||||||
|
returns exit 0 but the box never reboots (verified twice on tech03l). Also `shutdown /c "comment"`
|
||||||
|
fails outright because the RMM->cmd.exe layer strips the quotes ([[feedback_windows_quote_stripping]]).
|
||||||
|
Reboot reliably with `Restart-Computer -Force` (encoded PS) or `shutdown /r /t 0 /f` (no comment).
|
||||||
|
NOTE: on a fast SSD the edition-upgrade reboot can be <30s of downtime — a 30s-interval last_seen
|
||||||
|
monitor can miss the gap entirely; confirm the reboot by `LastBootUpTime` advancing, not by catching
|
||||||
|
the offline window. `is_connected` is null fleet-wide — track `last_seen` age, never that flag.
|
||||||
|
|
||||||
|
**Gotcha 3 — the ACG MAK `infrastructure/windows-pro-mak` is a Pro-for-WORKSTATIONS MAK** (mislabeled
|
||||||
|
"Pro"). `slmgr /ipk` of it on a Professional machine AUTO-UPGRADES the edition to ProfessionalWorkstation
|
||||||
|
(a strict superset of Pro), live, **no extra reboot** (verified DALLAS 2026-07-07). If a client genuinely
|
||||||
|
needs *plain* Pro, this key will over-shoot to PfW — there is no plain-Pro MAK in the vault. Billing:
|
||||||
|
each activation consumes one MAK count = $99 to the customer (ACG-internal machines: no invoice, still
|
||||||
|
burns a count).
|
||||||
|
|
||||||
|
**Gotcha 4 — `changepk.exe` TIMES OUT under SYSTEM but STILL flips the edition. Do NOT treat the timeout
|
||||||
|
as failure — VERIFY EditionID.** Run as SYSTEM via `/rmm`, `changepk.exe /ProductKey <key>` does not return
|
||||||
|
cleanly (the RMM command comes back `failed` / exit -1 `Command timeout`, or your Start-Process/`&` wrapper
|
||||||
|
hangs to the agent timeout), yet the registry `EditionID` flip to `Professional` DID happen. Always confirm
|
||||||
|
with a one-liner (`Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' EditionID`) before
|
||||||
|
concluding anything — if it reads `Professional`, proceed to the finalize reboot; the running OS still
|
||||||
|
reports Home in `Win32_OperatingSystem.Caption` until that reboot. (Verified DALLAS 2026-07-07 — the
|
||||||
|
`changepk` command timed out but EditionID was already `Professional`.)
|
||||||
|
|
||||||
|
**Gotcha 5 — Modern Standby laptops (S0 Low Power Idle) drop the RMM/SC agent when idle/lid-closed, which
|
||||||
|
mimics an endless reboot.** On a Core Ultra / S0-only machine, idle or a closed lid drops Wi-Fi -> agent
|
||||||
|
offline for long stretches (looked like a 25-min "slow reboot"; the box had actually been up 25 days and
|
||||||
|
never rebooted). Before a reboot-driven upgrade on a laptop, pin it awake first:
|
||||||
|
`powercfg /change standby-timeout-ac 0; powercfg /setacvalueindex SCHEME_CURRENT SUB_BUTTONS LIDACTION 0;
|
||||||
|
powercfg /setactive SCHEME_CURRENT` (AC only — safe; DON'T lid-nothing on DC/battery). A command dispatched
|
||||||
|
to an already-offline agent goes `pending` and fires (uncontrolled) on next wake — cancel stale ones with
|
||||||
|
`/rmm cancel`. NOTE: an RDP *target* laptop must STAY awake to be reachable, so keep the AC no-sleep in place.
|
||||||
247
.claude/scripts/ask-forum.sh
Normal file
247
.claude/scripts/ask-forum.sh
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ask-forum.sh — ask a human a question in the #ct-forum Discord forum and BLOCK
|
||||||
|
# until a person answers, then print their answer and exit. Built for a live
|
||||||
|
# three-way: the user in the terminal, THIS Claude session, and a teammate (e.g.
|
||||||
|
# Mike) in the forum — all the same session. The wait loop runs here in bash, so
|
||||||
|
# the calling model pays for ONE tool call, not a polling loop.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ask-forum.sh "question text" [--title "short title"] [--timeout SEC] [--poll SEC] [--tag @id ...]
|
||||||
|
# echo "question" | ask-forum.sh [flags]
|
||||||
|
# ask-forum.sh --read <thread_id> [limit] # one-shot read, no wait
|
||||||
|
# ask-forum.sh --wait <thread_id> [--timeout SEC] [--poll SEC] [--after <msg_id>]
|
||||||
|
#
|
||||||
|
# Flow (ask): POST a forum post (thread) with the question, then poll that thread
|
||||||
|
# for the first NON-BOT reply (human answer), print it, exit 0.
|
||||||
|
#
|
||||||
|
# Correlation is automatic: we create the thread and read only that thread.
|
||||||
|
# Exit codes: 0 answered; 1 usage; 2 token missing; 3 Discord API error; 4 timeout.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
export LC_ALL="${LC_ALL:-C.UTF-8}" # count code points, not bytes, when slicing $CONTENT
|
||||||
|
|
||||||
|
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
API="https://discord.com/api/v10"
|
||||||
|
UA="ClaudeToolsBot (claudetools, 1.0)"
|
||||||
|
FORUM_ID="1522960388432465950" # #ct-forum (private forum; Howard + bot + Mike)
|
||||||
|
LIMIT_CHARS=1900 # Discord caps message content at 2000
|
||||||
|
|
||||||
|
TIMEOUT=600
|
||||||
|
POLL=8
|
||||||
|
TITLE=""
|
||||||
|
TAGS=()
|
||||||
|
MSG=""
|
||||||
|
|
||||||
|
# --- token resolver (shared by read + wait + ask) ---
|
||||||
|
resolve_token() {
|
||||||
|
local t
|
||||||
|
t="$(bash "$ROOT/.claude/scripts/vault.sh" get-field \
|
||||||
|
projects/discord-bot/bot-token.sops.yaml credentials.bot_token 2>/dev/null)"
|
||||||
|
if [ -z "$t" ] || [ "$t" = "null" ]; then
|
||||||
|
local env_file="$ROOT/projects/discord-bot/.env"
|
||||||
|
[ -f "$env_file" ] && t="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$env_file" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
|
||||||
|
fi
|
||||||
|
printf '%s' "$t"
|
||||||
|
}
|
||||||
|
|
||||||
|
# trim a string to the last newline within it (keeps split chunks on line boundaries)
|
||||||
|
trim_at_newline() {
|
||||||
|
local s="$1" nl
|
||||||
|
nl="${s%$'\n'*}"
|
||||||
|
if [ "${#nl}" -lt "${#s}" ] && [ "${#nl}" -gt 0 ]; then printf '%s' "$nl"; else printf '%s' "$s"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# clamp a read limit to Discord's valid 1..100 (guard huge values that overflow bash math)
|
||||||
|
clamp_limit() {
|
||||||
|
local n="$1"
|
||||||
|
printf '%s' "$n" | grep -qE '^[0-9]+$' || { printf '25'; return; }
|
||||||
|
[ "${#n}" -ge 4 ] && { printf '100'; return; } # >=1000 always exceeds 100
|
||||||
|
[ "$n" -lt 1 ] && n=1; [ "$n" -gt 100 ] && n=100
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Given a non-array poll response body, decide what to do. Echoes: "retry" (transient/
|
||||||
|
# rate-limited — sleeps any retry_after first) or "bail:<reason>" (permanent Discord error).
|
||||||
|
poll_disposition() {
|
||||||
|
local body="$1" ra code
|
||||||
|
ra="$(printf '%s' "$body" | jq -r '.retry_after // empty' 2>/dev/null)"
|
||||||
|
if [ -n "$ra" ]; then
|
||||||
|
# rate limited: honor Discord's back-off (ceil), then retry
|
||||||
|
sleep "$(printf '%s\n' "$ra" | awk '{printf "%d", ($1==int($1)?$1:int($1)+1)}')" 2>/dev/null || sleep 2
|
||||||
|
printf 'retry'; return
|
||||||
|
fi
|
||||||
|
code="$(printf '%s' "$body" | jq -r '.code // empty' 2>/dev/null)"
|
||||||
|
if [ -n "$code" ]; then printf 'bail:%s %s' "$code" "$(printf '%s' "$body" | jq -r '.message // ""')"; return; fi
|
||||||
|
printf 'retry' # unrecognized/transient (network blip, empty body)
|
||||||
|
}
|
||||||
|
|
||||||
|
# emit the human (non-bot) replies from a messages array as ">>> user: text" lines
|
||||||
|
emit_human() {
|
||||||
|
printf '%s' "$1" | jq -r 'sort_by(.id)[] | select(.author.bot|not) | ">>> " + .author.username + ": " + ((.content//"")|gsub("\n";" "))'
|
||||||
|
}
|
||||||
|
# newest message id in an array (string/lexical max — snowflakes are monotonic; avoids jq number precision loss)
|
||||||
|
newest_id() { printf '%s' "$1" | jq -r 'sort_by(.id)|.[-1].id // empty'; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --read: one-shot fetch of a thread's messages, no posting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "${1:-}" = "--read" ]; then
|
||||||
|
THREAD="${2:-}"
|
||||||
|
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --read <thread_id> [limit]" >&2; exit 1; }
|
||||||
|
LIMIT="$(clamp_limit "${3:-25}")"
|
||||||
|
TOKEN="$(resolve_token)"
|
||||||
|
{ [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; } && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
||||||
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
||||||
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?limit=${LIMIT}")"
|
||||||
|
printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1 || { echo "[ERROR] could not read thread ${THREAD}: $(printf '%s' "$MSGS" | head -c 160)" >&2; exit 3; }
|
||||||
|
echo "[OK] thread ${THREAD} (oldest first; '>>>' = human, ' ' = bot):"
|
||||||
|
printf '%s' "$MSGS" | jq -r 'sort_by(.id)[] | ((if (.author.bot // false) then " " else ">>> " end) + .author.username + ": " + ((.content // "")|gsub("\n";" ")))'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --wait: block on an EXISTING thread until a human replies, no posting
|
||||||
|
# Baselines on the starter (thread id == starter message id for a forum post),
|
||||||
|
# so a reply that already landed before --wait began is caught (not missed).
|
||||||
|
# Override the baseline with --after <msg_id> to wait for replies strictly after
|
||||||
|
# a specific message (use when resuming a thread whose earlier answer you already
|
||||||
|
# consumed and you want only the NEXT reply).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "${1:-}" = "--wait" ]; then
|
||||||
|
THREAD="${2:-}"; shift 2
|
||||||
|
[ -z "$THREAD" ] && { echo "[ERROR] usage: ask-forum.sh --wait <thread_id> [--timeout SEC] [--poll SEC] [--after <msg_id>]" >&2; exit 1; }
|
||||||
|
AFTER="$THREAD"
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--timeout) TIMEOUT="${2:-600}"; shift 2 ;;
|
||||||
|
--poll) POLL="${2:-8}"; shift 2 ;;
|
||||||
|
--after) AFTER="${2:-$THREAD}"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '%s' "$TIMEOUT" | grep -qE '^[0-9]+$' || TIMEOUT=600
|
||||||
|
printf '%s' "$POLL" | grep -qE '^[0-9]+$' || POLL=8
|
||||||
|
TOKEN="$(resolve_token)"
|
||||||
|
{ [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; } && { echo "[ERROR] no Discord bot token" >&2; exit 2; }
|
||||||
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "User-Agent: ${UA}")
|
||||||
|
echo "[INFO] waiting on thread ${THREAD} for a human reply (up to ${TIMEOUT}s)..." >&2
|
||||||
|
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}&limit=100")"
|
||||||
|
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
|
D="$(poll_disposition "$MSGS")"
|
||||||
|
case "$D" in bail:*) echo "[ERROR] thread ${THREAD}: ${D#bail:}" >&2; exit 3 ;; esac
|
||||||
|
sleep "$POLL"; continue
|
||||||
|
fi
|
||||||
|
HUMAN="$(emit_human "$MSGS")"
|
||||||
|
if [ -n "$HUMAN" ]; then
|
||||||
|
echo "[OK] answer received in thread ${THREAD}:"; printf '%s\n' "$HUMAN"; echo "THREAD_ID=${THREAD}"; exit 0
|
||||||
|
fi
|
||||||
|
# all-bot batch: page forward so a reply beyond the 100-msg window isn't missed
|
||||||
|
NID="$(newest_id "$MSGS")"; [ -n "$NID" ] && AFTER="$NID"
|
||||||
|
sleep "$POLL"
|
||||||
|
done
|
||||||
|
echo "[TIMEOUT] no human reply on thread ${THREAD} within ${TIMEOUT}s" >&2
|
||||||
|
echo "THREAD_ID=${THREAD}" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ask mode (default): post a question and block for the first human reply
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--title) TITLE="${2:-}"; shift 2 ;;
|
||||||
|
--timeout) TIMEOUT="${2:-600}"; shift 2 ;;
|
||||||
|
--poll) POLL="${2:-8}"; shift 2 ;;
|
||||||
|
--tag) TAGS+=("${2:-}"); shift 2 ;;
|
||||||
|
-h|--help) sed -n '2,20p' "$0"; exit 1 ;;
|
||||||
|
*) MSG="${MSG:+$MSG }$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
|
||||||
|
if [ -z "$MSG" ]; then echo "[ERROR] no question given" >&2; exit 1; fi
|
||||||
|
printf '%s' "$TIMEOUT" | grep -qE '^[0-9]+$' || TIMEOUT=600
|
||||||
|
printf '%s' "$POLL" | grep -qE '^[0-9]+$' || POLL=8
|
||||||
|
|
||||||
|
if [ -z "$TITLE" ]; then
|
||||||
|
TITLE="$(printf '%s' "$MSG" | tr '\n' ' ' | cut -c1-60)"
|
||||||
|
[ -z "$TITLE" ] && TITLE="Question"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# prepend any @mentions so the tagged person gets pinged
|
||||||
|
MENTION=""
|
||||||
|
for t in "${TAGS[@]:-}"; do
|
||||||
|
[ -z "$t" ] && continue
|
||||||
|
id="${t#@}"
|
||||||
|
printf '%s' "$id" | grep -qE '^[0-9]{17,19}$' && MENTION="${MENTION}<@${id}> "
|
||||||
|
done
|
||||||
|
CONTENT="${MENTION}${MSG}"
|
||||||
|
|
||||||
|
TOKEN="$(resolve_token)"
|
||||||
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||||
|
echo "[ERROR] no Discord bot token (vault + .env both empty)" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "ask-forum" "no Discord bot token" >/dev/null 2>&1
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
|
||||||
|
|
||||||
|
# keep the forum starter <=1900 chars; any overflow goes out as follow-up messages
|
||||||
|
STARTER_CONTENT="$CONTENT"; OVERFLOW=""
|
||||||
|
if [ "${#CONTENT}" -gt "$LIMIT_CHARS" ]; then
|
||||||
|
STARTER_CONTENT="$(trim_at_newline "${CONTENT:0:$LIMIT_CHARS}")"
|
||||||
|
OVERFLOW="${CONTENT:${#STARTER_CONTENT}}"; OVERFLOW="${OVERFLOW#$'\n'}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- create the forum post (thread) with the question ---
|
||||||
|
BODY="$(jq -nc --arg name "$TITLE" --arg c "$STARTER_CONTENT" '{name:$name, message:{content:$c}}')"
|
||||||
|
RESP="$(printf '%s' "$BODY" | curl -s -m 20 -w $'\n%{http_code}' "${auth[@]}" \
|
||||||
|
-X POST "$API/channels/${FORUM_ID}/threads" --data-binary @-)"
|
||||||
|
HTTP="$(printf '%s' "$RESP" | tail -n1)"
|
||||||
|
JSON="$(printf '%s' "$RESP" | sed '$d')"
|
||||||
|
if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then
|
||||||
|
echo "[ERROR] could not post forum question (HTTP ${HTTP:-none}): $(printf '%s' "$JSON" | head -c 200)" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "ask-forum" "forum post failed" --context "http=${HTTP:-none} resp=$(printf '%s' "$JSON" | head -c 80)" >/dev/null 2>&1
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
THREAD="$(printf '%s' "$JSON" | jq -r '.id // empty')"
|
||||||
|
STARTER="$(printf '%s' "$JSON" | jq -r '.message.id // empty')"
|
||||||
|
if [ -z "$THREAD" ]; then
|
||||||
|
echo "[ERROR] posted but no thread id in response: $(printf '%s' "$JSON" | head -c 200)" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- post any overflow chunks as follow-up messages (check each; warn if one fails) ---
|
||||||
|
rest="$OVERFLOW"
|
||||||
|
while [ -n "$rest" ]; do
|
||||||
|
piece="$(trim_at_newline "${rest:0:$LIMIT_CHARS}")"
|
||||||
|
fhttp="$(printf '%s' "$(jq -nc --arg c "$piece" '{content:$c}')" | \
|
||||||
|
curl -s -m 15 -o /dev/null -w '%{http_code}' "${auth[@]}" -X POST "$API/channels/${THREAD}/messages" --data-binary @-)"
|
||||||
|
[ "$fhttp" != "200" ] && echo "[WARNING] overflow chunk failed (HTTP ${fhttp}) — question may be truncated in the forum" >&2
|
||||||
|
rest="${rest:${#piece}}"; rest="${rest#$'\n'}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[INFO] asked in #ct-forum (thread=${THREAD}) — waiting up to ${TIMEOUT}s for a human reply..." >&2
|
||||||
|
|
||||||
|
# --- block-poll the thread for the first NON-BOT reply ---
|
||||||
|
AFTER="${STARTER:-$THREAD}" # starter id == thread id for a forum post
|
||||||
|
DEADLINE=$(( $(date +%s) + TIMEOUT ))
|
||||||
|
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||||
|
sleep "$POLL"
|
||||||
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${THREAD}/messages?after=${AFTER}&limit=100")"
|
||||||
|
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
|
D="$(poll_disposition "$MSGS")"
|
||||||
|
case "$D" in bail:*) echo "[ERROR] thread ${THREAD}: ${D#bail:}" >&2; exit 3 ;; esac
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
HUMAN="$(emit_human "$MSGS")"
|
||||||
|
if [ -n "$HUMAN" ]; then
|
||||||
|
echo "[OK] answer received in thread ${THREAD}:"; printf '%s\n' "$HUMAN"; echo "THREAD_ID=${THREAD}"; exit 0
|
||||||
|
fi
|
||||||
|
NID="$(newest_id "$MSGS")"; [ -n "$NID" ] && AFTER="$NID" # page forward past bot-only batches
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[TIMEOUT] no human reply within ${TIMEOUT}s. Thread stays open for a later read:" >&2
|
||||||
|
echo "THREAD_ID=${THREAD}" >&2
|
||||||
|
echo " re-read: bash .claude/scripts/ask-forum.sh --read ${THREAD} | resume wait: --wait ${THREAD}" >&2
|
||||||
|
exit 4
|
||||||
268
.claude/scripts/backup_common.py
Normal file
268
.claude/scripts/backup_common.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Shared helpers for the backup-verification skills (seafile / owncloud / datto-workplace).
|
||||||
|
|
||||||
|
Three sibling skills answer the same GPS-audit question from different backends:
|
||||||
|
"which customers/machines are actually backing up here?" Each skill has its own
|
||||||
|
server-side client (Seafile Web API, ownCloud occ over SSH, Datto Workplace file
|
||||||
|
API). What they SHARE — and what lives here — is:
|
||||||
|
|
||||||
|
* ClaudeTools repo-root resolution (env -> identity.json -> derived).
|
||||||
|
* A one-field SOPS vault read via the canonical vault.sh wrapper.
|
||||||
|
* An RmmClient for the "endpoint" half of the "server + RMM endpoint" cross-check:
|
||||||
|
log in to GuruRMM, list agents, and run a detection PowerShell on an agent to
|
||||||
|
prove a given sync client (SeaDrive / ownCloud / Datto Workplace) is actually
|
||||||
|
installed and signed in on the machine.
|
||||||
|
|
||||||
|
Standalone: stdlib-only transport (urllib), no third-party dependency. Skills add
|
||||||
|
this file's directory to sys.path and `import backup_common`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# backup_common.py lives at <root>/.claude/scripts/backup_common.py
|
||||||
|
_THIS = Path(__file__).resolve()
|
||||||
|
_DERIVED_ROOT = _THIS.parent.parent.parent # scripts -> .claude -> repo root
|
||||||
|
|
||||||
|
RMM_BASE = os.environ.get("GURURMM_API", "http://172.16.3.30:3001")
|
||||||
|
RMM_VAULT_ENTRY = "infrastructure/gururmm-server.sops.yaml"
|
||||||
|
RMM_FIELD_EMAIL = "credentials.gururmm-api.admin-email"
|
||||||
|
RMM_FIELD_PASSWORD = "credentials.gururmm-api.admin-password"
|
||||||
|
|
||||||
|
HTTP_TIMEOUT = 60.0
|
||||||
|
|
||||||
|
|
||||||
|
class BackupError(RuntimeError):
|
||||||
|
"""Transport / auth / API error, with an optional HTTP status."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, *, status: Optional[int] = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
|
||||||
|
# --- repo-root + vault --------------------------------------------------------
|
||||||
|
def resolve_root() -> Path:
|
||||||
|
"""Resolve the ClaudeTools repo root: env var, then identity.json, then derived."""
|
||||||
|
env_root = os.environ.get("CLAUDETOOLS_ROOT")
|
||||||
|
if env_root:
|
||||||
|
return Path(env_root)
|
||||||
|
identity = _DERIVED_ROOT / ".claude" / "identity.json"
|
||||||
|
if identity.exists():
|
||||||
|
try:
|
||||||
|
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||||
|
root = data.get("claudetools_root")
|
||||||
|
if root:
|
||||||
|
return Path(root)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
return _DERIVED_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
def vault_get_field(entry: str, field: str) -> str:
|
||||||
|
"""Read one field from a SOPS vault entry via the canonical vault.sh wrapper."""
|
||||||
|
root = resolve_root()
|
||||||
|
vault = root / ".claude" / "scripts" / "vault.sh"
|
||||||
|
if not vault.exists():
|
||||||
|
raise BackupError(f"vault wrapper not found at {vault}")
|
||||||
|
try:
|
||||||
|
done = subprocess.run(
|
||||||
|
["bash", str(vault), "get-field", entry, field],
|
||||||
|
capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise BackupError("'bash' not found on PATH (need Git Bash for vault reads).") from exc
|
||||||
|
except subprocess.TimeoutExpired as exc:
|
||||||
|
raise BackupError(f"vault read timed out for {entry}:{field}") from exc
|
||||||
|
if done.returncode != 0:
|
||||||
|
raise BackupError(
|
||||||
|
f"vault read failed for {entry}:{field} (exit {done.returncode}): {done.stderr.strip()}"
|
||||||
|
)
|
||||||
|
value = done.stdout.strip()
|
||||||
|
if not value:
|
||||||
|
raise BackupError(f"vault returned empty for {entry}:{field}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# --- minimal JSON HTTP --------------------------------------------------------
|
||||||
|
def http_json(method: str, url: str, *, headers: Optional[dict] = None,
|
||||||
|
body: Optional[dict] = None, form: Optional[dict] = None,
|
||||||
|
timeout: float = HTTP_TIMEOUT,
|
||||||
|
basic: Optional[tuple[str, str]] = None) -> tuple[int, Any]:
|
||||||
|
"""Do an HTTP request and parse a JSON response. Returns (status, parsed).
|
||||||
|
|
||||||
|
`body` sends a JSON payload; `form` sends application/x-www-form-urlencoded
|
||||||
|
(list values expand via doseq, e.g. repeated fields the Seafile admin share
|
||||||
|
endpoint reads with getlist). Pass at most one. 4xx/5xx are returned (not
|
||||||
|
raised) so callers can inspect the error body; transport failures raise.
|
||||||
|
"""
|
||||||
|
hdrs = dict(headers or {})
|
||||||
|
data = None
|
||||||
|
if body is not None and form is not None:
|
||||||
|
raise BackupError("http_json: pass body OR form, not both")
|
||||||
|
if body is not None:
|
||||||
|
data = json.dumps(body).encode("utf-8")
|
||||||
|
hdrs.setdefault("Content-Type", "application/json")
|
||||||
|
elif form is not None:
|
||||||
|
data = urllib.parse.urlencode(form, doseq=True).encode("utf-8")
|
||||||
|
hdrs.setdefault("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
if basic is not None:
|
||||||
|
import base64
|
||||||
|
tok = base64.b64encode(f"{basic[0]}:{basic[1]}".encode()).decode("ascii")
|
||||||
|
hdrs["Authorization"] = f"Basic {tok}"
|
||||||
|
req = urllib.request.Request(url, data=data, method=method, headers=hdrs)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
raw = resp.read().decode("utf-8", errors="replace")
|
||||||
|
return resp.getcode(), _parse(raw)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
raw = exc.read().decode("utf-8", errors="replace")
|
||||||
|
return exc.code, _parse(raw)
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
raise BackupError(f"request to {url} failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(text: str) -> Any:
|
||||||
|
if not text:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"_raw": text[:600]}
|
||||||
|
|
||||||
|
|
||||||
|
# --- GuruRMM client (endpoint cross-check) ------------------------------------
|
||||||
|
# One detection script, parametrized by the product name-fragments to look for.
|
||||||
|
# Returns a single JSON line describing installed products, running processes, and
|
||||||
|
# per-user config folders that match — enough to say "this client is installed and
|
||||||
|
# signed in" without guessing.
|
||||||
|
_DETECT_PS = r'''
|
||||||
|
$ErrorActionPreference='SilentlyContinue'
|
||||||
|
$patterns = @(__PATTERNS__)
|
||||||
|
function match($s){ if(-not $s){return $false}; foreach($p in $patterns){ if($s -match [regex]::Escape($p)){return $true} }; return $false }
|
||||||
|
|
||||||
|
$installed=@()
|
||||||
|
foreach($hive in @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')){
|
||||||
|
Get-ItemProperty $hive | Where-Object { match($_.DisplayName) } | ForEach-Object {
|
||||||
|
$installed += [pscustomobject]@{ name=$_.DisplayName; version=$_.DisplayVersion; location=$_.InstallLocation }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$procs = Get-Process | Where-Object { match($_.ProcessName) } | Select-Object -Expand ProcessName -Unique
|
||||||
|
$cfg=@()
|
||||||
|
foreach($d in @('Seafile','SeaDrive','ownCloud','ownCloudSync','Datto','Workplace','Workplace2')){
|
||||||
|
foreach($base in @($env:APPDATA,$env:LOCALAPPDATA,$env:USERPROFILE,"$env:USERPROFILE\Desktop")){
|
||||||
|
$pth = Join-Path $base $d
|
||||||
|
if(Test-Path $pth){ if(match($d) -or match($pth)){ $cfg += $pth } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# signed-in heuristic: config/db files with recent writes
|
||||||
|
$fresh=@()
|
||||||
|
foreach($c in $cfg){
|
||||||
|
Get-ChildItem $c -Recurse -File -ErrorAction SilentlyContinue -Depth 2 |
|
||||||
|
Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-14) } |
|
||||||
|
Select-Object -First 1 | ForEach-Object { $fresh += $c }
|
||||||
|
}
|
||||||
|
[pscustomobject]@{
|
||||||
|
host=$env:COMPUTERNAME
|
||||||
|
installed=$installed
|
||||||
|
processes=@($procs)
|
||||||
|
config_dirs=@($cfg)
|
||||||
|
active_config_dirs=@($fresh | Select-Object -Unique)
|
||||||
|
detected=([bool]($installed.Count -or $procs.Count))
|
||||||
|
} | ConvertTo-Json -Depth 5 -Compress
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class RmmClient:
|
||||||
|
"""Thin GuruRMM API client: login, list agents, run a detection PowerShell."""
|
||||||
|
|
||||||
|
def __init__(self, base: str = RMM_BASE):
|
||||||
|
self.base = base.rstrip("/")
|
||||||
|
self._token: Optional[str] = None
|
||||||
|
self._agents: Optional[list[dict]] = None
|
||||||
|
|
||||||
|
def login(self) -> None:
|
||||||
|
email = vault_get_field(RMM_VAULT_ENTRY, RMM_FIELD_EMAIL)
|
||||||
|
pw = vault_get_field(RMM_VAULT_ENTRY, RMM_FIELD_PASSWORD)
|
||||||
|
status, body = http_json("POST", f"{self.base}/api/auth/login",
|
||||||
|
body={"email": email, "password": pw})
|
||||||
|
if status != 200 or not isinstance(body, dict) or not body.get("token"):
|
||||||
|
raise BackupError(f"GuruRMM login failed (HTTP {status}): {body}", status=status)
|
||||||
|
self._token = body["token"]
|
||||||
|
|
||||||
|
def _auth(self) -> dict:
|
||||||
|
if not self._token:
|
||||||
|
self.login()
|
||||||
|
return {"Authorization": f"Bearer {self._token}"}
|
||||||
|
|
||||||
|
def list_agents(self) -> list[dict]:
|
||||||
|
if self._agents is None:
|
||||||
|
status, body = http_json("GET", f"{self.base}/api/agents", headers=self._auth())
|
||||||
|
if status != 200 or not isinstance(body, list):
|
||||||
|
raise BackupError(f"GET /api/agents failed (HTTP {status})", status=status)
|
||||||
|
self._agents = body
|
||||||
|
return self._agents
|
||||||
|
|
||||||
|
def find_agents(self, client_substr: Optional[str] = None,
|
||||||
|
hostname_substr: Optional[str] = None) -> list[dict]:
|
||||||
|
out = []
|
||||||
|
for a in self.list_agents():
|
||||||
|
if client_substr and client_substr.lower() not in (a.get("client_name") or "").lower():
|
||||||
|
continue
|
||||||
|
if hostname_substr and hostname_substr.lower() not in (a.get("hostname") or "").lower():
|
||||||
|
continue
|
||||||
|
out.append(a)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_ps(self, agent_id: str, script: str, timeout_seconds: int = 90,
|
||||||
|
poll_seconds: int = 4, max_polls: int = 30) -> dict:
|
||||||
|
"""Dispatch a PowerShell command to an agent and poll to completion."""
|
||||||
|
status, body = http_json(
|
||||||
|
"POST", f"{self.base}/api/agents/{agent_id}/command", headers=self._auth(),
|
||||||
|
body={"command_type": "powershell", "command": script,
|
||||||
|
"timeout_seconds": timeout_seconds})
|
||||||
|
if status not in (200, 201) or not isinstance(body, dict):
|
||||||
|
raise BackupError(f"command dispatch failed (HTTP {status}): {body}", status=status)
|
||||||
|
cid = body.get("command_id") or body.get("id")
|
||||||
|
if not cid:
|
||||||
|
raise BackupError(f"no command_id in dispatch response: {body}")
|
||||||
|
terminal = {"completed", "failed", "cancelled", "interrupted", "timeout"}
|
||||||
|
last: dict = {}
|
||||||
|
for _ in range(max_polls):
|
||||||
|
s, cb = http_json("GET", f"{self.base}/api/commands/{cid}", headers=self._auth())
|
||||||
|
if s == 200 and isinstance(cb, dict):
|
||||||
|
last = cb
|
||||||
|
if (cb.get("status") or "") in terminal:
|
||||||
|
break
|
||||||
|
time.sleep(poll_seconds)
|
||||||
|
return last
|
||||||
|
|
||||||
|
def detect_client(self, agent: dict, patterns: list[str], **kw) -> dict:
|
||||||
|
"""Run the detection script on one agent for the given product name-fragments."""
|
||||||
|
pat_lits = ",".join("'" + p.replace("'", "''") + "'" for p in patterns)
|
||||||
|
script = _DETECT_PS.replace("__PATTERNS__", pat_lits)
|
||||||
|
res = self.run_ps(agent["id"], script, **kw)
|
||||||
|
out = {"hostname": agent.get("hostname"), "client_name": agent.get("client_name"),
|
||||||
|
"agent_id": agent.get("id"), "status": res.get("status"),
|
||||||
|
"exit_code": res.get("exit_code"), "detected": None, "detail": None}
|
||||||
|
stdout = (res.get("stdout") or "").strip()
|
||||||
|
if stdout:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(stdout)
|
||||||
|
out["detected"] = bool(parsed.get("detected"))
|
||||||
|
out["detail"] = parsed
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
out["detail"] = {"_raw": stdout[:600]}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def eprint(*a: Any) -> None:
|
||||||
|
print(*a, file=sys.stderr)
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# discord-dm.sh <recipient> "message text"
|
# discord-dm.sh <recipient> "message text"
|
||||||
# echo "message text" | discord-dm.sh <recipient>
|
# echo "message text" | discord-dm.sh <recipient>
|
||||||
|
# discord-dm.sh read <recipient> [limit] # READ recent messages — SEE replies (default 15)
|
||||||
# discord-dm.sh list # show known users + channels
|
# discord-dm.sh list # show known users + channels
|
||||||
#
|
#
|
||||||
# <recipient> is one of:
|
# <recipient> is one of:
|
||||||
@@ -51,11 +52,29 @@ if [ -z "$RECIPIENT" ] || [ "$RECIPIENT" = "-h" ] || [ "$RECIPIENT" = "--help" ]
|
|||||||
sed -n '2,30p' "$0"; exit 1
|
sed -n '2,30p' "$0"; exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi
|
if [ "$RECIPIENT" = "list" ]; then print_dir; exit 0; fi
|
||||||
|
|
||||||
|
# --- read mode: fetch recent messages so we can SEE replies (mike/winter/etc.).
|
||||||
|
# `discord-dm.sh read <user|#channel> [limit]` — reuses the recipient resolution,
|
||||||
|
# token, and DM-channel-open below, then prints history instead of sending. The
|
||||||
|
# bot participates in each DM channel it opened, so REST history returns full
|
||||||
|
# content (the Message Content intent only gates gateway events, not this fetch).
|
||||||
|
ACTION=send
|
||||||
|
if [ "$RECIPIENT" = "read" ] || [ "$RECIPIENT" = "inbox" ]; then
|
||||||
|
ACTION=read; shift
|
||||||
|
RECIPIENT="${1:-}"
|
||||||
|
[ -z "$RECIPIENT" ] && { echo "[ERROR] usage: discord-dm.sh read <user|#channel> [limit]" >&2; exit 1; }
|
||||||
|
fi
|
||||||
shift
|
shift
|
||||||
|
|
||||||
# --- message (remaining args, else stdin) ---
|
# --- message (send) or limit (read) ---
|
||||||
if [ "$#" -gt 0 ]; then MSG="$*"; elif [ ! -t 0 ]; then MSG="$(cat)"; else MSG=""; fi
|
if [ "$ACTION" = "read" ]; then
|
||||||
if [ -z "$MSG" ]; then echo "[ERROR] empty message — nothing sent" >&2; exit 1; fi
|
READ_LIMIT="${1:-15}"
|
||||||
|
printf '%s' "$READ_LIMIT" | grep -qE '^[0-9]+$' || READ_LIMIT=15
|
||||||
|
[ "$READ_LIMIT" -gt 100 ] && READ_LIMIT=100
|
||||||
|
else
|
||||||
|
if [ "$#" -gt 0 ]; then MSG="$*"; elif [ ! -t 0 ]; then MSG="$(cat)"; else MSG=""; fi
|
||||||
|
if [ -z "$MSG" ]; then echo "[ERROR] empty message — nothing sent" >&2; exit 1; fi
|
||||||
|
fi
|
||||||
|
|
||||||
# --- resolve recipient -> mode (dm|channel) + target id ---
|
# --- resolve recipient -> mode (dm|channel) + target id ---
|
||||||
MODE=""; TARGET=""; LABEL=""
|
MODE=""; TARGET=""; LABEL=""
|
||||||
@@ -99,6 +118,22 @@ if [ "$MODE" = "dm" ]; then
|
|||||||
TARGET="$CHID"
|
TARGET="$CHID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- read mode: fetch + print recent messages, then exit (no send) ---
|
||||||
|
if [ "$ACTION" = "read" ]; then
|
||||||
|
MSGS="$(curl -s -m 20 "${auth[@]}" "$API/channels/${TARGET}/messages?limit=${READ_LIMIT}")"
|
||||||
|
if ! printf '%s' "$MSGS" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
||||||
|
echo "[ERROR] could not read $LABEL: $(printf '%s' "$MSGS" | head -c 200)" >&2
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "read history failed for $LABEL" --context "resp=$(printf '%s' "$MSGS" | head -c 80)" >/dev/null 2>&1
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
echo "[OK] discord-dm: last ${READ_LIMIT} message(s) in ${LABEL} (oldest first; '>>>' = reply from them, ' ' = us):"
|
||||||
|
printf '%s' "$MSGS" | jq -r 'reverse[] |
|
||||||
|
((if (.author.bot // false) then " " else ">>> " end)
|
||||||
|
+ (.timestamp[0:16]) + " " + .author.username + ": "
|
||||||
|
+ ((.content // "") | gsub("\n";" ")))'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# --- chunk: Discord caps content at 2000 chars (code 50035 on overflow).
|
# --- chunk: Discord caps content at 2000 chars (code 50035 on overflow).
|
||||||
# This tool's job is delivering LONG content intact, so split into <=1900-char
|
# This tool's job is delivering LONG content intact, so split into <=1900-char
|
||||||
# pieces (preferring newline boundaries) and send them in order — no markers
|
# pieces (preferring newline boundaries) and send them in order — no markers
|
||||||
|
|||||||
6
.claude/scripts/edr-isolation-watch-hidden.vbs
Normal file
6
.claude/scripts/edr-isolation-watch-hidden.vbs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
' edr-isolation-watch-hidden.vbs — launch the EDR isolation watcher with no visible window.
|
||||||
|
' Run via the "ClaudeTools - EDR Isolation Watcher" scheduled task through wscript.exe (a GUI
|
||||||
|
' host, no console), which starts bash with window style 0 (hidden) so the every-10-min watch
|
||||||
|
' no longer flashes a console window on the desktop. Runtime env is identical to a direct bash
|
||||||
|
' launch. Mirrors gps-rmm-progress-hidden.vbs.
|
||||||
|
CreateObject("WScript.Shell").Run """C:\Program Files\Git\bin\bash.exe"" -lc ""/c/claudetools/.claude/scripts/edr-isolation-watch.sh""", 0, False
|
||||||
71
.claude/scripts/edr-isolation-watch.sh
Normal file
71
.claude/scripts/edr-isolation-watch.sh
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# edr-isolation-watch.sh — interim EDR auto-isolation alerter.
|
||||||
|
#
|
||||||
|
# Polls Datto EDR for detections whose automated response included `isolate-host`
|
||||||
|
# (i.e. a machine was auto-isolated / "quarantined") and posts each NEW one to the
|
||||||
|
# private #dev-alerts channel (Mike + Howard). Runs as a plain scheduled job — no
|
||||||
|
# LLM, no tokens. Retire once the GuruRMM EDR webhook integration (Feature 6) lands.
|
||||||
|
#
|
||||||
|
# Schedule (every 10 min), e.g. cron:
|
||||||
|
# */10 * * * * bash /path/to/claudetools/.claude/scripts/edr-isolation-watch.sh >/dev/null 2>&1
|
||||||
|
#
|
||||||
|
# State: .edr-watch-state.json (gitignored) holds already-alerted alert IDs so each
|
||||||
|
# isolation is announced exactly once, even though Mike may un-isolate before the poll.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
EDR="bash $ROOT/.claude/scripts/py.sh $ROOT/.claude/skills/datto-edr/scripts/edr.py"
|
||||||
|
DM="bash $ROOT/.claude/scripts/discord-dm.sh"
|
||||||
|
export STATE="${STATE:-$ROOT/.claude/scripts/.edr-watch-state.json}"
|
||||||
|
TARGET="dev" # #dev-alerts = Mike + Howard, private
|
||||||
|
|
||||||
|
# Pull last 24h of detections (list includes responseData) and emit any NEW
|
||||||
|
# isolate-host events as one pipe-delimited line each; update state in-place.
|
||||||
|
new_events="$($EDR --json detections --days 1 --limit 500 2>/dev/null | python -c '
|
||||||
|
import sys, json, os
|
||||||
|
state_path = os.environ["STATE"]
|
||||||
|
try:
|
||||||
|
rows = json.load(sys.stdin)
|
||||||
|
rows = rows if isinstance(rows, list) else rows.get("data", rows.get("alerts", []))
|
||||||
|
except Exception:
|
||||||
|
sys.exit(0) # API hiccup: emit nothing, do not churn state
|
||||||
|
try:
|
||||||
|
seen = set(json.load(open(state_path)))
|
||||||
|
except Exception:
|
||||||
|
seen = set()
|
||||||
|
fresh = []
|
||||||
|
for a in rows:
|
||||||
|
rd = a.get("responseData") or []
|
||||||
|
names = [r.get("name") for r in rd] if isinstance(rd, list) else []
|
||||||
|
if "isolate-host" not in names:
|
||||||
|
continue
|
||||||
|
aid = a.get("id")
|
||||||
|
if not aid or aid in seen:
|
||||||
|
continue
|
||||||
|
seen.add(aid)
|
||||||
|
fresh.append("|".join(str(a.get(k, "")) for k in
|
||||||
|
("id", "hostname", "organizationName", "severity", "sourceName", "eventTime")))
|
||||||
|
# keep state bounded
|
||||||
|
json.dump(sorted(seen)[-500:], open(state_path, "w"))
|
||||||
|
print("\n".join(fresh))
|
||||||
|
' 2>/dev/null)"
|
||||||
|
|
||||||
|
[ -z "$new_events" ] && exit 0
|
||||||
|
|
||||||
|
while IFS='|' read -r aid host org sev rule ts; do
|
||||||
|
[ -z "$aid" ] && continue
|
||||||
|
msg="[EDR AUTO-ISOLATION] **$host** was auto-isolated by Datto EDR
|
||||||
|
- Client: $org
|
||||||
|
- Rule: $rule (severity: $sev)
|
||||||
|
- Time: $ts
|
||||||
|
- This machine was cut off the network by an EDR automated response. Verify it is intended before restoring.
|
||||||
|
- Console: https://azcomp4587.infocyte.com (alert id: $aid)"
|
||||||
|
if [ "${DRY_RUN:-0}" = "1" ]; then
|
||||||
|
printf '[DRY_RUN] would post to #%s:\n%s\n\n' "$TARGET" "$msg"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! printf '%s' "$msg" | $DM "$TARGET" 2>/dev/null; then
|
||||||
|
bash "$ROOT/.claude/scripts/log-skill-error.sh" "edr-isolation-watch" "discord post failed for $host ($aid)" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done <<< "$new_events"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Generate base64 of the WatchdogAlertsSection component."""
|
"""Generate base64 of the WatchdogAlertsSection component."""
|
||||||
import base64, sys
|
import base64, os, sys
|
||||||
|
|
||||||
BT = chr(96)
|
BT = chr(96)
|
||||||
BULLET = "\xb7"
|
BULLET = "\xb7"
|
||||||
@@ -195,7 +195,9 @@ lines = [
|
|||||||
component = "".join(lines)
|
component = "".join(lines)
|
||||||
b64 = base64.b64encode(component.encode("utf-8")).decode("ascii")
|
b64 = base64.b64encode(component.encode("utf-8")).decode("ascii")
|
||||||
|
|
||||||
with open("D:/claudetools/.claude/scripts/component.b64", "w") as f:
|
# Write next to this script — the repo root is machine-specific, never hardcode it.
|
||||||
|
out_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "component.b64")
|
||||||
|
with open(out_path, "w") as f:
|
||||||
f.write(b64)
|
f.write(b64)
|
||||||
|
|
||||||
print(f"Component: {len(component)} chars")
|
print(f"Component: {len(component)} chars")
|
||||||
|
|||||||
45
.claude/scripts/get-identity.sh
Normal file
45
.claude/scripts/get-identity.sh
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# get-identity.sh — Read identity.json and export user/machine vars for attribution
|
||||||
|
#
|
||||||
|
# Source this at the start of any skill that needs attribution (bot alerts, commits,
|
||||||
|
# logs, RMM operations). Exports $USER_NAME, $USER_SHORT, $MACHINE, $USER_EMAIL.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# source .claude/scripts/get-identity.sh
|
||||||
|
# echo "[RMM] $USER_SHORT deployed to X machines..."
|
||||||
|
#
|
||||||
|
# Soft-fails: if identity.json is missing, exports "Unknown" values and returns 1
|
||||||
|
# (but does NOT exit, so the caller continues). This ensures skills never break on
|
||||||
|
# missing identity - they just attribute to "Unknown".
|
||||||
|
|
||||||
|
# Resolve the repo root from this script's own location (mirrors vault.sh) so callers
|
||||||
|
# don't have to know it. An already-set CLAUDETOOLS_ROOT still wins, and identity.json's
|
||||||
|
# claudetools_root overrides both. Never hardcode a drive letter.
|
||||||
|
if [ -z "${CLAUDETOOLS_ROOT:-}" ]; then
|
||||||
|
export CLAUDETOOLS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||||
|
|
||||||
|
if [ ! -f "$IDENTITY_FILE" ]; then
|
||||||
|
echo "[WARNING] identity.json not found at $IDENTITY_FILE - attribution will be 'Unknown'" >&2
|
||||||
|
export USER_NAME="Unknown User"
|
||||||
|
export USER_SHORT="unknown"
|
||||||
|
export MACHINE="unknown-machine"
|
||||||
|
export USER_EMAIL="unknown@unknown.com"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export USER_NAME=$(jq -r '.full_name // .user // "Unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "Unknown")
|
||||||
|
export USER_SHORT=$(jq -r '.user // "unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown")
|
||||||
|
export MACHINE=$(jq -r '.machine // "unknown"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown")
|
||||||
|
export USER_EMAIL=$(jq -r '.email // "unknown@unknown.com"' "$IDENTITY_FILE" 2>/dev/null || echo "unknown@unknown.com")
|
||||||
|
|
||||||
|
# identity.json is authoritative for the root and the vault location.
|
||||||
|
_root_override=$(jq -r '.claudetools_root // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||||
|
[ -n "$_root_override" ] && [ -d "$_root_override" ] && export CLAUDETOOLS_ROOT="$_root_override"
|
||||||
|
export VAULT_ROOT=$(jq -r '.vault_path // empty' "$IDENTITY_FILE" 2>/dev/null)
|
||||||
|
unset _root_override
|
||||||
|
|
||||||
|
# Success
|
||||||
|
return 0
|
||||||
36
.claude/scripts/gps-rmm-autoenroll.sh
Normal file
36
.claude/scripts/gps-rmm-autoenroll.sh
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# gps-rmm-autoenroll.sh — auto-enroll loop for the GPS->RMM audit.
|
||||||
|
# Every run: push the generic Staging installer (DARK-STORM-3150) to GPS-customer
|
||||||
|
# machines that are ONLINE in ScreenConnect but missing from GuruRMM, wait for them
|
||||||
|
# to enroll, then reassign Staging agents to their real client (hostname->Syncro).
|
||||||
|
# Registered as Windows task GPS-RMM-AutoEnroll (every 30 min). Remove the task when
|
||||||
|
# targets.json is fully enrolled. Safe to re-run: server v6.77+ dedups by device_id.
|
||||||
|
set -uo pipefail
|
||||||
|
cd /c/claudetools || exit 1
|
||||||
|
LOG="projects/gps-rmm-audit/autoenroll.log"
|
||||||
|
TS="$(date '+%Y-%m-%d %H:%M')"
|
||||||
|
|
||||||
|
eval "$(bash .claude/scripts/rmm-auth.sh 2>/dev/null)" >/dev/null 2>&1
|
||||||
|
if [ -z "${TOKEN:-}" ]; then echo "$TS auth FAILED" >> "$LOG"; exit 0; fi
|
||||||
|
SEC="$(bash .claude/scripts/vault.sh get-field msp-tools/screenconnect.sops.yaml credentials.api_secret 2>/dev/null | tr -d '\r\n')"
|
||||||
|
SK="$(bash .claude/scripts/vault.sh get-field msp-tools/syncro-howard credentials.credential 2>/dev/null | tr -d '\r\n')"
|
||||||
|
if [ -z "$SK" ]; then echo "$TS syncro key read FAILED" >> "$LOG"; exit 0; fi
|
||||||
|
|
||||||
|
OUT="$(RMM="$RMM" TOK="$TOKEN" SK="$SK" SC_SECRET="$SEC" python projects/gps-rmm-audit/tools/rebuild-and-push.py 2>/dev/null | tail -2)"
|
||||||
|
PUSHED="$(echo "$OUT" | grep -oE 'pushed: [0-9]+' | grep -oE '[0-9]+' | head -1 || true)"
|
||||||
|
PUSHED="${PUSHED:-0}"
|
||||||
|
echo "$TS sweep: $(echo "$OUT" | head -1)" >> "$LOG"
|
||||||
|
|
||||||
|
if [ "${PUSHED:-0}" -gt 0 ]; then
|
||||||
|
sleep 150
|
||||||
|
RES="$(RMM="$RMM" TOK="$TOKEN" SK="$SK" python projects/gps-rmm-audit/tools/reassign-staging.py 2>/dev/null)"
|
||||||
|
MOVED="$(echo "$RES" | grep -cE '^ .+ -> ' || true)"
|
||||||
|
MOVED="${MOVED:-0}"
|
||||||
|
echo "$TS pushed $PUSHED, reassigned $MOVED:" >> "$LOG"
|
||||||
|
echo "$RES" | grep -E '^ ' >> "$LOG"
|
||||||
|
if [ "${MOVED:-0}" -gt 0 ]; then
|
||||||
|
NAMES="$(echo "$RES" | grep -E '^ .+ -> ' | sed 's/^ //' | tr '\n' '; ')"
|
||||||
|
bash .claude/scripts/post-bot-alert.sh "[RMM] auto-enroll: $NAMES" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
5
.claude/scripts/gps-rmm-progress-hidden.vbs
Normal file
5
.claude/scripts/gps-rmm-progress-hidden.vbs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
' gps-rmm-progress-hidden.vbs — launch the GPS->RMM progress check with no visible window.
|
||||||
|
' Run via the GPS-RMM-Progress scheduled task through wscript.exe (GUI host, no console),
|
||||||
|
' which starts bash with window style 0 (hidden) so the daily check no longer flashes a
|
||||||
|
' console window on the desktop. Runtime env is identical to a direct bash launch.
|
||||||
|
CreateObject("WScript.Shell").Run """C:\Program Files\Git\bin\bash.exe"" -lc ""cd /c/claudetools && bash .claude/scripts/gps-rmm-progress-check.sh""", 0, False
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# guruscan-agent-test.sh - Deploy GuruScan to a Windows GuruRMM agent and run an
|
# guruscan-agent-test.sh - Deploy GuruScan to a Windows GuruRMM agent and run an
|
||||||
# end-to-end smoke test (full 3-engine chain, EICAR-seeded, clean mode), pulling
|
# end-to-end smoke test (full 3-engine chain, clean mode), pulling every log back
|
||||||
# every log back for review.
|
# for review.
|
||||||
#
|
#
|
||||||
# Phases:
|
# Phases:
|
||||||
# prep - upload module to C:\GuruScan, Defender-exclude tool/test dirs,
|
# prep - upload module to C:\GuruScan, Defender-exclude tool dirs,
|
||||||
# download RKill+Emsisoft, fetch HitmanPro, seed EICAR, verify ready
|
# download RKill+Emsisoft, fetch HitmanPro, verify ready
|
||||||
# scan - dispatch Invoke-GuruScan.ps1 -Headless (clean mode) and poll
|
# scan - dispatch Invoke-GuruScan.ps1 -Headless (clean mode) and poll
|
||||||
# collect - pull results.json + per-scanner logs into the repo
|
# collect - pull results.json + per-scanner logs into the repo
|
||||||
# all - prep then scan then collect
|
# all - prep then scan then collect
|
||||||
@@ -14,10 +14,9 @@
|
|||||||
# bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>
|
# bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>
|
||||||
#
|
#
|
||||||
# Mirrors the RMM plumbing in run-onboarding-diagnostic.sh (vault auth -> JWT ->
|
# Mirrors the RMM plumbing in run-onboarding-diagnostic.sh (vault auth -> JWT ->
|
||||||
# chunked base64 upload -> dispatch -> poll). EICAR is the standard harmless AV
|
# chunked base64 upload -> dispatch -> poll). Detection testing uses the real
|
||||||
# test file; it is assembled on the endpoint (never written to this host) and
|
# malware-samples set via the scan-one / verify-each phases. Prep's EICAR seed is
|
||||||
# dropped only into a Defender-excluded folder so GuruScan's own engines are what
|
# DISABLED by default (kept for future scanners) -- enable with SEED_EICAR=1.
|
||||||
# detect it.
|
|
||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
@@ -25,6 +24,11 @@ TARGET="${1:-}"
|
|||||||
PHASE="${2:-all}"
|
PHASE="${2:-all}"
|
||||||
SCANNER_ARG="${3:-}"
|
SCANNER_ARG="${3:-}"
|
||||||
|
|
||||||
|
# EICAR test-file seeding in the prep phase. DISABLED by default -- kept (not
|
||||||
|
# deleted) so it can be switched back on to smoke-test detection as new scanners
|
||||||
|
# are added to the chain. Enable per-run with: SEED_EICAR=1 bash guruscan-agent-test.sh ...
|
||||||
|
SEED_EICAR="${SEED_EICAR:-0}"
|
||||||
|
|
||||||
if [ -z "$TARGET" ]; then
|
if [ -z "$TARGET" ]; then
|
||||||
echo "[ERROR] Usage: bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>" >&2
|
echo "[ERROR] Usage: bash guruscan-agent-test.sh <hostname|uuid> <prep|scan|collect|all>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -273,12 +277,12 @@ phase_prep() {
|
|||||||
upload_file "$GS_DIR/$f" "C:\\GuruScan\\$f" || { _logerr "upload failed" --context "file=$f host=$AGENT_HOST"; return 1; }
|
upload_file "$GS_DIR/$f" "C:\\GuruScan\\$f" || { _logerr "upload failed" --context "file=$f host=$AGENT_HOST"; return 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
# 2) Defender exclusions for tool + downloads + test dirs (so Defender does not
|
# 2) Defender exclusions for tool + downloads dirs (so Defender does not nuke
|
||||||
# nuke the scanner EXEs or grab EICAR before GuruScan's engines run).
|
# the scanner EXEs before GuruScan's engines run).
|
||||||
local sf="$WORK_DIR/defender.ps1"
|
local sf="$WORK_DIR/defender.ps1"
|
||||||
cat > "$sf" <<'PS'
|
cat > "$sf" <<'PS'
|
||||||
$ErrorActionPreference='Continue'
|
$ErrorActionPreference='Continue'
|
||||||
foreach($p in @('C:\GuruScan','C:\GuruScan\downloads','C:\GuruScanTest','C:\EmsisoftCmd')){
|
foreach($p in @('C:\GuruScan','C:\GuruScan\downloads','C:\EmsisoftCmd')){
|
||||||
try { Add-MpPreference -ExclusionPath $p -ErrorAction Stop; Write-Output "EXCLUDED $p" }
|
try { Add-MpPreference -ExclusionPath $p -ErrorAction Stop; Write-Output "EXCLUDED $p" }
|
||||||
catch { Write-Output ("EXCLUDE-SKIP " + $p + " : " + $_.Exception.Message) }
|
catch { Write-Output ("EXCLUDE-SKIP " + $p + " : " + $_.Exception.Message) }
|
||||||
}
|
}
|
||||||
@@ -310,12 +314,16 @@ try {
|
|||||||
PS
|
PS
|
||||||
run_ps "$sf3" 300 70 "download-hitmanpro" || echo "[WARN] HitmanPro download dispatch issue"
|
run_ps "$sf3" 300 70 "download-hitmanpro" || echo "[WARN] HitmanPro download dispatch issue"
|
||||||
|
|
||||||
# 5) seed EICAR into the Defender-excluded test folder (assembled on endpoint)
|
# 5) [OPTIONAL] seed EICAR into a Defender-excluded test folder (assembled on
|
||||||
local sf4="$WORK_DIR/eicar.ps1"
|
# the endpoint). DISABLED unless SEED_EICAR=1 -- retained so detection can be
|
||||||
cat > "$sf4" <<'PS'
|
# smoke-tested as new scanners are added to the chain.
|
||||||
|
if [ "$SEED_EICAR" = "1" ]; then
|
||||||
|
local sfe="$WORK_DIR/eicar.ps1"
|
||||||
|
cat > "$sfe" <<'PS'
|
||||||
$ErrorActionPreference='Continue'
|
$ErrorActionPreference='Continue'
|
||||||
$dir='C:\GuruScanTest'
|
$dir='C:\GuruScanTest'
|
||||||
if(-not (Test-Path $dir)){New-Item -ItemType Directory -Path $dir -Force|Out-Null}
|
if(-not (Test-Path $dir)){New-Item -ItemType Directory -Path $dir -Force|Out-Null}
|
||||||
|
try { Add-MpPreference -ExclusionPath $dir -ErrorAction Stop } catch {}
|
||||||
# Standard EICAR test signature, assembled from fragments so it is never stored
|
# Standard EICAR test signature, assembled from fragments so it is never stored
|
||||||
# contiguously anywhere except the on-disk test file.
|
# contiguously anywhere except the on-disk test file.
|
||||||
$e = 'X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR' + '-STANDARD-ANTIVIRUS-' + 'TEST-FILE!$H+H*'
|
$e = 'X5O!P%@AP[4\PZX54(P^)7CC)7}' + '$EICAR' + '-STANDARD-ANTIVIRUS-' + 'TEST-FILE!$H+H*'
|
||||||
@@ -329,9 +337,12 @@ if(Test-Path $f){
|
|||||||
Write-Output "EICAR MISSING after write - Defender (or other AV) grabbed it despite exclusion"
|
Write-Output "EICAR MISSING after write - Defender (or other AV) grabbed it despite exclusion"
|
||||||
}
|
}
|
||||||
PS
|
PS
|
||||||
run_ps "$sf4" 60 24 "seed-eicar" || { _logerr "EICAR seed failed" --context "host=$AGENT_HOST"; return 1; }
|
run_ps "$sfe" 60 24 "seed-eicar" || { _logerr "EICAR seed failed" --context "host=$AGENT_HOST"; return 1; }
|
||||||
|
else
|
||||||
|
echo "[INFO] EICAR seeding disabled (set SEED_EICAR=1 to enable)"
|
||||||
|
fi
|
||||||
|
|
||||||
# 6) readiness check - confirm all three scanner binaries present + EICAR present
|
# 6) readiness check - confirm all three scanner binaries + module present
|
||||||
local sf5="$WORK_DIR/ready.ps1"
|
local sf5="$WORK_DIR/ready.ps1"
|
||||||
cat > "$sf5" <<'PS'
|
cat > "$sf5" <<'PS'
|
||||||
$ErrorActionPreference='Continue'
|
$ErrorActionPreference='Continue'
|
||||||
@@ -339,8 +350,7 @@ $need=@{
|
|||||||
'RKill' ='C:\GuruScan\downloads\rkill.exe';
|
'RKill' ='C:\GuruScan\downloads\rkill.exe';
|
||||||
'Emsisoft' ='C:\GuruScan\downloads\EmsisoftCommandlineScanner64.exe';
|
'Emsisoft' ='C:\GuruScan\downloads\EmsisoftCommandlineScanner64.exe';
|
||||||
'HitmanPro' ='C:\GuruScan\downloads\HitmanPro_x64.exe';
|
'HitmanPro' ='C:\GuruScan\downloads\HitmanPro_x64.exe';
|
||||||
'Module' ='C:\GuruScan\GuruScan.psm1';
|
'Module' ='C:\GuruScan\GuruScan.psm1'
|
||||||
'EICAR' ='C:\GuruScanTest\eicar_test.com'
|
|
||||||
}
|
}
|
||||||
$ok=$true
|
$ok=$true
|
||||||
foreach($k in $need.Keys){
|
foreach($k in $need.Keys){
|
||||||
@@ -353,7 +363,7 @@ PS
|
|||||||
if grep -q 'ALL-READY' "$WORK_DIR/last_result.json" 2>/dev/null || \
|
if grep -q 'ALL-READY' "$WORK_DIR/last_result.json" 2>/dev/null || \
|
||||||
echo "$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)" | grep -q 'ALL-READY'; then
|
echo "$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)" | grep -q 'ALL-READY'; then
|
||||||
echo "[OK] prep complete - endpoint is READY for scan"
|
echo "[OK] prep complete - endpoint is READY for scan"
|
||||||
post_alert "[RMM] GuruScan test: prepped $AGENT_HOST (module+3 scanners+EICAR staged)"
|
post_alert "[RMM] GuruScan test: prepped $AGENT_HOST (module+3 scanners staged)"
|
||||||
else
|
else
|
||||||
echo "[WARN] prep finished but not all components READY - review output above before scanning"
|
echo "[WARN] prep finished but not all components READY - review output above before scanning"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
# bash post-bot-alert.sh "message text" bot # force #bot-alerts
|
# bash post-bot-alert.sh "message text" bot # force #bot-alerts
|
||||||
# echo "message text" | bash post-bot-alert.sh
|
# echo "message text" | bash post-bot-alert.sh
|
||||||
#
|
#
|
||||||
|
# Identity: This script sources get-identity.sh, making $USER_SHORT, $USER_NAME,
|
||||||
|
# $MACHINE, $USER_EMAIL available. Callers can use these in messages for correct
|
||||||
|
# attribution (e.g., "[RMM] $USER_SHORT deployed..."). The script itself doesn't
|
||||||
|
# auto-inject identity - callers must explicitly use the vars when needed.
|
||||||
|
#
|
||||||
# Token resolution (first hit wins):
|
# Token resolution (first hit wins):
|
||||||
# 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token
|
# 1. SOPS vault: projects/discord-bot/bot-token.sops.yaml field credentials.bot_token
|
||||||
# 2. projects/discord-bot/.env key DISCORD_TOKEN
|
# 2. projects/discord-bot/.env key DISCORD_TOKEN
|
||||||
@@ -27,6 +32,9 @@ BOT_CHANNEL_ID="624710699771232265" # #bot-alerts — default (Syncro + gene
|
|||||||
DEV_CHANNEL_ID="1509998508198068484" # #dev-alerts — private RMM/Dev alerts (Howard + Mike only)
|
DEV_CHANNEL_ID="1509998508198068484" # #dev-alerts — private RMM/Dev alerts (Howard + Mike only)
|
||||||
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
||||||
|
|
||||||
|
# Load identity for attribution (soft-fail if missing)
|
||||||
|
source "$ROOT/.claude/scripts/get-identity.sh" 2>/dev/null || true
|
||||||
|
|
||||||
# --- message (arg or stdin) ---
|
# --- message (arg or stdin) ---
|
||||||
MSG="${1:-}"
|
MSG="${1:-}"
|
||||||
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
|
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
|
||||||
|
|||||||
@@ -38,7 +38,20 @@ read_script() { # $1 = file path or "-"
|
|||||||
}
|
}
|
||||||
|
|
||||||
encode_b64() { # stdin: UTF-8 script -> stdout: UTF-16LE base64 (no BOM, no wraps)
|
encode_b64() { # stdin: UTF-8 script -> stdout: UTF-16LE base64 (no BOM, no wraps)
|
||||||
iconv -f UTF-8 -t UTF-16LE | base64 -w0
|
if command -v iconv >/dev/null 2>&1; then
|
||||||
|
iconv -f UTF-8 -t UTF-16LE | base64 -w0
|
||||||
|
else
|
||||||
|
# Git-for-Windows ships the iconv *library* (msys-iconv-2.dll) but NOT iconv.exe,
|
||||||
|
# and has no pacman to install it. Fall back to Python (present fleet-wide) for the
|
||||||
|
# UTF-16LE -> base64 encode. Binary stdin/stdout so no CRLF translation.
|
||||||
|
local py
|
||||||
|
py="$(command -v py || command -v python3 || command -v python || true)"
|
||||||
|
if [ -z "$py" ]; then
|
||||||
|
echo "[ERROR] neither iconv nor python available for UTF-16LE encoding" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
"$py" -c "import sys,base64; sys.stdout.write(base64.b64encode(sys.stdin.buffer.read().decode('utf-8').encode('utf-16-le')).decode())"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
size_gate() { # $1 = encoded string, $2 = force flag (0/1)
|
size_gate() { # $1 = encoded string, $2 = force flag (0/1)
|
||||||
|
|||||||
51
.claude/scripts/register-edr-watcher.ps1
Normal file
51
.claude/scripts/register-edr-watcher.ps1
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# register-edr-watcher.ps1
|
||||||
|
# Register the "ClaudeTools - EDR Isolation Watcher" scheduled task on this Windows
|
||||||
|
# machine (the same box that runs GPS-RMM-AutoEnroll et al). The task runs
|
||||||
|
# edr-isolation-watch.sh every 10 minutes, which polls Datto EDR for hosts auto-
|
||||||
|
# isolated by an EDR response and posts each NEW one to #dev-alerts (Mike + Howard).
|
||||||
|
#
|
||||||
|
# Interim alerting until the GuruRMM EDR webhook integration (Feature 6) lands.
|
||||||
|
# Idempotent: -Force replaces any existing task with the same name.
|
||||||
|
#
|
||||||
|
# Run from an ordinary (non-admin) PowerShell:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File C:\claudetools\.claude\scripts\register-edr-watcher.ps1
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$TaskName = "ClaudeTools - EDR Isolation Watcher"
|
||||||
|
|
||||||
|
# Launch via a wscript.exe VBS wrapper (GUI-subsystem host) so bash starts hidden and
|
||||||
|
# NO console window flashes on the desktop every 10 min. Launching bash.exe directly is
|
||||||
|
# a console-subsystem app with LogonType=Interactive + Hidden=False -> visible flash.
|
||||||
|
# Mirrors the gps-rmm-progress-hidden.vbs fix. Do NOT revert to a raw bash.exe action.
|
||||||
|
$BashExe = "C:\Program Files\Git\bin\bash.exe"
|
||||||
|
if (-not (Test-Path $BashExe)) { Write-Host "[ERROR] Git bash not found at $BashExe" -ForegroundColor Red; exit 1 }
|
||||||
|
|
||||||
|
$ScriptWin = "C:\claudetools\.claude\scripts\edr-isolation-watch.sh"
|
||||||
|
if (-not (Test-Path $ScriptWin)) { Write-Host "[ERROR] Watcher not found at $ScriptWin" -ForegroundColor Red; exit 1 }
|
||||||
|
|
||||||
|
$WScript = "C:\Windows\System32\wscript.exe"
|
||||||
|
$Vbs = "C:\claudetools\.claude\scripts\edr-isolation-watch-hidden.vbs"
|
||||||
|
if (-not (Test-Path $Vbs)) { Write-Host "[ERROR] Hidden wrapper not found at $Vbs" -ForegroundColor Red; exit 1 }
|
||||||
|
|
||||||
|
$Action = New-ScheduledTaskAction -Execute $WScript -Argument "`"$Vbs`""
|
||||||
|
|
||||||
|
# Every 10 minutes, indefinitely (10-year duration avoids the MaxValue quirk).
|
||||||
|
$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date `
|
||||||
|
-RepetitionInterval (New-TimeSpan -Minutes 10) `
|
||||||
|
-RepetitionDuration (New-TimeSpan -Days 3650)
|
||||||
|
|
||||||
|
# Run as the current user, only when logged on (needs the interactive vault/env,
|
||||||
|
# same posture as the GrepAI watcher). No admin required.
|
||||||
|
$Principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
|
||||||
|
|
||||||
|
$Settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||||
|
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
|
||||||
|
-MultipleInstances IgnoreNew -Hidden
|
||||||
|
|
||||||
|
Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger `
|
||||||
|
-Principal $Principal -Settings $Settings `
|
||||||
|
-Description "Polls Datto EDR every 10 min for auto-isolated hosts; posts new ones to #dev-alerts. Interim until GuruRMM EDR webhook (Feature 6)." -Force | Out-Null
|
||||||
|
|
||||||
|
Write-Host "[OK] Registered scheduled task: $TaskName (every 10 min)" -ForegroundColor Green
|
||||||
|
Get-ScheduledTask -TaskName $TaskName | Select-Object TaskName, State | Format-Table -AutoSize
|
||||||
@@ -47,14 +47,26 @@ if (-not (Test-Path $Script)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve the py launcher's full path (the action's Execute wants an absolute
|
# Resolve pythonW.exe (the GUI-subsystem Python host) so the detector runs with NO
|
||||||
# path; "py" alone usually resolves but we pin it for reliability under the
|
# console window flashing on the desktop at logon / every 4h. Using py.exe or
|
||||||
# Task Scheduler's environment).
|
# python.exe (console subsystem) draws a visible window each run. Prefer pythonw next
|
||||||
$PyCmd = Get-Command py -ErrorAction SilentlyContinue
|
# to the active interpreter; fall back to a PATH lookup, then py.exe as a last resort.
|
||||||
|
$PyPath = $null
|
||||||
|
$PyCmd = Get-Command py -ErrorAction SilentlyContinue
|
||||||
if ($null -ne $PyCmd) {
|
if ($null -ne $PyCmd) {
|
||||||
$PyPath = $PyCmd.Source
|
try {
|
||||||
} else {
|
$exe = (& py -c "import sys;print(sys.executable)").Trim()
|
||||||
$PyPath = "py" # fall back to PATH resolution at run time
|
$wexe = Join-Path (Split-Path $exe) "pythonw.exe"
|
||||||
|
if (Test-Path $wexe) { $PyPath = $wexe }
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if ($null -eq $PyPath) {
|
||||||
|
$PwCmd = Get-Command pythonw -ErrorAction SilentlyContinue
|
||||||
|
if ($null -ne $PwCmd) { $PyPath = $PwCmd.Source }
|
||||||
|
}
|
||||||
|
if ($null -eq $PyPath) {
|
||||||
|
if ($null -ne $PyCmd) { $PyPath = $PyCmd.Source } else { $PyPath = "py" }
|
||||||
|
Write-Host "[WARNING] pythonw.exe not found; task may flash a console window." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
$Action = New-ScheduledTaskAction `
|
$Action = New-ScheduledTaskAction `
|
||||||
@@ -76,7 +88,8 @@ $Settings = New-ScheduledTaskSettingsSet `
|
|||||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
|
-ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
|
||||||
-MultipleInstances IgnoreNew `
|
-MultipleInstances IgnoreNew `
|
||||||
-StartWhenAvailable `
|
-StartWhenAvailable `
|
||||||
-DontStopOnIdleEnd
|
-DontStopOnIdleEnd `
|
||||||
|
-Hidden
|
||||||
|
|
||||||
Register-ScheduledTask `
|
Register-ScheduledTask `
|
||||||
-TaskName $TaskName `
|
-TaskName $TaskName `
|
||||||
|
|||||||
82
.claude/scripts/syncro-env.sh
Normal file
82
.claude/scripts/syncro-env.sh
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# syncro-env.sh — Resolve the repo root and the caller's Syncro API key from the SOPS vault.
|
||||||
|
#
|
||||||
|
# Source this instead of hardcoding a repo path or an API key:
|
||||||
|
# source "$(dirname "${BASH_SOURCE[0]}")/syncro-env.sh" # or by absolute path
|
||||||
|
# curl -s "$SYNCRO_BASE/customers?api_key=$SYNCRO_API_KEY"
|
||||||
|
#
|
||||||
|
# Exports: CLAUDETOOLS_ROOT, VAULT_ROOT, SYNCRO_USER, SYNCRO_BASE, SYNCRO_API_KEY
|
||||||
|
#
|
||||||
|
# Root resolution mirrors vault.sh: derive from this script's own location, then let
|
||||||
|
# identity.json's claudetools_root override. Never hardcode a drive letter — the repo
|
||||||
|
# lives at C:/claudetools on some machines and D:/claudetools on others.
|
||||||
|
#
|
||||||
|
# Soft-fails like get-identity.sh: on any failure it warns, leaves SYNCRO_API_KEY empty,
|
||||||
|
# and returns 1 without exiting, so a sourcing skill degrades instead of dying.
|
||||||
|
|
||||||
|
_syncro_env_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
export CLAUDETOOLS_ROOT="$(cd "$_syncro_env_dir/../.." && pwd)"
|
||||||
|
_identity="$CLAUDETOOLS_ROOT/.claude/identity.json"
|
||||||
|
|
||||||
|
export SYNCRO_BASE="https://computerguru.syncromsp.com/api/v1"
|
||||||
|
export SYNCRO_API_KEY=""
|
||||||
|
export SYNCRO_USER=""
|
||||||
|
export VAULT_ROOT=""
|
||||||
|
|
||||||
|
if [ ! -f "$_identity" ]; then
|
||||||
|
echo "[WARNING] identity.json not found at $_identity — Syncro calls will be unauthenticated" >&2
|
||||||
|
return 1 2>/dev/null || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# identity.json is authoritative for both the repo root and the vault location.
|
||||||
|
_root_override=$(jq -r '.claudetools_root // empty' "$_identity" 2>/dev/null)
|
||||||
|
[ -n "$_root_override" ] && [ -d "$_root_override" ] && export CLAUDETOOLS_ROOT="$_root_override"
|
||||||
|
export VAULT_ROOT=$(jq -r '.vault_path // empty' "$_identity" 2>/dev/null)
|
||||||
|
export SYNCRO_USER=$(jq -r '.user // empty' "$_identity" 2>/dev/null)
|
||||||
|
|
||||||
|
# Per-user token — attribution in Syncro depends on using the right one.
|
||||||
|
# Map a user name to their vaulted key path; empty = no key vaulted for them.
|
||||||
|
_syncro_key_path() {
|
||||||
|
case "$1" in
|
||||||
|
mike) echo "msp-tools/syncro" ;;
|
||||||
|
howard) echo "msp-tools/syncro-howard" ;;
|
||||||
|
winter) echo "msp-tools/syncro-winter" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prefer the requester's key when running on someone's behalf (e.g. the Discord
|
||||||
|
# bot sets CLAUDETOOLS_REQUESTER_USER per thread) so Syncro attributes actions
|
||||||
|
# to the person who asked. Fall back to the identity.json user when the
|
||||||
|
# requester has no vaulted key (e.g. rob) — never hard-fail on the preference.
|
||||||
|
_vault_path=""
|
||||||
|
_requester="${CLAUDETOOLS_REQUESTER_USER:-}"
|
||||||
|
if [ -n "$_requester" ] && [ "$_requester" != "$SYNCRO_USER" ]; then
|
||||||
|
_req_path=$(_syncro_key_path "$_requester")
|
||||||
|
if [ -n "$_req_path" ]; then
|
||||||
|
_vault_path="$_req_path"
|
||||||
|
export SYNCRO_USER="$_requester"
|
||||||
|
echo "[INFO] Using Syncro key for requester '$_requester' (attribution)" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -z "$_vault_path" ] && _vault_path=$(_syncro_key_path "$SYNCRO_USER")
|
||||||
|
|
||||||
|
if [ -z "$_vault_path" ]; then
|
||||||
|
echo "[WARNING] No Syncro key vaulted for user '$SYNCRO_USER' — enrichment skipped" >&2
|
||||||
|
return 1 2>/dev/null || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SYNCRO_API_KEY=$(bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" \
|
||||||
|
get-field "$_vault_path" credentials.credential 2>/dev/null | tail -1)
|
||||||
|
export SYNCRO_API_KEY
|
||||||
|
|
||||||
|
if [ -z "$SYNCRO_API_KEY" ]; then
|
||||||
|
echo "[ERROR] Could not read $_vault_path from vault — is SOPS/age configured?" >&2
|
||||||
|
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "syncro-env" \
|
||||||
|
"vault read failed for $_vault_path" --context "user=$SYNCRO_USER" >/dev/null 2>&1 || true
|
||||||
|
return 1 2>/dev/null || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset _syncro_env_dir _identity _root_override _vault_path _requester _req_path
|
||||||
|
unset -f _syncro_key_path
|
||||||
|
return 0 2>/dev/null || true
|
||||||
128
.claude/skills/ask-forum/SKILL.md
Normal file
128
.claude/skills/ask-forum/SKILL.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
name: ask-forum
|
||||||
|
description: "Ask a teammate (Mike/Winter/Howard/anyone with forum access) a question in the private #ct-forum Discord forum and get their human answer back IN THIS SAME SESSION, then act on it - a live three-way between the user, this Claude session, and the teammate. No second bot, no DMs, one tool call (the wait runs server-side in bash, zero tokens while waiting). Triggers: ask Mike/Winter in the forum, ask the team, post a question to ct-forum, get a human answer/decision/sign-off, human-in-the-loop, run something by <person>, wait for their reply."
|
||||||
|
---
|
||||||
|
|
||||||
|
# ask-forum — human-in-the-loop questions via #ct-forum
|
||||||
|
|
||||||
|
Thin wrapper over `.claude/scripts/ask-forum.sh`. Lets THIS session put a question to a
|
||||||
|
human in the private **#ct-forum** Discord forum and receive their answer back in-session,
|
||||||
|
so it can keep going. Use it whenever you need a person's answer, decision, or sign-off
|
||||||
|
mid-task and want it to flow back to the running session — instead of stopping and asking
|
||||||
|
the user to relay it.
|
||||||
|
|
||||||
|
**USE THIS SKILL — do not hand-roll `curl` against the Discord API or the bot token.**
|
||||||
|
Free-handing the raw call is exactly what produced the `&`-on-background bug (a wait that
|
||||||
|
exited instantly and never delivered the answer). The script + this contract encode the
|
||||||
|
correct thread-create, the non-bot reply filter, and the background-wait discipline. Reach
|
||||||
|
for raw API only if this skill genuinely can't do what's needed — and say so.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
- You need a teammate's **answer / decision / approval** and want it back in this session
|
||||||
|
(e.g. "ask Mike whether to deploy", "run this naming by Winter", "get a go/no-go").
|
||||||
|
- A back-and-forth where the human's reply should drive the session's next action.
|
||||||
|
|
||||||
|
Not for: one-way copy-paste delivery of a link/command (that's `discord-dm` → `mike`), or
|
||||||
|
routine `[SYNCRO]`/`[RMM]` status alerts (`post-bot-alert.sh`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ask + BLOCK until a human replies, then act on the answer:
|
||||||
|
bash .claude/scripts/ask-forum.sh "your question" --tag @264814939619721216
|
||||||
|
|
||||||
|
# Re-read a thread you already asked in (one-shot, no waiting):
|
||||||
|
bash .claude/scripts/ask-forum.sh --read <thread_id>
|
||||||
|
|
||||||
|
# Resume blocking on an already-posted thread (e.g. after a timeout):
|
||||||
|
bash .claude/scripts/ask-forum.sh --wait <thread_id> --timeout 1800 --poll 10
|
||||||
|
# add --after <msg_id> to wait for replies STRICTLY AFTER a message you already
|
||||||
|
# consumed (default baseline catches any reply since the thread's starter).
|
||||||
|
```
|
||||||
|
|
||||||
|
## Robustness (hardened after the 2026-07-08 review)
|
||||||
|
- **Pages past the 100-message window:** the poll advances its `after` cursor each
|
||||||
|
cycle, so a reply is found even behind many bot/overflow messages (no false timeout).
|
||||||
|
- **Rate-limit aware:** a `429` is honored (sleeps `retry_after`); a permanent Discord
|
||||||
|
error (Unknown Channel 10003, Missing Access 50001, etc.) **bails immediately** instead
|
||||||
|
of burning the full timeout; only genuine transients keep polling.
|
||||||
|
- **Long questions** (>1900 chars) chunk into the starter + follow-up messages, and each
|
||||||
|
overflow POST is status-checked (warns if one drops, so the question isn't silently
|
||||||
|
truncated).
|
||||||
|
- **`--wait` baseline** is the thread's starter id (starter id == thread id for a forum
|
||||||
|
post), so a reply that arrived *before* the wait started is caught, not missed. Because
|
||||||
|
of this, `--wait` on a thread that already holds a consumed answer re-returns it — pass
|
||||||
|
`--after <msg_id>` when you only want the NEXT reply.
|
||||||
|
- Residual caveat: content slicing assumes a UTF-8 locale (the script sets
|
||||||
|
`LC_ALL=C.UTF-8`); a forced byte-counting `LC_ALL=C` could split a multibyte char at the
|
||||||
|
1900-char cut. Not an issue under the fleet default locale.
|
||||||
|
|
||||||
|
Flags: `--title "short title"` (forum post name; defaults to first 60 chars of the
|
||||||
|
question) · `--timeout SEC` (default 600) · `--poll SEC` (default 8) · `--tag @<id>`
|
||||||
|
(repeatable — pings that person). Discord IDs: mike `264814939619721216`,
|
||||||
|
howard `624667664501178379`, winter `624666486362996755`, rob `261978810713505792`
|
||||||
|
(also in `.claude/users.json`).
|
||||||
|
|
||||||
|
## HARD RULE — long waits run as a proper background task (no shell `&`)
|
||||||
|
|
||||||
|
A blocking `--wait`/ask can sit for many minutes. Run it with the tool's
|
||||||
|
**`run_in_background: true`** and **nothing else** — do NOT append a shell `&`, `disown`,
|
||||||
|
or `nohup`. Adding `&` forks the wait off and the shell exits 0 immediately, orphaning the
|
||||||
|
poll loop so the answer is never delivered (logged friction, twice). Done right, the wait
|
||||||
|
costs **zero tokens** while pending and the harness notifies you the instant a human
|
||||||
|
replies — then you surface the answer to the user **unprompted**. See
|
||||||
|
[[feedback_background_task_no_ampersand]].
|
||||||
|
|
||||||
|
## How it works / correlation
|
||||||
|
- Posts a forum post via `POST /channels/<forum>/threads`, returns a **`thread_id`**.
|
||||||
|
- The session reads only **its own** `thread_id`, so an answer can never be confused with
|
||||||
|
another question's. Replies route to whoever asked — Mike's thread → Mike's session,
|
||||||
|
yours → yours, no crossover.
|
||||||
|
- **Any non-bot reply counts as the answer** (first human wins). Anyone with #ct-forum
|
||||||
|
access can answer; the script never filters by person. Bot chatter is filtered out.
|
||||||
|
|
||||||
|
## Model & scope (decided with Mike, 2026-07-08)
|
||||||
|
- **Forum-only.** No DM replies — answers stay team-visible.
|
||||||
|
- **Good-enough durability:** answers are captured while the session is open; Discord keeps
|
||||||
|
the history, so any thread is cheap to re-read (`--read`) later in the same session. It is
|
||||||
|
NOT restart-safe (close the session → nothing is watching); that was an accepted tradeoff
|
||||||
|
for the cheapest option. Full restart/close-safe capture would be the coord-backed
|
||||||
|
registry or the event-driven bot-router — build only if asked.
|
||||||
|
- The **BEAST bot ignores #ct-forum** (patch in `projects/discord-bot/bot/main.py`) so it
|
||||||
|
doesn't auto-answer and collide with the asking session.
|
||||||
|
|
||||||
|
## Access & permissions
|
||||||
|
- **Channel:** #ct-forum (`1522960388432465950`), private — `@everyone` View DENY; Howard +
|
||||||
|
the bot explicitly allowed; Mike sees it as owner. To let someone else answer, they need
|
||||||
|
**View Channel** on #ct-forum (grant a role like Techs, or the person). Winter/Rob are not
|
||||||
|
in by default.
|
||||||
|
|
||||||
|
### Bot capability map — the limit of control (tested 2026-07-08)
|
||||||
|
What the bot can do in #ct-forum with its current permissions, and where the wall is:
|
||||||
|
|
||||||
|
| Bot CAN (no grant needed) | Bot CANNOT (needs a permission grant) |
|
||||||
|
|---|---|
|
||||||
|
| Read messages / thread / members / guild channels | **Pin** a message → 403 (needs *Manage Messages*) |
|
||||||
|
| Create a forum post (thread), post follow-up messages | **Delete** a thread → 403 (needs *Manage Threads*) |
|
||||||
|
| Edit its OWN messages; add/remove reactions; typing | **Archive** a thread → 403 (needs *Manage Threads*) |
|
||||||
|
| Rename / lock a thread it owns (see trap below) | **Unlock** a thread once locked → 403 (needs *Manage Threads*) |
|
||||||
|
|
||||||
|
- **One-way trap:** as thread owner the bot can *lock* a thread (200) but then **cannot
|
||||||
|
unlock or rename it** (403) — locking without *Manage Threads* is irreversible. So the
|
||||||
|
skill does NOT lock/rename threads; treat thread lifecycle as off-limits until granted.
|
||||||
|
- **To enable cleanup:** owner grants the ClaudeTools bot **Manage Threads** on #ct-forum
|
||||||
|
(Edit Channel → Permissions → bot → Manage Threads → Allow). Then delete/archive/unlock
|
||||||
|
work. **Manage Messages** additionally enables pin.
|
||||||
|
- Reactions + own-message edits DO work with no grant — usable for lightweight
|
||||||
|
acknowledgement (react to a reply, or edit the question to "[answered]") if ever wanted.
|
||||||
|
|
||||||
|
## Exit codes
|
||||||
|
`0` answered · `1` usage · `2` no token · `3` Discord API error · `4` timeout (thread stays
|
||||||
|
open — resume with `--wait <thread_id>`).
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
- Bot token resolves from the SOPS vault
|
||||||
|
(`projects/discord-bot/bot-token.sops.yaml` field `credentials.bot_token`), falling back
|
||||||
|
to `projects/discord-bot/.env` `DISCORD_TOKEN` — works from any machine.
|
||||||
|
- Engine: `.claude/scripts/ask-forum.sh`. Command entry point: `/ask-forum`. Related:
|
||||||
|
[[discord-dm]] (person-targeted DMs / channel posts).
|
||||||
@@ -130,6 +130,16 @@ erroring `a value is required for '--url'`. Push via **GuruRMM** (`/rmm`) or any
|
|||||||
remote-exec channel. `RegKey` auto-approves + adds to the key's target group; without
|
remote-exec channel. `RegKey` auto-approves + adds to the key's target group; without
|
||||||
it the agent registers but awaits manual approval.
|
it the agent registers but awaits manual approval.
|
||||||
|
|
||||||
|
**Offline endpoint whose GuruRMM agent is down but ScreenConnect is up:** the one-liner
|
||||||
|
is a single self-contained command, so queue it via the `screenconnect` skill's
|
||||||
|
`send-command --session <id> --command "powershell -NoProfile -ExecutionPolicy Bypass
|
||||||
|
-Command \"...Install-EDR...\"" --confirm` — SC holds it in the session's one command slot
|
||||||
|
and runs it on reconnect (verify by `agents --org` / `sweep` afterward; SC returns no
|
||||||
|
output). Used 2026-07-08 to stage REPAIRADMIN (IMC) while it was offline overnight.
|
||||||
|
**AV-swap ordering:** when replacing another AV (e.g. Bitdefender), install + verify EDR
|
||||||
|
live FIRST, then remove the old AV — never leave an unprotected window. Note Bitdefender's
|
||||||
|
API has NO uninstall (console-only); pull it via its local uninstall tool over `/rmm`.
|
||||||
|
|
||||||
## Safety gating
|
## Safety gating
|
||||||
|
|
||||||
- Reads never mutate and run without confirmation.
|
- Reads never mutate and run without confirmation.
|
||||||
|
|||||||
@@ -108,6 +108,57 @@ installed, hook, network, events`, plus `extensions:[{id,args,order}]`.
|
|||||||
the extension id via `GET /Extensions` (e.g. `Host Isolation [Win/Linux]`) and test on
|
the extension id via `GET /Extensions` (e.g. `Host Isolation [Win/Linux]`) and test on
|
||||||
an ACG-internal box first.
|
an ACG-internal box first.
|
||||||
|
|
||||||
|
## Alert Suppression Rules (verified live 2026-07-07)
|
||||||
|
|
||||||
|
Suppress a false-positive detection so it stops firing (and stops triggering any
|
||||||
|
attached auto-response like `isolate-host`). LoopBack models are API-reachable:
|
||||||
|
|
||||||
|
| Op | Method/path |
|
||||||
|
|---|---|
|
||||||
|
| List rules | `GET /SuppressionRules` (`[id, alertId, name, versionCount, description, organizationId, locationId, active, deleted]`) |
|
||||||
|
| Count | `GET /SuppressionRules/count` |
|
||||||
|
| Match criteria (per rule) | `GET /SuppressionRules/{id}/versions` → `[{id, suppressionRuleId, name, description, metadata:{...}}]` |
|
||||||
|
|
||||||
|
**`metadata`** is the full match-field map; each key is `{value, active, display, dataType,
|
||||||
|
operator}`. A field participates in the match ONLY when `active:true`; all active fields are
|
||||||
|
**AND**-ed. Available fields (`display`): Alert Type, Item Type, Organization Name, Location
|
||||||
|
Name, Hostname, IP Address, Name, File Path, File SHA1, File SHA256, File Signature Issuer,
|
||||||
|
**Process Command Line**, AV Threat Name, Operating System, Threat Status, Severity, Rule
|
||||||
|
Name, EPP Type, Threat Category, Process Owner, Process Owner UID, AV Hits, Parent Process
|
||||||
|
Name, Grand Parent Process Name.
|
||||||
|
|
||||||
|
**Scoping guidance (learned from the vwp-qbs RMM false-positive incident):** the tightest
|
||||||
|
safe suppression matches on **Process Command Line** (a unique fingerprint of the exact
|
||||||
|
script) plus **Grand Parent Process Name** (the launching agent, e.g. `gururmm-agent.exe`).
|
||||||
|
That trusts one exact known-good automation without blinding a whole binary. NEVER suppress
|
||||||
|
on `Name`/`File Path`/`File SHA*` of a LOLBin (powershell/rundll32/etc.) alone — it disables
|
||||||
|
the rule for that binary everywhere. Add `Rule Name` to limit to one rule; add `Hostname`/
|
||||||
|
`Organization Name`/`Location Name` to limit scope (empty `organizationId`/`locationId` on the
|
||||||
|
rule = fleet-wide, gated by the metadata match). **Do NOT blanket-whitelist an RMM agent as
|
||||||
|
grandparent by itself** — the RMM is a SYSTEM-level RCE channel and the top MSP attack path;
|
||||||
|
whitelisting all of it blinds EDR exactly where it matters.
|
||||||
|
|
||||||
|
**[DANGER] API create footgun (verified live 2026-07-07).** `POST /SuppressionRules` on this
|
||||||
|
tenant (a) **ignores `active:false` and forces the rule LIVE immediately**, and (b) auto-creates
|
||||||
|
an **EMPTY-metadata version that matches EVERYTHING**. So the two-step "create rule → then add
|
||||||
|
version" path leaves a live match-all suppression window (observed ~2 min → deleted). Also note
|
||||||
|
`GET`/`PATCH /SuppressionRules/{id}` (single-resource route) **HTTP 400** "undefined is not valid
|
||||||
|
JSON" on this tenant — read via `GET /SuppressionRules?filter={"where":{"id":"..."}}` instead;
|
||||||
|
**`DELETE /SuppressionRules/{id}` DOES work** (returns null, sets `deleted:true`). Until an
|
||||||
|
**atomic** create (metadata embedded in the POST body) is verified, **create suppressions in the
|
||||||
|
CONSOLE**, not via `raw POST`. If you must POST, verify the version's active fields IMMEDIATELY
|
||||||
|
and `DELETE` on any doubt.
|
||||||
|
|
||||||
|
**Create — CONSOLE-observed, API create RUN-unverified.** Console flow: open the alert →
|
||||||
|
**Create Suppression Rule** → check the desired Match fields → Save. This POSTs a
|
||||||
|
`SuppressionRule` (carrying `alertId`, `name`, `description`, `organizationId`, `locationId`,
|
||||||
|
`active`) plus a version whose `metadata` has the chosen fields flipped to `active:true`. To
|
||||||
|
replicate via API, POST `/SuppressionRules` then the version to `/SuppressionRules/{id}/
|
||||||
|
versions` with that metadata shape — verify the exact envelope on the next real create (build
|
||||||
|
→ read back via the GET above → delete if wrong) before relying on it. Example on this tenant:
|
||||||
|
rule `e4dd55bf-…` (name "Exfiltration Over HTTP Protocol", alertId `5d5f39b1-…`) matches
|
||||||
|
Process Command Line + Grand Parent = `gururmm-agent.exe`.
|
||||||
|
|
||||||
## Deployment (not a REST call)
|
## Deployment (not a REST call)
|
||||||
|
|
||||||
The agent installs by running the binary on the endpoint:
|
The agent installs by running the binary on the endpoint:
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
sp.add_argument("method", help="GET/POST/PUT/DELETE")
|
sp.add_argument("method", help="GET/POST/PUT/DELETE")
|
||||||
sp.add_argument("path", help="e.g. Agents or Agents/scan")
|
sp.add_argument("path", help="e.g. Agents or Agents/scan")
|
||||||
sp.add_argument("--filter", help="LoopBack filter JSON (GET)")
|
sp.add_argument("--filter", help="LoopBack filter JSON (GET)")
|
||||||
sp.add_argument("--data", help="request body JSON")
|
sp.add_argument("--data", help="request body JSON, or @path to read JSON from a file")
|
||||||
sp.add_argument("--confirm", action="store_true",
|
sp.add_argument("--confirm", action="store_true",
|
||||||
help="required for non-GET methods")
|
help="required for non-GET methods")
|
||||||
return p
|
return p
|
||||||
@@ -408,7 +408,13 @@ def main(argv=None) -> int:
|
|||||||
print(f"[BLOCKED] {method} requires --confirm.", file=sys.stderr)
|
print(f"[BLOCKED] {method} requires --confirm.", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
filt = json.loads(args.filter) if args.filter else None
|
filt = json.loads(args.filter) if args.filter else None
|
||||||
body = json.loads(args.data) if args.data else None
|
if args.data and args.data.startswith("@"):
|
||||||
|
with open(args.data[1:], "r", encoding="utf-8") as _f:
|
||||||
|
body = json.load(_f)
|
||||||
|
elif args.data:
|
||||||
|
body = json.loads(args.data)
|
||||||
|
else:
|
||||||
|
body = None
|
||||||
# Same tenant-wide footgun guard the scan command has: a POST to any
|
# Same tenant-wide footgun guard the scan command has: a POST to any
|
||||||
# */scan endpoint with no non-empty `where` scans the ENTIRE tenant.
|
# */scan endpoint with no non-empty `where` scans the ENTIRE tenant.
|
||||||
if method == "POST" and args.path.rstrip("/").lower().endswith("scan"):
|
if method == "POST" and args.path.rstrip("/").lower().endswith("scan"):
|
||||||
|
|||||||
3
.claude/skills/datto-workplace/.gitignore
vendored
Normal file
3
.claude/skills/datto-workplace/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# No live secrets are cached (Basic auth is per-request), but keep scratch out.
|
||||||
|
.cache/
|
||||||
|
__pycache__/
|
||||||
74
.claude/skills/datto-workplace/SKILL.md
Normal file
74
.claude/skills/datto-workplace/SKILL.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: datto-workplace
|
||||||
|
description: "Read-only Datto Workplace backup verification for the GPS audit: confirm the file API is reachable under Basic auth, and (primary path) detect which GuruRMM endpoints actually run the Datto Workplace sync client. The file API is file-access only and this key sees no shared projects, so RMM endpoint detection is the real answer. Triggers: datto workplace, dattoworkplace, dwp, who backs up to datto workplace, datto sync."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Datto Workplace Skill
|
||||||
|
|
||||||
|
Read-only client for ACG's live **Datto Workplace** tenant, built for GPS backup
|
||||||
|
verification. Answers: **which enrolled machines actually run the Datto Workplace
|
||||||
|
sync client** (and, where the file API allows, what projects/files are visible).
|
||||||
|
|
||||||
|
This skill is one of the backup-verification siblings alongside `seafile`,
|
||||||
|
`b2` (Backblaze), and `owncloud`. It never writes.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DWP=".claude/skills/datto-workplace/scripts/datto_workplace.py"
|
||||||
|
py "$DWP" status # file API reachable + endpoint/project count
|
||||||
|
py "$DWP" projects [--json] # file-API projects (likely empty for this key)
|
||||||
|
py "$DWP" audit --rmm --client "Birth" # GPS view: detect the client on GuruRMM endpoints
|
||||||
|
py "$DWP" audit --rmm --client Birth --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--json` to any command for machine-readable output. `--url` overrides the
|
||||||
|
file-API base.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
Loaded at runtime from the SOPS vault (never hardcoded):
|
||||||
|
`msp-tools/datto-workplace.sops.yaml` -> `credentials.api-key` / `credentials.api-secret`.
|
||||||
|
Auth is **HTTP Basic**: username = api-key, password = api-secret, sent on every
|
||||||
|
request (no token exchange, nothing cached).
|
||||||
|
|
||||||
|
## The file API is limited (why RMM detection is primary)
|
||||||
|
|
||||||
|
Datto Workplace exposes a **file-access REST API v1** only. Base:
|
||||||
|
`https://acg.workplace.datto.com/api/v1` (the team path `/6/api/v1` is normalized
|
||||||
|
by the server to `/api/v1`). Endpoints (confirm with `GET /openapi.json`, surfaced
|
||||||
|
by `status`): `GET /file/projects`, `GET /file/{parentID}/files`,
|
||||||
|
`GET /file/{fileID}`, `GET /file/{fileID}/data`, `GET /file/search`, plus
|
||||||
|
`/webhook*`. There are **NO** user / device / member / team endpoints.
|
||||||
|
|
||||||
|
GOTCHA: `GET /file/projects` currently returns `{"result": []}` for this key -- the
|
||||||
|
API principal has no projects shared to it. So the file API alone **cannot
|
||||||
|
enumerate the team's backups**. `projects` handles this and says so.
|
||||||
|
|
||||||
|
Because of that, **RMM endpoint detection is the PRIMARY source of truth**.
|
||||||
|
|
||||||
|
## The `--rmm` cross-check (primary path)
|
||||||
|
|
||||||
|
`audit --rmm` logs in to GuruRMM (`infrastructure/gururmm-server.sops.yaml`) and,
|
||||||
|
for each agent (scope with `--client` so it doesn't sweep all 350+ agents), runs a
|
||||||
|
**read-only** detection PowerShell that reports installed products / running
|
||||||
|
processes / config folders matching the Datto Workplace client. An agent is
|
||||||
|
reported only if the client is actually present.
|
||||||
|
|
||||||
|
**Detection patterns must be precise:** this skill uses `["Datto Workplace",
|
||||||
|
"Workplace2"]`. A bare `"Datto"` false-matches **Datto EDR Agent**, Datto RMM, and
|
||||||
|
Datto AV -- do NOT use it. The v10 client installs as DisplayName
|
||||||
|
"Datto Workplace v10.x" in `C:\Program Files\Datto\Workplace2\` (process
|
||||||
|
`Workplace.exe`); the legacy v8 is "Datto Workplace Desktop". Matching
|
||||||
|
`Datto Workplace` + `Workplace2` catches both without catching EDR.
|
||||||
|
|
||||||
|
## Notes / gotchas
|
||||||
|
|
||||||
|
- Always scope `audit --rmm` with `--client` unless you truly mean to sweep the
|
||||||
|
whole fleet -- detection dispatches a live command to each endpoint.
|
||||||
|
- Shared plumbing (repo-root resolve, vault read, HTTP, GuruRMM client + endpoint
|
||||||
|
detection) lives in `.claude/scripts/backup_common.py`, used by all the
|
||||||
|
backup-verification siblings. Detection patterns are per-backend; this skill's
|
||||||
|
are the precise Datto Workplace set above.
|
||||||
|
- Known user: **Birth Biologic** backs up to Datto Workplace (agent hostnames
|
||||||
|
include `BB-*`, `KSTEEN*`, `EVO-X1`) -- a good `--client "Birth"` smoke test.
|
||||||
164
.claude/skills/datto-workplace/scripts/datto_client.py
Normal file
164
.claude/skills/datto-workplace/scripts/datto_client.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Datto Workplace REST API v1 client for the `datto-workplace` skill.
|
||||||
|
|
||||||
|
Talks to ACG's live Datto Workplace tenant file API. Read-only: this is a
|
||||||
|
FILE-ACCESS API (projects + files), NOT an admin/reporting API -- there are no
|
||||||
|
user/device/member/team endpoints. It answers "what shared projects + files does
|
||||||
|
this API principal see" for the GPS backup audit.
|
||||||
|
|
||||||
|
Auth: HTTP Basic, username = api-key, password = api-secret (no token exchange;
|
||||||
|
Basic is sent on every request). Credentials come from the SOPS vault.
|
||||||
|
|
||||||
|
Base URL defaults to the team file-API host. The team path /6/api/v1 is normalized
|
||||||
|
by the server to /api/v1; override with DATTO_WORKPLACE_URL if needed.
|
||||||
|
|
||||||
|
IMPORTANT GOTCHA: GET /file/projects currently returns {"result": []} for this
|
||||||
|
key -- the principal has no projects shared to it, so the file API alone CANNOT
|
||||||
|
enumerate the team's backups. Handle empty gracefully. Because of that, RMM
|
||||||
|
endpoint detection (see datto_workplace.py `audit --rmm`) is the PRIMARY source
|
||||||
|
of "who backs up to Datto Workplace".
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# Import the shared backup helpers from .claude/scripts.
|
||||||
|
_SKILL_DIR = Path(__file__).resolve().parent.parent # .../skills/datto-workplace
|
||||||
|
_SCRIPTS_DIR = _SKILL_DIR.parent.parent / "scripts" # .../.claude/scripts
|
||||||
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
VAULT_ENTRY = "msp-tools/datto-workplace.sops.yaml"
|
||||||
|
VAULT_FIELD_KEY = "credentials.api-key"
|
||||||
|
VAULT_FIELD_SECRET = "credentials.api-secret"
|
||||||
|
|
||||||
|
DEFAULT_URL = os.environ.get("DATTO_WORKPLACE_URL", "https://acg.workplace.datto.com/api/v1")
|
||||||
|
|
||||||
|
|
||||||
|
class DattoWorkplaceClient:
|
||||||
|
"""Basic-auth GET client for the Datto Workplace file API.
|
||||||
|
|
||||||
|
No token caching is needed -- HTTP Basic is sent per request. Credentials are
|
||||||
|
read once from the vault on first use and reused for the process lifetime.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base: Optional[str] = None):
|
||||||
|
self.base = (base or DEFAULT_URL).rstrip("/")
|
||||||
|
self._basic: Optional[tuple[str, str]] = None
|
||||||
|
|
||||||
|
# -- auth ------------------------------------------------------------------
|
||||||
|
def _creds(self) -> tuple[str, str]:
|
||||||
|
if self._basic is None:
|
||||||
|
key = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_KEY)
|
||||||
|
secret = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_SECRET)
|
||||||
|
self._basic = (key, secret)
|
||||||
|
return self._basic
|
||||||
|
|
||||||
|
def _get_raw(self, path: str, params: Optional[dict] = None) -> tuple[int, Any]:
|
||||||
|
"""Low-level GET returning (status, body); callers decide how to react."""
|
||||||
|
url = f"{self.base}{path}"
|
||||||
|
if params:
|
||||||
|
url += "?" + urllib.parse.urlencode(params)
|
||||||
|
return bc.http_json("GET", url, basic=self._creds())
|
||||||
|
|
||||||
|
def _get(self, path: str, params: Optional[dict] = None) -> Any:
|
||||||
|
status, body = self._get_raw(path, params)
|
||||||
|
if status == 401:
|
||||||
|
raise bc.BackupError(
|
||||||
|
f"Datto Workplace auth failed (HTTP 401) for {path}: check api-key/api-secret",
|
||||||
|
status=status)
|
||||||
|
if status != 200:
|
||||||
|
raise bc.BackupError(
|
||||||
|
f"Datto Workplace GET {path} failed (HTTP {status}): {body}", status=status)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# -- read methods ----------------------------------------------------------
|
||||||
|
def openapi(self) -> dict:
|
||||||
|
"""Fetch the OpenAPI spec (confirms reachability + exact endpoint list)."""
|
||||||
|
body = self._get("/openapi.json")
|
||||||
|
return body if isinstance(body, dict) else {"_raw": body}
|
||||||
|
|
||||||
|
def endpoint_paths(self) -> list[str]:
|
||||||
|
"""Sorted list of paths the OpenAPI spec exposes."""
|
||||||
|
spec = self.openapi()
|
||||||
|
paths = spec.get("paths") if isinstance(spec, dict) else None
|
||||||
|
return sorted(paths.keys()) if isinstance(paths, dict) else []
|
||||||
|
|
||||||
|
def projects(self) -> list[dict]:
|
||||||
|
"""Top-level file projects visible to this API principal.
|
||||||
|
|
||||||
|
Returns [] gracefully -- for this key the principal currently has no
|
||||||
|
projects shared to it. The server is inconsistent here: some responses
|
||||||
|
are 200 {"result": []}, others are 403 "Access denied" for the exact same
|
||||||
|
Basic auth that authorizes /openapi.json seconds earlier. Both mean the
|
||||||
|
same thing for the audit (no project-level file access), so a 403/401 on
|
||||||
|
this endpoint is treated as empty rather than a hard error. A genuine
|
||||||
|
credential failure surfaces on /openapi.json (status/openapi).
|
||||||
|
"""
|
||||||
|
status, body = self._get_raw("/file/projects")
|
||||||
|
if status in (401, 403):
|
||||||
|
return []
|
||||||
|
if status != 200:
|
||||||
|
raise bc.BackupError(
|
||||||
|
f"Datto Workplace GET /file/projects failed (HTTP {status}): {body}",
|
||||||
|
status=status)
|
||||||
|
return self._as_list(body)
|
||||||
|
|
||||||
|
def files(self, parent_id: str) -> list[dict]:
|
||||||
|
"""List files/folders under a parent project or folder ID."""
|
||||||
|
body = self._get(f"/file/{parent_id}/files")
|
||||||
|
return self._as_list(body)
|
||||||
|
|
||||||
|
def file_info(self, file_id: str) -> dict:
|
||||||
|
"""Metadata for a single file/folder ID."""
|
||||||
|
body = self._get(f"/file/{file_id}")
|
||||||
|
if isinstance(body, dict):
|
||||||
|
return body.get("result") if isinstance(body.get("result"), dict) else body
|
||||||
|
return {"_raw": body}
|
||||||
|
|
||||||
|
def search(self, query: str, parent_id: Optional[str] = None) -> list[dict]:
|
||||||
|
"""Search files by name. Optional parent scope."""
|
||||||
|
params = {"query": query}
|
||||||
|
if parent_id:
|
||||||
|
params["parentID"] = parent_id
|
||||||
|
body = self._get("/file/search", params)
|
||||||
|
return self._as_list(body)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _as_list(body: Any) -> list[dict]:
|
||||||
|
"""Normalize the Datto file-API envelope to a list.
|
||||||
|
|
||||||
|
Responses wrap payloads as {"result": [...]} (or a bare list on some
|
||||||
|
endpoints); anything else yields [].
|
||||||
|
"""
|
||||||
|
if isinstance(body, dict):
|
||||||
|
res = body.get("result")
|
||||||
|
if isinstance(res, list):
|
||||||
|
return res
|
||||||
|
if isinstance(res, dict):
|
||||||
|
return [res]
|
||||||
|
return []
|
||||||
|
if isinstance(body, list):
|
||||||
|
return body
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
c = DattoWorkplaceClient()
|
||||||
|
paths = c.endpoint_paths()
|
||||||
|
projects = c.projects()
|
||||||
|
print(f"[OK] Datto Workplace file API reachable at {c.base}; "
|
||||||
|
f"{len(paths)} endpoints, {len(projects)} project(s) visible")
|
||||||
|
return 0
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
165
.claude/skills/datto-workplace/scripts/datto_workplace.py
Normal file
165
.claude/skills/datto-workplace/scripts/datto_workplace.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI for the `datto-workplace` skill -- read-only Datto Workplace backup
|
||||||
|
verification for the GPS audit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
status file API reachable under Basic auth + endpoint/project count
|
||||||
|
projects [--json] list file-API projects (likely empty for this principal)
|
||||||
|
audit [--client NAME] [--rmm] [--json]
|
||||||
|
the GPS view: server-side projects (if any), optionally
|
||||||
|
cross-checked against GuruRMM endpoints (Datto Workplace
|
||||||
|
client installed + signed in?)
|
||||||
|
|
||||||
|
The Datto Workplace file API is FILE-ACCESS ONLY and this principal sees no shared
|
||||||
|
projects, so it CANNOT enumerate the team's backups on its own. RMM endpoint
|
||||||
|
detection (`audit --rmm`) is therefore the PRIMARY source: it detects the Datto
|
||||||
|
Workplace client on GuruRMM agents using precise patterns that do NOT false-match
|
||||||
|
Datto EDR / Datto RMM / Datto AV.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from datto_client import DattoWorkplaceClient # noqa: E402
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
# Precise detection patterns. "Datto Workplace" catches the DisplayName of both the
|
||||||
|
# v10 client ("Datto Workplace v10.x") and legacy v8 ("Datto Workplace Desktop");
|
||||||
|
# "Workplace2" catches the v10 install path / process family. A bare "Datto" would
|
||||||
|
# false-match "Datto EDR Agent", Datto RMM, and Datto AV -- do NOT use it.
|
||||||
|
DETECT_PATTERNS = ["Datto Workplace", "Workplace2"]
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status(c: DattoWorkplaceClient, args) -> int:
|
||||||
|
paths = c.endpoint_paths()
|
||||||
|
projects = c.projects()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"base": c.base, "reachable": True,
|
||||||
|
"endpoints": paths, "project_count": len(projects)}, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"[OK] Datto Workplace file API reachable at {c.base} (Basic auth 200)")
|
||||||
|
print(f"[INFO] {len(paths)} endpoints exposed:")
|
||||||
|
for p in paths:
|
||||||
|
print(f" {p}")
|
||||||
|
print(f"[INFO] {len(projects)} project(s) visible to this API principal", end="")
|
||||||
|
if not projects:
|
||||||
|
print(" - file API cannot enumerate backups; use `audit --rmm`")
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_projects(c: DattoWorkplaceClient, args) -> int:
|
||||||
|
projects = c.projects()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(projects, indent=2))
|
||||||
|
return 0
|
||||||
|
if not projects:
|
||||||
|
print("[INFO] no file-API projects visible to this principal.")
|
||||||
|
print("[INFO] The api-key has no projects shared to it, so GET /file/projects "
|
||||||
|
"returns []. This is expected - the Datto Workplace file API is file-access "
|
||||||
|
"only. Use `audit --rmm` to verify backups via GuruRMM endpoint detection.")
|
||||||
|
return 0
|
||||||
|
print(f"{'NAME':40} {'ID':38} {'FILES':>8}")
|
||||||
|
for p in projects:
|
||||||
|
print(f"{str(p.get('name') or '')[:40]:40} "
|
||||||
|
f"{str(p.get('id') or p.get('projectID') or '')[:38]:38} "
|
||||||
|
f"{str(p.get('fileCount') or p.get('count') or '?'):>8}")
|
||||||
|
print(f"\n{len(projects)} project(s)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_audit(c: DattoWorkplaceClient, args) -> int:
|
||||||
|
projects = c.projects()
|
||||||
|
result = {"backend": "datto-workplace", "base": c.base,
|
||||||
|
"file_api_projects": projects, "file_api_limited": not projects}
|
||||||
|
|
||||||
|
if args.rmm:
|
||||||
|
rmm = bc.RmmClient()
|
||||||
|
rmm.login()
|
||||||
|
checks = []
|
||||||
|
agents = rmm.find_agents(client_substr=args.client) if args.client else rmm.list_agents()
|
||||||
|
for a in agents:
|
||||||
|
det = rmm.detect_client(a, DETECT_PATTERNS)
|
||||||
|
if det.get("detected"):
|
||||||
|
checks.append(det)
|
||||||
|
result["rmm_endpoints_with_client"] = checks
|
||||||
|
result["rmm_agents_scanned"] = len(agents)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"=== Datto Workplace backup audit - {c.base} ===")
|
||||||
|
if projects:
|
||||||
|
print(f"{'PROJECT':40} {'ID':38} {'FILES':>8}")
|
||||||
|
for p in projects:
|
||||||
|
print(f"{str(p.get('name') or '')[:40]:40} "
|
||||||
|
f"{str(p.get('id') or p.get('projectID') or '')[:38]:38} "
|
||||||
|
f"{str(p.get('fileCount') or p.get('count') or '?'):>8}")
|
||||||
|
print(f"\n{len(projects)} file-API project(s)")
|
||||||
|
else:
|
||||||
|
print("[INFO] file API shows 0 projects (principal has none shared) - "
|
||||||
|
"server-side enumeration not available; relying on RMM detection.")
|
||||||
|
|
||||||
|
if args.rmm:
|
||||||
|
eps = result.get("rmm_endpoints_with_client", [])
|
||||||
|
scanned = result.get("rmm_agents_scanned", 0)
|
||||||
|
scope = f"client~'{args.client}'" if args.client else "ALL agents"
|
||||||
|
print(f"\n--- GuruRMM endpoints running the Datto Workplace client "
|
||||||
|
f"({len(eps)} of {scanned} scanned, {scope}) ---")
|
||||||
|
for e in eps:
|
||||||
|
det = e.get("detail") or {}
|
||||||
|
installed = det.get("installed") or []
|
||||||
|
prod = "-"
|
||||||
|
if isinstance(installed, list) and installed:
|
||||||
|
first = installed[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
prod = f"{first.get('name') or '?'} {first.get('version') or ''}".strip()
|
||||||
|
elif isinstance(installed, dict):
|
||||||
|
prod = f"{installed.get('name') or '?'} {installed.get('version') or ''}".strip()
|
||||||
|
print(f" {str(e.get('hostname') or '')[:25]:25} "
|
||||||
|
f"client={str(e.get('client_name') or ''):20} "
|
||||||
|
f"product={prod}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(prog="datto-workplace",
|
||||||
|
description="Datto Workplace backup verification (GPS audit)")
|
||||||
|
p.add_argument("--url", help="override file-API base URL (default team file-API host)")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
s = sub.add_parser("status"); s.add_argument("--json", action="store_true")
|
||||||
|
pr = sub.add_parser("projects"); pr.add_argument("--json", action="store_true")
|
||||||
|
a = sub.add_parser("audit")
|
||||||
|
a.add_argument("--client", help="scope RMM detection to agents whose client_name matches")
|
||||||
|
a.add_argument("--rmm", action="store_true", help="cross-check GuruRMM endpoints (primary path)")
|
||||||
|
a.add_argument("--json", action="store_true")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
c = DattoWorkplaceClient(args.url)
|
||||||
|
handlers = {"status": cmd_status, "projects": cmd_projects, "audit": cmd_audit}
|
||||||
|
try:
|
||||||
|
return handlers[args.cmd](c, args)
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
# best-effort error logging per house rule
|
||||||
|
try:
|
||||||
|
root = bc.resolve_root()
|
||||||
|
bc.subprocess.run(["bash", str(root / ".claude/scripts/log-skill-error.sh"),
|
||||||
|
"datto-workplace", str(exc)[:200]], timeout=15)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: discord-dm
|
name: discord-dm
|
||||||
description: "Send a Discord message to an org member DM or team channel via the ClaudeTools bot - copy-paste-friendly delivery of links/commands the terminal would mangle; address people by name (mike/howard/rob/winter). Triggers: DM <person> in discord, send that link to my discord, ping <person>."
|
description: "Send OR read Discord messages to/from an org member DM or team channel via the ClaudeTools bot - copy-paste-friendly delivery of links/commands the terminal would mangle, and READ replies (coord/repo sync does not carry Discord answers); address people by name (mike/howard/rob/winter). Triggers: DM <person> in discord, send that link to my discord, ping <person>, did <person> reply/answer, read discord replies, check discord DMs."
|
||||||
---
|
---
|
||||||
|
|
||||||
# discord-dm — direct Discord messaging to the org
|
# discord-dm — direct Discord messaging to the org
|
||||||
@@ -27,9 +27,26 @@ DMs** and deliberate channel posts.
|
|||||||
```bash
|
```bash
|
||||||
bash .claude/scripts/discord-dm.sh <recipient> "message text"
|
bash .claude/scripts/discord-dm.sh <recipient> "message text"
|
||||||
echo "message text" | bash .claude/scripts/discord-dm.sh <recipient>
|
echo "message text" | bash .claude/scripts/discord-dm.sh <recipient>
|
||||||
|
bash .claude/scripts/discord-dm.sh read <recipient> [limit] # READ recent messages — SEE replies (default 15)
|
||||||
bash .claude/scripts/discord-dm.sh list # print known users + channels
|
bash .claude/scripts/discord-dm.sh list # print known users + channels
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reading replies (`read`)
|
||||||
|
|
||||||
|
Sends are one-way, but people **answer** — always `read` before assuming silence. The bot
|
||||||
|
participates in every DM channel it opened, so `GET /channels/<id>/messages` returns full
|
||||||
|
content (the Message Content privileged intent only gates *gateway* events, not this REST
|
||||||
|
fetch). Works for users and channels:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .claude/scripts/discord-dm.sh read mike 10 # last 10 in the mike DM
|
||||||
|
bash .claude/scripts/discord-dm.sh read #dev-alerts # last 15 in #dev-alerts
|
||||||
|
```
|
||||||
|
|
||||||
|
Output is oldest-first; lines from **them** are marked `>>>`, lines from us are indented.
|
||||||
|
After DMing someone a question, `read` that user to check for their answer — coord/repo sync
|
||||||
|
does NOT carry Discord replies, so this is the only way to see them.
|
||||||
|
|
||||||
`<recipient>`:
|
`<recipient>`:
|
||||||
|
|
||||||
| Form | Effect |
|
| Form | Effect |
|
||||||
|
|||||||
3
.claude/skills/msp360/.gitignore
vendored
Normal file
3
.claude/skills/msp360/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Skill scratch/cache - never commit. Auth token is per-run (not cached to disk).
|
||||||
|
.cache/
|
||||||
|
__pycache__/
|
||||||
103
.claude/skills/msp360/SKILL.md
Normal file
103
.claude/skills/msp360/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: msp360
|
||||||
|
description: "Manage ACG's MSP360 Managed Backup Service (MBS) tenant via the provider REST API: the authoritative fleet backup-status source (who is backing up, last run, failures, stale plans) plus user/company/license provisioning. Reads run freely; writes (create/delete user or company, grant/release/revoke license) are gated --confirm. Triggers: msp360, mspbackups, managed backup, is X backing up, backup status, backup failures, create backup user, add backup company, grant backup license."
|
||||||
|
---
|
||||||
|
|
||||||
|
# MSP360 Managed Backup Service Skill
|
||||||
|
|
||||||
|
Live client for ACG's **MSP360 Managed Backup Service (MBS)** provider tenant. This is the
|
||||||
|
**authoritative answer to "is client X actually backing up"** — the monitoring feed reports
|
||||||
|
every backup plan's last run, status, and data across the whole fleet. B2/Seafile/ownCloud
|
||||||
|
are just where the data lands; MSP360 is where you see whether a backup *ran*.
|
||||||
|
|
||||||
|
Built as a sibling to the `b2` / `seafile` / `owncloud` / `datto-workplace` backup skills;
|
||||||
|
shares `.claude/scripts/backup_common.py` for repo-root/vault/HTTP plumbing.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MSP=".claude/skills/msp360/scripts/msp360.py"
|
||||||
|
|
||||||
|
# --- reads (free) ---
|
||||||
|
py "$MSP" status # reachable + counts + plans needing attention
|
||||||
|
py "$MSP" monitoring # every plan: company/computer/status/last-run/data
|
||||||
|
py "$MSP" monitoring --failed # only plans NOT in a clean state (the triage view)
|
||||||
|
py "$MSP" monitoring --company Dataforth # scope to one client
|
||||||
|
py "$MSP" monitoring --stale 30 # plans whose last run is >=30 days old (or never)
|
||||||
|
py "$MSP" companies # 37 companies with ids + storage limits
|
||||||
|
py "$MSP" users --company "Business Services" # backup users (+ space used) in a company
|
||||||
|
py "$MSP" licenses --available # free vs taken licenses
|
||||||
|
py "$MSP" billing --json # billing feed
|
||||||
|
py "$MSP" raw GET /api/Users/{id} # generic passthrough for any GET endpoint
|
||||||
|
|
||||||
|
# --- writes (require --confirm; without it you get a dry-run preview) ---
|
||||||
|
py "$MSP" create-company --name "New Client LLC" --confirm
|
||||||
|
py "$MSP" create-user --email backup@client.com --company "New Client LLC" \
|
||||||
|
--first Jane --last Doe --notify admin@acg.com --confirm
|
||||||
|
py "$MSP" grant-license --user-id <UUID> --license-id <UUID> --confirm
|
||||||
|
py "$MSP" release-license --user-id <UUID> --confirm
|
||||||
|
py "$MSP" delete-user --id <UUID> [--keep-data] --confirm
|
||||||
|
py "$MSP" delete-company --id <UUID> --confirm
|
||||||
|
py "$MSP" raw POST /api/Accounts/CreateDestination --data '{...}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--json` to any read for machine-readable output. `--base` overrides the API URL.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
Loaded at runtime from the SOPS vault (never hardcoded):
|
||||||
|
**`msp-tools/msp360-api.sops.yaml`** -> `credentials.login` / `credentials.password`.
|
||||||
|
These are the **API-specific** creds
|
||||||
|
generated in the MSP360 console (Settings > General) by the main admin account — NOT the
|
||||||
|
console login. Auth flow: `POST /api/Provider/Login {UserName, Password}` -> Bearer
|
||||||
|
`access_token` (per-run, never cached to disk) -> `Authorization: Bearer <token>` on every
|
||||||
|
request. Base: `https://api.mspbackups.com`.
|
||||||
|
|
||||||
|
## What "backing up?" looks like
|
||||||
|
|
||||||
|
`monitoring` returns one row per backup plan with `Status` (0/1=Success, 2=Warning,
|
||||||
|
3=Failed, 4=Running, 6=Interrupted, 7=Completed-with-warnings), `LastStart`, `DataCopied`,
|
||||||
|
`CompanyName`, `ComputerName`, `PlanName`. Key reads:
|
||||||
|
|
||||||
|
- **A company with 0 rows in `monitoring` is not backing up** even if it exists (this is how
|
||||||
|
the GPS audit found Arizona Medical Transit + T&C Sorensen billed-but-unprotected).
|
||||||
|
- `--failed` surfaces failing/warning plans; `--stale N` surfaces plans that stopped running.
|
||||||
|
- A B2 bucket with files does NOT prove a recent backup — always confirm with `monitoring`.
|
||||||
|
|
||||||
|
## Writes & safety
|
||||||
|
|
||||||
|
- **Every write is gated `--confirm`.** Without it the command prints a `[DRY-RUN]` line (and,
|
||||||
|
for create-*, the exact JSON payload) and changes nothing.
|
||||||
|
- `delete-user` defaults to removing metadata **and all backup data**; pass `--keep-data` to
|
||||||
|
drop only the metadata (`DELETE /api/Users/{id}/Account`) and retain the backups.
|
||||||
|
- `delete-company` / `delete-user` print a `[WARNING]` describing the blast radius first.
|
||||||
|
- create-user / grant-license payloads are built from the live API schema (User fields:
|
||||||
|
Email/FirstName/LastName/Company/Enabled/NotificationEmails; grant: UserID + LicenseID).
|
||||||
|
On first real provisioning, eyeball the API's response — it returns the created object or a
|
||||||
|
validation error, which the command prints verbatim.
|
||||||
|
|
||||||
|
## Full endpoint surface (via `raw` when a typed command doesn't exist)
|
||||||
|
|
||||||
|
The MBS API groups (all reachable through `raw <METHOD> <path>`): **Provider** (login),
|
||||||
|
**Users** (GET/POST/PUT, DELETE `/{id}` or `/{id}/Account`), **Companies** (GET/POST/PUT/DELETE),
|
||||||
|
**Monitoring** (GET), **Licenses** (GET, POST Grant/Release/Revoke), **Destinations**
|
||||||
|
(GET/POST/PUT/DELETE), **Accounts** (GET/POST/PUT + Add/Create/Edit/RemoveDestination),
|
||||||
|
**Packages** (GET/POST/PUT/DELETE), **Billing** (GET/PUT), **Builds** (GET, POST
|
||||||
|
RequestCustomBuilds), **Administrators** (GET/POST/PUT/DELETE). Reference:
|
||||||
|
`https://help.mspbackups.com/mbs-api-specification/methods`.
|
||||||
|
|
||||||
|
## Notes / gotchas
|
||||||
|
|
||||||
|
- **`/api/Computers` returns HTTP 400 "Remote Management API methods are not enabled"** —
|
||||||
|
that feature is off on this account. `computers` prints a clean [INFO] and points you at
|
||||||
|
`users` + `monitoring`, which cover endpoint/backup data without it.
|
||||||
|
- **Git-Bash path mangling:** a leading-slash `raw` path like `/api/Users` gets rewritten to
|
||||||
|
`C:/Program Files/Git/api/Users` by MSYS. The `raw` command auto-recovers the `/api/...`
|
||||||
|
tail, so pass the path normally.
|
||||||
|
- MSP360 CompanyName is the join key to B2 buckets (e.g. `ACG-PST`=Peaceful Spirit,
|
||||||
|
`ACG-TCA`=Tucson Coin and Autograph) — useful when reconciling destinations.
|
||||||
|
- Backup *targets* are a per-client decision: report mismatches, don't provision jobs off a
|
||||||
|
billing count. See memory `feedback_backup_targets_never_guessed` +
|
||||||
|
`reference_msp360_backup_monitoring`.
|
||||||
|
- Failures are logged to the fleet `errorlog.md` via the canonical helper; expected
|
||||||
|
conditions (feature-gated endpoint, no matching rows) are not.
|
||||||
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)
|
||||||
3
.claude/skills/owncloud/.gitignore
vendored
Normal file
3
.claude/skills/owncloud/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Skill scratch/cache - never commit.
|
||||||
|
.cache/
|
||||||
|
__pycache__/
|
||||||
175
.claude/skills/owncloud/SKILL.md
Normal file
175
.claude/skills/owncloud/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
name: owncloud
|
||||||
|
description: "Manage ACG's ownCloud server (cloud.acghosting.com / 172.16.3.22, ownCloud 10.16 Community) via occ over SSH: read-only GPS backup inventory (who has an account, per-user storage + quota + last login, which GuruRMM endpoints run the desktop client) PLUS gated admin writes - user lifecycle, quota, groups, apps/config, maintenance mode, files scan/transfer, trashbin/versions cleanup. Reads run freely; writes require --confirm. Triggers: owncloud, cloud.acghosting, who backs up to owncloud, owncloud usage, owncloud audit, create/disable owncloud user, set owncloud quota, owncloud group, owncloud maintenance mode."
|
||||||
|
---
|
||||||
|
|
||||||
|
# ownCloud Skill
|
||||||
|
|
||||||
|
Client for ACG's live **ownCloud 10.16.0 Community** server
|
||||||
|
(`cloud.acghosting.com`, VM `172.16.3.22`, Rocky Linux 9.6), driven entirely
|
||||||
|
through the `occ` admin CLI over SSH.
|
||||||
|
|
||||||
|
Two halves:
|
||||||
|
- **Read (GPS audit, ungated):** which customers back up here and how much data
|
||||||
|
they hold - and, with `--rmm`, which enrolled machines actually run the
|
||||||
|
ownCloud desktop client.
|
||||||
|
- **Manage (gated):** user/quota/group lifecycle, apps/config, maintenance mode,
|
||||||
|
filesystem scans, ownership transfer, trashbin/versions cleanup. Every
|
||||||
|
state-changing command **refuses without `--confirm`** (prints the WOULD line,
|
||||||
|
exits 3), mirroring the `b2` / `seafile` fleet convention.
|
||||||
|
|
||||||
|
This skill is one of the backup-verification siblings alongside `seafile`,
|
||||||
|
`b2` (Backblaze) and `datto-workplace`.
|
||||||
|
|
||||||
|
## Running - read (GPS audit)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OWNCLOUD=".claude/skills/owncloud/scripts/owncloud.py"
|
||||||
|
py "$OWNCLOUD" status # SSH ok + occ version / edition
|
||||||
|
py "$OWNCLOUD" users [--json] # all users + enabled/quota/used/last login
|
||||||
|
py "$OWNCLOUD" usage [--json] # per-user rollup, used GB desc (the audit view)
|
||||||
|
py "$OWNCLOUD" audit [--client NAME] [--rmm] [--json]
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--json` to `users` / `usage` / `audit` for machine-readable output.
|
||||||
|
`--host` overrides the server IP (default `172.16.3.22`).
|
||||||
|
|
||||||
|
## Running - manage (writes gated behind `--confirm`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# users
|
||||||
|
py "$OWNCLOUD" user-add jdoe --display-name "J Doe" --email j@x.com -g Clients --gen-password --confirm
|
||||||
|
py "$OWNCLOUD" user-disable jdoe --confirm # user-enable to re-enable
|
||||||
|
py "$OWNCLOUD" user-reset-password jdoe --password-env OC_NEW_PW --confirm
|
||||||
|
py "$OWNCLOUD" user-modify jdoe --email new@x.com --confirm
|
||||||
|
py "$OWNCLOUD" user-delete jdoe --confirm # DELETES the user AND all their data
|
||||||
|
|
||||||
|
# quota (quota <uid> reads it; value like '5 GB', 'none' = unlimited)
|
||||||
|
py "$OWNCLOUD" quota jdoe
|
||||||
|
py "$OWNCLOUD" quota-set jdoe "50 GB" --confirm # quota-reset -> back to default
|
||||||
|
|
||||||
|
# groups (groups / group-members / user-groups are read-only)
|
||||||
|
py "$OWNCLOUD" groups
|
||||||
|
py "$OWNCLOUD" group-add Clients --confirm
|
||||||
|
py "$OWNCLOUD" group-add-member Clients -m jdoe -m asmith --confirm
|
||||||
|
py "$OWNCLOUD" group-remove-member Clients -m jdoe --confirm
|
||||||
|
py "$OWNCLOUD" group-delete Clients --confirm
|
||||||
|
|
||||||
|
# shares (read-only - occ cannot create shares; see Notes)
|
||||||
|
py "$OWNCLOUD" shares [--user jdoe]
|
||||||
|
|
||||||
|
# server / apps / config
|
||||||
|
py "$OWNCLOUD" maintenance status|on|off [--confirm] # on/off need --confirm
|
||||||
|
py "$OWNCLOUD" apps
|
||||||
|
py "$OWNCLOUD" app-enable|app-disable <app> --confirm
|
||||||
|
py "$OWNCLOUD" config-get [name] # no name -> dump all system config
|
||||||
|
py "$OWNCLOUD" config-set <name> --value V [--type string|integer|double|boolean|json] --confirm
|
||||||
|
|
||||||
|
# files admin
|
||||||
|
py "$OWNCLOUD" files-scan (--user jdoe | --all) [--path sub/dir] --confirm
|
||||||
|
py "$OWNCLOUD" transfer-ownership <src> <dst> [--path folder] --confirm
|
||||||
|
py "$OWNCLOUD" trashbin-cleanup --user jdoe --confirm
|
||||||
|
py "$OWNCLOUD" versions-cleanup --user jdoe --confirm
|
||||||
|
# fleet-wide purge needs the extra ack flag on top of --confirm:
|
||||||
|
py "$OWNCLOUD" versions-cleanup --all-users --force-all-users --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Passwords + the credential rule
|
||||||
|
|
||||||
|
`user-add` / `user-reset-password` take the password one of three ways:
|
||||||
|
`--gen-password` (generates a strong one, prints it **once**), `--password-env VAR`
|
||||||
|
(reads it from that env var - preferred, keeps it out of shell history), or a
|
||||||
|
literal `--password` (discouraged: lands in history + `ps`). On the server the
|
||||||
|
value is passed via occ's own `OC_PASS` + `--password-from-env`.
|
||||||
|
|
||||||
|
**Any password created or reset here MUST be stored in the SOPS vault** (house
|
||||||
|
rule) - the command prints a `[CRITICAL]` reminder with the exact `vault.sh`
|
||||||
|
line (suggested path `infrastructure/owncloud-users/<uid>.sops.yaml`).
|
||||||
|
|
||||||
|
### Gating / exit codes
|
||||||
|
|
||||||
|
- write command without `--confirm` -> prints `Would: ...`, **exit 3**, no change.
|
||||||
|
- `trashbin-cleanup` / `versions-cleanup` with **no** `--user` and **no**
|
||||||
|
`--all-users` -> **hard-fail exit 2** (occ treats "no user" as *every* user; a
|
||||||
|
fleet-wide purge must be made explicit with `--all-users`).
|
||||||
|
- `--all-users` on those two also **hard-fails exit 2** unless
|
||||||
|
`--force-all-users` is passed **in addition to** `--confirm` - a single stray
|
||||||
|
`--confirm` must never be enough to irreversibly purge every user's history.
|
||||||
|
- invalid uid on `user-add` -> **exit 2**.
|
||||||
|
|
||||||
|
## Access channel
|
||||||
|
|
||||||
|
There is **no ownCloud admin web / OCS account** in the vault, so the channel is
|
||||||
|
**SSH + the `occ` CLI**, not the OCS HTTP API (the OCS endpoint is reachable at
|
||||||
|
`https://cloud.acghosting.com/status.php` but we lack an OCS admin cred). The
|
||||||
|
client SSHes to `root@172.16.3.22:22` with **paramiko** (password auth,
|
||||||
|
AutoAddPolicy) and runs occ **as the web user**:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo -u apache php /var/www/owncloud/occ <cmd>
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials load lazily from the SOPS vault (never hardcoded):
|
||||||
|
`infrastructure/owncloud-vm.sops.yaml` -> `credentials.username` (root) /
|
||||||
|
`credentials.password`.
|
||||||
|
|
||||||
|
## The occ / DB commands it settled on
|
||||||
|
|
||||||
|
- **status:** `occ status --output=json` (+ `occ -V` for the display version).
|
||||||
|
- **users:** `occ user:list --show-all-attributes --output=json` -> per user:
|
||||||
|
`uid, displayName, email, quota, enabled, lastLogin, creationTime, home,
|
||||||
|
backend`. `lastLogin`/`creationTime` are Unix epoch seconds (0 = never); the
|
||||||
|
client renders them as UTC.
|
||||||
|
- **used bytes:** occ has **no `user:info` / per-user usage** on this version, so
|
||||||
|
used storage is read from ownCloud's own **filecache** (the exact figure the
|
||||||
|
UI shows). DB creds are read live from `config.php` (we already have root), then:
|
||||||
|
`SELECT s.id, fc.size FROM oc_storages s JOIN oc_filecache fc ON
|
||||||
|
fc.storage=s.numeric_id WHERE s.id LIKE 'home::%' AND fc.path='';`
|
||||||
|
Each row is `home::<uid>` -> root size in bytes.
|
||||||
|
|
||||||
|
## What the data means
|
||||||
|
|
||||||
|
- **used_bytes** is the authoritative per-account storage used; an `enabled` user
|
||||||
|
with non-zero usage is actively backing up here. `usage`/`audit` roll this up,
|
||||||
|
sorted desc, with a total.
|
||||||
|
- **quota** is a string: `default`, `none` (unlimited), or a byte/size value.
|
||||||
|
- Usage is concentrated: as of the build (2026-07-05) a single account (`pavon`)
|
||||||
|
holds ~14 TB and `sysadmin` ~2.9 TB, dwarfing the rest - ownCloud is used by a
|
||||||
|
handful of clients, not the whole GPS base.
|
||||||
|
|
||||||
|
## The "both" cross-check (`--rmm`)
|
||||||
|
|
||||||
|
Server-side (occ) tells you who has an account; `--rmm` adds the endpoint half. It
|
||||||
|
logs in to GuruRMM (`infrastructure/gururmm-server.sops.yaml`) and, for each agent
|
||||||
|
(filter with `--client`), runs a **read-only** detection PowerShell reporting
|
||||||
|
installed products / running processes / config folders matching **`ownCloud`**.
|
||||||
|
An agent is reported only if the client is actually present. This dispatches a
|
||||||
|
command to live endpoints - **always scope with `--client`** rather than sweeping
|
||||||
|
all 350+ agents.
|
||||||
|
|
||||||
|
## Notes / gotchas / limitations
|
||||||
|
|
||||||
|
- **Dirty filecache (-1):** if a user's cached root size is `-1` (cache not yet
|
||||||
|
computed), the client falls back to `du -sb <home>/files` for that one user.
|
||||||
|
That du can take minutes on a multi-hundred-GB home (timeout is generous, 900s);
|
||||||
|
`used_source` in `--json` reports `filecache` vs `du` per user. At build time
|
||||||
|
one account (`anaise`) was dirty and resolved via du (~0.6 TB).
|
||||||
|
- **Orphan storages:** the filecache can contain `home::<uid>` rows for deleted
|
||||||
|
users (e.g. a former `QWM-Sheila`); the skill keys off the live `occ user:list`,
|
||||||
|
so orphans do not appear in `users`/`usage`.
|
||||||
|
- **No OCS/web API:** deeper per-file / sharing queries would need an OCS admin
|
||||||
|
cred we do not have; occ + filecache cover the audit needs.
|
||||||
|
- **Shares are read-only here:** occ on this version exposes no share create/list
|
||||||
|
(only `sharing:cleanup-remote-storages`), so `shares` reads straight from the
|
||||||
|
`oc_share` DB table (joined to `oc_filecache` for the real path). Creating a
|
||||||
|
share would need the OCS API / an admin web cred we do not have.
|
||||||
|
- **occ command surface was verified live** against this exact 10.16 box (`occ
|
||||||
|
list` / `occ help <cmd>`) before wiring the writes - flags differ by version.
|
||||||
|
Notable ones used: `user:modify <uid> displayname|email`, quota via
|
||||||
|
`user:setting <uid> files quota --value=`, passwords via `--password-from-env`,
|
||||||
|
`files:transfer-ownership -s`, `trashbin:cleanup`/`versions:cleanup` (no user
|
||||||
|
arg = ALL users, hence the `--all-users` guard).
|
||||||
|
- Shared plumbing (repo-root resolve, vault read, GuruRMM client + endpoint
|
||||||
|
detection) lives in `.claude/scripts/backup_common.py`, used by all the
|
||||||
|
server-backup skills. Detection patterns must be precise per backend - this
|
||||||
|
skill uses `ownCloud` (not a bare `cloud`, which would over-match).
|
||||||
76
.claude/skills/owncloud/scripts/dry_test.sh
Normal file
76
.claude/skills/owncloud/scripts/dry_test.sh
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Non-destructive verification for the owncloud skill.
|
||||||
|
# - every WRITE command is run WITHOUT --confirm -> must refuse (rc 3), no SSH write
|
||||||
|
# - hard-fail guards are run (validation precedes the gate, so no execution) -> rc 2
|
||||||
|
# - read-only commands must succeed (rc 0) and JSON reads must parse
|
||||||
|
# NOTHING here runs a state change on the server.
|
||||||
|
OC=".claude/skills/owncloud/scripts/owncloud.py"
|
||||||
|
pass=0; fail=0
|
||||||
|
chk() { # chk <expected_rc> <label> -- <cmd...>
|
||||||
|
local exp="$1" label="$2"; shift 3
|
||||||
|
"$@" >/dev/null 2>&1; local rc=$?
|
||||||
|
if [ "$rc" = "$exp" ]; then printf ' [PASS] %-40s (rc %s)\n' "$label" "$rc"; pass=$((pass+1))
|
||||||
|
else printf ' [FAIL] %-40s (got rc %s, want %s)\n' "$label" "$rc" "$exp"; fail=$((fail+1)); fi
|
||||||
|
}
|
||||||
|
jchk() { # jchk <label> -- <cmd...> (stdout must be valid JSON)
|
||||||
|
local label="$1"; shift 2
|
||||||
|
if "$@" 2>/dev/null | py -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||||
|
printf ' [PASS] %-40s (valid JSON)\n' "$label"; pass=$((pass+1))
|
||||||
|
else printf ' [FAIL] %-40s (bad JSON)\n' "$label"; fail=$((fail+1)); fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== WRITE commands without --confirm must REFUSE (rc 3) =="
|
||||||
|
chk 3 "user-add" -- py "$OC" user-add zz.dry --gen-password
|
||||||
|
chk 3 "user-delete" -- py "$OC" user-delete zz.dry
|
||||||
|
chk 3 "user-enable" -- py "$OC" user-enable zz.dry
|
||||||
|
chk 3 "user-disable" -- py "$OC" user-disable zz.dry
|
||||||
|
chk 3 "user-reset-password" -- py "$OC" user-reset-password zz.dry --gen-password
|
||||||
|
chk 3 "user-modify" -- py "$OC" user-modify zz.dry --email a@b.com
|
||||||
|
chk 3 "quota-set" -- py "$OC" quota-set zz.dry "5 GB"
|
||||||
|
chk 3 "quota-reset" -- py "$OC" quota-reset zz.dry
|
||||||
|
chk 3 "group-add" -- py "$OC" group-add zz.dry
|
||||||
|
chk 3 "group-delete" -- py "$OC" group-delete zz.dry
|
||||||
|
chk 3 "group-add-member" -- py "$OC" group-add-member zz.dry -m sysadmin
|
||||||
|
chk 3 "group-remove-member" -- py "$OC" group-remove-member zz.dry -m sysadmin
|
||||||
|
chk 3 "maintenance on" -- py "$OC" maintenance on
|
||||||
|
chk 3 "maintenance off" -- py "$OC" maintenance off
|
||||||
|
chk 3 "app-enable" -- py "$OC" app-enable activity
|
||||||
|
chk 3 "app-disable" -- py "$OC" app-disable activity
|
||||||
|
chk 3 "config-set" -- py "$OC" config-set zz.key --value v
|
||||||
|
chk 3 "files-scan --user" -- py "$OC" files-scan --user zz.dry
|
||||||
|
chk 3 "files-scan --all" -- py "$OC" files-scan --all
|
||||||
|
chk 3 "transfer-ownership" -- py "$OC" transfer-ownership a b
|
||||||
|
chk 3 "trashbin-cleanup" -- py "$OC" trashbin-cleanup --user zz.dry
|
||||||
|
chk 3 "versions-cleanup" -- py "$OC" versions-cleanup --user zz.dry
|
||||||
|
|
||||||
|
echo "== hard-fail guards (validation precedes the gate; no execution) rc 2 =="
|
||||||
|
chk 2 "user-add bad uid" -- py "$OC" user-add "bad uid!" --gen-password --confirm
|
||||||
|
chk 2 "user-add no password" -- py "$OC" user-add validuid --confirm
|
||||||
|
chk 2 "user-modify no fields" -- py "$OC" user-modify zz.dry --confirm
|
||||||
|
chk 2 "files-scan no target" -- py "$OC" files-scan --confirm
|
||||||
|
chk 2 "trashbin-cleanup no target"-- py "$OC" trashbin-cleanup --confirm
|
||||||
|
chk 2 "versions-cleanup no target"-- py "$OC" versions-cleanup --confirm
|
||||||
|
# --all-users must NOT execute on --confirm alone (needs --force-all-users) -> rc 2
|
||||||
|
chk 2 "versions --all-users no force" -- py "$OC" versions-cleanup --all-users --confirm
|
||||||
|
chk 2 "trashbin --all-users no force" -- py "$OC" trashbin-cleanup --all-users --confirm
|
||||||
|
|
||||||
|
echo "== read-only commands must succeed (rc 0) =="
|
||||||
|
chk 0 "status" -- py "$OC" status
|
||||||
|
chk 0 "groups" -- py "$OC" groups
|
||||||
|
chk 0 "group-members" -- py "$OC" group-members admin
|
||||||
|
chk 0 "user-groups" -- py "$OC" user-groups sysadmin
|
||||||
|
chk 0 "shares" -- py "$OC" shares
|
||||||
|
chk 0 "shares --user" -- py "$OC" shares --user sysadmin
|
||||||
|
chk 0 "apps" -- py "$OC" apps
|
||||||
|
chk 0 "maintenance status" -- py "$OC" maintenance status
|
||||||
|
chk 0 "config-get key" -- py "$OC" config-get maintenance
|
||||||
|
chk 0 "quota (read)" -- py "$OC" quota sysadmin
|
||||||
|
|
||||||
|
echo "== JSON read outputs must parse =="
|
||||||
|
jchk "groups --json" -- py "$OC" groups --json
|
||||||
|
jchk "apps --json" -- py "$OC" apps --json
|
||||||
|
jchk "shares --json" -- py "$OC" shares --json
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "==== $pass passed, $fail failed ===="
|
||||||
|
[ "$fail" = 0 ]
|
||||||
626
.claude/skills/owncloud/scripts/owncloud.py
Normal file
626
.claude/skills/owncloud/scripts/owncloud.py
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI for the `owncloud` skill - read-only ownCloud 10.16 inventory for the GPS
|
||||||
|
backup audit (server: cloud.acghosting.com / 172.16.3.22, driven via occ over SSH).
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
status SSH ok + occ version / edition
|
||||||
|
users [--json] all users + display name, enabled, quota, used, last login
|
||||||
|
usage [--json] per-user rollup: used GB (desc) + quota + last login (audit view)
|
||||||
|
audit [--client NAME] [--rmm] [--json]
|
||||||
|
active users holding data; with --rmm cross-check GuruRMM
|
||||||
|
endpoints for the ownCloud desktop client (scope with --client)
|
||||||
|
|
||||||
|
Server-side (occ) is the source of "who has an account + how much data"; --rmm adds
|
||||||
|
the "which enrolled machine actually runs the ownCloud client" half.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from owncloud_client import OwncloudClient # noqa: E402
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
GB = 1_000_000_000
|
||||||
|
|
||||||
|
# occ enforces this uid charset (a-z A-Z 0-9 and + _ . @ - ').
|
||||||
|
UID_RE = re.compile(r"^[A-Za-z0-9+_.@'\-]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def _gb(n) -> float:
|
||||||
|
try:
|
||||||
|
return round(int(n) / GB, 2)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# --- write-op gating (mirrors the fleet convention: refuse without --confirm) --
|
||||||
|
def _gated(desc: str, confirm: bool) -> bool:
|
||||||
|
if not confirm:
|
||||||
|
print("[WARNING] Refusing state-changing action without --confirm.")
|
||||||
|
print(f"[INFO] Would: {desc}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_password(n: int = 20) -> str:
|
||||||
|
alpha = string.ascii_letters + string.digits + "!@#%^*-_=+"
|
||||||
|
return "".join(secrets.choice(alpha) for _ in range(n))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_password(args):
|
||||||
|
"""Return (password, was_generated) from --password / --password-env / --gen-password.
|
||||||
|
Prefer --password-env or --gen-password: a literal --password lands in shell
|
||||||
|
history and `ps` on the operator's machine."""
|
||||||
|
if getattr(args, "password", None):
|
||||||
|
return args.password, False
|
||||||
|
env_var = getattr(args, "password_env", None)
|
||||||
|
if env_var:
|
||||||
|
v = os.environ.get(env_var)
|
||||||
|
if not v:
|
||||||
|
raise SystemExit(f"[ERROR] env var {env_var} is unset/empty")
|
||||||
|
return v, False
|
||||||
|
if getattr(args, "gen_password", False):
|
||||||
|
return _gen_password(), True
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def _vault_reminder(uid: str, stream=None) -> None:
|
||||||
|
out = stream or sys.stdout
|
||||||
|
print("[CRITICAL] House rule: store this credential in the SOPS vault now, e.g.:",
|
||||||
|
file=out)
|
||||||
|
print(f" bash .claude/scripts/vault.sh set-field "
|
||||||
|
f"infrastructure/owncloud-users/{uid}.sops.yaml credentials.password '<paste-here>'",
|
||||||
|
file=out)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status(c: OwncloudClient, args) -> int:
|
||||||
|
s = c.server_status()
|
||||||
|
print(f"[OK] {c.host}")
|
||||||
|
print(json.dumps(s, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_users(c: OwncloudClient, args) -> int:
|
||||||
|
users = c.list_users()
|
||||||
|
users.sort(key=lambda u: int(u.get("used_bytes") or 0), reverse=True)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(users, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'UID':20} {'NAME':22} {'EN':3} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
||||||
|
for u in users:
|
||||||
|
print(f"{(u.get('uid') or '')[:20]:20} "
|
||||||
|
f"{(u.get('display_name') or '')[:22]:22} "
|
||||||
|
f"{'yes' if u.get('enabled') else 'NO':3} "
|
||||||
|
f"{_gb(u.get('used_bytes')):>7} GB "
|
||||||
|
f"{str(u.get('quota') or '-')[:12]:>12} "
|
||||||
|
f"{str(u.get('last_login') or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(users)} users; total used "
|
||||||
|
f"{round(sum(_gb(u.get('used_bytes')) for u in users), 1)} GB")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _usage_rollup(c: OwncloudClient) -> list[dict]:
|
||||||
|
users = c.list_users()
|
||||||
|
rollup = []
|
||||||
|
for u in users:
|
||||||
|
rollup.append({
|
||||||
|
"uid": u.get("uid"),
|
||||||
|
"display_name": u.get("display_name"),
|
||||||
|
"email": u.get("email"),
|
||||||
|
"enabled": bool(u.get("enabled")),
|
||||||
|
"used_bytes": int(u.get("used_bytes") or 0),
|
||||||
|
"used_gb": _gb(u.get("used_bytes")),
|
||||||
|
"used_source": u.get("used_source"),
|
||||||
|
"quota": u.get("quota"),
|
||||||
|
"last_login": u.get("last_login"),
|
||||||
|
})
|
||||||
|
rollup.sort(key=lambda x: x["used_bytes"], reverse=True)
|
||||||
|
return rollup
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_usage(c: OwncloudClient, args) -> int:
|
||||||
|
rollup = _usage_rollup(c)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(rollup, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'UID':20} {'EN':3} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
||||||
|
for r in rollup:
|
||||||
|
print(f"{(r['uid'] or '')[:20]:20} "
|
||||||
|
f"{'yes' if r['enabled'] else 'NO':3} "
|
||||||
|
f"{r['used_gb']:>7} GB "
|
||||||
|
f"{str(r['quota'] or '-')[:12]:>12} "
|
||||||
|
f"{str(r['last_login'] or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(rollup)} users; total used "
|
||||||
|
f"{round(sum(r['used_gb'] for r in rollup), 1)} GB")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_audit(c: OwncloudClient, args) -> int:
|
||||||
|
rollup = _usage_rollup(c)
|
||||||
|
# enabled users holding data are the ones actually backing up here
|
||||||
|
active = [r for r in rollup if r["enabled"] and r["used_bytes"] > 0]
|
||||||
|
result = {"backend": "owncloud", "server": c.host, "accounts": active}
|
||||||
|
|
||||||
|
if args.rmm:
|
||||||
|
rmm = bc.RmmClient()
|
||||||
|
rmm.login()
|
||||||
|
checks = []
|
||||||
|
agents = rmm.find_agents(client_substr=args.client) if args.client else rmm.list_agents()
|
||||||
|
for a in agents:
|
||||||
|
det = rmm.detect_client(a, ["ownCloud"])
|
||||||
|
if det.get("detected"):
|
||||||
|
checks.append(det)
|
||||||
|
result["rmm_endpoints_with_client"] = checks
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"=== ownCloud backup accounts - {c.host} ===")
|
||||||
|
print(f"{'UID':20} {'USED':>10} {'QUOTA':>12} {'LAST LOGIN':20}")
|
||||||
|
for r in active:
|
||||||
|
print(f"{(r['uid'] or '')[:20]:20} {r['used_gb']:>7} GB "
|
||||||
|
f"{str(r['quota'] or '-')[:12]:>12} {str(r['last_login'] or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(active)} active accounts with data; "
|
||||||
|
f"{round(sum(r['used_gb'] for r in active), 1)} GB total")
|
||||||
|
if args.rmm:
|
||||||
|
eps = result.get("rmm_endpoints_with_client", [])
|
||||||
|
print(f"\n--- GuruRMM endpoints running an ownCloud client ({len(eps)}) ---")
|
||||||
|
for e in eps:
|
||||||
|
print(f" {e['hostname']:25} client={e.get('client_name')}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Management commands (writes gated behind --confirm; reads are ungated)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def _ok(msg: str, payload: dict, as_json: bool) -> int:
|
||||||
|
if as_json:
|
||||||
|
print(json.dumps(payload, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"[OK] {msg}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# -- user lifecycle ---------------------------------------------------------
|
||||||
|
def cmd_user_add(c: OwncloudClient, args) -> int:
|
||||||
|
if not UID_RE.match(args.uid):
|
||||||
|
print(f"[ERROR] invalid uid {args.uid!r}: allowed chars are "
|
||||||
|
"a-z A-Z 0-9 + _ . @ - '", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
pw, generated = _resolve_password(args)
|
||||||
|
if not pw:
|
||||||
|
print("[ERROR] supply --password, --password-env VAR, or --gen-password",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
desc = f"create ownCloud user '{args.uid}'"
|
||||||
|
if args.group:
|
||||||
|
desc += f" in group(s) {', '.join(args.group)}"
|
||||||
|
if not _gated(desc, args.confirm):
|
||||||
|
return 3
|
||||||
|
res = c.create_user(args.uid, pw, args.display_name, args.email, args.group)
|
||||||
|
if args.json:
|
||||||
|
# keep stdout parseable: password + vault reminder go to stderr
|
||||||
|
if generated:
|
||||||
|
print(f"[CRITICAL] generated password for '{args.uid}' (shown once); "
|
||||||
|
"store in vault", file=sys.stderr)
|
||||||
|
_vault_reminder(args.uid, stream=sys.stderr)
|
||||||
|
print(json.dumps({"uid": args.uid, "created": True,
|
||||||
|
"generated_password": pw if generated else None,
|
||||||
|
"detail": res}, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"[OK] created user '{args.uid}'")
|
||||||
|
if generated:
|
||||||
|
print(f"[CRITICAL] generated password (shown once): {pw}")
|
||||||
|
_vault_reminder(args.uid)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_delete(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"DELETE user '{args.uid}' and ALL their data"
|
||||||
|
+ (" (forced)" if args.force else ""), args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.delete_user(args.uid, force=args.force)
|
||||||
|
return _ok(f"deleted user '{args.uid}'", {"uid": args.uid, "deleted": True,
|
||||||
|
"detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_enable(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"enable user '{args.uid}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.set_user_enabled(args.uid, True)
|
||||||
|
return _ok(f"enabled '{args.uid}'", {"uid": args.uid, "enabled": True, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_disable(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"disable user '{args.uid}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.set_user_enabled(args.uid, False)
|
||||||
|
return _ok(f"disabled '{args.uid}'", {"uid": args.uid, "enabled": False, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_reset_password(c: OwncloudClient, args) -> int:
|
||||||
|
pw, generated = _resolve_password(args)
|
||||||
|
if not pw:
|
||||||
|
print("[ERROR] supply --password, --password-env VAR, or --gen-password",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if not _gated(f"reset password for user '{args.uid}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.reset_password(args.uid, pw)
|
||||||
|
if args.json:
|
||||||
|
if generated:
|
||||||
|
print(f"[CRITICAL] generated password for '{args.uid}' (shown once); "
|
||||||
|
"store in vault", file=sys.stderr)
|
||||||
|
_vault_reminder(args.uid, stream=sys.stderr)
|
||||||
|
print(json.dumps({"uid": args.uid, "password_reset": True,
|
||||||
|
"generated_password": pw if generated else None},
|
||||||
|
indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"[OK] reset password for '{args.uid}'")
|
||||||
|
if generated:
|
||||||
|
print(f"[CRITICAL] generated password (shown once): {pw}")
|
||||||
|
_vault_reminder(args.uid)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_modify(c: OwncloudClient, args) -> int:
|
||||||
|
changes = []
|
||||||
|
if args.display_name is not None:
|
||||||
|
changes.append(("displayname", args.display_name))
|
||||||
|
if args.email is not None:
|
||||||
|
changes.append(("email", args.email))
|
||||||
|
if not changes:
|
||||||
|
print("[ERROR] nothing to change: pass --display-name and/or --email",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
desc = "set " + ", ".join(f"{k}={v!r}" for k, v in changes) + f" on '{args.uid}'"
|
||||||
|
if not _gated(desc, args.confirm):
|
||||||
|
return 3
|
||||||
|
for key, val in changes:
|
||||||
|
c.modify_user(args.uid, key, val)
|
||||||
|
return _ok(f"modified '{args.uid}'", {"uid": args.uid,
|
||||||
|
"changed": dict(changes)}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
# -- quota ------------------------------------------------------------------
|
||||||
|
def cmd_quota(c: OwncloudClient, args) -> int:
|
||||||
|
"""Read a user's quota (ungated)."""
|
||||||
|
q = c.get_quota(args.uid)
|
||||||
|
return _ok(f"{args.uid} quota: {q or 'default'}",
|
||||||
|
{"uid": args.uid, "quota": q}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_quota_set(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"set quota for '{args.uid}' to {args.value!r}", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.set_quota(args.uid, args.value)
|
||||||
|
return _ok(f"set '{args.uid}' quota = {args.value}",
|
||||||
|
{"uid": args.uid, "quota": args.value}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_quota_reset(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"reset '{args.uid}' quota to default", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.reset_quota(args.uid)
|
||||||
|
return _ok(f"reset '{args.uid}' quota to default",
|
||||||
|
{"uid": args.uid, "quota": "default"}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
# -- groups -----------------------------------------------------------------
|
||||||
|
def cmd_groups(c: OwncloudClient, args) -> int:
|
||||||
|
g = c.list_groups()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(g, indent=2))
|
||||||
|
return 0
|
||||||
|
names = g if isinstance(g, list) else list(g.keys())
|
||||||
|
for n in names:
|
||||||
|
print(f" {n}")
|
||||||
|
print(f"\n{len(names)} groups")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_group_members(c: OwncloudClient, args) -> int:
|
||||||
|
m = c.group_members(args.group)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(m, indent=2))
|
||||||
|
return 0
|
||||||
|
members = m if isinstance(m, list) else list(m.keys())
|
||||||
|
for u in members:
|
||||||
|
print(f" {u}")
|
||||||
|
print(f"\n{len(members)} members of '{args.group}'")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_groups(c: OwncloudClient, args) -> int:
|
||||||
|
g = c.user_groups(args.uid)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(g, indent=2))
|
||||||
|
return 0
|
||||||
|
groups = g if isinstance(g, list) else list(g.keys())
|
||||||
|
for n in groups:
|
||||||
|
print(f" {n}")
|
||||||
|
print(f"\n'{args.uid}' is in {len(groups)} group(s)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_group_add(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"create group '{args.group}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.create_group(args.group)
|
||||||
|
return _ok(f"created group '{args.group}'", {"group": args.group, "created": True}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_group_delete(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"delete group '{args.group}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.delete_group(args.group)
|
||||||
|
return _ok(f"deleted group '{args.group}'", {"group": args.group, "deleted": True}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_group_add_member(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"add {', '.join(args.member)} to group '{args.group}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.add_group_members(args.group, args.member)
|
||||||
|
return _ok(f"added {len(args.member)} member(s) to '{args.group}'",
|
||||||
|
{"group": args.group, "added": args.member}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_group_remove_member(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"remove {', '.join(args.member)} from group '{args.group}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.remove_group_members(args.group, args.member)
|
||||||
|
return _ok(f"removed {len(args.member)} member(s) from '{args.group}'",
|
||||||
|
{"group": args.group, "removed": args.member}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
# -- shares (read-only) -----------------------------------------------------
|
||||||
|
def cmd_shares(c: OwncloudClient, args) -> int:
|
||||||
|
shares = c.list_shares(args.user)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(shares, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'ID':>7} {'TYPE':12} {'OWNER':16} {'SHARE WITH':16} {'PATH/TARGET':30}")
|
||||||
|
for s in shares:
|
||||||
|
print(f"{str(s['id']):>7} {s['share_type'][:12]:12} "
|
||||||
|
f"{str(s['owner'] or '-')[:16]:16} {str(s['share_with'] or '-')[:16]:16} "
|
||||||
|
f"{str(s['path'] or s['target'] or '-')[:30]:30}")
|
||||||
|
print(f"\n{len(shares)} share(s)"
|
||||||
|
+ (f" involving '{args.user}'" if args.user else ""))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# -- server / apps / config -------------------------------------------------
|
||||||
|
def cmd_maintenance(c: OwncloudClient, args) -> int:
|
||||||
|
if args.state == "status":
|
||||||
|
on = c.maintenance_status()
|
||||||
|
return _ok(f"maintenance mode is {'ON' if on else 'off'}",
|
||||||
|
{"maintenance": on}, args.json)
|
||||||
|
want_on = args.state == "on"
|
||||||
|
if not _gated(f"turn maintenance mode {'ON' if want_on else 'off'}", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.maintenance_mode(want_on)
|
||||||
|
return _ok(f"maintenance mode {'ON' if want_on else 'off'}",
|
||||||
|
{"maintenance": want_on}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_apps(c: OwncloudClient, args) -> int:
|
||||||
|
apps = c.list_apps()
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(apps, indent=2))
|
||||||
|
return 0
|
||||||
|
enabled = apps.get("enabled", {}) if isinstance(apps, dict) else {}
|
||||||
|
disabled = apps.get("disabled", {}) if isinstance(apps, dict) else {}
|
||||||
|
print(f"--- enabled ({len(enabled)}) ---")
|
||||||
|
for a in sorted(enabled):
|
||||||
|
print(f" {a}")
|
||||||
|
print(f"--- disabled ({len(disabled)}) ---")
|
||||||
|
for a in sorted(disabled):
|
||||||
|
print(f" {a}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_app_enable(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"enable app '{args.app}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.enable_app(args.app)
|
||||||
|
return _ok(f"enabled app '{args.app}'", {"app": args.app, "enabled": True, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_app_disable(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"disable app '{args.app}'", args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.disable_app(args.app)
|
||||||
|
return _ok(f"disabled app '{args.app}'", {"app": args.app, "enabled": False, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_get(c: OwncloudClient, args) -> int:
|
||||||
|
if args.name:
|
||||||
|
val = c.config_get(args.name)
|
||||||
|
return _ok(f"{args.name} = {val}", {"name": args.name, "value": val}, args.json)
|
||||||
|
cfg = c.config_list()
|
||||||
|
print(json.dumps(cfg, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_set(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"set system config {args.name} = {args.value!r} ({args.type})", args.confirm):
|
||||||
|
return 3
|
||||||
|
c.config_set(args.name, args.value, args.type)
|
||||||
|
return _ok(f"set {args.name} = {args.value}",
|
||||||
|
{"name": args.name, "value": args.value, "type": args.type}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
# -- files admin ------------------------------------------------------------
|
||||||
|
def cmd_files_scan(c: OwncloudClient, args) -> int:
|
||||||
|
if not args.all and not args.user:
|
||||||
|
print("[ERROR] specify a user or --all", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
target = "ALL users" if args.all else f"'{args.user}'"
|
||||||
|
if not _gated(f"filesystem rescan for {target}"
|
||||||
|
+ (f" path={args.path}" if args.path else ""), args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.files_scan(uid=args.user, all_users=args.all, path=args.path)
|
||||||
|
return _ok(f"scanned {target}", {"scanned": target, "detail": out.splitlines()[-1:] if out else []}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_transfer_ownership(c: OwncloudClient, args) -> int:
|
||||||
|
if not _gated(f"transfer files from '{args.source}' to '{args.dest}'"
|
||||||
|
+ (f" path={args.path}" if args.path else ""), args.confirm):
|
||||||
|
return 3
|
||||||
|
out = c.transfer_ownership(args.source, args.dest, args.path)
|
||||||
|
return _ok(f"transferred '{args.source}' -> '{args.dest}'",
|
||||||
|
{"source": args.source, "dest": args.dest, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup(c: OwncloudClient, args, kind: str) -> int:
|
||||||
|
"""Shared guard for trashbin/versions cleanup: refuse a fleet-wide sweep
|
||||||
|
unless it is made explicit with --all-users (occ treats 'no user' as ALL)."""
|
||||||
|
users = args.user or []
|
||||||
|
if not users and not args.all_users:
|
||||||
|
print(f"[ERROR] {kind} cleanup with no --user targets ALL users; "
|
||||||
|
"pass explicit --user, or --all-users to sweep everyone.",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
# A fleet-wide, irreversible purge must not be reachable by a single stray
|
||||||
|
# --confirm: require a dedicated acknowledgement flag on top of it.
|
||||||
|
if args.all_users and not args.force_all_users:
|
||||||
|
print(f"[ERROR] {kind}-cleanup --all-users irreversibly purges EVERY "
|
||||||
|
f"user's {kind} history. This needs --force-all-users IN ADDITION "
|
||||||
|
"to --confirm to proceed.", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
target = "ALL users" if args.all_users else ", ".join(users)
|
||||||
|
if not _gated(f"permanently delete {kind} for {target}", args.confirm):
|
||||||
|
return 3
|
||||||
|
fn = c.trashbin_cleanup if kind == "trashbin" else c.versions_cleanup
|
||||||
|
out = fn([] if args.all_users else users)
|
||||||
|
return _ok(f"{kind} cleaned for {target}",
|
||||||
|
{"cleanup": kind, "target": target, "detail": out}, args.json)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_trashbin_cleanup(c: OwncloudClient, args) -> int:
|
||||||
|
return _cleanup(c, args, "trashbin")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_versions_cleanup(c: OwncloudClient, args) -> int:
|
||||||
|
return _cleanup(c, args, "versions")
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(prog="owncloud", description="ownCloud backup inventory (GPS audit)")
|
||||||
|
p.add_argument("--host", help="override server host/IP (default 172.16.3.22)")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("status")
|
||||||
|
u = sub.add_parser("users"); u.add_argument("--json", action="store_true")
|
||||||
|
us = sub.add_parser("usage"); us.add_argument("--json", action="store_true")
|
||||||
|
a = sub.add_parser("audit"); a.add_argument("--client"); a.add_argument("--rmm", action="store_true"); a.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
def _pw_opts(sp):
|
||||||
|
sp.add_argument("--password", help="password literal (avoid: lands in shell history/ps)")
|
||||||
|
sp.add_argument("--password-env", metavar="VAR", help="read password from this env var")
|
||||||
|
sp.add_argument("--gen-password", action="store_true", help="generate a strong password")
|
||||||
|
|
||||||
|
# --- user lifecycle (writes) ---
|
||||||
|
ua = sub.add_parser("user-add")
|
||||||
|
ua.add_argument("uid"); ua.add_argument("--display-name"); ua.add_argument("--email")
|
||||||
|
ua.add_argument("-g", "--group", action="append", help="group to add to (repeatable)")
|
||||||
|
_pw_opts(ua); ua.add_argument("--confirm", action="store_true"); ua.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
ud = sub.add_parser("user-delete"); ud.add_argument("uid")
|
||||||
|
ud.add_argument("-f", "--force", action="store_true"); ud.add_argument("--confirm", action="store_true"); ud.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
ue = sub.add_parser("user-enable"); ue.add_argument("uid"); ue.add_argument("--confirm", action="store_true"); ue.add_argument("--json", action="store_true")
|
||||||
|
udi = sub.add_parser("user-disable"); udi.add_argument("uid"); udi.add_argument("--confirm", action="store_true"); udi.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
urp = sub.add_parser("user-reset-password"); urp.add_argument("uid")
|
||||||
|
_pw_opts(urp); urp.add_argument("--confirm", action="store_true"); urp.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
um = sub.add_parser("user-modify"); um.add_argument("uid")
|
||||||
|
um.add_argument("--display-name"); um.add_argument("--email")
|
||||||
|
um.add_argument("--confirm", action="store_true"); um.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- quota ---
|
||||||
|
q = sub.add_parser("quota"); q.add_argument("uid"); q.add_argument("--json", action="store_true")
|
||||||
|
qs = sub.add_parser("quota-set"); qs.add_argument("uid"); qs.add_argument("value", help="e.g. '5 GB', 'none' (unlimited)")
|
||||||
|
qs.add_argument("--confirm", action="store_true"); qs.add_argument("--json", action="store_true")
|
||||||
|
qr = sub.add_parser("quota-reset"); qr.add_argument("uid"); qr.add_argument("--confirm", action="store_true"); qr.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- groups ---
|
||||||
|
gl = sub.add_parser("groups"); gl.add_argument("--json", action="store_true")
|
||||||
|
gm = sub.add_parser("group-members"); gm.add_argument("group"); gm.add_argument("--json", action="store_true")
|
||||||
|
ug = sub.add_parser("user-groups"); ug.add_argument("uid"); ug.add_argument("--json", action="store_true")
|
||||||
|
gadd = sub.add_parser("group-add"); gadd.add_argument("group"); gadd.add_argument("--confirm", action="store_true"); gadd.add_argument("--json", action="store_true")
|
||||||
|
gdel = sub.add_parser("group-delete"); gdel.add_argument("group"); gdel.add_argument("--confirm", action="store_true"); gdel.add_argument("--json", action="store_true")
|
||||||
|
gam = sub.add_parser("group-add-member"); gam.add_argument("group"); gam.add_argument("-m", "--member", action="append", required=True); gam.add_argument("--confirm", action="store_true"); gam.add_argument("--json", action="store_true")
|
||||||
|
grm = sub.add_parser("group-remove-member"); grm.add_argument("group"); grm.add_argument("-m", "--member", action="append", required=True); grm.add_argument("--confirm", action="store_true"); grm.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- shares (read-only) ---
|
||||||
|
sh = sub.add_parser("shares"); sh.add_argument("--user"); sh.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- server / apps / config ---
|
||||||
|
mnt = sub.add_parser("maintenance"); mnt.add_argument("state", choices=["status", "on", "off"]); mnt.add_argument("--confirm", action="store_true"); mnt.add_argument("--json", action="store_true")
|
||||||
|
ap = sub.add_parser("apps"); ap.add_argument("--json", action="store_true")
|
||||||
|
ape = sub.add_parser("app-enable"); ape.add_argument("app"); ape.add_argument("--confirm", action="store_true"); ape.add_argument("--json", action="store_true")
|
||||||
|
apd = sub.add_parser("app-disable"); apd.add_argument("app"); apd.add_argument("--confirm", action="store_true"); apd.add_argument("--json", action="store_true")
|
||||||
|
cg = sub.add_parser("config-get"); cg.add_argument("name", nargs="?", help="omit to dump all system config"); cg.add_argument("--json", action="store_true")
|
||||||
|
cs = sub.add_parser("config-set"); cs.add_argument("name"); cs.add_argument("--value", required=True); cs.add_argument("--type", default="string", choices=["string", "integer", "double", "boolean", "json"]); cs.add_argument("--confirm", action="store_true"); cs.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- files admin ---
|
||||||
|
fs = sub.add_parser("files-scan"); fs.add_argument("--user"); fs.add_argument("--all", action="store_true"); fs.add_argument("--path"); fs.add_argument("--confirm", action="store_true"); fs.add_argument("--json", action="store_true")
|
||||||
|
to = sub.add_parser("transfer-ownership"); to.add_argument("source"); to.add_argument("dest"); to.add_argument("--path"); to.add_argument("--confirm", action="store_true"); to.add_argument("--json", action="store_true")
|
||||||
|
tc = sub.add_parser("trashbin-cleanup"); tc.add_argument("--user", action="append"); tc.add_argument("--all-users", action="store_true"); tc.add_argument("--force-all-users", action="store_true", help="required with --all-users + --confirm to purge everyone"); tc.add_argument("--confirm", action="store_true"); tc.add_argument("--json", action="store_true")
|
||||||
|
vc = sub.add_parser("versions-cleanup"); vc.add_argument("--user", action="append"); vc.add_argument("--all-users", action="store_true"); vc.add_argument("--force-all-users", action="store_true", help="required with --all-users + --confirm to purge everyone"); vc.add_argument("--confirm", action="store_true"); vc.add_argument("--json", action="store_true")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
c = OwncloudClient(args.host)
|
||||||
|
handlers = {"status": cmd_status, "users": cmd_users,
|
||||||
|
"usage": cmd_usage, "audit": cmd_audit,
|
||||||
|
"user-add": cmd_user_add, "user-delete": cmd_user_delete,
|
||||||
|
"user-enable": cmd_user_enable, "user-disable": cmd_user_disable,
|
||||||
|
"user-reset-password": cmd_user_reset_password,
|
||||||
|
"user-modify": cmd_user_modify,
|
||||||
|
"quota": cmd_quota, "quota-set": cmd_quota_set,
|
||||||
|
"quota-reset": cmd_quota_reset,
|
||||||
|
"groups": cmd_groups, "group-members": cmd_group_members,
|
||||||
|
"user-groups": cmd_user_groups,
|
||||||
|
"group-add": cmd_group_add, "group-delete": cmd_group_delete,
|
||||||
|
"group-add-member": cmd_group_add_member,
|
||||||
|
"group-remove-member": cmd_group_remove_member,
|
||||||
|
"shares": cmd_shares,
|
||||||
|
"maintenance": cmd_maintenance, "apps": cmd_apps,
|
||||||
|
"app-enable": cmd_app_enable, "app-disable": cmd_app_disable,
|
||||||
|
"config-get": cmd_config_get, "config-set": cmd_config_set,
|
||||||
|
"files-scan": cmd_files_scan,
|
||||||
|
"transfer-ownership": cmd_transfer_ownership,
|
||||||
|
"trashbin-cleanup": cmd_trashbin_cleanup,
|
||||||
|
"versions-cleanup": cmd_versions_cleanup}
|
||||||
|
try:
|
||||||
|
return handlers[args.cmd](c, args)
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
# best-effort error logging per house rule
|
||||||
|
try:
|
||||||
|
root = bc.resolve_root()
|
||||||
|
bc.subprocess.run(["bash", str(root / ".claude/scripts/log-skill-error.sh"),
|
||||||
|
"owncloud", str(exc)[:200]], timeout=15)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
510
.claude/skills/owncloud/scripts/owncloud_client.py
Normal file
510
.claude/skills/owncloud/scripts/owncloud_client.py
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""ownCloud server client for the `owncloud` skill (GPS backup verification).
|
||||||
|
|
||||||
|
Talks to ACG's live ownCloud 10.16 server (cloud.acghosting.com / 172.16.3.22)
|
||||||
|
over SSH and drives the `occ` admin CLI. Read-only: enumerates users, their
|
||||||
|
quota + actual storage used, and last login so the GPS backup audit can see who
|
||||||
|
is actually backing up to ownCloud and how much data they hold.
|
||||||
|
|
||||||
|
There is NO ownCloud admin WEB/OCS account in the vault, so the access channel is
|
||||||
|
SSH (root@172.16.3.22, paramiko password auth) + `occ`, not the OCS HTTP API.
|
||||||
|
`occ` must run as the web/service user, so every invocation is:
|
||||||
|
|
||||||
|
sudo -u apache php /var/www/owncloud/occ <cmd>
|
||||||
|
|
||||||
|
Per-user "used bytes" is not exposed by occ on this version (no `user:info`).
|
||||||
|
Instead we read the authoritative figure straight from ownCloud's filecache: the
|
||||||
|
size ownCloud itself reports for each user's root. DB creds are read live from
|
||||||
|
the server's config.php (we already have root), so nothing is duplicated and it
|
||||||
|
survives a DB-password rotation. If a user's cached size is dirty (-1), we fall
|
||||||
|
back to `du -sb <home>/files` for that one user (slower, generous timeout).
|
||||||
|
|
||||||
|
Credentials load lazily from the SOPS vault (never hardcoded):
|
||||||
|
`infrastructure/owncloud-vm.sops.yaml` -> credentials.username / credentials.password.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
# Import the shared backup helpers from .claude/scripts.
|
||||||
|
_SKILL_DIR = Path(__file__).resolve().parent.parent # .../skills/owncloud
|
||||||
|
_SCRIPTS_DIR = _SKILL_DIR.parent.parent / "scripts" # .../.claude/scripts
|
||||||
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
VAULT_ENTRY = "infrastructure/owncloud-vm.sops.yaml"
|
||||||
|
VAULT_FIELD_USER = "credentials.username"
|
||||||
|
VAULT_FIELD_PASS = "credentials.password"
|
||||||
|
|
||||||
|
DEFAULT_HOST = "172.16.3.22"
|
||||||
|
SSH_PORT = 22
|
||||||
|
# occ must run as the web user (apache) with the system php against the install dir.
|
||||||
|
OCC_INSTALL_DIR = "/var/www/owncloud"
|
||||||
|
OCC = f"sudo -u apache php {OCC_INSTALL_DIR}/occ"
|
||||||
|
CONFIG_PHP = f"{OCC_INSTALL_DIR}/config/config.php"
|
||||||
|
|
||||||
|
EXEC_TIMEOUT = 60 # seconds for normal occ / mysql calls
|
||||||
|
DU_TIMEOUT = 900 # du on a dirty-cache home can be hundreds of GB
|
||||||
|
|
||||||
|
|
||||||
|
class OwncloudClient:
|
||||||
|
def __init__(self, host: Optional[str] = None):
|
||||||
|
self.host = host or DEFAULT_HOST
|
||||||
|
self._ssh: Optional[paramiko.SSHClient] = None
|
||||||
|
self._db: Optional[dict] = None
|
||||||
|
|
||||||
|
# -- ssh -------------------------------------------------------------------
|
||||||
|
def _connect(self) -> paramiko.SSHClient:
|
||||||
|
if self._ssh is not None:
|
||||||
|
return self._ssh
|
||||||
|
user = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_USER)
|
||||||
|
pw = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_PASS)
|
||||||
|
cli = paramiko.SSHClient()
|
||||||
|
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
cli.connect(self.host, port=SSH_PORT, username=user, password=pw,
|
||||||
|
timeout=20, look_for_keys=False, allow_agent=False)
|
||||||
|
except Exception as exc: # paramiko raises several distinct types
|
||||||
|
raise bc.BackupError(f"SSH connect to {user}@{self.host} failed: {exc}") from exc
|
||||||
|
self._ssh = cli
|
||||||
|
return cli
|
||||||
|
|
||||||
|
def run(self, cmd: str, timeout: int = EXEC_TIMEOUT) -> tuple[int, str, str]:
|
||||||
|
"""Run a shell command over SSH; return (exit_status, stdout, stderr)."""
|
||||||
|
cli = self._connect()
|
||||||
|
try:
|
||||||
|
_in, out, err = cli.exec_command(cmd, timeout=timeout)
|
||||||
|
stdout = out.read().decode("utf-8", "replace")
|
||||||
|
stderr = err.read().decode("utf-8", "replace")
|
||||||
|
rc = out.channel.recv_exit_status()
|
||||||
|
except Exception as exc:
|
||||||
|
raise bc.BackupError(f"SSH command failed ({cmd[:60]}...): {exc}") from exc
|
||||||
|
return rc, stdout, stderr
|
||||||
|
|
||||||
|
def occ(self, args: str, timeout: int = EXEC_TIMEOUT) -> tuple[int, str, str]:
|
||||||
|
return self.run(f"{OCC} {args}", timeout=timeout)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if self._ssh is not None:
|
||||||
|
try:
|
||||||
|
self._ssh.close()
|
||||||
|
finally:
|
||||||
|
self._ssh = None
|
||||||
|
|
||||||
|
# -- server status ---------------------------------------------------------
|
||||||
|
def server_status(self) -> dict:
|
||||||
|
"""occ status (installed/version/edition) plus the `occ -V` version string."""
|
||||||
|
rc, out, err = self.occ("status --output=json")
|
||||||
|
if rc != 0:
|
||||||
|
raise bc.BackupError(f"occ status failed (rc={rc}): {err.strip()[:300]}")
|
||||||
|
try:
|
||||||
|
status = json.loads(out)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise bc.BackupError(f"occ status returned non-JSON: {out.strip()[:300]}")
|
||||||
|
rc2, ver, _ = self.occ("-V")
|
||||||
|
status["occ_version"] = ver.strip() if rc2 == 0 else None
|
||||||
|
status["host"] = self.host
|
||||||
|
return status
|
||||||
|
|
||||||
|
# -- storage used (filecache primary, du fallback) -------------------------
|
||||||
|
def _db_config(self) -> dict:
|
||||||
|
"""Read DB name/user/password/prefix live from config.php (we have root)."""
|
||||||
|
if self._db is not None:
|
||||||
|
return self._db
|
||||||
|
rc, out, err = self.run(
|
||||||
|
"grep -E \"'db(name|user|password|tableprefix)'\" " + CONFIG_PHP)
|
||||||
|
if rc != 0:
|
||||||
|
raise bc.BackupError(f"reading {CONFIG_PHP} failed (rc={rc}): {err.strip()[:200]}")
|
||||||
|
cfg = {"dbname": "owncloud", "dbuser": "owncloud",
|
||||||
|
"dbpassword": "", "dbtableprefix": "oc_"}
|
||||||
|
for line in out.splitlines():
|
||||||
|
# form: 'dbname' => 'value',
|
||||||
|
if "=>" not in line:
|
||||||
|
continue
|
||||||
|
key, _, val = line.partition("=>")
|
||||||
|
k = key.strip().strip("'\" ")
|
||||||
|
v = val.strip().rstrip(",").strip().strip("'\"")
|
||||||
|
if k in cfg:
|
||||||
|
cfg[k] = v
|
||||||
|
self._db = cfg
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def _mysql(self, sql: str) -> str:
|
||||||
|
cfg = self._db_config()
|
||||||
|
cmd = (f"MYSQL_PWD={shlex.quote(cfg['dbpassword'])} "
|
||||||
|
f"mysql -N -B -u {shlex.quote(cfg['dbuser'])} "
|
||||||
|
f"{shlex.quote(cfg['dbname'])} -e {shlex.quote(sql)}")
|
||||||
|
rc, out, err = self.run(cmd)
|
||||||
|
if rc != 0:
|
||||||
|
raise bc.BackupError(f"mysql query failed (rc={rc}): {err.strip()[:200]}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _filecache_sizes(self) -> dict[str, int]:
|
||||||
|
"""uid -> cached used bytes for each per-user home storage (ownCloud's own
|
||||||
|
figure). A size of -1 means the cache is dirty; such uids are omitted here
|
||||||
|
and handled by the du fallback."""
|
||||||
|
pfx = self._db_config()["dbtableprefix"]
|
||||||
|
sql = (f"SELECT s.id, fc.size FROM {pfx}storages s "
|
||||||
|
f"JOIN {pfx}filecache fc ON fc.storage=s.numeric_id "
|
||||||
|
f"WHERE s.id LIKE 'home::%' AND fc.path='';")
|
||||||
|
out = self._mysql(sql)
|
||||||
|
sizes: dict[str, int] = {}
|
||||||
|
for row in out.splitlines():
|
||||||
|
if "\t" not in row:
|
||||||
|
continue
|
||||||
|
sid, _, val = row.partition("\t")
|
||||||
|
if not sid.startswith("home::"):
|
||||||
|
continue
|
||||||
|
uid = sid[len("home::"):]
|
||||||
|
try:
|
||||||
|
n = int(val.strip())
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if n >= 0:
|
||||||
|
sizes[uid] = n
|
||||||
|
return sizes
|
||||||
|
|
||||||
|
def _du_bytes(self, uid: str, home: str) -> Optional[int]:
|
||||||
|
"""Fallback used-bytes via du on the on-disk home (dirty-cache users only)."""
|
||||||
|
if not home:
|
||||||
|
return None
|
||||||
|
rc, out, err = self.run(
|
||||||
|
f"du -sb {shlex.quote(home + '/files')} 2>/dev/null | cut -f1",
|
||||||
|
timeout=DU_TIMEOUT)
|
||||||
|
try:
|
||||||
|
return int(out.strip())
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# -- users -----------------------------------------------------------------
|
||||||
|
def list_users(self, with_usage: bool = True) -> list[dict]:
|
||||||
|
"""All users with attributes (uid, display name, email, quota, enabled,
|
||||||
|
last login, home, backend). When with_usage, enrich each with used bytes
|
||||||
|
from the filecache, falling back to du for dirty-cache users."""
|
||||||
|
rc, out, err = self.occ("user:list --show-all-attributes --output=json")
|
||||||
|
if rc != 0:
|
||||||
|
raise bc.BackupError(f"occ user:list failed (rc={rc}): {err.strip()[:300]}")
|
||||||
|
try:
|
||||||
|
raw = json.loads(out)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise bc.BackupError(f"occ user:list returned non-JSON: {out.strip()[:300]}")
|
||||||
|
|
||||||
|
users: list[dict] = []
|
||||||
|
for uid, attrs in raw.items():
|
||||||
|
attrs = attrs or {}
|
||||||
|
users.append({
|
||||||
|
"uid": uid,
|
||||||
|
"display_name": attrs.get("displayName"),
|
||||||
|
"email": attrs.get("email"),
|
||||||
|
"quota": attrs.get("quota"),
|
||||||
|
"enabled": _as_bool(attrs.get("enabled")),
|
||||||
|
"last_login": _epoch(attrs.get("lastLogin")),
|
||||||
|
"creation_time": _epoch(attrs.get("creationTime")),
|
||||||
|
"backend": attrs.get("backend"),
|
||||||
|
"home": attrs.get("home"),
|
||||||
|
"used_bytes": None,
|
||||||
|
"used_source": None,
|
||||||
|
})
|
||||||
|
|
||||||
|
if with_usage:
|
||||||
|
sizes = self._filecache_sizes()
|
||||||
|
for u in users:
|
||||||
|
uid = u["uid"]
|
||||||
|
if uid in sizes:
|
||||||
|
u["used_bytes"] = sizes[uid]
|
||||||
|
u["used_source"] = "filecache"
|
||||||
|
else:
|
||||||
|
# dirty cache (-1) or no storage row: compute on disk
|
||||||
|
du = self._du_bytes(uid, u.get("home"))
|
||||||
|
u["used_bytes"] = du
|
||||||
|
u["used_source"] = "du" if du is not None else None
|
||||||
|
return users
|
||||||
|
|
||||||
|
# -- occ helpers for management ops ----------------------------------------
|
||||||
|
def occ_env(self, env: dict, args: str,
|
||||||
|
timeout: int = EXEC_TIMEOUT) -> tuple[int, str, str]:
|
||||||
|
"""Run occ with extra environment (e.g. OC_PASS) exported for the apache
|
||||||
|
user. `sudo -u apache env KEY=val php occ ...` — the explicit `env` sets
|
||||||
|
the var inside apache's context regardless of sudo's env_reset policy.
|
||||||
|
|
||||||
|
Caveat: the value is briefly visible in `ps` on the server to root; this
|
||||||
|
is occ's own documented mechanism for passwords and the box is root-only.
|
||||||
|
"""
|
||||||
|
prefix = " ".join(f"{k}={shlex.quote(str(v))}" for k, v in env.items())
|
||||||
|
cmd = f"sudo -u apache env {prefix} php {OCC_INSTALL_DIR}/occ {args}"
|
||||||
|
return self.run(cmd, timeout=timeout)
|
||||||
|
|
||||||
|
def _occ_ok(self, args: str, timeout: int = EXEC_TIMEOUT) -> str:
|
||||||
|
"""Run occ, raise BackupError on non-zero, return stdout."""
|
||||||
|
rc, out, err = self.occ(args, timeout=timeout)
|
||||||
|
if rc != 0:
|
||||||
|
label = args.split()[0] if args else "occ"
|
||||||
|
raise bc.BackupError(f"occ {label} failed (rc={rc}): "
|
||||||
|
f"{(err or out).strip()[:300]}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _occ_env_ok(self, env: dict, args: str,
|
||||||
|
timeout: int = EXEC_TIMEOUT) -> str:
|
||||||
|
"""occ_env variant of _occ_ok (occ_env is not wrapped by _occ_ok)."""
|
||||||
|
rc, out, err = self.occ_env(env, args, timeout=timeout)
|
||||||
|
if rc != 0:
|
||||||
|
label = args.split()[0] if args else "occ"
|
||||||
|
raise bc.BackupError(f"occ {label} failed (rc={rc}): "
|
||||||
|
f"{(err or out).strip()[:300]}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _occ_json(self, args: str, timeout: int = EXEC_TIMEOUT) -> Any:
|
||||||
|
"""occ with --output=json; parse defensively so a non-JSON banner
|
||||||
|
(maintenance notice, PHP deprecation, update line) surfaces as a clean
|
||||||
|
BackupError rather than an uncaught JSONDecodeError traceback."""
|
||||||
|
out = self._occ_ok(args, timeout=timeout).strip()
|
||||||
|
try:
|
||||||
|
return json.loads(out or "null")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
label = args.split()[0] if args else "occ"
|
||||||
|
raise bc.BackupError(f"occ {label} returned non-JSON: {out[:300]}")
|
||||||
|
|
||||||
|
# -- user lifecycle --------------------------------------------------------
|
||||||
|
def create_user(self, uid: str, password: str,
|
||||||
|
display_name: Optional[str] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
groups: Optional[list[str]] = None) -> dict:
|
||||||
|
# options first, then `--`, then the positional uid so a uid that
|
||||||
|
# happens to start with '-' is never parsed by occ as an option flag.
|
||||||
|
parts = ["user:add --password-from-env -n"]
|
||||||
|
if display_name:
|
||||||
|
parts.append(f"--display-name={shlex.quote(display_name)}")
|
||||||
|
if email:
|
||||||
|
parts.append(f"--email={shlex.quote(email)}")
|
||||||
|
for g in (groups or []):
|
||||||
|
parts.append(f"--group={shlex.quote(g)}")
|
||||||
|
parts.append(f"-- {shlex.quote(uid)}")
|
||||||
|
out = self._occ_env_ok({"OC_PASS": password}, " ".join(parts))
|
||||||
|
return {"uid": uid, "output": out.strip()}
|
||||||
|
|
||||||
|
def delete_user(self, uid: str, force: bool = False) -> str:
|
||||||
|
return self._occ_ok(f"user:delete{' -f' if force else ''} -- "
|
||||||
|
f"{shlex.quote(uid)}", timeout=DU_TIMEOUT).strip()
|
||||||
|
|
||||||
|
def set_user_enabled(self, uid: str, enabled: bool) -> str:
|
||||||
|
verb = "user:enable" if enabled else "user:disable"
|
||||||
|
return self._occ_ok(f"{verb} -- {shlex.quote(uid)}").strip()
|
||||||
|
|
||||||
|
def reset_password(self, uid: str, password: str) -> str:
|
||||||
|
return self._occ_env_ok(
|
||||||
|
{"OC_PASS": password},
|
||||||
|
f"user:resetpassword --password-from-env -- {shlex.quote(uid)}").strip()
|
||||||
|
|
||||||
|
def modify_user(self, uid: str, key: str, value: str) -> str:
|
||||||
|
"""key is one of: displayname, email (per occ user:modify)."""
|
||||||
|
return self._occ_ok(
|
||||||
|
f"user:modify -- {shlex.quote(uid)} {shlex.quote(key)} "
|
||||||
|
f"{shlex.quote(value)}").strip()
|
||||||
|
|
||||||
|
# -- quota (occ user:setting <uid> files quota) ----------------------------
|
||||||
|
def get_quota(self, uid: str) -> Optional[str]:
|
||||||
|
"""Return the user's quota string, or None when it is unset (= default).
|
||||||
|
A non-existent user or a genuine occ failure raises BackupError - occ
|
||||||
|
returns rc=1 for both 'setting unset' and 'user missing', so we key off
|
||||||
|
the message ('setting does not exist' == unset)."""
|
||||||
|
rc, out, err = self.occ(f"user:setting -- {shlex.quote(uid)} files quota")
|
||||||
|
if rc == 0:
|
||||||
|
return out.strip() or None
|
||||||
|
msg = (out or err).strip()
|
||||||
|
if "setting does not exist" in msg.lower():
|
||||||
|
return None
|
||||||
|
raise bc.BackupError(f"occ user:setting quota failed (rc={rc}): {msg[:300]}")
|
||||||
|
|
||||||
|
def set_quota(self, uid: str, value: str) -> str:
|
||||||
|
return self._occ_ok(
|
||||||
|
f"user:setting --value={shlex.quote(value)} "
|
||||||
|
f"-- {shlex.quote(uid)} files quota").strip()
|
||||||
|
|
||||||
|
def reset_quota(self, uid: str) -> str:
|
||||||
|
return self._occ_ok(
|
||||||
|
f"user:setting --delete -- {shlex.quote(uid)} files quota").strip()
|
||||||
|
|
||||||
|
# -- groups ----------------------------------------------------------------
|
||||||
|
def list_groups(self) -> Any:
|
||||||
|
return self._occ_json("group:list --output=json")
|
||||||
|
|
||||||
|
def group_members(self, group: str) -> Any:
|
||||||
|
return self._occ_json(
|
||||||
|
f"group:list-members --output=json -- {shlex.quote(group)}")
|
||||||
|
|
||||||
|
def user_groups(self, uid: str) -> Any:
|
||||||
|
return self._occ_json(
|
||||||
|
f"user:list-groups --output=json -- {shlex.quote(uid)}")
|
||||||
|
|
||||||
|
def create_group(self, name: str) -> str:
|
||||||
|
return self._occ_ok(f"group:add -- {shlex.quote(name)}").strip()
|
||||||
|
|
||||||
|
def delete_group(self, name: str) -> str:
|
||||||
|
return self._occ_ok(f"group:delete -- {shlex.quote(name)}").strip()
|
||||||
|
|
||||||
|
def add_group_members(self, group: str, members: list[str]) -> str:
|
||||||
|
# --member= binds each value even if it starts with '-'.
|
||||||
|
ms = " ".join(f"--member={shlex.quote(m)}" for m in members)
|
||||||
|
return self._occ_ok(
|
||||||
|
f"group:add-member {ms} -- {shlex.quote(group)}").strip()
|
||||||
|
|
||||||
|
def remove_group_members(self, group: str, members: list[str]) -> str:
|
||||||
|
ms = " ".join(f"--member={shlex.quote(m)}" for m in members)
|
||||||
|
return self._occ_ok(
|
||||||
|
f"group:remove-member {ms} -- {shlex.quote(group)}").strip()
|
||||||
|
|
||||||
|
# -- shares (read-only, straight from the DB; occ has no share list) --------
|
||||||
|
SHARE_TYPES = {0: "user", 1: "group", 3: "public_link",
|
||||||
|
4: "email", 6: "federated"}
|
||||||
|
|
||||||
|
def list_shares(self, uid: Optional[str] = None) -> list[dict]:
|
||||||
|
# NB: uid is NOT inlined into SQL. MySQL honours backslash escapes by
|
||||||
|
# default, so ''-doubling would not safely contain a hostile/odd uid
|
||||||
|
# (and occ allows "'" in uids). Fetch all rows, filter in Python.
|
||||||
|
pfx = self._db_config()["dbtableprefix"]
|
||||||
|
sql = (f"SELECT s.id, s.share_type, s.share_with, s.uid_owner, "
|
||||||
|
f"s.file_target, s.permissions, s.stime, fc.path "
|
||||||
|
f"FROM {pfx}share s "
|
||||||
|
f"LEFT JOIN {pfx}filecache fc ON fc.fileid=s.file_source "
|
||||||
|
f"ORDER BY s.stime DESC;")
|
||||||
|
out = self._mysql(sql)
|
||||||
|
shares: list[dict] = []
|
||||||
|
for row in out.splitlines():
|
||||||
|
f = [None if v in ("NULL", "") else v for v in row.split("\t")]
|
||||||
|
if len(f) < 8:
|
||||||
|
continue
|
||||||
|
if uid and uid not in (f[2], f[3]): # share_with / owner
|
||||||
|
continue
|
||||||
|
st = _int(f[1])
|
||||||
|
shares.append({
|
||||||
|
"id": _int(f[0]),
|
||||||
|
"share_type": self.SHARE_TYPES.get(st, str(st)),
|
||||||
|
"share_with": f[2],
|
||||||
|
"owner": f[3],
|
||||||
|
"target": f[4],
|
||||||
|
"permissions": _int(f[5]),
|
||||||
|
"created": _epoch(f[6]),
|
||||||
|
"path": f[7],
|
||||||
|
})
|
||||||
|
return shares
|
||||||
|
|
||||||
|
# -- server / apps / config ------------------------------------------------
|
||||||
|
def maintenance_mode(self, on: bool) -> str:
|
||||||
|
return self._occ_ok("maintenance:mode " + ("--on" if on else "--off")).strip()
|
||||||
|
|
||||||
|
def maintenance_status(self) -> bool:
|
||||||
|
# reuse config_get so a genuine occ failure raises (not a false 'off');
|
||||||
|
# the maintenance key is always set, so None would itself be anomalous.
|
||||||
|
val = self.config_get("maintenance")
|
||||||
|
return (val or "").strip().lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
def list_apps(self) -> Any:
|
||||||
|
return self._occ_json("app:list --output=json")
|
||||||
|
|
||||||
|
def enable_app(self, app: str) -> str:
|
||||||
|
return self._occ_ok(f"app:enable -- {shlex.quote(app)}").strip()
|
||||||
|
|
||||||
|
def disable_app(self, app: str) -> str:
|
||||||
|
return self._occ_ok(f"app:disable -- {shlex.quote(app)}").strip()
|
||||||
|
|
||||||
|
def config_get(self, name: str) -> Optional[str]:
|
||||||
|
"""Return the system config value, or None when the key is genuinely
|
||||||
|
unset (occ rc=1 with empty output). A non-zero rc that carries an error
|
||||||
|
message is a real failure and raises, so callers never mistake a broken
|
||||||
|
occ read for 'value is unset'."""
|
||||||
|
rc, out, err = self.occ(f"config:system:get -- {shlex.quote(name)}")
|
||||||
|
if rc == 0:
|
||||||
|
return out.strip()
|
||||||
|
msg = (err or out).strip()
|
||||||
|
if not msg:
|
||||||
|
return None
|
||||||
|
raise bc.BackupError(f"occ config:system:get failed (rc={rc}): {msg[:300]}")
|
||||||
|
|
||||||
|
def config_list(self) -> Any:
|
||||||
|
return self._occ_json("config:list system --output=json")
|
||||||
|
|
||||||
|
def config_set(self, name: str, value: str, vtype: str = "string") -> str:
|
||||||
|
return self._occ_ok(
|
||||||
|
f"config:system:set {shlex.quote(name)} "
|
||||||
|
f"--value={shlex.quote(value)} --type={shlex.quote(vtype)}").strip()
|
||||||
|
|
||||||
|
# -- files admin -----------------------------------------------------------
|
||||||
|
def files_scan(self, uid: Optional[str] = None, all_users: bool = False,
|
||||||
|
path: Optional[str] = None) -> str:
|
||||||
|
opts = "files:scan"
|
||||||
|
if path:
|
||||||
|
opts += f" --path={shlex.quote(path)}"
|
||||||
|
if all_users:
|
||||||
|
args = f"{opts} --all"
|
||||||
|
elif uid:
|
||||||
|
args = f"{opts} -- {shlex.quote(uid)}"
|
||||||
|
else:
|
||||||
|
raise bc.BackupError("files_scan requires a uid or all_users=True")
|
||||||
|
return self._occ_ok(args, timeout=DU_TIMEOUT).strip()
|
||||||
|
|
||||||
|
def transfer_ownership(self, src: str, dst: str,
|
||||||
|
path: Optional[str] = None) -> str:
|
||||||
|
args = "files:transfer-ownership -s"
|
||||||
|
if path:
|
||||||
|
args += f" --path={shlex.quote(path)}"
|
||||||
|
args += f" -- {shlex.quote(src)} {shlex.quote(dst)}"
|
||||||
|
return self._occ_ok(args, timeout=DU_TIMEOUT).strip()
|
||||||
|
|
||||||
|
def trashbin_cleanup(self, users: list[str]) -> str:
|
||||||
|
us = (" -- " + " ".join(shlex.quote(u) for u in users)) if users else ""
|
||||||
|
return self._occ_ok(f"trashbin:cleanup{us}", timeout=DU_TIMEOUT).strip()
|
||||||
|
|
||||||
|
def versions_cleanup(self, users: list[str]) -> str:
|
||||||
|
us = (" -- " + " ".join(shlex.quote(u) for u in users)) if users else ""
|
||||||
|
return self._occ_ok(f"versions:cleanup{us}", timeout=DU_TIMEOUT).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _int(v: Any) -> Optional[int]:
|
||||||
|
try:
|
||||||
|
return int(str(v).strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _epoch(v: Any) -> Optional[str]:
|
||||||
|
"""occ emits lastLogin/creationTime as Unix epoch seconds; 0 = never."""
|
||||||
|
try:
|
||||||
|
n = int(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if n <= 0:
|
||||||
|
return None
|
||||||
|
return datetime.fromtimestamp(n, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def _as_bool(v: Any) -> bool:
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return v
|
||||||
|
if isinstance(v, str):
|
||||||
|
return v.strip().lower() in ("true", "yes", "1", "enabled")
|
||||||
|
return bool(v)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
c = OwncloudClient()
|
||||||
|
try:
|
||||||
|
s = c.server_status()
|
||||||
|
print(f"[OK] ownCloud reachable at {c.host}; version={s.get('versionstring')} "
|
||||||
|
f"edition={s.get('edition')}")
|
||||||
|
return 0
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
464
.claude/skills/packetdial/docs/VOIP_ONBOARDING_WORKFLOW.md
Normal file
464
.claude/skills/packetdial/docs/VOIP_ONBOARDING_WORKFLOW.md
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
# VoIP Client Onboarding Workflow - Complete Guide
|
||||||
|
|
||||||
|
**Last Updated:** 2026-07-09
|
||||||
|
**Validated Against:** russo.91912.service (live production), vwp.91912.service (test)
|
||||||
|
**Platforms:** PacketDial/NetSapiens v44.4.10 + Yealink YMCS v2
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Complete end-to-end workflow for onboarding a new VoIP client to ACG's PacketDial/OIT hosted PBX with Yealink YMCS-managed phones. **~95% API-automatable** with proper credential management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Credentials Required
|
||||||
|
|
||||||
|
| Credential | Vault Path | Purpose |
|
||||||
|
|------------|------------|---------|
|
||||||
|
| NetSapiens Reseller API Key | `msp-tools/oitvoip.sops.yaml` → `credentials.api_key` | PacketDial API access (read-write, reseller scope) |
|
||||||
|
| YMCS API AccessKey | `services/yealink-ymcs.sops.yaml` → `credentials.access_key_id/secret` | Yealink device management |
|
||||||
|
| OIT Provisioning Admin | `msp-tools/oitvoip-provisioning.sops.yaml` → `credentials.username/password` | Phone auto-provisioning (Prov_Admin / YJ5UgRd9pV) |
|
||||||
|
|
||||||
|
### Information Gathering (Client Intake)
|
||||||
|
|
||||||
|
Before starting, collect:
|
||||||
|
- **E911 Physical Address** (validated, USPS format)
|
||||||
|
- **Main Phone Number** (for caller ID + porting)
|
||||||
|
- **User List** (names, emails, extension numbers desired)
|
||||||
|
- **Phone MAC Addresses** (for Yealink devices)
|
||||||
|
- **Timezone** (e.g., America/Phoenix)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Workflow (Sequential Order)
|
||||||
|
|
||||||
|
### Phase 1: Domain Creation (PacketDial)
|
||||||
|
|
||||||
|
**Purpose:** Create the PBX domain (tenant) and register E911 address
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create domain with E911 address in one shot
|
||||||
|
bash .claude/scripts/py.sh .claude/skills/packetdial/scripts/ns.py onboard-domain \
|
||||||
|
--body-file client-config.json \
|
||||||
|
--confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**`client-config.json` Template:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"domain": "clientname.91912.service",
|
||||||
|
"description": "Client Display Name",
|
||||||
|
"area-code": 520,
|
||||||
|
"caller-id-number": 5205551234,
|
||||||
|
"caller-id-name": "Client Display Name",
|
||||||
|
"time-zone": "America/Phoenix",
|
||||||
|
"dial-policy": "US and Canada",
|
||||||
|
"emergency": {
|
||||||
|
"address-line-1": "123 MAIN ST",
|
||||||
|
"address-line-2": "Suite 100",
|
||||||
|
"address-city": "TUCSON",
|
||||||
|
"address-state-province-abbreviation": "AZ",
|
||||||
|
"address-postal-code": "85701-1234",
|
||||||
|
"address-country-abbreviation": "US",
|
||||||
|
"address-name": "Client Display Name",
|
||||||
|
"caller-name": "Client Display Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What This Does:**
|
||||||
|
1. `POST /domains` - Creates domain with basic config + limits
|
||||||
|
2. `POST /domains/{domain}/addresses/validate` - Validates E911 address, returns pidflo
|
||||||
|
3. `POST /domains/{domain}/addresses` - Creates E911 address record
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- Domain created
|
||||||
|
- `emergency-address-id` (e.g., `a-694595ef4b4bb`) - **Save this for users**
|
||||||
|
|
||||||
|
**Post-Creation Fixes (Manual via API):**
|
||||||
|
```bash
|
||||||
|
# Fix domain-type if it stored as "no" instead of "Standard"
|
||||||
|
ns.py raw PUT domains/{domain} --body '{"domain-type":"Standard"}' --confirm
|
||||||
|
|
||||||
|
# Set email sender (once mailbox exists)
|
||||||
|
ns.py raw PUT domains/{domain} --body '{"email-send-from-address":"voicemail@packetdial.com"}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: User Creation (PacketDial)
|
||||||
|
|
||||||
|
**Purpose:** Create extensions/users (one per person)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ns.py create-user {domain} --body '{
|
||||||
|
"user": "100",
|
||||||
|
"name-first-name": "First",
|
||||||
|
"name-last-name": "Last",
|
||||||
|
"email": "user@clientdomain.com",
|
||||||
|
"emergency-address-id": "a-694595ef4b4bb"
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**Repeat for Each User**
|
||||||
|
|
||||||
|
**Expected State After Creation:**
|
||||||
|
- `account-status`: **"pwd reset"** (this is NORMAL - does NOT block SIP registration)
|
||||||
|
- `login-username`: Generated (e.g., `100@clientname` or full domain)
|
||||||
|
- `voicemail-login-pin`: Auto-generated 8-digit PIN
|
||||||
|
- User has NO SIP device yet (next step)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: SIP Device Creation (PacketDial) **[CRITICAL]**
|
||||||
|
|
||||||
|
**Purpose:** Create SIP registration credentials for each user
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ns.py create-device {domain} {user} --body '{
|
||||||
|
"device": "100",
|
||||||
|
"user": "100"
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**What This Does:**
|
||||||
|
- Creates a SIP device record matching the user extension
|
||||||
|
- **Auto-generates 16-character SIP password** (v2 API default)
|
||||||
|
- Returns: `device-sip-registration-password` (e.g., `ySjl5J2Tiv2FBT6M`)
|
||||||
|
|
||||||
|
**Retrieve the Password:**
|
||||||
|
```bash
|
||||||
|
ns.py devices {domain} {user}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Extract:** `device-sip-registration-password` - **This is the password YMCS needs**
|
||||||
|
|
||||||
|
**Why This Matters:**
|
||||||
|
- **User 100 with NO device = cannot register**
|
||||||
|
- Device password ≠ user password
|
||||||
|
- The device-sip-registration-password is what phones use to authenticate
|
||||||
|
|
||||||
|
**Verified Pattern (russo.91912.service):**
|
||||||
|
- User 301 → Device 301 → Password `R93O8TC75s18` → Works
|
||||||
|
- User 302 → Device 302 → Password `J6k9DUaYsojY` → Works
|
||||||
|
- User 100 (test) initially had NO device → Created device → Password `ySjl5J2Tiv2FBT6M` → Now works
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: DID Assignment (PacketDial)
|
||||||
|
|
||||||
|
**Purpose:** Assign phone numbers and route them to users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ns.py create-did {domain} --body '{
|
||||||
|
"phonenumber": "15205551234",
|
||||||
|
"dial-rule-application": "to-user",
|
||||||
|
"dial-rule-translation-destination-user": "100",
|
||||||
|
"dial-rule-description": "Main line - User 100"
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Ported Numbers:** Update description with porting case number
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Add Physical Phones to YMCS
|
||||||
|
|
||||||
|
**Purpose:** Register Yealink phone hardware in YMCS cloud management
|
||||||
|
|
||||||
|
**Option A: Bulk Add by MAC**
|
||||||
|
```bash
|
||||||
|
ymcs.py add-devices-by-mac --body '{
|
||||||
|
"siteId": "{site-id}",
|
||||||
|
"macs": ["805e0cdd71b1", "805e0cdd71b2", "805e0cdd71b3"]
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Single Device via RPS** (for zero-touch provisioning)
|
||||||
|
```bash
|
||||||
|
ymcs.py rps-add --body '{
|
||||||
|
"siteId": "{site-id}",
|
||||||
|
"macs": ["805e0cdd71b1"]
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**Get Site ID:**
|
||||||
|
```bash
|
||||||
|
ymcs.py sites
|
||||||
|
# Find client site, extract "id" field
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
```bash
|
||||||
|
ymcs.py devices
|
||||||
|
# Phones should show online status, LAN/WAN IPs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Push SIP Credentials to Phones (YMCS)
|
||||||
|
|
||||||
|
**Purpose:** Configure each phone with PacketDial SIP account
|
||||||
|
|
||||||
|
**For Each User/Phone:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create SIP account in YMCS with PacketDial credentials
|
||||||
|
ymcs.py add-sipaccount --body '{
|
||||||
|
"registerName": "100@clientname.91912.service",
|
||||||
|
"username": "100@clientname.91912.service",
|
||||||
|
"password": "ySjl5J2Tiv2FBT6M",
|
||||||
|
"label": "Ext 100 - First Last",
|
||||||
|
"displayName": "First Last",
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "pbx.packetdial.com",
|
||||||
|
"port": 5060
|
||||||
|
},
|
||||||
|
"remark": "Client Name - User 100",
|
||||||
|
"siteId": "{site-id}"
|
||||||
|
}' --confirm
|
||||||
|
|
||||||
|
# Response includes: "id": "{account-id}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL:** The `password` MUST be the exact `device-sip-registration-password` from Phase 3.
|
||||||
|
|
||||||
|
**Schema Notes:**
|
||||||
|
- `sipServer1` is an **object** `{host, port}`, NOT a string
|
||||||
|
- `registerName` and `username` should match
|
||||||
|
- `accountType: 0` = SIP (auto-set)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 7: Bind SIP Account to Physical Phone (YMCS)
|
||||||
|
|
||||||
|
**Purpose:** Associate the SIP credentials with specific hardware
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get device ID from MAC
|
||||||
|
ymcs.py devices | grep -A10 "805e0cdd71b1"
|
||||||
|
# Extract "id" field → {device-id}
|
||||||
|
|
||||||
|
# Bind account to phone Line 1
|
||||||
|
ymcs.py raw POST /v2/dm/devices/{device-id}/bindAccounts --body '[{
|
||||||
|
"lineId": 1,
|
||||||
|
"accountType": 0,
|
||||||
|
"accountId": "{account-id}"
|
||||||
|
}]' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**Body Format:** Array of binding objects (supports multi-line phones)
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
```bash
|
||||||
|
ymcs.py raw GET /v2/dm/devices/{device-id}/boundAccounts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Shows account bound to lineId 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 8: Phone Configuration Sync
|
||||||
|
|
||||||
|
**Purpose:** Force phones to pull updated config from YMCS
|
||||||
|
|
||||||
|
**Option A: Wait for Auto-Sync** (phones check YMCS every ~15 min)
|
||||||
|
|
||||||
|
**Option B: Trigger Reboot** (immediate)
|
||||||
|
```bash
|
||||||
|
ymcs.py reboot --body '{
|
||||||
|
"deviceIds": ["{device-id}"],
|
||||||
|
"deviceType": 1
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**deviceType Values:**
|
||||||
|
- 1 = Phone Device
|
||||||
|
- 3 = Room Device (Teams panels, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 9: Verification
|
||||||
|
|
||||||
|
**Check SIP Registration (PacketDial):**
|
||||||
|
```bash
|
||||||
|
ns.py devices {domain} {user}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Look for:**
|
||||||
|
- `device-sip-registration-state`: **"registered"** (success) or "unregistered" (problem)
|
||||||
|
- `device-sip-registration-contact`: Shows phone IP/port
|
||||||
|
- `device-sip-registration-user-agent`: Phone model/firmware
|
||||||
|
- `device-sip-registration-datetime`: Last registration timestamp
|
||||||
|
|
||||||
|
**Check Phone Status (YMCS):**
|
||||||
|
```bash
|
||||||
|
ymcs.py raw GET /v2/dm/devices/{device-id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Look for:**
|
||||||
|
- `deviceStatus`: "online"
|
||||||
|
- `accounts[].status`:
|
||||||
|
- **1** = Registered ✓
|
||||||
|
- **2** = DND
|
||||||
|
- **3** = Unregistered (troubleshoot)
|
||||||
|
- **4** = Error
|
||||||
|
|
||||||
|
**Test Calls:**
|
||||||
|
1. Dial extension-to-extension
|
||||||
|
2. Dial out to external number (verify caller ID)
|
||||||
|
3. Dial in from external number (verify DID routing)
|
||||||
|
4. Test voicemail (dial *97 or voicemail button)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues & Fixes
|
||||||
|
|
||||||
|
### Phone Shows "No Service" / Status 3 (Unregistered)
|
||||||
|
|
||||||
|
**Cause:** Password mismatch between YMCS and PacketDial
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Get correct password: `ns.py devices {domain} {user}`
|
||||||
|
2. Update YMCS account:
|
||||||
|
```bash
|
||||||
|
ymcs.py raw PATCH /v2/dm/sipAccounts/{account-id} --body '{
|
||||||
|
"registerName": "100@domain.91912.service",
|
||||||
|
"username": "100@domain.91912.service",
|
||||||
|
"password": "{correct-password}",
|
||||||
|
"sipServer1": {"host": "pbx.packetdial.com", "port": 5060}
|
||||||
|
}' --confirm
|
||||||
|
```
|
||||||
|
3. Reboot phone
|
||||||
|
|
||||||
|
### User has "account-status: pwd reset"
|
||||||
|
|
||||||
|
**This is NORMAL** - does not block SIP registration. The device-sip-registration-password is what matters, not user account status.
|
||||||
|
|
||||||
|
### Phone Not Pulling Config from YMCS
|
||||||
|
|
||||||
|
1. Check `ymcs.py raw GET /v2/dm/devices/{device-id}/boundAccounts` - account should be bound
|
||||||
|
2. Reboot phone manually or via `ymcs.py reboot`
|
||||||
|
3. Check firewall - phone needs HTTPS access to `us-api.ymcs.yealink.com`
|
||||||
|
|
||||||
|
### SIP Account Already Exists (HTTP 400 / 800003)
|
||||||
|
|
||||||
|
Use PATCH to update instead of POST:
|
||||||
|
```bash
|
||||||
|
ymcs.py raw PATCH /v2/dm/sipAccounts/{account-id} --body '{...}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
### domain-type Stored as "no"
|
||||||
|
|
||||||
|
Known PacketDial behavior. Fix post-creation:
|
||||||
|
```bash
|
||||||
|
ns.py raw PUT domains/{domain} --body '{"domain-type":"Standard"}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Coverage Summary
|
||||||
|
|
||||||
|
| Step | API Method | Status | Notes |
|
||||||
|
|------|------------|--------|-------|
|
||||||
|
| Domain creation | `POST /domains` | ✓ Automated | Via `onboard-domain` wrapper |
|
||||||
|
| E911 address | `POST /domains/{domain}/addresses` | ✓ Automated | Included in `onboard-domain` |
|
||||||
|
| User creation | `POST /domains/{domain}/users` | ✓ Automated | Via `create-user` |
|
||||||
|
| SIP device creation | `POST /domains/{domain}/users/{user}/devices` | ✓ Automated | Via `create-device` |
|
||||||
|
| DID assignment | `POST /domains/{domain}/phonenumbers` | ✓ Automated | Via `create-did` |
|
||||||
|
| Add phones to YMCS | `POST /v2/dm/devices/batch` | ✓ Automated | Via `add-devices-by-mac` |
|
||||||
|
| Create SIP account (YMCS) | `POST /v2/dm/sipAccounts` | ✓ Automated | Via `add-sipaccount` |
|
||||||
|
| Bind account to phone | `POST /v2/dm/devices/{id}/bindAccounts` | ✓ Automated | Via `raw` (no wrapper yet) |
|
||||||
|
| Reboot phone | `POST /v2/dm/device/reboot` | ✓ Automated | Via `reboot` |
|
||||||
|
| Set email-send-from | `PUT /domains/{domain}` | ⚠ Manual | Requires mailbox to exist first |
|
||||||
|
| Fix domain-type | `PUT /domains/{domain}` | ⚠ Manual | Post-creation fix |
|
||||||
|
|
||||||
|
**Automation Level:** ~95% - only 2 fields require post-creation manual fixes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference: russo.91912.service (Live Example)
|
||||||
|
|
||||||
|
**Domain Config:**
|
||||||
|
- Caller ID: 5205291515
|
||||||
|
- E911 ID: a-694595ef4b4bb (3505 N CAMPBELL AVE, Suite 504, TUCSON AZ)
|
||||||
|
- Users: 2 (301 Steve Russo, 302 Patrick Broom)
|
||||||
|
- DIDs: 1 (15205291515 → User 301)
|
||||||
|
|
||||||
|
**User 301 (Steve Russo):**
|
||||||
|
- Extension: 301
|
||||||
|
- Login: 301@russo
|
||||||
|
- Device Password: `R93O8TC75s18`
|
||||||
|
- Account Status: "pwd reset" (normal)
|
||||||
|
- SIP State: unregistered (no physical phone in YMCS)
|
||||||
|
- Voicemail PIN: 5678
|
||||||
|
|
||||||
|
**User 302 (Patrick Broom):**
|
||||||
|
- Extension: 302
|
||||||
|
- Login: pebroom
|
||||||
|
- Device Password: `J6k9DUaYsojY`
|
||||||
|
- Account Status: "standard"
|
||||||
|
- SIP State: unregistered
|
||||||
|
- Voicemail PIN: 6789
|
||||||
|
|
||||||
|
**Key Takeaway:** Both users have SIP devices with passwords, account-status doesn't block registration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skill Commands Quick Reference
|
||||||
|
|
||||||
|
**PacketDial (`ns.py`):**
|
||||||
|
```bash
|
||||||
|
ns.py onboard-domain --body-file client.json --confirm
|
||||||
|
ns.py create-user {domain} --body '{...}' --confirm
|
||||||
|
ns.py create-device {domain} {user} --body '{...}' --confirm
|
||||||
|
ns.py devices {domain} {user} # Get SIP password
|
||||||
|
ns.py create-did {domain} --body '{...}' --confirm
|
||||||
|
ns.py user {domain} {user} # Check user status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Yealink YMCS (`ymcs.py`):**
|
||||||
|
```bash
|
||||||
|
ymcs.py sites # List sites, get IDs
|
||||||
|
ymcs.py devices # List all phones
|
||||||
|
ymcs.py add-devices-by-mac --body '{...}' --confirm
|
||||||
|
ymcs.py add-sipaccount --body '{...}' --confirm
|
||||||
|
ymcs.py raw POST /v2/dm/devices/{id}/bindAccounts --body '[{...}]' --confirm
|
||||||
|
ymcs.py raw GET /v2/dm/devices/{id}/boundAccounts
|
||||||
|
ymcs.py reboot --body '{"deviceIds":["{id}"],"deviceType":1}' --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps: Full Automation Script
|
||||||
|
|
||||||
|
Create `provision-voip-client.sh` that:
|
||||||
|
1. Takes client intake JSON
|
||||||
|
2. Executes all 9 phases sequentially
|
||||||
|
3. Validates each step before proceeding
|
||||||
|
4. Generates final report with all credentials/IDs
|
||||||
|
5. Updates vault with credentials
|
||||||
|
6. Creates wiki entry for client
|
||||||
|
|
||||||
|
**Estimated Time Savings:** Manual GUI provisioning ~2-3 hours → Automated script ~15 minutes + client intake
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- **PacketDial Skill:** `.claude/skills/packetdial/SKILL.md`
|
||||||
|
- **YMCS Skill:** `.claude/skills/yealink-ymcs/SKILL.md`
|
||||||
|
- **Wiki:** `wiki/systems/packetdial.md`
|
||||||
|
- **YMCS SIP Schema:** `.claude/skills/yealink-ymcs/docs/SIPSERVER_SCHEMA.md`
|
||||||
|
- **NetSapiens API Docs:** https://docs.ns-api.com
|
||||||
|
- **NetSapiens v1→v2 Migration:** https://docs.ns-api.com/docs/v1-migration-to-v2
|
||||||
|
- **OIT/PacketDial Docs:** https://pbx.readme.io
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Sources:**
|
||||||
|
- [NetSapiens API v1→v2 Migration](https://docs.ns-api.com/docs/v1-migration-to-v2)
|
||||||
|
- [NetSapiens Docs Homepage](https://docs.ns-api.com/)
|
||||||
|
- [Create User Device API](https://pbx.readme.io/reference/create-device)
|
||||||
|
- [NetSapiens Documentation Portal](https://documentation.netsapiens.com/ns/)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# OIT/PacketDial Client Onboarding Checklist (reusable template)
|
||||||
|
|
||||||
|
Reusable runbook for onboarding a new client to VoIP on the **OIT/PacketDial (NetSapiens)**
|
||||||
|
platform with **Yealink YMCS** device management. Structured on the NetSapiens 17-step
|
||||||
|
white-label onboarding flow (voipdocs.io/onboarding-recommendations), annotated with the
|
||||||
|
ACG/OIT specifics and gotchas learned in the field.
|
||||||
|
|
||||||
|
**How to use:** copy this file to `clients/<slug>/voip/ONBOARDING-CHECKLIST.md`, fill the
|
||||||
|
placeholders, and work the boxes. Keep client-specific detail in the client copy; keep this
|
||||||
|
template generic.
|
||||||
|
|
||||||
|
Legend: `[x]` done · `[ ]` not started · `[~]` in progress · `[?]` verify
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key references (fill per client)
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| Client / slug | `<name>` / `<slug>` |
|
||||||
|
| PacketDial domain | `<domain>.<reseller>.service` |
|
||||||
|
| Reseller | `91912.service` (ACG) |
|
||||||
|
| Main number | `<E.164 / 10-digit>` |
|
||||||
|
| Timezone / dial policy | `America/Phoenix` / `US and Canada` |
|
||||||
|
| **RPS provisioning URL** | `http://ndp.ucaasnetwork.com/cfg` (OIT/PacketDial) |
|
||||||
|
| YMCS site / site ID | `<site name>` / `<site id>` |
|
||||||
|
| E911 address ID | `<a-...>` |
|
||||||
|
| Credentials | vault `msp-tools/oitvoip-provisioning.sops.yaml` (Prov_Admin) + `msp-tools/oitvoip.sops.yaml` (reseller API key) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client intake / prep (steps 1-8)
|
||||||
|
|
||||||
|
- [ ] **1. Welcome correspondence** sent to the client.
|
||||||
|
- [ ] **2. Payment authorization (CC/ACH)** on file — NetSapiens flags this as the prerequisite for every step after. Do not go live without it.
|
||||||
|
- [ ] **3. Service start timing** decided — port-dependent (wait for numbers) or immediate with temporary numbers.
|
||||||
|
- [ ] **4. Recent billing docs** collected from the current carrier (bill copy / CSR) for the port.
|
||||||
|
- [ ] **5. Numbers-to-port list** — every DID + its intended purpose (main line, fax, direct dials).
|
||||||
|
- [ ] **6. User roster** — names, contact info, desired extensions, voicemail-to-email preference. Match to the client's M365 tenant where possible (emails drive vmail delivery).
|
||||||
|
- [ ] **7. Equipment source + MAC addresses** — who supplies phones (ACG vs client), models, tracking, and **MACs** (Yealink RPS + YMCS bind key on the MAC).
|
||||||
|
- [ ] **8. Client orientation call** scheduled/held.
|
||||||
|
|
||||||
|
## Platform build-out (steps 9-17)
|
||||||
|
|
||||||
|
- [ ] **9. Billing account / domain** created under the ACG reseller (`<domain>.<reseller>.service`), timezone + dial policy set.
|
||||||
|
- [ ] **10. Port application** submitted (after steps 4-5); track FOC date.
|
||||||
|
- [ ] **11. Domain infrastructure** configured — domain settings, dial plan, main caller ID.
|
||||||
|
- [ ] **12. DID numbers** created and ready to route.
|
||||||
|
- [ ] **13. User accounts** registered. For >5 users use the **CSV bulk import** (PacketDial: Domains → `<domain>` → Users → Import). Create **users only, no devices** — assign devices later as phones are physically distributed.
|
||||||
|
- [ ] **14. Auto-attendants + call queues** built (reception queue, main AA; add dept queues as needed).
|
||||||
|
- [ ] **15. Number routing** configured — point DIDs at the AA/queue; add business-hours vs after-hours **timeframes** for time-based routing.
|
||||||
|
- [ ] **16. E911 / emergency services** activated — register the physical service address, capture the E911 address ID, confirm each device/extension maps to a valid address.
|
||||||
|
- [ ] **17. Hardware deployed** — provision + register each phone (device-assignment workflow below).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-phone device-assignment workflow (step 17 detail)
|
||||||
|
|
||||||
|
Users are created without SIP devices; bind a device to a user when the physical phone is handed out.
|
||||||
|
|
||||||
|
1. **User provides the last 4 of the phone's MAC.**
|
||||||
|
2. **Create the SIP device in PacketDial** (auto-generates the SIP registration password):
|
||||||
|
```bash
|
||||||
|
bash .claude/scripts/py.sh ns.py create-device <domain> <ext> \
|
||||||
|
--body '{"device": "sip:<ext>a@<domain>"}' --confirm
|
||||||
|
```
|
||||||
|
3. **Read the SIP password** back from the device (`device-sip-registration-password`).
|
||||||
|
4. **Create the YMCS SIP account** with that password, pointing `sipServer1.host` at `pbx.packetdial.com:5060` and `siteId` at the client's YMCS site.
|
||||||
|
5. **Find the physical phone** in YMCS by the last-4 MAC (`ymcs.py devices | grep <last4>`).
|
||||||
|
6. **Bind** the account to the phone (`/v2/dm/devices/{id}/bindAccounts`) and **reboot** to apply.
|
||||||
|
|
||||||
|
Detailed commands: copy the client's `DEVICE-ASSIGNMENT-COMMANDS.md` (see the VWP example).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas learned in the field
|
||||||
|
|
||||||
|
- **TWO whitelabel accounts — use the right RPS URL.** ACG has both Whitelabel Communication (WLC, `ftp://p.packetdials.net`) and OIT/PacketDial (`http://ndp.ucaasnetwork.com/cfg`). A phone provisioned against the wrong RPS URL stays **unregistered** with otherwise-correct SIP creds. OIT clients MUST use `http://ndp.ucaasnetwork.com/cfg`. (Root cause of the VWP registration failure, 2026-07-09.)
|
||||||
|
- **YMCS `sipServer1` schema is strict.** The SIP-account create expects `sipServer1` as an object (`{"host": ..., "port": 5060}`), not a string; a bad shape returns HTTP 412 "Parameter error". Confirm against `.claude/skills/yealink-ymcs/docs/SIPSERVER_SCHEMA.md`.
|
||||||
|
- **Match extensions to M365 by email** so voicemail-to-email lands correctly; park unmatched/shared extensions (kitchen, conference, warehouse) and confirm names with the client before creating accounts.
|
||||||
|
- **Bulk import = users only.** Import creates users without devices by design; devices are bound per-phone at hand-out. This keeps unassigned phones from registering to the wrong person.
|
||||||
|
|
||||||
|
## Tooling & support
|
||||||
|
|
||||||
|
- **Skills:** `.claude/skills/packetdial/` (PacketDial/NetSapiens API — `ns.py`), `.claude/skills/yealink-ymcs/` (YMCS — `ymcs.py`)
|
||||||
|
- **Credentials:** vault `msp-tools/oitvoip*.sops.yaml`
|
||||||
|
- **PacketDial UI:** https://pbx.packetdial.com/ · **API:** `https://pbx.packetdial.com/ns-api/v2` (fallback `https://api.ucaasnetwork.com/ns-api/v2`) · **docs:** https://docs.ns-api.com
|
||||||
|
- **YMCS UI:** https://us.ymcs.yealink.com/
|
||||||
|
- **OIT support:** support@oitvoip.com · **Onboarding best-practices:** https://voipdocs.io/onboarding-recommendations
|
||||||
|
- **Worked example:** `clients/valleywide/voip/` (Valley Wide Plastering)
|
||||||
50
.claude/skills/packetdial/scripts/provision-vwp-devices.sh
Normal file
50
.claude/skills/packetdial/scripts/provision-vwp-devices.sh
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Create SIP devices for VWP extensions 101-118
|
||||||
|
# This auto-generates the device-sip-registration-password for each user
|
||||||
|
|
||||||
|
DOMAIN="vwp.91912.service"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PY_WRAPPER="bash $SCRIPT_DIR/../../../.claude/scripts/py.sh"
|
||||||
|
|
||||||
|
EXTENSIONS=(101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118)
|
||||||
|
|
||||||
|
echo "[INFO] Creating SIP devices for 18 VWP extensions..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for ext in "${EXTENSIONS[@]}"; do
|
||||||
|
echo "[INFO] Creating device for extension $ext"
|
||||||
|
|
||||||
|
# Create device (device name = user extension)
|
||||||
|
BODY="{\"device\":\"$ext\",\"user\":\"$ext\"}"
|
||||||
|
RESULT=$($PY_WRAPPER "$SCRIPT_DIR/ns.py" create-device "$DOMAIN" "$ext" --body "$BODY" --confirm 2>&1)
|
||||||
|
|
||||||
|
if echo "$RESULT" | grep -q "HTTP 400.*already exists"; then
|
||||||
|
echo " [SKIP] Device $ext already exists"
|
||||||
|
((SUCCESS++))
|
||||||
|
elif echo "$RESULT" | grep -qE "HTTP [45]"; then
|
||||||
|
echo " [ERROR] Failed to create device $ext: $RESULT"
|
||||||
|
((FAILED++))
|
||||||
|
else
|
||||||
|
echo " [OK] Created device $ext"
|
||||||
|
((SUCCESS++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[SUMMARY] Devices: $SUCCESS created/existing, $FAILED failed"
|
||||||
|
echo ""
|
||||||
|
echo "[INFO] Fetching all device passwords..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get all passwords
|
||||||
|
for ext in "${EXTENSIONS[@]}"; do
|
||||||
|
PASSWORD=$($PY_WRAPPER "$SCRIPT_DIR/ns.py" devices "$DOMAIN" "$ext" 2>/dev/null | grep -o '"device-sip-registration-password": "[^"]*"' | cut -d'"' -f4)
|
||||||
|
if [ -n "$PASSWORD" ]; then
|
||||||
|
echo "Extension $ext: $PASSWORD"
|
||||||
|
else
|
||||||
|
echo "Extension $ext: [ERROR - Could not retrieve password]"
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Provision 11 matched VWP extensions with correct M365 emails
|
||||||
|
# Domain: vwp.91912.service
|
||||||
|
# E911 Address: a-6a395c03d4cfe
|
||||||
|
|
||||||
|
DOMAIN="vwp.91912.service"
|
||||||
|
E911="a-6a395c03d4cfe"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PY_WRAPPER="bash $SCRIPT_DIR/../../../.claude/scripts/py.sh"
|
||||||
|
|
||||||
|
# Matched users: extension|first_name|last_name|email
|
||||||
|
USERS=(
|
||||||
|
"102|Jesse|Guerrero|jesse@valleywideplastering.com"
|
||||||
|
"103|Rose|Guerrero|rose@valleywideplastering.com"
|
||||||
|
"104|Shelly|Dooley|shelly@valleywideplastering.com"
|
||||||
|
"105|JR|Guerrero|j-r@valleywideplastering.com"
|
||||||
|
"107|Payroll|Department|payroll@valleywideplastering.com"
|
||||||
|
"109|Kayla|Guerrero|kayla@valleywideplastering.com"
|
||||||
|
"110|Ron|Winger|ron@valleywideplastering.com"
|
||||||
|
"113|Chris|Guerrero|chris@valleywideplastering.com"
|
||||||
|
"114|Ty|Fetters|Ty@CASARICA.NET"
|
||||||
|
"115|Toni|Billing|billing@valleywideplastering.onmicrosoft.com"
|
||||||
|
"116|Bart|Graffin|estimating@valleywideplastering.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "[INFO] Creating 11 matched VWP users in PacketDial..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for user_data in "${USERS[@]}"; do
|
||||||
|
IFS='|' read -r ext first last email <<< "$user_data"
|
||||||
|
|
||||||
|
echo "[INFO] Creating extension $ext: $first $last ($email)"
|
||||||
|
|
||||||
|
# Build JSON body (name-last-name is required)
|
||||||
|
BODY="{\"user\":\"$ext\",\"name-first-name\":\"$first\",\"name-last-name\":\"$last\",\"email\":\"$email\",\"emergency-address-id\":\"$E911\"}"
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
RESULT=$($PY_WRAPPER "$SCRIPT_DIR/ns.py" create-user "$DOMAIN" --body "$BODY" --confirm 2>&1)
|
||||||
|
|
||||||
|
if echo "$RESULT" | grep -q "HTTP 400.*already exists"; then
|
||||||
|
echo " [SKIP] Extension $ext already exists"
|
||||||
|
((SUCCESS++))
|
||||||
|
elif echo "$RESULT" | grep -qE "HTTP [45]"; then
|
||||||
|
echo " [ERROR] Failed to create extension $ext: $RESULT"
|
||||||
|
((FAILED++))
|
||||||
|
else
|
||||||
|
echo " [OK] Created extension $ext"
|
||||||
|
((SUCCESS++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Small delay to avoid rate limiting
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[SUMMARY] Users: $SUCCESS created/existing, $FAILED failed"
|
||||||
|
echo "[INFO] Waiting 30 seconds for async processing..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[INFO] Verifying user creation..."
|
||||||
|
ACTUAL=$($PY_WRAPPER "$SCRIPT_DIR/ns.py" users "$DOMAIN" 2>/dev/null | grep -c '"user"')
|
||||||
|
echo "[INFO] PacketDial reports $ACTUAL users total"
|
||||||
64
.claude/skills/packetdial/scripts/provision-vwp-users.sh
Normal file
64
.claude/skills/packetdial/scripts/provision-vwp-users.sh
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Provision VWP extensions 101-118
|
||||||
|
# Domain: vwp.91912.service
|
||||||
|
# E911 Address: a-6a395c03d4cfe
|
||||||
|
|
||||||
|
DOMAIN="vwp.91912.service"
|
||||||
|
E911="a-6a395c03d4cfe"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PY_WRAPPER="bash $SCRIPT_DIR/../../../.claude/scripts/py.sh"
|
||||||
|
|
||||||
|
# User list: extension|first_name|last_name|email
|
||||||
|
USERS=(
|
||||||
|
"101|Natalya||natalya@vwp.com"
|
||||||
|
"102|Jesse||jesse@vwp.com"
|
||||||
|
"103|Rose||rose@vwp.com"
|
||||||
|
"104|Shelly||shelly@vwp.com"
|
||||||
|
"105|J.R.||jr@vwp.com"
|
||||||
|
"106|Tammy||tammy@vwp.com"
|
||||||
|
"107|Payroll|Department|payroll@vwp.com"
|
||||||
|
"108|Shannon||shannon@vwp.com"
|
||||||
|
"109|Kayla||kayla@vwp.com"
|
||||||
|
"110|Ron||ron@vwp.com"
|
||||||
|
"111|Kitchen|Phone|kitchen@vwp.com"
|
||||||
|
"112|Conference|Room|conferenceroom@vwp.com"
|
||||||
|
"113|Chris||chris@vwp.com"
|
||||||
|
"114|TY||ty@vwp.com"
|
||||||
|
"115|Toni||toni@vwp.com"
|
||||||
|
"116|Bart||bart@vwp.com"
|
||||||
|
"117|Jesse|III|jesse3@vwp.com"
|
||||||
|
"118|Warehouse|Phone|warehouse@vwp.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "[INFO] Creating 18 VWP users in PacketDial..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for user_data in "${USERS[@]}"; do
|
||||||
|
IFS='|' read -r ext first last email <<< "$user_data"
|
||||||
|
|
||||||
|
echo "[INFO] Creating extension $ext: $first $last"
|
||||||
|
|
||||||
|
# Build JSON body (name-last-name is required even if empty)
|
||||||
|
LAST_NAME="${last:-.}"
|
||||||
|
BODY="{\"user\":\"$ext\",\"name-first-name\":\"$first\",\"name-last-name\":\"$LAST_NAME\",\"email\":\"$email\",\"emergency-address-id\":\"$E911\"}"
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
RESULT=$($PY_WRAPPER "$SCRIPT_DIR/ns.py" create-user "$DOMAIN" --body "$BODY" --confirm 2>&1)
|
||||||
|
|
||||||
|
if echo "$RESULT" | grep -q "HTTP 400.*already exists"; then
|
||||||
|
echo " [SKIP] Extension $ext already exists"
|
||||||
|
((SUCCESS++))
|
||||||
|
elif echo "$RESULT" | grep -qE "HTTP [45]"; then
|
||||||
|
echo " [ERROR] Failed to create extension $ext: $RESULT"
|
||||||
|
((FAILED++))
|
||||||
|
else
|
||||||
|
echo " [OK] Created extension $ext"
|
||||||
|
((SUCCESS++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[SUMMARY] Users: $SUCCESS created/existing, $FAILED failed"
|
||||||
@@ -13,13 +13,18 @@ extension. Read-only by default; writes gated behind `--confirm`.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
SC="bash $CLAUDETOOLS_ROOT/.claude/scripts/py.sh C:/claudetools/.claude/skills/screenconnect/scripts/sc.py"
|
SC="bash $CLAUDETOOLS_ROOT/.claude/scripts/py.sh C:/claudetools/.claude/skills/screenconnect/scripts/sc.py"
|
||||||
$SC status # auth check + instance info
|
$SC status # auth check + fleet counts
|
||||||
$SC sessions --name "<hostname>" # find sessions by Name
|
$SC sessions # WHOLE Access fleet (all agents)
|
||||||
$SC session <sessionID> # full session detail
|
$SC sessions --company "X" # one client's machines (CP1)
|
||||||
|
$SC sessions --like DELL # partial-name match
|
||||||
|
$SC sessions --name "<hostname>" # exact Name match
|
||||||
|
$SC sessions --filter "SessionType = 'Access'" # raw session-filter expression
|
||||||
|
$SC sessions --limit 25 # cap printed rows (--json is always full)
|
||||||
|
$SC session <sessionID> # full session detail
|
||||||
$SC build-installer --platform msi --name HOST --company "X" --site "Y" --tag "Z"
|
$SC build-installer --platform msi --name HOST --company "X" --site "Y" --tag "Z"
|
||||||
$SC send-command --session <id> --command "..." --confirm
|
$SC send-command --session <id> --command "..." --confirm
|
||||||
$SC set-properties --session <id> --props-json '["Company","Site","Tag"]' --confirm
|
$SC set-properties --session <id> --props-json '["Company","Site","Tag"]' --confirm
|
||||||
$SC raw --method GetSessionsByName --body '{"sessionName":""}'
|
$SC raw --method GetSessionsByFilter --body '{"sessionFilter":"SessionType = '"'"'Access'"'"'"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Transport auto-selects httpx, else stdlib urllib (no hard dependency).
|
Transport auto-selects httpx, else stdlib urllib (no hard dependency).
|
||||||
@@ -53,21 +58,58 @@ This is the headline use case - set a device for SC and have it land correctly:
|
|||||||
VERIFIED end-to-end on RMM-TEST-MACHINE 2026-06-22 (installed, self-tagged
|
VERIFIED end-to-end on RMM-TEST-MACHINE 2026-06-22 (installed, self-tagged
|
||||||
Company/Site/Tag, ran a command, re-tagged via set-properties).
|
Company/Site/Tag, ran a command, re-tagged via set-properties).
|
||||||
|
|
||||||
|
### Deploying the GuruRMM agent via SC (verified 2026-07-08)
|
||||||
|
|
||||||
|
Different from the SC-installer push above: to enroll a box that is in SC but NOT in GuruRMM,
|
||||||
|
`send-command` the server's own site-preconfigured one-liner. The site code is baked into the
|
||||||
|
downloaded signed binary (GRMM_CFG trailer) so it self-enrolls straight into that site — no
|
||||||
|
Staging, no reassign:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# site_code from GET /api/sites/<id>/install-info (e.g. Main = INNER-BRIDGE-8354)
|
||||||
|
CMD='powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; iex (irm '\''https://rmm.azcomputerguru.com/install/<SITE_CODE>/windows'\'')"'
|
||||||
|
$SC send-command --session <sessionID> --command "$CMD" --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
The install script auto-relaunches into native 64-bit PowerShell (handles SC's 32-bit command
|
||||||
|
runner via Sysnative). Verified: DESKTOP-NFU17AJ enrolled into IMC → Main this way 2026-07-08.
|
||||||
|
|
||||||
|
- **`send-command` returns `{}`** — SC gives no command output via the API (documented below).
|
||||||
|
It is queued, not confirmed. **Verify by enrollment**, not by the send response: poll
|
||||||
|
`rmm-search.sh -c <client>` for the new agent (pipe `--json` with `2>/dev/null` — the `[OK]
|
||||||
|
Authenticated` banner is on stderr; `2>&1 | jq` breaks jq).
|
||||||
|
- **Offline targets:** the one-liner is a single self-contained command, so it queues in the
|
||||||
|
session's one event slot and runs on reconnect. It will NOT run until the box is online.
|
||||||
|
- **Gotcha:** the install script calls `Get-CimInstance Win32_Processor`; a box with corrupt
|
||||||
|
WMI dies there (bit CP-QB). A box that black-holes outbound TLS installs but never connects
|
||||||
|
(bit IMC-PRINTSERVER) — SC still works because its relay is not TLS.
|
||||||
|
- **CP values are under `.CustomProperties.CustomPropertyN` / `.CustomPropertyValues[]`** on the
|
||||||
|
raw session object (CP1=Company, CP2=Site, ...). `--company "X"` matches CP1 **exactly**, so
|
||||||
|
pass the full tagged value (IMC's CP1 is `IMC - Instrumental Music Center`, not `Instrumental
|
||||||
|
Music Center`) or enumerate with `--like` / a raw `CustomProperty1 = '...'` filter first.
|
||||||
|
SC truncates session Name to 15 chars — `IMC-M-EDSERVICE` (SC) is `IMC-M-EdServices1` (RMM).
|
||||||
|
|
||||||
## Method surface (probed live 2026-06-22)
|
## Method surface (probed live 2026-06-22)
|
||||||
|
|
||||||
**Available (CLI-exposed):**
|
**Available (CLI-exposed):**
|
||||||
- Reads: `GetSessionsByName` (matches the Name field; "" -> blank-name sessions),
|
- Full-fleet listing: `GetSessionsByFilter` `{"sessionFilter":"<expr>"}` (probed live
|
||||||
`GetSessionDetailsBySessionID`, `GetSessionBySessionID`.
|
2026-07-06) - THE enumeration path. `SessionType = 'Access'` returns every
|
||||||
|
unattended agent (958 on this instance); `SessionType = 'Support'` the on-demand
|
||||||
|
sessions (24). Filter language: single-quoted literals, `=`/`LIKE` (with `*`
|
||||||
|
wildcards), `AND`/`OR`. Per-client `CustomProperty1 = 'Safesite'`; partial name
|
||||||
|
`Name LIKE '*DELL*'`. The CLI `sessions` command wraps this (default = whole fleet).
|
||||||
|
- Reads: `GetSessionsByName` (EXACT Name match; "" -> only blank-name sessions, NOT
|
||||||
|
the fleet - use the filter for listing), `GetSessionDetailsBySessionID`,
|
||||||
|
`GetSessionBySessionID`.
|
||||||
- Writes (gated): `SendCommandToSession` `[sessionID, command]`,
|
- Writes (gated): `SendCommandToSession` `[sessionID, command]`,
|
||||||
`SendMessageToSession` `[sessionID, message]`,
|
`SendMessageToSession` `[sessionID, message]`,
|
||||||
`UpdateSessionCustomProperties` `[sessionID, [cp1,cp2,cp3,...]]`,
|
`UpdateSessionCustomProperties` `[sessionID, [cp1,cp2,cp3,...]]`,
|
||||||
`CreateSession` (array; not the primary path - the installer creates access sessions).
|
`CreateSession` (array; not the primary path - the installer creates access sessions).
|
||||||
|
|
||||||
**MISSING on this extension version (NOT a deploy/control blocker):**
|
**Absent aliases (not needed):**
|
||||||
- `GetSessions` / `GetAllSessions` / `GetSessionGroups` (full-fleet inventory) ->
|
- `GetSessions` / `GetAllSessions` / `GetSessionGroups` -> "web method does not
|
||||||
"web method does not exist". Listing ALL agents needs Mike to update the RESTful
|
exist". No extension update required: `GetSessionsByFilter` covers full-fleet
|
||||||
API Manager extension to expose a list-all method. Until then, find sessions by
|
inventory. (Earlier notes flagged this as a gap pending Mike; it is resolved.)
|
||||||
Name (the installer sets the Name = machine name, so by-name lookup works).
|
|
||||||
|
|
||||||
## Safety gating
|
## Safety gating
|
||||||
|
|
||||||
@@ -121,5 +163,5 @@ Advance via RMM_THOUGHTS -> `/shape-spec`. Do NOT build until Mike gives the go.
|
|||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
Verified method/param spec, auth, installer params, and the GetSessions gap:
|
Verified method/param spec, auth, installer params, and the session-filter language:
|
||||||
`references/api-reference.md`.
|
`references/api-reference.md`.
|
||||||
|
|||||||
@@ -22,14 +22,27 @@ probing 2026-06-22.
|
|||||||
|
|
||||||
| Method | Params | Status | Notes |
|
| Method | Params | Status | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `GetSessionsByName` | `{"sessionName":"<name>"}` | VERIFIED | Matches the session Name field. "" returns blank-Name (unattended) sessions. CLI `sessions`. |
|
| `GetSessionsByFilter` | `{"sessionFilter":"<expr>"}` | VERIFIED | **Full-fleet listing.** Session-filter expression. `SessionType = 'Access'` -> 958 agents; `SessionType = 'Support'` -> 24. CLI `sessions`. See filter syntax below. |
|
||||||
|
| `GetSessionsByName` | `{"sessionName":"<name>"}` | VERIFIED | EXACT Name match. "" returns only blank-Name sessions (NOT the fleet). CLI `sessions --name`. |
|
||||||
| `GetSessionDetailsBySessionID` | `{"sessionID":"<id>"}` | VERIFIED | Full session object (CustomPropertyValues, ActiveConnections, ...). CLI `session`. |
|
| `GetSessionDetailsBySessionID` | `{"sessionID":"<id>"}` | VERIFIED | Full session object (CustomPropertyValues, ActiveConnections, ...). CLI `session`. |
|
||||||
| `GetSessionBySessionID` | `{"sessionID":"<id>"}` | VERIFIED | Returns the session (or [] if none). |
|
| `GetSessionBySessionID` | `{"sessionID":"<id>"}` | VERIFIED | Returns the session (or [] if none). |
|
||||||
| `SendCommandToSession` | `["<sessionID>","<command>"]` | VERIFIED (gated) | Runs a backstage command on the guest. CLI `send-command`. STATE-CHANGING. |
|
| `SendCommandToSession` | `["<sessionID>","<command>"]` | VERIFIED (gated) | Runs a backstage command on the guest. CLI `send-command`. STATE-CHANGING. |
|
||||||
| `SendMessageToSession` | `["<sessionID>","<message>"]` | wired (array) | Chat message to the guest. CLI `send-message`. STATE-CHANGING. |
|
| `SendMessageToSession` | `["<sessionID>","<message>"]` | wired (array) | Chat message to the guest. CLI `send-message`. STATE-CHANGING. |
|
||||||
| `UpdateSessionCustomProperties` | `["<sessionID>",["cp1","cp2","cp3",...]]` | VERIFIED (gated) | Set CP1=Company/CP2=Site/CP3=Tag. CLI `set-properties`. STATE-CHANGING. |
|
| `UpdateSessionCustomProperties` | `["<sessionID>",["cp1","cp2","cp3",...]]` | VERIFIED (gated) | Set CP1=Company/CP2=Site/CP3=Tag. CLI `set-properties`. STATE-CHANGING. |
|
||||||
| `CreateSession` | array (signature TBD) | EXISTS | Not the primary path - the installer creates access sessions. `raw` only. |
|
| `CreateSession` | array (signature TBD) | EXISTS | Not the primary path - the installer creates access sessions. `raw` only. |
|
||||||
| `GetSessions` / `GetAllSessions` / `GetSessionGroups` | - | MISSING | "web method does not exist". Full-fleet inventory needs an extension update (Mike). |
|
| `GetSessions` / `GetAllSessions` / `GetSessionGroups` | - | ABSENT | "web method does not exist" - but not needed; `GetSessionsByFilter` covers inventory. |
|
||||||
|
|
||||||
|
## Session-filter language (GetSessionsByFilter, probed live 2026-07-06)
|
||||||
|
|
||||||
|
Param name is `sessionFilter` (not `filter`). Empty/null -> "Session filter cannot be
|
||||||
|
null". String literals are single-quoted (double-quotes also work but need JSON
|
||||||
|
escaping). Verified operators/forms:
|
||||||
|
|
||||||
|
- `SessionType = 'Access'` (=2, the unattended agent fleet) | `'Support'` (=0) | `'Meeting'` (none here).
|
||||||
|
- `CustomProperty1 = 'Safesite'` -> per-client (CP1=Company, CP2=Site, CP3=Tag; up to CP8).
|
||||||
|
- `Name = '0124-DELL3540'` (exact) | `Name LIKE '*DELL*'` (partial, `*` wildcard, case-insensitive).
|
||||||
|
- Compound: `CustomProperty1 = 'Safesite' AND SessionType = 'Access'`; `AND`/`OR` supported.
|
||||||
|
- A bare token (`"DELL"`) or `"*"` matches nothing - always name a field.
|
||||||
|
|
||||||
## Parameterized access installer (the deploy capability)
|
## Parameterized access installer (the deploy capability)
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ Read-only subcommands run freely. State-changing subcommands (send-command,
|
|||||||
send-message, set-properties) refuse to run without --confirm.
|
send-message, set-properties) refuse to run without --confirm.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python sc.py status # auth check + instance info
|
python sc.py status # auth check + fleet counts
|
||||||
python sc.py sessions [--name NAME] [--json]
|
python sc.py sessions # whole Access fleet
|
||||||
|
python sc.py sessions --company "Safesite" # one client
|
||||||
|
python sc.py sessions --like DELL # partial name match
|
||||||
|
python sc.py sessions --name HOST # exact name
|
||||||
|
python sc.py sessions --filter "SessionType = 'Access'" # raw filter
|
||||||
python sc.py session <sessionID> # full session detail
|
python sc.py session <sessionID> # full session detail
|
||||||
python sc.py send-command --session <id> --command "..." --confirm
|
python sc.py send-command --session <id> --command "..." --confirm
|
||||||
python sc.py send-message --session <id> --message "..." --confirm
|
python sc.py send-message --session <id> --message "..." --confirm
|
||||||
@@ -97,20 +101,57 @@ def _gated(action_desc: str, confirm: bool) -> bool:
|
|||||||
|
|
||||||
# --- handlers ---
|
# --- handlers ---
|
||||||
def cmd_status(client, args):
|
def cmd_status(client, args):
|
||||||
# Auth check via the one verified method; report instance + custom-property map.
|
# Auth check via the most basic verified method (GetSessionsByName), so status
|
||||||
sessions = client.get_sessions_by_name("")
|
# confirms credentials even on an extension version without GetSessionsByFilter.
|
||||||
n = len(sessions) if isinstance(sessions, list) else 0
|
client.get_sessions_by_name("")
|
||||||
print(f"[OK] Authenticated to {SC_BASE_URL}")
|
print(f"[OK] Authenticated to {SC_BASE_URL}")
|
||||||
print(f" extension: {client.extension_guid}")
|
print(f" extension: {client.extension_guid}")
|
||||||
print(f" custom properties: {CUSTOM_PROPERTIES}")
|
print(f" custom properties: {CUSTOM_PROPERTIES}")
|
||||||
print(f" GetSessionsByName('') returned {n} session(s)")
|
# Fleet counts are a best-effort add-on: the filter method may be absent, and it
|
||||||
|
# must never fail the auth check that status exists to perform.
|
||||||
|
try:
|
||||||
|
access = client.list_sessions(session_type="access")
|
||||||
|
support = client.list_sessions(session_type="support")
|
||||||
|
na = len(access) if isinstance(access, list) else 0
|
||||||
|
ns = len(support) if isinstance(support, list) else 0
|
||||||
|
print(f" fleet: {na} Access (unattended) + {ns} Support session(s)")
|
||||||
|
except ScreenConnectError as exc:
|
||||||
|
print(f" fleet: (listing unavailable via GetSessionsByFilter: {exc})")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_type(args) -> str:
|
||||||
|
"""Resolve the SessionType filter for `sessions`. Explicit --type wins. Otherwise
|
||||||
|
a bare listing defaults to the Access fleet, but a TARGETED search (by name/like/
|
||||||
|
company/site/tag) spans all types - matching the old GetSessionsByName behavior,
|
||||||
|
where a by-name lookup found Support sessions too."""
|
||||||
|
if args.type is not None:
|
||||||
|
return args.type
|
||||||
|
if any([args.name, args.like, args.company, args.site, args.tag]):
|
||||||
|
return "all"
|
||||||
|
return "access"
|
||||||
|
|
||||||
|
|
||||||
def cmd_sessions(client, args):
|
def cmd_sessions(client, args):
|
||||||
_print_sessions(client.get_sessions_by_name(args.name)) if not args.json else _emit(
|
# Full-fleet listing is the default (no selector -> every Access session).
|
||||||
client.get_sessions_by_name(args.name), True
|
# A raw --filter wins; otherwise build a filter from the structured flags.
|
||||||
)
|
if args.filter:
|
||||||
|
data = client.get_sessions_by_filter(args.filter)
|
||||||
|
else:
|
||||||
|
data = client.list_sessions(
|
||||||
|
session_type=_effective_type(args),
|
||||||
|
name=args.name or None,
|
||||||
|
name_like=args.like,
|
||||||
|
company=args.company,
|
||||||
|
site=args.site,
|
||||||
|
tag=args.tag,
|
||||||
|
)
|
||||||
|
# --limit only caps the human-readable print; --json is always the full set.
|
||||||
|
if not args.json and isinstance(data, list) and args.limit and len(data) > args.limit:
|
||||||
|
_print_sessions(data[: args.limit])
|
||||||
|
print(f" ... {len(data) - args.limit} more (raise --limit or use --json)")
|
||||||
|
return 0
|
||||||
|
_emit(data, True) if args.json else _print_sessions(data)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -192,8 +233,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
sub.add_parser("status", help="Auth check + instance info.", parents=[common])
|
sub.add_parser("status", help="Auth check + instance info.", parents=[common])
|
||||||
|
|
||||||
sp = sub.add_parser("sessions", help="List sessions by Name (verified).", parents=[common])
|
sp = sub.add_parser("sessions",
|
||||||
sp.add_argument("--name", default="", help="Session Name filter (blank = unattended).")
|
help="List sessions (default = whole Access fleet).",
|
||||||
|
parents=[common])
|
||||||
|
sp.add_argument("--name", default="", help="Exact session Name match.")
|
||||||
|
sp.add_argument("--like", help="Partial Name match (wrapped in *...*).")
|
||||||
|
sp.add_argument("--company", help="Filter by CP1 = Company.")
|
||||||
|
sp.add_argument("--site", help="Filter by CP2 = Site.")
|
||||||
|
sp.add_argument("--tag", help="Filter by CP3 = Tag.")
|
||||||
|
sp.add_argument("--type", default=None,
|
||||||
|
help="access | support | meeting | all. Default: access for a "
|
||||||
|
"bare listing, all when a name/company/etc selector is given.")
|
||||||
|
sp.add_argument("--filter", help="Raw session-filter expression (overrides the flags).")
|
||||||
|
sp.add_argument("--limit", type=int, default=0,
|
||||||
|
help="Cap rows printed (0 = no cap; --json is always full).")
|
||||||
|
|
||||||
sp = sub.add_parser("session", help="Session detail.", parents=[common])
|
sp = sub.add_parser("session", help="Session detail.", parents=[common])
|
||||||
sp.add_argument("session_id")
|
sp.add_argument("session_id")
|
||||||
|
|||||||
@@ -16,13 +16,20 @@ application/json and the body is the method's parameters (object or array).
|
|||||||
Credentials: never hardcoded. api_secret loaded at runtime from the SOPS vault,
|
Credentials: never hardcoded. api_secret loaded at runtime from the SOPS vault,
|
||||||
or the SCREENCONNECT_API_SECRET env var (testing override).
|
or the SCREENCONNECT_API_SECRET env var (testing override).
|
||||||
|
|
||||||
INSTANCE METHOD SURFACE (probed live 2026-06-22): control IS available -
|
INSTANCE METHOD SURFACE (probed live 2026-06-22, extended 2026-07-06): control IS
|
||||||
GetSessionsByName, GetSessionDetailsBySessionID, GetSessionBySessionID (reads,
|
available - GetSessionsByFilter, GetSessionsByName, GetSessionDetailsBySessionID,
|
||||||
JSON-object params); SendCommandToSession, SendMessageToSession,
|
GetSessionBySessionID (reads, JSON-object params); SendCommandToSession,
|
||||||
UpdateSessionCustomProperties, CreateSession (writes, POSITIONAL-ARRAY params).
|
SendMessageToSession, UpdateSessionCustomProperties, CreateSession (writes,
|
||||||
MISSING on this extension version: GetSessions/GetAllSessions/GetSessionGroups
|
POSITIONAL-ARRAY params).
|
||||||
(full-fleet inventory) - "web method does not exist"; pending an admin update of
|
|
||||||
the RESTful API Manager extension. `raw()` probes arbitrary methods.
|
FULL-FLEET LISTING (fixed 2026-07-06): GetSessionsByFilter({"sessionFilter": <expr>})
|
||||||
|
IS exposed and is the enumeration path. It takes a ScreenConnect session-filter
|
||||||
|
expression, e.g. SessionType = 'Access' (all 958 unattended agents), CustomProperty1
|
||||||
|
= 'Safesite' (per-client), Name LIKE '*DELL*' (partial name). SessionType 'Access'
|
||||||
|
(=2) is the agent fleet; 'Support' (=0) the on-demand sessions. The bare aliases
|
||||||
|
GetSessions/GetAllSessions/GetSessionGroups remain absent, but GetSessionsByFilter
|
||||||
|
covers the inventory use case fully - no extension update needed. `raw()` probes
|
||||||
|
arbitrary methods.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -63,6 +70,10 @@ SKILL_DIR = Path(__file__).resolve().parent.parent
|
|||||||
# Custom-property mapping on this instance (from the vault notes).
|
# Custom-property mapping on this instance (from the vault notes).
|
||||||
CUSTOM_PROPERTIES = {"CP1": "Company", "CP2": "Site", "CP3": "Tag"}
|
CUSTOM_PROPERTIES = {"CP1": "Company", "CP2": "Site", "CP3": "Tag"}
|
||||||
|
|
||||||
|
# SessionType filter literals (verified live 2026-07-06): 'Access' (=2) is the
|
||||||
|
# unattended agent fleet; 'Support' (=0) the on-demand sessions.
|
||||||
|
SESSION_TYPE_LITERALS = {"access": "Access", "support": "Support", "meeting": "Meeting"}
|
||||||
|
|
||||||
|
|
||||||
class ScreenConnectError(RuntimeError):
|
class ScreenConnectError(RuntimeError):
|
||||||
"""Raised for transport or API errors."""
|
"""Raised for transport or API errors."""
|
||||||
@@ -200,17 +211,79 @@ class ScreenConnectClient:
|
|||||||
# VERIFIED methods (work on the current instance)
|
# VERIFIED methods (work on the current instance)
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
def get_sessions_by_name(self, session_name: str = "") -> Any:
|
def get_sessions_by_name(self, session_name: str = "") -> Any:
|
||||||
"""List sessions whose Name matches `session_name` (RESTful API Manager
|
"""List sessions whose Name EXACTLY equals `session_name` (RESTful API
|
||||||
GetSessionsByName). VERIFIED LIVE. Empty string returns sessions with a
|
Manager GetSessionsByName). VERIFIED LIVE. This is an exact match, not a
|
||||||
blank Name (the unattended access agents on this instance)."""
|
contains match ("" returns only the blank-Name sessions, not the fleet).
|
||||||
|
For full-fleet or partial-name listing use list_sessions()/get_sessions_by_filter()."""
|
||||||
return self.call("GetSessionsByName", {"sessionName": session_name})
|
return self.call("GetSessionsByName", {"sessionName": session_name})
|
||||||
|
|
||||||
|
def get_sessions_by_filter(self, session_filter: str) -> Any:
|
||||||
|
"""Enumerate sessions matching a ScreenConnect session-filter expression
|
||||||
|
(GetSessionsByFilter, param `sessionFilter`). VERIFIED LIVE 2026-07-06 -
|
||||||
|
this IS the full-fleet listing path. Examples:
|
||||||
|
SessionType = 'Access' -> every unattended agent (the fleet)
|
||||||
|
CustomProperty1 = 'Safesite' -> one client's machines
|
||||||
|
Name LIKE '*DELL*' -> partial name (case-insensitive)
|
||||||
|
Combine with AND/OR. String literals are single-quoted."""
|
||||||
|
return self.call("GetSessionsByFilter", {"sessionFilter": session_filter})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_literal(value: str) -> str:
|
||||||
|
"""Single-quote a value for the session-filter language, escaping any
|
||||||
|
embedded single quote by doubling it. VERIFIED LIVE 2026-07-06:
|
||||||
|
`CustomProperty1 LIKE '*Andy''s*'` matched the "Andy's Mobile Fuel" session,
|
||||||
|
so doubling is the correct escape for this filter grammar."""
|
||||||
|
return "'" + str(value).replace("'", "''") + "'"
|
||||||
|
|
||||||
|
def build_session_filter(
|
||||||
|
self,
|
||||||
|
session_type: Optional[str] = "access",
|
||||||
|
name: Optional[str] = None,
|
||||||
|
name_like: Optional[str] = None,
|
||||||
|
company: Optional[str] = None,
|
||||||
|
site: Optional[str] = None,
|
||||||
|
tag: Optional[str] = None,
|
||||||
|
extra: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Assemble a session-filter expression from structured parts (AND-joined).
|
||||||
|
`session_type`: access | support | meeting | all (None/all -> no type clause).
|
||||||
|
`name` exact, `name_like` partial (wrapped in *...*), company/site/tag map to
|
||||||
|
CP1/CP2/CP3, `extra` is a raw clause appended verbatim. Defaults to the whole
|
||||||
|
Access fleet when nothing else is specified."""
|
||||||
|
clauses: list[str] = []
|
||||||
|
if session_type and session_type.lower() == "all":
|
||||||
|
# 'all' = every session type. Only Access + Support exist on this
|
||||||
|
# instance; an OR clause is the verified match-all (a bare '*' matches
|
||||||
|
# nothing). VERIFIED LIVE 2026-07-06 -> 982 = 958 Access + 24 Support.
|
||||||
|
clauses.append("(SessionType = 'Access' OR SessionType = 'Support')")
|
||||||
|
elif session_type:
|
||||||
|
literal = SESSION_TYPE_LITERALS.get(session_type.lower(), session_type)
|
||||||
|
clauses.append(f"SessionType = {self._filter_literal(literal)}")
|
||||||
|
if name:
|
||||||
|
clauses.append(f"Name = {self._filter_literal(name)}")
|
||||||
|
if name_like:
|
||||||
|
clauses.append(f"Name LIKE {self._filter_literal('*' + name_like + '*')}")
|
||||||
|
if company:
|
||||||
|
clauses.append(f"CustomProperty1 = {self._filter_literal(company)}")
|
||||||
|
if site:
|
||||||
|
clauses.append(f"CustomProperty2 = {self._filter_literal(site)}")
|
||||||
|
if tag:
|
||||||
|
clauses.append(f"CustomProperty3 = {self._filter_literal(tag)}")
|
||||||
|
if extra:
|
||||||
|
clauses.append(extra)
|
||||||
|
return " AND ".join(clauses) if clauses else "SessionType = 'Access'"
|
||||||
|
|
||||||
|
def list_sessions(self, **kwargs) -> Any:
|
||||||
|
"""Convenience: build a filter from keyword parts and enumerate. See
|
||||||
|
build_session_filter for the accepted keywords."""
|
||||||
|
return self.get_sessions_by_filter(self.build_session_filter(**kwargs))
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
# Control + detail methods (all VERIFIED LIVE 2026-06-22 on the ACG instance).
|
# Control + detail methods (all VERIFIED LIVE 2026-06-22 on the ACG instance).
|
||||||
# Reads take a JSON object {sessionID:...}; the POST/write methods take a
|
# Reads take a JSON object {sessionID:...}; the POST/write methods take a
|
||||||
# POSITIONAL ARRAY. NOTE: full-fleet inventory (GetSessions) is NOT exposed by
|
# POSITIONAL ARRAY. Full-fleet inventory is served by GetSessionsByFilter
|
||||||
# this extension version (returns "web method does not exist") - pending an
|
# (see get_sessions_by_filter / list_sessions above) - the bare GetSessions
|
||||||
# admin update of the RESTful API Manager extension (see SKILL.md).
|
# alias is absent but not needed.
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
def get_session_details(self, session_id: str) -> Any:
|
def get_session_details(self, session_id: str) -> Any:
|
||||||
"""GetSessionDetailsBySessionID - full detail for one session. VERIFIED."""
|
"""GetSessionDetailsBySessionID - full detail for one session. VERIFIED."""
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ def run(args):
|
|||||||
return p.returncode, p.stdout, p.stderr
|
return p.returncode, p.stdout, p.stderr
|
||||||
|
|
||||||
|
|
||||||
def check(name, args, *, want_rc=None, out_has=None, out_json_ok=False):
|
def check(name, args, *, want_rc=None, out_has=None, out_json_ok=False, out_json_min_len=None):
|
||||||
rc, out, err = run(args)
|
rc, out, err = run(args)
|
||||||
problems = []
|
problems = []
|
||||||
if want_rc is not None and rc != want_rc:
|
if want_rc is not None and rc != want_rc:
|
||||||
problems.append(f"rc={rc} want {want_rc}")
|
problems.append(f"rc={rc} want {want_rc}")
|
||||||
if out_has and out_has not in out:
|
if out_has and out_has not in out:
|
||||||
problems.append(f"stdout missing {out_has!r}")
|
problems.append(f"stdout missing {out_has!r}")
|
||||||
if out_json_ok:
|
if out_json_ok or out_json_min_len is not None:
|
||||||
try:
|
try:
|
||||||
json.loads(out)
|
parsed = json.loads(out)
|
||||||
|
if out_json_min_len is not None and len(parsed) < out_json_min_len:
|
||||||
|
problems.append(f"json len {len(parsed)} < {out_json_min_len}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
problems.append(f"stdout not JSON: {e}")
|
problems.append(f"stdout not JSON: {e}")
|
||||||
results.append(("PASS" if not problems else "FAIL", name, "; ".join(problems)))
|
results.append(("PASS" if not problems else "FAIL", name, "; ".join(problems)))
|
||||||
@@ -44,8 +46,21 @@ def check(name, args, *, want_rc=None, out_has=None, out_json_ok=False):
|
|||||||
|
|
||||||
# --- reads: succeed (rc 0) [hit the live instance] ---
|
# --- reads: succeed (rc 0) [hit the live instance] ---
|
||||||
check("status", ["status"], want_rc=0, out_has="Authenticated")
|
check("status", ["status"], want_rc=0, out_has="Authenticated")
|
||||||
check("sessions", ["sessions"], want_rc=0, out_has="Sessions:")
|
check("status shows fleet", ["status"], want_rc=0, out_has="fleet:")
|
||||||
|
check("sessions (full fleet)", ["sessions"], want_rc=0, out_has="Sessions:")
|
||||||
check("sessions json", ["sessions", "--json"], want_rc=0, out_json_ok=True)
|
check("sessions json", ["sessions", "--json"], want_rc=0, out_json_ok=True)
|
||||||
|
check("sessions --limit", ["sessions", "--limit", "5"], want_rc=0, out_has="Sessions:")
|
||||||
|
check("sessions --like", ["sessions", "--like", "DELL", "--json"], want_rc=0, out_json_ok=True)
|
||||||
|
check("sessions --type all", ["sessions", "--type", "all", "--json"], want_rc=0, out_json_ok=True)
|
||||||
|
check("sessions --filter", ["sessions", "--filter", "SessionType = 'Access'", "--json"],
|
||||||
|
want_rc=0, out_json_ok=True)
|
||||||
|
check("sessions --company", ["sessions", "--company", "Safesite", "--json"],
|
||||||
|
want_rc=0, out_json_ok=True)
|
||||||
|
# --limit must NOT truncate --json (documented "--json is always full" contract):
|
||||||
|
# Safesite has 62 machines; even with --limit 1 the JSON must return them all.
|
||||||
|
check("sessions --limit does not truncate json",
|
||||||
|
["sessions", "--company", "Safesite", "--limit", "1", "--json"],
|
||||||
|
want_rc=0, out_json_min_len=10)
|
||||||
|
|
||||||
# --- build-installer: pure URL build (no network) ---
|
# --- build-installer: pure URL build (no network) ---
|
||||||
check("build-installer", ["build-installer", "--name", "HOST", "--company", "AZ Computer Guru",
|
check("build-installer", ["build-installer", "--name", "HOST", "--company", "AZ Computer Guru",
|
||||||
|
|||||||
3
.claude/skills/seafile/.gitignore
vendored
Normal file
3
.claude/skills/seafile/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Holds a live Seafile API token — never commit.
|
||||||
|
.cache/
|
||||||
|
__pycache__/
|
||||||
118
.claude/skills/seafile/SKILL.md
Normal file
118
.claude/skills/seafile/SKILL.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
name: seafile
|
||||||
|
description: "Manage ACG's Seafile Pro server (the SeaCloud / SeaDrive backup backend on Jupiter): read-only inventory (who has an account, per-user storage, libraries, which GuruRMM endpoints run the SeaDrive/Seafile client) plus gated admin writes (create/deactivate/delete user, set quota, create/transfer/delete library, share/unshare). Reads run freely; writes preview by default and require --confirm. Built for GPS backup verification + client provisioning. Triggers: seafile, seacloud, seadrive, sync.azcomputerguru, who backs up to seafile, seafile usage, create seafile user, set seafile quota, seafile library."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Seafile (SeaCloud / SeaDrive) Skill
|
||||||
|
|
||||||
|
Admin client for ACG's live **Seafile Pro** server — the backend clients call
|
||||||
|
"SeaCloud" (the server) and "SeaDrive" (the desktop virtual-drive client). Runs in
|
||||||
|
Docker on Jupiter (`172.16.3.20:8082`, public `https://sync.azcomputerguru.com`).
|
||||||
|
Answers the GPS backup-audit question: **which customers back up here and how
|
||||||
|
much data do they hold** — and, with `--rmm`, which enrolled machines actually run
|
||||||
|
the client.
|
||||||
|
|
||||||
|
This skill is one of the backup-verification siblings alongside `b2` (Backblaze),
|
||||||
|
`owncloud`, and `datto-workplace`. Inventory is read-only; admin **writes**
|
||||||
|
(user/quota/library/share) exist but are gated behind `--confirm` (see below) and
|
||||||
|
preview by default.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SEAFILE=".claude/skills/seafile/scripts/seafile.py"
|
||||||
|
py "$SEAFILE" status # server reachable + version
|
||||||
|
py "$SEAFILE" usage # per-user storage + library count (audit view)
|
||||||
|
py "$SEAFILE" users [--inactive] # all accounts + used/quota + last login
|
||||||
|
py "$SEAFILE" libraries # all libraries (repos) + owner + size
|
||||||
|
py "$SEAFILE" devices [--user EMAIL] # desktop/sync devices the server has seen
|
||||||
|
py "$SEAFILE" audit # active accounts with data (the GPS slice)
|
||||||
|
py "$SEAFILE" audit --rmm # + cross-check GuruRMM endpoints for the client
|
||||||
|
py "$SEAFILE" audit --client Russo --rmm --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--json` to any command for machine-readable output. `--url` overrides the
|
||||||
|
server base (default is the internal Docker endpoint; use the public host when
|
||||||
|
off-LAN).
|
||||||
|
|
||||||
|
## Write ops (admin) — preview by default, `--confirm` to apply
|
||||||
|
|
||||||
|
Every write is **gated**: run it once to see the `[PLAN]`, add `--confirm` to
|
||||||
|
execute. Preview is fully offline (no auth, no network), so it is safe to eyeball
|
||||||
|
first. All writes go out **form-encoded** (the share endpoint reads `share_to` via
|
||||||
|
`getlist`, which requires form encoding, not JSON) against the Seafile Pro 12.0
|
||||||
|
admin API (`/api/v2.1/admin/...`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# users
|
||||||
|
py "$SEAFILE" user-create client@acme.com --password 'PW' --name 'Acme' --quota-gb 500 --confirm
|
||||||
|
py "$SEAFILE" user-set client@acme.com --inactive --confirm # deactivate
|
||||||
|
py "$SEAFILE" user-set client@acme.com --quota-gb 1024 --confirm # bump quota
|
||||||
|
py "$SEAFILE" quota client@acme.com --gb 1024 --confirm # quota shortcut
|
||||||
|
py "$SEAFILE" user-delete client@acme.com --confirm # IRREVERSIBLE
|
||||||
|
|
||||||
|
# libraries
|
||||||
|
py "$SEAFILE" lib-create "Acme Backup" --owner client@acme.com --confirm
|
||||||
|
py "$SEAFILE" lib-transfer <repo_id> --to newowner@acme.com --confirm
|
||||||
|
py "$SEAFILE" lib-delete <repo_id> --confirm # IRREVERSIBLE
|
||||||
|
|
||||||
|
# sharing
|
||||||
|
py "$SEAFILE" share <repo_id> --to a@acme.com b@acme.com --perm rw --confirm
|
||||||
|
py "$SEAFILE" unshare <repo_id> --to a@acme.com --confirm
|
||||||
|
py "$SEAFILE" share <repo_id> --to <group_id> --group --perm r --confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**Quota unit:** `--quota-gb` / `--gb` are **decimal GB** — deliberately the same
|
||||||
|
unit the read commands (`users` / `usage` / `audit`) display, so a quota you set
|
||||||
|
round-trips with the quota you later see here. Internally Seafile stores quota in
|
||||||
|
MB (1 MB = 1 MiB); the preview shows the exact MB it will send (e.g.
|
||||||
|
`500 GB (-> 476837 MB stored)`) so you can verify before `--confirm`. Caveat:
|
||||||
|
Seafile's own web admin UI renders quota in binary GiB, so the same quota reads
|
||||||
|
~7% smaller there — trust this tool's `users` view for the round-trip.
|
||||||
|
|
||||||
|
**Any password you set is a credential** — `user-create`/`user-set` print a
|
||||||
|
reminder; store it in the SOPS vault via the `vault` skill (house rule). Never
|
||||||
|
hand out or log a Seafile password without vaulting it.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
Loaded at runtime from the SOPS vault (never hardcoded):
|
||||||
|
`services/seafile-pro.sops.yaml` -> `credentials.username` / `credentials.password`
|
||||||
|
(the Seafile admin account). Auth is `POST /api2/auth-token/` (form) -> a
|
||||||
|
long-lived API token used as `Authorization: Token <tok>`. The token is a secret;
|
||||||
|
it is cached at `.claude/skills/seafile/.cache/token.json` (gitignored, mode 0600)
|
||||||
|
and re-fetched weekly or on a 401/403.
|
||||||
|
|
||||||
|
## What the data means
|
||||||
|
|
||||||
|
- **quota_usage** (bytes) is the authoritative per-account storage used. A user
|
||||||
|
with `is_active: true` and non-zero usage is actively backing up here.
|
||||||
|
- **libraries** (repos) are the sync roots; `size` + `file_count` + `last_modified`
|
||||||
|
show freshness. `usage` rolls libraries up per owner.
|
||||||
|
- Accounts are emails, so the customer is usually obvious from the domain
|
||||||
|
(e.g. `russo@rrs-law.com` = Russo Law, `greg@gstoltzlaw.com` = Stoltz Law).
|
||||||
|
- As of the build (2026-07-05) only a **handful** of accounts hold data
|
||||||
|
(~5 TB total) — Seafile is used by a few clients, not the whole GPS base.
|
||||||
|
|
||||||
|
## The "both" cross-check (`--rmm`)
|
||||||
|
|
||||||
|
Server-side tells you who has an account; `--rmm` adds the endpoint half. It logs
|
||||||
|
in to GuruRMM (`infrastructure/gururmm-server.sops.yaml`), and for each agent
|
||||||
|
(optionally filtered by `--client`) runs a **read-only** detection PowerShell that
|
||||||
|
reports installed products / running processes / config folders matching
|
||||||
|
`Seafile` / `SeaDrive`. An agent is reported only if the client is actually
|
||||||
|
present. Detection dispatches a command to live endpoints — scope with `--client`
|
||||||
|
rather than sweeping all 350+ agents unless you mean to.
|
||||||
|
|
||||||
|
## Notes / gotchas
|
||||||
|
|
||||||
|
- Base defaults to `http://172.16.3.20:8082` (direct, no Cloudflare); set
|
||||||
|
`SEAFILE_URL=https://sync.azcomputerguru.com` when off the LAN/Tailscale.
|
||||||
|
- `devices` depends on the admin devices endpoint, which varies by Seafile
|
||||||
|
version; it returns `[]` (with a note) rather than failing if unsupported.
|
||||||
|
- Shared plumbing (repo-root resolve, vault read, GuruRMM client + endpoint
|
||||||
|
detection) lives in `.claude/scripts/backup_common.py`, used by all three
|
||||||
|
server-backup skills. Detection patterns must be precise per backend — this
|
||||||
|
skill uses `Seafile`/`SeaDrive`.
|
||||||
|
- Seafile Pro also has a MySQL backend on Jupiter (vault `credentials.database.*`)
|
||||||
|
for deeper queries; not used here (the admin API covers the audit needs).
|
||||||
435
.claude/skills/seafile/scripts/seafile.py
Normal file
435
.claude/skills/seafile/scripts/seafile.py
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI for the `seafile` skill — read-only Seafile Pro (SeaCloud/SeaDrive) inventory
|
||||||
|
for the GPS backup audit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
status server reachable + version
|
||||||
|
users [--inactive] [--json] all accounts + storage used + last login
|
||||||
|
libraries [--json] all libraries (repos) + owner + size
|
||||||
|
usage [--json] per-user rollup: storage + library count (the audit view)
|
||||||
|
devices [--user EMAIL] [--json] desktop/sync devices the server has seen
|
||||||
|
audit [--client NAME] [--rmm] [--json]
|
||||||
|
the GPS view: users/usage, optionally cross-checked
|
||||||
|
against GuruRMM endpoints (SeaDrive/Seafile installed?)
|
||||||
|
|
||||||
|
Write ops (admin) -- ALL preview by default; add --confirm to apply:
|
||||||
|
user-create EMAIL --password P [--name N] [--quota-gb G] [--role R] [--inactive]
|
||||||
|
user-set EMAIL [--active|--inactive] [--quota-gb G] [--name N] [--role R] [--password P]
|
||||||
|
quota EMAIL --gb G set storage quota (decimal GB, as shown by `users`)
|
||||||
|
user-delete EMAIL remove account (irreversible)
|
||||||
|
lib-create NAME [--owner EMAIL] create a library
|
||||||
|
lib-transfer REPO_ID --to EMAIL transfer ownership
|
||||||
|
lib-delete REPO_ID delete a library (irreversible)
|
||||||
|
share REPO_ID --to EMAIL... [--perm r|rw] [--group]
|
||||||
|
unshare REPO_ID --to EMAIL [--group]
|
||||||
|
|
||||||
|
Server-side is the source of "who has an account + how much data"; --rmm adds the
|
||||||
|
"which enrolled machine actually runs the client" half (the 'both' cross-check).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from seafile_client import SeafileClient # noqa: E402
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
GB = 1_000_000_000
|
||||||
|
|
||||||
|
|
||||||
|
def _gb(n) -> float:
|
||||||
|
try:
|
||||||
|
return round(int(n) / GB, 2)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_quota(total) -> str:
|
||||||
|
try:
|
||||||
|
t = int(total)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "?"
|
||||||
|
return "unlimited" if t < 0 else f"{_gb(t)} GB"
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status(c: SeafileClient, args) -> int:
|
||||||
|
info = c.server_info()
|
||||||
|
print(f"[OK] {c.base}")
|
||||||
|
print(json.dumps(info, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_users(c: SeafileClient, args) -> int:
|
||||||
|
users = c.list_users()
|
||||||
|
if args.inactive:
|
||||||
|
users = [u for u in users if not u.get("is_active")]
|
||||||
|
users.sort(key=lambda u: int(u.get("quota_usage") or 0), reverse=True)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(users, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'EMAIL':40} {'ACTIVE':6} {'USED':>10} {'QUOTA':>10} {'LAST LOGIN':20}")
|
||||||
|
for u in users:
|
||||||
|
print(f"{(u.get('email') or '')[:40]:40} "
|
||||||
|
f"{'yes' if u.get('is_active') else 'NO':6} "
|
||||||
|
f"{_gb(u.get('quota_usage')):>7} GB "
|
||||||
|
f"{_fmt_quota(u.get('quota_total')):>10} "
|
||||||
|
f"{(u.get('last_login') or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(users)} users; total used "
|
||||||
|
f"{round(sum(_gb(u.get('quota_usage')) for u in users), 1)} GB")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_libraries(c: SeafileClient, args) -> int:
|
||||||
|
repos = c.list_libraries()
|
||||||
|
repos.sort(key=lambda r: int(r.get("size") or 0), reverse=True)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(repos, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'LIBRARY':30} {'OWNER':35} {'SIZE':>10} {'FILES':>8} {'MODIFIED':20}")
|
||||||
|
for r in repos:
|
||||||
|
print(f"{(r.get('name') or '')[:30]:30} "
|
||||||
|
f"{(r.get('owner_email') or r.get('owner') or '')[:35]:35} "
|
||||||
|
f"{_gb(r.get('size')):>7} GB "
|
||||||
|
f"{str(r.get('file_count') or '?'):>8} "
|
||||||
|
f"{(r.get('last_modified') or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(repos)} libraries; total "
|
||||||
|
f"{round(sum(_gb(r.get('size')) for r in repos), 1)} GB")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _usage_rollup(c: SeafileClient) -> list[dict]:
|
||||||
|
users = {u.get("email"): u for u in c.list_users()}
|
||||||
|
repos = c.list_libraries()
|
||||||
|
by_owner: dict[str, dict] = {}
|
||||||
|
for r in repos:
|
||||||
|
owner = r.get("owner_email") or r.get("owner")
|
||||||
|
d = by_owner.setdefault(owner, {"libraries": 0, "lib_bytes": 0})
|
||||||
|
d["libraries"] += 1
|
||||||
|
d["lib_bytes"] += int(r.get("size") or 0)
|
||||||
|
rollup = []
|
||||||
|
for email, u in users.items():
|
||||||
|
o = by_owner.get(email, {"libraries": 0, "lib_bytes": 0})
|
||||||
|
rollup.append({
|
||||||
|
"email": email,
|
||||||
|
"name": u.get("name"),
|
||||||
|
"is_active": bool(u.get("is_active")),
|
||||||
|
"quota_used_bytes": int(u.get("quota_usage") or 0),
|
||||||
|
"quota_used_gb": _gb(u.get("quota_usage")),
|
||||||
|
"quota_total": u.get("quota_total"),
|
||||||
|
"libraries": o["libraries"],
|
||||||
|
"library_gb": _gb(o["lib_bytes"]),
|
||||||
|
"last_login": u.get("last_login"),
|
||||||
|
"last_access": u.get("last_access_time"),
|
||||||
|
})
|
||||||
|
rollup.sort(key=lambda x: x["quota_used_bytes"], reverse=True)
|
||||||
|
return rollup
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_usage(c: SeafileClient, args) -> int:
|
||||||
|
rollup = _usage_rollup(c)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(rollup, indent=2))
|
||||||
|
return 0
|
||||||
|
print(f"{'USER':40} {'ACT':4} {'USED':>9} {'LIBS':>5} {'LAST LOGIN':20}")
|
||||||
|
for r in rollup:
|
||||||
|
print(f"{(r['email'] or '')[:40]:40} "
|
||||||
|
f"{'yes' if r['is_active'] else 'NO':4} "
|
||||||
|
f"{r['quota_used_gb']:>6} GB "
|
||||||
|
f"{r['libraries']:>5} "
|
||||||
|
f"{(r['last_login'] or '-')[:19]:20}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_devices(c: SeafileClient, args) -> int:
|
||||||
|
devices = c.list_devices(args.user)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(devices, indent=2))
|
||||||
|
return 0
|
||||||
|
if not devices:
|
||||||
|
print("[INFO] no devices returned (endpoint may be unsupported on this Seafile version)")
|
||||||
|
return 0
|
||||||
|
print(f"{'USER':35} {'DEVICE':25} {'PLATFORM':12} {'LAST ACCESS':20}")
|
||||||
|
for d in devices:
|
||||||
|
print(f"{(d.get('user') or '')[:35]:35} "
|
||||||
|
f"{(d.get('device_name') or '')[:25]:25} "
|
||||||
|
f"{(d.get('platform') or '')[:12]:12} "
|
||||||
|
f"{(d.get('last_accessed') or d.get('last_login_ip') or '-')[:19]:20}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_audit(c: SeafileClient, args) -> int:
|
||||||
|
rollup = _usage_rollup(c)
|
||||||
|
# active accounts with data are the ones actually backing up here
|
||||||
|
active = [r for r in rollup if r["is_active"] and r["quota_used_bytes"] > 0]
|
||||||
|
result = {"backend": "seafile", "server": c.base, "accounts": active}
|
||||||
|
|
||||||
|
if args.rmm:
|
||||||
|
rmm = bc.RmmClient()
|
||||||
|
rmm.login()
|
||||||
|
checks = []
|
||||||
|
agents = rmm.find_agents(client_substr=args.client) if args.client else rmm.list_agents()
|
||||||
|
for a in agents:
|
||||||
|
det = rmm.detect_client(a, ["Seafile", "SeaDrive"])
|
||||||
|
if det.get("detected"):
|
||||||
|
checks.append(det)
|
||||||
|
result["rmm_endpoints_with_client"] = checks
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"=== Seafile (SeaCloud/SeaDrive) backup accounts - {c.base} ===")
|
||||||
|
print(f"{'USER':40} {'USED':>9} {'LIBS':>5} {'LAST LOGIN':20}")
|
||||||
|
for r in active:
|
||||||
|
print(f"{(r['email'] or '')[:40]:40} {r['quota_used_gb']:>6} GB "
|
||||||
|
f"{r['libraries']:>5} {(r['last_login'] or '-')[:19]:20}")
|
||||||
|
print(f"\n{len(active)} active accounts with data; "
|
||||||
|
f"{round(sum(r['quota_used_gb'] for r in active), 1)} GB total")
|
||||||
|
if args.rmm:
|
||||||
|
eps = result.get("rmm_endpoints_with_client", [])
|
||||||
|
print(f"\n--- GuruRMM endpoints running a Seafile/SeaDrive client ({len(eps)}) ---")
|
||||||
|
for e in eps:
|
||||||
|
print(f" {e['hostname']:25} client={e.get('client_name')}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# --- write ops (gated) --------------------------------------------------------
|
||||||
|
# Quota units: Seafile's admin API takes quota_total in MB, where 1 MB == 1 MiB
|
||||||
|
# (get_file_size_unit('MB') == 2**20). This tool's read commands (users/usage/audit)
|
||||||
|
# render quota as decimal GB (bytes / 1e9, see _gb/_fmt_quota). To make a quota you
|
||||||
|
# SET round-trip with the quota you later SEE here, --gb is decimal GB and we convert
|
||||||
|
# to the stored MB count accordingly. (Note: Seafile's own web admin UI shows quota
|
||||||
|
# in binary GiB, so the same quota reads ~7% smaller there.)
|
||||||
|
MB_BYTES = 1 << 20 # Seafile's MB unit (1 MiB)
|
||||||
|
GB_BYTES = 1_000_000_000 # this tool's display GB (matches _gb)
|
||||||
|
|
||||||
|
|
||||||
|
def _gb_to_mb(gb) -> int:
|
||||||
|
return int(round(float(gb) * GB_BYTES / MB_BYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _do_write(args, label: str, detail: dict, fn, reminder: str | None = None,
|
||||||
|
on_result=None) -> int:
|
||||||
|
"""Print the plan; execute only with --confirm. Preview is fully offline.
|
||||||
|
|
||||||
|
`on_result(result) -> int`: optional post-execute check that may print its own
|
||||||
|
warnings and return a non-zero exit code (e.g. partial-success responses that
|
||||||
|
still come back HTTP 200). A non-zero return suppresses the [OK] line.
|
||||||
|
"""
|
||||||
|
print(f"[PLAN] {label}")
|
||||||
|
for k, v in detail.items():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
if not getattr(args, "confirm", False):
|
||||||
|
print("[PREVIEW] not executed. Re-run with --confirm to apply.")
|
||||||
|
return 0
|
||||||
|
result = fn()
|
||||||
|
if on_result is not None:
|
||||||
|
rc = on_result(result)
|
||||||
|
if rc:
|
||||||
|
return rc
|
||||||
|
print(f"[OK] {label}")
|
||||||
|
if reminder:
|
||||||
|
print(f"[REMINDER] {reminder}")
|
||||||
|
if result:
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_create(c: SeafileClient, args) -> int:
|
||||||
|
quota_mb = _gb_to_mb(args.quota_gb) if args.quota_gb is not None else None
|
||||||
|
detail = {"email": args.email, "active": not args.inactive, "password": "<set>"}
|
||||||
|
if args.name:
|
||||||
|
detail["name"] = args.name
|
||||||
|
if quota_mb is not None:
|
||||||
|
detail["quota"] = f"{args.quota_gb} GB (-> {quota_mb} MB stored)"
|
||||||
|
if args.role:
|
||||||
|
detail["role"] = args.role
|
||||||
|
return _do_write(
|
||||||
|
args, f"create user {args.email}", detail,
|
||||||
|
lambda: c.create_user(args.email, args.password, name=args.name,
|
||||||
|
quota_mb=quota_mb, role=args.role,
|
||||||
|
is_active=not args.inactive),
|
||||||
|
reminder="password is a credential -- store it in the SOPS vault (vault skill).")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_set(c: SeafileClient, args) -> int:
|
||||||
|
if args.active and args.inactive:
|
||||||
|
print("[ERROR] --active and --inactive are mutually exclusive", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
is_active = True if args.active else (False if args.inactive else None)
|
||||||
|
quota_mb = _gb_to_mb(args.quota_gb) if args.quota_gb is not None else None
|
||||||
|
detail: dict = {"email": args.email}
|
||||||
|
if is_active is not None:
|
||||||
|
detail["active"] = is_active
|
||||||
|
if quota_mb is not None:
|
||||||
|
detail["quota"] = f"{args.quota_gb} GB (-> {quota_mb} MB stored)"
|
||||||
|
if args.name is not None:
|
||||||
|
detail["name"] = args.name
|
||||||
|
if args.role is not None:
|
||||||
|
detail["role"] = args.role
|
||||||
|
if args.password is not None:
|
||||||
|
detail["password"] = "<set>"
|
||||||
|
if len(detail) == 1:
|
||||||
|
print("[ERROR] nothing to change (give --active/--inactive/--quota-gb/--name/--role/--password)",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
reminder = ("password is a credential -- store it in the SOPS vault (vault skill)."
|
||||||
|
if args.password is not None else None)
|
||||||
|
return _do_write(
|
||||||
|
args, f"update user {args.email}", detail,
|
||||||
|
lambda: c.update_user(args.email, is_active=is_active, password=args.password,
|
||||||
|
name=args.name, quota_mb=quota_mb, role=args.role),
|
||||||
|
reminder=reminder)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_quota(c: SeafileClient, args) -> int:
|
||||||
|
quota_mb = _gb_to_mb(args.gb)
|
||||||
|
detail = {"email": args.email, "quota": f"{args.gb} GB (-> {quota_mb} MB stored)"}
|
||||||
|
return _do_write(args, f"set quota for {args.email}", detail,
|
||||||
|
lambda: c.update_user(args.email, quota_mb=quota_mb))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_user_delete(c: SeafileClient, args) -> int:
|
||||||
|
detail = {"email": args.email, "note": "IRREVERSIBLE - removes the account"}
|
||||||
|
return _do_write(args, f"DELETE user {args.email}", detail,
|
||||||
|
lambda: c.delete_user(args.email))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_lib_create(c: SeafileClient, args) -> int:
|
||||||
|
detail = {"name": args.name, "owner": args.owner or "<admin account>"}
|
||||||
|
return _do_write(args, f"create library '{args.name}'", detail,
|
||||||
|
lambda: c.create_library(args.name, owner=args.owner))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_lib_transfer(c: SeafileClient, args) -> int:
|
||||||
|
detail = {"repo_id": args.repo_id, "new_owner": args.to}
|
||||||
|
return _do_write(args, f"transfer library {args.repo_id}", detail,
|
||||||
|
lambda: c.transfer_library(args.repo_id, args.to))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_lib_delete(c: SeafileClient, args) -> int:
|
||||||
|
detail = {"repo_id": args.repo_id, "note": "IRREVERSIBLE - deletes the library + data"}
|
||||||
|
return _do_write(args, f"DELETE library {args.repo_id}", detail,
|
||||||
|
lambda: c.delete_library(args.repo_id))
|
||||||
|
|
||||||
|
|
||||||
|
def _share_result_check(result) -> int:
|
||||||
|
"""Seafile's admin bulk-share returns HTTP 200 with success[]/failed[] arrays;
|
||||||
|
surface any per-recipient failure instead of reporting a blanket [OK]."""
|
||||||
|
if isinstance(result, dict):
|
||||||
|
failed = result.get("failed") or []
|
||||||
|
if failed:
|
||||||
|
print("[WARNING] some recipients were NOT shared:")
|
||||||
|
for f in failed:
|
||||||
|
who = f.get("email") or f.get("group_id") or f if isinstance(f, dict) else f
|
||||||
|
emsg = f.get("error_msg") or f.get("error") or "failed" if isinstance(f, dict) else "failed"
|
||||||
|
print(f" {who}: {emsg}")
|
||||||
|
ok = result.get("success") or result.get("shared_to") or []
|
||||||
|
print(f"[PARTIAL] shared to {len(ok)}, failed {len(failed)}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_share(c: SeafileClient, args) -> int:
|
||||||
|
stype = "group" if args.group else "user"
|
||||||
|
detail = {"repo_id": args.repo_id, "share_to": args.to,
|
||||||
|
"permission": args.perm, "type": stype}
|
||||||
|
return _do_write(args, f"share library {args.repo_id}", detail,
|
||||||
|
lambda: c.share_library(args.repo_id, args.to,
|
||||||
|
permission=args.perm, share_type=stype),
|
||||||
|
on_result=_share_result_check)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_unshare(c: SeafileClient, args) -> int:
|
||||||
|
stype = "group" if args.group else "user"
|
||||||
|
detail = {"repo_id": args.repo_id, "share_to": args.to, "type": stype}
|
||||||
|
return _do_write(args, f"revoke share on {args.repo_id}", detail,
|
||||||
|
lambda: c.unshare_library(args.repo_id, args.to, share_type=stype))
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
p = argparse.ArgumentParser(prog="seafile", description="Seafile Pro backup inventory + admin")
|
||||||
|
p.add_argument("--url", help="override server base URL (default internal Docker endpoint)")
|
||||||
|
sub = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
sub.add_parser("status")
|
||||||
|
u = sub.add_parser("users"); u.add_argument("--inactive", action="store_true"); u.add_argument("--json", action="store_true")
|
||||||
|
l = sub.add_parser("libraries"); l.add_argument("--json", action="store_true")
|
||||||
|
us = sub.add_parser("usage"); us.add_argument("--json", action="store_true")
|
||||||
|
d = sub.add_parser("devices"); d.add_argument("--user"); d.add_argument("--json", action="store_true")
|
||||||
|
a = sub.add_parser("audit"); a.add_argument("--client"); a.add_argument("--rmm", action="store_true"); a.add_argument("--json", action="store_true")
|
||||||
|
|
||||||
|
# --- write ops (all preview-by-default; require --confirm to apply) ---
|
||||||
|
uc = sub.add_parser("user-create", help="create an account")
|
||||||
|
uc.add_argument("email"); uc.add_argument("--password", required=True)
|
||||||
|
uc.add_argument("--name"); uc.add_argument("--quota-gb", type=float, dest="quota_gb")
|
||||||
|
uc.add_argument("--role"); uc.add_argument("--inactive", action="store_true")
|
||||||
|
uc.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
ust = sub.add_parser("user-set", help="update an account (activate/quota/name/role/password)")
|
||||||
|
ust.add_argument("email")
|
||||||
|
ust.add_argument("--active", action="store_true"); ust.add_argument("--inactive", action="store_true")
|
||||||
|
ust.add_argument("--quota-gb", type=float, dest="quota_gb"); ust.add_argument("--name")
|
||||||
|
ust.add_argument("--role"); ust.add_argument("--password")
|
||||||
|
ust.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
q = sub.add_parser("quota", help="set a user's storage quota (decimal GB)")
|
||||||
|
q.add_argument("email"); q.add_argument("--gb", type=float, required=True)
|
||||||
|
q.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
ud = sub.add_parser("user-delete", help="delete an account (irreversible)")
|
||||||
|
ud.add_argument("email"); ud.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
lc = sub.add_parser("lib-create", help="create a library")
|
||||||
|
lc.add_argument("name"); lc.add_argument("--owner", help="owner email (default: admin)")
|
||||||
|
lc.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
lt = sub.add_parser("lib-transfer", help="transfer library ownership")
|
||||||
|
lt.add_argument("repo_id"); lt.add_argument("--to", required=True, help="new owner email")
|
||||||
|
lt.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
ld = sub.add_parser("lib-delete", help="delete a library (irreversible)")
|
||||||
|
ld.add_argument("repo_id"); ld.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
sh = sub.add_parser("share", help="share a library to user(s)/group(s)")
|
||||||
|
sh.add_argument("repo_id"); sh.add_argument("--to", nargs="+", required=True,
|
||||||
|
help="recipient email(s), or group id(s) with --group")
|
||||||
|
sh.add_argument("--perm", choices=["r", "rw"], default="rw")
|
||||||
|
sh.add_argument("--group", action="store_true", help="share_to are group ids")
|
||||||
|
sh.add_argument("--confirm", action="store_true")
|
||||||
|
|
||||||
|
un = sub.add_parser("unshare", help="revoke a library share")
|
||||||
|
un.add_argument("repo_id"); un.add_argument("--to", required=True,
|
||||||
|
help="recipient email, or group id with --group")
|
||||||
|
un.add_argument("--group", action="store_true"); un.add_argument("--confirm", action="store_true")
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
args = build_parser().parse_args(argv)
|
||||||
|
c = SeafileClient(args.url)
|
||||||
|
handlers = {"status": cmd_status, "users": cmd_users, "libraries": cmd_libraries,
|
||||||
|
"usage": cmd_usage, "devices": cmd_devices, "audit": cmd_audit,
|
||||||
|
"user-create": cmd_user_create, "user-set": cmd_user_set,
|
||||||
|
"quota": cmd_quota, "user-delete": cmd_user_delete,
|
||||||
|
"lib-create": cmd_lib_create, "lib-transfer": cmd_lib_transfer,
|
||||||
|
"lib-delete": cmd_lib_delete, "share": cmd_share, "unshare": cmd_unshare}
|
||||||
|
try:
|
||||||
|
return handlers[args.cmd](c, args)
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
# best-effort error logging per house rule
|
||||||
|
try:
|
||||||
|
root = bc.resolve_root()
|
||||||
|
bc.subprocess.run(["bash", str(root / ".claude/scripts/log-skill-error.sh"),
|
||||||
|
"seafile", str(exc)[:200]], timeout=15)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
284
.claude/skills/seafile/scripts/seafile_client.py
Normal file
284
.claude/skills/seafile/scripts/seafile_client.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Seafile Pro Web API v2.1 client for the `seafile` skill.
|
||||||
|
|
||||||
|
Talks to ACG's live Seafile Pro server (the "SeaCloud" / "SeaDrive" backend) that
|
||||||
|
runs in Docker on Jupiter. Read-only: enumerates users, libraries (repos) and
|
||||||
|
per-user storage so the GPS backup audit can see who is actually backing up here.
|
||||||
|
|
||||||
|
Auth: POST /api2/auth-token/ (form username+password) -> a long-lived API token,
|
||||||
|
then Bearer-style `Authorization: Token <tok>` on every call. The token is a
|
||||||
|
secret; it is cached under the skill's .cache/ (gitignored) like the b2 skill.
|
||||||
|
|
||||||
|
Base URL defaults to the internal Docker endpoint (http://172.16.3.20:8082) which
|
||||||
|
is directly reachable on the LAN/Tailscale; override with SEAFILE_URL (e.g. the
|
||||||
|
public https://sync.azcomputerguru.com).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# Import the shared backup helpers from .claude/scripts.
|
||||||
|
_SKILL_DIR = Path(__file__).resolve().parent.parent # .../skills/seafile
|
||||||
|
_SCRIPTS_DIR = _SKILL_DIR.parent.parent / "scripts" # .../.claude/scripts
|
||||||
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
||||||
|
import backup_common as bc # noqa: E402
|
||||||
|
|
||||||
|
VAULT_ENTRY = "services/seafile-pro.sops.yaml"
|
||||||
|
VAULT_FIELD_USER = "credentials.username"
|
||||||
|
VAULT_FIELD_PASS = "credentials.password"
|
||||||
|
|
||||||
|
DEFAULT_URL = os.environ.get("SEAFILE_URL", "http://172.16.3.20:8082")
|
||||||
|
|
||||||
|
CACHE_DIR = _SKILL_DIR / ".cache"
|
||||||
|
TOKEN_CACHE = CACHE_DIR / "token.json"
|
||||||
|
# Seafile API tokens don't rotate on their own; re-auth weekly to stay safe.
|
||||||
|
TOKEN_TTL_SECONDS = 7 * 24 * 3600
|
||||||
|
|
||||||
|
|
||||||
|
class SeafileClient:
|
||||||
|
def __init__(self, base: Optional[str] = None):
|
||||||
|
self.base = (base or DEFAULT_URL).rstrip("/")
|
||||||
|
self._token: Optional[str] = None
|
||||||
|
|
||||||
|
# -- auth ------------------------------------------------------------------
|
||||||
|
def _read_cache(self) -> Optional[str]:
|
||||||
|
if not TOKEN_CACHE.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
c = json.loads(TOKEN_CACHE.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return None
|
||||||
|
if c.get("base") != self.base:
|
||||||
|
return None
|
||||||
|
ts = c.get("authorized_at")
|
||||||
|
try:
|
||||||
|
when = datetime.fromisoformat(ts)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if when.tzinfo is None:
|
||||||
|
when = when.replace(tzinfo=timezone.utc)
|
||||||
|
if (datetime.now(timezone.utc) - when).total_seconds() > TOKEN_TTL_SECONDS:
|
||||||
|
return None
|
||||||
|
return c.get("token")
|
||||||
|
|
||||||
|
def _write_cache(self, token: str) -> None:
|
||||||
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
TOKEN_CACHE.write_text(json.dumps({
|
||||||
|
"base": self.base, "token": token,
|
||||||
|
"authorized_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}, indent=2), encoding="utf-8")
|
||||||
|
try:
|
||||||
|
os.chmod(TOKEN_CACHE, 0o600)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _authorize(self) -> str:
|
||||||
|
user = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_USER)
|
||||||
|
pw = bc.vault_get_field(VAULT_ENTRY, VAULT_FIELD_PASS)
|
||||||
|
form = urllib.parse.urlencode({"username": user, "password": pw}).encode()
|
||||||
|
req = bc.urllib.request.Request(
|
||||||
|
f"{self.base}/api2/auth-token/", data=form, method="POST",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||||
|
try:
|
||||||
|
with bc.urllib.request.urlopen(req, timeout=bc.HTTP_TIMEOUT) as resp:
|
||||||
|
body = json.loads(resp.read().decode("utf-8", errors="replace"))
|
||||||
|
except bc.urllib.error.HTTPError as exc:
|
||||||
|
raw = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise bc.BackupError(f"Seafile auth-token failed (HTTP {exc.code}): {raw[:300]}")
|
||||||
|
except bc.urllib.error.URLError as exc:
|
||||||
|
raise bc.BackupError(f"Seafile auth request failed: {exc}")
|
||||||
|
token = body.get("token")
|
||||||
|
if not token:
|
||||||
|
raise bc.BackupError(f"Seafile auth-token response missing token: {body}")
|
||||||
|
self._write_cache(token)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def _tok(self) -> str:
|
||||||
|
if not self._token:
|
||||||
|
self._token = self._read_cache() or self._authorize()
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
def _get(self, path: str, params: Optional[dict] = None, retry_auth: bool = True) -> Any:
|
||||||
|
url = f"{self.base}{path}"
|
||||||
|
if params:
|
||||||
|
url += "?" + urllib.parse.urlencode(params)
|
||||||
|
status, body = bc.http_json("GET", url, headers={"Authorization": f"Token {self._tok()}"})
|
||||||
|
if status in (401, 403) and retry_auth:
|
||||||
|
self._token = self._authorize()
|
||||||
|
return self._get(path, params, retry_auth=False)
|
||||||
|
if status != 200:
|
||||||
|
raise bc.BackupError(f"Seafile GET {path} failed (HTTP {status}): {body}", status=status)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# -- read methods ----------------------------------------------------------
|
||||||
|
def server_info(self) -> dict:
|
||||||
|
return self._get("/api2/server-info/")
|
||||||
|
|
||||||
|
def list_users(self) -> list[dict]:
|
||||||
|
"""All users via the admin API, paginated. Includes quota_usage (bytes)."""
|
||||||
|
users: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
body = self._get("/api/v2.1/admin/users/", {"page": page, "per_page": 100})
|
||||||
|
batch = body.get("data", []) if isinstance(body, dict) else []
|
||||||
|
users.extend(batch)
|
||||||
|
info = body.get("page_info") if isinstance(body, dict) else None
|
||||||
|
# Admin users endpoint returns has_next_page in page_info on some
|
||||||
|
# versions; else stop when a short page comes back.
|
||||||
|
if info and not info.get("has_next_page"):
|
||||||
|
break
|
||||||
|
if not info and len(batch) < 100:
|
||||||
|
break
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
return users
|
||||||
|
|
||||||
|
def list_libraries(self) -> list[dict]:
|
||||||
|
"""All libraries (repos) via the admin API, paginated. Includes size + owner."""
|
||||||
|
repos: list[dict] = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
body = self._get("/api/v2.1/admin/libraries/", {"page": page, "per_page": 100})
|
||||||
|
batch = body.get("repos", []) if isinstance(body, dict) else []
|
||||||
|
repos.extend(batch)
|
||||||
|
info = body.get("page_info") if isinstance(body, dict) else None
|
||||||
|
if info and not info.get("has_next_page"):
|
||||||
|
break
|
||||||
|
if not info and len(batch) < 100:
|
||||||
|
break
|
||||||
|
if not batch:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
return repos
|
||||||
|
|
||||||
|
def list_devices(self, user_email: Optional[str] = None) -> list[dict]:
|
||||||
|
"""Sync/desktop devices seen by the server (admin). Optional per-user filter.
|
||||||
|
|
||||||
|
Proves a machine actually connected a Seafile/SeaDrive client. Endpoint
|
||||||
|
availability varies by Seafile version; returns [] if unsupported.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
body = self._get("/api/v2.1/admin/devices/", {"page": 1, "per_page": 500})
|
||||||
|
except bc.BackupError:
|
||||||
|
return []
|
||||||
|
devices = body.get("devices", []) if isinstance(body, dict) else []
|
||||||
|
if user_email:
|
||||||
|
devices = [d for d in devices if d.get("user") == user_email]
|
||||||
|
return devices
|
||||||
|
|
||||||
|
# -- write plumbing --------------------------------------------------------
|
||||||
|
def _write(self, method: str, path: str, form: Optional[dict] = None,
|
||||||
|
retry_auth: bool = True) -> Any:
|
||||||
|
"""POST/PUT/DELETE against the admin API using form-encoding.
|
||||||
|
|
||||||
|
The Seafile admin endpoints read from request.data; the share endpoints
|
||||||
|
use QueryDict.getlist('share_to'), which only works for form-encoded
|
||||||
|
bodies (not JSON) -- so all writes go out as form-urlencoded via the
|
||||||
|
shared transport (bc.http_json form=). Any 2xx is success; on error we
|
||||||
|
surface the server's message.
|
||||||
|
"""
|
||||||
|
status, parsed = bc.http_json(
|
||||||
|
method, f"{self.base}{path}", form=form or {},
|
||||||
|
headers={"Authorization": f"Token {self._tok()}"})
|
||||||
|
if status in (401, 403) and retry_auth:
|
||||||
|
self._token = self._authorize()
|
||||||
|
return self._write(method, path, form, retry_auth=False)
|
||||||
|
if not (200 <= status < 300):
|
||||||
|
msg = ""
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
msg = parsed.get("error_msg") or parsed.get("detail") or ""
|
||||||
|
if not msg:
|
||||||
|
msg = json.dumps(parsed)[:400] if parsed else ""
|
||||||
|
raise bc.BackupError(f"Seafile {method} {path} failed (HTTP {status}): {msg}",
|
||||||
|
status=status)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _uenc(email: str) -> str:
|
||||||
|
return urllib.parse.quote(email, safe="")
|
||||||
|
|
||||||
|
# -- user writes -----------------------------------------------------------
|
||||||
|
def create_user(self, email: str, password: str, name: Optional[str] = None,
|
||||||
|
quota_mb: Optional[int] = None, role: Optional[str] = None,
|
||||||
|
is_active: bool = True) -> dict:
|
||||||
|
form: dict = {"email": email, "password": password,
|
||||||
|
"is_active": "true" if is_active else "false"}
|
||||||
|
if name:
|
||||||
|
form["name"] = name
|
||||||
|
if quota_mb is not None:
|
||||||
|
form["quota_total"] = str(int(quota_mb))
|
||||||
|
if role:
|
||||||
|
form["role"] = role
|
||||||
|
return self._write("POST", "/api/v2.1/admin/users/", form)
|
||||||
|
|
||||||
|
def update_user(self, email: str, is_active: Optional[bool] = None,
|
||||||
|
password: Optional[str] = None, name: Optional[str] = None,
|
||||||
|
quota_mb: Optional[int] = None, role: Optional[str] = None) -> dict:
|
||||||
|
form: dict = {}
|
||||||
|
if is_active is not None:
|
||||||
|
form["is_active"] = "true" if is_active else "false"
|
||||||
|
if password is not None:
|
||||||
|
form["password"] = password
|
||||||
|
if name is not None:
|
||||||
|
form["name"] = name
|
||||||
|
if quota_mb is not None:
|
||||||
|
form["quota_total"] = str(int(quota_mb))
|
||||||
|
if role is not None:
|
||||||
|
form["role"] = role
|
||||||
|
if not form:
|
||||||
|
raise bc.BackupError("update_user: nothing to change")
|
||||||
|
return self._write("PUT", f"/api/v2.1/admin/users/{self._uenc(email)}/", form)
|
||||||
|
|
||||||
|
def delete_user(self, email: str) -> dict:
|
||||||
|
return self._write("DELETE", f"/api/v2.1/admin/users/{self._uenc(email)}/")
|
||||||
|
|
||||||
|
# -- library writes --------------------------------------------------------
|
||||||
|
def create_library(self, name: str, owner: Optional[str] = None) -> dict:
|
||||||
|
form: dict = {"name": name}
|
||||||
|
if owner:
|
||||||
|
form["owner"] = owner
|
||||||
|
return self._write("POST", "/api/v2.1/admin/libraries/", form)
|
||||||
|
|
||||||
|
def transfer_library(self, repo_id: str, new_owner: str) -> dict:
|
||||||
|
return self._write("PUT", f"/api/v2.1/admin/libraries/{repo_id}/",
|
||||||
|
{"owner": new_owner})
|
||||||
|
|
||||||
|
def delete_library(self, repo_id: str) -> dict:
|
||||||
|
return self._write("DELETE", f"/api/v2.1/admin/libraries/{repo_id}/")
|
||||||
|
|
||||||
|
# -- sharing writes --------------------------------------------------------
|
||||||
|
def share_library(self, repo_id: str, share_to, permission: str = "rw",
|
||||||
|
share_type: str = "user") -> dict:
|
||||||
|
recipients = share_to if isinstance(share_to, list) else [share_to]
|
||||||
|
form = {"share_type": share_type, "permission": permission,
|
||||||
|
"share_to": recipients}
|
||||||
|
return self._write("POST", f"/api/v2.1/admin/libraries/{repo_id}/shares/", form)
|
||||||
|
|
||||||
|
def unshare_library(self, repo_id: str, share_to: str,
|
||||||
|
share_type: str = "user") -> dict:
|
||||||
|
return self._write("DELETE", f"/api/v2.1/admin/libraries/{repo_id}/shares/",
|
||||||
|
{"share_type": share_type, "share_to": share_to})
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
c = SeafileClient()
|
||||||
|
info = c.server_info()
|
||||||
|
print(f"[OK] Seafile reachable at {c.base}; version={info.get('version')} "
|
||||||
|
f"edition={'pro' if 'seafile-pro' in (info.get('features') or []) else 'ce'}")
|
||||||
|
return 0
|
||||||
|
except bc.BackupError as exc:
|
||||||
|
print(f"[ERROR] {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
171
.claude/skills/tailscale/SKILL.md
Normal file
171
.claude/skills/tailscale/SKILL.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
---
|
||||||
|
name: tailscale
|
||||||
|
description: "Manage an ACG-administered Tailscale tailnet via the Tailscale REST API v2: list/inspect devices, delete a device/node, authorize a device, list/create/revoke tagged pre-auth keys. ADMIN rights (add + delete). Reads run freely; every device delete/authorize and key create/delete is gated --confirm. Triggers: tailscale, tailnet, add device, delete device/node, revoke/create auth key, tailscale offboard."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tailscale Skill
|
||||||
|
|
||||||
|
Standalone bash CLI for a Tailscale tailnet that ACG administers, via the **Tailscale
|
||||||
|
REST API v2** (`https://api.tailscale.com/api/v2`). Read-only by default; every
|
||||||
|
destructive or state-changing operation (delete a device, authorize a device, create an
|
||||||
|
auth key, revoke an auth key) is **gated behind `--confirm`** and prints a preview first.
|
||||||
|
|
||||||
|
This skill implements the API side of ACG's **per-client tailnet doctrine** — see
|
||||||
|
`wiki/patterns/tailscale-client-management.md`. One tailnet per client, ACG holds an
|
||||||
|
Admin/Owner seat, devices enroll as **tagged nodes via reusable + pre-approved pre-auth
|
||||||
|
keys** pushed from GuruRMM. This skill is how you drive that tailnet from the terminal:
|
||||||
|
mint the tagged keys, watch devices land, authorize/prune nodes, and revoke keys at
|
||||||
|
offboarding.
|
||||||
|
|
||||||
|
## Running the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TS="bash $CLAUDETOOLS_ROOT/.claude/skills/tailscale/scripts/tailscale-api.sh"
|
||||||
|
$TS status # auth check + tailnet + device count
|
||||||
|
$TS devices # list devices (table)
|
||||||
|
$TS devices --json # machine output
|
||||||
|
$TS device <name|id|100.x> # device detail
|
||||||
|
$TS keys # list auth keys
|
||||||
|
$TS create-key --tag tag:roberts --reusable --preauth --expiry-days 90 --desc "onboard" --confirm
|
||||||
|
$TS delete-key <keyId> --confirm # revoke an auth key
|
||||||
|
$TS delete-device <name|id|100.x> --confirm # remove a node
|
||||||
|
$TS authorize <name|id|100.x> --confirm # approve a device
|
||||||
|
$TS --help # full usage (runs with no creds)
|
||||||
|
```
|
||||||
|
|
||||||
|
`--vault <path>` selects a different vault entry (per-client tailnets each have their own).
|
||||||
|
`CLAUDETOOLS_ROOT` resolves from the env var, else the repo root via `git rev-parse`,
|
||||||
|
else `C:/claudetools`.
|
||||||
|
|
||||||
|
## Credentials & auth (vault-first, OAuth preferred)
|
||||||
|
|
||||||
|
The bearer is NEVER hardcoded. At runtime it is resolved from the SOPS vault entry
|
||||||
|
`tailscale/api-access.sops.yaml` (override with `--vault` or `TAILSCALE_VAULT`). Two
|
||||||
|
credential shapes are supported, **OAuth preferred**:
|
||||||
|
|
||||||
|
- **OAuth client (preferred, non-expiring):** `credentials.client_id` +
|
||||||
|
`credentials.client_secret`. The CLI POSTs
|
||||||
|
`grant_type=client_credentials&client_id=...&client_secret=...` (secret fed over
|
||||||
|
**stdin**, never argv) to `/oauth/token` and uses the returned short-lived
|
||||||
|
`access_token` as the bearer. Required scopes: **`devices:core`** (list/delete/authorize
|
||||||
|
devices) and **`auth_keys`** (create/list/delete keys).
|
||||||
|
- **Raw API access token (fallback):** `credentials.api_key` (a `tskey-api-...` token)
|
||||||
|
used directly as `Authorization: Bearer <token>`.
|
||||||
|
|
||||||
|
**Tailnet** is a path segment, resolved (first hit wins) from: env `TAILSCALE_TAILNET`,
|
||||||
|
`credentials.tailnet`, top-level plaintext `tailnet:`, else default **`-`** (the default
|
||||||
|
tailnet of the authenticated principal). Set a specific org name (e.g. `example.com`) only
|
||||||
|
when the account administers more than one tailnet.
|
||||||
|
|
||||||
|
The bearer is written to a `0600` temp curl config (`-K`) and removed on exit, so it never
|
||||||
|
appears in argv, the process list, or stdout.
|
||||||
|
|
||||||
|
### Provision the vault entry (admin, one-time — entry does NOT exist yet)
|
||||||
|
|
||||||
|
OAuth client (preferred):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
|
||||||
|
--kind oauth-client --name "ACG Tailscale API (OAuth client)" \
|
||||||
|
--url https://api.tailscale.com/api/v2 --tag tailscale \
|
||||||
|
--set client_id='<OAUTH_CLIENT_ID>' \
|
||||||
|
--set client_secret='<OAUTH_CLIENT_SECRET>' \
|
||||||
|
--set tailnet='-'
|
||||||
|
```
|
||||||
|
|
||||||
|
Raw API access token (fallback):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
|
||||||
|
--kind api-key --name "ACG Tailscale API (access token)" \
|
||||||
|
--url https://api.tailscale.com/api/v2 --tag tailscale \
|
||||||
|
--set api_key='tskey-api-XXXXXXXXXXXXXXXX' \
|
||||||
|
--set tailnet='-'
|
||||||
|
```
|
||||||
|
|
||||||
|
`vault-helper.sh new` places every `--set` under the encrypted `credentials:` block, so
|
||||||
|
`tailnet` reads back via `credentials.tailnet` (the CLI also honors a top-level plaintext
|
||||||
|
`tailnet:` if an admin prefers it in cleartext). After creating: `vault-helper.sh verify
|
||||||
|
tailscale/api-access.sops.yaml`, then publish with `bash .claude/scripts/sync.sh`. For a
|
||||||
|
**per-client** tailnet, store its own entry (e.g. `tailscale/roberts.sops.yaml`) and pass
|
||||||
|
`--vault tailscale/roberts.sops.yaml`.
|
||||||
|
|
||||||
|
## Endpoint map (Tailscale REST API v2)
|
||||||
|
|
||||||
|
| Subcommand | Method + path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `devices` | `GET /tailnet/{tailnet}/devices?fields=all` | full device inventory |
|
||||||
|
| `device <q>` | `GET /device/{deviceId}?fields=all` | detail after name->id resolve |
|
||||||
|
| `delete-device <q>` | `DELETE /device/{deviceId}` | **GATED** |
|
||||||
|
| `authorize <q>` | `POST /device/{deviceId}/authorized` `{"authorized":true}` | **GATED** |
|
||||||
|
| `keys` | `GET /tailnet/{tailnet}/keys` | list auth keys |
|
||||||
|
| `create-key` | `POST /tailnet/{tailnet}/keys` | **GATED**; tagged reusable/preauth pattern |
|
||||||
|
| `delete-key <keyId>` | `DELETE /tailnet/{tailnet}/keys/{keyId}` | **GATED** revoke |
|
||||||
|
| `status` | `GET .../devices` | reachability + counts |
|
||||||
|
|
||||||
|
`create-key` body (matches the wiki's reusable + pre-approved + tagged pattern):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"capabilities":{"devices":{"create":{"reusable":true,"ephemeral":false,"preauthorized":true,"tags":["tag:roberts"]}}},"expirySeconds":7776000,"description":"onboard roberts"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Built from `--tag` (repeatable), `--reusable`, `--preauth`, `--ephemeral`,
|
||||||
|
`--expiry-days N` (default 90), `--desc`. The created key secret is returned **once** — the
|
||||||
|
CLI prints a `[WARNING]` and the secret; store it in the vault immediately, never in
|
||||||
|
chat/commit.
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
- **Read before write.** Device ops accept a NAME, the stable numeric/node id, or a 100.x
|
||||||
|
address; the name is resolved to an id against the live device list before any action.
|
||||||
|
- **Gated destructive ops.** `delete-device`, `authorize`, `create-key`, `delete-key`
|
||||||
|
refuse to run without `--confirm` — they print the exact preview and exit 3. Never run a
|
||||||
|
delete casually; deleting a node drops it off the tailnet.
|
||||||
|
- **Ambiguous-match STOP.** If a device query matches >1 device, the CLI lists the
|
||||||
|
candidates and refuses to act (exit 4). Re-run with a specific id or a unique name/address.
|
||||||
|
- **Per-client isolation.** Never merge a client into ACG's own tailnet and never share one
|
||||||
|
tailnet across clients — one tailnet per client, one vault entry per tailnet
|
||||||
|
(`wiki/patterns/tailscale-client-management.md`). Confirm you are pointed at the right
|
||||||
|
`--vault`/tailnet before a write.
|
||||||
|
- **Secrets stay out of argv/stdout/logs.** Bearer via `-K` config file; OAuth secret via
|
||||||
|
stdin. Do not paste key secrets into tickets/commits — vault the path.
|
||||||
|
|
||||||
|
## Bot alert (after every write)
|
||||||
|
|
||||||
|
Mirroring the `/rmm` and `screenconnect` skills, every successful **write** posts a
|
||||||
|
one-line `[TAILSCALE] ...` alert via `.claude/scripts/post-bot-alert.sh` (soft-fail,
|
||||||
|
ASCII only). Read operations (`status`, `devices`, `device`, `keys`) do NOT alert. Covered
|
||||||
|
writes: `delete-device`, `authorize`, `create-key`, `delete-key`.
|
||||||
|
|
||||||
|
## Verified API response shapes
|
||||||
|
|
||||||
|
- **Device (list item / detail):** `id` (stable numeric string), `nodeId`, `name` (MagicDNS
|
||||||
|
FQDN), `hostname`, `addresses` (`["100.x.y.z", "fd7a:..."]`), `os`, `user`, `tags`
|
||||||
|
(`["tag:roberts"]`), `authorized` (bool), `lastSeen`, `clientVersion`, `updateAvailable`.
|
||||||
|
Device ops use `id`.
|
||||||
|
- **Auth key (list / create):** `id` (keyId), `key` (the secret — **create only, once**),
|
||||||
|
`created`, `expires`, `capabilities.devices.create.{reusable,ephemeral,preauthorized,tags}`,
|
||||||
|
`description`.
|
||||||
|
- **Errors:** JSON `{"message":"..."}` with a non-2xx status.
|
||||||
|
|
||||||
|
## Error table
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `[BLOCKED] no Tailscale credential` | vault entry missing / wrong fields | provision per above; check `--vault` path |
|
||||||
|
| `OAuth token exchange failed (HTTP 401)` | bad client_id/secret or missing scopes | verify OAuth client has `devices:core` + `auth_keys` |
|
||||||
|
| `HTTP 403` on a write | token/OAuth scope lacks the capability | add `auth_keys` (keys) / `devices:core` (devices) scope |
|
||||||
|
| `HTTP 404` on device/key | wrong id, or wrong tailnet | check `status` tailnet; re-resolve by name |
|
||||||
|
| `AMBIGUOUS (>N matches)` | name matches several nodes | pass a specific id or a unique name/address |
|
||||||
|
| `no device matches` | typo / device gone | run `devices` to see current inventory |
|
||||||
|
|
||||||
|
On a GENUINE functional error (auth failure, non-2xx API response) the CLI logs to
|
||||||
|
`errorlog.md` via `log-skill-error.sh` (soft-fail). Expected/handled conditions (a `--confirm`
|
||||||
|
gate blocking, an empty match, a preview) are NOT logged.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- Doctrine: `wiki/patterns/tailscale-client-management.md` (per-client tailnets, tagged
|
||||||
|
pre-auth keys, enrollment via GuruRMM, offboarding).
|
||||||
|
- Enrollment script: `wiki/patterns/tailscale-client-enroll.ps1`.
|
||||||
|
- Tailscale API docs: https://tailscale.com/api and https://tailscale.com/kb/1085/auth-keys
|
||||||
293
.claude/skills/tailscale/scripts/tailscale-api.sh
Normal file
293
.claude/skills/tailscale/scripts/tailscale-api.sh
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# tailscale-api.sh — CLI for the ACG Tailscale tailnet via the Tailscale REST API v2
|
||||||
|
# (base: https://api.tailscale.com/api/v2). Read-only by default; every device
|
||||||
|
# delete/authorize and every auth-key create/delete is GATED behind --confirm.
|
||||||
|
#
|
||||||
|
# AUTH (resolved from the SOPS vault, OAuth preferred, token fallback):
|
||||||
|
# vault path: tailscale/api-access.sops.yaml (override: --vault <path> or env TAILSCALE_VAULT)
|
||||||
|
# credentials.client_id + credentials.client_secret -> OAuth client (preferred, non-expiring)
|
||||||
|
# POST /oauth/token -> short-lived bearer
|
||||||
|
# credentials.api_key -> raw API access token (tskey-api-...) fallback
|
||||||
|
# tailnet (path segment): credentials.tailnet | top-level plaintext `tailnet:` | env
|
||||||
|
# TAILSCALE_TAILNET | default `-` (the authenticated principal's default tailnet).
|
||||||
|
#
|
||||||
|
# Secrets never touch argv/stdout: the bearer is written to a 0600 curl config file
|
||||||
|
# (-K) and the OAuth secret is fed to curl over stdin (--data @-).
|
||||||
|
#
|
||||||
|
# Subcommands:
|
||||||
|
# status auth check + tailnet + device count
|
||||||
|
# devices [--json] list devices (name/id/addr/os/user/lastSeen/tags/update/online)
|
||||||
|
# device <name|id|100.x> [--json] device detail
|
||||||
|
# delete-device <name|id|100.x> [--confirm] DELETE a device (GATED)
|
||||||
|
# authorize <name|id|100.x> [--confirm] POST authorized=true (GATED)
|
||||||
|
# keys [--json] list auth keys
|
||||||
|
# create-key [--tag tag:x]... [--reusable] [--preauth] [--ephemeral]
|
||||||
|
# [--expiry-days N] [--desc "..."] [--confirm] create an auth key (GATED)
|
||||||
|
# delete-key <keyId> [--confirm] DELETE/revoke an auth key (GATED)
|
||||||
|
#
|
||||||
|
# Device args accept a NAME, the stable numeric/node id, or a 100.x address; the name is
|
||||||
|
# resolved to an id against the live device list. On an AMBIGUOUS match (>1 device) the
|
||||||
|
# candidates are printed and the op STOPS — it never acts on a guess.
|
||||||
|
#
|
||||||
|
# Usage: bash .claude/skills/tailscale/scripts/tailscale-api.sh <subcommand> [args] [--confirm]
|
||||||
|
# Runs `--help` with no credentials.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
API="https://api.tailscale.com/api/v2"
|
||||||
|
OAUTH_URL="https://api.tailscale.com/api/v2/oauth/token"
|
||||||
|
|
||||||
|
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDETOOLS_ROOT:-C:/claudetools}")"
|
||||||
|
VAULT_SH="$REPO/.claude/scripts/vault.sh"
|
||||||
|
ALERT_SH="$REPO/.claude/scripts/post-bot-alert.sh"
|
||||||
|
|
||||||
|
logerr() { bash "$REPO/.claude/scripts/log-skill-error.sh" "tailscale" "$1" ${2:+--context "$2"} >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit "${1:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- arg parse (global) -------------------------------------------------------
|
||||||
|
VAULT_PATH="${TAILSCALE_VAULT:-tailscale/api-access.sops.yaml}"
|
||||||
|
CONFIRM=0; JSON=0; TAGS=(); REUSABLE=false; PREAUTH=false; EPHEMERAL=false
|
||||||
|
EXPIRY_DAYS=90; DESC=""; POS=()
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help|help) usage 0;;
|
||||||
|
--vault) VAULT_PATH="${2:?--vault needs a path}"; shift 2;;
|
||||||
|
--confirm) CONFIRM=1; shift;;
|
||||||
|
--json) JSON=1; shift;;
|
||||||
|
--tag) TAGS+=("${2:?--tag needs a value e.g. tag:roberts}"); shift 2;;
|
||||||
|
--reusable) REUSABLE=true; shift;;
|
||||||
|
--preauth) PREAUTH=true; shift;;
|
||||||
|
--ephemeral) EPHEMERAL=true; shift;;
|
||||||
|
--expiry-days) EXPIRY_DAYS="${2:?--expiry-days needs N}"; shift 2;;
|
||||||
|
--desc) DESC="${2:-}"; shift 2;;
|
||||||
|
*) POS+=("$1"); shift;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
SUB="${POS[0]:-}"; [ -n "$SUB" ] || usage 2
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || { echo "[ERROR] jq is required"; exit 2; }
|
||||||
|
command -v curl >/dev/null 2>&1 || { echo "[ERROR] curl is required"; exit 2; }
|
||||||
|
|
||||||
|
# ---- credentials + bearer -----------------------------------------------------
|
||||||
|
CURL_CFG="$(mktemp)"; chmod 600 "$CURL_CFG" 2>/dev/null || true
|
||||||
|
trap 'rm -f "$CURL_CFG"' EXIT
|
||||||
|
|
||||||
|
vget() { bash "$VAULT_SH" get-field "$VAULT_PATH" "$1" 2>/dev/null; }
|
||||||
|
|
||||||
|
TAILNET="${TAILSCALE_TAILNET:-}"
|
||||||
|
BEARER=""
|
||||||
|
resolve_auth() {
|
||||||
|
[ -n "$TAILNET" ] || TAILNET="$(vget credentials.tailnet)"
|
||||||
|
[ -n "$TAILNET" ] || TAILNET="$(vget tailnet)"
|
||||||
|
[ -n "$TAILNET" ] || TAILNET="-"
|
||||||
|
|
||||||
|
local cid csec key
|
||||||
|
cid="$(vget credentials.client_id)"; csec="$(vget credentials.client_secret)"
|
||||||
|
if [ -n "$cid" ] && [ -n "$csec" ]; then
|
||||||
|
# OAuth: secret over stdin (not argv); scopes devices:core + auth_keys.
|
||||||
|
local out code body
|
||||||
|
out="$(printf 'grant_type=client_credentials&client_id=%s&client_secret=%s' "$cid" "$csec" \
|
||||||
|
| curl -sS --data @- -w $'\n%{http_code}' "$OAUTH_URL" 2>/dev/null)"
|
||||||
|
code="${out##*$'\n'}"; body="${out%$'\n'*}"
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
BEARER="$(printf '%s' "$body" | jq -r '.access_token // empty')"
|
||||||
|
fi
|
||||||
|
[ -n "$BEARER" ] || { echo "[ERROR] OAuth token exchange failed (HTTP $code) at vault:$VAULT_PATH"; logerr "OAuth token exchange failed" "http=$code vault=$VAULT_PATH"; exit 2; }
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
key="$(vget credentials.api_key)"
|
||||||
|
if [ -n "$key" ]; then BEARER="$key"; return 0; fi
|
||||||
|
|
||||||
|
echo "[BLOCKED] no Tailscale credential at vault:$VAULT_PATH"
|
||||||
|
echo " need credentials.client_id + credentials.client_secret (OAuth) OR credentials.api_key (token)."
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
resolve_auth
|
||||||
|
printf 'header = "Authorization: Bearer %s"\n' "$BEARER" > "$CURL_CFG"
|
||||||
|
|
||||||
|
# ---- API wrapper (HTTP_CODE / HTTP_BODY) --------------------------------------
|
||||||
|
HTTP_CODE=""; HTTP_BODY=""
|
||||||
|
api() { # METHOD PATH [JSON_BODY]
|
||||||
|
local method="$1" path="$2" data="${3:-}" out
|
||||||
|
local args=(-sS -X "$method" -H "Accept: application/json" -K "$CURL_CFG" -w $'\n%{http_code}')
|
||||||
|
if [ -n "$data" ]; then
|
||||||
|
out="$(printf '%s' "$data" | curl "${args[@]}" -H "Content-Type: application/json" --data-binary @- "$API/$path" 2>/dev/null)"
|
||||||
|
else
|
||||||
|
out="$(curl "${args[@]}" "$API/$path" 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
HTTP_CODE="${out##*$'\n'}"; HTTP_BODY="${out%$'\n'*}"
|
||||||
|
}
|
||||||
|
api_ok() { case "$HTTP_CODE" in 2*) return 0;; *) return 1;; esac; }
|
||||||
|
api_fail() { # context
|
||||||
|
echo "[ERROR] Tailscale API $1 -> HTTP $HTTP_CODE"
|
||||||
|
printf '%s\n' "$HTTP_BODY" | jq -r '.message // .' 2>/dev/null || printf '%s\n' "$HTTP_BODY"
|
||||||
|
logerr "API $1 HTTP $HTTP_CODE" "tailnet=$TAILNET vault=$VAULT_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
alert() { [ -f "$ALERT_SH" ] && bash "$ALERT_SH" "$1" >/dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
# ---- device list + name->id resolution ----------------------------------------
|
||||||
|
DEVLIST_JSON=""
|
||||||
|
load_devices() {
|
||||||
|
[ -n "$DEVLIST_JSON" ] && return 0
|
||||||
|
api GET "tailnet/$TAILNET/devices?fields=all"
|
||||||
|
api_ok || { api_fail "list devices"; exit 2; }
|
||||||
|
DEVLIST_JSON="$HTTP_BODY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# echoes the resolved device id on success; on none/ambiguous prints to stderr, non-zero.
|
||||||
|
resolve_device() {
|
||||||
|
local q="$1"; load_devices
|
||||||
|
local filt='.devices[] | select(%s) | .id'
|
||||||
|
local exact
|
||||||
|
exact="$(printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||||
|
'.devices[] | select((.id==$q) or (.nodeId==$q) or ((.addresses//[])|index($q))
|
||||||
|
or ((.hostname//""|ascii_downcase)==($q|ascii_downcase))
|
||||||
|
or ((.name//""|ascii_downcase|split(".")[0])==($q|ascii_downcase))) | .id')"
|
||||||
|
local n; n="$(printf '%s\n' "$exact" | grep -c . || true)"
|
||||||
|
if [ "$n" -eq 1 ]; then printf '%s' "$exact"; return 0; fi
|
||||||
|
if [ "$n" -eq 0 ]; then
|
||||||
|
# substring fallback on name/hostname
|
||||||
|
exact="$(printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||||
|
'.devices[] | select(((.name//""|ascii_downcase)|contains($q|ascii_downcase))
|
||||||
|
or ((.hostname//""|ascii_downcase)|contains($q|ascii_downcase))) | .id')"
|
||||||
|
n="$(printf '%s\n' "$exact" | grep -c . || true)"
|
||||||
|
fi
|
||||||
|
if [ "$n" -eq 1 ]; then printf '%s' "$exact"; return 0; fi
|
||||||
|
if [ "$n" -eq 0 ]; then echo "[ERROR] no device matches '$q' in tailnet $TAILNET" >&2; return 3; fi
|
||||||
|
echo "[BLOCKED] '$q' is AMBIGUOUS ($n matches) — refusing to act. Candidates:" >&2
|
||||||
|
printf '%s' "$DEVLIST_JSON" | jq -r --arg q "$q" \
|
||||||
|
'.devices[] | select(((.name//""|ascii_downcase)|contains($q|ascii_downcase))
|
||||||
|
or ((.hostname//""|ascii_downcase)|contains($q|ascii_downcase)))
|
||||||
|
| " " + .id + " " + (.name//"?") + " " + ((.addresses//[])|join(","))' >&2
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_label() { # id -> "name (addr)"
|
||||||
|
printf '%s' "$DEVLIST_JSON" | jq -r --arg id "$1" \
|
||||||
|
'.devices[] | select(.id==$id) | (.name//"?") + " (" + ((.addresses//[])|join(","))+")"'
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- subcommands --------------------------------------------------------------
|
||||||
|
case "$SUB" in
|
||||||
|
status)
|
||||||
|
load_devices
|
||||||
|
local_count="$(printf '%s' "$DEVLIST_JSON" | jq -r '.devices | length')"
|
||||||
|
echo "[OK] Tailscale API reachable"
|
||||||
|
echo " tailnet : $TAILNET"
|
||||||
|
echo " auth : $([ -n "$(vget credentials.client_id)" ] && echo OAuth-client || echo api-token)"
|
||||||
|
echo " devices : $local_count"
|
||||||
|
;;
|
||||||
|
|
||||||
|
devices)
|
||||||
|
load_devices
|
||||||
|
if [ "$JSON" = 1 ]; then printf '%s\n' "$DEVLIST_JSON" | jq '.'; exit 0; fi
|
||||||
|
printf '%s' "$DEVLIST_JSON" | jq -r '
|
||||||
|
.devices | sort_by(.name) | .[]
|
||||||
|
| [ (.name//"?"), .id, ((.addresses//[])|join(",")), (.os//"?"),
|
||||||
|
(.user//"?"), (.lastSeen//"?"), ((.tags//[])|join(",")),
|
||||||
|
(if .updateAvailable then "update" else "-" end) ] | @tsv' \
|
||||||
|
| { echo -e "NAME\tID\tADDRESSES\tOS\tUSER\tLASTSEEN\tTAGS\tUPDATE"; cat; } | column -t -s $'\t'
|
||||||
|
;;
|
||||||
|
|
||||||
|
device)
|
||||||
|
Q="${POS[1]:?usage: device <name|id|100.x>}"
|
||||||
|
ID="$(resolve_device "$Q")" || exit $?
|
||||||
|
api GET "device/$ID?fields=all"; api_ok || { api_fail "device detail"; exit 2; }
|
||||||
|
if [ "$JSON" = 1 ]; then printf '%s\n' "$HTTP_BODY" | jq '.'; else
|
||||||
|
printf '%s\n' "$HTTP_BODY" | jq -r '
|
||||||
|
"name : " + (.name//"?"),
|
||||||
|
"id : " + (.id//"?"),
|
||||||
|
"nodeId : " + (.nodeId//"?"),
|
||||||
|
"addresses : " + ((.addresses//[])|join(", ")),
|
||||||
|
"os : " + (.os//"?"),
|
||||||
|
"user : " + (.user//"?"),
|
||||||
|
"tags : " + ((.tags//[])|join(", ")),
|
||||||
|
"authorized : " + ((.authorized//false)|tostring),
|
||||||
|
"lastSeen : " + (.lastSeen//"?"),
|
||||||
|
"clientVer : " + (.clientVersion//"?"),
|
||||||
|
"update : " + ((.updateAvailable//false)|tostring)'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
delete-device)
|
||||||
|
Q="${POS[1]:?usage: delete-device <name|id|100.x> [--confirm]}"
|
||||||
|
ID="$(resolve_device "$Q")" || exit $?
|
||||||
|
LBL="$(dev_label "$ID")"
|
||||||
|
if [ "$CONFIRM" != 1 ]; then
|
||||||
|
echo "[BLOCKED] would DELETE device $ID $LBL from tailnet $TAILNET"
|
||||||
|
echo " re-run with --confirm to delete."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
api DELETE "device/$ID"; api_ok || { api_fail "delete device"; exit 2; }
|
||||||
|
echo "[OK] deleted device $ID $LBL"
|
||||||
|
alert "[TAILSCALE] deleted device $LBL ($ID) from tailnet $TAILNET"
|
||||||
|
;;
|
||||||
|
|
||||||
|
authorize)
|
||||||
|
Q="${POS[1]:?usage: authorize <name|id|100.x> [--confirm]}"
|
||||||
|
ID="$(resolve_device "$Q")" || exit $?
|
||||||
|
LBL="$(dev_label "$ID")"
|
||||||
|
if [ "$CONFIRM" != 1 ]; then
|
||||||
|
echo "[BLOCKED] would AUTHORIZE device $ID $LBL on tailnet $TAILNET"
|
||||||
|
echo " re-run with --confirm to authorize."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
api POST "device/$ID/authorized" '{"authorized":true}'; api_ok || { api_fail "authorize device"; exit 2; }
|
||||||
|
echo "[OK] authorized device $ID $LBL"
|
||||||
|
alert "[TAILSCALE] authorized device $LBL ($ID) on tailnet $TAILNET"
|
||||||
|
;;
|
||||||
|
|
||||||
|
keys)
|
||||||
|
api GET "tailnet/$TAILNET/keys"; api_ok || { api_fail "list keys"; exit 2; }
|
||||||
|
if [ "$JSON" = 1 ]; then printf '%s\n' "$HTTP_BODY" | jq '.'; exit 0; fi
|
||||||
|
printf '%s' "$HTTP_BODY" | jq -r '
|
||||||
|
.keys // [] | .[]
|
||||||
|
| [ .id, (.description//"-"),
|
||||||
|
((.capabilities.devices.create.tags//[])|join(",") // "-"),
|
||||||
|
((.capabilities.devices.create.reusable//false)|tostring),
|
||||||
|
(.created//"?"), (.expires//"?") ] | @tsv' \
|
||||||
|
| { echo -e "KEYID\tDESC\tTAGS\tREUSABLE\tCREATED\tEXPIRES"; cat; } | column -t -s $'\t'
|
||||||
|
;;
|
||||||
|
|
||||||
|
create-key)
|
||||||
|
TAGS_JSON="$(printf '%s\n' "${TAGS[@]:-}" | jq -R . | jq -s 'map(select(length>0))')"
|
||||||
|
EXPIRY_SECONDS=$(( EXPIRY_DAYS * 86400 ))
|
||||||
|
BODY="$(jq -n \
|
||||||
|
--argjson reusable "$REUSABLE" --argjson ephemeral "$EPHEMERAL" --argjson preauth "$PREAUTH" \
|
||||||
|
--argjson expiry "$EXPIRY_SECONDS" --arg desc "$DESC" --argjson tags "$TAGS_JSON" \
|
||||||
|
'{capabilities:{devices:{create:{reusable:$reusable,ephemeral:$ephemeral,preauthorized:$preauth,tags:$tags}}},expirySeconds:$expiry,description:$desc}')"
|
||||||
|
if [ "$CONFIRM" != 1 ]; then
|
||||||
|
echo "[BLOCKED] would CREATE auth key on tailnet $TAILNET with:"
|
||||||
|
printf '%s\n' "$BODY" | jq '.'
|
||||||
|
echo " re-run with --confirm to create."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
api POST "tailnet/$TAILNET/keys" "$BODY"; api_ok || { api_fail "create key"; exit 2; }
|
||||||
|
KID="$(printf '%s' "$HTTP_BODY" | jq -r '.id // "?"')"
|
||||||
|
echo "[OK] created auth key $KID (tags: $(printf '%s' "$TAGS_JSON" | jq -r 'join(",")'))"
|
||||||
|
echo "[WARNING] the key secret is shown ONCE below — store it in the vault, never in chat/commit:"
|
||||||
|
printf '%s' "$HTTP_BODY" | jq -r '.key // "(no key field returned)"'
|
||||||
|
alert "[TAILSCALE] created auth key $KID on tailnet $TAILNET (tags: $(printf '%s' "$TAGS_JSON" | jq -r 'join(",")'))"
|
||||||
|
;;
|
||||||
|
|
||||||
|
delete-key)
|
||||||
|
KID="${POS[1]:?usage: delete-key <keyId> [--confirm]}"
|
||||||
|
if [ "$CONFIRM" != 1 ]; then
|
||||||
|
echo "[BLOCKED] would DELETE/revoke auth key $KID on tailnet $TAILNET"
|
||||||
|
echo " re-run with --confirm to revoke."
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
api DELETE "tailnet/$TAILNET/keys/$KID"; api_ok || { api_fail "delete key"; exit 2; }
|
||||||
|
echo "[OK] revoked auth key $KID"
|
||||||
|
alert "[TAILSCALE] revoked auth key $KID on tailnet $TAILNET"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "[ERROR] unknown subcommand: $SUB"
|
||||||
|
usage 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
116
.claude/skills/yealink-ymcs/docs/SIPSERVER_SCHEMA.md
Normal file
116
.claude/skills/yealink-ymcs/docs/SIPSERVER_SCHEMA.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# YMCS SIP Account Schema - Critical Field Formats
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X, pages 59-60 (chunk-03)
|
||||||
|
|
||||||
|
## Add SIP Account Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v2/dm/sipAccounts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Request Body Schema
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| registerName | String | Yes | Registered name, max 128 chars |
|
||||||
|
| username | String | Yes | Username, max 128 chars |
|
||||||
|
| password | String | Yes | Password, max 128 chars |
|
||||||
|
| displayName | String | No | Display name, max 128 chars |
|
||||||
|
| label | String | No | Label, max 128 chars |
|
||||||
|
| **sipServer1** | **SipServer** | **Yes** | **SIP server 1 address (OBJECT, not string)** |
|
||||||
|
| sipServer2 | SipServer | No | SIP server 2 address (object) |
|
||||||
|
| remark | String | No | Note, max 512 chars |
|
||||||
|
| siteId | String | No | Site ID to assign |
|
||||||
|
|
||||||
|
## SipServer Object Definition
|
||||||
|
|
||||||
|
**CRITICAL:** sipServer1/sipServer2 are **objects**, not strings.
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| host | String | Server address, max 256 chars |
|
||||||
|
| port | Integer | Server port, 0-65535 |
|
||||||
|
|
||||||
|
## Working Example (from official docs)
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /v2/dm/sipAccounts HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"registerName": "2552",
|
||||||
|
"username": "2552",
|
||||||
|
"password": "******",
|
||||||
|
"label": "2552",
|
||||||
|
"displayName": "2552",
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "ume.yealink.com",
|
||||||
|
"port": 5061
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PacketDial Integration Example
|
||||||
|
|
||||||
|
For provisioning to PacketDial/NetSapiens PBX:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registerName": "100@vwp.91912.service",
|
||||||
|
"username": "100@vwp.91912.service",
|
||||||
|
"password": "TestPass123",
|
||||||
|
"label": "Extension 100",
|
||||||
|
"displayName": "Test User",
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "pbx.packetdial.com",
|
||||||
|
"port": 5060
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP 412 Error Resolution
|
||||||
|
|
||||||
|
**Error:** `HTTP 412: {"code": "900444", "details": [{"field": "sipServer1", "message": "Parameter error, please refer to documentation related to interface parameters"}]}`
|
||||||
|
|
||||||
|
**Cause:** Passing sipServer1 as a string instead of an object
|
||||||
|
|
||||||
|
**Fix:** Use the SipServer object format with both `host` and `port` fields
|
||||||
|
|
||||||
|
## Response (201 Created)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "604e67944c7c43fe8b66099254ec3439",
|
||||||
|
"username": "2552",
|
||||||
|
"registerInfo": "2552",
|
||||||
|
"serverAddress": "ume.yealink.com:5061",
|
||||||
|
"accountType": 0,
|
||||||
|
"remark": "",
|
||||||
|
"createTime": 1234567890000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response fields:
|
||||||
|
- `id`: Account ID (use for binding to devices)
|
||||||
|
- `accountType`: 0=SIP, 1=H323, 2=SFB
|
||||||
|
- `serverAddress`: Combined host:port from sipServer1
|
||||||
|
- `createTime`: Unix timestamp in milliseconds
|
||||||
|
|
||||||
|
## Next Step: Bind Account to Device
|
||||||
|
|
||||||
|
After creating the SIP account, bind it to a phone:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v2/dm/devices/{deviceId}/bindAccounts
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accountIds": ["604e67944c7c43fe8b66099254ec3439"],
|
||||||
|
"lineId": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See chunk-03 pages 52-56 for bindAccounts endpoint details.
|
||||||
104
.claude/skills/yealink-ymcs/docs/chunks/README.md
Normal file
104
.claude/skills/yealink-ymcs/docs/chunks/README.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# YMCS Documentation Chunks
|
||||||
|
|
||||||
|
Large PDF documentation broken into bite-sized markdown files for easier processing.
|
||||||
|
|
||||||
|
## Generated From
|
||||||
|
|
||||||
|
4 YMCS PDFs downloaded 2026-07-09, totaling ~300MB:
|
||||||
|
|
||||||
|
1. **Open API for Yealink Management Cloud Service V4X.pdf** (164 pages, 1.1MB)
|
||||||
|
- 9 chunks, 20 pages each
|
||||||
|
- Directory: `open-api-for-yealink-management-cloud-service-v4x/`
|
||||||
|
- **KEY:** SIP account schema (chunk-03, pages 41-60)
|
||||||
|
|
||||||
|
2. **YMCS 4X Enterprise Open API Webhook.pdf** (5 pages, 47KB)
|
||||||
|
- 1 chunk
|
||||||
|
- Directory: `ymcs-4x-enterprise-open-api-webhook/`
|
||||||
|
|
||||||
|
3. **Yealink Management Cloud Service(YMCS)_User Guide.pdf** (953 pages, 90MB)
|
||||||
|
- 48 chunks, 20 pages each
|
||||||
|
- Directory: `yealink-management-cloud-serviceymcs_user-guide/`
|
||||||
|
|
||||||
|
4. **Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf** (84 pages, 210MB)
|
||||||
|
- 5 chunks, 20 pages each
|
||||||
|
- Directory: `yealink-management-cloud-serviceymcs_channel-user-guide/`
|
||||||
|
|
||||||
|
**Total:** 63 markdown files, ~1MB
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Find SIP Account Provisioning
|
||||||
|
- **chunk-03** (pages 41-60): Add SIP account endpoint and schema
|
||||||
|
- **chunk-04** (pages 61-80): SIP account management operations
|
||||||
|
- See: `../SIPSERVER_SCHEMA.md` for critical field formats
|
||||||
|
|
||||||
|
### Find Device Management
|
||||||
|
- **chunk-02** (pages 21-40): Device operations
|
||||||
|
- **chunk-03** (pages 41-60): Bind/unbind accounts to devices
|
||||||
|
|
||||||
|
### Search All Chunks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for a topic across all chunks
|
||||||
|
grep -r "your search term" .
|
||||||
|
|
||||||
|
# Find which chunk contains specific API endpoint
|
||||||
|
grep -r "POST /v2/dm/" .
|
||||||
|
|
||||||
|
# List all chunks for a specific PDF
|
||||||
|
ls -1 open-api-for-yealink-management-cloud-service-v4x/
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Chunks Were Created
|
||||||
|
|
||||||
|
Script: `../scripts/chunk-pdf-docs.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Process all PDFs
|
||||||
|
python3 ../scripts/chunk-pdf-docs.py --all --pages-per-chunk 20
|
||||||
|
|
||||||
|
# Process single PDF
|
||||||
|
python3 ../scripts/chunk-pdf-docs.py ~/Downloads/example.pdf --output-dir ./custom-output
|
||||||
|
|
||||||
|
# Extract just table of contents
|
||||||
|
python3 ../scripts/chunk-pdf-docs.py ~/Downloads/example.pdf --toc-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Naming Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
{PDF-stem}-chunk-{number:02d}-p{start}-{end}.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `Open API for Yealink Management Cloud Service V4X-chunk-03-p41-60.md`
|
||||||
|
- `Yealink Management Cloud Service(YMCS)_User Guide-chunk-01-p1-20.md`
|
||||||
|
|
||||||
|
Each chunk includes:
|
||||||
|
- Source PDF name
|
||||||
|
- Page range (X-Y of total)
|
||||||
|
- Chunk number
|
||||||
|
- Full extracted text with layout preserved
|
||||||
|
|
||||||
|
## Critical Findings
|
||||||
|
|
||||||
|
### SIP Account sipServer1 Field (HTTP 412 Fix)
|
||||||
|
|
||||||
|
**Problem:** YMCS API returns HTTP 412 "Parameter error" on sipServer1 field
|
||||||
|
|
||||||
|
**Solution:** sipServer1 is an **object**, not a string:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "pbx.packetdial.com",
|
||||||
|
"port": 5060
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See `../SIPSERVER_SCHEMA.md` for complete schema and examples.
|
||||||
|
|
||||||
|
## Tools Used
|
||||||
|
|
||||||
|
- **poppler-utils** (pdftotext, pdfinfo) - PDF text extraction
|
||||||
|
- Install: `brew install poppler`
|
||||||
|
- **Python 3** - Chunking script
|
||||||
@@ -0,0 +1,825 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 1-20
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 1-20 of 164
|
||||||
|
**Chunk:** 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Open API for Yealink Management
|
||||||
|
Cloud Service V4X
|
||||||
|
1. YMCS APIs
|
||||||
|
1.1 Introduction
|
||||||
|
YMCS Open API provides third-party developers with a portal to securely use
|
||||||
|
device management services. Through YMCS APIs, developers can use YMCS's
|
||||||
|
device management, account management, configuration management and
|
||||||
|
other functions.
|
||||||
|
YMCS API is a REST-like API based on HTTP. The REST-like style indicates that the
|
||||||
|
resource is tagged using a URI and allows access to the API via the HTTP
|
||||||
|
protocol. The API relies on HTTP semantics and methods.
|
||||||
|
In order to ensure the security of using the YMCS API, the transport protocol is
|
||||||
|
unified using HTTPS and all requests need to be authenticated. If the YMCS API
|
||||||
|
is used without carrying the correct identity credentials information, the request
|
||||||
|
will be rejected outright. More information about YMCS authentication will be
|
||||||
|
detailed in 1.2.2 Authentication.
|
||||||
|
The YMCS platform limits the frequency of API calls to ensure system stability.
|
||||||
|
Most endpoints are rate-limited to 50 requests per second. This rate limit is at
|
||||||
|
the enterprise level, for each enterprise that supports the YMCS API, we allow 50
|
||||||
|
requests per second. More information on rate limiting is detailed in Invoking Rate
|
||||||
|
Limiting.
|
||||||
|
|
||||||
|
1.2 Use YMCS APIs
|
||||||
|
YMCS APIs allow developers to access and manipulate resources under YMCS,
|
||||||
|
including but not limited to device management, account management,
|
||||||
|
configuration management, and other operations. This chapter will describe
|
||||||
|
how to correctly call the YMCS API.
|
||||||
|
YMCS API
|
||||||
|
1 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
Version: 1.0.0
|
||||||
|
Host:
|
||||||
|
If your enterprise is in the EU region: eu-api.ymcs.yealink.com
|
||||||
|
If your enterprise is in the US region: us-api.ymcs.yealink.com
|
||||||
|
If your enterprise is in the AU region: au-api.ymcs.yealink.com
|
||||||
|
Protocols: HTTPS
|
||||||
|
Accepts: application/json
|
||||||
|
Responds With: application/json
|
||||||
|
|
||||||
|
Authentication
|
||||||
|
Every HTTP request made to the YMCS API must go through authentication. This
|
||||||
|
operation is to ensure whether the client accessing the service is a registered
|
||||||
|
user of the system. During the process of authentication, OAuth2.0 protocol will
|
||||||
|
be used.
|
||||||
|
Before calling the API, you need to obtain the Client ID and Client Secret from
|
||||||
|
the YMCS platform to apply for an access token. An enterprise can only apply for
|
||||||
|
one set of Client ID and Client Secret.
|
||||||
|
Process Description
|
||||||
|
The process for a user to request access tokens and initiate requests is as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1. The third-party application server initiates and sends a request, carrying the Client ID and Client Secret, to the
|
||||||
|
YMCS API server to access a token.
|
||||||
|
2. The API server verifies whether the Client ID and Client Secret information is correct.
|
||||||
|
3. Yealink API server returns access token after successful verification.
|
||||||
|
4. The third-party application server initiates a business request and carries an access token.
|
||||||
|
5. Yealink API server checks for the existence of an access token, and then verifies the validity of the access
|
||||||
|
token.
|
||||||
|
6. Yealink API server forwards the request to the Yealink business server.
|
||||||
|
7. Yealink business server will return the processed result to the Yealink API server.
|
||||||
|
8. The API server will pass the response result to the third-party application server.
|
||||||
|
|
||||||
|
|
||||||
|
If the access token is invalid, you will need to provide the corresponding code
|
||||||
|
to retrieve the token from the server again.
|
||||||
|
|
||||||
|
Apply for access token
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/token
|
||||||
|
Request parameters
|
||||||
|
|
||||||
|
3 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Param Parame Data Requ Description
|
||||||
|
eter ter Type ired
|
||||||
|
Type
|
||||||
|
Author Header String Yes Basic
|
||||||
|
ization base64Encode(client_id:client_secret),
|
||||||
|
connect Client ID and Client Secret with
|
||||||
|
a colon, and then perform Base64
|
||||||
|
encoding.
|
||||||
|
times Header String Yes Timestamp, the number of
|
||||||
|
tamp milliseconds from January 1, 1970,
|
||||||
|
00:00:00 to now.
|
||||||
|
nonce Header String Yes Random number, maximum length 32
|
||||||
|
bits
|
||||||
|
grant_ Body String Yes client_credentials
|
||||||
|
type
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter transmission exception, see exception response
|
||||||
|
parameters for details.
|
||||||
|
401 Authentication failed, see exception response parameters.
|
||||||
|
500 Server exception, see exception response parameters for details.
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
access_token String access token
|
||||||
|
token_type String bearer
|
||||||
|
expires_in Long Access token validity period, in seconds.
|
||||||
|
|
||||||
|
Exception response parameters
|
||||||
|
|
||||||
|
Par Data Description
|
||||||
|
ame Type
|
||||||
|
ter
|
||||||
|
4 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
erro String Provide error codes according to the definition of the OAuth2
|
||||||
|
r protocol. Error coded can be used to categorize and handle
|
||||||
|
errors.
|
||||||
|
cod String Server-defined error code, used for quick problem
|
||||||
|
e localization.
|
||||||
|
requ String Request ID generated by the server to track the execution
|
||||||
|
estI status of the request on the server side. It can help
|
||||||
|
d developers quickly identify issues.
|
||||||
|
mes String Clear and concise error Description that can be understood
|
||||||
|
sage by end users.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/token HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
|
||||||
|
|
||||||
|
{
|
||||||
|
"grant_type": "client_credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example of response parameters
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
Cache-Control: no-store
|
||||||
|
Pragma: no-cache
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "[JWT TOKEN]",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"expires_in": 86400
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Exception response parameters
|
||||||
|
|
||||||
|
httpHTTP/1.1 400 Bad Request
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
|
5 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
Pragma: no-cache
|
||||||
|
|
||||||
|
{
|
||||||
|
"error": "invalid_request",
|
||||||
|
"code": "70011",
|
||||||
|
"requestId": "255d1aef",
|
||||||
|
"message": "The provided value for the input parameter 'grant_type' is not valid."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Initiate business request
|
||||||
|
All API requests must be made via HTTPS. The base URL for requests is https://
|
||||||
|
api.ymcs.yealink.com/v2/. The complete URL varies depending on the resource of
|
||||||
|
|
||||||
|
the operation.
|
||||||
|
Each time you request the API, you must provide 3 HTTP Request Headers, as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
Name Data Type Description
|
||||||
|
Authoriza String Authentication information. The format is: Bearer
|
||||||
|
tion [ACCESS TOKEN]
|
||||||
|
|
||||||
|
timestam String Timestamp, the number of milliseconds from
|
||||||
|
p January 1, 1970, 00:00:00 to now.
|
||||||
|
nonce String Random number, maximum length 32 bits
|
||||||
|
|
||||||
|
HTTP Request Header Example
|
||||||
|
|
||||||
|
httpAuthorization: Bearer [ACCESS TOKEN]
|
||||||
|
timestamp: 1568693976264
|
||||||
|
nonce: 097e0ac619ba41f68f16f1955787feb9
|
||||||
|
|
||||||
|
|
||||||
|
1.3 Error Definitions
|
||||||
|
Yealink API uses HTTP status codes to reflect the success or failure of request
|
||||||
|
operations. 2XX status codes indicate successful operations, while 4XX or 5XX
|
||||||
|
status codes indicate operation errors. If an erroneous status code is received,
|
||||||
|
the error code and error message in the response body can be used to
|
||||||
|
understand the reason for the error.
|
||||||
|
|
||||||
|
6 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Status code Description Scenario example
|
||||||
|
2XX Operation successful
|
||||||
|
400 Request data error Invalid or incomplete
|
||||||
|
request data.
|
||||||
|
401Authenticati Authentication error The request did not carry
|
||||||
|
on error an access token.
|
||||||
|
403 Access to certain resources Authentication failed
|
||||||
|
is not allowed
|
||||||
|
404 No data matching the No data found.
|
||||||
|
request
|
||||||
|
429 Request rate exceeds Request too frequent
|
||||||
|
frequency limit
|
||||||
|
500 Server Error Internal server error
|
||||||
|
|
||||||
|
Error object definition
|
||||||
|
Error object
|
||||||
|
|
||||||
|
Na Data Description
|
||||||
|
me Typ
|
||||||
|
e
|
||||||
|
cod Stri Error codes defined by the server, used for quick problem
|
||||||
|
e ng localization.
|
||||||
|
req Stri Request ID generated by the server, used for tracking the
|
||||||
|
uest ng execution status of requests on the server. It helps developers
|
||||||
|
Id quickly locate issues.
|
||||||
|
mes Stri A simple and clear error Description that can be understood by
|
||||||
|
sag ng the end user.
|
||||||
|
e
|
||||||
|
deta Error List of details that caused the error, may be empty
|
||||||
|
ils Detai
|
||||||
|
l[]
|
||||||
|
|
||||||
|
ErrorDetail object
|
||||||
|
|
||||||
|
Name Data Description
|
||||||
|
Type
|
||||||
|
7 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
field String Error in the request parameter name
|
||||||
|
messag String A simple and clear error Description that can be
|
||||||
|
e understood by the end user.
|
||||||
|
|
||||||
|
Error Response Example
|
||||||
|
|
||||||
|
json{
|
||||||
|
"code": "{errorCode}",
|
||||||
|
"requestId": "{requestId}",
|
||||||
|
"message": "Validation Failed",
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"field": "email",
|
||||||
|
"message": "Invalid field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "type",
|
||||||
|
"message": "Invalid field"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Opera Description Description
|
||||||
|
tional
|
||||||
|
code
|
||||||
|
90020 Operation successful Operate Successfully
|
||||||
|
0
|
||||||
|
90040 Incorrect request parameters Request parameters
|
||||||
|
0 are incorrect
|
||||||
|
90040 The user has not logged in or the login has User is not logged in
|
||||||
|
1 expired, please log in again or the account has
|
||||||
|
expired, please log in
|
||||||
|
again
|
||||||
|
90040 The request has been forbidden. This request is
|
||||||
|
3 forbidden
|
||||||
|
90040 The requested resource cannot be found. Requested resource is
|
||||||
|
8 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4 not found
|
||||||
|
90040 Request timed out, please try again later. Time out, please try
|
||||||
|
8 again
|
||||||
|
90040 Conflict request. Request conflict
|
||||||
|
9
|
||||||
|
90041 Concurrent editing error. Concurrent editing
|
||||||
|
2 error
|
||||||
|
90042 Too many requests. Too Many Requests
|
||||||
|
9
|
||||||
|
90044 Session expired. Login Time-out
|
||||||
|
0
|
||||||
|
90050 The server is busy, please try again later. The server is busy,
|
||||||
|
0 please try again later
|
||||||
|
90050 This operation is not supported. Not Implemented
|
||||||
|
1
|
||||||
|
90050 When a server acting as a gateway or proxy Bad Gateway
|
||||||
|
2 attempts to process a request, it receives an
|
||||||
|
invalid response from the upstream server.
|
||||||
|
90050 Service Unavailable. Service Unavailable
|
||||||
|
3
|
||||||
|
90050 The upstream server is not responding. Gateway Timeout
|
||||||
|
4
|
||||||
|
90051 Internal server error Server Internal Error
|
||||||
|
1
|
||||||
|
90059 Unknown error unknown mistake
|
||||||
|
9
|
||||||
|
90040 Parameters cannot be null Cannot be null
|
||||||
|
0
|
||||||
|
90040 Parameters cannot be null Cannot be empty
|
||||||
|
0
|
||||||
|
90040 The parameter length is incorrect. Incorrect length
|
||||||
|
0
|
||||||
|
90040 ID cannot be empty. ID cannot be empty
|
||||||
|
|
||||||
|
9 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
0
|
||||||
|
90040 Resource does not exist The resource does not
|
||||||
|
0 exist or has been
|
||||||
|
deleted
|
||||||
|
80000 Mac is not legal. Invalid MAC
|
||||||
|
1
|
||||||
|
80000 Invalid SN. SN is invalid
|
||||||
|
2
|
||||||
|
80000 Resource already exists. Resource already
|
||||||
|
3 exists
|
||||||
|
80000 The device has been added by another MAC has been added
|
||||||
|
4 company. by another
|
||||||
|
enterprise/
|
||||||
|
organization
|
||||||
|
80000 Device type illegal Device Type is invalid
|
||||||
|
5
|
||||||
|
80000 The number of devices added in a batch The number of added
|
||||||
|
6 exceeds the limit devices exceeds the
|
||||||
|
limit
|
||||||
|
80000 Invalid parameters. Incorrect parameter
|
||||||
|
7 format
|
||||||
|
80000 Data exceeds limit. Data exceeds limit
|
||||||
|
8
|
||||||
|
80013 Account already exists Account already exists
|
||||||
|
0
|
||||||
|
80020 The authentication username and Username and
|
||||||
|
0 password must appear in pairs. password must
|
||||||
|
appear in pairs
|
||||||
|
|
||||||
|
1.4 Invoke Rate Limiting
|
||||||
|
In order to maintain the reliability of the YMCS API platform, our API has the
|
||||||
|
following rate limits.
|
||||||
|
The rate limit for the normal API is 50 requests/second unless otherwise noted.
|
||||||
|
Please control the frequency of your application calls and do not exceed the
|
||||||
|
10 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
access rate limit or you will receive a 429 status response.
|
||||||
|
Invokers of the YMCS API shall retry Error 429 using the exponential backoff
|
||||||
|
algorithm and with a minimum delay of 30 seconds.
|
||||||
|
|
||||||
|
2. Device Management
|
||||||
|
2.1 Device Basic Information Management
|
||||||
|
2.1.1 Add Devices
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Required Description
|
||||||
|
er Type
|
||||||
|
mac String Yes Device MAC, minimum length 12,
|
||||||
|
maximum length 17 characters
|
||||||
|
sn String Yes SN code, maximum length 128
|
||||||
|
deviceT Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
ype Device
|
||||||
|
modelId String Yes Model ID
|
||||||
|
name String No Device name, maximum length 128
|
||||||
|
siteId String No Site ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
|
||||||
|
11 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device ID.
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The group ID.
|
||||||
|
name String The device name.
|
||||||
|
modelId String The device model ID.
|
||||||
|
siteId String The site ID.
|
||||||
|
programVersion String The version number of the device
|
||||||
|
firmware.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/devices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"siteId":"a624453e1cbb44ecb9bb6ee31731a856",
|
||||||
|
"programVersion":"70.83.0.68"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
12 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
2.1.2 Add Device in Batches
|
||||||
|
Note: You can add up to 100 devices at a time.
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/addDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Type Required Description
|
||||||
|
r
|
||||||
|
mac String Yes Device MAC, minimum length 12,
|
||||||
|
maximum length 17
|
||||||
|
sn String Yes SN code, maximum length 128
|
||||||
|
deviceTyp Integer Yes Device Type 1: Phone Device 3:
|
||||||
|
e Room Device
|
||||||
|
modelId String Yes Model ID
|
||||||
|
name String No Device name, maximum length
|
||||||
|
128
|
||||||
|
siteId String No Site ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
total Integer The total number of batch additions.
|
||||||
|
successCoun Integer The total number of successful additions.
|
||||||
|
t
|
||||||
|
|
||||||
|
13 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors AddError[] Error message.
|
||||||
|
|
||||||
|
AddError object
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The device SN address.
|
||||||
|
errorInfo String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/addDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 1,
|
||||||
|
"successCount":0,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"errorInfo":"device.mac.invalid"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
14 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
2.1.3 Add device without SN
|
||||||
|
Note: You can add up to 100 devices at a time.
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/addDevicesByMac
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Param ParameterT Data Requi Description
|
||||||
|
eter ype Type red
|
||||||
|
mac Body String Yes Device MAC, minimum length
|
||||||
|
12, maximum length 17
|
||||||
|
device Body Integer Yes Device Type 1: Phone Device 3:
|
||||||
|
Type Room Device
|
||||||
|
model Body String Yes Model ID
|
||||||
|
Id
|
||||||
|
name Body String No Device name, maximum length
|
||||||
|
128
|
||||||
|
siteId Body String No Site ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
total Integer The total number of batch additions.
|
||||||
|
successCoun Integer The total number of successful additions.
|
||||||
|
t
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
15 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
errors AddError[] Error message.
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The device SN address.
|
||||||
|
errorInfo String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/addDevicesByMac HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 1,
|
||||||
|
"successCount":0,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"errorInfo":"device.mac.invalid"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.1.4 Delete Device
|
||||||
|
Request Method
|
||||||
|
|
||||||
|
16 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpDELETE /v2/dm/devices/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
2.1.5 Delete Devices in Batches
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/delDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Data Req Description
|
||||||
|
ter Type uire
|
||||||
|
d
|
||||||
|
|
||||||
|
17 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
device Integ Yes Device Type 1: Phone Device 3: Room Device
|
||||||
|
Type er
|
||||||
|
deviceI Strin No Indicates the type of device identifier, default id,
|
||||||
|
dType g optional mac, id
|
||||||
|
deviceI Strin Yes Device identifier, maximum length 200
|
||||||
|
ds g[]
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/delDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
18 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"deviceType":1,
|
||||||
|
"deviceIds":["001565bbb1a9","001567"],
|
||||||
|
"deviceIdType":"mac"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"001567",
|
||||||
|
"msg":"Invalid MAC"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.1.6 Device List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Par Data Req Description
|
||||||
|
ame Type uir
|
||||||
|
ter ed
|
||||||
|
ski Long No Number of skipped records, default is 0
|
||||||
|
p
|
||||||
|
limi Long No Maximum number of records to retrieve, default is 10,
|
||||||
|
t maximum is 100.
|
||||||
|
aut Bool No Whether to respond with the total count, it is
|
||||||
|
oCo ean recommended to set it to true when querying the first
|
||||||
|
unt page, and pass false for subsequent page turns.
|
||||||
|
|
||||||
|
19 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
filte Filter No search parameters
|
||||||
|
r
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Para Dat Re Description
|
||||||
|
mete a qu
|
||||||
|
r Ty ire
|
||||||
|
pe d
|
||||||
|
mac Str No Device MAC fuzzy search keyword, maximum length 17,
|
||||||
|
ing supports with : or -, such as 00:15:65:bb:b1:a9
|
||||||
|
model Str No Model ID
|
||||||
|
Id ing
|
||||||
|
devic Int No Device status, 1: online, 0: offline, -1: not reported
|
||||||
|
eStat ege
|
||||||
|
us r
|
||||||
|
accou Int No Account status, 1: Registered 2: dnd 3: Unregistered
|
||||||
|
ntSta ege
|
||||||
|
tus r
|
||||||
|
devic Int No Device type status, 1: Phone Device 3: Room Device, if
|
||||||
|
eType ege not filled in, it means all.
|
||||||
|
r
|
||||||
|
siteId Str No Site ID
|
||||||
|
ing
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long The offset value.
|
||||||
|
20 / 164
|
||||||
@@ -0,0 +1,886 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 21-40
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 21-40 of 164
|
||||||
|
**Chunk:** 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long The total number.
|
||||||
|
data Device[] Device information array
|
||||||
|
|
||||||
|
Device object definition
|
||||||
|
|
||||||
|
Paramete Data Description
|
||||||
|
r Type
|
||||||
|
id Strin device id
|
||||||
|
g
|
||||||
|
mac Strin Device MAC
|
||||||
|
g
|
||||||
|
sn Strin Device SN
|
||||||
|
g
|
||||||
|
name Strin Device Name
|
||||||
|
g
|
||||||
|
modelId Strin Model ID
|
||||||
|
g
|
||||||
|
siteId Strin Site ID
|
||||||
|
g
|
||||||
|
programV Strin Device firmware version number
|
||||||
|
ersion g
|
||||||
|
deviceSta Strin Device status, online: online, offline: offline, pending:
|
||||||
|
tus g not reported
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 20,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"deviceStatus":1,
|
||||||
|
|
||||||
|
21 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"siteId":"a624453e1cbb44ecb9bb6ee31731a856"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"siteId":"a624453e1cbb44ecb9bb6ee31731a856",
|
||||||
|
"programVersion":"70.83.0.68",
|
||||||
|
"deviceStatus":"online"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.1.7 Edit Device
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID address.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Require Description
|
||||||
|
r Type d
|
||||||
|
|
||||||
|
22 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
name String No Device name, maximum length 128
|
||||||
|
siteId String No Site ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/devices/8d07a56207074d26b61026099625b9e2 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name":"my t54s2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
2.1.8 Device Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
|
||||||
|
23 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Paramet Data Description
|
||||||
|
er Type
|
||||||
|
id String device id
|
||||||
|
mac String Device MAC
|
||||||
|
sn String group id
|
||||||
|
name String Device Name
|
||||||
|
modelId String Model ID
|
||||||
|
modelN String Model Name
|
||||||
|
ame
|
||||||
|
siteId String Site ID
|
||||||
|
siteNam String Site Name
|
||||||
|
e
|
||||||
|
lanIp String Intranet IP
|
||||||
|
deviceSt String Device status, online: online, offline: offline,
|
||||||
|
atus pending: not reported
|
||||||
|
programV String Firmware version
|
||||||
|
ersion
|
||||||
|
lastRepo long Last reporting time
|
||||||
|
rtTime
|
||||||
|
accounts ReportAcc Reported account information
|
||||||
|
ount[]
|
||||||
|
|
||||||
|
ReportAccount object definition
|
||||||
|
|
||||||
|
Paramet Data Description
|
||||||
|
er Type
|
||||||
|
|
||||||
|
24 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
accountI String Account ID
|
||||||
|
d
|
||||||
|
lineId Intege Account line, starting from 1
|
||||||
|
r
|
||||||
|
accountT Intege Account type: 0: SIP, 1: H323, 2: SFB
|
||||||
|
ype r
|
||||||
|
accountS String Account server address
|
||||||
|
erver
|
||||||
|
register String Registration Name
|
||||||
|
Name
|
||||||
|
usernam String User Name
|
||||||
|
e
|
||||||
|
status Intege Account status: 1: Registered 2: dnd 3: Not registered 4:
|
||||||
|
r Unknown
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/devices/8d07a56207074d26b61026099625b9e2 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"modelName":"SIP-T54S",
|
||||||
|
"siteId":"a624453e1cbb44ecb9bb6ee31731a856",
|
||||||
|
"siteName":"mysite",
|
||||||
|
"lanIp":"10.50.198.6",
|
||||||
|
"deviceStatus":"online",
|
||||||
|
"programVersion":"70.83.0.68",
|
||||||
|
"lastReportTime":1711093807136,
|
||||||
|
"accounts":[{
|
||||||
|
25 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"accountId":"604e67944c7c43fe8b66099254ec3439"
|
||||||
|
"lineId":1,
|
||||||
|
"accountType":0,
|
||||||
|
"accountServer":"10.200.108.48",
|
||||||
|
"registerName":"1000",
|
||||||
|
"username":"1000",
|
||||||
|
"status":0
|
||||||
|
|
||||||
|
},{
|
||||||
|
"accountId":"a45a6b4a7e0447b8a6c3f2839d902acd"
|
||||||
|
"lineId":2,
|
||||||
|
"accountType":0,
|
||||||
|
"accountServer":"10.200.108.48",
|
||||||
|
"registerName":"2000",
|
||||||
|
"username":"2000",
|
||||||
|
"status":0
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.1.9 Device Combination Configuration Query
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/configs
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
26 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
deviceConfig String Device MAC Configuration
|
||||||
|
siteConfig String Device Site Configuration
|
||||||
|
globalConfig String Enterprise Configurations
|
||||||
|
enforceConfig String Site forced inheritance configuration
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/devices/8d07a56207074d26b61026099625b9e2/configs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceConfig": "account.1.auth_name = test1101",
|
||||||
|
"siteConfig":"lang.gui=English",
|
||||||
|
"globalConfig":"auto_provision.server.url=\r\nlang.gui=English",
|
||||||
|
"enforceConfig":"security.user_password=user2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.2 Device Group Management
|
||||||
|
2.2.1 Add Group
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceGroups
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requi Description
|
||||||
|
r Type red
|
||||||
|
name String Yes Group name, length 64
|
||||||
|
deviceTyp Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
27 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
e Device
|
||||||
|
Descripti String No Group Description information, length 256
|
||||||
|
on
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The group ID.
|
||||||
|
name String Group Name
|
||||||
|
deviceType Integer Device Types
|
||||||
|
description String Group description information
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceGroups HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name":"t54sgroup",
|
||||||
|
"deviceType":1,
|
||||||
|
"Description":"test"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
28 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "00884cef841e48f4acd213c084e65f67",
|
||||||
|
"name":"t54sgroup",
|
||||||
|
"deviceType":1,
|
||||||
|
"Description":"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.2.2 Edit Group
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceGroups/{deviceGroupId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceGroupId String Yes Group ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requi Description
|
||||||
|
r Type red
|
||||||
|
name String Yes Group name, length 64
|
||||||
|
deviceTyp Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
e Device
|
||||||
|
Descripti String No Group Description information, length 256
|
||||||
|
on
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
29 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/deviceGroups/00884cef841e48f4acd213c084e65f67/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name":"t54sgroup2",
|
||||||
|
"deviceType":1,
|
||||||
|
"Description":"test2"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
2.2.3 Group List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listDeviceGroups
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Param Data Requir Description
|
||||||
|
eter Type ed
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve,
|
||||||
|
default is 10, maximum is 500.
|
||||||
|
autoCo Boolean No Whether to respond with the total count, it is
|
||||||
|
unt recommended to set it to true when querying
|
||||||
|
the first page, and pass false for subsequent
|
||||||
|
page turns.
|
||||||
|
filter Filter No search parameters
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Para Data Req Description
|
||||||
|
mete uir
|
||||||
|
|
||||||
|
30 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
r Typ ed
|
||||||
|
e
|
||||||
|
devi Inte No Device type status, 1: Phone Device 3: Room Device, if
|
||||||
|
ceTy ger not filled in, it means all.
|
||||||
|
pe
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data DeviceGroup[] DeviceGroup information array
|
||||||
|
|
||||||
|
DeviceGroup object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Group ID
|
||||||
|
name String Group name, length 64
|
||||||
|
deviceType Integer Device Type 1: Phone Device 3: Room Device
|
||||||
|
Description String Group Description information, length 256
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listDeviceGroups HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
|
||||||
|
31 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"deviceType":1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "00884cef841e48f4acd213c084e65f67",
|
||||||
|
"name":"t54sgroup",
|
||||||
|
"deviceType":1,
|
||||||
|
"Description":"test"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.2.4 Delete Group
|
||||||
|
Request Method
|
||||||
|
DELETE
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceGroups/{deviceGroupId}
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceGroupId String Yes Group ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
|
||||||
|
32 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpDELETE /v2/dm/deviceGroups/00884cef841e48f4acd213c084e65f67 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
2.2.5 Add Device to Group
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceGroups/{deviceGroupId}/addDevices
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceGroupId String Yes Group ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Require Description
|
||||||
|
r Type d
|
||||||
|
deviceIds String[] Yes Device ID list, limited to 200.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
33 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceGroups/00884cef841e48f4acd213c084e65f67/addDevices
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
34 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"field":"00099642675e4d4bb5e91fd9ae5ce585",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.2.6 Remove devices from the group
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceGroups/{deviceGroupId}/delDevices
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceGroupId String Yes Group ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Require Description
|
||||||
|
er Type d
|
||||||
|
deviceIds String[] Yes Device ID list, maximum length 200
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
|
||||||
|
35 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceGroups/00884cef841e48f4acd213c084e65f67/delDevices
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"00099642675e4d4bb5e91fd9ae5ce585",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.2.7 Query Device List within Group
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
36 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/v2/dm/deviceGroups/{deviceGroupId}/listDevices
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceGroupId String Yes Group ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
sk Lo N Number of skipped records, default is 0
|
||||||
|
ip ng o
|
||||||
|
li Lo N Maximum number of records to retrieve, default is 10,
|
||||||
|
mi ng o maximum is 500.
|
||||||
|
t
|
||||||
|
au Bo N Whether to respond with the total count, it is recommended
|
||||||
|
to ol o to set it to true when querying the first page, and pass false
|
||||||
|
Co ea for subsequent page turns.
|
||||||
|
un n
|
||||||
|
t
|
||||||
|
fil Fil N search parameters
|
||||||
|
te te o
|
||||||
|
r r
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Para Dat Re Description
|
||||||
|
mete a qu
|
||||||
|
r Ty ire
|
||||||
|
pe d
|
||||||
|
mac Str No Device MAC fuzzy search keyword, maximum length 17,
|
||||||
|
ing supports with : or -, such as 00:15:65:bb:b1:a9
|
||||||
|
mode Str No Model ID
|
||||||
|
lId ing
|
||||||
|
37 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
devic Int No Device status, 1: online, 0: offline, -1: not reported
|
||||||
|
eStat ege
|
||||||
|
us r
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Device[] Device information array
|
||||||
|
|
||||||
|
Device object definition
|
||||||
|
|
||||||
|
Paramete Data Description
|
||||||
|
r Type
|
||||||
|
id Strin device id
|
||||||
|
g
|
||||||
|
mac Strin Device MAC
|
||||||
|
g
|
||||||
|
sn Strin Device SN
|
||||||
|
g
|
||||||
|
name Strin Device Name
|
||||||
|
g
|
||||||
|
modelId Strin Model ID
|
||||||
|
g
|
||||||
|
siteId Strin Site ID
|
||||||
|
g
|
||||||
|
38 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
programV Strin Device firmware version number
|
||||||
|
ersion g
|
||||||
|
deviceSta Strin Device status, online: online, offline: offline, pending:
|
||||||
|
tus g not reported
|
||||||
|
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceGroups/00884cef841e48f4acd213c084e65f67/listDevices
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"name":"my t54s",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"siteId":"a624453e1cbb44ecb9bb6ee31731a856",
|
||||||
|
"programVersion":"70.83.0.68",
|
||||||
|
"deviceStatus":"online"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.3 Device Control
|
||||||
|
2.3.1 Device Restart
|
||||||
|
Request Method
|
||||||
|
39 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/device/reboot
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
deviceIds String[] Yes Device ID list, maximum length 200
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
pe Device
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Total number of factory restored devices
|
||||||
|
successCoun Integer Number of successful factory restored
|
||||||
|
t
|
||||||
|
failureCount Integer Number of failed factory restored
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
40 / 164
|
||||||
@@ -0,0 +1,891 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 41-60
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 41-60 of 164
|
||||||
|
**Chunk:** 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPOST /v2/dm/device/reboot HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"],
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"0006572538f74e8683716cf961caa95b",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.3.2 Device Factory Reset
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/device/reset
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
deviceIds String[] Yes Device ID list
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
pe Device
|
||||||
|
|
||||||
|
41 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Total number of factory restored devices
|
||||||
|
successCoun Integer Number of successful factory restored
|
||||||
|
t
|
||||||
|
failureCount Integer Number of failed factory restored
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/device/reset HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"],
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
42 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"0006572538f74e8683716cf961caa95b",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.4 Device Identification Management
|
||||||
|
2.4.1 Obtain Device ID
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceId
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Param Data Req Description
|
||||||
|
eter Type uir
|
||||||
|
ed
|
||||||
|
device Integ Yes Device Type 1: Phone Device 3: Room Device
|
||||||
|
Type er
|
||||||
|
device Strin No Indicates the type of device identifier, default is
|
||||||
|
IdType g mac, optional is mac.
|
||||||
|
deviceI Strin Yes Device ID, limit 200 characters
|
||||||
|
ds g[]
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
43 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
key String Device identification
|
||||||
|
deviceId String The device ID.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceId HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceType":1,
|
||||||
|
"deviceIds":["001565bbb1a9"],
|
||||||
|
"deviceIdType":"mac"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
[{
|
||||||
|
"key":"001565bbb1a9",
|
||||||
|
"deviceId":"8d07a56207074d26b61026099625b9e2"
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
2.5 Device Accessories Management
|
||||||
|
2.5.1 Accessory List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/listParts
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
|
||||||
|
44 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
sk Lo N Number of skipped records, default is 0
|
||||||
|
ip ng o
|
||||||
|
li Lo N Maximum number of records to retrieve, default is 10,
|
||||||
|
mi ng o maximum is 500.
|
||||||
|
t
|
||||||
|
au Bo N Whether to respond with the total count, it is recommended
|
||||||
|
to ol o to set it to true when querying the first page, and pass false
|
||||||
|
Co ea for subsequent page turns.
|
||||||
|
un n
|
||||||
|
t
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String accessory id
|
||||||
|
mac String Accessory MAC
|
||||||
|
sn String Accessory SN
|
||||||
|
|
||||||
|
45 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
modelId String Model ID
|
||||||
|
connectWa String Connection methods for accessories, including USB,
|
||||||
|
y BT, etc.
|
||||||
|
connStatus Intege Status, 1: online, 0: offline
|
||||||
|
r
|
||||||
|
lanIp String Intranet IP
|
||||||
|
programVer String Firmware version number
|
||||||
|
sion
|
||||||
|
lastReport Long Last reporting time
|
||||||
|
Time
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/devices/8d07a56207074d26b61026099625b9e2/listParts HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 20,
|
||||||
|
"autoCount": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "5d6017f6fe004eb7ac1107c80c1c44b7",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"modelId":"e7c5c0c406cf4a02bd9ed64183d1de05",
|
||||||
|
|
||||||
|
46 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"modelName": "CP700",
|
||||||
|
"connectWay":"USB",
|
||||||
|
"connStatus":1,
|
||||||
|
"lanIp":"10.60.50.22",
|
||||||
|
"programVersion":"153.433.0.5",
|
||||||
|
"lastReportTime":1709577211630
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.5.2 Accessory Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/parts/{partId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
partId String Yes Accessories ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Par Da Description
|
||||||
|
ame ta
|
||||||
|
ter Ty
|
||||||
|
p
|
||||||
|
e
|
||||||
|
id St accessory id
|
||||||
|
47 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ri
|
||||||
|
ng
|
||||||
|
mac St Accessory MAC
|
||||||
|
ri
|
||||||
|
ng
|
||||||
|
sn St Accessory SN
|
||||||
|
ri
|
||||||
|
ng
|
||||||
|
mod St Model ID
|
||||||
|
elId ri
|
||||||
|
ng
|
||||||
|
con St Connection methods for accessories, including USB, BT, etc.
|
||||||
|
nec ri
|
||||||
|
tWa ng
|
||||||
|
y
|
||||||
|
conn In Status, 1: online, 0: offline
|
||||||
|
Stat te
|
||||||
|
us ge
|
||||||
|
r
|
||||||
|
lanI St Intranet IP
|
||||||
|
p ri
|
||||||
|
ng
|
||||||
|
prog St Firmware version number
|
||||||
|
ramV ri
|
||||||
|
ersi ng
|
||||||
|
on
|
||||||
|
last Lo Last reporting time
|
||||||
|
Rep ng
|
||||||
|
ortT
|
||||||
|
ime
|
||||||
|
extr Ex Additional information related to the model, when the model is
|
||||||
|
aInf tr a sensor, the response content includes temperature, battery
|
||||||
|
o aI level, and other information.
|
||||||
|
nf
|
||||||
|
o
|
||||||
|
|
||||||
|
48 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ExtraInfo object definition
|
||||||
|
When the model is a sensor, the content is as follows.
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
batteryLevel Integer Battery Level
|
||||||
|
humidity Integer Humidity
|
||||||
|
irradiance IntegerIlluminance
|
||||||
|
temperature Integer Temperature
|
||||||
|
numPeople Integer People Count Statistics
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/devices/8d07a56207074d26b61026099625b9e2/
|
||||||
|
parts/104aa43ca9994de0825ec8d32f3a8c60 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "104aa43ca9994de0825ec8d32f3a8c60",
|
||||||
|
"mac":"84fd27df8045",
|
||||||
|
"sn":"803111D070000358",
|
||||||
|
"modelId":"50b2674518a648968367ea2bb61a5572",
|
||||||
|
"modelName":"RoomSensor",
|
||||||
|
"connectWay":"BT",
|
||||||
|
"connStatus":1,
|
||||||
|
"lanIp":"10.60.50.21",
|
||||||
|
"programVersion":"153.433.0.5",
|
||||||
|
"lastReportTime":1709577211630,
|
||||||
|
"extraInfo":{
|
||||||
|
"batteryLevel":100,
|
||||||
|
"humidity":55,
|
||||||
|
"irradiance":27,
|
||||||
|
"temperature":31,
|
||||||
|
"numPeople":2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
49 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.5.2 Accessory Restart
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/parts/reboot
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Dat Re Description
|
||||||
|
ram a qu
|
||||||
|
ete Ty ir
|
||||||
|
r pe ed
|
||||||
|
par Stri No Accessory ID list, maximum length 200, not passing
|
||||||
|
tId ng[ indicates a restart of all accessories under this device.
|
||||||
|
s ]
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
total Integer Total number of rebooted devices
|
||||||
|
successCount Integer Number of successful reboots
|
||||||
|
failureCount Integer Number of failed reboots
|
||||||
|
|
||||||
|
50 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/device/0006572538f74e8683716cf961caa95b/parts/reboot HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"partIds":["00ab7c8da8ff4345af3857feb68aedef","0299d1254f7042068c4ef1c5895b7e5a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"00ab7c8da8ff4345af3857feb68aedef",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.5.4 Restore Accessories to Factory Settings
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/parts/reset
|
||||||
|
Path parameter
|
||||||
|
51 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
pa St N Accessory ID list, maximum length of 200, if not provided,
|
||||||
|
rt ri o indicates that all accessories under this device will be
|
||||||
|
Id ng restored to factory settings.
|
||||||
|
s []
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Total number of factory restored devices
|
||||||
|
successCoun Integer Number of successful factory restored
|
||||||
|
t
|
||||||
|
failureCount Integer Number of failed factory restored
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
|
||||||
|
52 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/device/0006572538f74e8683716cf961caa95b/parts/reset HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"partIds":["00ab7c8da8ff4345af3857feb68aedef","0299d1254f7042068c4ef1c5895b7e5a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"00ab7c8da8ff4345af3857feb68aedef",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.6 Device Account Management
|
||||||
|
2.6.1 Device Binding Account
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/bindAccounts
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
53 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parame Data Type Requi Description
|
||||||
|
ter red
|
||||||
|
account BindAccount Yes List of account information to be bound
|
||||||
|
s []
|
||||||
|
|
||||||
|
BindAccount object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
lineId integer Account line, starting from 1
|
||||||
|
accountType Integer Account type: 0: SIP, 1: H323, 2: SFB
|
||||||
|
accountId String Account ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
total Integer Total number of bindings
|
||||||
|
successCount Integer Number of successful bindings
|
||||||
|
failureCount Integer Number of failed bindings
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
|
||||||
|
54 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPOST /v2/dm/devices/8d07a56207074d26b61026099625b9e2/bindAccounts
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"lineId":1,
|
||||||
|
"accountType":0,
|
||||||
|
"accountId":"604e67944c7c43fe8b66099254ec3439"
|
||||||
|
},{
|
||||||
|
"lineId":2,
|
||||||
|
"accountType":0,
|
||||||
|
"accountId":"a45a6b4a7e0447b8a6c3f2839d902acd"
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"604e67944c7c43fe8b66099254ec3439",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.6.2 Device Unbinding Account
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/unbindAccounts
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
55 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Require Description
|
||||||
|
r Type d
|
||||||
|
accountIds String[] Yes List of account IDs to be unbound
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
total Integer The total number of unbundled
|
||||||
|
successCount Integer Number of successful unbindings
|
||||||
|
failureCount Integer Number of failed unbindings
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/devices/8d07a56207074d26b61026099625b9e2/unbindAccounts
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"accountIds":
|
||||||
|
["604e67944c7c43fe8b66099254ec3439","a45a6b4a7e0447b8a6c3f2839d902acd"]
|
||||||
|
|
||||||
|
56 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"604e67944c7c43fe8b66099254ec3439",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
2.6.3 List of accounts bound to the device
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/boundAccounts
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
57 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data BindAccount[] Bind account information array
|
||||||
|
|
||||||
|
BindAccount object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
accountId String Account ID
|
||||||
|
lineId Integer Account line, starting from 1
|
||||||
|
accountType Integer Account type: 0: SIP, 1: H323, 2: SFB
|
||||||
|
accountServer String Account server address
|
||||||
|
registerName String Registration Name
|
||||||
|
username String User Name
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/devices/8d07a56207074d26b61026099625b9e2/boundAccounts
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"accountId":"604e67944c7c43fe8b66099254ec3439"
|
||||||
|
"lineId":1,
|
||||||
|
"accountType":0,
|
||||||
|
"accountServer":"10.200.108.48",
|
||||||
|
"registerName":"1000",
|
||||||
|
"username":"1000"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
},{
|
||||||
|
"accountId":"a45a6b4a7e0447b8a6c3f2839d902acd"
|
||||||
|
"lineId":2,
|
||||||
|
"accountType":0,
|
||||||
|
"accountServer":"10.200.108.48",
|
||||||
|
"registerName":"2000",
|
||||||
|
58 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"username":"2000"
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
3.Account Management
|
||||||
|
3.1 Add SIP account
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sipAccounts
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requ Description
|
||||||
|
er Type ired
|
||||||
|
register String Yes The registered name, with no more than 128
|
||||||
|
Name characters.
|
||||||
|
usernam String Yes Username, maximum length 128
|
||||||
|
e
|
||||||
|
passwor String Yes Password, maximum length 128
|
||||||
|
d
|
||||||
|
display String No Display name, maximum length 128
|
||||||
|
Name
|
||||||
|
label String No Label, maximum length 128
|
||||||
|
sipServe SipSer Yes sip server 1 address
|
||||||
|
r1 ver
|
||||||
|
sipServe SipSer No sip server 2 address
|
||||||
|
r2 ver
|
||||||
|
remark String No Note, maximum length 512
|
||||||
|
siteId String No Site ID to be assigned
|
||||||
|
|
||||||
|
SipServer object definition
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
host String Server address, maximum length 256
|
||||||
|
|
||||||
|
59 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
port Integer Server port, 0~65535
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String account id
|
||||||
|
username String username
|
||||||
|
registerInfo String Registration Information
|
||||||
|
serverAddress String Server address
|
||||||
|
accountType Integer Account type, 0: SIP, 1: H323, 2: SFB
|
||||||
|
remark String Note
|
||||||
|
createTime Long Creation time
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/sipAccounts HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"registerName": "2552",
|
||||||
|
"username": "2552",
|
||||||
|
"password": "******",
|
||||||
|
"label": "2552",
|
||||||
|
"displayName": "2552",
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "ume.yealink.com",
|
||||||
|
"port":5061
|
||||||
|
},
|
||||||
|
|
||||||
|
60 / 164
|
||||||
@@ -0,0 +1,883 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 61-80
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 61-80 of 164
|
||||||
|
**Chunk:** 4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"siteId":"0006d62003684754b11c09c5d94ea687",
|
||||||
|
"remark":"test"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "e33b8f25247e45de84dd4c74503b241a",
|
||||||
|
"registerInfo": "2552",
|
||||||
|
"username": "2552",
|
||||||
|
"serverAddress": "ume.yealink.com",
|
||||||
|
"accountType": 0,
|
||||||
|
"createTime": 1698052518923
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
3.2 Edit SIP Account
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sipAccounts/{accountId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
accountId String Yes Account
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Requir Description
|
||||||
|
Type ed
|
||||||
|
registerNam String Yes Name,maximum length 128
|
||||||
|
e
|
||||||
|
username String Yes Username, maximum length 128
|
||||||
|
password String Yes Password, maximum length 128
|
||||||
|
|
||||||
|
61 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
displayName String No Display name, maximum length 128
|
||||||
|
label String No Label, maximum length 128
|
||||||
|
sipServer1 SipServe Yes sip server 1 address
|
||||||
|
r
|
||||||
|
sipServer2 SipServe No sip server 2 address
|
||||||
|
r
|
||||||
|
remark String No Note, maximum length 512
|
||||||
|
siteId String No Site ID to be assigned
|
||||||
|
|
||||||
|
SipServer object definition
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
host String Server address, maximum length 256
|
||||||
|
port Integer Server port, 0~65535
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/sipAccounts/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"registerName": "2553",
|
||||||
|
"username": "2552",
|
||||||
|
"password": "******",
|
||||||
|
"label": "2552",
|
||||||
|
"displayName": "2552",
|
||||||
|
"sipServer1": {
|
||||||
|
"host": "ume.yealink.com",
|
||||||
|
"port":5061
|
||||||
|
|
||||||
|
62 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
"siteId":"0006d62003684754b11c09c5d94ea687",
|
||||||
|
"remark":"test"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
3.3 Account List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listAccounts
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Par Dat Req Description
|
||||||
|
ame a uir
|
||||||
|
ter Ty ed
|
||||||
|
pe
|
||||||
|
skip Lo No Number of skipped records, default is 0
|
||||||
|
ng
|
||||||
|
limit Lo No Maximum number of records to retrieve, default is 10,
|
||||||
|
ng maximum is 500.
|
||||||
|
aut Boo No Whether to respond with the total count, it is
|
||||||
|
oCo lea recommended to set it to true when querying the first
|
||||||
|
unt n page, and pass false for subsequent page turns.
|
||||||
|
filte Filt No search parameters
|
||||||
|
r er
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Paramete Data Type Require Description
|
||||||
|
r d
|
||||||
|
username String No Username fuzzy search keyword
|
||||||
|
|
||||||
|
63 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Account[] Account information array
|
||||||
|
|
||||||
|
Account object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String account id
|
||||||
|
username String username
|
||||||
|
registerInfo String Registration Information
|
||||||
|
serverAddress String Server address
|
||||||
|
accountType Integer Account type, 0: SIP, 1: H323, 2: SFB
|
||||||
|
remark String Note
|
||||||
|
createTime Long Creation time
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listAccounts HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 20,
|
||||||
|
64 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"username":"2552"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "e33b8f25247e45de84dd4c74503b241a",
|
||||||
|
"registerInfo": "2552",
|
||||||
|
"username": "h323",
|
||||||
|
"accountType": 1,
|
||||||
|
"createTime": 1698052518923
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
3.4 Delete Account
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/delAccounts
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requir Description
|
||||||
|
r Type ed
|
||||||
|
accountId String[] Yes Account ID list, maximum length 200
|
||||||
|
s
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
65 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/delAccounts HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"accountIds":
|
||||||
|
["e33b8f25247e45de84dd4c74503b241a","2d3e77b736e240eabf25aa1f5448aa0c"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
|
||||||
|
66 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"2d3e77b736e240eabf25aa1f5448aa0c",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.Configuration Management
|
||||||
|
4.1 Telephone Device Configuration Management
|
||||||
|
4.1.1 Device Configuration List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listDeviceConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Para Data Requ Description
|
||||||
|
mete Type ired
|
||||||
|
r
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
auto Bool No Whether to return the total number of records
|
||||||
|
Coun ean
|
||||||
|
t
|
||||||
|
filter Filter No filter condition
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Par Dat Re Description
|
||||||
|
ame a qu
|
||||||
|
ter Typ ire
|
||||||
|
e d
|
||||||
|
ma Stri No Device MAC fuzzy search keyword, maximum length 17,
|
||||||
|
c ng supports with : or -, such as 00:15:65:bb:b1:a9
|
||||||
|
67 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total data volume
|
||||||
|
data Config[] Config information array
|
||||||
|
|
||||||
|
Config object definition
|
||||||
|
|
||||||
|
parameter Data Description
|
||||||
|
Type
|
||||||
|
id String The primary key ID.
|
||||||
|
name String The configuration name.
|
||||||
|
modelId String The device model ID.
|
||||||
|
Description String Enter a description for the configuration.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listDeviceConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"mac":"001565bbb1a9"
|
||||||
|
}
|
||||||
|
68 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"name":"my config",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"Description":"my t54s config"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.1.2 Add Device Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requ Description
|
||||||
|
r Type ired
|
||||||
|
deviceId Strin Yes Device ID
|
||||||
|
g
|
||||||
|
content Strin Yes cfg file content
|
||||||
|
g
|
||||||
|
autoPush Bool No Whether to automatically push this device
|
||||||
|
ean configuration when the phone device is
|
||||||
|
powered on for the first time or restored to
|
||||||
|
factory settings, true: yes false: no
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
69 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceId": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"autoPush":true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "d916a46b4557464c87c278cb37477bef"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.1.3 Delete Device Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/delDeviceConfigs
|
||||||
|
|
||||||
|
70 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Data Requi Description
|
||||||
|
ter Type red
|
||||||
|
configI String[] Yes Device configuration ID list, maximum length
|
||||||
|
ds 200
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/delDeviceConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"configIds":
|
||||||
|
|
||||||
|
71 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
["e33b8f25247e45de84dd4c74503b241a","8d07a56207074d26b61026099625b9e2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"e33b8f25247e45de84dd4c74503b241a",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.1.4 Device Configuration Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Paramete Data Type Require Description
|
||||||
|
r d
|
||||||
|
configId String Yes The device configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
72 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Description
|
||||||
|
Type
|
||||||
|
id String The device configuration ID.
|
||||||
|
name String The configuration name.
|
||||||
|
modelId String The device model ID.
|
||||||
|
content String Configuration Content
|
||||||
|
Description String Enter a description for the configuration.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/deviceConfigs/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"name":"my config",
|
||||||
|
"modelId":"61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"content":"#!version:1.0.0.1\naccount.1.codec.g722.enable=1"
|
||||||
|
"Description":"my t54s config"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.1.5 Push Device Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/deviceConfigs/{configId}/push
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
configId String Yes The configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
73 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/deviceConfigs/e33b8f25247e45de84dd4c74503b241a/push HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
4.2 Telephone Substation Configuration Management
|
||||||
|
4.2.1 Add Sub-site Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/siteConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
name String Yes Sub-site configuration name, length 64
|
||||||
|
siteId String Yes Site ID
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device
|
||||||
|
pe
|
||||||
|
modelId String No Model ID, leaving it blank indicates all models
|
||||||
|
content String No Configuration file content
|
||||||
|
Descripti String No Description, length 256
|
||||||
|
on
|
||||||
|
|
||||||
|
|
||||||
|
74 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/siteConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "site1 config",
|
||||||
|
"siteId":"048a97f00ece46bd8d8bf97f5002992a",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8b7f1739ad6d4a578267d09b53a262a3"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.2.2 Edit Subsite Configuration
|
||||||
|
Request Method
|
||||||
|
|
||||||
|
75 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/siteConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
configId String Yes Subsite configuration ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
name String Yes Sub-site configuration name, length 64
|
||||||
|
siteId String Yes Site ID
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device
|
||||||
|
pe
|
||||||
|
modelId String No Model ID, leaving it blank indicates all models
|
||||||
|
content String No Configuration file content
|
||||||
|
Descripti String No Description, length 256
|
||||||
|
on
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/siteConfigs/8b7f1739ad6d4a578267d09b53a262a3 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
76 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"name": "site1 config2",
|
||||||
|
"siteId":"048a97f00ece46bd8d8bf97f5002992a",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
4.2.3 Delete Subsite Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/delSiteConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Param Data Requi Description
|
||||||
|
eter Type red
|
||||||
|
configI String[] Yes Sub-site configuration ID list, maximum length
|
||||||
|
ds 200
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
77 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/delSiteConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"configIds":
|
||||||
|
["8b7f1739ad6d4a578267d09b53a262a3","e33b8f25247e45de84dd4c74503b241a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"e33b8f25247e45de84dd4c74503b241a",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.2.4 Subsite Configuration Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
|
||||||
|
78 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/dm/siteConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
configId String Yes Subsite configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Sub-site Configuration ID
|
||||||
|
name String Sub-site configuration name, length 64
|
||||||
|
siteId String Site ID
|
||||||
|
deviceType Integer Device Type 1: Phone Device
|
||||||
|
modelId String Model ID
|
||||||
|
content String Configuration file content
|
||||||
|
Description String Description, length 256
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/siteConfigs/8b7f1739ad6d4a578267d09b53a262a3 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
79 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8b7f1739ad6d4a578267d09b53a262a3",
|
||||||
|
"name": "site1 config2",
|
||||||
|
"siteId":"048a97f00ece46bd8d8bf97f5002992a",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.2.5 Substation Configuration List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listSiteConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Para Data Requ Description
|
||||||
|
mete Type ired
|
||||||
|
r
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
auto Bool No Whether to return the total number of records
|
||||||
|
Coun ean
|
||||||
|
t
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total data volume
|
||||||
|
data Config[] Config information array
|
||||||
|
|
||||||
|
Config definition
|
||||||
|
80 / 164
|
||||||
@@ -0,0 +1,884 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 81-100
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 81-100 of 164
|
||||||
|
**Chunk:** 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Sub-site Configuration ID
|
||||||
|
name String Sub-site configuration name, length 64
|
||||||
|
siteId String Site ID
|
||||||
|
deviceType Integer Device Type 1: Phone Device
|
||||||
|
modelId String Model ID
|
||||||
|
Description String Description, length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listSiteConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
|
||||||
|
81 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"data":[{
|
||||||
|
"id": "8b7f1739ad6d4a578267d09b53a262a3",
|
||||||
|
"name": "site1 config2",
|
||||||
|
"siteId":"048a97f00ece46bd8d8bf97f5002992a",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"Description":"test2"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.2.6 Push Subsite Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/siteConfigs/{configId}/push
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
configId String Yes The configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/siteConfigs/8b7f1739ad6d4a578267d09b53a262a3/push HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
82 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
4.3 Phone Group Configuration Management
|
||||||
|
4.3.1 Add Group Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/groupConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requi Description
|
||||||
|
r Type red
|
||||||
|
name String Yes Group configuration name, length 64
|
||||||
|
deviceGro String Yes Device Group ID
|
||||||
|
upId
|
||||||
|
deviceTyp Integer Yes Device Type 1: Phone Device
|
||||||
|
e
|
||||||
|
modelId String No Model ID, leaving it blank indicates all
|
||||||
|
models
|
||||||
|
content String No Configuration file content
|
||||||
|
Descriptio String No Description, length 256
|
||||||
|
n
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String Group Configuration ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
83 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPOST /v2/dm/groupConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "group config",
|
||||||
|
"deviceGroupId":"1185861ed00840ea99a7b07aa0f28f88",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8faa39e34929425f85b4cd9925a2bbcb"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.3.2 Edit Group Configuration
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/groupConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
configId String Yes Group Configuration ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramete Data Requi Description
|
||||||
|
r Type red
|
||||||
|
name String Yes Group configuration name, length 64
|
||||||
|
deviceGro String Yes Device Group ID
|
||||||
|
|
||||||
|
84 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
upId
|
||||||
|
deviceTyp Integer Yes Device Type 1: Phone Device
|
||||||
|
e
|
||||||
|
modelId String No Model ID, leaving it blank indicates all
|
||||||
|
models
|
||||||
|
content String No Configuration file content
|
||||||
|
Descriptio String No Description, length 256
|
||||||
|
n
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/groupConfigs/8faa39e34929425f85b4cd9925a2bbcb HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "group config2",
|
||||||
|
"deviceGroupId":"1185861ed00840ea99a7b07aa0f28f88",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
4.3.3 Delete Group Configuration
|
||||||
|
Request Method
|
||||||
|
85 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/delGroupConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Data Requi Description
|
||||||
|
ter Type red
|
||||||
|
configI String[] Yes Group configuration ID list, maximum length
|
||||||
|
ds 200
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpDELETE /v2/dm/delGroupConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
86 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"configIds":
|
||||||
|
["8faa39e34929425f85b4cd9925a2bbcb","e33b8f25247e45de84dd4c74503b241a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"e33b8f25247e45de84dd4c74503b241a",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.3.4 Group Configuration Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/groupConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
configId String Yes Group Configuration ID
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Group Configuration ID
|
||||||
|
name String Group configuration name, length 64
|
||||||
|
|
||||||
|
87 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deviceGroupId String Group ID
|
||||||
|
deviceType Integer Device Type 1: Phone Device
|
||||||
|
modelId String Model ID
|
||||||
|
content String Configuration file content
|
||||||
|
Description String Description, length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/groupConfigs/8faa39e34929425f85b4cd9925a2bbcb HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8faa39e34929425f85b4cd9925a2bbcb",
|
||||||
|
"name": "group config2",
|
||||||
|
"deviceGroupId":"1185861ed00840ea99a7b07aa0f28f88",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English",
|
||||||
|
"Description":"test2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.3.5 Group Configuration List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
|
||||||
|
88 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listGroupConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Para Data Requ Description
|
||||||
|
mete Type ired
|
||||||
|
r
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
auto Bool No Whether to return the total number of records
|
||||||
|
Coun ean
|
||||||
|
t
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total data volume
|
||||||
|
data Config[] Config information array
|
||||||
|
|
||||||
|
Config definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Group Configuration ID
|
||||||
|
name String Group configuration name, length 64
|
||||||
|
deviceGroupId String Group ID
|
||||||
|
deviceType Integer Device Type 1: Phone Device
|
||||||
|
modelId String Model ID
|
||||||
|
Description String Description, length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
89 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listGroupConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "8faa39e34929425f85b4cd9925a2bbcb",
|
||||||
|
"name": "group config2",
|
||||||
|
"deviceGroupId":"1185861ed00840ea99a7b07aa0f28f88",
|
||||||
|
"deviceType":1,
|
||||||
|
"modelId":"db249ca8f83f425baeda09214288d0a9",
|
||||||
|
"Description":"test2"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.3.6 Push Group Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
|
||||||
|
90 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/dm/groupConfigs/{configId}/push
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
configId String Yes The configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/groupConfigs/8faa39e34929425f85b4cd9925a2bbcb/push HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
4.4 Conference Room Equipment Configuration Management
|
||||||
|
4.4.1 Save Device Configuration
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
deviceId String Yes Device ID
|
||||||
|
content String Yes cfg file content
|
||||||
|
91 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/rooms/deviceConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceId": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "d916a46b4557464c87c278cb37477bef"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.4.2 Device Configuration Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs/{configId}
|
||||||
|
92 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Paramete Data Type Require Description
|
||||||
|
r d
|
||||||
|
configId String Yes The device configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
content String Configuration ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/rooms/deviceConfigs/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"content":"#!version:1.0.0.1\naccount.1.codec.g722.enable=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.4.3 Push Device Configuration
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
93 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs/{configId}/push
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
configId String Yes The configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/rooms/deviceConfigs/e33b8f25247e45de84dd4c74503b241a/push
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
4.4 Conference Room Equipment Configuration Management
|
||||||
|
4.4.1 Save Device Configuration
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
deviceId String Yes Device ID
|
||||||
|
|
||||||
|
94 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
content String Yes cfg file content
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/rooms/deviceConfigs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceId": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"content": "lang.wui=English\nlang.gui=English"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "d916a46b4557464c87c278cb37477bef"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.4.2 Device Configuration Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
|
||||||
|
|
||||||
|
95 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs/{configId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Paramete Data Type Require Description
|
||||||
|
r d
|
||||||
|
configId String Yes The device configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The device configuration ID.
|
||||||
|
content String Configuration ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/rooms/deviceConfigs/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"content":"#!version:1.0.0.1\naccount.1.codec.g722.enable=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
4.4.3 Push Device Configuration
|
||||||
|
96 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/rooms/deviceConfigs/{configId}/push
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
configId String Yes The configuration ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/rooms/deviceConfigs/e33b8f25247e45de84dd4c74503b241a/push
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
5.RPS management
|
||||||
|
5.1 RPS Device Management
|
||||||
|
5.1.1 Add Device
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/devices
|
||||||
|
|
||||||
|
97 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Requ Description
|
||||||
|
Type ired
|
||||||
|
mac String Yes Device MAC, minimum length 12, maximum
|
||||||
|
length 17
|
||||||
|
sn String Yes SN code, maximum length 128
|
||||||
|
serverId String No Server ID
|
||||||
|
uniqueServe String No Server address, maximum length 256
|
||||||
|
rUrl
|
||||||
|
authName String No Authentication username, maximum
|
||||||
|
length 128
|
||||||
|
password String No Authentication password, maximum length
|
||||||
|
128
|
||||||
|
remark String No Note, maximum length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
id String The device ID.
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The group ID.
|
||||||
|
serverId String The server ID.
|
||||||
|
uniqueServerUrl String The server address.
|
||||||
|
authName String The username for authentication.
|
||||||
|
remark String The remark.
|
||||||
|
|
||||||
|
98 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/devices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"serverId":"ba7c7b13ed114a5fa6f12063ea9dff41",
|
||||||
|
"remark":"SeakeerDevice",
|
||||||
|
"authName":"Seakeer",
|
||||||
|
"password":"654321"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"serverId":"ba7c7b13ed114a5fa6f12063ea9dff41",
|
||||||
|
"remark":"SeakeerDevice",
|
||||||
|
"authName":"Seakeer",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
5.1.2 Batch Add Devices
|
||||||
|
Note: A maximum of 100 items at a time.
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/addDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
99 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Requ Description
|
||||||
|
Type ired
|
||||||
|
mac String Yes Device MAC, minimum length 12, maximum
|
||||||
|
length 17
|
||||||
|
sn String Yes SN code, maximum length 128
|
||||||
|
serverId String No Server ID
|
||||||
|
uniqueServe String No Server address, maximum length 256
|
||||||
|
rUrl
|
||||||
|
authName String No Authentication username, maximum
|
||||||
|
length 128
|
||||||
|
password String No Authentication password, maximum length
|
||||||
|
128
|
||||||
|
remark String No Note, maximum length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
total Integer The total number of batch additions.
|
||||||
|
successCoun Integer The total number of successful additions.
|
||||||
|
t
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors AddError[] Error message.
|
||||||
|
|
||||||
|
AddError object
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
mac String The device MAC address.
|
||||||
|
|
||||||
|
100 / 164
|
||||||
@@ -0,0 +1,880 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 101-120
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 101-120 of 164
|
||||||
|
**Chunk:** 6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
sn String The device SN address.
|
||||||
|
errorInfo String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/addDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"serverId":"ba7c7b13ed114a5fa6f12063ea9dff41",
|
||||||
|
"remark":"SeakeerDevice",
|
||||||
|
"authName":"Seakeer",
|
||||||
|
"password":"654321"
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 1,
|
||||||
|
"successCount":0,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"errorInfo":"Invalid MAC"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
5.1.3 Add device without SN
|
||||||
|
Note: A maximum of 100 items at a time.
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
|
||||||
|
|
||||||
|
101 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/rps/addDevicesByMac
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Requ Description
|
||||||
|
Type ired
|
||||||
|
mac String Yes Device MAC, minimum length 12, maximum
|
||||||
|
length 17
|
||||||
|
serverId String No Server ID
|
||||||
|
uniqueServe String No Server address, maximum length 256
|
||||||
|
rUrl
|
||||||
|
authName String No Authentication username, maximum
|
||||||
|
length 128
|
||||||
|
password String No Authentication password, maximum length
|
||||||
|
128
|
||||||
|
remark String No Note, maximum length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
total Integer The total number of batch additions.
|
||||||
|
successCoun Integer The total number of successful additions.
|
||||||
|
t
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors AddError[] Error message.
|
||||||
|
|
||||||
|
AddError object
|
||||||
|
|
||||||
|
102 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The device SN address.
|
||||||
|
errorInfo String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/addDevicesByMac HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"serverId":"ba7c7b13ed114a5fa6f12063ea9dff41",
|
||||||
|
"remark":"SeakeerDevice",
|
||||||
|
"authName":"Seakeer",
|
||||||
|
"password":"654321"
|
||||||
|
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 1,
|
||||||
|
"successCount":0,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"mac":"3a1565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"errorInfo":"Invalid MAC"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
5.1.4 Edit Device
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
|
||||||
|
103 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request URL
|
||||||
|
/v2/rps/devices/{deviceId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Requi Description
|
||||||
|
Type red
|
||||||
|
serverId String No Server ID
|
||||||
|
uniqueServe String No Address of server, maximum length 256
|
||||||
|
rUrl
|
||||||
|
authName String No Username for authentication, maximum
|
||||||
|
length 128
|
||||||
|
password String No Password for authentication, maximum
|
||||||
|
length 128
|
||||||
|
remark String No Note, maximum length 256
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/rps/devices/8d07a56207074d26b61026099625b9e2 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"serverId":"ba7c7b13ed114a5fa6f12063ea9dff41",
|
||||||
|
"remark":"SeakeerDevice",
|
||||||
|
|
||||||
|
104 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"authName":"Seakeer",
|
||||||
|
"password":"654321"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
5.1.5 Device Paging List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/listDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Dat Re Description
|
||||||
|
er a qu
|
||||||
|
Ty ire
|
||||||
|
pe d
|
||||||
|
skip Lon No Number of skipped records, default is 0
|
||||||
|
g
|
||||||
|
limit Lon No Maximum number of records to retrieve, default is
|
||||||
|
g 10, maximum is 500.
|
||||||
|
autoCou Boo No Whether to respond with the total count, it is
|
||||||
|
nt lea recommended to set it to true when querying the
|
||||||
|
n first page, and set it to false for queries on other
|
||||||
|
pages.
|
||||||
|
filter Filt No search parameters
|
||||||
|
er
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Para Data Req Description
|
||||||
|
met Typ uir
|
||||||
|
er e ed
|
||||||
|
|
||||||
|
105 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mac Strin No Device MAC, maximum length 17, supports with : or -,
|
||||||
|
g such as 00:15:65:bb:b1:a1
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Device[] Device information array
|
||||||
|
|
||||||
|
Device object definition
|
||||||
|
|
||||||
|
parameter Data Description
|
||||||
|
Type
|
||||||
|
id String The device ID.
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The device serial number.
|
||||||
|
serverId String The server ID.
|
||||||
|
serverName String The server name.
|
||||||
|
serverUrl String The server address.
|
||||||
|
uniqueServe String The address of the unique server.
|
||||||
|
rUrl
|
||||||
|
ipAddress String The IP address.
|
||||||
|
remark String The remark.
|
||||||
|
dateRegiste Long The binding time.
|
||||||
|
red
|
||||||
|
106 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
lastConnect Long The last time when the device is connected to the
|
||||||
|
ed platform.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/listDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 20,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"mac":"001565bbb1a9"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"serverId": "b25ac1016caf416a90d5ca1ee438153a",
|
||||||
|
"serverName": "SeakeerServerTest",
|
||||||
|
"serverUrl": "https://dm30-devtest.yealinkclient.com/dm.cfg",
|
||||||
|
"ipAddress": null,
|
||||||
|
"dateRegistered": 1542680124026,
|
||||||
|
"lastConnected": null,
|
||||||
|
"remark": "edit"
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
107 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
5.1.6 Device Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/rps/devices/{deviceId}
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Description
|
||||||
|
Type
|
||||||
|
id String The device ID.
|
||||||
|
mac String The device MAC address.
|
||||||
|
sn String The device serial number.
|
||||||
|
serverId String The server ID.
|
||||||
|
serverName String The server name.
|
||||||
|
serverUrl String The server address.
|
||||||
|
uniqueServe String The address of the unique server.
|
||||||
|
rUrl
|
||||||
|
ipAddress String The IP address.
|
||||||
|
remark String The remark.
|
||||||
|
dateRegiste Long The binding time.
|
||||||
|
red
|
||||||
|
lastConnect Long The last time when the device is connected to the
|
||||||
|
ed platform.
|
||||||
|
authName String The username for authentication.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/rps/devices/8d07a56207074d26b61026099625b9e2 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
108 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "8d07a56207074d26b61026099625b9e2",
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"sn":"1106312113402006",
|
||||||
|
"serverId": "b25ac1016caf416a90d5ca1ee438153a",
|
||||||
|
"serverName": "SeakeerServerTest",
|
||||||
|
"serverUrl": "https://dm30-devtest.yealinkclient.com/dm.cfg",
|
||||||
|
"ipAddress": null,
|
||||||
|
"dateRegistered": 1542680124026,
|
||||||
|
"lastConnected": null,
|
||||||
|
"remark": "edit",
|
||||||
|
"authName": "edit",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
5.1.7 Delete Device
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/delDevices
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Require Description
|
||||||
|
er Type d
|
||||||
|
deviceIds String Yes Device ID list, maximum length 200
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
109 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/delDevices HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIdType":"mac",
|
||||||
|
"deviceIds":["001565bbb1a9","001567"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"001567",
|
||||||
|
"msg":"Invalid MAC"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
110 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5.2 RPS server management
|
||||||
|
5.2.1 Add Server
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/servers
|
||||||
|
Body parameters:
|
||||||
|
|
||||||
|
Request Parame Re Description
|
||||||
|
parameters ter qui
|
||||||
|
Type re
|
||||||
|
d
|
||||||
|
serverName String Yes The server name, with no more than 20
|
||||||
|
characters.
|
||||||
|
url String Yes The address of server, with no more
|
||||||
|
than 512 characters.
|
||||||
|
authName String No The username for authentication, with
|
||||||
|
no more than 32 characters.
|
||||||
|
password String No The password for authentication, with
|
||||||
|
no more than 32 characters.
|
||||||
|
certificateUrl String No The certificate URL.
|
||||||
|
serverCertificateUr String No The URL of the server certificate.
|
||||||
|
l
|
||||||
|
serverCertificateE Boolea No Set whether to enable the custom
|
||||||
|
nable n certificate or not.
|
||||||
|
serverCertificate Boolea No Enable this when the certificate type is
|
||||||
|
EnableWithSHA25 n non-SHA256.
|
||||||
|
6
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful, see response parameters.
|
||||||
|
|
||||||
|
111 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
id String The server ID.
|
||||||
|
serverName String The server name.
|
||||||
|
url String The server address.
|
||||||
|
authName String The username for authentication.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/servers HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"serverName":"TestServer",
|
||||||
|
"url":"https://https://www.yealink.com",
|
||||||
|
"serverCertificateEnable": true,
|
||||||
|
"serverCertificateEnableWithSHA256": true,
|
||||||
|
"authName":"Seakeer",
|
||||||
|
"password":"123456",
|
||||||
|
"certificateUrl":"https://www.yealink.com/certificate"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "b38dea23a4e6458188799833b72d950f",
|
||||||
|
"serverName": "TestServer",
|
||||||
|
"url": "https://www.yealink.com",
|
||||||
|
"authName": "Seakeer"
|
||||||
|
}
|
||||||
|
112 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5.2.2 Edit Server
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/rps/servers/{serverId}
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
serverId String Yes The server ID.
|
||||||
|
|
||||||
|
Body parameters:
|
||||||
|
|
||||||
|
Request Parame Re Description
|
||||||
|
parameters ter qui
|
||||||
|
Type re
|
||||||
|
d
|
||||||
|
serverName String Yes The server name, with no more than 20
|
||||||
|
characters.
|
||||||
|
url String Yes The address of server, with no more
|
||||||
|
than 512 characters.
|
||||||
|
authName String No The username for authentication, with
|
||||||
|
no more than 32 characters.
|
||||||
|
password String No The password for authentication, with
|
||||||
|
no more than 32 characters.
|
||||||
|
certificateUrl String No The certificate URL.
|
||||||
|
serverCertificateUr String No The URL of the server certificate.
|
||||||
|
l
|
||||||
|
serverCertificateE Boolea No Set whether to enable the custom
|
||||||
|
nable n certificate or not.
|
||||||
|
serverCertificate Boolea No Enable this when the certificate type is
|
||||||
|
EnableWithSHA25 n non-SHA256.
|
||||||
|
6
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
113 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/rps/servers/b38dea23a4e6458188799833b72d950f HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"serverName":"YealinkServer",
|
||||||
|
"url":"http://www.yealink.com",
|
||||||
|
"authName":"Yealink",
|
||||||
|
"password":"Yealink",
|
||||||
|
"certificateUrl":"http://cer/cer.cer",
|
||||||
|
"serverCertificateEnable": true,
|
||||||
|
"serverCertificateEnableWithSHA256": true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
5.2.3 Delete Server
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/delServers
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Require Description
|
||||||
|
er Type d
|
||||||
|
serverIds String[] Yes Server ID list, maximum length 200
|
||||||
|
|
||||||
|
114 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/delServers HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"serverIds":["b38dea23a4e6458188799833b72d950f","1234"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"1234",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
115 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
5.2.4 Server Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/rps/servers/{serverId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
serverId String Yes The server ID.
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Description
|
||||||
|
Type
|
||||||
|
id Strin The server ID.
|
||||||
|
g
|
||||||
|
serverName Strin The server name.
|
||||||
|
g
|
||||||
|
url Strin The server address.
|
||||||
|
g
|
||||||
|
authName Strin The username for authentication.
|
||||||
|
g
|
||||||
|
certificateUrl Strin The certificate URL.
|
||||||
|
g
|
||||||
|
serverCertificateUrl Strin The URL of the server certificate.
|
||||||
|
g
|
||||||
|
serverCertificateEnable Bool Set whether to enable the custom
|
||||||
|
ean certificate or not.
|
||||||
|
serverCertificateEnable Bool Enable this when the certificate type is
|
||||||
|
WithSHA256 ean non-SHA256.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/rps/servers/b38dea23a4e6458188799833b72d950f HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
116 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "b38dea23a4e6458188799833b72d950f",
|
||||||
|
"serverName": "TestServer",
|
||||||
|
"url": "https://www.yealink.com",
|
||||||
|
"authName": "Seakeer",
|
||||||
|
"certificateUrl":"http://cer/cer.cer",
|
||||||
|
"serverCertificateEnable": true,
|
||||||
|
"serverCertificateEnableWithSHA256": true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
5.2.5 Server Pagination List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/rps/listServers
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Para Data Requ Description
|
||||||
|
mete Type ired
|
||||||
|
r
|
||||||
|
searc Strin No Search keywords, support server name, URL search
|
||||||
|
hKey g
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
auto Bool No Whether to return the total number of records
|
||||||
|
Coun ean
|
||||||
|
t
|
||||||
|
filter Filter No search parameters
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
117 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
name String No The server name.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total data volume
|
||||||
|
data Server[] Server information array
|
||||||
|
|
||||||
|
Server object definition
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
id String The server ID.
|
||||||
|
serverName String The server name.
|
||||||
|
url String The server address.
|
||||||
|
authName String The username for authentication.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/rps/listServers HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 20,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
118 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"name":"test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "b38dea23a4e6458188799833b72d950f",
|
||||||
|
"serverName": "test",
|
||||||
|
"url": "https://www.yealink.com",
|
||||||
|
"authName": "Seakeer"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
6.Call Quality Management
|
||||||
|
6.1 Pagination List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listQoes
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Data Req Parameter Description
|
||||||
|
terNam Type uire
|
||||||
|
e d
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
autoCou Bool No Whether to return the total number of records
|
||||||
|
nt ean
|
||||||
|
119 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
filter Filter No search parameters
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Para Data Req Description
|
||||||
|
met Typ uir
|
||||||
|
er e ed
|
||||||
|
mac Strin No Device MAC, maximum length 17, supports with : or -,
|
||||||
|
g such as 00:15:65:bb:b1:a9
|
||||||
|
siteI List fals List of site IDs to query
|
||||||
|
ds e
|
||||||
|
star Long fals Start time to query
|
||||||
|
tTim e
|
||||||
|
e
|
||||||
|
endT Long fals End time to query
|
||||||
|
ime e
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total data volume
|
||||||
|
data QoeInfo[] QoeInfo information array
|
||||||
|
|
||||||
|
QoeInfo object definition
|
||||||
|
|
||||||
|
Name Type Description
|
||||||
|
id String QOE record id
|
||||||
|
deviceName String Device Name
|
||||||
|
mac String MAC
|
||||||
|
modelName String Device Model
|
||||||
|
firmwareVersion String Firmware version
|
||||||
|
username String Account \u2014 Username
|
||||||
|
displayName String Account \u2014 Display Name
|
||||||
|
|
||||||
|
120 / 164
|
||||||
@@ -0,0 +1,880 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 121-140
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 121-140 of 164
|
||||||
|
**Chunk:** 7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
siteName String Affiliated site
|
||||||
|
quality String Call Quality Good, Poor, Bad
|
||||||
|
startTime long The call start timestamp.
|
||||||
|
endTime long The call end timestamp.
|
||||||
|
callerURI String Caller URL, fromURI
|
||||||
|
calleeURI String Called URL, toURI
|
||||||
|
duration long Call duration (ms)
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listQoes HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip":0,
|
||||||
|
"limit":10,
|
||||||
|
"filter":{
|
||||||
|
"startTime":1700876148000,
|
||||||
|
"endTime":1703468148329,
|
||||||
|
"siteIds":["126b25122362470daca91ae8ad038a27"]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "a26b25122362470daca91ae8ad038a27",
|
||||||
|
"deviceName": null,
|
||||||
|
"mac": "805ec091cfc6",
|
||||||
|
"modelName": "UNKNOW",
|
||||||
|
"firmwareVersion": "108.86.0.57",
|
||||||
|
121 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"username": "8195",
|
||||||
|
"displayName": "8195",
|
||||||
|
"siteName": "global测试-baiyf",
|
||||||
|
"quality": "Good",
|
||||||
|
"startTime": 1660542991000,
|
||||||
|
"endTime": 1660543218000,
|
||||||
|
"callerURI": "\"8195\" <sip:8195@ume.yealink.com:5061>",
|
||||||
|
"calleeURI": "<sip:77031@ume.yealink.com>",
|
||||||
|
"duration": 227000
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
6.2 QOE details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/qoe/{qoeId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
qoeId String Yes QOE记录 ID
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Name Ty Description
|
||||||
|
pe
|
||||||
|
id Str QOE record ID.
|
||||||
|
ing
|
||||||
|
sessionId Str The meeting ID.
|
||||||
|
ing
|
||||||
|
reportTime lon The reporting time of the call data.
|
||||||
|
g
|
||||||
|
deviceName Str The device name.
|
||||||
|
ing
|
||||||
|
mac Str MAC
|
||||||
|
ing
|
||||||
|
|
||||||
|
|
||||||
|
122 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
modelName Str The device model.
|
||||||
|
ing
|
||||||
|
firmwareVersio Str The firmware version.
|
||||||
|
n ing
|
||||||
|
ip Str The device IP address.
|
||||||
|
ing
|
||||||
|
deviceType Str The device type: audio, video.
|
||||||
|
ing
|
||||||
|
username Str Account-username.
|
||||||
|
ing
|
||||||
|
displayName Str Account-display name.
|
||||||
|
ing
|
||||||
|
serverType Str The account type.
|
||||||
|
ing
|
||||||
|
siteName Str The belonging site.
|
||||||
|
ing
|
||||||
|
quality Str The call quality, including Good, Poor, and Bad.
|
||||||
|
ing
|
||||||
|
startTime lon The call start timestamp.
|
||||||
|
g
|
||||||
|
endTime lon The call end timestamp.
|
||||||
|
g
|
||||||
|
callId Str The call ID.
|
||||||
|
ing
|
||||||
|
callerURI Str Calling URL, fromURI.
|
||||||
|
ing
|
||||||
|
calleeURI Str Called URL, toURI.
|
||||||
|
ing
|
||||||
|
isCaller boo Check whether the user is the caller of the querying
|
||||||
|
lea call record or not.
|
||||||
|
n
|
||||||
|
callType Str The call type.
|
||||||
|
ing
|
||||||
|
|
||||||
|
123 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
confURI Str Configure the URL.
|
||||||
|
ing
|
||||||
|
time lon The current time.
|
||||||
|
g
|
||||||
|
event Str The events.
|
||||||
|
ing
|
||||||
|
duration lon The call duration (ms).
|
||||||
|
g
|
||||||
|
inJitterAvg int Input the average jitter.
|
||||||
|
inJitterMax int Input the maximum jitter.
|
||||||
|
inLossRateAvg dou Input the average loss rate.
|
||||||
|
ble
|
||||||
|
inLossRateMax dou Input the maximum loss rate.
|
||||||
|
ble
|
||||||
|
inLossTotal int Input the missing value.
|
||||||
|
inDelayAvg int Input the average delay.
|
||||||
|
inDelayMax int Input the maximum delay.
|
||||||
|
inListenMosAvg dou Input the average value for the call answering
|
||||||
|
ble quality.
|
||||||
|
inListenMosMin dou Input the minimum value for the call answering
|
||||||
|
ble quality.
|
||||||
|
inConversation dou Input the average value for the call quality rating.
|
||||||
|
alMosAvg ble
|
||||||
|
inConversation dou Input the minimum value for the call quality rating.
|
||||||
|
alMosMin ble
|
||||||
|
inReceivePacke int The total number of input packages.
|
||||||
|
tTotal
|
||||||
|
inPayloadNam Str Input the load name.
|
||||||
|
e ing
|
||||||
|
outJitterAvg int Output the average jitter.
|
||||||
|
outJitterMax int Output the maximum jitter.
|
||||||
|
outLossRateAv dou Output the average loss rate.
|
||||||
|
g ble
|
||||||
|
124 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
outLossRateMa dou Output the maximum loss rate.
|
||||||
|
x ble
|
||||||
|
outLossTotal int Output the maximum loss.
|
||||||
|
outDelayAvg int Output the average delay.
|
||||||
|
outDelayMax int Output maximum delay.
|
||||||
|
outListenMosA dou Output the average value for the call answering
|
||||||
|
vg ble quality.
|
||||||
|
outListenMosM dou Output the minimum value for the call answering
|
||||||
|
in ble quality.
|
||||||
|
outConversati dou Output the average value for the call quality.
|
||||||
|
onalMosAvg ble
|
||||||
|
outConversati dou Output the minimum value for the call quality.
|
||||||
|
onalMosMin ble
|
||||||
|
outReceivePack int The total number of output packages.
|
||||||
|
etTotal
|
||||||
|
outPayloadNa Str Output the load name.
|
||||||
|
me ing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/qoe/c5a56e74bab546c8bc3cccf2e82aeb0c HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
{
|
||||||
|
"id": "a26b25122362470daca91ae8ad038a27",
|
||||||
|
"sessionId": "805ec091cfc6-0_934910374@10.50.144.6",
|
||||||
|
"reportTime": 1660543237204,
|
||||||
|
"deviceName": null,
|
||||||
|
"mac": "805ec091cfc6",
|
||||||
|
"modelName": "UNKNOW",
|
||||||
|
|
||||||
|
125 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"firmwareVersion": "108.86.0.57",
|
||||||
|
"ip": "10.50.144.6",
|
||||||
|
"deviceType": "UNKNOW",
|
||||||
|
"username": "8195",
|
||||||
|
"displayName": "8195",
|
||||||
|
"serverType": "SIP",
|
||||||
|
"siteName": "global测试-baiyf",
|
||||||
|
"quality": "Good",
|
||||||
|
"startTime": 1660542991000,
|
||||||
|
"endTime": 1660543218000,
|
||||||
|
"callId": "0_934910374@10.50.144.6",
|
||||||
|
"callerURI": "\"8195\" <sip:8195@ume.yealink.com:5061>",
|
||||||
|
"calleeURI": "<sip:77031@ume.yealink.com>",
|
||||||
|
"remoteIP": null,
|
||||||
|
"callType": "p2p",
|
||||||
|
"confURI": null,
|
||||||
|
"time": 1660543218000,
|
||||||
|
"event": "0",
|
||||||
|
"duration": 227000,
|
||||||
|
"inJitterAvg": 3,
|
||||||
|
"inJitterMax": 3,
|
||||||
|
"inLossRateAvg": 0.0,
|
||||||
|
"inLossRateMax": 0.0,
|
||||||
|
"inDelayAvg": 5,
|
||||||
|
"inDelayMax": 5,
|
||||||
|
"inListenMosAvg": 4.0,
|
||||||
|
"inListenMosMin": 4.0,
|
||||||
|
"inConversationalMosAvg": 4.0,
|
||||||
|
"inConversationalMosMin": 4.0,
|
||||||
|
"inReceivePacketTotal": 11299,
|
||||||
|
"inPayloadName": "G722",
|
||||||
|
"outJitterAvg": 3,
|
||||||
|
"outJitterMax": 4,
|
||||||
|
"outLossRateAvg": 0.0,
|
||||||
|
"outLossRateMax": 0.0,
|
||||||
|
"outLossTotal": 0,
|
||||||
|
"outDelayAvg": 5,
|
||||||
|
"outDelayMax": 5,
|
||||||
|
"outListenMosAvg": 4.4,
|
||||||
|
"outListenMosMin": 4.0,
|
||||||
|
"outConversationalMosAvg": 4.4,
|
||||||
|
"outConversationalMosMin": 4.4,
|
||||||
|
"outReceivePacketTotal": 11333,
|
||||||
|
|
||||||
|
126 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"outPayloadName": "G722",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
7.Statistics
|
||||||
|
7.1 Total number of devices
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/statistics/deviceCount
|
||||||
|
Query parameter
|
||||||
|
|
||||||
|
Para Dat Re Parameter Description
|
||||||
|
met a qu
|
||||||
|
erN Ty ir
|
||||||
|
ame pe ed
|
||||||
|
devic Int fal Device status, 1: online, 0: offline, -1: not reported, if this
|
||||||
|
eStat ege se parameter is not passed, get the total count.
|
||||||
|
us r
|
||||||
|
devi Int fal Device Type
|
||||||
|
ceTy ege se
|
||||||
|
pe r
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Name Type Description
|
||||||
|
total Long Device quantity.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/statistics/deviceCount?deviceStatus=1 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
|
||||||
|
127 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total":100
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
7.2 QOE statistics
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/statistics/qoe
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Para Dat Re Parameter Description
|
||||||
|
met a qu
|
||||||
|
erN Ty ir
|
||||||
|
ame pe ed
|
||||||
|
siteId Str fal List of site IDs to be counted
|
||||||
|
s ing se
|
||||||
|
[]
|
||||||
|
start Lo fal The start time to be counted; this value must be used
|
||||||
|
Time ng se together with endTime, otherwise it will not take effect.
|
||||||
|
endT Lo fal The end time to be counted must be used together with
|
||||||
|
ime ng se startTime; otherwise, it will not take effect.
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Name Type Description
|
||||||
|
total long The total number of calls.
|
||||||
|
badPercentage doubl The percentage of poor quality.
|
||||||
|
e
|
||||||
|
badTotal long Poor call quality.
|
||||||
|
goodPercentage doubl The percentage of good quality.
|
||||||
|
e
|
||||||
|
goodTotal long Good call quality.
|
||||||
|
128 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
meetingPercentage doubl The percentage of total meetings.
|
||||||
|
e
|
||||||
|
meetingTotal long The total number of meetings.
|
||||||
|
p2pPercentage doubl The percentage of p2p calls.
|
||||||
|
e
|
||||||
|
p2pTotal long The total number of p2p calls.
|
||||||
|
poorPercentage doubl The percentage of average quality.
|
||||||
|
e
|
||||||
|
poorTotal long General call quality.
|
||||||
|
voiceMailPercentage doubl The total number of voice mails.
|
||||||
|
e
|
||||||
|
voiceMailTotal long The total number of voice mails.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/statistics/qoe HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"startTime":1700876148000,
|
||||||
|
"endTime":1703468148329,
|
||||||
|
"siteIds":["126b25122362470daca91ae8ad038a27"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
"total":25,
|
||||||
|
"goodTotal": 24,
|
||||||
|
"poorTotal": 1,
|
||||||
|
"badTotal": 0,
|
||||||
|
"goodPercentage": 0.96,
|
||||||
|
"poorPercentage": 0.04,
|
||||||
|
"badPercentage": 0.0,
|
||||||
|
129 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"p2pTotal": 25,
|
||||||
|
"meetingTotal": 0,
|
||||||
|
"voiceMailTotal": 0,
|
||||||
|
"p2pPercentage": 1.0,
|
||||||
|
"meetingPercentage": 0.0,
|
||||||
|
"voiceMailPercentage": 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
8.Model Management
|
||||||
|
8.1 Model List
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/models
|
||||||
|
Query parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
pe Device
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
data Model[] The model information array.
|
||||||
|
|
||||||
|
Model object definition
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String The model ID.
|
||||||
|
name String The model name.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/models?deviceType=1 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
130 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
[{
|
||||||
|
"id": "61e659e4d78d42ebada88ef1eb751b64",
|
||||||
|
"name":"SIP-T54S",
|
||||||
|
|
||||||
|
},{
|
||||||
|
"id":"02c47b640c3046dc86853c9ccfd37dd0",
|
||||||
|
"name":"SIP-T31P"
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9.Site Management
|
||||||
|
9.1 Add Site
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sites
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Require Description
|
||||||
|
Type d
|
||||||
|
name String Yes Site name, maximum length 128
|
||||||
|
parentId String Yes Parent Site ID
|
||||||
|
Description String No Description, maximum length 1024
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
201 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
131 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
id String Site ID
|
||||||
|
name String Site Name
|
||||||
|
parentId String Parent Site ID
|
||||||
|
siteNumber String Site number
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/sites HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name":"site1",
|
||||||
|
"parentId":"d3a6ff43f2154b868ba1157eb3677c1e",
|
||||||
|
"Description":"site1"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 201 Created
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
"name":"site1",
|
||||||
|
"parentId":"d3a6ff43f2154b868ba1157eb3677c1e",
|
||||||
|
"siteNumber":"ofpb5gvg"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
9.2 Edit Site
|
||||||
|
Request Method
|
||||||
|
PATCH
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sites/{siteId}
|
||||||
|
|
||||||
|
132 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
siteId String Yes Site ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parameter Data Require Description
|
||||||
|
Type d
|
||||||
|
name String No Site Name
|
||||||
|
parentId String No Parent Site ID
|
||||||
|
Description String No Description,maximum length 1024.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPATCH /v2/dm/sites/0006d62003684754b11c09c5d94ea687 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name":"site11",
|
||||||
|
"Description":"site11",
|
||||||
|
"parentId":"d3a6ff43f2154b868ba1157eb3677c1e"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
9.3 Delete Site
|
||||||
|
|
||||||
|
133 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request Method
|
||||||
|
DELETE
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sites/{siteId}
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
siteId String Yes Site ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpDELETE /v2/dm/sites/e33b8f25247e45de84dd4c74503b241a HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
9.4 Site List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listSites
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Data Req Parameter Description
|
||||||
|
terNam Type uire
|
||||||
|
e d
|
||||||
|
skip Long No Number of skipped records, default is 0
|
||||||
|
limit Long No Maximum number of records to retrieve, default is
|
||||||
|
10, maximum is 500.
|
||||||
|
autoCou Bool No Whether to return the total number of records
|
||||||
|
nt ean
|
||||||
|
filter Filter No search parameters
|
||||||
|
|
||||||
|
134 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
name String No Site Name
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
data Site[] Site information array
|
||||||
|
|
||||||
|
Site object definition
|
||||||
|
|
||||||
|
Paramet Data Description
|
||||||
|
er Type
|
||||||
|
id String site id
|
||||||
|
parentId String Parent Site ID
|
||||||
|
name String Site Name
|
||||||
|
level Integer Site level, 0 indicates the root site.
|
||||||
|
sequence Integer “Serial number of the site in the hierarchy”
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listSites HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip":0,
|
||||||
|
"limit":10,
|
||||||
|
"filter":{
|
||||||
|
"name":"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
|
||||||
|
135 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id":"1e876bc0fca24b728eeb3e52bbfebf5e",
|
||||||
|
"parentId":"23b63cac1f6d4134b5459a4face901c3",
|
||||||
|
"name":"test",
|
||||||
|
"level":2,
|
||||||
|
"sequence":1,
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
9.5 site details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/sites/{siteId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
siteId String Yes Site ID
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
id String Site ID
|
||||||
|
parentId String Parent Site ID
|
||||||
|
name String Site Name
|
||||||
|
siteNumber String Site number
|
||||||
|
Description String Description information
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
|
||||||
|
136 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpGET /v2/dm/sites/288ff0d0bd4948b0a95785ba7bc72a8e HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "288ff0d0bd4948b0a95785ba7bc72a8e",
|
||||||
|
"name":"site1",
|
||||||
|
"parentId":"d3a6ff43f2154b868ba1157eb3677c1e",
|
||||||
|
"siteNumber":"ofpb5gvg",
|
||||||
|
"Description":"site1"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10.Resource Management
|
||||||
|
10.1 Firmware Management
|
||||||
|
10.1.1 Firmware List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listFirmwares
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
|
||||||
|
137 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
sk Lo N Number of skipped records, default is 0
|
||||||
|
ip ng o
|
||||||
|
li Lo N Maximum number of records to retrieve, default is 10,
|
||||||
|
mi ng o maximum is 500.
|
||||||
|
t
|
||||||
|
au Bo N Whether to respond with the total count, it is recommended
|
||||||
|
to ol o to set it to true when querying the first page, and pass false
|
||||||
|
Co ea for subsequent page turns.
|
||||||
|
un n
|
||||||
|
t
|
||||||
|
fil Fil N search parameters
|
||||||
|
te te o
|
||||||
|
r r
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Paramete Data Requi Description
|
||||||
|
r Type red
|
||||||
|
modelId String No Firmware Model ID
|
||||||
|
firmwareT Integer No Firmware Type 0: Master Device 1:
|
||||||
|
ype Accessory
|
||||||
|
deviceTyp Integer No Device Type 1: Phone Device 3: Room
|
||||||
|
e Device
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
138 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Firmware[] Firmware information array
|
||||||
|
|
||||||
|
Firmware object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String firmware id
|
||||||
|
name String Firmware Name
|
||||||
|
deviceType Integer Device Type 1: Phone Device 3: Room Device
|
||||||
|
firmwareTyp Integer Firmware Type 0: Master Device 1: Accessory
|
||||||
|
e
|
||||||
|
version String Firmware version number
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listFirmwares HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"firmwareType":0,
|
||||||
|
"deviceType":1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
139 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "01d7ae1897f7418dad2d26609329be38",
|
||||||
|
"name":"t54s firmware",
|
||||||
|
"deviceType":1,
|
||||||
|
"firmwareType":0,
|
||||||
|
"version":"108.85.3.19",
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10.1.2 Firmware Details
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/firmwares/{firmwareId}
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
firmwareId String Yes firmware ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String firmware id
|
||||||
|
name String Firmware Name
|
||||||
|
filename String Firmware file name
|
||||||
|
deviceType Integer Device Type 1: Phone Device 3: Room Device
|
||||||
|
140 / 164
|
||||||
@@ -0,0 +1,892 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 141-160
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 141-160 of 164
|
||||||
|
**Chunk:** 8
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
firmwareTyp Integer Firmware Type 0: Master Device 1: Accessory
|
||||||
|
e
|
||||||
|
downloadUrl String Download address
|
||||||
|
version String Firmware version number
|
||||||
|
supportMode String[] Supported model list
|
||||||
|
ls
|
||||||
|
siteId String Site ID
|
||||||
|
Description String Firmware Description
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/firmwares/01d7ae1897f7418dad2d26609329be38 HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "01d7ae1897f7418dad2d26609329be38",
|
||||||
|
"name":"t54s firmware",
|
||||||
|
"filename":"t54s.rom",
|
||||||
|
"deviceType":1,
|
||||||
|
"firmwareType":0,
|
||||||
|
"downloadUrl":"https://resources.yiot.yealink.com/yiot-manager/api/v1/terminal/
|
||||||
|
resource/download/69847884d3be4327b86bffd0513c4572/1/
|
||||||
|
T46S(T48S,T42S,T41S)-66.86.0.15.rom",
|
||||||
|
"version":"108.85.3.19",
|
||||||
|
"supportModels":["SIP-T54S"],
|
||||||
|
"siteId":"ee06cddee78948a298fc12565a35cdbe"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10.1.3 Official Firmware List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
141 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/v2/dm/listOfficalFirmwares
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Parame Dat Re Description
|
||||||
|
ter a qu
|
||||||
|
Ty ire
|
||||||
|
pe d
|
||||||
|
skip Lo No Number of skipped records, default is 0
|
||||||
|
ng
|
||||||
|
limit Lo No Maximum number of records to retrieve, default is 10,
|
||||||
|
ng maximum is 500.
|
||||||
|
autoCou Boo No Whether to respond with the total count, it is
|
||||||
|
nt lea recommended to set it to true when querying the first
|
||||||
|
n page, and pass false for subsequent page turns.
|
||||||
|
filter Filt Yes search parameters
|
||||||
|
er
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Parameter Data Type Required Description
|
||||||
|
modelId String Yes Firmware model ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
|
||||||
|
142 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
data Firmware[] Firmware information array
|
||||||
|
|
||||||
|
Firmware object definition
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
id String Firmware ID
|
||||||
|
name String Firmware name
|
||||||
|
version String Firmware version number
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listOfficalFirmwares HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"modelId":"23a5da55f3534f84abc7956a9f183330"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "01d7ae1897f7418dad2d26609329be38",
|
||||||
|
"name":"t54s firmware",
|
||||||
|
"version":"108.85.3.19"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10.1.4 Push Firmware
|
||||||
|
143 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/firmwares/{firmwareId}/push
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
firmwareId String Yes Firmware ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
deviceIds String[] Yes Device ID list, maximum length 200
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
pe Device
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
|
||||||
|
144 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/firmwares/09972370b360461d868e3e02f9cdec77/push HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"],
|
||||||
|
"deviceType":1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"00099642675e4d4bb5e91fd9ae5ce585",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
10.1.5 Push Official Firmware
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/officalFirmwares/{officalFirmwareId}/push
|
||||||
|
PATH parameter
|
||||||
|
|
||||||
|
145 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Type Require Description
|
||||||
|
d
|
||||||
|
officalFirmwareId String Yes Official firmware ID
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requi Description
|
||||||
|
er Type red
|
||||||
|
deviceIds String[] Yes Device ID list, maximum length 200
|
||||||
|
deviceTy Integer Yes Device Type 1: Phone Device 3: Room
|
||||||
|
pe Device
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
total Integer Delete total count
|
||||||
|
successCount Integer Total number of successful deletions.
|
||||||
|
failureCount Integer The total number of failures.
|
||||||
|
errors OpError[] Error message.
|
||||||
|
|
||||||
|
OpError object
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
field String Error field
|
||||||
|
msg String Error message.
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
|
||||||
|
146 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPOST /v2/dm/officalFirmwares/09972370b360461d868e3e02f9cdec77/push
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceIds":
|
||||||
|
["0006572538f74e8683716cf961caa95b","00099642675e4d4bb5e91fd9ae5ce585"],
|
||||||
|
"deviceType":1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"total": 2,
|
||||||
|
"successCount":1,
|
||||||
|
"failureCount":1,
|
||||||
|
"errors":[{
|
||||||
|
"field":"00099642675e4d4bb5e91fd9ae5ce585",
|
||||||
|
"msg":"The resource does not exist or has been deleted"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.Device Diagnosis
|
||||||
|
User Manual: After successfully calling the diagnostic interface, obtain the
|
||||||
|
diagnosisId from the response, and periodically poll (recommended every 10
|
||||||
|
seconds) to check the diagnostic status. If the status response is successful
|
||||||
|
(status=success), you can obtain the download URL for the diagnostic file from
|
||||||
|
the response and retrieve the file using the URL.
|
||||||
|
11.1 Get the list of network port types
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/networkInterfaces
|
||||||
|
147 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
data String[] Type list
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/devices/8d07a56207074d26b61026099625b9e2/networkInterfaces
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
["wan","wlan0","ext0"]
|
||||||
|
|
||||||
|
|
||||||
|
11.2 Start packet capture
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/startPacketCapture
|
||||||
|
|
||||||
|
148 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
ne St Ye Network port, the specific value is obtained from the network
|
||||||
|
tw ri s interface type list, generally includes wan (Wide Area Network
|
||||||
|
or ng port), ext0 (external telephone line port), wlan0 (Wireless
|
||||||
|
kI Local Area Network port), default is wan.
|
||||||
|
nt
|
||||||
|
er
|
||||||
|
fa
|
||||||
|
ce
|
||||||
|
ty In N Packet capture type, 0 – Custom, 1 – SIP or H245 or H225, 2 –
|
||||||
|
pe te o RTP, 3 – Not RTP
|
||||||
|
ge
|
||||||
|
r
|
||||||
|
fil St N Capture filtering information, this value is only needed when
|
||||||
|
te ri o the capture type = 0.
|
||||||
|
r ng
|
||||||
|
du In Ye Capture maximum duration, unit seconds, range: 180~3600
|
||||||
|
ra te s
|
||||||
|
ti ge
|
||||||
|
on r
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
|
||||||
|
149 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
diagnosisId String Diagnosis Session ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/startPacketCapture
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"networkInterface":"wan",
|
||||||
|
"type":0,
|
||||||
|
"duration":180
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.3 End Capture
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/stopPacketCapture
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
150 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
diagnosisId String Yes Diagnosis ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
204 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/stopPacketCapture
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 204
|
||||||
|
|
||||||
|
|
||||||
|
11.4 screenshots
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/captureScreen
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
|
||||||
|
151 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
diagnosisId String Diagnosis ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/captureScreen
|
||||||
|
HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.5 Export system log
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/exportSyslog
|
||||||
|
|
||||||
|
152 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
diagnosisId String Diagnosis ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/exportSyslog HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.6 Export Configuration File
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
|
||||||
|
|
||||||
|
153 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/v2/dm/devices/{deviceId}/exportConfig
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/exportConfig HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.7 Detect Network \u2014 ping
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/ping
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
154 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requir Description
|
||||||
|
er Type ed
|
||||||
|
host String Yes IP/domain name to ping
|
||||||
|
times Integer Yes Number of times, minimum 1, maximum
|
||||||
|
30
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
diagnosisId String Diagnosis ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/ping HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"host":"www.google.com",
|
||||||
|
"times":1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
|
||||||
|
155 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.8 Network Detection \u2014 traceroute
|
||||||
|
Request Method
|
||||||
|
PUT
|
||||||
|
Request URL
|
||||||
|
/v2/dm/devices/{deviceId}/traceroute
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
deviceId String Yes The device ID.
|
||||||
|
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Paramet Data Requir Description
|
||||||
|
er Type ed
|
||||||
|
host String Yes IP/domain name to traceroute
|
||||||
|
times Integer Yes Number of times, minimum 1, maximum
|
||||||
|
30
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
diagnosisId String Diagnosis ID
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
|
||||||
|
156 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
httpPUT /v2/dm/devices/8d07a56207074d26b61026099625b9e2/traceroute HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"host":"www.google.com",
|
||||||
|
"times":1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"diagnosisId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
11.9 Query Diagnostic Status
|
||||||
|
Request Method
|
||||||
|
GET
|
||||||
|
Request URL
|
||||||
|
/v2/dm/diagnosis/{diagnosisId}/status
|
||||||
|
Path parameter
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
diagnosisId String Yes Diagnosis ID
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful, see response parameters.
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
157 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Para Data Description
|
||||||
|
mete Type
|
||||||
|
r
|
||||||
|
devic Strin Device ID
|
||||||
|
eId g
|
||||||
|
statu Strin Diagnosis Status: inprogress: In Progress success: Success
|
||||||
|
s g failure: Failure
|
||||||
|
url Strin File download address, available when status = success.
|
||||||
|
g
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpGET /v2/dm/diagnosis/0006d62003684754b11c09c5d94ea687/status HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"deviceId": "0006d62003684754b11c09c5d94ea687",
|
||||||
|
"status":"success",
|
||||||
|
"url":https://yealinkussdev.blob.core.windows.net/single-12/allinone-10.122.131.12-uss%2F40ae7a
|
||||||
|
sv=2021-06-08&spr=https%2Chttp&se=2024-01-10T02%3A06%3A01Z&sr=b&sp=r&sig=JHco76T2o4i
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
12.Alarm Management
|
||||||
|
12.1 Alarm List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listAlarms
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
158 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
r e d
|
||||||
|
sk Lo N Number of skipped records, default is 0
|
||||||
|
ip ng o
|
||||||
|
li Lo N Maximum number of records to retrieve, default is 10,
|
||||||
|
mi ng o maximum is 500.
|
||||||
|
t
|
||||||
|
au Bo N Whether to respond with the total count, it is recommended
|
||||||
|
to ol o to set it to true when querying the first page, and pass false
|
||||||
|
Co ea for subsequent page turns.
|
||||||
|
un n
|
||||||
|
t
|
||||||
|
fil Fil N search parameters
|
||||||
|
te te o
|
||||||
|
r r
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
Par Dat Re Description
|
||||||
|
ame a qu
|
||||||
|
ter Typ ire
|
||||||
|
e d
|
||||||
|
mac Stri No Device MAC fuzzy search keyword, maximum length 17,
|
||||||
|
ng supports with : or -, such as 00:15:65:bb:b1:a9
|
||||||
|
dev Int No Device Type, 1: Phone Device 3: Room Device, leave blank
|
||||||
|
iceT ege for all
|
||||||
|
ype r
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
|
||||||
|
159 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Alarm[] Alarm information array
|
||||||
|
|
||||||
|
Alarm object definition
|
||||||
|
|
||||||
|
Parameter Data Description
|
||||||
|
Type
|
||||||
|
id String Alarm ID
|
||||||
|
event String Alarm Event Name
|
||||||
|
level Integer Alarm Level 1-Minor 2-Major 3-Critical
|
||||||
|
mac String MAC
|
||||||
|
model String model
|
||||||
|
ip String IP
|
||||||
|
siteName String site
|
||||||
|
status Integer Processing status 1-active 2-solve 3-ignore
|
||||||
|
firstAlarmTim Long First alarm time
|
||||||
|
e
|
||||||
|
lastAlarmTime Long Last alarm time
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listAlarms HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
160 / 164
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Open API for Yealink Management Cloud Service V4X.pdf - Pages 161-164
|
||||||
|
|
||||||
|
**Source:** Open API for Yealink Management Cloud Service V4X.pdf
|
||||||
|
**Pages:** 161-164 of 164
|
||||||
|
**Chunk:** 9
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"mac":"001565"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"id": "01d7ae1897f7418dad2d26609329be38",
|
||||||
|
"event":"Offline",
|
||||||
|
"level":3,
|
||||||
|
"mac":"001565bbb1a9",
|
||||||
|
"model":"SIP-T54S",
|
||||||
|
"ip":"10.50.198.156",
|
||||||
|
"siteName":"test",
|
||||||
|
"status":1,
|
||||||
|
"firstAlarmTime":1737082468768
|
||||||
|
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
13.Operation Log Management
|
||||||
|
13.1 Operation Log List
|
||||||
|
Request Method
|
||||||
|
POST
|
||||||
|
Request URL
|
||||||
|
/v2/dm/listOpLogs
|
||||||
|
Body parameter
|
||||||
|
|
||||||
|
Pa Da Re Description
|
||||||
|
ra ta qu
|
||||||
|
me Ty ir
|
||||||
|
te p e
|
||||||
|
161 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
r e d
|
||||||
|
sk Lo N Number of skipped records, default is 0
|
||||||
|
ip ng o
|
||||||
|
li Lo N Maximum number of records to retrieve, default is 10,
|
||||||
|
mi ng o maximum is 500.
|
||||||
|
t
|
||||||
|
au Bo N Whether to respond with the total count, it is recommended
|
||||||
|
to ol o to set it to true when querying the first page, and pass false
|
||||||
|
Co ea for subsequent page turns.
|
||||||
|
un n
|
||||||
|
t
|
||||||
|
fil Fil Ye search parameters
|
||||||
|
te te s
|
||||||
|
r r
|
||||||
|
|
||||||
|
Filter object definition
|
||||||
|
|
||||||
|
parameter Data Type Required Description
|
||||||
|
startTime Long No Start time
|
||||||
|
endTime Long No End time
|
||||||
|
|
||||||
|
HTTP status code
|
||||||
|
|
||||||
|
Code Description
|
||||||
|
200 Operation successful
|
||||||
|
400 Client parameter exception
|
||||||
|
401 Authentication failed
|
||||||
|
500 Server exception
|
||||||
|
|
||||||
|
Response parameters
|
||||||
|
|
||||||
|
Parameter Data Type Description
|
||||||
|
skip Long offset
|
||||||
|
limit Long The maximum returned number.
|
||||||
|
total Long Total quantity
|
||||||
|
data Log[] Log information array
|
||||||
|
162 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Log object definition
|
||||||
|
|
||||||
|
parameter Data Type Description
|
||||||
|
module String Log Module
|
||||||
|
operationType String Log Type
|
||||||
|
operationObject String Log Operation Object
|
||||||
|
operator String Operator
|
||||||
|
ip String IP
|
||||||
|
createTime Long Time
|
||||||
|
result String Result
|
||||||
|
|
||||||
|
Example Request
|
||||||
|
|
||||||
|
httpPOST /v2/dm/listOpLogs HTTP/1.1
|
||||||
|
Host: api.ymcs.yealink.com
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"autoCount": true,
|
||||||
|
"filter":{
|
||||||
|
"startTime":1737099689158,
|
||||||
|
"endTime":1738205662000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Example Response
|
||||||
|
|
||||||
|
httpHTTP/1.1 200
|
||||||
|
Content-Type: application/json;charset=UTF-8
|
||||||
|
|
||||||
|
{
|
||||||
|
"skip": 0,
|
||||||
|
"limit": 10,
|
||||||
|
"total": 1,
|
||||||
|
"data":[{
|
||||||
|
"module":"i18n.yiot.backend.module.device.management",
|
||||||
|
"operationTypetype":"i18n.yiot.backend.operation.device.management.restart",
|
||||||
|
|
||||||
|
163 / 164
|
||||||
|
Open API for Yealink Management Cloud Service V4X
|
||||||
|
|
||||||
|
|
||||||
|
"operationObject":"805ec0985cd2",
|
||||||
|
"operator":"yiot123@yealink.com",
|
||||||
|
"ip":"10.122.131.12",
|
||||||
|
"createTime":"1736999161388",
|
||||||
|
"result":"success"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
164 / 164
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf - Pages 1-20
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf
|
||||||
|
**Pages:** 1-20 of 84
|
||||||
|
**Chunk:** 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Single Sign-On
|
||||||
|
You can also log in via SSO , which is using your Microsoft account to easily log in
|
||||||
|
to the YMCS platform, effectively improving login efficiency, and avoiding some
|
||||||
|
of the security risks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign In with General Account
|
||||||
|
Sign in with a reseller account
|
||||||
|
Procedure
|
||||||
|
1. For the first login, click the login address in the email or enter the login address (https://ymcs.yealink.com/
|
||||||
|
reseller) in the browser address bar.
|
||||||
|
2. Optional: Switch the language in the top-right corner.
|
||||||
|
3. Enter your account and password and click Sign In.
|
||||||
|
|
||||||
|
💡 Note:
|
||||||
|
If you are a first-time user, the system will prompt you to agree to the use of cookies, accept the
|
||||||
|
privacy policy and terms of service, and update your password if necessary.
|
||||||
|
If the login protection is enabled to enhance login security, you need to perform identity verification
|
||||||
|
after logging in. For more information, refer to the login protection.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign in with a support account
|
||||||
|
Prerequisites
|
||||||
|
You own a support account, and the support account has channel permissions.
|
||||||
|
|
||||||
|
2 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
About how to apply for an account with channel permissions, please refer to:
|
||||||
|
FAQ.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. For the first login, click the login address in the email or enter the login address (https://ymcs.yealink.com/
|
||||||
|
reseller) in the browser address bar.
|
||||||
|
2. Optional: Switch the language in the top-right corner.
|
||||||
|
3. Click Support.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Enter your account and password and agree to the privacy policy.
|
||||||
|
5. Click Login.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Multiple Data Centers
|
||||||
|
YMCS has three data centers, EU, US, and AU regions.
|
||||||
|
The data between each region is isolated, meaning that data belonging to
|
||||||
|
enterprises under the EU region will be stored in the EU region data center,
|
||||||
|
while data belonging to enterprises under the US region will be stored in the US
|
||||||
|
region data center.
|
||||||
|
You can view and manage the enterprises and devices under different regions. For more information, see
|
||||||
|
manage enterprise users.
|
||||||
|
When creating enterprise users, you can select the belonging region for the enterprise account. And the
|
||||||
|
enterprise data will be stored in the region you select. For more information, see add enterprise users.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Edit Account Information
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
Click and go to the Account Settings page to view and edit the account
|
||||||
|
information. You can also switch languages, view the privacy policy and terms of
|
||||||
|
service, download documents, or sign out.
|
||||||
|
|
||||||
|
Edit the Login Password
|
||||||
|
Introduction
|
||||||
|
To ensure account security, we recommend that you change the password
|
||||||
|
regularly.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. Click > Account Settings.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. In the Password field, click Edit.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Enter the current password, the new password, and the confirm password.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
Password requirements:
|
||||||
|
The password must be between 8 and 32 characters.
|
||||||
|
The password must contain at least three of the following: numbers, lowercase and uppercase
|
||||||
|
letters, and special characters.
|
||||||
|
The password must only contain numbers, lowercase and uppercase letters, and special characters
|
||||||
|
(excluding space).
|
||||||
|
Password cannot contain account.
|
||||||
|
The password cannot contain continuous or repeated more than 3 times characters.
|
||||||
|
|
||||||
|
|
||||||
|
5. Click OK.
|
||||||
|
6.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
7 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Edit Contacts and Contact Number
|
||||||
|
Introduction
|
||||||
|
You can modify the contact person and contact phone number associated with
|
||||||
|
your account, which is used to facilitate communication with your superiors.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. Click > Account Settings.
|
||||||
|
3. In the Contact/Phone Number field, click Edit.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. When you finish editing, click Save.
|
||||||
|
5.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trusted Devices
|
||||||
|
Procedure
|
||||||
|
1. Log in to YMCS.
|
||||||
|
2. Click System > Account Information.
|
||||||
|
3. In the Trusted Devices section, click View.
|
||||||
|
4. In the pop-up window, you can view all the devices where you have previously checked the "Skip re-
|
||||||
|
verification within 30 days" option during login verification. You can remove devices, and the next time you
|
||||||
|
|
||||||
|
9 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
log in with those devices, you will need to undergo MFA verification again.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
10 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Configure Login Verification (Sub
|
||||||
|
Account)
|
||||||
|
Prerequisite
|
||||||
|
The super administrator has enabled login protection for you. Otherwise, you cannot
|
||||||
|
enable the login protection.
|
||||||
|
Perform Login Verification
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
After the super administrator has enabled the login protection for you, you need
|
||||||
|
to choose the login verification for the initial login. The selected verification
|
||||||
|
method will serve as your subsequent login protection method.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Perform Email Verification
|
||||||
|
If you select email verification during the initial login, it will serve as your
|
||||||
|
subsequent login protection method.
|
||||||
|
Procedure
|
||||||
|
1. Select Email, and enter the dynamic code in the mail you received.
|
||||||
|
|
||||||
|
(Optional) Check the "Skip re-verification within 30 days" option, and you will
|
||||||
|
11 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
not need to undergo verification when logging in to this account using the
|
||||||
|
same device within 30 days.
|
||||||
|
2. Click OK.
|
||||||
|
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Perform Virtual MFA Verification
|
||||||
|
Procedure
|
||||||
|
1. Select Virtual MFA Device.
|
||||||
|
|
||||||
|
2. Download and open Google Authenticator on your phone and click Get
|
||||||
|
|
||||||
|
started.
|
||||||
|
3. Sign in to a Google account.
|
||||||
|
|
||||||
|
|
||||||
|
💡 TIP
|
||||||
|
After you sign in to a Google account, you can back up the verification codes to your Google
|
||||||
|
account for easy retrieval. To avoid situations where verification codes cannot be retrieved
|
||||||
|
after Google Authenticator is uninstalled or data is cleared, it is recommended that you do not
|
||||||
|
skip the login steps.
|
||||||
|
|
||||||
|
|
||||||
|
4. Do one of the following:
|
||||||
|
|
||||||
|
Select Scan a QR code in Google Authenticator and scan the QR code in the
|
||||||
|
pop-up window on the YMCS portal.
|
||||||
|
12 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Select Enter a setup key on Google Authenticator, then enter the YMCS
|
||||||
|
account and the key displayed in the pop-up window, and click Add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
13 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. The dynamic code will be displayed in Google Authenticator.
|
||||||
|
|
||||||
|
6. Enter the dynamic code into the corresponding area of YMCS.
|
||||||
|
|
||||||
|
(Optional) Check the "Skip re-verification within 30 days" option, and you will
|
||||||
|
not need to undergo verification when logging in to this account using the
|
||||||
|
same device within 30 days.
|
||||||
|
7. Click OK.
|
||||||
|
|
||||||
|
8.
|
||||||
|
|
||||||
|
|
||||||
|
14 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Change Login Protection Methods
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. Click > Account Settings.
|
||||||
|
3. In the Login Protection field, click Edit.
|
||||||
|
|
||||||
|
4. Do one of the following to select the desired login protection method:
|
||||||
|
|
||||||
|
Select Email, enter the dynamic code in the mail you received, and click OK.
|
||||||
|
|
||||||
|
i. Select Virtual MFA Device.
|
||||||
|
|
||||||
|
ii. Install and open Google Authenticator on the mobile side, and click Get
|
||||||
|
|
||||||
|
started.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
15 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
iii.
|
||||||
|
|
||||||
|
|
||||||
|
16 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
iii. Sign in to a Google account.
|
||||||
|
|
||||||
|
|
||||||
|
💡 Tip:
|
||||||
|
After you sign in to a Google account, you can back up the verification codes to your Google
|
||||||
|
account for easy retrieval. To avoid situations where verification codes cannot be retrieved
|
||||||
|
after Google Authenticator is uninstalled or data is cleared, it is recommended that you do not
|
||||||
|
skip the login steps.
|
||||||
|
|
||||||
|
|
||||||
|
iv. Do one of the following:
|
||||||
|
|
||||||
|
Select Scan a QR code in Google Authenticator and scan the QR code in
|
||||||
|
the pop-up window on the YMCS portal.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
17 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Select Enter a setup key on Google Authenticator, then enter the YMCS
|
||||||
|
account and the key displayed in the pop-up window, and click Add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
18 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
v. The dynamic code is displayed in Google Authenticator.
|
||||||
|
|
||||||
|
vi. Enter the dynamic code into YMCS and click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
19 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Configure Login Verification (Super
|
||||||
|
Administrator)
|
||||||
|
Introduction
|
||||||
|
For single-factor authentication, the passwords are easily cracked by brute
|
||||||
|
force. To solve that, YMCS supports multi-factor authentication (MFA), requiring
|
||||||
|
users to pass two authentications before they can sign in to YMCS. By enabling
|
||||||
|
login protection, you will need to authenticate through both password and
|
||||||
|
dynamic verification code to gain authorization and sign in to YMCS.
|
||||||
|
Email Verification
|
||||||
|
Enable Email Verification
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. Click > Account Settings.
|
||||||
|
3. In the Login Protection field, click Edit.
|
||||||
|
4. Select Email, enter the dynamic code in the mail you received, and click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5.
|
||||||
|
|
||||||
|
Sign In with Email Verification
|
||||||
|
After enabling it, users will receive a verification email after entering their
|
||||||
|
account and password to log in. Enter the verification code in the corresponding
|
||||||
|
area to complete the login process.
|
||||||
|
Procedure
|
||||||
|
1. After clicking Sign In, enter the verification code received in your email in the corresponding area.
|
||||||
|
20 / 41
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf - Pages 21-40
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf
|
||||||
|
**Pages:** 21-40 of 84
|
||||||
|
**Chunk:** 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
(Optional) Check the "Skip re-verification within 30 days" option, and you will not need to undergo verification
|
||||||
|
when logging in to this account using the same device within 30 days.
|
||||||
|
2. Click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
MFA Verification
|
||||||
|
Enable MFA Verification
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. Click > Account Settings.
|
||||||
|
3. In the Login Protection field, click Edit.
|
||||||
|
4. Select Virtual MFA Device.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5.
|
||||||
|
|
||||||
|
21 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
5. Hover over Check how to get the dynamic code and do the following to get the dynamic code.
|
||||||
|
6. Install and open Google Authenticator on the mobile side, and click Get started.
|
||||||
|
7. Sign in to a Google account.
|
||||||
|
|
||||||
|
💡 TIP
|
||||||
|
After you sign in to a Google account, you can back up the verification codes to your Google
|
||||||
|
account for easy retrieval. To avoid situations where verification codes cannot be retrieved
|
||||||
|
after Google Authenticator is uninstalled or data is cleared, it is recommended that you do not
|
||||||
|
skip the login steps.
|
||||||
|
|
||||||
|
8. Do one of the following:
|
||||||
|
Select Scan a QR code in Google Authenticator and scan the QR code in the pop-up window on the YMCS
|
||||||
|
portal.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
22 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Select Enter a setup key on Google Authenticator, then enter the YMCS account and the key displayed in
|
||||||
|
the pop-up window, and click Add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
23 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9. The dynamic code is displayed in Google Authenticator.
|
||||||
|
10. Enter the dynamic code into YMCS and click OK.
|
||||||
|
11.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
24 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign In with MFA Verification
|
||||||
|
After enabling it, users will be required to enter the dynamic code obtained from
|
||||||
|
Google Authenticator after entering their account and password to complete the
|
||||||
|
login process.
|
||||||
|
Procedure
|
||||||
|
1. After clicking Sign In, enter the dynamic code into the corresponding area of YMCS.
|
||||||
|
(Optional) Check the "Skip re-verification within 30 days" option, and you will not need to undergo verification
|
||||||
|
when logging in to this account using the same device within 30 days.
|
||||||
|
2. Click OK.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
25 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
26 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Channel Users
|
||||||
|
Add Channel Accounts
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
You can add sub-channels, and the sub-channels can add their sub-channels.
|
||||||
|
You can also control whether your sub-channel can add their sub-channels.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Channel Management > Add.
|
||||||
|
3. Configure channel information and click OK.
|
||||||
|
Channel Name: Enter the channel name.
|
||||||
|
Set Administrator:
|
||||||
|
If you choose to set it as an administrator, you need to enter an email address, and the sub-channel can
|
||||||
|
log in using this email address.
|
||||||
|
If you do not, an email address is not required, and you can manage the sub-channels on their behalf.
|
||||||
|
Country/Region: Select the country or region where this channel account is located.
|
||||||
|
Time Zone: Select a time zone.
|
||||||
|
Contact: Enter a contact.
|
||||||
|
Phone Number: Enter a contact phone number.
|
||||||
|
Add Sub-Channel: If you select Enable in the Add Sub-Channel field, the sub-channel can add and
|
||||||
|
manage their sub-channels (add channel, edit channel information, etc.).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
27 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Confirm in the pop-up window to add the channel successfully.
|
||||||
|
|
||||||
|
|
||||||
|
28 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Channels
|
||||||
|
|
||||||
|
View the Channel List
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Channel Management.
|
||||||
|
3. View the channel name, status and other information in the channel list.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign In to YMCS on Behalf of Channel Accounts
|
||||||
|
Procedure
|
||||||
|
1. In the channel list, click Manage on the right side of the channel account.
|
||||||
|
|
||||||
|
💡 Note
|
||||||
|
This feature is unavailable to frozen or unauthorized channels.
|
||||||
|
|
||||||
|
|
||||||
|
Edit Channel Account
|
||||||
|
Procedure
|
||||||
|
1. In the channel list, click > Edit.
|
||||||
|
|
||||||
|
Reset Password for Channel Account
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
29 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
You can reset the password to deal with password leakage and manage
|
||||||
|
personnel changes to enhance security.
|
||||||
|
Procedure
|
||||||
|
1. In the channel list, click > Reset Password.
|
||||||
|
|
||||||
|
Freeze Channel Account
|
||||||
|
Introduction
|
||||||
|
You can efficiently deal with situations such as service expiration by freezing
|
||||||
|
channels. After the channel account is frozen, the channel cannot use the
|
||||||
|
account to log in to YMCS for Channel.
|
||||||
|
Procedure
|
||||||
|
1. In the channel list, click > Freeze.
|
||||||
|
2. Enter the freezing reason in the pop-up window and click OK.
|
||||||
|
|
||||||
|
Unfreeze Channel Account
|
||||||
|
Introduction
|
||||||
|
You can unfreeze the frozen channel. After unfreezing, the channel can log in to
|
||||||
|
YMCS for channel again.
|
||||||
|
Procedure
|
||||||
|
1. In the channel list, click > Unfreeze.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
30 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Enterprise Users
|
||||||
|
Add Enterprise Accounts
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
You can add enterprise accounts and edit the enterprise information. When an
|
||||||
|
exception occurs to the enterprise, you can freeze the enterprise account. From
|
||||||
|
the channel platform, you can log in to YMCS for enterprise or YMCS for RPS
|
||||||
|
enterprise to manage devices for enterprises. For more information on how to
|
||||||
|
manage devices on YMCS for enterprise, see the YMCS for enterprise user guide.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Enterprise Management > Enterprise List > Add.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Configure the information of the enterprise account.
|
||||||
|
Data Center: The data center is divided into EU, US, and AU regions. The data between the regions is
|
||||||
|
isolated, meaning that data belonging to companies under the EU region will be stored in the EU region
|
||||||
|
data center, while data belonging to companies under the US region will be stored in the US region data
|
||||||
|
center. Please select the data center according to the actual location of the enterprise.
|
||||||
|
Enterprise Name: Enter the name of the enterprise.
|
||||||
|
Permission: Assign permissions to the enterprise account. Device Management (RPS): The enterprise
|
||||||
|
account can log in to YMCS to use device management and RPS features. RPS: The enterprise account can
|
||||||
|
log in to YMCS but can only use the RPS feature.
|
||||||
|
|
||||||
|
31 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
Set Administrator:
|
||||||
|
If you select Yes, enter an email address, and this enterprise account can use the email to log in to YMCS.
|
||||||
|
If you select No, an email address is not required, and you can use this channel account to manage
|
||||||
|
devices for the enterprise account.
|
||||||
|
Email: Enter the email address of this enterprise account. The account information will be sent by email.
|
||||||
|
Enterprise Info: Set the following information, including country/region, time zone, contact person,
|
||||||
|
contact number, etc.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
32 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click OK.
|
||||||
|
|
||||||
|
💡 Note:
|
||||||
|
Please confirm the enterprise information and the data center you select. It cannot be
|
||||||
|
modified after saving.
|
||||||
|
33 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
5. Click Confirm in the pop-up window to successfully add the enterprise.
|
||||||
|
|
||||||
|
💡 Note:
|
||||||
|
When the enterprise administrator logs in to YMCS for the first time, the page will prompt
|
||||||
|
whether to allow authorization to the channel, and the enterprise administrator can accept or
|
||||||
|
reject it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Enterprises
|
||||||
|
|
||||||
|
View the Enterprise List
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Enterprise Management > Enterprise List.
|
||||||
|
3. View the enterprise name, the permission, the status and other information in the enterprise list.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign In to YMCS on Behalf of Enterprise Accounts
|
||||||
|
Introduction
|
||||||
|
You can log in to YMCS for Enterprise with enterprise authorization to manage
|
||||||
|
devices or perform other operations on their behalf.
|
||||||
|
Prerequisites
|
||||||
|
34 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
The enterprise has authorized the management to you.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click Manage on the right side of the enterprise account to go to YMCS for Enterprise.
|
||||||
|
|
||||||
|
Edit Enterprise Account Information
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Edit.
|
||||||
|
|
||||||
|
Reset Password for Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can reset the password to deal with password leakage and manage
|
||||||
|
personnel changes to enhance security.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Reset Password.
|
||||||
|
|
||||||
|
💡 Note:
|
||||||
|
Password reset is unavailable to non-mailbox enterprises.
|
||||||
|
|
||||||
|
|
||||||
|
Freeze Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can efficiently deal with situations such as service expiration by freezing
|
||||||
|
businesses. After the enterprise account is frozen, the enterprise cannot use the
|
||||||
|
account to log in to YMCS for Enterprise or RPS Enterprise.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Freeze.
|
||||||
|
2. Enter the freezing reason in the pop-up window and click OK.
|
||||||
|
|
||||||
|
Unfreeze Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can unfreeze the frozen enterprise. After unfreezing, the enterprise can log
|
||||||
|
in to YMCS for Enterprise.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Unfreeze.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
35 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
Manage Enterprise Authorization
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
The enterprises you create can choose whether to authorize the management to
|
||||||
|
you when they log in to YMCS for the first time.
|
||||||
|
If the enterprise initially created by you rejects authorizing the management to
|
||||||
|
you, they also need to use an authorization code to apply for management by
|
||||||
|
you.
|
||||||
|
You can manage enterprises not created by you through an authorized code
|
||||||
|
application, and you can approve or reject the application.
|
||||||
|
|
||||||
|
Channel Authorization Code
|
||||||
|
Introduction
|
||||||
|
You can send the authorization code to the enterprise that needs to be managed
|
||||||
|
and manage it after the enterprise is authorized.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Enterprise Management > Enterprise Authorization.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Click to copy the authorization code and send it to the enterprise to be managed.
|
||||||
|
Click to refresh the authorization code.
|
||||||
|
|
||||||
|
Approve/Reject Authorization Application
|
||||||
|
Introduction
|
||||||
|
36 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
You can view the list of enterprises that have requested you to manage their
|
||||||
|
devices on their behalf. You can approve or reject the applications.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click User Management > Enterprise Management > Enterprise Authorization.
|
||||||
|
3. Do one of the following:
|
||||||
|
Click Agree/Reject on the right side of the enterprise.
|
||||||
|
Select multiple enterprises and click Agree/Reject.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
37 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Devices
|
||||||
|
Manage Room Devices
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Device Management > Room Device.
|
||||||
|
3. Click EU Region/US Region/AU Region in the top-right corner to view the devices belonging to the selected
|
||||||
|
region.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. On the right side of the device, click Manage to go to the room device list of the corresponding enterprise.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
This feature is unavailable to frozen enterprises.
|
||||||
|
|
||||||
|
5. Manage the device. For more information, refer to YMCS for enterprise user guide - manage room devices.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage USB Devices
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Device Management > USB Device.
|
||||||
|
3. Click EU Region/US Region/AU Region in the top-right corner to view the devices belonging to the selected
|
||||||
|
|
||||||
|
38 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
region.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. On the right side of the device, click Manage to go to the USB device list of the corresponding enterprise.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
This feature is unavailable to frozen enterprises.
|
||||||
|
|
||||||
|
5. Manage the device. For more information, refer to YMCS for enterprise user guide - manage USB devices.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage RPS Devices
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Device Management > RPS Device.
|
||||||
|
3. Click EU Region/US Region/AU Region in the top-right corner to view the devices belonging to the selected
|
||||||
|
region.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
39 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. On the right side of the device, click Manage to go to the RPS device list of the corresponding enterprise.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
This feature is unavailable to frozen enterprises.
|
||||||
|
|
||||||
|
5. Perform RPS device management. For more information, refer to YMCS for enterprise user guide - manage RPS
|
||||||
|
devices.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
40 / 41
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf - Pages 41-60
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf
|
||||||
|
**Pages:** 41-60 of 84
|
||||||
|
**Chunk:** 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
View Device List
|
||||||
|
Introduction
|
||||||
|
You can view and manage the devices of authorized enterprises, including
|
||||||
|
phone devices, room devices, USB devices and RPS devices.
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
You can only view and manage the devices of authorized enterprises.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Device Management > Phone Device/Room Device/USB Device/RPS Device.
|
||||||
|
View the device list.
|
||||||
|
|
||||||
|
💡 TIP
|
||||||
|
You can click EU Region/US Region/AU Region in the top-right corner to view the devices
|
||||||
|
belonging to the selected region.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
41 / 41
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Monitor Wall
|
||||||
|
Introduction
|
||||||
|
By viewing the monitor wall, you can have a comprehensive and real-time grasp
|
||||||
|
of various data and conditions of the device of each enterprise without the need
|
||||||
|
for frequent manual refreshes.
|
||||||
|
Prerequisites
|
||||||
|
Please contact and provide the email of your account to the Yealink Support in order to enable this feature.
|
||||||
|
Only super admin can use this feature, normal users can obtain a link from the super admin and enter the link
|
||||||
|
to view the monitor wall.
|
||||||
|
Only support viewing the device status within the enterprises authorized and managed by your channel.
|
||||||
|
|
||||||
|
View the Monitor Wall
|
||||||
|
Procedure
|
||||||
|
1. Log in to YMCS.
|
||||||
|
2. Click Monitor Wall.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
Only the device status within the enterprises authorized and managed by
|
||||||
|
your channel can be displayed.
|
||||||
|
|
||||||
|
1. Hover the mouse over the thumbnail of the monitor wall and click Preview.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
2. View various indicators of the device in real time on the monitor wall.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Numbe Description
|
||||||
|
r
|
||||||
|
1 Current time.
|
||||||
|
2 The total number of enterprises and the number of enterprises
|
||||||
|
authorized to be managed.
|
||||||
|
3 Device statistics. Count the number of various types of device.
|
||||||
|
4 Device status. Count the total number of devices and the number
|
||||||
|
and proportion of online/offline/pending devices.
|
||||||
|
5 Model statistics. Count different types of device and their
|
||||||
|
quantities.
|
||||||
|
6 Displays the number of new alarms today.
|
||||||
|
7 Active alarm records. Count the enterprises with the largest
|
||||||
|
number of active alarms and their detailed information, including:
|
||||||
|
alarm type, company name, device name, model, and last alarm
|
||||||
|
time.
|
||||||
|
8 The number of new alarms in the past 7 days. Statistics of the
|
||||||
|
number and severity of daily alarms in the past 7 days.
|
||||||
|
|
||||||
|
2 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
9 Top 5 Active Alarm Enterprises. Count the enterprises with the
|
||||||
|
largest number of active alarms.
|
||||||
|
10 Top 5 Offline Devices Enterprises. Count the enterprises with the
|
||||||
|
largest number of offline devices.
|
||||||
|
11 Top 5 Active Alarm Types. Count the 5 largest existing alarm types.
|
||||||
|
|
||||||
|
Set the Monitor Wall
|
||||||
|
Procedure
|
||||||
|
1. Log in to YMCS.
|
||||||
|
2. Click Monitor Wall.
|
||||||
|
3. Click > Settings in the lower right corner of the thumbnail of the monitor wall.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Data Refresh Period: Set the time for automatic refresh of the monitor wall. It will be updated with real-
|
||||||
|
time data at that frequency.
|
||||||
|
Access Whitelist: If not enabled, devices under any IP can access the monitor wall through links; if enabled,
|
||||||
|
only IP addresses in the whitelist can access.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Copy the Link of the Monitor Wall
|
||||||
|
You can obtain the link of the monitor wall, which can be accessed by users in
|
||||||
|
the IP whitelist. They can view the specific content of the monitor wall, or
|
||||||
|
display it as material/content on Yealink Digital Signage or Third Party Digital Signage.
|
||||||
|
Procedure
|
||||||
|
1. Log in to YMCS.
|
||||||
|
2. Click Monitor Wall.
|
||||||
|
3. Click > Copy Link in the lower right corner of the thumbnail of the monitor wall.
|
||||||
|
4. Make relevant settings, click Save.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Room Device Calculator
|
||||||
|
Introduction
|
||||||
|
You can use Yealink Room Device Calculator to create room projects and
|
||||||
|
manage them by group.
|
||||||
|
Procedure
|
||||||
|
Create a room project on Room Device Calculator
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Room Device Calculator > Create.
|
||||||
|
3. About how to use the tool, please refer to: Yealink Room Device Calculator.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage room projects by group
|
||||||
|
1. In the Room Device Calculator page, click Save.
|
||||||
|
2. Select a group or create a new group in the pop-up window, click Confirm.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Do one of the following to manage room projects by group:
|
||||||
|
|
||||||
|
In the Room Device Calculator page, click > My Files.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Enter the Room Device Calculator page of YMCS. All the grouping-related operations you perform on the
|
||||||
|
Room Device Calculator will be synchronized here.
|
||||||
|
4. Do one of the following:
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
Deleting the group will delete the files in the group as well.
|
||||||
|
|
||||||
|
7 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
In the Group catalog, click to add new group.
|
||||||
|
|
||||||
|
In the Group catalog, click > Rename on the right side of the group, renane the group.
|
||||||
|
|
||||||
|
In the Group catalog, click > Delete on the right side of the group, delete the group.
|
||||||
|
In the file list, you can perform Edit, Move, Download and other operations.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Login Management
|
||||||
|
Login Whitelist
|
||||||
|
IP Whitelist
|
||||||
|
Introduction
|
||||||
|
Support editing login whitelist, only IP addresses within the whitelist can log in
|
||||||
|
to the enterprise management platform, which further enhance the enterprise
|
||||||
|
security.
|
||||||
|
|
||||||
|
💡 Note: Whether the channel end can access the enterprise management
|
||||||
|
platform is not restricted by the login whitelist.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Login Mangement > Login Whitelist.
|
||||||
|
|
||||||
|
3. Click to add and edit the login whitelist, you can add a maximum of 20 IP addresses.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Save.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Click to enable IP Whitelist.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Password Change Reminder
|
||||||
|
Introduction
|
||||||
|
You can set a password change reminder and the frequency of the reminder. If
|
||||||
|
an administrator's password exceeds the set frequency, they will receive an
|
||||||
|
10 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
email reminder to change their password.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Login Mangement > Login Whitelist.
|
||||||
|
|
||||||
|
3. In the Password Change Reminder section, click to enable it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Edit the reminder frequency and save.
|
||||||
|
|
||||||
|
💡 Note:
|
||||||
|
The maximum setting is 365 days.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
11 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Single Sign-On
|
||||||
|
Step 1 Configure Microsoft Azure AD Login Method
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Log in to the Azure admin portal.
|
||||||
|
|
||||||
|
2. Click > Microsoft Entra ID in the top-left corner.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
12 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Click Enterprise applications in the left navigation bar.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Manage > Enterprise applications > New application.
|
||||||
|
5.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
13 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Click Create your own application. Enter the application name and check Register an application to
|
||||||
|
integrate with Microsoft Entra ID(App you’re deploying) and click Create.
|
||||||
|
6.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. Check Accounts in this organizational directory only (yealinkwmtest only - Single tenant) to support trial
|
||||||
|
for users in the current tenant directory and click Register.
|
||||||
|
|
||||||
|
14 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
7.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
7. Go back to the Microsoft Entra ID page, click Manage > App registrations, find and click the application you
|
||||||
|
just created.
|
||||||
|
8.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8. Go to the application details page. The Application (client) ID is the Client ID and the directory (tenant) is the
|
||||||
|
|
||||||
|
15 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
Tenant ID.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9. Click Manage > Authentication > Add a platform in the left navigation bar and select Web.
|
||||||
|
10.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
10. Enter the login address in the Redirect URIs field, the logout address in the Front-channel logout URL field,
|
||||||
|
|
||||||
|
16 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
and select the check box of ID tokens (used for implicit and hybrid flows) and click Configure. Please
|
||||||
|
enter the following login and logout addresses.
|
||||||
|
Login address: https://reseller.ymcs.yealink.com/reseller/front/auth/signin-oidc
|
||||||
|
Logout address: https://reseller.ymcs.yealink.com/reseller/front/auth/logout
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
11. The configuration is complete. You can perform further SSO configuration in the YMCS platform.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Step 2 Enable and Configure SSO
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
After enabling single sign-on and creating third-party administrator accounts,
|
||||||
|
you can use these accounts to sign in to the YMCS platform with the account and
|
||||||
|
password of the third-party platform (only Microsoft accounts are supported),
|
||||||
|
improving login efficiency and avoiding security risks.
|
||||||
|
Procedure
|
||||||
|
Enable SSO
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Login Management > Single Sign-On.
|
||||||
|
3. Click the toggle beside Single Sign-On to enable it.
|
||||||
|
4. Select an authentication platform (currently, only Microsoft Azure AD is supported).
|
||||||
|
17 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
5. Refer to the Microsoft AD login configuration method and enter the Client ID of Microsoft Azure AD as well as
|
||||||
|
the Tenant ID to configure the authentication platform.
|
||||||
|
6. Click Save.
|
||||||
|
|
||||||
|
Create a Sub Account with SSO Feature
|
||||||
|
Please refer to add sub accounts to create a sub account with SSO feature.
|
||||||
|
Once configured, this sub account can log in to the YMCS platform using SSO.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Step 3 Sign In via SSO
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
You can use your Microsoft account to easily log in to the YMCS platform,
|
||||||
|
effectively improving login efficiency and avoiding some of the security risks.
|
||||||
|
Prerequisites
|
||||||
|
Your channel has configure Microsoft Azure AD login method and enabled SSO.
|
||||||
|
The channel administrator has created a third-party administrator account for you.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. For the first login, click the login address in the email or enter the login address in the browser address bar.
|
||||||
|
|
||||||
|
💡 Tip: You can use the global address (https://ymcs.yealink.com/manager/
|
||||||
|
login) to sign in, and YMCS will automatically detect and redirect you to
|
||||||
|
your region.
|
||||||
|
|
||||||
|
2. Optional: Switch the language in the top-right corner.
|
||||||
|
3. Click SSO.
|
||||||
|
4. Enter the third-party account and click Continue.
|
||||||
|
5. On the Microsoft login authentication page, enter your Microsoft account and password. After finishing the
|
||||||
|
verification, you can log in to YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
18 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Sub Accounts and Their
|
||||||
|
Permission
|
||||||
|
Roles
|
||||||
|
Add Roles
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
The default role and its permission are described below:
|
||||||
|
|
||||||
|
Name Description
|
||||||
|
Super Have all the function permission under the device management,
|
||||||
|
admin RPS management,
|
||||||
|
No Only the permission to log in.
|
||||||
|
permissi
|
||||||
|
on
|
||||||
|
|
||||||
|
In addition to the default roles, you can add other roles and assign the
|
||||||
|
corresponding permission to the role.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts > Role.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
19 / 43
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf - Pages 61-80
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf
|
||||||
|
**Pages:** 61-80 of 84
|
||||||
|
**Chunk:** 4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Do one of the following:
|
||||||
|
Select a group from the group list on the left panel and click Add. You can also click Add Group if you do
|
||||||
|
not want to specify a group.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Click on the right side of the group and click Add Role.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
20 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Edit the role information and click Save.
|
||||||
|
Role Name: Edit the role name.
|
||||||
|
Group: Select a belonging group for this role for future management.
|
||||||
|
Description: Enter a description.
|
||||||
|
Data Permission-Channel: Edit data permissions for this role to only view and manage channel data
|
||||||
|
within the specified scope.
|
||||||
|
Data Permission-Enterprise: Edit data permissions for this role to only view and manage enterprise and
|
||||||
|
related device records within the specified scope.
|
||||||
|
Permission: Assign management permissions for all functions in User Management, Device Management,
|
||||||
|
System Settings to the role.
|
||||||
|
Read Only: Can only browse and cannot perform related operations.
|
||||||
|
Read & Write: Can browse and perform related operations.
|
||||||
|
All Invisible: The functionality module will not be displayed to them.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
21 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Roles
|
||||||
|
|
||||||
|
Go to the Role List
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts > Role.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
22 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
Add Groups
|
||||||
|
Introduction
|
||||||
|
You can add groups and manage the roles by group.
|
||||||
|
Procedure
|
||||||
|
1. Do one of the following:
|
||||||
|
In the role list, click Add Group.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
In the group list, click > Add Group on the right side of All.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
23 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
2. Enter the group name and click Confirm.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. After adding a group, you can find the group from the group list.
|
||||||
|
|
||||||
|
Manage Roles by Group
|
||||||
|
Introduction
|
||||||
|
You can organize and manage administrators within the enterprise by grouping
|
||||||
|
them. Each role can only belong to one group.
|
||||||
|
Procedure
|
||||||
|
1. In the role list, click the desired group.
|
||||||
|
2. Do one of the following:
|
||||||
|
Check the corresponding role information within this group.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
24 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Click on the right side of the group to edit groups, add roles to the group, and delete groups.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
View Sub Accounts Associated with a Role
|
||||||
|
Procedure
|
||||||
|
1. In the role list, click the desired role.
|
||||||
|
2. Click Sub Accounts.
|
||||||
|
3. View the sub accounts associated with this role. Those sub accounts have all the permissions assigned to this
|
||||||
|
role.
|
||||||
|
|
||||||
|
25 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Assign a Role to a Sub Account
|
||||||
|
For more information, please refer to: Assign a Role to a Sub Account.
|
||||||
|
|
||||||
|
Edit Role Permission
|
||||||
|
Procedure
|
||||||
|
1. In the role list, click the desired role.
|
||||||
|
2. Click Permission.
|
||||||
|
3. Edit the role permission and click Save.
|
||||||
|
|
||||||
|
💡 Note
|
||||||
|
You cannot edit the permission of the default role (super admin/no permission).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sub Accounts
|
||||||
|
Add Sub Accounts
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
According to the actual needs, you can add sub-accounts and give them
|
||||||
|
different function and data permission (data permission are divided based on
|
||||||
|
the scope of enterprises/channels that the account will manage), so as to
|
||||||
|
|
||||||
|
26 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
achieve efficient and convenient permission allocation. After that, you can also
|
||||||
|
use the sub account to log in to YMCS for channel.
|
||||||
|
Procedure
|
||||||
|
Add sub-accounts manually
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts > Sub Accounts > Add Sub Account.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Configure the corresponding information.
|
||||||
|
|
||||||
|
💡 Note:You need to configure Single Sign-On (SSO)first to be able to select a third-party
|
||||||
|
account.
|
||||||
|
|
||||||
|
Account Type : Select Ordinary / External.
|
||||||
|
Edit the registration email, contact person, contact phone, description, and select whether to enable login
|
||||||
|
protection for this account.
|
||||||
|
|
||||||
|
💡 Note:If you enable login protection for a sub-account, the sub-account will need to choose
|
||||||
|
between email verification or virtual MFA verification as the login verification method during
|
||||||
|
the first login. For more information, please refer to Login Verification.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
27 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Invite sub-accounts using an invitation link
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts > Sub Accounts > Invite Members.
|
||||||
|
|
||||||
|
|
||||||
|
3. Click to copy the invitation link and send it to the corresponding user.
|
||||||
|
|
||||||
|
💡 TIP
|
||||||
|
|
||||||
|
Click to regenerate the invitation link. The original link will become
|
||||||
|
invalid.
|
||||||
|
You can set an expiration date for the invitation link.
|
||||||
|
You can enable Joining requires moderation . If enabled, you need to
|
||||||
|
click Go to audit to approve/reject/delete the application in the review
|
||||||
|
list. If disabled, users can join directly through the invitation link
|
||||||
|
without requiring approval.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
28 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. In the sub-account list, find the user and click on the Edit button on the right
|
||||||
|
side to configure the relevant information.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Sub Accounts
|
||||||
|
|
||||||
|
Access the Sub Account List
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts > Sub Accounts.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
29 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Edit Sub Account Information
|
||||||
|
1. In the sub account list, click Edit to edit sub account information.
|
||||||
|
|
||||||
|
Enable/Disable Login Protection
|
||||||
|
1. Do one of the following:
|
||||||
|
|
||||||
|
In the sub account list, click and select Enable/Disable Login Protection.
|
||||||
|
In the sub account list, select the desired sub accounts and select Enable/Disable Login Protection.
|
||||||
|
|
||||||
|
Reset Password
|
||||||
|
You can reset the password to deal with password leakage and manage
|
||||||
|
personnel changes to enhance security.
|
||||||
|
1. In the sub account list, click > Reset password and confirm the action.
|
||||||
|
|
||||||
|
Disable/Enable the Sub Account
|
||||||
|
The sub account is enabled by default. You can enable/disable the sub account.
|
||||||
|
When its account status changes, the sub account will receive an email
|
||||||
|
notification.
|
||||||
|
1. Do one of the following:
|
||||||
|
|
||||||
|
In the sub account list, click > Disable. This sub account cannot be used to log in to YMCS.
|
||||||
|
|
||||||
|
In the sub account list, click > Enable. This sub account can use this account to log in to YMCS.
|
||||||
|
|
||||||
|
Delete Sub Accounts
|
||||||
|
1. In the sub account list, click > Delete and confirm the action.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Assign a Role to a Sub Account
|
||||||
|
Introduction
|
||||||
|
The allocation of function permissions is achieved by associating sub-accounts
|
||||||
|
with roles.
|
||||||
|
Procedure
|
||||||
|
1. Log in to YMCS.
|
||||||
|
2. Click System Settings > Sub Accounts.
|
||||||
|
3. Do one of the following:
|
||||||
|
On the Sub Accounts page, add a sub account.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
30 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
On the Role page, add a role.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Perform one of the following operations:
|
||||||
|
On the Sub Accounts page, click Edit on the right side of the sub-administrator and select the
|
||||||
|
corresponding role.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
31 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
On the Role page, click Add on the right side of the role and select the corresponding role.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
32 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Logs
|
||||||
|
Introduction
|
||||||
|
Log management records all operation records on the channel side. You can
|
||||||
|
view the operation information, including modules, types, objects, operators,
|
||||||
|
IPs, results and time.
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
|
||||||
|
2. Click System Settings > Log Management.
|
||||||
|
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. View all operation logs.
|
||||||
|
|
||||||
|
You can view the operation log for a period of time by selecting the time
|
||||||
|
range.
|
||||||
|
You can enter the operator/IP in the search box to match the corresponding
|
||||||
|
operation log.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
33 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
34 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Alarms
|
||||||
|
Introduction
|
||||||
|
When a problem occurs to the device, for example, the call failure or firmware
|
||||||
|
update failure will be reported to YMCS. You can quickly locate the problem by
|
||||||
|
viewing the alarm details and diagnosing the devices.
|
||||||
|
Prerequisites
|
||||||
|
You can only manage authorized enterprises. For detailed information, please
|
||||||
|
refer to Manage Enterprise Authorization.
|
||||||
|
|
||||||
|
Go to Alarm List
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Alarm.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Add an Alarm
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click System Settings > Alarm > Add.
|
||||||
|
3. Set the alarm configuration.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
35 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Save.
|
||||||
|
|
||||||
|
Go to the Enterprise Corresponding to the Alarm
|
||||||
|
1. Click Manage to quickly enter the enterprise corresponding to the alarm.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Edit an Alarm
|
||||||
|
1. Click > Edit.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Delete an Alarm
|
||||||
|
1. Click > Delete.
|
||||||
|
36 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
37 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Un-migrated Enterprise
|
||||||
|
Accounts
|
||||||
|
Introduction
|
||||||
|
For enterprises you previously created, they can still use the YMCS-Legacy
|
||||||
|
Edition (Legacy YMCS), and you can manage these enterprises on the new YMCS.
|
||||||
|
At the same time, we recommend that you assist your enterprise acccounts in
|
||||||
|
migrating to the new platform for easier management in the future.
|
||||||
|
|
||||||
|
💡 Note:Editing the information of un-migrated enterprise accounts is not
|
||||||
|
available.
|
||||||
|
|
||||||
|
View the Un-migrated Enterprise List
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Enterprise Management under the YMCS-Legacy Edition module.
|
||||||
|
3.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Sign In to YMCS on Behalf of Enterprise Accounts
|
||||||
|
Introduction
|
||||||
|
38 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
You can log in to YMCS for Enterprise with enterprise authorization to manage
|
||||||
|
devices or perform other operations on their behalf.
|
||||||
|
Prerequisites
|
||||||
|
The enterprise has authorized the management to you.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click Manage enterprise on the right side of the enterprise account to go to YMCS for
|
||||||
|
Enterprise.
|
||||||
|
|
||||||
|
Reset Password for Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can reset the password to deal with password leakage and manage
|
||||||
|
personnel changes to enhance security.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Reset Password.
|
||||||
|
|
||||||
|
💡 Note:Password reset is unavailable to non-mailbox enterprises.
|
||||||
|
|
||||||
|
|
||||||
|
Freeze Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can efficiently deal with situations such as service expiration by freezing
|
||||||
|
businesses. After the enterprise account is frozen, the enterprise cannot use the
|
||||||
|
account to log in to YMCS for Enterprise or RPS Enterprise.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Freeze.
|
||||||
|
2. Enter the freezing reason in the pop-up window and click OK.
|
||||||
|
|
||||||
|
Unfreeze Enterprise Account
|
||||||
|
Introduction
|
||||||
|
You can unfreeze the frozen enterprise. After unfreezing, the enterprise can log
|
||||||
|
in to YMCS for Enterprise.
|
||||||
|
Procedure
|
||||||
|
1. In the enterprise list, click > Unfreeze.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
39 / 43
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf - Pages 81-84
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_Channel User Guide.pdf
|
||||||
|
**Pages:** 81-84 of 84
|
||||||
|
**Chunk:** 5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Manage Un-migrated Devices
|
||||||
|
Introduction
|
||||||
|
For enterprises you previously created, they can still use the YMCS-Legacy
|
||||||
|
Edition (Classic YMCS), and you can manage these enterprises on the new YMCS.
|
||||||
|
At the same time, we recommend that you assist your enterprise accounts in
|
||||||
|
migrating to the new platform for easier management in the future.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Device Management > Phone Device/USB Device/Room Device/Workspace Device/RPS Device.
|
||||||
|
3. On the right side of the device, click Manage enterprise to go to the device list of the corresponding
|
||||||
|
enterprise.
|
||||||
|
4.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
40 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
View Term of Services and Privacy
|
||||||
|
Policy
|
||||||
|
View the Latest Version of the Terms of Services and
|
||||||
|
Privacy Policy
|
||||||
|
Procedure
|
||||||
|
1. Do one of the following:
|
||||||
|
For the initial login, you can check the Terms of Services and Privacy Policy after you enter your account
|
||||||
|
credentials and click Sign In.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
After you sign in to YMCS, you can also click > Term of Services/Privacy Policy to view the
|
||||||
|
Term of Services and Privacy Policy.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
41 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
View the History Version
|
||||||
|
Introduction
|
||||||
|
The Terms of Service and Privacy Policy will be periodically refined and
|
||||||
|
updated. Please read them carefully and confirm whether you wish to continue
|
||||||
|
using the service.
|
||||||
|
Procedure
|
||||||
|
|
||||||
|
1. After you sign in to YMCS, click > Term of Services/Privacy Policy.
|
||||||
|
2. Click History to view the history versions of the Terms of Services and Privacy Policy.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
42 / 43
|
||||||
|
Yealink Management Cloud Service(YMCS) Channel User
|
||||||
|
Guide
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
43 / 43
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_User Guide.pdf - Pages 1-20
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_User Guide.pdf
|
||||||
|
**Pages:** 1-20 of 953
|
||||||
|
**Chunk:** 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Switch Between Device
|
||||||
|
Management Platform
|
||||||
|
Workspace Management
|
||||||
|
Platform RPS Service
|
||||||
|
Introduction
|
||||||
|
Device Management (DM) Platform: Designed specifically for device administrators, it
|
||||||
|
provides a powerful and user-friendly solution for batch management and operation. It
|
||||||
|
includes features such as monitoring, upgrading, configuring, alerting, and diagnostics.
|
||||||
|
Workspace Management (WM) Platform: It offers a management solution for enterprise
|
||||||
|
hybrid workspaces. In addition to the existing features, WM will gradually introduce more
|
||||||
|
functionalities related to enterprise spaces.
|
||||||
|
RPS Service: Simplifying the deployment and configuration process of devices, it provides a
|
||||||
|
more efficient, convenient, and reliable solution for device deployment and configuration.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. You can switch between the DM Platform / the WM Platform / RPS Service in the bottom left
|
||||||
|
corner.
|
||||||
|
|
||||||
|
💡 TIP
|
||||||
|
You can also set the DM Platform / the WM Platform / RPS Service as
|
||||||
|
default login platforms in Account Settingsaccording to your needs.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Connect Phone Devices
|
||||||
|
Via YMCS RPS Service (Batch Deployment)
|
||||||
|
Procedure
|
||||||
|
1. Import Devices to the Device List
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Phone Device > Device > Device List > Import.
|
||||||
|
3. Select Yes or No to turn on or turn off Import Server.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
If you select Yes to turn on this feature, the devices will also be imported to the RPS server
|
||||||
|
you select, and you can see them on RPS Device list.
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Download Template. Edit the device information according to the guidance in the
|
||||||
|
template.
|
||||||
|
5. Save the file and upload it.
|
||||||
|
6. Click Upload.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. Initialize the Device
|
||||||
|
For brand-new devices, power up the device to complete initialization.
|
||||||
|
For devices you have used, restore the device to factory settings to complete initialization.
|
||||||
|
3. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
|
||||||
|
Via Auto Provisioning Server (Batch Deployment)
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
|
||||||
|
4 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
If your device's firmware version is too low, do any of the following to upgrade it
|
||||||
|
to the latest version:
|
||||||
|
Visit Yealink Support to get the latest device firmware version.
|
||||||
|
Execute the following command to upgrade the device firmware.
|
||||||
|
static.firmware.url=http://192.168.1.20/150.86.0.5.rom
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
Please change the above http://192.168.1.20/150.86.0.5.rom to the address of
|
||||||
|
your auto-provisioning server.
|
||||||
|
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Configure the Device CFG File
|
||||||
|
1. Perform the following configuration.
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=eu-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the EU region, configure the following
|
||||||
|
commands:
|
||||||
|
5 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=us-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the US region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=us-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the AU region, configure the following
|
||||||
|
commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=au-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the UAE region, configure the following
|
||||||
|
commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=uae-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
|
||||||
|
6 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can find this address in the DM Server address field by going to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information on YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static.dm.enterprise_id=Enterprise ID
|
||||||
|
static.dm.site_id=site ID
|
||||||
|
|
||||||
|
2. If you want to connect the device directly to a specific site in your enterprise, add the
|
||||||
|
following command to the CFG file:
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
|
||||||
|
7 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the enterprise ID.
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Settings > Device > Site to get the site ID from the site information.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. The CFG file is configured.
|
||||||
|
2. Make Devices Access the CFG File
|
||||||
|
Make the devices access the CFG files stored in your auto-provisioning servers.
|
||||||
|
The operation of different auto-provisioning servers might vary, so we do not
|
||||||
|
elaborate on the process here.
|
||||||
|
3. Trigger the Device to Perform Auto Provisioning
|
||||||
|
There are various ways to trigger the device to complete the auto-provisioning,
|
||||||
|
e.g., turn the device on. Please select your desired method.
|
||||||
|
4. Import Devices to the Device List
|
||||||
|
|
||||||
|
9 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
If you have set the enterprise ID and site ID in the Configure Device CFG
|
||||||
|
file operation, the device will be automatically added to the device list in
|
||||||
|
YMCS; if not, see Import devices.
|
||||||
|
|
||||||
|
5. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
|
||||||
|
Via DHCP Option (Batch Deployment)
|
||||||
|
Introduction
|
||||||
|
You can configure devices to obtain the provisioning server address via DHCP.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Set Up the DHCP Server
|
||||||
|
If you do not have a DHCP server, you can see this section to set up one.
|
||||||
|
Following, we use DHCP Server for Windows to illustrate how to set up a DHCP
|
||||||
|
server on the Windows operating system.
|
||||||
|
1. Download DHCP Server.
|
||||||
|
2. Run dhcpsrv.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
10 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Select your network interface card and click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Configure your IP pool, click DHCP Options, and click Add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
11 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. Do one of the following:
|
||||||
|
Enter the DHCP option and Yealink auto-provisioning server addresses, and click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Param Introduction
|
||||||
|
eter
|
||||||
|
|
||||||
|
Option Enter the option number.
|
||||||
|
numbe
|
||||||
|
r
|
||||||
|
|
||||||
|
Option Set the DHCP option value (the address of the auto-provisioning
|
||||||
|
string server) and enclose it with quotation marks.
|
||||||
|
You can use Yealink auto-provisioning server addresses. Following
|
||||||
|
are Yealink auto-provisioning addresses:
|
||||||
|
If your enterprise is in the EU region, enter https://eu-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
12 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
If your enterprise is in the US region, enter https://us-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
If your enterprise is in the AU region, enter https://au-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
If your enterprise is in the UAE region, enter https://uae-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the Yealink auto-provisioning
|
||||||
|
address.
|
||||||
|
|
||||||
|
Enter the DHCP option and your own auto-provisioning server addresses, and
|
||||||
|
click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Param Introduction
|
||||||
|
eter
|
||||||
|
|
||||||
|
Option Enter the option number.
|
||||||
|
numbe
|
||||||
|
r
|
||||||
|
|
||||||
|
13 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
Option Set the DHCP option value (the address of the auto-provisioning
|
||||||
|
string server) and enclose it with quotation marks.
|
||||||
|
You can use your own auto-provisioning server addresses.
|
||||||
|
|
||||||
|
Please follow the steps below to configure the Device CFG File:
|
||||||
|
7. Perform the following configuration.
|
||||||
|
If your enterprise is located in the EU region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=eu-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the US region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=us-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the AU region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=au-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the UAE region, configure the following commands:
|
||||||
|
|
||||||
|
|
||||||
|
14 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=uae-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can find this address in the DM Server address field by going to
|
||||||
|
System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information on YMCS.
|
||||||
|
|
||||||
|
15 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
If you want to connect the device directly to a specific site in your enterprise, add the
|
||||||
|
following command to the CFG file:
|
||||||
|
|
||||||
|
static.dm.enterprise_id=Enterprise ID
|
||||||
|
static.dm.site_id=site ID
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the enterprise ID.
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
16 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Settings > Device > Site to get the site ID from the site information.
|
||||||
|
|
||||||
|
i. The CFG file is configured.
|
||||||
|
8. Click OK > Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
17 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9. Select Overwrite existing file, click Write INI file, and click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
10. Click Start to run the server and click Finish to exit.
|
||||||
|
2. Make Devices Obtain Server Address through DHCP
|
||||||
|
Yealink IP phones can obtain the provisioning server address by detecting DHCP
|
||||||
|
options during startup.
|
||||||
|
|
||||||
|
18 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
If you do not use the following default option value, you need to configure the
|
||||||
|
phone to obtain the provisioning server address via a custom DHCP option.
|
||||||
|
|
||||||
|
Netw Default Introduction
|
||||||
|
ork option
|
||||||
|
type value
|
||||||
|
|
||||||
|
IPv4 Option 66, Option 66 is used to identify the TFTP server.
|
||||||
|
Option 43 Option 43 is a vendor-specific option, which is used to
|
||||||
|
transfer the vendor-specific information.
|
||||||
|
|
||||||
|
IPv6 Option 59 Option 59 is used to specify a URL for the boot file to be
|
||||||
|
downloaded by the client.
|
||||||
|
|
||||||
|
If you use the default option value:
|
||||||
|
1. Reconnect the network cable of your devices or restart the devices to make them obtain
|
||||||
|
the server address.
|
||||||
|
|
||||||
|
If you use a custom DHCP option:
|
||||||
|
2. Enter https://phone IP address (for example, https://1.2.3.4) in the browser address bar and
|
||||||
|
log in to the phone web user interface as an administrator.
|
||||||
|
3. Click Settings > Auto Provision.
|
||||||
|
4. Select the On radio box in the DHCP Active field.
|
||||||
|
5. Do one of the following:
|
||||||
|
If you are using IPv4 network, enter the desired value in the IPv4 Custom Option field.
|
||||||
|
If you are using IPv6 network, enter the desired value in the IPv6 Custom Option field.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
The IPv4 or IPv6 custom DHCP option must be in accordance with the one defined in the
|
||||||
|
DHCP server.
|
||||||
|
|
||||||
|
|
||||||
|
6. Click Confirm to accept the change.
|
||||||
|
7. Reconnect the network cable of your devices or restart the devices to make them obtain
|
||||||
|
the server address.
|
||||||
|
3. Add Devices to the Device List
|
||||||
|
💡 NOTE
|
||||||
|
|
||||||
|
|
||||||
|
19 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
If you have set the enterprise ID and site ID in Step 1, the device will be
|
||||||
|
automatically added to the device list in YMCS; if not, see import devices.
|
||||||
|
|
||||||
|
4. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
Via the Device Web User Interface (for a Single
|
||||||
|
Device)
|
||||||
|
Procedure
|
||||||
|
1. Configure Auto-Provisioning Server Address
|
||||||
|
1. (Take T41U as an example) Enter https://phone IP address (for example, https://1.2.3.4) in
|
||||||
|
the browser address bar and log in to the phone web user interface as an administrator.
|
||||||
|
2. Click Settings > Auto Provision.
|
||||||
|
3. Enter the URL of your auto-provisioning server in the Server URL field.
|
||||||
|
If your enterprise is in the EU region, enter https://eu-resource.ymcs.yealink.com/
|
||||||
|
hardware/autop/$MAC.boot
|
||||||
|
If your enterprise is in the US region, enter https://us-resource.ymcs.yealink.com/
|
||||||
|
hardware/autop/$MAC.boot
|
||||||
|
If your enterprise is in the AU region, enter https://au-resource.ymcs.yealink.com/
|
||||||
|
hardware/autop/$MAC.boot
|
||||||
|
If your enterprise is in the UAE region, enter https://uae-resource.ymcs.yealink.com/
|
||||||
|
hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
20 / 82
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
# Yealink Management Cloud Service(YMCS)_User Guide.pdf - Pages 21-40
|
||||||
|
|
||||||
|
**Source:** Yealink Management Cloud Service(YMCS)_User Guide.pdf
|
||||||
|
**Pages:** 21-40 of 953
|
||||||
|
**Chunk:** 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the AutoP address.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
21 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Click Auto Provision Now, and reconfirm in the pop-up window.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. Add Devices to the Device List on YMCS
|
||||||
|
1. Sign in to YMCS.
|
||||||
|
2. Click Phone Device > Device > Device List > Add
|
||||||
|
3. Edit device-related information.
|
||||||
|
|
||||||
|
Paramet Introduction
|
||||||
|
er
|
||||||
|
|
||||||
|
Device Enter the device name.
|
||||||
|
Name
|
||||||
|
|
||||||
|
22 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
MAC Enter the MAC address of the device. MAC must and can only
|
||||||
|
contain 12 letters or numbers; the length is limited to 12-17
|
||||||
|
characters; the characters that can be entered include 0-9, A-z,
|
||||||
|
"-", ":"; for example, 00-15-65-1a-2b-3c, 00:15:65:1a:2b:3c,
|
||||||
|
0015651a2B3c.
|
||||||
|
|
||||||
|
Machine Enter the Machine ID of the device. The Machine ID is the SN
|
||||||
|
ID corresponding to the body sticker or the Machine ID displayed in
|
||||||
|
the device status information.
|
||||||
|
|
||||||
|
Model Select the device model.
|
||||||
|
|
||||||
|
Site Select a site for the device.
|
||||||
|
|
||||||
|
Bind Bind an account for the device.
|
||||||
|
Account
|
||||||
|
|
||||||
|
Descript Enter a description.
|
||||||
|
ion
|
||||||
|
|
||||||
|
4. Click Save.
|
||||||
|
3. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
23 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Connect Android-based Video
|
||||||
|
Conferencing Endpoints & Smart
|
||||||
|
Workspace Devices
|
||||||
|
💡 TIP
|
||||||
|
This connecting method is applicable to models: MeetingBar AX0/
|
||||||
|
MeetingBoard/DeskVision A24/RoomCast/RoomPanel/RoomPanel Plus/
|
||||||
|
CTP18/CTP25.
|
||||||
|
For CTP18/CTP25, independent connecting through this method is only
|
||||||
|
supported when paired with a host device. About pairing with a host
|
||||||
|
device, please refer to Pair with Devices.
|
||||||
|
|
||||||
|
Via Auto Provisioning Server (Batch Deployment)
|
||||||
|
Via DHCP Option (Batch Deployment)
|
||||||
|
Via the Device Web User Interface (for a Single Device)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Via Auto Provisioning Server (Batch
|
||||||
|
Deployment)
|
||||||
|
Introduction
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
24 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
Prerequisites
|
||||||
|
If your device's firmware version is too low, do any of the following to upgrade it
|
||||||
|
to the latest version:
|
||||||
|
Visit Yealink Support to get the latest device firmware version.
|
||||||
|
Execute the following command to upgrade the device firmware
|
||||||
|
|
||||||
|
static.firmware.url=http://192.168.1.20/150.86.0.5.rom
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
Please change the above http://192.168.1.20/150.86.0.5.rom to the address of
|
||||||
|
your auto-provisioning server.
|
||||||
|
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Configure the Device CFG File
|
||||||
|
1. Perform the following configuration.
|
||||||
|
If your enterprise is located in the EU region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=eu-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the US region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=us-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the AU region, configure the following commands:
|
||||||
|
|
||||||
|
25 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=au-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the UAE region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=uae-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can find this address in the DM Server address field by going to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
26 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information on YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. If you want to connect the device directly to a specific site in your enterprise, add the
|
||||||
|
following command to the CFG file:
|
||||||
|
|
||||||
|
static.dm.enterprise_id=Enterprise ID
|
||||||
|
static.dm.site_id=site ID
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
27 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the enterprise ID.
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
28 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Settings > Device > Site to get the site ID from the site information.
|
||||||
|
|
||||||
|
3. The CFG file is configured.
|
||||||
|
2. Make Devices Access the CFG File
|
||||||
|
Make the devices access the CFG files stored in your auto-provisioning servers.
|
||||||
|
The operation of different auto-provisioning servers might vary, so we do not
|
||||||
|
elaborate on the process here.
|
||||||
|
3. Trigger the Device to Perform Auto Provisioning
|
||||||
|
There are various ways to trigger the device to complete the auto-provisioning,
|
||||||
|
e.g., turn the device on. Please select your desired method.
|
||||||
|
4. Import Devices to the Device List
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
29 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
If you have set the enterprise ID and site ID in the Configure Device CFG
|
||||||
|
file operation, the device will be automatically added to the device list in
|
||||||
|
YMCS; if not, see Import Devices.
|
||||||
|
|
||||||
|
5. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Via DHCP Option (Batch Deployment)
|
||||||
|
Introduction
|
||||||
|
You can configure devices to obtain the provisioning server address via DHCP.
|
||||||
|
|
||||||
|
Procedure
|
||||||
|
1. Set Up the DHCP Server
|
||||||
|
If you do not have a DHCP server, you can see this section to set up one.
|
||||||
|
Following, we use DHCP Server for Windows to illustrate how to set up a DHCP
|
||||||
|
server on the Windows operating system.
|
||||||
|
1. Download DHCP Server.
|
||||||
|
2. Run dhcpsrv.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
30 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4. Select your network interface card and click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Configure your IP pool, click DHCP Options, and click Add.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
31 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. Do one of the following:
|
||||||
|
Enter the DHCP option and Yealink auto-provisioning server addresses, and click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Paramet Introduction
|
||||||
|
er
|
||||||
|
|
||||||
|
Option Enter the option number.
|
||||||
|
number
|
||||||
|
|
||||||
|
Option Set the DHCP option value (the address of the auto-provisioning
|
||||||
|
string server) and enclose it with quotation marks.
|
||||||
|
You can use Yealink auto-provisioning server addresses.
|
||||||
|
Following are Yealink auto-provisioning addresses:
|
||||||
|
If your enterprise is in the EU region, enter https://eu-
|
||||||
|
|
||||||
|
|
||||||
|
32 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
If your enterprise is in the US region, enter https://us-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
If your enterprise is in the AU region, enter https://au-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
If your enterprise is in the UAE region, enter https://uae-
|
||||||
|
resource.ymcs.yealink.com/hardware/autop/$MAC.boot
|
||||||
|
|
||||||
|
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the Yealink auto-provisioning
|
||||||
|
address.
|
||||||
|
|
||||||
|
Enter the DHCP option and your own auto-provisioning server addresses, and click OK.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Param Introduction
|
||||||
|
eter
|
||||||
|
|
||||||
|
Option Enter the option number.
|
||||||
|
numbe
|
||||||
|
r
|
||||||
|
|
||||||
|
33 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
Option Set the DHCP option value (the address of the auto-provisioning
|
||||||
|
string server) and enclose it with quotation marks.
|
||||||
|
You can use your own auto-provisioning server addresses.
|
||||||
|
|
||||||
|
Please follow the steps below to configure the Device CFG File:
|
||||||
|
1. Perform the following configuration.
|
||||||
|
If your enterprise is located in the EU region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=eu-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the US region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=us-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the AU region, configure the following commands:
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=au-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
If your enterprise is located in the UAE region, configure the following commands:
|
||||||
|
|
||||||
|
|
||||||
|
34 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
#!version:1.0.0.1
|
||||||
|
##The header above must appear as-is in the first line
|
||||||
|
dm.enable=1
|
||||||
|
dm.server.address=uae-device-scheduler.ymcs.yealink.com
|
||||||
|
dm.server.port=443
|
||||||
|
dm.file_upload.http_post_mode=0
|
||||||
|
dm.file_upload.http_method=1
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can find this address in the DM Server address field by going to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information on YMCS.
|
||||||
|
|
||||||
|
|
||||||
|
2. If you want to connect the device directly to a specific site in your enterprise, add the
|
||||||
|
|
||||||
|
|
||||||
|
35 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
following command to the CFG file:
|
||||||
|
|
||||||
|
static.dm.enterprise_id=Enterprise ID
|
||||||
|
static.dm.site_id=site ID
|
||||||
|
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Enterprise Information to get the enterprise ID.
|
||||||
|
You can go to System (
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
36 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) > Settings > Device > Site to get the site ID from the site information.
|
||||||
|
|
||||||
|
3. The CFG file is configured.
|
||||||
|
4. Click OK > Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
37 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Select Overwrite existing file, click Write INI file, and click Next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6. Click Start to run the server and click Finish to exit.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
38 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. Make Devices Obtain Server Address through DHCP
|
||||||
|
Yealink IP phones can obtain the provisioning server address by detecting DHCP
|
||||||
|
options during startup.
|
||||||
|
If you do not use the following default option value, you need to configure the
|
||||||
|
phone to obtain the provisioning server address via a custom DHCP option.
|
||||||
|
|
||||||
|
Netw Default Introduction
|
||||||
|
ork option
|
||||||
|
type value
|
||||||
|
|
||||||
|
IPv4 Option 66, Option 66 is used to identify the TFTP server.
|
||||||
|
Option 43 Option 43 is a vendor-specific option, which is used to
|
||||||
|
transfer the vendor-specific information.
|
||||||
|
|
||||||
|
IPv6 Option 59 Option 59 is used to specify a URL for the boot file to be
|
||||||
|
downloaded by the client.
|
||||||
|
|
||||||
|
39 / 82
|
||||||
|
Yealink Management Cloud Service(YMCS) U...
|
||||||
|
|
||||||
|
If you use the default option value:
|
||||||
|
1. Reconnect the network cable of your devices or restart the devices to make them obtain
|
||||||
|
the server address.
|
||||||
|
|
||||||
|
If you use a custom DHCP option:
|
||||||
|
1. Enter https://phone IP address (for example, https://1.2.3.4) in the browser address bar and
|
||||||
|
log in to the phone web user interface as an administrator.
|
||||||
|
2. Click System > Auto Provision.
|
||||||
|
3. Enable DHCP Active.
|
||||||
|
4. Do one of the following:
|
||||||
|
If you are using IPv4 network, enter the desired value in the IPv4 Custom Option field.
|
||||||
|
If you are using IPv6 network, enter the desired value in the IPv6 Custom Option field.
|
||||||
|
|
||||||
|
💡 NOTE
|
||||||
|
The IPv4 or IPv6 custom DHCP option must be in accordance with the one defined in the
|
||||||
|
DHCP server.
|
||||||
|
|
||||||
|
|
||||||
|
5. Click Confirm to accept the change.
|
||||||
|
6. Reconnect the network cable of your devices or restart the devices to make them obtain
|
||||||
|
the server address.
|
||||||
|
3. Add Devices to the Device List
|
||||||
|
💡 NOTE
|
||||||
|
If you have set the enterprise ID and site ID in Step 1, the device will be
|
||||||
|
automatically added to the device list in YMCS; if not, see Import Devices.
|
||||||
|
|
||||||
|
4. Devices Are Successfully Connected to YMCS
|
||||||
|
After the device successfully connects to YMCS, the device status changes from
|
||||||
|
"offline" to "online" in the phone device list; the device status changes from
|
||||||
|
"unbound" to "bound" in the RPS device list, which means that the device is
|
||||||
|
successfully connected through the RPS server.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Via the Device Web User Interface (for a
|
||||||
|
Single Device)
|
||||||
|
|
||||||
|
40 / 82
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user