Update pve RustDesk GA sync scripts
This commit is contained in:
@@ -1,30 +1,27 @@
|
|||||||
# Temporary public Gitea repo for curling pve install scripts (delete when fleet rollout is done).
|
# Temporary public Gitea repo — pve curl install for Blair RustDesk GA sync.
|
||||||
# Raw URLs:
|
# Delete or make private after fleet rollout.
|
||||||
# https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh
|
|
||||||
# https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/blair-sync-rustdesk-via-ga.sh
|
|
||||||
|
|
||||||
## pve install (one command)
|
## Architecture (canonical)
|
||||||
|
|
||||||
|
- **pve only:** `/usr/local/sbin/blair-sync-rustdesk-via-ga.sh` every **2 minutes**
|
||||||
|
- **Per seat:** QEMU guest agent only (no scheduled reporter task, no share JSON)
|
||||||
|
- Reads `RustDesk2.toml` via GA file-read; PowerShell exec fallback
|
||||||
|
|
||||||
|
## pve install / upgrade
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh | bash
|
curl -fsSL https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Prerequisite: VM 230 running, `qm guest cmd 230 ping` OK.
|
|
||||||
|
|
||||||
## Manual test
|
## Manual test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run
|
BLAIR_VERBOSE=1 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run
|
||||||
BLAIR_VERBOSE=1 BLAIR_VMIDS=230 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh --dry-run
|
|
||||||
BLAIR_VMIDS=230 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh
|
BLAIR_VMIDS=230 /usr/local/sbin/blair-sync-rustdesk-via-ga.sh
|
||||||
|
tail -20 /var/log/blair-rustdesk-sync.log
|
||||||
```
|
```
|
||||||
|
|
||||||
Re-install after updates:
|
## Deprecates
|
||||||
|
|
||||||
```bash
|
- `sync-rustdesk-proxmox-notes.sh` (share report reader)
|
||||||
curl -fsSL https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main/install.sh | bash
|
- Per-VM `Blair\Register-RustDeskId` scheduled task (optional to remove)
|
||||||
```
|
|
||||||
|
|
||||||
## Delete this repo when done
|
|
||||||
|
|
||||||
Make private or delete from Gitea after fleet rollout.
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Poll RustDesk IDs via QEMU guest agent; update Proxmox VM notes + registry.json.
|
# Poll RustDesk IDs via QEMU guest agent; update Proxmox VM notes + registry.json.
|
||||||
# Install to /usr/local/sbin on pve. Cron every 5m. No guest reporter or share reports needed.
|
# Canonical Blair fleet sync — no per-VM scheduled tasks or share reports required.
|
||||||
# Usage: blair-sync-rustdesk-via-ga.sh [--dry-run]
|
# Install: /usr/local/sbin/blair-sync-rustdesk-via-ga.sh Cron: every 2 minutes.
|
||||||
|
# Usage: blair-sync-rustdesk-via-ga.sh [--dry-run] BLAIR_VERBOSE=1 for detail.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
export BLAIR_SHARE="${BLAIR_SHARE:-/tank/blair-share}"
|
export BLAIR_SHARE="${BLAIR_SHARE:-/tank/blair-share}"
|
||||||
@@ -29,23 +30,24 @@ DRY_RUN = os.environ.get('BLAIR_DRY_RUN', '') in ('1', 'true', 'yes')
|
|||||||
VERBOSE = os.environ.get('BLAIR_VERBOSE', '') in ('1', 'true', 'yes')
|
VERBOSE = os.environ.get('BLAIR_VERBOSE', '') in ('1', 'true', 'yes')
|
||||||
VMIDS_ENV = os.environ.get('BLAIR_VMIDS', '').strip()
|
VMIDS_ENV = os.environ.get('BLAIR_VMIDS', '').strip()
|
||||||
|
|
||||||
|
# RustDesk2.toml (LocalService) is the source of truth on Windows seats.
|
||||||
GUEST_READ_PATHS = [
|
GUEST_READ_PATHS = [
|
||||||
r'C:\ProgramData\Blair\rustdesk-report.json',
|
|
||||||
r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
||||||
r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml',
|
r'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml',
|
||||||
r'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
r'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
||||||
r'C:\ProgramData\RustDesk\config\RustDesk2.toml',
|
r'C:\ProgramData\RustDesk\config\RustDesk2.toml',
|
||||||
|
r'C:\ProgramData\Blair\rustdesk-report.json',
|
||||||
]
|
]
|
||||||
|
|
||||||
PS_READ_ID = r'''
|
PS_READ_ID = r'''
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
$ErrorActionPreference = 'SilentlyContinue'
|
$ErrorActionPreference = 'SilentlyContinue'
|
||||||
$paths = @(
|
$paths = @(
|
||||||
'C:\ProgramData\Blair\rustdesk-report.json',
|
|
||||||
'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
||||||
'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml',
|
'C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\RustDesk\config\RustDesk.toml',
|
||||||
'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
'C:\Windows\System32\config\systemprofile\AppData\Roaming\RustDesk\config\RustDesk2.toml',
|
||||||
'C:\ProgramData\RustDesk\config\RustDesk2.toml'
|
'C:\ProgramData\RustDesk\config\RustDesk2.toml',
|
||||||
|
'C:\ProgramData\Blair\rustdesk-report.json'
|
||||||
)
|
)
|
||||||
foreach ($p in $paths) {
|
foreach ($p in $paths) {
|
||||||
if (-not (Test-Path -LiteralPath $p)) { continue }
|
if (-not (Test-Path -LiteralPath $p)) { continue }
|
||||||
@@ -74,11 +76,13 @@ exit 1
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str) -> None:
|
def log(msg: str, *, force: bool = False) -> None:
|
||||||
|
if VERBOSE or force:
|
||||||
print(msg, flush=True)
|
print(msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
def update_desc(old: str, new_id: str) -> str:
|
def update_desc(old: str, new_id: str) -> str:
|
||||||
|
old = old.replace('Registry%3A S%3A', 'Registry%3A Z%3A').replace('Registry: S:', 'Registry: Z:')
|
||||||
if re.search(r'RustDesk\s+\d+', old):
|
if re.search(r'RustDesk\s+\d+', old):
|
||||||
return re.sub(r'RustDesk\s+\d+', f'RustDesk {new_id}', old)
|
return re.sub(r'RustDesk\s+\d+', f'RustDesk {new_id}', old)
|
||||||
if not old:
|
if not old:
|
||||||
@@ -141,17 +145,10 @@ def decode_ga_payload(raw) -> str:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def decode_exec_payload(st: dict) -> tuple[str, str]:
|
|
||||||
out = decode_ga_payload(st.get('out-data'))
|
|
||||||
err = decode_ga_payload(st.get('err-data'))
|
|
||||||
return out, err
|
|
||||||
|
|
||||||
|
|
||||||
def clean_exec_text(text: str) -> str:
|
def clean_exec_text(text: str) -> str:
|
||||||
if not text:
|
if not text:
|
||||||
return ''
|
return ''
|
||||||
text = re.sub(r'#< CLIXML.*', '', text, flags=re.DOTALL)
|
return re.sub(r'#< CLIXML.*', '', text, flags=re.DOTALL).strip()
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def extract_id_from_exec(out: str, err: str) -> str | None:
|
def extract_id_from_exec(out: str, err: str) -> str | None:
|
||||||
@@ -167,15 +164,14 @@ def extract_id_from_exec(out: str, err: str) -> str | None:
|
|||||||
def parse_exec_result(st: dict) -> tuple[str | None, str | None]:
|
def parse_exec_result(st: dict) -> tuple[str | None, str | None]:
|
||||||
try:
|
try:
|
||||||
exitcode = int(st.get('exitcode', 1))
|
exitcode = int(st.get('exitcode', 1))
|
||||||
out, err = decode_exec_payload(st)
|
out = decode_ga_payload(st.get('out-data'))
|
||||||
|
err = decode_ga_payload(st.get('err-data'))
|
||||||
rid = extract_id_from_exec(out, err)
|
rid = extract_id_from_exec(out, err)
|
||||||
if rid:
|
if rid:
|
||||||
return rid, None
|
return rid, None
|
||||||
if exitcode != 0:
|
if exitcode != 0:
|
||||||
brief = clean_exec_text(err) or clean_exec_text(out) or f'guest exit {exitcode}'
|
brief = clean_exec_text(err) or clean_exec_text(out) or f'guest exit {exitcode}'
|
||||||
if len(brief) > 120:
|
return None, brief[:120] + ('...' if len(brief) > 120 else '')
|
||||||
brief = brief[:120] + '...'
|
|
||||||
return None, brief
|
|
||||||
return None, f'no id in exec output: {out!r}'
|
return None, f'no id in exec output: {out!r}'
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
return None, f'exec decode error: {exc}'
|
return None, f'exec decode error: {exc}'
|
||||||
@@ -209,21 +205,16 @@ def guest_exec_text(vmid: int, command: list[str], timeout: int = 90) -> tuple[s
|
|||||||
try:
|
try:
|
||||||
st = json.loads(st_proc.stdout)
|
st = json.loads(st_proc.stdout)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
time.sleep(2)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
if st.get('exited') != 1:
|
if st.get('exited') != 1:
|
||||||
time.sleep(2)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
return parse_exec_result(st)
|
return parse_exec_result(st)
|
||||||
|
|
||||||
return None, 'guest exec timeout'
|
return None, 'guest exec timeout'
|
||||||
|
|
||||||
|
|
||||||
def decode_agent_file_content(raw) -> str | None:
|
|
||||||
text = decode_ga_payload(raw)
|
|
||||||
return text or None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_id_from_content(text: str, path: str) -> str | None:
|
def parse_id_from_content(text: str, path: str) -> str | None:
|
||||||
if path.lower().endswith('.json'):
|
if path.lower().endswith('.json'):
|
||||||
try:
|
try:
|
||||||
@@ -264,7 +255,7 @@ def guest_file_read(vmid: int, guest_path: str) -> tuple[str | None, str | None]
|
|||||||
raw = data.get('content')
|
raw = data.get('content')
|
||||||
if raw is None:
|
if raw is None:
|
||||||
continue
|
continue
|
||||||
text = decode_agent_file_content(raw)
|
text = decode_ga_payload(raw)
|
||||||
if text:
|
if text:
|
||||||
return text, None
|
return text, None
|
||||||
except (json.JSONDecodeError, ValueError, UnicodeError):
|
except (json.JSONDecodeError, ValueError, UnicodeError):
|
||||||
@@ -272,45 +263,31 @@ def guest_file_read(vmid: int, guest_path: str) -> tuple[str | None, str | None]
|
|||||||
return None, f'file-read failed for {guest_path}'
|
return None, f'file-read failed for {guest_path}'
|
||||||
|
|
||||||
|
|
||||||
def powershell_encoded(script: str) -> str:
|
|
||||||
return base64.b64encode(script.strip().encode('utf-16-le')).decode('ascii')
|
|
||||||
|
|
||||||
|
|
||||||
def guest_exec_powershell(vmid: int) -> tuple[str | None, str | None]:
|
|
||||||
enc = powershell_encoded(PS_READ_ID)
|
|
||||||
return guest_exec_text(
|
|
||||||
vmid,
|
|
||||||
['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', enc],
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def read_rustdesk_id(vmid: int) -> tuple[str | None, str | None]:
|
def read_rustdesk_id(vmid: int) -> tuple[str | None, str | None]:
|
||||||
errors: list[str] = []
|
|
||||||
for path in GUEST_READ_PATHS:
|
for path in GUEST_READ_PATHS:
|
||||||
try:
|
try:
|
||||||
text, err = guest_file_read(vmid, path)
|
text, err = guest_file_read(vmid, path)
|
||||||
if not text:
|
if not text:
|
||||||
if VERBOSE:
|
log(f'VM {vmid}: file-read miss {path}: {err}')
|
||||||
errors.append(f'{path}: {err}')
|
|
||||||
continue
|
continue
|
||||||
rid = parse_id_from_content(text, path)
|
rid = parse_id_from_content(text, path)
|
||||||
if rid:
|
if rid:
|
||||||
if VERBOSE:
|
log(f'VM {vmid}: ID {rid} from {path}')
|
||||||
log(f'VM {vmid}: ID {rid} from file-read {path}')
|
|
||||||
return rid, None
|
return rid, None
|
||||||
errors.append(f'{path}: no id in file')
|
log(f'VM {vmid}: no id in {path}')
|
||||||
except Exception as exc: # noqa: BLE001 — keep sync loop alive
|
except Exception as exc: # noqa: BLE001
|
||||||
errors.append(f'{path}: {exc}')
|
log(f'VM {vmid}: {path}: {exc}')
|
||||||
continue
|
|
||||||
|
|
||||||
rid, err = guest_exec_powershell(vmid)
|
enc = base64.b64encode(PS_READ_ID.strip().encode('utf-16-le')).decode('ascii')
|
||||||
|
rid, err = guest_exec_text(
|
||||||
|
vmid,
|
||||||
|
['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', enc],
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
if rid:
|
if rid:
|
||||||
if VERBOSE:
|
|
||||||
log(f'VM {vmid}: ID {rid} from guest exec')
|
log(f'VM {vmid}: ID {rid} from guest exec')
|
||||||
return rid, None
|
return rid, None
|
||||||
errors.append(f'exec: {err}')
|
return None, err or 'no RustDesk ID found'
|
||||||
return None, '; '.join(errors[-4:]) if errors else 'unknown'
|
|
||||||
|
|
||||||
|
|
||||||
def qm_description(vmid: int) -> str:
|
def qm_description(vmid: int) -> str:
|
||||||
@@ -336,31 +313,29 @@ def save_registry(registry: dict) -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if not os.path.isfile(REGISTRY):
|
if not os.path.isfile(REGISTRY):
|
||||||
log(f'Missing registry: {REGISTRY}')
|
log(f'Missing registry: {REGISTRY}', force=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if DRY_RUN:
|
if DRY_RUN:
|
||||||
log('DRY RUN - no qm set or registry writes')
|
log('DRY RUN - no qm set or registry writes', force=True)
|
||||||
|
|
||||||
registry = load_registry()
|
registry = load_registry()
|
||||||
vmids = fleet_vmids(registry)
|
vmids = fleet_vmids(registry)
|
||||||
if not vmids:
|
if not vmids:
|
||||||
log('No fleet VMIDs found')
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
registry_dirty = False
|
updated: list[int] = []
|
||||||
for vmid in vmids:
|
for vmid in vmids:
|
||||||
if not qm_running(vmid):
|
if not qm_running(vmid):
|
||||||
log(f'VM {vmid}: skip (not running)')
|
log(f'VM {vmid}: skip (not running)')
|
||||||
continue
|
continue
|
||||||
if not guest_ping(vmid):
|
if not guest_ping(vmid):
|
||||||
log(f'VM {vmid}: skip (guest agent not responding)')
|
log(f'VM {vmid}: skip (guest agent not responding)', force=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
new_id, err = read_rustdesk_id(vmid)
|
new_id, err = read_rustdesk_id(vmid)
|
||||||
if not new_id:
|
if not new_id:
|
||||||
hint = ' (try BLAIR_VERBOSE=1)' if not VERBOSE else ''
|
log(f'VM {vmid}: skip ({err})', force=True)
|
||||||
log(f'VM {vmid}: skip (could not read RustDesk ID: {err}){hint}')
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
old_registry_id = registry_old_id(registry, vmid)
|
old_registry_id = registry_old_id(registry, vmid)
|
||||||
@@ -372,11 +347,12 @@ def main() -> int:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
newdesc = update_desc(current_desc, new_id)
|
newdesc = update_desc(current_desc, new_id)
|
||||||
log(f'VM {vmid}: {old_registry_id or old_desc_id or "(none)"} -> {new_id}')
|
log(f'VM {vmid}: {old_registry_id or old_desc_id or "(none)"} -> {new_id}', force=True)
|
||||||
|
|
||||||
if DRY_RUN:
|
if DRY_RUN:
|
||||||
preview = newdesc if len(newdesc) <= 120 else newdesc[:120] + '...'
|
preview = newdesc if len(newdesc) <= 120 else newdesc[:120] + '...'
|
||||||
log(f' would set description: {preview}')
|
log(f' would set: {preview}', force=True)
|
||||||
|
updated.append(vmid)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
subprocess.run(['qm', 'set', str(vmid), '-description', newdesc], check=True)
|
subprocess.run(['qm', 'set', str(vmid), '-description', newdesc], check=True)
|
||||||
@@ -384,11 +360,13 @@ def main() -> int:
|
|||||||
for seat in registry.get('windows_gpu_seats', []):
|
for seat in registry.get('windows_gpu_seats', []):
|
||||||
if seat.get('vmid') == vmid:
|
if seat.get('vmid') == vmid:
|
||||||
seat['rustdesk_id'] = new_id
|
seat['rustdesk_id'] = new_id
|
||||||
registry_dirty = True
|
|
||||||
|
|
||||||
if registry_dirty and not DRY_RUN:
|
|
||||||
save_registry(registry)
|
save_registry(registry)
|
||||||
log('Updated registry.json')
|
updated.append(vmid)
|
||||||
|
|
||||||
|
if updated and not DRY_RUN:
|
||||||
|
log(f'Updated VM(s): {", ".join(str(v) for v in updated)}', force=True)
|
||||||
|
elif DRY_RUN and updated:
|
||||||
|
log(f'Would update VM(s): {", ".join(str(v) for v in updated)}', force=True)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
+9
-10
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Install blair-sync-rustdesk-via-ga.sh from Gitea raw URL. Run on pve as root.
|
# Install or upgrade blair-sync-rustdesk-via-ga.sh from Gitea. Run on pve as root.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BASE_URL="${BLAIR_SYNC_BASE_URL:-https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main}"
|
BASE_URL="${BLAIR_SYNC_BASE_URL:-https://git.easygoingaming.com/Davoguha/blair-pve-sync-temp/raw/main}"
|
||||||
@@ -7,6 +7,7 @@ TARGET='/usr/local/sbin/blair-sync-rustdesk-via-ga.sh'
|
|||||||
SHARE_COPY='/tank/blair-share/tools/golden/host/sync-rustdesk-via-guest-agent.sh'
|
SHARE_COPY='/tank/blair-share/tools/golden/host/sync-rustdesk-via-guest-agent.sh'
|
||||||
CRON='/etc/cron.d/blair-rustdesk-sync'
|
CRON='/etc/cron.d/blair-rustdesk-sync'
|
||||||
LOG='/var/log/blair-rustdesk-sync.log'
|
LOG='/var/log/blair-rustdesk-sync.log'
|
||||||
|
CRON_LINE="*/2 * * * * root $TARGET >> $LOG 2>&1"
|
||||||
|
|
||||||
echo "=== Download sync script ==="
|
echo "=== Download sync script ==="
|
||||||
curl -fsSL "$BASE_URL/blair-sync-rustdesk-via-ga.sh" -o "$TARGET"
|
curl -fsSL "$BASE_URL/blair-sync-rustdesk-via-ga.sh" -o "$TARGET"
|
||||||
@@ -17,22 +18,20 @@ mkdir -p "$(dirname "$SHARE_COPY")"
|
|||||||
cp -f "$TARGET" "$SHARE_COPY"
|
cp -f "$TARGET" "$SHARE_COPY"
|
||||||
chmod +x "$SHARE_COPY"
|
chmod +x "$SHARE_COPY"
|
||||||
|
|
||||||
echo "=== shebang check ==="
|
echo "=== cron (every 2 min) ==="
|
||||||
head -1 "$TARGET" | od -An -tx1
|
printf '%s\n' "$CRON_LINE" > "$CRON"
|
||||||
file "$TARGET"
|
|
||||||
|
|
||||||
echo "=== cron ==="
|
|
||||||
printf '%s\n' "*/5 * * * * root $TARGET >> $LOG 2>&1" > "$CRON"
|
|
||||||
chmod 644 "$CRON"
|
chmod 644 "$CRON"
|
||||||
cat "$CRON"
|
cat "$CRON"
|
||||||
|
|
||||||
echo "=== dry run ==="
|
echo "=== dry run (verbose) ==="
|
||||||
BLAIR_VERBOSE=1 "$TARGET" --dry-run
|
BLAIR_VERBOSE=1 "$TARGET" --dry-run
|
||||||
|
|
||||||
echo "=== live run ==="
|
echo "=== live run ==="
|
||||||
"$TARGET"
|
"$TARGET"
|
||||||
|
|
||||||
echo "=== VM 230 description ==="
|
echo "=== fleet descriptions ==="
|
||||||
qm config 230 | grep ^description || true
|
for v in 230 231 232 233 234 235; do
|
||||||
|
qm config "$v" 2>/dev/null | grep ^description || true
|
||||||
|
done
|
||||||
|
|
||||||
echo "=== done ==="
|
echo "=== done ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user