Add option to select network interface model while importing VM to Harvester using the vm-import-controller (#78)

Add the field `DefaultNetworkInterfaceModel` to `VirtualMachineImportSpec` and `NetworkInterfaceModel` to `NetworkMapping`. With this new fields it is possible to customize the interface models of the VM NICs.

Related to: https://github.com/harvester/harvester/issues/7999

Signed-off-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
Volker Theile 2025-05-26 10:55:54 +02:00 committed by GitHub
parent 8d4b8a01d9
commit d3ad44e039
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 173 additions and 151 deletions

2
go.mod
View File

@ -20,6 +20,7 @@ require (
k8s.io/apiextensions-apiserver v0.31.3
k8s.io/apimachinery v0.31.3
k8s.io/client-go v12.0.0+incompatible
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
kubevirt.io/api v1.1.0
kubevirt.io/kubevirt v1.1.0
sigs.k8s.io/cluster-api v1.9.4
@ -110,7 +111,6 @@ require (
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-aggregator v0.26.4 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
kubevirt.io/client-go v1.1.0 // indirect
kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 // indirect
kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect

View File

@ -4,6 +4,7 @@ import (
"github.com/rancher/wrangler/pkg/condition"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
kubevirtv1 "kubevirt.io/api/core/v1"
"github.com/harvester/vm-import-controller/pkg/apis/common"
@ -30,9 +31,17 @@ type VirtualMachineImportSpec struct {
// Examples: "vm-1234", "my-VM" or "5649cac7-3871-4bb5-aab6-c72b8c18d0a2"
VirtualMachineName string `json:"virtualMachineName"`
Folder string `json:"folder,omitempty"`
Mapping []NetworkMapping `json:"networkMapping,omitempty"` //If empty new VirtualMachineImport will be mapped to Management Network
StorageClass string `json:"storageClass,omitempty"`
Folder string `json:"folder,omitempty"`
// If empty new VirtualMachineImport will be mapped to Management Network.
Mapping []NetworkMapping `json:"networkMapping,omitempty"`
// The default network interface model. This is always used when:
// - Auto-detection fails (OpenStack source client does not have auto-detection, therefore this field is used for every network interface).
// - No network mapping is provided and a "pod-network" is auto-created.
// Defaults to "virtio".
DefaultNetworkInterfaceModel *string `json:"defaultNetworkInterfaceModel,omitempty" wrangler:"type=string,options=e1000|e1000e|ne2k_pci|pcnet|rtl8139|virtio"`
StorageClass string `json:"storageClass,omitempty"`
}
// VirtualMachineImportStatus tracks the status of the VirtualMachineImport export from migration and import into the Harvester cluster
@ -70,6 +79,9 @@ type DiskInfo struct {
type NetworkMapping struct {
SourceNetwork string `json:"sourceNetwork"`
DestinationNetwork string `json:"destinationNetwork"`
// Override the network interface model that is auto-detected (VMware)
// or defaulted (OpenStack).
NetworkInterfaceModel *string `json:"networkInterfaceModel,omitempty" wrangler:"type=string,options=e1000|e1000e|ne2k_pci|pcnet|rtl8139|virtio"`
}
type ImportStatus string
@ -93,3 +105,23 @@ const (
VirtualMachineExportFailed condition.Cond = "VMExportFailed"
VirtualMachineMigrationFailed ImportStatus = "VMMigrationFailed"
)
// The supported network interface models.
// This can be: e1000, e1000e, ne2k_pci, pcnet, rtl8139, virtio.
// See https://kubevirt.io/user-guide/network/interfaces_and_networks/#interfaces
const (
NetworkInterfaceModelE1000 = "e1000"
NetworkInterfaceModelE1000e = "e1000e"
NetworkInterfaceModelNe2kPci = "ne2k_pci"
NetworkInterfaceModelPcnet = "pcnet"
NetworkInterfaceModelRtl8139 = "rtl8139"
NetworkInterfaceModelVirtio = "virtio"
)
func (in *VirtualMachineImport) GetDefaultNetworkInterfaceModel() string {
return ptr.Deref[string](in.Spec.DefaultNetworkInterfaceModel, NetworkInterfaceModelVirtio)
}
func (in *NetworkMapping) GetNetworkInterfaceModel() string {
return ptr.Deref[string](in.NetworkInterfaceModel, NetworkInterfaceModelVirtio)
}

83
pkg/source/network.go Normal file
View File

@ -0,0 +1,83 @@
package source
import (
"fmt"
kubevirt "kubevirt.io/api/core/v1"
migration "github.com/harvester/vm-import-controller/pkg/apis/migration.harvesterhci.io/v1beta1"
)
type NetworkInfo struct {
NetworkName string
MAC string
MappedNetwork string
Model string
}
func MapNetworks(networkInfos []NetworkInfo, networkMappings []migration.NetworkMapping) []NetworkInfo {
result := make([]NetworkInfo, 0)
for _, ni := range networkInfos {
for _, nm := range networkMappings {
if nm.SourceNetwork == ni.NetworkName {
ni.MappedNetwork = nm.DestinationNetwork
// Override the auto-detected interface model if it is
// customized by the user via the `NetworkMapping`.
if nm.NetworkInterfaceModel != nil {
ni.Model = nm.GetNetworkInterfaceModel()
}
result = append(result, ni)
}
}
}
return result
}
func GenerateNetworkInterfaceConfigs(networkInfos []NetworkInfo, defaultNetworkInterfaceModel string) ([]kubevirt.Network, []kubevirt.Interface) {
networks := make([]kubevirt.Network, 0, len(networkInfos))
interfaces := make([]kubevirt.Interface, 0, len(networkInfos))
for i, ni := range networkInfos {
networks = append(networks, kubevirt.Network{
NetworkSource: kubevirt.NetworkSource{
Multus: &kubevirt.MultusNetwork{
NetworkName: ni.MappedNetwork,
},
},
Name: fmt.Sprintf("migrated-%d", i),
})
interfaces = append(interfaces, kubevirt.Interface{
Name: fmt.Sprintf("migrated-%d", i),
MacAddress: ni.MAC,
Model: ni.Model,
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Bridge: &kubevirt.InterfaceBridge{},
},
})
}
// If there is no network, attach to Pod network. Essential for VM to
// be booted up.
if len(networks) == 0 {
networks = append(networks, kubevirt.Network{
Name: "pod-network",
NetworkSource: kubevirt.NetworkSource{
Pod: &kubevirt.PodNetwork{},
},
})
interfaces = append(interfaces, kubevirt.Interface{
Name: "pod-network",
Model: defaultNetworkInterfaceModel,
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Masquerade: &kubevirt.InterfaceMasquerade{},
},
})
}
return networks, interfaces
}

View File

@ -435,7 +435,7 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
return nil, fmt.Errorf("error getting firware settings: %v", err)
}
networkInfos, err := generateNetworkInfo(vmObj.Addresses)
networkInfos, err := generateNetworkInfos(vmObj.Addresses, vm.GetDefaultNetworkInterfaceModel())
if err != nil {
return nil, err
}
@ -488,46 +488,8 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
},
}
mappedNetwork := mapNetworkCards(networkInfos, vm.Spec.Mapping)
networkConfig := make([]kubevirt.Network, 0, len(mappedNetwork))
for i, v := range mappedNetwork {
networkConfig = append(networkConfig, kubevirt.Network{
NetworkSource: kubevirt.NetworkSource{
Multus: &kubevirt.MultusNetwork{
NetworkName: v.MappedNetwork,
},
},
Name: fmt.Sprintf("migrated-%d", i),
})
}
interfaces := make([]kubevirt.Interface, 0, len(mappedNetwork))
for i, v := range mappedNetwork {
interfaces = append(interfaces, kubevirt.Interface{
Name: fmt.Sprintf("migrated-%d", i),
MacAddress: v.MAC,
Model: "virtio",
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Bridge: &kubevirt.InterfaceBridge{},
},
})
}
// if there is no network, attach to Pod network. Essential for VM to be booted up
if len(networkConfig) == 0 {
networkConfig = append(networkConfig, kubevirt.Network{
Name: "pod-network",
NetworkSource: kubevirt.NetworkSource{
Pod: &kubevirt.PodNetwork{},
},
})
interfaces = append(interfaces, kubevirt.Interface{
Name: "pod-network",
Model: "virtio",
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Masquerade: &kubevirt.InterfaceMasquerade{},
},
})
}
mappedNetwork := source.MapNetworks(networkInfos, vm.Spec.Mapping)
networkConfig, interfaceConfig := source.GenerateNetworkInterfaceConfigs(mappedNetwork, vm.GetDefaultNetworkInterfaceModel())
// Setup BIOS/EFI, SecureBoot and TPM settings.
if uefi {
@ -535,7 +497,7 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
}
vmSpec.Template.Spec.Networks = networkConfig
vmSpec.Template.Spec.Domain.Devices.Interfaces = interfaces
vmSpec.Template.Spec.Domain.Devices.Interfaces = interfaceConfig
newVM.Spec = vmSpec
// disk attachment needs query by core controller for storage classes, so will be added by the migration controller
@ -663,26 +625,6 @@ func (c *Client) findVM(name string) (*ExtendedServer, error) {
return &s, err
}
type networkInfo struct {
NetworkName string
MAC string
MappedNetwork string
}
func mapNetworkCards(networkCards []networkInfo, mapping []migration.NetworkMapping) []networkInfo {
var retNetwork []networkInfo
for _, nc := range networkCards {
for _, m := range mapping {
if m.SourceNetwork == nc.NetworkName {
nc.MappedNetwork = m.DestinationNetwork
retNetwork = append(retNetwork, nc)
}
}
}
return retNetwork
}
func (c *Client) ImageFirmwareSettings(instance *servers.Server) (bool, bool, bool, error) {
var imageID string
var uefiType, tpmEnabled, secureBoot bool
@ -721,9 +663,10 @@ func (c *Client) ImageFirmwareSettings(instance *servers.Server) (bool, bool, bo
return uefiType, tpmEnabled, secureBoot, nil
}
func generateNetworkInfo(info map[string]interface{}) ([]networkInfo, error) {
networkInfos := make([]networkInfo, 0)
uniqueNetworks := make([]networkInfo, 0)
func generateNetworkInfos(info map[string]interface{}, defaultInterfaceModel string) ([]source.NetworkInfo, error) {
networkInfos := make([]source.NetworkInfo, 0)
uniqueNetworks := make([]source.NetworkInfo, 0)
for network, values := range info {
valArr, ok := values.([]interface{})
if !ok {
@ -734,15 +677,19 @@ func generateNetworkInfo(info map[string]interface{}) ([]networkInfo, error) {
if !ok {
return nil, fmt.Errorf("error asserting network array element into map[string]string")
}
networkInfos = append(networkInfos, networkInfo{
networkInfos = append(networkInfos, source.NetworkInfo{
NetworkName: network,
MAC: valMap["OS-EXT-IPS-MAC:mac_addr"].(string),
// Note, the interface model is not provided via the OpenStack
// Nova API, therefore we need to set it ourselves.
Model: defaultInterfaceModel,
})
}
}
// in case of interfaces with ipv6 and ipv4 addresses they are reported twice, so we need to dedup them
// based on a mac address
networksMap := make(map[string]networkInfo)
networksMap := make(map[string]source.NetworkInfo)
for _, v := range networkInfos {
networksMap[v.MAC] = v
}
@ -750,6 +697,7 @@ func generateNetworkInfo(info map[string]interface{}) ([]networkInfo, error) {
for _, v := range networksMap {
uniqueNetworks = append(uniqueNetworks, v)
}
return uniqueNetworks, nil
}

View File

@ -126,8 +126,9 @@ func Test_GenerateVirtualMachine(t *testing.T) {
assert.NoError(err, "expected no error during GenerateVirtualMachine")
assert.NotEmpty(newVM.Spec.Template.Spec.Domain.CPU, "expected CPU's to not be empty")
assert.NotEmpty(newVM.Spec.Template.Spec.Domain.Resources.Limits.Memory(), "expected memory limit to not be empty")
assert.NotEmpty(newVM.Spec.Template.Spec.Networks, "expected to find atleast 1 network as pod network should have been applied")
assert.NotEmpty(newVM.Spec.Template.Spec.Domain.Devices.Interfaces, "expected to find atleast 1 interface for pod-network")
assert.NotEmpty(newVM.Spec.Template.Spec.Networks, "expected to find at least 1 network as pod network should have been applied")
assert.NotEmpty(newVM.Spec.Template.Spec.Domain.Devices.Interfaces, "expected to find at least 1 interface for pod-network")
assert.Equal(newVM.Spec.Template.Spec.Domain.Devices.Interfaces[0].Model, migration.NetworkInterfaceModelVirtio, "expected to have a NIC with virtio model")
}
func Test_generateNetworkInfo(t *testing.T) {
@ -137,10 +138,11 @@ func Test_generateNetworkInfo(t *testing.T) {
err := json.Unmarshal(networkInfoByte, &networkInfoMap)
assert.NoError(err, "expected no error while unmarshalling network info")
vmInterfaceDetails, err := generateNetworkInfo(networkInfoMap)
vmInterfaceDetails, err := generateNetworkInfos(networkInfoMap, migration.NetworkInterfaceModelVirtio)
assert.NoError(err, "expected no error while generating network info")
assert.Len(vmInterfaceDetails, 2, "expected to find 2 interfaces only")
assert.Equal(vmInterfaceDetails[0].Model, migration.NetworkInterfaceModelVirtio, "expected to have a NIC with virtio model")
assert.Equal(vmInterfaceDetails[1].Model, migration.NetworkInterfaceModelVirtio, "expected to have a NIC with virtio model")
}
func Test_ClientOptions(t *testing.T) {

View File

@ -323,8 +323,7 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
"spec": o,
}, []string{"spec"})).Info("Origin spec of the VM to be imported")
// Need CPU, Socket, Memory, VirtualNIC information to perform the mapping
networkInfo := identifyNetworkCards(o.Config.Hardware.Device)
networkInfos := generateNetworkInfos(o.Config.Hardware.Device)
vmSpec := kubevirt.VirtualMachineSpec{
RunStrategy: &[]kubevirt.VirtualMachineRunStrategy{kubevirt.RunStrategyRerunOnFailure}[0],
@ -360,46 +359,8 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
},
}
mappedNetwork := mapNetworkCards(networkInfo, vm.Spec.Mapping)
networkConfig := make([]kubevirt.Network, 0, len(mappedNetwork))
for i, v := range mappedNetwork {
networkConfig = append(networkConfig, kubevirt.Network{
NetworkSource: kubevirt.NetworkSource{
Multus: &kubevirt.MultusNetwork{
NetworkName: v.MappedNetwork,
},
},
Name: fmt.Sprintf("migrated-%d", i),
})
}
interfaces := make([]kubevirt.Interface, 0, len(mappedNetwork))
for i, v := range mappedNetwork {
interfaces = append(interfaces, kubevirt.Interface{
Name: fmt.Sprintf("migrated-%d", i),
MacAddress: v.MAC,
Model: "virtio",
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Bridge: &kubevirt.InterfaceBridge{},
},
})
}
// if there is no network, attach to Pod network. Essential for VM to be booted up
if len(networkConfig) == 0 {
networkConfig = append(networkConfig, kubevirt.Network{
Name: "pod-network",
NetworkSource: kubevirt.NetworkSource{
Pod: &kubevirt.PodNetwork{},
},
})
interfaces = append(interfaces, kubevirt.Interface{
Name: "pod-network",
Model: "virtio",
InterfaceBindingMethod: kubevirt.InterfaceBindingMethod{
Masquerade: &kubevirt.InterfaceMasquerade{},
},
})
}
mappedNetwork := source.MapNetworks(networkInfos, vm.Spec.Mapping)
networkConfig, interfaceConfig := source.GenerateNetworkInterfaceConfigs(mappedNetwork, vm.GetDefaultNetworkInterfaceModel())
// Setup BIOS/EFI, SecureBoot and TPM settings.
uefi := strings.EqualFold(o.Config.Firmware, string(types.GuestOsDescriptorFirmwareTypeEfi))
@ -413,7 +374,7 @@ func (c *Client) GenerateVirtualMachine(vm *migration.VirtualMachineImport) (*ku
}
vmSpec.Template.Spec.Networks = networkConfig
vmSpec.Template.Spec.Domain.Devices.Interfaces = interfaces
vmSpec.Template.Spec.Domain.Devices.Interfaces = interfaceConfig
newVM.Spec = vmSpec
// disk attachment needs query by core controller for storage classes, so will be added by the migration controller
@ -430,64 +391,57 @@ func (c *Client) findVM(path, name string) (*object.VirtualMachine, error) {
return f.VirtualMachine(c.ctx, vmPath)
}
type networkInfo struct {
NetworkName string
MAC string
MappedNetwork string
}
func generateNetworkInfos(devices []types.BaseVirtualDevice) []source.NetworkInfo {
result := make([]source.NetworkInfo, 0, len(devices))
func identifyNetworkCards(devices []types.BaseVirtualDevice) []networkInfo {
var resp []networkInfo
for _, d := range devices {
switch d := d.(type) {
case *types.VirtualVmxnet:
obj := d
resp = append(resp, networkInfo{
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelVirtio,
})
case *types.VirtualE1000e:
obj := d
resp = append(resp, networkInfo{
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelE1000e,
})
case *types.VirtualE1000:
obj := d
resp = append(resp, networkInfo{
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelE1000,
})
case *types.VirtualVmxnet3:
obj := d
resp = append(resp, networkInfo{
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelVirtio,
})
case *types.VirtualVmxnet2:
obj := d
resp = append(resp, networkInfo{
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelVirtio,
})
case *types.VirtualPCNet32:
obj := d
result = append(result, source.NetworkInfo{
NetworkName: obj.DeviceInfo.GetDescription().Summary,
MAC: obj.MacAddress,
Model: migration.NetworkInterfaceModelPcnet,
})
}
}
return resp
}
func mapNetworkCards(networkCards []networkInfo, mapping []migration.NetworkMapping) []networkInfo {
var retNetwork []networkInfo
for _, nc := range networkCards {
for _, m := range mapping {
if m.SourceNetwork == nc.NetworkName {
nc.MappedNetwork = m.DestinationNetwork
retNetwork = append(retNetwork, nc)
}
}
}
return retNetwork
return result
}
// adapterType tries to identify the disk bus type from vmware

View File

@ -19,6 +19,7 @@ import (
migration "github.com/harvester/vm-import-controller/pkg/apis/migration.harvesterhci.io/v1beta1"
"github.com/harvester/vm-import-controller/pkg/server"
"github.com/harvester/vm-import-controller/pkg/source"
)
var vcsimPort string
@ -256,7 +257,7 @@ func Test_GenerateVirtualMachine(t *testing.T) {
assert.Len(newVM.Spec.Template.Spec.Domain.Devices.Interfaces, 1, "should have found a network map")
assert.Equal(newVM.Spec.Template.Spec.Domain.Memory.Guest.String(), "32M", "expected VM to have 32M memory")
assert.NotEmpty(newVM.Spec.Template.Spec.Domain.Resources.Limits, "expect to find resource requests to be present")
assert.Equal(newVM.Spec.Template.Spec.Domain.Devices.Interfaces[0].Model, migration.NetworkInterfaceModelE1000, "expected to have a NIC with e1000 model")
}
func Test_GenerateVirtualMachine_secureboot(t *testing.T) {
@ -365,7 +366,7 @@ func Test_identifyNetworkCards(t *testing.T) {
err = vmObj.Properties(c.ctx, vmObj.Reference(), []string{}, &o)
assert.NoError(err, "expected no error looking up vmObj properties")
networkInfo := identifyNetworkCards(o.Config.Hardware.Device)
networkInfo := generateNetworkInfos(o.Config.Hardware.Device)
assert.Len(networkInfo, 1, "expected to find only 1 item in the networkInfo")
networkMapping := []migration.NetworkMapping{
{
@ -373,15 +374,17 @@ func Test_identifyNetworkCards(t *testing.T) {
DestinationNetwork: "harvester1",
},
{
SourceNetwork: "DVSwitch: fea97929-4b2d-5972-b146-930c6d0b4014",
DestinationNetwork: "pod-network",
SourceNetwork: "DVSwitch: fea97929-4b2d-5972-b146-930c6d0b4014",
DestinationNetwork: "pod-network",
NetworkInterfaceModel: pointer.String(migration.NetworkInterfaceModelRtl8139),
},
}
mappedInfo := mapNetworkCards(networkInfo, networkMapping)
mappedInfo := source.MapNetworks(networkInfo, networkMapping)
assert.Len(mappedInfo, 1, "expected to find only 1 item in the mapped networkinfo")
assert.Equal(mappedInfo[0].Model, "rtl8139", "expected to have a NIC with rtl8139 model")
noNetworkMapping := []migration.NetworkMapping{}
noMappedInfo := mapNetworkCards(networkInfo, noNetworkMapping)
noMappedInfo := source.MapNetworks(networkInfo, noNetworkMapping)
assert.Len(noMappedInfo, 0, "expected to find no item in the mapped networkinfo")
}