]>
Commit | Line | Data |
---|---|---|
66613974 DB |
1 | # QEMU library |
2 | # | |
3 | # Copyright (C) 2015-2016 Red Hat Inc. | |
4 | # Copyright (C) 2012 IBM Corp. | |
5 | # | |
6 | # Authors: | |
7 | # Fam Zheng <famz@redhat.com> | |
8 | # | |
9 | # This work is licensed under the terms of the GNU GPL, version 2. See | |
10 | # the COPYING file in the top-level directory. | |
11 | # | |
12 | # Based on qmp.py. | |
13 | # | |
14 | ||
15 | import errno | |
66613974 DB |
16 | import os |
17 | import sys | |
18 | import subprocess | |
19 | import qmp.qmp | |
20 | ||
21 | ||
22 | class QEMUMachine(object): | |
d792bc38 SH |
23 | '''A QEMU VM |
24 | ||
25 | Use this object as a context manager to ensure the QEMU process terminates:: | |
26 | ||
27 | with VM(binary) as vm: | |
28 | ... | |
29 | # vm is guaranteed to be shut down here | |
30 | ''' | |
66613974 | 31 | |
2782fc51 | 32 | def __init__(self, binary, args=None, wrapper=None, name=None, |
2d853c70 LD |
33 | test_dir="/var/tmp", monitor_address=None, |
34 | socket_scm_helper=None, debug=False): | |
35 | ''' | |
36 | Initialize a QEMUMachine | |
37 | ||
38 | @param binary: path to the qemu binary | |
39 | @param args: list of extra arguments | |
40 | @param wrapper: list of arguments used as prefix to qemu binary | |
41 | @param name: prefix for socket and log file names (default: qemu-PID) | |
42 | @param test_dir: where to create socket and log file | |
43 | @param monitor_address: address for QMP monitor | |
44 | @param socket_scm_helper: helper program, required for send_fd_scm()" | |
45 | @param debug: enable debug mode | |
46 | @note: Qemu process is not started until launch() is used. | |
47 | ''' | |
2782fc51 LD |
48 | if args is None: |
49 | args = [] | |
50 | if wrapper is None: | |
51 | wrapper = [] | |
66613974 DB |
52 | if name is None: |
53 | name = "qemu-%d" % os.getpid() | |
54 | if monitor_address is None: | |
55 | monitor_address = os.path.join(test_dir, name + "-monitor.sock") | |
56 | self._monitor_address = monitor_address | |
57 | self._qemu_log_path = os.path.join(test_dir, name + ".log") | |
58 | self._popen = None | |
59 | self._binary = binary | |
2d853c70 | 60 | self._args = list(args) # Force copy args in case we modify them |
66613974 DB |
61 | self._wrapper = wrapper |
62 | self._events = [] | |
63 | self._iolog = None | |
4c44b4a4 | 64 | self._socket_scm_helper = socket_scm_helper |
66613974 | 65 | self._debug = debug |
2d853c70 | 66 | self._qmp = None |
66613974 | 67 | |
d792bc38 SH |
68 | def __enter__(self): |
69 | return self | |
70 | ||
71 | def __exit__(self, exc_type, exc_val, exc_tb): | |
72 | self.shutdown() | |
73 | return False | |
74 | ||
66613974 DB |
75 | # This can be used to add an unused monitor instance. |
76 | def add_monitor_telnet(self, ip, port): | |
77 | args = 'tcp:%s:%d,server,nowait,telnet' % (ip, port) | |
78 | self._args.append('-monitor') | |
79 | self._args.append(args) | |
80 | ||
81 | def add_fd(self, fd, fdset, opaque, opts=''): | |
82 | '''Pass a file descriptor to the VM''' | |
83 | options = ['fd=%d' % fd, | |
84 | 'set=%d' % fdset, | |
85 | 'opaque=%s' % opaque] | |
86 | if opts: | |
87 | options.append(opts) | |
88 | ||
89 | self._args.append('-add-fd') | |
90 | self._args.append(','.join(options)) | |
91 | return self | |
92 | ||
93 | def send_fd_scm(self, fd_file_path): | |
94 | # In iotest.py, the qmp should always use unix socket. | |
95 | assert self._qmp.is_scm_available() | |
4c44b4a4 DB |
96 | if self._socket_scm_helper is None: |
97 | print >>sys.stderr, "No path to socket_scm_helper set" | |
66613974 | 98 | return -1 |
2d853c70 | 99 | if not os.path.exists(self._socket_scm_helper): |
4c44b4a4 DB |
100 | print >>sys.stderr, "%s does not exist" % self._socket_scm_helper |
101 | return -1 | |
102 | fd_param = ["%s" % self._socket_scm_helper, | |
66613974 DB |
103 | "%d" % self._qmp.get_sock_fd(), |
104 | "%s" % fd_file_path] | |
105 | devnull = open('/dev/null', 'rb') | |
2d853c70 LD |
106 | proc = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, |
107 | stderr=sys.stderr) | |
108 | return proc.wait() | |
66613974 DB |
109 | |
110 | @staticmethod | |
111 | def _remove_if_exists(path): | |
112 | '''Remove file object at path if it exists''' | |
113 | try: | |
114 | os.remove(path) | |
115 | except OSError as exception: | |
116 | if exception.errno == errno.ENOENT: | |
117 | return | |
118 | raise | |
119 | ||
37bbcd57 EH |
120 | def is_running(self): |
121 | return self._popen and (self._popen.returncode is None) | |
122 | ||
b2b8d986 EH |
123 | def exitcode(self): |
124 | if self._popen is None: | |
125 | return None | |
126 | return self._popen.returncode | |
127 | ||
66613974 | 128 | def get_pid(self): |
37bbcd57 | 129 | if not self.is_running(): |
66613974 DB |
130 | return None |
131 | return self._popen.pid | |
132 | ||
133 | def _load_io_log(self): | |
2d853c70 LD |
134 | with open(self._qemu_log_path, "r") as iolog: |
135 | self._iolog = iolog.read() | |
66613974 DB |
136 | |
137 | def _base_args(self): | |
138 | if isinstance(self._monitor_address, tuple): | |
139 | moncdev = "socket,id=mon,host=%s,port=%s" % ( | |
140 | self._monitor_address[0], | |
141 | self._monitor_address[1]) | |
142 | else: | |
143 | moncdev = 'socket,id=mon,path=%s' % self._monitor_address | |
144 | return ['-chardev', moncdev, | |
145 | '-mon', 'chardev=mon,mode=control', | |
146 | '-display', 'none', '-vga', 'none'] | |
147 | ||
148 | def _pre_launch(self): | |
2d853c70 LD |
149 | self._qmp = qmp.qmp.QEMUMonitorProtocol(self._monitor_address, |
150 | server=True, | |
66613974 DB |
151 | debug=self._debug) |
152 | ||
153 | def _post_launch(self): | |
154 | self._qmp.accept() | |
155 | ||
156 | def _post_shutdown(self): | |
157 | if not isinstance(self._monitor_address, tuple): | |
158 | self._remove_if_exists(self._monitor_address) | |
159 | self._remove_if_exists(self._qemu_log_path) | |
160 | ||
161 | def launch(self): | |
162 | '''Launch the VM and establish a QMP connection''' | |
163 | devnull = open('/dev/null', 'rb') | |
164 | qemulog = open(self._qemu_log_path, 'wb') | |
165 | try: | |
166 | self._pre_launch() | |
2d853c70 LD |
167 | args = (self._wrapper + [self._binary] + self._base_args() + |
168 | self._args) | |
66613974 | 169 | self._popen = subprocess.Popen(args, stdin=devnull, stdout=qemulog, |
2d853c70 LD |
170 | stderr=subprocess.STDOUT, |
171 | shell=False) | |
66613974 DB |
172 | self._post_launch() |
173 | except: | |
37bbcd57 | 174 | if self.is_running(): |
66613974 | 175 | self._popen.kill() |
37bbcd57 | 176 | self._popen.wait() |
66613974 DB |
177 | self._load_io_log() |
178 | self._post_shutdown() | |
66613974 DB |
179 | raise |
180 | ||
181 | def shutdown(self): | |
182 | '''Terminate the VM and clean up''' | |
37bbcd57 | 183 | if self.is_running(): |
66613974 DB |
184 | try: |
185 | self._qmp.cmd('quit') | |
186 | self._qmp.close() | |
187 | except: | |
188 | self._popen.kill() | |
189 | ||
190 | exitcode = self._popen.wait() | |
191 | if exitcode < 0: | |
2d853c70 LD |
192 | sys.stderr.write('qemu received signal %i: %s\n' |
193 | % (-exitcode, ' '.join(self._args))) | |
66613974 DB |
194 | self._load_io_log() |
195 | self._post_shutdown() | |
66613974 | 196 | |
66613974 | 197 | def qmp(self, cmd, conv_keys=True, **args): |
2d853c70 | 198 | '''Invoke a QMP command and return the response dict''' |
66613974 | 199 | qmp_args = dict() |
7f33ca78 | 200 | for key, value in args.iteritems(): |
66613974 | 201 | if conv_keys: |
41f714b1 | 202 | qmp_args[key.replace('_', '-')] = value |
66613974 | 203 | else: |
7f33ca78 | 204 | qmp_args[key] = value |
66613974 DB |
205 | |
206 | return self._qmp.cmd(cmd, args=qmp_args) | |
207 | ||
208 | def command(self, cmd, conv_keys=True, **args): | |
2d853c70 LD |
209 | ''' |
210 | Invoke a QMP command. | |
211 | On success return the response dict. | |
212 | On failure raise an exception. | |
213 | ''' | |
66613974 DB |
214 | reply = self.qmp(cmd, conv_keys, **args) |
215 | if reply is None: | |
216 | raise Exception("Monitor is closed") | |
217 | if "error" in reply: | |
218 | raise Exception(reply["error"]["desc"]) | |
219 | return reply["return"] | |
220 | ||
221 | def get_qmp_event(self, wait=False): | |
222 | '''Poll for one queued QMP events and return it''' | |
223 | if len(self._events) > 0: | |
224 | return self._events.pop(0) | |
225 | return self._qmp.pull_event(wait=wait) | |
226 | ||
227 | def get_qmp_events(self, wait=False): | |
228 | '''Poll for queued QMP events and return a list of dicts''' | |
229 | events = self._qmp.get_events(wait=wait) | |
230 | events.extend(self._events) | |
231 | del self._events[:] | |
232 | self._qmp.clear_events() | |
233 | return events | |
234 | ||
235 | def event_wait(self, name, timeout=60.0, match=None): | |
2d853c70 LD |
236 | ''' |
237 | Wait for specified timeout on named event in QMP; optionally filter | |
238 | results by match. | |
239 | ||
240 | The 'match' is checked to be a recursive subset of the 'event'; skips | |
241 | branch processing on match's value None | |
242 | {"foo": {"bar": 1}} matches {"foo": None} | |
243 | {"foo": {"bar": 1}} does not matches {"foo": {"baz": None}} | |
244 | ''' | |
4c44b4a4 DB |
245 | def event_match(event, match=None): |
246 | if match is None: | |
247 | return True | |
248 | ||
249 | for key in match: | |
250 | if key in event: | |
251 | if isinstance(event[key], dict): | |
252 | if not event_match(event[key], match[key]): | |
253 | return False | |
254 | elif event[key] != match[key]: | |
255 | return False | |
256 | else: | |
257 | return False | |
258 | ||
259 | return True | |
260 | ||
66613974 DB |
261 | # Search cached events |
262 | for event in self._events: | |
263 | if (event['event'] == name) and event_match(event, match): | |
264 | self._events.remove(event) | |
265 | return event | |
266 | ||
267 | # Poll for new events | |
268 | while True: | |
269 | event = self._qmp.pull_event(wait=timeout) | |
270 | if (event['event'] == name) and event_match(event, match): | |
271 | return event | |
272 | self._events.append(event) | |
273 | ||
274 | return None | |
275 | ||
276 | def get_log(self): | |
2d853c70 LD |
277 | ''' |
278 | After self.shutdown or failed qemu execution, this returns the output | |
279 | of the qemu process. | |
280 | ''' | |
66613974 | 281 | return self._iolog |