From d4384557c5e6047cb0b083b0ac04bb32b58a5c0d Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Mon, 5 Oct 2026 11:40:23 -0400 Subject: [PATCH 1/2] fix site-to-site vpn state on vpc routers with no running vms --- .../router/VirtualNetworkApplianceManagerImpl.java | 12 +++++++----- .../VirtualNetworkApplianceManagerImplTest.java | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index a166e894be1b..2518f2e44c7c 100644 --- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -1076,18 +1076,20 @@ public CheckRouterTask() { @Override protected void runInContext() { try { - final List routers = _routerDao.listIsolatedByHostId(null); - logger.debug("Found " + routers.size() + " routers to update status. "); - - updateSite2SiteVpnConnectionState(routers); - + // Site-to-site VPN connections only exist on VPC routers, so check every VPC router, + // including ones with no tier in use (no running VMs) + final List routers = new ArrayList<>(); List networks = new ArrayList<>(); for (Vpc vpc : _vpcDao.listAll()) { + routers.addAll(_routerDao.listByVpcId(vpc.getId())); List vpcNetworks = _networkDao.listByVpc(vpc.getId()); if (!vpcNetworks.isEmpty()) { networks.add(vpcNetworks.get(0)); } } + logger.debug("Found " + routers.size() + " routers to update status. "); + updateSite2SiteVpnConnectionState(routers); + logger.debug("Found " + networks.size() + " VPC's to update Redundant State. "); pushToUpdateQueue(networks); diff --git a/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java b/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java index 0365ae20175e..9cce5d71001b 100644 --- a/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/router/VirtualNetworkApplianceManagerImplTest.java @@ -338,6 +338,20 @@ public void testUpdateSite2SiteVpnConnectionState() throws Exception{ } } + @Test + public void testCheckRouterTaskChecksVpnOnVpcRouterWithoutTiers() { + VpcVO vpc = Mockito.mock(VpcVO.class); + when(vpc.getId()).thenReturn(10L); + DomainRouterVO router = Mockito.mock(DomainRouterVO.class); + when(_vpcDao.listAll()).thenReturn(List.of(vpc)); + when(_routerDao.listByVpcId(10L)).thenReturn(List.of(router)); + + virtualNetworkApplianceManagerImpl.new CheckRouterTask().runInContext(); + + Mockito.verify(_s2sVpnMgr).getConnectionsForRouter(router); + Mockito.verify(_routerDao, Mockito.never()).listIsolatedByHostId(null); + } + @Test public void checkLogrotateTimerPatternTestDoNotMatchWithRegex(){ String foo = "non-sense"; From 4349d820fc5ad14c0b9a03311205ac42d7205246 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Mon, 5 Oct 2026 14:50:34 -0400 Subject: [PATCH 2/2] start site-to-site vpn on the active side when the vpc has no vms --- systemvm/debian/opt/cloud/bin/configure.py | 13 ++++++ systemvm/test/TestCsSite2SiteVpn.py | 53 ++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 systemvm/test/TestCsSite2SiteVpn.py diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index 77b56779d5b8..f9fe90dd5630 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -1276,6 +1276,19 @@ def configure_ipsec(self, local_ip, obj): ipinsubnet = '.'.join(octets) CsHelper.execute("timeout 5 ping -c 3 %s" % ipinsubnet) + # The ping above only matches the tunnel when the router has an address in + # the local subnet. A VPC tier gets one only once a VM is deployed in it, so + # without VMs the ping leaves from the public IP and the tunnel never starts. + # The active side starts it explicitly instead. The passive side waits. + if not obj.get('passive', False): + self.start_connections(rightpeer, len(peerlistarr) if splitconnections else 1) + + def start_connections(self, rightpeer, count): + for peeridx in range(0, count): + conn = 'vpn-%s' % rightpeer if peeridx == 0 else 'vpn-%s-%d' % (rightpeer, peeridx + 1) + if len(CsHelper.execute('ipsec status %s | grep ESTABLISHED' % conn)) == 0: + CsHelper.execute('timeout 20 ipsec up %s' % conn) + def convert_sec_to_min(self, val): mins = int(val / 60) return "%sm" % mins diff --git a/systemvm/test/TestCsSite2SiteVpn.py b/systemvm/test/TestCsSite2SiteVpn.py new file mode 100644 index 000000000000..2e8bd012765f --- /dev/null +++ b/systemvm/test/TestCsSite2SiteVpn.py @@ -0,0 +1,53 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import unittest +import mock +import configure +from configure import CsSite2SiteVpn + + +class TestCsSite2SiteVpn(unittest.TestCase): + + def setUp(self): + self.vpn = CsSite2SiteVpn.__new__(CsSite2SiteVpn) + + @mock.patch('configure.CsHelper.execute') + def test_start_connections_brings_up_a_connection_that_is_not_established(self, mock_execute): + mock_execute.return_value = [] + self.vpn.start_connections('10.0.0.2', 1) + mock_execute.assert_any_call('timeout 20 ipsec up vpn-10.0.0.2') + + @mock.patch('configure.CsHelper.execute') + def test_start_connections_skips_a_connection_that_is_established(self, mock_execute): + mock_execute.return_value = ['vpn-10.0.0.2[1]: ESTABLISHED 5 seconds ago'] + self.vpn.start_connections('10.0.0.2', 1) + for call in mock_execute.call_args_list: + self.assertNotIn('ipsec up', call[0][0]) + + @mock.patch('configure.CsHelper.execute') + def test_start_connections_covers_every_split_connection(self, mock_execute): + mock_execute.return_value = [] + self.vpn.start_connections('10.0.0.2', 3) + ups = [call[0][0] for call in mock_execute.call_args_list if 'ipsec up' in call[0][0]] + self.assertEqual(ups, ['timeout 20 ipsec up vpn-10.0.0.2', + 'timeout 20 ipsec up vpn-10.0.0.2-2', + 'timeout 20 ipsec up vpn-10.0.0.2-3']) + + +if __name__ == '__main__': + unittest.main()