]>
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 <[email protected]> | |
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 | |
16 | import string | |
17 | import os | |
18 | import sys | |
19 | import subprocess | |
20 | import qmp.qmp | |
21 | ||
22 | ||
23 | class QEMUMachine(object): | |
d792bc38 SH |
24 | '''A QEMU VM |
25 | ||
26 | Use this object as a context manager to ensure the QEMU process terminates:: | |
27 | ||
28 | with VM(binary) as vm: | |
29 | ... | |
30 | # vm is guaranteed to be shut down here | |
31 | ''' | |
66613974 DB |
32 | |
33 | def __init__(self, binary, args=[], wrapper=[], name=None, test_dir="/var/tmp", | |
4c44b4a4 | 34 | monitor_address=None, socket_scm_helper=None, debug=False): |
66613974 DB |
35 | if name is None: |
36 | name = "qemu-%d" % os.getpid() | |
37 | if monitor_address is None: | |
38 | monitor_address = os.path.join(test_dir, name + "-monitor.sock") | |
39 | self._monitor_address = monitor_address | |
40 | self._qemu_log_path = os.path.join(test_dir, name + ".log") | |
41 | self._popen = None | |
42 | self._binary = binary | |
4c44b4a4 | 43 | self._args = list(args) # Force copy args in case we modify them |
66613974 DB |
44 | self._wrapper = wrapper |
45 | self._events = [] | |
46 | self._iolog = None | |
4c44b4a4 | 47 | self._socket_scm_helper = socket_scm_helper |
66613974 DB |
48 | self._debug = debug |
49 | ||
d792bc38 SH |
50 | def __enter__(self): |
51 | return self | |
52 | ||
53 | def __exit__(self, exc_type, exc_val, exc_tb): | |
54 | self.shutdown() | |
55 | return False | |
56 | ||
66613974 DB |
57 | # This can be used to add an unused monitor instance. |
58 | def add_monitor_telnet(self, ip, port): | |
59 | args = 'tcp:%s:%d,server,nowait,telnet' % (ip, port) | |
60 | self._args.append('-monitor') | |
61 | self._args.append(args) | |
62 | ||
63 | def add_fd(self, fd, fdset, opaque, opts=''): | |
64 | '''Pass a file descriptor to the VM''' | |
65 | options = ['fd=%d' % fd, | |
66 | 'set=%d' % fdset, | |
67 | 'opaque=%s' % opaque] | |
68 | if opts: | |
69 | options.append(opts) | |
70 | ||
71 | self._args.append('-add-fd') | |
72 | self._args.append(','.join(options)) | |
73 | return self | |
74 | ||
75 | def send_fd_scm(self, fd_file_path): | |
76 | # In iotest.py, the qmp should always use unix socket. | |
77 | assert self._qmp.is_scm_available() | |
4c44b4a4 DB |
78 | if self._socket_scm_helper is None: |
79 | print >>sys.stderr, "No path to socket_scm_helper set" | |
66613974 | 80 | return -1 |
4c44b4a4 DB |
81 | if os.path.exists(self._socket_scm_helper) == False: |
82 | print >>sys.stderr, "%s does not exist" % self._socket_scm_helper | |
83 | return -1 | |
84 | fd_param = ["%s" % self._socket_scm_helper, | |
66613974 DB |
85 | "%d" % self._qmp.get_sock_fd(), |
86 | "%s" % fd_file_path] | |
87 | devnull = open('/dev/null', 'rb') | |
88 | p = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, | |
89 | stderr=sys.stderr) | |
90 | return p.wait() | |
91 | ||
92 | @staticmethod | |
93 | def _remove_if_exists(path): | |
94 | '''Remove file object at path if it exists''' | |
95 | try: | |
96 | os.remove(path) | |
97 | except OSError as exception: | |
98 | if exception.errno == errno.ENOENT: | |
99 | return | |
100 | raise | |
101 | ||
37bbcd57 EH |
102 | def is_running(self): |
103 | return self._popen and (self._popen.returncode is None) | |
104 | ||
b2b8d986 EH |
105 | def exitcode(self): |
106 | if self._popen is None: | |
107 | return None | |
108 | return self._popen.returncode | |
109 | ||
66613974 | 110 | def get_pid(self): |
37bbcd57 | 111 | if not self.is_running(): |
66613974 DB |
112 | return None |
113 | return self._popen.pid | |
114 | ||
115 | def _load_io_log(self): | |
116 | with open(self._qemu_log_path, "r") as fh: | |
117 | self._iolog = fh.read() | |
118 | ||
119 | def _base_args(self): | |
120 | if isinstance(self._monitor_address, tuple): | |
121 | moncdev = "socket,id=mon,host=%s,port=%s" % ( | |
122 | self._monitor_address[0], | |
123 | self._monitor_address[1]) | |
124 | else: | |
125 | moncdev = 'socket,id=mon,path=%s' % self._monitor_address | |
126 | return ['-chardev', moncdev, | |
127 | '-mon', 'chardev=mon,mode=control', | |
128 | '-display', 'none', '-vga', 'none'] | |
129 | ||
130 | def _pre_launch(self): | |
131 | self._qmp = qmp.qmp.QEMUMonitorProtocol(self._monitor_address, server=True, | |
132 | debug=self._debug) | |
133 | ||
134 | def _post_launch(self): | |
135 | self._qmp.accept() | |
136 | ||
137 | def _post_shutdown(self): | |
138 | if not isinstance(self._monitor_address, tuple): | |
139 | self._remove_if_exists(self._monitor_address) | |
140 | self._remove_if_exists(self._qemu_log_path) | |
141 | ||
142 | def launch(self): | |
143 | '''Launch the VM and establish a QMP connection''' | |
144 | devnull = open('/dev/null', 'rb') | |
145 | qemulog = open(self._qemu_log_path, 'wb') | |
146 | try: | |
147 | self._pre_launch() | |
148 | args = self._wrapper + [self._binary] + self._base_args() + self._args | |
149 | self._popen = subprocess.Popen(args, stdin=devnull, stdout=qemulog, | |
150 | stderr=subprocess.STDOUT, shell=False) | |
151 | self._post_launch() | |
152 | except: | |
37bbcd57 | 153 | if self.is_running(): |
66613974 | 154 | self._popen.kill() |
37bbcd57 | 155 | self._popen.wait() |
66613974 DB |
156 | self._load_io_log() |
157 | self._post_shutdown() | |
66613974 DB |
158 | raise |
159 | ||
160 | def shutdown(self): | |
161 | '''Terminate the VM and clean up''' | |
37bbcd57 | 162 | if self.is_running(): |
66613974 DB |
163 | try: |
164 | self._qmp.cmd('quit') | |
165 | self._qmp.close() | |
166 | except: | |
167 | self._popen.kill() | |
168 | ||
169 | exitcode = self._popen.wait() | |
170 | if exitcode < 0: | |
171 | sys.stderr.write('qemu received signal %i: %s\n' % (-exitcode, ' '.join(self._args))) | |
172 | self._load_io_log() | |
173 | self._post_shutdown() | |
66613974 DB |
174 | |
175 | underscore_to_dash = string.maketrans('_', '-') | |
176 | def qmp(self, cmd, conv_keys=True, **args): | |
177 | '''Invoke a QMP command and return the result dict''' | |
178 | qmp_args = dict() | |
179 | for k in args.keys(): | |
180 | if conv_keys: | |
181 | qmp_args[k.translate(self.underscore_to_dash)] = args[k] | |
182 | else: | |
183 | qmp_args[k] = args[k] | |
184 | ||
185 | return self._qmp.cmd(cmd, args=qmp_args) | |
186 | ||
187 | def command(self, cmd, conv_keys=True, **args): | |
188 | reply = self.qmp(cmd, conv_keys, **args) | |
189 | if reply is None: | |
190 | raise Exception("Monitor is closed") | |
191 | if "error" in reply: | |
192 | raise Exception(reply["error"]["desc"]) | |
193 | return reply["return"] | |
194 | ||
195 | def get_qmp_event(self, wait=False): | |
196 | '''Poll for one queued QMP events and return it''' | |
197 | if len(self._events) > 0: | |
198 | return self._events.pop(0) | |
199 | return self._qmp.pull_event(wait=wait) | |
200 | ||
201 | def get_qmp_events(self, wait=False): | |
202 | '''Poll for queued QMP events and return a list of dicts''' | |
203 | events = self._qmp.get_events(wait=wait) | |
204 | events.extend(self._events) | |
205 | del self._events[:] | |
206 | self._qmp.clear_events() | |
207 | return events | |
208 | ||
209 | def event_wait(self, name, timeout=60.0, match=None): | |
4c44b4a4 DB |
210 | # Test if 'match' is a recursive subset of 'event' |
211 | def event_match(event, match=None): | |
212 | if match is None: | |
213 | return True | |
214 | ||
215 | for key in match: | |
216 | if key in event: | |
217 | if isinstance(event[key], dict): | |
218 | if not event_match(event[key], match[key]): | |
219 | return False | |
220 | elif event[key] != match[key]: | |
221 | return False | |
222 | else: | |
223 | return False | |
224 | ||
225 | return True | |
226 | ||
66613974 DB |
227 | # Search cached events |
228 | for event in self._events: | |
229 | if (event['event'] == name) and event_match(event, match): | |
230 | self._events.remove(event) | |
231 | return event | |
232 | ||
233 | # Poll for new events | |
234 | while True: | |
235 | event = self._qmp.pull_event(wait=timeout) | |
236 | if (event['event'] == name) and event_match(event, match): | |
237 | return event | |
238 | self._events.append(event) | |
239 | ||
240 | return None | |
241 | ||
242 | def get_log(self): | |
243 | return self._iolog |