Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
796d8459b6 | ||
|
|
a6be8247eb | ||
|
|
826042f3ea | ||
|
|
aa2543369c | ||
|
|
5add98190f | ||
|
|
1643b305b2 | ||
|
|
657e3d0d92 | ||
|
|
ac2d5d13a1 | ||
|
|
ed3be3156b |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user