kubernetes/apps/miroir-system/miroir-medic/app/configmap.yaml
1
---
2
apiVersion: v1
3
kind: ConfigMap
4
metadata:
5
name: miroir-medic
6
data:
7
medic.sh: |
8
#!/bin/sh
9
set -u
11
PERSISTENT_STATE=/var/lib/miroir-medic
12
BOOT_STATE=/run/miroir-medic
13
FATAL_ASSERTION_PATTERN='ASSERTION.*FAILED in (put_ldev|drbd_al_begin_io_fastpath)'
14
PEER_IPS="${PEER_IPS:-192.168.69.20 192.168.69.21 192.168.69.22}"
15
NODE="${NODE_NAME:-unknown}"
16
SELF_IP="${HOST_IP:?HOST_IP is required}"
17
SETTLE_UPTIME_SECONDS=900
18
KUBELET_CONSECUTIVE_FAILS_BEFORE_RESET=20
19
STRANDED_TASKS_MIN=3
20
STRANDED_COMMS_MIN=2
21
STRANDED_POLLS_BEFORE_RESET=30
22
PEERS_DOWN_OVERRIDE_SECONDS=1800
23
MIN_SECONDS_BETWEEN_RESETS=1800
24
MAX_RESETS_PER_6H=3
25
MAX_RESETS_PER_24H=5
26
STANDDOWN_RENOTIFY_SECONDS=21600
27
POLL_SECONDS=30
29
mkdir -p "$PERSISTENT_STATE" "$BOOT_STATE"
31
log() { echo "$(date -u +%FT%TZ) $*"; }
33
epoch_now() { date +%s; }
35
uptime_seconds() { cut -d. -f1 /proc/uptime; }
37
notify() {
38
[ -n "${PUSHOVER_TOKEN:-}" ] && [ -n "${PUSHOVER_USER:-}" ] || return 0
39
wget -T 8 -q -O /dev/null --post-data \
40
"token=$PUSHOVER_TOKEN&user=$PUSHOVER_USER&priority=1&title=miroir-medic $NODE&message=$1" \
41
https://api.pushover.net/1/messages.json 2>/dev/null || log "notify failed: $1"
42
}
44
resets_within() {
45
cutoff=$(( $(epoch_now) - $1 ))
46
[ -f "$PERSISTENT_STATE/resets" ] || { echo 0; return; }
47
awk -v c="$cutoff" '$1 >= c' "$PERSISTENT_STATE/resets" | wc -l
48
}
50
seconds_since_last_reset() {
51
last=$(tail -1 "$PERSISTENT_STATE/resets" 2>/dev/null)
52
echo $(( $(epoch_now) - ${last:-0} ))
53
}
55
latch_wedge_if_asserting() {
56
[ -f "$BOOT_STATE/wedge" ] && return 0
57
if dmesg 2>/dev/null | grep -qE "$FATAL_ASSERTION_PATTERN"; then
58
log "fatal DRBD assertion sighted in kernel log; latching wedge"
59
dmesg 2>/dev/null | grep -E "$FATAL_ASSERTION_PATTERN" | tail -3
60
touch "$BOOT_STATE/wedge"
61
fi
62
return 0
63
}
65
wedged() { [ -f "$BOOT_STATE/wedge" ]; }
67
scan_d_tasks() {
68
: > "$BOOT_STATE/dtasks.now"
69
for stat_file in /proc/[0-9]*/stat; do
70
read -r stat_line 2>/dev/null < "$stat_file" || continue
71
after_comm="${stat_line##*) }"
72
task_state="${after_comm%% *}"
73
[ "$task_state" = "D" ] || continue
74
task_pid="${stat_line%% *}"
75
[ -s "/proc/$task_pid/cmdline" ] || continue
76
task_comm="${stat_line#*(}"
77
task_comm="${task_comm%)*}"
78
echo "$task_pid ($task_comm)" >> "$BOOT_STATE/dtasks.now"
79
done
80
}
82
stranded_storage_check() {
83
scan_d_tasks
84
if [ ! -f "$BOOT_STATE/dtasks.baseline" ]; then
85
cp "$BOOT_STATE/dtasks.now" "$BOOT_STATE/dtasks.baseline"
86
return 1
87
fi
88
: > "$BOOT_STATE/dtasks.streak.new"
89
: > "$BOOT_STATE/dtasks.stuck"
90
stuck_count=0
91
while IFS= read -r task; do
92
grep -Fxq "$task" "$BOOT_STATE/dtasks.baseline" && continue
93
prev_line=$(grep -F "|$task" "$BOOT_STATE/dtasks.streak" 2>/dev/null | head -1)
94
prev_count="${prev_line%%|*}"
95
streak=$(( ${prev_count:-0} + 1 ))
96
echo "$streak|$task" >> "$BOOT_STATE/dtasks.streak.new"
97
if [ "$streak" -ge "$STRANDED_POLLS_BEFORE_RESET" ]; then
98
stuck_count=$(( stuck_count + 1 ))
99
echo "${task#* }" >> "$BOOT_STATE/dtasks.stuck"
100
fi
101
done < "$BOOT_STATE/dtasks.now"
102
mv "$BOOT_STATE/dtasks.streak.new" "$BOOT_STATE/dtasks.streak"
103
distinct_comms=$(sort -u "$BOOT_STATE/dtasks.stuck" | wc -l)
104
if [ "$stuck_count" -ge "$STRANDED_TASKS_MIN" ] && [ "$distinct_comms" -ge "$STRANDED_COMMS_MIN" ]; then
105
stuck_tasks=$(sort -rn "$BOOT_STATE/dtasks.streak" | head -5 | cut -d'|' -f2 | tr '\n' ' ')
106
log "storage stranded: $stuck_count userspace tasks ($distinct_comms distinct commands) in uninterruptible sleep beyond the window: $stuck_tasks"
107
return 0
108
fi
109
return 1
110
}
112
kubelet_probe_ok() { wget -T 5 -q -O /dev/null http://127.0.0.1:10248/healthz 2>/dev/null; }
114
kubelet_unhealthy_too_long() {
115
if kubelet_probe_ok; then
116
rm -f "$BOOT_STATE/kubelet_fails"
117
return 1
118
fi
119
fails=$(( $(cat "$BOOT_STATE/kubelet_fails" 2>/dev/null || echo 0) + 1 ))
120
echo "$fails" > "$BOOT_STATE/kubelet_fails"
121
[ "$fails" = "$KUBELET_CONSECUTIVE_FAILS_BEFORE_RESET" ] && log "kubelet healthz failing ($fails consecutive)"
122
[ "$fails" -ge "$KUBELET_CONSECUTIVE_FAILS_BEFORE_RESET" ]
123
}
125
peer_serving_apiserver() {
126
wget -T 4 -q -O /dev/null --no-check-certificate "https://$1:6443/healthz" 2>/dev/null
127
}
129
peers_gate() {
130
any_down=0
131
for peer in $PEER_IPS; do
132
[ "$peer" = "$SELF_IP" ] && continue
133
peer_serving_apiserver "$peer" || any_down=1
134
done
135
if [ "$any_down" = 0 ]; then
136
rm -f "$BOOT_STATE/peers_down_since"
137
return 0
138
fi
139
[ -f "$BOOT_STATE/peers_down_since" ] || epoch_now > "$BOOT_STATE/peers_down_since"
140
down_since=$(cat "$BOOT_STATE/peers_down_since")
141
if [ "$1" = allow_override ] && [ $(( $(epoch_now) - down_since )) -ge "$PEERS_DOWN_OVERRIDE_SECONDS" ]; then
142
log "peer apiserver down ${PEERS_DOWN_OVERRIDE_SECONDS}s+ while self wedged; overriding quorum hold"
143
return 0
144
fi
145
return 1
146
}
148
stagger_by_node() {
149
case "$SELF_IP" in
150
*.20) sleep 0 ;;
151
*.21) sleep 90 ;;
152
*.22) sleep 180 ;;
153
*) sleep 45 ;;
154
esac
155
}
157
attempt_reset() {
158
reason="$1"
159
used_6h=$(resets_within 21600)
160
used_24h=$(resets_within 86400)
161
if [ "$(seconds_since_last_reset)" -lt "$MIN_SECONDS_BETWEEN_RESETS" ] \
162
|| [ "$used_6h" -ge "$MAX_RESETS_PER_6H" ] \
163
|| [ "$used_24h" -ge "$MAX_RESETS_PER_24H" ]; then
164
last_warned=$(cat "$PERSISTENT_STATE/standdown_warned" 2>/dev/null || echo 0)
165
if [ $(( $(epoch_now) - last_warned )) -ge "$STANDDOWN_RENOTIFY_SECONDS" ]; then
166
epoch_now > "$PERSISTENT_STATE/standdown_warned"
167
log "reset budget exhausted ($reason, 6h=$used_6h 24h=$used_24h); standing down"
168
notify "$reason persists but reset budget is exhausted (6h=$used_6h 24h=$used_24h); node left fenced, needs manual attention"
169
fi
170
return 0
171
fi
172
stagger_by_node
173
if [ "$reason" = kubelet ] && kubelet_probe_ok; then
174
log "kubelet recovered during stagger; aborting reset"
175
rm -f "$BOOT_STATE/kubelet_fails"
176
return 0
177
fi
178
if [ "$reason" = stranded ] && ! stranded_storage_check; then
179
log "stranded tasks drained during stagger; aborting reset"
180
return 0
181
fi
182
epoch_now >> "$PERSISTENT_STATE/resets"
183
echo "$reason $(date -u +%FT%TZ)" > "$PERSISTENT_STATE/pending"
184
log "hard reset now: $reason (reset $((used_24h + 1)) of $MAX_RESETS_PER_24H per 24h)"
185
notify "$reason detected; hard-resetting this node (reset $((used_24h + 1)) of $MAX_RESETS_PER_24H per 24h)"
186
sleep 2
187
echo s > /proc/sysrq-trigger 2>/dev/null
188
sleep 8
189
echo b > /proc/sysrq-trigger 2>/dev/null || notify "sysrq reset write FAILED; node still wedged"
190
sleep 300
191
}
193
if [ -f "$PERSISTENT_STATE/pending" ]; then
194
if [ "$(uptime_seconds)" -lt 3600 ]; then
195
notify "node is back after hard reset ($(cat "$PERSISTENT_STATE/pending")); uptime $(uptime_seconds)s"
196
fi
197
rm -f "$PERSISTENT_STATE/pending"
198
fi
200
log "miroir-medic watching on $NODE ($SELF_IP); resets used 24h=$(resets_within 86400)"
202
while :; do
203
sleep "$POLL_SECONDS"
204
[ "$(uptime_seconds)" -ge "$SETTLE_UPTIME_SECONDS" ] || continue
205
latch_wedge_if_asserting
206
if wedged; then
207
peers_gate allow_override && attempt_reset wedge
208
continue
209
fi
210
if stranded_storage_check; then
211
peers_gate no_override && attempt_reset stranded
212
continue
213
fi
214
if kubelet_unhealthy_too_long; then
215
peers_gate no_override && attempt_reset kubelet
216
fi
217
done