From e833d33db199b6e8ca9f1877b2fd7914f376b433 Mon Sep 17 00:00:00 2001 From: Kevin Benton Date: Sat, 2 May 2015 05:08:26 -0700 Subject: [PATCH] Add capability to wait for IPv6 address in ip_lib When an IPv6 address is added to an interface, it goes into a tentative state for a couple of seconds for duplicate address detection. During this time, use of the address will fail. This is an issue for functional tests where they may add an address to an interface and then immediately run a ping and expect success. This patch adds a new wait_until_address_ready function to ip_lib that will poll the interface every 200 ms until the status transitions off of tentative or until a time limit is exceeded. If the time limit is exceeded, it will raise an exception. It also adds unit tests and updates a functional test to make use of the new feature. Change-Id: I2fa51e3f55847f7b5062bec0c1c666f5c11364d5 --- neutron/agent/linux/ip_lib.py | 32 +++++++++++- .../tests/functional/agent/test_ovs_flows.py | 5 +- neutron/tests/unit/agent/linux/test_ip_lib.py | 49 ++++++++++++++++--- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py index 943fd543cb3..6df0094224c 100644 --- a/neutron/agent/linux/ip_lib.py +++ b/neutron/agent/linux/ip_lib.py @@ -38,6 +38,11 @@ LOOPBACK_DEVNAME = 'lo' SYS_NET_PATH = '/sys/class/net' +class AddressNotReady(exceptions.NeutronException): + message = _("Failure waiting for address %(address)s to " + "become ready: %(reason)s") + + class SubProcessBase(object): def __init__(self, namespace=None, log_fail_as_error=True): @@ -383,9 +388,34 @@ class IpAddrCommand(IpDeviceCommandBase): retval.append(dict(cidr=parts[1], scope=scope, - dynamic=('dynamic' == parts[-1]))) + dynamic=('dynamic' == parts[-1]), + tentative=('tentative' in line), + dadfailed=('dadfailed' == parts[-1]))) return retval + def wait_until_address_ready(self, address, wait_time=30): + """Wait until an address is no longer marked 'tentative' + + raises AddressNotReady if times out or address not present on interface + """ + def is_address_ready(): + try: + addr_info = self.list(to=address)[0] + except IndexError: + raise AddressNotReady( + address=address, + reason=_LE('Address not present on interface')) + if not addr_info['tentative']: + return True + if addr_info['dadfailed']: + raise AddressNotReady( + address=address, reason=_LE('Duplicate adddress detected')) + errmsg = _LE("Exceeded %s second limit waiting for " + "address to leave the tentative state.") % wait_time + utils.utils.wait_until_true( + is_address_ready, timeout=wait_time, sleep=0.20, + exception=AddressNotReady(address=address, reason=errmsg)) + class IpRouteCommand(IpDeviceCommandBase): COMMAND = 'route' diff --git a/neutron/tests/functional/agent/test_ovs_flows.py b/neutron/tests/functional/agent/test_ovs_flows.py index e1ccbeca6ad..410cfe186a8 100644 --- a/neutron/tests/functional/agent/test_ovs_flows.py +++ b/neutron/tests/functional/agent/test_ovs_flows.py @@ -60,8 +60,9 @@ class ARPSpoofTestCase(test_ovs_lib.OVSBridgeTestBase, self._setup_arp_spoof_for_port(self.dst_p.name, [self.dst_addr]) self.src_p.addr.add('%s/64' % self.src_addr) self.dst_p.addr.add('%s/64' % self.dst_addr) - # IPv6 addresses seem to take longer to initialize - self.pinger._max_attempts = 4 + # make sure the IPv6 addresses are ready before pinging + self.src_p.addr.wait_until_address_ready(self.src_addr) + self.dst_p.addr.wait_until_address_ready(self.dst_addr) self.pinger.assert_ping(self.dst_addr) def test_arp_spoof_blocks_response(self): diff --git a/neutron/tests/unit/agent/linux/test_ip_lib.py b/neutron/tests/unit/agent/linux/test_ip_lib.py index 54d703e6eac..7b34d353742 100644 --- a/neutron/tests/unit/agent/linux/test_ip_lib.py +++ b/neutron/tests/unit/agent/linux/test_ip_lib.py @@ -15,6 +15,7 @@ import mock import netaddr +import testtools from neutron.agent.common import utils # noqa from neutron.agent.linux import ip_lib @@ -80,6 +81,10 @@ ADDR_SAMPLE = (""" inet 172.16.77.240/24 brd 172.16.77.255 scope global eth0 inet6 2001:470:9:1224:5595:dd51:6ba2:e788/64 scope global temporary dynamic valid_lft 14187sec preferred_lft 3387sec + inet6 fe80::3023:39ff:febc:22ae/64 scope link tentative + valid_lft forever preferred_lft forever + inet6 fe80::3023:39ff:febc:22af/64 scope link tentative dadfailed + valid_lft forever preferred_lft forever inet6 2001:470:9:1224:fd91:272:581e:3a32/64 scope global temporary """ """deprecated dynamic valid_lft 14187sec preferred_lft 0sec @@ -98,6 +103,10 @@ ADDR_SAMPLE2 = (""" inet 172.16.77.240/24 scope global eth0 inet6 2001:470:9:1224:5595:dd51:6ba2:e788/64 scope global temporary dynamic valid_lft 14187sec preferred_lft 3387sec + inet6 fe80::3023:39ff:febc:22ae/64 scope link tentative + valid_lft forever preferred_lft forever + inet6 fe80::3023:39ff:febc:22af/64 scope link tentative dadfailed + valid_lft forever preferred_lft forever inet6 2001:470:9:1224:fd91:272:581e:3a32/64 scope global temporary """ """deprecated dynamic valid_lft 14187sec preferred_lft 0sec @@ -687,29 +696,53 @@ class TestIpAddrCommand(TestIPCmdBase): def test_list(self): expected = [ - dict(scope='global', + dict(scope='global', dadfailed=False, tentative=False, dynamic=False, cidr='172.16.77.240/24'), - dict(scope='global', + dict(scope='global', dadfailed=False, tentative=False, dynamic=True, cidr='2001:470:9:1224:5595:dd51:6ba2:e788/64'), - dict(scope='global', + dict(scope='link', dadfailed=False, tentative=True, + dynamic=False, cidr='fe80::3023:39ff:febc:22ae/64'), + dict(scope='link', dadfailed=True, tentative=True, + dynamic=False, cidr='fe80::3023:39ff:febc:22af/64'), + dict(scope='global', dadfailed=False, tentative=False, dynamic=True, cidr='2001:470:9:1224:fd91:272:581e:3a32/64'), - dict(scope='global', + dict(scope='global', dadfailed=False, tentative=False, dynamic=True, cidr='2001:470:9:1224:4508:b885:5fb:740b/64'), - dict(scope='global', + dict(scope='global', dadfailed=False, tentative=False, dynamic=True, cidr='2001:470:9:1224:dfcc:aaff:feb9:76ce/64'), - dict(scope='link', + dict(scope='link', dadfailed=False, tentative=False, dynamic=False, cidr='fe80::dfcc:aaff:feb9:76ce/64')] test_cases = [ADDR_SAMPLE, ADDR_SAMPLE2] for test_case in test_cases: self.parent._run = mock.Mock(return_value=test_case) - self.assertEqual(self.addr_cmd.list(), expected) + self.assertEqual(expected, self.addr_cmd.list()) self._assert_call([], ('show', 'tap0')) + def test_wait_until_address_ready(self): + self.parent._run.return_value = ADDR_SAMPLE + # this address is not tentative or failed so it should return + self.assertIsNone(self.addr_cmd.wait_until_address_ready( + '2001:470:9:1224:fd91:272:581e:3a32')) + + def test_wait_until_address_ready_non_existent_address(self): + self.addr_cmd.list = mock.Mock(return_value=[]) + with testtools.ExpectedException(ip_lib.AddressNotReady): + self.addr_cmd.wait_until_address_ready('abcd::1234') + + def test_wait_until_address_ready_timeout(self): + tentative_address = 'fe80::3023:39ff:febc:22ae' + self.addr_cmd.list = mock.Mock(return_value=[ + dict(scope='link', dadfailed=False, tentative=True, dynamic=False, + cidr=tentative_address + '/64')]) + with testtools.ExpectedException(ip_lib.AddressNotReady): + self.addr_cmd.wait_until_address_ready(tentative_address, + wait_time=1) + def test_list_filtered(self): expected = [ - dict(scope='global', + dict(scope='global', tentative=False, dadfailed=False, dynamic=False, cidr='172.16.77.240/24')] test_cases = [ADDR_SAMPLE, ADDR_SAMPLE2]