From nobody Tue Feb 10 23:01:23 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) client-ip=208.118.235.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Authentication-Results: mx.zohomail.com; spf=pass (zoho.com: domain of gnu.org designates 208.118.235.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=fail(p=none dis=none) header.from=redhat.com Return-Path: Received: from lists.gnu.org (lists.gnu.org [208.118.235.17]) by mx.zohomail.com with SMTPS id 1524248586951716.4112079913713; Fri, 20 Apr 2018 11:23:06 -0700 (PDT) Received: from localhost ([::1]:50153 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1f9agb-0006KE-WB for importer@patchew.org; Fri, 20 Apr 2018 14:23:06 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:37527) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1f9aeJ-0004q1-6L for qemu-devel@nongnu.org; Fri, 20 Apr 2018 14:20:46 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1f9aeF-0005Zs-8w for qemu-devel@nongnu.org; Fri, 20 Apr 2018 14:20:43 -0400 Received: from mx1.redhat.com ([209.132.183.28]:53226) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1f9aeE-0005Y9-UC for qemu-devel@nongnu.org; Fri, 20 Apr 2018 14:20:39 -0400 Received: from smtp.corp.redhat.com (int-mx05.intmail.prod.int.phx2.redhat.com [10.5.11.15]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 3DF9D3002A8B; Fri, 20 Apr 2018 18:20:38 +0000 (UTC) Received: from localhost (ovpn-116-52.gru2.redhat.com [10.97.116.52]) by smtp.corp.redhat.com (Postfix) with ESMTP id 6BB485D6A8; Fri, 20 Apr 2018 18:20:16 +0000 (UTC) From: Eduardo Habkost To: qemu-devel@nongnu.org Date: Fri, 20 Apr 2018 15:19:29 -0300 Message-Id: <20180420181951.7252-3-ehabkost@redhat.com> In-Reply-To: <20180420181951.7252-1-ehabkost@redhat.com> References: <20180420181951.7252-1-ehabkost@redhat.com> X-Scanned-By: MIMEDefang 2.79 on 10.5.11.15 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.47]); Fri, 20 Apr 2018 18:20:38 +0000 (UTC) X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 209.132.183.28 Subject: [Qemu-devel] [RFC 02/24] Introduce the basic framework to run Avocado tests X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: =?UTF-8?q?Luk=C3=A1=C5=A1=20Doktor?= , Fam Zheng , Stefan Hajnoczi , Amador Pahim , Cleber Rosa , Alistair Francis Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: "Qemu-devel" X-ZohoMail: RSF_0 Z_629925259 SPT_0 Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" From: Amador Pahim Avocado Testing Framework can help with functional tests in qemu development process. This patch creates the basic infrastructure to use with Avocado tests. It contains: - A README file with the initial documentation. - The test module which inherits from avocado.Test and adds a VM object, created from scripts/qemu.py module. The QemuTest class is the test API for the actual Qemu tests to inherit from. - A parameters yaml file with the supported keys. - A variants yaml file with the Qemu supported architectures. After this commit, you can expect a series of real-world tests, written using this new framework. Signed-off-by: Amador Pahim Signed-off-by: Eduardo Habkost --- scripts/qemu.py | 12 +- tests/avocado/README.rst | 111 ++++++++++ tests/avocado/avocado_qemu/__init__.py | 0 tests/avocado/avocado_qemu/test.py | 365 +++++++++++++++++++++++++++++= ++++ tests/avocado/parameters.yaml | 28 +++ tests/avocado/variants.yaml | 62 ++++++ tests/qemu-iotests/iotests.py | 28 +-- 7 files changed, 586 insertions(+), 20 deletions(-) create mode 100644 tests/avocado/README.rst create mode 100644 tests/avocado/avocado_qemu/__init__.py create mode 100644 tests/avocado/avocado_qemu/test.py create mode 100644 tests/avocado/parameters.yaml create mode 100644 tests/avocado/variants.yaml diff --git a/scripts/qemu.py b/scripts/qemu.py index 9e9d502543..bd66620f45 100644 --- a/scripts/qemu.py +++ b/scripts/qemu.py @@ -81,7 +81,7 @@ class QEMUMachine(object): self._qemu_log_file =3D None self._popen =3D None self._binary =3D binary - self._args =3D list(args) # Force copy args in case we modify = them + self.args =3D list(args) # Force copy args in case we modify t= hem self._wrapper =3D wrapper self._events =3D [] self._iolog =3D None @@ -109,8 +109,8 @@ class QEMUMachine(object): # This can be used to add an unused monitor instance. def add_monitor_telnet(self, ip, port): args =3D 'tcp:%s:%d,server,nowait,telnet' % (ip, port) - self._args.append('-monitor') - self._args.append(args) + self.args.append('-monitor') + self.args.append(args) =20 def add_fd(self, fd, fdset, opaque, opts=3D''): '''Pass a file descriptor to the VM''' @@ -120,8 +120,8 @@ class QEMUMachine(object): if opts: options.append(opts) =20 - self._args.append('-add-fd') - self._args.append(','.join(options)) + self.args.append('-add-fd') + self.args.append(','.join(options)) return self =20 def send_fd_scm(self, fd_file_path): @@ -184,7 +184,7 @@ class QEMUMachine(object): '-display', 'none', '-vga', 'none'] =20 def _create_console(self, console_address): - for item in self._args: + for item in self.args: for option in ['isa-serial', 'spapr-vty', 'sclpconsole']: if option in item: return [] diff --git a/tests/avocado/README.rst b/tests/avocado/README.rst new file mode 100644 index 0000000000..f0e703fe06 --- /dev/null +++ b/tests/avocado/README.rst @@ -0,0 +1,111 @@ +This directory is hosting functional tests written using Avocado Testing +Framework. To install Avocado, follow the instructions from this link:: + + http://avocado-framework.readthedocs.io/en/latest/GetStartedGuide.html= #installing-avocado + +Tests here are written keeping the minimum amount of dependencies. To +run the tests, you need the Avocado core package (`python-avocado` on +Fedora, `avocado-framework` on pip). Extra dependencies should be +documented in this file. + +In this directory, an ``avocado_qemu`` package is provided, containing +the ``test`` module, which inherits from ``avocado.Test`` and provides +a builtin and easy-to-use Qemu virtual machine. Here's a template that +can be used as reference to start writing your own tests:: + + from avocado_qemu import test + + class MyTest(test.QemuTest): + """ + :avocado: enable + """ + + def setUp(self): + self.vm.args.extend(['-m', '512']) + self.vm.launch() + + def test_01(self): + res =3D self.vm.qmp('human-monitor-command', + command_line=3D'info version') + self.assertIn('v2.9.0', res['return']) + + def tearDown(self): + self.vm.shutdown() + +To execute your test, run:: + + avocado run test_my_test.py + +To execute all tests, run:: + + avocado run . + +If you don't specify the Qemu binary to use, the ``avocado_qemu`` +package will automatically probe it. The probe will try to use the Qemu +binary from the git tree build directory, using the same architecture as +the local system (if the architecture is not specified). If the Qemu +binary is not available in the git tree build directory, the next try is +to use the system installed Qemu binary. + +You can define a number of optional parameters, providing them via YAML +file using the Avocado parameters system: + +- ``qemu_bin``: Use a given Qemu binary, skipping the automatic + probe. Example: ``qemu_bin: /usr/libexec/qemu-kvm``. +- ``qemu_dst_bin``: Use a given Qemu binary to create the destination VM + when the migration process takes place. If it's not provided, the same + binary used in the source VM will be used for the destination VM. + Example: ``qemu_dst_bin: /usr/libexec/qemu-kvm-binary2``. +- ``arch``: Probe the Qemu binary from a given architecture. It has no + effect if ``qemu_bin`` is specified. If not provided, the binary probe + will use the system architecture. Example: ``arch: x86_64`` +- ``image_path``: VMs are defined without image. If the ``image_path`` + is specified, it will be used as the VM image. The ``-snapshot`` + option will then be used to avoid writing into the image. Example: + ``image_path: /var/lib/images/fedora-25.img`` +- ``image_user`` and ``image_pass``: When using a ``image_path``, if you + want to get the console from the Guest OS you have to define the Guest + OS credentials. Example: ``image_user: root`` and + ``image_pass: p4ssw0rd`` +- ``machine_type``: Use this option to define a machine type for the VM. + Example: ``machine_type: pc`` +- ``machine_accel``: Use this option to define a machine acceleration + for the VM. Example: ``machine_accel: kvm``. +- ``machine_kvm_type``: Use this option to select the KVM type when the + ``accel`` is ``kvm`` and there are more than one KVM types available. + Example: ``machine_kvm_type: PR`` + +To use a parameters file, you have to install the yaml_to_mux plugin +(`python2-avocado-plugins-varianter-yaml-to-mux` on Fedora, +`avocado-framework-plugin-varianter-yaml-to-mux` on pip). + +Run the test with:: + + $ avocado run test_my_test.py -m parameters.yaml + +Additionally, you can use a variants file to to set different values +for each parameter. Using the YAML tag ``!mux`` Avocado will execute the +tests once per combination of parameters. Example:: + + $ cat variants.yaml + architecture: !mux + x86_64: + arch: x86_64 + i386: + arch: i386 + +Run it the with:: + + $ avocado run test_my_test.py -m variants.yaml + +You can use both the parameters file and the variants file in the same +command line:: + + $ avocado run test_my_test.py -m parameters.yaml variants.yaml + +Avocado will then merge the parameters from both files and create the +proper variants. + +See ``avocado run --help`` and ``man avocado`` for several other +options, such as ``--filter-by-tags``, ``--show-job-log``, +``--failfast``, etc. diff --git a/tests/avocado/avocado_qemu/__init__.py b/tests/avocado/avocado= _qemu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/avocado/avocado_qemu/test.py b/tests/avocado/avocado_qem= u/test.py new file mode 100644 index 0000000000..e74de97a3e --- /dev/null +++ b/tests/avocado/avocado_qemu/test.py @@ -0,0 +1,365 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See LICENSE for more details. +# +# Copyright (C) 2017 Red Hat Inc +# +# Authors: +# Amador Pahim +# +# Based on code from: +# https://github.com/avocado-framework/avocado-virt + + +""" +Avocado Qemu Test module to extend the Avocado Test module providing +extra features intended for Qemu testing. +""" + + +import logging +import os +import sys +import time +import uuid + +import aexpect + +from avocado import Test +from avocado.utils.data_structures import Borg +from avocado.utils import network +from avocado.utils import process +from avocado.utils import path as utils_path +from avocado.utils import wait +sys.path.append(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'scripts')) +import qemu + + +class QEMULoginTimeoutError(Exception): + """ + If timeout expires + """ + pass + + +class QEMULoginAuthenticationError(Exception): + """ + If authentication fails + """ + pass + + +class QEMULoginProcessTerminatedError(Exception): + """ + If the client terminates during login + """ + pass + + +class QEMULoginError(Exception): + """ + If some other error occurs + """ + pass + + +class QEMUConsoleError(Exception): + """ + If some error with the console access happens + """ + pass + + +class QEMUMigrationError(Exception): + """ + If some error with the migration happens + """ + pass + + +def _get_qemu_bin(arch): + git_root =3D process.system_output('git rev-parse --show-toplevel', + ignore_status=3DTrue, + verbose=3DFalse) + qemu_binary =3D os.path.join(git_root, + "%s-softmmu" % arch, + "qemu-system-%s" % arch) + if not os.path.exists(qemu_binary): + qemu_binary =3D utils_path.find_command('qemu-system-%s' % arch) + return qemu_binary + + +def _handle_prompts(session, username, password, prompt, timeout=3D10, + debug=3DFalse): + """ + Connect to a remote host (guest) using SSH or Telnet or else. + + Wait for questions and provide answers. If timeout expires while + waiting for output from the child (e.g. a password prompt or + a shell prompt) -- fail. + + :param session: An Expect or ShellSession instance to operate on + :param username: The username to send in reply to a login prompt + :param password: The password to send in reply to a password prompt + :param prompt: The shell prompt that indicates a successful login + :param timeout: The maximal time duration (in seconds) to wait for each + step of the login procedure (i.e. the "Are you sure" prompt, t= he + password prompt, the shell prompt, etc) + :raise QEMULoginTimeoutError: If timeout expires + :raise QEMULoginAuthenticationError: If authentication fails + :raise QEMULoginProcessTerminatedError: If the client terminates durin= g login + :raise QEMULoginError: If some other error occurs + :return: If connect succeed return the output text to script for furth= er + debug. + """ + password_prompt_count =3D 0 + login_prompt_count =3D 0 + last_chance =3D False + + output =3D "" + while True: + try: + match, text =3D session.read_until_last_line_matches( + [r"[Aa]re you sure", r"[Pp]assword:\s*", + # Prompt of rescue mode for Red Hat. + r"\(or (press|type) Control-D to continue\):\s*$", + r"[Gg]ive.*[Ll]ogin:\s*$", # Prompt of rescue mode for S= USE. + r"(? 0: + msg =3D "Got username prompt twice" + else: + msg =3D "Got username prompt after password prompt" + raise QEMULoginAuthenticationError(msg, text) + elif match =3D=3D 5: # "Connection closed" + raise QEMULoginError("Client said 'connection closed'", te= xt) + elif match =3D=3D 6: # "Connection refused" + raise QEMULoginError("Client said 'connection refused'", t= ext) + elif match =3D=3D 11: # Connection timeout + raise QEMULoginError("Client said 'connection timeout'", t= ext) + elif match =3D=3D 7: # "Please wait" + if debug: + logging.debug("Got 'Please wait'") + timeout =3D 30 + continue + elif match =3D=3D 8: # "Warning added RSA" + if debug: + logging.debug("Got 'Warning added RSA to known host li= st") + continue + elif match =3D=3D 12: # prompt + if debug: + logging.debug("Got shell prompt -- logged in") + break + elif match =3D=3D 13: # console prompt + logging.debug("Got console prompt, send return to show log= in") + session.sendline() + except aexpect.ExpectTimeoutError as details: + # sometimes, linux kernel print some message to console + # the message maybe impact match login pattern, so send + # a empty line to avoid unexpect login timeout + if not last_chance: + time.sleep(0.5) + session.sendline() + last_chance =3D True + continue + else: + raise QEMULoginTimeoutError(details.output) + except aexpect.ExpectProcessTerminatedError as details: + raise QEMULoginProcessTerminatedError(details.status, details.= output) + + return output + + +class _PortTracker(Borg): + + """ + Tracks ports used in the host machine. + """ + + def __init__(self): + Borg.__init__(self) + self.address =3D 'localhost' + self.start_port =3D 5000 + if not hasattr(self, 'retained_ports'): + self._reset_retained_ports() + + def __str__(self): + return 'Ports tracked: %r' % self.retained_ports + + def _reset_retained_ports(self): + self.retained_ports =3D [] + + def register_port(self, port): + if ((port not in self.retained_ports) and + (network.is_port_free(port, self.address))): + self.retained_ports.append(port) + else: + raise ValueError('Port %d in use' % port) + return port + + def find_free_port(self, start_port=3DNone): + if start_port is None: + start_port =3D self.start_port + port =3D start_port + while ((port in self.retained_ports) or + (not network.is_port_free(port, self.address))): + port +=3D 1 + self.retained_ports.append(port) + return port + + def release_port(self, port): + if port in self.retained: + self.retained.remove(port) + + +class _VM(qemu.QEMUMachine): + '''A QEMU VM''' + + def __init__(self, qemu_bin=3DNone, arch=3DNone, username=3DNone, pass= word=3DNone, + qemu_dst_bin=3DNone): + if arch is None: + arch =3D os.uname()[4] + self.ports =3D _PortTracker() + self.name =3D "qemu-%s" % str(uuid.uuid4())[:8] + if qemu_bin is None: + qemu_bin =3D _get_qemu_bin(arch) + if qemu_dst_bin is None: + qemu_dst_bin =3D qemu_bin + self.qemu_bin =3D qemu_bin + self.qemu_dst_bin =3D qemu_dst_bin + self.username =3D username + self.password =3D password + super(_VM, self).__init__(qemu_bin, name=3Dself.name, arch=3Darch) + + def get_console(self, console_address=3DNone, prompt=3D"[\#\$]"): + """ + :param address: Socket address, can be either a unix socket path + (string) or a tuple in the form (address, port) + for a TCP connection + :param prompt: The regex to identify we reached the prompt. + """ + + if not self.is_running(): + raise QEMUConsoleError('VM is not running.') + + if console_address is None: + if self._console_address is None: + raise QEMUConsoleError("Can't determine the console addres= s " + "to connect to.") + else: + console_address =3D self._console_address + + nc_cmd =3D 'nc' + if isinstance(console_address, tuple): + nc_cmd +=3D ' %s %s' % (console_address[0], console_address[1]) + else: + nc_cmd +=3D ' -U %s' % console_address + + console =3D aexpect.ShellSession(nc_cmd) + try: + logging.info('Console: Waiting login prompt...') + _handle_prompts(console, self.username, self.password, "[\#\$]= ") + logging.info('Console: Ready!') + except: + console.close() + raise + + return console + + def migrate(self, console_address=3DNone, timeout=3D20): + def migrate_complete(): + cmd =3D 'info migrate' + res =3D self.qmp('human-monitor-command', command_line=3Dcmd) + if 'completed' in res['return']: + logging.info("Migration successful") + return True + elif 'failed' in res['return']: + raise QEMUMigrateError("Migration of %s failed" % self) + return False + + port =3D self.ports.find_free_port() + newvm =3D _VM(self.qemu_dst_bin, self._arch, self.username, self.p= assword) + newvm.args =3D self.args + newvm.args.extend(['-incoming', 'tcp:0:%s' % port]) + + newvm.launch(console_address) + cmd =3D 'migrate -d tcp:0:%s' % port + self.qmp('human-monitor-command', command_line=3Dcmd) + mig_result =3D wait.wait_for(migrate_complete, timeout=3Dtimeout, + text=3D'Waiting for migration to comple= te') + + if mig_result is None: + raise QEMUMigrateError("Migration of %s did not complete after= " + "%s s" % (self.name, timeout)) + + return newvm + + +class QemuTest(Test): + + def __init__(self, methodName=3DNone, name=3DNone, params=3DNone, + base_logdir=3DNone, job=3DNone, runner_queue=3DNone): + super(QemuTest, self).__init__(methodName=3DmethodName, name=3Dnam= e, + params=3Dparams, base_logdir=3Dbase= _logdir, + job=3Djob, runner_queue=3Drunner_qu= eue) + self.vm =3D _VM(qemu_bin=3Dself.params.get('qemu_bin'), + arch=3Dself.params.get('arch'), + username=3Dself.params.get('image_user'), + password=3Dself.params.get('image_pass'), + qemu_dst_bin=3Dself.params.get('qemu_dst_bin')) + + self.vm.image =3D self.params.get('image_path') + if self.vm.image is not None: + self.vm.args.extend(['-drive', 'file=3D%s' % self.vm.image]) + self.vm.args.append('-snapshot') + + machine_type =3D self.params.get('machine_type') + machine_accel =3D self.params.get('machine_accel') + machine_kvm_type =3D self.params.get('machine_kvm_type') + machine =3D "" + if machine_type is not None: + machine +=3D "%s," % machine_type + if machine_accel is not None: + machine +=3D "accel=3D%s," % machine_accel + if machine_kvm_type is not None: + machine +=3D "kvm-type=3D%s," % machine_kvm_type + if machine: + self.vm.args.extend(['-machine', machine]) diff --git a/tests/avocado/parameters.yaml b/tests/avocado/parameters.yaml new file mode 100644 index 0000000000..3c5a0f92e0 --- /dev/null +++ b/tests/avocado/parameters.yaml @@ -0,0 +1,28 @@ +# Probe the Qemu binary from a given architecture. It has no effect if +# 'qemu_bin' is specified. If not provided, the binary probe will use +# the local system architecture. +arch: null + +# Use a given Qemu binary, skipping the automatic probe. +qemu_bin: null +# Use a given Qemu binary to create the destination VM when the +# migration process is called. If it's not provided, the same binary +# used in the source VM will be used for the destination VM. +qemu_dst_bin: null + +# VMs are defined without image. If the 'image_path' is specified, it +# will be used as the VM image. The '-snapshot' option will then be used +# to avoid writing data to the image. +image_path: null +# Username used to get the console from the Guest OS. +image_user: null +# Password used to get the console from the Guest OS. +image_pass: null + +# Use this option to define a machine type for the VM. +machine_type: null +# Use this option to define a machine acceleration for the VM. +machine_accel: null +# Use this option to select the KVM type when the 'machine_accel' is set +# to 'kvm' and there are more than one KVM types available. +machine_kvm_type: null diff --git a/tests/avocado/variants.yaml b/tests/avocado/variants.yaml new file mode 100644 index 0000000000..6fc689b3ba --- /dev/null +++ b/tests/avocado/variants.yaml @@ -0,0 +1,62 @@ +architecture: !mux + # Set the architecture of the qemu binary to execute tests + # with. This setting has no effect if you're using custom + # qemu_bin configuration. + x86_64: + arch: x86_64 + aarch64: + arch: aarch64 + alpha: + arch: alpha + arm: + arch: arm + cris: + arch: cris + i386: + arch: i386 + lm32: + arch: lm32 + m68k: + arch: m68k + microblazeel: + arch: microblazeel + microblaze: + arch: microblaze + mips64el: + arch: mips64el + mips64: + arch: mips64 + mipsel: + arch: mipsel + mips: + arch: mips + moxie: + arch: moxie + nios2: + arch: nios2 + or1k: + arch: or1k + ppc64: + arch: ppc64 + ppcemb: + arch: ppcemb + ppc: + arch: ppc + s390x: + arch: s390x + sh4eb: + arch: sh4eb + sh4: + arch: sh4 + sparc64: + arch: sparc64 + sparc: + arch: sparc + tricore: + arch: tricore + unicore32: + arch: unicore32 + xtensaeb: + arch: xtensaeb + xtensa: + arch: xtensa diff --git a/tests/qemu-iotests/iotests.py b/tests/qemu-iotests/iotests.py index b25d48a91b..a2e4f03743 100644 --- a/tests/qemu-iotests/iotests.py +++ b/tests/qemu-iotests/iotests.py @@ -293,18 +293,18 @@ class VM(qtest.QEMUQtestMachine): self._num_drives =3D 0 =20 def add_object(self, opts): - self._args.append('-object') - self._args.append(opts) + self.args.append('-object') + self.args.append(opts) return self =20 def add_device(self, opts): - self._args.append('-device') - self._args.append(opts) + self.args.append('-device') + self.args.append(opts) return self =20 def add_drive_raw(self, opts): - self._args.append('-drive') - self._args.append(opts) + self.args.append('-drive') + self.args.append(opts) return self =20 def add_drive(self, path, opts=3D'', interface=3D'virtio', format=3Dim= gfmt): @@ -322,27 +322,27 @@ class VM(qtest.QEMUQtestMachine): =20 if format =3D=3D 'luks' and 'key-secret' not in opts: # default luks support - if luks_default_secret_object not in self._args: + if luks_default_secret_object not in self.args: self.add_object(luks_default_secret_object) =20 options.append(luks_default_key_secret_opt) =20 - self._args.append('-drive') - self._args.append(','.join(options)) + self.args.append('-drive') + self.args.append(','.join(options)) self._num_drives +=3D 1 return self =20 def add_blockdev(self, opts): - self._args.append('-blockdev') + self.args.append('-blockdev') if isinstance(opts, str): - self._args.append(opts) + self.args.append(opts) else: - self._args.append(','.join(opts)) + self.args.append(','.join(opts)) return self =20 def add_incoming(self, addr): - self._args.append('-incoming') - self._args.append(addr) + self.args.append('-incoming') + self.args.append(addr) return self =20 def pause_drive(self, drive, event=3DNone): --=20 2.14.3