# Halt the greenthreads
self._kill_event.send()
- pid = self._get_pid_to_kill()
+ pid = utils.get_root_helper_child_pid(
+ self._process.pid, self.root_helper)
if pid:
self._kill_process(pid)
# explicitly started again.
self._kill_event = None
- def _get_pid_to_kill(self):
- pid = self._process.pid
- # If root helper was used, two or more processes will be created:
- #
- # - a root helper process (e.g. sudo myscript)
- # - possibly a rootwrap script (e.g. neutron-rootwrap)
- # - a child process (e.g. myscript)
- #
- # Killing the root helper process will leave the child process
- # running, re-parented to init, so the only way to ensure that both
- # die is to target the child process directly.
- if self.root_helper:
- try:
- pid = utils.find_child_pids(pid)[0]
- except IndexError:
- # Process is already dead
- return None
- while True:
- try:
- # We shouldn't have more than one child per process
- # so keep getting the children of the first one
- pid = utils.find_child_pids(pid)[0]
- except IndexError:
- # Last process in the tree, return it
- break
- return pid
-
def _kill_process(self, pid):
try:
# A process started by a root helper will be running as
"""Retrieve a list of the pids of child processes of the given pid."""
try:
- raw_pids = execute(['ps', '--ppid', pid, '-o', 'pid='])
+ raw_pids = execute(['ps', '--ppid', pid, '-o', 'pid='],
+ log_fail_as_error=False)
except RuntimeError as e:
# Unexpected errors are the responsibility of the caller
with excutils.save_and_reraise_exception() as ctxt:
# Exception has already been logged by execute
- no_children_found = 'Exit code: 1' in str(e)
+ no_children_found = 'Exit code: 1' in e.message
if no_children_found:
ctxt.reraise = False
return []
conf_file = get_conf_file_name(cfg_root, uuid, cfg_file)
if os.path.exists(conf_file):
os.unlink(conf_file)
+
+
+def get_root_helper_child_pid(pid, root_helper=None):
+ """
+ Get the lowest child pid in the process hierarchy
+
+ If root helper was used, two or more processes would be created:
+
+ - a root helper process (e.g. sudo myscript)
+ - possibly a rootwrap script (e.g. neutron-rootwrap)
+ - a child process (e.g. myscript)
+
+ Killing the root helper process will leave the child process
+ running, re-parented to init, so the only way to ensure that both
+ die is to target the child process directly.
+ """
+ pid = str(pid)
+ if root_helper:
+ try:
+ pid = find_child_pids(pid)[0]
+ except IndexError:
+ # Process is already dead
+ return None
+ while True:
+ try:
+ # We shouldn't have more than one child per process
+ # so keep getting the children of the first one
+ pid = find_child_pids(pid)[0]
+ except IndexError:
+ # Last process in the tree, return it
+ break
+ return pid
# enable curl from namespace
curl_filter: CommandFilter, curl, root
+tee_filter: CommandFilter, tee, root
+tee_kill: KillFilter, root, tee, -9
# License for the specific language governing permissions and limitations
# under the License.
import eventlet
+import functools
+import os
+import select
+import shlex
+import subprocess
+
+from neutron.agent.linux import utils
+
+CHILD_PROCESS_TIMEOUT = os.environ.get('OS_TEST_CHILD_PROCESS_TIMEOUT', 20)
+CHILD_PROCESS_SLEEP = os.environ.get('OS_TEST_CHILD_PROCESS_SLEEP', 0.5)
+READ_TIMEOUT = os.environ.get('OS_TEST_READ_TIMEOUT', 5)
def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
eventlet.sleep(sleep)
+def remove_abs_path(cmd):
+ """Remove absolute path of executable in cmd
+
+ Note: New instance of list is returned
+
+ :param cmd: parsed shlex command (e.g. ['/bin/foo', 'param1', 'param two'])
+
+ """
+ if cmd and os.path.isabs(cmd[0]):
+ cmd = list(cmd)
+ cmd[0] = os.path.basename(cmd[0])
+
+ return cmd
+
+
+def get_cmdline_from_pid(pid):
+ if pid is None or not os.path.exists('/proc/%s' % pid):
+ return list()
+ with open('/proc/%s/cmdline' % pid, 'r') as f:
+ return f.readline().split('\0')[:-1]
+
+
+def cmdlines_are_equal(cmd1, cmd2):
+ """Validate provided lists containing output of /proc/cmdline are equal
+
+ This function ignores absolute paths of executables in order to have
+ correct results in case one list uses absolute path and the other does not.
+ """
+ cmd1 = remove_abs_path(cmd1)
+ cmd2 = remove_abs_path(cmd2)
+ return cmd1 == cmd2
+
+
+def pid_invoked_with_cmdline(pid, expected_cmd):
+ """Validate process with given pid is running with provided parameters
+
+ """
+ cmdline = get_cmdline_from_pid(pid)
+ return cmdlines_are_equal(expected_cmd, cmdline)
+
+
class Pinger(object):
def __init__(self, testcase, timeout=1, max_attempts=1):
self.testcase = testcase
{'src_ns': src_ns.namespace, 'dst_ip': dst_ip})
except RuntimeError:
pass
+
+
+class RootHelperProcess(subprocess.Popen):
+ def __init__(self, cmd, *args, **kwargs):
+ for arg in ('stdin', 'stdout', 'stderr'):
+ kwargs.setdefault(arg, subprocess.PIPE)
+ self.namespace = kwargs.pop('namespace', None)
+ self.root_helper = kwargs.pop('root_helper', None)
+ self.cmd = cmd
+ if self.namespace is not None:
+ cmd = ['ip', 'netns', 'exec', self.namespace] + cmd
+ if self.root_helper is not None:
+ cmd = shlex.split(self.root_helper) + cmd
+ self.child_pid = None
+ super(RootHelperProcess, self).__init__(cmd, *args, **kwargs)
+ if self.root_helper:
+ self._wait_for_child_process()
+
+ def kill(self):
+ pid = self.child_pid or str(self.pid)
+ utils.execute(['kill', '-9', pid],
+ root_helper=self.root_helper)
+
+ def read_stdout(self, timeout=None):
+ return self._read_stream(self.stdout, timeout)
+
+ @staticmethod
+ def _read_stream(stream, timeout):
+ if timeout:
+ poller = select.poll()
+ poller.register(stream.fileno())
+ poll_predicate = functools.partial(poller.poll, 1)
+ wait_until_true(poll_predicate, timeout, 0.1,
+ RuntimeError(
+ 'No output in %.2f seconds' % timeout))
+ return stream.readline()
+
+ def writeline(self, data):
+ self.stdin.write(data + os.linesep)
+ self.stdin.flush()
+
+ def _wait_for_child_process(self, timeout=CHILD_PROCESS_TIMEOUT,
+ sleep=CHILD_PROCESS_SLEEP):
+ def child_is_running():
+ child_pid = utils.get_root_helper_child_pid(
+ self.pid, root_helper=self.root_helper)
+ if pid_invoked_with_cmdline(child_pid, self.cmd):
+ return True
+
+ wait_until_true(
+ child_is_running,
+ timeout,
+ exception=RuntimeError("Process %s hasn't been spawned "
+ "in %d seconds" % (self.cmd, timeout)))
+ self.child_pid = utils.get_root_helper_child_pid(
+ self.pid, root_helper=self.root_helper)
from six import moves
from neutron.agent.linux import async_process
+from neutron.agent.linux import utils
from neutron.tests import base
# Ensure that the same output is read twice
self._check_stdout(proc)
- pid = proc._get_pid_to_kill()
+ pid = utils.get_root_helper_child_pid(proc._process.pid,
+ proc.root_helper)
proc._kill_process(pid)
self._check_stdout(proc)
proc.stop()
--- /dev/null
+# Copyright 2014 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.tests.functional.agent.linux import base
+from neutron.tests.functional.agent.linux import helpers
+
+
+class TestRootHelperProcess(base.BaseLinuxTestCase):
+
+ def test_process_read_write(self):
+ proc = helpers.RootHelperProcess(['tee'], root_helper=self.root_helper)
+ proc.writeline('foo')
+ output = proc.read_stdout(helpers.READ_TIMEOUT)
+ self.assertEqual('foo\n', output)
+
+ def test_process_kill(self):
+ with self.assert_max_execution_time(100):
+ proc = helpers.RootHelperProcess(
+ ['tee'], root_helper=self.root_helper)
+ proc.kill()
+ proc.wait()
+ # sudo returns 137 and
+ # rootwrap returns 247 (bug 1364822)
+ self.assertIn(proc.returncode, [137, 247])
import eventlet
from neutron.agent.linux import ovsdb_monitor
+from neutron.agent.linux import utils
from neutron.tests.functional.agent.linux import base as linux_base
from neutron.tests.functional import base as functional_base
self.monitor.respawn_interval = 0
old_pid = self.monitor._process.pid
output1 = self.collect_initial_output()
- pid = self.monitor._get_pid_to_kill()
+ pid = utils.get_root_helper_child_pid(old_pid, self.root_helper)
self.monitor._kill_process(pid)
self.monitor._reset_queues()
while (self.monitor._process.pid == old_pid):
# License for the specific language governing permissions and limitations
# under the License.
+import contextlib
+
import eventlet.event
import eventlet.queue
import eventlet.timeout
from neutron.tests import base
-_marker = ()
-
-
class TestAsyncProcess(base.BaseTestCase):
def setUp(self):
self._test_iter_output_calls_iter_queue_on_output_queue('stderr')
def _test__kill(self, respawning, pid=None):
- with mock.patch.object(self.proc, '_kill_event') as mock_kill_event:
- with mock.patch.object(self.proc, '_get_pid_to_kill',
- return_value=pid):
- with mock.patch.object(self.proc,
- '_kill_process') as mock_kill_process:
- self.proc._kill(respawning)
+ with contextlib.nested(
+ mock.patch.object(self.proc, '_kill_event'),
+ mock.patch.object(utils, 'get_root_helper_child_pid',
+ return_value=pid),
+ mock.patch.object(self.proc, '_kill_process'),
+ mock.patch.object(self.proc, '_process')) as (
+ mock_kill_event,
+ mock_get_child_pid,
+ mock_kill_process,
+ mock_process):
+ self.proc._kill(respawning)
if respawning:
self.assertIsNotNone(self.proc._kill_event)
def test__kill_targets_process_for_pid(self):
self._test__kill(False, pid='1')
- def _test__get_pid_to_kill(self, expected=_marker,
- root_helper=None, pids=None):
- def _find_child_pids(x):
- if not pids:
- return []
- pids.pop(0)
- return pids
-
- if root_helper:
- self.proc.root_helper = root_helper
-
- with mock.patch.object(self.proc, '_process') as mock_process:
- with mock.patch.object(mock_process, 'pid') as mock_pid:
- with mock.patch.object(utils, 'find_child_pids',
- side_effect=_find_child_pids):
- actual = self.proc._get_pid_to_kill()
- if expected is _marker:
- expected = mock_pid
- self.assertEqual(expected, actual)
-
- def test__get_pid_to_kill_returns_process_pid_without_root_helper(self):
- self._test__get_pid_to_kill()
-
- def test__get_pid_to_kill_returns_child_pid_with_root_helper(self):
- self._test__get_pid_to_kill(expected='2', pids=['1', '2'],
- root_helper='a')
-
- def test__get_pid_to_kill_returns_last_child_pid_with_root_Helper(self):
- self._test__get_pid_to_kill(expected='3', pids=['1', '2', '3'],
- root_helper='a')
-
- def test__get_pid_to_kill_returns_none_with_root_helper(self):
- self._test__get_pid_to_kill(expected=None, root_helper='a')
-
def _test__kill_process(self, pid, expected, exception_message=None):
self.proc.root_helper = 'foo'
if exception_message:
from neutron.tests import base
+_marker = object()
+
+
class FakeCreateProcess(object):
class FakeStdin(object):
def close(self):
with mock.patch.object(utils, 'execute',
side_effect=RuntimeError()):
utils.find_child_pids(-1)
+
+
+class TestGetRoothelperChildPid(base.BaseTestCase):
+ def _test_get_root_helper_child_pid(self, expected=_marker,
+ root_helper=None, pids=None):
+ def _find_child_pids(x):
+ if not pids:
+ return []
+ pids.pop(0)
+ return pids
+
+ mock_pid = object()
+ with mock.patch.object(utils, 'find_child_pids',
+ side_effect=_find_child_pids):
+ actual = utils.get_root_helper_child_pid(mock_pid, root_helper)
+ if expected is _marker:
+ expected = str(mock_pid)
+ self.assertEqual(expected, actual)
+
+ def test_returns_process_pid_without_root_helper(self):
+ self._test_get_root_helper_child_pid()
+
+ def test_returns_child_pid_with_root_helper(self):
+ self._test_get_root_helper_child_pid(expected='2', pids=['1', '2'],
+ root_helper='a')
+
+ def test_returns_last_child_pid_with_root_helper(self):
+ self._test_get_root_helper_child_pid(expected='3',
+ pids=['1', '2', '3'],
+ root_helper='a')
+
+ def test_returns_none_with_root_helper(self):
+ self._test_get_root_helper_child_pid(expected=None, root_helper='a')