diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java index b0950376a93d..6a02461ca821 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPlugNicCommandWrapper.java @@ -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 { @@ -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"); + } + vm.attachDevice(interfaceDef.toString()); // apply default network rules on new nic @@ -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 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 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)); + } + + // 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; + } + } + + 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; + } + } } diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java index ed163787b112..5c63fcc0a2ba 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResourceTest.java @@ -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 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( + "" + + "
" + + ""); + final String interfaceDefStr = interfaceDef.toString(); doNothing().when(vm).attachDevice(interfaceDefStr);