Skip to content

Commit 7bb291b

Browse files
committed
extension: split network services and capabilities into two configurations
1 parent 6150ea4 commit 7bb291b

4 files changed

Lines changed: 209 additions & 181 deletions

File tree

api/src/main/java/org/apache/cloudstack/extension/ExtensionHelper.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,19 @@
2525

2626
public interface ExtensionHelper {
2727

28-
/** Detail key used to store the JSON network capabilities of a NetworkOrchestrator extension. */
29-
String NETWORK_CAPABILITIES_DETAIL_KEY = "network.capabilities";
28+
/**
29+
* Detail key used to store the comma-separated list of network services provided
30+
* by a NetworkOrchestrator extension (e.g. {@code "SourceNat,StaticNat,Firewall"}).
31+
*/
32+
String NETWORK_SERVICES_DETAIL_KEY = "network.services";
33+
34+
/**
35+
* Detail key used to store a JSON object mapping each service name to its
36+
* CloudStack {@link com.cloud.network.Network.Capability} key/value pairs.
37+
* Example: {@code {"SourceNat":{"SupportedSourceNatTypes":"peraccount"}}}.
38+
* Used together with {@link #NETWORK_SERVICES_DETAIL_KEY}.
39+
*/
40+
String NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY = "network.service.capabilities";
3041

3142
Long getExtensionIdForCluster(long clusterId);
3243
Extension getExtension(long id);

extensions/network-namespace/README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -254,36 +254,49 @@ cmk createExtension \
254254
name=my-extnet \
255255
type=NetworkOrchestrator \
256256
path=network-namespace \
257-
"details[0].key=network.capabilities" \
258-
"details[0].value={\"services\":[\"SourceNat\",\"StaticNat\",\"PortForwarding\",\"Firewall\",\"Gateway\"],\"capabilities\":{\"SourceNat\":{\"SupportedSourceNatTypes\":\"peraccount\",\"RedundantRouter\":\"false\"},\"Firewall\":{\"TrafficStatistics\":\"per public ip\"}}}"
257+
"details[0].key=network.services" \
258+
"details[0].value=SourceNat,StaticNat,PortForwarding,Firewall,Gateway" \
259+
"details[1].key=network.service.capabilities" \
260+
"details[1].value={\"SourceNat\":{\"SupportedSourceNatTypes\":\"peraccount\",\"RedundantRouter\":\"false\"},\"Firewall\":{\"TrafficStatistics\":\"per public ip\"}}"
259261
```
260262

261-
The `network.capabilities` detail declares which services this extension provides
262-
and their CloudStack capability values. These are consulted when listing network
263-
service providers and when validating network offerings.
263+
The two details declare which services this extension provides and their
264+
CloudStack capability values. These are consulted when listing network service
265+
providers and when validating network offerings.
264266

265-
**`network.capabilities` JSON format:**
267+
**`network.services`** — comma-separated list of service names:
268+
```
269+
SourceNat,StaticNat,PortForwarding,Firewall,Gateway
270+
```
271+
Valid service names include: `Vpn`, `Dhcp`, `Dns`, `SourceNat`,
272+
`PortForwarding`, `Lb`, `UserData`, `StaticNat`, `NetworkACL`, `Firewall`,
273+
`Gateway`, `SecurityGroup`.
274+
275+
**`network.service.capabilities`** — JSON object mapping each service to its
276+
CloudStack `Capability` key/value pairs:
266277
```json
267278
{
268-
"services": ["SourceNat", "StaticNat", "PortForwarding", "Firewall", "Gateway"],
269-
"capabilities": {
270-
"SourceNat": {
271-
"SupportedSourceNatTypes": "peraccount",
272-
"RedundantRouter": "false"
273-
},
274-
"Firewall": {
275-
"TrafficStatistics": "per public ip"
276-
}
279+
"SourceNat": {
280+
"SupportedSourceNatTypes": "peraccount",
281+
"RedundantRouter": "false"
282+
},
283+
"Firewall": {
284+
"TrafficStatistics": "per public ip"
277285
}
278286
}
279287
```
280288

281-
Services not listed in `capabilities` (e.g. `StaticNat`, `PortForwarding`,
289+
Services listed in `network.services` that have no entry in
290+
`network.service.capabilities` (e.g. `StaticNat`, `PortForwarding`,
282291
`Gateway`) are still offered — CloudStack treats missing capability values as
283292
"no constraint" and accepts any value when creating the network offering.
284293

285-
If you omit the `network.capabilities` detail entirely, the extension defaults
286-
to all five services with `SourceNat.SupportedSourceNatTypes=peraccount`.
294+
If you omit both details entirely, the extension defaults to an empty set of
295+
services and no capabilities.
296+
297+
> **Backward compatibility:** the old combined `network.capabilities` JSON
298+
> key (with a `"services"` array and `"capabilities"` object in one blob) is
299+
> still accepted but deprecated. Prefer the split keys above.
287300
288301
Verify the extension was created and its state is `Enabled`:
289302
```bash

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java

Lines changed: 91 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,7 +1093,7 @@ protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetw
10931093
}
10941094
}
10951095

1096-
// Resolve which services this extension provides from its network.capabilities detail
1096+
// Resolve which services this extension provides from its network.services detail
10971097
Set<String> services = resolveExtensionServices(extension);
10981098

10991099
return Transaction.execute((TransactionCallbackWithException<ExtensionResourceMap, CloudRuntimeException>) status -> {
@@ -1135,22 +1135,98 @@ protected ExtensionResourceMap registerExtensionWithPhysicalNetwork(PhysicalNetw
11351135

11361136
/**
11371137
* Resolves the set of network service names declared in the extension's
1138-
* {@code network.capabilities} detail. Falls back to the full default set
1139-
* if no capabilities are declared.
1138+
* {@code network.services} detail. Falls back to an empty set if not present
11401139
*/
11411140
private Set<String> resolveExtensionServices(Extension extension) {
11421141
Map<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
1143-
if (extDetails != null && extDetails.containsKey(ExtensionHelper.NETWORK_CAPABILITIES_DETAIL_KEY)) {
1144-
Set<String> parsed = parseServicesFromCapabilitiesJson(
1145-
extDetails.get(ExtensionHelper.NETWORK_CAPABILITIES_DETAIL_KEY));
1146-
if (!parsed.isEmpty()) {
1147-
return parsed;
1148-
}
1142+
Set<String> parsed = parseServicesFromDetailKeys(extDetails);
1143+
if (!parsed.isEmpty()) {
1144+
return parsed;
11491145
}
11501146
// Default: the full set of services NetworkExtensionElement supports
11511147
return new HashSet<>();
11521148
}
11531149

1150+
/**
1151+
* Resolves the set of service names from the extension detail map.
1152+
* From {@code network.services} comma-separated key.
1153+
*/
1154+
@SuppressWarnings("deprecation")
1155+
private Set<String> parseServicesFromDetailKeys(Map<String, String> extDetails) {
1156+
if (extDetails == null) {
1157+
return Collections.emptySet();
1158+
}
1159+
// New format: "network.services" = "SourceNat,StaticNat,..."
1160+
if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
1161+
String value = extDetails.get(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY);
1162+
if (StringUtils.isNotBlank(value)) {
1163+
Set<String> services = new HashSet<>();
1164+
for (String s : value.split(",")) {
1165+
String trimmed = s.trim();
1166+
if (!trimmed.isEmpty()) {
1167+
services.add(trimmed);
1168+
}
1169+
}
1170+
if (!services.isEmpty()) {
1171+
return services;
1172+
}
1173+
}
1174+
}
1175+
1176+
return Collections.emptySet();
1177+
}
1178+
1179+
/**
1180+
* Builds a full {@code Map<Service, Map<Capability, String>>} from the
1181+
* extension detail map. From the split keys
1182+
* {@code network.services} + {@code network.service.capabilities}.
1183+
*/
1184+
@SuppressWarnings("deprecation")
1185+
private Map<Service, Map<Capability, String>> buildCapabilitiesFromDetailKeys(
1186+
Map<String, String> extDetails) {
1187+
if (extDetails == null) {
1188+
return new HashMap<>();
1189+
}
1190+
// New split format
1191+
if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
1192+
Set<String> serviceNames = parseServicesFromDetailKeys(extDetails);
1193+
if (!serviceNames.isEmpty()) {
1194+
JsonObject capsObj = null;
1195+
if (extDetails.containsKey(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY)) {
1196+
try {
1197+
capsObj = JsonParser.parseString(
1198+
extDetails.get(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY))
1199+
.getAsJsonObject();
1200+
} catch (Exception e) {
1201+
logger.warn("Failed to parse network.service.capabilities JSON: {}", e.getMessage());
1202+
}
1203+
}
1204+
Map<Service, Map<Capability, String>> result = new HashMap<>();
1205+
for (String svcName : serviceNames) {
1206+
Service service = Service.getService(svcName);
1207+
if (service == null) {
1208+
logger.warn("Unknown network service '{}' in network.services — skipping", svcName);
1209+
continue;
1210+
}
1211+
Map<Capability, String> capMap = new HashMap<>();
1212+
if (capsObj != null && capsObj.has(svcName)) {
1213+
JsonObject svcCaps = capsObj.getAsJsonObject(svcName);
1214+
for (Map.Entry<String, JsonElement> entry : svcCaps.entrySet()) {
1215+
Capability cap = Capability.getCapability(entry.getKey());
1216+
if (cap != null) {
1217+
capMap.put(cap, entry.getValue().getAsString());
1218+
}
1219+
}
1220+
}
1221+
result.put(service, capMap);
1222+
}
1223+
return result;
1224+
}
1225+
}
1226+
1227+
return new HashMap<>();
1228+
}
1229+
11541230
/**
11551231
* Sets the boolean service-provided flags on a {@link PhysicalNetworkServiceProviderVO}
11561232
* based on a set of service names.
@@ -1176,23 +1252,18 @@ private void applyServicesToNsp(PhysicalNetworkServiceProviderVO nsp, Set<String
11761252

11771253
/**
11781254
* Validates that the comma-separated or JSON-array {@code servicesValue} is a
1179-
* subset of the services declared in the extension's {@code network.capabilities}
1180-
* detail. Throws {@link InvalidParameterValueException} if any service in the
1181-
* request is not offered by the extension.
1255+
* subset of the services declared in the extension's {@code network.services}
1256+
* Throws {@link InvalidParameterValueException} if any service in the request is not
1257+
* offered by the extension.
11821258
*/
11831259
protected void validateNetworkServicesSubset(Extension extension, String servicesValue) {
11841260
if (StringUtils.isBlank(servicesValue)) {
11851261
return;
11861262
}
1187-
// Parse the extension's network.capabilities JSON to get the declared services
11881263
Map<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
1189-
if (extDetails == null || !extDetails.containsKey("network.capabilities")) {
1190-
// No capabilities declared → accept any services
1191-
return;
1192-
}
1193-
String capsJson = extDetails.get("network.capabilities");
1194-
Set<String> allowedServices = parseServicesFromCapabilitiesJson(capsJson);
1264+
Set<String> allowedServices = parseServicesFromDetailKeys(extDetails);
11951265
if (allowedServices.isEmpty()) {
1266+
// No services declared → accept any
11961267
return;
11971268
}
11981269

@@ -1209,31 +1280,6 @@ protected void validateNetworkServicesSubset(Extension extension, String service
12091280
}
12101281
}
12111282

1212-
/**
1213-
* Parses the {@code services} array from a {@code network.capabilities} JSON string.
1214-
* Returns an empty set if parsing fails or services are not defined.
1215-
*/
1216-
private Set<String> parseServicesFromCapabilitiesJson(String json) {
1217-
if (StringUtils.isBlank(json)) {
1218-
return Collections.emptySet();
1219-
}
1220-
try {
1221-
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
1222-
JsonArray arr = root.getAsJsonArray("services");
1223-
if (arr == null) {
1224-
return Collections.emptySet();
1225-
}
1226-
Set<String> services = new HashSet<>();
1227-
for (JsonElement el : arr) {
1228-
services.add(el.getAsString());
1229-
}
1230-
return services;
1231-
} catch (Exception e) {
1232-
logger.warn("Failed to parse network.capabilities JSON: {}", e.getMessage());
1233-
return Collections.emptySet();
1234-
}
1235-
}
1236-
12371283
/**
12381284
* Parses a services list from either a comma-separated string (e.g.
12391285
* {@code "SourceNat,StaticNat"}) or a JSON array (e.g.
@@ -2342,61 +2388,6 @@ public Map<Service, Map<Capability, String>> getNetworkCapabilitiesForProvider(L
23422388
return new HashMap<>();
23432389
}
23442390
Map<String, String> extDetails = extensionDetailsDao.listDetailsKeyPairs(extension.getId());
2345-
if (extDetails == null || !extDetails.containsKey(ExtensionHelper.NETWORK_CAPABILITIES_DETAIL_KEY)) {
2346-
return new HashMap<>();
2347-
}
2348-
return parseCapabilitiesFromJson(extDetails.get(ExtensionHelper.NETWORK_CAPABILITIES_DETAIL_KEY));
2349-
}
2350-
2351-
/**
2352-
* Parses a {@code network.capabilities} JSON string into a
2353-
* {@code Map<Service, Map<Capability, String>>} suitable for
2354-
* {@link com.cloud.network.element.NetworkElement#getCapabilities()}.
2355-
*
2356-
* <p>Expected JSON format:</p>
2357-
* <pre>
2358-
* {
2359-
* "services": ["SourceNat", "StaticNat", ...],
2360-
* "capabilities": {
2361-
* "SourceNat": { "SupportedSourceNatTypes": "peraccount", "RedundantRouter": "false" }
2362-
* }
2363-
* }
2364-
* </pre>
2365-
*/
2366-
private Map<Service, Map<Capability, String>> parseCapabilitiesFromJson(String json) {
2367-
Map<Service, Map<Capability, String>> result = new HashMap<>();
2368-
if (StringUtils.isBlank(json)) {
2369-
return result;
2370-
}
2371-
try {
2372-
JsonObject root = JsonParser.parseString(json).getAsJsonObject();
2373-
JsonArray servicesArr = root.getAsJsonArray("services");
2374-
if (servicesArr == null) {
2375-
return result;
2376-
}
2377-
JsonObject capsObj = root.has("capabilities") ? root.getAsJsonObject("capabilities") : null;
2378-
for (JsonElement el : servicesArr) {
2379-
String svcName = el.getAsString();
2380-
Service service = Service.getService(svcName);
2381-
if (service == null) {
2382-
logger.warn("Unknown network service '{}' in extension capabilities JSON — skipping", svcName);
2383-
continue;
2384-
}
2385-
Map<Capability, String> capMap = new HashMap<>();
2386-
if (capsObj != null && capsObj.has(svcName)) {
2387-
JsonObject svcCaps = capsObj.getAsJsonObject(svcName);
2388-
for (Map.Entry<String, JsonElement> entry : svcCaps.entrySet()) {
2389-
Capability cap = Capability.getCapability(entry.getKey());
2390-
if (cap != null) {
2391-
capMap.put(cap, entry.getValue().getAsString());
2392-
}
2393-
}
2394-
}
2395-
result.put(service, capMap);
2396-
}
2397-
} catch (Exception e) {
2398-
logger.warn("Failed to parse network.capabilities JSON: {}", e.getMessage());
2399-
}
2400-
return result;
2391+
return buildCapabilitiesFromDetailKeys(extDetails);
24012392
}
24022393
}

0 commit comments

Comments
 (0)