fix(proxmox): require confirmed battery state before running Proxmox actions
This commit is contained in:
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## Pending
|
||||
|
||||
### Fixes
|
||||
|
||||
- require confirmed battery state before running Proxmox actions (proxmox)
|
||||
- Skip Proxmox actions when UPS status is unreachable, unknown, or online
|
||||
- Respect triggerMode for confirmed onBattery power changes and threshold violations
|
||||
- Add tests for Proxmox action execution gating
|
||||
- Bump @git.zone/tsdeno to ^1.4.0
|
||||
|
||||
## 2026-04-16 - 5.11.1 - fix(deps)
|
||||
remove unused smartchangelog dependency
|
||||
|
||||
|
||||
+1
-1
@@ -64,6 +64,6 @@
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"devDependencies": {
|
||||
"@git.zone/tsdeno": "^1.3.1"
|
||||
"@git.zone/tsdeno": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
'@git.zone/tsdeno':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -36,8 +36,8 @@ packages:
|
||||
'@design.estate/dees-element@2.2.4':
|
||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||
|
||||
'@git.zone/tsdeno@1.3.1':
|
||||
resolution: {integrity: sha512-1shZOSwMUfmonIe8OsX40EcQcS7/WFt+y+5+TQ7N6QmU+Hp6zjrfFvQwanr5Qcgh/E54WFNTfJk/PsJ26s6Oxw==}
|
||||
'@git.zone/tsdeno@1.4.0':
|
||||
resolution: {integrity: sha512-84kFa/uKPTlzeLxtHoFxefk6O9khsWWQ2PCWNbCNYIUqWHUvN9COpGq0GXWtsoxLWPhTTIeHsOX4+O55uT2MPw==}
|
||||
hasBin: true
|
||||
|
||||
'@lit-labs/ssr-dom-shim@1.5.1':
|
||||
@@ -826,7 +826,7 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
'@git.zone/tsdeno@1.3.1':
|
||||
'@git.zone/tsdeno@1.4.0':
|
||||
dependencies:
|
||||
'@push.rocks/early': 4.0.4
|
||||
'@push.rocks/smartcli': 4.0.21
|
||||
|
||||
@@ -42,6 +42,7 @@ import { createInitialUpsStatus } from '../ts/ups-status.ts';
|
||||
import { MigrationV4_2ToV4_3 } from '../ts/migrations/migration-v4.2-to-v4.3.ts';
|
||||
import { MigrationV4_3ToV4_4 } from '../ts/migrations/migration-v4.3-to-v4.4.ts';
|
||||
import { ActionHandler } from '../ts/cli/action-handler.ts';
|
||||
import { ProxmoxAction } from '../ts/actions/proxmox-action.ts';
|
||||
import { renderUpgradeChangelog } from '../ts/upgrade-changelog.ts';
|
||||
|
||||
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
||||
@@ -927,6 +928,12 @@ class TestAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
class TestProxmoxAction extends ProxmoxAction {
|
||||
public testShouldExecute(context: IActionContext): boolean {
|
||||
return this.shouldExecute(context);
|
||||
}
|
||||
}
|
||||
|
||||
function createMockContext(overrides: Partial<IActionContext> = {}): IActionContext {
|
||||
return {
|
||||
upsId: 'test-ups',
|
||||
@@ -1063,6 +1070,84 @@ Deno.test('Action.shouldExecute: anyChange mode always returns true', () => {
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('ProxmoxAction.shouldExecute: skips unreachable and unknown UPS status changes', () => {
|
||||
const action = new TestProxmoxAction({ type: 'proxmox' });
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({
|
||||
powerStatus: 'unreachable',
|
||||
previousPowerStatus: 'online',
|
||||
triggerReason: 'powerStatusChange',
|
||||
})),
|
||||
false,
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({
|
||||
powerStatus: 'unknown',
|
||||
previousPowerStatus: 'online',
|
||||
triggerReason: 'powerStatusChange',
|
||||
})),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('ProxmoxAction.shouldExecute: skips thresholds unless UPS is confirmed on battery', () => {
|
||||
const action = new TestProxmoxAction({
|
||||
type: 'proxmox',
|
||||
triggerMode: 'onlyThresholds',
|
||||
thresholds: { battery: 50, runtime: 20 },
|
||||
});
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({
|
||||
powerStatus: 'unreachable',
|
||||
triggerReason: 'thresholdViolation',
|
||||
batteryCapacity: 10,
|
||||
batteryRuntime: 5,
|
||||
})),
|
||||
false,
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
action.testShouldExecute(createMockContext({
|
||||
powerStatus: 'online',
|
||||
triggerReason: 'thresholdViolation',
|
||||
batteryCapacity: 10,
|
||||
batteryRuntime: 5,
|
||||
})),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test('ProxmoxAction.shouldExecute: allows confirmed battery events', () => {
|
||||
const powerChangeAction = new TestProxmoxAction({ type: 'proxmox' });
|
||||
const thresholdAction = new TestProxmoxAction({
|
||||
type: 'proxmox',
|
||||
triggerMode: 'onlyThresholds',
|
||||
thresholds: { battery: 50, runtime: 20 },
|
||||
});
|
||||
|
||||
assertEquals(
|
||||
powerChangeAction.testShouldExecute(createMockContext({
|
||||
powerStatus: 'onBattery',
|
||||
previousPowerStatus: 'online',
|
||||
triggerReason: 'powerStatusChange',
|
||||
})),
|
||||
true,
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
thresholdAction.testShouldExecute(createMockContext({
|
||||
powerStatus: 'onBattery',
|
||||
triggerReason: 'thresholdViolation',
|
||||
batteryCapacity: 10,
|
||||
batteryRuntime: 5,
|
||||
})),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Action Handler Tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -30,6 +30,69 @@ export class ProxmoxAction extends Action {
|
||||
readonly type = 'proxmox';
|
||||
private static readonly activeRunKeys = new Set<string>();
|
||||
|
||||
protected override shouldExecute(context: IActionContext): boolean {
|
||||
const mode = this.config.triggerMode || 'powerChangesAndThresholds';
|
||||
|
||||
if (context.powerStatus !== 'onBattery') {
|
||||
if (context.powerStatus === 'unreachable') {
|
||||
logger.info(
|
||||
'Proxmox action skipped: UPS is unreachable (communication failure, actual state unknown)',
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Proxmox action skipped: UPS is not on battery (status: ${context.powerStatus})`,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.triggerReason === 'thresholdViolation') {
|
||||
if (mode === 'onlyPowerChanges') {
|
||||
logger.info('Proxmox action skipped: triggerMode is onlyPowerChanges, ignoring threshold');
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime);
|
||||
}
|
||||
|
||||
if (context.triggerReason === 'powerStatusChange') {
|
||||
if (mode === 'onlyThresholds') {
|
||||
logger.info('Proxmox action skipped: triggerMode is onlyThresholds, ignoring power change');
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousPowerStatus = context.previousPowerStatus;
|
||||
if (previousPowerStatus === 'online') {
|
||||
logger.info('Proxmox action triggered: power loss detected (online -> onBattery)');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previousPowerStatus === 'unknown') {
|
||||
if (
|
||||
mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' ||
|
||||
mode === 'anyChange'
|
||||
) {
|
||||
logger.info(
|
||||
'Proxmox action triggered: UPS on battery at daemon startup (unknown -> onBattery)',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Proxmox action skipped: non-emergency transition (${previousPowerStatus} -> ${context.powerStatus})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode === 'anyChange') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static findCliTool(command: string): string | null {
|
||||
for (const dir of PROXMOX.CLI_TOOL_PATHS) {
|
||||
const candidate = `${dir}/${command}`;
|
||||
|
||||
Reference in New Issue
Block a user