Skip the check cycle for a Floating IP already occupied by another port
The cloud is live: the address list submitted as "free" (bootstrap config
or POST /api/v1/admin/ips) can drift by the time the orchestrator claims
it, or an operator can queue an already-occupied address by mistake.
Neutron's floating-IP association is a blind "last write wins" PUT with
no conflict error to catch, so associateFIP now checks the FIP's PortID
(already fetched via GetFloatingIPByAddress) before associating, guarded
against the false-positive of the FIP already belonging to this same
validator's own port.
A match routes the address straight to a new terminal ip_queue.state
("occupied", distinct from failed/fail) via db.MarkFIPOccupied — no
retries, since Neutron won't free it on its own and requeuing would let
it be reclaimed again next tick, starving the rest of the queue — plus a
dedicated fip_occupied audit event. Resubmitting the address later (once
the conflict is resolved) resets it to queued via the existing
POST /api/v1/admin/ips resubmit path (CancelIP/ListExpiredLeases updated
to treat occupied as terminal too). admin-dashboard gets its own "занят"
badge, distinct from fail/partial/cancelled.
Rebuilt bin/{control-api,admin-dashboard,prober,validator-agent} and
bin/SHA256SUMS per docs/SETUP.md's documented build recipe, since
control-api and admin-dashboard source changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeVbMVEiE7XQAkBd7HQgj6
This commit is contained in:
1 parent
2246369b64
commit
93c79b63ea
17 files changed
+326
-17
No files matched your search
@@ -20,7 +20,10 @@ type Badge struct{ Class, Label string }
|
||||
// elapsed since the floating IP was associated, the row gets a distinct
|
||||
// "прогрев FIP" badge instead of looking identical to a row just waiting
|
||||
// on the agent's next poll. The time math happens here, not in the
|
||||
// template, same as fmtTime's existing precedent.
|
||||
// template, same as fmtTime's existing precedent. state=="occupied" gets
|
||||
// its own pill, deliberately separate from the done/failed result switch
|
||||
// below — it means the check cycle never ran (the floating IP was already
|
||||
// bound to another port at claim time), not that a check failed.
|
||||
func ipBadge(state, result string, fipAssociatedAt *time.Time, settleSeconds int) Badge {
|
||||
switch state {
|
||||
case "done", "failed":
|
||||
@@ -36,6 +39,8 @@ func ipBadge(state, result string, fipAssociatedAt *time.Time, settleSeconds int
|
||||
}
|
||||
case "queued":
|
||||
return Badge{"pill-neutral", "queued"}
|
||||
case "occupied":
|
||||
return Badge{"pill-occupied", "занят"}
|
||||
case "awaiting_self_check":
|
||||
if settleSeconds > 0 && fipAssociatedAt != nil && time.Now().Before(fipAssociatedAt.Add(time.Duration(settleSeconds)*time.Second)) {
|
||||
return Badge{"pill-warning", "прогрев FIP"}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
--info: #0E6FA8; --info-soft: #DDEEF8;
|
||||
--neutral: #5B6576; --neutral-soft: #E4E8EE;
|
||||
--cancel: #6D4FC4; --cancel-soft: #ECE7FA;
|
||||
--occupied: #0F7B72; --occupied-soft: #DCF3F0;
|
||||
|
||||
--overlay: rgba(10, 14, 20, .32);
|
||||
|
||||
@@ -82,6 +83,7 @@
|
||||
--info: #5AC0FF; --info-soft: #10222E;
|
||||
--neutral: #A3ACBE; --neutral-soft: #1B212B;
|
||||
--cancel: #B79CFF; --cancel-soft: #221B3A;
|
||||
--occupied: #4FD8C9; --occupied-soft: #102B28;
|
||||
|
||||
--overlay: rgba(0, 0, 0, .55);
|
||||
|
||||
@@ -114,6 +116,7 @@
|
||||
--info: #5AC0FF; --info-soft: #10222E;
|
||||
--neutral: #A3ACBE; --neutral-soft: #1B212B;
|
||||
--cancel: #B79CFF; --cancel-soft: #221B3A;
|
||||
--occupied: #4FD8C9; --occupied-soft: #102B28;
|
||||
|
||||
--overlay: rgba(0, 0, 0, .55);
|
||||
|
||||
@@ -411,6 +414,7 @@ td.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; colo
|
||||
.pill-warning { background: var(--warning-soft); color: var(--warning); }
|
||||
.pill-danger { background: var(--danger-soft); color: var(--danger); }
|
||||
.pill-cancel { background: var(--cancel-soft); color: var(--cancel); }
|
||||
.pill-occupied { background: var(--occupied-soft); color: var(--occupied); }
|
||||
|
||||
/* ---------- alerts ---------- */
|
||||
.alert {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<tbody>
|
||||
{{range .Items}}
|
||||
{{$b := ipBadge .State .OverallResult .FIPAssociatedAt $.FIPSettleSeconds}}
|
||||
{{$terminal := or (eq .State "done") (eq .State "failed")}}
|
||||
{{$terminal := or (eq .State "done") (eq .State "failed") (eq .State "occupied")}}
|
||||
<tr>
|
||||
<td data-label=""><input type="checkbox" name="addresses" value="{{.IPAddress}}"></td>
|
||||
<td class="addr" data-label="Адрес"><a href="/ips/{{.IPAddress}}">{{.IPAddress}}</a></td>
|
||||
|
||||
Reference in new issue
Block a user