Skip to content

Commit 4d5c616

Browse files
feat(metrics): add CPU_NAME parameter to include processor model in reported metrics
- Added detection of CPU model name via lscpu, /proc/cpuinfo, or platform fallback - Included `cpu_name` field in metrics payload sent to backend - Provides better hardware visibility in NodeSniff dashboard
1 parent 05e32fd commit 4d5c616

File tree

1 file changed

+81
-21
lines changed

1 file changed

+81
-21
lines changed

nsagent.py

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# --clean - full cleanup: removes service, config, logs, metrics and dev dirs (safe to call repeatedly)
1515
# ------------------------------------------------------------------------------
1616

17-
AGENT_VERSION = "1.1.1"
17+
AGENT_VERSION = "1.1.2"
1818

1919
import os, sys, json, time, yaml, uuid, hmac, psutil, socket, hashlib, logging, requests, platform, subprocess, shutil
2020
from pathlib import Path
@@ -102,15 +102,57 @@ def get_domain_name():
102102
for line in f:
103103
if line.startswith("search"):
104104
parts = line.strip().split()
105-
if len(parts) > 1 and parts[1] != ".":
105+
if len(parts) > 1 and parts[1] not in (".", "localdomain"):
106106
return parts[1]
107-
domain = subprocess.check_output("hostname -d", shell=True).decode().strip()
108-
if domain and domain != "(none)":
107+
domain = subprocess.check_output("hostname -d", shell=True, text=True).strip()
108+
if domain and domain not in ("(none)", ".", "localdomain"):
109109
return domain
110110
except Exception as e:
111111
logging.warning(f"Cannot determine domain_name: {e}")
112112
return ""
113113

114+
115+
def get_cpu_name():
116+
"""
117+
Returns a detailed human-readable CPU model name with multiple fallbacks.
118+
Example: 'Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz'
119+
"""
120+
import platform, subprocess
121+
122+
# 1. Try lscpu (most reliable)
123+
try:
124+
output = subprocess.check_output("lscpu | grep 'Model name'", shell=True, text=True)
125+
if ":" in output:
126+
name = output.split(":", 1)[1].strip()
127+
if name:
128+
return name
129+
except Exception:
130+
pass
131+
132+
# 2. Try /proc/cpuinfo
133+
try:
134+
with open("/proc/cpuinfo", "r") as f:
135+
for line in f:
136+
if "model name" in line:
137+
return line.split(":", 1)[1].strip()
138+
except Exception:
139+
pass
140+
141+
# 3. Try platform.processor()
142+
cpu = platform.processor()
143+
if cpu and cpu.strip() and cpu.lower() != "unknown" and cpu.lower() != "x86_64":
144+
return cpu.strip()
145+
146+
# 4. Fallback: use uname
147+
try:
148+
return subprocess.check_output("uname -p", shell=True, text=True).strip()
149+
except Exception:
150+
pass
151+
152+
return "Unknown CPU"
153+
154+
155+
114156
def ensure_files():
115157
"""
116158
Ensures that all required agent files and directories exist.
@@ -189,6 +231,7 @@ def register_agent(cfg, company_key):
189231
hardware_id = cfg["hardware_id"]
190232
headers = {
191233
"X-Company-Key": company_key,
234+
"X-Agent-Version": AGENT_VERSION,
192235
"Content-Type": "application/json"
193236
}
194237
payload = {
@@ -331,7 +374,7 @@ def install_service():
331374
Type=simple
332375
User=nodesniff
333376
Group=nodesniff
334-
ExecStart=/usr/bin/python3 {AGENT_DEST_PATH}
377+
ExecStart=/usr/bin/python3 {AGENT_DEST_PATH} --daemon
335378
Restart=on-failure
336379
RestartSec=10
337380
StandardOutput=journal
@@ -447,6 +490,7 @@ def collect_metrics(cfg):
447490
"cpu_freq_current": int(cpu_freq.current) if cpu_freq else None,
448491
"cpu_freq_max": int(cpu_freq.max) if cpu_freq else None,
449492
"cpu_temp": cpu_temp,
493+
"cpu_name": get_cpu_name(),
450494
"memory_total_mb": int(vmem.total / 1024 / 1024),
451495
"memory_used_mb": int(vmem.used / 1024 / 1024),
452496
"memory_cached_mb": int(getattr(vmem, "cached", 0) / 1024 / 1024),
@@ -465,7 +509,7 @@ def collect_metrics(cfg):
465509
"cloud_region": None,
466510
"hardware_id": hardware_id,
467511
"extra_metrics": extra_metrics,
468-
"timestamp": int(time.time())
512+
"timestamp": int(time.time())
469513
}
470514

471515
def send_metrics(cfg, metrics):
@@ -490,13 +534,17 @@ def send_metrics(cfg, metrics):
490534
elif r.status_code == 403:
491535
print_error("❌ Agent was removed via UI or token is invalid (HTTP 403).")
492536
print_error("You must re-register this agent to continue monitoring.")
493-
# Remove config & token to force re-registration
494537
if os.path.exists(CONFIG_PATH): os.remove(CONFIG_PATH)
495538
if os.path.exists(TOKEN_PATH): os.remove(TOKEN_PATH)
496539
print_info("Agent config and token removed, registration required on next start.")
497540
sys.exit(1)
541+
elif r.status_code == 426:
542+
print_error(f"❌ Agent version too old. Minimum required version: {r.json().get('detail')}")
543+
logging.error(f"Agent version too old. Response: {r.text}")
544+
sys.exit(1)
498545
else:
499546
logging.error(f"Send failed: {r.status_code} {r.text}")
547+
sys.exit(1)
500548
except Exception as e:
501549
logging.exception("Error sending metrics")
502550

@@ -563,6 +611,7 @@ def clean_agent():
563611
if "--clean" in sys.argv:
564612
clean_agent()
565613
sys.exit(0)
614+
566615
if "--service" in sys.argv:
567616
install_service()
568617
sys.exit(0)
@@ -571,16 +620,20 @@ def clean_agent():
571620
ensure_files()
572621
cfg = load_config()
573622

574-
# Register the agent if not registered yet
575-
if "api_token" not in cfg or "hmac_secret" not in cfg:
576-
key = load_company_key()
577-
register_agent(cfg, key)
578-
metrics = collect_metrics(cfg)
579-
send_metrics(cfg, metrics)
580-
print_ok("Registration complete. You can now start nsagent in background with '&' (./nsagent.py &) or use '--service' to install as a systemd service.")
581-
sys.exit(0)
582-
583-
# Check if machine-id in config matches the current host
623+
# --- registration mode (default) ---
624+
if "--daemon" not in sys.argv:
625+
if "api_token" not in cfg or "hmac_secret" not in cfg:
626+
key = load_company_key()
627+
register_agent(cfg, key)
628+
metrics = collect_metrics(cfg)
629+
send_metrics(cfg, metrics)
630+
print_ok(
631+
"Registration complete. You can now start nsagent in background with '&' (./nsagent.py &) "
632+
"or use '--service' to install as a systemd service."
633+
)
634+
sys.exit(0)
635+
636+
# --- validate machine-id before starting loop ---
584637
config_machine_id = cfg.get("machine_id")
585638
actual_machine_id = get_machine_id()
586639
if not config_machine_id or not actual_machine_id:
@@ -590,11 +643,18 @@ def clean_agent():
590643
print_error("Agent config was copied from another host (machine-id mismatch). Registration required!")
591644
sys.exit(1)
592645

593-
# Main metrics loop
646+
# --- daemon mode (continuous metrics loop) ---
594647
interval = max(cfg.get("interval_seconds", 60), 10)
595648
while True:
596-
metrics = collect_metrics(cfg)
597-
send_metrics(cfg, metrics)
598-
time.sleep(interval)
649+
try:
650+
metrics = collect_metrics(cfg)
651+
send_metrics(cfg, metrics)
652+
time.sleep(interval)
653+
except Exception as e:
654+
logging.exception(f"Error in metrics loop: {e}")
655+
time.sleep(interval)
656+
657+
658+
599659

600660

0 commit comments

Comments
 (0)