mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 09:50:55 +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
|
|
@ -39,7 +39,14 @@ def normalize_iface_rules(raw: Any) -> list[dict[str, str]]:
|
|||
fr = str(item.get("from") or item.get("src") or "").strip()
|
||||
to = str(item.get("to") or item.get("dst") or "").strip()
|
||||
if fr and to:
|
||||
out.append({"from": fr, "to": to})
|
||||
rule = {"from": fr, "to": to}
|
||||
side = str(item.get("side") or "").strip().lower()
|
||||
if side in ("before", "after"):
|
||||
rule["side"] = side
|
||||
vendor = str(item.get("vendor") or "").strip().lower()
|
||||
if vendor:
|
||||
rule["vendor"] = vendor
|
||||
out.append(rule)
|
||||
# Longest prefix first so XXVGE wins over XGE / GE.
|
||||
out.sort(key=lambda r: len(r["from"]), reverse=True)
|
||||
return out
|
||||
|
|
@ -107,6 +114,47 @@ def apply_iface_normalize_rows(
|
|||
]
|
||||
|
||||
|
||||
def normalize_port_map(
|
||||
port_map: Mapping[str, str] | None,
|
||||
rules: Sequence[Mapping[str, str]] | None,
|
||||
value_rules: Sequence[Mapping[str, str]] | None = None,
|
||||
*,
|
||||
key_normalizer: Any = None,
|
||||
value_normalizer: Any = None,
|
||||
) -> tuple[dict[str, str], list[dict[str, str]]]:
|
||||
"""Normalize port-map keys and values with the same rules as iface fields.
|
||||
|
||||
``value_rules`` (default: ``rules``) normalizes the after-side values, for
|
||||
per-side rules. ``key_normalizer`` / ``value_normalizer`` are callables that
|
||||
replace the rule-based normalization (used by the identity pipeline).
|
||||
|
||||
Returns ``(normalized_map, conflicts)``. A conflict is two raw keys that
|
||||
normalize to the same key but point at different (normalized) targets; the
|
||||
first one wins in ``normalized_map``.
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
origin: dict[str, str] = {}
|
||||
conflicts: list[dict[str, str]] = []
|
||||
for raw_k, raw_v in (port_map or {}).items():
|
||||
k = key_normalizer(raw_k) if key_normalizer else normalize_iface_name(raw_k, rules)
|
||||
v = (
|
||||
value_normalizer(raw_v) if value_normalizer
|
||||
else normalize_iface_name(raw_v, rules if value_rules is None else value_rules)
|
||||
)
|
||||
if not k or not v:
|
||||
continue
|
||||
if k in out:
|
||||
if out[k] != v:
|
||||
conflicts.append({
|
||||
"before_if": k, "kept_from": origin[k], "kept_to": out[k],
|
||||
"dropped_from": str(raw_k), "dropped_to": v,
|
||||
})
|
||||
continue
|
||||
out[k] = v
|
||||
origin[k] = str(raw_k)
|
||||
return out, conflicts
|
||||
|
||||
|
||||
def resolve_mapped_iface(name: str, port_map: Mapping[str, str] | None) -> str:
|
||||
"""Exact map hit, else longest mapped parent + remaining suffix (QinQ-safe).
|
||||
|
||||
|
|
@ -142,3 +190,53 @@ def default_zte_iface_normalize_rules() -> list[dict[str, str]]:
|
|||
{"from": "SG", "to": "smartgroup"},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in per-vendor canonical names (same-vendor synonyms only; cross-vendor
|
||||
# numbering is the port map's job). Applied before template alias rules.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
VENDOR_CANONICAL: dict[str, list[dict[str, str]]] = {
|
||||
"huawei": [
|
||||
{"from": "XGigabitEthernet", "to": "XGE"}, {"from": "GigabitEthernet", "to": "GE"},
|
||||
{"from": "100GE", "to": "100GE"}, {"from": "40GE", "to": "40GE"}, {"from": "25GE", "to": "25GE"},
|
||||
{"from": "10GE", "to": "10GE"}, {"from": "Eth-Trunk", "to": "Eth-Trunk"},
|
||||
],
|
||||
"cisco": [
|
||||
{"from": "GigabitEthernet", "to": "Gi"}, {"from": "TenGigabitEthernet", "to": "Te"},
|
||||
{"from": "TenGigE", "to": "Te"}, {"from": "HundredGigE", "to": "Hu"}, {"from": "FortyGigE", "to": "Fo"},
|
||||
{"from": "TwentyFiveGigE", "to": "Twe"}, {"from": "FastEthernet", "to": "Fa"},
|
||||
{"from": "Bundle-Ether", "to": "BE"}, {"from": "Loopback", "to": "Lo"},
|
||||
],
|
||||
"h3c": [
|
||||
{"from": "Ten-GigabitEthernet", "to": "XGE"}, {"from": "GigabitEthernet", "to": "GE"},
|
||||
{"from": "FortyGigE", "to": "FGE"}, {"from": "HundredGigE", "to": "HGE"},
|
||||
{"from": "Bridge-Aggregation", "to": "BAGG"},
|
||||
],
|
||||
"zte": [
|
||||
{"from": "xxvgei", "to": "xxvgei"}, {"from": "xgei", "to": "xgei"}, {"from": "cgei", "to": "cgei"},
|
||||
{"from": "gei", "to": "gei"}, {"from": "smartgroup", "to": "smartgroup"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def vendor_canonical_rules(vendor: str) -> list[dict[str, str]]:
|
||||
return normalize_iface_rules(VENDOR_CANONICAL.get(str(vendor or "").strip().lower(), []))
|
||||
|
||||
|
||||
def rules_for_side(
|
||||
rules: Sequence[Mapping[str, str]] | None, side: str, vendor: str = "",
|
||||
) -> list[dict[str, str]]:
|
||||
"""Template alias rules that apply to ``side`` (``before``/``after``) and ``vendor``."""
|
||||
vendor_n = str(vendor or "").strip().lower()
|
||||
out: list[dict[str, str]] = []
|
||||
for r in rules or []:
|
||||
rs = str(r.get("side") or "").strip().lower()
|
||||
if rs and rs != side:
|
||||
continue
|
||||
rv = str(r.get("vendor") or "").strip().lower()
|
||||
if rv and rv != vendor_n:
|
||||
continue
|
||||
out.append({"from": str(r.get("from") or ""), "to": str(r.get("to") or "")})
|
||||
return normalize_iface_rules(out)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue