fix(proxmox): require confirmed battery state before running Proxmox actions
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-16 - 5.11.1 - fix(deps)
|
||||||
remove unused smartchangelog dependency
|
remove unused smartchangelog dependency
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -64,6 +64,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsdeno": "^1.3.1"
|
"@git.zone/tsdeno": "^1.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsdeno':
|
'@git.zone/tsdeno':
|
||||||
specifier: ^1.3.1
|
specifier: ^1.4.0
|
||||||
version: 1.3.1
|
version: 1.4.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ packages:
|
|||||||
'@design.estate/dees-element@2.2.4':
|
'@design.estate/dees-element@2.2.4':
|
||||||
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
resolution: {integrity: sha512-O9cA6flBMMd+pBwMQrZXwAWel9yVxgokolb+Em6gvkXxPJ0P/B5UDn4Vc2d4ts3ta55PTBm+l2dPeDVGx/bl7Q==}
|
||||||
|
|
||||||
'@git.zone/tsdeno@1.3.1':
|
'@git.zone/tsdeno@1.4.0':
|
||||||
resolution: {integrity: sha512-1shZOSwMUfmonIe8OsX40EcQcS7/WFt+y+5+TQ7N6QmU+Hp6zjrfFvQwanr5Qcgh/E54WFNTfJk/PsJ26s6Oxw==}
|
resolution: {integrity: sha512-84kFa/uKPTlzeLxtHoFxefk6O9khsWWQ2PCWNbCNYIUqWHUvN9COpGq0GXWtsoxLWPhTTIeHsOX4+O55uT2MPw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@lit-labs/ssr-dom-shim@1.5.1':
|
'@lit-labs/ssr-dom-shim@1.5.1':
|
||||||
@@ -826,7 +826,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsdeno@1.3.1':
|
'@git.zone/tsdeno@1.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/smartcli': 4.0.21
|
'@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_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 { MigrationV4_3ToV4_4 } from '../ts/migrations/migration-v4.3-to-v4.4.ts';
|
||||||
import { ActionHandler } from '../ts/cli/action-handler.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 { renderUpgradeChangelog } from '../ts/upgrade-changelog.ts';
|
||||||
|
|
||||||
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
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 {
|
function createMockContext(overrides: Partial<IActionContext> = {}): IActionContext {
|
||||||
return {
|
return {
|
||||||
upsId: 'test-ups',
|
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
|
// Action Handler Tests
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -30,6 +30,69 @@ export class ProxmoxAction extends Action {
|
|||||||
readonly type = 'proxmox';
|
readonly type = 'proxmox';
|
||||||
private static readonly activeRunKeys = new Set<string>();
|
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 {
|
private static findCliTool(command: string): string | null {
|
||||||
for (const dir of PROXMOX.CLI_TOOL_PATHS) {
|
for (const dir of PROXMOX.CLI_TOOL_PATHS) {
|
||||||
const candidate = `${dir}/${command}`;
|
const candidate = `${dir}/${command}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user