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