diff --git a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java index 84105dfe5ce0..8f79583216ae 100644 --- a/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java +++ b/api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java @@ -43,6 +43,30 @@ public interface ExtensionHelper { */ String NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY = "network.service.capabilities"; + /** + * Detail key used by an OVS-backed NetworkOrchestrator extension to declare + * how its Logical Switch Port name should be matched against the OVS + * {@code external_ids:iface-id} written by libvirt on the hypervisor. + * + *

Currently supported value:

+ * + * + *

If absent, the framework keeps the network's broadcast type unchanged + * (typically {@code Vlan}) and does not propagate {@code --nic-uuid}.

+ */ + String VIF_BINDING_DETAIL_KEY = "vif.binding"; + + /** Value of {@link #VIF_BINDING_DETAIL_KEY} that selects the Lswitch path. */ + String VIF_BINDING_LSWITCH = "lswitch"; + String getExtensionScriptPath(Extension extension); /** diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java index d80768390431..5b26d3fca4c6 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java @@ -19,6 +19,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,6 +52,8 @@ import com.cloud.network.NetworkModel; import com.cloud.network.Networks; import com.cloud.network.dao.NetworkDetailVO; +import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkVO; import com.cloud.network.dao.PhysicalNetworkDao; import com.cloud.network.dao.PhysicalNetworkVO; import com.cloud.network.PhysicalNetworkServiceProvider; @@ -118,6 +121,7 @@ import org.apache.cloudstack.extension.Extension; import org.apache.cloudstack.extension.ExtensionHelper; import org.apache.cloudstack.extension.NetworkCustomActionProvider; +import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao; import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao; import org.apache.commons.lang3.StringUtils; @@ -241,6 +245,10 @@ public class NetworkExtensionElement extends AdapterBase implements @Inject private PhysicalNetworkDao physicalNetworkDao; @Inject + private ExtensionDetailsDao extensionDetailsDao; + @Inject + private NetworkDao networkDao; + @Inject private DataCenterDao dataCenterDao; @Inject private VlanDao vlanDao; @@ -306,6 +314,8 @@ public NetworkExtensionElement withProviderName(String providerName) { copy.networkDetailsDao = this.networkDetailsDao; copy.ipAddressManager = this.ipAddressManager; copy.physicalNetworkDao = this.physicalNetworkDao; + copy.extensionDetailsDao = this.extensionDetailsDao; + copy.networkDao = this.networkDao; copy.dataCenterDao = this.dataCenterDao; copy.vlanDao = this.vlanDao; copy.guestOSCategoryDao = this.guestOSCategoryDao; @@ -461,6 +471,27 @@ public boolean implement(Network network, NetworkOffering offering, DeployDestin return false; } + // When the extension declares vif.binding=lswitch, also update the + // Network row itself so listNetworks / DB queries advertise the + // OVN-flavoured identifier instead of the cosmetic VLAN URI the + // GuestNetworkGuru allocated at design-time. Format follows the + // legacy ovn-plugin convention: ``ovn://cs-net-``. + if (isLswitchVifBinding(network)) { + try { + NetworkVO networkVo = networkDao.findById(network.getId()); + if (networkVo != null) { + java.net.URI ovnUri = java.net.URI.create("ovn://cs-net-" + network.getId()); + networkVo.setBroadcastDomainType(Networks.BroadcastDomainType.Lswitch); + networkVo.setBroadcastUri(ovnUri); + networkDao.update(networkVo.getId(), networkVo); + logger.debug("implement: applied Lswitch broadcast type and ovn:// URI to network {} per extension vif.binding hint", + network.getId()); + } + } catch (Exception e) { + logger.warn("Failed to persist OVN URI on network {}: {}", network.getId(), e.getMessage()); + } + } + // Step 3: Configure source NAT for both VPC and non-VPC networks for // compatibility (other network-element providers may also implement VPC tiers). // When this is a VPC tier, the script's assign-ip does nothing for source-nat @@ -506,12 +537,93 @@ public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vm return false; } + // VIF binding hint -- when the extension declares vif.binding=lswitch, + // override the NicProfile's broadcast type so OvsVifDriver picks the + // Lswitch path on the KVM agent. That path already emits libvirt + // and + // libvirt sets external_ids:iface-id atomically with tap creation. + // No agent patch is required for this binding mode. + if (isLswitchVifBinding(network)) { + // Override broadcast type + URI on the NicProfile (in-memory), + // and persist the same to the underlying nics row so listNics + // / DB queries report consistent OVN identifiers instead of + // the stale VLAN URI the GuestNetworkGuru allocated at + // design-time. + java.net.URI ovnUri = null; + try { + ovnUri = java.net.URI.create("ovn://cs-net-" + network.getId()); + } catch (Exception e) { + logger.warn("Failed to build OVN URI for NIC {}: {}", nic.getId(), e.getMessage()); + } + nic.setBroadcastType(Networks.BroadcastDomainType.Lswitch); + if (ovnUri != null) { + nic.setBroadcastUri(ovnUri); + nic.setIsolationUri(ovnUri); + try { + com.cloud.vm.NicVO nicVo = nicDao.findById(nic.getId()); + if (nicVo != null) { + nicVo.setBroadcastUri(ovnUri); + nicVo.setIsolationUri(ovnUri); + nicDao.update(nicVo.getId(), nicVo); + } + } catch (Exception e) { + logger.warn("Failed to persist OVN URI on nics row {}: {}", nic.getId(), e.getMessage()); + } + } + logger.debug("prepare: applied Lswitch broadcast type and ovn:// URI to NIC {} (uuid={}) on network {} per extension vif.binding hint", + nic.getId(), nic.getUuid(), network.getId()); + } + final NetworkOfferingVO offering = networkOfferingDao.findById(network.getNetworkOfferingId()); implement(network, offering, dest, context); return true; } + /** + * Returns {@code true} when the extension that owns the given network + * declares {@code vif.binding=lswitch} in its {@code extension_details}. + * Used by {@link #prepare(Network, NicProfile, VirtualMachineProfile, + * DeployDestination, ReservationContext)} to switch the NIC's + * {@link Networks.BroadcastDomainType} to {@code Lswitch} so the KVM + * agent's existing {@code OvsVifDriver} Lswitch path is exercised -- + * see the framework README for the full contract. + */ + private boolean isLswitchVifBinding(Network network) { + try { + Extension extension = resolveExtension(network); + if (extension == null) { + return false; + } + Map details = extensionDetailsDao.listDetailsKeyPairs(extension.getId()); + if (details == null) { + return false; + } + String vifBinding = details.get(ExtensionHelper.VIF_BINDING_DETAIL_KEY); + return ExtensionHelper.VIF_BINDING_LSWITCH.equalsIgnoreCase(vifBinding); + } catch (Exception e) { + logger.debug("Failed to resolve vif.binding for network {}: {}", network.getId(), e.getMessage()); + return false; + } + } + + /** + * Returns {@code ["--nic-uuid", ""]} when the extension prefers the + * Lswitch VIF binding path so the script can use the same UUID as the LSP + * name (matching the {@code interfaceid} that {@code OvsVifDriver} emits). + * Returns an empty list when the extension does not opt in -- existing + * extensions that derive identifiers from the MAC keep working unchanged. + */ + private List getNicUuidArgs(Network network, NicProfile nic) { + if (nic == null || nic.getUuid() == null || nic.getUuid().isBlank()) { + return Collections.emptyList(); + } + if (!isLswitchVifBinding(network)) { + return Collections.emptyList(); + } + return List.of("--nic-uuid", nic.getUuid()); + } + @Override public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { @@ -1346,6 +1458,7 @@ public boolean addDhcpEntry(Network network, NicProfile nic, VirtualMachineProfi args.add("--default-nic"); args.add(String.valueOf(nic.isDefaultNic())); args.add("--domain"); args.add(safeStr(network.getNetworkDomain())); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "add-dhcp-entry", args.toArray(new String[0])); } @@ -1434,6 +1547,7 @@ public boolean removeDhcpEntry(Network network, NicProfile nic, VirtualMachinePr args.add("--mac"); args.add(safeStr(nic.getMacAddress())); args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "remove-dhcp-entry", args.toArray(new String[0])); } @@ -1456,6 +1570,7 @@ public boolean addDnsEntry(Network network, NicProfile nic, VirtualMachineProfil args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); args.add("--hostname"); args.add(safeStr(hostname)); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "add-dns-entry", args.toArray(new String[0])); } @@ -1632,6 +1747,7 @@ public boolean addPasswordAndUserdata(Network network, NicProfile nic, VirtualMa args.add("--ip"); args.add(safeStr(nicIpAddress)); args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); args.add("--extension-ip"); args.add(safeStr(ensureExtensionIp(network))); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScriptWithFilePayload(network, "save-vm-data", "--vm-data-file", vmDataArg, args.toArray(new String[0])); @@ -1655,6 +1771,7 @@ public boolean savePassword(Network network, NicProfile nic, VirtualMachineProfi args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); args.add("--password"); args.add(password); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "save-password", args.toArray(new String[0])); } @@ -1681,6 +1798,7 @@ public boolean saveUserData(Network network, NicProfile nic, VirtualMachineProfi args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); args.add("--userdata"); args.add(userData); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "save-userdata", args.toArray(new String[0])); } @@ -1704,6 +1822,7 @@ public boolean saveSSHKey(Network network, NicProfile nic, VirtualMachineProfile args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); args.add("--sshkey"); args.add(sshKeyBase64); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "save-sshkey", args.toArray(new String[0])); } @@ -1727,6 +1846,7 @@ public boolean saveHypervisorHostname(NicProfile nic, Network network, VirtualMa args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); args.add("--hypervisor-hostname"); args.add(hostname); args.add("--extension-ip"); args.add(safeStr(extensionIp)); + args.addAll(getNicUuidArgs(network, nic)); args.addAll(getVpcIdArgs(network)); return executeScript(network, "save-hypervisor-hostname", args.toArray(new String[0])); } diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md index 5d9a4c36000f..2d7ce01e1e2a 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md @@ -71,8 +71,9 @@ hosts. Use it as a working example. 8. [Capabilities Configuration](#capabilities-configuration) 9. [VPC Networks](#vpc-networks) 10. [Extension IP](#extension-ip) -11. [Exit Codes](#exit-codes) -12. [Minimal Script Skeleton](#minimal-script-skeleton) +11. [VIF Binding for OVS-backed Extensions](#vif-binding-for-ovs-backed-extensions) +12. [Exit Codes](#exit-codes) +13. [Minimal Script Skeleton](#minimal-script-skeleton) --- @@ -636,6 +637,7 @@ network whose DHCP service is provided by this extension. | `--default-nic ` | `true` if this is the VM's default NIC. | | `--domain ` | Network domain suffix (e.g. `cs.example.com`). | | `--extension-ip ` | | +| `--nic-uuid ` | (optional) Present only when the extension declared `vif.binding=lswitch`. Carries `nic.getUuid()` so the extension can use it as the SDN-side port identifier (matches `external_ids:iface-id` set by libvirt on the OVS tap). See [VIF Binding for OVS-backed Extensions](#vif-binding-for-ovs-backed-extensions). | | `--vpc-id ` | (optional) | **`remove-dhcp-entry` arguments:** @@ -1106,6 +1108,77 @@ To use this extension as a VPC provider: --- +## VIF Binding for OVS-backed Extensions + +Extensions that drive OVS-based fabrics (OVN, NSX-OVS, …) need the OVS +tap interface that libvirt creates for each VM NIC to carry the +`external_ids:iface-id` value that the SDN controller expects. CloudStack +already does the right thing for `BroadcastDomainType.Lswitch` networks: +its `OvsVifDriver` emits + +```xml + + + +``` + +and libvirt sets `external_ids:iface-id=` on the tap atomically +with port creation. No agent patch is required. + +To opt into this binding mode an extension declares it as a top-level +capability hint in its `extension_details`: + +```bash +cmk createExtension \ + name=my-ovs-sdn \ + type=NetworkOrchestrator \ + "details[0].key=network.services" \ + "details[0].value=Dhcp,Dns,UserData,SourceNat,…" \ + "details[1].key=network.service.capabilities" \ + "details[1].value=$(cat my-caps.json)" \ + "details[2].key=vif.binding" \ + "details[2].value=lswitch" +``` + +When `vif.binding=lswitch` is present: + +1. **`prepare()` overrides the NIC broadcast type.** + `NetworkExtensionElement.prepare(...)` calls + `nic.setBroadcastType(Networks.BroadcastDomainType.Lswitch)` so + `OvsVifDriver` on the KVM agent picks the existing Lswitch path and + emits the libvirt `` shown above. + +2. **Per-NIC commands receive `--nic-uuid `.** + `add-dhcp-entry`, `remove-dhcp-entry`, `add-dns-entry`, `save-vm-data`, + `save-password`, `save-userdata`, `save-sshkey`, and + `save-hypervisor-hostname` all gain a `--nic-uuid ` argument + carrying `nic.getUuid()`. + +3. **The script must use `--nic-uuid` as the SDN-side port identifier.** + Whatever object the extension creates on its controller (OVN + Logical_Switch_Port, NSX logical port, …) **must be named exactly the + value of `--nic-uuid`**. That is the string libvirt will write to + `external_ids:iface-id` on the tap, so the SDN controller's local + agent (e.g. `ovn-controller`) finds a match and binds the port. + +When the extension does not declare `vif.binding`, the framework keeps +the default `BroadcastDomainType.Vlan` and does not propagate +`--nic-uuid` -- existing reference extensions (e.g. +`network-namespace`) are unaffected. + +### Why not the extension setting `iface-id` remotely? + +The OVS tap only exists *after* libvirt creates the VM, so any remote +write from the extension would race `ovn-controller` on the host. By +letting libvirt do the write atomically with tap creation, the binding +is ready by the time the controller scans the bridge. + +The extension may still talk OVSDB to the host (read-only checks, +`bridge-mappings` setup, post-incident repair) -- but never for the +boot path. + +--- + ## Exit Codes | Exit code | Meaning |