Skip to content

Commit 31a79f1

Browse files
brianphaleyralonsoh
authored andcommitted
Delete network namespace on last port deletion
Since a DHCP agent can handle multiple networks, each with their own unique network namespace, we must track in-use ports by network ID, so we delete the namespace when the last port on that network is removed. If we group all the ports together then we could have stale, empty namespaces until we either delete all the ports (unlikely) or the dhcp-agent is restarted and does cleanup. Regression introduced by https://review.opendev.org/c/openstack/neutron/+/840421 Closes-bug: #2015388 Change-Id: I36991328cabcbd6fa473b8d1d140ba88c774fb23 (cherry picked from commit dfe29e6)
1 parent a5f1788 commit 31a79f1

File tree

2 files changed

+65
-16
lines changed

2 files changed

+65
-16
lines changed

neutron/agent/linux/dhcp.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ def clean_devices(self):
245245
class DhcpLocalProcess(DhcpBase, metaclass=abc.ABCMeta):
246246
PORTS = []
247247

248-
# Track running interfaces.
249-
_interfaces = set()
248+
# Track running interfaces, indexed by network ID, for example,
249+
# {net-id-1: set(intf_1, intf_2), net-id-2: set(intf_3, intf_4), ...}
250+
_interfaces = collections.defaultdict(set)
250251

251252
def __init__(self, conf, network, process_monitor, version=None,
252253
plugin=None, segment=None):
@@ -267,20 +268,24 @@ def __init__(self, conf, network, process_monitor, version=None,
267268
fileutils.ensure_tree(self.network_conf_dir, mode=0o755)
268269

269270
@classmethod
270-
def _add_running_interface(cls, interface):
271-
"""Safe method that add running interface"""
272-
cls._interfaces.add(interface)
271+
def _add_running_interface(cls, interface, network_id):
272+
"""Safe method that adds a given interface"""
273+
cls._interfaces[network_id].add(interface)
273274

274275
@classmethod
275-
def _del_running_interface(cls, interface):
276-
"""Safe method that remove given interface"""
277-
if interface in cls._interfaces:
278-
cls._interfaces.remove(interface)
276+
def _del_running_interface(cls, interface, network_id):
277+
"""Safe method that removes a given interface"""
278+
if cls._interfaces.get(network_id):
279+
if interface in cls._interfaces[network_id]:
280+
cls._interfaces[network_id].remove(interface)
281+
# no entries, cleanup
282+
if not cls._interfaces[network_id]:
283+
del cls._interfaces[network_id]
279284

280285
@classmethod
281-
def _has_running_interfaces(cls):
282-
"""Safe method that remove given interface"""
283-
return bool(cls._interfaces)
286+
def _has_running_interfaces(cls, network_id):
287+
"""Safe method that checks for interfaces"""
288+
bool(cls._interfaces.get(network_id))
284289

285290
@staticmethod
286291
def get_confs_dir(conf):
@@ -332,7 +337,8 @@ def _enable(self):
332337
self.network, self.segment)
333338
self.interface_name = interface_name
334339
self.spawn_process()
335-
self._add_running_interface(self.interface_name)
340+
self._add_running_interface(self.interface_name,
341+
self.network.id)
336342
return True
337343
except exceptions.ProcessExecutionError as error:
338344
LOG.debug("Spawning DHCP process for network %s failed; "
@@ -364,7 +370,7 @@ def disable(self, retain_port=False, block=False, **kwargs):
364370
pm.pid, SIGTERM_TIMEOUT)
365371
pm.disable(sig=str(int(signal.SIGKILL)))
366372
common_utils.wait_until_true(lambda: not self.active)
367-
self._del_running_interface(self.interface_name)
373+
self._del_running_interface(self.interface_name, self.network.id)
368374
if not retain_port:
369375
self._destroy_namespace_and_port()
370376
self._remove_config_files()
@@ -376,8 +382,9 @@ def _destroy_namespace_and_port(self):
376382
except RuntimeError:
377383
LOG.warning('Failed trying to delete interface: %s',
378384
self.interface_name)
379-
if not self._has_running_interfaces():
380-
# Delete nm only if we don't serve different segmentation id.
385+
# Delete namespace only if there are no running interfaces in it,
386+
# which covers the case where a network has multiple segmentation ids.
387+
if not self._has_running_interfaces(self.network.id):
381388
try:
382389
ip_lib.delete_network_namespace(self.network.namespace)
383390
except RuntimeError:

neutron/tests/unit/agent/linux/test_dhcp.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,17 @@ def __init__(self, domain='openstacklocal'):
719719
FakeRouterPort(domain=domain)]
720720

721721

722+
class FakeDualNetworkV2(object):
723+
def __init__(self, domain='openstacklocal'):
724+
self.id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
725+
self.subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
726+
self.namespace = 'qdhcp-ns-v2'
727+
self.ports = [FakePort1(domain=domain), FakeV6Port(domain=domain),
728+
FakeDualPort(domain=domain),
729+
FakeRouterHAPort(),
730+
FakeRouterPort(domain=domain)]
731+
732+
722733
class FakeDeviceManagerNetwork(object):
723734
def __init__(self):
724735
self.id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
@@ -1259,6 +1270,37 @@ def test_disable(self):
12591270

12601271
delete_ns.assert_called_with('qdhcp-ns')
12611272

1273+
def test_enable_disable_two_networks(self):
1274+
attrs_to_mock = {'active': mock.DEFAULT}
1275+
1276+
with mock.patch.multiple(LocalChild, **attrs_to_mock) as mocks:
1277+
mocks['active'].__get__ = mock.Mock(return_value=False)
1278+
lp = LocalChild(self.conf, FakeDualNetwork())
1279+
lp2 = LocalChild(self.conf, FakeDualNetworkV2())
1280+
lp.enable()
1281+
lp2.enable()
1282+
with mock.patch('neutron.agent.linux.ip_lib.'
1283+
'delete_network_namespace') as delete_ns:
1284+
lp.disable()
1285+
self.rmtree.assert_called_once()
1286+
1287+
self._assert_disabled(lp)
1288+
1289+
delete_ns.assert_called_once()
1290+
delete_ns.assert_called_with('qdhcp-ns')
1291+
1292+
delete_ns.reset_mock()
1293+
self.rmtree.reset_mock()
1294+
with mock.patch('neutron.agent.linux.ip_lib.'
1295+
'delete_network_namespace') as delete_ns:
1296+
lp2.disable()
1297+
self.rmtree.assert_called_once()
1298+
1299+
self._assert_disabled(lp2)
1300+
1301+
delete_ns.assert_called_once()
1302+
delete_ns.assert_called_with('qdhcp-ns-v2')
1303+
12621304
def test_disable_config_dir_removed_after_destroy(self):
12631305
parent = mock.MagicMock()
12641306
parent.attach_mock(self.rmtree, 'rmtree')

0 commit comments

Comments
 (0)