Ensure get_pid_to_kill works with rootwrap script
To ensure that correct process is killed when using a rootwrap script, we must recursively list the children of our top-level process and kill the last one. This patch uses the psutil python module which is already used in the heat-cfntools project. Change-Id: I702bb9dd794c08fcaab637284ee303de1778cbb9
This commit is contained in:
parent
c64da0fa64
commit
c84480dfc2
@ -18,6 +18,7 @@ import eventlet
|
|||||||
import eventlet.event
|
import eventlet.event
|
||||||
import eventlet.queue
|
import eventlet.queue
|
||||||
import eventlet.timeout
|
import eventlet.timeout
|
||||||
|
import psutil
|
||||||
|
|
||||||
from neutron.agent.linux import utils
|
from neutron.agent.linux import utils
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
@ -129,21 +130,22 @@ class AsyncProcess(object):
|
|||||||
|
|
||||||
def _get_pid_to_kill(self):
|
def _get_pid_to_kill(self):
|
||||||
pid = self._process.pid
|
pid = self._process.pid
|
||||||
# If root helper was used, two processes will be created:
|
# If root helper was used, two or more processes will be created:
|
||||||
#
|
#
|
||||||
# - a root helper process (e.g. sudo myscript)
|
# - a root helper process (e.g. sudo myscript)
|
||||||
|
# - possibly a rootwrap script (e.g. neutron-rootwrap)
|
||||||
# - a child process (e.g. myscript)
|
# - a child process (e.g. myscript)
|
||||||
#
|
#
|
||||||
# Killing the root helper process will leave the child process
|
# Killing the root helper process will leave the child process
|
||||||
# as a zombie, so the only way to ensure that both die is to
|
# running, re-parented to init, so the only way to ensure that both
|
||||||
# target the child process directly.
|
# die is to target the child process directly.
|
||||||
if self.root_helper:
|
if self.root_helper:
|
||||||
pids = utils.find_child_pids(pid)
|
try:
|
||||||
if pids:
|
# This assumes that there are not multiple children in any
|
||||||
# The root helper will only ever launch a single child.
|
# level of the process tree under the parent process.
|
||||||
pid = pids[0]
|
pid = psutil.Process(
|
||||||
else:
|
self._process.pid).get_children(recursive=True)[-1].pid
|
||||||
# Process is already dead.
|
except (psutil.NoSuchProcess, IndexError):
|
||||||
pid = None
|
pid = None
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
|
@ -108,17 +108,3 @@ def replace_file(file_name, data):
|
|||||||
tmp_file.close()
|
tmp_file.close()
|
||||||
os.chmod(tmp_file.name, 0o644)
|
os.chmod(tmp_file.name, 0o644)
|
||||||
os.rename(tmp_file.name, file_name)
|
os.rename(tmp_file.name, file_name)
|
||||||
|
|
||||||
|
|
||||||
def find_child_pids(pid):
|
|
||||||
"""Retrieve a list of the pids of child processes of the given pid."""
|
|
||||||
try:
|
|
||||||
raw_pids = execute(['ps', '--ppid', pid, '-o', 'pid='])
|
|
||||||
except RuntimeError as e:
|
|
||||||
# Exception has already been logged by execute
|
|
||||||
no_children_found = 'Exit code: 1' in str(e)
|
|
||||||
if no_children_found:
|
|
||||||
return []
|
|
||||||
# Unexpected errors are the responsibility of the caller
|
|
||||||
raise
|
|
||||||
return [x.strip() for x in raw_pids.split('\n') if x.strip()]
|
|
||||||
|
@ -69,7 +69,8 @@ class BaseMonitorTest(base.BaseTestCase):
|
|||||||
|
|
||||||
self._check_test_requirements()
|
self._check_test_requirements()
|
||||||
|
|
||||||
self.root_helper = 'sudo'
|
# Emulate using a rootwrap script with sudo
|
||||||
|
self.root_helper = 'sudo sudo'
|
||||||
self.ovs = ovs_lib.BaseOVS(self.root_helper)
|
self.ovs = ovs_lib.BaseOVS(self.root_helper)
|
||||||
self.bridge = create_ovs_resource('test-br-', self.ovs.add_bridge)
|
self.bridge = create_ovs_resource('test-br-', self.ovs.add_bridge)
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
import eventlet.event
|
import eventlet.event
|
||||||
import eventlet.queue
|
import eventlet.queue
|
||||||
import eventlet.timeout
|
import eventlet.timeout
|
||||||
@ -187,11 +189,17 @@ class TestAsyncProcess(base.BaseTestCase):
|
|||||||
root_helper=None, pids=None):
|
root_helper=None, pids=None):
|
||||||
if root_helper:
|
if root_helper:
|
||||||
self.proc.root_helper = root_helper
|
self.proc.root_helper = root_helper
|
||||||
|
|
||||||
|
xpid = collections.namedtuple('xpid', ['pid'])
|
||||||
|
xpids = [xpid(pid) for pid in pids or []]
|
||||||
|
|
||||||
with mock.patch.object(self.proc, '_process') as mock_process:
|
with mock.patch.object(self.proc, '_process') as mock_process:
|
||||||
with mock.patch.object(mock_process, 'pid') as mock_pid:
|
with mock.patch.object(mock_process, 'pid') as mock_pid:
|
||||||
with mock.patch.object(utils, 'find_child_pids',
|
with mock.patch('psutil.Process') as mock_ps_process:
|
||||||
return_value=pids):
|
instance = mock_ps_process.return_value
|
||||||
|
instance.get_children.return_value = xpids
|
||||||
actual = self.proc._get_pid_to_kill()
|
actual = self.proc._get_pid_to_kill()
|
||||||
|
|
||||||
if expected is _marker:
|
if expected is _marker:
|
||||||
expected = mock_pid
|
expected = mock_pid
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
@ -202,6 +210,10 @@ class TestAsyncProcess(base.BaseTestCase):
|
|||||||
def test__get_pid_to_kill_returns_child_pid_with_root_helper(self):
|
def test__get_pid_to_kill_returns_child_pid_with_root_helper(self):
|
||||||
self._test__get_pid_to_kill(expected='1', pids=['1'], root_helper='a')
|
self._test__get_pid_to_kill(expected='1', pids=['1'], 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):
|
def test__get_pid_to_kill_returns_none_with_root_helper(self):
|
||||||
self._test__get_pid_to_kill(expected=None, root_helper='a')
|
self._test__get_pid_to_kill(expected=None, root_helper='a')
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
import testtools
|
|
||||||
|
|
||||||
from neutron.agent.linux import utils
|
from neutron.agent.linux import utils
|
||||||
from neutron.tests import base
|
from neutron.tests import base
|
||||||
@ -107,25 +106,3 @@ class AgentUtilsReplaceFile(base.BaseTestCase):
|
|||||||
ntf.assert_has_calls(expected)
|
ntf.assert_has_calls(expected)
|
||||||
chmod.assert_called_once_with('/baz', 0o644)
|
chmod.assert_called_once_with('/baz', 0o644)
|
||||||
rename.assert_called_once_with('/baz', '/foo')
|
rename.assert_called_once_with('/baz', '/foo')
|
||||||
|
|
||||||
|
|
||||||
class TestFindChildPids(base.BaseTestCase):
|
|
||||||
|
|
||||||
def test_returns_empty_list_for_exit_code_1(self):
|
|
||||||
with mock.patch.object(utils, 'execute',
|
|
||||||
side_effect=RuntimeError('Exit code: 1')):
|
|
||||||
self.assertEqual(utils.find_child_pids(-1), [])
|
|
||||||
|
|
||||||
def test_returns_empty_list_for_no_output(self):
|
|
||||||
with mock.patch.object(utils, 'execute', return_value=''):
|
|
||||||
self.assertEqual(utils.find_child_pids(-1), [])
|
|
||||||
|
|
||||||
def test_returns_list_of_child_process_ids_for_good_ouput(self):
|
|
||||||
with mock.patch.object(utils, 'execute', return_value=' 123 \n 185\n'):
|
|
||||||
self.assertEqual(utils.find_child_pids(-1), ['123', '185'])
|
|
||||||
|
|
||||||
def test_raises_unknown_exception(self):
|
|
||||||
with testtools.ExpectedException(RuntimeError):
|
|
||||||
with mock.patch.object(utils, 'execute',
|
|
||||||
side_effect=RuntimeError()):
|
|
||||||
utils.find_child_pids(-1)
|
|
||||||
|
@ -16,6 +16,7 @@ jsonrpclib
|
|||||||
Jinja2
|
Jinja2
|
||||||
kombu>=2.4.8
|
kombu>=2.4.8
|
||||||
netaddr>=0.7.6
|
netaddr>=0.7.6
|
||||||
|
psutil>=0.6.1,<1.0
|
||||||
python-neutronclient>=2.3.0,<3
|
python-neutronclient>=2.3.0,<3
|
||||||
SQLAlchemy>=0.7.8,<=0.7.99
|
SQLAlchemy>=0.7.8,<=0.7.99
|
||||||
WebOb>=1.2.3,<1.3
|
WebOb>=1.2.3,<1.3
|
||||||
|
Loading…
Reference in New Issue
Block a user