Merge remote-tracking branch 'remotes/jsnow-gitlab/tags/python-pull-request' into staging

Pull request

# gpg: Signature made Tue 20 Oct 2020 20:04:54 BST
# gpg:                using RSA key F9B7ABDBBCACDF95BE76CBD07DEF8106AAFC390E
# gpg: Good signature from "John Snow (John Huston) <jsnow@redhat.com>" [full]
# Primary key fingerprint: FAEB 9711 A12C F475 812F  18F2 88A9 064D 1835 61EB
#      Subkey fingerprint: F9B7 ABDB BCAC DF95 BE76  CBD0 7DEF 8106 AAFC 390E

* remotes/jsnow-gitlab/tags/python-pull-request: (21 commits)
  python/qemu/qmp.py: Fix settimeout operation
  python/qemu/qmp.py: re-raise OSError when encountered
  python: add mypy config
  python/qemu/qmp.py: Preserve error context on re-raise
  python/qemu/console_socket.py: avoid encoding to/from string
  python/qemu/console_socket.py: Add type hint annotations
  python/qemu/console_socket.py: Clarify type of drain_thread
  python/qemu/console_socket.py: fix typing of settimeout
  python/qemu/console_socket.py: Correct type of recv()
  python/qemu: Add mypy type annotations
  iotests.py: Adjust HMP kwargs typing
  python/qemu: make 'args' style arguments immutable
  python/machine.py: fix _popen access
  python/machine.py: Add _qmp access shim
  python/machine.py: use qmp.command
  python/machine.py: Handle None events in events_wait
  python/machine.py: Don't modify state in _base_args()
  python/machine.py: reorder __init__
  python/machine.py: Fix monitor address typing
  python/qemu: use isort to lay out imports
  ...

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell
2020-10-21 11:09:13 +01:00
9 changed files with 326 additions and 211 deletions
+8 -1
View File
@@ -2373,11 +2373,18 @@ S: Maintained
F: include/sysemu/cryptodev*.h
F: backends/cryptodev*.c
Python library
M: John Snow <jsnow@redhat.com>
M: Cleber Rosa <crosa@redhat.com>
R: Eduardo Habkost <ehabkost@redhat.com>
S: Maintained
F: python/
T: git https://gitlab.com/jsnow/qemu.git python
Python scripts
M: Eduardo Habkost <ehabkost@redhat.com>
M: Cleber Rosa <crosa@redhat.com>
S: Odd fixes
F: python/qemu/*py
F: scripts/*.py
F: tests/*.py
+4
View File
@@ -0,0 +1,4 @@
[mypy]
strict = True
python_version = 3.6
warn_unused_configs = True
+7
View File
@@ -0,0 +1,7 @@
[settings]
force_grid_wrap=4
force_sort_within_sections=True
include_trailing_comma=True
line_length=72
lines_after_imports=2
multi_line_output=3
+6 -3
View File
@@ -17,6 +17,8 @@ accelerators.
import logging
import os
import subprocess
from typing import List, Optional
LOG = logging.getLogger(__name__)
@@ -29,7 +31,7 @@ ADDITIONAL_ARCHES = {
}
def list_accel(qemu_bin):
def list_accel(qemu_bin: str) -> List[str]:
"""
List accelerators enabled in the QEMU binary.
@@ -49,7 +51,8 @@ def list_accel(qemu_bin):
return [acc.strip() for acc in out.splitlines()[1:]]
def kvm_available(target_arch=None, qemu_bin=None):
def kvm_available(target_arch: Optional[str] = None,
qemu_bin: Optional[str] = None) -> bool:
"""
Check if KVM is available using the following heuristic:
- Kernel module is present in the host;
@@ -72,7 +75,7 @@ def kvm_available(target_arch=None, qemu_bin=None):
return True
def tcg_available(qemu_bin):
def tcg_available(qemu_bin: str) -> bool:
"""
Check if TCG is available.
+23 -31
View File
@@ -13,10 +13,11 @@ which can drain a socket and optionally dump the bytes to file.
# the COPYING file in the top-level directory.
#
from collections import deque
import socket
import threading
from collections import deque
import time
from typing import Deque, Optional
class ConsoleSocket(socket.socket):
@@ -29,22 +30,22 @@ class ConsoleSocket(socket.socket):
Optionally a file path can be passed in and we will also
dump the characters to this file for debugging purposes.
"""
def __init__(self, address, file=None, drain=False):
self._recv_timeout_sec = 300
def __init__(self, address: str, file: Optional[str] = None,
drain: bool = False):
self._recv_timeout_sec = 300.0
self._sleep_time = 0.5
self._buffer = deque()
self._buffer: Deque[int] = deque()
socket.socket.__init__(self, socket.AF_UNIX, socket.SOCK_STREAM)
self.connect(address)
self._logfile = None
if file:
self._logfile = open(file, "w")
self._logfile = open(file, "bw")
self._open = True
self._drain_thread = None
if drain:
self._drain_thread = self._thread_start()
else:
self._drain_thread = None
def _drain_fn(self):
def _drain_fn(self) -> None:
"""Drains the socket and runs while the socket is open."""
while self._open:
try:
@@ -55,7 +56,7 @@ class ConsoleSocket(socket.socket):
# self._open is set to False.
time.sleep(self._sleep_time)
def _thread_start(self):
def _thread_start(self) -> threading.Thread:
"""Kick off a thread to drain the socket."""
# Configure socket to not block and timeout.
# This allows our drain thread to not block
@@ -67,7 +68,7 @@ class ConsoleSocket(socket.socket):
drain_thread.start()
return drain_thread
def close(self):
def close(self) -> None:
"""Close the base object and wait for the thread to terminate"""
if self._open:
self._open = False
@@ -79,51 +80,42 @@ class ConsoleSocket(socket.socket):
self._logfile.close()
self._logfile = None
def _drain_socket(self):
def _drain_socket(self) -> None:
"""process arriving characters into in memory _buffer"""
data = socket.socket.recv(self, 1)
# latin1 is needed since there are some chars
# we are receiving that cannot be encoded to utf-8
# such as 0xe2, 0x80, 0xA6.
string = data.decode("latin1")
if self._logfile:
self._logfile.write("{}".format(string))
self._logfile.write(data)
self._logfile.flush()
for c in string:
self._buffer.extend(c)
self._buffer.extend(data)
def recv(self, bufsize=1):
def recv(self, bufsize: int = 1, flags: int = 0) -> bytes:
"""Return chars from in memory buffer.
Maintains the same API as socket.socket.recv.
"""
if self._drain_thread is None:
# Not buffering the socket, pass thru to socket.
return socket.socket.recv(self, bufsize)
return socket.socket.recv(self, bufsize, flags)
assert not flags, "Cannot pass flags to recv() in drained mode"
start_time = time.time()
while len(self._buffer) < bufsize:
time.sleep(self._sleep_time)
elapsed_sec = time.time() - start_time
if elapsed_sec > self._recv_timeout_sec:
raise socket.timeout
chars = ''.join([self._buffer.popleft() for i in range(bufsize)])
# We choose to use latin1 to remain consistent with
# handle_read() and give back the same data as the user would
# receive if they were reading directly from the
# socket w/o our intervention.
return chars.encode("latin1")
return bytes((self._buffer.popleft() for i in range(bufsize)))
def setblocking(self, value):
def setblocking(self, value: bool) -> None:
"""When not draining we pass thru to the socket,
since when draining we control socket blocking.
"""
if self._drain_thread is None:
socket.socket.setblocking(self, value)
def settimeout(self, seconds):
def settimeout(self, value: Optional[float]) -> None:
"""When not draining we pass thru to the socket,
since when draining we control the timeout.
"""
if seconds is not None:
self._recv_timeout_sec = seconds
if value is not None:
self._recv_timeout_sec = value
if self._drain_thread is None:
socket.socket.settimeout(self, seconds)
socket.socket.settimeout(self, value)
+187 -121
View File
File diff suppressed because it is too large Load Diff
+54 -35
View File
@@ -7,21 +7,22 @@
# This work is licensed under the terms of the GNU GPL, version 2. See
# the COPYING file in the top-level directory.
import json
import errno
import socket
import json
import logging
import socket
from types import TracebackType
from typing import (
Any,
cast,
Dict,
List,
Optional,
TextIO,
Type,
Tuple,
Type,
Union,
cast,
)
from types import TracebackType
# QMPMessage is a QMP Message of any kind.
@@ -90,7 +91,9 @@ class QEMUMonitorProtocol:
#: Logger object for debugging messages
logger = logging.getLogger('QMP')
def __init__(self, address, server=False, nickname=None):
def __init__(self, address: SocketAddrT,
server: bool = False,
nickname: Optional[str] = None):
"""
Create a QEMUMonitorProtocol class.
@@ -102,7 +105,7 @@ class QEMUMonitorProtocol:
@note No connection is established, this is done by the connect() or
accept() methods
"""
self.__events = []
self.__events: List[QMPMessage] = []
self.__address = address
self.__sock = self.__get_sock()
self.__sockfile: Optional[TextIO] = None
@@ -114,14 +117,14 @@ class QEMUMonitorProtocol:
self.__sock.bind(self.__address)
self.__sock.listen(1)
def __get_sock(self):
def __get_sock(self) -> socket.socket:
if isinstance(self.__address, tuple):
family = socket.AF_INET
else:
family = socket.AF_UNIX
return socket.socket(family, socket.SOCK_STREAM)
def __negotiate_capabilities(self):
def __negotiate_capabilities(self) -> QMPMessage:
greeting = self.__json_read()
if greeting is None or "QMP" not in greeting:
raise QMPConnectError
@@ -131,7 +134,7 @@ class QEMUMonitorProtocol:
return greeting
raise QMPCapabilitiesError
def __json_read(self, only_event=False):
def __json_read(self, only_event: bool = False) -> Optional[QMPMessage]:
assert self.__sockfile is not None
while True:
data = self.__sockfile.readline()
@@ -148,7 +151,7 @@ class QEMUMonitorProtocol:
continue
return resp
def __get_events(self, wait=False):
def __get_events(self, wait: Union[bool, float] = False) -> None:
"""
Check for new events in the stream and cache them in __events.
@@ -161,15 +164,19 @@ class QEMUMonitorProtocol:
retrieved or if some other error occurred.
"""
# Current timeout and blocking status
current_timeout = self.__sock.gettimeout()
# Check for new events regardless and pull them into the cache:
self.__sock.setblocking(False)
self.__sock.settimeout(0) # i.e. setblocking(False)
try:
self.__json_read()
except OSError as err:
if err.errno == errno.EAGAIN:
# No data available
pass
self.__sock.setblocking(True)
# EAGAIN: No data available; not critical
if err.errno != errno.EAGAIN:
raise
finally:
self.__sock.settimeout(current_timeout)
# Wait for new events, if needed.
# if wait is 0.0, this means "no wait" and is also implicitly false.
@@ -178,15 +185,18 @@ class QEMUMonitorProtocol:
self.__sock.settimeout(wait)
try:
ret = self.__json_read(only_event=True)
except socket.timeout:
raise QMPTimeoutError("Timeout waiting for event")
except:
raise QMPConnectError("Error while reading from socket")
except socket.timeout as err:
raise QMPTimeoutError("Timeout waiting for event") from err
except Exception as err:
msg = "Error while reading from socket"
raise QMPConnectError(msg) from err
finally:
self.__sock.settimeout(current_timeout)
if ret is None:
raise QMPConnectError("Error while reading from socket")
self.__sock.settimeout(None)
def __enter__(self):
def __enter__(self) -> 'QEMUMonitorProtocol':
# Implement context manager enter function.
return self
@@ -199,7 +209,7 @@ class QEMUMonitorProtocol:
# Implement context manager exit function.
self.close()
def connect(self, negotiate=True):
def connect(self, negotiate: bool = True) -> Optional[QMPMessage]:
"""
Connect to the QMP Monitor and perform capabilities negotiation.
@@ -214,7 +224,7 @@ class QEMUMonitorProtocol:
return self.__negotiate_capabilities()
return None
def accept(self, timeout=15.0):
def accept(self, timeout: Optional[float] = 15.0) -> QMPMessage:
"""
Await connection from QMP Monitor and perform capabilities negotiation.
@@ -250,7 +260,9 @@ class QEMUMonitorProtocol:
self.logger.debug("<<< %s", resp)
return resp
def cmd(self, name, args=None, cmd_id=None):
def cmd(self, name: str,
args: Optional[Dict[str, Any]] = None,
cmd_id: Optional[Any] = None) -> QMPMessage:
"""
Build a QMP command and send it to the QMP Monitor.
@@ -258,14 +270,14 @@ class QEMUMonitorProtocol:
@param args: command arguments (dict)
@param cmd_id: command id (dict, list, string or int)
"""
qmp_cmd = {'execute': name}
qmp_cmd: QMPMessage = {'execute': name}
if args:
qmp_cmd['arguments'] = args
if cmd_id:
qmp_cmd['id'] = cmd_id
return self.cmd_obj(qmp_cmd)
def command(self, cmd, **kwds):
def command(self, cmd: str, **kwds: Any) -> QMPReturnValue:
"""
Build and send a QMP command to the monitor, report errors if any
"""
@@ -278,7 +290,8 @@ class QEMUMonitorProtocol:
)
return cast(QMPReturnValue, ret['return'])
def pull_event(self, wait=False):
def pull_event(self,
wait: Union[bool, float] = False) -> Optional[QMPMessage]:
"""
Pulls a single event.
@@ -298,7 +311,7 @@ class QEMUMonitorProtocol:
return self.__events.pop(0)
return None
def get_events(self, wait=False):
def get_events(self, wait: bool = False) -> List[QMPMessage]:
"""
Get a list of available QMP events.
@@ -315,13 +328,13 @@ class QEMUMonitorProtocol:
self.__get_events(wait)
return self.__events
def clear_events(self):
def clear_events(self) -> None:
"""
Clear current list of pending events.
"""
self.__events = []
def close(self):
def close(self) -> None:
"""
Close the socket and socket file.
"""
@@ -330,16 +343,22 @@ class QEMUMonitorProtocol:
if self.__sockfile:
self.__sockfile.close()
def settimeout(self, timeout):
def settimeout(self, timeout: Optional[float]) -> None:
"""
Set the socket timeout.
@param timeout (float): timeout in seconds, or None.
@param timeout (float): timeout in seconds (non-zero), or None.
@note This is a wrap around socket.settimeout
@raise ValueError: if timeout was set to 0.
"""
if timeout == 0:
msg = "timeout cannot be 0; this engages non-blocking mode."
msg += " Use 'None' instead to disable timeouts."
raise ValueError(msg)
self.__sock.settimeout(timeout)
def get_sock_fd(self):
def get_sock_fd(self) -> int:
"""
Get the socket file descriptor.
@@ -347,7 +366,7 @@ class QEMUMonitorProtocol:
"""
return self.__sock.fileno()
def is_scm_available(self):
def is_scm_available(self) -> bool:
"""
Check if the socket allows for SCM_RIGHTS.
+36 -19
View File
@@ -17,11 +17,17 @@ subclass of QEMUMachine, respectively.
# Based on qmp.py.
#
import socket
import os
from typing import Optional, TextIO
import socket
from typing import (
List,
Optional,
Sequence,
TextIO,
)
from .machine import QEMUMachine
from .qmp import SocketAddrT
class QEMUQtestProtocol:
@@ -38,7 +44,8 @@ class QEMUQtestProtocol:
No conection is estabalished by __init__(), this is done
by the connect() or accept() methods.
"""
def __init__(self, address, server=False):
def __init__(self, address: SocketAddrT,
server: bool = False):
self._address = address
self._sock = self._get_sock()
self._sockfile: Optional[TextIO] = None
@@ -46,14 +53,14 @@ class QEMUQtestProtocol:
self._sock.bind(self._address)
self._sock.listen(1)
def _get_sock(self):
def _get_sock(self) -> socket.socket:
if isinstance(self._address, tuple):
family = socket.AF_INET
else:
family = socket.AF_UNIX
return socket.socket(family, socket.SOCK_STREAM)
def connect(self):
def connect(self) -> None:
"""
Connect to the qtest socket.
@@ -62,7 +69,7 @@ class QEMUQtestProtocol:
self._sock.connect(self._address)
self._sockfile = self._sock.makefile(mode='r')
def accept(self):
def accept(self) -> None:
"""
Await connection from QEMU.
@@ -71,7 +78,7 @@ class QEMUQtestProtocol:
self._sock, _ = self._sock.accept()
self._sockfile = self._sock.makefile(mode='r')
def cmd(self, qtest_cmd):
def cmd(self, qtest_cmd: str) -> str:
"""
Send a qtest command on the wire.
@@ -82,14 +89,16 @@ class QEMUQtestProtocol:
resp = self._sockfile.readline()
return resp
def close(self):
"""Close this socket."""
def close(self) -> None:
"""
Close this socket.
"""
self._sock.close()
if self._sockfile:
self._sockfile.close()
self._sockfile = None
def settimeout(self, timeout):
def settimeout(self, timeout: Optional[float]) -> None:
"""Set a timeout, in seconds."""
self._sock.settimeout(timeout)
@@ -99,8 +108,13 @@ class QEMUQtestMachine(QEMUMachine):
A QEMU VM, with a qtest socket available.
"""
def __init__(self, binary, args=None, name=None, test_dir="/var/tmp",
socket_scm_helper=None, sock_dir=None):
def __init__(self,
binary: str,
args: Sequence[str] = (),
name: Optional[str] = None,
test_dir: str = "/var/tmp",
socket_scm_helper: Optional[str] = None,
sock_dir: Optional[str] = None):
if name is None:
name = "qemu-%d" % os.getpid()
if sock_dir is None:
@@ -108,16 +122,19 @@ class QEMUQtestMachine(QEMUMachine):
super().__init__(binary, args, name=name, test_dir=test_dir,
socket_scm_helper=socket_scm_helper,
sock_dir=sock_dir)
self._qtest = None
self._qtest: Optional[QEMUQtestProtocol] = None
self._qtest_path = os.path.join(sock_dir, name + "-qtest.sock")
def _base_args(self):
args = super()._base_args()
args.extend(['-qtest', 'unix:path=' + self._qtest_path,
'-accel', 'qtest'])
@property
def _base_args(self) -> List[str]:
args = super()._base_args
args.extend([
'-qtest', f"unix:path={self._qtest_path}",
'-accel', 'qtest'
])
return args
def _pre_launch(self):
def _pre_launch(self) -> None:
super()._pre_launch()
self._qtest = QEMUQtestProtocol(self._qtest_path, server=True)
@@ -126,7 +143,7 @@ class QEMUQtestMachine(QEMUMachine):
super()._post_launch()
self._qtest.accept()
def _post_shutdown(self):
def _post_shutdown(self) -> None:
super()._post_shutdown()
self._remove_if_exists(self._qtest_path)
+1 -1
View File
@@ -605,7 +605,7 @@ class VM(qtest.QEMUQtestMachine):
def hmp(self, command_line: str, use_log: bool = False) -> QMPMessage:
cmd = 'human-monitor-command'
kwargs = {'command-line': command_line}
kwargs: Dict[str, Any] = {'command-line': command_line}
if use_log:
return self.qmp_log(cmd, **kwargs)
else: