mirror of
https://github.com/hansjone/netx.git
synced 2026-10-17 08:33:25 +08:00
Optimize cutover compare/monitor per the 2026-10-12 plan.
Correctness (A): normalize port-map names with the template rules (snapshot v2, v1 replays unchanged); protect mappings/templates that are referenced; stop dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated; validate baseline ownership on project create; one canonical key form; fix built-in template tolerances and upgrade old packaged values in place. Cross-vendor identity (B): single identity pipeline for every path, per-side and per-vendor interface rules, value maps, filters after normalization, mapping pre-flight for compare and cutover projects, SQL eligibility fixes. Data quality (V): no_data / suspect_parse / not_collected sheet states, command slice coverage, directional compare modes, value-sorted duplicate pairing, before-side raw names in results and CSV. Robustness (H): bounded auto-run history, background evaluate/finish with polling, manual route tables out of the acceptance gate, baseline row cache, scoped duplicate-key failures, merged HF tasks with a per-NE session cap, same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red tickets. Frontend: pre-flight report, mapping table view, template editor support for the new rules, data-state banners. Docs: canonical field contract. Not included: Huawei status parsers (no real samples), SQL-path route compare. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fff7d63b9b
commit
315539e0fd
59 changed files with 6483 additions and 487 deletions
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
|
@ -25,6 +27,11 @@ def max_concurrent_tasks() -> int:
|
|||
return max(1, int(getattr(settings, "biz_state_max_concurrent_tasks", 16) or 16))
|
||||
|
||||
|
||||
def max_sessions_per_ne() -> int:
|
||||
"""Concurrent SSH collections allowed per network element (>= 1)."""
|
||||
return max(1, int(getattr(settings, "biz_state_max_sessions_per_ne", 2) or 2))
|
||||
|
||||
|
||||
def enqueue_collect(task_id: str, *, manual: bool = False) -> dict[str, Any]:
|
||||
"""Create a queued batch and mark task collect_running. Does not open SSH.
|
||||
|
||||
|
|
@ -135,15 +142,16 @@ def enqueue_collect_group(group_id: str, *, manual: bool = False,
|
|||
BizStateTask.id).with_for_update().populate_existing().all()
|
||||
result: dict[str, Any] = {"ok": False, "queued": False, "task_id": requested_task_id,
|
||||
"collect_group_id": group_id}
|
||||
if len(tasks) != 2 or len({t.ne_id for t in tasks}) != 2:
|
||||
# One task = a single device used as both sides (same-device cutover); two tasks = a pair.
|
||||
if len(tasks) not in (1, 2) or len({t.ne_id for t in tasks}) != len(tasks):
|
||||
return {**result, "reason": "collect_pair_invalid"}
|
||||
if max_concurrent_tasks() < 2:
|
||||
if max_concurrent_tasks() < len(tasks):
|
||||
return {**result, "reason": "collect_pair_capacity"}
|
||||
from ..cli_budget import clamp_cli_workers
|
||||
|
||||
worker_cap = clamp_cli_workers(int(getattr(settings, "biz_state_worker_collect_threads", None)
|
||||
or getattr(settings, "biz_state_dispatch_workers", 8) or 8))
|
||||
if worker_cap < 2:
|
||||
if worker_cap < len(tasks):
|
||||
return {**result, "reason": "collect_pair_capacity"}
|
||||
if any(t.collect_running for t in tasks):
|
||||
return {**result, "reason": "collect_pair_busy"}
|
||||
|
|
@ -230,13 +238,12 @@ def claim_queued_batches(limit: int | None = None) -> list[dict[str, Any]]:
|
|||
if slots <= 0:
|
||||
return []
|
||||
|
||||
busy_nes = {
|
||||
str(r[0] or "").strip()
|
||||
for r in db.query(BizStateBatch.ne_id)
|
||||
.filter(BizStateBatch.status == "running")
|
||||
.all()
|
||||
if str(r[0] or "").strip()
|
||||
}
|
||||
# Running collections per NE; an NE accepts new ones until it reaches its session cap.
|
||||
per_ne_cap = max_sessions_per_ne()
|
||||
ne_running: dict[str, int] = defaultdict(int)
|
||||
for (running_ne,) in db.query(BizStateBatch.ne_id).filter(BizStateBatch.status == "running").all():
|
||||
if str(running_ne or "").strip():
|
||||
ne_running[str(running_ne).strip()] += 1
|
||||
|
||||
# Manual route rounds yield to fresh monitoring, but must eventually
|
||||
# run even when a busy device always has regular metrics queued.
|
||||
|
|
@ -266,14 +273,19 @@ def claim_queued_batches(limit: int | None = None) -> list[dict[str, Any]]:
|
|||
peers = db.query(BizStateBatch).filter(
|
||||
BizStateBatch.collect_round_id == batch.collect_round_id,
|
||||
).with_for_update(skip_locked=pg).all()
|
||||
if len(peers) != 2 or any(p.status != "queued" for p in peers):
|
||||
expected = db.query(BizStateTask.id).filter(
|
||||
BizStateTask.collect_group_id == batch.collect_group_id).count() if batch.collect_group_id else 2
|
||||
if len(peers) != max(1, expected) or any(p.status != "queued" for p in peers):
|
||||
continue
|
||||
group = peers
|
||||
visited.update(p.id for p in group)
|
||||
if len(group) > slots - len(claimed_rows):
|
||||
continue
|
||||
nes = {str(p.ne_id or "").strip() for p in group}
|
||||
if nes & busy_nes:
|
||||
wanted: dict[str, int] = defaultdict(int)
|
||||
for p in group:
|
||||
if str(p.ne_id or "").strip():
|
||||
wanted[str(p.ne_id).strip()] += 1
|
||||
if any(ne_running[ne] + n > per_ne_cap for ne, n in wanted.items()):
|
||||
continue
|
||||
now = _utcnow()
|
||||
for member in group:
|
||||
|
|
@ -285,7 +297,8 @@ def claim_queued_batches(limit: int | None = None) -> list[dict[str, Any]]:
|
|||
if task:
|
||||
task.last_collect_started_at = now
|
||||
claimed_rows.append(member)
|
||||
busy_nes.update(nes - {""})
|
||||
for ne, n in wanted.items():
|
||||
ne_running[ne] += n
|
||||
|
||||
if not claimed_rows:
|
||||
db.rollback()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue