Skip to content

Commit

Permalink
Merge pull request kubevirt#13850 from nirdothan/mng-link-state-base
Browse files Browse the repository at this point in the history
api, net: Manage Link State for vNICs
  • Loading branch information
kubevirt-bot authored Feb 9, 2025
2 parents 93edde7 + ecb915b commit 408167e
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 22 deletions.
2 changes: 1 addition & 1 deletion api/openapi-spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -14221,7 +14221,7 @@
"$ref": "#/definitions/v1.InterfaceSRIOV"
},
"state": {
"description": "State represents the requested operational state of the interface. The (only) value supported is `absent`, expressing a request to remove the interface.",
"description": "State represents the requested operational state of the interface. The supported values are: `absent`, expressing a request to remove the interface. `down`, expressing a request to set the link down. `up`, expressing a request to set the link up. Empty value functions as `up`.",
"type": "string"
},
"tag": {
Expand Down
2 changes: 2 additions & 0 deletions pkg/network/admitter/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ go_test(
"//staging/src/kubevirt.io/client-go/testutils:go_default_library",
"//vendor/github.com/onsi/ginkgo/v2:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/github.com/onsi/gomega/gstruct:go_default_library",
"//vendor/github.com/onsi/gomega/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
Expand Down
15 changes: 14 additions & 1 deletion pkg/network/admitter/admit.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,26 @@ import (
func validateInterfaceStateValue(field *k8sfield.Path, spec *v1.VirtualMachineInstanceSpec) []metav1.StatusCause {
var causes []metav1.StatusCause
for idx, iface := range spec.Domain.Devices.Interfaces {
if iface.State != "" && iface.State != v1.InterfaceStateAbsent {
if iface.State != "" &&
iface.State != v1.InterfaceStateAbsent &&
iface.State != v1.InterfaceStateLinkDown &&
iface.State != v1.InterfaceStateLinkUp {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("logical %s interface state value is unsupported: %s", iface.Name, iface.State),
Field: field.Child("domain", "devices", "interfaces").Index(idx).Child("state").String(),
})
}

if iface.SRIOV != nil &&
(iface.State == v1.InterfaceStateLinkDown || iface.State == v1.InterfaceStateLinkUp) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("%q interface's state %q is not supported for SR-IOV NICs", iface.Name, iface.State),
Field: field.Child("domain", "devices", "interfaces").Index(idx).Child("state").String(),
})
}

if iface.State == v1.InterfaceStateAbsent && iface.Bridge == nil {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Expand Down
28 changes: 18 additions & 10 deletions pkg/network/admitter/admit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ package admitter_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
"github.com/onsi/gomega/types"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sfield "k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -51,6 +53,8 @@ var _ = Describe("Validating VMI network spec", func() {
},
Entry("is empty", v1.InterfaceState("")),
Entry("is absent when bridge binding is used", v1.InterfaceStateAbsent),
Entry("is up when bridge binding is used", v1.InterfaceStateLinkUp),
Entry("is down when bridge binding is used", v1.InterfaceStateLinkDown),
)

It("network interface state value is invalid", func() {
Expand All @@ -66,24 +70,28 @@ var _ = Describe("Validating VMI network spec", func() {
}))
})

It("network interface state value of absent is not supported when bridge-binding is not used", func() {
DescribeTable("network interface state ", func(state v1.InterfaceState, messageRegex types.GomegaMatcher) {
vm := api.NewMinimalVMI("testvm")
vm.Spec.Domain.Devices.Interfaces = []v1.Interface{{
Name: "foo",
State: v1.InterfaceStateAbsent,
State: state,
InterfaceBindingMethod: v1.InterfaceBindingMethod{SRIOV: &v1.InterfaceSRIOV{}},
}}
vm.Spec.Networks = []v1.Network{
{Name: "foo", NetworkSource: v1.NetworkSource{Multus: &v1.MultusNetwork{NetworkName: "net"}}},
}
validator := admitter.NewValidator(k8sfield.NewPath("fake"), &vm.Spec, stubClusterConfigChecker{})
Expect(validator.Validate()).To(
ConsistOf(metav1.StatusCause{
Type: "FieldValueInvalid",
Message: "\"foo\" interface's state \"absent\" is supported only for bridge binding",
Field: "fake.domain.devices.interfaces[0].state",
}))
})
statusCause := admitter.NewValidator(k8sfield.NewPath("fake"), &vm.Spec, stubClusterConfigChecker{}).Validate()
Expect(statusCause).To(HaveLen(1))
Expect(statusCause[0]).To(MatchAllFields(Fields{
"Type": Equal(metav1.CauseType("FieldValueInvalid")),
"Field": Equal("fake.domain.devices.interfaces[0].state"),
"Message": messageRegex,
}))
},
Entry("down is not supported for sriov", v1.InterfaceStateLinkDown, MatchRegexp("down.+SR-IOV")),
Entry("up is not supported for sriov", v1.InterfaceStateLinkUp, MatchRegexp("up.+SR-IOV")),
Entry("absent is not supported when bridge-binding is not used", v1.InterfaceStateAbsent, MatchRegexp("absent.+bridge")),
)

It("network interface state value of absent is not supported on the default network", func() {
vm := api.NewMinimalVMI("testvm")
Expand Down
12 changes: 12 additions & 0 deletions pkg/virt-launcher/virtwrap/converter/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1556,7 +1556,19 @@ var _ = Describe("Converter", func() {
},
}
})
It("Should set domain interface state down", func() {
v1.SetObjectDefaults_VirtualMachineInstance(vmi)
vmi.Spec.Domain.Devices.Interfaces = []v1.Interface{
*v1.DefaultBridgeNetworkInterface(),
}
vmi.Spec.Domain.Devices.Interfaces[0].State = v1.InterfaceStateLinkDown
vmi.Spec.Networks = []v1.Network{*v1.DefaultPodNetwork()}

domain := vmiToDomain(vmi, c)
Expect(domain).ToNot(BeNil())
Expect(domain.Spec.Devices.Interfaces).To(HaveLen(1))
Expect(domain.Spec.Devices.Interfaces[0].LinkState.State).To(Equal("down"))
})
It("Should set domain interface source correctly for multus", func() {
v1.SetObjectDefaults_VirtualMachineInstance(vmi)
vmi.Spec.Domain.Devices.Interfaces = []v1.Interface{
Expand Down
4 changes: 4 additions & 0 deletions pkg/virt-launcher/virtwrap/converter/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ func CreateDomainInterfaces(vmi *v1.VirtualMachineInstance, c *ConverterContext)
}
}
}

if iface.State == v1.InterfaceStateLinkDown {
domainIface.LinkState = &api.LinkState{State: "down"}
}
domainInterfaces = append(domainInterfaces, domainIface)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6400,7 +6400,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual network interface
Expand Down Expand Up @@ -11620,7 +11624,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual network interface address
Expand Down Expand Up @@ -14820,7 +14828,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual network interface address
Expand Down Expand Up @@ -17234,7 +17246,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual network interface
Expand Down Expand Up @@ -21728,7 +21744,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual network
Expand Down Expand Up @@ -26914,7 +26934,11 @@ var CRDsValidation map[string]string = map[string]string{
state:
description: |-
State represents the requested operational state of the interface.
The (only) value supported is 'absent', expressing a request to remove the interface.
The supported values are:
'absent', expressing a request to remove the interface.
'down', expressing a request to set the link down.
'up', expressing a request to set the link up.
Empty value functions as 'up'.
type: string
tag:
description: If specified, the virtual
Expand Down
10 changes: 8 additions & 2 deletions staging/src/kubevirt.io/api/core/v1/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -1280,15 +1280,21 @@ type Interface struct {
// +optional
ACPIIndex int `json:"acpiIndex,omitempty"`
// State represents the requested operational state of the interface.
// The (only) value supported is `absent`, expressing a request to remove the interface.
// The supported values are:
// `absent`, expressing a request to remove the interface.
// `down`, expressing a request to set the link down.
// `up`, expressing a request to set the link up.
// Empty value functions as `up`.
// +optional
State InterfaceState `json:"state,omitempty"`
}

type InterfaceState string

const (
InterfaceStateAbsent InterfaceState = "absent"
InterfaceStateAbsent InterfaceState = "absent"
InterfaceStateLinkUp InterfaceState = "up"
InterfaceStateLinkDown InterfaceState = "down"
)

// Extra DHCP options to use in the interface.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion staging/src/kubevirt.io/client-go/api/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/network/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"framework.go",
"hotplug_bridge.go",
"hotplug_sriov.go",
"link_state.go",
"networkpolicy.go",
"port_forward.go",
"primary_pod_network.go",
Expand Down
136 changes: 136 additions & 0 deletions tests/network/link_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* This file is part of the kubevirt project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright the KubeVirt Authors.
*
*/

package network

import (
"context"
"fmt"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "kubevirt.io/api/core/v1"

"kubevirt.io/kubevirt/pkg/libvmi"
"kubevirt.io/kubevirt/tests/console"
"kubevirt.io/kubevirt/tests/framework/kubevirt"
"kubevirt.io/kubevirt/tests/framework/matcher"
"kubevirt.io/kubevirt/tests/libnet"
"kubevirt.io/kubevirt/tests/libvmifact"
"kubevirt.io/kubevirt/tests/testsuite"
)

var _ = SIGDescribe("interface state up/down", func() {

It("status and guest should show correct iface state", func() {
const (
primaryLogicalNetName = "default"
secondary2LogicalNetName = "bridge2"
nadName = "bridge-nad"
)

testNamespace := testsuite.GetTestNamespace(nil)

var err error
_, err = libnet.CreateNetAttachDef(context.Background(), testNamespace,
libnet.NewBridgeNetAttachDef(nadName, "br02"))
Expect(err).NotTo(HaveOccurred())

mac1, err := libnet.GenerateRandomMac()
Expect(err).NotTo(HaveOccurred())
mac2, err := libnet.GenerateRandomMac()
Expect(err).NotTo(HaveOccurred())

vmi := libvmifact.NewFedora(
libvmi.WithInterface(v1.Interface{
Name: primaryLogicalNetName,
InterfaceBindingMethod: v1.InterfaceBindingMethod{
Masquerade: &v1.InterfaceMasquerade{},
},
MacAddress: mac1.String(),
State: v1.InterfaceStateLinkUp,
}),
libvmi.WithNetwork(v1.DefaultPodNetwork()),
libvmi.WithInterface(v1.Interface{
Name: secondary2LogicalNetName,
InterfaceBindingMethod: v1.InterfaceBindingMethod{
Bridge: &v1.InterfaceBridge{},
},
MacAddress: mac2.String(),
State: v1.InterfaceStateLinkDown,
}),
libvmi.WithNetwork(libvmi.MultusNetwork(secondary2LogicalNetName, nadName)),
)

vm := libvmi.NewVirtualMachine(vmi, libvmi.WithRunStrategy(v1.RunStrategyAlways))
vm, err = kubevirt.Client().VirtualMachine(testNamespace).Create(context.Background(), vm, metav1.CreateOptions{})
Eventually(matcher.ThisVM(vm)).WithTimeout(6 * time.Minute).WithPolling(3 * time.Second).Should(matcher.HaveConditionTrue(v1.VirtualMachineInstanceAgentConnected))
vmi, err = kubevirt.Client().VirtualMachineInstance(testNamespace).Get(context.Background(), vm.Name, metav1.GetOptions{})
Expect(err).ToNot(HaveOccurred())
Expect(console.LoginToFedora(vmi)).To(Succeed())

expectedIfaceStatuses := []v1.VirtualMachineInstanceNetworkInterface{
{Name: primaryLogicalNetName, LinkState: string(v1.InterfaceStateLinkUp)},
{Name: secondary2LogicalNetName, LinkState: string(v1.InterfaceStateLinkDown)},
}

Eventually(func() ([]v1.VirtualMachineInstanceNetworkInterface, error) {
vmi, err := kubevirt.Client().VirtualMachineInstance(vm.Namespace).Get(context.Background(), vm.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return normalizeIfaceStatuses(vmi.Status.Interfaces), nil
}).WithTimeout(60 * time.Second).Should(ConsistOf(expectedIfaceStatuses))

timeout := 5 * time.Second
Expect(console.RunCommand(vmi, assertLinkStateCmd(mac1.String(), v1.InterfaceStateLinkUp), timeout)).To(Succeed())
Expect(console.RunCommand(vmi, assertLinkStateCmd(mac2.String(), v1.InterfaceStateLinkDown), timeout)).To(Succeed())

})

})

func normalizeIfaceStatuses(ifaceStatuses []v1.VirtualMachineInstanceNetworkInterface) []v1.VirtualMachineInstanceNetworkInterface {
var result []v1.VirtualMachineInstanceNetworkInterface
for _, ifaceStatus := range ifaceStatuses {
result = append(result, v1.VirtualMachineInstanceNetworkInterface{Name: ifaceStatus.Name, LinkState: ifaceStatus.LinkState})
}
return result
}

func assertLinkStateCmd(mac string, desiredLinkState v1.InterfaceState) string {
const (
linkStateUPRegex = "'state[[:space:]]+UP'"
linkStateDOWNRegex = "'NO-CARRIER.+state[[:space:]]+DOWN'"
ipLinkTemplate = "ip -one link | grep %s | grep -E %s\n"
)

var linkStateRegex string

switch desiredLinkState {
case v1.InterfaceStateLinkUp:
linkStateRegex = linkStateUPRegex
case v1.InterfaceStateLinkDown:
linkStateRegex = linkStateDOWNRegex
}
return fmt.Sprintf(ipLinkTemplate, mac, linkStateRegex)
}

0 comments on commit 408167e

Please sign in to comment.