|
30 | 30 | import com.cloud.resource.CommandWrapper; |
31 | 31 | import com.cloud.resource.ResourceWrapper; |
32 | 32 | import com.cloud.vm.VirtualMachine; |
| 33 | +import org.apache.cloudstack.utils.security.ParserUtils; |
33 | 34 | import org.libvirt.Connect; |
34 | 35 | import org.libvirt.Domain; |
35 | 36 | import org.libvirt.LibvirtException; |
36 | | - |
| 37 | +import org.w3c.dom.Document; |
| 38 | +import org.w3c.dom.Element; |
| 39 | +import org.w3c.dom.NodeList; |
| 40 | +import org.xml.sax.InputSource; |
| 41 | +import org.xml.sax.SAXException; |
| 42 | + |
| 43 | +import javax.xml.parsers.DocumentBuilder; |
| 44 | +import javax.xml.parsers.ParserConfigurationException; |
| 45 | +import java.io.IOException; |
| 46 | +import java.io.StringReader; |
37 | 47 | import java.util.HashSet; |
38 | 48 | import java.util.List; |
39 | 49 | import java.util.Set; |
40 | | -import java.util.regex.Matcher; |
41 | | -import java.util.regex.Pattern; |
42 | 50 |
|
43 | 51 | @ResourceWrapper(handles = PlugNicCommand.class) |
44 | 52 | public final class LibvirtPlugNicCommandWrapper extends CommandWrapper<PlugNicCommand, Answer, LibvirtComputingResource> { |
45 | 53 |
|
| 54 | + /** Highest PCI slot number on a bus (0x00 is the host bridge). */ |
| 55 | + private static final int MAX_PCI_SLOT = 0x1f; |
46 | 56 |
|
47 | 57 | @Override |
48 | 58 | public Answer execute(final PlugNicCommand command, final LibvirtComputingResource libvirtComputingResource) { |
@@ -70,10 +80,8 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour |
70 | 80 | libvirtComputingResource.setInterfaceDefQueueSettings(command.getDetails(), null, interfaceDef); |
71 | 81 | } |
72 | 82 |
|
73 | | - // Explicitly assign PCI slot to ensure sequential NIC naming in the guest. |
74 | | - // Without this, libvirt auto-assigns the next free PCI slot which may be |
75 | | - // non-sequential with existing NICs (e.g. ens9 instead of ens5), causing |
76 | | - // guest network configuration to fail. |
| 83 | + // Pin the PCI slot to the lowest free one above the existing NICs so the guest sees a |
| 84 | + // deterministic, monotonic NIC order across hot-plugs (see findNextAvailablePciSlot). |
77 | 85 | Integer nextSlot = findNextAvailablePciSlot(vm, pluggedNics); |
78 | 86 | if (nextSlot != null) { |
79 | 87 | interfaceDef.setSlot(nextSlot); |
@@ -113,52 +121,84 @@ public Answer execute(final PlugNicCommand command, final LibvirtComputingResour |
113 | 121 | } |
114 | 122 |
|
115 | 123 | /** |
116 | | - * Finds the next available PCI slot for a hot-plugged NIC by examining |
117 | | - * all PCI slots currently in use by the domain. This ensures the new NIC |
118 | | - * gets a sequential PCI address relative to existing NICs, resulting in |
119 | | - * predictable interface naming in the guest OS (e.g. ens5 instead of ens9). |
| 124 | + * Picks the PCI slot for the NIC being hot-plugged: the lowest free slot above the highest slot |
| 125 | + * already used by a NIC. The choice is deterministic and monotonic with respect to the NICs that |
| 126 | + * are already present (a new NIC never lands below an existing one), which is what keeps the |
| 127 | + * guest's predictable interface names stable across hot-plugs. It does not guarantee contiguity: |
| 128 | + * slots between the last NIC and the new one may already be taken by other devices (disks, |
| 129 | + * controllers, balloon), in which case the next free slot above them is used. |
| 130 | + * |
| 131 | + * @return the slot to assign, or {@code null} to let libvirt auto-assign (domain XML unavailable |
| 132 | + * or unparseable, or no free slot left). |
120 | 133 | */ |
121 | | - private Integer findNextAvailablePciSlot(final Domain vm, final List<InterfaceDef> pluggedNics) { |
| 134 | + protected Integer findNextAvailablePciSlot(final Domain vm, final List<InterfaceDef> pluggedNics) { |
122 | 135 | try { |
123 | | - String domXml = vm.getXMLDesc(0); |
124 | | - |
125 | | - // Defensive: getXMLDesc can return null on certain libvirt error paths (and is |
126 | | - // null in unit tests where the Domain mock isn't stubbed for this call). Fall |
127 | | - // back to letting libvirt auto-assign the PCI slot. |
| 136 | + final String domXml = vm.getXMLDesc(0); |
| 137 | + // getXMLDesc can return null on certain libvirt error paths; fall back to libvirt's own choice. |
128 | 138 | if (domXml == null) { |
129 | 139 | logger.debug("Domain XML unavailable, letting libvirt auto-assign PCI slot"); |
130 | 140 | return null; |
131 | 141 | } |
132 | | - |
133 | | - // Parse all PCI slot numbers currently in use |
134 | | - Set<Integer> usedSlots = new HashSet<>(); |
135 | | - Pattern slotPattern = Pattern.compile("slot='0x([0-9a-fA-F]+)'"); |
136 | | - Matcher matcher = slotPattern.matcher(domXml); |
137 | | - while (matcher.find()) { |
138 | | - usedSlots.add(Integer.parseInt(matcher.group(1), 16)); |
| 142 | + final Set<Integer> usedSlots = getUsedPciSlots(domXml); |
| 143 | + if (usedSlots == null) { |
| 144 | + return null; |
139 | 145 | } |
| 146 | + final Integer slot = getFirstFreeSlotAbove(getHighestNicSlot(pluggedNics), usedSlots); |
| 147 | + if (slot == null) { |
| 148 | + logger.warn("No free PCI slots available, letting libvirt auto-assign"); |
| 149 | + } |
| 150 | + return slot; |
| 151 | + } catch (final LibvirtException e) { |
| 152 | + logger.warn("Failed to get domain XML for PCI slot calculation, letting libvirt auto-assign", e); |
| 153 | + return null; |
| 154 | + } |
| 155 | + } |
140 | 156 |
|
141 | | - // Find the highest PCI slot used by existing NICs |
142 | | - int maxNicSlot = 0; |
143 | | - for (InterfaceDef pluggedNic : pluggedNics) { |
144 | | - if (pluggedNic.getSlot() != null && pluggedNic.getSlot() > maxNicSlot) { |
145 | | - maxNicSlot = pluggedNic.getSlot(); |
| 157 | + /** |
| 158 | + * Collects the slot numbers of every {@code <address type='pci' .../>} element in the domain XML, |
| 159 | + * whichever device or bus they belong to. Returns {@code null} if the XML cannot be parsed. |
| 160 | + */ |
| 161 | + protected Set<Integer> getUsedPciSlots(final String domXml) { |
| 162 | + final Set<Integer> usedSlots = new HashSet<>(); |
| 163 | + try { |
| 164 | + final DocumentBuilder builder = ParserUtils.getSaferDocumentBuilderFactory().newDocumentBuilder(); |
| 165 | + final Document doc = builder.parse(new InputSource(new StringReader(domXml))); |
| 166 | + final NodeList addresses = doc.getElementsByTagName("address"); |
| 167 | + for (int i = 0; i < addresses.getLength(); i++) { |
| 168 | + final Element address = (Element) addresses.item(i); |
| 169 | + if (!"pci".equals(address.getAttribute("type")) || address.getAttribute("slot").isEmpty()) { |
| 170 | + continue; |
146 | 171 | } |
| 172 | + usedSlots.add(Integer.decode(address.getAttribute("slot"))); |
147 | 173 | } |
| 174 | + } catch (final ParserConfigurationException | SAXException | IOException | NumberFormatException e) { |
| 175 | + logger.warn("Failed to parse domain XML for PCI slot calculation, letting libvirt auto-assign", e); |
| 176 | + return null; |
| 177 | + } |
| 178 | + return usedSlots; |
| 179 | + } |
148 | 180 |
|
149 | | - // Find next free slot starting from maxNicSlot + 1 |
150 | | - // PCI slots range from 0x01 to 0x1f (slot 0 is reserved for host bridge) |
151 | | - for (int slot = maxNicSlot + 1; slot <= 0x1f; slot++) { |
152 | | - if (!usedSlots.contains(slot)) { |
153 | | - return slot; |
154 | | - } |
| 181 | + /** Highest PCI slot used by an existing NIC, or 0 when no NIC carries a slot. */ |
| 182 | + protected static int getHighestNicSlot(final List<InterfaceDef> pluggedNics) { |
| 183 | + int highest = 0; |
| 184 | + for (final InterfaceDef pluggedNic : pluggedNics) { |
| 185 | + if (pluggedNic.getSlot() != null && pluggedNic.getSlot() > highest) { |
| 186 | + highest = pluggedNic.getSlot(); |
155 | 187 | } |
| 188 | + } |
| 189 | + return highest; |
| 190 | + } |
156 | 191 |
|
157 | | - logger.warn("No free PCI slots available, letting libvirt auto-assign"); |
158 | | - return null; |
159 | | - } catch (LibvirtException e) { |
160 | | - logger.warn("Failed to get domain XML for PCI slot calculation, letting libvirt auto-assign", e); |
161 | | - return null; |
| 192 | + /** |
| 193 | + * Lowest slot strictly above {@code from} (and no higher than {@link #MAX_PCI_SLOT}) that is not in |
| 194 | + * {@code usedSlots}; {@code null} when the range is exhausted. Slot 0 is reserved for the host bridge. |
| 195 | + */ |
| 196 | + protected static Integer getFirstFreeSlotAbove(final int from, final Set<Integer> usedSlots) { |
| 197 | + for (int slot = from + 1; slot <= MAX_PCI_SLOT; slot++) { |
| 198 | + if (!usedSlots.contains(slot)) { |
| 199 | + return slot; |
| 200 | + } |
162 | 201 | } |
| 202 | + return null; |
163 | 203 | } |
164 | 204 | } |
0 commit comments