Merge pull request #2847 from bensmrs/nic-attached-connected

Add `attached` and `connected` keys to NIC devices
This commit is contained in:
Stéphane Graber 2026-01-22 21:49:26 -05:00 committed by GitHub
commit 2591dceca4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 848 additions and 40 deletions

View File

@ -2974,3 +2974,10 @@ Adds support for selecting an SR-IOV network interface by vendor ID, product ID,
## `network_zones_dns_contact`
Adds a `dns.contact` configuration key to network zones.
## `nic_attached_connected`
This introduces two new properties for NICs:
* `attached`, behaving like the `attached` key for disk and USB devices;
* `connected`, setting the up/down link state for the NIC (when supported).

View File

@ -443,6 +443,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-infiniband end -->
<!-- config group devices-nic_bridged start -->
```{config:option} attached devices-nic_bridged
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_bridged
:managed: "no"
:shortdesc: "Boot priority for VMs (higher value boots first)"
@ -450,6 +458,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
```
```{config:option} connected devices-nic_bridged
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network"
:type: "bool"
```
```{config:option} host_name devices-nic_bridged
:default: "randomly assigned"
:managed: "no"
@ -668,6 +684,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-nic_bridged end -->
<!-- config group devices-nic_ipvlan start -->
```{config:option} attached devices-nic_ipvlan
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} gvrp devices-nic_ipvlan
:default: "false"
:shortdesc: "Register VLAN using GARP VLAN Registration Protocol"
@ -755,6 +779,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-nic_ipvlan end -->
<!-- config group devices-nic_macvlan start -->
```{config:option} attached devices-nic_macvlan
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_macvlan
:managed: "no"
:shortdesc: "Boot priority for VMs (higher value boots first)"
@ -762,6 +794,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
```
```{config:option} connected devices-nic_macvlan
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network (VM only)"
:type: "bool"
```
```{config:option} gvrp devices-nic_macvlan
:default: "false"
:managed: "no"
@ -841,6 +881,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
```
```{config:option} attached devices-nic_ovn
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_ovn
:managed: "no"
:shortdesc: "Boot priority for VMs (higher value boots first)"
@ -848,6 +896,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
```
```{config:option} connected devices-nic_ovn
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network (container support requires setting `acceleration` to `none`)"
:type: "bool"
```
```{config:option} host_name devices-nic_ovn
:default: "randomly assigned"
:managed: "no"
@ -1035,12 +1091,28 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-nic_ovn end -->
<!-- config group devices-nic_p2p start -->
```{config:option} boot.priotiry devices-nic_p2p
```{config:option} attached devices-nic_p2p
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_p2p
:shortdesc: "Boot priority for VMs (higher value boots first)"
:type: "integer"
```
```{config:option} connected devices-nic_p2p
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network"
:type: "bool"
```
```{config:option} host_name devices-nic_p2p
:default: "randomly assigned"
:shortdesc: "The name of the interface on the host"
@ -1120,6 +1192,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-nic_p2p end -->
<!-- config group devices-nic_physical start -->
```{config:option} attached devices-nic_physical
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_physical
:managed: "no"
:shortdesc: "Boot priority for VMs (higher value boots first)"
@ -1127,6 +1207,14 @@ With VMs, this option supports mounting file system disk devices and paths withi
```
```{config:option} connected devices-nic_physical
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network (VM only)"
:type: "bool"
```
```{config:option} gvrp devices-nic_physical
:default: "false"
:managed: "no"
@ -1189,6 +1277,22 @@ With VMs, this option supports mounting file system disk devices and paths withi
<!-- config group devices-nic_physical end -->
<!-- config group devices-nic_routed start -->
```{config:option} attached devices-nic_routed
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} connected devices-nic_routed
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network"
:type: "bool"
```
```{config:option} gvrp devices-nic_routed
:default: "false"
:shortdesc: "Register VLAN using GARP VLAN Registration Protocol"
@ -1375,6 +1479,14 @@ The custom policy routing table ID to add IPv6 static routes to (in addition to
<!-- config group devices-nic_routed end -->
<!-- config group devices-nic_sriov start -->
```{config:option} attached devices-nic_sriov
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is plugged in or not"
:type: "bool"
```
```{config:option} boot.priority devices-nic_sriov
:managed: "no"
:shortdesc: "Boot priority for VMs (higher value boots first)"
@ -1382,6 +1494,14 @@ The custom policy routing table ID to add IPv6 static routes to (in addition to
```
```{config:option} connected devices-nic_sriov
:default: "`true`"
:required: "no"
:shortdesc: "Whether the NIC is connected to the host network (VM only)"
:type: "bool"
```
```{config:option} hwaddr devices-nic_sriov
:default: "randomly assigned"
:managed: "no"

View File

@ -118,3 +118,14 @@ func (d *deviceCommon) generateHostName(prefix string, hwaddr string) (string, e
// Handle instances.nic.host_name random mode or where no MAC address supplied.
return network.RandomDevName(prefix), nil
}
// setNICLink sets the link status (connected/disconnected) for the given NIC.
func (d *deviceCommon) setNICLink() error {
runConf := deviceConfig.RunConfig{}
runConf.NetworkInterface = []deviceConfig.RunConfigItem{
{Key: "devName", Value: d.name},
{Key: "connected", Value: d.config["connected"]},
}
return d.inst.DeviceEventHandler(&runConf)
}

View File

@ -611,12 +611,12 @@ func networkSetupHostVethLimits(d *deviceCommon, oldConfig deviceConfig.Device,
// networkClearHostVethLimits clears any network rate limits to the veth device specified in the config.
func networkClearHostVethLimits(d *deviceCommon) error {
err := d.state.Firewall.InstanceClearNetPrio(d.inst.Project().Name, d.inst.Name(), d.config["host_name"])
if err != nil {
return err
// Detached NICs cannot be cleaned up this way.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil
}
return nil
return d.state.Firewall.InstanceClearNetPrio(d.inst.Project().Name, d.inst.Name(), d.config["host_name"])
}
// networkValidGateway validates the gateway value.

View File

@ -60,6 +60,8 @@ func nicValidationRules(requiredFields []string, optionalFields []string, instCo
"vendorid": validate.Optional(validate.IsDeviceID),
"productid": validate.Optional(validate.IsDeviceID),
"pci": validate.IsPCIAddress,
"attached": validate.Optional(validate.IsBool),
"connected": validate.Optional(validate.IsBool),
}
validators := map[string]func(value string) error{}

View File

@ -316,6 +316,24 @@ func (d *nicBridged) validateConfig(instConf instance.ConfigReader) error {
// managed: no
// shortdesc: Override the bus for the device (can be `virtio` or `usb`) (VM only)
"io.bus",
// gendoc:generate(entity=devices, group=nic_bridged, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_bridged, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network
"connected",
}
// checkWithManagedNetwork validates the device's settings against the managed network.
@ -693,7 +711,7 @@ func (d *nicBridged) UpdatableFields(oldDevice Type) []string {
return []string{}
}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes", "ipv4.routes.external", "ipv6.routes.external", "ipv4.address", "ipv6.address", "security.mac_filtering", "security.ipv4_filtering", "security.ipv6_filtering", "security.acls", "security.acls.default.egress.action", "security.acls.default.egress.logged", "security.acls.default.ingress.action", "security.acls.default.ingress.logged"}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes", "ipv4.routes.external", "ipv6.routes.external", "ipv4.address", "ipv6.address", "security.mac_filtering", "security.ipv4_filtering", "security.ipv6_filtering", "security.acls", "security.acls.default.egress.action", "security.acls.default.egress.logged", "security.acls.default.ingress.action", "security.acls.default.ingress.logged", "connected"}
}
// Add is run when a device is added to a non-snapshot instance whether or not the instance is running.
@ -726,6 +744,11 @@ func (d *nicBridged) PreStartCheck() error {
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicBridged) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -916,6 +939,7 @@ func (d *nicBridged) Start() (*deviceConfig.RunConfig, error) {
{Key: "flags", Value: "up"},
{Key: "link", Value: peerName},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.config["io.bus"] == "usb" {
@ -1049,6 +1073,13 @@ func (d *nicBridged) Update(oldDevices deviceConfig.Devices, isRunning bool) err
return err
}
if isRunning {
err = d.setNICLink()
if err != nil {
return err
}
}
reverter.Success()
return nil

View File

@ -108,6 +108,15 @@ func (d *nicIPVLAN) validateConfig(instConf instance.ConfigReader) error {
// default: false
// shortdesc: Register VLAN using GARP VLAN Registration Protocol
"gvrp",
// gendoc:generate(entity=devices, group=nic_ipvlan, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
}
rules := nicValidationRules(requiredFields, optionalFields, instConf)
@ -301,6 +310,11 @@ func (d *nicIPVLAN) validateEnvironment() error {
// Start is run when the instance is starting up (IPVLAN doesn't support hot plugging).
func (d *nicIPVLAN) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err

View File

@ -128,6 +128,24 @@ func (d *nicMACVLAN) validateConfig(instConf instance.ConfigReader) error {
// managed: no
// shortdesc: Override the bus for the device (can be `virtio` or `usb`) (VM only)
"io.bus",
// gendoc:generate(entity=devices, group=nic_macvlan, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_macvlan, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network (VM only)
"connected",
}
// Check that if network proeperty is set that conflicting keys are not present.
@ -175,6 +193,10 @@ func (d *nicMACVLAN) validateConfig(instConf instance.ConfigReader) error {
requiredFields = append(requiredFields, "parent")
}
if instConf.Type() != instancetype.VM && d.config["connected"] != "" {
return errors.New("The \"connected\" option is only supported on virtual machines for macvlan NICs")
}
err := d.config.Validate(nicValidationRules(requiredFields, optionalFields, instConf))
if err != nil {
return err
@ -213,6 +235,11 @@ func (d *nicMACVLAN) validateEnvironment() error {
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicMACVLAN) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -339,6 +366,7 @@ func (d *nicMACVLAN) Start() (*deviceConfig.RunConfig, error) {
{Key: "flags", Value: "up"},
{Key: "link", Value: saveData["host_name"]},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.config["io.bus"] == "usb" {
@ -363,9 +391,12 @@ func (d *nicMACVLAN) Stop() (*deviceConfig.RunConfig, error) {
v := d.volatileGet()
runConf := deviceConfig.RunConfig{
PostHooks: []func() error{d.postStop},
NetworkInterface: []deviceConfig.RunConfigItem{
}
if util.IsTrueOrEmpty(d.config["attached"]) {
runConf.NetworkInterface = []deviceConfig.RunConfigItem{
{Key: "link", Value: v["host_name"]},
},
}
}
return &runConf, nil
@ -408,3 +439,23 @@ func (d *nicMACVLAN) postStop() error {
return nil
}
// UpdatableFields returns a list of fields that can be updated without triggering a device remove & add.
func (d *nicMACVLAN) UpdatableFields(oldDevice Type) []string {
// Check old and new device types match.
_, match := oldDevice.(*nicMACVLAN)
if !match {
return []string{}
}
return []string{"connected"}
}
// Update applies configuration changes to a started device.
func (d *nicMACVLAN) Update(oldDevices deviceConfig.Devices, isRunning bool) error {
if isRunning {
return d.setNICLink()
}
return nil
}

View File

@ -79,7 +79,7 @@ func (d *nicOVN) UpdatableFields(oldDevice Type) []string {
return []string{}
}
return []string{"security.acls", "limits.ingress", "limits.egress", "limits.max", "limits.priority"}
return []string{"security.acls", "limits.ingress", "limits.egress", "limits.max", "limits.priority", "connected"}
}
// validateConfig checks the supplied config for correctness.
@ -317,6 +317,24 @@ func (d *nicOVN) validateConfig(instConf instance.ConfigReader) error {
// managed: no
// shortdesc: The priority for outgoing traffic, to be used by the kernel queuing discipline to prioritize network packets
"limits.priority",
// gendoc:generate(entity=devices, group=nic_ovn, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_ovn, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network (container support requires setting `acceleration` to `none`)
"connected",
}
// The NIC's network may be a non-default project, so lookup project and get network's project name.
@ -575,6 +593,11 @@ func (d *nicOVN) validateConfig(instConf instance.ConfigReader) error {
}
}
// The connected option can only be handled properly on containers if acceleration is set to none.
if d.config["connected"] != "" && !d.canSetLink(instConf) {
return errors.New("The \"connected\" option requires setting acceleration=none on containers for OVN NICs")
}
return nil
}
@ -693,6 +716,11 @@ func (d *nicOVN) init(inst instance.Instance, s *state.State, name string, conf
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicOVN) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -987,15 +1015,22 @@ func (d *nicOVN) Start() (*deviceConfig.RunConfig, error) {
{Key: "link", Value: peerName},
}
if d.canSetLink(nil) {
runConf.NetworkInterface = append(runConf.NetworkInterface, deviceConfig.RunConfigItem{Key: "connected", Value: d.config["connected"]})
}
instType := d.inst.Type()
if instType == instancetype.VM {
runConf.NetworkInterface = append(runConf.NetworkInterface,
[]deviceConfig.RunConfigItem{
{Key: "devName", Value: d.name},
{Key: "mtu", Value: fmt.Sprintf("%d", mtu)},
}...)
if d.config["acceleration"] == "sriov" {
runConf.NetworkInterface = append(runConf.NetworkInterface,
[]deviceConfig.RunConfigItem{
{Key: "devName", Value: d.name},
{Key: "pciSlotName", Value: vfPCIDev.SlotName},
{Key: "pciIOMMUGroup", Value: fmt.Sprintf("%d", pciIOMMUGroup)},
{Key: "mtu", Value: fmt.Sprintf("%d", mtu)},
}...)
} else if d.config["acceleration"] == "vdpa" {
if vDPADevice == nil {
@ -1004,20 +1039,16 @@ func (d *nicOVN) Start() (*deviceConfig.RunConfig, error) {
runConf.NetworkInterface = append(runConf.NetworkInterface,
[]deviceConfig.RunConfigItem{
{Key: "devName", Value: d.name},
{Key: "pciSlotName", Value: vfPCIDev.SlotName},
{Key: "pciIOMMUGroup", Value: fmt.Sprintf("%d", pciIOMMUGroup)},
{Key: "maxVQP", Value: fmt.Sprintf("%d", vDPADevice.MaxVQs/2)},
{Key: "vDPADevName", Value: vDPADevice.Name},
{Key: "vhostVDPAPath", Value: vDPADevice.VhostVDPA.Path},
{Key: "mtu", Value: fmt.Sprintf("%d", mtu)},
}...)
} else {
runConf.NetworkInterface = append(runConf.NetworkInterface,
[]deviceConfig.RunConfigItem{
{Key: "devName", Value: d.name},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "mtu", Value: fmt.Sprintf("%d", mtu)},
}...)
}
} else if instType == instancetype.Container {
@ -1142,6 +1173,10 @@ func (d *nicOVN) Update(oldDevices deviceConfig.Devices, isRunning bool) error {
return err
}
if isRunning && d.canSetLink(nil) {
return d.setNICLink()
}
return nil
}
@ -1534,3 +1569,15 @@ func (d *nicOVN) setupHostNIC(hostName string, ovnPortName ovn.OVNSwitchPort) (r
return cleanup, err
}
// canSetLink determines whether the device supports setting a link state.
func (d *nicOVN) canSetLink(instConf instance.ConfigReader) bool {
var instType instancetype.Type
if instConf != nil {
instType = instConf.Type()
} else {
instType = d.inst.Type()
}
return instType == instancetype.VM || slices.Contains([]string{"", "none"}, d.config["acceleration"])
}

View File

@ -111,7 +111,7 @@ func (d *nicP2P) validateConfig(instConf instance.ConfigReader) error {
// shortdesc: Comma-delimited list of IPv6 static routes to add on host to NIC
"ipv6.routes",
// gendoc:generate(entity=devices, group=nic_p2p, key=boot.priotiry)
// gendoc:generate(entity=devices, group=nic_p2p, key=boot.priority)
//
// ---
// type: integer
@ -125,6 +125,24 @@ func (d *nicP2P) validateConfig(instConf instance.ConfigReader) error {
// default: `virtio`
// shortdesc: Override the bus for the device (can be `virtio` or `usb`) (VM only)
"io.bus",
// gendoc:generate(entity=devices, group=nic_p2p, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_p2p, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network
"connected",
}
err := d.config.Validate(nicValidationRules([]string{}, optionalFields, instConf))
@ -152,11 +170,16 @@ func (d *nicP2P) UpdatableFields(oldDevice Type) []string {
return []string{}
}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes"}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "ipv4.routes", "ipv6.routes", "connected"}
}
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicP2P) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -232,6 +255,7 @@ func (d *nicP2P) Start() (*deviceConfig.RunConfig, error) {
{Key: "flags", Value: "up"},
{Key: "link", Value: peerName},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.config["io.bus"] == "usb" {
@ -284,7 +308,7 @@ func (d *nicP2P) Update(oldDevices deviceConfig.Devices, isRunning bool) error {
return err
}
return nil
return d.setNICLink()
}
// Stop is run when the device is removed from the instance.

View File

@ -85,6 +85,24 @@ func (d *nicPhysical) validateConfig(instConf instance.ConfigReader) error {
// managed: no
// shortdesc: The Maximum Transmit Unit (MTU) of the new interface
"mtu",
// gendoc:generate(entity=devices, group=nic_physical, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_physical, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network (VM only)
"connected",
}
if instConf.Type() == instancetype.Container || instConf.Type() == instancetype.Any {
@ -168,6 +186,10 @@ func (d *nicPhysical) validateConfig(instConf instance.ConfigReader) error {
requiredFields = append(requiredFields, "parent")
}
if instConf.Type() != instancetype.VM && d.config["connected"] != "" {
return errors.New("The \"connected\" option is only supported on virtual machines for physical NICs")
}
err := d.config.Validate(nicValidationRules(requiredFields, optionalFields, instConf))
if err != nil {
return err
@ -196,6 +218,11 @@ func (d *nicPhysical) validateEnvironment() error {
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicPhysical) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -332,6 +359,7 @@ func (d *nicPhysical) Start() (*deviceConfig.RunConfig, error) {
{Key: "name", Value: d.config["name"]},
{Key: "flags", Value: "up"},
{Key: "link", Value: saveData["host_name"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.inst.Type() == instancetype.VM {
@ -443,7 +471,7 @@ func (d *nicPhysical) Stop() (*deviceConfig.RunConfig, error) {
DeviceName: fmt.Sprintf("%s-%s-%s", d.name, v["last_state.usb.bus"], v["last_state.usb.device"]),
HostDevicePath: fmt.Sprintf("/dev/bus/usb/%s/%s", v["last_state.usb.bus"], v["last_state.usb.device"]),
})
} else {
} else if util.IsTrueOrEmpty(d.config["attached"]) {
// Handle all other NICs.
runConf.NetworkInterface = []deviceConfig.RunConfigItem{
{Key: "link", Value: v["host_name"]},
@ -536,3 +564,23 @@ func IsPhysicalNICWithBridge(s *state.State, deviceProjectName string, d deviceC
return false
}
// UpdatableFields returns a list of fields that can be updated without triggering a device remove & add.
func (d *nicPhysical) UpdatableFields(oldDevice Type) []string {
// Check old and new device types match.
_, match := oldDevice.(*nicPhysical)
if !match {
return []string{}
}
return []string{"connected"}
}
// Update applies configuration changes to a started device.
func (d *nicPhysical) Update(oldDevices deviceConfig.Devices, isRunning bool) error {
if isRunning {
return d.setNICLink()
}
return nil
}

View File

@ -44,7 +44,7 @@ func (d *nicRouted) UpdatableFields(oldDevice Type) []string {
return []string{}
}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority"}
return []string{"limits.ingress", "limits.egress", "limits.max", "limits.priority", "connected"}
}
// validateConfig checks the supplied config for correctness.
@ -243,6 +243,24 @@ func (d *nicRouted) validateConfig(instConf instance.ConfigReader) error {
// default: `virtio`
// shortdesc: Override the bus for the device (can be `virtio` or `usb`) (VM only)
"io.bus",
// gendoc:generate(entity=devices, group=nic_routed, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_routed, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network
"connected",
}
rules := nicValidationRules(requiredFields, optionalFields, instConf)
@ -467,6 +485,11 @@ func (d *nicRouted) checkIPAvailability(parent string) error {
// Start is run when the instance is starting up (Routed mode doesn't support hot plugging).
func (d *nicRouted) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -760,6 +783,7 @@ func (d *nicRouted) Start() (*deviceConfig.RunConfig, error) {
{Key: "flags", Value: "up"},
{Key: "link", Value: peerName},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.config["io.bus"] == "usb" {
@ -856,6 +880,8 @@ func (d *nicRouted) Update(oldDevices deviceConfig.Devices, isRunning bool) erro
if err != nil {
return err
}
return d.setNICLink()
}
return nil

View File

@ -133,6 +133,24 @@ func (d *nicSRIOV) validateConfig(instConf instance.ConfigReader) error {
// required: no
// shortdesc: The PCI address of the parent host device
"pci",
// gendoc:generate(entity=devices, group=nic_sriov, key=attached)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is plugged in or not
"attached",
// gendoc:generate(entity=devices, group=nic_sriov, key=connected)
//
// ---
// type: bool
// default: `true`
// required: no
// shortdesc: Whether the NIC is connected to the host network (VM only)
"connected",
}
// Check that if network property is set that conflicting keys are not present.
@ -180,6 +198,10 @@ func (d *nicSRIOV) validateConfig(instConf instance.ConfigReader) error {
requiredFields = append(requiredFields, "parent")
}
if instConf.Type() != instancetype.VM && d.config["connected"] != "" {
return errors.New("The \"connected\" option is only supported on virtual machines for SR-IOV NICs")
}
err := d.config.Validate(nicValidationRules(requiredFields, optionalFields, instConf))
if err != nil {
return err
@ -240,6 +262,11 @@ func (d *nicSRIOV) validateEnvironment() error {
// Start is run when the device is added to a running instance or instance is starting up.
func (d *nicSRIOV) Start() (*deviceConfig.RunConfig, error) {
// Ignore detached NICs.
if !util.IsTrueOrEmpty(d.config["attached"]) {
return nil, nil
}
err := d.validateEnvironment()
if err != nil {
return nil, err
@ -310,6 +337,7 @@ func (d *nicSRIOV) Start() (*deviceConfig.RunConfig, error) {
{Key: "flags", Value: "up"},
{Key: "link", Value: saveData["host_name"]},
{Key: "hwaddr", Value: d.config["hwaddr"]},
{Key: "connected", Value: d.config["connected"]},
}
if d.inst.Type() == instancetype.VM {
@ -329,9 +357,12 @@ func (d *nicSRIOV) Stop() (*deviceConfig.RunConfig, error) {
v := d.volatileGet()
runConf := deviceConfig.RunConfig{
PostHooks: []func() error{d.postStop},
NetworkInterface: []deviceConfig.RunConfigItem{
}
if util.IsTrueOrEmpty(d.config["attached"]) {
runConf.NetworkInterface = []deviceConfig.RunConfigItem{
{Key: "link", Value: v["host_name"]},
},
}
}
return &runConf, nil
@ -504,3 +535,23 @@ func nicSelected(device deviceConfig.Device, nic api.ResourcesNetworkCard) bool
return false
}
// UpdatableFields returns a list of fields that can be updated without triggering a device remove & add.
func (d *nicSRIOV) UpdatableFields(oldDevice Type) []string {
// Check old and new device types match.
_, match := oldDevice.(*nicSRIOV)
if !match {
return []string{}
}
return []string{"connected"}
}
// Update applies configuration changes to a started device.
func (d *nicSRIOV) Update(oldDevices deviceConfig.Devices, isRunning bool) error {
if isRunning {
return d.setNICLink()
}
return nil
}

View File

@ -60,6 +60,7 @@ import (
"github.com/lxc/incus/v6/internal/server/instance"
"github.com/lxc/incus/v6/internal/server/instance/instancetype"
"github.com/lxc/incus/v6/internal/server/instance/operationlock"
"github.com/lxc/incus/v6/internal/server/ip"
"github.com/lxc/incus/v6/internal/server/lifecycle"
"github.com/lxc/incus/v6/internal/server/locking"
"github.com/lxc/incus/v6/internal/server/metrics"
@ -1493,7 +1494,7 @@ func (d *lxc) deviceStart(dev device.Device, instanceRunning bool) (*deviceConfi
// Attach network interface if requested.
if len(runConf.NetworkInterface) > 0 {
err = d.deviceAttachNIC(configCopy, runConf.NetworkInterface)
err = d.deviceAttachNIC(dev.Name(), configCopy, runConf.NetworkInterface)
if err != nil {
return nil, err
}
@ -1570,16 +1571,18 @@ func (d *lxc) deviceAddCgroupRules(cgroups []deviceConfig.RunConfigItem) error {
}
// deviceAttachNIC live attaches a NIC device to a container.
func (d *lxc) deviceAttachNIC(configCopy map[string]string, netIF []deviceConfig.RunConfigItem) error {
devName := ""
func (d *lxc) deviceAttachNIC(devName string, configCopy map[string]string, netIF []deviceConfig.RunConfigItem) error {
ctDevName := ""
connected := true
for _, dev := range netIF {
if dev.Key == "link" {
devName = dev.Value
break
ctDevName = dev.Value
} else if dev.Key == "connected" {
connected = util.IsTrueOrEmpty(dev.Value)
}
}
if devName == "" {
if ctDevName == "" {
return errors.New("Device didn't provide a link property to use")
}
@ -1590,12 +1593,12 @@ func (d *lxc) deviceAttachNIC(configCopy map[string]string, netIF []deviceConfig
}
// Add the interface to the container.
err = cc.AttachInterface(devName, configCopy["name"])
err = cc.AttachInterface(ctDevName, configCopy["name"])
if err != nil {
return fmt.Errorf("Failed to attach interface: %s to %s: %w", devName, configCopy["name"], err)
return fmt.Errorf("Failed to attach interface: %s to %s: %w", ctDevName, configCopy["name"], err)
}
return nil
return d.setNICLink(devName, connected, true)
}
// deviceStop loads a new device and calls its Stop() function.
@ -1817,6 +1820,25 @@ func (d *lxc) DeviceEventHandler(runConf *deviceConfig.RunConfig) error {
}
}
// Handle NIC reconfiguration.
var devName string
var connected bool
for _, dev := range runConf.NetworkInterface {
switch dev.Key {
case "devName":
devName = dev.Value
case "connected":
connected = util.IsTrueOrEmpty(dev.Value)
}
}
if devName != "" {
err := d.setNICLink(devName, connected, false)
if err != nil {
return err
}
}
// Run any post hooks requested.
err := d.runHooks(runConf.PostHooks)
if err != nil {
@ -2302,6 +2324,15 @@ func (d *lxc) startCommon() (string, []func() error, error) {
}
for _, nicItem := range runConf.NetworkInterface {
// The connected state is not a LXC configuration key; we defer its handling to a post hook.
if nicItem.Key == "connected" {
runConf.PostHooks = append(runConf.PostHooks, func() error {
return d.setNICLink(dev.Name(), util.IsTrueOrEmpty(nicItem.Value), true)
})
continue
}
err = lxcSetConfigItem(cc, fmt.Sprintf("%s.%d.%s", networkKeyPrefix, nicID, nicItem.Key), nicItem.Value)
if err != nil {
return "", nil, fmt.Errorf("Failed to setup device network interface %q: %w", dev.Name(), err)
@ -4813,6 +4844,12 @@ func (d *lxc) Update(args db.InstanceArgs, userRequested bool) error {
return []string{} // Couldn't create Device, so this cannot be an update.
}
// Detached devices need to be fully recreated on update so that the update logic doesn't
// try to access non-existing LXC devices.
if !util.IsTrueOrEmpty(oldDevice["attached"]) {
return []string{}
}
return newDevType.UpdatableFields(oldDevType)
})
@ -9452,3 +9489,30 @@ func (d *lxc) CreateQcow2Snapshot(snapName string, backingFilename string) error
func (d *lxc) DeleteQcow2Snapshot(snapshotIndex int, backingFilename string) error {
return nil
}
// setNICLink sets the link status of the given device.
func (d *lxc) setNICLink(devName string, connected bool, assumeUp bool) error {
// This check is added so that devices that cannot handle link states do not fail to initialize.
if connected && assumeUp {
return nil
}
link, err := ip.LinkByName(d.localConfig["volatile."+devName+".host_name"])
if err != nil {
return fmt.Errorf("Failed to find interface %s: %w", devName, err)
}
if connected {
err = link.SetUp()
if err != nil {
return fmt.Errorf("Failed to bring %s up: %w", devName, err)
}
} else {
err = link.SetDown()
if err != nil {
return fmt.Errorf("Failed to bring %s down: %w", devName, err)
}
}
return nil
}

View File

@ -4929,6 +4929,7 @@ func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndex
defer reverter.Fail()
var devName, nicName, devHwaddr, pciSlotName, pciIOMMUGroup, vDPADevName, vhostVDPAPath, maxVQP string
var connected bool
for _, nicItem := range nicConfig {
if nicItem.Key == "devName" {
devName = nicItem.Value
@ -4946,6 +4947,8 @@ func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndex
vhostVDPAPath = nicItem.Value
} else if nicItem.Key == "maxVQP" {
maxVQP = nicItem.Value
} else if nicItem.Key == "connected" {
connected = util.IsTrueOrEmpty(nicItem.Value)
}
}
@ -5062,7 +5065,7 @@ func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndex
qemuDev["netdev"] = qemuNetDev["id"].(string)
qemuDev["mac"] = devHwaddr
err = m.AddNIC(qemuNetDev, qemuDev)
err = m.AddNIC(qemuNetDev, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
@ -5171,7 +5174,7 @@ func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndex
qemuDev["iommu_platform"] = true
qemuDev["disable-legacy"] = true
err = m.AddNIC(qemuNetDev, qemuDev)
err = m.AddNIC(qemuNetDev, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
@ -5204,7 +5207,7 @@ func (d *qemu) addNetDevConfig(busName string, qemuDev map[string]any, bootIndex
}
monHook = func(m *qmp.Monitor) error {
err := m.AddNIC(nil, qemuDev)
err := m.AddNIC(nil, qemuDev, connected)
if err != nil {
return fmt.Errorf("Failed setting up device %q: %w", devName, err)
}
@ -6327,6 +6330,12 @@ func (d *qemu) Update(args db.InstanceArgs, userRequested bool) error {
return []string{} // Couldn't create Device, so this cannot be an update.
}
// Detached devices need to be fully recreated on update so that the update logic doesn't
// try to access non-existing QEMU devices.
if !util.IsTrueOrEmpty(oldDevice["attached"]) {
return []string{}
}
return newDevType.UpdatableFields(oldDevType)
})
@ -9159,6 +9168,34 @@ func (d *qemu) DeviceEventHandler(runConf *deviceConfig.RunConfig) error {
}
}
// Handle NIC reconfiguration.
var devName string
var connected bool
for _, dev := range runConf.NetworkInterface {
switch dev.Key {
case "devName":
devName = dev.Value
case "connected":
connected = util.IsTrueOrEmpty(dev.Value)
}
}
if devName != "" {
// Get the QMP monitor.
m, err := d.qmpConnect()
if err != nil {
return err
}
// Figure out the QEMU device ID.
devID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, linux.PathNameEncode(devName))
err = m.SetNICLink(devID, connected)
if err != nil {
return fmt.Errorf("Failed setting NIC device link status: %w", err)
}
}
return nil
}

View File

@ -867,7 +867,7 @@ func (m *Monitor) RemoveDevice(deviceID string) error {
}
// AddNIC adds a NIC device.
func (m *Monitor) AddNIC(netDev map[string]any, device map[string]any) error {
func (m *Monitor) AddNIC(netDev map[string]any, device map[string]any, connected bool) error {
reverter := revert.New()
defer reverter.Fail()
@ -894,6 +894,16 @@ func (m *Monitor) AddNIC(netDev map[string]any, device map[string]any) error {
return fmt.Errorf("Failed adding NIC device: %w", err)
}
id, ok := device["id"].(string)
if !ok {
return errors.New("NIC device must have an id")
}
err = m.SetNICLink(id, connected)
if err != nil {
return fmt.Errorf("Failed setting NIC device link status: %w", err)
}
reverter.Success()
return nil
@ -1562,3 +1572,15 @@ func (m *Monitor) DumpGuestMemory(path string, format string) error {
return m.Run("dump-guest-memory", args, &queryResp)
}
// SetNICLink sets the link status of the given device.
func (m *Monitor) SetNICLink(id string, connected bool) error {
var args struct {
Name string `json:"name"`
Up bool `json:"up"`
}
args.Name = id
args.Up = connected
return m.Run("set_link", args, nil)
}

View File

@ -97,11 +97,9 @@ func LinkByName(name string) (LinkInfo, error) {
if link.Attrs().ParentIndex != 0 {
parentLink, err := netlink.LinkByIndex(link.Attrs().ParentIndex)
if err != nil {
return LinkInfo{}, err
if err == nil {
parent = parentLink.Attrs().Name
}
parent = parentLink.Attrs().Name
}
if link.Attrs().MasterIndex != 0 {

View File

@ -484,6 +484,15 @@
},
"nic_bridged": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
@ -492,6 +501,15 @@
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network",
"type": "bool"
}
},
{
"host_name": {
"default": "randomly assigned",
@ -741,6 +759,15 @@
},
"nic_ipvlan": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"gvrp": {
"default": "false",
@ -843,6 +870,15 @@
},
"nic_macvlan": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
@ -851,6 +887,15 @@
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network (VM only)",
"type": "bool"
}
},
{
"gvrp": {
"default": "false",
@ -942,6 +987,15 @@
"type": "string"
}
},
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
@ -950,6 +1004,15 @@
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network (container support requires setting `acceleration` to `none`)",
"type": "bool"
}
},
{
"host_name": {
"default": "randomly assigned",
@ -1165,12 +1228,30 @@
"nic_p2p": {
"keys": [
{
"boot.priotiry": {
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
"shortdesc": "Boot priority for VMs (higher value boots first)",
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network",
"type": "bool"
}
},
{
"host_name": {
"default": "randomly assigned",
@ -1264,6 +1345,15 @@
},
"nic_physical": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
@ -1272,6 +1362,15 @@
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network (VM only)",
"type": "bool"
}
},
{
"gvrp": {
"default": "false",
@ -1344,6 +1443,24 @@
},
"nic_routed": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network",
"type": "bool"
}
},
{
"gvrp": {
"default": "false",
@ -1558,6 +1675,15 @@
},
"nic_sriov": {
"keys": [
{
"attached": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is plugged in or not",
"type": "bool"
}
},
{
"boot.priority": {
"longdesc": "",
@ -1566,6 +1692,15 @@
"type": "integer"
}
},
{
"connected": {
"default": "`true`",
"longdesc": "",
"required": "no",
"shortdesc": "Whether the NIC is connected to the host network (VM only)",
"type": "bool"
}
},
{
"hwaddr": {
"default": "randomly assigned",

View File

@ -516,6 +516,7 @@ var APIExtensions = []string{
"file_delete_force",
"nic_sriov_select_ext",
"network_zones_dns_contact",
"nic_attached_connected",
}
// APIExtensionsCount returns the number of available API extensions.

View File

@ -709,6 +709,30 @@ test_container_devices_nic_bridged() {
! incus start foo2 || false
incus delete -f foo foo2
# Test attached and connected keys.
incus launch testimage foo
incus config device add foo eth0 nic nictype=bridged name=eth0 parent=${brName}
incus exec foo ip link set eth0 up
[ "$(incus file pull foo/sys/class/net/eth0/operstate -)" = "up" ]
incus config device set foo eth0 connected=false
incus exec foo ip link set eth0 up
[ "$(incus file pull foo/sys/class/net/eth0/operstate -)" = "down" ]
incus config device set foo eth0 attached=false
! incus file pull foo/sys/class/net/eth0/operstate - || false
incus config device set foo eth0 connected=true
! incus file pull foo/sys/class/net/eth0/operstate - || false
incus config device set foo eth0 attached=true
incus exec foo ip link set eth0 up
[ "$(incus file pull foo/sys/class/net/eth0/operstate -)" = "up" ]
incus config device set foo eth0 connected=false
incus exec foo ip link set eth0 up
[ "$(incus file pull foo/sys/class/net/eth0/operstate -)" = "down" ]
# Check that it survives a reboot.
incus restart foo
incus exec foo ip link set eth0 up
[ "$(incus file pull foo/sys/class/net/eth0/operstate -)" = "down" ]
incus delete -f foo
# Check we haven't left any NICS lying around.
endNicCount=$(find /sys/class/net | wc -l)
if [ "$startNicCount" != "$endNicCount" ]; then

View File

@ -115,6 +115,18 @@ test_container_devices_nic_macvlan() {
incus exec "${ctName}" -- ping -c2 -W5 "192.0.2.2${ipRand}"
incus exec "${ctName}" -- ping6 -c2 -W5 "2001:db8::2${ipRand}"
incus config device remove "${ctName}" eth0
# Test attached key.
incus config device add "${ctName}" eth0 nic network="${ctName}net" name=eth0
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 attached=false
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 attached=true
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device remove "${ctName}" eth0
incus network delete "${ctName}net"
# Check we haven't left any NICS lying around.

View File

@ -358,6 +358,30 @@ test_container_devices_nic_p2p() {
# Test hotplugging nic with new name (rather than updating existing nic).
incus config device add "${ctName}" eth1 nic nictype=p2p
incus config device remove "${ctName}" eth1
# Test attached and connected keys.
incus config device add "${ctName}" eth0 nic nictype=p2p name=eth0
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
incus config device set "${ctName}" eth0 attached=false
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 connected=true
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 attached=true
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
# Check that it survives a reboot.
incus restart "${ctName}"
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
incus stop -f "${ctName}"
# Check we haven't left any NICS lying around.

View File

@ -216,6 +216,30 @@ test_container_devices_nic_physical() {
fi
fi
incus config device remove "${ctName}" eth1
# Test attached and connected keys.
incus config device add "${ctName}" eth0 nic nictype=p2p name=eth0
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
incus config device set "${ctName}" eth0 attached=false
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 connected=true
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 attached=true
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
# Check that it survives a reboot.
incus restart "${ctName}"
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
incus delete "${ctName}" -f
incus network delete "${networkName}"

View File

@ -267,6 +267,31 @@ test_container_devices_nic_routed() {
false
fi
incus config device remove "${ctName}" eth0
incus start "${ctName}"
# Test attached and connected keys.
incus config device add "${ctName}" eth0 nic network="${ctName}" name=eth0
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
incus config device set "${ctName}" eth0 attached=false
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 connected=true
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 attached=true
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 connected=false
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
# Check that it survives a reboot.
incus restart "${ctName}"
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "down" ]
# Cleanup routed checks
incus delete "${ctName}" -f
incus delete "${ctName}2" -f

View File

@ -154,7 +154,17 @@ test_container_devices_nic_sriov() {
false
fi
# Test attached key.
incus config device add "${ctName}" eth0 nic network="${ctName}" name=eth0
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device set "${ctName}" eth0 attached=false
! incus file pull "${ctName}/sys/class/net/eth0/operstate" - || false
incus config device set "${ctName}" eth0 attached=true
incus exec "${ctName}" ip link set eth0 up
[ "$(incus file pull "${ctName}/sys/class/net/eth0/operstate" -)" = "up" ]
incus config device remove "${ctName}" eth0
incus network delete "${ctName}net"
incus delete -f "${ctName}"