diff --git a/api/v1alpha1/proxmoxmachine_conversion.go b/api/v1alpha1/proxmoxmachine_conversion.go index e92042415..381568593 100644 --- a/api/v1alpha1/proxmoxmachine_conversion.go +++ b/api/v1alpha1/proxmoxmachine_conversion.go @@ -102,6 +102,7 @@ func restoreProxmoxMachineSpec(src *ProxmoxMachineSpec, dst *v1alpha2.ProxmoxMac if i < len(restored.Network.NetworkDevices) { dst.Network.NetworkDevices[i].DefaultIPv4 = restored.Network.NetworkDevices[i].DefaultIPv4 dst.Network.NetworkDevices[i].DefaultIPv6 = restored.Network.NetworkDevices[i].DefaultIPv6 + dst.Network.NetworkDevices[i].Queues = restored.Network.NetworkDevices[i].Queues } } } diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index f33158012..722e5c289 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -611,6 +611,7 @@ func autoConvert_v1alpha2_NetworkDevice_To_v1alpha1_NetworkDevice(in *v1alpha2.N } else { out.VLAN = nil } + // WARNING: in.Queues requires manual conversion: does not exist in peer-type // WARNING: in.Name requires manual conversion: does not exist in peer-type // WARNING: in.InterfaceConfig requires manual conversion: does not exist in peer-type return nil diff --git a/api/v1alpha2/proxmoxmachine_types.go b/api/v1alpha2/proxmoxmachine_types.go index 4a2745ea6..85c770a7f 100644 --- a/api/v1alpha2/proxmoxmachine_types.go +++ b/api/v1alpha2/proxmoxmachine_types.go @@ -455,6 +455,13 @@ type NetworkDevice struct { // +kubebuilder:validation:Maximum=4094 VLAN *int32 `json:"vlan,omitempty"` + // queues is the number of queues assigned to the device. + // This value is passed to the Multiqueue field in PROXMOX. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + Queues *int32 `json:"queues,omitempty"` + // name is the network device name. // +default="net0" // +optional diff --git a/api/v1alpha2/proxmoxmachine_types_test.go b/api/v1alpha2/proxmoxmachine_types_test.go index 597d33884..bfd381845 100644 --- a/api/v1alpha2/proxmoxmachine_types_test.go +++ b/api/v1alpha2/proxmoxmachine_types_test.go @@ -367,6 +367,55 @@ var _ = Describe("ProxmoxMachine Test", func() { }) }) + Context("Queues", func() { + It("Should not allow machine with network device queue equal to 0", func() { + dm := defaultMachine() + dm.Spec.Network = &NetworkSpec{ + NetworkDevices: []NetworkDevice{{ + Bridge: ptr.To("vmbr0"), + Queues: ptr.To(int32(0)), + }}, + } + + Expect(k8sClient.Create(context.Background(), dm)).Should(MatchError(ContainSubstring("should be greater than or equal to 1"))) + }) + + It("Should not allow machine with network device queue greater than 65535", func() { + dm := defaultMachine() + dm.Spec.Network = &NetworkSpec{ + NetworkDevices: []NetworkDevice{{ + Bridge: ptr.To("vmbr0"), + Queues: ptr.To(int32(65536)), + }}, + } + + Expect(k8sClient.Create(context.Background(), dm)).Should(MatchError(ContainSubstring("should be less than or equal to 65535"))) + }) + + It("Should allow machine with network device queue between 1 and 65535", func() { + dm := defaultMachine() + dm.Spec.Network = &NetworkSpec{ + NetworkDevices: []NetworkDevice{{ + Bridge: ptr.To("vmbr0"), + Queues: ptr.To(int32(4)), + }}, + } + + Expect(k8sClient.Create(context.Background(), dm)).Should(Succeed()) + }) + + It("Should allow machine with network device queue unset", func() { + dm := defaultMachine() + dm.Spec.Network = &NetworkSpec{ + NetworkDevices: []NetworkDevice{{ + Bridge: ptr.To("vmbr0"), + }}, + } + + Expect(k8sClient.Create(context.Background(), dm)).Should(Succeed()) + }) + }) + Context("VMIDRange", func() { It("Should only allow spec.vmIDRange.start >= 100", func() { dm := defaultMachine() diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 8606a5e41..284aed501 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -207,6 +207,11 @@ func (in *NetworkDevice) DeepCopyInto(out *NetworkDevice) { *out = new(int32) **out = **in } + if in.Queues != nil { + in, out := &in.Queues, &out.Queues + *out = new(int32) + **out = **in + } in.InterfaceConfig.DeepCopyInto(&out.InterfaceConfig) } diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml index 6b391edc9..fb86ee5b4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachines.yaml @@ -1114,6 +1114,14 @@ spec: minLength: 4 pattern: ^net[0-9]+$ type: string + queues: + description: |- + queues is the number of queues assigned to the device. + This value is passed to the Multiqueue field in PROXMOX. + format: int32 + maximum: 65535 + minimum: 1 + type: integer routes: description: routes are the routes associated with this interface. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml index 220fca029..54d95213a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_proxmoxmachinetemplates.yaml @@ -980,6 +980,14 @@ spec: minLength: 4 pattern: ^net[0-9]+$ type: string + queues: + description: |- + queues is the number of queues assigned to the device. + This value is passed to the Multiqueue field in PROXMOX. + format: int32 + maximum: 65535 + minimum: 1 + type: integer routes: description: routes are the routes associated with this interface. diff --git a/internal/service/vmservice/utils.go b/internal/service/vmservice/utils.go index d2b09ec0c..9c0fb9531 100644 --- a/internal/service/vmservice/utils.go +++ b/internal/service/vmservice/utils.go @@ -137,6 +137,21 @@ func extractNetworkVLAN(input string) int32 { return 0 } +// extractNetworkQueue returns the queue out of net device input e.g. virtio=A6:23:64:4D:84:CB,bridge=vmbr1,mtu=1500,tag=100,queues=4. +func extractNetworkQueue(input string) int32 { + re := regexp.MustCompile(`queues=(\d+)`) + match := re.FindStringSubmatch(input) + if len(match) > 1 { + queue, err := strconv.ParseUint(match[1], 10, 16) + if err != nil { + return 0 + } + return int32(queue) + } + + return 0 +} + func shouldUpdateNetworkDevices(machineScope *scope.MachineScope) bool { if machineScope.ProxmoxMachine.Spec.Network == nil { // no network config needed @@ -180,14 +195,22 @@ func shouldUpdateNetworkDevices(machineScope *scope.MachineScope) bool { return true } } + + if v.Queues != nil { + queues := extractNetworkQueue(net) + + if queues != *v.Queues { + return true + } + } } return false } // formatNetworkDevice formats a network device config -// example 'virtio,bridge=vmbr0,tag=100'. -func formatNetworkDevice(model, bridge string, mtu *int32, vlan *int32) string { +// example 'virtio,bridge=vmbr0,tag=100,queues=4'. +func formatNetworkDevice(model, bridge string, mtu *int32, vlan *int32, queues *int32) string { var components = []string{model, fmt.Sprintf("bridge=%s", bridge)} if mtu != nil { @@ -198,6 +221,10 @@ func formatNetworkDevice(model, bridge string, mtu *int32, vlan *int32) string { components = append(components, fmt.Sprintf("tag=%d", *vlan)) } + if queues != nil { + components = append(components, fmt.Sprintf("queues=%d", *queues)) + } + return strings.Join(components, ",") } diff --git a/internal/service/vmservice/vm.go b/internal/service/vmservice/vm.go index f9fd3a920..ed22295e0 100644 --- a/internal/service/vmservice/vm.go +++ b/internal/service/vmservice/vm.go @@ -327,7 +327,7 @@ func reconcileVirtualMachineConfig(ctx context.Context, machineScope *scope.Mach for _, v := range devices { vmOptions = append(vmOptions, proxmox.VirtualMachineOption{ Name: string(v.Name), - Value: formatNetworkDevice(ptr.Deref(v.Model, "virtio"), ptr.Deref(v.Bridge, ""), v.MTU, v.VLAN), + Value: formatNetworkDevice(ptr.Deref(v.Model, "virtio"), ptr.Deref(v.Bridge, ""), v.MTU, v.VLAN, v.Queues), }) } } diff --git a/internal/service/vmservice/vm_test.go b/internal/service/vmservice/vm_test.go index f7cfba286..f4c8b3fbf 100644 --- a/internal/service/vmservice/vm_test.go +++ b/internal/service/vmservice/vm_test.go @@ -449,8 +449,8 @@ func TestReconcileVirtualMachineConfig_ApplyConfig(t *testing.T) { proxmox.VirtualMachineOption{Name: optionCores, Value: *machineScope.ProxmoxMachine.Spec.NumCores}, proxmox.VirtualMachineOption{Name: optionMemory, Value: *machineScope.ProxmoxMachine.Spec.MemoryMiB}, proxmox.VirtualMachineOption{Name: optionDescription, Value: machineScope.ProxmoxMachine.Spec.Description}, - proxmox.VirtualMachineOption{Name: "net0", Value: formatNetworkDevice("virtio", "vmbr0", ptr.To[int32](1500), nil)}, - proxmox.VirtualMachineOption{Name: "net1", Value: formatNetworkDevice("virtio", "vmbr1", ptr.To[int32](1500), nil)}, + proxmox.VirtualMachineOption{Name: "net0", Value: formatNetworkDevice("virtio", "vmbr0", ptr.To[int32](1500), nil, nil)}, + proxmox.VirtualMachineOption{Name: "net1", Value: formatNetworkDevice("virtio", "vmbr1", ptr.To[int32](1500), nil, nil)}, } proxmoxClient.EXPECT().ConfigureVM(context.Background(), vm, expectedOptions...).Return(task, nil).Once() @@ -624,8 +624,39 @@ func TestReconcileVirtualMachineConfigVLAN(t *testing.T) { proxmox.VirtualMachineOption{Name: optionSockets, Value: *machineScope.ProxmoxMachine.Spec.NumSockets}, proxmox.VirtualMachineOption{Name: optionCores, Value: *machineScope.ProxmoxMachine.Spec.NumCores}, proxmox.VirtualMachineOption{Name: optionMemory, Value: *machineScope.ProxmoxMachine.Spec.MemoryMiB}, - proxmox.VirtualMachineOption{Name: "net0", Value: formatNetworkDevice("virtio", "vmbr0", nil, ptr.To(int32(100)))}, - proxmox.VirtualMachineOption{Name: "net1", Value: formatNetworkDevice("virtio", "vmbr1", nil, ptr.To(int32(100)))}, + proxmox.VirtualMachineOption{Name: "net0", Value: formatNetworkDevice("virtio", "vmbr0", nil, ptr.To(int32(100)), nil)}, + proxmox.VirtualMachineOption{Name: "net1", Value: formatNetworkDevice("virtio", "vmbr1", nil, ptr.To(int32(100)), nil)}, + } + + proxmoxClient.EXPECT().ConfigureVM(context.TODO(), vm, expectedOptions...).Return(task, nil).Once() + + requeue, err := reconcileVirtualMachineConfig(context.TODO(), machineScope) + require.NoError(t, err) + require.True(t, requeue) + require.EqualValues(t, task.UPID, *machineScope.ProxmoxMachine.Status.TaskRef) +} + +func TestReconcileVirtualMachineConfigQueue(t *testing.T) { + machineScope, proxmoxClient, _ := setupReconcilerTestWithCondition(t, infrav1.ProxmoxMachineVirtualMachineProvisionedCloningReason) + machineScope.ProxmoxMachine.Spec.NumSockets = ptr.To(int32(4)) + machineScope.ProxmoxMachine.Spec.NumCores = ptr.To(int32(4)) + machineScope.ProxmoxMachine.Spec.MemoryMiB = ptr.To(int32(16 * 1024)) + machineScope.ProxmoxMachine.Spec.Network = &infrav1.NetworkSpec{ + NetworkDevices: []infrav1.NetworkDevice{ + {Name: infrav1.NetName("net0"), Bridge: ptr.To("vmbr0"), Model: ptr.To("virtio"), VLAN: ptr.To(int32(100)), Queues: ptr.To(int32(4))}, + {Name: infrav1.NetName("net1"), Bridge: ptr.To("vmbr1"), Model: ptr.To("virtio"), VLAN: ptr.To(int32(100)), Queues: ptr.To(int32(4))}, + }, + } + + vm := newStoppedVM() + task := newTask() + machineScope.SetVirtualMachine(vm) + expectedOptions := []interface{}{ + proxmox.VirtualMachineOption{Name: optionSockets, Value: *machineScope.ProxmoxMachine.Spec.NumSockets}, + proxmox.VirtualMachineOption{Name: optionCores, Value: *machineScope.ProxmoxMachine.Spec.NumCores}, + proxmox.VirtualMachineOption{Name: optionMemory, Value: *machineScope.ProxmoxMachine.Spec.MemoryMiB}, + proxmox.VirtualMachineOption{Name: "net0", Value: formatNetworkDevice("virtio", "vmbr0", nil, ptr.To(int32(100)), ptr.To(int32(4)))}, + proxmox.VirtualMachineOption{Name: "net1", Value: formatNetworkDevice("virtio", "vmbr1", nil, ptr.To(int32(100)), ptr.To(int32(4)))}, } proxmoxClient.EXPECT().ConfigureVM(context.TODO(), vm, expectedOptions...).Return(task, nil).Once()