Skip to content

Commit f02394d

Browse files
committed
extension: support update-vpc-source-nat-ip
1 parent 44b6b94 commit f02394d

File tree

5 files changed

+509
-12
lines changed

5 files changed

+509
-12
lines changed

extensions/network-namespace/README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ Actions:
808808
> are NOT removed on destroy — they may still be used by other networks or for
809809
> VM connectivity.
810810
811-
### VPC lifecycle commands: `implement-vpc`, `shutdown-vpc`, `destroy-vpc`
811+
### VPC lifecycle commands: `implement-vpc`, `update-vpc-source-nat-ip`, `shutdown-vpc`, `destroy-vpc`
812812

813813
These commands manage VPC-level state. Called by `NetworkExtensionElement` when
814814
implementing, shutting down, or destroying a VPC (before or after per-tier
@@ -841,6 +841,35 @@ Actions:
841841
> This command runs **before** any tier networks are implemented. Tier networks
842842
> inherit the same namespace.
843843
844+
#### `update-vpc-source-nat-ip`
845+
846+
```
847+
network-namespace-wrapper.sh update-vpc-source-nat-ip \
848+
--vpc-id <vpc-id> \
849+
--public-ip <new-source-nat-ip> \
850+
[--cidr <vpc-cidr>] \
851+
[--public-vlan <pvlan>] \
852+
[--public-gateway <gw>] \
853+
[--public-cidr <cidr>] \
854+
[--source-nat true|false]
855+
```
856+
857+
Actions:
858+
1. Ensure the target public veth pair exists (`vph-<pvlan>-<vpc-id>` / `vpn-<pvlan>-<vpc-id>`) and assign the new public IP inside the VPC namespace.
859+
2. Update host and namespace routes for the new source NAT egress path:
860+
* keep host route `<public-ip>/32` via `vph-<pvlan>-<vpc-id>`
861+
* replace namespace default route via `--public-gateway` on `vpn-<pvlan>-<vpc-id>` when provided.
862+
3. Rebuild VPC SNAT chain `CS_EXTNET_<vpc-id>_VPC_POST` so exactly one SNAT rule remains:
863+
* `-s <vpc-cidr> -o vpn-<pvlan>-<vpc-id> -j SNAT --to-source <public-ip>`.
864+
4. Reconcile persisted VPC IP markers under
865+
`/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/ips/`:
866+
* set the new source NAT IP file to `true`
867+
* set all other VPC public IP marker files to `false`
868+
* persist/update `<ip>.pvlan` for the new source NAT IP.
869+
870+
> This command is used by `NetworkExtensionElement.updateVpcSourceNatIp()` when
871+
> `updateVPC` is called with `sourcenatipaddress`; it avoids full VPC restart.
872+
844873
#### `shutdown-vpc`
845874

846875
```
@@ -1483,7 +1512,8 @@ isolated networks. Key differences from isolated networks:
14831512
instead of each network getting its own (`cs-net-<networkId>`).
14841513
* **Host affinity**: All tiers of a VPC land on the same KVM host via stable hash-based
14851514
selection using the VPC ID as the routing key.
1486-
* **VPC-level operations**: `implement-vpc`, `shutdown-vpc`, `destroy-vpc` commands
1515+
* **VPC-level operations**: `implement-vpc`, `update-vpc-source-nat-ip`,
1516+
`shutdown-vpc`, `destroy-vpc` commands
14871517
manage VPC-wide state (namespace creation/teardown).
14881518
* **VPC tier operations**: `implement-network`, `shutdown-network`, `destroy-network`
14891519
commands manage per-tier bridges and routes; the namespace is preserved across
@@ -1514,6 +1544,8 @@ The test covers:
15141544
* Full network lifecycle: implement → assign-ip (source NAT) → static NAT →
15151545
port forwarding → firewall rules → DHCP/DNS → shutdown / destroy.
15161546
* VPC multi-tier networks with shared namespace and automatic host affinity.
1547+
* VPC source NAT IP update flow (`test_09_vpc_source_nat_ip_update`) including
1548+
source NAT flag flip from old public IP to new public IP.
15171549
* NSP state transitions: Disabled → Enabled → Disabled → Deleted.
15181550
* Tests `test_04`, `test_05`, `test_06` (DHCP, DNS, LB) require `arping`,
15191551
`dnsmasq`, and `haproxy` on the KVM hosts; the test skips them automatically

extensions/network-namespace/network-namespace-wrapper.sh

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3383,6 +3383,129 @@ cmd_implement_vpc() {
33833383
log "implement-vpc: done vpc=${VPC_ID} namespace=${NAMESPACE}"
33843384
}
33853385

3386+
##############################################################################
3387+
# Command: update-vpc-source-nat-ip
3388+
# Updates VPC source NAT egress to a new public IP without restarting tiers.
3389+
# Reconciles public veth/IP state, default route, VPC SNAT iptables chain,
3390+
# and source NAT markers under ${STATE_DIR}/vpc-<vpcId>/ips/.
3391+
##############################################################################
3392+
3393+
cmd_update_vpc_source_nat_ip() {
3394+
parse_vpc_args "$@"
3395+
acquire_lock "vpc-${VPC_ID}"
3396+
3397+
[ -z "${PUBLIC_IP}" ] && die "update-vpc-source-nat-ip: missing --public-ip"
3398+
3399+
local vsd="${STATE_DIR}/vpc-${VPC_ID}"
3400+
mkdir -p "${vsd}/ips"
3401+
3402+
# Load persisted values when omitted by the caller.
3403+
if [ -z "${VPC_CIDR}" ] && [ -f "${vsd}/cidr" ]; then
3404+
VPC_CIDR=$(cat "${vsd}/cidr" 2>/dev/null || true)
3405+
fi
3406+
if [ -z "${PUBLIC_VLAN}" ] && [ -f "${vsd}/ips/${PUBLIC_IP}.pvlan" ]; then
3407+
PUBLIC_VLAN=$(cat "${vsd}/ips/${PUBLIC_IP}.pvlan" 2>/dev/null || true)
3408+
fi
3409+
3410+
[ -z "${VPC_CIDR}" ] && die "update-vpc-source-nat-ip: missing --cidr (or persisted vpc cidr)"
3411+
[ -z "${PUBLIC_VLAN}" ] && die "update-vpc-source-nat-ip: missing --public-vlan"
3412+
3413+
log "update-vpc-source-nat-ip: vpc=${VPC_ID} ns=${NAMESPACE} old=? new=${PUBLIC_IP} pvlan=${PUBLIC_VLAN} cidr=${VPC_CIDR}"
3414+
3415+
local old_source_nat_ip=""
3416+
local old_public_vlan=""
3417+
local f ip flag
3418+
for f in "${vsd}/ips/"*; do
3419+
[ -f "${f}" ] || continue
3420+
ip=$(basename "${f}")
3421+
case "${ip}" in
3422+
*.pvlan|*.tier) continue ;;
3423+
esac
3424+
flag=$(cat "${f}" 2>/dev/null || true)
3425+
if [ "${flag}" = "true" ]; then
3426+
old_source_nat_ip="${ip}"
3427+
break
3428+
fi
3429+
done
3430+
3431+
if [ -n "${old_source_nat_ip}" ] && [ -f "${vsd}/ips/${old_source_nat_ip}.pvlan" ]; then
3432+
old_public_vlan=$(cat "${vsd}/ips/${old_source_nat_ip}.pvlan" 2>/dev/null || true)
3433+
fi
3434+
3435+
local new_pveth_h new_pveth_n pub_br
3436+
new_pveth_h=$(pub_veth_host_name "${PUBLIC_VLAN}" "${VPC_ID}")
3437+
new_pveth_n=$(pub_veth_ns_name "${PUBLIC_VLAN}" "${VPC_ID}")
3438+
ensure_host_bridge "${PUB_ETH}" "${PUBLIC_VLAN}"
3439+
pub_br=$(host_bridge_name "${PUB_ETH}" "${PUBLIC_VLAN}")
3440+
3441+
if ! ip link show "${new_pveth_h}" >/dev/null 2>&1; then
3442+
ip link add "${new_pveth_h}" type veth peer name "${new_pveth_n}"
3443+
ip link set "${new_pveth_n}" netns "${NAMESPACE}"
3444+
ip link set "${new_pveth_h}" master "${pub_br}"
3445+
ip link set "${new_pveth_h}" up
3446+
ip netns exec "${NAMESPACE}" ip link set "${new_pveth_n}" up
3447+
log "update-vpc-source-nat-ip: created public veth ${new_pveth_h} <-> ${new_pveth_n}"
3448+
else
3449+
ip link set "${new_pveth_h}" up 2>/dev/null || true
3450+
ip netns exec "${NAMESPACE}" ip link set "${new_pveth_n}" up 2>/dev/null || true
3451+
fi
3452+
3453+
ensure_public_ip_on_namespace "${PUBLIC_IP}" "${PUBLIC_CIDR}" "${new_pveth_n}" "${new_pveth_h}"
3454+
ip route replace "${PUBLIC_IP}/32" dev "${new_pveth_h}" 2>/dev/null || true
3455+
3456+
if [ -n "${old_source_nat_ip}" ] && [ "${old_source_nat_ip}" != "${PUBLIC_IP}" ] && [ -n "${old_public_vlan}" ]; then
3457+
local old_pveth_n
3458+
old_pveth_n=$(pub_veth_ns_name "${old_public_vlan}" "${VPC_ID}")
3459+
if [ "${old_pveth_n}" != "${new_pveth_n}" ]; then
3460+
ip netns exec "${NAMESPACE}" ip route show default 2>/dev/null | \
3461+
grep " dev ${old_pveth_n}\b" | \
3462+
while read -r route; do
3463+
ip netns exec "${NAMESPACE}" ip route del ${route} 2>/dev/null || true
3464+
done
3465+
fi
3466+
fi
3467+
3468+
if [ -n "${PUBLIC_GATEWAY}" ]; then
3469+
ip netns exec "${NAMESPACE}" ip route replace default \
3470+
via "${PUBLIC_GATEWAY}" dev "${new_pveth_n}" 2>/dev/null || \
3471+
ip netns exec "${NAMESPACE}" ip route add default \
3472+
via "${PUBLIC_GATEWAY}" dev "${new_pveth_n}" 2>/dev/null || true
3473+
log "update-vpc-source-nat-ip: default route via ${PUBLIC_GATEWAY} dev ${new_pveth_n}"
3474+
fi
3475+
3476+
local vpc_post_chain="${CHAIN_PREFIX}_${VPC_ID}_VPC_POST"
3477+
ensure_chain nat "${vpc_post_chain}"
3478+
ensure_jump nat POSTROUTING "${vpc_post_chain}"
3479+
3480+
# This chain is dedicated to VPC source NAT egress; rebuild to a single rule.
3481+
ip netns exec "${NAMESPACE}" iptables -t nat -F "${vpc_post_chain}"
3482+
ip netns exec "${NAMESPACE}" iptables -t nat \
3483+
-A "${vpc_post_chain}" -s "${VPC_CIDR}" -o "${new_pveth_n}" -j SNAT --to-source "${PUBLIC_IP}"
3484+
3485+
# Keep exactly one source-NAT marker: new public IP=true, all others=false.
3486+
for f in "${vsd}/ips/"*; do
3487+
[ -f "${f}" ] || continue
3488+
ip=$(basename "${f}")
3489+
case "${ip}" in
3490+
*.pvlan|*.tier) continue ;;
3491+
esac
3492+
echo "false" > "${f}"
3493+
done
3494+
echo "true" > "${vsd}/ips/${PUBLIC_IP}"
3495+
echo "${PUBLIC_VLAN}" > "${vsd}/ips/${PUBLIC_IP}.pvlan"
3496+
3497+
local _arping_bin
3498+
_arping_bin=$(_find_arping) || true
3499+
if [ -n "${_arping_bin}" ]; then
3500+
ip netns exec "${NAMESPACE}" "${_arping_bin}" -c 3 -U -I "${new_pveth_n}" "${PUBLIC_IP}" \
3501+
>/dev/null 2>&1 || true
3502+
fi
3503+
3504+
_dump_iptables "${NAMESPACE}"
3505+
release_lock
3506+
log "update-vpc-source-nat-ip: done vpc=${VPC_ID} old=${old_source_nat_ip:-none} new=${PUBLIC_IP}"
3507+
}
3508+
33863509
##############################################################################
33873510
# Command: shutdown-vpc
33883511
# Removes the VPC namespace after all tiers have been shut down.
@@ -3590,6 +3713,7 @@ case "${COMMAND}" in
35903713
destroy-network) cmd_destroy_network "$@" ;;
35913714
# VPC lifecycle
35923715
implement-vpc) cmd_implement_vpc "$@" ;;
3716+
update-vpc-source-nat-ip) cmd_update_vpc_source_nat_ip "$@" ;;
35933717
shutdown-vpc) cmd_shutdown_vpc "$@" ;;
35943718
destroy-vpc) cmd_destroy_vpc "$@" ;;
35953719
assign-ip) cmd_assign_ip "$@" ;;
@@ -3624,7 +3748,7 @@ case "${COMMAND}" in
36243748
custom-action) cmd_custom_action "$@" ;;
36253749
"")
36263750
echo "Usage: $0 {implement-network|shutdown-network|destroy-network|" \
3627-
"implement-vpc|shutdown-vpc|destroy-vpc|" \
3751+
"implement-vpc|update-vpc-source-nat-ip|shutdown-vpc|destroy-vpc|" \
36283752
"assign-ip|release-ip|" \
36293753
"add-static-nat|delete-static-nat|add-port-forward|delete-port-forward|" \
36303754
"config-dhcp-subnet|remove-dhcp-subnet|add-dhcp-entry|remove-dhcp-entry|set-dhcp-options|" \

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2488,37 +2488,56 @@ public boolean shutdownVpc(Vpc vpc, ReservationContext context)
24882488
return result;
24892489
}
24902490

2491-
/** Private gateways are not supported by the network extension element. */
24922491
@Override
24932492
public boolean createPrivateGateway(PrivateGateway gateway)
24942493
throws ConcurrentOperationException, ResourceUnavailableException {
2495-
return true;
2494+
throw new UnsupportedOperationException("Private gateways are not supported by the network extension element.");
24962495
}
24972496

24982497
/** Private gateways are not supported by the network extension element. */
24992498
@Override
25002499
public boolean deletePrivateGateway(PrivateGateway gateway)
25012500
throws ConcurrentOperationException, ResourceUnavailableException {
2502-
return true;
2501+
throw new UnsupportedOperationException("Private gateways are not supported by the network extension element.");
25032502
}
25042503

25052504
/** Static routes are not supported by the network extension element. */
25062505
@Override
25072506
public boolean applyStaticRoutes(Vpc vpc, List<StaticRouteProfile> routes)
25082507
throws ResourceUnavailableException {
2509-
return true;
2508+
throw new UnsupportedOperationException("Static routes are not supported by the network extension element.");
25102509
}
25112510

25122511
/** ACL items on private gateways are not supported by the network extension element. */
25132512
@Override
25142513
public boolean applyACLItemsToPrivateGw(PrivateGateway gateway, List<? extends NetworkACLItem> rules)
25152514
throws ResourceUnavailableException {
2516-
return true;
2515+
throw new UnsupportedOperationException("ACL items on private gateways are not supported by the network extension element.");
25172516
}
25182517

25192518
@Override
25202519
public boolean updateVpcSourceNatIp(Vpc vpc, IpAddress address) {
2521-
return true;
2520+
if (vpc == null || address == null || address.getAddress() == null) {
2521+
logger.warn("updateVpcSourceNatIp: invalid input (vpc={}, address={})", vpc, address);
2522+
return false;
2523+
}
2524+
2525+
final List<String> args = new ArrayList<>();
2526+
final VlanVO vlan = vlanDao.findById(address.getVlanId());
2527+
args.add("--vpc-id"); args.add(String.valueOf(vpc.getId()));
2528+
args.add("--cidr"); args.add(safeStr(vpc.getCidr()));
2529+
args.add("--public-ip"); args.add(safeStr(address.getAddress().addr()));
2530+
args.add("--public-vlan"); args.add(safeStr(getPublicVlanTag(address.getId())));
2531+
args.add("--public-gateway"); args.add(vlan != null ? safeStr(vlan.getVlanGateway()) : "");
2532+
args.add("--public-cidr"); args.add(safeStr(getPublicCidr(address.getId())));
2533+
args.add("--source-nat"); args.add("true");
2534+
2535+
final boolean result = executeVpcScript(vpc, "update-vpc-source-nat-ip", args.toArray(new String[0]));
2536+
if (!result) {
2537+
logger.warn("updateVpcSourceNatIp: failed to update source NAT IP for VPC {} to {}",
2538+
vpc.getId(), address.getAddress().addr());
2539+
}
2540+
return result;
25222541
}
25232542

25242543
/**

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,9 +1683,12 @@ private boolean checkAndUpdateRouterSourceNatIp(Vpc vpc, String sourceNatIp) {
16831683
if (! userIps.isEmpty()) {
16841684
try {
16851685
_ipAddrMgr.updateSourceNatIpAddress(requestedIp, userIps);
1686-
if (isVpcForProvider(Provider.Nsx, vpc) || isVpcForProvider(Provider.Netris, vpc)) {
1686+
if (isVpcForProvider(Provider.Nsx, vpc) || isVpcForProvider(Provider.Netris, vpc)
1687+
|| isVpcForProvider(Provider.NetworkExtension, vpc)) {
16871688
boolean isForNsx = _vpcOffSvcMapDao.isProviderForVpcOffering(Provider.Nsx, vpc.getVpcOfferingId());
1688-
String providerName = isForNsx ? Provider.Nsx.getName() : Provider.Netris.getName();
1689+
boolean isForNetris = _vpcOffSvcMapDao.isProviderForVpcOffering(Provider.Netris, vpc.getVpcOfferingId());
1690+
String providerName = isForNsx ? Provider.Nsx.getName()
1691+
: (isForNetris ? Provider.Netris.getName() : Provider.NetworkExtension.getName());
16891692
VpcProvider providerElement = (VpcProvider) _ntwkModel.getElementImplementingProvider(providerName);
16901693
if (Objects.nonNull(providerElement)) {
16911694
providerElement.updateVpcSourceNatIp(vpc, requestedIp);

0 commit comments

Comments
 (0)