Skip to content

Commit 6504f81

Browse files
weizhouapachenvazquez
authored andcommitted
Netris VPN: create vpc gateway with specified IP (#63)
1 parent b9a60dd commit 6504f81

File tree

6 files changed

+188
-11
lines changed

6 files changed

+188
-11
lines changed

api/src/main/java/org/apache/cloudstack/api/command/user/vpn/CreateVpnGatewayCmd.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.cloudstack.api.BaseAsyncCreateCmd;
2929
import org.apache.cloudstack.api.Parameter;
3030
import org.apache.cloudstack.api.ServerApiException;
31+
import org.apache.cloudstack.api.response.IPAddressResponse;
3132
import org.apache.cloudstack.api.response.Site2SiteVpnGatewayResponse;
3233
import org.apache.cloudstack.api.response.VpcResponse;
3334
import org.apache.cloudstack.context.CallContext;
@@ -44,9 +45,15 @@ public class CreateVpnGatewayCmd extends BaseAsyncCreateCmd {
4445
type = CommandType.UUID,
4546
entityType = VpcResponse.class,
4647
required = true,
47-
description = "public ip address id of the vpn gateway")
48+
description = "id of the vpc")
4849
private Long vpcId;
4950

51+
@Parameter(name = ApiConstants.IP_ADDRESS_ID,
52+
type = CommandType.UUID,
53+
entityType = IPAddressResponse.class,
54+
description = "the public IP address ID for which VPN gateway is being enabled. By default the source NAT IP or router IP will be used.")
55+
private Long ipAddressId;
56+
5057
@Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the vpn to the end user or not", since = "4.4", authorized = {RoleType.Admin})
5158
private Boolean display;
5259

@@ -58,6 +65,10 @@ public Long getVpcId() {
5865
return vpcId;
5966
}
6067

68+
public Long getIpAddressId() {
69+
return ipAddressId;
70+
}
71+
6172
@Deprecated
6273
public Boolean getDisplay() {
6374
return display;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3799,6 +3799,9 @@ public IPAddressVO getIpAddressForVpcVr(Vpc vpc, IPAddressVO ipAddress, boolean
37993799
}
38003800
return ipAddressForVR;
38013801
} else if (ipAddress != null) {
3802+
if (ipAddress.isSourceNat()) {
3803+
throw new InvalidParameterValueException("Vpn service can not be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId());
3804+
}
38023805
return ipAddress;
38033806
}
38043807

server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,16 @@ public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRan
226226
Pair<String, Integer> cidr = null;
227227

228228
if (networkId != null) {
229+
Network network = _networkMgr.getNetwork(networkId);
229230
long ipAddressOwner = ipAddr.getAccountId();
230231
vpnVO = _remoteAccessVpnDao.findByAccountAndNetwork(ipAddressOwner, networkId);
231232
if (vpnVO != null) {
232233
if (vpnVO.getState() == RemoteAccessVpn.State.Added) {
233234
return vpnVO;
234235
}
235236

236-
throw new InvalidParameterValueException(String.format("A remote access VPN already exists for the account [%s].", ipAddressOwner));
237+
throw new InvalidParameterValueException(String.format("A remote access VPN already exists for the network %s.", network));
237238
}
238-
Network network = _networkMgr.getNetwork(networkId);
239239
if (!_networkMgr.areServicesSupportedInNetwork(network.getId(), Service.Vpn)) {
240240
throw new InvalidParameterValueException("Vpn service is not supported in network id=" + ipAddr.getAssociatedWithNetworkId());
241241
}
@@ -311,9 +311,6 @@ private void validateIpAddressForVpnServiceOnVpc(Vpc vpc, IPAddressVO ipAddress)
311311
if (isVpcVRSourceNat && !ipAddress.isSourceNat()) {
312312
throw new InvalidParameterValueException("Vpn service can only be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId());
313313
}
314-
if (!isVpcVRSourceNat && ipAddress.isSourceNat()) {
315-
throw new InvalidParameterValueException("Vpn service can not be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId());
316-
}
317314
if (!isVpcVRSourceNat) {
318315
IPAddressVO ipAddressForVpcVR = vpcManager.getIpAddressForVpcVr(vpc, ipAddress, true);
319316
if (!vpcManager.configStaticNatForVpcVr(vpc, ipAddressForVpcVR)) {

server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ public Site2SiteVpnGateway createVpnGateway(CreateVpnGatewayCmd cmd) {
149149
throw new InvalidParameterValueException(String.format("The VPN gateway of VPC %s already existed!", vpc));
150150
}
151151

152-
IPAddressVO ipAddress = getIpAddressIdForVpn(vpcId, vpc.getVpcOfferingId());
152+
IPAddressVO requestedIp = _ipAddressDao.findById(cmd.getIpAddressId());
153+
IPAddressVO ipAddress = getIpAddressIdForVpn(vpcId, vpc.getVpcOfferingId(), requestedIp);
153154
Site2SiteVpnGatewayVO gw = new Site2SiteVpnGatewayVO(owner.getAccountId(), owner.getDomainId(), ipAddress.getId(), vpcId);
154155

155156
if (cmd.getDisplay() != null) {
@@ -160,15 +161,15 @@ public Site2SiteVpnGateway createVpnGateway(CreateVpnGatewayCmd cmd) {
160161
return gw;
161162
}
162163

163-
private IPAddressVO getIpAddressIdForVpn(Long vpcId, Long vpcOferingId) {
164+
private IPAddressVO getIpAddressIdForVpn(Long vpcId, Long vpcOferingId, IPAddressVO requestedIp) {
164165
VpcOfferingServiceMapVO mapForSourceNat = vpcOfferingServiceMapDao.findByServiceProviderAndOfferingId(Network.Service.SourceNat.getName(), Network.Provider.VPCVirtualRouter.getName(), vpcOferingId);
165166
VpcOfferingServiceMapVO mapForVpn = vpcOfferingServiceMapDao.findByServiceProviderAndOfferingId(Network.Service.Vpn.getName(), Network.Provider.VPCVirtualRouter.getName(), vpcOferingId);
166167
if (mapForSourceNat == null && mapForVpn != null) {
167168
// Use Static NAT IP of VPC VR
168169
logger.debug(String.format("The VPC VR provides %s Service, however it does not provide %s service, trying to configure using IP of VPC VR", Network.Service.Vpn.getName(), Network.Service.SourceNat.getName()));
169170

170171
Vpc vpc = _vpcDao.findById(vpcId);
171-
IPAddressVO ipAddressForVpcVR = vpcManager.getIpAddressForVpcVr(vpc, null, true);
172+
IPAddressVO ipAddressForVpcVR = vpcManager.getIpAddressForVpcVr(vpc, requestedIp, true);
172173
if (!vpcManager.configStaticNatForVpcVr(vpc, ipAddressForVpcVR)) {
173174
throw new CloudRuntimeException("Failed to enable static nat for VPC VR as part of vpn gateway");
174175
}
@@ -179,6 +180,9 @@ private IPAddressVO getIpAddressIdForVpn(Long vpcId, Long vpcOferingId) {
179180
if (ips.size() != 1) {
180181
throw new CloudRuntimeException("Cannot found source nat ip of vpc " + vpcId);
181182
}
183+
if (requestedIp != null && requestedIp.getId() != ips.get(0).getId()) {
184+
throw new CloudRuntimeException(String.format("Cannot use requested IP %s as it is not the Source NAT IP", requestedIp.getAddress().addr()));
185+
}
182186
return ips.get(0);
183187
}
184188
}

ui/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,6 +3011,7 @@
30113011
"message.delete.vpn.connection": "Please confirm that you want to delete VPN connection.",
30123012
"message.delete.vpn.customer.gateway": "Please confirm that you want to delete this VPN customer gateway.",
30133013
"message.delete.vpn.gateway": "Please confirm that you want to delete this VPN Gateway.",
3014+
"message.delete.vpn.gateway.failed": "Failed to delete VPN Gateway.",
30143015
"message.delete.webhook": "Please confirm that you want to delete this Webhook.",
30153016
"message.delete.webhook.delivery": "Please confirm that you want to delete this Webhook delivery.",
30163017
"message.deleting.firewall.policy": "Deleting Firewall Policy",
@@ -3573,6 +3574,7 @@
35733574
"message.success.delete.tungsten.router.table": "Successfully removed Router Table",
35743575
"message.success.delete.tungsten.tag": "Successfully removed Tag",
35753576
"message.success.delete.vm": "Successfully deleted Instance",
3577+
"message.success.delete.vpn.gateway": "Successfully deleted VPN gateway",
35763578
"message.success.disable.saml.auth": "Successfully disabled SAML authorization",
35773579
"message.success.disable.vpn": "Successfully disabled VPN",
35783580
"message.success.edit.acl": "Successfully edited ACL rule",

ui/src/views/network/VpnDetails.vue

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
<p>{{ $t('message.enabled.vpn') }} <strong>{{ remoteAccessVpn.publicip }}</strong></p>
2222
<p>{{ $t('message.enabled.vpn.ip.sec') }} <strong>{{ remoteAccessVpn.presharedkey }}</strong></p>
2323
<a-divider/>
24-
<a-button><router-link :to="{ path: '/vpnuser'}">{{ $t('label.manage.vpn.user') }}</router-link></a-button>
2524
<a-button
2625
style="margin-left: 10px"
2726
type="primary"
@@ -30,6 +29,7 @@
3029
:disabled="!('deleteRemoteAccessVpn' in $store.getters.apis)">
3130
{{ $t('label.disable.vpn') }}
3231
</a-button>
32+
<a-button><router-link :to="{ path: '/vpnuser'}">{{ $t('label.manage.vpn.user') }}</router-link></a-button>
3333
</div>
3434

3535
<a-modal
@@ -53,7 +53,11 @@
5353

5454
</div>
5555
<div v-else>
56-
<a-button :disabled="!('createRemoteAccessVpn' in $store.getters.apis)" type="primary" @click="enableVpn = true">
56+
<a-button
57+
:disabled="!('createRemoteAccessVpn' in $store.getters.apis)"
58+
type="primary"
59+
style="margin-left: 10px"
60+
@click="enableVpn = true">
5761
{{ $t('label.enable.vpn') }}
5862
</a-button>
5963

@@ -77,6 +81,62 @@
7781
</a-modal>
7882

7983
</div>
84+
85+
<br>
86+
<div v-if="vpnGateway">
87+
<div>
88+
<a-button
89+
:disabled="!('deleteVpnGateway' in $store.getters.apis)"
90+
style="margin-left: 10px"
91+
danger
92+
type="primary"
93+
@click="deleteVpnGateway = true">
94+
{{ $t('label.delete.vpn.gateway') }}
95+
</a-button>
96+
</div>
97+
<a-modal
98+
:visible="deleteVpnGateway"
99+
:footer="null"
100+
:title="$t('label.enable.vpn')"
101+
:maskClosable="false"
102+
:closable="true"
103+
@cancel="deleteVpnGateway = false">
104+
<div v-ctrl-enter="handleDeleteVpnGateway">
105+
<p>{{ $t('message.delete.vpn.gateway') }}</p>
106+
<div :span="24" class="action-button">
107+
<a-button @click="deleteVpnGateway = false">{{ $t('label.cancel') }}</a-button>
108+
<a-button :loading="loading" type="primary" @click="handleDeleteVpnGateway" ref="submit">{{ $t('label.ok') }}</a-button>
109+
</div>
110+
</div>
111+
</a-modal>
112+
</div>
113+
<div v-else-if="vpnGatewayEnabled">
114+
<div>
115+
<a-button
116+
:disabled="!('createVpnGateway' in $store.getters.apis)"
117+
style="margin-left: 10px"
118+
type="primary"
119+
@click="createVpnGateway = true">
120+
{{ $t('label.add.vpn.gateway') }}
121+
</a-button>
122+
</div>
123+
<a-modal
124+
:visible="createVpnGateway"
125+
:footer="null"
126+
:title="$t('label.add.vpn.gateway')"
127+
:maskClosable="false"
128+
:closable="true"
129+
@cancel="createVpnGateway = false">
130+
<div v-ctrl-enter="handleCreateVpnGateway">
131+
<p>{{ $t('message.add.vpn.gateway') }}</p>
132+
<div :span="24" class="action-button">
133+
<a-button @click="createVpnGateway = false">{{ $t('label.cancel') }}</a-button>
134+
<a-button :loading="loading" type="primary" @click="handleCreateVpnGateway" ref="submit">{{ $t('label.ok') }}</a-button>
135+
</div>
136+
</div>
137+
</a-modal>
138+
</div>
139+
80140
</template>
81141

82142
<script>
@@ -94,6 +154,10 @@ export default {
94154
remoteAccessVpn: null,
95155
enableVpn: false,
96156
disableVpn: false,
157+
vpnGateway: null,
158+
vpnGatewayEnabled: false,
159+
createVpnGateway: false,
160+
deleteVpnGateway: false,
97161
isSubmitted: false
98162
}
99163
},
@@ -124,6 +188,23 @@ export default {
124188
console.log(error)
125189
this.$notifyError(error)
126190
})
191+
if (this.resource.vpcid) {
192+
this.vpnGatewayEnabled = true
193+
api('listVpnGateways', {
194+
vpcid: this.resource.vpcid,
195+
listAll: true
196+
}).then(response => {
197+
const vpnGateways = response.listvpngatewaysresponse.vpngateway || []
198+
for (const vpnGateway of vpnGateways) {
199+
if (vpnGateway.publicip === this.resource.ipaddress) {
200+
this.vpnGateway = vpnGateway
201+
}
202+
}
203+
}).catch(error => {
204+
console.log(error)
205+
this.$notifyError(error)
206+
})
207+
}
127208
},
128209
handleCreateVpn () {
129210
if (this.isSubmitted) return
@@ -211,6 +292,85 @@ export default {
211292
this.parentToggleLoading()
212293
this.isSubmitted = false
213294
})
295+
},
296+
handleCreateVpnGateway () {
297+
if (this.isSubmitted) return
298+
this.isSubmitted = true
299+
this.parentToggleLoading()
300+
this.createVpnGateway = false
301+
const params = {
302+
vpcid: this.resource.vpcid,
303+
ipaddressid: this.resource.id
304+
}
305+
api('createVpnGateway', params).then(response => {
306+
this.$pollJob({
307+
jobId: response.createvpngatewayresponse.jobid,
308+
successMessage: this.$t('message.success.add.vpn.gateway'),
309+
successMethod: result => {
310+
this.fetchData()
311+
this.parentToggleLoading()
312+
this.isSubmitted = false
313+
},
314+
errorMessage: this.$t('message.add.vpn.gateway.failed'),
315+
errorMethod: () => {
316+
this.fetchData()
317+
this.parentToggleLoading()
318+
this.isSubmitted = false
319+
},
320+
loadingMessage: this.$t('message.add.vpn.gateway.processing'),
321+
catchMessage: this.$t('error.fetching.async.job.result'),
322+
catchMethod: () => {
323+
this.fetchData()
324+
this.parentFetchData()
325+
this.parentToggleLoading()
326+
this.isSubmitted = false
327+
}
328+
})
329+
}).catch(error => {
330+
this.$notifyError(error)
331+
this.fetchData()
332+
this.parentFetchData()
333+
this.parentToggleLoading()
334+
this.isSubmitted = false
335+
})
336+
},
337+
handleDeleteVpnGateway () {
338+
if (this.isSubmitted) return
339+
this.isSubmitted = true
340+
this.parentToggleLoading()
341+
this.deleteVpnGateway = false
342+
api('deleteVpnGateway', {
343+
id: this.vpnGateway.id
344+
}).then(response => {
345+
this.$pollJob({
346+
jobId: response.deletevpngatewayresponse.jobid,
347+
successMessage: this.$t('message.success.delete.vpn.gateway'),
348+
successMethod: () => {
349+
this.fetchData()
350+
this.parentToggleLoading()
351+
this.isSubmitted = false
352+
},
353+
errorMessage: this.$t('message.delete.vpn.gateway.failed'),
354+
errorMethod: () => {
355+
this.fetchData()
356+
this.parentToggleLoading()
357+
this.isSubmitted = false
358+
},
359+
catchMessage: this.$t('error.fetching.async.job.result'),
360+
catchMethod: () => {
361+
this.fetchData()
362+
this.parentFetchData()
363+
this.parentToggleLoading()
364+
this.isSubmitted = false
365+
}
366+
})
367+
}).catch(error => {
368+
this.$notifyError(error)
369+
this.fetchData()
370+
this.parentFetchData()
371+
this.parentToggleLoading()
372+
this.isSubmitted = false
373+
})
214374
}
215375
}
216376
}

0 commit comments

Comments
 (0)