qemu/scripts/qemu.py
<<
>>
Prefs
   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