From 33dc63cd6d69e36733c8552d2b04046f846ef7a2 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Tue, 17 Feb 2026 15:17:01 +0300 Subject: [PATCH 01/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/kvbuilder/kvvm.go | 7 ++++--- .../pkg/controller/kvbuilder/kvvm_utils.go | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go index 606fae973b..7084be8e20 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm.go @@ -554,7 +554,7 @@ func (b *KVVM) ClearNetworkInterfaces() { b.Resource.Spec.Template.Spec.Domain.Devices.Interfaces = nil } -func (b *KVVM) SetNetworkInterface(name, macAddress string) { +func (b *KVVM) SetNetworkInterface(name, macAddress string, acpiIndex int) { net := virtv1.Network{ Name: name, NetworkSource: virtv1.NetworkSource{ @@ -571,8 +571,9 @@ func (b *KVVM) SetNetworkInterface(name, macAddress string) { devPreset := DeviceOptionsPresets.Find(b.opts.EnableParavirtualization) iface := virtv1.Interface{ - Name: name, - Model: devPreset.InterfaceModel, + Name: name, + Model: devPreset.InterfaceModel, + ACPIIndex: acpiIndex, } iface.InterfaceBindingMethod.Bridge = &virtv1.InterfaceBridge{} if macAddress != "" { diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 284c21eaa1..2f758d4fd0 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -356,7 +356,10 @@ func ApplyMigrationVolumes(kvvm *KVVM, vm *v1alpha2.VirtualMachine, vdsByName ma func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { kvvm.ClearNetworkInterfaces() for _, n := range networkSpec { - kvvm.SetNetworkInterface(n.InterfaceName, n.MAC) + if n.Type == v1alpha2.NetworksTypeMain { + kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, 100) + } + kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, 101) } } From 90acbe901b728a35d7889e27339d4b4a089219b4 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 10:52:49 +0300 Subject: [PATCH 02/19] ++ Signed-off-by: Dmitry Lopatin --- api/core/v1alpha2/virtual_machine.go | 2 ++ crds/doc-ru-virtualmachines.yaml | 6 ++++++ crds/virtualmachines.yaml | 8 ++++++++ 3 files changed, 16 insertions(+) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index b7895501f5..beb06a192b 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -262,6 +262,7 @@ const ( ) type NetworksSpec struct { + Id int `json:"id"` Type string `json:"type"` Name string `json:"name,omitempty"` VirtualMachineMACAddressName string `json:"virtualMachineMACAddressName,omitempty"` @@ -423,6 +424,7 @@ type Versions struct { } type NetworksStatus struct { + Id int `json:"id"` Type string `json:"type"` Name string `json:"name,omitempty"` MAC string `json:"macAddress,omitempty"` diff --git a/crds/doc-ru-virtualmachines.yaml b/crds/doc-ru-virtualmachines.yaml index 1979a58ba6..b34d2eade0 100644 --- a/crds/doc-ru-virtualmachines.yaml +++ b/crds/doc-ru-virtualmachines.yaml @@ -550,6 +550,9 @@ spec: Список конфигураций сетевых интерфейсов. items: properties: + id: + description: | + ID сетевого интерфейса. type: description: | Тип сетевого интерфейса. @@ -743,6 +746,9 @@ spec: Список сетевых интерфейсов, подключенных к ВМ. items: properties: + id: + description: | + ID сетевого интерфейса. type: description: | Тип сетевого интерфейса. diff --git a/crds/virtualmachines.yaml b/crds/virtualmachines.yaml index c8234cf73f..82a4f44102 100644 --- a/crds/virtualmachines.yaml +++ b/crds/virtualmachines.yaml @@ -981,6 +981,10 @@ spec: required: - type properties: + id: + type: integer + description: | + Network interface ID. type: type: string description: | @@ -1333,6 +1337,10 @@ spec: required: - type properties: + id: + type: integer + description: | + Network interface ID. type: type: string description: | From b53d373785a3ffa3fe812ff41488aff8bc3e4f30 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 11:12:33 +0300 Subject: [PATCH 03/19] ++ Signed-off-by: Dmitry Lopatin --- .../vm/internal/validators/networks_validator_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go index 9a66e5c0fa..72f7bbc3f8 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go @@ -62,10 +62,10 @@ func TestNetworksValidateCreate(t *testing.T) { _, err := networkValidator.ValidateCreate(t.Context(), vm) if test.valid && err != nil { - t.Errorf("Validation failed for spec %s: expected valid, but got an error: %v", test.networks, err) + t.Errorf("Validation failed for spec %v: expected valid, but got an error: %v", test.networks, err) } if !test.valid && err == nil { - t.Errorf("Validation succeeded for spec %s: expected error, but got none", test.networks) + t.Errorf("Validation succeeded for spec %v: expected error, but got none", test.networks) } }) } From a1390c27b3c2a4cf9001a8e071f99b39bfcadbc8 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 11:31:13 +0300 Subject: [PATCH 04/19] ++ Signed-off-by: Dmitry Lopatin --- api/core/v1alpha2/virtual_machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index beb06a192b..78e04bc4b5 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -262,7 +262,7 @@ const ( ) type NetworksSpec struct { - Id int `json:"id"` + Id int `json:"id,omitempty"` Type string `json:"type"` Name string `json:"name,omitempty"` VirtualMachineMACAddressName string `json:"virtualMachineMACAddressName,omitempty"` From 6e25b4aa88caca31acfa28404ed4050fa7b8827b Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 11:52:18 +0300 Subject: [PATCH 05/19] ++ Signed-off-by: Dmitry Lopatin --- api/core/v1alpha2/virtual_machine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/v1alpha2/virtual_machine.go b/api/core/v1alpha2/virtual_machine.go index 78e04bc4b5..6ae2338e5a 100644 --- a/api/core/v1alpha2/virtual_machine.go +++ b/api/core/v1alpha2/virtual_machine.go @@ -424,7 +424,7 @@ type Versions struct { } type NetworksStatus struct { - Id int `json:"id"` + Id int `json:"id,omitempty"` Type string `json:"type"` Name string `json:"name,omitempty"` MAC string `json:"macAddress,omitempty"` From a2d67c1e0906cfece52f2cd2f90de83093189956 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 17:22:37 +0300 Subject: [PATCH 06/19] ++ Signed-off-by: Dmitry Lopatin --- .../internal/validators/networks_validator.go | 62 ++++++++++ .../validators/networks_validator_test.go | 111 ++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go index b41451f1b1..0469143e65 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go @@ -28,6 +28,10 @@ import ( "github.com/deckhouse/virtualization/api/core/v1alpha2" ) +const ( + maxNetworkId = 16*1024 - 1 // 16383 +) + type NetworksValidator struct { featureGate featuregate.FeatureGate } @@ -61,6 +65,10 @@ func (v *NetworksValidator) ValidateUpdate(_ context.Context, oldVM, newVM *v1al return nil, fmt.Errorf("network configuration requires SDN to be enabled") } + if err := v.validateNetworkIdsUnchanged(oldVM.Spec.Networks, newNetworksSpec); err != nil { + return nil, err + } + isChanged := !equality.Semantic.DeepEqual(newNetworksSpec, oldVM.Spec.Networks) if isChanged { return v.validateNetworksSpec(newNetworksSpec) @@ -85,6 +93,10 @@ func (v *NetworksValidator) validateNetworksSpec(networksSpec []v1alpha2.Network if err := v.validateNetworkUniqueness(typ, name, namesSet); err != nil { return nil, err } + + if err := v.validateNetworkId(network); err != nil { + return nil, err + } } return nil, nil @@ -116,3 +128,53 @@ func (v *NetworksValidator) validateNetworkUniqueness(networkType, networkName s namesSet[key] = struct{}{} return nil } + +func (v *NetworksValidator) validateNetworkIdsUnchanged(oldNetworksSpec, newNetworksSpec []v1alpha2.NetworksSpec) error { + oldNetworksMap := v.buildNetworksMap(oldNetworksSpec) + newNetworksMap := v.buildNetworksMap(newNetworksSpec) + + for key, oldNetwork := range oldNetworksMap { + newNetwork, exists := newNetworksMap[key] + if !exists { + continue + } + + if oldNetwork.Id == newNetwork.Id { + continue + } + + networkIdentifier := v.getNetworkIdentifier(oldNetwork) + return fmt.Errorf("network id cannot be changed for network %s", networkIdentifier) + } + + return nil +} + +func (v *NetworksValidator) buildNetworksMap(networksSpec []v1alpha2.NetworksSpec) map[string]v1alpha2.NetworksSpec { + networksMap := make(map[string]v1alpha2.NetworksSpec) + for _, network := range networksSpec { + key := v.getNetworkIdentifier(network) + networksMap[key] = network + } + return networksMap +} + +func (v *NetworksValidator) validateNetworkId(network v1alpha2.NetworksSpec) error { + if network.Id == 0 { + return nil + } + + if network.Id < 1 || network.Id > maxNetworkId { + networkIdentifier := v.getNetworkIdentifier(network) + return fmt.Errorf("network id must be between 1 and %d for network %s, got %d", maxNetworkId, networkIdentifier, network.Id) + } + + return nil +} + +func (v *NetworksValidator) getNetworkIdentifier(network v1alpha2.NetworksSpec) string { + if network.Type == v1alpha2.NetworksTypeMain { + return network.Type + } + return fmt.Sprintf("%s/%s", network.Type, network.Name) +} diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go index 72f7bbc3f8..33d98a9a6d 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go @@ -47,6 +47,19 @@ func TestNetworksValidateCreate(t *testing.T) { {[]v1alpha2.NetworksSpec{mainNetwork, networkTest, networkTest}, true, false}, {[]v1alpha2.NetworksSpec{mainNetwork, {Type: v1alpha2.NetworksTypeNetwork}}, true, false}, {[]v1alpha2.NetworksSpec{mainNetwork}, false, false}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeMain, Id: 1}}, true, true}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 2}}, true, true}, + {[]v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 1}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test1", Id: 1}, + {Type: v1alpha2.NetworksTypeClusterNetwork, Name: "test2", Id: 2}, + }, true, true}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 16383}}, true, true}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 0}}, true, true}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 16384}}, true, false}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: -1}}, true, false}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeMain, Id: 16383}}, true, true}, + {[]v1alpha2.NetworksSpec{{Type: v1alpha2.NetworksTypeMain, Id: 16384}}, true, false}, } for i, test := range tests { @@ -160,6 +173,104 @@ func TestNetworksValidateUpdate(t *testing.T) { sdnEnabled: true, valid: true, }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 1}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 2}, + }, + sdnEnabled: true, + valid: false, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 1}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 2}, + }, + sdnEnabled: true, + valid: false, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeClusterNetwork, Name: "cluster", Id: 5}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeClusterNetwork, Name: "cluster", Id: 10}, + }, + sdnEnabled: true, + valid: false, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 1}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 2}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 1}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 2}, + }, + sdnEnabled: true, + valid: true, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test1", Id: 1}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test2", Id: 2}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test1", Id: 1}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test2", Id: 3}, + }, + sdnEnabled: true, + valid: false, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "new", Id: 5}, + }, + sdnEnabled: true, + valid: true, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 1}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeMain, Id: 0}, + }, + sdnEnabled: true, + valid: true, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 0}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 1}, + }, + sdnEnabled: true, + valid: false, + }, + { + oldNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 1}, + }, + newNetworksSpec: []v1alpha2.NetworksSpec{ + {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 0}, + }, + sdnEnabled: true, + valid: false, + }, } for i, test := range tests { From 38f2cb72eff8f268c38bd6dd647ec91d0df9874b Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 17:50:58 +0300 Subject: [PATCH 07/19] ++ Signed-off-by: Dmitry Lopatin --- .../vm/internal/validators/networks_validator.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go index 0469143e65..51fd361f93 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go @@ -29,7 +29,7 @@ import ( ) const ( - maxNetworkId = 16*1024 - 1 // 16383 + maxNetworkID = 16*1024 - 1 // 16383 ) type NetworksValidator struct { @@ -65,7 +65,7 @@ func (v *NetworksValidator) ValidateUpdate(_ context.Context, oldVM, newVM *v1al return nil, fmt.Errorf("network configuration requires SDN to be enabled") } - if err := v.validateNetworkIdsUnchanged(oldVM.Spec.Networks, newNetworksSpec); err != nil { + if err := v.validateNetworkIDsUnchanged(oldVM.Spec.Networks, newNetworksSpec); err != nil { return nil, err } @@ -94,7 +94,7 @@ func (v *NetworksValidator) validateNetworksSpec(networksSpec []v1alpha2.Network return nil, err } - if err := v.validateNetworkId(network); err != nil { + if err := v.validateNetworkID(network); err != nil { return nil, err } } @@ -129,7 +129,7 @@ func (v *NetworksValidator) validateNetworkUniqueness(networkType, networkName s return nil } -func (v *NetworksValidator) validateNetworkIdsUnchanged(oldNetworksSpec, newNetworksSpec []v1alpha2.NetworksSpec) error { +func (v *NetworksValidator) validateNetworkIDsUnchanged(oldNetworksSpec, newNetworksSpec []v1alpha2.NetworksSpec) error { oldNetworksMap := v.buildNetworksMap(oldNetworksSpec) newNetworksMap := v.buildNetworksMap(newNetworksSpec) @@ -159,14 +159,14 @@ func (v *NetworksValidator) buildNetworksMap(networksSpec []v1alpha2.NetworksSpe return networksMap } -func (v *NetworksValidator) validateNetworkId(network v1alpha2.NetworksSpec) error { +func (v *NetworksValidator) validateNetworkID(network v1alpha2.NetworksSpec) error { if network.Id == 0 { return nil } - if network.Id < 1 || network.Id > maxNetworkId { + if network.Id < 1 || network.Id > maxNetworkID { networkIdentifier := v.getNetworkIdentifier(network) - return fmt.Errorf("network id must be between 1 and %d for network %s, got %d", maxNetworkId, networkIdentifier, network.Id) + return fmt.Errorf("network id must be between 1 and %d for network %s, got %d", maxNetworkID, networkIdentifier, network.Id) } return nil From 9684864f00d1e4ab1521857abfe99f1ed3a6d9a2 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 18:39:50 +0300 Subject: [PATCH 08/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/common/network/network.go | 3 + .../pkg/common/network/network_test.go | 158 ++++++++++++++++++ .../pkg/controller/kvbuilder/kvvm_utils.go | 6 +- .../pkg/controller/vm/internal/sync_kvvm.go | 7 +- 4 files changed, 163 insertions(+), 11 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/network/network.go b/images/virtualization-artifact/pkg/common/network/network.go index ba92f1e812..47075655ed 100644 --- a/images/virtualization-artifact/pkg/common/network/network.go +++ b/images/virtualization-artifact/pkg/common/network/network.go @@ -50,6 +50,7 @@ func HasMainNetworkSpec(networks []v1alpha2.NetworksSpec) bool { } type InterfaceSpec struct { + ID int `json:"id"` Type string `json:"type"` Name string `json:"name"` InterfaceName string `json:"ifName"` @@ -96,6 +97,7 @@ func CreateNetworkSpec(vm *v1alpha2.VirtualMachine, vmmacs []*v1alpha2.VirtualMa for _, n := range vm.Spec.Networks { if n.Type == v1alpha2.NetworksTypeMain { res = append(res, InterfaceSpec{ + ID: n.Id, Type: n.Type, Name: n.Name, InterfaceName: NameDefaultInterface, @@ -117,6 +119,7 @@ func CreateNetworkSpec(vm *v1alpha2.VirtualMachine, vmmacs []*v1alpha2.VirtualMa } if mac != "" { res = append(res, InterfaceSpec{ + ID: n.Id, Type: n.Type, Name: n.Name, InterfaceName: generateInterfaceName(mac, n.Type), diff --git a/images/virtualization-artifact/pkg/common/network/network_test.go b/images/virtualization-artifact/pkg/common/network/network_test.go index 7643aa0ed3..18dea7a925 100644 --- a/images/virtualization-artifact/pkg/common/network/network_test.go +++ b/images/virtualization-artifact/pkg/common/network/network_test.go @@ -78,6 +78,7 @@ var _ = Describe("Network Config Generation", func() { Expect(configs[0].Name).To(Equal("")) Expect(configs[0].InterfaceName).To(HavePrefix("default")) Expect(configs[0].MAC).To(HavePrefix("")) + Expect(configs[0].ID).To(Equal(0)) }) It("should generate correct interface name for Network type", func() { @@ -97,10 +98,12 @@ var _ = Describe("Network Config Generation", func() { Expect(configs[0].Name).To(Equal("")) Expect(configs[0].InterfaceName).To(HavePrefix("default")) Expect(configs[0].MAC).To(HavePrefix("")) + Expect(configs[0].ID).To(Equal(0)) Expect(configs[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) Expect(configs[1].Name).To(Equal("mynet")) Expect(configs[1].InterfaceName).To(HavePrefix("veth_n")) + Expect(configs[1].ID).To(Equal(0)) }) It("should generate correct interface name for ClusterNetwork type", func() { @@ -281,4 +284,159 @@ var _ = Describe("Network Config Generation", func() { Expect(configs[3].Name).To(Equal("name1")) Expect(configs[3].MAC).To(Equal("00:1A:2B:3C:4D:6A")) }) + + It("should preserve id from spec for Main network", func() { + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(1)) + Expect(configs[0].ID).To(Equal(1)) + }) + + It("should preserve id from spec for Main network", func() { + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(1)) + Expect(configs[0].ID).To(Equal(1)) + }) + + It("should preserve id from spec for Network type with MAC", func() { + vm.Status.Networks = []v1alpha2.NetworksStatus{ + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "mynet", + MAC: "00:1A:2B:3C:4D:5E", + }, + } + vmmac1 := newMACAddress("mac1", "00:1A:2B:3C:4D:5E", v1alpha2.VirtualMachineMACAddressPhaseAttached, "vm1") + vmmacs = []*v1alpha2.VirtualMachineMACAddress{vmmac1} + + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "mynet", + Id: 5, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(2)) + Expect(configs[0].ID).To(Equal(0)) + Expect(configs[1].ID).To(Equal(5)) + }) + + It("should preserve id from spec for ClusterNetwork type with MAC", func() { + vm.Status.Networks = []v1alpha2.NetworksStatus{ + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "clusternet", + MAC: "00:1A:2B:3C:4D:5E", + }, + } + vmmac1 := newMACAddress("mac1", "00:1A:2B:3C:4D:5E", v1alpha2.VirtualMachineMACAddressPhaseAttached, "vm1") + vmmacs = []*v1alpha2.VirtualMachineMACAddress{vmmac1} + + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 10, + }, + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "clusternet", + Id: 20, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(2)) + Expect(configs[0].ID).To(Equal(10)) + Expect(configs[1].ID).To(Equal(20)) + }) + + It("should preserve different ids for multiple networks with MACs", func() { + vm.Status.Networks = []v1alpha2.NetworksStatus{ + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "net1", + MAC: "00:1A:2B:3C:4D:5E", + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "net2", + MAC: "00:1A:2B:3C:4D:5F", + }, + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "cluster1", + MAC: "00:1A:2B:3C:4D:6A", + }, + } + vmmac1 := newMACAddress("mac1", "00:1A:2B:3C:4D:5E", v1alpha2.VirtualMachineMACAddressPhaseAttached, "vm1") + vmmac2 := newMACAddress("mac2", "00:1A:2B:3C:4D:5F", v1alpha2.VirtualMachineMACAddressPhaseAttached, "vm1") + vmmac3 := newMACAddress("mac3", "00:1A:2B:3C:4D:6A", v1alpha2.VirtualMachineMACAddressPhaseAttached, "vm1") + vmmacs = []*v1alpha2.VirtualMachineMACAddress{vmmac1, vmmac2, vmmac3} + + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "net1", + Id: 2, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "net2", + Id: 3, + }, + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "cluster1", + Id: 4, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(4)) + Expect(configs[0].ID).To(Equal(1)) + Expect(configs[1].ID).To(Equal(2)) + Expect(configs[2].ID).To(Equal(3)) + Expect(configs[3].ID).To(Equal(4)) + }) + + It("should set id to zero when not specified", func() { + vm.Spec.Networks = []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + }, + } + + configs := CreateNetworkSpec(vm, vmmacs) + + Expect(configs).To(HaveLen(1)) + Expect(configs[0].ID).To(Equal(0)) + }) }) diff --git a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go index 2f758d4fd0..6d963e0fea 100644 --- a/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go +++ b/images/virtualization-artifact/pkg/controller/kvbuilder/kvvm_utils.go @@ -93,7 +93,6 @@ func ApplyVirtualMachineSpec( vdByName map[string]*v1alpha2.VirtualDisk, viByName map[string]*v1alpha2.VirtualImage, cviByName map[string]*v1alpha2.ClusterVirtualImage, - vmbdas map[v1alpha2.VMBDAObjectRef][]*v1alpha2.VirtualMachineBlockDeviceAttachment, class *v1alpha2.VirtualMachineClass, ipAddress string, networkSpec network.InterfaceSpecList, @@ -356,10 +355,7 @@ func ApplyMigrationVolumes(kvvm *KVVM, vm *v1alpha2.VirtualMachine, vdsByName ma func setNetwork(kvvm *KVVM, networkSpec network.InterfaceSpecList) { kvvm.ClearNetworkInterfaces() for _, n := range networkSpec { - if n.Type == v1alpha2.NetworksTypeMain { - kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, 100) - } - kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, 101) + kvvm.SetNetworkInterface(n.InterfaceName, n.MAC, n.ID) } } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go index 370ee737a6..530551a938 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/sync_kvvm.go @@ -434,13 +434,8 @@ func MakeKVVMFromVMSpec(ctx context.Context, s state.VirtualMachineState) (*virt networkSpec := network.CreateNetworkSpec(current, vmmacs) - vmbdas, err := s.VirtualMachineBlockDeviceAttachments(ctx) - if err != nil { - return nil, fmt.Errorf("get vmbdas: %w", err) - } - // Create kubevirt VirtualMachine resource from d8 VirtualMachine spec. - err = kvbuilder.ApplyVirtualMachineSpec(kvvmBuilder, current, bdState.VDByName, bdState.VIByName, bdState.CVIByName, vmbdas, class, ipAddress, networkSpec) + err = kvbuilder.ApplyVirtualMachineSpec(kvvmBuilder, current, bdState.VDByName, bdState.VIByName, bdState.CVIByName, class, ipAddress, networkSpec) if err != nil { return nil, err } From 5face0a41e3bb6e949e86909b906d58644f9ee04 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 19:20:08 +0300 Subject: [PATCH 09/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 32 ++ .../controller/vm/internal/network_test.go | 294 ++++++++++++++++++ .../internal/validators/networks_validator.go | 5 + .../validators/networks_validator_test.go | 2 +- .../pkg/controller/vm/vm_reconciler.go | 20 +- 5 files changed, 351 insertions(+), 2 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index 51da70ea0c..ec6918fcf5 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -95,9 +95,41 @@ func (h *NetworkInterfaceHandler) Handle(ctx context.Context, s state.VirtualMac } } + h.lazyInitialization(vm) return h.UpdateNetworkStatus(ctx, s, vm) } +func (h *NetworkInterfaceHandler) lazyInitialization(vm *v1alpha2.VirtualMachine) { + // First pass: assign id=1 to Main network if it has id=0 + for i := range vm.Spec.Networks { + if vm.Spec.Networks[i].Type == v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { + vm.Spec.Networks[i].Id = 1 + } + } + + // Second pass: assign sequential ids starting from 2 to other networks with id=0 + nextID := 2 + for i := range vm.Spec.Networks { + if vm.Spec.Networks[i].Type != v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { + vm.Spec.Networks[i].Id = nextID + nextID++ + // Ensure we never use id=1 (reserved for Main) + if nextID == 1 { + nextID = 2 + } + } else if vm.Spec.Networks[i].Id > 0 { + // Track the highest ID used to avoid conflicts + if vm.Spec.Networks[i].Id >= nextID { + nextID = vm.Spec.Networks[i].Id + 1 + // Ensure we never use id=1 (reserved for Main) + if nextID == 1 { + nextID = 2 + } + } + } + } +} + func hasOnlyDefaultNetwork(vm *v1alpha2.VirtualMachine) bool { nets := vm.Spec.Networks return len(nets) == 0 || (len(nets) == 1 && nets[0].Type == v1alpha2.NetworksTypeMain) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/network_test.go index 0ce49c45f7..d8e0696ce6 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network_test.go @@ -292,4 +292,298 @@ var _ = Describe("NetworkInterfaceHandler", func() { }) }) }) + + Describe("Lazy initialization of network IDs", func() { + It("should assign id=1 to Main network when id=0", func() { + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 0, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(1)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + }) + + It("should not change Main network id when it is already set to 1", func() { + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(1)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + }) + + It("should assign sequential ids starting from 2 to networks with id=0", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + mac2 := newMACAddress("test-mac-address2", "aa:bb:cc:dd:ee:00", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 0, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-1", + Id: 0, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-2", + Id: 0, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1, mac2) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(3)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[1].Name).To(Equal("test-network-1")) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(2)) + Expect(changedVM.Spec.Networks[2].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[2].Name).To(Equal("test-network-2")) + Expect(changedVM.Spec.Networks[2].Id).To(Equal(3)) + }) + + It("should not change network id when it is already set", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network", + Id: 5, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(2)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(5)) + }) + + It("should assign sequential ids considering already set ids", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + mac2 := newMACAddress("test-mac-address2", "aa:bb:cc:dd:ee:00", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 0, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-1", + Id: 5, // Already set + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-2", + Id: 0, // Should get next available id after 5 + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1, mac2) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(3)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[1].Name).To(Equal("test-network-1")) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(5)) + Expect(changedVM.Spec.Networks[2].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[2].Name).To(Equal("test-network-2")) + Expect(changedVM.Spec.Networks[2].Id).To(Equal(6)) + }) + + It("should handle ClusterNetwork type correctly", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 0, + }, + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "test-cluster-network", + Id: 0, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(2)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeClusterNetwork)) + Expect(changedVM.Spec.Networks[1].Name).To(Equal("test-cluster-network")) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(2)) + }) + + It("should skip id=1 when assigning to non-Main networks", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeMain, + Id: 1, // Already set to 1 + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network", + Id: 0, // Should get 2, not 1 + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(2)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeMain)) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(1)) + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(2)) + }) + + It("should assign sequential ids starting from 2 when there is no Main network", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + mac2 := newMACAddress("test-mac-address2", "aa:bb:cc:dd:ee:00", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-1", + Id: 0, + }, + { + Type: v1alpha2.NetworksTypeNetwork, + Name: "test-network-2", + Id: 0, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1, mac2) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(2)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[0].Name).To(Equal("test-network-1")) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(2)) // Starts from 2, skipping 1 + Expect(changedVM.Spec.Networks[1].Type).To(Equal(v1alpha2.NetworksTypeNetwork)) + Expect(changedVM.Spec.Networks[1].Name).To(Equal("test-network-2")) + Expect(changedVM.Spec.Networks[1].Id).To(Equal(3)) + }) + + It("should handle only ClusterNetwork without Main network", func() { + mac1 := newMACAddress("test-mac-address1", "aa:bb:cc:dd:ee:ff", v1alpha2.VirtualMachineMACAddressPhaseAttached, name) + networkSpec := []v1alpha2.NetworksSpec{ + { + Type: v1alpha2.NetworksTypeClusterNetwork, + Name: "test-cluster-network", + Id: 0, + }, + } + vm.Spec.Networks = networkSpec + fakeClient, resource, vmState = setupEnvironment(vm, vmPod, mac1) + + gate, _, setFromMap, err := featuregates.New() + Expect(err).NotTo(HaveOccurred()) + Expect(setFromMap(map[string]bool{string(featuregates.SDN): true})).To(Succeed()) + + h := NewNetworkInterfaceHandler(gate) + _, err = h.Handle(ctx, vmState) + Expect(err).NotTo(HaveOccurred()) + + changedVM := vmState.VirtualMachine().Changed() + Expect(changedVM.Spec.Networks).To(HaveLen(1)) + Expect(changedVM.Spec.Networks[0].Type).To(Equal(v1alpha2.NetworksTypeClusterNetwork)) + Expect(changedVM.Spec.Networks[0].Name).To(Equal("test-cluster-network")) + Expect(changedVM.Spec.Networks[0].Id).To(Equal(2)) // Starts from 2, skipping 1 + }) + }) }) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go index 51fd361f93..1f36a0dfac 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go @@ -143,6 +143,11 @@ func (v *NetworksValidator) validateNetworkIDsUnchanged(oldNetworksSpec, newNetw continue } + // Allow changing id from 0 to a valid value (lazy initialization) + if oldNetwork.Id == 0 && newNetwork.Id > 0 && newNetwork.Id <= maxNetworkID { + continue + } + networkIdentifier := v.getNetworkIdentifier(oldNetwork) return fmt.Errorf("network id cannot be changed for network %s", networkIdentifier) } diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go index 33d98a9a6d..dbf15f4096 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator_test.go @@ -259,7 +259,7 @@ func TestNetworksValidateUpdate(t *testing.T) { {Type: v1alpha2.NetworksTypeNetwork, Name: "test", Id: 1}, }, sdnEnabled: true, - valid: false, + valid: true, }, { oldNetworksSpec: []v1alpha2.NetworksSpec{ diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index ba45da80bd..b08fbe7aca 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -106,7 +106,25 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return h.Handle(ctx, s) }) rec.SetResourceUpdater(func(ctx context.Context) error { - return vm.Update(ctx) + var specToUpdate *v1alpha2.VirtualMachineSpec + if !reflect.DeepEqual(vm.Current().Spec, vm.Changed().Spec) { + specToUpdate = vm.Changed().Spec.DeepCopy() + } + + err := vm.Update(ctx) + if err != nil { + return fmt.Errorf("update status: %w", err) + } + + if specToUpdate != nil { + vm.Changed().Spec = *specToUpdate + err = r.client.Update(ctx, vm.Changed()) + if err != nil { + return fmt.Errorf("update spec: %w", err) + } + } + + return nil }) return rec.Reconcile(ctx) From 69fbe7b39a2c19df2951dfbd594c2ba5b9c360ea Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 21:18:13 +0300 Subject: [PATCH 10/19] ++ Signed-off-by: Dmitry Lopatin --- .../virtualization-artifact/pkg/common/network/network_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/common/network/network_test.go b/images/virtualization-artifact/pkg/common/network/network_test.go index 18dea7a925..7c13c83e01 100644 --- a/images/virtualization-artifact/pkg/common/network/network_test.go +++ b/images/virtualization-artifact/pkg/common/network/network_test.go @@ -339,7 +339,7 @@ var _ = Describe("Network Config Generation", func() { configs := CreateNetworkSpec(vm, vmmacs) Expect(configs).To(HaveLen(2)) - Expect(configs[0].ID).To(Equal(0)) + Expect(configs[0].ID).To(Equal(1)) Expect(configs[1].ID).To(Equal(5)) }) From 7371b165194253e400e21a5cdca3ce5617bc6449 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 21:20:54 +0300 Subject: [PATCH 11/19] ++ Signed-off-by: Dmitry Lopatin --- .../virtualization-artifact/pkg/common/network/network_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/common/network/network_test.go b/images/virtualization-artifact/pkg/common/network/network_test.go index 7c13c83e01..1a815b528f 100644 --- a/images/virtualization-artifact/pkg/common/network/network_test.go +++ b/images/virtualization-artifact/pkg/common/network/network_test.go @@ -357,7 +357,7 @@ var _ = Describe("Network Config Generation", func() { vm.Spec.Networks = []v1alpha2.NetworksSpec{ { Type: v1alpha2.NetworksTypeMain, - Id: 10, + Id: 1, }, { Type: v1alpha2.NetworksTypeClusterNetwork, From 29b3646425027f0aedf2e7cacad6297629e957d4 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 21:26:25 +0300 Subject: [PATCH 12/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index ec6918fcf5..beb711acf7 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -100,33 +100,8 @@ func (h *NetworkInterfaceHandler) Handle(ctx context.Context, s state.VirtualMac } func (h *NetworkInterfaceHandler) lazyInitialization(vm *v1alpha2.VirtualMachine) { - // First pass: assign id=1 to Main network if it has id=0 for i := range vm.Spec.Networks { - if vm.Spec.Networks[i].Type == v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { - vm.Spec.Networks[i].Id = 1 - } - } - - // Second pass: assign sequential ids starting from 2 to other networks with id=0 - nextID := 2 - for i := range vm.Spec.Networks { - if vm.Spec.Networks[i].Type != v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { - vm.Spec.Networks[i].Id = nextID - nextID++ - // Ensure we never use id=1 (reserved for Main) - if nextID == 1 { - nextID = 2 - } - } else if vm.Spec.Networks[i].Id > 0 { - // Track the highest ID used to avoid conflicts - if vm.Spec.Networks[i].Id >= nextID { - nextID = vm.Spec.Networks[i].Id + 1 - // Ensure we never use id=1 (reserved for Main) - if nextID == 1 { - nextID = 2 - } - } - } + vm.Spec.Networks[i].Id = 100 + i } } From de23b4375dd5fa586e1d1a0e6a4c04b4ecacda46 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 21:40:57 +0300 Subject: [PATCH 13/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 2 +- .../pkg/controller/vm/vm_reconciler.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index beb711acf7..62cf0acd45 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -101,7 +101,7 @@ func (h *NetworkInterfaceHandler) Handle(ctx context.Context, s state.VirtualMac func (h *NetworkInterfaceHandler) lazyInitialization(vm *v1alpha2.VirtualMachine) { for i := range vm.Spec.Networks { - vm.Spec.Networks[i].Id = 100 + i + vm.Spec.Networks[i].Id = 10 + i } } diff --git a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go index b08fbe7aca..f11f9d2e67 100644 --- a/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go +++ b/images/virtualization-artifact/pkg/controller/vm/vm_reconciler.go @@ -21,6 +21,7 @@ import ( "fmt" "reflect" + k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -111,15 +112,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco specToUpdate = vm.Changed().Spec.DeepCopy() } + vm.Changed().Status.ObservedGeneration = vm.Changed().GetGeneration() + err := vm.Update(ctx) - if err != nil { + if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("update status: %w", err) } if specToUpdate != nil { vm.Changed().Spec = *specToUpdate err = r.client.Update(ctx, vm.Changed()) - if err != nil { + if err != nil && !k8serrors.IsNotFound(err) { return fmt.Errorf("update spec: %w", err) } } From 40127ddd065d071d1d64a4a88861919153cfb25d Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 22:08:11 +0300 Subject: [PATCH 14/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index 62cf0acd45..961f47fd1a 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -101,7 +101,7 @@ func (h *NetworkInterfaceHandler) Handle(ctx context.Context, s state.VirtualMac func (h *NetworkInterfaceHandler) lazyInitialization(vm *v1alpha2.VirtualMachine) { for i := range vm.Spec.Networks { - vm.Spec.Networks[i].Id = 10 + i + vm.Spec.Networks[i].Id = 1 + i } } From 204784d63d114bfd357e3354b2804187613e07fa Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 22:32:14 +0300 Subject: [PATCH 15/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index 961f47fd1a..ec6918fcf5 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -100,8 +100,33 @@ func (h *NetworkInterfaceHandler) Handle(ctx context.Context, s state.VirtualMac } func (h *NetworkInterfaceHandler) lazyInitialization(vm *v1alpha2.VirtualMachine) { + // First pass: assign id=1 to Main network if it has id=0 for i := range vm.Spec.Networks { - vm.Spec.Networks[i].Id = 1 + i + if vm.Spec.Networks[i].Type == v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { + vm.Spec.Networks[i].Id = 1 + } + } + + // Second pass: assign sequential ids starting from 2 to other networks with id=0 + nextID := 2 + for i := range vm.Spec.Networks { + if vm.Spec.Networks[i].Type != v1alpha2.NetworksTypeMain && vm.Spec.Networks[i].Id == 0 { + vm.Spec.Networks[i].Id = nextID + nextID++ + // Ensure we never use id=1 (reserved for Main) + if nextID == 1 { + nextID = 2 + } + } else if vm.Spec.Networks[i].Id > 0 { + // Track the highest ID used to avoid conflicts + if vm.Spec.Networks[i].Id >= nextID { + nextID = vm.Spec.Networks[i].Id + 1 + // Ensure we never use id=1 (reserved for Main) + if nextID == 1 { + nextID = 2 + } + } + } } } From 5266d66c975b7a3bee0337e853632af850113f7e Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 22:33:25 +0300 Subject: [PATCH 16/19] ++ Signed-off-by: Dmitry Lopatin --- .../vm/internal/validators/networks_validator.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go index 1f36a0dfac..734754ecb7 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go @@ -143,10 +143,10 @@ func (v *NetworksValidator) validateNetworkIDsUnchanged(oldNetworksSpec, newNetw continue } - // Allow changing id from 0 to a valid value (lazy initialization) - if oldNetwork.Id == 0 && newNetwork.Id > 0 && newNetwork.Id <= maxNetworkID { - continue - } + //// Allow changing id from 0 to a valid value (lazy initialization) + //if oldNetwork.Id == 0 && newNetwork.Id > 0 && newNetwork.Id <= maxNetworkID { + // continue + //} networkIdentifier := v.getNetworkIdentifier(oldNetwork) return fmt.Errorf("network id cannot be changed for network %s", networkIdentifier) From 15a9a807aad255619a171a1e7da20cbd05630464 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Thu, 19 Feb 2026 22:54:00 +0300 Subject: [PATCH 17/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/common/network/network_test.go | 2 +- .../vm/internal/validators/networks_validator.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/images/virtualization-artifact/pkg/common/network/network_test.go b/images/virtualization-artifact/pkg/common/network/network_test.go index 1a815b528f..268673e911 100644 --- a/images/virtualization-artifact/pkg/common/network/network_test.go +++ b/images/virtualization-artifact/pkg/common/network/network_test.go @@ -369,7 +369,7 @@ var _ = Describe("Network Config Generation", func() { configs := CreateNetworkSpec(vm, vmmacs) Expect(configs).To(HaveLen(2)) - Expect(configs[0].ID).To(Equal(10)) + Expect(configs[0].ID).To(Equal(1)) Expect(configs[1].ID).To(Equal(20)) }) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go index 734754ecb7..1f36a0dfac 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/validators/networks_validator.go @@ -143,10 +143,10 @@ func (v *NetworksValidator) validateNetworkIDsUnchanged(oldNetworksSpec, newNetw continue } - //// Allow changing id from 0 to a valid value (lazy initialization) - //if oldNetwork.Id == 0 && newNetwork.Id > 0 && newNetwork.Id <= maxNetworkID { - // continue - //} + // Allow changing id from 0 to a valid value (lazy initialization) + if oldNetwork.Id == 0 && newNetwork.Id > 0 && newNetwork.Id <= maxNetworkID { + continue + } networkIdentifier := v.getNetworkIdentifier(oldNetwork) return fmt.Errorf("network id cannot be changed for network %s", networkIdentifier) From 1e42bf0868f4e15934b41407848c4286bd5cb703 Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Fri, 20 Feb 2026 20:13:57 +0300 Subject: [PATCH 18/19] ++ Signed-off-by: Dmitry Lopatin --- .../pkg/controller/vm/internal/network.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/images/virtualization-artifact/pkg/controller/vm/internal/network.go b/images/virtualization-artifact/pkg/controller/vm/internal/network.go index ec6918fcf5..6e7eb0fe39 100644 --- a/images/virtualization-artifact/pkg/controller/vm/internal/network.go +++ b/images/virtualization-artifact/pkg/controller/vm/internal/network.go @@ -150,6 +150,7 @@ func (h *NetworkInterfaceHandler) UpdateNetworkStatus(ctx context.Context, s sta if hasOnlyDefaultNetwork(vm) { vm.Status.Networks = []v1alpha2.NetworksStatus{ { + Id: 1, Type: v1alpha2.NetworksTypeMain, Name: network.NameDefaultInterface, }, @@ -185,6 +186,7 @@ func (h *NetworkInterfaceHandler) UpdateNetworkStatus(ctx context.Context, s sta for _, interfaceSpec := range network.CreateNetworkSpec(vm, vmmacs) { if interfaceSpec.Type == v1alpha2.NetworksTypeMain { networksStatus = append(networksStatus, v1alpha2.NetworksStatus{ + Id: interfaceSpec.ID, Type: v1alpha2.NetworksTypeMain, Name: network.NameDefaultInterface, }) @@ -192,6 +194,7 @@ func (h *NetworkInterfaceHandler) UpdateNetworkStatus(ctx context.Context, s sta } networksStatus = append(networksStatus, v1alpha2.NetworksStatus{ + Id: interfaceSpec.ID, Type: interfaceSpec.Type, Name: interfaceSpec.Name, MAC: macAddressesByInterfaceName[interfaceSpec.InterfaceName], From 12676483ec3087c124c4e5f161a4ad0693070f0f Mon Sep 17 00:00:00 2001 From: Dmitry Lopatin Date: Fri, 20 Feb 2026 23:35:29 +0300 Subject: [PATCH 19/19] ++ Signed-off-by: Dmitry Lopatin --- test/e2e/internal/util/sdn.go | 31 +++++ test/e2e/vm/additional_network_interfaces.go | 129 ++++++++++++++++++- 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/test/e2e/internal/util/sdn.go b/test/e2e/internal/util/sdn.go index 74dbd39f24..9404a81ec7 100644 --- a/test/e2e/internal/util/sdn.go +++ b/test/e2e/internal/util/sdn.go @@ -45,6 +45,22 @@ spec: type: VLAN vlan: id: 1003 +EOF` + ClusterNetwork2Name = "cn-1004-for-e2e-test" + ClusterNetwork2CreateCommand = `kubectl apply -f - <