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 15import errno 16import logging 17import os 18import subprocess 19import qmp.qmp 20import re 21import shutil 22import socket 23import tempfile 24 25 26LOG = logging.getLogger(__name__) 27 28 29#: Maps machine types to the preferred console device types 30CONSOLE_DEV_TYPES = { 31 r'^clipper$': 'isa-serial', 32 r'^malta': 'isa-serial', 33 r'^(pc.*|q35.*|isapc)$': 'isa-serial', 34 r'^(40p|powernv|prep)$': 'isa-serial', 35 r'^pseries.*': 'spapr-vty', 36 r'^s390-ccw-virtio.*': 'sclpconsole', 37 } 38 39 40class QEMUMachineError(Exception): 41 """ 42 Exception called when an error in QEMUMachine happens. 43 """ 44 45 46class QEMUMachineAddDeviceError(QEMUMachineError): 47 """ 48 Exception raised when a request to add a device can not be fulfilled 49 50 The failures are caused by limitations, lack of information or conflicting 51 requests on the QEMUMachine methods. This exception does not represent 52 failures reported by the QEMU binary itself. 53 """ 54 55class MonitorResponseError(qmp.qmp.QMPError): 56 ''' 57 Represents erroneous QMP monitor reply 58 ''' 59 def __init__(self, reply): 60 try: 61 desc = reply["error"]["desc"] 62 except KeyError: 63 desc = reply 64 super(MonitorResponseError, self).__init__(desc) 65 self.reply = reply 66 67 68class QEMUMachine(object): 69 '''A QEMU VM 70 71 Use this object as a context manager to ensure the QEMU process terminates:: 72 73 with VM(binary) as vm: 74 ... 75 # vm is guaranteed to be shut down here 76 ''' 77 78 def __init__(self, binary, args=None, wrapper=None, name=None, 79 test_dir="/var/tmp", monitor_address=None, 80 socket_scm_helper=None): 81 ''' 82 Initialize a QEMUMachine 83 84 @param binary: path to the qemu binary 85 @param args: list of extra arguments 86 @param wrapper: list of arguments used as prefix to qemu binary 87 @param name: prefix for socket and log file names (default: qemu-PID) 88 @param test_dir: where to create socket and log file 89 @param monitor_address: address for QMP monitor 90 @param socket_scm_helper: helper program, required for send_fd_scm()" 91 @note: Qemu process is not started until launch() is used. 92 ''' 93 if args is None: 94 args = [] 95 if wrapper is None: 96 wrapper = [] 97 if name is None: 98 name = "qemu-%d" % os.getpid() 99 self._name = name 100 self._monitor_address = monitor_address 101 self._vm_monitor = None 102 self._qemu_log_path = None 103 self._qemu_log_file = None 104 self._popen = None 105 self._binary = binary 106 self._args = list(args) # Force copy args in case we modify them 107 self._wrapper = wrapper 108 self._events = [] 109 self._iolog = None 110 self._socket_scm_helper = socket_scm_helper 111 self._qmp = None 112 self._qemu_full_args = None 113 self._test_dir = test_dir 114 self._temp_dir = None 115 self._launched = False 116 self._machine = None 117 self._console_device_type = None 118 self._console_address = None 119 self._console_socket = None 120 121 # just in case logging wasn't configured by the main script: 122 logging.basicConfig() 123 124 def __enter__(self): 125 return self 126 127 def __exit__(self, exc_type, exc_val, exc_tb): 128 self.shutdown() 129 return False 130 131 # This can be used to add an unused monitor instance. 132 def add_monitor_telnet(self, ip, port): 133 args = 'tcp:%s:%d,server,nowait,telnet' % (ip, port) 134 self._args.append('-monitor') 135 self._args.append(args) 136 137 def add_fd(self, fd, fdset, opaque, opts=''): 138 '''Pass a file descriptor to the VM''' 139 options = ['fd=%d' % fd, 140 'set=%d' % fdset, 141 'opaque=%s' % opaque] 142 if opts: 143 options.append(opts) 144 145 self._args.append('-add-fd') 146 self._args.append(','.join(options)) 147 return self 148 149 def send_fd_scm(self, fd_file_path): 150 # In iotest.py, the qmp should always use unix socket. 151 assert self._qmp.is_scm_available() 152 if self._socket_scm_helper is None: 153 raise QEMUMachineError("No path to socket_scm_helper set") 154 if not os.path.exists(self._socket_scm_helper): 155 raise QEMUMachineError("%s does not exist" % 156 self._socket_scm_helper) 157 fd_param = ["%s" % self._socket_scm_helper, 158 "%d" % self._qmp.get_sock_fd(), 159 "%s" % fd_file_path] 160 devnull = open(os.path.devnull, 'rb') 161 proc = subprocess.Popen(fd_param, stdin=devnull, stdout=subprocess.PIPE, 162 stderr=subprocess.STDOUT) 163 output = proc.communicate()[0] 164 if output: 165 LOG.debug(output) 166 167 return proc.returncode 168 169 @staticmethod 170 def _remove_if_exists(path): 171 '''Remove file object at path if it exists''' 172 try: 173 os.remove(path) 174 except OSError as exception: 175 if exception.errno == errno.ENOENT: 176 return 177 raise 178 179 def is_running(self): 180 return self._popen is not None and self._popen.poll() is None 181 182 def exitcode(self): 183 if self._popen is None: 184 return None 185 return self._popen.poll() 186 187 def get_pid(self): 188 if not self.is_running(): 189 return None 190 return self._popen.pid 191 192 def _load_io_log(self): 193 if self._qemu_log_path is not None: 194 with open(self._qemu_log_path, "r") as iolog: 195 self._iolog = iolog.read() 196 197 def _base_args(self): 198 if isinstance(self._monitor_address, tuple): 199 moncdev = "socket,id=mon,host=%s,port=%s" % ( 200 self._monitor_address[0], 201 self._monitor_address[1]) 202 else: 203 moncdev = 'socket,id=mon,path=%s' % self._vm_monitor 204 args = ['-chardev', moncdev, 205 '-mon', 'chardev=mon,mode=control', 206 '-display', 'none', '-vga', 'none'] 207 if self._machine is not None: 208 args.extend(['-machine', self._machine]) 209 if self._console_device_type is not None: 210 self._console_address = os.path.join(self._temp_dir, 211 self._name + "-console.sock") 212 chardev = ('socket,id=console,path=%s,server,nowait' % 213 self._console_address) 214 device = '%s,chardev=console' % self._console_device_type 215 args.extend(['-chardev', chardev, '-device', device]) 216 return args 217 218 def _pre_launch(self): 219 self._temp_dir = tempfile.mkdtemp(dir=self._test_dir) 220 if self._monitor_address is not None: 221 self._vm_monitor = self._monitor_address 222 else: 223 self._vm_monitor = os.path.join(self._temp_dir, 224 self._name + "-monitor.sock") 225 self._qemu_log_path = os.path.join(self._temp_dir, self._name + ".log") 226 self._qemu_log_file = open(self._qemu_log_path, 'wb') 227 228 self._qmp = qmp.qmp.QEMUMonitorProtocol(self._vm_monitor, 229 server=True) 230 231 def _post_launch(self): 232 self._qmp.accept() 233 234 def _post_shutdown(self): 235 if self._qemu_log_file is not None: 236 self._qemu_log_file.close() 237 self._qemu_log_file = None 238 239 self._qemu_log_path = None 240 241 if self._console_socket is not None: 242 self._console_socket.close() 243 self._console_socket = None 244 245 if self._temp_dir is not None: 246 shutil.rmtree(self._temp_dir) 247 self._temp_dir = None 248 249 def launch(self): 250 """ 251 Launch the VM and make sure we cleanup and expose the 252 command line/output in case of exception 253 """ 254 255 if self._launched: 256 raise QEMUMachineError('VM already launched') 257 258 self._iolog = None 259 self._qemu_full_args = None 260 try: 261 self._launch() 262 self._launched = True 263 except: 264 self.shutdown() 265 266 LOG.debug('Error launching VM') 267 if self._qemu_full_args: 268 LOG.debug('Command: %r', ' '.join(self._qemu_full_args)) 269 if self._iolog: 270 LOG.debug('Output: %r', self._iolog) 271 raise 272 273 def _launch(self): 274 '''Launch the VM and establish a QMP connection''' 275 devnull = open(os.path.devnull, 'rb') 276 self._pre_launch() 277 self._qemu_full_args = (self._wrapper + [self._binary] + 278 self._base_args() + self._args) 279 self._popen = subprocess.Popen(self._qemu_full_args, 280 stdin=devnull, 281 stdout=self._qemu_log_file, 282 stderr=subprocess.STDOUT, 283 shell=False) 284 self._post_launch() 285 286 def wait(self): 287 '''Wait for the VM to power off''' 288 self._popen.wait() 289 self._qmp.close() 290 self._load_io_log() 291 self._post_shutdown() 292 293 def shutdown(self): 294 '''Terminate the VM and clean up''' 295 if self.is_running(): 296 try: 297 self._qmp.cmd('quit') 298 self._qmp.close() 299 except: 300 self._popen.kill() 301 self._popen.wait() 302 303 self._load_io_log() 304 self._post_shutdown() 305 306 exitcode = self.exitcode() 307 if exitcode is not None and exitcode < 0: 308 msg = 'qemu received signal %i: %s' 309 if self._qemu_full_args: 310 command = ' '.join(self._qemu_full_args) 311 else: 312 command = '' 313 LOG.warn(msg, exitcode, command) 314 315 self._launched = False 316 317 def qmp(self, cmd, conv_keys=True, **args): 318 '''Invoke a QMP command and return the response dict''' 319 qmp_args = dict() 320 for key, value in args.items(): 321 if conv_keys: 322 qmp_args[key.replace('_', '-')] = value 323 else: 324 qmp_args[key] = value 325 326 return self._qmp.cmd(cmd, args=qmp_args) 327 328 def command(self, cmd, conv_keys=True, **args): 329 ''' 330 Invoke a QMP command. 331 On success return the response dict. 332 On failure raise an exception. 333 ''' 334 reply = self.qmp(cmd, conv_keys, **args) 335 if reply is None: 336 raise qmp.qmp.QMPError("Monitor is closed") 337 if "error" in reply: 338 raise MonitorResponseError(reply) 339 return reply["return"] 340 341 def get_qmp_event(self, wait=False): 342 '''Poll for one queued QMP events and return it''' 343 if len(self._events) > 0: 344 return self._events.pop(0) 345 return self._qmp.pull_event(wait=wait) 346 347 def get_qmp_events(self, wait=False): 348 '''Poll for queued QMP events and return a list of dicts''' 349 events = self._qmp.get_events(wait=wait) 350 events.extend(self._events) 351 del self._events[:] 352 self._qmp.clear_events() 353 return events 354 355 def event_wait(self, name, timeout=60.0, match=None): 356 ''' 357 Wait for specified timeout on named event in QMP; optionally filter 358 results by match. 359 360 The 'match' is checked to be a recursive subset of the 'event'; skips 361 branch processing on match's value None 362 {"foo": {"bar": 1}} matches {"foo": None} 363 {"foo": {"bar": 1}} does not matches {"foo": {"baz": None}} 364 ''' 365 def event_match(event, match=None): 366 if match is None: 367 return True 368 369 for key in match: 370 if key in event: 371 if isinstance(event[key], dict): 372 if not event_match(event[key], match[key]): 373 return False 374 elif event[key] != match[key]: 375 return False 376 else: 377 return False 378 379 return True 380 381 # Search cached events 382 for event in self._events: 383 if (event['event'] == name) and event_match(event, match): 384 self._events.remove(event) 385 return event 386 387 # Poll for new events 388 while True: 389 event = self._qmp.pull_event(wait=timeout) 390 if (event['event'] == name) and event_match(event, match): 391 return event 392 self._events.append(event) 393 394 return None 395 396 def get_log(self): 397 ''' 398 After self.shutdown or failed qemu execution, this returns the output 399 of the qemu process. 400 ''' 401 return self._iolog 402 403 def add_args(self, *args): 404 ''' 405 Adds to the list of extra arguments to be given to the QEMU binary 406 ''' 407 self._args.extend(args) 408 409 def set_machine(self, machine_type): 410 ''' 411 Sets the machine type 412 413 If set, the machine type will be added to the base arguments 414 of the resulting QEMU command line. 415 ''' 416 self._machine = machine_type 417 418 def set_console(self, device_type=None): 419 ''' 420 Sets the device type for a console device 421 422 If set, the console device and a backing character device will 423 be added to the base arguments of the resulting QEMU command 424 line. 425 426 This is a convenience method that will either use the provided 427 device type, of if not given, it will used the device type set 428 on CONSOLE_DEV_TYPES. 429 430 The actual setting of command line arguments will be be done at 431 machine launch time, as it depends on the temporary directory 432 to be created. 433 434 @param device_type: the device type, such as "isa-serial" 435 @raises: QEMUMachineAddDeviceError if the device type is not given 436 and can not be determined. 437 ''' 438 if device_type is None: 439 if self._machine is None: 440 raise QEMUMachineAddDeviceError("Can not add a console device:" 441 " QEMU instance without a " 442 "defined machine type") 443 for regex, device in CONSOLE_DEV_TYPES.items(): 444 if re.match(regex, self._machine): 445 device_type = device 446 break 447 if device_type is None: 448 raise QEMUMachineAddDeviceError("Can not add a console device:" 449 " no matching console device " 450 "type definition") 451 self._console_device_type = device_type 452 453 @property 454 def console_socket(self): 455 """ 456 Returns a socket connected to the console 457 """ 458 if self._console_socket is None: 459 self._console_socket = socket.socket(socket.AF_UNIX, 460 socket.SOCK_STREAM) 461 self._console_socket.connect(self._console_address) 462 return self._console_socket 463