Release: alert hygiene batch + UPS shutdown chain (dry-run) + restore runbook #3
+16
-5
@@ -79,14 +79,25 @@
|
|||||||
- sensor.pfv_ups_runtime_minutes
|
- sensor.pfv_ups_runtime_minutes
|
||||||
- sensor.pfv_tripp_lite_runtime_minutes
|
- sensor.pfv_tripp_lite_runtime_minutes
|
||||||
below: 10
|
below: 10
|
||||||
|
# 5-min level re-check: numeric_state alone fires only on the threshold
|
||||||
|
# CROSSING — a blip at that minute (or an HA restart mid-outage) would
|
||||||
|
# silence the page for the whole discharge. Level-based polling turns
|
||||||
|
# this into an escalation ladder while the outage persists.
|
||||||
|
- trigger: time_pattern
|
||||||
|
minutes: /5
|
||||||
conditions:
|
conditions:
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: >-
|
value_template: >-
|
||||||
{% set status_map = {
|
{% set ns = namespace(hit=false) %}
|
||||||
'sensor.pfv_ups_runtime_minutes': 'sensor.tsys1_ups_status',
|
{% for rt, st in [
|
||||||
'sensor.pfv_tripp_lite_runtime_minutes': 'sensor.tsys1_triplite_status'} %}
|
('sensor.pfv_ups_runtime_minutes', 'sensor.tsys1_ups_status'),
|
||||||
{{ states(status_map.get(trigger.entity_id, 'sensor.tsys1_ups_status'))
|
('sensor.pfv_tripp_lite_runtime_minutes', 'sensor.tsys1_triplite_status')] %}
|
||||||
not in ['Online', 'OL', 'unknown', 'unavailable'] }}
|
{% if is_number(states(rt)) and (states(rt) | float) < 10
|
||||||
|
and states(st) not in ['Online', 'OL', 'unknown', 'unavailable'] %}
|
||||||
|
{% set ns.hit = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ ns.hit }}
|
||||||
actions:
|
actions:
|
||||||
- action: notify.send_message
|
- action: notify.send_message
|
||||||
target:
|
target:
|
||||||
|
|||||||
+38
-13
@@ -20,6 +20,10 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
input_boolean:
|
input_boolean:
|
||||||
|
# NOTE: `initial:` wins over restored state at EVERY HA start — a deploy
|
||||||
|
# mid-test silently reverts these to the safe defaults (armed OFF,
|
||||||
|
# dry-run ON). Safe direction, but re-arm after any restart during the
|
||||||
|
# supervised Sept 11-12 live test.
|
||||||
pfv_ups_shutdown_armed:
|
pfv_ups_shutdown_armed:
|
||||||
name: UPS shutdown chain ARMED
|
name: UPS shutdown chain ARMED
|
||||||
description: Founder-level arm. ON + dry-run OFF = real shutdown dispatch.
|
description: Founder-level arm. ON + dry-run OFF = real shutdown dispatch.
|
||||||
@@ -61,26 +65,39 @@ automation:
|
|||||||
- sensor.tsys1_ups_battery_charge
|
- sensor.tsys1_ups_battery_charge
|
||||||
- sensor.tsys1_triplite_battery_charge
|
- sensor.tsys1_triplite_battery_charge
|
||||||
below: 50
|
below: 50
|
||||||
|
# 5-min level re-check — same edge-vs-level reasoning as the
|
||||||
|
# runtime-low alert: never let a threshold-crossing blip silence
|
||||||
|
# the plan page for an entire discharge.
|
||||||
|
- trigger: time_pattern
|
||||||
|
minutes: /5
|
||||||
conditions:
|
conditions:
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: >-
|
value_template: >-
|
||||||
{% set sm = {
|
{% set ns = namespace(hit=false) %}
|
||||||
'sensor.pfv_ups_runtime_minutes': 'sensor.tsys1_ups_status',
|
{% for rt, st in [
|
||||||
'sensor.pfv_tripp_lite_runtime_minutes': 'sensor.tsys1_triplite_status',
|
('sensor.pfv_ups_runtime_minutes', 'sensor.tsys1_ups_status'),
|
||||||
'sensor.tsys1_ups_battery_charge': 'sensor.tsys1_ups_status',
|
('sensor.pfv_tripp_lite_runtime_minutes', 'sensor.tsys1_triplite_status')] %}
|
||||||
'sensor.tsys1_triplite_battery_charge': 'sensor.tsys1_triplite_status'} %}
|
{% if is_number(states(rt)) and (states(rt) | float) < 20
|
||||||
{{ states(sm.get(trigger.entity_id, 'sensor.tsys1_ups_status'))
|
and states(st) not in ['Online', 'OL', 'unknown', 'unavailable'] %}
|
||||||
not in ['Online', 'OL', 'unknown', 'unavailable'] }}
|
{% set ns.hit = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% for ch, st in [
|
||||||
|
('sensor.tsys1_ups_battery_charge', 'sensor.tsys1_ups_status'),
|
||||||
|
('sensor.tsys1_triplite_battery_charge', 'sensor.tsys1_triplite_status')] %}
|
||||||
|
{% if is_number(states(ch)) and (states(ch) | float) < 50
|
||||||
|
and states(st) not in ['Online', 'OL', 'unknown', 'unavailable'] %}
|
||||||
|
{% set ns.hit = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{{ ns.hit }}
|
||||||
|
# Status 'unavailable' suppresses (anti-noise); UPS COMMS LOST covers
|
||||||
|
# that failure mode separately after 5 min.
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: >-
|
value_template: >-
|
||||||
{{ states('input_datetime.pfv_ups_shutdown_last_page') in ['unknown', 'unavailable', 'none']
|
{{ states('input_datetime.pfv_ups_shutdown_last_page') in ['unknown', 'unavailable', 'none']
|
||||||
or as_timestamp(now()) - as_timestamp(states('input_datetime.pfv_ups_shutdown_last_page')) > 21600 }}
|
or as_timestamp(now()) - as_timestamp(states('input_datetime.pfv_ups_shutdown_last_page')) > 21600 }}
|
||||||
actions:
|
actions:
|
||||||
- action: input_datetime.set_datetime
|
|
||||||
target:
|
|
||||||
entity_id: input_datetime.pfv_ups_shutdown_last_page
|
|
||||||
data:
|
|
||||||
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
|
|
||||||
- variables:
|
- variables:
|
||||||
execute_mode: >-
|
execute_mode: >-
|
||||||
{{ is_state('input_boolean.pfv_ups_shutdown_armed', 'on')
|
{{ is_state('input_boolean.pfv_ups_shutdown_armed', 'on')
|
||||||
@@ -97,7 +114,8 @@ automation:
|
|||||||
(ca LAST — signing path for everything above)
|
(ca LAST — signing path for everything above)
|
||||||
5. pfv-bms VM 100 LAST of the VMs — the brain stays up until now
|
5. pfv-bms VM 100 LAST of the VMs — the brain stays up until now
|
||||||
6. Hypervisors: iDRAC graceful off tsys6/tsys7 (PowerEdge);
|
6. Hypervisors: iDRAC graceful off tsys6/tsys7 (PowerEdge);
|
||||||
tsys1/3/4/5/9 idle out on UPS exhaustion (OptiPlex/Precision)
|
tsys1, tsys3, tsys4, tsys5, tsys9 idle out on UPS exhaustion
|
||||||
|
(OptiPlex/Precision, no BMC)
|
||||||
mode_note: >-
|
mode_note: >-
|
||||||
{{ 'MODE: EXECUTE (armed, live dispatch)' if execute_mode
|
{{ 'MODE: EXECUTE (armed, live dispatch)' if execute_mode
|
||||||
else 'MODE: DRY RUN (plan only — nothing will be shut down)' }}
|
else 'MODE: DRY RUN (plan only — nothing will be shut down)' }}
|
||||||
@@ -122,3 +140,10 @@ automation:
|
|||||||
data:
|
data:
|
||||||
title: "{{ 'UPS SHUTDOWN EXECUTING' if execute_mode else 'UPS SHUTDOWN PLAN (dry run)' }}"
|
title: "{{ 'UPS SHUTDOWN EXECUTING' if execute_mode else 'UPS SHUTDOWN PLAN (dry run)' }}"
|
||||||
message: "{{ plan }}"
|
message: "{{ plan }}"
|
||||||
|
# Suppression stamp set LAST — a failed notify must not suppress the
|
||||||
|
# next 5-min re-page cycle.
|
||||||
|
- action: input_datetime.set_datetime
|
||||||
|
target:
|
||||||
|
entity_id: input_datetime.pfv_ups_shutdown_last_page
|
||||||
|
data:
|
||||||
|
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
|
||||||
|
|||||||
+16
-3
@@ -63,17 +63,30 @@ t "trend sensors fed from smoothed sources" \
|
|||||||
t "runtime-low covers Tripp Lite" \
|
t "runtime-low covers Tripp Lite" \
|
||||||
"sed -n '/id: pfv_plant_ups_runtime_low/,/^ mode:/p' automations.yaml | grep -q 'sensor.pfv_tripp_lite_runtime_minutes'"
|
"sed -n '/id: pfv_plant_ups_runtime_low/,/^ mode:/p' automations.yaml | grep -q 'sensor.pfv_tripp_lite_runtime_minutes'"
|
||||||
t "runtime-low status check maps per-UPS" \
|
t "runtime-low status check maps per-UPS" \
|
||||||
"sed -n '/id: pfv_plant_ups_runtime_low/,/^ mode:/p' automations.yaml | grep -q 'status_map'"
|
"sed -n '/id: pfv_plant_ups_runtime_low/,/^ mode:/p' automations.yaml | grep -q \"sensor.pfv_tripp_lite_runtime_minutes', 'sensor.tsys1_triplite_status'\""
|
||||||
|
|
||||||
echo "=== UPS shutdown chain (#790 — dry-run is the shipped mode) ==="
|
echo "=== UPS shutdown chain (#790 — dry-run is the shipped mode) ==="
|
||||||
t "shutdown chain package present, dry-run default ON" \
|
t "shutdown chain package present, dry-run default ON" \
|
||||||
"grep -q 'id: pfv_ups_shutdown_plan' packages/ups_shutdown.yaml && grep -A4 'pfv_ups_shutdown_dry_run:' packages/ups_shutdown.yaml | grep -q 'initial: true'"
|
"grep -q 'id: pfv_ups_shutdown_plan' packages/ups_shutdown.yaml && grep -A4 'pfv_ups_shutdown_dry_run:' packages/ups_shutdown.yaml | grep -q 'initial: true'"
|
||||||
t "armed switch default OFF" \
|
t "armed switch default OFF" \
|
||||||
"grep -A4 'pfv_ups_shutdown_armed:' packages/ups_shutdown.yaml | grep -q 'initial: false'"
|
"grep -A4 'pfv_ups_shutdown_armed:' packages/ups_shutdown.yaml | grep -q 'initial: false'"
|
||||||
t "plan covers all seven hypervisors" \
|
# Host coverage must hold in the PLAN BODY, not the header comment —
|
||||||
"for h in tsys1 tsys3 tsys4 tsys5 tsys6 tsys7 tsys9; do grep -q \$h packages/ups_shutdown.yaml || exit 1; done"
|
# scope the grep to the plan template variable.
|
||||||
|
PLAN_OK=1
|
||||||
|
for h in tsys1 tsys3 tsys4 tsys5 tsys6 tsys7 tsys9; do
|
||||||
|
# full-read grep (not -q): the script runs with pipefail — -q's early
|
||||||
|
# exit SIGPIPEs sed and flips the pipeline status to 141
|
||||||
|
sed -n '/plan: >-/,/mode_note:/p' packages/ups_shutdown.yaml | grep "$h" >/dev/null || PLAN_OK=0
|
||||||
|
done
|
||||||
|
t "plan covers all seven hypervisors (plan body)" "[ \$PLAN_OK -eq 1 ]"
|
||||||
t "pfv-bms VM 100 ordered last of the VMs (#455)" \
|
t "pfv-bms VM 100 ordered last of the VMs (#455)" \
|
||||||
"grep -q 'pfv-bms VM 100 LAST' packages/ups_shutdown.yaml"
|
"grep -q 'pfv-bms VM 100 LAST' packages/ups_shutdown.yaml"
|
||||||
|
|
||||||
|
echo "=== level-based re-checks (review finding: edge-only triggers miss pages) ==="
|
||||||
|
t "runtime-low re-polls every 5 min" \
|
||||||
|
"sed -n '/id: pfv_plant_ups_runtime_low/,/^ mode:/p' automations.yaml | grep -q 'minutes: /5'"
|
||||||
|
t "shutdown chain re-polls every 5 min" \
|
||||||
|
"grep -A30 'id: pfv_ups_shutdown_plan' packages/ups_shutdown.yaml | grep -q 'minutes: /5'"
|
||||||
|
|
||||||
echo "=== SDLC SUITE: $FAIL failures ==="
|
echo "=== SDLC SUITE: $FAIL failures ==="
|
||||||
exit "$FAIL"
|
exit "$FAIL"
|
||||||
|
|||||||
Reference in New Issue
Block a user