Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a7b8dd1
wip
loktev-d Apr 6, 2026
4357da0
wip
loktev-d Apr 6, 2026
c6eea11
wip
loktev-d Apr 6, 2026
aeedb02
wip
loktev-d Apr 6, 2026
d16fed3
wip
loktev-d Apr 6, 2026
e737138
wip
loktev-d Apr 6, 2026
afc481d
wip
loktev-d Apr 6, 2026
1bc8b68
wip
loktev-d Apr 6, 2026
b16d905
wip
loktev-d Apr 7, 2026
93695f2
wip
loktev-d Apr 7, 2026
745bc07
wip
loktev-d Apr 8, 2026
713d5a9
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d Apr 8, 2026
6e43337
wip
loktev-d Apr 9, 2026
d86a51e
add doc
loktev-d Apr 16, 2026
b5007c9
add e2e
loktev-d Apr 16, 2026
02d5695
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d Apr 16, 2026
4ec3be4
fix linter errors
loktev-d Apr 16, 2026
8ff2633
fix linter errors
loktev-d Apr 16, 2026
228e43c
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d May 8, 2026
f990e89
add fromCacheVersion
loktev-d May 8, 2026
2d91ed3
refactor isOnlyNonMainNetworksChanged
loktev-d May 8, 2026
b9dc54a
add validation for adding non existing networks
loktev-d May 12, 2026
c67e3dd
fix main network removal
loktev-d May 12, 2026
0bb955f
add networks to rbac
loktev-d May 12, 2026
72fd956
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d May 19, 2026
5324297
add handling of non-ready networks
loktev-d May 19, 2026
6b51fd5
add indexer for networks
loktev-d May 19, 2026
252e0bf
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d May 19, 2026
973a798
wip
loktev-d May 19, 2026
124f8fc
update cache version
loktev-d May 20, 2026
b8757f1
update cache version
loktev-d May 20, 2026
ea18b8b
Merge branch 'main' into feat/network/dynamic-network-interfaces
loktev-d May 20, 2026
c2192bd
fix vmmac removal when unplugging
loktev-d May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/components/versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ firmware:
libvirt: v10.9.0
edk2: stable202411
core:
3p-kubevirt: v1.6.2-v12n.34
3p-kubevirt: feat/core/network-hotplug-support
3p-containerized-data-importer: v1.60.3-v12n.19
distribution: 2.8.3
package:
Expand Down
3 changes: 2 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3097,7 +3097,8 @@ If you specify the main network, it must be the first entry in the `.spec.networ
Important considerations when working with additional network interfaces:

- The order of listing networks in `.spec.networks` determines the order in which interfaces are connected inside the virtual machine.
- Adding or removing additional networks takes effect only after the VM is rebooted.
- Adding or removing an additional network (`Network` or `ClusterNetwork`) on a running VM is applied live without reboot. ACPI indexes of existing interfaces are preserved across add/remove cycles, so interface names in the guest OS stay stable.
- Adding or removing the main network (`type: Main`) still requires a VM reboot, because it is tied to the pod's primary network interface and cannot be reconfigured on a running pod.
- To preserve the order of network interfaces inside the guest operating system, it is recommended to add new networks to the end of the `.spec.networks` list (do not change the order of existing ones).
- Network security policies (NetworkPolicy) do not apply to additional network interfaces.
- Network parameters (IP addresses, gateways, DNS, etc.) for additional networks are configured manually from within the guest OS (for example, using Cloud-Init).
Expand Down
3 changes: 2 additions & 1 deletion docs/USER_GUIDE.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -3128,7 +3128,8 @@ EOF
Особенности и важные моменты работы с дополнительными сетевыми интерфейсами:

- порядок перечисления сетей в `.spec.networks` определяет порядок подключения интерфейсов внутри виртуальной машины;
- добавление или удаление дополнительных сетей вступает в силу только после перезагрузки ВМ;
- добавление или удаление дополнительной сети (`Network` или `ClusterNetwork`) на работающей ВМ применяется без перезагрузки. ACPI-индексы существующих интерфейсов сохраняются при добавлении/удалении, поэтому имена интерфейсов в гостевой ОС остаются стабильными;
- добавление или удаление основной сети (`type: Main`) по-прежнему требует перезагрузки ВМ, так как она связана с основным сетевым интерфейсом пода и не может быть изменена на работающем поде;
- чтобы сохранить порядок сетевых интерфейсов внутри гостевой операционной системы, рекомендуется добавлять новые сети в конец списка `.spec.networks` (не менять порядок уже существующих);
- политики сетевой безопасности (NetworkPolicy) не применяются к дополнительным сетевым интерфейсам;
- параметры сети (IP-адреса, шлюзы, DNS и т.д.) для дополнительных сетей настраиваются вручную изнутри гостевой ОС (например, с помощью Cloud-Init).
Expand Down
2 changes: 2 additions & 0 deletions images/virt-artifact/werf.inc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact
final: false
fromImage: builder/src
fromCacheVersion: "2026-05-20-1"
secrets:
- id: SOURCE_REPO
value: {{ $.SOURCE_REPO }}
Expand Down Expand Up @@ -44,6 +45,7 @@ packages:
image: {{ .ModuleNamePrefix }}{{ .ImageName }}
final: false
fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-alt-1.25" "builder/golang-alt-svace-1.25" }}
fromCacheVersion: "2026-05-20-1"
mount:
{{- include "mount points for golang builds" . }}
secrets:
Expand Down
84 changes: 84 additions & 0 deletions images/virtualization-artifact/pkg/common/network/readiness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package network

import (
"context"
"fmt"

k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/deckhouse/virtualization/api/core/v1alpha2"
)

var (
NetworkGVK = schema.GroupVersionKind{Group: "network.deckhouse.io", Version: "v1alpha1", Kind: "Network"}
ClusterNetworkGVK = schema.GroupVersionKind{Group: "network.deckhouse.io", Version: "v1alpha1", Kind: "ClusterNetwork"}
)

func SpecKey(ns v1alpha2.NetworksSpec) string {
return fmt.Sprintf("%s/%s", ns.Type, ns.Name)
}

func IsNetworkSpecReady(ctx context.Context, c client.Client, namespace string, ns v1alpha2.NetworksSpec) (bool, error) {
if ns.Type == v1alpha2.NetworksTypeMain {
return true, nil
}
obj := &unstructured.Unstructured{}
key := types.NamespacedName{Name: ns.Name}
switch ns.Type {
case v1alpha2.NetworksTypeClusterNetwork:
obj.SetGroupVersionKind(ClusterNetworkGVK)
case v1alpha2.NetworksTypeNetwork:
obj.SetGroupVersionKind(NetworkGVK)
key.Namespace = namespace
default:
return false, nil
}
if err := c.Get(ctx, key, obj); err != nil {
if k8serrors.IsNotFound(err) {
return false, nil
}
return false, err
}
return isReadyTrue(obj), nil
}

func isReadyTrue(obj *unstructured.Unstructured) bool {
conds, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
if err != nil || !found {
return false
}
for _, c := range conds {
condMap, ok := c.(map[string]any)
if !ok {
continue
}
typ, _, _ := unstructured.NestedString(condMap, "type")
if typ != "Ready" {
continue
}
status, _, _ := unstructured.NestedString(condMap, "status")
return status == string(metav1.ConditionTrue)
}
return false
}
8 changes: 8 additions & 0 deletions images/virtualization-artifact/pkg/common/network/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ func CreateNetworkSpec(vm *v1alpha2.VirtualMachine, vmmacs []*v1alpha2.VirtualMa
macPool := NewMacAddressPool(vm, vmmacs)
var specs InterfaceSpecList

if len(vm.Spec.Networks) == 0 {
specs = append(specs, createMainInterfaceSpec(v1alpha2.NetworksSpec{
Type: v1alpha2.NetworksTypeMain,
ID: ptr.To(ReservedMainID),
}))
return specs
}

for _, net := range vm.Spec.Networks {
if net.Type == v1alpha2.NetworksTypeMain {
specs = append(specs, createMainInterfaceSpec(net))
Expand Down
13 changes: 8 additions & 5 deletions images/virtualization-artifact/pkg/common/network/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,16 @@ func HasMainNetworkStatus(networks []v1alpha2.NetworksStatus) bool {
}

func HasMainNetworkSpec(networks []v1alpha2.NetworksSpec) bool {
for _, network := range networks {
if network.Type == v1alpha2.NetworksTypeMain {
return true
return GetMainNetworkSpec(networks) != nil
}

func GetMainNetworkSpec(networks []v1alpha2.NetworksSpec) *v1alpha2.NetworksSpec {
for i := range networks {
if networks[i].Type == v1alpha2.NetworksTypeMain {
return &networks[i]
}
}

return false
return nil
}

type InterfaceSpec struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"reflect"

"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiruntime "k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
virtv1 "kubevirt.io/api/core/v1"
Expand All @@ -31,11 +32,17 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"

"github.com/deckhouse/deckhouse/pkg/log"
commonnetwork "github.com/deckhouse/virtualization-controller/pkg/common/network"
"github.com/deckhouse/virtualization-controller/pkg/controller/indexer"
"github.com/deckhouse/virtualization/api/core/v1alpha2"
"github.com/deckhouse/virtualization/api/core/v1alpha3"
)

func registerNetworkGVKs(scheme *apiruntime.Scheme) {
scheme.AddKnownTypeWithName(commonnetwork.NetworkGVK, &unstructured.Unstructured{})
scheme.AddKnownTypeWithName(commonnetwork.ClusterNetworkGVK, &unstructured.Unstructured{})
}

func NewFakeClientWithObjects(objs ...client.Object) (client.WithWatch, error) {
scheme := apiruntime.NewScheme()
for _, f := range []func(*apiruntime.Scheme) error{
Expand All @@ -50,6 +57,7 @@ func NewFakeClientWithObjects(objs ...client.Object) (client.WithWatch, error) {
return nil, err
}
}
registerNetworkGVKs(scheme)
var newObjs []client.Object
for _, obj := range objs {
if reflect.ValueOf(obj).IsNil() {
Expand Down Expand Up @@ -79,6 +87,7 @@ func NewFakeClientWithInterceptorWithObjects(interceptor interceptor.Funcs, objs
return nil, err
}
}
registerNetworkGVKs(scheme)
var newObjs []client.Object
for _, obj := range objs {
if reflect.ValueOf(obj).IsNil() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ const (
)

const (
IndexFieldVMByClass = "spec.virtualMachineClassName"
IndexFieldVMByVD = "spec.blockDeviceRefs.VirtualDisk"
IndexFieldVMByVI = "spec.blockDeviceRefs.VirtualImage"
IndexFieldVMByCVI = "spec.blockDeviceRefs.ClusterVirtualImage"
IndexFieldVMByUSBDevice = "spec.usbDevices.name"
IndexFieldVMByNode = "status.node"
IndexFieldVMByClass = "spec.virtualMachineClassName"
IndexFieldVMByVD = "spec.blockDeviceRefs.VirtualDisk"
IndexFieldVMByVI = "spec.blockDeviceRefs.VirtualImage"
IndexFieldVMByCVI = "spec.blockDeviceRefs.ClusterVirtualImage"
IndexFieldVMByUSBDevice = "spec.usbDevices.name"
IndexFieldVMByNode = "status.node"
IndexFieldVMByNetwork = "spec.networks.Network.name"
IndexFieldVMByClusterNetwork = "spec.networks.ClusterNetwork.name"

IndexFieldVDByVDSnapshot = "vd,spec.DataSource.ObjectRef.Name,.Kind=VirtualDiskSnapshot"
IndexFieldVIByVDSnapshot = "vi,spec.DataSource.ObjectRef.Name,.Kind=VirtualDiskSnapshot"
Expand Down Expand Up @@ -84,6 +86,8 @@ var IndexGetters = []IndexGetter{
IndexVMByVI,
IndexVMByCVI,
IndexVMByNode,
IndexVMByNetwork,
IndexVMByClusterNetwork,
IndexVMByProvisioningSecret,
IndexVMSnapshotByVM,
IndexVMSnapshotByVDSnapshot,
Expand Down Expand Up @@ -173,6 +177,32 @@ func IndexVMByNode() (obj client.Object, field string, extractValue client.Index
}
}

func IndexVMByNetwork() (obj client.Object, field string, extractValue client.IndexerFunc) {
return &v1alpha2.VirtualMachine{}, IndexFieldVMByNetwork, func(object client.Object) []string {
return getNetworkNamesByType(object, v1alpha2.NetworksTypeNetwork)
}
}

func IndexVMByClusterNetwork() (obj client.Object, field string, extractValue client.IndexerFunc) {
return &v1alpha2.VirtualMachine{}, IndexFieldVMByClusterNetwork, func(object client.Object) []string {
return getNetworkNamesByType(object, v1alpha2.NetworksTypeClusterNetwork)
}
}

func getNetworkNamesByType(obj client.Object, typ string) []string {
vm, ok := obj.(*v1alpha2.VirtualMachine)
if !ok || vm == nil {
return nil
}
var names []string
for _, n := range vm.Spec.Networks {
if n.Type == typ && n.Name != "" {
names = append(names, n.Name)
}
}
return names
}

func IndexVMByProvisioningSecret() (obj client.Object, field string, extractValue client.IndexerFunc) {
return &v1alpha2.VirtualMachine{}, IndexFieldVMByProvisioningSecret, func(object client.Object) []string {
vm, ok := object.(*v1alpha2.VirtualMachine)
Expand Down
40 changes: 35 additions & 5 deletions images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,35 @@ func (b *KVVM) ClearNetworkInterfaces() {
b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = nil
}

func (b *KVVM) SetNetworkInterfaceAbsent(name string) {
for i, iface := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces {
if iface.Name == name {
b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces[i].State = virtv1.InterfaceStateAbsent
return
}
}
}

func (b *KVVM) RemoveNetworkInterface(name string) {
ifaces := b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces
filtered := ifaces[:0]
for _, iface := range ifaces {
if iface.Name != name {
filtered = append(filtered, iface)
}
}
b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = filtered

nets := b.Resource.Spec.Template.Spec.Networks
filteredNets := nets[:0]
for _, n := range nets {
if n.Name != name {
filteredNets = append(filteredNets, n)
}
}
b.Resource.Spec.Template.Spec.Networks = filteredNets
}

func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) {
net := virtv1.Network{
Name: name,
Expand All @@ -796,15 +825,16 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) {
iface.MacAddress = macAddress
}

ifaceExists := false
for _, i := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces {
if i.Name == name {
ifaceExists = true
updated := false
for i, existing := range b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces {
if existing.Name == name {
b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces[i] = iface
updated = true
break
}
}

if !ifaceExists {
if !updated {
b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = append(b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces, iface)
}
}
Expand Down
Loading
Loading