feat(daemon): Add UPSD (NUT) protocol support, Proxmox VM shutdown action, pause/resume monitoring, and network-loss/unreachable handling; bump config version to 4.2
This commit is contained in:
@@ -5,8 +5,11 @@ import { type ITableColumn, logger } from '../logger.ts';
|
||||
import { theme } from '../colors.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus, TUpsModel } from '../snmp/types.ts';
|
||||
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 { UPSD } from '../constants.ts';
|
||||
|
||||
/**
|
||||
* Thresholds configuration for CLI display
|
||||
@@ -89,31 +92,46 @@ export class UpsHandler {
|
||||
const upsId = helpers.shortId();
|
||||
const name = await prompt('UPS Name: ');
|
||||
|
||||
// Select protocol
|
||||
logger.log('');
|
||||
logger.info('Communication Protocol:');
|
||||
logger.dim(' 1) SNMP (network UPS with SNMP agent)');
|
||||
logger.dim(' 2) UPSD/NIS (local NUT server, e.g. USB-connected UPS)');
|
||||
const protocolInput = await prompt('Select protocol [1]: ');
|
||||
const protocolChoice = parseInt(protocolInput, 10) || 1;
|
||||
const protocol: TProtocol = protocolChoice === 2 ? 'upsd' : 'snmp';
|
||||
|
||||
// Create a new UPS configuration object with defaults
|
||||
const newUps = {
|
||||
const newUps: Record<string, unknown> & { id: string; name: string; groups: string[]; actions: IActionConfig[]; protocol: TProtocol; snmp?: ISnmpConfig; upsd?: IUpsdConfig } = {
|
||||
id: upsId,
|
||||
name: name || `UPS-${upsId}`,
|
||||
snmp: {
|
||||
protocol,
|
||||
groups: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
if (protocol === 'snmp') {
|
||||
newUps.snmp = {
|
||||
host: '127.0.0.1',
|
||||
port: 161,
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
upsModel: 'cyberpower' as TUpsModel,
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60,
|
||||
runtime: 20,
|
||||
},
|
||||
groups: [],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
// Gather SNMP settings
|
||||
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
||||
|
||||
// Gather UPS model settings
|
||||
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
||||
};
|
||||
// Gather SNMP settings
|
||||
await this.gatherSnmpSettings(newUps.snmp, prompt);
|
||||
// Gather UPS model settings
|
||||
await this.gatherUpsModelSettings(newUps.snmp, prompt);
|
||||
} else {
|
||||
newUps.upsd = {
|
||||
host: '127.0.0.1',
|
||||
port: UPSD.DEFAULT_PORT,
|
||||
upsName: UPSD.DEFAULT_UPS_NAME,
|
||||
timeout: UPSD.DEFAULT_TIMEOUT_MS,
|
||||
};
|
||||
await this.gatherUpsdSettings(newUps.upsd, prompt);
|
||||
}
|
||||
|
||||
// Get access to GroupHandler for group assignments
|
||||
const groupHandler = this.nupst.getGroupHandler();
|
||||
@@ -132,10 +150,14 @@ export class UpsHandler {
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config as INupstConfig);
|
||||
|
||||
this.displayUpsConfigSummary(newUps);
|
||||
this.displayUpsConfigSummary(newUps as unknown as IUpsConfig);
|
||||
|
||||
// Test the connection if requested
|
||||
await this.optionallyTestConnection(newUps.snmp, prompt);
|
||||
if (protocol === 'snmp' && newUps.snmp) {
|
||||
await this.optionallyTestConnection(newUps.snmp as ISnmpConfig, prompt);
|
||||
} else if (protocol === 'upsd' && newUps.upsd) {
|
||||
await this.optionallyTestUpsdConnection(newUps.upsd, prompt);
|
||||
}
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
await this.restartServiceIfRunning();
|
||||
@@ -232,11 +254,51 @@ export class UpsHandler {
|
||||
upsToEdit.name = newName;
|
||||
}
|
||||
|
||||
// Edit SNMP settings
|
||||
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
||||
// Show current protocol and allow changing
|
||||
const currentProtocol = upsToEdit.protocol || 'snmp';
|
||||
logger.log('');
|
||||
logger.info(`Current Protocol: ${currentProtocol.toUpperCase()}`);
|
||||
logger.dim(' 1) SNMP (network UPS with SNMP agent)');
|
||||
logger.dim(' 2) UPSD/NIS (local NUT server, e.g. USB-connected UPS)');
|
||||
const protocolInput = await prompt(`Select protocol [${currentProtocol === 'upsd' ? '2' : '1'}]: `);
|
||||
const protocolChoice = parseInt(protocolInput, 10);
|
||||
if (protocolChoice === 2) {
|
||||
upsToEdit.protocol = 'upsd';
|
||||
} else if (protocolChoice === 1) {
|
||||
upsToEdit.protocol = 'snmp';
|
||||
}
|
||||
// else keep current
|
||||
|
||||
// Edit UPS model settings
|
||||
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
||||
const editProtocol = upsToEdit.protocol || 'snmp';
|
||||
|
||||
if (editProtocol === 'snmp') {
|
||||
// Initialize SNMP config if switching from UPSD
|
||||
if (!upsToEdit.snmp) {
|
||||
upsToEdit.snmp = {
|
||||
host: '127.0.0.1',
|
||||
port: 161,
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
upsModel: 'cyberpower' as TUpsModel,
|
||||
};
|
||||
}
|
||||
// Edit SNMP settings
|
||||
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
|
||||
// Edit UPS model settings
|
||||
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
|
||||
} else {
|
||||
// Initialize UPSD config if switching from SNMP
|
||||
if (!upsToEdit.upsd) {
|
||||
upsToEdit.upsd = {
|
||||
host: '127.0.0.1',
|
||||
port: UPSD.DEFAULT_PORT,
|
||||
upsName: UPSD.DEFAULT_UPS_NAME,
|
||||
timeout: UPSD.DEFAULT_TIMEOUT_MS,
|
||||
};
|
||||
}
|
||||
await this.gatherUpsdSettings(upsToEdit.upsd, prompt);
|
||||
}
|
||||
|
||||
// Get access to GroupHandler for group assignments
|
||||
const groupHandler = this.nupst.getGroupHandler();
|
||||
@@ -260,7 +322,11 @@ export class UpsHandler {
|
||||
this.displayUpsConfigSummary(upsToEdit);
|
||||
|
||||
// Test the connection if requested
|
||||
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
||||
if (editProtocol === 'snmp' && upsToEdit.snmp) {
|
||||
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
|
||||
} else if (editProtocol === 'upsd' && upsToEdit.upsd) {
|
||||
await this.optionallyTestUpsdConnection(upsToEdit.upsd, prompt);
|
||||
}
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
await this.restartServiceIfRunning();
|
||||
@@ -397,17 +463,31 @@ export class UpsHandler {
|
||||
}
|
||||
|
||||
// Prepare table data
|
||||
const rows = config.upsDevices.map((ups) => ({
|
||||
id: ups.id,
|
||||
name: ups.name || '',
|
||||
host: `${ups.snmp.host}:${ups.snmp.port}`,
|
||||
model: ups.snmp.upsModel || 'cyberpower',
|
||||
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||
}));
|
||||
const rows = config.upsDevices.map((ups) => {
|
||||
const protocol = ups.protocol || 'snmp';
|
||||
let host = 'N/A';
|
||||
let model = '';
|
||||
if (protocol === 'upsd' && ups.upsd) {
|
||||
host = `${ups.upsd.host}:${ups.upsd.port}`;
|
||||
model = `NUT:${ups.upsd.upsName}`;
|
||||
} else if (ups.snmp) {
|
||||
host = `${ups.snmp.host}:${ups.snmp.port}`;
|
||||
model = ups.snmp.upsModel || 'cyberpower';
|
||||
}
|
||||
return {
|
||||
id: ups.id,
|
||||
name: ups.name || '',
|
||||
protocol: protocol.toUpperCase(),
|
||||
host,
|
||||
model,
|
||||
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
|
||||
};
|
||||
});
|
||||
|
||||
const columns: ITableColumn[] = [
|
||||
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
|
||||
{ header: 'Name', key: 'name', align: 'left' },
|
||||
{ header: 'Protocol', key: 'protocol', align: 'left' },
|
||||
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||
{ header: 'Model', key: 'model', align: 'left' },
|
||||
{ header: 'Groups', key: 'groups', align: 'left' },
|
||||
@@ -482,58 +562,71 @@ export class UpsHandler {
|
||||
// Type guard: IUpsConfig has 'id' and 'name' at root level, INupstConfig doesn't
|
||||
const isUpsConfig = 'id' in config && 'name' in config;
|
||||
|
||||
// Get SNMP config and other values based on config type
|
||||
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||
? (config as IUpsConfig).snmp
|
||||
: (config as INupstConfig).snmp;
|
||||
const checkInterval = isUpsConfig ? 30000 : (config as INupstConfig).checkInterval || 30000;
|
||||
const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS';
|
||||
const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default';
|
||||
const protocol = isUpsConfig ? ((config as IUpsConfig).protocol || 'snmp') : 'snmp';
|
||||
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
||||
logger.logBoxLine(`UPS ID: ${upsId}`);
|
||||
logger.logBoxLine(`Protocol: ${protocol.toUpperCase()}`);
|
||||
|
||||
if (!snmpConfig) {
|
||||
logger.logBoxLine('SNMP Settings: Not configured');
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
if (protocol === 'upsd' && isUpsConfig && (config as IUpsConfig).upsd) {
|
||||
const upsdConfig = (config as IUpsConfig).upsd!;
|
||||
logger.logBoxLine('UPSD/NIS Settings:');
|
||||
logger.logBoxLine(` Host: ${upsdConfig.host}`);
|
||||
logger.logBoxLine(` Port: ${upsdConfig.port}`);
|
||||
logger.logBoxLine(` UPS Name: ${upsdConfig.upsName}`);
|
||||
logger.logBoxLine(` Timeout: ${upsdConfig.timeout / 1000} seconds`);
|
||||
if (upsdConfig.username) {
|
||||
logger.logBoxLine(` Auth: ${upsdConfig.username}`);
|
||||
}
|
||||
} else {
|
||||
// SNMP display
|
||||
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||
? (config as IUpsConfig).snmp
|
||||
: (config as INupstConfig).snmp;
|
||||
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(` Host: ${snmpConfig.host}`);
|
||||
logger.logBoxLine(` Port: ${snmpConfig.port}`);
|
||||
logger.logBoxLine(` Version: ${snmpConfig.version}`);
|
||||
logger.logBoxLine(` UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
||||
logger.logBoxLine(` Community: ${snmpConfig.community}`);
|
||||
} else if (snmpConfig.version === 3) {
|
||||
logger.logBoxLine(` Security Level: ${snmpConfig.securityLevel}`);
|
||||
logger.logBoxLine(` Username: ${snmpConfig.username}`);
|
||||
|
||||
// Show auth and privacy details based on security level
|
||||
if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') {
|
||||
logger.logBoxLine(` Auth Protocol: ${snmpConfig.authProtocol || 'None'}`);
|
||||
if (!snmpConfig) {
|
||||
logger.logBoxLine('SNMP Settings: Not configured');
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (snmpConfig.securityLevel === 'authPriv') {
|
||||
logger.logBoxLine(` Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(` Host: ${snmpConfig.host}`);
|
||||
logger.logBoxLine(` Port: ${snmpConfig.port}`);
|
||||
logger.logBoxLine(` Version: ${snmpConfig.version}`);
|
||||
logger.logBoxLine(` UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`);
|
||||
|
||||
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
|
||||
logger.logBoxLine(` Community: ${snmpConfig.community}`);
|
||||
} else if (snmpConfig.version === 3) {
|
||||
logger.logBoxLine(` Security Level: ${snmpConfig.securityLevel}`);
|
||||
logger.logBoxLine(` Username: ${snmpConfig.username}`);
|
||||
|
||||
if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') {
|
||||
logger.logBoxLine(` Auth Protocol: ${snmpConfig.authProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
if (snmpConfig.securityLevel === 'authPriv') {
|
||||
logger.logBoxLine(` Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`);
|
||||
}
|
||||
|
||||
logger.logBoxLine(` Timeout: ${snmpConfig.timeout / 1000} seconds`);
|
||||
}
|
||||
|
||||
// Show timeout value
|
||||
logger.logBoxLine(` Timeout: ${snmpConfig.timeout / 1000} seconds`);
|
||||
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
||||
logger.logBoxLine('Custom OIDs:');
|
||||
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show OIDs if custom model is selected
|
||||
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
|
||||
logger.logBoxLine('Custom OIDs:');
|
||||
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
// Show group assignments if this is a UPS config
|
||||
if (isUpsConfig) {
|
||||
const groups = (config as IUpsConfig).groups;
|
||||
@@ -555,25 +648,36 @@ export class UpsHandler {
|
||||
const isUpsConfig = 'id' in config && 'name' in config;
|
||||
const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default';
|
||||
const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS';
|
||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
||||
const protocol = isUpsConfig ? ((config as IUpsConfig).protocol || 'snmp') : 'snmp';
|
||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId}) via ${protocol.toUpperCase()}...`);
|
||||
|
||||
try {
|
||||
// Get SNMP config based on config type
|
||||
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||
? (config as IUpsConfig).snmp
|
||||
: (config as INupstConfig).snmp;
|
||||
let status: ISnmpUpsStatus;
|
||||
|
||||
if (!snmpConfig) {
|
||||
throw new Error('SNMP configuration not found');
|
||||
if (protocol === 'upsd' && isUpsConfig && (config as IUpsConfig).upsd) {
|
||||
const upsdConfig = (config as IUpsConfig).upsd!;
|
||||
const testConfig = {
|
||||
...upsdConfig,
|
||||
timeout: Math.min(upsdConfig.timeout, 10000),
|
||||
};
|
||||
status = await this.nupst.getUpsd().getUpsStatus(testConfig);
|
||||
} else {
|
||||
// SNMP protocol
|
||||
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||
? (config as IUpsConfig).snmp
|
||||
: (config as INupstConfig).snmp;
|
||||
|
||||
if (!snmpConfig) {
|
||||
throw new Error('SNMP configuration not found');
|
||||
}
|
||||
|
||||
const testConfig: ISnmpConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000),
|
||||
};
|
||||
status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
||||
}
|
||||
|
||||
const testConfig: ISnmpConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||
};
|
||||
|
||||
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
|
||||
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle(`Connection Successful: ${upsName}`, boxWidth);
|
||||
logger.logBoxLine('UPS Status:');
|
||||
@@ -872,6 +976,97 @@ export class UpsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather UPSD/NIS connection settings
|
||||
* @param upsdConfig UPSD configuration object to update
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async gatherUpsdSettings(
|
||||
upsdConfig: IUpsdConfig,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
logger.log('');
|
||||
logger.info('UPSD/NIS Connection Settings:');
|
||||
logger.dim('Connect to a local NUT (Network UPS Tools) server');
|
||||
|
||||
// Host
|
||||
const defaultHost = upsdConfig.host || '127.0.0.1';
|
||||
const host = await prompt(`UPSD Host [${defaultHost}]: `);
|
||||
upsdConfig.host = host.trim() || defaultHost;
|
||||
|
||||
// Port
|
||||
const defaultPort = upsdConfig.port || UPSD.DEFAULT_PORT;
|
||||
const portInput = await prompt(`UPSD Port [${defaultPort}]: `);
|
||||
const port = parseInt(portInput, 10);
|
||||
upsdConfig.port = portInput.trim() && !isNaN(port) ? port : defaultPort;
|
||||
|
||||
// UPS Name
|
||||
const defaultUpsName = upsdConfig.upsName || UPSD.DEFAULT_UPS_NAME;
|
||||
const upsName = await prompt(`NUT UPS Name [${defaultUpsName}]: `);
|
||||
upsdConfig.upsName = upsName.trim() || defaultUpsName;
|
||||
|
||||
// Timeout
|
||||
const defaultTimeout = (upsdConfig.timeout || UPSD.DEFAULT_TIMEOUT_MS) / 1000;
|
||||
const timeoutInput = await prompt(`Timeout in seconds [${defaultTimeout}]: `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
upsdConfig.timeout = timeout * 1000;
|
||||
}
|
||||
|
||||
// Authentication (optional)
|
||||
logger.log('');
|
||||
logger.info('Authentication (optional):');
|
||||
logger.dim('Leave blank if your NUT server does not require authentication');
|
||||
const username = await prompt(`Username [${upsdConfig.username || ''}]: `);
|
||||
if (username.trim()) {
|
||||
upsdConfig.username = username.trim();
|
||||
const password = await prompt(`Password: `);
|
||||
if (password.trim()) {
|
||||
upsdConfig.password = password.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally test UPSD connection
|
||||
* @param upsdConfig UPSD configuration to test
|
||||
* @param prompt Function to prompt for user input
|
||||
*/
|
||||
private async optionallyTestUpsdConnection(
|
||||
upsdConfig: IUpsdConfig,
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
const testConnection = await prompt(
|
||||
'Would you like to test the connection to your UPS? (y/N): ',
|
||||
);
|
||||
if (testConnection.toLowerCase() === 'y') {
|
||||
logger.log('\nTesting connection to UPSD server...');
|
||||
try {
|
||||
const testConfig = {
|
||||
...upsdConfig,
|
||||
timeout: Math.min(upsdConfig.timeout, 10000),
|
||||
};
|
||||
|
||||
const status = await this.nupst.getUpsd().getUpsStatus(testConfig);
|
||||
const boxWidth = 45;
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Connection Successful!', boxWidth);
|
||||
logger.logBoxLine('UPS Status:');
|
||||
logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
const errorBoxWidth = 45;
|
||||
logger.log('');
|
||||
logger.logBoxTitle('Connection Failed!', errorBoxWidth);
|
||||
logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxEnd();
|
||||
logger.log('\nPlease check your NUT server settings and try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather action configuration settings
|
||||
* @param actions Actions array to configure
|
||||
@@ -901,6 +1096,7 @@ export class UpsHandler {
|
||||
logger.dim(' 1) Shutdown (system shutdown)');
|
||||
logger.dim(' 2) Webhook (HTTP notification)');
|
||||
logger.dim(' 3) Custom Script (run .sh file from /etc/nupst)');
|
||||
logger.dim(' 4) Proxmox (gracefully shut down VMs/LXCs before host shutdown)');
|
||||
|
||||
const typeInput = await prompt('Select action type [1]: ');
|
||||
const typeValue = parseInt(typeInput, 10) || 1;
|
||||
@@ -955,6 +1151,61 @@ export class UpsHandler {
|
||||
if (timeoutInput.trim() && !isNaN(timeout)) {
|
||||
action.scriptTimeout = timeout * 1000; // Convert to ms
|
||||
}
|
||||
} else if (typeValue === 4) {
|
||||
// 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');
|
||||
|
||||
const pxHost = await prompt('Proxmox Host [localhost]: ');
|
||||
action.proxmoxHost = pxHost.trim() || 'localhost';
|
||||
|
||||
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 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();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const timeoutInput = await prompt('VM shutdown timeout in seconds [120]: ');
|
||||
const stopTimeout = parseInt(timeoutInput, 10);
|
||||
if (timeoutInput.trim() && !isNaN(stopTimeout)) {
|
||||
action.proxmoxStopTimeout = stopTimeout;
|
||||
}
|
||||
|
||||
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.');
|
||||
} else {
|
||||
logger.warn('Invalid action type, skipping');
|
||||
continue;
|
||||
@@ -1032,12 +1283,20 @@ export class UpsHandler {
|
||||
*/
|
||||
private displayUpsConfigSummary(ups: IUpsConfig): void {
|
||||
const boxWidth = 45;
|
||||
const protocol = ups.protocol || 'snmp';
|
||||
logger.log('');
|
||||
logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`UPS ID: ${ups.id}`);
|
||||
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||
logger.logBoxLine(`Protocol: ${protocol.toUpperCase()}`);
|
||||
|
||||
if (protocol === 'upsd' && ups.upsd) {
|
||||
logger.logBoxLine(`UPSD Host: ${ups.upsd.host}:${ups.upsd.port}`);
|
||||
logger.logBoxLine(`NUT UPS Name: ${ups.upsd.upsName}`);
|
||||
} else if (ups.snmp) {
|
||||
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||
}
|
||||
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
||||
|
||||
Reference in New Issue
Block a user