Compare commits

..
Author SHA1 Message Date
k.renner 796d8459b6 Merge pull request 'fix(zwave-gateway): zwave-js-ui 11.25.2 (Server-Schema 51), Renovate für edge_*_image' (#1562) from fix/zwave-js-ui-11.25.2-renovate into main
Reviewed-on: #1562
Reviewed-by: Review Bot <review-bot@renner.uno>
2026-11-04 20:54:36 +00:00
security-bot a6be8247eb chore(image-automation): bump ghcr.io/usetrmnl/terminus:0.80.0
Updates managed by image-automation-controller.
2026-11-04 20:51:49 +00:00
hermes-bot 826042f3ea fix(zwave-gateway): zwave-js-ui 11.25.2, Renovate for edge images
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
Home Assistant 2026.11.0 requires Z-Wave JS Server schema 51. zwave-js-ui
11.21.1 ships @zwave-js/server 3.10 (schema 50), so the zwave_js entry is
stuck in setup_retry. 11.24.2 is the first release on @zwave-js/server
^3.11.0 (schema 51); bump to the current 11.25.2, pinned by digest.

11.25.2 is built on Alpine 3.23 (was 3.22). The role now reads the
Alpine release from the image and names the musl libeatmydata.so after
it, instead of a hand-synced edge_zwave_alpine_base: Renovate bumps the
image from now on, and the old `creates:` never refreshed the .so.
Builds for other Alpine releases are removed.

Renovate never read ansible/roles/*/defaults/main.yml: enabledManagers
had no custom.regex and there were no customManagers. Add one for
edge_<name>_image (docker datasource and versioning), and give ansible/**
updates their own PR, since the play runs from a workstation after the
merge (no Ansible pipeline since 0c1fb0a2).
2026-11-04 20:46:03 +00:00
security-bot aa2543369c chore(image-automation): bump ghcr.io/immich-app/immich-server:v3.3.6
Updates managed by image-automation-controller.
2026-11-04 18:24:23 +00:00
security-bot 5add98190f chore(image-automation): bump ghcr.io/immich-app/immich-machine-learning:v3.3.6
Updates managed by image-automation-controller.
2026-11-04 18:18:59 +00:00
security-bot 1643b305b2 chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.11.0
Updates managed by image-automation-controller.
2026-11-04 17:55:55 +00:00
k.renner 657e3d0d92 Merge pull request 'feat(radius): proxy UniFi Endpoint logins to the UDM over VLAN 11' (#1561) from feat/radius-proxy-vlan11 into main
Reviewed-on: #1561
2026-11-04 14:10:09 +00:00
k.renner ac2d5d13a1 Merge pull request 'feat(network): VLAN 11 link radius0 on the nodes, Cilium VLAN bypass' (#1560) from feat/vlan11-radius0-base into main
Reviewed-on: #1560
2026-11-04 14:07:19 +00:00
claude-botandClaude Opus 5.5 ed3be3156b feat(network): VLAN 11 link radius0 on the nodes, Cilium VLAN bypass
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
VLAN 11 "(11) RADIUS" carries the FreeRADIUS proxy traffic to the UDM
RADIUS, which accepts clients from its own networks only.

- Talos k8s-{1,2,3}: VLANConfig radius0 (VLAN 11 on uplink), link only,
  no addresses, IPv6 disabled (VLAN 11 sends RAs).
- Cilium bpf.vlanBypass [30, 11]: radius0 is address-less like ens19, so
  its tagged frames meet the VLAN filter on uplink (#592).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:05:23 +01:00
9 changed files with 81 additions and 25 deletions
+11 -9
View File
@@ -134,11 +134,13 @@ Afterwards:
## Day-2 notes
- Image bumps: edit `edge_zwavejs_image` / `edge_sml2mqtt_image` in
defaults and re-run the play (no flux image-automation out here —
deliberate for sml2mqtt, acceptable for zwave-js). If a zwave-js bump
changes its Alpine major, also update `edge_zwave_alpine_base` and refresh
the preloaded `.so` (see "Logging & eMMC" → zwave-js).
- Image bumps: Renovate opens a PR for `edge_*_image` in
`roles/*/defaults/main.yml` (tag + digest, `customManagers` in
`renovate.json`). No pipeline deploys to edge-1, so after the merge run
`ansible-playbook plays/install-zwave-gateway.yml` from `ansible/` on a
mesh-connected workstation. The play reads the image's Alpine release
itself and rebuilds the preloaded `.so` when it changes (see
"Logging & eMMC" → zwave-js).
- open3e / E3onCAN updates: both are pinned to commits in
`roles/heating-can/defaults/main.yml`, so re-running the play does NOT pick
up upstream changes -- that is deliberate, these drive the heat pump. The
@@ -190,7 +192,7 @@ journal commit).
- **Fix:** the `zwave-js` container preloads **`libeatmydata`**
(`LD_PRELOAD=/usr/lib/libeatmydata.so`, mounted read-only from
`zwave-js/libeatmydata.so`). This makes `fsync`/`fdatasync` no-ops *for that
`zwave-js/libeatmydata-alpine<X.Y>.so`). This makes `fsync`/`fdatasync` no-ops *for that
one process*, so the kernel batches the writes via normal writeback (~30 s)
instead of forcing each one. The WS/MQTT value stream to Home Assistant and
the on-disk cache are **unchanged** — full data resolution, no re-interview
@@ -199,9 +201,9 @@ journal commit).
batch it); the only mount-level alternative is putting the cache on tmpfs,
which makes the *whole* store volatile → re-interview on every restart.
- **The `.so` is musl** (the image is Alpine-based), extracted from
`edge_zwave_alpine_base` — keep that var in sync with the image's Alpine
version; to refresh after a major bump, `rm zwave-js/libeatmydata.so` and
re-run.
`alpine:<X.Y>`, where `X.Y` is read from the image's `/etc/alpine-release`
on every run. A new Alpine release gives a new file name, so the play
extracts it, recreates the container and deletes the old file.
- **Durability guard:** because `fsync` is neutralized, a hard power-loss
during the (rare) settings write could truncate `settings.json`, which holds
the **Z-Wave network keys**. `edge-zwave-settings-backup.timer` keeps a
@@ -13,12 +13,8 @@ zwave_base_dir: "{{ edge_base_dir }}/zwave-js"
# survives moving the stick between machines.
edge_zwave_device: /dev/serial/by-id/usb-Nabu_Casa_ZWA-2_80B54EE15720-if00
edge_zwavejs_image: ghcr.io/zwave-js/zwave-js-ui:11.21.1
# Alpine version the zwave-js-ui image is built on -- the source for the musl
# libeatmydata.so preloaded into that container. Keep in sync if the image
# bumps its Alpine major (check: docker exec zwave-js cat /etc/os-release).
edge_zwave_alpine_base: alpine:3.22
# Renovate-tracked (customManagers in renovate.json).
edge_zwavejs_image: ghcr.io/zwave-js/zwave-js-ui:11.25.2@sha256:ac4874a3cbea61c0c60002a8fd8ae05104cc8eb2eae91aa05bd184972aa06853
# Secret from inventory/group_vars/edge_host/vault.yml:
# edge_zwavejs_session_secret
+32 -3
View File
@@ -15,17 +15,31 @@
- "{{ zwave_base_dir }}"
- "{{ zwave_base_dir }}/store"
# The preloaded .so is musl and must come from the image's own Alpine release.
- name: Read the Alpine release of the zwave-js-ui image
ansible.builtin.command:
cmd: docker run --rm --entrypoint cat {{ edge_zwavejs_image }} /etc/alpine-release
register: zwave_alpine_release
changed_when: false
check_mode: false
- name: Name libeatmydata.so after that Alpine release
ansible.builtin.set_fact:
zwave_alpine_minor: "{{ zwave_alpine_release.stdout.split('.')[:2] | join('.') }}"
zwave_libeatmydata: "libeatmydata-alpine{{ zwave_alpine_release.stdout.split('.')[:2] | join('.') }}.so"
- name: Extract musl libeatmydata.so (matches the zwave-js-ui Alpine base)
ansible.builtin.shell:
cmd: |
set -euo pipefail
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
docker run --rm --network=host {{ edge_zwave_alpine_base }} \
docker run --rm --network=host alpine:{{ zwave_alpine_minor }} \
sh -c 'apk add --no-cache libeatmydata >/dev/null 2>&1 && cat /usr/lib/libeatmydata.so' > "$tmp"
test "$(stat -c%s "$tmp")" -ge 8000 # sanity: real .so is ~14 KB, guards against a failed/empty pull
install -m 0644 "$tmp" {{ zwave_base_dir }}/libeatmydata.so
creates: "{{ zwave_base_dir }}/libeatmydata.so"
install -m 0644 "$tmp" {{ zwave_base_dir }}/{{ zwave_libeatmydata }}
creates: "{{ zwave_base_dir }}/{{ zwave_libeatmydata }}"
executable: /bin/bash
notify: Restart zwave-js
# Because fsync is neutralized, a hard power-loss during the (rare) settings
@@ -93,3 +107,18 @@
zwave-js could not start -- the ZWA-2 radio is not present:
{{ zwave_compose_up.stderr_lines | select('search', 'device') | list }}
when: zwave_compose_up.rc != 0
- name: Find libeatmydata.so builds of other Alpine releases
ansible.builtin.find:
paths: "{{ zwave_base_dir }}"
patterns: "libeatmydata*.so"
excludes: "{{ zwave_libeatmydata }}"
register: zwave_stale_libeatmydata
- name: Remove libeatmydata.so builds of other Alpine releases
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ zwave_stale_libeatmydata.files }}"
loop_control:
label: "{{ item.path }}"
@@ -31,7 +31,7 @@ services:
- "{{ edge_zwave_device }}:/dev/zwave"
volumes:
# musl libeatmydata.so extracted from the image's Alpine base (ro)
- ./libeatmydata.so:/usr/lib/libeatmydata.so:ro
- ./{{ zwave_libeatmydata }}:/usr/lib/libeatmydata.so:ro
# Persistent store: settings.json, device DB, network keys. Path is
# unchanged from the previous shared project, so nothing is migrated.
- ./store:/usr/src/app/store
@@ -128,7 +128,7 @@ spec:
memory: 32Mi
containers:
- name: home-assistant
image: ghcr.io/home-assistant/home-assistant:2026.10.4 # {"$imagepolicy": "flux-system:home-assistant"}
image: ghcr.io/home-assistant/home-assistant:2026.11.0 # {"$imagepolicy": "flux-system:home-assistant"}
env:
- name: TZ
value: Europe/Berlin
+1 -1
View File
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: immich-machine-learning
image: ghcr.io/immich-app/immich-machine-learning:v3.3.5 # {"$imagepolicy": "flux-system:immich-machine-learning"}
image: ghcr.io/immich-app/immich-machine-learning:v3.3.6 # {"$imagepolicy": "flux-system:immich-machine-learning"}
ports:
- name: http
containerPort: 3003
+1 -1
View File
@@ -87,7 +87,7 @@ spec:
mountPath: /ca
containers:
- name: immich-server
image: ghcr.io/immich-app/immich-server:v3.3.5 # {"$imagepolicy": "flux-system:immich-server"}
image: ghcr.io/immich-app/immich-server:v3.3.6 # {"$imagepolicy": "flux-system:immich-server"}
ports:
- name: http
containerPort: 3001
+2 -2
View File
@@ -134,7 +134,7 @@ spec:
# is a device-bricking class of risk. Bumps are a reviewed, manual
# act — see kubernetes/infrastructure/image-automation/README.md.
- name: terminus
image: ghcr.io/usetrmnl/terminus:0.79.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.80.0 # {"$imagepolicy": "flux-system:terminus"}
ports:
- name: http
containerPort: 2345
@@ -197,7 +197,7 @@ spec:
capabilities:
drop: ["ALL"]
- name: sidekiq
image: ghcr.io/usetrmnl/terminus:0.79.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.80.0 # {"$imagepolicy": "flux-system:terminus"}
command: ["bundle", "exec", "sidekiq", "-r", "./config/sidekiq.rb"]
envFrom:
- configMapRef:
+30 -1
View File
@@ -7,7 +7,8 @@
"flux",
"kubernetes",
"woodpecker",
"dockerfile"
"dockerfile",
"custom.regex"
],
"ignorePaths": [
"kubernetes/clusters/homelab/flux-system/**",
@@ -23,6 +24,20 @@
"/^kubernetes/.+\\.ya?ml$/"
]
},
"customManagers": [
{
"customType": "regex",
"description": "Container images of the Ansible roles: `edge_<name>_image: <image>:<tag>@<digest>` in ansible/roles/*/defaults/main.yml. No other manager reads these files.",
"managerFilePatterns": [
"/^ansible/roles/[^/]+/defaults/main\\.ya?ml$/"
],
"matchStrings": [
"(?:^|\\n)edge_[a-z0-9_]+_image:[ \\t]*[\"']?(?<depName>[^\\s:@\"']+):(?<currentValue>[^\\s@\"']+)(?:@(?<currentDigest>sha256:[a-f0-9]{64}))?"
],
"datasourceTemplate": "docker",
"versioningTemplate": "docker"
}
],
"packageRules": [
{
"description": "Flux image automation (kubernetes/infrastructure/image-automation) already bumps these images. Renovate must not open a second, competing update. Keep this list equal to the ImageRepository images there.",
@@ -134,6 +149,20 @@
"groupName": "CI non-major",
"groupSlug": "ci-minor-patch"
},
{
"description": "Edge-host images (ansible/): no pipeline deploys them, the play runs from a workstation after the merge. One PR of its own, so the merge and the play run go together.",
"matchFileNames": [
"ansible/**"
],
"matchUpdateTypes": [
"patch",
"minor",
"digest",
"pin"
],
"groupName": "edge hosts (ansible)",
"groupSlug": "edge-hosts-ansible"
},
{
"description": "Stateful apps and the CNI: one PR each, never in a group (REN-315). A git revert does not undo a DB migration or a CNI change, so each needs its own review and, where it can disrupt the cluster, a window.",
"matchUpdateTypes": [