feat(rook-ceph): add the VLAN 49 attachment for Ceph's cluster network #638

Merged
k.renner merged 2 commits from ceph-san-nad into master 2026-08-26 19:41:39 +00:00
Member

Step 2 of 3. Inert on its own — a NetworkAttachmentDefinition does nothing until a pod references it, and nothing will until cephcluster.yaml names it under network.selectors.cluster. That is step 3.

The problem it exists to solve. addressRanges only takes effect under provider: host, so carrying replication over the dedicated 10G link meant setting a provider that is not per-daemon — it puts mons on host networking too. A mon's address is then the node's DHCP lease, and when that lease moves the mon cannot bind. Three mons on three nodes each hold the mon host ports, so Rook cannot schedule the replacement it just decided to create, and the failover stalls. Both happened during the v1.12.11 upgrade, on two of three nodes, and needed manual intervention each time.

None of that serves the goal. Per Ceph's documentation the cluster network carries OSD heartbeat, replication and recovery only — "Ceph Monitors always operate on the public network". This attaches the link to the OSDs and leaves every other daemon on the pod network, where Rook manages identity itself.

Its own subnet, not the existing /29. The nodes hold 10.99.0.1-.3 for as long as Ceph still uses host networking — that leaves exactly three addresses for three OSDs, no headroom during the one phase where things actually move, and a leaked allocation from a hard node loss would mean an OSD without an address. OSDs never need to reach the node addresses (the cluster network is OSD-to-OSD), so the pool sits in 10.99.1.0/24 on the same segment. A /24 for three addresses costs nothing here: nothing else can ever live on this segment, and it takes address count off the table when OSDs are added.

Once the provider switch is confirmed, the node addresses on VLAN 49 have no remaining purpose and can come out of the Talos configs.

Isolation is preserved, not weakened. No gateway and no routes — there is no router on this segment and there must not be one. That also means no sbr chaining, unlike the v30iot attachments, because there is no DHCP-supplied default route to confine.

whereabouts for IPAM: dhcp needs a server that must not exist on the SAN, and host-local keeps state per node with no cluster-wide coordination, so two nodes would hand out the same address on shared L2.

MTU 9000 matches the link — macvlan inherits it, but stating it means a change to the parent cannot silently halve replication throughput.

Verified: whereabouts from step 1 running on all three nodes, binary installed next to macvlan; NAD passes a server-side dry run; embedded CNI JSON parses with the intended master, mtu, ipam type and range.

Step 2 of 3. **Inert on its own** — a NetworkAttachmentDefinition does nothing until a pod references it, and nothing will until `cephcluster.yaml` names it under `network.selectors.cluster`. That is step 3. **The problem it exists to solve.** `addressRanges` only takes effect under `provider: host`, so carrying replication over the dedicated 10G link meant setting a provider that is not per-daemon — it puts mons on host networking too. A mon's address is then the node's DHCP lease, and when that lease moves the mon cannot bind. Three mons on three nodes each hold the mon host ports, so Rook cannot schedule the replacement it just decided to create, and the failover stalls. Both happened during the v1.12.11 upgrade, on two of three nodes, and needed manual intervention each time. None of that serves the goal. Per Ceph's documentation the cluster network carries OSD heartbeat, replication and recovery only — *"Ceph Monitors always operate on the public network"*. This attaches the link to the OSDs and leaves every other daemon on the pod network, where Rook manages identity itself. **Its own subnet, not the existing /29.** The nodes hold `10.99.0.1-.3` for as long as Ceph still uses host networking — that leaves exactly three addresses for three OSDs, no headroom during the one phase where things actually move, and a leaked allocation from a hard node loss would mean an OSD without an address. OSDs never need to reach the node addresses (the cluster network is OSD-to-OSD), so the pool sits in `10.99.1.0/24` on the same segment. A /24 for three addresses costs nothing here: nothing else can ever live on this segment, and it takes address count off the table when OSDs are added. Once the provider switch is confirmed, the node addresses on VLAN 49 have no remaining purpose and can come out of the Talos configs. **Isolation is preserved, not weakened.** No gateway and no routes — there is no router on this segment and there must not be one. That also means no `sbr` chaining, unlike the v30iot attachments, because there is no DHCP-supplied default route to confine. `whereabouts` for IPAM: `dhcp` needs a server that must not exist on the SAN, and `host-local` keeps state per node with no cluster-wide coordination, so two nodes would hand out the same address on shared L2. MTU 9000 matches the link — macvlan inherits it, but stating it means a change to the parent cannot silently halve replication throughput. **Verified:** whereabouts from step 1 running on all three nodes, binary installed next to `macvlan`; NAD passes a server-side dry run; embedded CNI JSON parses with the intended master, mtu, ipam type and range.
claude-bot added 1 commit 2026-08-26 19:33:26 +00:00
feat(rook-ceph): add the VLAN 49 attachment for Ceph's cluster network
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
296f79469e
Step 2 of moving Ceph's cluster network off host networking. Inert on its
own: a NetworkAttachmentDefinition does nothing until a pod references it,
and nothing will until cephcluster.yaml names it under
network.selectors.cluster.

Why this exists at all: `addressRanges` only takes effect under
`provider: host`, so carrying replication over the dedicated 10G link meant
setting a provider that is not per-daemon. It puts mons on host networking
too, which makes a mon's address the node's DHCP lease — and when that lease
moves the mon cannot bind. Three mons on three nodes then each hold the mon
host ports, so Rook cannot schedule the replacement it just decided to create
and the failover stalls. Both happened during the v1.12.11 upgrade, on two of
three nodes.

None of that serves the goal. Per the Ceph documentation the cluster network
carries OSD heartbeat, replication and recovery only — "Ceph Monitors always
operate on the public network". Attaching it here gives the OSDs the link and
leaves every other daemon on the pod network.

Its own subnet rather than the existing /29. The nodes hold 10.99.0.1-.3 for
as long as Ceph still uses host networking, which leaves exactly three
addresses for three OSDs — no headroom during the one phase where things
actually move, and a leaked allocation from a hard node loss would mean an
OSD without an address. OSDs never need to reach the node addresses, since
the cluster network is OSD-to-OSD, so the pool sits in its own range on the
same segment instead. A /24 for three addresses costs nothing: nothing else
can ever live on this segment, and it takes address count off the table when
OSDs are added.

After the provider switch is confirmed, the node addresses on VLAN 49 have no
remaining purpose and can come out of the Talos configs.

No gateway and no routes, because there is no router on this segment and
there must not be one. That also means no `sbr` chaining, unlike the v30iot
attachments — there is no DHCP-supplied default route to confine here.

whereabouts for IPAM: dhcp needs a server that must not exist on the SAN, and
host-local keeps state per node with no cluster-wide coordination, so two
nodes would hand out the same address on shared L2.

MTU 9000 matches the link. macvlan inherits it, but stating it means a change
to the parent cannot silently halve replication throughput with nothing in
the tree recording the expectation.

Verified: whereabouts from step 1 is running on all three nodes with its
binary installed next to macvlan; the NAD passes a server-side dry run; the
embedded CNI JSON parses and carries the intended master, mtu, ipam type and
range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review-bot left a comment
Member

🤖 Automated review — model CLI default (complexity: complex), commit 296f79469e29f30256224c5bcfb80800e72b59cd.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a Multus NetworkAttachmentDefinition (ceph-san, macvlan on enp2s0f1np1, MTU 9000, whereabouts IPAM over 10.99.1.0/24) in rook-ceph-cluster/, wired into the kustomization. This is step 2 of 3: cephcluster.yaml is untouched in this diff and still sets network.provider: host / addressRanges.cluster: ["10.99.0.0/29"], so the PR is genuinely inert on its own — nothing currently references ceph-san. No blocking issues found.

🟡 Suggestions

  • san-network-attachment.yaml:79 — the NAD name ceph-san doesn't obviously read as "cluster network" at a glance (the interface is called the "SAN"/cluster network throughout the PR body, but Ceph terminology distinguishes "public" vs "cluster" networks, and a future reader skimming network.selectors in step 3 will need to cross-reference this file to know which is which). Not a defect — the extensive header comment makes the intent clear — just something to keep in mind when step 3 lands, e.g. a name like ceph-cluster-net might self-document better. Optional.

Tests

No automated tests apply to this kind of manifest-only, infra-as-code change, consistent with the rest of this directory (none of the sibling files under kubernetes/infrastructure/rook-ceph-cluster/ carry tests). The PR body states server-side dry-run and CNI JSON parsing were verified manually; I independently confirmed the claims that matter for correctness against the checkout:

  • enp2s0f1np1 is the correct, identical interface name on all three OSD nodes (kubernetes/talos/patches/k8s-{1,2,3}.yaml), matching the nodes actually listed under cephcluster.yaml's storage.nodes.
  • MTU 9000 matches the parent link's configured MTU in the Talos LinkConfig for all three nodes.
  • 10.99.1.0/24 doesn't overlap the existing 10.99.0.0/29 node-address pool used by addressRanges.cluster today.
  • whereabouts (the IPAM type used) is already deployed cluster-wide via kubernetes/infrastructure/multus/whereabouts-daemonset.yaml, so the dependency step 1 refers to is actually present.
  • metadata.namespace: rook-ceph matches every other resource in this kustomization (the Flux Kustomization for this path sets no targetNamespace, so an implicit-namespace resource here would have been a real bug — it isn't).
  • No gateway/routes keys are set, and the CNI config has no sbr stage, consistent with the "no router on this segment" rationale in the comments.

Recommendation

Looks reasonable — no blocking issues; comment above is optional.

🤖 **Automated review** — model `CLI default` (complexity: complex), commit `296f79469e29f30256224c5bcfb80800e72b59cd`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a Multus `NetworkAttachmentDefinition` (`ceph-san`, macvlan on `enp2s0f1np1`, MTU 9000, `whereabouts` IPAM over `10.99.1.0/24`) in `rook-ceph-cluster/`, wired into the kustomization. This is step 2 of 3: `cephcluster.yaml` is untouched in this diff and still sets `network.provider: host` / `addressRanges.cluster: ["10.99.0.0/29"]`, so the PR is genuinely inert on its own — nothing currently references `ceph-san`. No blocking issues found. ## 🟡 Suggestions - `san-network-attachment.yaml:79` — the NAD name `ceph-san` doesn't obviously read as "cluster network" at a glance (the interface is called the "SAN"/cluster network throughout the PR body, but Ceph terminology distinguishes "public" vs "cluster" networks, and a future reader skimming `network.selectors` in step 3 will need to cross-reference this file to know which is which). Not a defect — the extensive header comment makes the intent clear — just something to keep in mind when step 3 lands, e.g. a name like `ceph-cluster-net` might self-document better. Optional. ## Tests No automated tests apply to this kind of manifest-only, infra-as-code change, consistent with the rest of this directory (none of the sibling files under `kubernetes/infrastructure/rook-ceph-cluster/` carry tests). The PR body states server-side dry-run and CNI JSON parsing were verified manually; I independently confirmed the claims that matter for correctness against the checkout: - `enp2s0f1np1` is the correct, identical interface name on all three OSD nodes (`kubernetes/talos/patches/k8s-{1,2,3}.yaml`), matching the nodes actually listed under `cephcluster.yaml`'s `storage.nodes`. - MTU 9000 matches the parent link's configured MTU in the Talos `LinkConfig` for all three nodes. - `10.99.1.0/24` doesn't overlap the existing `10.99.0.0/29` node-address pool used by `addressRanges.cluster` today. - `whereabouts` (the IPAM type used) is already deployed cluster-wide via `kubernetes/infrastructure/multus/whereabouts-daemonset.yaml`, so the dependency step 1 refers to is actually present. - `metadata.namespace: rook-ceph` matches every other resource in this kustomization (the Flux `Kustomization` for this path sets no `targetNamespace`, so an implicit-namespace resource here would have been a real bug — it isn't). - No `gateway`/`routes` keys are set, and the CNI config has no `sbr` stage, consistent with the "no router on this segment" rationale in the comments. ## Recommendation Looks reasonable — no blocking issues; comment above is optional. <!-- pr-review:sha=296f79469e29f30256224c5bcfb80800e72b59cd -->
claude-bot added 1 commit 2026-08-26 19:37:50 +00:00
fix(rook-ceph): keep the SAN pool on-link with the node addresses
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
585ea36c9f
The pool was 10.99.1.0/24, a separate subnet on the same segment, chosen to
avoid carving up a /29 with only three addresses left. That choice would have
broken the migration it exists for.

OSDs move one at a time, so the cluster runs mixed: an OSD already on the
attachment has to reach an OSD still using its node's host address on
10.99.0.x. There is no router on this segment — by design, it carries
Ceph-internal traffic and nothing else — so a pool outside the /29 is
unreachable from the un-migrated side. The two OSDs would mark each other
down, during precisely the window that is supposed to be the safe one.

Moves the pool into 10.99.0.0/29 with range_start/range_end reserving .4-.6,
leaving .1-.3 to the nodes. That is one address per OSD with nothing spare,
which is sufficient rather than merely tolerable: osdMaxUpdatesInParallel
bounds the migration to one OSD at a time, so allocations grow 1, 2, 3 and
never overlap. Headroom returns after the switch is confirmed and the node
addresses come out of the Talos configs — then the pool takes the whole /29.

Verified: the embedded CNI JSON parses with the intended range and bounds;
the NAD passes a server-side dry run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Correction pushed in 585ea36c — the original pool would have broken the migration it exists for.

It was 10.99.1.0/24, a separate subnet on the same segment, chosen to avoid carving up a /29 with only three addresses left. But OSDs move one at a time, so the cluster runs mixed: an OSD already on the attachment has to reach an OSD still using its node's host address on 10.99.0.x. There is no router on this segment — by design — so anything outside the /29 is unreachable from the un-migrated side, and the two OSDs would mark each other down during precisely the window that is meant to be safe.

Pool is now 10.99.0.0/29 with range_start/range_end reserving .4-.6, leaving .1-.3 to the nodes. One address per OSD with nothing spare, which is sufficient rather than merely tolerable: spec.storage.osdMaxUpdatesInParallel bounds the migration to one OSD at a time, so allocations grow 1, 2, 3 and never overlap. Headroom returns once the switch is confirmed and the node addresses come out of the Talos configs — then the pool takes the whole /29.

Related finding for step 3, which this now depends on: osdMaxUpdatesInParallel defaults to 20 and upgradeOSDRequiresHealthyPGs defaults to false. With three OSDs that means all three restart at once with no PG health gate — a full outage, not a degraded cluster. Step 3 must set spec.storage.osdMaxUpdatesInParallel: 1 and spec.upgradeOSDRequiresHealthyPGs: true, and those should land and be verified before the provider change rather than alongside it.

Correction pushed in 585ea36c — the original pool would have broken the migration it exists for. It was `10.99.1.0/24`, a separate subnet on the same segment, chosen to avoid carving up a /29 with only three addresses left. But OSDs move one at a time, so the cluster runs mixed: an OSD already on the attachment has to reach an OSD still using its node's host address on `10.99.0.x`. There is no router on this segment — by design — so anything outside the /29 is unreachable from the un-migrated side, and the two OSDs would mark each other down during precisely the window that is meant to be safe. Pool is now `10.99.0.0/29` with `range_start`/`range_end` reserving `.4-.6`, leaving `.1-.3` to the nodes. One address per OSD with nothing spare, which is sufficient rather than merely tolerable: `spec.storage.osdMaxUpdatesInParallel` bounds the migration to one OSD at a time, so allocations grow 1, 2, 3 and never overlap. Headroom returns once the switch is confirmed and the node addresses come out of the Talos configs — then the pool takes the whole /29. Related finding for step 3, which this now depends on: `osdMaxUpdatesInParallel` defaults to **20** and `upgradeOSDRequiresHealthyPGs` defaults to **false**. With three OSDs that means all three restart at once with no PG health gate — a full outage, not a degraded cluster. Step 3 must set `spec.storage.osdMaxUpdatesInParallel: 1` and `spec.upgradeOSDRequiresHealthyPGs: true`, and those should land and be verified before the provider change rather than alongside it.
Author
Member

Correcting the severity in my previous comment and in 585ea36c's message — both said the separate subnet "would have broken the migration it exists for". That understates it.

The failure is not a migration step that fails to complete. At the first migrated OSD, cluster-network heartbeats fail in both directions: the two un-migrated OSDs report the migrated one down, and it reports both of them down. With three OSDs marking each other down, all 217 PGs go inactive — every pool, every RBD volume, both CephFS mounts. After mon_osd_down_out_interval the OSDs are additionally marked out, and with three hosts at size 3 there is no target for CRUSH to remap to.

So: a stopped cluster with every pool unreachable, starting at OSD number one — not a partial rollout that gets noticed and aborted.

That puts the on-link requirement in the same class as osdMaxUpdatesInParallel: 1. Neither is a refinement; both are preconditions, and step 3 must not proceed without both in place and verified.

Correcting the severity in my previous comment and in 585ea36c's message — both said the separate subnet "would have broken the migration it exists for". That understates it. The failure is not a migration step that fails to complete. At the **first** migrated OSD, cluster-network heartbeats fail in both directions: the two un-migrated OSDs report the migrated one down, and it reports both of them down. With three OSDs marking each other down, all 217 PGs go inactive — every pool, every RBD volume, both CephFS mounts. After `mon_osd_down_out_interval` the OSDs are additionally marked out, and with three hosts at size 3 there is no target for CRUSH to remap to. So: a stopped cluster with every pool unreachable, starting at OSD number one — not a partial rollout that gets noticed and aborted. That puts the on-link requirement in the same class as `osdMaxUpdatesInParallel: 1`. Neither is a refinement; both are preconditions, and step 3 must not proceed without both in place and verified.
review-bot left a comment
Member

🤖 Automated review — model CLI default (complexity: complex), commit 585ea36c9fb6390f6c5ffc9d2ab9b911edfb63a1.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

Adds a new, currently-inert NetworkAttachmentDefinition (macvlan on VLAN 49 / enp2s0f1np1, whereabouts IPAM) and wires it into the rook-ceph-cluster kustomization. Nothing references it yet (cephcluster.yaml is untouched), so this change has no runtime effect by itself, as the PR description says. The manifest itself is internally consistent and well cross-checked against the rest of the repo (interface name, MTU, node addresses, existing addressRanges.cluster). No blocking issues, but the PR description's stated design contradicts what was actually shipped, which is worth resolving before step 3 lands.

🟡 Suggestions

  • PR description contradicts the shipped IPAM range. The PR body argues explicitly for not reusing the existing /29: "Its own subnet, not the existing /29... the pool sits in 10.99.1.0/24" — reasoning that OSDs never need to reach node addresses. The shipped file, kubernetes/infrastructure/rook-ceph-cluster/san-network-attachment.yaml:99-101, does the opposite: "range": "10.99.0.0/29", range_start: "10.99.0.4", range_end: "10.99.0.6" — the same /29 the nodes already use (10.99.0.1-.3, confirmed in kubernetes/talos/patches/k8s-{1,2,3}.yaml and matching cephcluster.yaml:162's existing addressRanges.cluster: ["10.99.0.0/29"]). The in-file comment (lines 53-66) gives the opposite rationale from the PR body: during the one-OSD-at-a-time migration, a macvlan OSD must still reach OSDs that haven't migrated yet and are bound to a node's host address in the same /29 — and since there's deliberately no gateway/router on this segment (also stated in both the PR body and the file), a separate 10.99.1.0/24 pool would in fact be unreachable from the un-migrated side on plain L2, which is exactly the kind of stall the PR is trying to eliminate. In other words, the code as shipped looks like the correct choice and the PR description looks stale/superseded — but as written, the description actively documents a plan that isn't what was implemented. Worth updating the PR body (or a follow-up commit message) so the rationale that ships in history matches the manifest, especially since step 3 (wiring cephcluster.yaml) will be reviewed against this same description.

🟢 Nits

  • kustomization.yaml:9-12 — comment is accurate and matches the file's own "inert" framing; no issue, just noting it was checked against cephcluster.yaml which indeed has no network.selectors yet.

Tests

No automated test applies here — this is a static Kubernetes manifest with no runtime references yet. .woodpecker/ci-manifests-test.yml should catch basic kustomize-build/YAML validity; I did not have a working kustomize/kubectl binary in this sandbox to run it directly, but the embedded CNI JSON parses correctly by inspection and the resource is straightforward enough that a build failure is unlikely. The real functional verification (does whereabouts actually hand out .4-.6, does the macvlan child reach the node addresses) can only happen once step 3 attaches this to cephcluster.yaml, which is explicitly out of scope here.

Recommendation

Looks reasonable — no blocking issues; the PR-description/subnet inconsistency above is worth fixing before step 3, but it does not affect the manifest actually being merged.

🤖 **Automated review** — model `CLI default` (complexity: complex), commit `585ea36c9fb6390f6c5ffc9d2ab9b911edfb63a1`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary Adds a new, currently-inert `NetworkAttachmentDefinition` (macvlan on VLAN 49 / `enp2s0f1np1`, whereabouts IPAM) and wires it into the `rook-ceph-cluster` kustomization. Nothing references it yet (`cephcluster.yaml` is untouched), so this change has no runtime effect by itself, as the PR description says. The manifest itself is internally consistent and well cross-checked against the rest of the repo (interface name, MTU, node addresses, existing `addressRanges.cluster`). No blocking issues, but the PR description's stated design contradicts what was actually shipped, which is worth resolving before step 3 lands. ## 🟡 Suggestions - **PR description contradicts the shipped IPAM range.** The PR body argues explicitly for *not* reusing the existing `/29`: *"Its own subnet, not the existing /29... the pool sits in `10.99.1.0/24`"* — reasoning that OSDs never need to reach node addresses. The shipped file, `kubernetes/infrastructure/rook-ceph-cluster/san-network-attachment.yaml:99-101`, does the opposite: `"range": "10.99.0.0/29"`, `range_start: "10.99.0.4"`, `range_end: "10.99.0.6"` — the *same* `/29` the nodes already use (`10.99.0.1-.3`, confirmed in `kubernetes/talos/patches/k8s-{1,2,3}.yaml` and matching `cephcluster.yaml:162`'s existing `addressRanges.cluster: ["10.99.0.0/29"]`). The in-file comment (lines 53-66) gives the opposite rationale from the PR body: during the one-OSD-at-a-time migration, a macvlan OSD must still reach OSDs that haven't migrated yet and are bound to a node's host address in the same `/29` — and since there's deliberately no gateway/router on this segment (also stated in both the PR body and the file), a separate `10.99.1.0/24` pool would in fact be unreachable from the un-migrated side on plain L2, which is exactly the kind of stall the PR is trying to eliminate. In other words, the code as shipped looks like the *correct* choice and the PR description looks stale/superseded — but as written, the description actively documents a plan that isn't what was implemented. Worth updating the PR body (or a follow-up commit message) so the rationale that ships in history matches the manifest, especially since step 3 (wiring `cephcluster.yaml`) will be reviewed against this same description. ## 🟢 Nits - `kustomization.yaml:9-12` — comment is accurate and matches the file's own "inert" framing; no issue, just noting it was checked against `cephcluster.yaml` which indeed has no `network.selectors` yet. ## Tests No automated test applies here — this is a static Kubernetes manifest with no runtime references yet. `.woodpecker/ci-manifests-test.yml` should catch basic kustomize-build/YAML validity; I did not have a working `kustomize`/`kubectl` binary in this sandbox to run it directly, but the embedded CNI JSON parses correctly by inspection and the resource is straightforward enough that a build failure is unlikely. The real functional verification (does whereabouts actually hand out `.4-.6`, does the macvlan child reach the node addresses) can only happen once step 3 attaches this to `cephcluster.yaml`, which is explicitly out of scope here. ## Recommendation Looks reasonable — no blocking issues; the PR-description/subnet inconsistency above is worth fixing before step 3, but it does not affect the manifest actually being merged. <!-- pr-review:sha=585ea36c9fb6390f6c5ffc9d2ab9b911edfb63a1 -->
k.renner merged commit b0a473d038 into master 2026-08-26 19:41:39 +00:00
Sign in to join this conversation.