From 2d853c70a25a2c4b812eadd304c9e35882eb2e0e Mon Sep 17 00:00:00 2001 From: Lukáš Doktor Date: Fri, 18 Aug 2017 16:26:04 +0200 Subject: qemu.py: Pylint/style fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No actual code changes, just several pylint/style fixes and docstring clarifications. Signed-off-by: Lukáš Doktor Reviewed-by: Stefan Hajnoczi Message-Id: <20170818142613.32394-2-ldoktor@redhat.com> Reviewed-by: Cleber Rosa Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 73 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 18 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index 4d8ee10943..b45e691538 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -30,8 +30,22 @@ class QEMUMachine(object): # vm is guaranteed to be shut down here ''' - def __init__(self, binary, args=[], wrapper=[], name=None, test_dir="/var/tmp", - monitor_address=None, socket_scm_helper=None, debug=False): + def __init__(self, binary, args=[], wrapper=[], name=None, + test_dir="/var/tmp", monitor_address=None, + socket_scm_helper=None, debug=False): + ''' + Initialize a QEMUMachine + + @param binary: path to the qemu binary + @param args: list of extra arguments + @param wrapper: list of arguments used as prefix to qemu binary + @param name: prefix for socket and log file names (default: qemu-PID) + @param test_dir: where to create socket and log file + @param monitor_address: address for QMP monitor + @param socket_scm_helper: helper program, required for send_fd_scm()" + @param debug: enable debug mode + @note: Qemu process is not started until launch() is used. + ''' if name is None: name = "qemu-%d" % os.getpid() if monitor_address is None: @@ -40,12 +54,13 @@ class QEMUMachine(object): self._qemu_log_path = os.path.join(test_dir, name + ".log") self._popen = None self._binary = binary - self._args = list(args) # Force copy args in case we modify them + self._args = list(args) # Force copy args in case we modify them self._wrapper = wrapper self._events = [] self._iolog = None self._socket_scm_helper = socket_scm_helper self._debug = debug + self._qmp = None def __enter__(self): return self @@ -78,16 +93,16 @@ class QEMUMachine(object): if self._socket_scm_helper is None: print >>sys.stderr, "No path to socket_scm_helper set" return -1 - if os.path.exists(self._socket_scm_helper) == False: + if not os.path.exists(self._socket_scm_helper): print >>sys.stderr, "%s does not exist" % self._socket_scm_helper return -1 fd_param = ["%s" % self._socket_scm_helper, "%d" % self._qmp.get_sock_fd(), "%s" % fd_file_path] devnull = open('/dev/null', 'rb') - p = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, - stderr=sys.stderr) - return p.wait() + proc = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, + stderr=sys.stderr) + return proc.wait() @staticmethod def _remove_if_exists(path): @@ -113,8 +128,8 @@ class QEMUMachine(object): return self._popen.pid def _load_io_log(self): - with open(self._qemu_log_path, "r") as fh: - self._iolog = fh.read() + with open(self._qemu_log_path, "r") as iolog: + self._iolog = iolog.read() def _base_args(self): if isinstance(self._monitor_address, tuple): @@ -128,7 +143,8 @@ class QEMUMachine(object): '-display', 'none', '-vga', 'none'] def _pre_launch(self): - self._qmp = qmp.qmp.QEMUMonitorProtocol(self._monitor_address, server=True, + self._qmp = qmp.qmp.QEMUMonitorProtocol(self._monitor_address, + server=True, debug=self._debug) def _post_launch(self): @@ -145,9 +161,11 @@ class QEMUMachine(object): qemulog = open(self._qemu_log_path, 'wb') try: self._pre_launch() - args = self._wrapper + [self._binary] + self._base_args() + self._args + args = (self._wrapper + [self._binary] + self._base_args() + + self._args) self._popen = subprocess.Popen(args, stdin=devnull, stdout=qemulog, - stderr=subprocess.STDOUT, shell=False) + stderr=subprocess.STDOUT, + shell=False) self._post_launch() except: if self.is_running(): @@ -168,23 +186,30 @@ class QEMUMachine(object): exitcode = self._popen.wait() if exitcode < 0: - sys.stderr.write('qemu received signal %i: %s\n' % (-exitcode, ' '.join(self._args))) + sys.stderr.write('qemu received signal %i: %s\n' + % (-exitcode, ' '.join(self._args))) self._load_io_log() self._post_shutdown() underscore_to_dash = string.maketrans('_', '-') + def qmp(self, cmd, conv_keys=True, **args): - '''Invoke a QMP command and return the result dict''' + '''Invoke a QMP command and return the response dict''' qmp_args = dict() - for k in args.keys(): + for key in args.keys(): if conv_keys: - qmp_args[k.translate(self.underscore_to_dash)] = args[k] + qmp_args[key.translate(self.underscore_to_dash)] = args[key] else: - qmp_args[k] = args[k] + qmp_args[key] = args[key] return self._qmp.cmd(cmd, args=qmp_args) def command(self, cmd, conv_keys=True, **args): + ''' + Invoke a QMP command. + On success return the response dict. + On failure raise an exception. + ''' reply = self.qmp(cmd, conv_keys, **args) if reply is None: raise Exception("Monitor is closed") @@ -207,7 +232,15 @@ class QEMUMachine(object): return events def event_wait(self, name, timeout=60.0, match=None): - # Test if 'match' is a recursive subset of 'event' + ''' + Wait for specified timeout on named event in QMP; optionally filter + results by match. + + The 'match' is checked to be a recursive subset of the 'event'; skips + branch processing on match's value None + {"foo": {"bar": 1}} matches {"foo": None} + {"foo": {"bar": 1}} does not matches {"foo": {"baz": None}} + ''' def event_match(event, match=None): if match is None: return True @@ -240,4 +273,8 @@ class QEMUMachine(object): return None def get_log(self): + ''' + After self.shutdown or failed qemu execution, this returns the output + of the qemu process. + ''' return self._iolog -- cgit 1.4.1 From 2782fc517d6720dbec24b4dfa08aa4606c72c76d Mon Sep 17 00:00:00 2001 From: Lukáš Doktor Date: Fri, 18 Aug 2017 16:26:05 +0200 Subject: qemu|qtest: Avoid dangerous arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list object is mutable in python and potentially might modify other object's arguments when used as default argument. Reproducer: >>> vm1 = QEMUMachine("qemu") >>> vm2 = QEMUMachine("qemu") >>> vm1._wrapper.append("foo") >>> print vm2._wrapper ['foo'] In this case the `args` is actually copied so it would be safe to keep it, but it's not a good practice to keep it. The same issue applies in inherited qtest module. Signed-off-by: Lukáš Doktor Reviewed-by: Eduardo Habkost Reviewed-by: John Snow Message-Id: <20170818142613.32394-3-ldoktor@redhat.com> Reviewed-by: Cleber Rosa Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 6 +++++- scripts/qtest.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index b45e691538..afd98a290e 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -30,7 +30,7 @@ class QEMUMachine(object): # vm is guaranteed to be shut down here ''' - def __init__(self, binary, args=[], wrapper=[], name=None, + def __init__(self, binary, args=None, wrapper=None, name=None, test_dir="/var/tmp", monitor_address=None, socket_scm_helper=None, debug=False): ''' @@ -46,6 +46,10 @@ class QEMUMachine(object): @param debug: enable debug mode @note: Qemu process is not started until launch() is used. ''' + if args is None: + args = [] + if wrapper is None: + wrapper = [] if name is None: name = "qemu-%d" % os.getpid() if monitor_address is None: diff --git a/scripts/qtest.py b/scripts/qtest.py index d5aecb5f49..ab183c0635 100644 --- a/scripts/qtest.py +++ b/scripts/qtest.py @@ -79,7 +79,7 @@ class QEMUQtestProtocol(object): class QEMUQtestMachine(qemu.QEMUMachine): '''A QEMU VM''' - def __init__(self, binary, args=[], name=None, test_dir="/var/tmp", + def __init__(self, binary, args=None, name=None, test_dir="/var/tmp", socket_scm_helper=None): if name is None: name = "qemu-%d" % os.getpid() -- cgit 1.4.1 From 7f33ca7878e3414f779a5d89f04c68c0438c3dc7 Mon Sep 17 00:00:00 2001 From: Lukáš Doktor Date: Fri, 18 Aug 2017 16:26:06 +0200 Subject: qemu.py: Use iteritems rather than keys() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let's avoid creating an in-memory list of keys and query for each value and use `iteritems` which is an iterator of key-value pairs. Signed-off-by: Lukáš Doktor Reviewed-by: Eduardo Habkost Reviewed-by: Philippe Mathieu-Daudé Message-Id: <20170818142613.32394-4-ldoktor@redhat.com> Reviewed-by: Cleber Rosa Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index afd98a290e..d8c169b31e 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -200,11 +200,11 @@ class QEMUMachine(object): def qmp(self, cmd, conv_keys=True, **args): '''Invoke a QMP command and return the response dict''' qmp_args = dict() - for key in args.keys(): + for key, value in args.iteritems(): if conv_keys: - qmp_args[key.translate(self.underscore_to_dash)] = args[key] + qmp_args[key.translate(self.underscore_to_dash)] = value else: - qmp_args[key] = args[key] + qmp_args[key] = value return self._qmp.cmd(cmd, args=qmp_args) -- cgit 1.4.1 From 41f714b190ffff7fefb3ad090bc02d089e4c7bda Mon Sep 17 00:00:00 2001 From: Lukáš Doktor Date: Fri, 18 Aug 2017 16:26:07 +0200 Subject: qemu.py: Simplify QMP key-conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The QMP key conversion consist of '_'s to be replaced with '-'s, which can easily be done by a single `str.replace` method which is faster and does not require `string` module import. Signed-off-by: Lukáš Doktor Reviewed-by: Eduardo Habkost Message-Id: <20170818142613.32394-5-ldoktor@redhat.com> Reviewed-by: Cleber Rosa Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index d8c169b31e..bde8da8fe7 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -13,7 +13,6 @@ # import errno -import string import os import sys import subprocess @@ -195,14 +194,12 @@ class QEMUMachine(object): self._load_io_log() self._post_shutdown() - underscore_to_dash = string.maketrans('_', '-') - def qmp(self, cmd, conv_keys=True, **args): '''Invoke a QMP command and return the response dict''' qmp_args = dict() for key, value in args.iteritems(): if conv_keys: - qmp_args[key.translate(self.underscore_to_dash)] = value + qmp_args[key.replace('_', '-')] = value else: qmp_args[key] = value -- cgit 1.4.1 From a004e249f035f8a0c859d1700abb4081c86f8c1f Mon Sep 17 00:00:00 2001 From: Lukáš Doktor Date: Fri, 18 Aug 2017 16:26:08 +0200 Subject: qemu.py: Use custom exceptions rather than Exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The naked Exception should not be widely used. It makes sense to be a bit more specific and use better-suited custom exceptions. As a benefit we can store the full reply in the exception in case someone needs it when catching the exception. Signed-off-by: Lukáš Doktor Reviewed-by: Eduardo Habkost Message-Id: <20170818142613.32394-6-ldoktor@redhat.com> Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index bde8da8fe7..7c6802609a 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -19,6 +19,19 @@ import subprocess import qmp.qmp +class MonitorResponseError(qmp.qmp.QMPError): + ''' + Represents erroneous QMP monitor reply + ''' + def __init__(self, reply): + try: + desc = reply["error"]["desc"] + except KeyError: + desc = reply + super(MonitorResponseError, self).__init__(desc) + self.reply = reply + + class QEMUMachine(object): '''A QEMU VM @@ -213,9 +226,9 @@ class QEMUMachine(object): ''' reply = self.qmp(cmd, conv_keys, **args) if reply is None: - raise Exception("Monitor is closed") + raise qmp.qmp.QMPError("Monitor is closed") if "error" in reply: - raise Exception(reply["error"]["desc"]) + raise MonitorResponseError(reply) return reply["return"] def get_qmp_event(self, wait=False): -- cgit 1.4.1 From f6cf7f5a227ca0c8cc540d78d4f0f943c51ea8d1 Mon Sep 17 00:00:00 2001 From: Amador Pahim Date: Fri, 1 Sep 2017 13:28:17 +0200 Subject: qemu.py: fix is_running() return before first launch() is_running() returns None when called before the first time we call launch(): >>> import qemu >>> vm = qemu.QEMUMachine('qemu-system-x86_64') >>> vm.is_running() >>> It should return False instead. This patch fixes that. For consistence, this patch removes the parenthesis from the second clause as it's not really needed. Signed-off-by: Amador Pahim Message-Id: <20170901112829.2571-2-apahim@redhat.com> Reviewed-by: Fam Zheng Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index 7c6802609a..f80b008f7f 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -131,7 +131,7 @@ class QEMUMachine(object): raise def is_running(self): - return self._popen and (self._popen.returncode is None) + return self._popen is not None and self._popen.returncode is None def exitcode(self): if self._popen is None: -- cgit 1.4.1 From 4738b0a85a0c2031fddc71b51cccebce0c4ba6b1 Mon Sep 17 00:00:00 2001 From: Amador Pahim Date: Fri, 1 Sep 2017 13:28:18 +0200 Subject: qemu.py: avoid writing to stdout/stderr This module should not write directly to stdout/stderr. Instead, it should either raise exceptions or just log the messages and let the callers handle them and decide what to do. For example, scripts could choose to send the log messages stderr or/and write them to a file if verbose or debugging mode is enabled. This patch replaces the writes to stderr by an exception in the send_fd_scm() when _socket_scm_helper is not set or not present. In the same method, the subprocess Popen will now redirect the stdout/stderr to logging.debug instead of writing to system stderr. As consequence, since the Popen.communicate() is now used (in order to get the stdout), the further call to wait() became redundant and was replaced by Popen.returncode. The shutdown() message on negative exit code will now be logged to logging.warn instead of written to system stderr. Signed-off-by: Amador Pahim Message-Id: <20170901112829.2571-3-apahim@redhat.com> Reviewed-by: Fam Zheng Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index f80b008f7f..8d3d24dd2b 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -13,12 +13,22 @@ # import errno +import logging import os import sys import subprocess import qmp.qmp +LOG = logging.getLogger(__name__) + + +class QEMUMachineError(Exception): + """ + Exception called when an error in QEMUMachine happens. + """ + + class MonitorResponseError(qmp.qmp.QMPError): ''' Represents erroneous QMP monitor reply @@ -107,18 +117,21 @@ class QEMUMachine(object): # In iotest.py, the qmp should always use unix socket. assert self._qmp.is_scm_available() if self._socket_scm_helper is None: - print >>sys.stderr, "No path to socket_scm_helper set" - return -1 + raise QEMUMachineError("No path to socket_scm_helper set") if not os.path.exists(self._socket_scm_helper): - print >>sys.stderr, "%s does not exist" % self._socket_scm_helper - return -1 + raise QEMUMachineError("%s does not exist" % + self._socket_scm_helper) fd_param = ["%s" % self._socket_scm_helper, "%d" % self._qmp.get_sock_fd(), "%s" % fd_file_path] devnull = open('/dev/null', 'rb') - proc = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, - stderr=sys.stderr) - return proc.wait() + proc = subprocess.Popen(fd_param, stdin=devnull, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + output = proc.communicate()[0] + if output: + LOG.debug(output) + + return proc.returncode @staticmethod def _remove_if_exists(path): @@ -202,8 +215,8 @@ class QEMUMachine(object): exitcode = self._popen.wait() if exitcode < 0: - sys.stderr.write('qemu received signal %i: %s\n' - % (-exitcode, ' '.join(self._args))) + LOG.warn('qemu received signal %i: %s', -exitcode, + ' '.join(self._args)) self._load_io_log() self._post_shutdown() -- cgit 1.4.1 From 63e0ba55222476c8b96d8c45131c830e00a80eaa Mon Sep 17 00:00:00 2001 From: Amador Pahim Date: Fri, 1 Sep 2017 13:28:19 +0200 Subject: qemu.py: use os.path.null instead of /dev/null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For increased portability, let's use os.path.devnull. Signed-off-by: Amador Pahim Message-Id: <20170901112829.2571-4-apahim@redhat.com> Reviewed-by: Philippe Mathieu-Daudé Reviewed-by: Fam Zheng Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index 8d3d24dd2b..c9bcaafe41 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -124,7 +124,7 @@ class QEMUMachine(object): fd_param = ["%s" % self._socket_scm_helper, "%d" % self._qmp.get_sock_fd(), "%s" % fd_file_path] - devnull = open('/dev/null', 'rb') + devnull = open(os.path.devnull, 'rb') proc = subprocess.Popen(fd_param, stdin=devnull, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output = proc.communicate()[0] @@ -186,7 +186,7 @@ class QEMUMachine(object): def launch(self): '''Launch the VM and establish a QMP connection''' - devnull = open('/dev/null', 'rb') + devnull = open(os.path.devnull, 'rb') qemulog = open(self._qemu_log_path, 'wb') try: self._pre_launch() -- cgit 1.4.1 From dab91d9aa00e41ee680524e4f6e99a1e7fe12eb2 Mon Sep 17 00:00:00 2001 From: Amador Pahim Date: Fri, 1 Sep 2017 13:28:20 +0200 Subject: qemu.py: improve message on negative exit code The current message shows 'self._args', which contains only part of the options used in the Qemu command line. This patch makes the qemu full args list an instance variable and then uses it in the negative exit code message. Message was moved outside the 'if is_running' block to make sure it will be logged if the VM finishes before the call to shutdown(). Signed-off-by: Amador Pahim Message-Id: <20170901112829.2571-5-apahim@redhat.com> [ehabkost: removed superfluous parenthesis] Reviewed-by: Fam Zheng Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index c9bcaafe41..9440261ac3 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -87,6 +87,7 @@ class QEMUMachine(object): self._socket_scm_helper = socket_scm_helper self._debug = debug self._qmp = None + self._qemu_full_args = None def __enter__(self): return self @@ -186,13 +187,16 @@ class QEMUMachine(object): def launch(self): '''Launch the VM and establish a QMP connection''' + self._qemu_full_args = None devnull = open(os.path.devnull, 'rb') qemulog = open(self._qemu_log_path, 'wb') try: self._pre_launch() - args = (self._wrapper + [self._binary] + self._base_args() + - self._args) - self._popen = subprocess.Popen(args, stdin=devnull, stdout=qemulog, + self._qemu_full_args = self._wrapper + [self._binary] + + self._base_args() + self._args + self._popen = subprocess.Popen(self._qemu_full_args, + stdin=devnull, + stdout=qemulog, stderr=subprocess.STDOUT, shell=False) self._post_launch() @@ -212,14 +216,20 @@ class QEMUMachine(object): self._qmp.close() except: self._popen.kill() + self._popen.wait() - exitcode = self._popen.wait() - if exitcode < 0: - LOG.warn('qemu received signal %i: %s', -exitcode, - ' '.join(self._args)) self._load_io_log() self._post_shutdown() + exitcode = self.exitcode() + if exitcode is not None and exitcode < 0: + msg = 'qemu received signal %i: %s' + if self._qemu_full_args: + command = ' '.join(self._qemu_full_args) + else: + command = '' + LOG.warn(msg, exitcode, command) + def qmp(self, cmd, conv_keys=True, **args): '''Invoke a QMP command and return the response dict''' qmp_args = dict() -- cgit 1.4.1 From b92a0011b1220aff549ae82c6104014d25e339ef Mon Sep 17 00:00:00 2001 From: Amador Pahim Date: Fri, 1 Sep 2017 13:28:21 +0200 Subject: qemu.py: include debug information on launch error When launching a VM, if an exception happens and the VM is not initiated, it might be useful to see the qemu command line and the qemu command output. This patch creates that message. Notice that self._iolog needs to be cleaned up in the beginning of the launch() to make sure we will not expose the qemu log from a previous launch if the current one fails. Signed-off-by: Amador Pahim Message-Id: <20170901112829.2571-6-apahim@redhat.com> Reviewed-by: Fam Zheng Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'scripts/qemu.py') diff --git a/scripts/qemu.py b/scripts/qemu.py index 9440261ac3..8c67595ec8 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -187,6 +187,7 @@ class QEMUMachine(object): def launch(self): '''Launch the VM and establish a QMP connection''' + self._iolog = None self._qemu_full_args = None devnull = open(os.path.devnull, 'rb') qemulog = open(self._qemu_log_path, 'wb') @@ -206,6 +207,12 @@ class QEMUMachine(object): self._popen.wait() self._load_io_log() self._post_shutdown() + + LOG.debug('Error launching VM') + if self._qemu_full_args: + LOG.debug('Command: %r', ' '.join(self._qemu_full_args)) + if self._iolog: + LOG.debug('Output: %r', self._iolog) raise def shutdown(self): -- cgit 1.4.1