Merge "Read in non-blocking fashion when starting console"
This commit is contained in:
commit
e4a1390b41
@ -20,6 +20,7 @@ Ironic console utilities.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -159,6 +160,41 @@ def get_shellinabox_console_url(port):
|
|||||||
'port': port}
|
'port': port}
|
||||||
|
|
||||||
|
|
||||||
|
class _PopenNonblockingPipe(object):
|
||||||
|
def __init__(self, source):
|
||||||
|
self._source = source
|
||||||
|
self._output = b''
|
||||||
|
self._wait = False
|
||||||
|
self._finished = False
|
||||||
|
self._set_async()
|
||||||
|
|
||||||
|
def _set_async(self):
|
||||||
|
flags = fcntl.fcntl(self._source, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(self._source, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||||
|
|
||||||
|
def read(self, num_bytes=4096):
|
||||||
|
if self._finished:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if self._wait:
|
||||||
|
time.sleep(1)
|
||||||
|
self._wait = False
|
||||||
|
data = os.read(self._source.fileno(), num_bytes)
|
||||||
|
self._output += data
|
||||||
|
if len(data) < num_bytes:
|
||||||
|
self._wait = True
|
||||||
|
except OSError:
|
||||||
|
self._finished = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output(self):
|
||||||
|
return self._output
|
||||||
|
|
||||||
|
@property
|
||||||
|
def finished(self):
|
||||||
|
return self._finished
|
||||||
|
|
||||||
|
|
||||||
def start_shellinabox_console(node_uuid, port, console_cmd):
|
def start_shellinabox_console(node_uuid, port, console_cmd):
|
||||||
"""Open the serial console for a node.
|
"""Open the serial console for a node.
|
||||||
|
|
||||||
@ -215,6 +251,9 @@ def start_shellinabox_console(node_uuid, port, console_cmd):
|
|||||||
'node': node_uuid,
|
'node': node_uuid,
|
||||||
'command': ' '.join(args)}
|
'command': ' '.join(args)}
|
||||||
|
|
||||||
|
stdout_pipe, stderr_pipe = (
|
||||||
|
_PopenNonblockingPipe(obj.stdout), _PopenNonblockingPipe(obj.stderr))
|
||||||
|
|
||||||
def _wait(node_uuid, popen_obj):
|
def _wait(node_uuid, popen_obj):
|
||||||
locals['returncode'] = popen_obj.poll()
|
locals['returncode'] = popen_obj.poll()
|
||||||
|
|
||||||
@ -226,14 +265,16 @@ def start_shellinabox_console(node_uuid, port, console_cmd):
|
|||||||
raise loopingcall.LoopingCallDone()
|
raise loopingcall.LoopingCallDone()
|
||||||
|
|
||||||
if locals['returncode'] is not None:
|
if locals['returncode'] is not None:
|
||||||
(stdout, stderr) = popen_obj.communicate()
|
watched = (stdout_pipe, stderr_pipe)
|
||||||
|
while time.time() < expiration and not all(
|
||||||
|
(i.finished for i in watched)):
|
||||||
|
for pipe in watched:
|
||||||
|
pipe.read()
|
||||||
locals['errstr'] = error_message + _(
|
locals['errstr'] = error_message + _(
|
||||||
"Exit code: %(return_code)s.\n"
|
"Exit code: %(return_code)s.\nStdout: %(stdout)r\n"
|
||||||
"Stdout: %(stdout)r\n"
|
|
||||||
"Stderr: %(stderr)r") % {
|
"Stderr: %(stderr)r") % {
|
||||||
'return_code': locals['returncode'],
|
'return_code': locals['returncode'],
|
||||||
'stdout': stdout,
|
'stdout': stdout_pipe.output, 'stderr': stderr_pipe.output}
|
||||||
'stderr': stderr}
|
|
||||||
raise loopingcall.LoopingCallDone()
|
raise loopingcall.LoopingCallDone()
|
||||||
|
|
||||||
if time.time() > expiration:
|
if time.time() > expiration:
|
||||||
|
@ -18,12 +18,14 @@
|
|||||||
"""Test class for console_utils driver module."""
|
"""Test class for console_utils driver module."""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
from ironic_lib import utils as ironic_utils
|
from ironic_lib import utils as ironic_utils
|
||||||
import mock
|
import mock
|
||||||
@ -262,10 +264,11 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
filepath,
|
filepath,
|
||||||
'password')
|
'password')
|
||||||
|
|
||||||
|
@mock.patch.object(fcntl, 'fcntl', autospec=True)
|
||||||
@mock.patch.object(console_utils, 'open',
|
@mock.patch.object(console_utils, 'open',
|
||||||
mock.mock_open(read_data='12345\n'))
|
mock.mock_open(read_data='12345\n'))
|
||||||
@mock.patch.object(os.path, 'exists', autospec=True)
|
@mock.patch.object(os.path, 'exists', autospec=True)
|
||||||
@mock.patch.object(subprocess, 'Popen', autospec=True)
|
@mock.patch.object(subprocess, 'Popen')
|
||||||
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
||||||
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -274,8 +277,10 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
mock_dir_exists,
|
mock_dir_exists,
|
||||||
mock_pid_exists,
|
mock_pid_exists,
|
||||||
mock_popen,
|
mock_popen,
|
||||||
mock_path_exists):
|
mock_path_exists, mock_fcntl):
|
||||||
mock_popen.return_value.poll.return_value = 0
|
mock_popen.return_value.poll.return_value = 0
|
||||||
|
mock_popen.return_value.stdout.return_value.fileno.return_value = 0
|
||||||
|
mock_popen.return_value.stderr.return_value.fileno.return_value = 1
|
||||||
mock_pid_exists.return_value = True
|
mock_pid_exists.return_value = True
|
||||||
mock_path_exists.return_value = True
|
mock_path_exists.return_value = True
|
||||||
|
|
||||||
@ -291,10 +296,11 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
mock_popen.return_value.poll.assert_called_once_with()
|
mock_popen.return_value.poll.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(fcntl, 'fcntl', autospec=True)
|
||||||
@mock.patch.object(console_utils, 'open',
|
@mock.patch.object(console_utils, 'open',
|
||||||
mock.mock_open(read_data='12345\n'))
|
mock.mock_open(read_data='12345\n'))
|
||||||
@mock.patch.object(os.path, 'exists', autospec=True)
|
@mock.patch.object(os.path, 'exists', autospec=True)
|
||||||
@mock.patch.object(subprocess, 'Popen', autospec=True)
|
@mock.patch.object(subprocess, 'Popen')
|
||||||
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
||||||
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -303,10 +309,12 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
mock_dir_exists,
|
mock_dir_exists,
|
||||||
mock_pid_exists,
|
mock_pid_exists,
|
||||||
mock_popen,
|
mock_popen,
|
||||||
mock_path_exists):
|
mock_path_exists, mock_fcntl):
|
||||||
# no existing PID file before starting
|
# no existing PID file before starting
|
||||||
mock_stop.side_effect = exception.NoConsolePid('/tmp/blah')
|
mock_stop.side_effect = exception.NoConsolePid('/tmp/blah')
|
||||||
mock_popen.return_value.poll.return_value = 0
|
mock_popen.return_value.poll.return_value = 0
|
||||||
|
mock_popen.return_value.stdout.return_value.fileno.return_value = 0
|
||||||
|
mock_popen.return_value.stderr.return_value.fileno.return_value = 1
|
||||||
mock_pid_exists.return_value = True
|
mock_pid_exists.return_value = True
|
||||||
mock_path_exists.return_value = True
|
mock_path_exists.return_value = True
|
||||||
|
|
||||||
@ -322,36 +330,54 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
mock_popen.return_value.poll.assert_called_once_with()
|
mock_popen.return_value.poll.assert_called_once_with()
|
||||||
|
|
||||||
@mock.patch.object(subprocess, 'Popen', autospec=True)
|
@mock.patch.object(time, 'sleep', autospec=True)
|
||||||
|
@mock.patch.object(os, 'read', autospec=True)
|
||||||
|
@mock.patch.object(fcntl, 'fcntl', autospec=True)
|
||||||
|
@mock.patch.object(subprocess, 'Popen')
|
||||||
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
||||||
def test_start_shellinabox_console_fail(self, mock_stop, mock_dir_exists,
|
def test_start_shellinabox_console_fail(
|
||||||
mock_popen):
|
self, mock_stop, mock_dir_exists, mock_popen, mock_fcntl,
|
||||||
|
mock_os_read, mock_sleep):
|
||||||
mock_popen.return_value.poll.return_value = 1
|
mock_popen.return_value.poll.return_value = 1
|
||||||
mock_popen.return_value.communicate.return_value = ('output', 'error')
|
stdout = mock_popen.return_value.stdout
|
||||||
|
stderr = mock_popen.return_value.stderr
|
||||||
|
stdout.return_value.fileno.return_value = 0
|
||||||
|
stderr.return_value.fileno.return_value = 1
|
||||||
|
err_output = b'error output'
|
||||||
|
mock_os_read.side_effect = [err_output] * 2 + [OSError] * 2
|
||||||
|
mock_fcntl.side_effect = [1, mock.Mock()] * 2
|
||||||
|
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
exception.ConsoleSubprocessFailed, "Stdout: 'output'",
|
exception.ConsoleSubprocessFailed, "Stdout: %r" % err_output,
|
||||||
console_utils.start_shellinabox_console, self.info['uuid'],
|
console_utils.start_shellinabox_console, self.info['uuid'],
|
||||||
self.info['port'], 'ls&')
|
self.info['port'], 'ls&')
|
||||||
|
|
||||||
mock_stop.assert_called_once_with(self.info['uuid'])
|
mock_stop.assert_called_once_with(self.info['uuid'])
|
||||||
|
mock_sleep.assert_has_calls([mock.call(1), mock.call(1)])
|
||||||
mock_dir_exists.assert_called_once_with()
|
mock_dir_exists.assert_called_once_with()
|
||||||
|
for obj in (stdout, stderr):
|
||||||
|
mock_fcntl.assert_has_calls([
|
||||||
|
mock.call(obj, fcntl.F_GETFL),
|
||||||
|
mock.call(obj, fcntl.F_SETFL, 1 | os.O_NONBLOCK)])
|
||||||
mock_popen.assert_called_once_with(mock.ANY,
|
mock_popen.assert_called_once_with(mock.ANY,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
mock_popen.return_value.poll.assert_called_with()
|
mock_popen.return_value.poll.assert_called_with()
|
||||||
|
|
||||||
@mock.patch.object(subprocess, 'Popen', autospec=True)
|
@mock.patch.object(fcntl, 'fcntl', autospec=True)
|
||||||
|
@mock.patch.object(subprocess, 'Popen')
|
||||||
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
||||||
def test_start_shellinabox_console_timeout(
|
def test_start_shellinabox_console_timeout(
|
||||||
self, mock_stop, mock_dir_exists, mock_popen):
|
self, mock_stop, mock_dir_exists, mock_popen, mock_fcntl):
|
||||||
self.config(subprocess_timeout=0, group='console')
|
self.config(subprocess_timeout=0, group='console')
|
||||||
self.config(subprocess_checking_interval=0, group='console')
|
self.config(subprocess_checking_interval=0, group='console')
|
||||||
mock_popen.return_value.poll.return_value = None
|
mock_popen.return_value.poll.return_value = None
|
||||||
|
mock_popen.return_value.stdout.return_value.fileno.return_value = 0
|
||||||
|
mock_popen.return_value.stderr.return_value.fileno.return_value = 1
|
||||||
|
|
||||||
self.assertRaisesRegex(
|
self.assertRaisesRegex(
|
||||||
exception.ConsoleSubprocessFailed, 'Timeout or error',
|
exception.ConsoleSubprocessFailed, 'Timeout or error',
|
||||||
@ -366,22 +392,28 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
mock_popen.return_value.poll.assert_called_with()
|
mock_popen.return_value.poll.assert_called_with()
|
||||||
self.assertEqual(0, mock_popen.return_value.communicate.call_count)
|
self.assertEqual(0, mock_popen.return_value.communicate.call_count)
|
||||||
|
|
||||||
|
@mock.patch.object(time, 'sleep', autospec=True)
|
||||||
|
@mock.patch.object(os, 'read', autospec=True)
|
||||||
|
@mock.patch.object(fcntl, 'fcntl', autospec=True)
|
||||||
@mock.patch.object(console_utils, 'open',
|
@mock.patch.object(console_utils, 'open',
|
||||||
mock.mock_open(read_data='12345\n'))
|
mock.mock_open(read_data='12345\n'))
|
||||||
@mock.patch.object(os.path, 'exists', autospec=True)
|
@mock.patch.object(os.path, 'exists', autospec=True)
|
||||||
@mock.patch.object(subprocess, 'Popen', autospec=True)
|
@mock.patch.object(subprocess, 'Popen')
|
||||||
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
@mock.patch.object(psutil, 'pid_exists', autospec=True)
|
||||||
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
@mock.patch.object(console_utils, '_ensure_console_pid_dir_exists',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
@mock.patch.object(console_utils, '_stop_console', autospec=True)
|
||||||
def test_start_shellinabox_console_fail_no_pid(self, mock_stop,
|
def test_start_shellinabox_console_fail_no_pid(
|
||||||
mock_dir_exists,
|
self, mock_stop, mock_dir_exists, mock_pid_exists, mock_popen,
|
||||||
mock_pid_exists,
|
mock_path_exists, mock_fcntl, mock_os_read, mock_sleep):
|
||||||
mock_popen,
|
|
||||||
mock_path_exists):
|
|
||||||
mock_popen.return_value.poll.return_value = 0
|
mock_popen.return_value.poll.return_value = 0
|
||||||
|
stdout = mock_popen.return_value.stdout
|
||||||
|
stderr = mock_popen.return_value.stderr
|
||||||
|
stdout.return_value.fileno.return_value = 0
|
||||||
|
stderr.return_value.fileno.return_value = 1
|
||||||
mock_pid_exists.return_value = False
|
mock_pid_exists.return_value = False
|
||||||
mock_popen.return_value.communicate.return_value = ('output', 'error')
|
mock_os_read.side_effect = [b'error output'] * 2 + [OSError] * 2
|
||||||
|
mock_fcntl.side_effect = [1, mock.Mock()] * 2
|
||||||
mock_path_exists.return_value = True
|
mock_path_exists.return_value = True
|
||||||
|
|
||||||
self.assertRaises(exception.ConsoleSubprocessFailed,
|
self.assertRaises(exception.ConsoleSubprocessFailed,
|
||||||
@ -391,7 +423,12 @@ class ConsoleUtilsTestCase(db_base.DbTestCase):
|
|||||||
'ls&')
|
'ls&')
|
||||||
|
|
||||||
mock_stop.assert_called_once_with(self.info['uuid'])
|
mock_stop.assert_called_once_with(self.info['uuid'])
|
||||||
|
mock_sleep.assert_has_calls([mock.call(1), mock.call(1)])
|
||||||
mock_dir_exists.assert_called_once_with()
|
mock_dir_exists.assert_called_once_with()
|
||||||
|
for obj in (stdout, stderr):
|
||||||
|
mock_fcntl.assert_has_calls([
|
||||||
|
mock.call(obj, fcntl.F_GETFL),
|
||||||
|
mock.call(obj, fcntl.F_SETFL, 1 | os.O_NONBLOCK)])
|
||||||
mock_pid_exists.assert_called_with(12345)
|
mock_pid_exists.assert_called_with(12345)
|
||||||
mock_popen.assert_called_once_with(mock.ANY,
|
mock_popen.assert_called_once_with(mock.ANY,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Fixes a possible
|
||||||
|
`console lockup issue <https://storyboard.openstack.org/#!/story/2006514>`_
|
||||||
|
in case of PID file not being yet created while daemon start has call
|
||||||
|
already returned success return code.
|
Loading…
Reference in New Issue
Block a user