Skip to content

docs(vm-instance): document attaching a VM to an external VLAN - #3196

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
docs/vm-external-vlan-bridge
Jul 8, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
docs/vm-external-vlan-bridge

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Adds docs/vm-external-vlan.md, a guide for attaching a vm-instance VM directly to an external, physically-routed VLAN via a Linux bridge and a bridge-type NetworkAttachmentDefinition.

The documented VM networking story is overlay-only (pod network + KubeOVN VPC subnets). Attaching a VM to a real VLAN — so it shares a broadcast domain with external hardware and takes an address from that VLAN's subnet — is undocumented, and the obvious macvlan approach silently fails with KubeVirt's default bridge binding: macvlan demultiplexes ingress by the child interface's MAC while bridge binding puts the guest's own MAC on the wire, so gateway replies reach the parent interface and are dropped before the guest. The working recipe is a Linux bridge + bridge CNI NAD.

The guide covers why bridge and not macvlan, the host bridge, the NAD (one per tenant namespace), assigning a static guest address through cloud-init (the chart has no networkData), the always-present dual-homed pod NIC, and the post-recreate ARP staleness from KubeVirt regenerating the guest MAC.

Docs-only; no chart or manifest changes. The recipe depends on the bridge CNI plugin being present in /opt/cni/bin; #3195 makes Cozystack's multus package stage the reference plugins there.

Screenshots

N/A — no UI changes.

Release note

docs(vm-instance): add a guide for attaching a VM to an external VLAN via a Linux bridge and a bridge-type NetworkAttachmentDefinition

Summary by CodeRabbit

  • Documentation
    • Added a new guide for connecting a vm-instance directly to an externally routed VLAN.
    • Includes setup steps for creating a bridged VLAN interface, defining a network attachment, and configuring the VM with a static IP.
    • Notes important networking caveats such as dual-homing behavior, MAC changes after recreation, and host-to-VM connectivity requirements.

Add a guide for bridging a vm-instance VM onto a physically-routed VLAN
via a Linux bridge and a bridge-type NetworkAttachmentDefinition. The
overlay-only default does not cover the case where a VM must share a
broadcast domain with external hardware and take an address from that
VLAN's subnet.

Explain why macvlan is incompatible with KubeVirt bridge binding (macvlan
demultiplexes ingress by the child MAC while bridge binding puts the
guest MAC on the wire, so replies are dropped) and why a Linux bridge
works, then walk through the host bridge, the NAD, and assigning a static
guest address through cloud-init. Cover the dual-homed NIC layout, the
absence of networkData, and the post-recreate ARP staleness.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65e0d7cc-9334-428d-8b05-90781c6ee7ef

📥 Commits

Reviewing files that changed from the base of the PR and between 10f8e2d and c4d023d.

📒 Files selected for processing (1)
  • docs/vm-external-vlan.md

📝 Walkthrough

Walkthrough

Adds a new documentation file, docs/vm-external-vlan.md, explaining how to attach a Cozystack vm-instance directly to an externally routed VLAN via a Linux bridge and Multus bridge CNI, covering setup steps, configuration examples, and operational caveats.

Changes

Documentation Addition

Layer / File(s) Summary
Overview and architecture
docs/vm-external-vlan.md
Introduces the VLAN attachment goal, the bridge-vs-macvlan constraint, overall architecture, and prerequisites (Multus, bridge CNI, no IPAM).
Setup steps: bridge, NAD, and VM config
docs/vm-external-vlan.md
Provides step-by-step instructions for configuring a node Linux bridge, a tenant-scoped NetworkAttachmentDefinition, and VM cloud-init/netplan static VLAN IP setup.
Gotchas and references
docs/vm-external-vlan.md
Lists operational caveats (dual-homing, no networkData support, MAC/ARP staleness on recreation, host-VM connectivity requirements) and links to related docs.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Poem

A rabbit hops through VLAN tags,
Bridging bytes in netplan bags,
No macvlan, but bridge will do,
Static IPs and cloud-init too,
Docs now written, clear and bright—
Hop along, the network's right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the docs change about attaching a vm-instance VM to an external VLAN.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/vm-external-vlan-bridge

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/documentation Categorizes issue or PR as related to documentation labels Jul 4, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) marked this pull request as ready for review July 4, 2026 11:45
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces documentation to address the gap in connecting virtual machines to external VLANs within the Cozystack environment. It explains the architectural requirements for bridging VM interfaces to external networks, specifically highlighting the incompatibility of macvlan with KubeVirt's default bridge binding and providing a robust alternative using Linux bridges.

Highlights

  • New Documentation: Added a comprehensive guide for attaching vm-instance VMs to external, physically-routed VLANs.
  • Technical Guidance: Clarified why a Linux bridge with the bridge CNI plugin is required instead of macvlan for this use case.
  • Configuration Steps: Provided detailed instructions for node-level bridge configuration, NetworkAttachmentDefinition setup, and in-guest static IP assignment via cloud-init.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Docs-only. Every technical claim verified against the current vm-instance chart
source (not stale behavior):

  • Default bridge: {} binding for default and every .networks entry
    (templates/vm.yaml:87-93); no VLAN-only mode, VM is always dual-homed.
  • NAD is resolved as {{ .Release.Namespace }}/{{ name }} (templates/vm.yaml:128),
    so a copy is required in each tenant namespace — correctly documented.
  • No networkData; static config only via cloud-init userData
    (templates/secret.yaml writes only userdata).
  • The values snippet and the NetworkAttachmentDefinition manifest are valid and match
    the chart's schema; the cross-reference to gpu-vgpu.md resolves.

markdownlint clean.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 12c3cd0 into main Jul 8, 2026
17 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the docs/vm-external-vlan-bridge branch July 8, 2026 07:39
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 6, 2026
## What this PR does

Multus-backed NetworkAttachmentDefinitions delegate to the upstream
containernetworking reference plugins (`bridge`, `macvlan`, `ipvlan`,
...), and nothing guarantees a node has them where Multus looks. On k3s
they live under `/var/lib/rancher/k3s/data/cni` while Multus and Cilium
install into `/opt/cni/bin`, which is left without a `bridge` binary. A
`bridge`-type NAD then fails with `failed to find plugin "bridge" in
path [/opt/cni/bin]`, and today you work around it by copying the binary
in by hand. #3196 documents one such pattern (attaching a VM to an
external VLAN) that this unblocks.

This bundles 14 of the reference plugins into the `multus-cni` image and
adds an `install-cni-plugins` init container that installs them into the
host `/opt/cni/bin`, mirroring the existing `install-multus-binary` init
container.

## Where it applies, and how to turn it off

The new `networking.stageCniPlugins` value defaults to on for both
bundles. Talos puts six binaries in that directory -- `bridge`,
`firewall`, `host-local`, `loopback` and `portmap` stripped from the
same upstream release, plus `flannel`, which is a different project --
so the other ten are missing there exactly as on generic Linux and a NAD
naming `macvlan`, `ipvlan` or `vlan` fails on either platform. Four of
those six are names this change stages; `loopback` is deliberately not
staged and `flannel` was never in the reference set. The four names that
overlap are rewritten on every Multus pod recreation with the same
upstream version -- Talos pins `containernetworking/plugins` `v1.9.1` in
`siderolabs/pkgs`, and so does this image -- so it is not a downgrade of
a system component. A marker in the image Dockerfile records the Talos
release that pin was compared against, and a test holds it to the
release the platform ships. `isp-hosted` ships no multus at all and is
unaffected. Both defaults can be overridden either way.

If you run a generic-Linux cluster and manage `/opt/cni/bin` yourself,
this changes behaviour on upgrade. Multus starts installing 14 plugins
into that directory every time its pod is recreated, replacing files of
the same name, so a hand-placed or locally patched plugin there will not
survive. Set `networking.stageCniPlugins: false` to decline, and set it
**before** the upgrade that first enables staging: nothing here removes
or restores a plugin, so declining afterwards protects what is still
yours and does not give back what was already replaced. Changing the
value edits the Multus DaemonSet's pod template, so Multus rolls node by
node, and while a node's daemon is down pod creation and deletion on
that node fail for the length of the restart. The four plugins the
package never touches are `loopback`, `dhcp`, `dummy` and `tap`.

Where staging runs it is unconditional, not only-when-absent, because
nothing here ever revisits a plugin it has written. Installing only when
absent would pin each node to whichever version arrived first, and a
later `CNI_PLUGINS_VERSION` bump, including one fixing a binary that
runs as root on every node, would reach zero existing nodes. The
directory is not ours alone and the decision does not assume it is:
kube-ovn copies `loopback`, `portmap` and `macvlan` there from its own
image, two of which this change also writes, node provisioning may place
the set, and `/opt/cni/bin` is also the CNI bin directory on RKE2.
Overwriting is what delivering a version requires, and
`networking.stageCniPlugins: false` is how an operator who owns those
files declines it.

## Notes

- The manifest change lives in `patches/customize-deployment.patch` so
it survives `make update`, and the release rewrite in that target runs
after the patch rather than before it, so the line the patch adds lands
on the same reference as the two it already had. The patch seeds that
line with the upstream image reference while the committed template
carries the pinned cozystack digest, because the package's `image:`
target rewrites every `multus-cni` image line to one digest. So it does
not reverse-apply against the committed template. That `make update`
reproduces the tree is verified forward from pristine upstream instead.
- `/opt/cni/bin` stays shared where staging is on. kube-ovn installs
`portmap` and `macvlan` there and has been seen to do so with a plain
copy onto the live path, which truncates in place. Each plugin is
installed under a temporary name and swapped in with a rename, atomic
within the mount, so the kubelet never execs a half-written binary. The
Dockerfile records which kube-ovn release the version pin was checked
against, and `hack/cni-plugins-staging-contract.bats` fails when that
marker and the vendored release part ways. That check fires on the next
kube-ovn bump and compares the marker with the vendored release; it does
not compare the two `CNI_PLUGINS_VERSION` pins. Moving the marker is the
acknowledgement that someone looked -- so read kube-ovn's
`dist/images/Dockerfile.base` before moving it, because a silent
divergence there makes `portmap` and `macvlan` flap between the two
DaemonSets on every pod recreation.
- The init container runs unprivileged, with capabilities dropped and no
mount propagation. Writing into a hostPath needs root inside the
container and nothing on the host, which is how cilium's
`install-cni-binaries` copies plugin binaries into the same directory on
the same nodes. `Bidirectional` propagation governs mounts made inside a
container, this script makes none, and asking for it is what would force
`privileged`. The tests pin the absence.
- `loopback` is excluded because replacing it is what a node cannot
survive: the runtime calls it for every sandbox, and Cilium and kube-ovn
already install it. `dhcp` needs a host daemon this package does not
run. `tap` and `dummy` have no consumer here. `portmap` is included
because a NAD conflist may chain it for hostPort.
- The plugin version is pinned via `ARG CNI_PLUGINS_VERSION` (v1.9.1)
and the release tarball is verified against a per-arch SHA-256 before
extraction. These are root-executed node binaries, so a tag alone is not
enough. An unknown or empty `TARGETARCH` fails the build.
- The init container skips staging when the image has no `/cni-plugins`,
rather than crashlooping the DaemonSet node-wide. The manifest and the
image digest are re-pinned on different schedules, so a tree taken
between a Dockerfile change and the next release bake can carry one
without the other. A plugin it cannot copy is reported on stderr and
skipped, for the same reason. `multusConfigFile: auto` makes
`00-multus.conf` the primary CNI config and there is no
`cleanupConfigOnExit`, so on a node where Multus has already run, an
init container that never completes leaves the shim's socket gone with
that config still primary, and the node stops scheduling anything that
goes through CNI; host-network pods still start. Failing on a copy error
would therefore leave the node worse off than before this change, which
only ever left it without one plugin. The cost of continuing is that the
pod still reports Ready, so the summary is also written to
`/dev/termination-log`, which the kubelet surfaces in `kubectl describe
pod` regardless of the exit code.
- The bundled plugins add roughly 67 MiB uncompressed per architecture
to an image pulled by every node, including installs where staging is
off. Measured by extracting the staged set from the v1.9.1 release
tarball.

## Testing

`packages/core/platform/tests/bundles_multus_staging_test.yaml` pins the
delivery point: both variant defaults, both explicit overrides, and that
multus is still shipped in each bundle at all. That last one because
multus is now included by hand in two branches rather than by the shared
helper.

`hack/multus-install-cni-plugins.bats` extracts the init container's
script from the rendered DaemonSet and runs it. It pins that every
plugin lands executable, that a replacement swaps the inode rather than
truncating in place, that temp files never leak, that a missing or empty
directory skips instead of failing, and that a plugin which cannot be
installed is reported on stderr and skipped rather than failing the
container. Both halves of that last one are asserted, because exit 0
alone would still pass with the report deleted.

`hack/cni-plugins-staging-contract.bats` covers what the rendered
manifest cannot show: that the Dockerfile stages the intended plugins
and only those, copies them into the final stage, and that nothing there
removes them again, that the tarball is verified before extraction, and
that the version marker matches the vendored kube-ovn release.
helm-unittest pins both states of the value, including off, which is
what most installs run, plus the quoted-string forms a values file can
produce. An explicit `null` is covered too: it is how a values file
clears the key, and the chart folds it onto its own default rather than
refusing it as an unreadable spelling.

The feature is inert until the image is re-baked. All three `image:`
lines still point at the pre-change digest, which has no `/cni-plugins`,
so on `main` after merge a generic install renders the init container,
logs `no /cni-plugins in this image; skipping staging`, and installs
nothing until release-prep re-pins. That is the intended flow
(first-party digests are release-managed, not hand-edited), and the
guide documents the log line, but anyone testing this on `main` will
meet it first and the pod will look healthy.

No CI job stages plugins on a node. CI boots Talos, where the value is
off, and `isp-full-generic` appears in no e2e workflow. The mechanics
are covered by the bats above, which run the real script from the real
manifest and also under `dash`, the shell in the runtime image, but
nothing demonstrates it end to end on a node.

Defaulting it on for the generic bundle is a deliberate maintainer
decision taken with that gap known, not a side effect of the diff.
Shipping it off would leave the bug in place on the only platform where
the feature does anything, and would not add coverage either, since the
code stays unexecuted in CI both ways. What makes the risk acceptable:
the init container skips rather than crashlooping when the image carries
no plugins, each plugin is swapped in with an atomic rename instead of
being written onto the live path, the tarball is checksummed before
extraction, and declining is a value rather than disabling the package.
And a plugin it cannot install is reported and skipped rather than
failing the pod, so the worst case leaves the node exactly as it was
before this change rather than unable to schedule.

### Screenshots

N/A, no UI changes.

### Release note

```release-note
feat(multus): install the reference CNI plugins (bridge, macvlan, ipvlan, ...) into /opt/cni/bin on generic-Linux installs, where nodes do not otherwise provide them. On by default on every platform: Talos ships five of the fourteen, and the four that overlap are rewritten with the same upstream version. On generic clusters this replaces same-named files in /opt/cni/bin every time the Multus pod is recreated, so a locally managed plugin there will not survive. Set networking.stageCniPlugins: false to decline.
```


BREAKING CHANGE: enabling stageCniPlugins replaces reference CNI plugins
in /opt/cni/bin on existing nodes, Talos included -- four of the names
it writes are ones the Talos node image also carries. Opt out before
upgrading: declining afterwards does not restore what was replaced.









<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Multus can now stage reference CNI plugins into `/opt/cni/bin`, adding
missing plugins and safely replacing matching versions.
- Added the `stageCniPlugins` setting, with platform-specific defaults
and explicit opt-out support.
  - Invalid configuration values now produce clear rendering errors.
  - Improved plugin installation diagnostics and failure reporting.

- **Documentation**
- Expanded guidance for CNI prerequisites, plugin staging behavior,
troubleshooting, rollout impact, and configuration.
  - Clarified Multus image reference handling in platform documentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/documentation Categorizes issue or PR as related to documentation size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants