337 lines
14 KiB
Python
337 lines
14 KiB
Python
# Copyright 2013 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 signal
|
|
import sys
|
|
from unittest import mock
|
|
|
|
import eventlet.event
|
|
from eventlet.green import subprocess
|
|
import eventlet.queue
|
|
import testtools
|
|
|
|
from neutron.agent.common import async_process
|
|
from neutron.agent.common import utils
|
|
from neutron.tests import base
|
|
from neutron.tests.unit.agent.linux import failing_process
|
|
|
|
|
|
class TestAsyncProcess(base.BaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestAsyncProcess, self).setUp()
|
|
self.proc = async_process.AsyncProcess(['fake'])
|
|
|
|
def test_construtor_raises_exception_for_negative_respawn_interval(self):
|
|
with testtools.ExpectedException(ValueError):
|
|
async_process.AsyncProcess(['fake'], respawn_interval=-1)
|
|
|
|
def test__spawn(self):
|
|
expected_process = 'Foo'
|
|
proc = self.proc
|
|
with mock.patch.object(utils, 'create_process') as mock_create_process:
|
|
mock_create_process.return_value = [expected_process, None]
|
|
with mock.patch('eventlet.spawn') as mock_spawn:
|
|
proc._spawn()
|
|
|
|
self.assertTrue(self.proc._is_running)
|
|
self.assertIsInstance(proc._kill_event, eventlet.event.Event)
|
|
self.assertEqual(proc._process, expected_process)
|
|
mock_spawn.assert_has_calls([
|
|
mock.call(proc._watch_process,
|
|
proc._read_stdout,
|
|
proc._kill_event),
|
|
mock.call(proc._watch_process,
|
|
proc._read_stderr,
|
|
proc._kill_event),
|
|
])
|
|
self.assertEqual(len(proc._watchers), 2)
|
|
|
|
def test__pid_none(self):
|
|
pid = 1
|
|
self.proc._pid = None
|
|
with mock.patch.object(self.proc, '_process') as _process:
|
|
with mock.patch.object(utils,
|
|
'get_root_helper_child_pid') as func:
|
|
func.return_value = pid
|
|
self.assertEqual(self.proc.pid, pid)
|
|
func.assert_called_once_with(_process.pid, ['fake'],
|
|
run_as_root=False)
|
|
self.assertEqual(self.proc._pid, pid)
|
|
|
|
def test__pid_not_none(self):
|
|
self.proc._pid = 1
|
|
with mock.patch.object(self.proc, '_process'),\
|
|
mock.patch.object(utils, 'get_root_helper_child_pid') as func:
|
|
self.assertEqual(self.proc.pid, 1)
|
|
func.assert_not_called()
|
|
|
|
def test__handle_process_error_kills_with_respawn(self):
|
|
with mock.patch.object(self.proc, '_kill') as kill:
|
|
self.proc._handle_process_error()
|
|
|
|
kill.assert_has_calls([mock.call(signal.SIGKILL)])
|
|
|
|
def test__handle_process_error_kills_without_respawn(self):
|
|
self.proc.respawn_interval = 1
|
|
with mock.patch.object(self.proc, '_kill') as kill:
|
|
with mock.patch.object(self.proc, '_spawn') as spawn:
|
|
with mock.patch('eventlet.sleep') as sleep:
|
|
self.proc._handle_process_error()
|
|
|
|
kill.assert_has_calls([mock.call(signal.SIGKILL)])
|
|
sleep.assert_has_calls([mock.call(self.proc.respawn_interval)])
|
|
spawn.assert_called_once_with()
|
|
|
|
def test__handle_process_error_no_crash_if_started(self):
|
|
self.proc._is_running = True
|
|
with mock.patch.object(self.proc, '_kill'):
|
|
with mock.patch.object(self.proc, '_spawn') as mock_spawn:
|
|
self.proc._handle_process_error()
|
|
mock_spawn.assert_not_called()
|
|
|
|
def _watch_process_exception(self):
|
|
raise Exception('Error!')
|
|
|
|
def _test__watch_process(self, callback, kill_event):
|
|
self.proc._is_running = True
|
|
self.proc._kill_event = kill_event
|
|
# Ensure the test times out eventually if the watcher loops endlessly
|
|
with self.assert_max_execution_time():
|
|
with mock.patch.object(self.proc,
|
|
'_handle_process_error') as func:
|
|
self.proc._watch_process(callback, kill_event)
|
|
|
|
if not kill_event.ready():
|
|
func.assert_called_once_with()
|
|
|
|
def test__watch_process_exits_on_callback_failure(self):
|
|
self._test__watch_process(lambda: None, eventlet.event.Event())
|
|
|
|
def test__watch_process_exits_on_exception(self):
|
|
self._test__watch_process(self._watch_process_exception,
|
|
eventlet.event.Event())
|
|
with mock.patch.object(self.proc,
|
|
'_handle_process_error') as func:
|
|
self.proc._watch_process(self._watch_process_exception,
|
|
self.proc._kill_event)
|
|
func.assert_not_called()
|
|
|
|
def test__watch_process_exits_on_sent_kill_event(self):
|
|
kill_event = eventlet.event.Event()
|
|
kill_event.send()
|
|
self._test__watch_process(None, kill_event)
|
|
|
|
def _test_read_output_queues_and_returns_result(self, output):
|
|
queue = eventlet.queue.LightQueue()
|
|
mock_stream = mock.Mock()
|
|
with mock.patch.object(mock_stream, 'readline') as mock_readline:
|
|
mock_readline.return_value = output
|
|
result = self.proc._read(mock_stream, queue)
|
|
|
|
if output:
|
|
self.assertEqual(output, result)
|
|
self.assertEqual(output, queue.get_nowait())
|
|
else:
|
|
self.assertFalse(result)
|
|
self.assertTrue(queue.empty())
|
|
|
|
def test__read_queues_and_returns_output(self):
|
|
self._test_read_output_queues_and_returns_result('foo')
|
|
|
|
def test__read_returns_none_for_missing_output(self):
|
|
self._test_read_output_queues_and_returns_result('')
|
|
|
|
def test_start_raises_exception_if_process_already_started(self):
|
|
self.proc._is_running = True
|
|
with testtools.ExpectedException(async_process.AsyncProcessException):
|
|
self.proc.start()
|
|
|
|
def test_start_invokes__spawn(self):
|
|
with mock.patch.object(self.proc, '_spawn') as mock_start:
|
|
self.proc.start()
|
|
|
|
mock_start.assert_called_once_with()
|
|
|
|
def test__iter_queue_returns_empty_list_for_empty_queue(self):
|
|
result = list(self.proc._iter_queue(eventlet.queue.LightQueue(),
|
|
False))
|
|
self.assertEqual([], result)
|
|
|
|
def test__iter_queue_returns_queued_data(self):
|
|
queue = eventlet.queue.LightQueue()
|
|
queue.put('foo')
|
|
result = list(self.proc._iter_queue(queue, False))
|
|
self.assertEqual(result, ['foo'])
|
|
|
|
def _test_iter_output_calls_iter_queue_on_output_queue(self, output_type):
|
|
expected_value = 'foo'
|
|
with mock.patch.object(self.proc, '_iter_queue') as mock_iter_queue:
|
|
mock_iter_queue.return_value = expected_value
|
|
target_func = getattr(self.proc, 'iter_%s' % output_type, None)
|
|
value = target_func()
|
|
|
|
self.assertEqual(value, expected_value)
|
|
queue = getattr(self.proc, '_%s_lines' % output_type, None)
|
|
mock_iter_queue.assert_called_with(queue, False)
|
|
|
|
def test_iter_stdout(self):
|
|
self._test_iter_output_calls_iter_queue_on_output_queue('stdout')
|
|
|
|
def test_iter_stderr(self):
|
|
self._test_iter_output_calls_iter_queue_on_output_queue('stderr')
|
|
|
|
def test__kill_targets_process_for_pid(self):
|
|
pid = 1
|
|
|
|
with mock.patch.object(self.proc, '_kill_event'
|
|
) as mock_kill_event,\
|
|
mock.patch.object(utils, 'get_root_helper_child_pid',
|
|
return_value=pid),\
|
|
mock.patch.object(self.proc, '_kill_process_and_wait'
|
|
) as mock_kill_process_and_wait,\
|
|
mock.patch.object(self.proc, '_process'):
|
|
self.proc._kill(signal.SIGKILL)
|
|
|
|
self.assertIsNone(self.proc._kill_event)
|
|
self.assertFalse(self.proc._is_running)
|
|
self.assertIsNone(self.proc._pid)
|
|
|
|
mock_kill_event.send.assert_called_once_with()
|
|
if pid:
|
|
mock_kill_process_and_wait.assert_called_once_with(
|
|
pid, signal.SIGKILL, None)
|
|
|
|
def _test__kill_process_and_wait(self, pid, expected,
|
|
exception_message=None,
|
|
kill_signal=signal.SIGKILL):
|
|
self.proc.run_as_root = True
|
|
if exception_message:
|
|
exc = RuntimeError(exception_message)
|
|
else:
|
|
exc = None
|
|
with mock.patch.object(utils, 'kill_process',
|
|
side_effect=exc) as mock_kill_process:
|
|
actual = self.proc._kill_process(pid, kill_signal)
|
|
|
|
self.assertEqual(expected, actual)
|
|
mock_kill_process.assert_called_with(pid,
|
|
kill_signal,
|
|
self.proc.run_as_root)
|
|
|
|
def test__kill_process_and_wait_returns_true_for_valid_pid(self):
|
|
self._test__kill_process_and_wait('1', True)
|
|
|
|
def test__kill_process_and_wait_returns_false_for_execute_exception(self):
|
|
self._test__kill_process_and_wait('1', False, 'Invalid')
|
|
|
|
def test_kill_process_and_wait_with_different_signal(self):
|
|
self._test__kill_process_and_wait(
|
|
'1', True, kill_signal=signal.SIGTERM)
|
|
|
|
def test__kill_process_timeout_reached(self):
|
|
self.proc.run_as_root = True
|
|
kill_timeout = 5
|
|
pid = '1'
|
|
with mock.patch.object(utils, 'kill_process') as mock_kill_process, \
|
|
mock.patch.object(self.proc, '_process') as process_mock:
|
|
process_mock.wait.side_effect = subprocess.TimeoutExpired(
|
|
self.proc.cmd, kill_timeout)
|
|
self.assertTrue(
|
|
self.proc._kill_process_and_wait(
|
|
pid, signal.SIGTERM, kill_timeout))
|
|
|
|
process_mock.wait.assert_called_once_with(kill_timeout)
|
|
mock_kill_process.assert_has_calls([
|
|
mock.call(pid, signal.SIGTERM, self.proc.run_as_root),
|
|
mock.call(pid, signal.SIGKILL, self.proc.run_as_root)])
|
|
|
|
def test_stop_calls_kill_with_provided_signal_number(self):
|
|
self.proc._is_running = True
|
|
with mock.patch.object(self.proc, '_kill') as mock_kill:
|
|
self.proc.stop(kill_signal=signal.SIGTERM)
|
|
mock_kill.assert_called_once_with(signal.SIGTERM, None)
|
|
|
|
def test_stop_raises_exception_if_already_started(self):
|
|
with testtools.ExpectedException(async_process.AsyncProcessException):
|
|
self.proc.stop()
|
|
|
|
def test_cmd(self):
|
|
for expected, cmd in (('ls -l file', ['ls', '-l', 'file']),
|
|
('fake', ['fake'])):
|
|
proc = async_process.AsyncProcess(cmd)
|
|
self.assertEqual(expected, proc.cmd)
|
|
|
|
|
|
class TestAsyncProcessLogging(base.BaseTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestAsyncProcessLogging, self).setUp()
|
|
self.log_mock = mock.patch.object(async_process, 'LOG').start()
|
|
|
|
def _test__read_stdout_logging(self, enable):
|
|
proc = async_process.AsyncProcess(['fakecmd'], log_output=enable)
|
|
with mock.patch.object(proc, '_read', return_value='fakedata'),\
|
|
mock.patch.object(proc, '_process'):
|
|
proc._read_stdout()
|
|
self.assertEqual(enable, self.log_mock.debug.called)
|
|
|
|
def _test__read_stderr_logging(self, enable):
|
|
proc = async_process.AsyncProcess(['fake'], log_output=enable)
|
|
with mock.patch.object(proc, '_read', return_value='fakedata'),\
|
|
mock.patch.object(proc, '_process'):
|
|
proc._read_stderr()
|
|
self.assertEqual(enable, self.log_mock.error.called)
|
|
|
|
def test__read_stdout_logging_enabled(self):
|
|
self._test__read_stdout_logging(enable=True)
|
|
|
|
def test__read_stdout_logging_disabled(self):
|
|
self._test__read_stdout_logging(enable=False)
|
|
|
|
def test__read_stderr_logging_enabled(self):
|
|
self._test__read_stderr_logging(enable=True)
|
|
|
|
def test__read_stderr_logging_disabled(self):
|
|
self._test__read_stderr_logging(enable=False)
|
|
|
|
|
|
class TestAsyncProcessDieOnError(base.BaseTestCase):
|
|
|
|
def test__read_stderr_returns_none_on_error(self):
|
|
proc = async_process.AsyncProcess(['fakecmd'], die_on_error=True)
|
|
with mock.patch.object(proc, '_read', return_value='fakedata'),\
|
|
mock.patch.object(proc, '_process'):
|
|
self.assertIsNone(proc._read_stderr())
|
|
|
|
|
|
class TestFailingAsyncProcess(base.BaseTestCase):
|
|
def setUp(self):
|
|
super(TestFailingAsyncProcess, self).setUp()
|
|
path = self.get_temp_file_path('async.tmp', self.get_new_temp_dir())
|
|
self.process = async_process.AsyncProcess([sys.executable,
|
|
failing_process.__file__,
|
|
path],
|
|
respawn_interval=0)
|
|
|
|
def test_failing_async_process_handle_error_once(self):
|
|
with mock.patch.object(self.process, '_handle_process_error')\
|
|
as handle_error_mock:
|
|
self.process.start()
|
|
self.process._process.wait()
|
|
# Wait for the monitor process to complete
|
|
for thread in self.process._watchers:
|
|
thread.wait()
|
|
self.assertEqual(1, handle_error_mock.call_count)
|