Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb69bb109 | ||
|
|
1b06a922a9 | ||
|
|
d457c7e639 | ||
|
|
a29e10e313 | ||
|
|
15d98d2702 | ||
|
|
4b7960c18c | ||
|
|
bc50d58723 | ||
|
|
f65015a423 | ||
|
|
db0c5a8d96 | ||
|
|
7f78f48ec6 | ||
|
|
73ede3d227 | ||
|
|
9c42c72037 | ||
|
|
5d0e705b30 | ||
|
|
6986f36237 | ||
|
|
4316d503d2 | ||
|
|
1b2684c35b | ||
|
|
ec91122621 | ||
|
|
fc720823db | ||
|
|
8d47e085cb | ||
|
|
54ae8ce463 | ||
|
|
cd2e7864a4 | ||
|
|
fec9f6e0e5 | ||
|
|
55fce76a2d | ||
|
|
5fcd6dc5c4 | ||
|
|
3a35e8481a | ||
|
|
a5be653044 | ||
|
|
1d5a262a84 | ||
|
|
ad6fc66875 | ||
|
|
861e9a3193 |
@@ -29,7 +29,7 @@ Build and boot the dedicated installer image — see
|
||||
`images/edge-host-iso/README.md`. Short version:
|
||||
|
||||
1. `sudo pacman -S archiso && sudo images/edge-host-iso/build.sh`
|
||||
2. Write ISO to a USB stick, boot the target box from it, run `edge-install`
|
||||
2. Write ISO to a USB stick, boot the target box from it, run `edge-install <hostname>`
|
||||
(partitions the internal disk — NVMe if present, else the eMMC; the
|
||||
installer autodetects — installs the ansible-ready base system with SSH
|
||||
keys, DHCP, docker — asks for confirmation before wiping).
|
||||
@@ -164,7 +164,9 @@ role keeps logs off local flash and monitors wear:
|
||||
- **Docker logs to journald** (`docker-daemon.json`, `log-driver: journald`)
|
||||
instead of unbounded json files under `/var/lib/docker/containers`. Both
|
||||
compose services inherit it; `docker logs` still works.
|
||||
- **Grafana Alloy ships the journal to Loki** (`edge-alloy.service`). Its
|
||||
- **Grafana Alloy ships the journal to Loki** (`edge-alloy.service`, which
|
||||
runs the upstream `grafana/alloy` image with `/run/log/journal` mounted
|
||||
read-only; Arch's `grafana-alloy` package has no `loki.source.journal`). Its
|
||||
WAL/positions sit on `/run/alloy` (tmpfs), so the agent writes nothing to
|
||||
the eMMC either. Net effect: no log writes hit local flash — the trade-off
|
||||
is that logs are RAM-only and lost on reboot unless already shipped.
|
||||
|
||||
@@ -22,6 +22,7 @@ proxy-2.renner.uno ansible_host=proxy-2.nb.renner.uno ansible_user=root
|
||||
# sub-interface. Nothing here assumes any peripheral is attached.
|
||||
[edge_host]
|
||||
edge-1.iot.renner.uno ansible_user=root
|
||||
edge-2.iot.renner.uno ansible_user=root
|
||||
|
||||
# Synology NAS (DSM): nas-1 RS1221+ on site, nas-2 DS1522+ off site.
|
||||
[synology_nas]
|
||||
@@ -48,20 +49,11 @@ edge-1.iot.renner.uno
|
||||
zwave_gateway
|
||||
heating_can
|
||||
|
||||
# NTP servers. edge-1 was added here so the role would survive the PVE hosts
|
||||
# being retired for bare-metal Talos, which cannot run chrony or keepalived
|
||||
# itself. That retirement has now happened, so edge-1 is the ONLY member.
|
||||
#
|
||||
# THIS IS NO LONGER HIGHLY AVAILABLE. The group was jupiter (MASTER, prio 100),
|
||||
# saturn (90), neptune (80) and edge-1 (BACKUP, 70); the first three are gone.
|
||||
# A single VRRP member takes the address regardless of its configured state, so
|
||||
# the service address is still served, but by one thin client with
|
||||
# no failover behind it. install-ntp-ha.yml still carries the four-host
|
||||
# ntp_servers priority map, which now describes hosts that do not exist.
|
||||
#
|
||||
# Restoring redundancy needs a second chrony host that is not a Talos node.
|
||||
# See plays/install-ntp-ha.yml for why keepalived is scoped more narrowly than
|
||||
# chrony within this group.
|
||||
# NTP servers: every edge host (edge-1 BACKUP prio 70, edge-2 BACKUP prio 60;
|
||||
# see the ntp_servers map in plays/install-ntp-ha.yml). Talos nodes cannot run
|
||||
# chrony or keepalived. The jupiter/saturn/neptune entries in that map
|
||||
# describe retired PVE hosts. See plays/install-ntp-ha.yml for why keepalived
|
||||
# is scoped more narrowly than chrony within this group.
|
||||
[ntp_cluster:children]
|
||||
edge_host
|
||||
|
||||
@@ -73,6 +65,7 @@ proxy-1.renner.uno
|
||||
proxy-2.renner.uno
|
||||
netbird.renner.uno
|
||||
edge-1.iot.renner.uno
|
||||
edge-2.iot.renner.uno
|
||||
nas-1.renner.uno
|
||||
nas-2.renner.uno
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
roles:
|
||||
- certificates
|
||||
- common
|
||||
- pacman-cache
|
||||
- edge-flash-wear
|
||||
- docker-install
|
||||
- edge-log-shipper
|
||||
|
||||
@@ -83,6 +83,9 @@
|
||||
edge-1.iot.renner.uno:
|
||||
priority: 70
|
||||
state: "BACKUP"
|
||||
edge-2.iot.renner.uno:
|
||||
priority: 60
|
||||
state: "BACKUP"
|
||||
|
||||
roles:
|
||||
- role: chrony-ntp-server
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
|
||||
edge_alloy_config_file: /etc/alloy/edge.alloy
|
||||
edge_alloy_http_port: 12345
|
||||
edge_alloy_bin: /usr/bin/grafana-alloy # Arch `grafana-alloy` pkg names the binary this, not `alloy`
|
||||
# Upstream image: Arch's grafana-alloy 1.21 is built without
|
||||
# loki.source.journal. Renovate-tracked (customManagers in renovate.json).
|
||||
edge_alloy_image: grafana/alloy:v1.21.3@sha256:8ab871206bd2d8f89277f53abffee5f452a293980788d52f85fa183a87a505a4
|
||||
edge_loki_push_url: "https://loki.renner.uno/loki/api/v1/push"
|
||||
edge_loki_username: edge-1
|
||||
edge_alloy_labels:
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
# directly, so one journal source covers everything. Alloy s storage/WAL sits
|
||||
# on /run (tmpfs) so it does not write to flash either.
|
||||
|
||||
- name: Install grafana-alloy
|
||||
# Alloy runs from the upstream image (see edge-alloy.service); the Arch package
|
||||
# lacks journal support.
|
||||
- name: Remove the grafana-alloy package
|
||||
ansible.builtin.package:
|
||||
name: grafana-alloy
|
||||
state: present
|
||||
state: absent
|
||||
|
||||
- name: Create alloy system group
|
||||
ansible.builtin.group:
|
||||
name: alloy
|
||||
system: true
|
||||
register: edge_alloy_group
|
||||
|
||||
- name: Create alloy system user
|
||||
ansible.builtin.user:
|
||||
@@ -24,6 +27,25 @@
|
||||
system: true
|
||||
create_home: false
|
||||
shell: /usr/bin/nologin
|
||||
register: edge_alloy_user
|
||||
|
||||
- name: Look up the systemd-journal group
|
||||
ansible.builtin.getent:
|
||||
database: group
|
||||
key: systemd-journal
|
||||
|
||||
- name: Set the container identity
|
||||
ansible.builtin.set_fact:
|
||||
edge_alloy_uid: "{{ edge_alloy_user.uid }}"
|
||||
edge_alloy_gid: "{{ edge_alloy_group.gid }}"
|
||||
edge_alloy_journal_gid: "{{ ansible_facts.getent_group['systemd-journal'][1] }}"
|
||||
|
||||
- name: Pull the alloy image
|
||||
ansible.builtin.command:
|
||||
argv: [docker, pull, "{{ edge_alloy_image }}"]
|
||||
register: edge_alloy_pull
|
||||
changed_when: "'Downloaded newer image' in edge_alloy_pull.stdout"
|
||||
notify: Restart alloy
|
||||
|
||||
- name: Create alloy config directory
|
||||
ansible.builtin.file:
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
[Unit]
|
||||
Description=Grafana Alloy (edge-1 log shipper -> Loki)
|
||||
Description=Grafana Alloy (journal -> Loki)
|
||||
Documentation=https://grafana.com/docs/alloy/
|
||||
After=network-online.target
|
||||
After=docker.service network-online.target
|
||||
Requires=docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=alloy
|
||||
Group=alloy
|
||||
# Read the (volatile) systemd journal.
|
||||
SupplementaryGroups=systemd-journal
|
||||
# WAL + positions on tmpfs so the agent never writes to the eMMC. %t is the
|
||||
# runtime dir root (/run); RuntimeDirectory creates /run/alloy owned by us.
|
||||
RuntimeDirectory=alloy
|
||||
ExecStart={{ edge_alloy_bin }} run \
|
||||
# WAL + positions on tmpfs so the agent never writes to the eMMC.
|
||||
ExecStartPre=/usr/bin/install -d -o alloy -g alloy -m 0750 /run/alloy
|
||||
ExecStartPre=-/usr/bin/docker rm -f edge-alloy
|
||||
# Attached run with --log-driver none: the container's output reaches the
|
||||
# journal once, through this unit.
|
||||
ExecStart=/usr/bin/docker run --rm --name edge-alloy \
|
||||
--log-driver none \
|
||||
--network host \
|
||||
--user {{ edge_alloy_uid }}:{{ edge_alloy_gid }} \
|
||||
--group-add {{ edge_alloy_journal_gid }} \
|
||||
--read-only --tmpfs /tmp \
|
||||
--cap-drop ALL --security-opt no-new-privileges \
|
||||
-v /run/log/journal:/run/log/journal:ro \
|
||||
-v /etc/machine-id:/etc/machine-id:ro \
|
||||
-v {{ edge_alloy_config_file }}:/etc/alloy/config.alloy:ro \
|
||||
-v /run/alloy:/var/lib/alloy \
|
||||
{{ edge_alloy_image }} run \
|
||||
--server.http.listen-addr=127.0.0.1:{{ edge_alloy_http_port }} \
|
||||
--storage.path=%t/alloy \
|
||||
{{ edge_alloy_config_file }}
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
--storage.path=/var/lib/alloy \
|
||||
/etc/alloy/config.alloy
|
||||
ExecStop=/usr/bin/docker stop edge-alloy
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
LimitNOFILE=65536
|
||||
|
||||
# Hardening (bare metal, no LXC namespace limits here).
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -16,7 +16,7 @@ edge_mqtt_bridge_cafile: /etc/ssl/certs/ca-certificates.crt
|
||||
# kubernetes/apps/home-assistant/mqtt-accounts.yaml). The controller pushes
|
||||
# its password to edge-mqtt-cred and rotates it when the bridge reports
|
||||
# "not connected"; there is no copy in ansible-vault.
|
||||
edge_mqtt_bridge_account: edge-1
|
||||
edge_mqtt_bridge_account: "{{ inventory_hostname_short }}"
|
||||
# HTTPS port of edge-mqtt-cred (esp32 device API, mTLS). Only a client
|
||||
# certificate from the MQTT CA with this CN is accepted.
|
||||
edge_mqtt_cred_port: 8443
|
||||
|
||||
@@ -7,7 +7,7 @@ connection {{ edge_mqtt_bridge_account }}
|
||||
address {{ edge_mqtt_cluster_host }}:{{ edge_mqtt_cluster_tls_port }}
|
||||
bridge_cafile {{ edge_mqtt_bridge_cafile }}
|
||||
bridge_insecure false
|
||||
remote_clientid edge1-bridge
|
||||
remote_clientid {{ edge_mqtt_bridge_account | replace('-', '') }}-bridge
|
||||
cleansession false
|
||||
try_private true
|
||||
restart_timeout 5 30
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Managed by ansible (roles/pacman-cache).
|
||||
# Downloaded packages are not kept; older versions come from the Arch Linux
|
||||
# Archive.
|
||||
[Trigger]
|
||||
Operation = Install
|
||||
Operation = Upgrade
|
||||
Operation = Remove
|
||||
Type = Package
|
||||
Target = *
|
||||
|
||||
[Action]
|
||||
Description = Removing downloaded packages from the cache...
|
||||
When = PostTransaction
|
||||
Exec = /usr/bin/find /var/cache/pacman/pkg -mindepth 1 -delete
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
# Keep the pacman package cache empty. pacman keeps every downloaded package;
|
||||
# on a 7 GB eMMC root the cache grows to gigabytes. Arch hosts only.
|
||||
|
||||
- name: Ensure the pacman hook directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/pacman.d/hooks
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Empty the package cache after every transaction
|
||||
ansible.builtin.copy:
|
||||
src: pacman-cache.hook
|
||||
dest: /etc/pacman.d/hooks/pacman-cache.hook
|
||||
mode: "0644"
|
||||
|
||||
- name: Empty the package cache
|
||||
ansible.builtin.command:
|
||||
argv: [find, /var/cache/pacman/pkg, -mindepth, "1", -delete, -print]
|
||||
register: pacman_cache_clean
|
||||
changed_when: pacman_cache_clean.stdout | length > 0
|
||||
@@ -143,6 +143,12 @@
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Install sudo
|
||||
ansible.builtin.package:
|
||||
name: sudo
|
||||
state: present
|
||||
when: not ssh_ca_synouser.stat.exists
|
||||
|
||||
- name: Install the Claude sudo rules
|
||||
ansible.builtin.template:
|
||||
src: sudoers-claude.j2
|
||||
|
||||
@@ -42,10 +42,11 @@ for the CA key; rebuild the ISO after a CA rotation.
|
||||
2. Boot the target from it (UEFI, Secure Boot off; one-time F12/setup
|
||||
visit if the box doesn't already boot USB first)
|
||||
3. Either on the console (auto-login as root) or headless —
|
||||
`ssh root@<dhcp-ip>` with an owner certificate — run `edge-install`. It auto-detects the internal disk
|
||||
(NVMe, else eMMC — covers the Wyse 3040 — else first non-USB disk),
|
||||
shows the target and requires typing the hostname before wiping
|
||||
anything. Override with `edge-install /dev/sdY`.
|
||||
`ssh root@<dhcp-ip>` with an owner certificate — run
|
||||
`edge-install <hostname>` (e.g. `edge-install edge-2`). It auto-detects
|
||||
the internal disk (NVMe, else eMMC — covers the Wyse 3040 — else first
|
||||
non-USB disk), shows the target and requires typing the hostname before
|
||||
wiping anything. Override with `edge-install <hostname> /dev/sdY`.
|
||||
4. Reboot. Confirm the box is reachable by name — `ssh root@<host>` — before
|
||||
going further; it is identified by name everywhere and its address is
|
||||
never recorded. Then add it to `[edge_host]` in
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installer for edge-1 (bare-metal edge host, replaces the k8s-edge VM).
|
||||
# Installer for the bare-metal edge hosts (edge-1, edge-2, ...).
|
||||
# Runs from the edge-host ISO live environment. WIPES the target disk.
|
||||
# Usage: edge-install [disk]
|
||||
# Default: the internal disk — first NVMe, else eMMC (Wyse 3040), else
|
||||
# Usage: edge-install <hostname> [disk]
|
||||
# Default disk: the internal disk — first NVMe, else eMMC (Wyse 3040), else
|
||||
# the first non-USB, non-optical disk. The live USB stick itself is
|
||||
# excluded via the ISO9660 label mount.
|
||||
set -euo pipefail
|
||||
|
||||
HOST=edge-1
|
||||
usage() { echo "usage: edge-install <hostname> [disk]"; exit 1; }
|
||||
[ $# -ge 1 ] && [ $# -le 2 ] || usage
|
||||
HOST=$1
|
||||
# RFC 1123 label: what systemd-networkd sends as the DHCP hostname.
|
||||
[[ "$HOST" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] \
|
||||
|| { echo "invalid hostname: $HOST"; exit 1; }
|
||||
shift
|
||||
|
||||
detect_disk() {
|
||||
# eMMC needs mmc_block before any /dev/mmcblk* exists (see
|
||||
@@ -51,7 +57,7 @@ mount --mkdir "$(part 1)" /mnt/boot
|
||||
|
||||
# Everything the edge-host ansible role expects to find; the role itself
|
||||
# is applied over SSH after first boot (see ansible/docs/edge-host.md).
|
||||
pacstrap -K /mnt base linux linux-firmware intel-ucode openssh python git \
|
||||
pacstrap -K /mnt base linux linux-firmware intel-ucode openssh sudo python git \
|
||||
docker docker-compose
|
||||
|
||||
genfstab -U /mnt >> /mnt/etc/fstab
|
||||
|
||||
@@ -34,7 +34,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: wlan-setup
|
||||
image: gitea.renner.uno/gitea_admin/wlan-setup:2026-11-05-1352 # {"$imagepolicy": "flux-system:wlan-setup"}
|
||||
image: gitea.renner.uno/gitea_admin/wlan-setup:2026-11-05-1517 # {"$imagepolicy": "flux-system:wlan-setup"}
|
||||
env:
|
||||
# Client secret of the "WLAN setup" OAuth2 provider.
|
||||
- name: WLAN_SETUP_CLIENT_SECRET
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gitea-mcp
|
||||
image: docker.gitea.com/gitea-mcp-server:1.12.0 # {"$imagepolicy": "flux-system:gitea-mcp"}
|
||||
image: docker.gitea.com/gitea-mcp-server:1.13.0 # {"$imagepolicy": "flux-system:gitea-mcp"}
|
||||
# Image has no ENTRYPOINT (binary is in CMD), so args alone would replace it.
|
||||
command:
|
||||
- /app/gitea-mcp
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
# No sml2mqtt/#: the SML meter reader is retired for good (owner,
|
||||
# 2026-09-19; grid data comes from the E380 on the CAN bus).
|
||||
acl: ["readwrite open3e/#"]
|
||||
# edge-2's local mosquitto bridge, same role and pinning as edge-1.
|
||||
- name: edge-2
|
||||
auth: password
|
||||
deliver: {esp32: "https://edge-2.iot.renner.uno:8443"}
|
||||
acl: ["readwrite open3e/#"]
|
||||
# Shellies (REN-1017): one password account per device, pushed by the
|
||||
# controller through shelly-manager's per-device API. Name shelly-<id> =
|
||||
# shelly-manager device id. ACL as the former shared user `shellies`
|
||||
|
||||
@@ -30,7 +30,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: controller
|
||||
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-10-30-1314 # {"$imagepolicy": "flux-system:kuma-sync"}
|
||||
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-11-05-1355 # {"$imagepolicy": "flux-system:kuma-sync"}
|
||||
env:
|
||||
- name: KUMA_URL
|
||||
value: https://kuma.renner.uno
|
||||
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gotenberg
|
||||
image: gotenberg/gotenberg:8.39.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
image: gotenberg/gotenberg:8.40.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
args:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
|
||||
@@ -155,7 +155,7 @@ spec:
|
||||
# exist on both registries — hence konairius/, which matches the ghcr
|
||||
# namespace. Steady state still pulls from Gitea and never leaves the
|
||||
# network.
|
||||
image: gitea.renner.uno/konairius/alpine-openssl:2026-10-29-1348 # {"$imagepolicy": "flux-system:alpine-openssl"}
|
||||
image: gitea.renner.uno/konairius/alpine-openssl:2026-11-05-1353 # {"$imagepolicy": "flux-system:alpine-openssl"}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
||||
@@ -76,6 +76,10 @@ spec:
|
||||
value: "netbird-ingress-$(NODE_NAME)"
|
||||
- name: NB_LOG_LEVEL
|
||||
value: "info"
|
||||
# stdout only; the image default also writes client.log into the
|
||||
# 16Mi state tmpfs.
|
||||
- name: NB_LOG_FILE
|
||||
value: "console"
|
||||
# rootless image needs a writable HOME for its state.
|
||||
- name: USER
|
||||
value: "netbird"
|
||||
|
||||
@@ -516,6 +516,7 @@ spec:
|
||||
- matchPattern: "*"
|
||||
- toFQDNs:
|
||||
- matchName: "edge-1.iot.renner.uno"
|
||||
- matchName: "edge-2.iot.renner.uno"
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "8443"
|
||||
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: backup-verify
|
||||
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-10-29 # {"$imagepolicy": "flux-system:backup-verify"}
|
||||
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-11-05 # {"$imagepolicy": "flux-system:backup-verify"}
|
||||
command: ["bash", "/script/backup-verify.sh"]
|
||||
env:
|
||||
- name: HOME
|
||||
|
||||
@@ -200,10 +200,10 @@ func addText(parent *etree.Element, tag, ns, text string) *etree.Element {
|
||||
}
|
||||
|
||||
// windowsProfile is the unsigned CarrierProvisioning document: one Hotspot 2.0 WLAN profile
|
||||
// matched by domain name, EAP-TTLS with inner PAP, and the user's credentials. The
|
||||
// CarrierControl WLAN schema allows authentication up to WPA2 (Enterprise); Windows adds the
|
||||
// PMF that the WPA3-Enterprise network requires. Elements follow Microsoft's signed Passpoint
|
||||
// provisioning samples.
|
||||
// (domain and NAI realm renner.uno), WPA3-Enterprise, EAP-TTLS with inner PAP, and the user's
|
||||
// credentials. Valid against the schemas Windows ships in %SYSTEMROOT%\schemas\provisioning
|
||||
// (TestWindowsProfileSchema); SubscriberId is base:SubscriberType, i.e. \w+ and at most 20
|
||||
// characters.
|
||||
func windowsProfile(user, password string) *etree.Document {
|
||||
doc := etree.NewDocument()
|
||||
doc.WriteSettings = etree.WriteSettings{CanonicalEndTags: true, CanonicalText: true, CanonicalAttrVal: true}
|
||||
@@ -213,16 +213,18 @@ func windowsProfile(user, password string) *etree.Document {
|
||||
|
||||
global := add(root, "Global", "")
|
||||
addText(global, "CarrierId", "", carrierID)
|
||||
addText(global, "SubscriberId", "", "renner-unifi")
|
||||
addText(global, "SubscriberId", "", "rennerunifi")
|
||||
|
||||
wlan := add(add(root, "WLANProfiles", ""), "WLANProfile", nsWLAN)
|
||||
addText(wlan, "name", "", "Renner UniFi")
|
||||
addText(add(add(wlan, "SSIDConfig", ""), "SSID", ""), "name", "", "Renner UniFi")
|
||||
addText(add(wlan, "Hotspot2", ""), "DomainName", "", "renner.uno")
|
||||
hotspot2 := add(wlan, "Hotspot2", "")
|
||||
addText(hotspot2, "DomainName", "", "renner.uno")
|
||||
addText(add(hotspot2, "NAIRealm", ""), "name", "", "renner.uno")
|
||||
|
||||
security := add(add(wlan, "MSM", ""), "security", "")
|
||||
auth := add(security, "authEncryption", "")
|
||||
addText(auth, "authentication", "", "WPA2")
|
||||
addText(auth, "authentication", "", "WPA3ENT")
|
||||
addText(auth, "encryption", "", "AES")
|
||||
addText(auth, "useOneX", "", "true")
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@@ -644,6 +645,61 @@ func TestCallbackWindows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsProfileSchema validates the signed document with xmllint against the schemas
|
||||
// Windows ships in %SYSTEMROOT%\schemas\provisioning. They are Microsoft's files and not in the
|
||||
// repo: point WLAN_SETUP_PROVISIONING_XSD at a copy of that folder to run it.
|
||||
func TestWindowsProfileSchema(t *testing.T) {
|
||||
dir := os.Getenv("WLAN_SETUP_PROVISIONING_XSD")
|
||||
if dir == "" {
|
||||
t.Skip("WLAN_SETUP_PROVISIONING_XSD not set")
|
||||
}
|
||||
xmllint, err := exec.LookPath("xmllint")
|
||||
if err != nil {
|
||||
t.Skip("xmllint not installed")
|
||||
}
|
||||
tc := writeSigningFiles(t)
|
||||
signer, err := loadSigner(tc.keyFile, tc.certFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := signer.Sign(windowsProfile("Alice.Smith", testKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The schemas import each other by namespace only; a wrapper schema gives libxml2 the
|
||||
// locations. Dependencies first, so the later imports find their namespaces loaded.
|
||||
wrapper := etree.NewDocument()
|
||||
schema := wrapper.CreateElement("xs:schema")
|
||||
schema.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
schema.CreateAttr("targetNamespace", "urn:wlan-setup:schema-test")
|
||||
for _, s := range []struct{ ns, file string }{
|
||||
{"http://www.microsoft.com/networking/CarrierControl/Base/v1", "provisioning_base_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/Plans/v1", "provisioning_plans_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/WWAN/v1", "provisioning_wwan_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/WLAN/v1", "provisioning_wlan_v1.xsd"},
|
||||
{dsig.Namespace, "provisioning_sig_v1.xsd"},
|
||||
{nsCarrierControl, "provisioning_v1.xsd"},
|
||||
} {
|
||||
imp := schema.CreateElement("xs:import")
|
||||
imp.CreateAttr("namespace", s.ns)
|
||||
imp.CreateAttr("schemaLocation", "file://"+filepath.Join(dir, s.file))
|
||||
}
|
||||
tmp := t.TempDir()
|
||||
wrapperFile := filepath.Join(tmp, "wrapper.xsd")
|
||||
if err := wrapper.WriteToFile(wrapperFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
docFile := filepath.Join(tmp, "profile.xml")
|
||||
if err := os.WriteFile(docFile, body, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := exec.Command(xmllint, "--noout", "--schema", wrapperFile, docFile).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("xmllint: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsProfileDocument(t *testing.T) {
|
||||
tc := writeSigningFiles(t)
|
||||
signer, err := loadSigner(tc.keyFile, tc.certFile)
|
||||
@@ -676,11 +732,12 @@ func TestWindowsProfileDocument(t *testing.T) {
|
||||
)
|
||||
for path, want := range map[string]string{
|
||||
"Global/CarrierId": carrierID,
|
||||
"Global/SubscriberId": "renner-unifi",
|
||||
"Global/SubscriberId": "rennerunifi",
|
||||
wlan + "name": "Renner UniFi",
|
||||
wlan + "SSIDConfig/SSID/name": "Renner UniFi",
|
||||
wlan + "Hotspot2/DomainName": "renner.uno",
|
||||
sec + "authEncryption/authentication": "WPA2",
|
||||
wlan + "Hotspot2/NAIRealm/name": "renner.uno",
|
||||
sec + "authEncryption/authentication": "WPA3ENT",
|
||||
sec + "authEncryption/encryption": "AES",
|
||||
sec + "authEncryption/useOneX": "true",
|
||||
sec + "OneX/authMode": "user",
|
||||
|
||||
Reference in New Issue
Block a user