diff --git a/etc/neutron/rootwrap.d/netns-cleanup.filters b/etc/neutron/rootwrap.d/netns-cleanup.filters deleted file mode 100644 index 1ee142e54c1..00000000000 --- a/etc/neutron/rootwrap.d/netns-cleanup.filters +++ /dev/null @@ -1,12 +0,0 @@ -# neutron-rootwrap command filters for nodes on which neutron is -# expected to control network -# -# This file should be owned by (and only-writeable by) the root user - -# format seems to be -# cmd-name: filter-name, raw-command, user, args - -[Filters] - -# netns-cleanup -netstat: CommandFilter, netstat, root diff --git a/neutron/cmd/netns_cleanup.py b/neutron/cmd/netns_cleanup.py index 4c38cd222f0..a12987228a1 100644 --- a/neutron/cmd/netns_cleanup.py +++ b/neutron/cmd/netns_cleanup.py @@ -35,6 +35,7 @@ from neutron.common import config from neutron.conf.agent import cmd from neutron.conf.agent import common as agent_config from neutron.conf.agent import dhcp as dhcp_config +from neutron.privileged.agent.linux import utils as priv_utils LOG = logging.getLogger(__name__) NS_PREFIXES = { @@ -43,7 +44,6 @@ NS_PREFIXES = { dvr_fip_ns.FIP_NS_PREFIX], } SIGTERM_WAITTIME = 10 -NETSTAT_PIDS_REGEX = re.compile(r'.* (?P\d{2,6})/.*') class PidsInNamespaceException(Exception): @@ -134,22 +134,6 @@ def unplug_device(device): device.set_log_fail_as_error(orig_log_fail_as_error) -def find_listen_pids_namespace(namespace): - """Retrieve a list of pids of listening processes within the given netns. - - It executes netstat -nlp and returns a set of unique pairs - """ - ip = ip_lib.IPWrapper(namespace=namespace) - pids = set() - cmd = ['netstat', '-nlp'] - output = ip.netns.execute(cmd, run_as_root=True) - for line in output.splitlines(): - m = NETSTAT_PIDS_REGEX.match(line) - if m: - pids.add(m.group('pid')) - return pids - - def wait_until_no_listen_pids_namespace(namespace, timeout=SIGTERM_WAITTIME): """Poll listening processes within the given namespace. @@ -164,7 +148,7 @@ def wait_until_no_listen_pids_namespace(namespace, timeout=SIGTERM_WAITTIME): # previous command start = end = time.time() while end - start < timeout: - if not find_listen_pids_namespace(namespace): + if not priv_utils.find_listen_pids_namespace(namespace): return time.sleep(1) end = time.time() @@ -179,7 +163,7 @@ def _kill_listen_processes(namespace, force=False): then a SIGKILL will be issued to all parents and all their children. Also, this function returns the number of listening processes. """ - pids = find_listen_pids_namespace(namespace) + pids = priv_utils.find_listen_pids_namespace(namespace) pids_to_kill = {utils.find_fork_top_parent(pid) for pid in pids} kill_signal = signal.SIGTERM if force: diff --git a/neutron/privileged/agent/linux/utils.py b/neutron/privileged/agent/linux/utils.py new file mode 100644 index 00000000000..c038814acea --- /dev/null +++ b/neutron/privileged/agent/linux/utils.py @@ -0,0 +1,42 @@ +# Copyright 2020 Red Hat, Inc. +# +# Licensed 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 re + +from oslo_concurrency import processutils + +from neutron import privileged + + +NETSTAT_PIDS_REGEX = re.compile(r'.* (?P\d{2,6})/.*') + + +@privileged.default.entrypoint +def find_listen_pids_namespace(namespace): + return _find_listen_pids_namespace(namespace) + + +def _find_listen_pids_namespace(namespace): + """Retrieve a list of pids of listening processes within the given netns + + This method is implemented separately to allow unit testing. + """ + pids = set() + cmd = ['ip', 'netns', 'exec', namespace, 'netstat', '-nlp'] + output = processutils.execute(*cmd) + for line in output[0].splitlines(): + m = NETSTAT_PIDS_REGEX.match(line) + if m: + pids.add(m.group('pid')) + return list(pids) diff --git a/neutron/tests/functional/privileged/agent/linux/test_utils.py b/neutron/tests/functional/privileged/agent/linux/test_utils.py new file mode 100644 index 00000000000..ae316630b88 --- /dev/null +++ b/neutron/tests/functional/privileged/agent/linux/test_utils.py @@ -0,0 +1,39 @@ +# Copyright 2020 Red Hat, Inc. +# +# Licensed 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. + +from neutron.agent.linux import ip_lib +from neutron.privileged.agent.linux import utils as priv_utils +from neutron.tests.common import net_helpers +from neutron.tests.functional import base as functional_base + + +class FindListenPidsNamespaceTestCase(functional_base.BaseSudoTestCase): + + def test_find_listen_pids_namespace(self): + ns = self.useFixture(net_helpers.NamespaceFixture()).name + ip_wrapper = ip_lib.IPWrapper(namespace=ns) + ip_wrapper.add_dummy('device') + device = ip_lib.IPDevice('device', namespace=ns) + device.addr.add('10.20.30.40/24') + device.link.set_up() + + self.assertEqual(tuple(), priv_utils.find_listen_pids_namespace(ns)) + + netcat = net_helpers.NetcatTester(ns, ns, '10.20.30.40', 12345, 'udp') + proc = netcat.server_process + self.assertEqual((str(proc.child_pid), ), + priv_utils.find_listen_pids_namespace(ns)) + + netcat.stop_processes() + self.assertEqual(tuple(), priv_utils.find_listen_pids_namespace(ns)) diff --git a/neutron/tests/unit/cmd/test_netns_cleanup.py b/neutron/tests/unit/cmd/test_netns_cleanup.py index 3753b7d9454..f2408827dcc 100644 --- a/neutron/tests/unit/cmd/test_netns_cleanup.py +++ b/neutron/tests/unit/cmd/test_netns_cleanup.py @@ -19,41 +19,9 @@ from unittest import mock import testtools from neutron.cmd import netns_cleanup as util +from neutron.privileged.agent.linux import utils as priv_utils from neutron.tests import base -NETSTAT_NETNS_OUTPUT = (""" -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State\ - PID/Program name -tcp 0 0 0.0.0.0:9697 0.0.0.0:* LISTEN\ - 1347/python -raw 0 0 0.0.0.0:112 0.0.0.0:* 7\ - 1279/keepalived -raw 0 0 0.0.0.0:112 0.0.0.0:* 7\ - 1279/keepalived -raw6 0 0 :::58 :::* 7\ - 1349/radvd -Active UNIX domain sockets (only servers) -Proto RefCnt Flags Type State I-Node PID/Program name\ - Path -unix 2 [ ACC ] STREAM LISTENING 82039530 1353/python\ - /tmp/rootwrap-VKSm8a/rootwrap.sock -""") - -NETSTAT_NO_NAMESPACE = (""" -Cannot open network namespace "qrouter-e6f206b2-4e8d-4597-a7e1-c3a20337e9c6":\ - No such file or directory -""") - -NETSTAT_NO_LISTEN_PROCS = (""" -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State\ - PID/Program name -Active UNIX domain sockets (only servers) -Proto RefCnt Flags Type State I-Node PID/Program name\ - Path -""") - class TestNetnsCleanup(base.BaseTestCase): def setUp(self): @@ -189,28 +157,6 @@ class TestNetnsCleanup(base.BaseTestCase): self.assertEqual([], ovs_br_cls.mock_calls) self.assertTrue(debug.called) - def _test_find_listen_pids_namespace_helper(self, expected, - netstat_output=None): - with mock.patch('neutron.agent.linux.ip_lib.IPWrapper') as ip_wrap: - ip_wrap.return_value.netns.execute.return_value = netstat_output - observed = util.find_listen_pids_namespace(mock.ANY) - self.assertEqual(expected, observed) - - def test_find_listen_pids_namespace_correct_output(self): - expected = set(['1347', '1279', '1349', '1353']) - self._test_find_listen_pids_namespace_helper(expected, - NETSTAT_NETNS_OUTPUT) - - def test_find_listen_pids_namespace_no_procs(self): - expected = set() - self._test_find_listen_pids_namespace_helper(expected, - NETSTAT_NO_LISTEN_PROCS) - - def test_find_listen_pids_namespace_no_namespace(self): - expected = set() - self._test_find_listen_pids_namespace_helper(expected, - NETSTAT_NO_NAMESPACE) - def _test__kill_listen_processes_helper(self, pids, parents, children, kills_expected, force): def _get_element(dct, x): @@ -234,7 +180,7 @@ class TestNetnsCleanup(base.BaseTestCase): mocks['find_fork_top_parent'].side_effect = _find_parent mocks['find_child_pids'].side_effect = _find_childs - with mock.patch.object(util, 'find_listen_pids_namespace', + with mock.patch.object(priv_utils, 'find_listen_pids_namespace', return_value=pids): calls = [] for pid, sig in kills_expected: diff --git a/neutron/tests/unit/privileged/agent/linux/test_utils.py b/neutron/tests/unit/privileged/agent/linux/test_utils.py new file mode 100644 index 00000000000..6141731b28e --- /dev/null +++ b/neutron/tests/unit/privileged/agent/linux/test_utils.py @@ -0,0 +1,76 @@ +# Copyright 2020 Red Hat, Inc. +# +# Licensed 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. + +from unittest import mock + +from oslo_concurrency import processutils + +from neutron.privileged.agent.linux import utils as priv_utils +from neutron.tests import base + + +NETSTAT_NETNS_OUTPUT = (""" +Active Internet connections (only servers) +Proto Recv-Q Send-Q Local Address Foreign Address State\ + PID/Program name +tcp 0 0 0.0.0.0:9697 0.0.0.0:* LISTEN\ + 1347/python +raw 0 0 0.0.0.0:112 0.0.0.0:* 7\ + 1279/keepalived +raw 0 0 0.0.0.0:112 0.0.0.0:* 7\ + 1279/keepalived +raw6 0 0 :::58 :::* 7\ + 1349/radvd +Active UNIX domain sockets (only servers) +Proto RefCnt Flags Type State I-Node PID/Program name\ + Path +unix 2 [ ACC ] STREAM LISTENING 82039530 1353/python\ + /tmp/rootwrap-VKSm8a/rootwrap.sock +""") + +NETSTAT_NO_NAMESPACE = (""" +Cannot open network namespace "qrouter-e6f206b2-4e8d-4597-a7e1-c3a20337e9c6":\ + No such file or directory +""") + +NETSTAT_NO_LISTEN_PROCS = (""" +Active Internet connections (only servers) +Proto Recv-Q Send-Q Local Address Foreign Address State\ + PID/Program name +Active UNIX domain sockets (only servers) +Proto RefCnt Flags Type State I-Node PID/Program name\ + Path +""") + + +class FindListenPidsNamespaceTestCase(base.BaseTestCase): + + def _test_find_listen_pids_namespace_helper(self, expected, + netstat_output=None): + with mock.patch.object(processutils, 'execute') as mock_execute: + mock_execute.return_value = (netstat_output, mock.ANY) + observed = priv_utils._find_listen_pids_namespace(mock.ANY) + self.assertEqual(sorted(expected), sorted(observed)) + + def test_find_listen_pids_namespace_correct_output(self): + expected = ['1347', '1279', '1349', '1353'] + self._test_find_listen_pids_namespace_helper(expected, + NETSTAT_NETNS_OUTPUT) + + def test_find_listen_pids_namespace_no_procs(self): + self._test_find_listen_pids_namespace_helper([], + NETSTAT_NO_LISTEN_PROCS) + + def test_find_listen_pids_namespace_no_namespace(self): + self._test_find_listen_pids_namespace_helper([], NETSTAT_NO_NAMESPACE)