mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 04:33:00 +02:00
Three bugs found while testing modem support on hardware: ietf_system.py: "interface": null written to DNS server entries when no interface is associated, causing LY_EVALID in the ietf-system schema validator. Only include the key when the interface name is non-empty. ietf_system.py: empty list inserts (user [], server [], options [], search []) overwrote configured data in the operational store instead of leaving it intact. Guard each insert so operational data only shadows configured data when there is something to report. show system: CPU temperature reported only the first sensor named exactly "cpu", "soc", or "core". Platforms with multiple thermal zones (e.g. BPI-R4 exposes cpu and cpu1) always showed only one reading. Collect all sensors whose name matches a CPU/SoC prefix and report the maximum. statd.c: yanger parse errors logged without model name or libyang error string, making failures hard to diagnose. Include both. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
465 lines
15 KiB
Python
465 lines
15 KiB
Python
import subprocess
|
|
import ipaddress
|
|
import re
|
|
|
|
from .common import insert,YangDate
|
|
from .host import HOST
|
|
|
|
def uboot_get_boot_order():
|
|
data = HOST.run_multiline("fw_printenv BOOT_ORDER".split(), [])
|
|
for line in data:
|
|
if "BOOT_ORDER" in line:
|
|
return line.strip().split("=")[1].split()
|
|
|
|
raise Exception
|
|
|
|
def grub_get_boot_order():
|
|
data = HOST.run_multiline("grub-editenv /mnt/aux/grub/grubenv list".split(), [])
|
|
for line in data:
|
|
if "ORDER" in line:
|
|
return line.split("=")[1].strip().split()
|
|
|
|
raise Exception
|
|
|
|
def get_boot_order():
|
|
order = None
|
|
try:
|
|
order = uboot_get_boot_order()
|
|
except:
|
|
pass
|
|
try:
|
|
if order is None:
|
|
order = grub_get_boot_order()
|
|
except:
|
|
pass
|
|
|
|
return order
|
|
|
|
def add_ntp(out):
|
|
"""Add NTP source information from chronyc sources.
|
|
|
|
The chronyc -c sources output is CSV with the following fields:
|
|
[0] Mode indicator:
|
|
^ server (we're a client to this source)
|
|
= peer (symmetric mode)
|
|
# local reference clock (GPS, PPS, etc.) - skipped, no IP address
|
|
[1] State indicator:
|
|
* selected (current sync source)
|
|
+ candidate
|
|
- outlier
|
|
? unusable
|
|
x falseticker
|
|
~ unstable
|
|
[2] Address (IP address or refclock name like "GPS")
|
|
[3] Stratum
|
|
[4] Poll interval (log2 seconds)
|
|
[5] Reach (octal reachability register)
|
|
[6] LastRx (seconds since last response)
|
|
[7] Last offset (seconds)
|
|
[8] Offset at last update (seconds)
|
|
[9] Error estimate (seconds)
|
|
"""
|
|
data = HOST.run_multiline(["chronyc", "-c", "sources"], [])
|
|
source = []
|
|
state_mode_map = {
|
|
"^": "server",
|
|
"=": "peer",
|
|
"#": "local-clock"
|
|
}
|
|
source_state_map = {
|
|
"*": "selected",
|
|
"+": "candidate",
|
|
"-": "outlier",
|
|
"?": "unusable",
|
|
"x": "falseticker",
|
|
"~": "unstable"
|
|
}
|
|
for line in data:
|
|
src = {}
|
|
line = line.split(',')
|
|
# Skip reference clocks (mode "#") as they have names like "GPS" instead of IP addresses
|
|
if line[0] == "#":
|
|
continue
|
|
src["address"] = line[2]
|
|
src["mode"] = state_mode_map[line[0]]
|
|
src["state"] = source_state_map[line[1]]
|
|
src["stratum"] = int(line[3])
|
|
src["poll"] = int(line[4])
|
|
source.append(src)
|
|
|
|
insert(out, "infix-system:ntp", "sources", "source", source)
|
|
|
|
|
|
def add_dns(out):
|
|
options = {}
|
|
servers = []
|
|
search = []
|
|
|
|
content = HOST.read_multiline("/etc/resolv.conf.head", [])
|
|
for line in content:
|
|
line = line.strip()
|
|
|
|
if line.startswith('nameserver'):
|
|
ip = line.split()[1]
|
|
try:
|
|
ipaddress.ip_address(ip)
|
|
servers.append({
|
|
"address": ip,
|
|
"origin": "static"
|
|
})
|
|
except ValueError:
|
|
continue
|
|
|
|
elif line.startswith('search'):
|
|
search.extend(line.split()[1:])
|
|
|
|
elif line.startswith('options'):
|
|
opts = line.split()[1:]
|
|
for opt in opts:
|
|
if opt.startswith('timeout:'):
|
|
options["timeout"] = int(opt.split(':')[1])
|
|
elif opt.startswith('attempts:'):
|
|
options["attempts"] = int(opt.split(':')[1])
|
|
|
|
output = HOST.run_multiline(['/sbin/resolvconf', '-l'], [])
|
|
for line in output:
|
|
line = line.strip()
|
|
if line.startswith('nameserver'):
|
|
parts = line.split('#', 1)
|
|
ip = parts[0].split()[1]
|
|
|
|
iface = None
|
|
if len(parts) > 1:
|
|
iface = parts[1].strip()
|
|
|
|
try:
|
|
ipaddress.ip_address(ip)
|
|
entry = {"address": ip, "origin": "dhcp"}
|
|
if iface:
|
|
entry["interface"] = iface
|
|
servers.append(entry)
|
|
except ValueError:
|
|
continue
|
|
|
|
elif line.startswith('search'):
|
|
parts = line.split('#', 1)
|
|
search.extend(parts[0].split()[1:])
|
|
|
|
if options:
|
|
insert(out, "infix-system:dns-resolver", "options", options)
|
|
if servers:
|
|
insert(out, "infix-system:dns-resolver", "server", servers)
|
|
if search:
|
|
insert(out, "infix-system:dns-resolver", "search", search)
|
|
|
|
def add_software_slots(out, data):
|
|
slots = []
|
|
for slot in data.get("slots", []):
|
|
for key, value in slot.items():
|
|
new = {}
|
|
new["name"] = key
|
|
new["bootname"] = slot[key].get("bootname")
|
|
new["class"] = slot[key].get("class")
|
|
new["state"] = slot[key].get("state")
|
|
new["bundle"] = {}
|
|
slot_status=value.get("slot_status", {})
|
|
if slot_status.get("bundle", {}).get("compatible"):
|
|
new["bundle"]["compatible"] = slot_status.get("bundle", {}).get("compatible")
|
|
if slot_status.get("bundle", {}).get("version"):
|
|
new["bundle"]["version"] = slot_status.get("bundle", {}).get("version")
|
|
if slot_status.get("checksum", {}).get("size"):
|
|
new["size"] = str(slot_status.get("checksum", {}).get("size"))
|
|
if slot_status.get("checksum", {}).get("sha256"):
|
|
new["sha256"] = slot_status.get("checksum", {}).get("sha256")
|
|
|
|
new["installed"] = {}
|
|
if slot_status.get("installed", {}).get("timestamp"):
|
|
new["installed"]["datetime"] = slot_status.get("installed", {}).get("timestamp")
|
|
|
|
if slot_status.get("installed", {}).get("count"):
|
|
new["installed"]["count"] = slot_status.get("installed", {}).get("count")
|
|
|
|
new["activated"] = {}
|
|
if slot_status.get("activated", {}).get("timestamp"):
|
|
new["activated"]["datetime"] = slot_status.get("activated", {}).get("timestamp")
|
|
|
|
if slot_status.get("activated", {}).get("count"):
|
|
new["activated"]["count"] = slot_status.get("activated", {}).get("count")
|
|
slots.append(new)
|
|
out["slot"] = slots
|
|
|
|
def add_platform(out):
|
|
platform = {}
|
|
pmap = {
|
|
"NAME": "os-name",
|
|
"VERSION_ID": "os-version",
|
|
"BUILD_ID": "os-release",
|
|
"ARCHITECTURE": "machine"
|
|
}
|
|
|
|
os_release = HOST.read("/etc/os-release")
|
|
for line in os_release.splitlines():
|
|
key, value = line.split('=')
|
|
name = pmap.get(key)
|
|
if name:
|
|
platform[name] = value.strip("\"")
|
|
|
|
insert(out, "platform", platform)
|
|
|
|
def add_services(out):
|
|
data = HOST.run_json(["initctl", "-j"], [])
|
|
services = []
|
|
|
|
for d in data:
|
|
try:
|
|
services.append({
|
|
"pid": d["pid"],
|
|
"name": d["identity"],
|
|
"status": d["status"],
|
|
"description": d["description"],
|
|
"statistics": {
|
|
"memory-usage": str(d.get("memory", 0)),
|
|
"uptime": str(d.get("uptime", 0)),
|
|
"restart-count": int(d.get("restarts", 0))
|
|
}
|
|
})
|
|
except KeyError:
|
|
continue
|
|
|
|
insert(out, "infix-system:services", "service", services)
|
|
|
|
def add_software(out):
|
|
software = {}
|
|
try:
|
|
data = HOST.run_json(["rauc", "status", "--detailed", "--output-format=json"], {})
|
|
software["compatible"] = data.get("compatible", "")
|
|
software["variant"] = data.get("variant", "")
|
|
software["booted"] = data.get("booted", "")
|
|
boot_order = get_boot_order()
|
|
if not boot_order is None:
|
|
software["boot-order"] = boot_order
|
|
add_software_slots(software, data)
|
|
except subprocess.CalledProcessError:
|
|
pass # Maybe an upgrade i progress, then rauc does not respond
|
|
|
|
installer = {}
|
|
installer_status = HOST.run_json(["rauc-installation-status"], {})
|
|
if installer_status.get("operation", {}):
|
|
installer["operation"] = installer_status["operation"]
|
|
if "progress" in installer_status:
|
|
progress = {}
|
|
|
|
if installer_status["progress"].get("percentage"):
|
|
progress["percentage"] = int(installer_status["progress"]["percentage"])
|
|
if installer_status["progress"].get("message"):
|
|
progress["message"] = installer_status["progress"]["message"]
|
|
installer["progress"] = progress
|
|
software["installer"] = installer
|
|
|
|
insert(out, "infix-system:software", software)
|
|
|
|
def add_hostname(out):
|
|
hostname = HOST.run(tuple(["hostname"]))
|
|
out["hostname"] = hostname.strip()
|
|
|
|
def add_timezone(out):
|
|
path = HOST.run(tuple("realpath /etc/localtime".split()), "")
|
|
timezone = None
|
|
prefixes = [
|
|
'/usr/share/zoneinfo/posix/',
|
|
'/usr/share/zoneinfo/right/',
|
|
'/usr/share/zoneinfo/'
|
|
]
|
|
|
|
for prefix in prefixes:
|
|
if path is not None and path.startswith(prefix):
|
|
timezone = path[len(prefix):]
|
|
break
|
|
if timezone is not None:
|
|
timezone=timezone.strip()
|
|
pattern = r'Etc/GMT([\+\-]\d{1,2})$'
|
|
match = re.search(pattern, timezone)
|
|
if match:
|
|
offset = -int(match.group(1))
|
|
insert(out, "clock", "timezone-utc-offset", offset)
|
|
else:
|
|
if timezone == "Etc/UTC":
|
|
insert(out, "clock", "timezone-utc-offset", 0)
|
|
else:
|
|
insert(out, "clock", "timezone-name", timezone)
|
|
|
|
|
|
def add_users(out):
|
|
# Map shell paths to YANG identity names
|
|
shell_map = {
|
|
"/bin/bash": "infix-system:bash",
|
|
"/bin/sh": "infix-system:sh",
|
|
"/usr/bin/clish": "infix-system:clish",
|
|
"/bin/false": "infix-system:false",
|
|
"/sbin/nologin": "infix-system:false",
|
|
"/usr/sbin/nologin": "infix-system:false",
|
|
}
|
|
|
|
# Get users from /etc/passwd - include users with 1000 <= uid < 10000 (added by confd)
|
|
passwd_output = HOST.run_multiline(["getent", "passwd"], [])
|
|
passwd_users = {}
|
|
for line in passwd_output:
|
|
parts = line.split(':')
|
|
if len(parts) >= 7:
|
|
username = parts[0]
|
|
uid = int(parts[2]) if parts[2].isdigit() else 0
|
|
shell = parts[6].strip()
|
|
if 1000 <= uid < 10000:
|
|
passwd_users[username] = shell_map.get(shell, "infix-system:false")
|
|
|
|
# Get password hashes from shadow
|
|
shadow_output = HOST.run_multiline(["getent", "shadow"], [])
|
|
shadow_hashes = {}
|
|
for line in shadow_output:
|
|
parts = line.split(':')
|
|
if len(parts) >= 2:
|
|
username = parts[0]
|
|
password_hash = parts[1]
|
|
# Only include valid password hashes (not locked/disabled)
|
|
if (password_hash and
|
|
not password_hash.startswith('*') and
|
|
not password_hash.startswith('!')):
|
|
shadow_hashes[username] = password_hash
|
|
|
|
# Build user list from passwd users (1000 <= uid < 10000)
|
|
users = []
|
|
for username, shell in passwd_users.items():
|
|
user = {"name": username}
|
|
if username in shadow_hashes:
|
|
user["password"] = shadow_hashes[username]
|
|
user["infix-system:shell"] = shell
|
|
|
|
# Read SSH authorized keys from /var/run/sshd/${user}.keys
|
|
keys_file = f"/var/run/sshd/{username}.keys"
|
|
keys_content = HOST.read(keys_file)
|
|
if keys_content:
|
|
authorized_keys = []
|
|
for line in keys_content.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
parts = line.split(None, 2)
|
|
if len(parts) >= 2:
|
|
algorithm = parts[0]
|
|
key_data = parts[1]
|
|
# Use comment as key name, or generate one
|
|
key_name = parts[2] if len(parts) > 2 else f"{username}-key-{len(authorized_keys)}"
|
|
authorized_keys.append({
|
|
"name": key_name,
|
|
"algorithm": algorithm,
|
|
"key-data": key_data
|
|
})
|
|
if authorized_keys:
|
|
user["authorized-key"] = authorized_keys
|
|
|
|
users.append(user)
|
|
|
|
if users:
|
|
insert(out, "authentication", "user", users)
|
|
|
|
def add_clock(out):
|
|
clock = {}
|
|
clock_now=YangDate()
|
|
|
|
uptime=HOST.read("/proc/uptime")
|
|
uptime = float(uptime.split()[0])
|
|
|
|
clock["boot-datetime"] = str(clock_now.from_seconds(uptime))
|
|
clock["current-datetime"] = str(clock_now)
|
|
insert(out, "clock", clock)
|
|
|
|
def add_resource_usage(out):
|
|
"""Add system resource usage (memory, load average, filesystem) to system-state"""
|
|
resource = {}
|
|
|
|
# Memory usage
|
|
try:
|
|
meminfo = HOST.read("/proc/meminfo")
|
|
if not meminfo:
|
|
return
|
|
mem_info = {}
|
|
for line in meminfo.splitlines():
|
|
parts = line.split(":")
|
|
if len(parts) == 2:
|
|
key = parts[0].strip()
|
|
value = parts[1].strip()
|
|
if key in ["MemTotal", "MemFree", "MemAvailable"]:
|
|
# Store in KiB (as provided by /proc/meminfo, mislabeled as kB)
|
|
mem_info[key] = int(value.split()[0])
|
|
|
|
if mem_info:
|
|
memory = {}
|
|
if "MemTotal" in mem_info:
|
|
memory["total"] = str(mem_info["MemTotal"])
|
|
if "MemFree" in mem_info:
|
|
memory["free"] = str(mem_info["MemFree"])
|
|
if "MemAvailable" in mem_info:
|
|
memory["available"] = str(mem_info["MemAvailable"])
|
|
resource["memory"] = memory
|
|
except (FileNotFoundError, ValueError):
|
|
pass
|
|
|
|
# Load average
|
|
try:
|
|
loadavg = HOST.read("/proc/loadavg")
|
|
load_parts = loadavg.strip().split()
|
|
if len(load_parts) >= 3:
|
|
load = {
|
|
"load-1min": load_parts[0],
|
|
"load-5min": load_parts[1],
|
|
"load-15min": load_parts[2]
|
|
}
|
|
resource["load-average"] = load
|
|
except (FileNotFoundError, ValueError):
|
|
pass
|
|
|
|
# Filesystem usage
|
|
filesystems = []
|
|
for mount in ["/", "/var", "/cfg"]:
|
|
try:
|
|
result = HOST.run_multiline(["df", "-k", mount], [])
|
|
if len(result) > 1:
|
|
parts = result[1].split()
|
|
if len(parts) >= 4:
|
|
filesystems.append({
|
|
"mount-point": mount,
|
|
"size": str(parts[1]),
|
|
"used": str(parts[2]),
|
|
"available": str(parts[3])
|
|
})
|
|
except (subprocess.CalledProcessError, ValueError, IndexError):
|
|
pass
|
|
|
|
if filesystems:
|
|
resource["filesystem"] = filesystems
|
|
|
|
if resource:
|
|
insert(out, "infix-system:resource-usage", resource)
|
|
|
|
def operational():
|
|
out = {
|
|
"ietf-system:system": {
|
|
},
|
|
"ietf-system:system-state": {
|
|
}
|
|
}
|
|
out_state = out["ietf-system:system-state"]
|
|
out_system = out["ietf-system:system"]
|
|
add_hostname(out_system)
|
|
add_users(out_system)
|
|
add_timezone(out_system)
|
|
add_software(out_state)
|
|
add_ntp(out_state)
|
|
add_dns(out_state)
|
|
add_clock(out_state)
|
|
add_platform(out_state)
|
|
add_services(out_state)
|
|
add_resource_usage(out_state)
|
|
|
|
return out
|