diff --git a/neutron/cmd/sanity/checks.py b/neutron/cmd/sanity/checks.py index 22570857e64..5d90ad9c306 100644 --- a/neutron/cmd/sanity/checks.py +++ b/neutron/cmd/sanity/checks.py @@ -14,16 +14,24 @@ # under the License. import re +import shutil +import tempfile import netaddr +from oslo_config import cfg from oslo_log import log as logging from oslo_utils import uuidutils import six from neutron.agent.common import ovs_lib +from neutron.agent.l3 import ha_router +from neutron.agent.l3 import namespaces +from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib from neutron.agent.linux import ip_link_support +from neutron.agent.linux import keepalived from neutron.agent.linux import utils as agent_utils +from neutron.common import constants as n_consts from neutron.common import utils from neutron.i18n import _LE from neutron.plugins.common import constants as const @@ -166,6 +174,124 @@ def dnsmasq_version_supported(): return True +class KeepalivedIPv6Test(object): + def __init__(self, ha_port, gw_port, gw_vip, default_gw): + self.ha_port = ha_port + self.gw_port = gw_port + self.gw_vip = gw_vip + self.default_gw = default_gw + self.manager = None + self.config = None + self.config_path = None + self.nsname = "keepalivedtest-" + uuidutils.generate_uuid() + self.pm = external_process.ProcessMonitor(cfg.CONF, 'router') + self.orig_interval = cfg.CONF.AGENT.check_child_processes_interval + + def configure(self): + config = keepalived.KeepalivedConf() + instance1 = keepalived.KeepalivedInstance('MASTER', self.ha_port, 1, + ['169.254.192.0/18'], + advert_int=5) + instance1.track_interfaces.append(self.ha_port) + + # Configure keepalived with an IPv6 address (gw_vip) on gw_port. + vip_addr1 = keepalived.KeepalivedVipAddress(self.gw_vip, self.gw_port) + instance1.vips.append(vip_addr1) + + # Configure keepalived with an IPv6 default route on gw_port. + gateway_route = keepalived.KeepalivedVirtualRoute(n_consts.IPv6_ANY, + self.default_gw, + self.gw_port) + instance1.virtual_routes.gateway_routes = [gateway_route] + config.add_instance(instance1) + self.config = config + + def start_keepalived_process(self): + # Disable process monitoring for Keepalived process. + cfg.CONF.set_override('check_child_processes_interval', 0, 'AGENT') + + # Create a temp directory to store keepalived configuration. + self.config_path = tempfile.mkdtemp() + + # Instantiate keepalived manager with the IPv6 configuration. + self.manager = keepalived.KeepalivedManager('router1', self.config, + namespace=self.nsname, process_monitor=self.pm, + conf_path=self.config_path) + self.manager.spawn() + + def verify_ipv6_address_assignment(self, gw_dev): + process = self.manager.get_process() + agent_utils.wait_until_true(lambda: process.active) + + def _gw_vip_assigned(): + iface_ip = gw_dev.addr.list(ip_version=6, scope='global') + if iface_ip: + return self.gw_vip == iface_ip[0]['cidr'] + + agent_utils.wait_until_true(_gw_vip_assigned) + + def __enter__(self): + ip_lib.IPWrapper().netns.add(self.nsname) + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.pm.stop() + if self.manager: + self.manager.disable() + if self.config_path: + shutil.rmtree(self.config_path, ignore_errors=True) + ip_lib.IPWrapper().netns.delete(self.nsname) + cfg.CONF.set_override('check_child_processes_interval', + self.orig_interval, 'AGENT') + + +def keepalived_ipv6_supported(): + """Check if keepalived supports IPv6 functionality. + + Validation is done as follows. + 1. Create a namespace. + 2. Create OVS bridge with two ports (ha_port and gw_port) + 3. Move the ovs ports to the namespace. + 4. Spawn keepalived process inside the namespace with IPv6 configuration. + 5. Verify if IPv6 address is assigned to gw_port. + 6. Verify if IPv6 default route is configured by keepalived. + """ + + random_str = utils.get_random_string(6) + br_name = "ka-test-" + random_str + ha_port = ha_router.HA_DEV_PREFIX + random_str + gw_port = namespaces.INTERNAL_DEV_PREFIX + random_str + gw_vip = 'fdf8:f53b:82e4::10/64' + expected_default_gw = 'fe80:f816::1' + + with ovs_lib.OVSBridge(br_name) as br: + with KeepalivedIPv6Test(ha_port, gw_port, gw_vip, + expected_default_gw) as ka: + br.add_port(ha_port, ('type', 'internal')) + br.add_port(gw_port, ('type', 'internal')) + + ha_dev = ip_lib.IPDevice(ha_port) + gw_dev = ip_lib.IPDevice(gw_port) + + ha_dev.link.set_netns(ka.nsname) + gw_dev.link.set_netns(ka.nsname) + + ha_dev.link.set_up() + gw_dev.link.set_up() + + ka.configure() + + ka.start_keepalived_process() + + ka.verify_ipv6_address_assignment(gw_dev) + + default_gw = gw_dev.route.get_gateway(ip_version=6) + if default_gw: + default_gw = default_gw['gateway'] + + return expected_default_gw == default_gw + + def ovsdb_native_supported(): # Running the test should ensure we are configured for OVSDB native try: diff --git a/neutron/cmd/sanity_check.py b/neutron/cmd/sanity_check.py index 9d5bae36df4..90895e2340f 100644 --- a/neutron/cmd/sanity_check.py +++ b/neutron/cmd/sanity_check.py @@ -21,6 +21,7 @@ from oslo_log import log as logging from neutron.agent import dhcp_agent from neutron.cmd.sanity import checks from neutron.common import config +from neutron.db import l3_hamode_db from neutron.i18n import _LE, _LW @@ -35,6 +36,7 @@ cfg.CONF.import_group('ml2', 'neutron.plugins.ml2.config') cfg.CONF.import_group('ml2_sriov', 'neutron.plugins.ml2.drivers.mech_sriov.mech_driver') dhcp_agent.register_options() +cfg.CONF.register_opts(l3_hamode_db.L3_HA_OPTS) class BoolOptCallback(cfg.BoolOpt): @@ -105,6 +107,15 @@ def check_dnsmasq_version(): return result +def check_keepalived_ipv6_support(): + result = checks.keepalived_ipv6_supported() + if not result: + LOG.error(_LE('The installed version of keepalived does not support ' + 'IPv6. Please update to at least version 1.2.10 for ' + 'IPv6 support.')) + return result + + def check_nova_notify(): result = checks.nova_notify_supported() if not result: @@ -181,6 +192,8 @@ OPTS = [ help=_('Check ovsdb native interface support')), BoolOptCallback('ebtables_installed', check_ebtables, help=_('Check ebtables installation')), + BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support, + help=_('Check keepalived IPv6 support')), ] @@ -214,6 +227,8 @@ def enable_tests_from_config(): cfg.CONF.set_override('dnsmasq_version', True) if cfg.CONF.OVS.ovsdb_interface == 'native': cfg.CONF.set_override('ovsdb_native', True) + if cfg.CONF.l3_ha: + cfg.CONF.set_override('keepalived_ipv6_support', True) def all_tests_passed(): diff --git a/neutron/tests/functional/sanity/test_sanity.py b/neutron/tests/functional/sanity/test_sanity.py index 55b0633f4fb..b65de687a5b 100644 --- a/neutron/tests/functional/sanity/test_sanity.py +++ b/neutron/tests/functional/sanity/test_sanity.py @@ -67,3 +67,6 @@ class SanityTestCaseRoot(functional_base.BaseSudoTestCase): def test_ovsdb_native_supported_runs(self): checks.ovsdb_native_supported() + + def test_keepalived_ipv6_support(self): + checks.keepalived_ipv6_supported()