Skip to content

[FEATURE] Add option to select network interface model while importing VM #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 35 additions & 3 deletions pkg/apis/migration.harvesterhci.io/v1beta1/virtualmachines.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 changes: 83 additions & 0 deletions pkg/source/network.go
Original file line number Diff line number Diff line change
@@ -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{},
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason not using the bridge interface here?

Copy link
Member Author

@votdev votdev May 20, 2025

Choose a reason for hiding this comment

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

Unfortunately, I can't say anything here as I have adopted the code 1:1.

@ibrokethecloud added the origin code. Could you tell use more please?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if there is no valid source to destination mapping we default to adding the masquerade network which is the current default behaviour of harvester during vm creation.

Copy link
Member

Choose a reason for hiding this comment

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

Understood. Thanks for clarifying. I was just considering what might be the most compatible configuration :D

},
})
}

return networks, interfaces
}
82 changes: 15 additions & 67 deletions pkg/source/openstack/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -488,54 +488,16 @@ 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 {
source.VMSpecSetupUEFISettings(&vmSpec, secureBoot, tpm)
}

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -734,22 +677,27 @@ 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
}

for _, v := range networksMap {
uniqueNetworks = append(uniqueNetworks, v)
}

return uniqueNetworks, nil
}

Expand Down
10 changes: 6 additions & 4 deletions pkg/source/openstack/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Loading