// as2.no — server wiki

Documentation and setup notes for the homelab server.


arild@as2.no:~/wiki$ grep -r '^#' .

arild@as2.no:~/wiki$ cd infrastructure/
arild@as2.no:~/wiki$ ssh root@192.168.10.20 pveversion

# Proxmox

Type-1 hypervisor (Proxmox VE) running the home lab. The as2.no host (Server4-Ubuntu2) is itself VM 104 on this hypervisor. Administered from as2.no over SSH as root (key auth). Last updated 2026-06-15.

VM memory reads high in Proxmox — that's cache, not a leak. Proxmox's per-VM gauge counts everything the guest has touched (incl. Linux page cache), so it shows ~85–90% for a VM using far less. Real usage is on the Linux Server page (from inside the guest, cache excluded) — e.g. VM 104 reads ~87% in Proxmox but ~41% real. qemu-guest-agent is installed (fixes graceful shutdown, fs-freeze backups, guest-IP reporting) but on PVE 9.x it does not change this gauge. Trust the guest/Linux-Server number for real usage.

SettingValue
NodeServer4-Proxmox
LocationBod — cabled to C2960CX-3 (.2); reaches LAN over the Bod AP's wireless backhaul
CPUIntel Core i7-9700 (8C/8T @ 3.0 GHz, turbo 4.7)
RAM64 GiB
VersionProxmox VE 9.1.4 (kernel 6.17.4-2-pve)
Installed2024-05-09 (originally PVE 8.2)
System diskSamsung 960 EVO 1TB NVMe
LAN IP192.168.10.20
Web UIhttps://proxmox.as2.no (valid cert; also https://192.168.10.20:8006)
Networkvmbr0 bridge → enp3s0, gateway 192.168.10.1
Admin accessSSH key (root) from as2.no
arild@as2.no:~/wiki$ ssh root@192.168.10.20 qm list
VMIDNameRAMDiskStateNotes
101Server4-Win10 GB100 GBrunningWindows; 6 passthrough drives (see disks below)
102Server4-Win11-test4 GB64 GBrunningWindows 11 test VM
104Server4-Ubuntu24 GB40 GBrunningas2.no host (this wiki)
105CML8 GB50 GBstoppedCisco Modeling Labs
106eve-ng16 GB40 GBstoppedEVE-NG network emulator
arild@as2.no:~/wiki$ ssh root@192.168.10.20 pvesm status
StorageTypeSizeUsedPurpose
localdir96 GB25%ISOs, templates, host backups
local-lvmlvmthin832 GB20%VM disk images (on the 960 EVO NVMe)
Server4-Win-backupcifs7.8 TB29%SMB share for VM backups
arild@as2.no:~/wiki$ ssh root@192.168.10.20 smartctl --scan

SMART status of the physical drives passed through to VM 101 (Server4-Win). Last checked 2026-06-21.

DriveDeviceWinSMARTPower-onStatus
WD Blue SN570 2TB NVMenvme0n1J:PASSED18,698 hOK — 0% wear, 100% spare
Samsung 860 EVO 1TB SSDsdaF:PASSED56,513 hOK — low wear, 0 reallocated
Seagate ST8000AS0002 (Z8401TPQ)sdcH:PASSED20,181 hOK — 0 pending, 0 CRC
Seagate ST8000AS0002 (Z8401V6Z)sdbI:PASSED*20,161 hFAILING — self-test read failure at LBA 273408
Seagate ST8000DM004 (WCT03T91)sdeE:PASSED74,101 hAGING — clean but ~8.5 yr runtime
Seagate ST8000DM004 (WCT04458)sddD:PASSED74,101 hAGING — clean but ~8.5 yr runtime
  • sdb (Z8401V6Z) is failing — a SMART short self-test (2026-06-05) ended in a read failure at LBA 273408, a confirmed bad sector unrepaired since ~hour 5,427 (~1.6 yr). Also 16 pending / 16 offline-uncorrectable sectors and 21 UDMA CRC errors. Re-checked 2026-06-21 (now ~hour 20,161): identical — counts unchanged and the fault has not spread. *SMART overall still reports PASSED because pending sectors don't trip the threshold — treat it as failing anyway. It holds only the backup copy of H: (healthy), so no primary data is at risk; replace it when convenient
  • The two ST8000DM004 drives have ~8.5 years of runtime — plan replacements
  • None of these passthrough disks are included in Proxmox backups (backup=0)

## Backups & offsite (Jottacloud)

VM images are dumped with vzdump (snapshot mode, zstd) to the Server4-Win-backup CIFS share — //192.168.10.3/Proxmox-backup on the Windows fileserver — then Jottacloud on that box uploads them offsite nightly at 02:00. Run manually for now (no scheduled job). Consistency: 101/104 get a guest-agent fs-freeze (101 = Windows/VSS-consistent, 104 = fs-consistent); 105/106 are consistent while stopped; 102 now has the guest agent too (added 2026-07-07), so its backups are fs-consistent.

Run the Proxmox backup BEFORE 02:00. It must finish the .vma.dat.vma.zst rename before Jottacloud starts — otherwise Jottacloud opens the freshly-written .dat to upload it and Proxmox's rename fails with a sharing violation (unable to rename ... .vma.dat to ... .vma.zst). This silently broke every backup from ~mid-2025 until 2026-07-07; the fix is scheduling Jottacloud (not real-time) and keeping the two from overlapping.

# Re-check one drive's health + attributes
ssh root@192.168.10.20 smartctl -H -A /dev/sdb

# Manual VM backup -> Windows share (run BEFORE 02:00 / Jottacloud)
ssh root@192.168.10.20 vzdump 104 --storage Server4-Win-backup --mode snapshot --compress zstd

# Manage VMs (list / start / stop / config)
ssh root@192.168.10.20 qm list
ssh root@192.168.10.20 qm start 106

arild@as2.no:~/wiki$ ssh root@192.168.10.20 qm config 101

# Server4-Win (VM 101)

The Windows 11 file server — VM 101 on the Proxmox host. Six physical drives are passed straight through from the host (raw NVMe/SATA passthrough), so Windows owns and formats them directly. The QEMU guest agent is enabled, so the disks can be queried remotely from as2.no. Files are shared over FTPS via FileZilla Server (explicit TLS required, min TLS 1.2).

SettingValue
OSWindows 11
vCPU4 cores (1 socket)
RAM10 GiB
FirmwareUEFI (OVMF), q35
System disk100 GB on local-lvm → C:
File sharingFileZilla Server — explicit FTPS on port 21212 (PASV 50000–50100)
Guest agentenabled
arild@as2.no:~/wiki$ qm guest exec 101 -- powershell Get-Disk

Physical disk → Windows drive-letter map. Windows reports generic QEMU HARDDISK serials, so drives are matched by SATA port order (cross-checked against size and the QEMU serial sequence).

Win diskDriveSizeLetterHost devNotes
0WD Blue SN570 2TB NVMe2 TBJ:nvme0n1
1Samsung 860 EVO 1TB1 TBF:sda
2Seagate ST8000AS0002 (Z8401TPQ)8 TBH:sdchealthy — primary
3Seagate ST8000AS0002 (Z8401V6Z)8 TBI:sdbFAILING — backup of H:
4Seagate ST8000DM004 (WCT03T91)8 TBE:sdeaging — primary
5Seagate ST8000DM004 (WCT04458)8 TBD:sddaging — backup of E:
6OS disk (local-lvm)100 GBC:Windows system
7Seagate Expansion (USB)8 TBG:external, not passthrough
  • Backup pairs: I: backs up H:, and D: backs up E:
  • I: (failing, Z8401V6Z) only holds the backup copy of H:, which is healthy — so no primary data is at immediate risk. OK for now; replace I: when convenient, but until then the H: → I: backup is unreliable
  • G: is a separate external USB drive (Seagate Expansion)

arild@as2.no:~/wiki$ ssh root@192.168.10.20 qm config 102

# Server4-Win11-test (VM 102)

Scratch Windows 11 VM for testing — VM 102 on the Proxmox host.

SettingValue
OSWindows 11
vCPU2 cores (1 socket)
RAM4 GiB
FirmwareUEFI (OVMF), q35
Disks64 GB system + 32 GB scratch (local-lvm)
Staterunning

arild@as2.no:~/wiki$ cat server4-ubuntu2.md

# Server4-Ubuntu2 (VM 104)

The Ubuntu host serving as2.noVM 104 on the Proxmox box (see Proxmox).

ComponentDetails
HostVM 104 on Server4-Proxmox
OSUbuntu 24.04.4 LTS
CPU4 vCPUs @ ~2GHz
RAM3.8 GiB
Disk40 GiB
Local IP192.168.10.15
Public IP85.165.99.110

arild@as2.no:~/wiki$ ssh root@192.168.10.20 qm config 105

# CML (VM 105)

Cisco Modeling Labs (CML 2.10.0) — Cisco's official network simulation platform. Runs virtual topologies of real Cisco node types (IOS, IOS-XE, IOS-XR, NX-OS) for building and testing network designs — certification study (CCNA / CCNP / CCIE) and pre-deployment validation. VM 105, normally powered off; start it when needed.

Upgraded 2.8 → 2.10.0 on 2026-07-08. Gotcha for next time: CML 2.10+ pulls docker-ce from download.docker.com (via the new cml-docker-shim package), but the repo line in /etc/apt/sources.list.d/docker.list ships commented out. Result: the Cockpit upgrade reports SUCCESS (code 0) but silently stays on the old version, because docker-ce is "not installable" so the whole cml2 upgrade is held. Fix: uncomment the deb ...download.docker.com... noble stable line, sudo apt-get update (confirm apt-cache policy docker-ce shows a candidate), then re-run the Cockpit upgrade and reboot. Two attempts failed silently before we found this.

SettingValue
OSLinux (CML 2.8.0)
vCPU4 cores (1 socket)
RAM8 GiB
FirmwareUEFI (OVMF)
Disk50 GB (local-lvm)
Statestopped

arild@as2.no:~/wiki$ ssh root@192.168.10.20 qm config 106

# eve-ng (VM 106)

EVE-NG Community Edition (6.2.0) — a multi-vendor network emulation platform. Runs virtual devices from many vendors (Cisco, Juniper, Arista, Fortinet and more) in a single topology, complementing CML for multi-vendor labs and certification practice. VM 106, normally powered off; start it when needed.

SettingValue
OSLinux (EVE-NG CE 6.2.0)
vCPU4 cores (1 socket)
RAM16 GiB
Disk40 GB (local-lvm)
Statestopped
arild@as2.no:~/wiki$ ssh root@192.168.10.20 pct exec 111 -- systemctl status jellyfin

# Jellyfin (CT 111)

Self-hosted media server for movies & TV, in a privileged LXC on Proxmox (host). Hardware transcoding runs on the host's Intel iGPU (QuickSync); the library is mounted read-only over CIFS from Server4-Win. Served at https://jellyfin.as2.no (valid cert; nginx → container :8096) — reachable on the LAN, over WireGuard, and publicly on the internet (family access, no VPN) as of 2026-07-31.

SettingValue
OSDebian 12 (LXC, privileged)
vCPU4 cores
RAM4 GiB
Disk16 GB (local-lvm) — OS + metadata; media on the share
Web UIjellyfin.as2.no / 192.168.10.11:8096
TranscodingIntel QuickSync (UHD 630) via VAAPI + HDR tone-map
MediaMovies + Series, read-only CIFS from 192.168.10.3
  • QuickSync passthrough: /dev/dri bound into the CT (device allows in /etc/pve/lxc/111.conf); jellyfin user in render+video; intel-opencl-icd for HDR tone-mapping
  • Media: host CIFS mounts (ro, root-only creds) bind-mounted into the CT at /media/Movies and /media/Series
  • Reverse proxy: nginx on 192.168.10.15192.168.10.11:8096 (websockets); shared as2.no Let's Encrypt cert (see HTTPS); Jellyfin KnownProxies = .15
  • Public exposure: ASUS router forwards TCP 443 → .15; hardened at nginx with a login rate-limit (60r/m on /Users/AuthenticateByName) + a fail2ban jail (5 fails / 10 min → 1 h ban, LAN/WG whitelisted) + HSTS/security headers
  • Up/down + version on the Services page via the summary poller

arild@as2.no:~/wiki$ cd ../networking/
arild@as2.no:~/wiki$ cat networking.md

# Networking

  • Firewall: UFW — ports 80/443 restricted to 192.168.10.0/24 (local only)
  • DNS: Domeneshop — as2.no and arildsorli.no A records point to 85.165.99.110
  • HTTPS / TLS: Let's Encrypt (DNS-01) — see the HTTPS section
  • LAN DNS resolver: dnsmasq on 192.168.10.15 — see dnsmasq

arild@as2.no:~/wiki$ ssh cx-1 show version

# Switches — Cisco Catalyst 2960CX

Uniform four-switch access layer, one per room (model WS-C2960CX-8PC-L, 8-port PoE). SSH key auth on all four; IOS version, health (CPU / mem / flash / temp / uptime) and up/down status are polled live over SNMP v2c and shown on the network topology page. All four currently up on IOS 15.2(7)E14 (fleet-upgraded from 15.2(7)E8).

HostnameIPLocationIOSRole
C2960CX-1192.168.10.4Stue15.2(7)E14feeds WLC root AP
C2960CX-2192.168.10.5Kjellerstue15.2(7)E14
C2960CX-3192.168.10.2Bod15.2(7)E14Proxmox uplink
C2960CX-4192.168.10.6Kontor15.2(7)E14via AP4800-4 wireless backhaul

arild@as2.no:~/wiki$ cat wireless.md

# Wireless — Cisco AP 4800 + WLC

Four Cisco Aironet AP4800s, one per room, joined to an embedded WLC (Mobility Express) running on AP4800-1. AP4800-1 (Stue) is the wireless root and backhauls to each room's AP; the controller is reachable at the VIP 192.168.10.7, AireOS 8.10.196.0 (version read live from the poller). Live WiFi client counts per radio are on the monitoring page.

APMgmt IPLocationMACNotes
AP4800-1192.168.10.120Stue78:72:5d:fb:8f:eaWLC ME controller — VIP .7
AP4800-2192.168.10.152Kjellerstue78:72:5d:3f:9a:c2
AP4800-3192.168.10.132Bod78:72:5d:fb:8f:f6
AP4800-4192.168.10.105Kontor78:72:5d:3f:89:2cwireless backhaul for C2960CX-4
  • Controller VIP 192.168.10.7; the WLC also forwards syslog under host label AP4800-1
  • SNMP RO community is locked to .15 — see device-side config

arild@as2.no:~/wiki$ cat router.md

# Router — ASUS (192.168.10.1)

The ASUS router is the LAN gateway and WAN edge. It serves DHCP, holds the public IP (dynamic, kept current by the Domeneshop updater), and forwards only WireGuard 51820/udp inbound. SSH-polled every 30s for per-interface throughput and WiFi-client counts.

SettingValue
RoleLAN gateway · default route
LAN IP192.168.10.1
Public IP85.165.99.110 (dynamic, DDNS)
DHCP pool192.168.10.100 – .250
InboundWireGuard 51820/udp only
MonitoringSSH poller, 30s (rx/tx per interface, WiFi clients per radio)

arild@as2.no:~/wiki$ cat dnsmasq.md

# dnsmasq (LAN DNS)

Local DNS resolver for the home network — forwards queries upstream and supports custom A records.

SettingValue
DNS port53/tcp+udp (LAN only)
Upstream208.67.222.222, 208.67.220.220 (OpenDNS)
Config~/dnsmasq/dnsmasq.conf
Bind address192.168.10.15
  • Point router's DNS server to 192.168.10.15 to use for all LAN clients
  • Add custom hostnames in dnsmasq.conf with address=/hostname.local/192.168.x.x
  • *.as2.no resolves to 192.168.10.15 on the LAN
  • UFW allows port 53 from 192.168.10.0/24 only
# Start / restart
cd ~/dnsmasq
docker compose up -d

# View logs
docker logs -f dnsmasq

# Test a query from the LAN
dig @192.168.10.15 google.com

arild@as2.no:~/wiki$ cat wireguard.md

# WireGuard VPN

Self-hosted VPN via wg-easy — a Docker image that bundles WireGuard with a web UI for managing clients.

SettingValue
Hostas2.no
VPN port51820/udp
Web UI port51821/tcp
DNS1.1.1.1 (Cloudflare)
Config~/wg-easy/docker-compose.yml
  • WireGuard config stored in Docker volume etc_wireguard
  • Web UI at https://wg.as2.no (valid cert; nginx -> container :51821) — manage clients, download configs
  • Requires kernel capabilities NET_ADMIN and SYS_MODULE
  • IP forwarding enabled via net.ipv4.ip_forward=1
# Start / restart
cd ~/wg-easy
docker compose up -d

# View logs
docker logs -f wg-easy

arild@as2.no:~/wiki$ cat certbot.md

# HTTPS — Let's Encrypt (DNS-01)

TLS certificates issued by Let's Encrypt using the DNS-01 challenge — no public port 80 required.

ItemDetails
Plugincertbot-dns-domeneshop
Domainas2.no (+ *.as2.no wildcard)
Cert path/etc/letsencrypt/live/as2.no/
RenewalAuto via systemd timer
  • DNS-01 works by creating a temporary _acme-challenge TXT record at Domeneshop
  • Plugin credentials stored in /etc/letsencrypt/renewal/as2.no.conf
  • Nginx uses fullchain.pem and privkey.pem from the cert path
# Check certificate expiry
sudo certbot certificates

# Force renewal (for testing)
sudo certbot renew --dry-run

# Check renewal timer
systemctl status snap.certbot.renew.timer

arild@as2.no:~/wiki$ cat domeneshop-ip-updater.md

# Domeneshop IP Updater

Monitors the server's public IP and automatically updates the as2.no A record at Domeneshop when it changes.

  • Domains: as2.no, arildsorli.no
  • Script: ~/domeneshop-ip-updater/updater.py
  • Credentials: /etc/domeneshop-ip-updater/config (chmod 600)
  • State file: /var/lib/domeneshop-ip-updater/last_ip
  • Runs every 5 minutes via domeneshop-ip-updater.timer
  • GitHub: github.com/arildsorli/domeneshop-ip-updater (private)
# Check logs
journalctl -u domeneshop-ip-updater.service -f

# Check timer status
systemctl status domeneshop-ip-updater.timer

arild@as2.no:~/wiki$ cd ../observability/
arild@as2.no:~/wiki$ cd ~/monitoring && docker compose ps

# Monitoring stack

Two pipelines, both Docker Compose at ~/monitoring on .15 (its own git repo). Metrics: Telegraf → InfluxDB v2 → Grafana. Logs: devices → AxoSyslog (514) → Loki → Grafana. Embedded dashboards are on the monitoring page; the v2 overview cards come from the host pollers.

ContainerImagePortRole
grafanagrafana/grafana:11.3.0:3000dashboards (nginx → grafana.as2.no)
influxdbinfluxdb:2.7127.0.0.1:8086metrics TSDB — bucket network, org as2
telegraftelegraf:1.32SNMP poller (switches + WLC) + host metrics
lokigrafana/loki:3.2.1127.0.0.1:3100log store — filesystem, 30-day
axosyslogaxoflow/axosyslog:4.8.1:514 udp+tcpsyslog receiver → Loki (see Syslog)

What Telegraf collects (every 30s): SNMP v2c from the four 2960CX switches and the AireOS WLC at .7 — interface counters/errors, CPU, memory, flash, temperature, uptime, plus each switch's IOS sysDescr and the WLC's swVersion (these drive the live version badges on the topology page) and per-AP client counts. It also reads this server's own CPU / memory / disk / NIC via a read-only /hostfs mount.

  • Grafana access: nginx vhost grafana.as2.no127.0.0.1:3000 (also :3000 direct on the LAN), on the as2.no Let's Encrypt cert. Anonymous Viewer + embedding are enabled so the dashboard iframes render; admin password is set in the UI (.env holds only the bootstrap value)
  • InfluxDB data model: measurements asus_netdev (WAN/wired/wireless rx/tx), snmp_interface (switch + WLC interface counters/errors), switch_cpu / switch_mem / switch_env / switch_flash, wlc_ap_clients, and host cpu/mem/disk/net. Throughput = non-negative derivative × 8 (bits/s)
  • Loki: single-binary, filesystem store, 30-day retention; labels host · program · severity · facility
  • Secrets (InfluxDB token, Grafana pw, SNMP communities) live in ~/monitoring/.env (chmod 600, never committed)

arild@as2.no:~/wiki$ cat axosyslog/syslog-ng.conf

# Syslog pipeline (AxoSyslog → Loki)

Switches, the WLC and the ASUS router forward syslog to 192.168.10.15:514. AxoSyslog normalises every format (including Cisco's non-standard frame), maps source IPs to friendly host names and ships to Loki — searchable in Grafana and on the monitoring page by host, severity, program and facility.

SettingValue
ReceiverAxoSyslog on .15:514 (udp/tcp)
StoreLoki — 30-day retention, 127.0.0.1:3100
Labelshost · severity · program · facility
Sourcesrouter (.1), C2960CX-1…4, WLC/AP (.7 → label AP4800-1)

arild@as2.no:~/wiki$ systemctl list-timers '*poll*' '*summary*' '*inventory*'

# Host pollers

Small scripts on .15 (in ~/monitoring/scripts, run by systemd timers) that feed data the SNMP path can't. Two write straight to InfluxDB; two emit the JSON the v2 dashboard reads — which is why the cards refresh roughly every minute.

TimerEveryScriptOutput
asus-wan-poll30sasus_wan_poll.shWAN / per-iface rx/tx → InfluxDB asus_netdev
asus-wifi-poll30sasus_wifi_poll.shWiFi clients per radio → InfluxDB
client-inventory5 minclient_inventory.pydata/clients.json — merged roster (router · DHCP · ARP · Cisco MAC)
as2-summary60sas2_summary.pydata/summary.json — overview cards
prune-code-extensionsdailyprune-code-extensions.shkeeps newest version of each code-server extension (they never self-prune — Claude Code had grown to ~7 GB)
update-ieee-datamonthlyupdate-ieee-data -frefreshes the IEEE OUI database so client_inventory resolves vendors from MAC prefixes
  • as2_summary.py assembles the overview: WAN throughput, switches + parsed IOS, WLC AireOS version, WiFi totals, Proxmox VM count + SMART disk health, server CPU/mem/disk, syslog rate
  • The ASUS pollers SSH in with a dedicated Dropbear key (~/.ssh/asus_poller_ed25519), independent of the router's single web-admin session
  • Version-controlled unit files live in ~/monitoring/systemd/; the live copies are in /etc/systemd/system/
# Poller status / logs
systemctl status as2-summary.timer
journalctl -u as2-summary.service -f

# Run a poller once by hand
~/monitoring/scripts/as2_summary.py

arild@as2.no:~/wiki$ cat device-config.md

# Device-side SNMP/syslog config

What was applied on each device so it reports to the stack on .15. SNMP is read-only and locked to .15; syslog is sent to .15:514. Community strings live in ~/monitoring/.env (shown as placeholders below).

Cisco 2960CX switches (cx-1…4) — in config mode, saved to startup-config:

access-list 99 permit 192.168.10.15
snmp-server community <SNMP_COMMUNITY> RO 99
logging host 192.168.10.15
logging trap informational
! end & write memory

WLC (.7, AireOS Mobility Express) — SNMP RO community locked to .15 + syslog at notifications level. Applied once via ~/monitoring/scripts/wlc_phase2_config.exp <community>:

config snmp community create <WLC_SNMP_COMMUNITY>
config snmp community accessmode ro <WLC_SNMP_COMMUNITY>
config snmp community ipaddr 192.168.10.15 255.255.255.255 <WLC_SNMP_COMMUNITY>
config snmp community mode enable <WLC_SNMP_COMMUNITY>
config snmp version v2c enable
config logging syslog host 192.168.10.15
config logging syslog level notifications
save config

ASUS router (.1) — no SNMP, so it only sends syslog. The helper ~/monitoring/scripts/asus_set_syslog.sh logs into the web UI and sets nvram log_ipaddr=192.168.10.15, applied with restart_logger (no reboot). Its metrics come from the SSH pollers instead of Telegraf.

  • The WLC needs a more complex community string than the switches — kept separate as WLC_SNMP_COMMUNITY
  • All three forward syslog to AxoSyslog on .15:514 (see Syslog pipeline); only the switches and WLC are polled over SNMP by Telegraf

arild@as2.no:~/wiki$ cat inventory.md

# Inventory DB & API

One asset table on .15 is the single source of truth behind the v2 clients, IP-plan, hardware and network (switches + APs, also WiFi) pages — each is a filtered view, so an IP or note reads the same everywhere and an edit updates every view. Curated fields are hand-edited in the browser; live fields belong to the poller. These CRT pages are read-only renderings of the same data.

Flow: client_inventory.py UPSERTs live state by MAC → inventory.db ← browser edits via the API; the inventory-api container serves the DB as JSON; render_crt.py regenerates the CRT tables between <!--GEN:*--> markers.

PieceWhereRole
DB~/monitoring/inventory/data/inventory.dbSQLite (WAL) — asset + ip_plan_doc; gitignored
APIinventory-api · 127.0.0.1:8090FastAPI container; nginx proxies /v2/api/
Seedinventory/migrate.pyone-time import, merged by MAC→IP→name
Live writerscripts/client_inventory.pyUPSERTs live fields every 5 min
CRT writerinventory/render_crt.pyregenerates CRT tables from the DB
  • Merge key is the MAC (stable; IPs move). ip_static = assigned IP (blank = DHCP); a view shows ip_static or else the live ip_current. Notes are per-pagenotes_ip_plan / notes_hardware / notes_network / notes_clients, each view its own note
  • type (host/desktop/laptop/spare/given_away/client/switch/ap) picks the view; switch/ap feed the network & WiFi tables, a hardware form-factor outranks host so HT-PC shows on hardware while ip_static keeps it on IP-plan
  • Live fields (online, ip_current, last_seen, switch_port, vendor) are poller-owned, never editable
  • Auth: GET open on the LAN; writes (POST/PATCH/DELETE on /v2/api/assets) need the shared password — nginx limit_except GET HEAD basic-auth, user arild, htpasswd /etc/nginx/.as2_inventory_htpasswd. API binds 127.0.0.1 only
  • Edit on v2 via the ✎ Edit button on IP-Plan / Hardware / Network (inline edit · add · delete); the Clients roster has an ✎ Edit notes toggle (notes only); password prompted once per session
  • All IP labels on v2 are DB-driven: the structured tables, the Topology diagram (network.js), the Proxmox VM table (live qm list + IP from the DB), and inline prose labels via <span data-host-ip="Device Name"> filled by app.js fillHostIPs(). Change a device's IP in the DB and every v2 label follows
# rebuild API / re-seed DB / regenerate CRT
cd ~/monitoring && docker compose up -d --build inventory-api
python3 ~/monitoring/inventory/migrate.py --write
python3 ~/monitoring/inventory/render_crt.py

# change the edit password
printf 'arild:%s\n' "$(openssl passwd -apr1)" | sudo tee /etc/nginx/.as2_inventory_htpasswd
sudo systemctl reload nginx

arild@as2.no:~/wiki$ cd ../services/
arild@as2.no:~/wiki$ cat services.md

# Services

ServiceURLDescription
Homepagehttps://as2.noNginx, local network only
VS Codehttps://code.as2.nocode-server browser IDE
Portainerhttps://portainer.as2.noDocker container manager
Grafanahttps://grafana.as2.noDashboards & logs
dnsmasq192.168.10.15:53LAN DNS resolver
FileZillaftps://192.168.10.3:21212FTPS file server (on Server4-Win, VM 101)

arild@as2.no:~/wiki$ cat nginx.md

# Nginx

Nginx (1.24.0) is the front door for everything on as2.no — it serves the static homepage and reverse-proxies the three other *.as2.no services. Every vhost redirects :80 → :443 and terminates TLS on the shared Let's Encrypt wildcard cert. UFW limits 80/443 to 192.168.10.0/24, so it's reachable on the LAN (or over WireGuard) only.

server_nameServesBackend
as2.no · www.as2.noHomepage (static)root /home/arild/as2-homepage
code.as2.nocode-server IDEproxy → 127.0.0.1:8080
portainer.as2.noPortainerproxy → 127.0.0.1:9000
grafana.as2.noGrafanaproxy → 127.0.0.1:3000
  • Vhosts live in /etc/nginx/sites-available/, enabled via symlinks in /etc/nginx/sites-enabled/ (one file per *.as2.no host)
  • Shared CSP / security headers come from /etc/nginx/snippets/security-headers.conf (main site, script-src 'none'), -v2.conf (v2 dashboard + games allow script-src 'self') and -games.conf
  • Theme switch: the as2.no vhost reads a theme cookie at /v2 redirects to /v2/; /theme/v2 and /theme/crt set the cookie. Works even for the no-JS CRT site
  • Live data proxy: /v2/grafana/api/ds/query is a same-origin reverse proxy to 127.0.0.1:3000/api/ds/query so the v2 dashboard fetches interface lists / uptime without CORS (read-only query endpoint only)
  • Caching: HTML and everything under /v2/ is no-cache (so ?v= cache-busters take effect); css/woff2/svg/ico cache for 7 days
# Test config, then reload (zero-downtime)
sudo nginx -t
sudo systemctl reload nginx

# Enable a new vhost
sudo ln -s /etc/nginx/sites-available/<name> /etc/nginx/sites-enabled/

# Tail access / error logs
sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

arild@as2.no:~/wiki$ cat stack.md

# Dev Stack

  • Web server: Nginx (serves the site + reverse-proxies code/portainer/grafana)
  • Containers: Docker + Docker Compose
  • Container UI: Portainer (Docker container on port 9000)
  • Observability: Grafana · InfluxDB · Loki · Telegraf · AxoSyslog — see Monitoring stack
  • IDE: code-server v4 (port 8080, proxied by Nginx)
  • Runtimes: Node.js v24 LTS, Python 3.12, pip
  • Version control: git + GitHub CLI (gh)

arild@as2.no:~/wiki$ cat docker.md

# Docker Workflows

Most services on .15 run as Docker containers, managed with Docker Compose across a few project directories.

StackContainersPurpose
~/wg-easywg-easyWireGuard VPN + web UI
~/dnsmasqdnsmasqLAN DNS resolver
~/monitoringgrafana · influxdb · loki · telegraf · axosyslogobservability stack (see Monitoring)
standaloneportainerDocker management UI
# List running containers
docker ps

# View resource usage
docker stats

# View logs for a container
docker logs -f <container-name>

# Restart a container
docker compose -f ~/wg-easy/docker-compose.yml restart

# Pull latest image and recreate container
docker compose pull
docker compose up -d

# Remove unused images / volumes
docker system prune

arild@as2.no:~/wiki$ cat as2-homepage.md

# as2-homepage

This website. Two maintained themes — the classic CRT terminal look (this page) and a modern v2 NOC dashboard — built with plain HTML, CSS and a little vanilla JS. No frameworks.

ItemDetails
URLhttps://as2.no
StackHTML, CSS, Nginx
Source~/as2-homepage/
Repogithub.com/arildsorli/as2-homepage (private)
  • Served directly from ~/as2-homepage/ by Nginx (v2 lives in v2/)
  • HTTPS via Let's Encrypt DNS-01 challenge
  • Theme switch is cookie-based; CRT keeps the scanlines, vignette, blinking cursor and ASCII header
cd ~/as2-homepage
# edit files...
git add .
git commit -m "describe change"
git push

arild@as2.no:~/wiki$ ls ../
index.html — homepage
network.html — network topology
monitoring.html — metrics & logs
ip-plan.html — IP plan
hardware.html — hardware specs
games.html — browser games
arild@as2.no:~/wiki$ cd ..