Update 2026-07-14 10:41:57
This commit is contained in:
parent
2ab8a46d44
commit
7d9c4f803f
@ -53,3 +53,13 @@ LOGS_INTERVAL=60
|
||||
PROJECT_LOG_CONTAINER=auto
|
||||
PROJECT_LOG_PATH=
|
||||
PROJECT_LOG_LABEL=
|
||||
|
||||
# ── remote dashboard (register the Sanad web UI for the fleet to embed) ───────
|
||||
# The agent probes these ports on the robot, finds the Sanad dashboard, and
|
||||
# POSTs its URL to /{sn}/remote (kind=web) — no changes to the Sanad app.
|
||||
REMOTE_ENABLE=1
|
||||
REMOTE_PORTS=8001,8014,8011,8012,8013,8000,8080
|
||||
REMOTE_KIND=web
|
||||
# REMOTE_HOST= # default: the robot's primary LAN IP (fleet-reachable)
|
||||
# REMOTE_URL= # pin an explicit URL (e.g. a public reverse-tunnel) — wins over discovery
|
||||
REMOTE_INTERVAL=60
|
||||
|
||||
@ -140,6 +140,14 @@ class Config:
|
||||
alert_endpoint: str
|
||||
logs_endpoint: str
|
||||
logs_interval: float
|
||||
# remote dashboard (register the Sanad UI URL for the fleet to embed)
|
||||
remote_enable: bool
|
||||
remote_endpoint: str
|
||||
remote_kind: str
|
||||
remote_host: str
|
||||
remote_ports: str
|
||||
remote_url: str
|
||||
remote_interval: float
|
||||
# project logs (e.g. the robot's Sanad app) shipped alongside agent logs
|
||||
project_log_container: str
|
||||
project_log_path: str
|
||||
@ -195,6 +203,13 @@ class Config:
|
||||
alert_endpoint=_env("ALERT_ENDPOINT", "/api/v1/fleet/ingest/{sn}/alert"),
|
||||
logs_endpoint=_env("LOGS_ENDPOINT", "/api/v1/fleet/ingest/{sn}/logs"),
|
||||
logs_interval=float(_env("LOGS_INTERVAL", "60")),
|
||||
remote_enable=_env_bool("REMOTE_ENABLE", True),
|
||||
remote_endpoint=_env("REMOTE_ENDPOINT", "/api/v1/fleet/ingest/{sn}/remote"),
|
||||
remote_kind=_env("REMOTE_KIND", "web"),
|
||||
remote_host=_env("REMOTE_HOST", ""),
|
||||
remote_ports=_env("REMOTE_PORTS", "8001,8014,8011,8012,8013,8000,8080"),
|
||||
remote_url=_env("REMOTE_URL", ""), # explicit URL (e.g. a public tunnel) wins
|
||||
remote_interval=float(_env("REMOTE_INTERVAL", "60")),
|
||||
project_log_container=_env("PROJECT_LOG_CONTAINER", "auto"),
|
||||
project_log_path=_env("PROJECT_LOG_PATH", ""),
|
||||
project_log_label=_env("PROJECT_LOG_LABEL", ""),
|
||||
@ -220,6 +235,9 @@ class Config:
|
||||
def logs_url(self) -> str:
|
||||
return self.server_url + self.logs_endpoint.format(sn=self.sn)
|
||||
|
||||
def remote_url_ep(self) -> str:
|
||||
return self.server_url + self.remote_endpoint.format(sn=self.sn)
|
||||
|
||||
def auth_headers(self) -> Dict[str, str]:
|
||||
return {"Authorization": f"Bearer {self.device_token}"}
|
||||
|
||||
@ -1295,6 +1313,78 @@ def logs_loop(cfg: Config, session: requests.Session) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# remote dashboard — discover the Sanad web UI and register its URL for the
|
||||
# fleet dashboard to embed ("full dashboard through the fleet API"). No changes
|
||||
# to the Sanad app: we only probe its port and POST the URL to /{sn}/remote.
|
||||
# --------------------------------------------------------------------------- #
|
||||
_REMOTE_STAT: Dict[str, Any] = {"url": None, "port": None, "kind": None, "ok": None}
|
||||
|
||||
|
||||
def _primary_ip() -> str:
|
||||
import socket
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("1.1.1.1", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def discover_dashboard(cfg: Config) -> Optional[Dict[str, Any]]:
|
||||
"""Find the Sanad dashboard: probe each candidate port on localhost; the one
|
||||
that returns 200 with a 'Sanad'/dashboard page wins. Returns {url, port}."""
|
||||
if cfg.remote_url: # explicit URL (e.g. a tunnel) overrides
|
||||
return {"url": cfg.remote_url, "port": None}
|
||||
host = cfg.remote_host or _primary_ip() # the LAN IP the fleet can reach
|
||||
for tok in cfg.remote_ports.split(","):
|
||||
tok = tok.strip()
|
||||
if not tok.isdigit():
|
||||
continue
|
||||
port = int(tok)
|
||||
try:
|
||||
r = requests.get(f"http://127.0.0.1:{port}/", timeout=2)
|
||||
except requests.RequestException:
|
||||
continue
|
||||
if r.status_code == 200 and ("sanad" in r.text.lower() or "dashboard" in r.text.lower()):
|
||||
return {"url": f"http://{host}:{port}", "port": port}
|
||||
return None
|
||||
|
||||
|
||||
def register_remote(cfg: Config, session: requests.Session) -> None:
|
||||
if not cfg.remote_enable:
|
||||
return
|
||||
d = discover_dashboard(cfg)
|
||||
if not d:
|
||||
_REMOTE_STAT.update(url=None, port=None, ok=None)
|
||||
return
|
||||
body = {"sn": cfg.sn, "name": cfg.name, "kind": cfg.remote_kind,
|
||||
"url": d["url"], "label": f"{cfg.name} — Sanad Dashboard",
|
||||
"port": d["port"], "ts": int(time.time())}
|
||||
try:
|
||||
r = session.post(cfg.remote_url_ep(), json=body, headers=cfg.auth_headers(),
|
||||
timeout=cfg.http_timeout, verify=cfg.verify_tls)
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=bool(r.ok))
|
||||
if r.ok:
|
||||
log.info("remote dashboard registered: %s -> HTTP %s", d["url"], r.status_code)
|
||||
else:
|
||||
log.warning("remote register failed: HTTP %s %s", r.status_code, r.text[:150])
|
||||
except requests.RequestException as e:
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=False)
|
||||
log.debug("remote register failed: %s", e)
|
||||
|
||||
|
||||
def remote_loop(cfg: Config, session: requests.Session) -> None:
|
||||
while True:
|
||||
try:
|
||||
register_remote(cfg, session)
|
||||
except Exception as e:
|
||||
log.debug("remote loop: %s", e)
|
||||
time.sleep(cfg.remote_interval)
|
||||
|
||||
|
||||
_ALERT_SEEN: set = set()
|
||||
|
||||
|
||||
@ -1407,6 +1497,8 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader],
|
||||
"project_logs": (_PROJECT_TAIL.label
|
||||
if _PROJECT_TAIL is not None and _PROJECT_TAIL.active
|
||||
else None), # e.g. "sanadr1-logs"; null = no project found
|
||||
"remote": (dict(_REMOTE_STAT) if _REMOTE_STAT.get("url") else None),
|
||||
# Sanad dashboard URL registered for the fleet UI
|
||||
"alerts": dict(_ALERTS_STAT), # alert status (sent, last, last_time, ok)
|
||||
"time": _now_str(), # full date+time of this post
|
||||
"started_at": _STARTED["now"], # when this agent run started
|
||||
@ -1516,6 +1608,12 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if args.once or args.dry_run:
|
||||
# one map pass first so the telemetry "map" field reflects it
|
||||
map_sync_once(cfg, session, force=args.force, dry_run=args.dry_run)
|
||||
if cfg.remote_enable and not args.dry_run:
|
||||
register_remote(cfg, session)
|
||||
elif cfg.remote_enable:
|
||||
d = discover_dashboard(cfg)
|
||||
if d:
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=None)
|
||||
if args.once:
|
||||
one_telemetry()
|
||||
ship_logs(cfg, session)
|
||||
@ -1525,9 +1623,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
time.sleep(min(cfg.poll_interval, 1.0))
|
||||
return 0
|
||||
|
||||
# loop mode: map sync + log shipping in background threads
|
||||
# loop mode: map sync + log shipping + remote registration in bg threads
|
||||
threading.Thread(target=map_loop, args=(cfg, session), daemon=True).start()
|
||||
threading.Thread(target=logs_loop, args=(cfg, session), daemon=True).start()
|
||||
if cfg.remote_enable:
|
||||
threading.Thread(target=remote_loop, args=(cfg, session), daemon=True).start()
|
||||
log.info("telemetry every %.1fs; map check every %.0fs; logs every %.0fs (Ctrl-C to stop)",
|
||||
cfg.poll_interval, cfg.map_poll_interval, cfg.logs_interval)
|
||||
while True:
|
||||
|
||||
@ -59,3 +59,13 @@ LOGS_INTERVAL=60
|
||||
PROJECT_LOG_CONTAINER=auto
|
||||
PROJECT_LOG_PATH=
|
||||
PROJECT_LOG_LABEL=
|
||||
|
||||
# ── remote dashboard (register the Sanad web UI for the fleet to embed) ───────
|
||||
# The agent probes these ports on the robot, finds the Sanad dashboard, and
|
||||
# POSTs its URL to /{sn}/remote (kind=web) — no changes to the Sanad app.
|
||||
REMOTE_ENABLE=1
|
||||
REMOTE_PORTS=8001,8014,8011,8012,8013,8000,8080
|
||||
REMOTE_KIND=web
|
||||
# REMOTE_HOST= # default: the robot's primary LAN IP (fleet-reachable)
|
||||
# REMOTE_URL= # pin an explicit URL (e.g. a public reverse-tunnel) — wins over discovery
|
||||
REMOTE_INTERVAL=60
|
||||
|
||||
@ -123,6 +123,13 @@ class Config:
|
||||
alert_endpoint: str
|
||||
logs_endpoint: str
|
||||
logs_interval: float
|
||||
remote_enable: bool
|
||||
remote_endpoint: str
|
||||
remote_kind: str
|
||||
remote_host: str
|
||||
remote_ports: str
|
||||
remote_url: str
|
||||
remote_interval: float
|
||||
# project logs (e.g. the robot's Sanad app) shipped alongside agent logs
|
||||
project_log_container: str
|
||||
project_log_path: str
|
||||
@ -174,6 +181,13 @@ class Config:
|
||||
alert_endpoint=_env("ALERT_ENDPOINT", "/api/v1/fleet/ingest/{sn}/alert"),
|
||||
logs_endpoint=_env("LOGS_ENDPOINT", "/api/v1/fleet/ingest/{sn}/logs"),
|
||||
logs_interval=float(_env("LOGS_INTERVAL", "60")),
|
||||
remote_enable=_env_bool("REMOTE_ENABLE", True),
|
||||
remote_endpoint=_env("REMOTE_ENDPOINT", "/api/v1/fleet/ingest/{sn}/remote"),
|
||||
remote_kind=_env("REMOTE_KIND", "web"),
|
||||
remote_host=_env("REMOTE_HOST", ""),
|
||||
remote_ports=_env("REMOTE_PORTS", "8001,8014,8011,8012,8013,8000,8080"),
|
||||
remote_url=_env("REMOTE_URL", ""),
|
||||
remote_interval=float(_env("REMOTE_INTERVAL", "60")),
|
||||
project_log_container=_env("PROJECT_LOG_CONTAINER", "auto"),
|
||||
project_log_path=_env("PROJECT_LOG_PATH", ""),
|
||||
project_log_label=_env("PROJECT_LOG_LABEL", ""),
|
||||
@ -197,6 +211,9 @@ class Config:
|
||||
def logs_url(self) -> str:
|
||||
return self.server_url + self.logs_endpoint.format(sn=self.sn)
|
||||
|
||||
def remote_url_ep(self) -> str:
|
||||
return self.server_url + self.remote_endpoint.format(sn=self.sn)
|
||||
|
||||
def auth_headers(self) -> Dict[str, str]:
|
||||
return {"Authorization": f"Bearer {self.device_token}"}
|
||||
|
||||
@ -1214,6 +1231,103 @@ def logs_loop(cfg: Config, session: requests.Session) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# remote dashboard (discover Sanad web UI, register its URL via /{sn}/remote)
|
||||
# --------------------------------------------------------------------------- #
|
||||
_REMOTE_STAT: Dict[str, Any] = {"url": None, "port": None, "kind": None, "ok": None}
|
||||
|
||||
|
||||
def _primary_ip() -> str:
|
||||
import socket
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("1.1.1.1", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def discover_dashboard(cfg: Config) -> Optional[Dict[str, Any]]:
|
||||
"""Find the Sanad dashboard: probe each candidate port on localhost; the one
|
||||
that returns 200 with a 'Sanad'/dashboard page wins. Returns {url, port}."""
|
||||
if cfg.remote_url: # explicit URL (e.g. a tunnel) overrides
|
||||
return {"url": cfg.remote_url, "port": None}
|
||||
host = cfg.remote_host or _primary_ip() # the LAN IP the fleet can reach
|
||||
for tok in cfg.remote_ports.split(","):
|
||||
tok = tok.strip()
|
||||
if not tok.isdigit():
|
||||
continue
|
||||
port = int(tok)
|
||||
try:
|
||||
r = requests.get(f"http://127.0.0.1:{port}/", timeout=2)
|
||||
except requests.RequestException:
|
||||
continue
|
||||
if r.status_code == 200 and ("sanad" in r.text.lower() or "dashboard" in r.text.lower()):
|
||||
return {"url": f"http://{host}:{port}", "port": port}
|
||||
return None
|
||||
|
||||
|
||||
def register_remote(cfg: Config, session: requests.Session) -> None:
|
||||
if not cfg.remote_enable:
|
||||
return
|
||||
d = discover_dashboard(cfg)
|
||||
if not d:
|
||||
_REMOTE_STAT.update(url=None, port=None, ok=None)
|
||||
return
|
||||
body = {"sn": cfg.sn, "name": cfg.name, "kind": cfg.remote_kind,
|
||||
"url": d["url"], "label": f"{cfg.name} — Sanad Dashboard",
|
||||
"port": d["port"], "ts": int(time.time())}
|
||||
try:
|
||||
r = session.post(cfg.remote_url_ep(), json=body, headers=cfg.auth_headers(),
|
||||
timeout=cfg.http_timeout, verify=cfg.verify_tls)
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=bool(r.ok))
|
||||
if r.ok:
|
||||
log.info("remote dashboard registered: %s -> HTTP %s", d["url"], r.status_code)
|
||||
else:
|
||||
log.warning("remote register failed: HTTP %s %s", r.status_code, r.text[:150])
|
||||
except requests.RequestException as e:
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=False)
|
||||
log.debug("remote register failed: %s", e)
|
||||
|
||||
|
||||
def remote_loop(cfg: Config, session: requests.Session) -> None:
|
||||
while True:
|
||||
try:
|
||||
register_remote(cfg, session)
|
||||
except Exception as e:
|
||||
log.debug("remote loop: %s", e)
|
||||
time.sleep(cfg.remote_interval)
|
||||
|
||||
|
||||
_ALERT_SEEN: set = set()
|
||||
|
||||
|
||||
def send_alerts(cfg: Config, session: requests.Session, faults: List[str]) -> None:
|
||||
"""POST each NEW fault (rising edge) to /{sn}/alert. Faults are strings."""
|
||||
global _ALERT_SEEN
|
||||
current = set(faults)
|
||||
new = current - _ALERT_SEEN
|
||||
_ALERT_SEEN = current
|
||||
for f in sorted(new):
|
||||
try:
|
||||
r = session.post(cfg.alert_url(),
|
||||
json={"sn": cfg.sn, "name": cfg.name,
|
||||
"alert": f, "message": f, "ts": int(time.time())},
|
||||
headers=cfg.auth_headers(),
|
||||
timeout=cfg.http_timeout, verify=cfg.verify_tls)
|
||||
_ALERTS_STAT.update(last=f, last_time=_now_str(), ok=bool(r.ok))
|
||||
if r.ok:
|
||||
_ALERTS_STAT["sent"] += 1
|
||||
log.info("alert sent: %s -> HTTP %s", f, r.status_code)
|
||||
except requests.RequestException as e:
|
||||
_ALERTS_STAT.update(last=f, last_time=_now_str(), ok=False)
|
||||
log.debug("alert send failed (%s): %s", f, e)
|
||||
|
||||
|
||||
|
||||
|
||||
_ALERT_SEEN: set = set()
|
||||
|
||||
|
||||
@ -1320,6 +1434,7 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader],
|
||||
"project_logs": (_PROJECT_TAIL.label
|
||||
if _PROJECT_TAIL is not None and _PROJECT_TAIL.active
|
||||
else None), # e.g. "sanadr1-logs"; null = no project found
|
||||
"remote": (dict(_REMOTE_STAT) if _REMOTE_STAT.get("url") else None),
|
||||
"alerts": dict(_ALERTS_STAT), # alert status (sent, last, last_time, ok)
|
||||
"time": _now_str(), # full date+time of this post
|
||||
"started_at": _STARTED["now"], # when this agent run started
|
||||
@ -1431,6 +1546,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
|
||||
threading.Thread(target=map_loop, args=(cfg, session), daemon=True).start()
|
||||
threading.Thread(target=logs_loop, args=(cfg, session), daemon=True).start()
|
||||
if cfg.remote_enable:
|
||||
threading.Thread(target=remote_loop, args=(cfg, session), daemon=True).start()
|
||||
log.info("loop every %.1fs; map every %.0fs; logs every %.0fs (Ctrl-C to stop)",
|
||||
cfg.poll_interval, cfg.map_poll_interval, cfg.logs_interval)
|
||||
while True:
|
||||
|
||||
@ -65,3 +65,13 @@ LOGS_INTERVAL=60
|
||||
PROJECT_LOG_CONTAINER=auto
|
||||
PROJECT_LOG_PATH=
|
||||
PROJECT_LOG_LABEL=
|
||||
|
||||
# ── remote dashboard (register the Sanad web UI for the fleet to embed) ───────
|
||||
# The agent probes these ports on the robot, finds the Sanad dashboard, and
|
||||
# POSTs its URL to /{sn}/remote (kind=web) — no changes to the Sanad app.
|
||||
REMOTE_ENABLE=1
|
||||
REMOTE_PORTS=8001,8014,8011,8012,8013,8000,8080
|
||||
REMOTE_KIND=web
|
||||
# REMOTE_HOST= # default: the robot's primary LAN IP (fleet-reachable)
|
||||
# REMOTE_URL= # pin an explicit URL (e.g. a public reverse-tunnel) — wins over discovery
|
||||
REMOTE_INTERVAL=60
|
||||
|
||||
@ -140,6 +140,14 @@ class Config:
|
||||
alert_endpoint: str
|
||||
logs_endpoint: str
|
||||
logs_interval: float
|
||||
# remote dashboard (register the Sanad UI URL for the fleet to embed)
|
||||
remote_enable: bool
|
||||
remote_endpoint: str
|
||||
remote_kind: str
|
||||
remote_host: str
|
||||
remote_ports: str
|
||||
remote_url: str
|
||||
remote_interval: float
|
||||
# project logs (e.g. the robot's Sanad app) shipped alongside agent logs
|
||||
project_log_container: str
|
||||
project_log_path: str
|
||||
@ -195,6 +203,13 @@ class Config:
|
||||
alert_endpoint=_env("ALERT_ENDPOINT", "/api/v1/fleet/ingest/{sn}/alert"),
|
||||
logs_endpoint=_env("LOGS_ENDPOINT", "/api/v1/fleet/ingest/{sn}/logs"),
|
||||
logs_interval=float(_env("LOGS_INTERVAL", "60")),
|
||||
remote_enable=_env_bool("REMOTE_ENABLE", True),
|
||||
remote_endpoint=_env("REMOTE_ENDPOINT", "/api/v1/fleet/ingest/{sn}/remote"),
|
||||
remote_kind=_env("REMOTE_KIND", "web"),
|
||||
remote_host=_env("REMOTE_HOST", ""),
|
||||
remote_ports=_env("REMOTE_PORTS", "8001,8014,8011,8012,8013,8000,8080"),
|
||||
remote_url=_env("REMOTE_URL", ""), # explicit URL (e.g. a public tunnel) wins
|
||||
remote_interval=float(_env("REMOTE_INTERVAL", "60")),
|
||||
project_log_container=_env("PROJECT_LOG_CONTAINER", "auto"),
|
||||
project_log_path=_env("PROJECT_LOG_PATH", ""),
|
||||
project_log_label=_env("PROJECT_LOG_LABEL", ""),
|
||||
@ -220,6 +235,9 @@ class Config:
|
||||
def logs_url(self) -> str:
|
||||
return self.server_url + self.logs_endpoint.format(sn=self.sn)
|
||||
|
||||
def remote_url_ep(self) -> str:
|
||||
return self.server_url + self.remote_endpoint.format(sn=self.sn)
|
||||
|
||||
def auth_headers(self) -> Dict[str, str]:
|
||||
return {"Authorization": f"Bearer {self.device_token}"}
|
||||
|
||||
@ -1295,6 +1313,78 @@ def logs_loop(cfg: Config, session: requests.Session) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# remote dashboard — discover the Sanad web UI and register its URL for the
|
||||
# fleet dashboard to embed ("full dashboard through the fleet API"). No changes
|
||||
# to the Sanad app: we only probe its port and POST the URL to /{sn}/remote.
|
||||
# --------------------------------------------------------------------------- #
|
||||
_REMOTE_STAT: Dict[str, Any] = {"url": None, "port": None, "kind": None, "ok": None}
|
||||
|
||||
|
||||
def _primary_ip() -> str:
|
||||
import socket
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("1.1.1.1", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def discover_dashboard(cfg: Config) -> Optional[Dict[str, Any]]:
|
||||
"""Find the Sanad dashboard: probe each candidate port on localhost; the one
|
||||
that returns 200 with a 'Sanad'/dashboard page wins. Returns {url, port}."""
|
||||
if cfg.remote_url: # explicit URL (e.g. a tunnel) overrides
|
||||
return {"url": cfg.remote_url, "port": None}
|
||||
host = cfg.remote_host or _primary_ip() # the LAN IP the fleet can reach
|
||||
for tok in cfg.remote_ports.split(","):
|
||||
tok = tok.strip()
|
||||
if not tok.isdigit():
|
||||
continue
|
||||
port = int(tok)
|
||||
try:
|
||||
r = requests.get(f"http://127.0.0.1:{port}/", timeout=2)
|
||||
except requests.RequestException:
|
||||
continue
|
||||
if r.status_code == 200 and ("sanad" in r.text.lower() or "dashboard" in r.text.lower()):
|
||||
return {"url": f"http://{host}:{port}", "port": port}
|
||||
return None
|
||||
|
||||
|
||||
def register_remote(cfg: Config, session: requests.Session) -> None:
|
||||
if not cfg.remote_enable:
|
||||
return
|
||||
d = discover_dashboard(cfg)
|
||||
if not d:
|
||||
_REMOTE_STAT.update(url=None, port=None, ok=None)
|
||||
return
|
||||
body = {"sn": cfg.sn, "name": cfg.name, "kind": cfg.remote_kind,
|
||||
"url": d["url"], "label": f"{cfg.name} — Sanad Dashboard",
|
||||
"port": d["port"], "ts": int(time.time())}
|
||||
try:
|
||||
r = session.post(cfg.remote_url_ep(), json=body, headers=cfg.auth_headers(),
|
||||
timeout=cfg.http_timeout, verify=cfg.verify_tls)
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=bool(r.ok))
|
||||
if r.ok:
|
||||
log.info("remote dashboard registered: %s -> HTTP %s", d["url"], r.status_code)
|
||||
else:
|
||||
log.warning("remote register failed: HTTP %s %s", r.status_code, r.text[:150])
|
||||
except requests.RequestException as e:
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=False)
|
||||
log.debug("remote register failed: %s", e)
|
||||
|
||||
|
||||
def remote_loop(cfg: Config, session: requests.Session) -> None:
|
||||
while True:
|
||||
try:
|
||||
register_remote(cfg, session)
|
||||
except Exception as e:
|
||||
log.debug("remote loop: %s", e)
|
||||
time.sleep(cfg.remote_interval)
|
||||
|
||||
|
||||
_ALERT_SEEN: set = set()
|
||||
|
||||
|
||||
@ -1407,6 +1497,8 @@ def build_telemetry(cfg: Config, mac: str, reader: Optional[DDSReader],
|
||||
"project_logs": (_PROJECT_TAIL.label
|
||||
if _PROJECT_TAIL is not None and _PROJECT_TAIL.active
|
||||
else None), # e.g. "sanadr1-logs"; null = no project found
|
||||
"remote": (dict(_REMOTE_STAT) if _REMOTE_STAT.get("url") else None),
|
||||
# Sanad dashboard URL registered for the fleet UI
|
||||
"alerts": dict(_ALERTS_STAT), # alert status (sent, last, last_time, ok)
|
||||
"time": _now_str(), # full date+time of this post
|
||||
"started_at": _STARTED["now"], # when this agent run started
|
||||
@ -1516,6 +1608,12 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if args.once or args.dry_run:
|
||||
# one map pass first so the telemetry "map" field reflects it
|
||||
map_sync_once(cfg, session, force=args.force, dry_run=args.dry_run)
|
||||
if cfg.remote_enable and not args.dry_run:
|
||||
register_remote(cfg, session)
|
||||
elif cfg.remote_enable:
|
||||
d = discover_dashboard(cfg)
|
||||
if d:
|
||||
_REMOTE_STAT.update(url=d["url"], port=d["port"], kind=cfg.remote_kind, ok=None)
|
||||
if args.once:
|
||||
one_telemetry()
|
||||
ship_logs(cfg, session)
|
||||
@ -1525,9 +1623,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
time.sleep(min(cfg.poll_interval, 1.0))
|
||||
return 0
|
||||
|
||||
# loop mode: map sync + log shipping in background threads
|
||||
# loop mode: map sync + log shipping + remote registration in bg threads
|
||||
threading.Thread(target=map_loop, args=(cfg, session), daemon=True).start()
|
||||
threading.Thread(target=logs_loop, args=(cfg, session), daemon=True).start()
|
||||
if cfg.remote_enable:
|
||||
threading.Thread(target=remote_loop, args=(cfg, session), daemon=True).start()
|
||||
log.info("telemetry every %.1fs; map check every %.0fs; logs every %.0fs (Ctrl-C to stop)",
|
||||
cfg.poll_interval, cfg.map_poll_interval, cfg.logs_interval)
|
||||
while True:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user