From 61bd94e83ff60f595f74e770799f59b9f1c06fe4 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Thu, 30 Apr 2026 14:50:52 +0200 Subject: [PATCH 1/3] NE: VIF binding hooks for OVS-backed extensions CloudStack's existing OvsVifDriver already binds NICs correctly when the NicProfile's BroadcastDomainType is Lswitch: it emits libvirt and libvirt sets external_ids:iface-id atomically with tap creation. No agent patch is required for OVS-backed extensions to consume this path -- they just need (a) a way to opt in, and (b) nic.getUuid() carried in per-NIC script commands so the SDN-side port identifier can match. Add the framework hooks to enable this without any KVM agent change: * ExtensionHelper.VIF_BINDING_DETAIL_KEY ("vif.binding") -- new top-level extension detail. Currently supported value: "lswitch". * NetworkExtensionElement.prepare(...) -- when the extension owning the NIC's network declares vif.binding=lswitch in its extension_details, override nic.setBroadcastType(Networks.BroadcastDomainType.Lswitch). OvsVifDriver on the KVM agent then picks the existing Lswitch path unchanged. Without the opt-in, the previous default (typically Vlan) is preserved -- existing reference extensions like network-namespace are unaffected. * NetworkExtensionElement.getNicUuidArgs(network, nic) -- helper that returns ["--nic-uuid", ""] only when vif.binding=lswitch is declared. Wired into add-dhcp-entry, remove-dhcp-entry, add-dns-entry, save-vm-data, save-password, save-userdata, save-sshkey, and save-hypervisor-hostname. Extensions that do not declare the hint never see --nic-uuid, so backwards-compatible. * README -- new section "VIF Binding for OVS-backed Extensions" documenting the contract end-to-end: cmk createExtension snippet, what prepare() does, how --nic-uuid flows, why the extension never writes iface-id remotely on the boot path. Also notes the new argument in the add-dhcp-entry table. Result: an OVN extension (or any future OVS-backed extension) gets correct VIF binding by adding a single detail key at extension creation time. No host-side agent patch, no libvirt patch, no OVS schema change. --- .../cloudstack/extension/ExtensionHelper.java | 24 ++++++ .../network/NetworkExtensionElement.java | 69 +++++++++++++++++ .../framework/extensions/network/README.md | 77 ++++++++++++++++++- 3 files changed, 168 insertions(+), 2 deletions(-) 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..84bea25f73ef 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; @@ -118,6 +119,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 +243,8 @@ public class NetworkExtensionElement extends AdapterBase implements @Inject private PhysicalNetworkDao physicalNetworkDao; @Inject + private ExtensionDetailsDao extensionDetailsDao; + @Inject private DataCenterDao dataCenterDao; @Inject private VlanDao vlanDao; @@ -306,6 +310,7 @@ public NetworkExtensionElement withProviderName(String providerName) { copy.networkDetailsDao = this.networkDetailsDao; copy.ipAddressManager = this.ipAddressManager; copy.physicalNetworkDao = this.physicalNetworkDao; + copy.extensionDetailsDao = this.extensionDetailsDao; copy.dataCenterDao = this.dataCenterDao; copy.vlanDao = this.vlanDao; copy.guestOSCategoryDao = this.guestOSCategoryDao; @@ -506,12 +511,68 @@ 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)) { + nic.setBroadcastType(Networks.BroadcastDomainType.Lswitch); + logger.debug("prepare: applied Lswitch broadcast type 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 +1407,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 +1496,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 +1519,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 +1696,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 +1720,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 +1747,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 +1771,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 +1795,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 | From c27f0d8f7f064893e308d783b3bfb48e4498c7fd Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Sun, 3 May 2026 12:21:27 +0200 Subject: [PATCH 2/3] NE: persist OVN broadcast/isolation URI on NIC for vif.binding=lswitch Delta 1 already overrides nic.setBroadcastType(Lswitch) on the NicProfile during prepare() so the KVM agent picks the OVS Lswitch path. But the underlying nics row still carried the cosmetic ``vlan://`` URI allocated by GuestNetworkGuru at design-time, which is misleading on listNics / DB queries: a NIC sitting on an OVN logical switch should not advertise a VLAN URI. Override broadcast_uri and isolation_uri on the NicProfile to ``ovn://cs-net-`` (the convention used by the legacy ovn-plugin) and persist the same on the nics row via nicDao.update. The VLAN that the guru allocated stays as a ghost in op_dc_vnet_alloc -- it is never used on the wire because the VIF attaches to br-int and traffic flows through OVN's logical pipeline over geneve. Releasing the VLAN back to the pool would require intercepting the design phase, which is out of scope for this hook. Verified end-to-end: i-2-24-VM on network 214 now lists broadcast_uri = ovn://cs-net-214 isolation_uri = ovn://cs-net-214 and the OVN NB LSP / OVS iface-id / OVN SB Port_Binding remain correctly bound to the chassis, as before. --- .../network/NetworkExtensionElement.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 84bea25f73ef..d10c6b74cb1c 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 @@ -518,8 +518,33 @@ public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vm // 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); - logger.debug("prepare: applied Lswitch broadcast type to NIC {} (uuid={}) on network {} per extension vif.binding hint", + 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()); } From c9466f72f7a138121f9bfc5556c2e681dc2f7e09 Mon Sep 17 00:00:00 2001 From: Marco Sinhoreli Date: Mon, 4 May 2026 13:32:26 +0200 Subject: [PATCH 3/3] NE: persist OVN broadcast type and URI on Network for vif.binding=lswitch Companion to the NIC-level URI override -- when an extension declares vif.binding=lswitch, the Network row itself should advertise ``broadcast_domain_type=Lswitch`` and ``broadcast_uri=ovn://cs-net-`` so listNetworks / details views are consistent with what the OVN control plane represents. Without this hook the GuestNetworkGuru still allocated a VLAN at design time, which leaks back into the UI: VLAN/VNI: 138 Broadcast URI: vlan://138 Apply the override on a successful ``implement-network`` script return in NetworkExtensionElement.implement(). The VLAN that the guru allocated stays as a ghost in op_dc_vnet_alloc -- it is never used on the wire because the VIF attaches to br-int and traffic flows through OVN's logical pipeline over geneve. Releasing the VLAN back to the pool would require intercepting the design phase and is out of scope for this hook. Verified end-to-end: i-2-27-VM on network 216 now lists networks.broadcast_uri = ovn://cs-net-216 networks.broadcast_domain_type = Lswitch nics.broadcast_uri = ovn://cs-net-216 nics.isolation_uri = ovn://cs-net-216 The OVN NB LSP / OVS iface-id / OVN SB Port_Binding remain bound, as before. --- .../network/NetworkExtensionElement.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 d10c6b74cb1c..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 @@ -52,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; @@ -245,6 +247,8 @@ public class NetworkExtensionElement extends AdapterBase implements @Inject private ExtensionDetailsDao extensionDetailsDao; @Inject + private NetworkDao networkDao; + @Inject private DataCenterDao dataCenterDao; @Inject private VlanDao vlanDao; @@ -311,6 +315,7 @@ public NetworkExtensionElement withProviderName(String providerName) { 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; @@ -466,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