diff --git a/kubernetes/apps/home-assistant-zwavejs/deployment.yaml b/kubernetes/apps/home-assistant-zwavejs/deployment.yaml deleted file mode 100644 index 5e817f07..00000000 --- a/kubernetes/apps/home-assistant-zwavejs/deployment.yaml +++ /dev/null @@ -1,114 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: zwave-js - namespace: home-assistant - labels: - app.kubernetes.io/name: zwave-js -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app.kubernetes.io/name: zwave-js - template: - metadata: - labels: - app.kubernetes.io/name: zwave-js - spec: - nodeSelector: - topology.kubernetes.io/zone: edge - tolerations: - - key: workload-class - operator: Equal - value: pinned - effect: NoSchedule - initContainers: - # Reconcile settings.json on every start. Our managed keys - # (zwave.port, zwave.serverEnabled/serverPort, security keys) - # come from a SOPS-encrypted Secret and must always be in - # effect; any fields the user adjusts via the zwave-js-ui UI - # (gateway/mqtt/ui sections, custom zwave overrides, etc.) are - # preserved through the deep-merge. - - name: bootstrap-settings - image: alpine:3.21 - command: - - sh - - -c - - | - set -eu - apk add --no-cache jq >/dev/null - mkdir -p /usr/src/app/store - if [ -s /usr/src/app/store/settings.json ]; then - echo "merging managed keys into existing settings.json" - jq -s '.[0] * .[1]' \ - /usr/src/app/store/settings.json \ - /etc/zwjs-bootstrap/settings.json \ - > /tmp/settings.json - mv /tmp/settings.json /usr/src/app/store/settings.json - else - echo "writing initial settings.json" - cp /etc/zwjs-bootstrap/settings.json /usr/src/app/store/settings.json - fi - volumeMounts: - - name: store - mountPath: /usr/src/app/store - - name: zwjs-bootstrap - mountPath: /etc/zwjs-bootstrap - readOnly: true - containers: - - name: zwave-js - image: ghcr.io/zwave-js/zwave-js-ui:11.21.1 # {"$imagepolicy": "flux-system:zwave-js-ui"} - env: - - name: TZ - value: Europe/Berlin - - name: SESSION_SECRET - valueFrom: - secretKeyRef: - name: zwave-js-session - key: session-secret - ports: - - name: ui - containerPort: 8091 - protocol: TCP - - name: websocket - containerPort: 3000 - protocol: TCP - volumeMounts: - - name: store - mountPath: /usr/src/app/store - - name: zwave-stick - mountPath: /dev/zwave - livenessProbe: - tcpSocket: - port: ui - initialDelaySeconds: 30 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: ui - initialDelaySeconds: 10 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - memory: 512Mi - securityContext: - privileged: true - volumes: - - name: store - persistentVolumeClaim: - claimName: zwavejs-store - - name: zwave-stick - hostPath: - path: /dev/serial/by-id/usb-Nabu_Casa_ZWA-2_80B54EE15720-if00 - - name: zwjs-bootstrap - secret: - secretName: zwave-js-session - items: - - key: settings.json - path: settings.json diff --git a/kubernetes/apps/home-assistant-zwavejs/kustomization.yaml b/kubernetes/apps/home-assistant-zwavejs/kustomization.yaml deleted file mode 100644 index ce494218..00000000 --- a/kubernetes/apps/home-assistant-zwavejs/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - pvc.yaml - - deployment.yaml - - service.yaml - - secret.sops.yaml diff --git a/kubernetes/apps/home-assistant-zwavejs/pvc.yaml b/kubernetes/apps/home-assistant-zwavejs/pvc.yaml deleted file mode 100644 index 28302a7b..00000000 --- a/kubernetes/apps/home-assistant-zwavejs/pvc.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: zwavejs-store - namespace: home-assistant - annotations: - kustomize.toolkit.fluxcd.io/prune: disabled -spec: - accessModes: - - ReadWriteOnce - storageClassName: ceph-block - resources: - requests: - storage: 2Gi diff --git a/kubernetes/apps/home-assistant-zwavejs/secret.sops.yaml b/kubernetes/apps/home-assistant-zwavejs/secret.sops.yaml deleted file mode 100644 index 97d8f91f..00000000 --- a/kubernetes/apps/home-assistant-zwavejs/secret.sops.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: ENC[AES256_GCM,data:6Lo=,iv:BFnlADFCQUkJhYMbBDsadPpVzpriP4IlY/c/JPnTfaw=,tag:G99xkPnA0Zo0+mjcbUGNrA==,type:str] -kind: ENC[AES256_GCM,data:v1AZMTMy,iv:3Bccu9++hZGp+unhysM1lUlEICv48EzD0MRT+3SeUpo=,tag:PR2x/jNtwaHKSwxrbfp9Og==,type:str] -metadata: - name: ENC[AES256_GCM,data:EsIq6rFM1nflhQzWCFx+BQ==,iv:9NV9dujzYyKX/atXXhkLFB8NaGm3FD/WMBEneW1zAAI=,tag:K9MFRGOKNn54LMFBdMGPJQ==,type:str] - namespace: ENC[AES256_GCM,data:a25NXKyQtB7v3NLEG4g=,iv:R3yoraMsMYLfGbQLs8NZIrCobs9wsMa2d4/bysFr+og=,tag:Eya8wLLsbVRMyPBIxZsXNw==,type:str] -type: ENC[AES256_GCM,data:yz/7JBb2,iv:Tpk7EnyUfjoXgJCokYL/rdnrTqaeFXUKmX2a9WEN2GM=,tag:1GhYa74EnIqj95zBw9IZqg==,type:str] -stringData: - session-secret: ENC[AES256_GCM,data:iYBSO+IE98bYhTF4Qw3REB/IBnxosTzxuVYWXtsBfDe3noKiuoswwIqo2V4+W+MCAgbhmEQ1mQd5N7hBNFsX/g==,iv:QJCf8RwcQZw8P+Kt1ptYiZe+fKAdNmDKsLcaSX/56zk=,tag:2R4lavEPbiRok6p3/VVYcQ==,type:str] - settings.json: ENC[AES256_GCM,data:bBxaIkIZE8bZxXIJnDLnRqidV3wPlJGcdmEE/1AmGUHDG8Qf3KxcDUDzkVw+9tc5DQ7424xApspJD3lmkGmAs2wGCLhIhlFgvIa20iBmM2fRdx7Y/H8DJxCYl2ZeRSNvqxcGfyvvc69+/7mLw/imrl2l34z1a/AA2IHdEBx+zZY1f/qGThHb0yrzHoQhnwpJ1x41uQ+P+djCR1IAfYqAiubE1hPAfJjRe7hzRYMguICK+gSrfglBBtZZKEhzU9UiRPXCJBRDS3+srXQXmZFFaF/jhEBYDzqT0BORJD0kn48kQP05feGW8SrYpItDUj6oJyNKJOAc/Ns5A2/qrwDUh5z2RRAUyUfaE/fGxzVbw257zgVpUnsEhrNfUFwmy7XI4edeLcgISR1va2vAbraFICGbWMqa7XQAd+njNZWPsIneHV15hI9w1q/xQcludkqq249Hkbj1GjIMT5LOz/RksslMSJYvkrZDpnJuB5J6H89PSIPGf0qLvW/fPP/2xRNE1CycoQKZh5WMY2g4VsdyVw6oq6kLOniXHfi7zRO6T0g4RKvM0ISSvEYJevD7bfWrfxN3en9rAXqm2g2RmZpzZgwY/xN+1hSkPYlQ/NmsiDoDRSB034uAUDUtk1Fg70QcTWjHDeF4p9Ku+GHn0D5QBNZjFbPHWviKQlXprcSlmL3ynReK2XkowuklD44ATMAFQQ==,iv:xYJAFECJ16IIZ/TebTMtPOhx7s8PsdZsZHpgp65CbRQ=,tag:UzYSBsih6HT9t9PbZ2zOrw==,type:str] -sops: - age: - - recipient: age1yqjvqkwwwxdj7mr546zfs5ckzuwndp69ya2n2drd4vypwns8w4ys04qfy3 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUaWtCeFViQ2hKajJ0YzFk - Wi8zZ1A3alh2S2JsUXhlc1c0Zy81RXc2aEdnCmkzZ1d2bVNkTFdnQ3VUOGQ1Tmxp - Qnk5blJqWjFJcytZRWdoN2dlRlEyTzQKLS0tIEVQMmZmWEFNcGxnYUxRVThpSWVq - Z0tSTU8xRERQZmg1TE43M2JSN3hFU2MKAAwYACp8jJ9B10vB5rvHVNnoIVkIpDZI - 6ATb3FfVb0nsenJkVv7iN634oWFxvaorbNKR+bajvN3FTuUhyJ5wuw== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-05-09T10:08:53Z" - mac: ENC[AES256_GCM,data:I/TLsO2wsPVrIYaPuOKJjHM9eSg+dAup4qmEK4W6pkpokgZaVr6HQcUm72ynsk/NrnqeTX40FO1he+/mWGajs58pph74KLW/xBAZ7gcvkfiL4r07JOSRwYRUet4y7YOAWZOZE89OKfZUIADh62phf2UpdPy+vc9xiq7GqumeZc8=,iv:95/a5aKXzFrfehf/Rhjm0OQF51QVom0oB478CAIxMec=,tag:bgD4xyW7/Zv3zn5KCfs9jQ==,type:str] - unencrypted_suffix: _unencrypted - version: 3.10.2 diff --git a/kubernetes/apps/home-assistant-zwavejs/service.yaml b/kubernetes/apps/home-assistant-zwavejs/service.yaml deleted file mode 100644 index b31bc6b6..00000000 --- a/kubernetes/apps/home-assistant-zwavejs/service.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: zwave-js - namespace: home-assistant - labels: - app.kubernetes.io/name: zwave-js -spec: - selector: - app.kubernetes.io/name: zwave-js - ports: - - name: websocket - port: 3000 - targetPort: websocket - - name: ui - port: 8091 - targetPort: ui diff --git a/kubernetes/apps/home-assistant/kustomization.yaml b/kubernetes/apps/home-assistant/kustomization.yaml index b52b75c4..3e18821d 100644 --- a/kubernetes/apps/home-assistant/kustomization.yaml +++ b/kubernetes/apps/home-assistant/kustomization.yaml @@ -18,8 +18,6 @@ resources: - mosquitto-service.yaml - mosquitto-iot-service.yaml - mosquitto-deployment.yaml - - sml2mqtt-config.yaml - - sml2mqtt-deployment.yaml - shelly-manager-pvc.yaml - shelly-manager-network-attachment.yaml - shelly-manager-deployment.yaml diff --git a/kubernetes/apps/home-assistant/sml2mqtt-config.yaml b/kubernetes/apps/home-assistant/sml2mqtt-config.yaml deleted file mode 100644 index ae201af5..00000000 --- a/kubernetes/apps/home-assistant/sml2mqtt-config.yaml +++ /dev/null @@ -1,44 +0,0 @@ ---- -# sml2mqtt v3 config TEMPLATE. The broker password is injected at pod -# start by the deployment's init container (sed of __MQTT_PASSWORD__ from -# the mosquitto-sml2mqtt-credentials Secret) into an emptyDir, because -# sml2mqtt reads only a config file and has no env-var override for the -# password. The generated password is [A-Za-z0-9] only, so it is sed-safe. -# -# `Wh in kWh: false` is deliberate: the legacy edl21 sensors publish Wh, -# and the new MQTT sensors reuse the same entity_ids — keeping Wh means -# Home Assistant long-term statistics continue with zero unit discontinuity. -apiVersion: v1 -kind: ConfigMap -metadata: - name: sml2mqtt-config - namespace: home-assistant -data: - config.yml: | - logging: - level: INFO - file: stdout # 'stdout' -> kubectl logs / Loki (NOT '' — that opens the dir) - - mqtt: - connection: - identifier: sml2mqtt-meter - host: mosquitto.home-assistant.svc.cluster.local - port: 1883 - user: sml2mqtt - password: __MQTT_PASSWORD__ - topic prefix: sml2mqtt - defaults: - qos: 0 - retain: false - last will: - topic: status - - general: - Wh in kWh: false # keep raw Wh to match edl21 history (see header) - republish after: 120 # re-emit unchanged values every 120 s - - inputs: - - type: serial - url: /dev/stromzaehler # FTDI FT230X, mounted by the deployment - timeout: 3 - # 9600 8N1 are sml2mqtt defaults for SML/EDL21 — no override needed. diff --git a/kubernetes/apps/home-assistant/sml2mqtt-deployment.yaml b/kubernetes/apps/home-assistant/sml2mqtt-deployment.yaml deleted file mode 100644 index f14e5998..00000000 --- a/kubernetes/apps/home-assistant/sml2mqtt-deployment.yaml +++ /dev/null @@ -1,106 +0,0 @@ ---- -# sml2mqtt — reads the German SML/EDL21 power meter off its USB FTDI -# serial reader and publishes OBIS values to the in-cluster Mosquitto -# broker. This replaces Home Assistant's built-in `edl21` integration so -# the heavy HA pod no longer needs the USB device and can leave k8s-edge. -# -# Pinned to k8s-edge because the meter's USB reader is physically on that -# node (a genuine hardware constraint, not a stability pin). The pod is -# tiny, so it's an acceptable resident of the soon-to-be-tiny edge box. -# -# SERIAL EXCLUSIVITY: the meter's /dev/ttyUSB0 is a single serial stream; -# edl21 (in the HA pod) and sml2mqtt cannot both read it without -# corrupting each other's SML framing. edl21 has now been disabled in HA, -# freeing the port, so this is activated (replicas: 1). Remaining cutover: -# add HA MQTT sensors reusing the old entity_ids -> evict HA from edge. -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sml2mqtt - namespace: home-assistant - labels: - app.kubernetes.io/name: sml2mqtt -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - app.kubernetes.io/name: sml2mqtt - template: - metadata: - labels: - app.kubernetes.io/name: sml2mqtt - spec: - # USB meter reader is on k8s-edge; tolerate the pinned taint to land there. - nodeSelector: - topology.kubernetes.io/zone: edge - tolerations: - - key: workload-class - operator: Equal - value: pinned - effect: NoSchedule - enableServiceLinks: false - # The injected config.yml (with the broker password) is written here - # by the init container and read by the main container at /sml2mqtt. - initContainers: - - name: render-config - image: alpine:3.21 - command: - - sh - - -c - - | - set -eu - sed "s/__MQTT_PASSWORD__/${MQTT_PASSWORD}/" \ - /tmpl/config.yml > /sml2mqtt/config.yml - env: - - name: MQTT_PASSWORD - valueFrom: - secretKeyRef: - name: mosquitto-sml2mqtt-credentials - key: password - volumeMounts: - - name: config-tmpl - mountPath: /tmpl - readOnly: true - - name: rendered - mountPath: /sml2mqtt - resources: - requests: - cpu: 10m - memory: 16Mi - limits: - memory: 32Mi - containers: - - name: sml2mqtt - image: spacemanspiff2007/sml2mqtt:3.6.0 # pinned; no image-automation by design (critical meter bridge) - volumeMounts: - - name: rendered - mountPath: /sml2mqtt - - name: stromzaehler - mountPath: /dev/stromzaehler - securityContext: - # The meter device is root:root 0600 on the node. Opening it - # needs BOTH: runAsUser 0 (file DAC) AND privileged (the device - # cgroup blocks a hostPath-mounted char device for non-privileged - # containers even as root — there's no device-plugin here). This - # mirrors how the HA pod accesses the same device today; the win - # is that it's now a tiny pod instead of the 8Gi HA pod. - privileged: true - runAsUser: 0 - runAsGroup: 0 - resources: - requests: - cpu: 25m - memory: 64Mi - limits: - memory: 128Mi - volumes: - - name: config-tmpl - configMap: - name: sml2mqtt-config - - name: rendered - emptyDir: {} - - name: stromzaehler - hostPath: - path: /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_DK0E1GP5-if00-port0 diff --git a/kubernetes/bootstrap.sh b/kubernetes/bootstrap.sh index 79d0cd54..cf77d250 100755 --- a/kubernetes/bootstrap.sh +++ b/kubernetes/bootstrap.sh @@ -15,7 +15,10 @@ TALOS_DIR="$(cd "$(dirname "$0")/talos" && pwd)" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" ACCESS_STAGING="$REPO_ROOT/kubernetes/access-staging" NODES=(k8s-1 k8s-2 k8s-3) -WORKERS=(k8s-edge) +# No dedicated workers since the k8s-edge VM was retired (2026-07-02) — +# hardware-bound services live on the bare-metal edge-1 Arch host +# (ansible/roles/edge-host). +WORKERS=() ALL_NODES=("${NODES[@]}" "${WORKERS[@]}") DOMAIN="srv.renner.uno" DNS_SERVER="10.2.40.53" diff --git a/kubernetes/clusters/homelab/home-assistant.yaml b/kubernetes/clusters/homelab/home-assistant.yaml index 39044671..cf8905f6 100644 --- a/kubernetes/clusters/homelab/home-assistant.yaml +++ b/kubernetes/clusters/homelab/home-assistant.yaml @@ -1,31 +1,5 @@ apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization -metadata: - name: home-assistant-zwavejs - namespace: flux-system -spec: - interval: 10m - retryInterval: 1m - timeout: 10m - sourceRef: - kind: GitRepository - name: flux-system - path: ./kubernetes/apps/home-assistant-zwavejs - prune: true - # Manifests apply, but don't block on Deployment health — the ZWA-2 hostPath - # is a TBD placeholder until the USB is physically moved to saturn at cutover. - wait: false - decryption: - provider: sops - secretRef: - name: sops-age - dependsOn: - - name: infrastructure - - name: infrastructure-crds - - name: infrastructure-ceph-cluster ---- -apiVersion: kustomize.toolkit.fluxcd.io/v1 -kind: Kustomization metadata: name: home-assistant namespace: flux-system diff --git a/kubernetes/infrastructure/image-automation/kustomization.yaml b/kubernetes/infrastructure/image-automation/kustomization.yaml index be736051..8e26aae6 100644 --- a/kubernetes/infrastructure/image-automation/kustomization.yaml +++ b/kubernetes/infrastructure/image-automation/kustomization.yaml @@ -23,4 +23,3 @@ resources: - velero-plugin-for-aws.yaml - vsftpd.yaml - wyoming.yaml - - zwave-js-ui.yaml diff --git a/kubernetes/infrastructure/image-automation/zwave-js-ui.yaml b/kubernetes/infrastructure/image-automation/zwave-js-ui.yaml deleted file mode 100644 index 245bfd51..00000000 --- a/kubernetes/infrastructure/image-automation/zwave-js-ui.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: image.toolkit.fluxcd.io/v1 -kind: ImageRepository -metadata: - name: zwave-js-ui - namespace: flux-system -spec: - image: ghcr.io/zwave-js/zwave-js-ui - interval: 1h ---- -apiVersion: image.toolkit.fluxcd.io/v1 -kind: ImagePolicy -metadata: - name: zwave-js-ui - namespace: flux-system -spec: - imageRepositoryRef: - name: zwave-js-ui - policy: - semver: - range: "11.x" diff --git a/kubernetes/talos/patches/k8s-edge.yaml b/kubernetes/talos/patches/k8s-edge.yaml deleted file mode 100644 index 9afbcdf3..00000000 --- a/kubernetes/talos/patches/k8s-edge.yaml +++ /dev/null @@ -1,44 +0,0 @@ -machine: - network: - hostname: k8s-edge - # k8s-edge has two NICs: eth0 on the primary LAN (10.2.40.0/24) and - # eth1 on v30iot. eth1 exists for OTBR/matter-server macvlan parents - # and for music-assistant's host-networked L2 locality with audio - # renderers — see opentofu/vm_153_k8s_edge.tf and the - # k8s-edge-node-pattern wiki note for the full design. - # - # `worker.yaml` is `network: {}`, so both NICs default to DHCP at - # the Talos-default routeMetric=1024. The IoT VLAN's DHCP server - # returns a default gateway too, so two same-priority default - # RouteSpecs land at metric 1024 (10.2.40.1 via eth0, 10.2.30.1 via - # eth1). The kernel can't install both at the same metric — one - # wins, the other returns EEXIST, and RouteSpecController retries - # the loser every ~30-70s, spamming the controller-runtime log. - # - # Talos v1.12.6's DHCPOptions exposes only `routeMetric`, `ipv4`, - # `ipv6`, `duidv6` — no discrete "ignore default gateway" toggle — - # so the lever is metric. Bumping eth1's routeMetric makes both - # default routes coexist at different metrics: eth0's stays at 1024 - # and wins normal egress; eth1's at 4096 is a fallback the kernel - # only consults if eth0 is down. Connected /24 routes on eth1 still - # work (Cilium L2-IPAM, OTBR/matter-server macvlan parents, music- - # assistant L2 reach to renderers all unaffected — they use the - # connected route, not the default). - interfaces: - - interface: eth1 - dhcp: true - dhcpOptions: - routeMetric: 4096 - install: - disk: /dev/vda - certSANs: - - k8s-edge.srv.renner.uno - nodeLabels: - topology.kubernetes.io/zone: edge - # NodeRestriction admission blocks post-join taint modification by worker - # kubelets, so `machine.nodeTaints` has no effect here. Pass the taint at - # kubelet registration instead — kubelets are allowed to self-register with - # taints via --register-with-taints. - kubelet: - extraArgs: - register-with-taints: workload-class=pinned:NoSchedule diff --git a/opentofu/vm_153_k8s_edge.tf b/opentofu/vm_153_k8s_edge.tf deleted file mode 100644 index d336c379..00000000 --- a/opentofu/vm_153_k8s_edge.tf +++ /dev/null @@ -1,95 +0,0 @@ -# Dedicated "edge" K8s node for pinned workloads: -# - latency-sensitive services (e.g. music-assistant) that need isolation -# from noisy neighbours -# - services bound to host USB devices (home-assistant with Z-Wave + EDI21) -# -# Topology contract: -# - Not a Ceph OSD (no second disk) -# - Not HA — USB passthrough pins the node to one physical host, so -# migration offers nothing and would lose the USB sticks -# - Tainted `workload-class=pinned:NoSchedule`; only pods that tolerate -# that taint land here (see patches/k8s-edge.yaml) -# -# Pinned to neptune: VM 103 (legacy HAOS) currently holds the ZWA-2 + -# stromzaehler USB mappings on neptune; at HA cutover the USBs rebind -# from VM 103 to this VM in-place on the same host (no physical move). -# DEPRECATION PATH (decision 2026-07-02, reverses 2026-05-05): this VM is -# being replaced by a bare-metal Arch N100 (`edge-1`, see -# ansible/roles/edge-host) because the new CAN/open3e service needs a -# kernel with CONFIG_CAN, which stock Talos does not ship. Remove this -# file after the cutover checklist in roles/edge-host/README.md is done. -module "k8s_edge" { - source = "./modules/proxmox_vm" - - name = "k8s-edge.srv.renner.uno" - target_node = "neptune" - vmid = 153 - # ~10k-entity HA at 4Gi pod limit OOM-looped (see deployment.yaml). The - # legacy HAOS VM 103 that ran this same workload had 16Gi; bump this VM so - # the HA pod can have an 8Gi limit with headroom for the other pinned pods - # (matter-server, otbr, zwave-js, music-assistant). Freed RAM comes from - # the now-stopped VM 103. - memory = 12288 - cores = 2 - - template_name = null - cloud_init = false - full_clone = false - bios = "ovmf" - boot = "order=virtio0" - agent_enabled = true - - # Second NIC on the IoT VLAN. Two independent tenants need this bridge - # and both constraints land on the same NIC: - # 1. Music Assistant — uses host networking for audio-streaming latency. - # Multi-room audio sync (Snapcast / SlimProto / Chromecast groups) - # cannot cross L3 boundaries without breaking timing, so the host- - # networked pod has to reach the audio renderers at L2; the renderers - # live on v30iot, so the host stack on k8s-edge needs to be there too. - # mDNS / uPnP / DLNA / SlimProto discovery working as a result is a - # side effect of being on the players' broadcast domain, not the - # reason hostNetwork was chosen. - # 2. OTBR (and matter-server) — attach a Multus macvlan NAD with eth1 - # as the parent (static IPAM on 10.2.30.0/24, no gateway) so the BR - # and Matter SDK have real L2 presence on the IoT VLAN: mDNS/SRP - # advertisement, IPv6 RA reception, and OMR-prefix publication into - # the Thread mesh. - # Removing either tenant would not let this NIC go away. - extra_network_bridges = ["v30iot"] - - disks = [ - { - storage = "local-zfs" - size = "64G" - format = "raw" - slot = "virtio0" - file_id = local.talos_image - } - ] - - # USB passthrough — both devices are now bound to this VM (cutover - # complete; vm_103 no longer holds them). Block order and usb3 setting - # match the live Proxmox state captured 2026-05-09 to avoid drift on - # tofu plan. - usb = [ - { - mapping_id = "ZWA-2" - usb3 = true - id = "0" - }, - { - mapping_id = "stromzaehler" - usb3 = true - id = "1" - } - ] - - tags = "kubernetes;talos;worker;edge" - ssh_public_keys = local.common_ssh_keys - - vm_state = "running" - - # Rebuild pipeline issues `-replace`; skip graceful shutdown so a - # non-responsive Talos guest can't hang the destroy for 30 minutes. - stop_on_destroy = true -}