06:25
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
NATS Registration Listener (with NetBox hostname lookup for fox100 + timing + problem counter)
|
NATS Registration Listener (fox100 + NetBox hostname/tag lookup + timing + problem counter)
|
||||||
----------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------
|
||||||
Adds:
|
Adds:
|
||||||
- nb_problems=<count> prefix on every stdout line
|
- nb_problems=<count> prefix on every stdout line
|
||||||
|
- For product=fox100: MAC -> interface -> device lookup in NetBox (urllib only)
|
||||||
|
- Device tag check for slug 'fox100-auto-upgrade-latest' when status=active
|
||||||
|
- If tag present & active: append "action=ok, i'm ready to schedule this device <hostname> upgrade"
|
||||||
- Measures per-fox100 NetBox lookup duration (netbox_ms)
|
- Measures per-fox100 NetBox lookup duration (netbox_ms)
|
||||||
- Measures total latency from message arrival till final output (total_ms)
|
- Measures total latency (total_ms)
|
||||||
- Increments problem counter for each NetBox anomaly or lookup issue
|
- Increments problem counter for NetBox anomalies, includes iface_id where applicable
|
||||||
- Adds iface_id=... to stderr diagnostics
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -21,7 +23,7 @@ import hashlib
|
|||||||
import time
|
import time
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any, Tuple
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
from urllib.error import URLError, HTTPError
|
from urllib.error import URLError, HTTPError
|
||||||
@@ -36,7 +38,13 @@ NB_URL = "http://netbox.gt-tiso.ikeja.co.za" # Base URL
|
|||||||
NB_TOKEN = "7648e4f5ee370cda7834682e61b47c2ee8e95623" # keep as provided
|
NB_TOKEN = "7648e4f5ee370cda7834682e61b47c2ee8e95623" # keep as provided
|
||||||
NB_TIMEOUT = 3.0 # seconds per HTTP GET
|
NB_TIMEOUT = 3.0 # seconds per HTTP GET
|
||||||
|
|
||||||
# Global counter for any NetBox-related errors/anomalies
|
# Product -> Tag slug mapping (future-proof for fox200 later)
|
||||||
|
PRODUCT_TAG_SLUG = {
|
||||||
|
"fox100": "fox100-auto-upgrade-latest",
|
||||||
|
# "fox200": "fox200-auto-upgrade-latest", # placeholder for later
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global counter for any NetBox-related problems
|
||||||
NB_PROBLEM_COUNTER = 0
|
NB_PROBLEM_COUNTER = 0
|
||||||
NB_PROBLEM_LOCK = asyncio.Lock()
|
NB_PROBLEM_LOCK = asyncio.Lock()
|
||||||
|
|
||||||
@@ -148,13 +156,15 @@ async def nb_problem(log_status, msg: str):
|
|||||||
await log_status(msg)
|
await log_status(msg)
|
||||||
|
|
||||||
|
|
||||||
def nb_get_hostname_by_mac(mac: str, log_status) -> (Optional[str], Optional[int]):
|
def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Return (hostname, iface_id or None)
|
Resolve MAC -> (hostname, iface_id, device_id, device_status_value)
|
||||||
|
- Logs problems (async) for anomalies, but per Pavel's ask we treat device-fetch failures as "no tag info"
|
||||||
|
(i.e., we WON'T count those as problems unless it's a clear anomaly like mac not found / unassigned / wrong type).
|
||||||
"""
|
"""
|
||||||
mac_norm = normalize_mac(mac)
|
mac_norm = normalize_mac(mac)
|
||||||
if not mac_norm:
|
if not mac_norm:
|
||||||
return None, None
|
return None, None, None, None
|
||||||
|
|
||||||
base = NB_URL.rstrip("/")
|
base = NB_URL.rstrip("/")
|
||||||
h = {
|
h = {
|
||||||
@@ -167,39 +177,75 @@ def nb_get_hostname_by_mac(mac: str, log_status) -> (Optional[str], Optional[int
|
|||||||
data, code = http_get_json(f"{base}/api/dcim/mac-addresses/", params={"mac_address": mac_norm, "limit": "2"}, headers=h)
|
data, code = http_get_json(f"{base}/api/dcim/mac-addresses/", params={"mac_address": mac_norm, "limit": "2"}, headers=h)
|
||||||
if code != 200 or not data:
|
if code != 200 or not data:
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac query http={code} mac={mac_norm}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac query http={code} mac={mac_norm}"))
|
||||||
return None, None
|
return None, None, None, None
|
||||||
|
|
||||||
results = (data or {}).get("results") or []
|
results = (data or {}).get("results") or []
|
||||||
if not results:
|
if not results:
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac not found mac={mac_norm}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac not found mac={mac_norm}"))
|
||||||
return None, None
|
return None, None, None, None
|
||||||
if len(results) > 1:
|
|
||||||
# no iface_id yet; fetch it below
|
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: multiple mac records mac={mac_norm}"))
|
|
||||||
|
|
||||||
rec = results[0]
|
rec = results[0]
|
||||||
aot = (rec.get("assigned_object_type") or "").strip()
|
aot = (rec.get("assigned_object_type") or "").strip()
|
||||||
aoid = rec.get("assigned_object_id")
|
aoid = rec.get("assigned_object_id")
|
||||||
|
|
||||||
|
# If multiple results, report with iface_id if we can infer one from the first record
|
||||||
|
if len(results) > 1:
|
||||||
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: multiple mac records mac={mac_norm} iface_id={aoid if aoid is not None else '-'}"))
|
||||||
|
|
||||||
if not aot or aoid is None:
|
if not aot or aoid is None:
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac unassigned mac={mac_norm}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac unassigned mac={mac_norm}"))
|
||||||
return None, None
|
return None, None, None, None
|
||||||
if aot != "dcim.interface":
|
if aot != "dcim.interface":
|
||||||
|
# We can still print the aoid for context
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac assigned to {aot} mac={mac_norm} iface_id={aoid}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: mac assigned to {aot} mac={mac_norm} iface_id={aoid}"))
|
||||||
return None, aoid
|
return None, aoid, None, None
|
||||||
|
|
||||||
# Step 2: Interface → Device
|
# Step 2: Interface -> Device (shallow device obj)
|
||||||
iface, code2 = http_get_json(f"{base}/api/dcim/interfaces/{aoid}/", headers=h)
|
iface, code2 = http_get_json(f"{base}/api/dcim/interfaces/{aoid}/", headers=h)
|
||||||
if code2 != 200 or not iface:
|
if code2 != 200 or not iface:
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: iface fetch http={code2} iface_id={aoid}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: iface fetch http={code2} iface_id={aoid}"))
|
||||||
return None, aoid
|
return None, aoid, None, None
|
||||||
|
|
||||||
dev = iface.get("device") or {}
|
dev = iface.get("device") or {}
|
||||||
host = dev.get("name") or dev.get("display")
|
host = dev.get("name") or dev.get("display")
|
||||||
if not host:
|
dev_id = dev.get("id")
|
||||||
|
if not host or dev_id is None:
|
||||||
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: iface has no device iface_id={aoid}"))
|
asyncio.create_task(nb_problem(log_status, f"[{ts()}] nb: iface has no device iface_id={aoid}"))
|
||||||
return None, aoid
|
return None, aoid, None, None
|
||||||
|
|
||||||
return host, aoid
|
# Step 3: Device detail for status/tags
|
||||||
|
# NOTE: As requested, failures here are treated as "no tag info" (no problem increment).
|
||||||
|
device, code3 = http_get_json(f"{base}/api/dcim/devices/{dev_id}/", headers=h)
|
||||||
|
if code3 != 200 or not device:
|
||||||
|
# Treat as no tag data; return host/id so upstream can still print hostname
|
||||||
|
return host, aoid, dev_id, None
|
||||||
|
|
||||||
|
status_val = ((device.get("status") or {}).get("value")) or None
|
||||||
|
return host, aoid, dev_id, status_val if isinstance(status_val, str) else None
|
||||||
|
|
||||||
|
|
||||||
|
def nb_device_has_tag(dev_id: int, desired_slug: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Return True/False if we can reliably determine presence of the tag by slug on the device.
|
||||||
|
Return None if we couldn't fetch/parse (treat as 'not present' by caller).
|
||||||
|
"""
|
||||||
|
if dev_id is None:
|
||||||
|
return None
|
||||||
|
base = NB_URL.rstrip("/")
|
||||||
|
h = {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Token {NB_TOKEN}",
|
||||||
|
}
|
||||||
|
device, code = http_get_json(f"{base}/api/dcim/devices/{dev_id}/", headers=h)
|
||||||
|
if code != 200 or not device:
|
||||||
|
return None
|
||||||
|
tags = device.get("tags") or []
|
||||||
|
for t in tags:
|
||||||
|
slug = t.get("slug")
|
||||||
|
if isinstance(slug, str) and slug == desired_slug:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
@@ -251,6 +297,7 @@ async def main():
|
|||||||
t_start = monotonic()
|
t_start = monotonic()
|
||||||
payload = msg.data
|
payload = msg.data
|
||||||
|
|
||||||
|
# strict payload dedupe
|
||||||
digest = hashlib.blake2b(payload, digest_size=16).digest()
|
digest = hashlib.blake2b(payload, digest_size=16).digest()
|
||||||
nowm = monotonic()
|
nowm = monotonic()
|
||||||
exp = recent_payloads.get(digest)
|
exp = recent_payloads.get(digest)
|
||||||
@@ -272,16 +319,32 @@ async def main():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
host_suffix = ""
|
host_suffix = ""
|
||||||
|
action_suffix = ""
|
||||||
netbox_time_ms = 0.0
|
netbox_time_ms = 0.0
|
||||||
|
|
||||||
|
# fox100 logic (future: can generalize by product -> tag)
|
||||||
|
desired_slug = PRODUCT_TAG_SLUG.get(product)
|
||||||
if product == "fox100":
|
if product == "fox100":
|
||||||
nb_start = time.perf_counter()
|
nb_start = time.perf_counter()
|
||||||
|
host = None
|
||||||
|
dev_id = None
|
||||||
try:
|
try:
|
||||||
host, iface_id = nb_get_hostname_by_mac(mac=mac, log_status=log_status)
|
host, iface_id, dev_id, status_val = nb_lookup_device_by_mac(mac=mac, log_status=log_status)
|
||||||
if host:
|
if host:
|
||||||
host_suffix = f" host={host}"
|
host_suffix = f" host={host}"
|
||||||
if iface_id and not host:
|
if iface_id and not host:
|
||||||
host_suffix += f" iface_id={iface_id}"
|
host_suffix += f" iface_id={iface_id}"
|
||||||
|
|
||||||
|
# Only attempt tag check if we have a device id AND status=active AND a desired slug
|
||||||
|
if dev_id is not None and status_val == "active" and isinstance(desired_slug, str):
|
||||||
|
has_tag = nb_device_has_tag(dev_id, desired_slug)
|
||||||
|
if has_tag is True:
|
||||||
|
# CAP: print readiness to schedule upgrade (no side effects yet)
|
||||||
|
# Keep it on the same stdout line
|
||||||
|
action_suffix = f" action=ok, i'm ready to schedule this device {host if host else dev_id} upgrade"
|
||||||
|
# If has_tag is False/None, just continue silently
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Unexpected NetBox error: count as a problem and continue
|
||||||
await nb_problem(log_status, f"[{ts()}] nb: unexpected error mac={mac!r} err={e!r}")
|
await nb_problem(log_status, f"[{ts()}] nb: unexpected error mac={mac!r} err={e!r}")
|
||||||
netbox_time_ms = (time.perf_counter() - nb_start) * 1000
|
netbox_time_ms = (time.perf_counter() - nb_start) * 1000
|
||||||
|
|
||||||
@@ -290,7 +353,7 @@ async def main():
|
|||||||
async with NB_PROBLEM_LOCK:
|
async with NB_PROBLEM_LOCK:
|
||||||
nb_problems_snapshot = NB_PROBLEM_COUNTER
|
nb_problems_snapshot = NB_PROBLEM_COUNTER
|
||||||
|
|
||||||
line = f"nb_problems={nb_problems_snapshot} [{ts()}] product={product} mac={mac} fw={fw}{host_suffix}"
|
line = f"nb_problems={nb_problems_snapshot} [{ts()}] product={product} mac={mac} fw={fw}{host_suffix}{action_suffix}"
|
||||||
if product == "fox100":
|
if product == "fox100":
|
||||||
line += f" netbox_ms={netbox_time_ms:.1f} total_ms={total_ms:.1f}"
|
line += f" netbox_ms={netbox_time_ms:.1f} total_ms={total_ms:.1f}"
|
||||||
if args.include_subject:
|
if args.include_subject:
|
||||||
|
|||||||
Reference in New Issue
Block a user