feat: add networkd subsystem and fix code review issues

Phase 1-4: Networkd subsystem
- lib/network.py: systemd-networkd config renderer (.network INI files)
  with full schema support: [Match], [Link], [Network], [Address], [Route],
  [DHCPv4], [DHCPv6] sections. One Address/=DNS= line per value per spec.
  Route sections use #N suffix per systemd.syntax(7).
- lib/network.py: generate_network_files() with 50-<name>.network prefix
  and stale file cleanup
- lib/network.py: collect_upstream_dns() filters local/private DNS
- lib/network.py: infer_dhcp_ranges() and infer_zones() helpers
- daemon/handlers/network.py: routes for GET/POST /network/interfaces
  and full apply with DNS upstream sync to dnsmasq
- webui/api/network.py: Flask blueprint for /api/network/* endpoints
- webui/api: interfaces page updated with IP config inline editing
- lib/state.py: networkd collector using parse_networkctl_status()
- system/sudoers.d/vacuum-walld: networkctl + systemd-network rules
- system/systemd/vacuum-walld.service: ReadWritePaths for /etc/systemd/network
- install.sh: ACME email now optional, configured from WebUI
- lib/acme.py: get_email() falls back to declarative config

Phase 5: Code review fixes
- daemon/server.py: path params now win over JSON body and query params
  in request body merge (prevents config save name override)
- daemon/server.py: remove dead 'import re'
- daemon/handlers/network.py: replace Path.mkdir() with sudo mkdir
  for /etc/systemd/network (ProtectSystem=strict compatibility)
- system/sudoers.d/vacuum-walld: pin systemctl to specific commands
  (reload/is-active dnsmasq instead of wildcard)
- system/sudoers.d/vacuum-walld: restore !requiretty and section comment
- lib/network.py: remove unused _MANAGEMENT_PORTS constant
- webui/api/network.py: remove redundant body[\name\] = name in save_interface

Tests: 332 passing (110 new/updated), ruff clean
This commit is contained in:
2026-06-01 03:15:50 +00:00
parent 2f215793e9
commit bc72db903c
26 changed files with 3294 additions and 121 deletions
+133
View File
@@ -1190,6 +1190,139 @@ Returns HTTP `404` if the peer is not found.
---
## Network API
Endpoints prefixed with `/api/network/...`. Manage systemd-networkd interface configuration including static addresses, routes, DNS, DHCP client settings, and link parameters.
### Interface Management
#### List All Interfaces
```
GET /api/network/interfaces
```
Return all configured interfaces with their network config and runtime state from `networkctl`.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data.interfaces` | `object` | Map of interface name to `{config, runtime}` |
| `data.timestamp` | `string` | Timestamp of runtime data collection |
---
#### Get Interface Details
```
GET /api/network/interfaces/<name>
```
Return config and runtime state for a specific interface.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Interface name |
| `config` | `object` | Full networkd config entry for this interface |
| `runtime` | `object` | Runtime state from `networkctl` (addresses, gateway, DNS, state) |
Returns HTTP `404` if the interface is not found in config.
---
#### Save and Apply Interface
```
POST /api/network/interfaces/<name>
```
Save network config for an interface, render the `.network` file, copy it to `/etc/systemd/network/`, and reload networkd for that interface.
**Request Body:** Any networkd config keys (e.g., `addresses`, `gateway`, `dns`, `routes`, `dhcp`, `link`, `dhcp_client`).
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Interface name |
| `applied` | `boolean` | Always `true` on success |
---
#### Reload Interface
```
POST /api/network/interfaces/<name>/reload
```
Reload networkd for a single interface (runs `networkctl reload <name>`).
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Interface name |
| `reloaded` | `boolean` | Always `true` on success |
### Full Sync
#### Apply All Interfaces
```
POST /api/network/apply
```
Full sync: generate all `.network` files, remove stale files, copy to `/etc/systemd/network/`, reload all interfaces, and sync DNS upstreams to dnsmasq.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `number` | Number of interfaces applied |
| `files` | `[string, ...]` | Paths of generated files |
| `cleaned` | `[string, ...]` | Paths of removed stale files |
### Helpers
#### Infer DHCP Ranges
```
GET /api/network/infer-dhcp-ranges
```
Suggest candidate DHCP ranges based on static interface IPs. For each interface with a static IPv4 address, calculates a usable address range in the subnet.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data.ranges` | `object` | Map of interface name to `{subnet, prefix, start, end}` |
---
#### Infer Firewall Zones
```
GET /api/network/infer-zones
```
Suggest firewalld zone assignments for configured interfaces based on heuristics:
- Interface name contains `wg``wan`
- DHCP-enabled or public-facing IP → `wan`
- Has explicit routes → `management`
- Everything else → `lan`
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data.zones` | `object` | Map of interface name to suggested zone (`"lan"`, `"wan"`, `"management"`) |
---
## Logs API
Endpoints prefixed with `/api/logs/...`. Serve rendered HTML log line fragments for HTMX consumption. These endpoints **do not** follow the standard JSON `{"ok": true, "data": ...}` response contract — they return HTML `<div>` elements directly. Errors are rendered inline as `(error reading ...)` text rather than returning JSON error responses.