From 0918f24d8dce631289eb1a6c66c6c8f6b6bc0ce7 Mon Sep 17 00:00:00 2001 From: Vojtech Horky Date: Sat, 25 Aug 2018 22:03:57 +0200 Subject: [PATCH] Start Pythonification of in-VM testing --- htest/tasks.py | 146 ++++++++++++++++++++++++++++++ htest/utils.py | 75 ++++++++++++++++ htest/vm/controller.py | 116 ++++++++++++++++++++++++ htest/vm/qemu.py | 225 ++++++++++++++++++++++++++++++++++++++++++++++ scenarios/base/fs.yml | 49 ++++++++++ scenarios/base/printf.yml | 10 +++ vm-test.py | 127 ++++++++++++++++++++++++++ 7 files changed, 748 insertions(+) create mode 100755 htest/tasks.py create mode 100755 htest/utils.py create mode 100755 htest/vm/controller.py create mode 100755 htest/vm/qemu.py create mode 100644 scenarios/base/fs.yml create mode 100644 scenarios/base/printf.yml create mode 100755 vm-test.py diff --git a/htest/tasks.py b/htest/tasks.py new file mode 100755 index 0000000..dfaf29d --- /dev/null +++ b/htest/tasks.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2018 Vojtech Horky +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import logging + +from htest.utils import retries + +class ScenarioTask: + """ + Base class for individual tasks that are executed in a scenario. + """ + + def __init__(self, name): + """ + Set @name to the task name (call from subclass). + """ + + self.name = name + self.machine = None + self.fail_message = '' + self.logger = logging.getLogger(name) + + def check_required_argument(self, args, name): + """ + To be used by subclasses to check that arguments + were specified. + """ + if not name in args: + raise Exception("Required argument {} missing.".format(name)) + + def is_vm_launcher(self): + """ + Whether this task starts a new VM. + """ + return False + + def get_name(self): + return self.name + + def set_machine(self, machine): + """ + Set machine responsible for executing this task. + """ + self.machine = machine + + def run(self): + """ + Actually execute this task. + """ + pass + +class ScenarioTaskBoot(ScenarioTask): + """ + Brings the machine up. + """ + def __init__(self, args): + ScenarioTask.__init__(self, 'boot') + self.args = args + + def is_vm_launcher(self): + return True + + def run(self): + self.machine.boot() + +class ScenarioTaskCommand(ScenarioTask): + """ + Run a command in vterm. + """ + + def __init__(self, args): + ScenarioTask.__init__(self, 'command') + if type(args) is str: + args = { 'args': args} + self.check_required_argument(args, 'args') + self.command = args['args'] + self.args = args + + def _grep(self, text, lines): + for l in lines: + if l.find(text) != -1: + return True + return False + + def run(self): + self.logger.info("Typing '{}' into {}.".format(self.command, self.machine.name)) + + self.machine.type(self.command) + self.machine.type('\n') + + for xxx in retries(timeout=60, interval=2, name="vterm-run", message="Failed to run command"): + lines = self.machine.capture_vterm() + if 'negassert' in self.args: + if self._grep(self.args['negassert'], lines): + raise Exception('Found forbidden text {} ...'.format(self.args['negassert'])) + if 'assert' in self.args: + if self._grep(self.args['assert'], lines): + break + if self._grep('Cannot spawn', lines) or self._grep('Command failed', lines): + raise Exception('Failed to run command') + if self._grep('# _', lines): + if 'assert' in self.args: + raise Exception('Missing expected text {} ...'.format(self.args['assert'])) + break + self.logger.info("Command '{}' done.".format(self.command)) + +class ScenarioTaskCls(ScenarioTask): + """ + Clear vterm screen. + """ + + def __init__(self, args): + ScenarioTask.__init__(self, 'vterm-cls') + + def run(self): + self.logger.info("Clearing the screen.") + + for i in range(30): + self.machine.type('\n') diff --git a/htest/utils.py b/htest/utils.py new file mode 100755 index 0000000..924e4f9 --- /dev/null +++ b/htest/utils.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2018 Vojtech Horky +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import time +import logging +import shlex + +def retries(n=None, interval=2, timeout=None, message="Operation timed-out (too many retries)", name=""): + """ + To be used in for-loops to try action multiple times. + Throws exception on time-out. + """ + + if (n is None) and (timeout is None): + raise Exception("Specify either n or timeout for retries") + + if name != "": + name = "-" + name + logger = logging.getLogger("rtr" + name) + + if timeout is None: + timeout = n * interval + remaining = timeout + n = 0 + while remaining > 0: + logger.debug("remaining={}, n={}, interval={}, \"{}\"".format( + remaining, n, interval, message)) + remaining = remaining - interval + n = n + 1 + yield n + time.sleep(interval) + logger.debug("timed-out, n={}, \"{}\"".format(n, message)) + raise Exception(message) + +def format_command(cmd): + """ + Escape shell command given as list of arguments. + """ + escaped = [shlex.quote(i) for i in cmd] + return ' '.join(escaped) + +def format_command_pipe(pipe): + """ + Escape shell pipe given as list of list of arguments. + """ + escaped = [format_command(i) for i in pipe] + return ' | '.join(escaped) + diff --git a/htest/vm/controller.py b/htest/vm/controller.py new file mode 100755 index 0000000..0bfc8c6 --- /dev/null +++ b/htest/vm/controller.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2018 Vojtech Horky +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import os + +class VMManager: + """ + Keeps track of running virtual machines. + """ + + def __init__(self, controller, architecture, boot_image): + self.controller_class = controller + self.architecture = architecture + self.boot_image = boot_image + self.instances = {} + self.last = None + + def create(self, name): + if name in self.instances: + raise Exception("Duplicate machine name {}.".format(name)) + self.instances[name] = self.controller_class(self.architecture, name, self.boot_image) + self.last = name + return self.instances[name] + + def get(self, name=None): + if name is None: + name = self.last + if name is None: + return None + if name in self.instances: + self.last = name + return self.instances[name] + else: + return None + + def terminate(self): + for i in self.instances: + self.instances[i].terminate() + + +class VMController: + """ + Base class for controllers of specific virtual machine emulators. + """ + + def __init__(self, provider): + self.provider_name = provider + # Patched by VMManager + self.name = 'XXX' + pass + + def boot(self, **kwargs): + """ + Bring the machine up. + """ + pass + + def terminate(self): + """ + Shutdown the VM. + """ + pass + + def type(self, what): + """ + Type given text into vterm. + """ + print("type('{}') @ {}".format(what, self.provider_name)) + pass + + def capture_vterm(self): + """ + Get contents of current terminal window. + """ + return self.capture_vterm_impl() + + def capture_vterm_impl(self): + """ + Do not call but reimplement in subclass. + """ + return [] + + def get_temp(self, id): + """ + Get temporary file name. + """ + os.makedirs('tmp-vm-python', exist_ok=True) + return 'tmp-vm-python/tmp-' + self.name + '-' + id + diff --git a/htest/vm/qemu.py b/htest/vm/qemu.py new file mode 100755 index 0000000..ed7c081 --- /dev/null +++ b/htest/vm/qemu.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2018 Vojtech Horky +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import subprocess +import socket +import logging + +from htest.utils import retries, format_command, format_command_pipe +from htest.vm.controller import VMController + +class QemuVMController(VMController): + """ + QEMU VM controller. + """ + + def __init__(self, arch, name, boot_image): + VMController.__init__(self, 'QEMU-' + arch) + self.arch = arch + self.booted = False + self.name = name + self.boot_image = boot_image + self.logger = logging.getLogger('qemu-{}'.format(name)) + + def _check_is_up(self): + if not self.booted: + raise Exception("Machine not launched") + + def _send_command(self, command): + self._check_is_up() + self.logger.debug("Sending command '{}'".format(command)) + command = command + '\n' + self.monitor.sendall(command.encode('utf-8')) + + def _run_command(self, command): + proc = subprocess.Popen(command) + proc.wait() + if proc.returncode != 0: + raise Exception("Command {} failed.".format(command)) + + def _run_pipe(self, commands): + self.logger.debug("Running pipe {}".format(format_command_pipe(commands))) + procs = [] + for command in commands: + inp = None + if len(procs) > 0: + inp = procs[-1].stdout + proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp) + procs.append(proc) + procs[-1].communicate() + + + def boot(self, **kwargs): + self.monitor_file = self.get_temp('monitor') + cmd = [ 'qemu-system-x86_64' , '-usb', '-m', '256' ] + cmd.append('-enable-kvm') + cmd.append('-cdrom') + cmd.append(self.boot_image) + #cmd.append('-daemonize') + cmd.append('-monitor') + cmd.append('unix:{},server,nowait'.format(self.monitor_file)) + self.logger.debug("Starting QEMU: {}".format(format_command(cmd))) + + self.proc = subprocess.Popen(cmd) + self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."): + try: + self.monitor.connect(self.monitor_file) + break + except FileNotFoundError: + pass + except ConnectionRefusedError: + pass + if self.proc.poll(): + raise Exception("QEMU not started, aborting.") + + self.booted = True + self.logger.info("Machine started.") + + # Skip past GRUB + self.type('\n') + + uspace_booted = False + for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"): + lines = self.capture_vterm() + for l in lines: + if l.find('to see a few survival tips') != -1: + uspace_booted = True + break + if uspace_booted: + break + + assert uspace_booted + + self.logger.info("Machine booted into userspace.") + + return + + def capture_vterm_impl(self): + screenshot_full = self.get_temp('screen-full.ppm') + screenshot_term = self.get_temp('screen-term.png') + screenshot_text = self.get_temp('screen-term.txt') + + self._send_command('screendump ' + screenshot_full) + + for xxx in retries(timeout=5, interval=1, name="scrdump", message="Failed to capture screen"): + try: + self._run_command([ + 'convert', + screenshot_full, + '-crop', '640x480+4+24', + '+repage', + '-colors', '2', + '-monochrome', + screenshot_term + ]) + break + except: + pass + + self._run_pipe([ + [ + 'convert', + screenshot_term, + '-crop', '640x480', + '+repage', + '-crop', '8x16', + '+repage', + '+adjoin', + 'txt:-', + ], + [ + 'sed', + '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|', + '-e', 's:^#.*:@:', + '-e', 's#000000#0#g', + '-e', 's#FFFFFF#F#', + ], + [ 'tee', self.get_temp('1.txt') ], + [ + 'sed', + '-e', ':a', + '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}', + '-e', 't a', + ], + [ 'tee', self.get_temp('2.txt') ], + [ + 'sed', + '-f', 'ocr.sed', + ], + [ + 'sed', + '/../s#.*#?#', + ], + [ 'tee', self.get_temp('3.txt') ], + [ + 'paste', + '-sd', '', + ], + [ + 'fold', + '-w', '80', + ], + [ 'tee', self.get_temp('4.txt') ], + [ + 'head', + '-n', '30', + ], + [ + 'tee', + screenshot_text, + ] + ]) + with open(screenshot_text, 'r') as f: + lines = [ l.strip('\n') for l in f.readlines() ] + self.logger.debug("Captured text:") + for l in lines: + self.logger.debug("| " + l) + return lines + + def terminate(self): + if not self.booted: + return + self._send_command('quit') + + def type(self, what): + translations = { + ' ': 'spc', + '.': 'dot', + '-': 'minus', + '/': 'slash', + '\n': 'ret', + '_': 'shift-minus', + } + for letter in what: + if letter in translations: + letter = translations[letter] + self._send_command('sendkey ' + letter) + pass diff --git a/scenarios/base/fs.yml b/scenarios/base/fs.yml new file mode 100644 index 0000000..97d0c78 --- /dev/null +++ b/scenarios/base/fs.yml @@ -0,0 +1,49 @@ +meta: + name: "Base file system test" + harbours: [] + +tasks: + - boot + - command: "mkfile --size 2m /tmp/img" + - name: Checking image file has the right size. + command: + args: "ls /tmp" + assert: "2097152" + - name: Starting file_bd + command: + args: "file_bd /tmp/img fbd0" + assert: "Accepting connections" + negassert: "Failed to start block device layer." + - name: Creating filesystem + command: + args: "mkfat --type 12 fbd0" + assert: "Success" + negassert: "Failed to create FAT file system" + - name: Creating a dedicated mount-point + command: "mkdir /tmp/mnt" + - name: Mounting the file system + command: + args: "mount fat /tmp/mnt fbd0" + negassert: "Unable" + - name: Copy the file to the mounted filesystem + command: "cp demo.txt /tmp/mnt" + - cls + - name: Checking file copying actually succeeded + command: + args: "ls /tmp/mnt" + assert: "demo.txt" + - cls + - command: "umount /tmp/mnt" + - name: Checking demo.txt is not present when unmounted + command: + args: "ls /tmp/mnt" + negassert: "demo.txt" + - cls + - command: + args: "mount fat /tmp/mnt fbd0" + negassert: "Unable" + - name: Checking the file is still there after re-mounting + command: + args: "ls /tmp/mnt" + assert: "demo.txt" + diff --git a/scenarios/base/printf.yml b/scenarios/base/printf.yml new file mode 100644 index 0000000..4a45dc0 --- /dev/null +++ b/scenarios/base/printf.yml @@ -0,0 +1,10 @@ +meta: + name: "tester printf" + harbours: [] + +tasks: + - boot + - command: + args: "tester print2" + assert: "Test passed" + negassert: "Test failed" diff --git a/vm-test.py b/vm-test.py new file mode 100755 index 0000000..ee03f71 --- /dev/null +++ b/vm-test.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2018 Vojtech Horky +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# - Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# - The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + + +import argparse +import yaml +import logging +import sys + +from htest.vm.controller import VMManager +from htest.vm.qemu import QemuVMController +from htest.tasks import * + +args = argparse.ArgumentParser(description='HelenOS VM tests') +args.add_argument('--scenario', + metavar='FILENAME.yml', + dest='scenario', + default='scenarios/base/pcut.yml', + help='Scenario file' +) +args.add_argument('--arch', + metavar='ARCHITECTURE', + dest='architecture', + default='amd64', + help='Emulated architecture identification.' +) +args.add_argument('--image', + metavar='FILENAME', + dest='boot_image', + required=True, + help='HelenOS boot image (e.g. ISO file).' +) +args.add_argument('--debug', + dest='debug', + default=False, + action='store_true', + help='Print debugging messages' +) + +config = args.parse_args() + +if config.debug: + config.logging_level = logging.DEBUG +else: + config.logging_level = logging.INFO + +logging.basicConfig( + format='[%(asctime)s %(name)-16s %(levelname)7s] %(message)s', + level=config.logging_level +) + +logger = logging.getLogger('main') + +with open(config.scenario, 'r') as f: + try: + scenario = yaml.load(f) + except yaml.YAMLError as ex: + logger.error(ex) + sys.exit(1) + +logger.debug(scenario) + +vmm = VMManager(QemuVMController, config.architecture, config.boot_image) + +scenario_tasks = [] +for t in scenario['tasks']: + task_name = None + if type(t) is dict: + k = list(set(t.keys()) - set(['name', 'machine'])) + if len(k) != 1: + raise Exception("Unknown task ({})!".format(k)) + task_name = k[0] + elif type(t) is str: + task_name = t + t = { + task_name: {} + } + else: + raise Exception("Unknown task!") + task_classname = 'ScenarioTask' + task_name.title().replace('-', '_') + task_class = globals()[task_classname] + task_inst = task_class(t[task_name]) + if not ('machine' in t): + t['machine'] = None + machine = vmm.get(t['machine']) + if machine is None: + if t['machine'] is None: + t['machine'] = 'default' + logger.debug("Creating new machine {}.".format(t['machine'])) + machine = vmm.create(t['machine']) + task_inst.set_machine(machine) + scenario_tasks.append(task_inst) + +try: + for t in scenario_tasks: + t.run() +except Exception as ex: + print(ex) + +vmm.terminate() -- 2.11.4.GIT