Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Currently supported value:</p>
* <ul>
* <li>{@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 <virtualport>} for
* {@code Lswitch} broadcast type.</li>
* </ul>
*
* <p>If absent, the framework keeps the network's broadcast type unchanged
* (typically {@code Vlan}) and does not propagate {@code --nic-uuid}.</p>
*/
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);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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-<networkId>``.
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
Expand Down Expand Up @@ -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
// <virtualport type='openvswitch' interfaceid='<nic.getUuid()>'/> 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<String, String> 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", "<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<String> 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 {
Expand Down Expand Up @@ -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]));
}
Expand Down Expand Up @@ -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]));
}
Expand All @@ -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]));
}
Expand Down Expand Up @@ -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]));
Expand All @@ -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]));
}
Expand All @@ -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]));
}
Expand All @@ -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]));
}
Expand All @@ -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]));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -636,6 +637,7 @@ network whose DHCP service is provided by this extension.
| `--default-nic <bool>` | `true` if this is the VM's default NIC. |
| `--domain <name>` | Network domain suffix (e.g. `cs.example.com`). |
| `--extension-ip <ip>` | |
| `--nic-uuid <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 <N>` | (optional) |

**`remove-dhcp-entry` arguments:**
Expand Down Expand Up @@ -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
<virtualport type='openvswitch'>
<parameters interfaceid='<nic.getUuid()>'/>
</virtualport>
```

and libvirt sets `external_ids:iface-id=<nic-uuid>` 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 `<virtualport>` shown above.

2. **Per-NIC commands receive `--nic-uuid <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 <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 |
Expand Down