mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Add ops task overview, chart ahead window, and WebCRT lifecycle status.
Unify live runners in /audit/tasks (including UME sync and connect tests), extend port-traffic compare past now via ahead_hours, and distinguish WebCRT connecting/ready/detached. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3f0787da11
commit
3908b78dbb
28 changed files with 1083 additions and 54 deletions
|
|
@ -236,6 +236,46 @@ class CompareTargetsTests(unittest.TestCase):
|
|||
self.assertGreaterEqual(len(out.baseline), 2)
|
||||
self.assertEqual(out.meta.offset_hours, 2.0)
|
||||
|
||||
def test_ahead_hours_extends_baseline_past_now(self):
|
||||
# Yesterday sample 30min after "now" clock — only visible when ahead_hours > 0.
|
||||
future_raw = self.now - timedelta(days=1) + timedelta(minutes=30)
|
||||
self.db.add(
|
||||
PortTrafficSample(
|
||||
id=uuid4().hex,
|
||||
target_row_id=self.target_id,
|
||||
series_id=self.series_id,
|
||||
ts=future_raw,
|
||||
in_bps=777.0,
|
||||
out_bps=888.0,
|
||||
raw_ok=True,
|
||||
)
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
without = compare_targets(
|
||||
self.db,
|
||||
target_row_id=self.target_id,
|
||||
range_hours=2,
|
||||
baseline="day",
|
||||
ahead_hours=0,
|
||||
to_ts=self.now,
|
||||
)
|
||||
self.assertFalse(any(abs((p.ts_raw or p.ts) - future_raw).total_seconds() < 1 for p in without.baseline))
|
||||
|
||||
with_ahead = compare_targets(
|
||||
self.db,
|
||||
target_row_id=self.target_id,
|
||||
range_hours=2,
|
||||
baseline="day",
|
||||
ahead_hours=1,
|
||||
to_ts=self.now,
|
||||
)
|
||||
self.assertEqual(with_ahead.meta.ahead_hours, 1.0)
|
||||
hit = [p for p in with_ahead.baseline if p.in_bps == 777.0]
|
||||
self.assertEqual(len(hit), 1)
|
||||
self.assertGreater(hit[0].ts, self.now)
|
||||
self.assertLessEqual(hit[0].ts, self.now + timedelta(hours=1, seconds=5))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -34,12 +34,15 @@ from netx_api.topology_schemas import (
|
|||
|
||||
|
||||
CISCO_LLDP_BRIEF = """
|
||||
R2#show lldp neighbors
|
||||
Capability codes:
|
||||
(R) Router, (B) Bridge
|
||||
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
|
||||
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
|
||||
|
||||
Device ID Local Intf Hold-time Capability Port ID
|
||||
R1 Gi0/0 120 R Gi0/1
|
||||
R3 Gi0/1 120 R Gi0/0
|
||||
r1 Gi0/1 120 B,R Ethernet1/0/1
|
||||
|
||||
Total entries displayed: 1
|
||||
"""
|
||||
|
||||
CISCO_LLDP_DETAIL = """
|
||||
|
|
@ -72,8 +75,16 @@ gei-0/1/0/1 0011.2233.4455 gei-0/1/0/2 R1
|
|||
class LldpParserTests(unittest.TestCase):
|
||||
def test_cisco_brief(self) -> None:
|
||||
hits = lldp.parse_cisco_lldp(CISCO_LLDP_BRIEF)
|
||||
self.assertGreaterEqual(len(hits), 1)
|
||||
self.assertTrue(any(h.remote_name.upper().startswith("R1") for h in hits))
|
||||
self.assertEqual(len(hits), 1)
|
||||
self.assertEqual(hits[0].remote_name.lower(), "r1")
|
||||
self.assertEqual(hits[0].local_port, "Gi0/1")
|
||||
self.assertEqual(hits[0].remote_port, "Ethernet1/0/1")
|
||||
|
||||
def test_cisco_detail_fallback(self) -> None:
|
||||
hits = lldp.parse_cisco_lldp(CISCO_LLDP_DETAIL)
|
||||
self.assertEqual(len(hits), 1)
|
||||
self.assertEqual(hits[0].remote_name.lower(), "r1")
|
||||
self.assertEqual(hits[0].remote_ip, "192.168.0.1")
|
||||
|
||||
def test_huawei(self) -> None:
|
||||
hits = lldp.parse_huawei_lldp(HUAWEI_LLDP)
|
||||
|
|
@ -83,7 +94,7 @@ class LldpParserTests(unittest.TestCase):
|
|||
def test_pick_command_lldp_only(self) -> None:
|
||||
cmd, tag = lldp.pick_neighbor_command(protocol="cdp", vendor="Cisco", device_type="cisco_ios")
|
||||
self.assertEqual(tag, "lldp")
|
||||
self.assertIn("lldp", cmd.lower())
|
||||
self.assertEqual(cmd, "show lldp neighbors detail")
|
||||
|
||||
|
||||
class FabricTopologyTests(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -503,6 +503,8 @@ class WebcrtServiceTests(unittest.TestCase):
|
|||
rows=24,
|
||||
conn=conn, # type: ignore[arg-type]
|
||||
)
|
||||
sess.state = "ready"
|
||||
sess.connect_finished_at = time.time()
|
||||
sess.attached = True
|
||||
with svc._sessions_lock:
|
||||
svc._sessions["grace"] = sess
|
||||
|
|
@ -522,6 +524,42 @@ class WebcrtServiceTests(unittest.TestCase):
|
|||
svc._reap_sessions()
|
||||
self.assertIsNone(svc.get_session("grace"))
|
||||
|
||||
@patch.object(svc, "_audit")
|
||||
def test_list_sessions_lifecycle_ready_vs_detached(self, _mock_audit: MagicMock) -> None:
|
||||
conn = _FakeConn()
|
||||
sess = svc.WebcrtSession(
|
||||
session_id="life",
|
||||
ne_id="ne1",
|
||||
ne_name="lab",
|
||||
ne_ip="1.2.3.4",
|
||||
protocol="ssh",
|
||||
cols=80,
|
||||
rows=24,
|
||||
conn=conn, # type: ignore[arg-type]
|
||||
)
|
||||
sess.state = "ready"
|
||||
sess.attached = True
|
||||
with svc._sessions_lock:
|
||||
svc._sessions["life"] = sess
|
||||
ready_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
|
||||
self.assertEqual(ready_rows["life"]["lifecycle"], "ready")
|
||||
self.assertTrue(ready_rows["life"]["attached"])
|
||||
|
||||
svc.detach_session("life", grace_sec=120.0, attach_gen=0)
|
||||
detached_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
|
||||
self.assertEqual(detached_rows["life"]["lifecycle"], "detached")
|
||||
self.assertFalse(detached_rows["life"]["attached"])
|
||||
self.assertIsNotNone(detached_rows["life"]["detach_deadline"])
|
||||
|
||||
sess.state = "connecting"
|
||||
sess.attached = False
|
||||
connecting_rows = {r["session_id"]: r for r in svc.list_sessions()["items"]}
|
||||
self.assertEqual(connecting_rows["life"]["lifecycle"], "connecting")
|
||||
self.assertIsInstance(connecting_rows["life"]["elapsed_ms"], int)
|
||||
|
||||
svc.close_session("life", reason="test")
|
||||
self.assertEqual(svc.list_sessions()["total"], 0)
|
||||
|
||||
@patch.object(svc, "_audit")
|
||||
def test_attach_timeout_reaper(self, _mock_audit: MagicMock) -> None:
|
||||
conn = _FakeConn()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue