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