Support VPNaaS with L3 HA

VPNaaS with a HA router is not supported now.
This patch will enable support for VPNaaS with an HA router.

When VPN service is created for HA router, it should run
only on master node.
On transition from master to backup, vpn service should be
shutdown (same like disabling radvd) on that agent.

On transition from backup to master, vpn service should be
enabled and running on that agent.

Closes-bug: #1478012
Change-Id: I22f55b72cdc6cf608f50db902e4e3636fd59a16c
This commit is contained in:
venkata anil 2015-07-24 12:19:52 +00:00
parent 819c7b8bec
commit 33c3fd0302
7 changed files with 330 additions and 65 deletions

View File

@ -38,6 +38,24 @@ class VPNAgent(l3_agent.L3NATAgentWithStateReport):
self.service = vpn_service.VPNService(self)
self.device_drivers = self.service.load_device_drivers(host)
def process_state_change(self, router_id, state):
"""Enable the vpn process when router transitioned to master.
And disable vpn process for backup router.
"""
for device_driver in self.device_drivers:
if router_id in device_driver.processes:
process = device_driver.processes[router_id]
if state == 'master':
process.enable()
else:
process.disable()
def enqueue_state_change(self, router_id, state):
"""Handle HA router state changes for vpn process"""
self.process_state_change(router_id, state)
super(VPNAgent, self).enqueue_state_change(router_id, state)
def main():
entry.main(manager='neutron_vpnaas.services.vpn.agent.VPNAgent')

View File

@ -144,7 +144,7 @@ class BaseSwanProcess(object):
self.namespace = namespace
self.connection_status = {}
self.config_dir = os.path.join(
cfg.CONF.ipsec.config_base_dir, self.id)
self.conf.ipsec.config_base_dir, self.id)
self.etc_dir = os.path.join(self.config_dir, 'etc')
self.log_dir = os.path.join(self.config_dir, 'logs')
self.update_vpnservice(vpnservice)
@ -204,7 +204,7 @@ class BaseSwanProcess(object):
template = _get_template(template_file)
return template.render(
{'vpnservice': vpnservice,
'state_path': cfg.CONF.state_path})
'state_path': self.conf.state_path})
@abc.abstractmethod
def get_status(self):
@ -703,6 +703,9 @@ class IPsecDriver(device_drivers.DeviceDriver):
# before router's namespace
process = self.processes[process_id]
self._update_nat(process.vpnservice, self.add_nat_rule)
# Don't run ipsec process for backup HA router
if router.router['ha'] and router.ha_state == 'backup':
return
process.enable()
def destroy_process(self, process_id):
@ -833,7 +836,15 @@ class IPsecDriver(device_drivers.DeviceDriver):
process = self.ensure_process(vpnservice['router_id'],
vpnservice=vpnservice)
self._update_nat(vpnservice, self.add_nat_rule)
process.update()
router = self.routers.get(vpnservice['router_id'])
if not router:
continue
# For HA router, spawn vpn process on master router
# and terminate vpn process on backup router
if router.router['ha'] and router.ha_state == 'backup':
process.disable()
else:
process.update()
def _delete_vpn_processes(self, sync_router_ids, vpn_router_ids):
# Delete any IPSec processes that are

View File

@ -151,6 +151,9 @@ class StrongSwanProcess(ipsec.BaseSwanProcess):
if not self.namespace:
return
self._execute([self.binary, 'start'])
# initiate ipsec connection
for ipsec_site_conn in self.vpnservice['ipsec_site_connections']:
self._execute([self.binary, 'up', ipsec_site_conn['id']])
def stop(self):
self._execute([self.binary, 'stop'])

View File

@ -11,19 +11,26 @@
# under the License.
import copy
import os
import fixtures
import functools
import mock
import netaddr
import os
from neutron.agent.common import config as agent_config
from neutron.agent.common import ovs_lib
from neutron.agent.l3 import namespaces
from neutron.agent.l3 import router_info
from neutron.agent import l3_agent as l3_agent_main
from neutron.agent.linux import external_process
from neutron.agent.linux import interface
from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils as linux_utils
from neutron.common import config as common_config
from neutron.common import constants as l3_constants
from neutron.common import utils as common_utils
from neutron.plugins.common import constants
from neutron.services.provider_configuration import serviceprovider_opts
from neutron.tests.common import l3_test_common
from neutron.tests.common import net_helpers
from neutron.tests.functional import base
from oslo_config import cfg
@ -34,6 +41,7 @@ from neutron_vpnaas.services.vpn import agent as vpn_agent
from neutron_vpnaas.services.vpn.agent import vpn_agent_opts
from neutron_vpnaas.services.vpn.device_drivers import ipsec
_uuid = uuidutils.generate_uuid
FAKE_IKE_POLICY = {
'auth_algorithm': 'sha1',
@ -143,20 +151,98 @@ FAKE_ROUTER = {
}
class RouterFixture(fixtures.Fixture):
def get_ovs_bridge(br_name):
return ovs_lib.OVSBridge(br_name)
def __init__(self, l3_agent, public_ip, private_cidr):
self.l3_agent = l3_agent
self.router_info = self._generate_info(public_ip, private_cidr)
self.router_id = self.router_info['id']
class TestIPSecScenario(base.BaseSudoTestCase):
vpn_agent_ini = os.environ.get('VPN_AGENT_INI',
'/etc/neutron/vpn_agent.ini')
NESTED_NAMESPACE_SEPARATOR = '@'
def setUp(self):
super(RouterFixture, self).setUp()
self.l3_agent._process_added_router(self.router_info)
self.router = self.l3_agent.router_info[self.router_id]
self.addCleanup(self.l3_agent._router_removed, self.router_id)
super(TestIPSecScenario, self).setUp()
mock.patch('neutron.agent.l3.agent.L3PluginApi').start()
# avoid report_status running periodically
mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall').start()
# Both the vpn agents try to use execute_rootwrap_daemon's socket
# simultaneously during test cleanup, but execute_rootwrap_daemon has
# limitations with simultaneous reads. So avoid using
# root_helper_daemon and instead use root_helper
# https://bugs.launchpad.net/neutron/+bug/1482622
cfg.CONF.set_override('root_helper_daemon', None, group='AGENT')
self.vpn_agent = self._configure_agent('agent1')
def _generate_info(self, public_ip, private_cidr):
def connect_agents(self, agent1, agent2):
"""Simulate both agents in the same host.
For packet flow between resources connected to these two agents,
agent's ovs bridges are connected through patch ports.
"""
br_int_1 = get_ovs_bridge(agent1.conf.ovs_integration_bridge)
br_int_2 = get_ovs_bridge(agent2.conf.ovs_integration_bridge)
net_helpers.create_patch_ports(br_int_1, br_int_2)
br_ex_1 = get_ovs_bridge(agent1.conf.external_network_bridge)
br_ex_2 = get_ovs_bridge(agent2.conf.external_network_bridge)
net_helpers.create_patch_ports(br_ex_1, br_ex_2)
def _get_config_opts(self):
"""Register default config options"""
config = cfg.ConfigOpts()
config.register_opts(common_config.core_opts)
config.register_opts(common_config.core_cli_opts)
config.register_opts(serviceprovider_opts, 'service_providers')
config.register_opts(vpn_agent_opts, 'vpnagent')
config.register_opts(ipsec.ipsec_opts, 'ipsec')
config.register_opts(ipsec.openswan_opts, 'openswan')
logging.register_options(config)
agent_config.register_process_monitor_opts(config)
return config
def _configure_agent(self, host):
"""Override specific config options"""
config = self._get_config_opts()
l3_agent_main.register_opts(config)
cfg.CONF.set_override('debug', True)
agent_config.setup_logging()
config.set_override(
'interface_driver',
'neutron.agent.linux.interface.OVSInterfaceDriver')
config.set_override('router_delete_namespaces', True)
br_int = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
br_ex = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
config.set_override('ovs_integration_bridge', br_int.br_name)
config.set_override('external_network_bridge', br_ex.br_name)
temp_dir = self.get_new_temp_dir()
get_temp_file_path = functools.partial(self.get_temp_file_path,
root=temp_dir)
config.set_override('state_path', temp_dir.path)
config.set_override('metadata_proxy_socket',
get_temp_file_path('metadata_proxy'))
config.set_override('ha_confs_path',
get_temp_file_path('ha_confs'))
config.set_override('external_pids',
get_temp_file_path('external/pids'))
config.set_override('host', host)
ipsec_config_base_dir = '%s/%s' % (temp_dir.path, 'ipsec')
config.set_override('config_base_dir',
ipsec_config_base_dir, group='ipsec')
config(['--config-file', self.vpn_agent_ini])
# Assign ip address to br-ex port because it is a gateway
ex_port = ip_lib.IPDevice(br_ex.br_name)
ex_port.addr.add(str(PUBLIC_NET[1]))
return vpn_agent.VPNAgent(host, config)
def _generate_info(self, public_ip, private_cidr, enable_ha=False):
"""Generate router info"""
info = copy.deepcopy(FAKE_ROUTER)
info['id'] = _uuid()
info['_interfaces'][0]['id'] = _uuid()
@ -170,48 +256,49 @@ class RouterFixture(fixtures.Fixture):
info['gw_port']['id'] = _uuid()
info['gw_port']['fixed_ips'][0]['ip_address'] = str(public_ip)
info['gw_port']['mac_address'] = common_utils.get_random_mac(MAC_BASE)
if enable_ha:
info['ha'] = True
info['ha_vr_id'] = 1
info[l3_constants.HA_INTERFACE_KEY] = (
l3_test_common.get_ha_interface())
else:
info['ha'] = False
return info
def manage_router(self, agent, router):
"""Create router from router_info"""
self.addCleanup(agent._safe_router_removed, router['id'])
class TestIPSecScenario(base.BaseSudoTestCase):
# Generate unique internal and external router device names using the
# agent's hostname. This is to allow multiple HA router replicas to
# co-exist on the same machine, otherwise they'd all use the same
# device names and OVS would freak out(OVS won't allow a port with
# same name connected to two bridges).
def _append_suffix(dev_name):
# If dev_name = 'xyz123' and the suffix is 'agent2' then the result
# will be 'xy-nt2'
return "{0}-{1}".format(dev_name[:-4], agent.host[-3:])
vpn_agent_ini = os.environ.get('VPN_AGENT_INI',
'/etc/neutron/vpn_agent.ini')
def get_internal_device_name(port_id):
return _append_suffix(
(namespaces.INTERNAL_DEV_PREFIX + port_id)
[:interface.LinuxInterfaceDriver.DEV_NAME_LEN])
def setUp(self):
super(TestIPSecScenario, self).setUp()
def get_external_device_name(port_id):
return _append_suffix(
(namespaces.EXTERNAL_DEV_PREFIX + port_id)
[:interface.LinuxInterfaceDriver.DEV_NAME_LEN])
mock.patch('neutron.agent.l3.agent.L3PluginApi').start()
mock_get_internal_device_name = mock.patch.object(
router_info.RouterInfo, 'get_internal_device_name').start()
mock_get_internal_device_name.side_effect = get_internal_device_name
mock_get_external_device_name = mock.patch.object(
router_info.RouterInfo, 'get_external_device_name').start()
mock_get_external_device_name.side_effect = get_external_device_name
cfg.CONF.set_override('debug', True)
agent_config.setup_logging()
config = cfg.ConfigOpts()
config.register_opts(common_config.core_opts)
config.register_opts(common_config.core_cli_opts)
logging.register_options(config)
agent_config.register_process_monitor_opts(config)
l3_agent_main.register_opts(config)
config.set_override(
'interface_driver',
'neutron.agent.linux.interface.OVSInterfaceDriver')
config.set_override('router_delete_namespaces', True)
config.register_opts(serviceprovider_opts, 'service_providers')
config.register_opts(vpn_agent_opts, 'vpnagent')
config.register_opts(ipsec.ipsec_opts, 'ipsec')
config.register_opts(ipsec.openswan_opts, 'openswan')
config.set_override('state_path', self.get_new_temp_dir().path)
agent._process_added_router(router)
self.br_int = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
self.br_ex = self.useFixture(net_helpers.OVSBridgeFixture()).bridge
config.set_override('ovs_integration_bridge', self.br_int.br_name)
config.set_override('external_network_bridge', self.br_ex.br_name)
config(['--config-file', self.vpn_agent_ini])
self.vpn_agent = vpn_agent.VPNAgent('agent1', config)
# Assign ip address to br-ex port because it is a gateway
ex_port = ip_lib.IPDevice(self.br_ex.br_name)
ex_port.addr.add(str(PUBLIC_NET[1]))
return agent.router_info[router['id']]
def prepare_vpn_service_info(self, router_id, external_ip, subnet_cidr):
service = copy.deepcopy(FAKE_VPN_SERVICE)
@ -234,17 +321,24 @@ class TestIPSecScenario(base.BaseSudoTestCase):
})
vpn_service['ipsec_site_connections'] = [ipsec_conn]
def port_setup(self, router):
def port_setup(self, router, bridge=None, offset=1, namespace=None):
"""Creates namespace and a port inside it on a client site."""
client_ns = self.useFixture(net_helpers.NamespaceFixture()).ip_wrapper
if not namespace:
client_ns = self.useFixture(
net_helpers.NamespaceFixture()).ip_wrapper
namespace = client_ns.namespace
router_ip_cidr = self._port_first_ip_cidr(router.internal_ports[0])
port_ip_cidr = net_helpers.increment_ip_cidr(router_ip_cidr)
port_ip_cidr = net_helpers.increment_ip_cidr(router_ip_cidr, offset)
if not bridge:
bridge = get_ovs_bridge(self.vpn_agent.conf.ovs_integration_bridge)
port = self.useFixture(
net_helpers.OVSPortFixture(self.br_int, client_ns.namespace)).port
net_helpers.OVSPortFixture(bridge, namespace)).port
port.addr.add(port_ip_cidr)
port.route.add_gateway(router_ip_cidr.partition('/')[0])
return client_ns.namespace, port_ip_cidr.partition('/')[0]
return namespace, port_ip_cidr.partition('/')[0]
def _port_first_ip_cidr(self, port):
fixed_ip = port['fixed_ips'][0]
@ -252,9 +346,8 @@ class TestIPSecScenario(base.BaseSudoTestCase):
fixed_ip['prefixlen'])
def site_setup(self, router_public_ip, private_net_cidr):
router = self.useFixture(
RouterFixture(self.vpn_agent, router_public_ip,
private_net_cidr)).router
router_info = self._generate_info(router_public_ip, private_net_cidr)
router = self.manage_router(self.vpn_agent, router_info)
port_namespace, port_ip = self.port_setup(router)
vpn_service = self.prepare_vpn_service_info(
@ -262,6 +355,36 @@ class TestIPSecScenario(base.BaseSudoTestCase):
return {"router": router, "port_namespace": port_namespace,
"port_ip": port_ip, "vpn_service": vpn_service}
def setup_ha_routers(self, router_public_ip, private_net_cidr):
"""Setup HA master router on agent1 and backup router on agent2"""
router_info = self._generate_info(router_public_ip,
private_net_cidr, enable_ha=True)
get_ns_name = mock.patch.object(
namespaces.RouterNamespace, '_get_ns_name').start()
get_ns_name.return_value = "qrouter-{0}-{1}".format(
router_info['id'], self.vpn_agent.host)
router1 = self.manage_router(self.vpn_agent, router_info)
router_info_2 = copy.deepcopy(router_info)
router_info_2[l3_constants.HA_INTERFACE_KEY] = (
l3_test_common.get_ha_interface(ip='169.254.192.2',
mac='22:22:22:22:22:22'))
get_ns_name.return_value = "qrouter-{0}-{1}".format(
router_info['id'], self.failover_agent.host)
router2 = self.manage_router(self.failover_agent, router_info_2)
linux_utils.wait_until_true(lambda: router1.ha_state == 'master')
linux_utils.wait_until_true(lambda: router2.ha_state == 'backup')
port_namespace, port_ip = self.port_setup(router1)
vpn_service = self.prepare_vpn_service_info(
router1.router_id, router_public_ip, private_net_cidr)
return {"router1": router1, "router2": router2,
"port_namespace": port_namespace, "port_ip": port_ip,
"vpn_service": vpn_service}
def _ping(self, namespace, ip):
"""Pings ip address from network namespace.
@ -272,11 +395,29 @@ class TestIPSecScenario(base.BaseSudoTestCase):
count = 4
cmd = ['ping', '-w', 2 * count, '-c', count, ip]
cmd = ip_lib.add_namespace_to_cmd(cmd, namespace)
linux_utils.execute(cmd, run_as_root=True)
linux_utils.execute(cmd, check_exit_code=True,
extra_ok_codes=[0], run_as_root=True)
return True
except RuntimeError:
return False
def _fail_ha_router(self, router):
"""Down the HA router."""
device_name = router.get_ha_device_name()
ha_device = ip_lib.IPDevice(device_name, router.ns_name)
ha_device.link.set_down()
def _ipsec_process_exists(self, conf, router, pid_files):
"""Check if *Swan process has started up."""
for pid_file in pid_files:
pm = external_process.ProcessManager(
conf,
"ipsec",
router.ns_name, pid_file=pid_file)
if pm.active:
break
return pm.active
def test_ipsec_site_connections(self):
device = self.vpn_agent.device_drivers[0]
# Mock the method below because it causes Exception:
@ -317,3 +458,92 @@ class TestIPSecScenario(base.BaseSudoTestCase):
self.assertTrue(self._ping(site1['port_namespace'], site2['port_ip']))
self.assertTrue(self._ping(site2['port_namespace'], site1['port_ip']))
def test_ipsec_site_connections_with_l3ha_routers(self):
"""Test ipsec site connection with HA routers.
This test creates two agents. First agent will have Legacy and HA
routers. Second agent will host only HA router. We setup ipsec
connection between legacy and HA router.
When HA router is created, agent1 will have master router and
agent2 will have backup router. Ipsec connection will be established
between legacy router and agent1's master HA router.
Then we fail the agent1's master HA router. Agent1's HA router will
transition to backup and agent2's HA router will become master.
Now ipsec connection will be established between legacy router and
agent2's master HA router
"""
self.failover_agent = self._configure_agent('agent2')
self.connect_agents(self.vpn_agent, self.failover_agent)
vpn_agent_driver = self.vpn_agent.device_drivers[0]
failover_agent_driver = self.failover_agent.device_drivers[0]
ip_lib.send_ip_addr_adv_notif = mock.Mock()
# There are no vpn services yet. get_vpn_services_on_host returns
# empty list
vpn_agent_driver.agent_rpc.get_vpn_services_on_host = mock.Mock(
return_value=[])
failover_agent_driver.agent_rpc.get_vpn_services_on_host = mock.Mock(
return_value=[])
# instantiate network resources "router", "private network"
private_nets = list(PRIVATE_NET.subnet(24))
site1 = self.site_setup(PUBLIC_NET[4], private_nets[1])
site2 = self.setup_ha_routers(PUBLIC_NET[5], private_nets[2])
router = site1['router']
router1 = site2['router1']
router2 = site2['router2']
# build vpn resources
self.prepare_ipsec_conn_info(site1['vpn_service'],
site2['vpn_service'])
self.prepare_ipsec_conn_info(site2['vpn_service'],
site1['vpn_service'])
vpn_agent_driver.report_status = mock.Mock()
failover_agent_driver.report_status = mock.Mock()
vpn_agent_driver.agent_rpc.get_vpn_services_on_host = mock.Mock(
return_value=[site1['vpn_service'],
site2['vpn_service']])
failover_agent_driver.agent_rpc.get_vpn_services_on_host = mock.Mock(
return_value=[site2['vpn_service']])
# No ipsec connection between legacy router and HA routers
self.assertFalse(self._ping(site1['port_namespace'], site2['port_ip']))
self.assertFalse(self._ping(site2['port_namespace'], site1['port_ip']))
# sync the routers
vpn_agent_driver.sync(mock.Mock(), [{'id': router.router_id},
{'id': router1.router_id}])
failover_agent_driver.sync(mock.Mock(), [{'id': router1.router_id}])
self.addCleanup(
vpn_agent_driver._delete_vpn_processes,
[router.router_id, router1.router_id], [])
# Test ipsec connection between legacy router and agent2's HA router
self.assertTrue(self._ping(site1['port_namespace'], site2['port_ip']))
self.assertTrue(self._ping(site2['port_namespace'], site1['port_ip']))
# Fail the agent1's HA router. Agent1's HA router will transition
# to backup and agent2's HA router will become master.
self._fail_ha_router(router1)
linux_utils.wait_until_true(lambda: router2.ha_state == 'master')
linux_utils.wait_until_true(lambda: router1.ha_state == 'backup')
# wait until ipsec process running in failover agent's HA router
# check for both strongswan and openswan processes
path = failover_agent_driver.processes[router2.router_id].config_dir
pid_files = ['%s/var/run/charon.pid' % path,
'%s/var/run/pluto.pid' % path]
linux_utils.wait_until_true(
lambda: self._ipsec_process_exists(
self.failover_agent.conf, router2, pid_files))
# Test ipsec connection between legacy router and agent2's HA router
self.assertTrue(self._ping(site1['port_namespace'], site2['port_ip']))
self.assertTrue(self._ping(site2['port_namespace'], site1['port_ip']))

View File

@ -109,8 +109,10 @@ class TestStrongSwanDeviceDriver(base.BaseSudoTestCase):
'oslo_service.loopingcall.FixedIntervalLoopingCall')
looping_call_p.start()
vpn_service = mock.Mock()
vpn_service.conf = self.conf
self.driver = strongswan_ipsec.StrongSwanDriver(
vpn_service=mock.Mock(), host=mock.sentinel.host)
vpn_service, host=mock.sentinel.host)
self.driver.routers[FAKE_ROUTER_ID] = self.router
self.driver.agent_rpc = mock.Mock()
self.driver._update_nat = mock.Mock()

View File

@ -261,13 +261,14 @@ class BaseIPsecDeviceDriver(base.BaseTestCase):
mock.patch(klass).start()
self._execute = mock.patch.object(ipsec_process, '_execute').start()
self.agent = mock.Mock()
self.conf = cfg.CONF
self.agent.conf = self.conf
self.driver = driver(
self.agent,
FAKE_HOST)
self.conf = cfg.CONF
self.conf.use_namespaces = True
self.driver.agent_rpc = mock.Mock()
self.ri_kwargs = {'router': {'id': FAKE_ROUTER_ID},
self.ri_kwargs = {'router': {'id': FAKE_ROUTER_ID, 'ha': False},
'agent_conf': self.conf,
'interface_driver': mock.sentinel.interface_driver}
self.iptables = mock.Mock()
@ -309,7 +310,7 @@ class IPSecDeviceLegacy(BaseIPsecDeviceDriver):
self._test_vpnservice_updated([])
def test_vpnservice_updated_with_router_info(self):
router_info = {'id': FAKE_ROUTER_ID}
router_info = {'id': FAKE_ROUTER_ID, 'ha': False}
kwargs = {'router': router_info}
self._test_vpnservice_updated([router_info], **kwargs)
@ -825,7 +826,7 @@ class TestLibreSwanProcess(base.BaseTestCase):
'OpenSwanProcess.stop').start()
self.os_remove = mock.patch('os.remove').start()
self.ipsec_process = libreswan_ipsec.LibreSwanProcess(mock.ANY,
self.ipsec_process = libreswan_ipsec.LibreSwanProcess(cfg.CONF,
'foo-process-id',
self.vpnservice,
mock.ANY)

View File

@ -52,7 +52,7 @@ class VPNBaseTestCase(base.BaseTestCase):
super(VPNBaseTestCase, self).setUp()
self.conf = cfg.CONF
self.conf.use_namespaces = True
self.ri_kwargs = {'router': {'id': FAKE_ROUTER_ID},
self.ri_kwargs = {'router': {'id': FAKE_ROUTER_ID, 'ha': False},
'agent_conf': self.conf,
'interface_driver': mock.sentinel.interface_driver}