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:
+ *
+ * - {@code "lswitch"} — the framework sets {@code BroadcastDomainType.Lswitch}
+ * on the {@link com.cloud.vm.NicProfile} during {@code prepare(...)} and
+ * propagates {@code nic.getUuid()} to per-NIC script commands as
+ * {@code --nic-uuid}. The extension is then expected to use that UUID as
+ * the LSP name, so it matches the {@code interfaceid} that
+ * {@code OvsVifDriver} emits in the libvirt {@code } for
+ * {@code Lswitch} broadcast type.
+ *
+ *
+ * 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 |