20:38
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
NATS Registration Listener (fox100 + NetBox hostname/tag lookup + timing + problem counter)
|
NATS Registration Listener (fox100 + NetBox hostname/upgrade_cmd lookup + timing + problem counter)
|
||||||
-------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------
|
||||||
- One device GET (status + tags), no duplicate fetch
|
- One device GET (status + tags + custom_fields.upgrade_cmd), no duplicate fetch
|
||||||
- On tag match (and status=active), prepend ASCII BEL to stdout line to alert
|
- If fox100 and upgrade_cmd present -> prepend 3x ASCII BEL to stdout line
|
||||||
|
- Publish logic for fox100:
|
||||||
|
* upgrade_cmd empty/absent -> publish with task_name="ot-updater"
|
||||||
|
* upgrade_cmd present -> publish with task_name="sot-updater-upgradecmd" (+ 3 BELs)
|
||||||
- Keeps: nb_problems counter, timings, iface_id diagnostics, same formatting
|
- Keeps: nb_problems counter, timings, iface_id diagnostics, same formatting
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ from typing import Optional, Dict, Any, Tuple, Set
|
|||||||
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
|
||||||
import base64 # NEW: for RabbitMQ Basic Auth
|
import base64 # for RabbitMQ Basic Auth
|
||||||
|
|
||||||
import nats
|
import nats
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ RMQ_EXCHANGE_WORK = "deviceconfig" # direct exchange (immediate)
|
|||||||
RMQ_ROUTING_KEY = "deviceconfig"
|
RMQ_ROUTING_KEY = "deviceconfig"
|
||||||
RMQ_TIMEOUT = 3.0
|
RMQ_TIMEOUT = 3.0
|
||||||
|
|
||||||
# Product -> Tag slug mapping (future-proof for fox200 later)
|
# Product -> Tag slug mapping (kept unchanged, though not used now)
|
||||||
PRODUCT_TAG_SLUG = {
|
PRODUCT_TAG_SLUG = {
|
||||||
"fox100": "fox100-auto-upgrade-latest",
|
"fox100": "fox100-auto-upgrade-latest",
|
||||||
# "fox200": "fox200-auto-upgrade-latest",
|
# "fox200": "fox200-auto-upgrade-latest",
|
||||||
@@ -156,7 +159,7 @@ def http_get_json(url: str, params: Optional[Dict[str, str]] = None, headers: Op
|
|||||||
return None, 597
|
return None, 597
|
||||||
|
|
||||||
|
|
||||||
# NEW: RabbitMQ management API POST helper (basic auth; JSON in/out)
|
# RabbitMQ management API POST helper (basic auth; JSON in/out)
|
||||||
def http_post_json(url: str, payload_obj: Dict[str, Any], user: Optional[str] = None, password: Optional[str] = None, timeout: float = RMQ_TIMEOUT):
|
def http_post_json(url: str, payload_obj: Dict[str, Any], user: Optional[str] = None, password: Optional[str] = None, timeout: float = RMQ_TIMEOUT):
|
||||||
body = json.dumps(payload_obj).encode("utf-8")
|
body = json.dumps(payload_obj).encode("utf-8")
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
@@ -187,15 +190,15 @@ async def nb_problem(log_status, msg: str):
|
|||||||
await log_status(msg)
|
await log_status(msg)
|
||||||
|
|
||||||
|
|
||||||
def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[str], Optional[Set[str]]]:
|
def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Optional[int], Optional[int], Optional[str], Optional[Set[str]], Optional[Any]]:
|
||||||
"""
|
"""
|
||||||
Resolve MAC -> (hostname, iface_id, device_id, device_status_value, tag_slugs_set)
|
Resolve MAC -> (hostname, iface_id, device_id, device_status_value, tag_slugs_set, upgrade_cmd)
|
||||||
- Logs problems for anomalies (mac not found, unassigned, wrong type, iface fetch fail).
|
- Logs problems for anomalies (mac not found, unassigned, wrong type, iface fetch fail).
|
||||||
- If device detail fetch fails, returns host/id with status/tags as None (treated as "no tag info").
|
- If device detail fetch fails, returns host/id with status/tags/upgrade_cmd as None.
|
||||||
"""
|
"""
|
||||||
mac_norm = normalize_mac(mac)
|
mac_norm = normalize_mac(mac)
|
||||||
if not mac_norm:
|
if not mac_norm:
|
||||||
return None, None, None, None, None
|
return None, None, None, None, None, None
|
||||||
|
|
||||||
base = NB_URL.rstrip("/")
|
base = NB_URL.rstrip("/")
|
||||||
h = {
|
h = {
|
||||||
@@ -208,12 +211,12 @@ def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Option
|
|||||||
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, None, None, None
|
return None, None, 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, None, None, None
|
return None, None, None, None, None, None
|
||||||
|
|
||||||
rec = results[0]
|
rec = results[0]
|
||||||
aot = (rec.get("assigned_object_type") or "").strip()
|
aot = (rec.get("assigned_object_type") or "").strip()
|
||||||
@@ -224,29 +227,29 @@ def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Option
|
|||||||
|
|
||||||
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, None, None, None
|
return None, None, None, None, None, None
|
||||||
if aot != "dcim.interface":
|
if aot != "dcim.interface":
|
||||||
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, None, None, None
|
return None, aoid, None, None, None, None
|
||||||
|
|
||||||
# Step 2: Interface -> Device (shallow)
|
# Step 2: Interface -> Device (shallow)
|
||||||
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, None, None, None
|
return None, aoid, None, None, 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")
|
||||||
dev_id = dev.get("id")
|
dev_id = dev.get("id")
|
||||||
if not host or dev_id is None:
|
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, None, None, None
|
return None, aoid, None, None, None, None
|
||||||
|
|
||||||
# Step 3: Device detail (single fetch for BOTH status and tags)
|
# Step 3: Device detail (single fetch for status, tags, custom_fields.upgrade_cmd)
|
||||||
device, code3 = http_get_json(f"{base}/api/dcim/devices/{dev_id}/", headers=h)
|
device, code3 = http_get_json(f"{base}/api/dcim/devices/{dev_id}/", headers=h)
|
||||||
if code3 != 200 or not device:
|
if code3 != 200 or not device:
|
||||||
# treat as "no tag info" (no problem increment)
|
# treat as "no extra info"
|
||||||
return host, aoid, dev_id, None, None
|
return host, aoid, dev_id, None, None, None
|
||||||
|
|
||||||
status_val = ((device.get("status") or {}).get("value")) or None
|
status_val = ((device.get("status") or {}).get("value")) or None
|
||||||
tags = device.get("tags") or []
|
tags = device.get("tags") or []
|
||||||
@@ -256,7 +259,10 @@ def nb_lookup_device_by_mac(mac: str, log_status) -> Tuple[Optional[str], Option
|
|||||||
if isinstance(slug, str):
|
if isinstance(slug, str):
|
||||||
tag_slugs.add(slug)
|
tag_slugs.add(slug)
|
||||||
|
|
||||||
return host, aoid, dev_id, (status_val if isinstance(status_val, str) else None), tag_slugs
|
cf = device.get("custom_fields") or {}
|
||||||
|
upgrade_cmd = cf.get("upgrade_cmd")
|
||||||
|
|
||||||
|
return host, aoid, dev_id, (status_val if isinstance(status_val, str) else None), tag_slugs, upgrade_cmd
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
@@ -330,30 +336,40 @@ async def main():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
host_suffix = ""
|
host_suffix = ""
|
||||||
action_suffix = ""
|
action_suffix = "" # kept; no longer used for tag action
|
||||||
bell_prefix = "" # ASCII BEL when we have a tag match
|
bell_prefix = "" # ASCII BEL when upgrade_cmd present (3x)
|
||||||
netbox_time_ms = 0.0
|
netbox_time_ms = 0.0
|
||||||
|
|
||||||
desired_slug = PRODUCT_TAG_SLUG.get(product)
|
# NOTE: no tag check anymore; behavior depends on upgrade_cmd only
|
||||||
if product == "fox100":
|
if product == "fox100":
|
||||||
nb_start = time.perf_counter()
|
nb_start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
host, iface_id, dev_id, status_val, tag_slugs = nb_lookup_device_by_mac(mac=mac, log_status=log_status)
|
host, iface_id, dev_id, status_val, tag_slugs, upgrade_cmd = 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}"
|
||||||
|
|
||||||
# Tag-based action (unchanged)
|
# NEW LOGIC:
|
||||||
if dev_id is not None and status_val == "active" and isinstance(desired_slug, str) and isinstance(tag_slugs, set):
|
# - If host known:
|
||||||
if desired_slug in tag_slugs:
|
# * upgrade_cmd empty/absent -> publish with task_name="ot-updater"
|
||||||
bell_prefix = "\a" * 5
|
# * upgrade_cmd present -> publish with task_name="sot-updater-upgradecmd" and ring 3 BELs
|
||||||
action_suffix = f" action=ok, i'm ready to schedule this device {host if host else dev_id} upgrade"
|
|
||||||
|
|
||||||
# NEW: Publish immediate RMQ message like rmq-ikeja-pub3.sh (no delay)
|
|
||||||
if host:
|
if host:
|
||||||
try:
|
try:
|
||||||
rmq_url = f"http://{RMQ_HOST}:{RMQ_PORT}/api/exchanges/{RMQ_VHOST}/{RMQ_EXCHANGE_WORK}/publish"
|
rmq_url = f"http://{RMQ_HOST}:{RMQ_PORT}/api/exchanges/{RMQ_VHOST}/{RMQ_EXCHANGE_WORK}/publish"
|
||||||
|
|
||||||
|
has_upgrade_cmd = False
|
||||||
|
try:
|
||||||
|
# consider non-empty string or any truthy value as "present"
|
||||||
|
if isinstance(upgrade_cmd, str):
|
||||||
|
has_upgrade_cmd = len(upgrade_cmd.strip()) > 0
|
||||||
|
else:
|
||||||
|
has_upgrade_cmd = bool(upgrade_cmd)
|
||||||
|
except Exception:
|
||||||
|
has_upgrade_cmd = False
|
||||||
|
|
||||||
|
task_name = "sot-updater-upgradecmd" if has_upgrade_cmd else "ot-updater"
|
||||||
|
|
||||||
rmq_body = {
|
rmq_body = {
|
||||||
"properties": {
|
"properties": {
|
||||||
"content_type": "application/json"
|
"content_type": "application/json"
|
||||||
@@ -361,7 +377,7 @@ async def main():
|
|||||||
"routing_key": RMQ_ROUTING_KEY,
|
"routing_key": RMQ_ROUTING_KEY,
|
||||||
"payload": json.dumps({
|
"payload": json.dumps({
|
||||||
"inscope_device": host,
|
"inscope_device": host,
|
||||||
"task_name": "sot-updater-upgradecmd",
|
"task_name": task_name,
|
||||||
}),
|
}),
|
||||||
"payload_encoding": "string",
|
"payload_encoding": "string",
|
||||||
}
|
}
|
||||||
@@ -369,7 +385,6 @@ async def main():
|
|||||||
if code != 200:
|
if code != 200:
|
||||||
await log_status(f"[{ts()}] rmq: publish immediate http={code} host={host}")
|
await log_status(f"[{ts()}] rmq: publish immediate http={code} host={host}")
|
||||||
else:
|
else:
|
||||||
# if response JSON has 'routed' false, note it (rmq-ikeja-pub3.sh warns in that case)
|
|
||||||
routed = False
|
routed = False
|
||||||
try:
|
try:
|
||||||
routed = bool((resp or {}).get("routed", False))
|
routed = bool((resp or {}).get("routed", False))
|
||||||
@@ -377,6 +392,11 @@ async def main():
|
|||||||
routed = False
|
routed = False
|
||||||
if not routed:
|
if not routed:
|
||||||
await log_status(f"[{ts()}] rmq: publish immediate routed=false host={host}")
|
await log_status(f"[{ts()}] rmq: publish immediate routed=false host={host}")
|
||||||
|
|
||||||
|
# Bell behavior: ring 3x BEL only if upgrade_cmd present
|
||||||
|
if has_upgrade_cmd:
|
||||||
|
bell_prefix = "\a" * 3
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await log_status(f"[{ts()}] rmq: unexpected error host={host!r} err={e!r}")
|
await log_status(f"[{ts()}] rmq: unexpected error host={host!r} err={e!r}")
|
||||||
|
|
||||||
@@ -396,7 +416,7 @@ async def main():
|
|||||||
line += f" subject={msg.subject}"
|
line += f" subject={msg.subject}"
|
||||||
|
|
||||||
async with print_lock:
|
async with print_lock:
|
||||||
# Prepend BEL only when we had a tag match
|
# Prepend BEL only when we had upgrade_cmd present (3x)
|
||||||
sys.stdout.write(bell_prefix + line + "\n")
|
sys.stdout.write(bell_prefix + line + "\n")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user