Collect Linux distribution and version telemetry across forwarders and web UI
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m7s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m7s
This commit is contained in:
@@ -333,12 +333,18 @@ def send_encrypted_logs_over_socket(
|
|||||||
if expected_fp and actual_fp != expected_fp:
|
if expected_fp and actual_fp != expected_fp:
|
||||||
raise ValueError(f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}.")
|
raise ValueError(f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}.")
|
||||||
|
|
||||||
|
inventory = get_full_system_inventory()
|
||||||
payload = {
|
payload = {
|
||||||
"server": actual_machine_id,
|
"server": actual_machine_id,
|
||||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
"logs": logs,
|
"logs": logs,
|
||||||
"metrics": collect_system_metrics(),
|
"metrics": collect_system_metrics(),
|
||||||
"version": LOGAR_VERSION
|
"version": LOGAR_VERSION,
|
||||||
|
"os_type": inventory.get("os_type", node_os),
|
||||||
|
"os_name": inventory.get("os_name", ""),
|
||||||
|
"is_server": inventory.get("is_server", False),
|
||||||
|
"server_roles": inventory.get("server_roles", []),
|
||||||
|
"client_info": inventory
|
||||||
}
|
}
|
||||||
payload_json = json.dumps(payload)
|
payload_json = json.dumps(payload)
|
||||||
pgp_msg = pgpy.PGPMessage.new(payload_json)
|
pgp_msg = pgpy.PGPMessage.new(payload_json)
|
||||||
|
|||||||
+174
-11
@@ -10,6 +10,7 @@ import json
|
|||||||
import socket
|
import socket
|
||||||
import shutil
|
import shutil
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
@@ -41,7 +42,154 @@ def get_ip_addresses() -> List[str]:
|
|||||||
return sorted(list(ips))
|
return sorted(list(ips))
|
||||||
|
|
||||||
|
|
||||||
def get_os_details() -> Dict[str, Any]:
|
def parse_os_release_text(content: str) -> Dict[str, str]:
|
||||||
|
"""Parses key=value lines from standard Linux /etc/os-release files."""
|
||||||
|
data = {}
|
||||||
|
for line in content.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
k = k.strip()
|
||||||
|
v = v.strip().strip('"\'')
|
||||||
|
data[k] = v
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_linux_distro_info(os_release_path: Optional[str] = None) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Accurately detects Linux distribution name, version, ID, and codename.
|
||||||
|
Uses platform.freedesktop_os_release() (Python 3.10+), followed by standard
|
||||||
|
/etc/os-release and /usr/lib/os-release parsing, with legacy release file fallbacks.
|
||||||
|
"""
|
||||||
|
res = {
|
||||||
|
"distro": "",
|
||||||
|
"distro_version": "",
|
||||||
|
"distro_id": "",
|
||||||
|
"distro_codename": "",
|
||||||
|
"pretty_name": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
raw: Dict[str, str] = {}
|
||||||
|
|
||||||
|
if os_release_path and os.path.exists(os_release_path):
|
||||||
|
try:
|
||||||
|
with open(os_release_path, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
raw = parse_os_release_text(f.read())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
# 1. Try standard library platform.freedesktop_os_release() (Python 3.10+)
|
||||||
|
if hasattr(platform, "freedesktop_os_release"):
|
||||||
|
try:
|
||||||
|
raw = dict(platform.freedesktop_os_release())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
# 2. Check standard /etc/os-release and /usr/lib/os-release
|
||||||
|
for fpath in ("/etc/os-release", "/usr/lib/os-release"):
|
||||||
|
if os.path.exists(fpath):
|
||||||
|
try:
|
||||||
|
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
raw = parse_os_release_text(f.read())
|
||||||
|
if raw:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
distro = raw.get("NAME") or raw.get("ID", "").capitalize() or "Linux"
|
||||||
|
version = raw.get("VERSION_ID") or raw.get("VERSION") or ""
|
||||||
|
pretty = raw.get("PRETTY_NAME") or f"{distro} {version}".strip()
|
||||||
|
distro_id = raw.get("ID", "").lower()
|
||||||
|
codename = raw.get("VERSION_CODENAME") or ""
|
||||||
|
if not codename and "(" in raw.get("VERSION", ""):
|
||||||
|
m = re.search(r"\(([^)]+)\)", raw.get("VERSION", ""))
|
||||||
|
if m:
|
||||||
|
codename = m.group(1).lower()
|
||||||
|
|
||||||
|
res["distro"] = distro
|
||||||
|
res["distro_version"] = version
|
||||||
|
res["distro_id"] = distro_id
|
||||||
|
res["distro_codename"] = codename
|
||||||
|
res["pretty_name"] = pretty
|
||||||
|
return res
|
||||||
|
|
||||||
|
# 3. Legacy file fallbacks for older or non-systemd Linux distributions
|
||||||
|
for rh_file in ("/etc/redhat-release", "/etc/centos-release", "/etc/system-release"):
|
||||||
|
if os.path.exists(rh_file):
|
||||||
|
try:
|
||||||
|
with open(rh_file, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
if content:
|
||||||
|
res["pretty_name"] = content
|
||||||
|
m = re.search(r"release\s+([\d\.]+)", content, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
res["distro_version"] = m.group(1)
|
||||||
|
parts = content.split("release")[0].strip()
|
||||||
|
res["distro"] = parts or "Red Hat / CentOS"
|
||||||
|
res["distro_id"] = "rhel" if "red hat" in content.lower() else ("centos" if "centos" in content.lower() else "linux")
|
||||||
|
return res
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists("/etc/debian_version"):
|
||||||
|
try:
|
||||||
|
with open("/etc/debian_version", "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
ver = f.read().strip()
|
||||||
|
res["distro"] = "Debian"
|
||||||
|
res["distro_version"] = ver
|
||||||
|
res["distro_id"] = "debian"
|
||||||
|
res["pretty_name"] = f"Debian {ver}".strip()
|
||||||
|
return res
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists("/etc/alpine-release"):
|
||||||
|
try:
|
||||||
|
with open("/etc/alpine-release", "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
ver = f.read().strip()
|
||||||
|
res["distro"] = "Alpine Linux"
|
||||||
|
res["distro_version"] = ver
|
||||||
|
res["distro_id"] = "alpine"
|
||||||
|
res["pretty_name"] = f"Alpine Linux {ver}".strip()
|
||||||
|
return res
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists("/etc/arch-release"):
|
||||||
|
res["distro"] = "Arch Linux"
|
||||||
|
res["distro_version"] = "Rolling"
|
||||||
|
res["distro_id"] = "arch"
|
||||||
|
res["pretty_name"] = "Arch Linux"
|
||||||
|
return res
|
||||||
|
|
||||||
|
if os.path.exists("/etc/SuSE-release"):
|
||||||
|
try:
|
||||||
|
with open("/etc/SuSE-release", "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
content = f.read()
|
||||||
|
m = re.search(r"VERSION\s*=\s*([\d\.]+)", content)
|
||||||
|
ver = m.group(1) if m else ""
|
||||||
|
res["distro"] = "openSUSE"
|
||||||
|
res["distro_version"] = ver
|
||||||
|
res["distro_id"] = "opensuse"
|
||||||
|
res["pretty_name"] = f"openSUSE {ver}".strip()
|
||||||
|
return res
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Generic Linux fallback
|
||||||
|
kernel_ver = platform.release()
|
||||||
|
res["distro"] = "Linux"
|
||||||
|
res["distro_version"] = kernel_ver
|
||||||
|
res["distro_id"] = "linux"
|
||||||
|
res["pretty_name"] = f"Linux {kernel_ver}".strip()
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_details(os_release_path: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Accurately classifies the operating system, explicitly differentiating between
|
Accurately classifies the operating system, explicitly differentiating between
|
||||||
Windows Client (Workstation) and Windows Server, as well as Linux distributions.
|
Windows Client (Workstation) and Windows Server, as well as Linux distributions.
|
||||||
@@ -51,8 +199,12 @@ def get_os_details() -> Dict[str, Any]:
|
|||||||
"os_type": "windows_server" | "windows" | "linux" | "darwin" | "unknown",
|
"os_type": "windows_server" | "windows" | "linux" | "darwin" | "unknown",
|
||||||
"os_name": str, # e.g. "Windows Server 2022 Datacenter", "Windows 11 Pro", "Ubuntu 22.04 LTS"
|
"os_name": str, # e.g. "Windows Server 2022 Datacenter", "Windows 11 Pro", "Ubuntu 22.04 LTS"
|
||||||
"is_server": bool, # True for Windows Server or Linux Server; False for desktop clients
|
"is_server": bool, # True for Windows Server or Linux Server; False for desktop clients
|
||||||
|
"distro": str, # e.g. "Ubuntu", "Debian GNU/Linux", "Red Hat Enterprise Linux"
|
||||||
|
"distro_version": str,# e.g. "22.04", "12", "9.3"
|
||||||
|
"distro_id": str, # e.g. "ubuntu", "debian", "rhel", "alpine"
|
||||||
|
"distro_codename": str,# e.g. "jammy", "bookworm"
|
||||||
"edition": str, # e.g. "ServerDatacenter", "Professional", "Enterprise"
|
"edition": str, # e.g. "ServerDatacenter", "Professional", "Enterprise"
|
||||||
"build": str, # OS build number
|
"build": str, # OS build number or kernel release
|
||||||
"architecture": str, # "AMD64", "x86_64", "ARM64"
|
"architecture": str, # "AMD64", "x86_64", "ARM64"
|
||||||
"hostname": str,
|
"hostname": str,
|
||||||
"ip_addresses": List[str]
|
"ip_addresses": List[str]
|
||||||
@@ -61,6 +213,10 @@ def get_os_details() -> Dict[str, Any]:
|
|||||||
is_server = False
|
is_server = False
|
||||||
os_type = "unknown"
|
os_type = "unknown"
|
||||||
os_name = platform.platform()
|
os_name = platform.platform()
|
||||||
|
distro = ""
|
||||||
|
distro_version = ""
|
||||||
|
distro_id = ""
|
||||||
|
distro_codename = ""
|
||||||
edition = ""
|
edition = ""
|
||||||
build = platform.version()
|
build = platform.version()
|
||||||
arch = platform.machine() or ("64-bit" if sys.maxsize > 2**32 else "32-bit")
|
arch = platform.machine() or ("64-bit" if sys.maxsize > 2**32 else "32-bit")
|
||||||
@@ -105,15 +261,14 @@ def get_os_details() -> Dict[str, Any]:
|
|||||||
elif sys.platform.startswith("linux"):
|
elif sys.platform.startswith("linux"):
|
||||||
os_type = "linux"
|
os_type = "linux"
|
||||||
is_server = True # Linux in enterprise log forwarding contexts is typically server/node
|
is_server = True # Linux in enterprise log forwarding contexts is typically server/node
|
||||||
if os.path.exists("/etc/os-release"):
|
lin_info = get_linux_distro_info(os_release_path=os_release_path)
|
||||||
try:
|
distro = lin_info.get("distro", "Linux")
|
||||||
with open("/etc/os-release", "r", encoding="utf-8") as f:
|
distro_version = lin_info.get("distro_version", "")
|
||||||
for line in f:
|
distro_id = lin_info.get("distro_id", "")
|
||||||
if line.startswith("PRETTY_NAME="):
|
distro_codename = lin_info.get("distro_codename", "")
|
||||||
os_name = line.strip().split("=", 1)[1].strip('"\'')
|
os_name = lin_info.get("pretty_name") or f"{distro} {distro_version}".strip() or "Linux"
|
||||||
break
|
edition = distro_codename or distro
|
||||||
except Exception:
|
build = platform.release()
|
||||||
pass
|
|
||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
os_type = "darwin"
|
os_type = "darwin"
|
||||||
is_server = False
|
is_server = False
|
||||||
@@ -126,6 +281,10 @@ def get_os_details() -> Dict[str, Any]:
|
|||||||
"os_type": os_type,
|
"os_type": os_type,
|
||||||
"os_name": os_name,
|
"os_name": os_name,
|
||||||
"is_server": is_server,
|
"is_server": is_server,
|
||||||
|
"distro": distro,
|
||||||
|
"distro_version": distro_version,
|
||||||
|
"distro_id": distro_id,
|
||||||
|
"distro_codename": distro_codename,
|
||||||
"edition": edition,
|
"edition": edition,
|
||||||
"build": build,
|
"build": build,
|
||||||
"architecture": arch,
|
"architecture": arch,
|
||||||
@@ -310,6 +469,10 @@ def get_full_system_inventory() -> Dict[str, Any]:
|
|||||||
"os_type": os_info.get("os_type"),
|
"os_type": os_info.get("os_type"),
|
||||||
"os_name": os_info.get("os_name"),
|
"os_name": os_info.get("os_name"),
|
||||||
"is_server": os_info.get("is_server"),
|
"is_server": os_info.get("is_server"),
|
||||||
|
"distro": os_info.get("distro", ""),
|
||||||
|
"distro_version": os_info.get("distro_version", ""),
|
||||||
|
"distro_id": os_info.get("distro_id", ""),
|
||||||
|
"distro_codename": os_info.get("distro_codename", ""),
|
||||||
"edition": os_info.get("edition"),
|
"edition": os_info.get("edition"),
|
||||||
"build": os_info.get("build"),
|
"build": os_info.get("build"),
|
||||||
"architecture": os_info.get("architecture"),
|
"architecture": os_info.get("architecture"),
|
||||||
|
|||||||
+33
-11
@@ -469,6 +469,9 @@ class WebUIRenderer:
|
|||||||
|
|
||||||
# OS classification badge & display name
|
# OS classification badge & display name
|
||||||
ost_lower = ost.lower()
|
ost_lower = ost.lower()
|
||||||
|
distro_val = str(info.get("distro") or "").strip()
|
||||||
|
distro_ver_val = str(info.get("distro_version") or "").strip()
|
||||||
|
|
||||||
if "server" in ost_lower or is_srv:
|
if "server" in ost_lower or is_srv:
|
||||||
if "windows" in ost_lower or "win" in ost_lower:
|
if "windows" in ost_lower or "win" in ost_lower:
|
||||||
filter_cat = "windows_server"
|
filter_cat = "windows_server"
|
||||||
@@ -478,9 +481,11 @@ class WebUIRenderer:
|
|||||||
os_display_name = html.escape(os_name or "Windows Server")
|
os_display_name = html.escape(os_name or "Windows Server")
|
||||||
else:
|
else:
|
||||||
filter_cat = "linux"
|
filter_cat = "linux"
|
||||||
os_badge_html = f'<span class="badge badge-os" title="{html.escape(os_name or "Linux Node")}">🐧 LINUX</span>'
|
lbl = distro_val.upper() if distro_val else "LINUX"
|
||||||
os_badge_full = '<span class="badge badge-os" style="font-size:10px;">🐧 LINUX SERVER</span>'
|
disp_name = os_name if os_name else (f"{distro_val} {distro_ver_val}".strip() or "Linux Server")
|
||||||
os_display_name = html.escape(os_name or "Linux Node")
|
os_badge_html = f'<span class="badge badge-os" title="{html.escape(disp_name)}">🐧 {html.escape(lbl)}</span>'
|
||||||
|
os_badge_full = f'<span class="badge badge-os" style="font-size:10px;">🐧 {html.escape(lbl)} SERVER</span>'
|
||||||
|
os_display_name = html.escape(disp_name)
|
||||||
elif "windows" in ost_lower or "win" in ost_lower:
|
elif "windows" in ost_lower or "win" in ost_lower:
|
||||||
filter_cat = "windows_client"
|
filter_cat = "windows_client"
|
||||||
os_badge_html = f'<span class="badge badge-os" title="{html.escape(os_name or "Windows Client")}">🖥️ WINDOWS</span>'
|
os_badge_html = f'<span class="badge badge-os" title="{html.escape(os_name or "Windows Client")}">🖥️ WINDOWS</span>'
|
||||||
@@ -488,19 +493,36 @@ class WebUIRenderer:
|
|||||||
os_display_name = html.escape(os_name or "Windows Workstation")
|
os_display_name = html.escape(os_name or "Windows Workstation")
|
||||||
elif "linux" in ost_lower:
|
elif "linux" in ost_lower:
|
||||||
filter_cat = "linux"
|
filter_cat = "linux"
|
||||||
os_badge_html = f'<span class="badge badge-os" title="{html.escape(os_name or "Linux Node")}">🐧 LINUX</span>'
|
lbl = distro_val.upper() if distro_val else "LINUX"
|
||||||
os_badge_full = '<span class="badge badge-os" style="font-size:10px;">🐧 LINUX</span>'
|
disp_name = os_name if os_name else (f"{distro_val} {distro_ver_val}".strip() or "Linux Node")
|
||||||
os_display_name = html.escape(os_name or "Linux Node")
|
os_badge_html = f'<span class="badge badge-os" title="{html.escape(disp_name)}">🐧 {html.escape(lbl)}</span>'
|
||||||
|
os_badge_full = f'<span class="badge badge-os" style="font-size:10px;">🐧 {html.escape(lbl)}</span>'
|
||||||
|
os_display_name = html.escape(disp_name)
|
||||||
else:
|
else:
|
||||||
filter_cat = "other"
|
filter_cat = "other"
|
||||||
os_badge_html = f'<span class="badge badge-os">{esc_ost.upper()}</span>'
|
os_badge_html = f'<span class="badge badge-os">{esc_ost.upper()}</span>'
|
||||||
os_badge_full = f'<span class="badge badge-os" style="font-size:10px;">{html.escape(ost.upper())}</span>'
|
os_badge_full = f'<span class="badge badge-os" style="font-size:10px;">{html.escape(ost.upper())}</span>'
|
||||||
os_display_name = html.escape(os_name or ost)
|
os_display_name = html.escape(os_name or ost)
|
||||||
|
|
||||||
edition_str = html.escape(str(info.get("edition") or ""))
|
edition_str = html.escape(str(info.get("edition") or "").strip())
|
||||||
build_str = html.escape(str(info.get("build") or ""))
|
build_str = html.escape(str(info.get("build") or "").strip())
|
||||||
arch_str = html.escape(str(info.get("architecture") or ""))
|
arch_str = html.escape(str(info.get("architecture") or "").strip())
|
||||||
specs_sub = f"{edition_str} • Build {build_str} • {arch_str}".strip(" •") if (edition_str or build_str or arch_str) else ""
|
distro_ver_str = html.escape(distro_ver_val)
|
||||||
|
codename_str = html.escape(str(info.get("distro_codename") or "").strip())
|
||||||
|
|
||||||
|
if "linux" in ost_lower:
|
||||||
|
sub_parts = []
|
||||||
|
if distro_ver_str and distro_ver_str not in os_display_name:
|
||||||
|
sub_parts.append(f"v{distro_ver_str}")
|
||||||
|
if codename_str:
|
||||||
|
sub_parts.append(f"({codename_str})")
|
||||||
|
if build_str:
|
||||||
|
sub_parts.append(f"Kernel {build_str}")
|
||||||
|
if arch_str:
|
||||||
|
sub_parts.append(arch_str)
|
||||||
|
specs_sub = " • ".join(sub_parts)
|
||||||
|
else:
|
||||||
|
specs_sub = f"{edition_str} • Build {build_str} • {arch_str}".strip(" •") if (edition_str or build_str or arch_str) else ""
|
||||||
|
|
||||||
# Roles badges
|
# Roles badges
|
||||||
role_badges = []
|
role_badges = []
|
||||||
@@ -602,7 +624,7 @@ class WebUIRenderer:
|
|||||||
|
|
||||||
# 2. Server Roles Row (Tab 2)
|
# 2. Server Roles Row (Tab 2)
|
||||||
server_roles_rows.append(f"""
|
server_roles_rows.append(f"""
|
||||||
<tr class="node-row" data-category="{filter_cat}" data-search="{esc_host.lower()} {esc_cid.lower()} {os_display_name.lower()} {' '.join(str(x).lower() for x in roles)} {ip_str.lower()}">
|
<tr class="node-row" data-category="{filter_cat}" data-search="{esc_host.lower()} {esc_cid.lower()} {os_display_name.lower()} {html.escape(distro_val).lower()} {html.escape(distro_ver_val).lower()} {' '.join(str(x).lower() for x in roles)} {ip_str.lower()}">
|
||||||
<td><span class="badge {badge_cls}">{esc_st.upper()}</span></td>
|
<td><span class="badge {badge_cls}">{esc_st.upper()}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<div style="font-weight:700; color:#fff; font-size:13px;">{esc_host}{host_tag}</div>
|
<div style="font-weight:700; color:#fff; font-size:13px;">{esc_host}{host_tag}</div>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ from src.common.system_info import (
|
|||||||
get_os_details,
|
get_os_details,
|
||||||
get_ip_addresses,
|
get_ip_addresses,
|
||||||
get_full_system_inventory,
|
get_full_system_inventory,
|
||||||
get_installed_windows_server_roles
|
get_installed_windows_server_roles,
|
||||||
|
get_linux_distro_info,
|
||||||
|
parse_os_release_text
|
||||||
)
|
)
|
||||||
from src.hub.best_practices import BestPracticeEngine, BestPracticeRule, BEST_PRACTICES_ENGINE
|
from src.hub.best_practices import BestPracticeEngine, BestPracticeRule, BEST_PRACTICES_ENGINE
|
||||||
from src.hub.best_practice_rules import BASELINE_RULES, get_baseline_rules
|
from src.hub.best_practice_rules import BASELINE_RULES, get_baseline_rules
|
||||||
@@ -283,6 +285,151 @@ class TestServerRolesAndBestPractices(unittest.TestCase):
|
|||||||
self.assertIn("id=\"versionInfoModal\"", res_client_info.text)
|
self.assertIn("id=\"versionInfoModal\"", res_client_info.text)
|
||||||
self.assertIn(f"v{LOGAR_VERSION}", res_client_info.text)
|
self.assertIn(f"v{LOGAR_VERSION}", res_client_info.text)
|
||||||
|
|
||||||
|
def test_linux_distro_collection_ubuntu(self):
|
||||||
|
"""Verifies parsing of standard Ubuntu os-release data."""
|
||||||
|
ubuntu_os_release = (
|
||||||
|
'NAME="Ubuntu"\n'
|
||||||
|
'VERSION="22.04.4 LTS (Jammy Jellyfish)"\n'
|
||||||
|
'ID=ubuntu\n'
|
||||||
|
'ID_LIKE=debian\n'
|
||||||
|
'PRETTY_NAME="Ubuntu 22.04.4 LTS"\n'
|
||||||
|
'VERSION_ID="22.04"\n'
|
||||||
|
'VERSION_CODENAME=jammy\n'
|
||||||
|
)
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
|
||||||
|
f.write(ubuntu_os_release)
|
||||||
|
tmp_path = f.name
|
||||||
|
try:
|
||||||
|
info = get_linux_distro_info(os_release_path=tmp_path)
|
||||||
|
self.assertEqual(info["distro"], "Ubuntu")
|
||||||
|
self.assertEqual(info["distro_version"], "22.04")
|
||||||
|
self.assertEqual(info["distro_id"], "ubuntu")
|
||||||
|
self.assertEqual(info["distro_codename"], "jammy")
|
||||||
|
self.assertEqual(info["pretty_name"], "Ubuntu 22.04.4 LTS")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
def test_linux_distro_collection_debian(self):
|
||||||
|
"""Verifies parsing of standard Debian os-release data."""
|
||||||
|
debian_os_release = (
|
||||||
|
'PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"\n'
|
||||||
|
'NAME="Debian GNU/Linux"\n'
|
||||||
|
'VERSION_ID="12"\n'
|
||||||
|
'VERSION="12 (bookworm)"\n'
|
||||||
|
'VERSION_CODENAME=bookworm\n'
|
||||||
|
'ID=debian\n'
|
||||||
|
)
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
|
||||||
|
f.write(debian_os_release)
|
||||||
|
tmp_path = f.name
|
||||||
|
try:
|
||||||
|
info = get_linux_distro_info(os_release_path=tmp_path)
|
||||||
|
self.assertEqual(info["distro"], "Debian GNU/Linux")
|
||||||
|
self.assertEqual(info["distro_version"], "12")
|
||||||
|
self.assertEqual(info["distro_id"], "debian")
|
||||||
|
self.assertEqual(info["distro_codename"], "bookworm")
|
||||||
|
self.assertEqual(info["pretty_name"], "Debian GNU/Linux 12 (bookworm)")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
def test_linux_distro_collection_rhel_and_alpine(self):
|
||||||
|
"""Verifies parsing of RHEL and Alpine os-release data."""
|
||||||
|
rhel_release = (
|
||||||
|
'NAME="Red Hat Enterprise Linux"\n'
|
||||||
|
'VERSION="9.3 (Plow)"\n'
|
||||||
|
'ID="rhel"\n'
|
||||||
|
'ID_LIKE="fedora"\n'
|
||||||
|
'VERSION_ID="9.3"\n'
|
||||||
|
'PRETTY_NAME="Red Hat Enterprise Linux 9.3 (Plow)"\n'
|
||||||
|
)
|
||||||
|
alpine_release = (
|
||||||
|
'NAME="Alpine Linux"\n'
|
||||||
|
'ID=alpine\n'
|
||||||
|
'VERSION_ID=3.19.1\n'
|
||||||
|
'PRETTY_NAME="Alpine Linux v3.19"\n'
|
||||||
|
)
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
|
||||||
|
f.write(rhel_release)
|
||||||
|
rhel_path = f.name
|
||||||
|
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
|
||||||
|
f.write(alpine_release)
|
||||||
|
alpine_path = f.name
|
||||||
|
try:
|
||||||
|
rhel_info = get_linux_distro_info(os_release_path=rhel_path)
|
||||||
|
self.assertEqual(rhel_info["distro"], "Red Hat Enterprise Linux")
|
||||||
|
self.assertEqual(rhel_info["distro_version"], "9.3")
|
||||||
|
self.assertEqual(rhel_info["distro_id"], "rhel")
|
||||||
|
|
||||||
|
alpine_info = get_linux_distro_info(os_release_path=alpine_path)
|
||||||
|
self.assertEqual(alpine_info["distro"], "Alpine Linux")
|
||||||
|
self.assertEqual(alpine_info["distro_version"], "3.19.1")
|
||||||
|
self.assertEqual(alpine_info["distro_id"], "alpine")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(rhel_path):
|
||||||
|
os.unlink(rhel_path)
|
||||||
|
if os.path.exists(alpine_path):
|
||||||
|
os.unlink(alpine_path)
|
||||||
|
|
||||||
|
def test_get_os_details_linux_simulation(self):
|
||||||
|
"""Tests get_os_details when simulated on Linux."""
|
||||||
|
ubuntu_os_release = (
|
||||||
|
'NAME="Ubuntu"\n'
|
||||||
|
'VERSION="24.04 LTS (Noble Numbat)"\n'
|
||||||
|
'ID=ubuntu\n'
|
||||||
|
'VERSION_ID="24.04"\n'
|
||||||
|
'PRETTY_NAME="Ubuntu 24.04 LTS"\n'
|
||||||
|
'VERSION_CODENAME=noble\n'
|
||||||
|
)
|
||||||
|
import tempfile
|
||||||
|
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as f:
|
||||||
|
f.write(ubuntu_os_release)
|
||||||
|
tmp_path = f.name
|
||||||
|
try:
|
||||||
|
with patch("sys.platform", "linux"):
|
||||||
|
details = get_os_details(os_release_path=tmp_path)
|
||||||
|
self.assertEqual(details["os_type"], "linux")
|
||||||
|
self.assertTrue(details["is_server"])
|
||||||
|
self.assertEqual(details["distro"], "Ubuntu")
|
||||||
|
self.assertEqual(details["distro_version"], "24.04")
|
||||||
|
self.assertEqual(details["distro_id"], "ubuntu")
|
||||||
|
self.assertEqual(details["distro_codename"], "noble")
|
||||||
|
self.assertEqual(details["os_name"], "Ubuntu 24.04 LTS")
|
||||||
|
finally:
|
||||||
|
if os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
def test_webui_renders_linux_distro_badge_and_specs(self):
|
||||||
|
"""Verifies /webui properly formats Linux distribution badges and specs."""
|
||||||
|
db_path = self.db_path
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("""
|
||||||
|
INSERT OR REPLACE INTO clients (
|
||||||
|
client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen,
|
||||||
|
client_version, os_name, is_server, server_roles, client_info_json
|
||||||
|
) VALUES (
|
||||||
|
'test-linux-node-01', 'ubuntu-srv-01.corp.internal', 'linux', 'FP-LINUX-TEST-01', 'active',
|
||||||
|
'2026-10-11 10:00:00', '2026-10-11 10:05:00', '1.2.0', 'Ubuntu 22.04.4 LTS', 1,
|
||||||
|
'["Web Server (Nginx)", "SSH Remote Access"]',
|
||||||
|
'{"distro": "Ubuntu", "distro_version": "22.04", "distro_id": "ubuntu", "distro_codename": "jammy", "architecture": "x86_64", "cpu_count": 4, "memory_total_mb": 8192}'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
auth_client = TestClient(app)
|
||||||
|
auth_client.cookies.set(SessionManager.SESSION_COOKIE_NAME, self.session_token)
|
||||||
|
res = auth_client.get("/webui")
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
self.assertIn("ubuntu-srv-01.corp.internal", res.text)
|
||||||
|
self.assertIn("Ubuntu 22.04.4 LTS", res.text)
|
||||||
|
self.assertIn("UBUNTU", res.text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user