Migrate "netstat" to oslo.privsep

Change-Id: If9e4c1513553c4bd10fd3b91c28c4d3f806ed816
Story: #2007686
Task: #40047
(cherry picked from commit 0c1818fbb0)
This commit is contained in:
Rodolfo Alonso Hernandez 2020-06-11 13:26:56 +00:00 committed by Slawek Kaplonski
parent 83fbb1db5a
commit 87fce78fee
6 changed files with 162 additions and 87 deletions

View File

@ -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

View File

@ -35,6 +35,7 @@ from neutron.common import config
from neutron.conf.agent import cmd from neutron.conf.agent import cmd
from neutron.conf.agent import common as agent_config from neutron.conf.agent import common as agent_config
from neutron.conf.agent import dhcp as dhcp_config from neutron.conf.agent import dhcp as dhcp_config
from neutron.privileged.agent.linux import utils as priv_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
NS_PREFIXES = { NS_PREFIXES = {
@ -43,7 +44,6 @@ NS_PREFIXES = {
dvr_fip_ns.FIP_NS_PREFIX], dvr_fip_ns.FIP_NS_PREFIX],
} }
SIGTERM_WAITTIME = 10 SIGTERM_WAITTIME = 10
NETSTAT_PIDS_REGEX = re.compile(r'.* (?P<pid>\d{2,6})/.*')
class PidsInNamespaceException(Exception): class PidsInNamespaceException(Exception):
@ -134,22 +134,6 @@ def unplug_device(device):
device.set_log_fail_as_error(orig_log_fail_as_error) 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): def wait_until_no_listen_pids_namespace(namespace, timeout=SIGTERM_WAITTIME):
"""Poll listening processes within the given namespace. """Poll listening processes within the given namespace.
@ -164,7 +148,7 @@ def wait_until_no_listen_pids_namespace(namespace, timeout=SIGTERM_WAITTIME):
# previous command # previous command
start = end = time.time() start = end = time.time()
while end - start < timeout: while end - start < timeout:
if not find_listen_pids_namespace(namespace): if not priv_utils.find_listen_pids_namespace(namespace):
return return
time.sleep(1) time.sleep(1)
end = time.time() 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, then a SIGKILL will be issued to all parents and all their children. Also,
this function returns the number of listening processes. 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} pids_to_kill = {utils.find_fork_top_parent(pid) for pid in pids}
kill_signal = signal.SIGTERM kill_signal = signal.SIGTERM
if force: if force:

View File

@ -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<pid>\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)

View File

@ -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))

View File

@ -19,41 +19,9 @@ import mock
import testtools import testtools
from neutron.cmd import netns_cleanup as util from neutron.cmd import netns_cleanup as util
from neutron.privileged.agent.linux import utils as priv_utils
from neutron.tests import base 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): class TestNetnsCleanup(base.BaseTestCase):
def setUp(self): def setUp(self):
@ -189,28 +157,6 @@ class TestNetnsCleanup(base.BaseTestCase):
self.assertEqual([], ovs_br_cls.mock_calls) self.assertEqual([], ovs_br_cls.mock_calls)
self.assertTrue(debug.called) 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, def _test__kill_listen_processes_helper(self, pids, parents, children,
kills_expected, force): kills_expected, force):
def _get_element(dct, x): def _get_element(dct, x):
@ -234,7 +180,7 @@ class TestNetnsCleanup(base.BaseTestCase):
mocks['find_fork_top_parent'].side_effect = _find_parent mocks['find_fork_top_parent'].side_effect = _find_parent
mocks['find_child_pids'].side_effect = _find_childs 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): return_value=pids):
calls = [] calls = []
for pid, sig in kills_expected: for pid, sig in kills_expected:

View File

@ -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)