Skip to content

Commit 0dc7e32

Browse files
committed
extension: implement VpcProvider, NetworkACLServiceProvider and VPC lifecycle
- NetworkExtensionElement now implements VpcProvider and NetworkACLServiceProvider - implementVpc: creates the VPC namespace via implement script and applies VPC source NAT - implement(network): skips source NAT assignment for VPC tier networks (handled by implementVpc) - destroy(network): uses 'shutdown' script for VPC tiers to preserve shared namespace - shutdownVpc: calls 'destroy' script on all extension-backed VPC tier networks to remove namespace - VpcManagerImpl: dynamically discovers extension-backed VpcProviders via extensionHelper - UI: add updateRegisteredExtension action to ExtensionResourcesTab
1 parent 73a8b0f commit 0dc7e32

5 files changed

Lines changed: 360 additions & 11 deletions

File tree

framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java

Lines changed: 169 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@
6767
import com.cloud.network.element.FirewallServiceProvider;
6868
import com.cloud.network.element.IpDeployer;
6969
import com.cloud.network.element.LoadBalancingServiceProvider;
70+
import com.cloud.network.element.NetworkACLServiceProvider;
7071
import com.cloud.network.element.NetworkElement;
7172
import com.cloud.network.element.PortForwardingServiceProvider;
7273
import com.cloud.network.element.SourceNatServiceProvider;
7374
import com.cloud.network.element.StaticNatServiceProvider;
7475
import com.cloud.network.element.UserDataServiceProvider;
76+
import com.cloud.network.element.VpcProvider;
77+
import com.cloud.network.vpc.NetworkACLItem;
78+
import com.cloud.network.vpc.PrivateGateway;
79+
import com.cloud.network.vpc.StaticRouteProfile;
80+
import com.cloud.network.vpc.Vpc;
7581
import com.cloud.network.lb.LoadBalancingRule;
7682
import com.cloud.network.rules.FirewallRule;
7783
import com.cloud.network.rules.FirewallRuleVO;
@@ -204,7 +210,7 @@ public class NetworkExtensionElement extends AdapterBase implements
204210
PortForwardingServiceProvider, IpDeployer, NetworkCustomActionProvider,
205211
DhcpServiceProvider, DnsServiceProvider, FirewallServiceProvider,
206212
UserDataServiceProvider, LoadBalancingServiceProvider,
207-
AggregatedCommandExecutor {
213+
VpcProvider, NetworkACLServiceProvider, AggregatedCommandExecutor {
208214

209215
private static final Map<Service, Map<Capability, String>> DEFAULT_CAPABILITIES = new HashMap<>();
210216

@@ -446,11 +452,11 @@ public boolean implement(Network network, NetworkOffering offering, DeployDestin
446452
return false;
447453
}
448454

449-
// Step 3: Configure source NAT if supported.
450-
if (canHandle(network, Service.SourceNat)) {
455+
// Step 3: Configure source NAT for non-VPC networks.
456+
// VPC source NAT is managed at implementVpc().
457+
if (network.getVpcId() == null && canHandle(network, Service.SourceNat)) {
451458
try {
452459
Account owner = accountService.getAccount(network.getAccountId());
453-
PublicIp sourceNatIp = null;
454460
PublicIpAddress existingIp = networkModel.getSourceNatIpAddressForGuestNetwork(owner, network);
455461
if (existingIp != null) {
456462
applyIps(network, List.of(existingIp), Set.of(Service.SourceNat));
@@ -522,7 +528,9 @@ public boolean destroy(Network network, ReservationContext context)
522528
args.add("--network-id"); args.add(String.valueOf(network.getId()));
523529
args.add("--vlan"); args.add(safeStr(getVlanId(network)));
524530
args.addAll(getVpcIdArgs(network));
525-
boolean result = executeScript(network, "destroy", args.toArray(new String[0]));
531+
// For VPC tiers, keep shared namespace until shutdownVpc() by using shutdown here.
532+
final String action = network.getVpcId() == null ? "destroy" : "shutdown";
533+
boolean result = executeScript(network, action, args.toArray(new String[0]));
526534
if (result) {
527535
cleanupPlaceholderNicIp(network, context);
528536
networkDetailsDao.removeDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS);
@@ -2137,4 +2145,160 @@ private String buildRestoreNetworkData(Network network, List<NicVO> nics,
21372145

21382146
return Base64.getEncoder().encodeToString(json.toString().getBytes(StandardCharsets.UTF_8));
21392147
}
2148+
2149+
// ---- VpcProvider ----
2150+
2151+
protected Network findVpcAnchorNetwork(long vpcId) {
2152+
final List<? extends Network> networks = networkModel.listNetworksByVpc(vpcId);
2153+
if (networks == null || networks.isEmpty()) {
2154+
return null;
2155+
}
2156+
2157+
for (final Network network : networks) {
2158+
if (canHandle(network, null)) {
2159+
return network;
2160+
}
2161+
}
2162+
return null;
2163+
}
2164+
2165+
protected PublicIpAddress getVpcSourceNatIp(long vpcId) {
2166+
final List<IPAddressVO> ips = ipAddressDao.listByAssociatedVpc(vpcId, true);
2167+
if (ips == null || ips.isEmpty()) {
2168+
return null;
2169+
}
2170+
IPAddressVO selected = null;
2171+
for (final IPAddressVO ip : ips) {
2172+
if (ip.getState() != IpAddress.State.Releasing) {
2173+
selected = ip;
2174+
break;
2175+
}
2176+
}
2177+
if (selected == null) {
2178+
selected = ips.get(0);
2179+
}
2180+
2181+
final VlanVO vlan = vlanDao.findById(selected.getVlanId());
2182+
if (vlan == null) {
2183+
logger.warn("No VLAN found for VPC source NAT IP {} (vpc={})", selected.getAddress(), vpcId);
2184+
return null;
2185+
}
2186+
return PublicIp.createFromAddrAndVlan(selected, vlan);
2187+
}
2188+
2189+
/**
2190+
* Creates the VPC namespace and applies VPC source NAT upfront.
2191+
*/
2192+
@Override
2193+
public boolean implementVpc(Vpc vpc, DeployDestination dest, ReservationContext context)
2194+
throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException {
2195+
final Network anchorNetwork = findVpcAnchorNetwork(vpc.getId());
2196+
if (anchorNetwork == null) {
2197+
logger.debug("implementVpc: no VPC tier network found for vpc {}", vpc.getId());
2198+
return true;
2199+
}
2200+
2201+
ensureExtensionDetails(anchorNetwork);
2202+
final String extensionIp = ensureExtensionIp(anchorNetwork);
2203+
2204+
final List<String> args = new ArrayList<>();
2205+
args.add("--network-id"); args.add(String.valueOf(anchorNetwork.getId()));
2206+
args.add("--vlan"); args.add(safeStr(getVlanId(anchorNetwork)));
2207+
args.add("--gateway"); args.add(safeStr(anchorNetwork.getGateway()));
2208+
args.add("--cidr"); args.add(safeStr(anchorNetwork.getCidr()));
2209+
args.add("--extension-ip"); args.add(safeStr(extensionIp));
2210+
args.addAll(getVpcIdArgs(anchorNetwork));
2211+
2212+
if (!executeScript(anchorNetwork, "implement", args.toArray(new String[0]))) {
2213+
return false;
2214+
}
2215+
2216+
if (canHandle(anchorNetwork, Service.SourceNat)) {
2217+
final PublicIpAddress sourceNatIp = getVpcSourceNatIp(vpc.getId());
2218+
if (sourceNatIp != null) {
2219+
applyIps(anchorNetwork, List.of(sourceNatIp), Set.of(Service.SourceNat));
2220+
}
2221+
}
2222+
2223+
return true;
2224+
}
2225+
2226+
/**
2227+
* Removes the shared VPC namespace by destroying all extension-backed VPC tiers.
2228+
*/
2229+
@Override
2230+
public boolean shutdownVpc(Vpc vpc, ReservationContext context)
2231+
throws ConcurrentOperationException, ResourceUnavailableException {
2232+
final List<? extends Network> networks = networkModel.listNetworksByVpc(vpc.getId());
2233+
if (networks == null || networks.isEmpty()) {
2234+
return true;
2235+
}
2236+
2237+
boolean result = true;
2238+
for (final Network network : networks) {
2239+
if (!canHandle(network, null)) {
2240+
continue;
2241+
}
2242+
2243+
final List<String> args = new ArrayList<>();
2244+
args.add("--network-id"); args.add(String.valueOf(network.getId()));
2245+
args.add("--vlan"); args.add(safeStr(getVlanId(network)));
2246+
args.addAll(getVpcIdArgs(network));
2247+
2248+
final boolean tierResult = executeScript(network, "destroy", args.toArray(new String[0]));
2249+
if (tierResult) {
2250+
networkDetailsDao.removeDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS);
2251+
}
2252+
result = result && tierResult;
2253+
}
2254+
2255+
return result;
2256+
}
2257+
2258+
/** Private gateways are not supported by the network extension element. */
2259+
@Override
2260+
public boolean createPrivateGateway(PrivateGateway gateway)
2261+
throws ConcurrentOperationException, ResourceUnavailableException {
2262+
return true;
2263+
}
2264+
2265+
/** Private gateways are not supported by the network extension element. */
2266+
@Override
2267+
public boolean deletePrivateGateway(PrivateGateway gateway)
2268+
throws ConcurrentOperationException, ResourceUnavailableException {
2269+
return true;
2270+
}
2271+
2272+
/** Static routes are not supported by the network extension element. */
2273+
@Override
2274+
public boolean applyStaticRoutes(Vpc vpc, List<StaticRouteProfile> routes)
2275+
throws ResourceUnavailableException {
2276+
return true;
2277+
}
2278+
2279+
/** ACL items on private gateways are not supported by the network extension element. */
2280+
@Override
2281+
public boolean applyACLItemsToPrivateGw(PrivateGateway gateway, List<? extends NetworkACLItem> rules)
2282+
throws ResourceUnavailableException {
2283+
return true;
2284+
}
2285+
2286+
@Override
2287+
public boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address) {
2288+
return true;
2289+
}
2290+
2291+
@Override
2292+
public boolean applyNetworkACLs(Network config, List<? extends NetworkACLItem> rules) throws ResourceUnavailableException {
2293+
if (!canHandle(config, Service.NetworkACL)) {
2294+
return true;
2295+
}
2296+
// ACL semantics for this extension are handled by script policy/rule processing.
2297+
return true;
2298+
}
2299+
2300+
@Override
2301+
public boolean reorderAclRules(Vpc vpc, List<? extends Network> networks, List<? extends NetworkACLItem> networkACLItems) {
2302+
return true;
2303+
}
21402304
}

server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
import org.apache.cloudstack.api.command.user.vpc.UpdateVPCCmd;
8585
import org.apache.cloudstack.context.CallContext;
8686
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
87+
import org.apache.cloudstack.extension.Extension;
8788
import org.apache.cloudstack.extension.ExtensionHelper;
8889
import org.apache.cloudstack.framework.config.ConfigKey;
8990
import org.apache.cloudstack.framework.config.Configurable;
@@ -2268,8 +2269,24 @@ private void CheckAccountsAccess(Vpc vpc, Account networkAccount) {
22682269
public List<VpcProvider> getVpcElements() {
22692270
if (vpcElements == null) {
22702271
vpcElements = new ArrayList<VpcProvider>();
2271-
vpcElements.add((VpcProvider) _ntwkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName()));
2272-
vpcElements.add((VpcProvider) _ntwkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName()));
2272+
final NetworkElement vpcVirtualRouter = _ntwkModel.getElementImplementingProvider(Provider.VPCVirtualRouter.getName());
2273+
if (vpcVirtualRouter instanceof VpcProvider) {
2274+
vpcElements.add((VpcProvider) vpcVirtualRouter);
2275+
}
2276+
2277+
final NetworkElement contrailVpcRouter = _ntwkModel.getElementImplementingProvider(Provider.JuniperContrailVpcRouter.getName());
2278+
if (contrailVpcRouter instanceof VpcProvider) {
2279+
vpcElements.add((VpcProvider) contrailVpcRouter);
2280+
}
2281+
2282+
// Add extension-backed network orchestrators that can serve as VPC providers.
2283+
for (final Extension extension : extensionHelper.listExtensionsByType(Extension.Type.NetworkOrchestrator)) {
2284+
final String providerName = extension.getName();
2285+
final NetworkElement element = _ntwkModel.getElementImplementingProvider(providerName);
2286+
if (element instanceof VpcProvider) {
2287+
vpcElements.add((VpcProvider) element);
2288+
}
2289+
}
22732290
}
22742291

22752292
if (vpcElements == null) {

ui/public/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
"label.action.unmanage.volume": "Unmanage Volume",
236236
"label.action.unmanage.volumes": "Unmanage Volumes",
237237
"label.action.unregister.extension.resource": "Unregister extension resource",
238+
"label.action.update.extension.resource": "Update extension resource details",
238239
"label.action.update.host": "Update Host",
239240
"label.action.update.security.groups": "Update security groups",
240241
"label.action.update.offering.access": "Update offering access",
@@ -4066,6 +4067,8 @@
40664067
"message.validate.min": "Please enter a value greater than or equal to {0}.",
40674068
"message.action.delete.object.storage": "Please confirm that you want to delete this Object Store",
40684069
"message.action.unregister.extension.resource": "Please confirm that you want to unregister extension with this resource",
4070+
"message.action.update.extension.resource": "Update the extension resource registration details",
4071+
"message.success.update.extension.resource": "Successfully updated extension resource registration",
40694072
"message.bgp.peers.null": "Please note, if no BGP peers are selected, the VR will connect to <br> (1) dedicated BGP peers the owner can access, if the owner has dedicated BGP peers and account setting use.system.bgp.peers is set to false; <br> (2) all BGP peers the owner can access, otherwise.<br>",
40704073
"message.bucket.delete": "Please confirm that you want to delete this Bucket",
40714074
"migrate.from": "Migrate from",

ui/src/views/extension/ExtensionResourcesTab.vue

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
{{ text && $toLocaleDate(text) }}
3535
</template>
3636
<template v-if="column.key === 'actions'">
37+
<span style="margin-right: 5px">
38+
<tooltip-button
39+
v-if="'updateRegisteredExtension' in $store.getters.apis"
40+
:tooltip="$t('label.action.update.extension.resource')"
41+
type="default"
42+
icon="edit-outlined"
43+
@onClick="openUpdateModal(record)" />
44+
</span>
3745
<span style="margin-right: 5px">
3846
<a-popconfirm
3947
v-if="'unregisterExtension' in $store.getters.apis"
@@ -59,6 +67,19 @@
5967
:data-map="record.details" />
6068
</template>
6169
</a-table>
70+
<a-modal
71+
v-if="updateModalVisible"
72+
:visible="updateModalVisible"
73+
:title="$t('label.action.update.extension.resource')"
74+
:closable="true"
75+
:footer="null"
76+
@cancel="closeUpdateModal">
77+
<update-registered-extension
78+
:resource="resource"
79+
:extension-resource="selectedResource"
80+
@refresh-data="$emit('refresh-data')"
81+
@close-action="closeUpdateModal" />
82+
</a-modal>
6283
</div>
6384
</template>
6485

@@ -67,12 +88,14 @@ import { postAPI } from '@/api'
6788
import eventBus from '@/config/eventBus'
6889
import ObjectListTable from '@/components/view/ObjectListTable.vue'
6990
import TooltipButton from '@/components/widgets/TooltipButton'
91+
import UpdateRegisteredExtension from '@/views/extension/UpdateRegisteredExtension'
7092
7193
export default {
7294
name: 'ExtensionResourcesTab',
7395
components: {
7496
ObjectListTable,
75-
TooltipButton
97+
TooltipButton,
98+
UpdateRegisteredExtension
7699
},
77100
props: {
78101
resource: {
@@ -103,7 +126,9 @@ export default {
103126
title: this.$t('label.actions')
104127
}
105128
],
106-
unregisterLoading: false
129+
unregisterLoading: false,
130+
updateModalVisible: false,
131+
selectedResource: null
107132
}
108133
},
109134
computed: {
@@ -112,13 +137,22 @@ export default {
112137
}
113138
},
114139
methods: {
140+
openUpdateModal (record) {
141+
this.selectedResource = record
142+
this.updateModalVisible = true
143+
},
144+
closeUpdateModal () {
145+
this.updateModalVisible = false
146+
this.selectedResource = null
147+
},
115148
unregisterExtension (record) {
116149
const params = {
117150
extensionid: this.resource.id,
118151
resourceid: record.id,
119152
resourcetype: record.type
120153
}
121-
postAPI('unregisterExtension', params).then(json => {
154+
this.unregisterLoading = true
155+
postAPI('unregisterExtension', params).then(() => {
122156
eventBus.emit('async-job-complete', null)
123157
this.$notification.success({
124158
message: this.$t('label.unregister.extension'),
@@ -127,7 +161,7 @@ export default {
127161
}).catch(error => {
128162
this.$notifyError(error)
129163
}).finally(() => {
130-
this.deleteLoading = false
164+
this.unregisterLoading = false
131165
})
132166
}
133167
}

0 commit comments

Comments
 (0)