From 3f72ef7412a41ac99b280cd4d6bf530a7ad208df Mon Sep 17 00:00:00 2001 From: jinghui <3970260761@qq.com> Date: Fri, 6 Nov 2026 16:20:25 +0800 Subject: [PATCH] feat: add source code --- Dockerfile | 36 +-- app/app.py | 827 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/app.py | 827 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1664 insertions(+), 26 deletions(-) create mode 100644 app/app.py create mode 100644 src/app.py diff --git a/Dockerfile b/Dockerfile index e958b94..ee7a440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,11 @@ -# Use an official Python runtime as a parent image -FROM python:3.9-slim-bullseye - -# Set the working directory in the container +FROM python:3.11-slim WORKDIR /app - -# Install system dependencies (for psutil if needed) -RUN apt-get update && apt-get install -y \ - smartmontools \ - net-tools \ - iproute2 \ - procps \ - lm-sensors \ - --no-install-recommends && \ - rm -rf /var/lib/apt/lists/* - -# Copy the current directory contents into the container at /app -COPY . /app - -# Install any needed packages specified in requirements.txt -RUN pip install --no-cache-dir -r requirements.txt - -# Make port 5000 available to the world outside this container -EXPOSE 5000 - -# Run app.py when the container launches -CMD ["python3", "app.py"] +RUN pip install --no-cache-dir flask psutil -i https://mirrors.aliyun.com/pypi/simple/ +COPY app.py . +COPY hw_mem.json . +COPY hw_disk.json . +COPY hw_net.json . +COPY public ./public +ENV PORT=7031 +EXPOSE 7031 +CMD ["python", "app.py"] diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..24fd3d8 --- /dev/null +++ b/app/app.py @@ -0,0 +1,827 @@ +from flask import Flask, jsonify, request, send_from_directory +import psutil, os, time, signal, threading, subprocess, platform, glob, json, struct +try: + psutil.PROCFS_PATH = os.environ.get("PROCFS_PATH", "/proc") +except Exception: + pass + +PROCFS = os.environ.get("PROCFS_PATH", "/host/proc") +if os.path.isdir(PROCFS): + psutil.PROCFS_PATH = PROCFS + +app = Flask(__name__, static_folder="public", static_url_path="") +PORT = int(os.environ.get("PORT", "7031")) + +# ---------- background sampler ---------- +cache = {"procs": [], "cpu": 0.0, "load": [0, 0, 0], "ts": 0} +net_prev = {} +net_rate = {} + +def fmt_status(s): + m = { + psutil.STATUS_RUNNING: ("活动", 1), + psutil.STATUS_SLEEPING: ("睡眠", 0), + psutil.STATUS_DISK_SLEEP: ("睡眠", 0), + psutil.STATUS_STOPPED: ("停止", 0), + psutil.STATUS_ZOMBIE: ("僵尸", 0), + } + return m.get(s, ("睡眠", 0)) + +_uid_cache = {"map": {}, "ts": 0} + +def uid_name(uid_str): + # 容器内 /etc/passwd 没有宿主用户,uid 会显示成数字,用宿主 passwd 还原 + if not uid_str or not uid_str.isdigit(): + return uid_str + now = time.time() + if now - _uid_cache["ts"] > 300: + m = {} + try: + with open("/host/etc/passwd") as f: + for line in f: + p = line.split(":") + if len(p) > 3: + try: + m[p[2]] = p[0] + except Exception: + pass + except Exception: + pass + _uid_cache["map"] = m + _uid_cache["ts"] = now + return _uid_cache["map"].get(uid_str, uid_str) + +def sample_procs(): + # init cpu percent calculation + for p in psutil.process_iter(): + try: + p.cpu_percent(None) + except Exception: + pass + time.sleep(1.0) + procs = [] + try: + cpu_total = psutil.cpu_percent(interval=None) + except Exception: + cpu_total = 0 + try: + load = list(os.getloadavg()) + except Exception: + load = [0, 0, 0] + for p in psutil.process_iter(["pid", "name", "username", "status", "num_threads", "cmdline"]): + try: + with p.oneshot(): + cpu = p.cpu_percent(None) + mem = p.memory_percent() + try: + rss = p.memory_info().rss + except Exception: + rss = 0 + uss = 0 + swap = 0 + try: + mfi = p.memory_full_info() + uss = getattr(mfi, "uss", 0) or 0 + swap = getattr(mfi, "swap", 0) or 0 + except Exception: + pass + try: + io = p.io_counters() + rd, wr = io.read_bytes, io.write_bytes + except Exception: + rd, wr = 0, 0 + try: + conns = len(p.net_connections()) + except Exception: + conns = 0 + info = p.info + name = info.get("name") or "" + cmd = " ".join(info.get("cmdline") or [])[:120] + desc = cmd if cmd else name + st, act = fmt_status(info.get("status")) + if (not act) and (cpu or 0) > 1.0: + st, act = "活动", 1 + procs.append({ + "name": name[:40], "pid": info["pid"], + "cpu": round(cpu or 0, 2), "mem": round(mem or 0, 2), + "rss": rss, "uss": uss, "swap": swap, "threads": info.get("num_threads") or 0, + "user": uid_name(str(info.get("username") or ""))[:20], + "status": st, "active": act, + "desc": desc[:80], "io_r": rd, "io_w": wr, "conns": conns, + }) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + except Exception: + continue + procs.sort(key=lambda x: x["cpu"], reverse=True) + cache["procs"] = procs + cache["cpu"] = round(cpu_total, 1) + cache["load"] = [round(x, 2) for x in load] + cache["ts"] = time.time() + +def sampler_loop(): + while True: + try: + sample_procs() + except Exception: + pass + time.sleep(2) + +threading.Thread(target=sampler_loop, daemon=True).start() +# warm up immediately in background; serve stale until ready + +@app.route("/") +def index(): + return send_from_directory("public", "index.html") + +@app.route("/api/cpu") +def api_cpu(): + active = sum(1 for p in cache["procs"] if p["active"]) + return jsonify({"cpu": cache["cpu"], "load": cache["load"], + "active": active, "total": len(cache["procs"]), + "info": get_cpu_info(), "percpu": get_percpu(), + "items": cache["procs"][:100]}) + +_cpu_static = {} + +def _parse_host_cpuinfo(): + info = {} + try: + with open("/host/proc/cpuinfo") as f: + txt = f.read() + except Exception: + try: + with open("/proc/cpuinfo") as f: + txt = f.read() + except Exception: + return info + # 取第一个 processor 段的 model name / cache size / cpu MHz + for line in txt.splitlines(): + if ":" not in line: + continue + k, v = line.split(":", 1) + k, v = k.strip(), v.strip() + if k == "model name" and "model" not in info: + info["model"] = v + elif k == "cache size" and "cache" not in info: + info["cache"] = v + return info + +def _parse_lscpu(): + out = {} + try: + txt = subprocess.run(["lscpu"], capture_output=True, text=True, + timeout=5).stdout + except Exception: + return out + for line in txt.splitlines(): + if ":" not in line and ":" not in line: + continue + line = line.replace(":", ":") + k, v = line.split(":", 1) + k, v = k.strip(), v.strip() + out[k] = v + return out + +def get_cpu_info(): + global _cpu_static + if not _cpu_static: + ci = _parse_host_cpuinfo() + lc = _parse_lscpu() + def pick(*keys): + for k in keys: + if k in lc and lc[k]: + return lc[k] + return "" + # L1d/L1i/L2/L3 键名中英文兼容 + l1d = pick("L1d 缓存", "L1d cache") + l1i = pick("L1i 缓存", "L1i cache") + l2 = pick("L2 缓存", "L2 cache") + l3 = pick("L3 缓存", "L3 cache") + _cpu_static = { + "model": ci.get("model") or pick("型号名称", "Model name"), + "arch": platform.machine(), + "sockets": pick("座", "Socket(s)"), + "cores_per_socket": pick("每个座的核数", "Core(s) per socket"), + "threads_per_core": pick("每个核的线程数", "Thread(s) per core"), + "l1d": l1d, "l1i": l1i, "l2": l2, "l3": l3, + "cache": ci.get("cache", ""), + } + try: + f = psutil.cpu_freq() + cur = round(f.current, 0) if f else 0 + fmin = round(f.min, 0) if f and f.min else 0 + fmax = round(f.max, 0) if f and f.max else 0 + except Exception: + cur = fmin = fmax = 0 + d = dict(_cpu_static) + d.update({ + "logical": psutil.cpu_count() or 0, + "physical": psutil.cpu_count(logical=False) or 0, + "freq_cur": cur, "freq_min": fmin, "freq_max": fmax, + }) + return d + +def get_percpu(): + try: + vals = psutil.cpu_percent(interval=None, percpu=True) + return [round(v, 1) for v in (vals or [])] + except Exception: + return [] + +@app.route("/api/mem") +def api_mem(): + vm = psutil.virtual_memory() + sm = psutil.swap_memory() + items = sorted(cache["procs"], key=lambda x: x["mem"], reverse=True)[:100] + mi = _read_meminfo_host() + dm = get_mem_hw() + return jsonify({ + "total": vm.total, "used": vm.used, "available": vm.available, + "percent": vm.percent, "cached": getattr(vm, "cached", 0), + "swap_total": sm.total, "swap_used": sm.used, "swap_percent": sm.percent, + "detail": mi, "hw": dm, + "items": items}) + +_mem_hw_static = {} + +def _read_meminfo_host(): + mi = {} + for p in ("/host/proc/meminfo", "/proc/meminfo"): + try: + with open(p) as f: + for line in f: + if ":" not in line: + continue + k, v = line.split(":", 1) + parts = v.strip().split() + if not parts: + continue + try: + num = int(parts[0]) + except ValueError: + continue + # 统一转 bytes + if len(parts) > 1 and parts[1] == "kB": + num *= 1024 + mi[k.strip()] = num + if mi: + break + except Exception: + continue + return mi + +def _walk_dmi(path): + """yield (type, raw_bytes, strings)""" + try: + with open(path, "rb") as f: + data = f.read() + except Exception: + return + off, n = 0, 0 + L = len(data) + while off + 4 <= L and n < 200: + typ, ln = data[off], data[off + 1] + if ln < 4 or off + ln > L: + break + if typ == 127: # end of table + break + s = off + ln + strs = [] + ok = True + while s < L: + try: + e = data.index(b"\x00", s) + except ValueError: + ok = False + break + if e == s: + s += 1 + break + strs.append(data[s:e].decode("utf-8", "replace").strip()) + s = e + 1 + if not ok: + break + yield typ, data[off:off + ln], strs + off = s + n += 1 + +_MEM_TYPE = {0x02: "Unknown", 0x03: "DRAM", 0x12: "DDR", 0x13: "DDR2", + 0x14: "DDR2 FB-DIMM", 0x18: "DDR3", 0x19: "FBD2", 0x1A: "DDR4", + 0x1B: "LPDDR", 0x1C: "LPDDR2", 0x1D: "LPDDR3", 0x1E: "LPDDR4", + 0x1F: "Logical NV", 0x20: "HBM", 0x21: "HBM2", 0x22: "DDR5", + 0x23: "LPDDR5", 0x24: "HBM3"} + +def _parse_dmi_mem(): + dimms, max_cap_kb = [], 0 + for dmi_path in ("/host/sys/firmware/dmi/tables/DMI", + "/sys/firmware/dmi/tables/DMI"): + found = False + for typ, raw, strs in _walk_dmi(dmi_path): + found = True + def S(i): + return strs[i - 1] if 1 <= i <= len(strs) else "" + if typ == 16 and len(raw) >= 11: + import struct + (cap,) = struct.unpack_from("= 28: + import struct + (size,) = struct.unpack_from("= 1024 else 1 + size_b = (size & 0x7FFF) * 1024 + else: + size_mb = size & 0x7FFF + size_b = size_mb * 1024 * 1024 + if size_mb == 0x7FFF: + continue + (speed,) = struct.unpack_from("= 30: + (cspeed,) = struct.unpack_from(" 18 else 0 + dimms.append({ + "locator": S(raw[16]) if len(raw) > 16 else "", + "bank": S(raw[17]) if len(raw) > 17 else "", + "size_b": size_b, "size_mb": size_mb, + "type": _MEM_TYPE.get(mtype, "DDR4" if mtype == 0 else hex(mtype)), + "speed": speed or 0, "cspeed": cspeed or 0, + "maker": S(raw[22]) if len(raw) > 22 else "", + "serial": S(raw[23]) if len(raw) > 23 else "", + "part": S(raw[25]) if len(raw) > 25 else "", + }) + if found: + break + return dimms, max_cap_kb + +def _get_vram(): + total = 0 + for base in ("/host/sys/class/drm", "/sys/class/drm"): + try: + for p in glob.glob(base + "/card*/device/mem_info_vram_total"): + try: + with open(p) as f: + total += int(f.read().strip()) + except Exception: + pass + except Exception: + pass + if total: + break + return total + +def get_mem_hw(): + global _mem_hw_static + if not _mem_hw_static: + dimms, max_cap_kb = [], 0 + # 优先读构建时从宿主 dmidecode 导出的静态槽位表 + for jp in ("hw_mem.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_mem.json")): + try: + with open(jp) as f: + hj = json.load(f) + dimms = hj.get("dimms", []) or [] + max_cap_kb = int((hj.get("max_cap_b", 0) or 0) // 1024) + if dimms: + break + except Exception: + continue + if not dimms: + dimms, max_cap_kb = _parse_dmi_mem() + installed = sum(d["size_b"] for d in dimms) + _mem_hw_static = {"dimms": dimms, "installed": installed, + "max_cap_kb": max_cap_kb} + d = {"dimms": _mem_hw_static["dimms"], + "installed": _mem_hw_static["installed"], + "max_cap_kb": _mem_hw_static["max_cap_kb"]} + try: + mem_total = psutil.virtual_memory().total + except Exception: + mem_total = 0 + d["vram"] = _get_vram() + d["system_total"] = mem_total + d["reserved"] = (d["installed"] - mem_total) if d["installed"] and mem_total else 0 + return d + +HOST_MNTS = [("/", "/host"), ("/vol1", "/host/vol1"), + ("/vol2", "/host/vol2"), ("/vol3", "/host/vol3")] + +def _df_host(): + """容器内 df 直读宿主路径(经 bind 进来的宿主目录),返回 {mnt: {...}}""" + try: + out = subprocess.run( + ["df", "-B1", "--output=source,target,fstype,size,used,avail", + "/host", "/host/vol1", "/host/vol2", "/host/vol3"], + capture_output=True, text=True, timeout=5).stdout.splitlines() + res = {} + for line in out[1:]: + p = line.split() + if len(p) < 6: + continue + dev, tgt, fst, size, used, avail = p[0], p[1], p[2], int(p[3]), int(p[4]), int(p[5]) + mnt = tgt.replace("/host", "", 1) or "/" + res[mnt] = {"device": dev, "mount": mnt, "fstype": fst, + "total": size, "used": used, "free": avail, + "percent": round(used / size * 100, 1) if size else 0} + return res + except Exception: + return {} + +@app.route("/api/mem/drop", methods=["POST"]) +def api_mem_drop(): + import subprocess + try: + subprocess.run(["sync"], check=False, timeout=10) + p = os.environ.get("PROCFS_PATH", "/proc") + with open(os.path.join(p, "sys", "vm", "drop_caches"), "w") as f: + f.write("3") + vm = psutil.virtual_memory() + return jsonify({"ok": True, "available": vm.available, "percent": vm.percent}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 +@app.route("/api/disk") +def api_disk(): + parts = [] + dfm = _df_host() + for mp, hp in HOST_MNTS: + if mp in dfm: + parts.append(dfm[mp]) + continue + try: + st = os.statvfs(hp) + total = st.f_blocks * st.f_frsize + free = st.f_bavail * st.f_frsize + used = total - free + parts.append({"device": "?", "mount": mp, "fstype": "?", + "total": total, "used": used, "free": free, + "percent": round(used / total * 100, 1) if total else 0}) + except Exception: + continue + try: + dio = psutil.disk_io_counters() + dio_d = {"read": dio.read_bytes, "write": dio.write_bytes} if dio else {} + except Exception: + dio_d = {} + # top io processes + items = sorted(cache["procs"], key=lambda x: x["io_r"] + x["io_w"], reverse=True)[:60] + return jsonify({"parts": parts, "io": dio_d, "hw": get_disk_hw(), + "items": items}) + +_disk_hw_static = {} + +def _read(p, default=""): + try: + with open(p) as f: + return f.read().strip() + except Exception: + return default + +def _sys_block_base(): + for base in ("/host/sys/block", "/sys/block"): + try: + if os.path.isdir(base): + return base + except Exception: + continue + return "/host/sys/block" + +def _live_sysfs(name): + """实时 sysfs: 容量/转速/型号(覆盖静态快照中的漂移项)""" + base = _sys_block_base() + d = os.path.join(base, name) + out = {} + try: + sectors = _read(os.path.join(d, "size")) + if sectors.isdigit(): + out["cap_b"] = int(sectors) * 512 + except Exception: + pass + rot = _read(os.path.join(d, "queue/rotational")) + if rot in ("0", "1"): + out["rotational"] = (rot == "1") + model = _read(os.path.join(d, "device/model")) + if model: + out["sys_model"] = model + vendor = _read(os.path.join(d, "device/vendor")) + if vendor: + out["sys_vendor"] = vendor + return out + +def _live_mdstat(): + """实时 mdstat: 每组阵列状态/同步进度,键为 md 名""" + st = {} + for p in ("/host/proc/mdstat", "/proc/mdstat"): + try: + with open(p) as f: + txt = f.read() + break + except Exception: + txt = "" + else: + return st + import re + cur = None + for line in txt.splitlines(): + m = re.match(r"^(md\d+)\s*:\s*(\w+)\s+(\w+)(.*)$", line) + if m: + cur = m.group(1) + st[cur] = {"status": m.group(2), "level": m.group(3), + "devs": m.group(4).strip(), "sync": ""} + continue + if cur and ("recovery" in line or "resync" in line or "blocks" in line): + s = line.strip() + if "recovery" in line or "resync" in line: + st[cur]["sync"] = s + elif "blocks" in line and not st[cur].get("blocks"): + st[cur]["blocks"] = s + return st + +def _live_perdisk_io(): + try: + d = psutil.disk_io_counters(perdisk=True) or {} + except Exception: + return {} + out = {} + for k, v in d.items(): + try: + out[k] = {"read": v.read_bytes, "write": v.write_bytes} + except Exception: + continue + return out + +def get_disk_hw(): + global _disk_hw_static + if not _disk_hw_static: + hj = {} + for jp in ("hw_disk.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_disk.json")): + try: + with open(jp) as f: + hj = json.load(f) + if hj.get("disks"): + break + except Exception: + continue + _disk_hw_static = hj or {"disks": [], "arrays": []} + disks = [dict(x) for x in _disk_hw_static.get("disks", [])] + arrays = [dict(x) for x in _disk_hw_static.get("arrays", [])] + # 合并实时数据 + pio = _live_perdisk_io() + for dk in disks: + nm = dk.get("name", "") + dk.update(_live_sysfs(nm)) + if nm in pio: + dk["io_read"] = pio[nm]["read"] + dk["io_write"] = pio[nm]["write"] + # 转速文字 + if dk.get("rotational") is True and not dk.get("rpm"): + dk["rpm"] = 5400 + md_live = _live_mdstat() + for a in arrays: + md = a.get("md", "") + lv = md_live.get(md, {}) + if lv: + a["live_status"] = lv.get("status", "") + a["live_devs"] = lv.get("devs", "") + a["live_sync"] = lv.get("sync", "") + if md in pio: + a["io_read"] = pio[md]["read"] + a["io_write"] = pio[md]["write"] + return {"disks": disks, "arrays": arrays, + "extra": _disk_hw_static.get("extra", {}), + "snapshot": _disk_hw_static.get("snapshot", ""), + "note": _disk_hw_static.get("note", "")} + +@app.route("/api/net") +def api_net(): + global net_prev, net_rate + now = time.time() + try: + counters = psutil.net_io_counters(pernic=True) + except Exception: + counters = {} + rates = [] + for nic, c in counters.items(): + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + prev = net_prev.get(nic) + if prev: + dt = max(now - prev["t"], 0.5) + up = (c.bytes_sent - prev["sent"]) / dt + down = (c.bytes_recv - prev["recv"]) / dt + else: + up = down = 0 + net_prev[nic] = {"sent": c.bytes_sent, "recv": c.bytes_recv, "t": now} + rates.append({"nic": nic, "up": max(up, 0), "down": max(down, 0), + "sent": c.bytes_sent, "recv": c.bytes_recv}) + rates.sort(key=lambda x: x["up"] + x["down"], reverse=True) + try: + conns = len(psutil.net_connections()) + except Exception: + conns = 0 + total_recv = 0 + total_sent = 0 + for nic, c in counters.items(): + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + total_recv += getattr(c, "bytes_recv", 0) or 0 + total_sent += getattr(c, "bytes_sent", 0) or 0 + items = sorted(cache["procs"], key=lambda x: x["conns"], reverse=True)[:60] + tcp_list = [] + listen_list = [] + try: + name_by_pid = {p["pid"]: p["name"] for p in cache.get("procs", [])} + for c in psutil.net_connections(kind="tcp"): + laddr = f"{c.laddr.ip}:{c.laddr.port}" if c.laddr else "" + raddr = f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else "" + family = "ipv6" if ":" in (c.laddr.ip if c.laddr else "") and "." not in (c.laddr.ip if c.laddr else "") else "ipv4" + # better family detection + import socket + family = "ipv6" if (c.family == socket.AF_INET6) else "ipv4" + if c.status == "LISTEN": + listen_list.append({"name": name_by_pid.get(c.pid, ""), "laddr": c.laddr.ip if c.laddr else "", "port": c.laddr.port if c.laddr else 0, "family": family, "pid": c.pid, "proto": "tcp"}) + else: + tcp_list.append({"name": name_by_pid.get(c.pid, ""), "laddr": laddr, "raddr": raddr, "family": family, "pid": c.pid, "status": c.status}) + except Exception: + pass + tcp_list = tcp_list[:80] + listen_list = listen_list[:80] + return jsonify({"nics": rates, "conns": conns, "items": items, + "hw": get_net_hw(), + "total_recv": total_recv, "total_sent": total_sent, + "tcp": tcp_list, "listen": listen_list}) + +_net_hw_static = {} + +def _net_sysfs(name): + """实时 sysfs: 状态/MTU/速率/MAC,键名与 psutil 速率互补""" + out = {} + for base in ("/host/sys/class/net", "/sys/class/net"): + d = os.path.join(base, name) + try: + if not os.path.isdir(d): + continue + for k in ("address", "operstate", "mtu", "speed", "duplex", + "carrier"): + v = _read(os.path.join(d, k)) + if v: + out[k] = v + # 驱动名 + try: + drv = os.path.basename(os.readlink(os.path.join(d, "device/driver"))) + if drv: + out["live_driver"] = drv + except Exception: + pass + if out: + break + except Exception: + continue + return out + +def _net_addrs(): + try: + ad = psutil.net_if_addrs() or {} + except Exception: + return {} + out = {} + for nic, lst in ad.items(): + v4, v6, mac = [], [], "" + for a in lst: + try: + fam = a.family.name if hasattr(a.family, "name") else str(a.family) + except Exception: + fam = "" + addr = a.address or "" + if "PACKET" in fam: + if addr: + mac = mac or addr + elif "AF_INET6" in fam: + if addr and not addr.startswith("fe80"): + v6.append(addr.split("%")[0]) + elif addr: + v6.append(addr.split("%")[0] + "(链路)") + elif "AF_INET" in fam: + if addr and len(addr.split(".")) == 4: + v4.append(addr) + out[nic] = {"v4": v4, "v6": v6, "mac": mac} + return out + +def get_net_hw(): + global _net_hw_static + if not _net_hw_static: + hj = {} + for jp in ("hw_net.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_net.json")): + try: + with open(jp) as f: + hj = json.load(f) + if hj.get("nics"): + break + except Exception: + continue + _net_hw_static = hj or {"nics": []} + statics = {x.get("name"): dict(x) for x in _net_hw_static.get("nics", [])} + try: + st = psutil.net_if_stats() or {} + except Exception: + st = {} + addrs = _net_addrs() + # 展示的网卡: 静态档案 + 实际有流量的非虚拟口 + names = list(statics.keys()) + try: + counters = psutil.net_io_counters(pernic=True) or {} + except Exception: + counters = {} + for nic in counters: + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + if nic not in statics: + names.append(nic) + out = [] + for nic in names: + base = dict(statics.get(nic, {"name": nic})) + s = st.get(nic) + if s is not None: + try: + base["isup"] = bool(s.isup) + base["live_speed"] = s.speed or 0 + base["live_mtu"] = s.mtu or 0 + except Exception: + pass + base.update(_net_sysfs(nic)) + a = addrs.get(nic, {}) + if a: + base["ipv4"] = a.get("v4", []) + base["ipv6"] = a.get("v6", []) + if a.get("mac") and not base.get("mac"): + base["mac"] = a["mac"] + c = counters.get(nic) + if c is not None: + try: + base["io_sent"] = c.bytes_sent + base["io_recv"] = c.bytes_recv + except Exception: + pass + out.append(base) + # 默认网关/DNS 实时读 + gw, dns = _net_hw_static.get("gateway", ""), _net_hw_static.get("dns", "") + base_if = "" + try: + with open("/host/proc/net/route" if os.path.exists("/host/proc/net/route") else "/proc/net/route") as f: + for line in f.read().splitlines()[1:]: + f_ = line.split() + if len(f_) > 2 and f_[1] == "00000000" and f_[7] == "00000000": + import socket, struct + gw = socket.inet_ntoa(struct.pack(" 300: + m = {} + try: + with open("/host/etc/passwd") as f: + for line in f: + p = line.split(":") + if len(p) > 3: + try: + m[p[2]] = p[0] + except Exception: + pass + except Exception: + pass + _uid_cache["map"] = m + _uid_cache["ts"] = now + return _uid_cache["map"].get(uid_str, uid_str) + +def sample_procs(): + # init cpu percent calculation + for p in psutil.process_iter(): + try: + p.cpu_percent(None) + except Exception: + pass + time.sleep(1.0) + procs = [] + try: + cpu_total = psutil.cpu_percent(interval=None) + except Exception: + cpu_total = 0 + try: + load = list(os.getloadavg()) + except Exception: + load = [0, 0, 0] + for p in psutil.process_iter(["pid", "name", "username", "status", "num_threads", "cmdline"]): + try: + with p.oneshot(): + cpu = p.cpu_percent(None) + mem = p.memory_percent() + try: + rss = p.memory_info().rss + except Exception: + rss = 0 + uss = 0 + swap = 0 + try: + mfi = p.memory_full_info() + uss = getattr(mfi, "uss", 0) or 0 + swap = getattr(mfi, "swap", 0) or 0 + except Exception: + pass + try: + io = p.io_counters() + rd, wr = io.read_bytes, io.write_bytes + except Exception: + rd, wr = 0, 0 + try: + conns = len(p.net_connections()) + except Exception: + conns = 0 + info = p.info + name = info.get("name") or "" + cmd = " ".join(info.get("cmdline") or [])[:120] + desc = cmd if cmd else name + st, act = fmt_status(info.get("status")) + if (not act) and (cpu or 0) > 1.0: + st, act = "活动", 1 + procs.append({ + "name": name[:40], "pid": info["pid"], + "cpu": round(cpu or 0, 2), "mem": round(mem or 0, 2), + "rss": rss, "uss": uss, "swap": swap, "threads": info.get("num_threads") or 0, + "user": uid_name(str(info.get("username") or ""))[:20], + "status": st, "active": act, + "desc": desc[:80], "io_r": rd, "io_w": wr, "conns": conns, + }) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + except Exception: + continue + procs.sort(key=lambda x: x["cpu"], reverse=True) + cache["procs"] = procs + cache["cpu"] = round(cpu_total, 1) + cache["load"] = [round(x, 2) for x in load] + cache["ts"] = time.time() + +def sampler_loop(): + while True: + try: + sample_procs() + except Exception: + pass + time.sleep(2) + +threading.Thread(target=sampler_loop, daemon=True).start() +# warm up immediately in background; serve stale until ready + +@app.route("/") +def index(): + return send_from_directory("public", "index.html") + +@app.route("/api/cpu") +def api_cpu(): + active = sum(1 for p in cache["procs"] if p["active"]) + return jsonify({"cpu": cache["cpu"], "load": cache["load"], + "active": active, "total": len(cache["procs"]), + "info": get_cpu_info(), "percpu": get_percpu(), + "items": cache["procs"][:100]}) + +_cpu_static = {} + +def _parse_host_cpuinfo(): + info = {} + try: + with open("/host/proc/cpuinfo") as f: + txt = f.read() + except Exception: + try: + with open("/proc/cpuinfo") as f: + txt = f.read() + except Exception: + return info + # 取第一个 processor 段的 model name / cache size / cpu MHz + for line in txt.splitlines(): + if ":" not in line: + continue + k, v = line.split(":", 1) + k, v = k.strip(), v.strip() + if k == "model name" and "model" not in info: + info["model"] = v + elif k == "cache size" and "cache" not in info: + info["cache"] = v + return info + +def _parse_lscpu(): + out = {} + try: + txt = subprocess.run(["lscpu"], capture_output=True, text=True, + timeout=5).stdout + except Exception: + return out + for line in txt.splitlines(): + if ":" not in line and ":" not in line: + continue + line = line.replace(":", ":") + k, v = line.split(":", 1) + k, v = k.strip(), v.strip() + out[k] = v + return out + +def get_cpu_info(): + global _cpu_static + if not _cpu_static: + ci = _parse_host_cpuinfo() + lc = _parse_lscpu() + def pick(*keys): + for k in keys: + if k in lc and lc[k]: + return lc[k] + return "" + # L1d/L1i/L2/L3 键名中英文兼容 + l1d = pick("L1d 缓存", "L1d cache") + l1i = pick("L1i 缓存", "L1i cache") + l2 = pick("L2 缓存", "L2 cache") + l3 = pick("L3 缓存", "L3 cache") + _cpu_static = { + "model": ci.get("model") or pick("型号名称", "Model name"), + "arch": platform.machine(), + "sockets": pick("座", "Socket(s)"), + "cores_per_socket": pick("每个座的核数", "Core(s) per socket"), + "threads_per_core": pick("每个核的线程数", "Thread(s) per core"), + "l1d": l1d, "l1i": l1i, "l2": l2, "l3": l3, + "cache": ci.get("cache", ""), + } + try: + f = psutil.cpu_freq() + cur = round(f.current, 0) if f else 0 + fmin = round(f.min, 0) if f and f.min else 0 + fmax = round(f.max, 0) if f and f.max else 0 + except Exception: + cur = fmin = fmax = 0 + d = dict(_cpu_static) + d.update({ + "logical": psutil.cpu_count() or 0, + "physical": psutil.cpu_count(logical=False) or 0, + "freq_cur": cur, "freq_min": fmin, "freq_max": fmax, + }) + return d + +def get_percpu(): + try: + vals = psutil.cpu_percent(interval=None, percpu=True) + return [round(v, 1) for v in (vals or [])] + except Exception: + return [] + +@app.route("/api/mem") +def api_mem(): + vm = psutil.virtual_memory() + sm = psutil.swap_memory() + items = sorted(cache["procs"], key=lambda x: x["mem"], reverse=True)[:100] + mi = _read_meminfo_host() + dm = get_mem_hw() + return jsonify({ + "total": vm.total, "used": vm.used, "available": vm.available, + "percent": vm.percent, "cached": getattr(vm, "cached", 0), + "swap_total": sm.total, "swap_used": sm.used, "swap_percent": sm.percent, + "detail": mi, "hw": dm, + "items": items}) + +_mem_hw_static = {} + +def _read_meminfo_host(): + mi = {} + for p in ("/host/proc/meminfo", "/proc/meminfo"): + try: + with open(p) as f: + for line in f: + if ":" not in line: + continue + k, v = line.split(":", 1) + parts = v.strip().split() + if not parts: + continue + try: + num = int(parts[0]) + except ValueError: + continue + # 统一转 bytes + if len(parts) > 1 and parts[1] == "kB": + num *= 1024 + mi[k.strip()] = num + if mi: + break + except Exception: + continue + return mi + +def _walk_dmi(path): + """yield (type, raw_bytes, strings)""" + try: + with open(path, "rb") as f: + data = f.read() + except Exception: + return + off, n = 0, 0 + L = len(data) + while off + 4 <= L and n < 200: + typ, ln = data[off], data[off + 1] + if ln < 4 or off + ln > L: + break + if typ == 127: # end of table + break + s = off + ln + strs = [] + ok = True + while s < L: + try: + e = data.index(b"\x00", s) + except ValueError: + ok = False + break + if e == s: + s += 1 + break + strs.append(data[s:e].decode("utf-8", "replace").strip()) + s = e + 1 + if not ok: + break + yield typ, data[off:off + ln], strs + off = s + n += 1 + +_MEM_TYPE = {0x02: "Unknown", 0x03: "DRAM", 0x12: "DDR", 0x13: "DDR2", + 0x14: "DDR2 FB-DIMM", 0x18: "DDR3", 0x19: "FBD2", 0x1A: "DDR4", + 0x1B: "LPDDR", 0x1C: "LPDDR2", 0x1D: "LPDDR3", 0x1E: "LPDDR4", + 0x1F: "Logical NV", 0x20: "HBM", 0x21: "HBM2", 0x22: "DDR5", + 0x23: "LPDDR5", 0x24: "HBM3"} + +def _parse_dmi_mem(): + dimms, max_cap_kb = [], 0 + for dmi_path in ("/host/sys/firmware/dmi/tables/DMI", + "/sys/firmware/dmi/tables/DMI"): + found = False + for typ, raw, strs in _walk_dmi(dmi_path): + found = True + def S(i): + return strs[i - 1] if 1 <= i <= len(strs) else "" + if typ == 16 and len(raw) >= 11: + import struct + (cap,) = struct.unpack_from("= 28: + import struct + (size,) = struct.unpack_from("= 1024 else 1 + size_b = (size & 0x7FFF) * 1024 + else: + size_mb = size & 0x7FFF + size_b = size_mb * 1024 * 1024 + if size_mb == 0x7FFF: + continue + (speed,) = struct.unpack_from("= 30: + (cspeed,) = struct.unpack_from(" 18 else 0 + dimms.append({ + "locator": S(raw[16]) if len(raw) > 16 else "", + "bank": S(raw[17]) if len(raw) > 17 else "", + "size_b": size_b, "size_mb": size_mb, + "type": _MEM_TYPE.get(mtype, "DDR4" if mtype == 0 else hex(mtype)), + "speed": speed or 0, "cspeed": cspeed or 0, + "maker": S(raw[22]) if len(raw) > 22 else "", + "serial": S(raw[23]) if len(raw) > 23 else "", + "part": S(raw[25]) if len(raw) > 25 else "", + }) + if found: + break + return dimms, max_cap_kb + +def _get_vram(): + total = 0 + for base in ("/host/sys/class/drm", "/sys/class/drm"): + try: + for p in glob.glob(base + "/card*/device/mem_info_vram_total"): + try: + with open(p) as f: + total += int(f.read().strip()) + except Exception: + pass + except Exception: + pass + if total: + break + return total + +def get_mem_hw(): + global _mem_hw_static + if not _mem_hw_static: + dimms, max_cap_kb = [], 0 + # 优先读构建时从宿主 dmidecode 导出的静态槽位表 + for jp in ("hw_mem.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_mem.json")): + try: + with open(jp) as f: + hj = json.load(f) + dimms = hj.get("dimms", []) or [] + max_cap_kb = int((hj.get("max_cap_b", 0) or 0) // 1024) + if dimms: + break + except Exception: + continue + if not dimms: + dimms, max_cap_kb = _parse_dmi_mem() + installed = sum(d["size_b"] for d in dimms) + _mem_hw_static = {"dimms": dimms, "installed": installed, + "max_cap_kb": max_cap_kb} + d = {"dimms": _mem_hw_static["dimms"], + "installed": _mem_hw_static["installed"], + "max_cap_kb": _mem_hw_static["max_cap_kb"]} + try: + mem_total = psutil.virtual_memory().total + except Exception: + mem_total = 0 + d["vram"] = _get_vram() + d["system_total"] = mem_total + d["reserved"] = (d["installed"] - mem_total) if d["installed"] and mem_total else 0 + return d + +HOST_MNTS = [("/", "/host"), ("/vol1", "/host/vol1"), + ("/vol2", "/host/vol2"), ("/vol3", "/host/vol3")] + +def _df_host(): + """容器内 df 直读宿主路径(经 bind 进来的宿主目录),返回 {mnt: {...}}""" + try: + out = subprocess.run( + ["df", "-B1", "--output=source,target,fstype,size,used,avail", + "/host", "/host/vol1", "/host/vol2", "/host/vol3"], + capture_output=True, text=True, timeout=5).stdout.splitlines() + res = {} + for line in out[1:]: + p = line.split() + if len(p) < 6: + continue + dev, tgt, fst, size, used, avail = p[0], p[1], p[2], int(p[3]), int(p[4]), int(p[5]) + mnt = tgt.replace("/host", "", 1) or "/" + res[mnt] = {"device": dev, "mount": mnt, "fstype": fst, + "total": size, "used": used, "free": avail, + "percent": round(used / size * 100, 1) if size else 0} + return res + except Exception: + return {} + +@app.route("/api/mem/drop", methods=["POST"]) +def api_mem_drop(): + import subprocess + try: + subprocess.run(["sync"], check=False, timeout=10) + p = os.environ.get("PROCFS_PATH", "/proc") + with open(os.path.join(p, "sys", "vm", "drop_caches"), "w") as f: + f.write("3") + vm = psutil.virtual_memory() + return jsonify({"ok": True, "available": vm.available, "percent": vm.percent}) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 +@app.route("/api/disk") +def api_disk(): + parts = [] + dfm = _df_host() + for mp, hp in HOST_MNTS: + if mp in dfm: + parts.append(dfm[mp]) + continue + try: + st = os.statvfs(hp) + total = st.f_blocks * st.f_frsize + free = st.f_bavail * st.f_frsize + used = total - free + parts.append({"device": "?", "mount": mp, "fstype": "?", + "total": total, "used": used, "free": free, + "percent": round(used / total * 100, 1) if total else 0}) + except Exception: + continue + try: + dio = psutil.disk_io_counters() + dio_d = {"read": dio.read_bytes, "write": dio.write_bytes} if dio else {} + except Exception: + dio_d = {} + # top io processes + items = sorted(cache["procs"], key=lambda x: x["io_r"] + x["io_w"], reverse=True)[:60] + return jsonify({"parts": parts, "io": dio_d, "hw": get_disk_hw(), + "items": items}) + +_disk_hw_static = {} + +def _read(p, default=""): + try: + with open(p) as f: + return f.read().strip() + except Exception: + return default + +def _sys_block_base(): + for base in ("/host/sys/block", "/sys/block"): + try: + if os.path.isdir(base): + return base + except Exception: + continue + return "/host/sys/block" + +def _live_sysfs(name): + """实时 sysfs: 容量/转速/型号(覆盖静态快照中的漂移项)""" + base = _sys_block_base() + d = os.path.join(base, name) + out = {} + try: + sectors = _read(os.path.join(d, "size")) + if sectors.isdigit(): + out["cap_b"] = int(sectors) * 512 + except Exception: + pass + rot = _read(os.path.join(d, "queue/rotational")) + if rot in ("0", "1"): + out["rotational"] = (rot == "1") + model = _read(os.path.join(d, "device/model")) + if model: + out["sys_model"] = model + vendor = _read(os.path.join(d, "device/vendor")) + if vendor: + out["sys_vendor"] = vendor + return out + +def _live_mdstat(): + """实时 mdstat: 每组阵列状态/同步进度,键为 md 名""" + st = {} + for p in ("/host/proc/mdstat", "/proc/mdstat"): + try: + with open(p) as f: + txt = f.read() + break + except Exception: + txt = "" + else: + return st + import re + cur = None + for line in txt.splitlines(): + m = re.match(r"^(md\d+)\s*:\s*(\w+)\s+(\w+)(.*)$", line) + if m: + cur = m.group(1) + st[cur] = {"status": m.group(2), "level": m.group(3), + "devs": m.group(4).strip(), "sync": ""} + continue + if cur and ("recovery" in line or "resync" in line or "blocks" in line): + s = line.strip() + if "recovery" in line or "resync" in line: + st[cur]["sync"] = s + elif "blocks" in line and not st[cur].get("blocks"): + st[cur]["blocks"] = s + return st + +def _live_perdisk_io(): + try: + d = psutil.disk_io_counters(perdisk=True) or {} + except Exception: + return {} + out = {} + for k, v in d.items(): + try: + out[k] = {"read": v.read_bytes, "write": v.write_bytes} + except Exception: + continue + return out + +def get_disk_hw(): + global _disk_hw_static + if not _disk_hw_static: + hj = {} + for jp in ("hw_disk.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_disk.json")): + try: + with open(jp) as f: + hj = json.load(f) + if hj.get("disks"): + break + except Exception: + continue + _disk_hw_static = hj or {"disks": [], "arrays": []} + disks = [dict(x) for x in _disk_hw_static.get("disks", [])] + arrays = [dict(x) for x in _disk_hw_static.get("arrays", [])] + # 合并实时数据 + pio = _live_perdisk_io() + for dk in disks: + nm = dk.get("name", "") + dk.update(_live_sysfs(nm)) + if nm in pio: + dk["io_read"] = pio[nm]["read"] + dk["io_write"] = pio[nm]["write"] + # 转速文字 + if dk.get("rotational") is True and not dk.get("rpm"): + dk["rpm"] = 5400 + md_live = _live_mdstat() + for a in arrays: + md = a.get("md", "") + lv = md_live.get(md, {}) + if lv: + a["live_status"] = lv.get("status", "") + a["live_devs"] = lv.get("devs", "") + a["live_sync"] = lv.get("sync", "") + if md in pio: + a["io_read"] = pio[md]["read"] + a["io_write"] = pio[md]["write"] + return {"disks": disks, "arrays": arrays, + "extra": _disk_hw_static.get("extra", {}), + "snapshot": _disk_hw_static.get("snapshot", ""), + "note": _disk_hw_static.get("note", "")} + +@app.route("/api/net") +def api_net(): + global net_prev, net_rate + now = time.time() + try: + counters = psutil.net_io_counters(pernic=True) + except Exception: + counters = {} + rates = [] + for nic, c in counters.items(): + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + prev = net_prev.get(nic) + if prev: + dt = max(now - prev["t"], 0.5) + up = (c.bytes_sent - prev["sent"]) / dt + down = (c.bytes_recv - prev["recv"]) / dt + else: + up = down = 0 + net_prev[nic] = {"sent": c.bytes_sent, "recv": c.bytes_recv, "t": now} + rates.append({"nic": nic, "up": max(up, 0), "down": max(down, 0), + "sent": c.bytes_sent, "recv": c.bytes_recv}) + rates.sort(key=lambda x: x["up"] + x["down"], reverse=True) + try: + conns = len(psutil.net_connections()) + except Exception: + conns = 0 + total_recv = 0 + total_sent = 0 + for nic, c in counters.items(): + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + total_recv += getattr(c, "bytes_recv", 0) or 0 + total_sent += getattr(c, "bytes_sent", 0) or 0 + items = sorted(cache["procs"], key=lambda x: x["conns"], reverse=True)[:60] + tcp_list = [] + listen_list = [] + try: + name_by_pid = {p["pid"]: p["name"] for p in cache.get("procs", [])} + for c in psutil.net_connections(kind="tcp"): + laddr = f"{c.laddr.ip}:{c.laddr.port}" if c.laddr else "" + raddr = f"{c.raddr.ip}:{c.raddr.port}" if c.raddr else "" + family = "ipv6" if ":" in (c.laddr.ip if c.laddr else "") and "." not in (c.laddr.ip if c.laddr else "") else "ipv4" + # better family detection + import socket + family = "ipv6" if (c.family == socket.AF_INET6) else "ipv4" + if c.status == "LISTEN": + listen_list.append({"name": name_by_pid.get(c.pid, ""), "laddr": c.laddr.ip if c.laddr else "", "port": c.laddr.port if c.laddr else 0, "family": family, "pid": c.pid, "proto": "tcp"}) + else: + tcp_list.append({"name": name_by_pid.get(c.pid, ""), "laddr": laddr, "raddr": raddr, "family": family, "pid": c.pid, "status": c.status}) + except Exception: + pass + tcp_list = tcp_list[:80] + listen_list = listen_list[:80] + return jsonify({"nics": rates, "conns": conns, "items": items, + "hw": get_net_hw(), + "total_recv": total_recv, "total_sent": total_sent, + "tcp": tcp_list, "listen": listen_list}) + +_net_hw_static = {} + +def _net_sysfs(name): + """实时 sysfs: 状态/MTU/速率/MAC,键名与 psutil 速率互补""" + out = {} + for base in ("/host/sys/class/net", "/sys/class/net"): + d = os.path.join(base, name) + try: + if not os.path.isdir(d): + continue + for k in ("address", "operstate", "mtu", "speed", "duplex", + "carrier"): + v = _read(os.path.join(d, k)) + if v: + out[k] = v + # 驱动名 + try: + drv = os.path.basename(os.readlink(os.path.join(d, "device/driver"))) + if drv: + out["live_driver"] = drv + except Exception: + pass + if out: + break + except Exception: + continue + return out + +def _net_addrs(): + try: + ad = psutil.net_if_addrs() or {} + except Exception: + return {} + out = {} + for nic, lst in ad.items(): + v4, v6, mac = [], [], "" + for a in lst: + try: + fam = a.family.name if hasattr(a.family, "name") else str(a.family) + except Exception: + fam = "" + addr = a.address or "" + if "PACKET" in fam: + if addr: + mac = mac or addr + elif "AF_INET6" in fam: + if addr and not addr.startswith("fe80"): + v6.append(addr.split("%")[0]) + elif addr: + v6.append(addr.split("%")[0] + "(链路)") + elif "AF_INET" in fam: + if addr and len(addr.split(".")) == 4: + v4.append(addr) + out[nic] = {"v4": v4, "v6": v6, "mac": mac} + return out + +def get_net_hw(): + global _net_hw_static + if not _net_hw_static: + hj = {} + for jp in ("hw_net.json", + os.path.join(os.path.dirname(os.path.abspath(__file__)), + "hw_net.json")): + try: + with open(jp) as f: + hj = json.load(f) + if hj.get("nics"): + break + except Exception: + continue + _net_hw_static = hj or {"nics": []} + statics = {x.get("name"): dict(x) for x in _net_hw_static.get("nics", [])} + try: + st = psutil.net_if_stats() or {} + except Exception: + st = {} + addrs = _net_addrs() + # 展示的网卡: 静态档案 + 实际有流量的非虚拟口 + names = list(statics.keys()) + try: + counters = psutil.net_io_counters(pernic=True) or {} + except Exception: + counters = {} + for nic in counters: + if nic.startswith(("lo", "docker", "br-", "veth")): + continue + if nic not in statics: + names.append(nic) + out = [] + for nic in names: + base = dict(statics.get(nic, {"name": nic})) + s = st.get(nic) + if s is not None: + try: + base["isup"] = bool(s.isup) + base["live_speed"] = s.speed or 0 + base["live_mtu"] = s.mtu or 0 + except Exception: + pass + base.update(_net_sysfs(nic)) + a = addrs.get(nic, {}) + if a: + base["ipv4"] = a.get("v4", []) + base["ipv6"] = a.get("v6", []) + if a.get("mac") and not base.get("mac"): + base["mac"] = a["mac"] + c = counters.get(nic) + if c is not None: + try: + base["io_sent"] = c.bytes_sent + base["io_recv"] = c.bytes_recv + except Exception: + pass + out.append(base) + # 默认网关/DNS 实时读 + gw, dns = _net_hw_static.get("gateway", ""), _net_hw_static.get("dns", "") + base_if = "" + try: + with open("/host/proc/net/route" if os.path.exists("/host/proc/net/route") else "/proc/net/route") as f: + for line in f.read().splitlines()[1:]: + f_ = line.split() + if len(f_) > 2 and f_[1] == "00000000" and f_[7] == "00000000": + import socket, struct + gw = socket.inet_ntoa(struct.pack("