]>
Commit | Line | Data |
---|---|---|
9b8ccd6d JS |
1 | """ |
2 | QEMU qtest library | |
3 | ||
4 | qtest offers the QEMUQtestProtocol and QEMUQTestMachine classes, which | |
5 | offer a connection to QEMU's qtest protocol socket, and a qtest-enabled | |
6 | subclass of QEMUMachine, respectively. | |
7 | """ | |
8 | ||
a628daa4 FZ |
9 | # Copyright (C) 2015 Red Hat Inc. |
10 | # | |
11 | # Authors: | |
12 | # Fam Zheng <famz@redhat.com> | |
13 | # | |
14 | # This work is licensed under the terms of the GNU GPL, version 2. See | |
15 | # the COPYING file in the top-level directory. | |
16 | # | |
17 | # Based on qmp.py. | |
18 | # | |
19 | ||
66613974 | 20 | import os |
932ca4bb | 21 | import socket |
aad3f3bb JS |
22 | from typing import ( |
23 | List, | |
24 | Optional, | |
25 | Sequence, | |
26 | TextIO, | |
d3967378 | 27 | Tuple, |
aad3f3bb | 28 | ) |
8f8fd9ed | 29 | |
37094b6d | 30 | from qemu.qmp import SocketAddrT |
beb6b57b | 31 | |
abf0bf99 | 32 | from .machine import QEMUMachine |
a628daa4 | 33 | |
4d934297 | 34 | |
9b8ccd6d JS |
35 | class QEMUQtestProtocol: |
36 | """ | |
37 | QEMUQtestProtocol implements a connection to a qtest socket. | |
a628daa4 | 38 | |
9b8ccd6d JS |
39 | :param address: QEMU address, can be either a unix socket path (string) |
40 | or a tuple in the form ( address, port ) for a TCP | |
41 | connection | |
d3967378 JS |
42 | :param sock: An existing socket can be provided as an alternative to |
43 | an address. One of address or sock must be provided. | |
44 | :param server: server mode, listens on the socket. Only meaningful | |
45 | in conjunction with an address and not an existing | |
46 | socket. | |
47 | ||
9b8ccd6d JS |
48 | :raise socket.error: on socket connection errors |
49 | ||
50 | .. note:: | |
af76484e | 51 | No connection is established by __init__(), this is done |
9b8ccd6d JS |
52 | by the connect() or accept() methods. |
53 | """ | |
d3967378 JS |
54 | def __init__(self, |
55 | address: Optional[SocketAddrT] = None, | |
56 | sock: Optional[socket.socket] = None, | |
f12a282f | 57 | server: bool = False): |
d3967378 JS |
58 | if address is None and sock is None: |
59 | raise ValueError("Either 'address' or 'sock' must be specified") | |
60 | if address is not None and sock is not None: | |
61 | raise ValueError( | |
62 | "Either 'address' or 'sock' must be specified, but not both") | |
63 | if sock is not None and server: | |
64 | raise ValueError("server=True is meaningless when passing socket") | |
65 | ||
a628daa4 | 66 | self._address = address |
d3967378 | 67 | self._sock = sock or self._get_sock() |
0add048f | 68 | self._sockfile: Optional[TextIO] = None |
d3967378 | 69 | |
a628daa4 | 70 | if server: |
d3967378 | 71 | assert self._address is not None |
a628daa4 FZ |
72 | self._sock.bind(self._address) |
73 | self._sock.listen(1) | |
74 | ||
f12a282f | 75 | def _get_sock(self) -> socket.socket: |
d3967378 | 76 | assert self._address is not None |
a628daa4 FZ |
77 | if isinstance(self._address, tuple): |
78 | family = socket.AF_INET | |
79 | else: | |
80 | family = socket.AF_UNIX | |
81 | return socket.socket(family, socket.SOCK_STREAM) | |
82 | ||
f12a282f | 83 | def connect(self) -> None: |
a628daa4 FZ |
84 | """ |
85 | Connect to the qtest socket. | |
86 | ||
87 | @raise socket.error on socket connection errors | |
88 | """ | |
d3967378 JS |
89 | if self._address is not None: |
90 | self._sock.connect(self._address) | |
0add048f | 91 | self._sockfile = self._sock.makefile(mode='r') |
a628daa4 | 92 | |
f12a282f | 93 | def accept(self) -> None: |
a628daa4 FZ |
94 | """ |
95 | Await connection from QEMU. | |
96 | ||
97 | @raise socket.error on socket connection errors | |
98 | """ | |
99 | self._sock, _ = self._sock.accept() | |
0add048f | 100 | self._sockfile = self._sock.makefile(mode='r') |
a628daa4 | 101 | |
f12a282f | 102 | def cmd(self, qtest_cmd: str) -> str: |
a628daa4 FZ |
103 | """ |
104 | Send a qtest command on the wire. | |
105 | ||
106 | @param qtest_cmd: qtest command text to be sent | |
107 | """ | |
0add048f | 108 | assert self._sockfile is not None |
8eb5e674 | 109 | self._sock.sendall((qtest_cmd + "\n").encode('utf-8')) |
a5df73ba AG |
110 | resp = self._sockfile.readline() |
111 | return resp | |
a628daa4 | 112 | |
f12a282f JS |
113 | def close(self) -> None: |
114 | """ | |
115 | Close this socket. | |
116 | """ | |
a628daa4 | 117 | self._sock.close() |
0add048f JS |
118 | if self._sockfile: |
119 | self._sockfile.close() | |
120 | self._sockfile = None | |
a628daa4 | 121 | |
f12a282f | 122 | def settimeout(self, timeout: Optional[float]) -> None: |
9b8ccd6d | 123 | """Set a timeout, in seconds.""" |
a628daa4 | 124 | self._sock.settimeout(timeout) |
66613974 DB |
125 | |
126 | ||
8f8fd9ed | 127 | class QEMUQtestMachine(QEMUMachine): |
9b8ccd6d JS |
128 | """ |
129 | A QEMU VM, with a qtest socket available. | |
130 | """ | |
66613974 | 131 | |
aad3f3bb JS |
132 | def __init__(self, |
133 | binary: str, | |
134 | args: Sequence[str] = (), | |
804f7695 | 135 | wrapper: Sequence[str] = (), |
aad3f3bb | 136 | name: Optional[str] = None, |
2ca6e26c | 137 | base_temp_dir: str = "/var/tmp", |
e2f948a8 EGE |
138 | sock_dir: Optional[str] = None, |
139 | qmp_timer: Optional[float] = None): | |
82e6517d JS |
140 | # pylint: disable=too-many-arguments |
141 | ||
4c44b4a4 DB |
142 | if name is None: |
143 | name = "qemu-%d" % os.getpid() | |
32558ce7 | 144 | if sock_dir is None: |
2ca6e26c | 145 | sock_dir = base_temp_dir |
804f7695 EGE |
146 | super().__init__(binary, args, wrapper=wrapper, name=name, |
147 | base_temp_dir=base_temp_dir, | |
e2f948a8 | 148 | sock_dir=sock_dir, qmp_timer=qmp_timer) |
f12a282f | 149 | self._qtest: Optional[QEMUQtestProtocol] = None |
d3967378 JS |
150 | self._qtest_sock_pair: Optional[ |
151 | Tuple[socket.socket, socket.socket]] = None | |
66613974 | 152 | |
652809df JS |
153 | @property |
154 | def _base_args(self) -> List[str]: | |
155 | args = super()._base_args | |
d3967378 JS |
156 | assert self._qtest_sock_pair is not None |
157 | fd = self._qtest_sock_pair[0].fileno() | |
aad3f3bb | 158 | args.extend([ |
d3967378 JS |
159 | '-chardev', f"socket,id=qtest,fd={fd}", |
160 | '-qtest', 'chardev:qtest', | |
aad3f3bb JS |
161 | '-accel', 'qtest' |
162 | ]) | |
66613974 DB |
163 | return args |
164 | ||
f12a282f | 165 | def _pre_launch(self) -> None: |
d3967378 JS |
166 | self._qtest_sock_pair = socket.socketpair() |
167 | os.set_inheritable(self._qtest_sock_pair[0].fileno(), True) | |
3797dbcb | 168 | super()._pre_launch() |
d3967378 | 169 | self._qtest = QEMUQtestProtocol(sock=self._qtest_sock_pair[1]) |
66613974 | 170 | |
c95dddce JS |
171 | def _post_launch(self) -> None: |
172 | assert self._qtest is not None | |
3797dbcb | 173 | super()._post_launch() |
d3967378 JS |
174 | if self._qtest_sock_pair: |
175 | self._qtest_sock_pair[0].close() | |
176 | self._qtest.connect() | |
66613974 | 177 | |
f12a282f | 178 | def _post_shutdown(self) -> None: |
d3967378 JS |
179 | if self._qtest_sock_pair: |
180 | self._qtest_sock_pair[0].close() | |
181 | self._qtest_sock_pair[1].close() | |
182 | self._qtest_sock_pair = None | |
3797dbcb | 183 | super()._post_shutdown() |
66613974 | 184 | |
c95dddce JS |
185 | def qtest(self, cmd: str) -> str: |
186 | """ | |
187 | Send a qtest command to the guest. | |
188 | ||
189 | :param cmd: qtest command to send | |
190 | :return: qtest server response | |
191 | """ | |
192 | if self._qtest is None: | |
193 | raise RuntimeError("qtest socket not available") | |
66613974 | 194 | return self._qtest.cmd(cmd) |