Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2adf1d5548 | |||
| 067a7666e4 |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-02 - 5.5.0 - feat(proxmox)
|
||||
add Proxmox CLI auto-detection and interactive action setup improvements
|
||||
|
||||
- Add Proxmox action support for CLI mode using qm/pct with automatic fallback to REST API mode
|
||||
- Expose proxmoxMode configuration and update CLI wizards to auto-detect local Proxmox tools before prompting for API credentials
|
||||
- Expand interactive action creation to support shutdown, webhook, script, and Proxmox actions with improved displayed details
|
||||
- Update documentation to cover Proxmox CLI/API modes and clarify shutdown delay units in minutes
|
||||
|
||||
## 2026-03-30 - 5.4.1 - fix(deps)
|
||||
bump tsdeno and net-snmp patch dependencies
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "5.4.1",
|
||||
"version": "5.5.0",
|
||||
"exports": "./mod.ts",
|
||||
"nodeModulesDir": "auto",
|
||||
"tasks": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/nupst",
|
||||
"version": "5.4.1",
|
||||
"version": "5.5.0",
|
||||
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
|
||||
"keywords": [
|
||||
"ups",
|
||||
|
||||
63
readme.md
63
readme.md
@@ -12,7 +12,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
||||
|
||||
- **🔌 Multi-UPS Support** — Monitor multiple UPS devices from a single daemon
|
||||
- **📡 Dual Protocol Support** — SNMP (v1/v2c/v3) for network UPS + UPSD/NIS for USB-connected UPS via NUT
|
||||
- **🖥️ Proxmox Integration** — Gracefully shut down QEMU VMs and LXC containers before host shutdown
|
||||
- **🖥️ Proxmox Integration** — Gracefully shut down QEMU VMs and LXC containers before host shutdown (auto-detects CLI tools — no API token needed on Proxmox hosts)
|
||||
- **👥 Group Management** — Organize UPS devices into groups with flexible operating modes
|
||||
- **Redundant Mode** — Only trigger actions when ALL UPS devices in a group are critical
|
||||
- **Non-Redundant Mode** — Trigger actions when ANY UPS device is critical
|
||||
@@ -250,10 +250,9 @@ NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to confi
|
||||
"type": "proxmox",
|
||||
"triggerMode": "onlyThresholds",
|
||||
"thresholds": { "battery": 30, "runtime": 15 },
|
||||
"proxmoxHost": "localhost",
|
||||
"proxmoxPort": 8006,
|
||||
"proxmoxTokenId": "root@pam!nupst",
|
||||
"proxmoxTokenSecret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
"proxmoxMode": "auto",
|
||||
"proxmoxExcludeIds": [],
|
||||
"proxmoxForceStop": true
|
||||
},
|
||||
{
|
||||
"type": "shutdown",
|
||||
@@ -364,7 +363,7 @@ Actions define automated responses to UPS conditions. They run **sequentially in
|
||||
| `shutdown` | Graceful system shutdown with configurable delay |
|
||||
| `webhook` | HTTP POST/GET notification to external services |
|
||||
| `script` | Execute custom shell scripts from `/etc/nupst/` |
|
||||
| `proxmox` | Shut down Proxmox QEMU VMs and LXC containers via REST API |
|
||||
| `proxmox` | Shut down Proxmox QEMU VMs and LXC containers (CLI or API) |
|
||||
|
||||
#### Common Fields
|
||||
|
||||
@@ -396,7 +395,7 @@ Actions define automated responses to UPS conditions. They run **sequentially in
|
||||
|
||||
| Field | Description | Default |
|
||||
| --------------- | ---------------------------------- | ------- |
|
||||
| `shutdownDelay` | Seconds to wait before shutdown | `5` |
|
||||
| `shutdownDelay` | Minutes to wait before shutdown | `5` |
|
||||
|
||||
#### Webhook Action
|
||||
|
||||
@@ -438,11 +437,38 @@ Actions define automated responses to UPS conditions. They run **sequentially in
|
||||
|
||||
Gracefully shuts down QEMU VMs and LXC containers on a Proxmox node before the host is shut down.
|
||||
|
||||
NUPST supports **two operation modes** for Proxmox:
|
||||
|
||||
| Mode | Description | Requirements |
|
||||
| ------ | -------------------------------------------------------------- | ------------------------- |
|
||||
| `cli` | Uses `qm`/`pct` commands directly — **no API token needed** 🎉 | Running as root on Proxmox host |
|
||||
| `api` | Uses Proxmox REST API via HTTPS | API token required |
|
||||
| `auto` | Prefers CLI if available, falls back to API (default) | — |
|
||||
|
||||
> 💡 **On a Proxmox host running as root** (the typical setup), NUPST auto-detects `qm` and `pct` CLI tools and uses them directly. No API token setup required!
|
||||
|
||||
**CLI mode example** (simplest — auto-detected on Proxmox hosts):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "proxmox",
|
||||
"thresholds": { "battery": 30, "runtime": 15 },
|
||||
"triggerMode": "onlyThresholds",
|
||||
"proxmoxMode": "auto",
|
||||
"proxmoxExcludeIds": [100, 101],
|
||||
"proxmoxStopTimeout": 120,
|
||||
"proxmoxForceStop": true
|
||||
}
|
||||
```
|
||||
|
||||
**API mode example** (for remote Proxmox hosts or non-root setups):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "proxmox",
|
||||
"thresholds": { "battery": 30, "runtime": 15 },
|
||||
"triggerMode": "onlyThresholds",
|
||||
"proxmoxMode": "api",
|
||||
"proxmoxHost": "localhost",
|
||||
"proxmoxPort": 8006,
|
||||
"proxmoxTokenId": "root@pam!nupst",
|
||||
@@ -456,17 +482,18 @@ Gracefully shuts down QEMU VMs and LXC containers on a Proxmox node before the h
|
||||
|
||||
| Field | Description | Default |
|
||||
| --------------------- | ----------------------------------------------- | ------------- |
|
||||
| `proxmoxHost` | Proxmox API host | `localhost` |
|
||||
| `proxmoxPort` | Proxmox API port | `8006` |
|
||||
| `proxmoxMode` | Operation mode | `auto` |
|
||||
| `proxmoxHost` | Proxmox API host (API mode only) | `localhost` |
|
||||
| `proxmoxPort` | Proxmox API port (API mode only) | `8006` |
|
||||
| `proxmoxNode` | Proxmox node name | Auto-detect via hostname |
|
||||
| `proxmoxTokenId` | API token ID (e.g. `root@pam!nupst`) | Required |
|
||||
| `proxmoxTokenSecret` | API token secret (UUID) | Required |
|
||||
| `proxmoxTokenId` | API token ID (API mode only) | — |
|
||||
| `proxmoxTokenSecret` | API token secret (API mode only) | — |
|
||||
| `proxmoxExcludeIds` | VM/CT IDs to skip | `[]` |
|
||||
| `proxmoxStopTimeout` | Seconds to wait for graceful shutdown | `120` |
|
||||
| `proxmoxForceStop` | Force-stop VMs/CTs that don't shut down | `true` |
|
||||
| `proxmoxInsecure` | Skip TLS verification (self-signed certs) | `true` |
|
||||
| `proxmoxInsecure` | Skip TLS verification (API mode only) | `true` |
|
||||
|
||||
**Setting up the API token on Proxmox:**
|
||||
**Setting up the API token** (only needed for API mode):
|
||||
|
||||
```bash
|
||||
# Create token with full privileges (no privilege separation)
|
||||
@@ -631,7 +658,7 @@ Full SNMPv3 support with authentication and encryption:
|
||||
|
||||
### Network Security
|
||||
|
||||
- Connects only to UPS devices and optionally Proxmox on local network
|
||||
- Connects only to UPS devices and optionally Proxmox on local network (CLI mode uses local tools — no network needed for VM shutdown)
|
||||
- HTTP API disabled by default; token-required when enabled
|
||||
- No external internet connections
|
||||
|
||||
@@ -741,7 +768,13 @@ upsc ups@localhost # if NUT CLI is installed
|
||||
### Proxmox VMs Not Shutting Down
|
||||
|
||||
```bash
|
||||
# Verify API token works
|
||||
# CLI mode: verify qm/pct are available and you're root
|
||||
which qm pct
|
||||
whoami # should be 'root'
|
||||
qm list # should list VMs
|
||||
pct list # should list containers
|
||||
|
||||
# API mode: verify API token works
|
||||
curl -k -H "Authorization: PVEAPIToken=root@pam!nupst=YOUR-SECRET" \
|
||||
https://localhost:8006/api2/json/nodes/$(hostname)/qemu
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '5.4.1',
|
||||
version: '5.5.0',
|
||||
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ export interface IActionConfig {
|
||||
proxmoxForceStop?: boolean;
|
||||
/** Skip TLS verification for self-signed certificates (default: true) */
|
||||
proxmoxInsecure?: boolean;
|
||||
/** Proxmox operation mode: 'auto' detects CLI tools, 'cli' forces CLI, 'api' forces REST API (default: 'auto') */
|
||||
proxmoxMode?: 'auto' | 'api' | 'cli';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import process from 'node:process';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { Action, type IActionContext } from './base-action.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import { PROXMOX, UI } from '../constants.ts';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* ProxmoxAction - Gracefully shuts down Proxmox VMs and LXC containers
|
||||
*
|
||||
* Uses the Proxmox REST API via HTTPS with API token authentication.
|
||||
* Shuts down running QEMU VMs and LXC containers, waits for completion,
|
||||
* and optionally force-stops any that don't respond.
|
||||
* Supports two operation modes:
|
||||
* - CLI mode: Uses qm/pct commands directly (requires running as root on a Proxmox host)
|
||||
* - API mode: Uses the Proxmox REST API via HTTPS with API token authentication
|
||||
*
|
||||
* In 'auto' mode (default), CLI is preferred when available, falling back to API.
|
||||
*
|
||||
* This action should be placed BEFORE shutdown actions in the action chain
|
||||
* so that VMs are stopped before the host is shut down.
|
||||
@@ -16,6 +24,77 @@ import { PROXMOX, UI } from '../constants.ts';
|
||||
export class ProxmoxAction extends Action {
|
||||
readonly type = 'proxmox';
|
||||
|
||||
/**
|
||||
* Check if Proxmox CLI tools (qm, pct) are available on the system
|
||||
* Used by CLI wizards and by execute() for auto-detection
|
||||
*/
|
||||
static detectCliAvailability(): {
|
||||
available: boolean;
|
||||
qmPath: string | null;
|
||||
pctPath: string | null;
|
||||
isRoot: boolean;
|
||||
} {
|
||||
let qmPath: string | null = null;
|
||||
let pctPath: string | null = null;
|
||||
|
||||
for (const dir of PROXMOX.CLI_TOOL_PATHS) {
|
||||
if (!qmPath) {
|
||||
const p = `${dir}/qm`;
|
||||
try {
|
||||
if (fs.existsSync(p)) qmPath = p;
|
||||
} catch (_e) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
if (!pctPath) {
|
||||
const p = `${dir}/pct`;
|
||||
try {
|
||||
if (fs.existsSync(p)) pctPath = p;
|
||||
} catch (_e) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isRoot = !!(process.getuid && process.getuid() === 0);
|
||||
|
||||
return {
|
||||
available: qmPath !== null && pctPath !== null && isRoot,
|
||||
qmPath,
|
||||
pctPath,
|
||||
isRoot,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the operation mode based on config and environment
|
||||
*/
|
||||
private resolveMode(): { mode: 'api' | 'cli'; qmPath: string; pctPath: string } | { mode: 'api'; qmPath?: undefined; pctPath?: undefined } {
|
||||
const configuredMode = this.config.proxmoxMode || 'auto';
|
||||
|
||||
if (configuredMode === 'api') {
|
||||
return { mode: 'api' };
|
||||
}
|
||||
|
||||
const detection = ProxmoxAction.detectCliAvailability();
|
||||
|
||||
if (configuredMode === 'cli') {
|
||||
if (!detection.qmPath || !detection.pctPath) {
|
||||
throw new Error('CLI mode requested but qm/pct not found. Are you on a Proxmox host?');
|
||||
}
|
||||
if (!detection.isRoot) {
|
||||
throw new Error('CLI mode requires root access');
|
||||
}
|
||||
return { mode: 'cli', qmPath: detection.qmPath, pctPath: detection.pctPath };
|
||||
}
|
||||
|
||||
// Auto-detect
|
||||
if (detection.available && detection.qmPath && detection.pctPath) {
|
||||
return { mode: 'cli', qmPath: detection.qmPath, pctPath: detection.pctPath };
|
||||
}
|
||||
return { mode: 'api' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the Proxmox shutdown action
|
||||
*/
|
||||
@@ -29,30 +108,21 @@ export class ProxmoxAction extends Action {
|
||||
return;
|
||||
}
|
||||
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
const resolved = this.resolveMode();
|
||||
const node = this.config.proxmoxNode || os.hostname();
|
||||
const tokenId = this.config.proxmoxTokenId;
|
||||
const tokenSecret = this.config.proxmoxTokenSecret;
|
||||
const excludeIds = new Set(this.config.proxmoxExcludeIds || []);
|
||||
const stopTimeout = (this.config.proxmoxStopTimeout || PROXMOX.DEFAULT_STOP_TIMEOUT_SECONDS) * 1000;
|
||||
const forceStop = this.config.proxmoxForceStop !== false; // default true
|
||||
const insecure = this.config.proxmoxInsecure !== false; // default true
|
||||
|
||||
if (!tokenId || !tokenSecret) {
|
||||
logger.error('Proxmox API token ID and secret are required');
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = `https://${host}:${port}${PROXMOX.API_BASE}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `PVEAPIToken=${tokenId}=${tokenSecret}`,
|
||||
};
|
||||
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Proxmox VM Shutdown', UI.WIDE_BOX_WIDTH, 'warning');
|
||||
logger.logBoxLine(`Mode: ${resolved.mode === 'cli' ? 'CLI (qm/pct)' : 'API (REST)'}`);
|
||||
logger.logBoxLine(`Node: ${node}`);
|
||||
logger.logBoxLine(`API: ${host}:${port}`);
|
||||
if (resolved.mode === 'api') {
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
logger.logBoxLine(`API: ${host}:${port}`);
|
||||
}
|
||||
logger.logBoxLine(`UPS: ${context.upsName} (${context.powerStatus})`);
|
||||
logger.logBoxLine(`Trigger: ${context.triggerReason}`);
|
||||
if (excludeIds.size > 0) {
|
||||
@@ -62,9 +132,34 @@ export class ProxmoxAction extends Action {
|
||||
logger.log('');
|
||||
|
||||
try {
|
||||
// Collect running VMs and CTs
|
||||
const runningVMs = await this.getRunningVMs(baseUrl, node, headers, insecure);
|
||||
const runningCTs = await this.getRunningCTs(baseUrl, node, headers, insecure);
|
||||
let runningVMs: Array<{ vmid: number; name: string }>;
|
||||
let runningCTs: Array<{ vmid: number; name: string }>;
|
||||
|
||||
if (resolved.mode === 'cli') {
|
||||
runningVMs = await this.getRunningVMsCli(resolved.qmPath);
|
||||
runningCTs = await this.getRunningCTsCli(resolved.pctPath);
|
||||
} else {
|
||||
// API mode - validate token
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
const tokenId = this.config.proxmoxTokenId;
|
||||
const tokenSecret = this.config.proxmoxTokenSecret;
|
||||
const insecure = this.config.proxmoxInsecure !== false;
|
||||
|
||||
if (!tokenId || !tokenSecret) {
|
||||
logger.error('Proxmox API token ID and secret are required for API mode');
|
||||
logger.error('Either provide tokens or run on a Proxmox host as root for CLI mode');
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = `https://${host}:${port}${PROXMOX.API_BASE}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `PVEAPIToken=${tokenId}=${tokenSecret}`,
|
||||
};
|
||||
|
||||
runningVMs = await this.getRunningVMsApi(baseUrl, node, headers, insecure);
|
||||
runningCTs = await this.getRunningCTsApi(baseUrl, node, headers, insecure);
|
||||
}
|
||||
|
||||
// Filter out excluded IDs
|
||||
const vmsToStop = runningVMs.filter((vm) => !excludeIds.has(vm.vmid));
|
||||
@@ -78,15 +173,33 @@ export class ProxmoxAction extends Action {
|
||||
|
||||
logger.info(`Shutting down ${vmsToStop.length} VMs and ${ctsToStop.length} containers...`);
|
||||
|
||||
// Send shutdown commands to all VMs and CTs
|
||||
for (const vm of vmsToStop) {
|
||||
await this.shutdownVM(baseUrl, node, vm.vmid, headers, insecure);
|
||||
logger.dim(` Shutdown sent to VM ${vm.vmid} (${vm.name || 'unnamed'})`);
|
||||
}
|
||||
// Send shutdown commands
|
||||
if (resolved.mode === 'cli') {
|
||||
for (const vm of vmsToStop) {
|
||||
await this.shutdownVMCli(resolved.qmPath, vm.vmid);
|
||||
logger.dim(` Shutdown sent to VM ${vm.vmid} (${vm.name || 'unnamed'})`);
|
||||
}
|
||||
for (const ct of ctsToStop) {
|
||||
await this.shutdownCTCli(resolved.pctPath, ct.vmid);
|
||||
logger.dim(` Shutdown sent to CT ${ct.vmid} (${ct.name || 'unnamed'})`);
|
||||
}
|
||||
} else {
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
const insecure = this.config.proxmoxInsecure !== false;
|
||||
const baseUrl = `https://${host}:${port}${PROXMOX.API_BASE}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `PVEAPIToken=${this.config.proxmoxTokenId}=${this.config.proxmoxTokenSecret}`,
|
||||
};
|
||||
|
||||
for (const ct of ctsToStop) {
|
||||
await this.shutdownCT(baseUrl, node, ct.vmid, headers, insecure);
|
||||
logger.dim(` Shutdown sent to CT ${ct.vmid} (${ct.name || 'unnamed'})`);
|
||||
for (const vm of vmsToStop) {
|
||||
await this.shutdownVMApi(baseUrl, node, vm.vmid, headers, insecure);
|
||||
logger.dim(` Shutdown sent to VM ${vm.vmid} (${vm.name || 'unnamed'})`);
|
||||
}
|
||||
for (const ct of ctsToStop) {
|
||||
await this.shutdownCTApi(baseUrl, node, ct.vmid, headers, insecure);
|
||||
logger.dim(` Shutdown sent to CT ${ct.vmid} (${ct.name || 'unnamed'})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll until all stopped or timeout
|
||||
@@ -95,23 +208,31 @@ export class ProxmoxAction extends Action {
|
||||
...ctsToStop.map((ct) => ({ type: 'lxc' as const, vmid: ct.vmid, name: ct.name })),
|
||||
];
|
||||
|
||||
const remaining = await this.waitForShutdown(
|
||||
baseUrl,
|
||||
node,
|
||||
allIds,
|
||||
headers,
|
||||
insecure,
|
||||
stopTimeout,
|
||||
);
|
||||
const remaining = await this.waitForShutdown(allIds, resolved, node, stopTimeout);
|
||||
|
||||
if (remaining.length > 0 && forceStop) {
|
||||
logger.warn(`${remaining.length} VMs/CTs didn't shut down gracefully, force-stopping...`);
|
||||
for (const item of remaining) {
|
||||
try {
|
||||
if (item.type === 'qemu') {
|
||||
await this.stopVM(baseUrl, node, item.vmid, headers, insecure);
|
||||
if (resolved.mode === 'cli') {
|
||||
if (item.type === 'qemu') {
|
||||
await this.stopVMCli(resolved.qmPath, item.vmid);
|
||||
} else {
|
||||
await this.stopCTCli(resolved.pctPath, item.vmid);
|
||||
}
|
||||
} else {
|
||||
await this.stopCT(baseUrl, node, item.vmid, headers, insecure);
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
const insecure = this.config.proxmoxInsecure !== false;
|
||||
const baseUrl = `https://${host}:${port}${PROXMOX.API_BASE}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `PVEAPIToken=${this.config.proxmoxTokenId}=${this.config.proxmoxTokenSecret}`,
|
||||
};
|
||||
if (item.type === 'qemu') {
|
||||
await this.stopVMApi(baseUrl, node, item.vmid, headers, insecure);
|
||||
} else {
|
||||
await this.stopCTApi(baseUrl, node, item.vmid, headers, insecure);
|
||||
}
|
||||
}
|
||||
logger.dim(` Force-stopped ${item.type} ${item.vmid} (${item.name || 'unnamed'})`);
|
||||
} catch (error) {
|
||||
@@ -134,6 +255,110 @@ export class ProxmoxAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CLI-based methods ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get list of running QEMU VMs via qm list
|
||||
*/
|
||||
private async getRunningVMsCli(
|
||||
qmPath: string,
|
||||
): Promise<Array<{ vmid: number; name: string }>> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(qmPath, ['list']);
|
||||
return this.parseQmList(stdout);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to list VMs via CLI: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of running LXC containers via pct list
|
||||
*/
|
||||
private async getRunningCTsCli(
|
||||
pctPath: string,
|
||||
): Promise<Array<{ vmid: number; name: string }>> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(pctPath, ['list']);
|
||||
return this.parsePctList(stdout);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to list CTs via CLI: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse qm list output
|
||||
* Format: VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID
|
||||
*/
|
||||
private parseQmList(output: string): Array<{ vmid: number; name: string }> {
|
||||
const results: Array<{ vmid: number; name: string }> = [];
|
||||
const lines = output.trim().split('\n');
|
||||
|
||||
// Skip header line
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const match = lines[i].match(/^\s*(\d+)\s+(\S+)\s+(running|stopped|paused)/);
|
||||
if (match && match[3] === 'running') {
|
||||
results.push({ vmid: parseInt(match[1], 10), name: match[2] });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse pct list output
|
||||
* Format: VMID Status Lock Name
|
||||
*/
|
||||
private parsePctList(output: string): Array<{ vmid: number; name: string }> {
|
||||
const results: Array<{ vmid: number; name: string }> = [];
|
||||
const lines = output.trim().split('\n');
|
||||
|
||||
// Skip header line
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const match = lines[i].match(/^\s*(\d+)\s+(running|stopped)\s+\S*\s*(.*)/);
|
||||
if (match && match[2] === 'running') {
|
||||
results.push({ vmid: parseInt(match[1], 10), name: match[3]?.trim() || '' });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private async shutdownVMCli(qmPath: string, vmid: number): Promise<void> {
|
||||
await execFileAsync(qmPath, ['shutdown', String(vmid)]);
|
||||
}
|
||||
|
||||
private async shutdownCTCli(pctPath: string, vmid: number): Promise<void> {
|
||||
await execFileAsync(pctPath, ['shutdown', String(vmid)]);
|
||||
}
|
||||
|
||||
private async stopVMCli(qmPath: string, vmid: number): Promise<void> {
|
||||
await execFileAsync(qmPath, ['stop', String(vmid)]);
|
||||
}
|
||||
|
||||
private async stopCTCli(pctPath: string, vmid: number): Promise<void> {
|
||||
await execFileAsync(pctPath, ['stop', String(vmid)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get VM/CT status via CLI
|
||||
* Returns the status string (e.g., 'running', 'stopped')
|
||||
*/
|
||||
private async getStatusCli(
|
||||
toolPath: string,
|
||||
vmid: number,
|
||||
): Promise<string> {
|
||||
const { stdout } = await execFileAsync(toolPath, ['status', String(vmid)]);
|
||||
// Output format: "status: running\n"
|
||||
const status = stdout.trim().split(':')[1]?.trim() || 'unknown';
|
||||
return status;
|
||||
}
|
||||
|
||||
// ─── API-based methods ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Make an API request to the Proxmox server
|
||||
*/
|
||||
@@ -173,9 +398,9 @@ export class ProxmoxAction extends Action {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of running QEMU VMs
|
||||
* Get list of running QEMU VMs via API
|
||||
*/
|
||||
private async getRunningVMs(
|
||||
private async getRunningVMsApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
headers: Record<string, string>,
|
||||
@@ -201,9 +426,9 @@ export class ProxmoxAction extends Action {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of running LXC containers
|
||||
* Get list of running LXC containers via API
|
||||
*/
|
||||
private async getRunningCTs(
|
||||
private async getRunningCTsApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
headers: Record<string, string>,
|
||||
@@ -228,10 +453,7 @@ export class ProxmoxAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send graceful shutdown to a QEMU VM
|
||||
*/
|
||||
private async shutdownVM(
|
||||
private async shutdownVMApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
vmid: number,
|
||||
@@ -246,10 +468,7 @@ export class ProxmoxAction extends Action {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send graceful shutdown to an LXC container
|
||||
*/
|
||||
private async shutdownCT(
|
||||
private async shutdownCTApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
vmid: number,
|
||||
@@ -264,10 +483,7 @@ export class ProxmoxAction extends Action {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-stop a QEMU VM
|
||||
*/
|
||||
private async stopVM(
|
||||
private async stopVMApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
vmid: number,
|
||||
@@ -282,10 +498,7 @@ export class ProxmoxAction extends Action {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-stop an LXC container
|
||||
*/
|
||||
private async stopCT(
|
||||
private async stopCTApi(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
vmid: number,
|
||||
@@ -300,15 +513,15 @@ export class ProxmoxAction extends Action {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shared methods ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wait for VMs/CTs to shut down, return any that are still running after timeout
|
||||
*/
|
||||
private async waitForShutdown(
|
||||
baseUrl: string,
|
||||
node: string,
|
||||
items: Array<{ type: 'qemu' | 'lxc'; vmid: number; name: string }>,
|
||||
headers: Record<string, string>,
|
||||
insecure: boolean,
|
||||
resolved: { mode: 'api' | 'cli'; qmPath?: string; pctPath?: string },
|
||||
node: string,
|
||||
timeout: number,
|
||||
): Promise<Array<{ type: 'qemu' | 'lxc'; vmid: number; name: string }>> {
|
||||
const startTime = Date.now();
|
||||
@@ -323,12 +536,27 @@ export class ProxmoxAction extends Action {
|
||||
|
||||
for (const item of remaining) {
|
||||
try {
|
||||
const statusUrl = `${baseUrl}/nodes/${node}/${item.type}/${item.vmid}/status/current`;
|
||||
const response = await this.apiRequest(statusUrl, 'GET', headers, insecure) as {
|
||||
data: { status: string };
|
||||
};
|
||||
let status: string;
|
||||
|
||||
if (response.data?.status === 'running') {
|
||||
if (resolved.mode === 'cli') {
|
||||
const toolPath = item.type === 'qemu' ? resolved.qmPath! : resolved.pctPath!;
|
||||
status = await this.getStatusCli(toolPath, item.vmid);
|
||||
} else {
|
||||
const host = this.config.proxmoxHost || PROXMOX.DEFAULT_HOST;
|
||||
const port = this.config.proxmoxPort || PROXMOX.DEFAULT_PORT;
|
||||
const insecure = this.config.proxmoxInsecure !== false;
|
||||
const baseUrl = `https://${host}:${port}${PROXMOX.API_BASE}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `PVEAPIToken=${this.config.proxmoxTokenId}=${this.config.proxmoxTokenSecret}`,
|
||||
};
|
||||
const statusUrl = `${baseUrl}/nodes/${node}/${item.type}/${item.vmid}/status/current`;
|
||||
const response = await this.apiRequest(statusUrl, 'GET', headers, insecure) as {
|
||||
data: { status: string };
|
||||
};
|
||||
status = response.data?.status || 'unknown';
|
||||
}
|
||||
|
||||
if (status === 'running') {
|
||||
stillRunning.push(item);
|
||||
} else {
|
||||
logger.dim(` ${item.type} ${item.vmid} (${item.name}) stopped`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Nupst } from '../nupst.ts';
|
||||
import { type ITableColumn, logger } from '../logger.ts';
|
||||
import { symbols, theme } from '../colors.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
import { ProxmoxAction } from '../actions/proxmox-action.ts';
|
||||
import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
|
||||
@@ -65,11 +66,146 @@ export class ActionHandler {
|
||||
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
|
||||
logger.log('');
|
||||
|
||||
// Action type (currently only shutdown is supported)
|
||||
const type = 'shutdown';
|
||||
logger.log(` ${theme.dim('Action type:')} ${theme.highlight('shutdown')}`);
|
||||
// Action type selection
|
||||
logger.log(` ${theme.dim('Action Type:')}`);
|
||||
logger.log(` ${theme.dim('1)')} Shutdown (system shutdown)`);
|
||||
logger.log(` ${theme.dim('2)')} Webhook (HTTP notification)`);
|
||||
logger.log(` ${theme.dim('3)')} Custom Script (run .sh file from /etc/nupst)`);
|
||||
logger.log(` ${theme.dim('4)')} Proxmox (gracefully shut down VMs/LXCs before host shutdown)`);
|
||||
|
||||
// Battery threshold
|
||||
const typeInput = await prompt(` ${theme.dim('Select action type')} ${theme.dim('[1]:')} `);
|
||||
const typeValue = parseInt(typeInput, 10) || 1;
|
||||
|
||||
const newAction: Partial<IActionConfig> = {};
|
||||
|
||||
if (typeValue === 1) {
|
||||
// Shutdown action
|
||||
newAction.type = 'shutdown';
|
||||
|
||||
const delayStr = await prompt(
|
||||
` ${theme.dim('Shutdown delay')} ${theme.dim('(minutes) [5]:')} `,
|
||||
);
|
||||
const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5;
|
||||
if (isNaN(shutdownDelay) || shutdownDelay < 0) {
|
||||
logger.error('Invalid shutdown delay. Must be >= 0.');
|
||||
process.exit(1);
|
||||
}
|
||||
newAction.shutdownDelay = shutdownDelay;
|
||||
} else if (typeValue === 2) {
|
||||
// Webhook action
|
||||
newAction.type = 'webhook';
|
||||
|
||||
const url = await prompt(` ${theme.dim('Webhook URL:')} `);
|
||||
if (!url.trim()) {
|
||||
logger.error('Webhook URL is required.');
|
||||
process.exit(1);
|
||||
}
|
||||
newAction.webhookUrl = url.trim();
|
||||
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('HTTP Method:')}`);
|
||||
logger.log(` ${theme.dim('1)')} POST (JSON body)`);
|
||||
logger.log(` ${theme.dim('2)')} GET (query parameters)`);
|
||||
const methodInput = await prompt(` ${theme.dim('Select method')} ${theme.dim('[1]:')} `);
|
||||
newAction.webhookMethod = methodInput === '2' ? 'GET' : 'POST';
|
||||
|
||||
const timeoutInput = await prompt(` ${theme.dim('Timeout in seconds')} ${theme.dim('[10]:')} `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
newAction.webhookTimeout = timeout * 1000;
|
||||
}
|
||||
} else if (typeValue === 3) {
|
||||
// Script action
|
||||
newAction.type = 'script';
|
||||
|
||||
const scriptPath = await prompt(` ${theme.dim('Script filename (in /etc/nupst/, must end with .sh):')} `);
|
||||
if (!scriptPath.trim() || !scriptPath.trim().endsWith('.sh')) {
|
||||
logger.error('Script path must end with .sh.');
|
||||
process.exit(1);
|
||||
}
|
||||
newAction.scriptPath = scriptPath.trim();
|
||||
|
||||
const timeoutInput = await prompt(` ${theme.dim('Script timeout in seconds')} ${theme.dim('[60]:')} `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
newAction.scriptTimeout = timeout * 1000;
|
||||
}
|
||||
} else if (typeValue === 4) {
|
||||
// Proxmox action
|
||||
newAction.type = 'proxmox';
|
||||
|
||||
// Auto-detect CLI availability
|
||||
const detection = ProxmoxAction.detectCliAvailability();
|
||||
|
||||
if (detection.available) {
|
||||
logger.log('');
|
||||
logger.success('Proxmox CLI tools detected (qm/pct). No API token needed.');
|
||||
logger.dim(` qm: ${detection.qmPath}`);
|
||||
logger.dim(` pct: ${detection.pctPath}`);
|
||||
newAction.proxmoxMode = 'cli';
|
||||
} else {
|
||||
logger.log('');
|
||||
if (!detection.isRoot) {
|
||||
logger.warn('Not running as root - CLI mode unavailable, using API mode.');
|
||||
} else {
|
||||
logger.warn('Proxmox CLI tools (qm/pct) not found - using API mode.');
|
||||
}
|
||||
logger.log('');
|
||||
logger.info('Proxmox API Settings:');
|
||||
logger.dim('Create a token with: pveum user token add root@pam nupst --privsep=0');
|
||||
|
||||
const pxHost = await prompt(` ${theme.dim('Proxmox Host')} ${theme.dim('[localhost]:')} `);
|
||||
newAction.proxmoxHost = pxHost.trim() || 'localhost';
|
||||
|
||||
const pxPortInput = await prompt(` ${theme.dim('Proxmox API Port')} ${theme.dim('[8006]:')} `);
|
||||
const pxPort = parseInt(pxPortInput, 10);
|
||||
newAction.proxmoxPort = pxPortInput.trim() && !isNaN(pxPort) ? pxPort : 8006;
|
||||
|
||||
const pxNode = await prompt(` ${theme.dim('Proxmox Node Name (empty = auto-detect):')} `);
|
||||
if (pxNode.trim()) {
|
||||
newAction.proxmoxNode = pxNode.trim();
|
||||
}
|
||||
|
||||
const tokenId = await prompt(` ${theme.dim('API Token ID (e.g., root@pam!nupst):')} `);
|
||||
if (!tokenId.trim()) {
|
||||
logger.error('Token ID is required for API mode.');
|
||||
process.exit(1);
|
||||
}
|
||||
newAction.proxmoxTokenId = tokenId.trim();
|
||||
|
||||
const tokenSecret = await prompt(` ${theme.dim('API Token Secret:')} `);
|
||||
if (!tokenSecret.trim()) {
|
||||
logger.error('Token Secret is required for API mode.');
|
||||
process.exit(1);
|
||||
}
|
||||
newAction.proxmoxTokenSecret = tokenSecret.trim();
|
||||
|
||||
const insecureInput = await prompt(` ${theme.dim('Skip TLS verification (self-signed cert)?')} ${theme.dim('(Y/n):')} `);
|
||||
newAction.proxmoxInsecure = insecureInput.toLowerCase() !== 'n';
|
||||
newAction.proxmoxMode = 'api';
|
||||
}
|
||||
|
||||
// Common Proxmox settings (both modes)
|
||||
const excludeInput = await prompt(` ${theme.dim('VM/CT IDs to exclude (comma-separated, or empty):')} `);
|
||||
if (excludeInput.trim()) {
|
||||
newAction.proxmoxExcludeIds = excludeInput.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
||||
}
|
||||
|
||||
const timeoutInput = await prompt(` ${theme.dim('VM shutdown timeout in seconds')} ${theme.dim('[120]:')} `);
|
||||
const stopTimeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(stopTimeout)) {
|
||||
newAction.proxmoxStopTimeout = stopTimeout;
|
||||
}
|
||||
|
||||
const forceInput = await prompt(` ${theme.dim('Force-stop VMs that don\'t shut down in time?')} ${theme.dim('(Y/n):')} `);
|
||||
newAction.proxmoxForceStop = forceInput.toLowerCase() !== 'n';
|
||||
} else {
|
||||
logger.error('Invalid action type.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Battery threshold (all action types)
|
||||
logger.log('');
|
||||
const batteryStr = await prompt(
|
||||
` ${theme.dim('Battery threshold')} ${theme.dim('(%):')} `,
|
||||
);
|
||||
@@ -89,6 +225,8 @@ export class ActionHandler {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
newAction.thresholds = { battery, runtime };
|
||||
|
||||
// Trigger mode
|
||||
logger.log('');
|
||||
logger.log(` ${theme.dim('Trigger mode:')}`);
|
||||
@@ -113,33 +251,13 @@ export class ActionHandler {
|
||||
'': 'onlyThresholds', // Default
|
||||
};
|
||||
const triggerMode = triggerModeMap[triggerChoice] || 'onlyThresholds';
|
||||
|
||||
// Shutdown delay
|
||||
const delayStr = await prompt(
|
||||
` ${theme.dim('Shutdown delay')} ${theme.dim('(seconds) [5]:')} `,
|
||||
);
|
||||
const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5;
|
||||
if (isNaN(shutdownDelay) || shutdownDelay < 0) {
|
||||
logger.error('Invalid shutdown delay. Must be >= 0.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the action
|
||||
const newAction: IActionConfig = {
|
||||
type,
|
||||
thresholds: {
|
||||
battery,
|
||||
runtime,
|
||||
},
|
||||
triggerMode: triggerMode as IActionConfig['triggerMode'],
|
||||
shutdownDelay,
|
||||
};
|
||||
newAction.triggerMode = triggerMode as IActionConfig['triggerMode'];
|
||||
|
||||
// Add to target (UPS or group)
|
||||
if (!target!.actions) {
|
||||
target!.actions = [];
|
||||
}
|
||||
target!.actions.push(newAction);
|
||||
target!.actions.push(newAction as IActionConfig);
|
||||
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
@@ -350,11 +468,19 @@ export class ActionHandler {
|
||||
];
|
||||
|
||||
const rows = target.actions.map((action, index) => {
|
||||
let details = `${action.shutdownDelay || 5}s delay`;
|
||||
let details = `${action.shutdownDelay || 5}min delay`;
|
||||
if (action.type === 'proxmox') {
|
||||
const host = action.proxmoxHost || 'localhost';
|
||||
const port = action.proxmoxPort || 8006;
|
||||
details = `${host}:${port}`;
|
||||
const mode = action.proxmoxMode || 'auto';
|
||||
if (mode === 'cli' || (mode === 'auto' && !action.proxmoxTokenId)) {
|
||||
details = 'CLI mode';
|
||||
} else {
|
||||
const host = action.proxmoxHost || 'localhost';
|
||||
const port = action.proxmoxPort || 8006;
|
||||
details = `API ${host}:${port}`;
|
||||
}
|
||||
if (action.proxmoxExcludeIds?.length) {
|
||||
details += `, excl: ${action.proxmoxExcludeIds.join(',')}`;
|
||||
}
|
||||
} else if (action.type === 'webhook') {
|
||||
details = action.webhookUrl || theme.dim('N/A');
|
||||
} else if (action.type === 'script') {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { IUpsdConfig } from '../upsd/types.ts';
|
||||
import type { TProtocol } from '../protocol/types.ts';
|
||||
import type { INupstConfig, IUpsConfig } from '../daemon.ts';
|
||||
import type { IActionConfig } from '../actions/base-action.ts';
|
||||
import { ProxmoxAction } from '../actions/proxmox-action.ts';
|
||||
import { UPSD } from '../constants.ts';
|
||||
|
||||
/**
|
||||
@@ -1184,37 +1185,58 @@ export class UpsHandler {
|
||||
// Proxmox action
|
||||
action.type = 'proxmox';
|
||||
|
||||
logger.log('');
|
||||
logger.info('Proxmox API Settings:');
|
||||
logger.dim('Requires a Proxmox API token. Create one with:');
|
||||
logger.dim(' pveum user token add root@pam nupst --privsep=0');
|
||||
// Auto-detect CLI availability
|
||||
const detection = ProxmoxAction.detectCliAvailability();
|
||||
|
||||
const pxHost = await prompt('Proxmox Host [localhost]: ');
|
||||
action.proxmoxHost = pxHost.trim() || 'localhost';
|
||||
if (detection.available) {
|
||||
logger.log('');
|
||||
logger.success('Proxmox CLI tools detected (qm/pct). No API token needed.');
|
||||
logger.dim(` qm: ${detection.qmPath}`);
|
||||
logger.dim(` pct: ${detection.pctPath}`);
|
||||
action.proxmoxMode = 'cli';
|
||||
} else {
|
||||
logger.log('');
|
||||
if (!detection.isRoot) {
|
||||
logger.warn('Not running as root - CLI mode unavailable, using API mode.');
|
||||
} else {
|
||||
logger.warn('Proxmox CLI tools (qm/pct) not found - using API mode.');
|
||||
}
|
||||
logger.log('');
|
||||
logger.info('Proxmox API Settings:');
|
||||
logger.dim('Create a token with: pveum user token add root@pam nupst --privsep=0');
|
||||
|
||||
const pxPortInput = await prompt('Proxmox API Port [8006]: ');
|
||||
const pxPort = parseInt(pxPortInput, 10);
|
||||
action.proxmoxPort = pxPortInput.trim() && !isNaN(pxPort) ? pxPort : 8006;
|
||||
const pxHost = await prompt('Proxmox Host [localhost]: ');
|
||||
action.proxmoxHost = pxHost.trim() || 'localhost';
|
||||
|
||||
const pxNode = await prompt('Proxmox Node Name (empty = auto-detect via hostname): ');
|
||||
if (pxNode.trim()) {
|
||||
action.proxmoxNode = pxNode.trim();
|
||||
const pxPortInput = await prompt('Proxmox API Port [8006]: ');
|
||||
const pxPort = parseInt(pxPortInput, 10);
|
||||
action.proxmoxPort = pxPortInput.trim() && !isNaN(pxPort) ? pxPort : 8006;
|
||||
|
||||
const pxNode = await prompt('Proxmox Node Name (empty = auto-detect via hostname): ');
|
||||
if (pxNode.trim()) {
|
||||
action.proxmoxNode = pxNode.trim();
|
||||
}
|
||||
|
||||
const tokenId = await prompt('API Token ID (e.g., root@pam!nupst): ');
|
||||
if (!tokenId.trim()) {
|
||||
logger.warn('Token ID is required for API mode, skipping');
|
||||
continue;
|
||||
}
|
||||
action.proxmoxTokenId = tokenId.trim();
|
||||
|
||||
const tokenSecret = await prompt('API Token Secret: ');
|
||||
if (!tokenSecret.trim()) {
|
||||
logger.warn('Token Secret is required for API mode, skipping');
|
||||
continue;
|
||||
}
|
||||
action.proxmoxTokenSecret = tokenSecret.trim();
|
||||
|
||||
const insecureInput = await prompt('Skip TLS verification (self-signed cert)? (Y/n): ');
|
||||
action.proxmoxInsecure = insecureInput.toLowerCase() !== 'n';
|
||||
action.proxmoxMode = 'api';
|
||||
}
|
||||
|
||||
const tokenId = await prompt('API Token ID (e.g., root@pam!nupst): ');
|
||||
if (!tokenId.trim()) {
|
||||
logger.warn('Token ID is required for Proxmox action, skipping');
|
||||
continue;
|
||||
}
|
||||
action.proxmoxTokenId = tokenId.trim();
|
||||
|
||||
const tokenSecret = await prompt('API Token Secret: ');
|
||||
if (!tokenSecret.trim()) {
|
||||
logger.warn('Token Secret is required for Proxmox action, skipping');
|
||||
continue;
|
||||
}
|
||||
action.proxmoxTokenSecret = tokenSecret.trim();
|
||||
|
||||
// Common Proxmox settings (both modes)
|
||||
const excludeInput = await prompt('VM/CT IDs to exclude (comma-separated, or empty): ');
|
||||
if (excludeInput.trim()) {
|
||||
action.proxmoxExcludeIds = excludeInput.split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n));
|
||||
@@ -1229,9 +1251,6 @@ export class UpsHandler {
|
||||
const forceInput = await prompt('Force-stop VMs that don\'t shut down in time? (Y/n): ');
|
||||
action.proxmoxForceStop = forceInput.toLowerCase() !== 'n';
|
||||
|
||||
const insecureInput = await prompt('Skip TLS verification (self-signed cert)? (Y/n): ');
|
||||
action.proxmoxInsecure = insecureInput.toLowerCase() !== 'n';
|
||||
|
||||
logger.log('');
|
||||
logger.info('Note: Place the Proxmox action BEFORE the shutdown action');
|
||||
logger.dim('in the action chain so VMs shut down before the host.');
|
||||
|
||||
@@ -157,6 +157,9 @@ export const PROXMOX = {
|
||||
|
||||
/** Proxmox API base path */
|
||||
API_BASE: '/api2/json',
|
||||
|
||||
/** Common paths to search for Proxmox CLI tools (qm, pct) */
|
||||
CLI_TOOL_PATHS: ['/usr/sbin', '/usr/bin', '/sbin', '/bin'] as readonly string[],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user