Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
import org.libvirt.Domain;
import org.libvirt.LibvirtException;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@ResourceWrapper(handles = PlugNicCommand.class)
public final class LibvirtPlugNicCommandWrapper extends CommandWrapper<PlugNicCommand, Answer, LibvirtComputingResource> {
Expand Down Expand Up @@ -65,6 +69,17 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour
if (command.getDetails() != null) {
libvirtComputingResource.setInterfaceDefQueueSettings(command.getDetails(), null, interfaceDef);
}

// Explicitly assign PCI slot to ensure sequential NIC naming in the guest.
// Without this, libvirt auto-assigns the next free PCI slot which may be
// non-sequential with existing NICs (e.g. ens9 instead of ens5), causing
// guest network configuration to fail.
Integer nextSlot = findNextAvailablePciSlot(vm, pluggedNics);
if (nextSlot != null) {
interfaceDef.setSlot(nextSlot);
logger.debug("Assigning PCI slot 0x" + String.format("%02x", nextSlot) + " to hot-plugged NIC");
}
Comment on lines +73 to +81

vm.attachDevice(interfaceDef.toString());
Comment on lines +73 to 83

// apply default network rules on new nic
Expand Down Expand Up @@ -96,4 +111,54 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour
}
}
}

/**
* Finds the next available PCI slot for a hot-plugged NIC by examining
* all PCI slots currently in use by the domain. This ensures the new NIC
* gets a sequential PCI address relative to existing NICs, resulting in
* predictable interface naming in the guest OS (e.g. ens5 instead of ens9).
*/
private Integer findNextAvailablePciSlot(final Domain vm, final List<InterfaceDef> pluggedNics) {
try {
String domXml = vm.getXMLDesc(0);

// Defensive: getXMLDesc can return null on certain libvirt error paths (and is
// null in unit tests where the Domain mock isn't stubbed for this call). Fall
// back to letting libvirt auto-assign the PCI slot.
if (domXml == null) {
logger.debug("Domain XML unavailable, letting libvirt auto-assign PCI slot");
return null;
}

// Parse all PCI slot numbers currently in use
Set<Integer> usedSlots = new HashSet<>();
Pattern slotPattern = Pattern.compile("slot='0x([0-9a-fA-F]+)'");
Matcher matcher = slotPattern.matcher(domXml);
while (matcher.find()) {
usedSlots.add(Integer.parseInt(matcher.group(1), 16));
}
Comment on lines +123 to +139
Comment on lines +123 to +139

// Find the highest PCI slot used by existing NICs
int maxNicSlot = 0;
for (InterfaceDef pluggedNic : pluggedNics) {
if (pluggedNic.getSlot() != null && pluggedNic.getSlot() > maxNicSlot) {
maxNicSlot = pluggedNic.getSlot();
}
}

// Find next free slot starting from maxNicSlot + 1
// PCI slots range from 0x01 to 0x1f (slot 0 is reserved for host bridge)
for (int slot = maxNicSlot + 1; slot <= 0x1f; slot++) {
if (!usedSlots.contains(slot)) {
return slot;
}
}
Comment on lines +133 to +155
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I’d prefer these three loops to be three separate methods. (just a style remark but might be useful in code de-dup and stack-trace analysis at some point)

Comment on lines +141 to +155

logger.warn("No free PCI slots available, letting libvirt auto-assign");
return null;
} catch (LibvirtException e) {
logger.warn("Failed to get domain XML for PCI slot calculation, letting libvirt auto-assign", e);
return null;
}
Comment on lines +115 to +162
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3548,6 +3548,15 @@ public void testPlugNicCommandNoMatchMack() {
when(vifDriver.plug(nic, "Other PV", "", null)).thenReturn(interfaceDef);
when(interfaceDef.toString()).thenReturn("Interface");

// Stub vm.getXMLDesc(0) so findNextAvailablePciSlot can scan the domain XML
// for in-use PCI slots. Returning a minimal <domain> with a single NIC at
// slot 0x03 exercises the production parser without forcing the production
// code into its null-fallback path.
when(vm.getXMLDesc(0)).thenReturn(
"<domain><devices><interface type='bridge'>" +
"<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>" +
"</interface></devices></domain>");

final String interfaceDefStr = interfaceDef.toString();
doNothing().when(vm).attachDevice(interfaceDefStr);

Expand Down
Loading