mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
Tighten cutover verdicts and scope iface metrics by port mapping.
Mapped interfaces are the expect set and must appear after cutover; unmapped same-name down/up is anomaly. Success presets no longer treat bare unchanged as green. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
27c6b7c61d
commit
88b7558a48
11 changed files with 4663 additions and 1497 deletions
|
|
@ -8,10 +8,12 @@ from netx_api.biz_migration.evaluate import (
|
|||
classify_status,
|
||||
dual_verdict,
|
||||
evaluate_metric_dual,
|
||||
expect_keys_for_metric,
|
||||
parse_expect_set,
|
||||
port_sheet_def,
|
||||
port_status_label,
|
||||
side_verdict,
|
||||
_remap_key_str,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -42,6 +44,49 @@ class ParseExpectSetTests(unittest.TestCase):
|
|||
self.assertEqual(got["bgp_peer"], {"AS1|1.1.1.1"})
|
||||
self.assertEqual(got["arp"], {"10.0.0.1"})
|
||||
|
||||
def test_multi_select_each_key_is_separate(self):
|
||||
"""UI must send one item per selected row, not a flat keys list."""
|
||||
got = parse_expect_set(
|
||||
{
|
||||
"items": [
|
||||
{"metric_id": "bgp_peer", "key": "1.1.1.1"},
|
||||
{"metric_id": "bgp_peer", "key": "2.2.2.2"},
|
||||
{"metric_id": "arp", "key": "10.0.0.1"},
|
||||
]
|
||||
}
|
||||
)
|
||||
self.assertEqual(got["bgp_peer"], {"1.1.1.1", "2.2.2.2"})
|
||||
self.assertEqual(got["arp"], {"10.0.0.1"})
|
||||
|
||||
def test_flat_keys_list_is_one_composite(self):
|
||||
got = parse_expect_set(
|
||||
{"items": [{"metric_id": "bgp_peer", "keys": ["1.1.1.1", "2.2.2.2"]}]}
|
||||
)
|
||||
self.assertEqual(got["bgp_peer"], {"1.1.1.1|2.2.2.2"})
|
||||
|
||||
def test_nested_keys_list(self):
|
||||
got = parse_expect_set(
|
||||
{
|
||||
"items": [
|
||||
{"metric_id": "isis_adjacency", "keys": [["p1", "gei-1", "sys1"], ["p1", "gei-2", "sys2"]]},
|
||||
{"metric_id": "arp", "key": ["10.0.0.1", "vrf1"]},
|
||||
]
|
||||
}
|
||||
)
|
||||
self.assertEqual(got["isis_adjacency"], {"p1|gei-1|sys1", "p1|gei-2|sys2"})
|
||||
self.assertEqual(got["arp"], {"10.0.0.1|vrf1"})
|
||||
|
||||
def test_ports_only_apply_to_interface_brief(self):
|
||||
expect = parse_expect_set({"ports": ["gei-1"]})
|
||||
self.assertEqual(
|
||||
expect_keys_for_metric(expect, metric_id="interface_brief", iface_fields=["interface"]),
|
||||
{"gei-1"},
|
||||
)
|
||||
self.assertEqual(
|
||||
expect_keys_for_metric(expect, metric_id="arp", iface_fields=["interface"]),
|
||||
set(),
|
||||
)
|
||||
|
||||
|
||||
class PortSheetTests(unittest.TestCase):
|
||||
def test_port_sheet_fields(self):
|
||||
|
|
@ -155,6 +200,75 @@ class VerdictTests(unittest.TestCase):
|
|||
("anomaly", "yellow"),
|
||||
)
|
||||
|
||||
def test_out_of_expect_new_side_only_removed(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=False,
|
||||
window_active=False,
|
||||
out_of_expect="strict",
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=False,
|
||||
window_active=False,
|
||||
out_of_expect="warn",
|
||||
),
|
||||
("anomaly", "yellow"),
|
||||
)
|
||||
|
||||
def test_rules_mode_skips_auto_migrate(self):
|
||||
"""With success patterns configured, bare kind migrate is not invented."""
|
||||
verdict, color = dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="added",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [
|
||||
[
|
||||
{"type": "presence", "value": "added"},
|
||||
{"type": "value", "field": "state", "op": "eq", "value": "up"},
|
||||
]
|
||||
],
|
||||
}
|
||||
],
|
||||
new_row={"state": "down"},
|
||||
)
|
||||
self.assertNotEqual(verdict, "migrated")
|
||||
self.assertEqual(color, "yellow")
|
||||
|
||||
def test_rules_mode_skips_catch_all_anomaly(self):
|
||||
"""With anomaly list present, mid-cutover stays migrating not invented anomaly."""
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="changed",
|
||||
new_kind="unchanged",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
}
|
||||
],
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
|
||||
def test_field_token_success(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
|
|
@ -174,6 +288,259 @@ class VerdictTests(unittest.TestCase):
|
|||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_structured_field_conds_all_mode(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="changed",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
old_status="other",
|
||||
new_status="other",
|
||||
success_patterns=[
|
||||
{
|
||||
"old_mode": "all",
|
||||
"new_mode": "all",
|
||||
"old_conds": [
|
||||
{"type": "kind", "value": "changed"},
|
||||
{"type": "field", "field": "state", "op": "in", "value": ["idle", "active"]},
|
||||
],
|
||||
"new_conds": [
|
||||
{"type": "kind", "value": "changed"},
|
||||
{"type": "field", "field": "state", "op": "eq", "value": "established"},
|
||||
],
|
||||
}
|
||||
],
|
||||
old_row={"state": "Idle"},
|
||||
new_row={"state": "Established"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_structured_conds_fail_when_all_incomplete(self):
|
||||
verdict, _color = dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_mode": "all",
|
||||
"new_mode": "any",
|
||||
"old_conds": [
|
||||
{"type": "kind", "value": "unchanged"},
|
||||
{"type": "field", "field": "state", "op": "eq", "value": "idle"},
|
||||
],
|
||||
"new_conds": [{"type": "kind", "value": "changed"}],
|
||||
}
|
||||
],
|
||||
old_row={"state": "Active"},
|
||||
new_row={"state": "Established"},
|
||||
)
|
||||
self.assertNotEqual(verdict, "migrated")
|
||||
|
||||
def test_groups_or_of_and(self):
|
||||
"""Within a group = AND; between groups = OR."""
|
||||
patterns = [
|
||||
{
|
||||
"old_groups": [
|
||||
[{"type": "presence", "value": "removed"}],
|
||||
[
|
||||
{"type": "presence", "value": "changed"},
|
||||
{"type": "value", "field": "state", "op": "in", "value": ["idle", "active"]},
|
||||
],
|
||||
],
|
||||
"new_groups": [
|
||||
[{"type": "value", "field": "state", "op": "eq", "value": "established"}],
|
||||
],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=patterns,
|
||||
old_row={},
|
||||
new_row={"state": "Established"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="changed",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=patterns,
|
||||
old_row={"state": "Idle"},
|
||||
new_row={"state": "Established"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
verdict, _color = dual_verdict(
|
||||
old_kind="changed",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=patterns,
|
||||
old_row={"state": "Established"},
|
||||
new_row={"state": "Established"},
|
||||
)
|
||||
self.assertNotEqual(verdict, "migrated")
|
||||
|
||||
def test_value_changed_vs_baseline(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="changed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "unchanged"}]],
|
||||
"new_groups": [
|
||||
[{"type": "value", "field": "state", "op": "changed"}]
|
||||
],
|
||||
}
|
||||
],
|
||||
old_row={"state": "Idle"},
|
||||
new_row={"state": "Established"},
|
||||
old_base={"state": "Idle"},
|
||||
new_base={"state": "Idle"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_screenshot_style_success(self):
|
||||
"""Old removed OR state!=up; new added AND state=up."""
|
||||
patterns = [
|
||||
{
|
||||
"old_groups": [
|
||||
[{"type": "presence", "value": "removed"}],
|
||||
[{"type": "value", "field": "state", "op": "ne", "value": "up"}],
|
||||
],
|
||||
"new_groups": [
|
||||
[
|
||||
{"type": "presence", "value": "added"},
|
||||
{"type": "value", "field": "state", "op": "eq", "value": "up"},
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="added",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=patterns,
|
||||
old_row={},
|
||||
new_row={"state": "up"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_anomaly_both_gone(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
}
|
||||
],
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
|
||||
def test_anomaly_new_side_dont_care_old(self):
|
||||
"""Side-only anomaly is deferred while window is active (avoid false red)."""
|
||||
anomaly = [
|
||||
{
|
||||
"old_groups": [],
|
||||
"new_groups": [
|
||||
[{"type": "presence", "value": "removed"}],
|
||||
[
|
||||
{
|
||||
"type": "value",
|
||||
"field": "state",
|
||||
"op": "ne",
|
||||
"value": "up",
|
||||
}
|
||||
],
|
||||
],
|
||||
}
|
||||
]
|
||||
success = [
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
acceptance=False,
|
||||
success_patterns=success,
|
||||
anomaly_patterns=anomaly,
|
||||
new_row={},
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=False,
|
||||
acceptance=True,
|
||||
success_patterns=success,
|
||||
anomaly_patterns=anomaly,
|
||||
new_row={},
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
|
||||
def test_success_beats_anomaly(self):
|
||||
success = [
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
]
|
||||
anomaly = [
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="added",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=success,
|
||||
anomaly_patterns=anomaly,
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
|
||||
class EvaluateMetricDualTests(unittest.TestCase):
|
||||
def test_port_migration_happy_path(self):
|
||||
|
|
@ -349,6 +716,400 @@ class EvaluateMetricDualTests(unittest.TestCase):
|
|||
reds = [r for r in out["rows"] if r["color"] == "red"]
|
||||
self.assertTrue(reds, out["rows"])
|
||||
|
||||
def test_new_baseline_missing_flag(self):
|
||||
old_base = [{"peer": "1.1.1.1", "state": "Established"}]
|
||||
new_cur = [{"peer": "1.1.1.1", "state": "Established"}]
|
||||
expect = parse_expect_set({"items": [{"metric_id": "bgp_peer", "key": "1.1.1.1"}]})
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="bgp_peer",
|
||||
key_fields=["peer"],
|
||||
iface_fields=[],
|
||||
compare_fields=["state"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=old_base,
|
||||
new_baseline_rows=None,
|
||||
new_current_rows=new_cur,
|
||||
port_map={},
|
||||
expect=expect,
|
||||
window_active=True,
|
||||
sheet_override={},
|
||||
)
|
||||
self.assertTrue(out["new_baseline_missing"])
|
||||
self.assertEqual(out["new_baseline_mode"], "empty")
|
||||
# With empty new before, current rows are all "added"
|
||||
kinds = {r["new_kind"] for r in out["rows"] if r.get("in_expect")}
|
||||
self.assertIn("added", kinds)
|
||||
|
||||
def test_new_baseline_port_mapped_not_missing(self):
|
||||
old_base = [{"interface": "gei-old", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
new_cur = [{"interface": "gei-new", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
expect = parse_expect_set({"ports": ["gei-old"]})
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="interface_brief",
|
||||
key_fields=["interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["admin", "phy", "prot"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=[],
|
||||
new_baseline_rows=None,
|
||||
new_current_rows=new_cur,
|
||||
port_map={"gei-old": "gei-new"},
|
||||
expect=expect,
|
||||
window_active=True,
|
||||
sheet_override={},
|
||||
)
|
||||
self.assertFalse(out["new_baseline_missing"])
|
||||
self.assertEqual(out["new_baseline_mode"], "port_mapped")
|
||||
|
||||
def test_empty_provided_new_baseline_falls_back_or_missing(self):
|
||||
old_base = [{"peer": "1.1.1.1", "state": "Established"}]
|
||||
new_cur = [{"peer": "1.1.1.1", "state": "Established"}]
|
||||
expect = parse_expect_set({"items": [{"metric_id": "bgp_peer", "key": "1.1.1.1"}]})
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="bgp_peer",
|
||||
key_fields=["peer"],
|
||||
iface_fields=[],
|
||||
compare_fields=["state"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=[],
|
||||
new_baseline_rows=[],
|
||||
new_current_rows=new_cur,
|
||||
port_map={},
|
||||
expect=expect,
|
||||
window_active=True,
|
||||
sheet_override={},
|
||||
)
|
||||
self.assertTrue(out["new_baseline_missing"])
|
||||
self.assertEqual(out["new_baseline_mode"], "empty")
|
||||
# Still reports presence as added vs empty before — callers should heed the flag.
|
||||
kinds = {r["new_kind"] for r in out["rows"] if r.get("in_expect")}
|
||||
self.assertIn("added", kinds)
|
||||
|
||||
def test_value_ne_false_when_removed(self):
|
||||
verdict, color = dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="unchanged",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [
|
||||
[{"type": "value", "field": "state", "op": "ne", "value": "up"}]
|
||||
],
|
||||
"new_groups": [[{"type": "presence", "value": "unchanged"}]],
|
||||
}
|
||||
],
|
||||
old_row={},
|
||||
new_row={"state": "up"},
|
||||
)
|
||||
self.assertNotEqual(verdict, "migrated")
|
||||
|
||||
def test_composite_key_port_remap(self):
|
||||
mapped = _remap_key_str(
|
||||
"gei-old|vrf1",
|
||||
key_fields=["interface", "vrf"],
|
||||
iface_fields=["interface"],
|
||||
port_map={"gei-old": "gei-new"},
|
||||
)
|
||||
self.assertEqual(mapped, "gei-new|vrf1")
|
||||
|
||||
def test_anomaly_not_gated_by_field_tokens_on_removed(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
}
|
||||
],
|
||||
sheet_override={
|
||||
"field_tokens": [{"side": "old", "field": "state", "in": ["up"]}],
|
||||
},
|
||||
old_row={},
|
||||
new_row={},
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
|
||||
def test_side_only_anomaly_deferred_during_window(self):
|
||||
"""Mid-cutover: new-side gone alone must not red while window is active."""
|
||||
side_only = [
|
||||
{
|
||||
"old_groups": [],
|
||||
"new_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
acceptance=False,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=side_only,
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
# Acceptance / window closed → side-only fires
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="unchanged",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=False,
|
||||
acceptance=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=side_only,
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
|
||||
def test_both_gone_anomaly_still_fires_during_window(self):
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="removed",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
anomaly_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
}
|
||||
],
|
||||
),
|
||||
("anomaly", "red"),
|
||||
)
|
||||
|
||||
def test_bare_unchanged_not_success_with_tight_preset(self):
|
||||
from netx_api.biz_migration.monitor_templates import preset_override_for_metric
|
||||
|
||||
ov = preset_override_for_metric("interface_brief")
|
||||
# Old removed + new still present but down → not green
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="unchanged",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
old_status="none",
|
||||
new_status="down",
|
||||
success_patterns=ov["success"],
|
||||
anomaly_patterns=ov["anomaly"],
|
||||
old_row={},
|
||||
new_row={"admin": "down", "phy": "down"},
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
# Old removed + new up → green
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="unchanged",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
old_status="none",
|
||||
new_status="up",
|
||||
success_patterns=ov["success"],
|
||||
anomaly_patterns=ov["anomaly"],
|
||||
old_row={},
|
||||
new_row={"admin": "up", "phy": "up"},
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_arp_preset_requires_added_not_bare_unchanged(self):
|
||||
from netx_api.biz_migration.monitor_templates import preset_override_for_metric
|
||||
|
||||
ov = preset_override_for_metric("arp")
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="unchanged",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=ov["success"],
|
||||
anomaly_patterns=ov["anomaly"],
|
||||
),
|
||||
("migrating", "yellow"),
|
||||
)
|
||||
self.assertEqual(
|
||||
dual_verdict(
|
||||
old_kind="removed",
|
||||
new_kind="added",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=ov["success"],
|
||||
anomaly_patterns=ov["anomaly"],
|
||||
),
|
||||
("migrated", "green"),
|
||||
)
|
||||
|
||||
def test_rule_hit_labels(self):
|
||||
from netx_api.biz_migration.evaluate import dual_verdict_ex
|
||||
|
||||
v, c, hit = dual_verdict_ex(
|
||||
old_kind="removed",
|
||||
new_kind="added",
|
||||
in_expect=True,
|
||||
window_active=True,
|
||||
success_patterns=[
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertEqual((v, c), ("migrated", "green"))
|
||||
self.assertTrue(hit.startswith("success#"))
|
||||
|
||||
|
||||
class PortMapScopeTests(unittest.TestCase):
|
||||
def test_mapping_is_expect_for_iface_sheet(self):
|
||||
old_base = [
|
||||
{"interface": "gei-old", "admin": "up", "phy": "up", "prot": "up"},
|
||||
{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"},
|
||||
]
|
||||
new_base = [
|
||||
{"interface": "gei-new", "admin": "up", "phy": "up", "prot": "up"},
|
||||
{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"},
|
||||
]
|
||||
new_cur = [
|
||||
{"interface": "gei-new", "admin": "up", "phy": "up", "prot": "up"},
|
||||
{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"},
|
||||
]
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="interface_brief",
|
||||
key_fields=["interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["admin", "phy", "prot"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=[
|
||||
{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"},
|
||||
],
|
||||
new_baseline_rows=new_base,
|
||||
new_current_rows=new_cur,
|
||||
port_map={"gei-old": "gei-new"},
|
||||
expect=parse_expect_set({}),
|
||||
window_active=True,
|
||||
sheet_override=PORT_OVERRIDE,
|
||||
)
|
||||
self.assertEqual(out["progress_total"], 1)
|
||||
keys = {r["key_str"] for r in out["rows"]}
|
||||
self.assertIn("gei-old", keys)
|
||||
self.assertNotIn("gei-keep", keys)
|
||||
|
||||
def test_unmapped_same_iface_old_down_new_up_is_anomaly(self):
|
||||
old_base = [
|
||||
{"interface": "gei-old", "admin": "up", "phy": "up", "prot": "up"},
|
||||
{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"},
|
||||
]
|
||||
old_cur = [{"interface": "gei-keep", "admin": "down", "phy": "down", "prot": "down"}]
|
||||
new_base = [{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
new_cur = [{"interface": "gei-keep", "admin": "up", "phy": "up", "prot": "up"}]
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="interface_brief",
|
||||
key_fields=["interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["admin", "phy", "prot"],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=old_cur,
|
||||
new_baseline_rows=new_base,
|
||||
new_current_rows=new_cur,
|
||||
port_map={"gei-old": "gei-new"},
|
||||
expect=parse_expect_set({}),
|
||||
window_active=True,
|
||||
sheet_override=PORT_OVERRIDE,
|
||||
)
|
||||
keep = [r for r in out["rows"] if r["key_str"] == "gei-keep"]
|
||||
self.assertEqual(len(keep), 1, out["rows"])
|
||||
self.assertEqual(keep[0]["verdict"], "anomaly")
|
||||
self.assertEqual(keep[0]["rule_hit"], "unmapped_same_iface")
|
||||
self.assertFalse(keep[0]["in_expect"])
|
||||
|
||||
def test_arp_filtered_by_iface_mapping(self):
|
||||
old_base = [
|
||||
{"ip": "10.0.0.1", "interface": "gei-old"},
|
||||
{"ip": "10.0.0.2", "interface": "gei-keep"},
|
||||
]
|
||||
new_cur = [{"ip": "10.0.0.1", "interface": "gei-new"}]
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="arp",
|
||||
key_fields=["ip", "interface"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=[],
|
||||
old_baseline_rows=old_base,
|
||||
old_current_rows=[],
|
||||
new_baseline_rows=[{"ip": "9.9.9.9", "interface": "gei-other"}],
|
||||
new_current_rows=new_cur,
|
||||
port_map={"gei-old": "gei-new"},
|
||||
expect=parse_expect_set({}),
|
||||
window_active=True,
|
||||
sheet_override={
|
||||
"success": [
|
||||
{
|
||||
"old_groups": [[{"type": "presence", "value": "removed"}]],
|
||||
"new_groups": [[{"type": "presence", "value": "added"}]],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
keys = {r["key_str"] for r in out["rows"]}
|
||||
self.assertIn("10.0.0.1|gei-old", keys)
|
||||
self.assertNotIn("10.0.0.2|gei-keep", keys)
|
||||
migrated = [r for r in out["rows"] if r["verdict"] == "migrated"]
|
||||
self.assertTrue(migrated, out["rows"])
|
||||
self.assertEqual(migrated[0]["new_key_str"], "10.0.0.1|gei-new")
|
||||
|
||||
def test_bgp_still_uses_expect_picker(self):
|
||||
out = evaluate_metric_dual(
|
||||
metric_id="bgp_peer",
|
||||
key_fields=["peer"],
|
||||
iface_fields=["interface"],
|
||||
compare_fields=["state"],
|
||||
old_baseline_rows=[{"peer": "1.1.1.1", "interface": "gei-old", "state": "Established"}],
|
||||
old_current_rows=[],
|
||||
new_baseline_rows=[{"peer": "1.1.1.1", "interface": "gei-new", "state": "Idle"}],
|
||||
new_current_rows=[{"peer": "1.1.1.1", "interface": "gei-new", "state": "Established"}],
|
||||
port_map={"gei-keep": "gei-other"},
|
||||
expect=parse_expect_set({"items": [{"metric_id": "bgp_peer", "key": "1.1.1.1"}]}),
|
||||
window_active=True,
|
||||
sheet_override={
|
||||
"success": [{"old": ["removed"], "new": ["changed", "up"]}],
|
||||
"status_fields": ["state"],
|
||||
"up_values": ["established"],
|
||||
"down_values": ["idle"],
|
||||
},
|
||||
)
|
||||
self.assertTrue(any(r["key_str"] == "1.1.1.1" and r["in_expect"] for r in out["rows"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue