[KVM-AUTOTEST PATCH] KVM test: refactor kvm_config.py

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This is a reimplementation of the dict generator.  It is much faster than the
current implementation and uses a very small amount of memory.  Running time
and memory usage scale polynomially with the number of defined variants,
compared to exponentially in the current implementation.

Instead of regular expressions in the filters, the following syntax is used:

, means OR
.. means AND
. means IMMEDIATELY-FOLLOWED-BY

Example:

only qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide

means select all dicts whose names have:

(qcow2 AND (Fedora IMMEDIATELY-FOLLOWED-BY 14)) OR
((RHEL IMMEDIATELY-FOLLOWED-BY 6) AND raw AND boot) OR
(smp2 AND qcow2 AND migrate AND ide)

'qcow2..Fedora.14' is equivalent to 'Fedora.14..qcow2'.
'qcow2..Fedora.14' is not equivalent to 'qcow2..14.Fedora'.
'ide, scsi' is equivalent to 'scsi, ide'.

Filters can be used in 3 ways:
only <filter>
no <filter>
<filter>:

The last one starts a conditional block, e.g.

Fedora.14..qcow2:
    no migrate, reboot
    foo = bar

Interface changes:
- The main class is now called 'Parser' instead of 'config'.
- fork_and_parse() has been removed.  parse_file() and parse_string() should be
  used instead.
- When run as a standalone program, kvm_config.py just prints the shortnames of
  the generated dicts by default, and can optionally print the full names and
  contents of the dicts.
- By default, debug messages are not printed, but they can be enabled by
  passing debug=True to Parser's constructor, or by running kvm_config.py -v.
- The 'depend' key has been renamed to 'dep'.

Signed-off-by: Michael Goldish <mgoldish@xxxxxxxxxx>
Signed-off-by: Uri Lublin <ulublin@xxxxxxxxxx>
---
 client/tests/kvm/control               |   28 +-
 client/tests/kvm/control.parallel      |   12 +-
 client/tests/kvm/kvm_config.py         | 1051 ++++++++++++++------------------
 client/tests/kvm/kvm_scheduler.py      |    9 +-
 client/tests/kvm/kvm_utils.py          |    2 +-
 client/tests/kvm/tests.cfg.sample      |   13 +-
 client/tests/kvm/tests_base.cfg.sample |   46 +-
 7 files changed, 513 insertions(+), 648 deletions(-)

diff --git a/client/tests/kvm/control b/client/tests/kvm/control
index d226adf..be37678 100644
--- a/client/tests/kvm/control
+++ b/client/tests/kvm/control
@@ -35,13 +35,11 @@ str = """
 # build configuration here.  For example:
 #release_tag = 84
 """
-build_cfg = kvm_config.config()
-# As the base test config is quite large, in order to save memory, we use the
-# fork_and_parse() method, that creates another parser process and destroys it
-# at the end of the parsing, so the memory spent can be given back to the OS.
-build_cfg_path = os.path.join(kvm_test_dir, "build.cfg")
-build_cfg.fork_and_parse(build_cfg_path, str)
-if not kvm_utils.run_tests(build_cfg.get_generator(), job):
+
+parser = kvm_config.Parser()
+parser.parse_file(os.path.join(kvm_test_dir, "build.cfg"))
+parser.parse_string(str)
+if not kvm_utils.run_tests(parser.get_dicts(), job):
     logging.error("KVM build step failed, exiting.")
     sys.exit(1)
 
@@ -49,10 +47,11 @@ str = """
 # This string will be parsed after tests.cfg.  Make any desired changes to the
 # test configuration here.  For example:
 #display = sdl
-#install|setup: timeout_multiplier = 3
+#install, setup: timeout_multiplier = 3
 """
-tests_cfg = kvm_config.config()
-tests_cfg_path = os.path.join(kvm_test_dir, "tests.cfg")
+
+parser = kvm_config.Parser()
+parser.parse_file(os.path.join(kvm_test_dir, "tests.cfg"))
 
 if args:
     # We get test parameters from command line
@@ -67,11 +66,12 @@ if args:
                 str += "%s = %s\n" % (key, value)
         except IndexError:
             pass
-tests_cfg.fork_and_parse(tests_cfg_path, str)
+parser.parse_string(str)
 
-# Run the tests
-kvm_utils.run_tests(tests_cfg.get_generator(), job)
+logging.info("Selected tests:")
+for i, d in enumerate(parser.get_dicts()):
+    logging.info("Test %4d:  %s" % (i + 1, d["shortname"]))
+kvm_utils.run_tests(parser.get_dicts(), job)
 
 # Generate a nice HTML report inside the job's results dir
 kvm_utils.create_report(kvm_test_dir, job.resultdir)
-
diff --git a/client/tests/kvm/control.parallel b/client/tests/kvm/control.parallel
index ac84638..640ccf5 100644
--- a/client/tests/kvm/control.parallel
+++ b/client/tests/kvm/control.parallel
@@ -163,16 +163,15 @@ import kvm_config
 str = """
 # This string will be parsed after tests.cfg.  Make any desired changes to the
 # test configuration here.  For example:
-#install|setup: timeout_multiplier = 3
-#only fc8_quick
+#install, setup: timeout_multiplier = 3
 #display = sdl
 """
-cfg = kvm_config.config()
-filename = os.path.join(pwd, "tests.cfg")
-cfg.fork_and_parse(filename, str)
 
-tests = cfg.get_list()
+parser = kvm_config.Parser()
+parser.parse_file(os.path.join(pwd, "tests.cfg"))
+parser.parse_string(str)
 
+tests = list(parser.get_dicts())
 
 # -------------
 # Run the tests
@@ -192,7 +191,6 @@ s = kvm_scheduler.scheduler(tests, num_workers, total_cpus, total_mem, pwd)
 job.parallel([s.scheduler],
              *[(s.worker, i, job.run_test) for i in range(num_workers)])
 
-
 # create the html report in result dir
 reporter = os.path.join(pwd, 'make_html_report.py')
 html_file = os.path.join(job.resultdir,'results.html')
diff --git a/client/tests/kvm/kvm_config.py b/client/tests/kvm/kvm_config.py
index 13cdfe2..1b27181 100755
--- a/client/tests/kvm/kvm_config.py
+++ b/client/tests/kvm/kvm_config.py
@@ -1,18 +1,149 @@
 #!/usr/bin/python
 """
-KVM configuration file utility functions.
+KVM test configuration file parser
 
-@copyright: Red Hat 2008-2010
+@copyright: Red Hat 2008-2011
 """
 
-import logging, re, os, sys, optparse, array, traceback, cPickle
-import common
-import kvm_utils
-from autotest_lib.client.common_lib import error
-from autotest_lib.client.common_lib import logging_manager
+import re, os, sys, optparse, collections
+
+
+# Filter syntax:
+# , means OR
+# .. means AND
+# . means IMMEDIATELY-FOLLOWED-BY
+
+# Example:
+# qcow2..Fedora.14, RHEL.6..raw..boot, smp2..qcow2..migrate..ide
+# means match all dicts whose names have:
+# (qcow2 AND (Fedora IMMEDIATELY-FOLLOWED-BY 14)) OR
+# ((RHEL IMMEDIATELY-FOLLOWED-BY 6) AND raw AND boot) OR
+# (smp2 AND qcow2 AND migrate AND ide)
+
+# Note:
+# 'qcow2..Fedora.14' is equivalent to 'Fedora.14..qcow2'.
+# 'qcow2..Fedora.14' is not equivalent to 'qcow2..14.Fedora'.
+# 'ide, scsi' is equivalent to 'scsi, ide'.
+
+# Filters can be used in 3 ways:
+# only <filter>
+# no <filter>
+# <filter>:
+# The last one starts a conditional block.
+
+
+num_failed_cases = 5
+
+
+class Node(object):
+    def __init__(self):
+        self.name = []
+        self.dep = []
+        self.content = []
+        self.children = []
+        self.labels = set()
+        self.append_to_shortname = False
+        self.failed_cases = collections.deque()
+
+
+# Filter must inherit from object (otherwise type() won't work)
+class Filter(object):
+    def __init__(self, s):
+        self.filter = []
+        for word in s.replace(",", " ").split():
+            word = [block.split(".") for block in word.split("..")]
+            self.filter += [word]
+
+
+    def match_adjacent(self, block, ctx, ctx_set):
+        # TODO: explain what this function does
+        if block[0] not in ctx_set:
+            return 0
+        if len(block) == 1:
+            return 1
+        if block[1] not in ctx_set:
+            return int(ctx[-1] == block[0])
+        k = 0
+        i = ctx.index(block[0])
+        while i < len(ctx):
+            if k > 0 and ctx[i] != block[k]:
+                i -= k - 1
+                k = 0
+            if ctx[i] == block[k]:
+                k += 1
+                if k >= len(block):
+                    break
+                if block[k] not in ctx_set:
+                    break
+            i += 1
+        return k
+
+
+    def might_match_adjacent(self, block, ctx, ctx_set, descendant_labels):
+        matched = self.match_adjacent(block, ctx, ctx_set)
+        for elem in block[matched:]:
+            if elem not in descendant_labels:
+                return False
+        return True
+
+
+    def match(self, ctx, ctx_set):
+        for word in self.filter:
+            for block in word:
+                if self.match_adjacent(block, ctx, ctx_set) != len(block):
+                    break
+            else:
+                return True
+        return False
+
+
+    def might_match(self, ctx, ctx_set, descendant_labels):
+        for word in self.filter:
+            for block in word:
+                if not self.might_match_adjacent(block, ctx, ctx_set,
+                                                 descendant_labels):
+                    break
+            else:
+                return True
+        return False
+
+
+class NoOnlyFilter(Filter):
+    def __init__(self, line):
+        Filter.__init__(self, line.split(None, 1)[1])
+        self.line = line
+
+
+class OnlyFilter(NoOnlyFilter):
+    def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
+                   descendant_labels):
+        for word in self.filter:
+            for block in word:
+                if (self.match_adjacent(block, ctx, ctx_set) >
+                    self.match_adjacent(block, failed_ctx, failed_ctx_set)):
+                    return self.might_match(ctx, ctx_set, descendant_labels)
+        return False
+
 
+class NoFilter(NoOnlyFilter):
+    def might_pass(self, failed_ctx, failed_ctx_set, ctx, ctx_set,
+                   descendant_labels):
+        for word in self.filter:
+            for block in word:
+                if (self.match_adjacent(block, ctx, ctx_set) <
+                    self.match_adjacent(block, failed_ctx, failed_ctx_set)):
+                    return not self.match(ctx, ctx_set)
+        return False
 
-class config:
+
+class Condition(NoFilter):
+    def __init__(self, line):
+        Filter.__init__(self, line.rstrip(":"))
+        self.line = line
+        self.content = []
+
+
+class Parser(object):
     """
     Parse an input file or string that follows the KVM Test Config File format
     and generate a list of dicts that will be later used as configuration
@@ -21,17 +152,14 @@ class config:
     @see: http://www.linux-kvm.org/page/KVM-Autotest/Test_Config_File
     """
 
-    def __init__(self, filename=None, debug=True):
+    def __init__(self, filename=None, debug=False):
         """
-        Initialize the list and optionally parse a file.
+        Initialize the parser and optionally parse a file.
 
-        @param filename: Path of the file that will be taken.
+        @param filename: Path of the file to parse.
         @param debug: Whether to turn on debugging output.
         """
-        self.list = [array.array("H", [4, 4, 4, 4])]
-        self.object_cache = []
-        self.object_cache_indices = {}
-        self.regex_cache = {}
+        self.node = Node()
         self.debug = debug
         if filename:
             self.parse_file(filename)
@@ -39,689 +167,436 @@ class config:
 
     def parse_file(self, filename):
         """
-        Parse file.  If it doesn't exist, raise an IOError.
+        Parse a file.
 
         @param filename: Path of the configuration file.
         """
-        if not os.path.exists(filename):
-            raise IOError("File %s not found" % filename)
-        str = open(filename).read()
-        self.list = self.parse(configreader(filename, str), self.list)
+        self.node = self._parse(FileReader(filename), self.node)
 
 
-    def parse_string(self, str):
+    def parse_string(self, s):
         """
         Parse a string.
 
-        @param str: String to parse.
+        @param s: String to parse.
         """
-        self.list = self.parse(configreader('<string>', str, real_file=False), self.list)
+        self.node = self._parse(StrReader(s), self.node)
 
 
-    def fork_and_parse(self, filename=None, str=None):
-        """
-        Parse a file and/or a string in a separate process to save memory.
-
-        Python likes to keep memory to itself even after the objects occupying
-        it have been destroyed.  If during a call to parse_file() or
-        parse_string() a lot of memory is used, it can only be freed by
-        terminating the process.  This function works around the problem by
-        doing the parsing in a forked process and then terminating it, freeing
-        any unneeded memory.
-
-        Note: if an exception is raised during parsing, its information will be
-        printed, and the resulting list will be empty.  The exception will not
-        be raised in the process calling this function.
-
-        @param filename: Path of file to parse (optional).
-        @param str: String to parse (optional).
-        """
-        r, w = os.pipe()
-        r, w = os.fdopen(r, "r"), os.fdopen(w, "w")
-        pid = os.fork()
-        if not pid:
-            # Child process
-            r.close()
-            try:
-                if filename:
-                    self.parse_file(filename)
-                if str:
-                    self.parse_string(str)
-            except:
-                traceback.print_exc()
-                self.list = []
-            # Convert the arrays to strings before pickling because at least
-            # some Python versions can't pickle/unpickle arrays
-            l = [a.tostring() for a in self.list]
-            cPickle.dump((l, self.object_cache), w, -1)
-            w.close()
-            os._exit(0)
-        else:
-            # Parent process
-            w.close()
-            (l, self.object_cache) = cPickle.load(r)
-            r.close()
-            os.waitpid(pid, 0)
-            self.list = []
-            for s in l:
-                a = array.array("H")
-                a.fromstring(s)
-                self.list.append(a)
-
-
-    def get_generator(self):
+    def get_dicts(self, node=None, ctx=[], content=[], shortname=[], dep=[]):
         """
         Generate dictionaries from the code parsed so far.  This should
-        probably be called after parsing something.
+        be called after parsing something.
 
         @return: A dict generator.
         """
-        for a in self.list:
-            name, shortname, depend, content = _array_get_all(a,
-                                                              self.object_cache)
-            dict = {"name": name, "shortname": shortname, "depend": depend}
-            self._apply_content_to_dict(dict, content)
-            yield dict
-
-
-    def get_list(self):
-        """
-        Generate a list of dictionaries from the code parsed so far.
-        This should probably be called after parsing something.
+        def apply_ops_to_dict(d, content):
+            for filename, linenum, s in content:
+                op_found = None
+                op_pos = len(s)
+                for op in ops:
+                    if op in s:
+                        pos = s.index(op)
+                        if pos < op_pos:
+                            op_found = op
+                            op_pos = pos
+                if not op_found:
+                    continue
+                left, value = map(str.strip, s.split(op_found, 1))
+                if value and ((value[0] == '"' and value[-1] == '"') or
+                              (value[0] == "'" and value[-1] == "'")):
+                    value = value[1:-1]
+                filters_and_key = map(str.strip, left.split(":"))
+                for f in filters_and_key[:-1]:
+                    if not Filter(f).match(ctx, ctx_set):
+                        break
+                else:
+                    key = filters_and_key[-1]
+                    ops[op_found](d, key, value)
+
+        def process_content(content, failed_filters):
+            # 1. Check that the filters in content are OK with the current
+            #    context (ctx).
+            # 2. Move the parts of content that are still relevant into
+            #    new_content and unpack conditional blocks if appropriate.
+            #    For example, if an 'only' statement fully matches ctx, it
+            #    becomes irrelevant and is not appended to new_content.
+            #    If a conditional block fully matches, its contents are
+            #    unpacked into new_content.
+            # 3. Move failed filters into failed_filters, so that next time we
+            #    reach this node or one of its ancestors, we'll check those
+            #    filters first.
+            for t in content:
+                filename, linenum, obj = t
+                if type(obj) is str:
+                    new_content.append(t)
+                    continue
+                elif type(obj) is OnlyFilter:
+                    if not obj.might_match(ctx, ctx_set, labels):
+                        self._debug("    filter did not pass: %r (%s:%s)",
+                                    obj.line, filename, linenum)
+                        failed_filters.append(t)
+                        return False
+                    elif obj.match(ctx, ctx_set):
+                        continue
+                elif type(obj) is NoFilter:
+                    if obj.match(ctx, ctx_set):
+                        self._debug("    filter did not pass: %r (%s:%s)",
+                                    obj.line, filename, linenum)
+                        failed_filters.append(t)
+                        return False
+                    elif not obj.might_match(ctx, ctx_set, labels):
+                        continue
+                elif type(obj) is Condition:
+                    if obj.match(ctx, ctx_set):
+                        self._debug("    conditional block matches: %r (%s:%s)",
+                                    obj.line, filename, linenum)
+                        # Check and unpack the content inside this Condition
+                        # object (note: the failed filters should go into
+                        # new_internal_filters because we don't expect them to
+                        # come from outside this node, even if the Condition
+                        # itself was external)
+                        if not process_content(obj.content,
+                                               new_internal_filters):
+                            failed_filters.append(t)
+                            return False
+                        continue
+                    elif not obj.might_match(ctx, ctx_set, labels):
+                        continue
+                new_content.append(t)
+            return True
+
+        def might_pass(failed_ctx,
+                       failed_ctx_set,
+                       failed_external_filters,
+                       failed_internal_filters):
+            for t in failed_external_filters:
+                if t not in content:
+                    return True
+                filename, linenum, filter = t
+                if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
+                                     labels):
+                    return True
+            for t in failed_internal_filters:
+                filename, linenum, filter = t
+                if filter.might_pass(failed_ctx, failed_ctx_set, ctx, ctx_set,
+                                     labels):
+                    return True
+            return False
+
+        def add_failed_case():
+            node.failed_cases.appendleft((ctx, ctx_set,
+                                          new_external_filters,
+                                          new_internal_filters))
+            if len(node.failed_cases) > num_failed_cases:
+                node.failed_cases.pop()
+
+        node = node or self.node
+        # Update dep
+        for d in node.dep:
+            temp = ctx + [d]
+            dep = dep + [".".join([s for s in temp if s])]
+        # Update ctx
+        ctx = ctx + node.name
+        ctx_set = set(ctx)
+        labels = node.labels
+        # Get the current name
+        name = ".".join([s for s in ctx if s])
+        if node.name:
+            self._debug("checking out %r", name)
+        # Check previously failed filters
+        for i, failed_case in enumerate(node.failed_cases):
+            if not might_pass(*failed_case):
+                self._debug("    this subtree has failed before")
+                del node.failed_cases[i]
+                node.failed_cases.appendleft(failed_case)
+                return
+        # Check content and unpack it into new_content
+        new_content = []
+        new_external_filters = []
+        new_internal_filters = []
+        if (not process_content(node.content, new_internal_filters) or
+            not process_content(content, new_external_filters)):
+            add_failed_case()
+            return
+        # Update shortname
+        if node.append_to_shortname:
+            shortname = shortname + node.name
+        # Recurse into children
+        count = 0
+        for n in node.children:
+            for d in self.get_dicts(n, ctx, new_content, shortname, dep):
+                count += 1
+                yield d
+        # Reached leaf?
+        if not node.children:
+            self._debug("    reached leaf, returning it")
+            d = {"name": name, "dep": dep,
+                 "shortname": ".".join([s for s in shortname if s])}
+            apply_ops_to_dict(d, new_content)
+            yield d
+        # If this node did not produce any dicts, remember the failed filters
+        # of its descendants
+        elif not count:
+            new_external_filters = []
+            new_internal_filters = []
+            for n in node.children:
+                (failed_ctx,
+                 failed_ctx_set,
+                 failed_external_filters,
+                 failed_internal_filters) = n.failed_cases[0]
+                for obj in failed_internal_filters:
+                    if obj not in new_internal_filters:
+                        new_internal_filters.append(obj)
+                for obj in failed_external_filters:
+                    if obj in content:
+                        if obj not in new_external_filters:
+                            new_external_filters.append(obj)
+                    else:
+                        if obj not in new_internal_filters:
+                            new_internal_filters.append(obj)
+            add_failed_case()
 
-        @return: A list of dicts.
-        """
-        return list(self.get_generator())
 
+    def _debug(self, s, *args):
+        if self.debug:
+            s = "DEBUG: %s" % s
+            print s % args
 
-    def count(self, filter=".*"):
-        """
-        Return the number of dictionaries whose names match filter.
 
-        @param filter: A regular expression string.
-        """
-        exp = self._get_filter_regex(filter)
-        count = 0
-        for a in self.list:
-            name = _array_get_name(a, self.object_cache)
-            if exp.search(name):
-                count += 1
-        return count
+    def _warn(self, s, *args):
+        s = "WARNING: %s" % s
+        print s % args
 
 
-    def parse_variants(self, cr, list, subvariants=False, prev_indent=-1):
+    def _parse_variants(self, cr, node, prev_indent=-1):
         """
-        Read and parse lines from a configreader object until a line with an
+        Read and parse lines from a FileReader object until a line with an
         indent level lower than or equal to prev_indent is encountered.
 
-        @brief: Parse a 'variants' or 'subvariants' block from a configreader
-            object.
-        @param cr: configreader object to be parsed.
-        @param list: List of arrays to operate on.
-        @param subvariants: If True, parse in 'subvariants' mode;
-            otherwise parse in 'variants' mode.
+        @param cr: A FileReader/StrReader object.
+        @param node: A node to operate on.
         @param prev_indent: The indent level of the "parent" block.
-        @return: The resulting list of arrays.
+        @return: A node object.
         """
-        new_list = []
+        node4 = Node()
 
         while True:
-            pos = cr.tell()
-            (indented_line, line, indent) = cr.get_next_line()
-            if indent <= prev_indent:
-                cr.seek(pos)
+            line, indent, linenum = cr.get_next_line(prev_indent)
+            if not line:
                 break
 
-            # Get name and dependencies
-            (name, depend) = map(str.strip, line.lstrip("- ").split(":"))
+            name, dep = map(str.strip, line.lstrip("- ").split(":"))
 
-            # See if name should be added to the 'shortname' field
-            add_to_shortname = not name.startswith("@")
-            name = name.lstrip("@")
+            node2 = Node()
+            node2.children = [node]
+            node2.labels = node.labels
 
-            # Store name and dependencies in cache and get their indices
-            n = self._store_str(name)
-            d = self._store_str(depend)
+            node3 = self._parse(cr, node2, prev_indent=indent)
+            node3.name = name.lstrip("@").split(".")
+            node3.dep = dep.replace(",", " ").split()
+            node3.append_to_shortname = not name.startswith("@")
 
-            # Make a copy of list
-            temp_list = [a[:] for a in list]
+            node4.children += [node3]
+            node4.labels.update(node3.labels)
+            node4.labels.update(node3.name)
 
-            if subvariants:
-                # If we're parsing 'subvariants', first modify the list
-                if add_to_shortname:
-                    for a in temp_list:
-                        _array_append_to_name_shortname_depend(a, n, d)
-                else:
-                    for a in temp_list:
-                        _array_append_to_name_depend(a, n, d)
-                temp_list = self.parse(cr, temp_list, restricted=True,
-                                       prev_indent=indent)
-            else:
-                # If we're parsing 'variants', parse before modifying the list
-                if self.debug:
-                    _debug_print(indented_line,
-                                 "Entering variant '%s' "
-                                 "(variant inherits %d dicts)" %
-                                 (name, len(list)))
-                temp_list = self.parse(cr, temp_list, restricted=False,
-                                       prev_indent=indent)
-                if add_to_shortname:
-                    for a in temp_list:
-                        _array_prepend_to_name_shortname_depend(a, n, d)
-                else:
-                    for a in temp_list:
-                        _array_prepend_to_name_depend(a, n, d)
+        return node4
 
-            new_list += temp_list
 
-        return new_list
-
-
-    def parse(self, cr, list, restricted=False, prev_indent=-1):
+    def _parse(self, cr, node, prev_indent=-1):
         """
-        Read and parse lines from a configreader object until a line with an
+        Read and parse lines from a StrReader object until a line with an
         indent level lower than or equal to prev_indent is encountered.
 
-        @brief: Parse a configreader object.
-        @param cr: A configreader object.
-        @param list: A list of arrays to operate on (list is modified in
-            place and should not be used after the call).
-        @param restricted: If True, operate in restricted mode
-            (prohibit 'variants').
+        @param cr: A FileReader/StrReader object.
+        @param node: A Node or a Condition object to operate on.
         @param prev_indent: The indent level of the "parent" block.
-        @return: The resulting list of arrays.
-        @note: List is destroyed and should not be used after the call.
-            Only the returned list should be used.
+        @return: A node object.
         """
-        current_block = ""
-
         while True:
-            pos = cr.tell()
-            (indented_line, line, indent) = cr.get_next_line()
-            if indent <= prev_indent:
-                cr.seek(pos)
-                self._append_content_to_arrays(list, current_block)
+            line, indent, linenum = cr.get_next_line(prev_indent)
+            if not line:
                 break
 
-            len_list = len(list)
-
-            # Parse assignment operators (keep lines in temporary buffer)
-            if "=" in line:
-                if self.debug and not restricted:
-                    _debug_print(indented_line,
-                                 "Parsing operator (%d dicts in current "
-                                 "context)" % len_list)
-                current_block += line + "\n"
-                continue
-
-            # Flush the temporary buffer
-            self._append_content_to_arrays(list, current_block)
-            current_block = ""
-
             words = line.split()
 
-            # Parse 'no' and 'only' statements
-            if words[0] == "no" or words[0] == "only":
-                if len(words) <= 1:
-                    continue
-                filters = map(self._get_filter_regex, words[1:])
-                filtered_list = []
-                if words[0] == "no":
-                    for a in list:
-                        name = _array_get_name(a, self.object_cache)
-                        for filter in filters:
-                            if filter.search(name):
-                                break
-                        else:
-                            filtered_list.append(a)
-                if words[0] == "only":
-                    for a in list:
-                        name = _array_get_name(a, self.object_cache)
-                        for filter in filters:
-                            if filter.search(name):
-                                filtered_list.append(a)
-                                break
-                list = filtered_list
-                if self.debug and not restricted:
-                    _debug_print(indented_line,
-                                 "Parsing no/only (%d dicts in current "
-                                 "context, %d remain)" %
-                                 (len_list, len(list)))
-                continue
-
             # Parse 'variants'
             if line == "variants:":
-                # 'variants' not allowed in restricted mode
-                # (inside an exception or inside subvariants)
-                if restricted:
-                    e_msg = "Using variants in this context is not allowed"
-                    cr.raise_error(e_msg)
-                if self.debug and not restricted:
-                    _debug_print(indented_line,
-                                 "Entering variants block (%d dicts in "
-                                 "current context)" % len_list)
-                list = self.parse_variants(cr, list, subvariants=False,
-                                           prev_indent=indent)
-                continue
-
-            # Parse 'subvariants' (the block is parsed for each dict
-            # separately)
-            if line == "subvariants:":
-                if self.debug and not restricted:
-                    _debug_print(indented_line,
-                                 "Entering subvariants block (%d dicts in "
-                                 "current context)" % len_list)
-                new_list = []
-                # Remember current position
-                pos = cr.tell()
-                # Read the lines in any case
-                self.parse_variants(cr, [], subvariants=True,
-                                    prev_indent=indent)
-                # Iterate over the list...
-                for index in xrange(len(list)):
-                    # Revert to initial position in this 'subvariants' block
-                    cr.seek(pos)
-                    # Everything inside 'subvariants' should be parsed in
-                    # restricted mode
-                    new_list += self.parse_variants(cr, list[index:index+1],
-                                                    subvariants=True,
-                                                    prev_indent=indent)
-                list = new_list
+                # 'variants' is not allowed inside a conditional block
+                if isinstance(node, Condition):
+                    raise ValueError("'variants' is not allowed inside a "
+                                     "conditional block (%s:%s)" %
+                                     (cr.filename, linenum))
+                node = self._parse_variants(cr, node, prev_indent=indent)
                 continue
 
             # Parse 'include' statements
             if words[0] == "include":
-                if len(words) <= 1:
+                if len(words) < 2:
+                    self._warn("%r (%s:%s): missing parameter. What are you "
+                               "including?", line, cr.filename, linenum)
                     continue
-                if self.debug and not restricted:
-                    _debug_print(indented_line, "Entering file %s" % words[1])
-
-                cur_filename = cr.real_filename()
-                if cur_filename is None:
-                    cr.raise_error("'include' is valid only when parsing a file")
-
-                filename = os.path.join(os.path.dirname(cur_filename),
-                                        words[1])
-                if not os.path.exists(filename):
-                    cr.raise_error("Cannot include %s -- file not found" % (filename))
-
-                str = open(filename).read()
-                list = self.parse(configreader(filename, str), list, restricted)
-                if self.debug and not restricted:
-                    _debug_print("", "Leaving file %s" % words[1])
+                if not isinstance(cr, FileReader):
+                    self._warn("%r (%s:%s): cannot include because no file is "
+                               "currently open", line, cr.filename, linenum)
+                    continue
+                filename = os.path.join(os.path.dirname(cr.filename), words[1])
+                if not os.path.isfile(filename):
+                    self._warn("%r (%s:%s): file doesn't exist or is not a "
+                               "regular file", line, cr.filename, linenum)
+                    continue
+                node = self._parse(FileReader(filename), node)
+                continue
 
+            # Parse 'only' and 'no' filters
+            if words[0] in ("only", "no"):
+                if len(words) < 2:
+                    self._warn("%r (%s:%s): missing parameter", line,
+                               cr.filename, linenum)
+                    continue
+                if words[0] == "only":
+                    node.content += [(cr.filename, linenum, OnlyFilter(line))]
+                elif words[0] == "no":
+                    node.content += [(cr.filename, linenum, NoFilter(line))]
                 continue
 
-            # Parse multi-line exceptions
-            # (the block is parsed for each dict separately)
+            # Parse conditional blocks
             if line.endswith(":"):
-                if self.debug and not restricted:
-                    _debug_print(indented_line,
-                                 "Entering multi-line exception block "
-                                 "(%d dicts in current context outside "
-                                 "exception)" % len_list)
-                line = line[:-1]
-                new_list = []
-                # Remember current position
-                pos = cr.tell()
-                # Read the lines in any case
-                self.parse(cr, [], restricted=True, prev_indent=indent)
-                # Iterate over the list...
-                exp = self._get_filter_regex(line)
-                for index in xrange(len(list)):
-                    name = _array_get_name(list[index], self.object_cache)
-                    if exp.search(name):
-                        # Revert to initial position in this exception block
-                        cr.seek(pos)
-                        # Everything inside an exception should be parsed in
-                        # restricted mode
-                        new_list += self.parse(cr, list[index:index+1],
-                                               restricted=True,
-                                               prev_indent=indent)
-                    else:
-                        new_list.append(list[index])
-                list = new_list
+                cond = Condition(line)
+                self._parse(cr, cond, prev_indent=indent)
+                node.content += [(cr.filename, linenum, cond)]
                 continue
 
-        return list
+            node.content += [(cr.filename, linenum, line)]
+            continue
 
-
-    def _get_filter_regex(self, filter):
-        """
-        Return a regex object corresponding to a given filter string.
-
-        All regular expressions given to the parser are passed through this
-        function first.  Its purpose is to make them more specific and better
-        suited to match dictionary names: it forces simple expressions to match
-        only between dots or at the beginning or end of a string.  For example,
-        the filter 'foo' will match 'foo.bar' but not 'foobar'.
-        """
-        try:
-            return self.regex_cache[filter]
-        except KeyError:
-            exp = re.compile(r"(\.|^)(%s)(\.|$)" % filter)
-            self.regex_cache[filter] = exp
-            return exp
-
-
-    def _store_str(self, str):
-        """
-        Store str in the internal object cache, if it isn't already there, and
-        return its identifying index.
-
-        @param str: String to store.
-        @return: The index of str in the object cache.
-        """
-        try:
-            return self.object_cache_indices[str]
-        except KeyError:
-            self.object_cache.append(str)
-            index = len(self.object_cache) - 1
-            self.object_cache_indices[str] = index
-            return index
-
-
-    def _append_content_to_arrays(self, list, content):
-        """
-        Append content (config code containing assignment operations) to a list
-        of arrays.
-
-        @param list: List of arrays to operate on.
-        @param content: String containing assignment operations.
-        """
-        if content:
-            str_index = self._store_str(content)
-            for a in list:
-                _array_append_to_content(a, str_index)
-
-
-    def _apply_content_to_dict(self, dict, content):
-        """
-        Apply the operations in content (config code containing assignment
-        operations) to a dict.
-
-        @param dict: Dictionary to operate on.  Must have 'name' key.
-        @param content: String containing assignment operations.
-        """
-        for line in content.splitlines():
-            op_found = None
-            op_pos = len(line)
-            for op in ops:
-                pos = line.find(op)
-                if pos >= 0 and pos < op_pos:
-                    op_found = op
-                    op_pos = pos
-            if not op_found:
-                continue
-            (left, value) = map(str.strip, line.split(op_found, 1))
-            if value and ((value[0] == '"' and value[-1] == '"') or
-                          (value[0] == "'" and value[-1] == "'")):
-                value = value[1:-1]
-            filters_and_key = map(str.strip, left.split(":"))
-            filters = filters_and_key[:-1]
-            key = filters_and_key[-1]
-            for filter in filters:
-                exp = self._get_filter_regex(filter)
-                if not exp.search(dict["name"]):
-                    break
-            else:
-                ops[op_found](dict, key, value)
+        return node
 
 
 # Assignment operators
 
-def _op_set(dict, key, value):
-    dict[key] = value
+def _op_set(d, key, value):
+    d[key] = value
 
 
-def _op_append(dict, key, value):
-    dict[key] = dict.get(key, "") + value
+def _op_append(d, key, value):
+    d[key] = d.get(key, "") + value
 
 
-def _op_prepend(dict, key, value):
-    dict[key] = value + dict.get(key, "")
+def _op_prepend(d, key, value):
+    d[key] = value + d.get(key, "")
 
 
-def _op_regex_set(dict, exp, value):
+def _op_regex_set(d, exp, value):
     exp = re.compile("^(%s)$" % exp)
-    for key in dict:
+    for key in d:
         if exp.match(key):
-            dict[key] = value
+            d[key] = value
 
 
-def _op_regex_append(dict, exp, value):
+def _op_regex_append(d, exp, value):
     exp = re.compile("^(%s)$" % exp)
-    for key in dict:
+    for key in d:
         if exp.match(key):
-            dict[key] += value
+            d[key] += value
 
 
-def _op_regex_prepend(dict, exp, value):
+def _op_regex_prepend(d, exp, value):
     exp = re.compile("^(%s)$" % exp)
-    for key in dict:
+    for key in d:
         if exp.match(key):
-            dict[key] = value + dict[key]
-
+            d[key] = value + d[key]
 
-ops = {
-    "=": _op_set,
-    "+=": _op_append,
-    "<=": _op_prepend,
-    "?=": _op_regex_set,
-    "?+=": _op_regex_append,
-    "?<=": _op_regex_prepend,
-}
-
-
-# Misc functions
-
-def _debug_print(str1, str2=""):
-    """
-    Nicely print two strings and an arrow.
 
-    @param str1: First string.
-    @param str2: Second string.
-    """
-    if str2:
-        str = "%-50s ---> %s" % (str1, str2)
-    else:
-        str = str1
-    logging.debug(str)
+ops = {"=": _op_set,
+       "+=": _op_append,
+       "<=": _op_prepend,
+       "?=": _op_regex_set,
+       "?+=": _op_regex_append,
+       "?<=": _op_regex_prepend}
 
 
-# configreader
+# StrReader and FileReader
 
-class configreader:
+class StrReader(object):
     """
-    Preprocess an input string and provide file-like services.
-    This is intended as a replacement for the file and StringIO classes,
-    whose readline() and/or seek() methods seem to be slow.
+    Preprocess an input string for easy reading.
     """
-
-    def __init__(self, filename, str, real_file=True):
+    def __init__(self, s):
         """
         Initialize the reader.
 
-        @param filename: the filename we're parsing
-        @param str: The string to parse.
-        @param real_file: Indicates if filename represents a real file. Defaults to True.
+        @param s: The string to parse.
         """
-        self.filename = filename
-        self.is_real_file = real_file
-        self.line_index = 0
-        self.lines = []
-        self.real_number = []
-        for num, line in enumerate(str.splitlines()):
+        self.filename = "<string>"
+        self._lines = []
+        self._line_index = 0
+        for linenum, line in enumerate(s.splitlines()):
             line = line.rstrip().expandtabs()
-            stripped_line = line.strip()
+            stripped_line = line.lstrip()
             indent = len(line) - len(stripped_line)
             if (not stripped_line
                 or stripped_line.startswith("#")
                 or stripped_line.startswith("//")):
                 continue
-            self.lines.append((line, stripped_line, indent))
-            self.real_number.append(num + 1)
-
-
-    def real_filename(self):
-        """Returns the filename we're reading, in case it is a real file
-
-        @returns the filename we are parsing, or None in case we're not parsing a real file
-        """
-        if self.is_real_file:
-            return self.filename
-
-    def get_next_line(self):
-        """
-        Get the next non-empty, non-comment line in the string.
+            self._lines.append((stripped_line, indent, linenum + 1))
 
-        @param file: File like object.
-        @return: (line, stripped_line, indent), where indent is the line's
-            indent level or -1 if no line is available.
-        """
-        try:
-            if self.line_index < len(self.lines):
-                return self.lines[self.line_index]
-            else:
-                return (None, None, -1)
-        finally:
-            self.line_index += 1
 
-
-    def tell(self):
-        """
-        Return the current line index.
-        """
-        return self.line_index
-
-
-    def seek(self, index):
-        """
-        Set the current line index.
+    def get_next_line(self, prev_indent):
         """
-        self.line_index = index
+        Get the next non-empty, non-comment line in the string, whose
+        indentation level is higher than prev_indent.
 
-    def raise_error(self, msg):
-        """Raise an error related to the last line returned by get_next_line()
+        @param prev_indent: The indentation level of the previous block.
+        @return: (line, indent, linenum), where indent is the line's
+            indentation level.  If no line is available, (None, -1, -1) is
+            returned.
         """
-        if self.line_index == 0: # nothing was read. shouldn't happen, but...
-            line_id = 'BEGIN'
-        elif self.line_index >= len(self.lines): # past EOF
-            line_id = 'EOF'
-        else:
-            # line_index is the _next_ line. get the previous one
-            line_id = str(self.real_number[self.line_index-1])
-        raise error.AutotestError("%s:%s: %s" % (self.filename, line_id, msg))
-
-
-# Array structure:
-# ----------------
-# The first 4 elements contain the indices of the 4 segments.
-# a[0] -- Index of beginning of 'name' segment (always 4).
-# a[1] -- Index of beginning of 'shortname' segment.
-# a[2] -- Index of beginning of 'depend' segment.
-# a[3] -- Index of beginning of 'content' segment.
-# The next elements in the array comprise the aforementioned segments:
-# The 'name' segment begins with a[a[0]] and ends with a[a[1]-1].
-# The 'shortname' segment begins with a[a[1]] and ends with a[a[2]-1].
-# The 'depend' segment begins with a[a[2]] and ends with a[a[3]-1].
-# The 'content' segment begins with a[a[3]] and ends at the end of the array.
-
-# The following functions append/prepend to various segments of an array.
-
-def _array_append_to_name_shortname_depend(a, name, depend):
-    a.insert(a[1], name)
-    a.insert(a[2] + 1, name)
-    a.insert(a[3] + 2, depend)
-    a[1] += 1
-    a[2] += 2
-    a[3] += 3
-
-
-def _array_prepend_to_name_shortname_depend(a, name, depend):
-    a[1] += 1
-    a[2] += 2
-    a[3] += 3
-    a.insert(a[0], name)
-    a.insert(a[1], name)
-    a.insert(a[2], depend)
-
+        if self._line_index >= len(self._lines):
+            return None, -1, -1
+        line, indent, linenum = self._lines[self._line_index]
+        if indent <= prev_indent:
+            return None, -1, -1
+        self._line_index += 1
+        return line, indent, linenum
 
-def _array_append_to_name_depend(a, name, depend):
-    a.insert(a[1], name)
-    a.insert(a[3] + 1, depend)
-    a[1] += 1
-    a[2] += 1
-    a[3] += 2
 
-
-def _array_prepend_to_name_depend(a, name, depend):
-    a[1] += 1
-    a[2] += 1
-    a[3] += 2
-    a.insert(a[0], name)
-    a.insert(a[2], depend)
-
-
-def _array_append_to_content(a, content):
-    a.append(content)
-
-
-def _array_get_name(a, object_cache):
-    """
-    Return the name of a dictionary represented by a given array.
-
-    @param a: Array representing a dictionary.
-    @param object_cache: A list of strings referenced by elements in the array.
+class FileReader(StrReader):
     """
-    return ".".join([object_cache[i] for i in a[a[0]:a[1]]])
-
-
-def _array_get_all(a, object_cache):
+    Preprocess an input file for easy reading.
     """
-    Return a 4-tuple containing all the data stored in a given array, in a
-    format that is easy to turn into an actual dictionary.
+    def __init__(self, filename):
+        """
+        Initialize the reader.
 
-    @param a: Array representing a dictionary.
-    @param object_cache: A list of strings referenced by elements in the array.
-    @return: A 4-tuple: (name, shortname, depend, content), in which all
-        members are strings except depend which is a list of strings.
-    """
-    name = ".".join([object_cache[i] for i in a[a[0]:a[1]]])
-    shortname = ".".join([object_cache[i] for i in a[a[1]:a[2]]])
-    content = "".join([object_cache[i] for i in a[a[3]:]])
-    depend = []
-    prefix = ""
-    for n, d in zip(a[a[0]:a[1]], a[a[2]:a[3]]):
-        for dep in object_cache[d].split():
-            depend.append(prefix + dep)
-        prefix += object_cache[n] + "."
-    return name, shortname, depend, content
+        @parse filename: The name of the input file.
+        """
+        StrReader.__init__(self, open(filename).read())
+        self.filename = filename
 
 
 if __name__ == "__main__":
-    parser = optparse.OptionParser("usage: %prog [options] [filename]")
-    parser.add_option('--verbose', dest="debug", action='store_true',
-                      help='include debug messages in console output')
+    parser = optparse.OptionParser("usage: %prog [options] <filename>")
+    parser.add_option("-v", "--verbose", dest="debug", action="store_true",
+                      help="include debug messages in console output")
+    parser.add_option("-f", "--fullname", dest="fullname", action="store_true",
+                      help="show full dict names instead of short names")
+    parser.add_option("-c", "--contents", dest="contents", action="store_true",
+                      help="show dict contents")
 
     options, args = parser.parse_args()
-    debug = options.debug
-    if args:
-        filenames = args
-    else:
-        filenames = [os.path.join(os.path.dirname(sys.argv[0]), "tests.cfg")]
-
-    # Here we configure the stand alone program to use the autotest
-    # logging system.
-    logging_manager.configure_logging(kvm_utils.KvmLoggingConfig(),
-                                      verbose=debug)
-    cfg = config(debug=debug)
-    for fn in filenames:
-        cfg.parse_file(fn)
-    dicts = cfg.get_generator()
-    for i, dict in enumerate(dicts):
-        print "Dictionary #%d:" % (i)
-        keys = dict.keys()
-        keys.sort()
-        for key in keys:
-            print "    %s = %s" % (key, dict[key])
+    if not args:
+        parser.error("filename required")
+
+    c = Parser(args[0], debug=options.debug)
+    for i, d in enumerate(c.get_dicts()):
+        if options.fullname:
+            print "dict %4d:  %s" % (i + 1, d["name"])
+        else:
+            print "dict %4d:  %s" % (i + 1, d["shortname"])
+        if options.contents:
+            keys = d.keys()
+            keys.sort()
+            for key in keys:
+                print "    %s = %s" % (key, d[key])
diff --git a/client/tests/kvm/kvm_scheduler.py b/client/tests/kvm/kvm_scheduler.py
index 95282e4..b96bb32 100644
--- a/client/tests/kvm/kvm_scheduler.py
+++ b/client/tests/kvm/kvm_scheduler.py
@@ -63,7 +63,6 @@ class scheduler:
                 test_index = int(cmd[1])
                 test = self.tests[test_index].copy()
                 test.update(self_dict)
-                test = kvm_utils.get_sub_pool(test, index, self.num_workers)
                 test_iterations = int(test.get("iterations", 1))
                 status = run_test_func("kvm", params=test,
                                        tag=test.get("shortname"),
@@ -129,7 +128,7 @@ class scheduler:
                     # If the test failed, mark all dependent tests as "failed" too
                     if not status:
                         for i, other_test in enumerate(self.tests):
-                            for dep in other_test.get("depend", []):
+                            for dep in other_test.get("dep", []):
                                 if dep in test["name"]:
                                     test_status[i] = "fail"
 
@@ -154,7 +153,7 @@ class scheduler:
                         continue
                     # Make sure the test's dependencies are satisfied
                     dependencies_satisfied = True
-                    for dep in test["depend"]:
+                    for dep in test["dep"]:
                         dependencies = [j for j, t in enumerate(self.tests)
                                         if dep in t["name"]]
                         bad_status_deps = [j for j in dependencies
@@ -200,14 +199,14 @@ class scheduler:
                     used_mem[worker] = test_used_mem
                     # Assign all related tests to this worker
                     for j, other_test in enumerate(self.tests):
-                        for other_dep in other_test["depend"]:
+                        for other_dep in other_test["dep"]:
                             # All tests that depend on this test
                             if other_dep in test["name"]:
                                 test_worker[j] = worker
                                 break
                             # ... and all tests that share a dependency
                             # with this test
-                            for dep in test["depend"]:
+                            for dep in test["dep"]:
                                 if dep in other_dep or other_dep in dep:
                                     test_worker[j] = worker
                                     break
diff --git a/client/tests/kvm/kvm_utils.py b/client/tests/kvm/kvm_utils.py
index 44ebb88..9e25a0a 100644
--- a/client/tests/kvm/kvm_utils.py
+++ b/client/tests/kvm/kvm_utils.py
@@ -1101,7 +1101,7 @@ def run_tests(test_list, job):
         if dict.get("skip") == "yes":
             continue
         dependencies_satisfied = True
-        for dep in dict.get("depend"):
+        for dep in dict.get("dep"):
             for test_name in status_dict.keys():
                 if not dep in test_name:
                     continue
diff --git a/client/tests/kvm/tests.cfg.sample b/client/tests/kvm/tests.cfg.sample
index bde7aba..4b3b965 100644
--- a/client/tests/kvm/tests.cfg.sample
+++ b/client/tests/kvm/tests.cfg.sample
@@ -18,10 +18,9 @@ include cdkeys.cfg
 image_name(_.*)? ?<= /tmp/kvm_autotest_root/images/
 cdrom(_.*)? ?<= /tmp/kvm_autotest_root/
 floppy ?<= /tmp/kvm_autotest_root/
-Linux:
-    unattended_install:
-        kernel ?<= /tmp/kvm_autotest_root/
-        initrd ?<= /tmp/kvm_autotest_root/
+Linux..unattended_install:
+    kernel ?<= /tmp/kvm_autotest_root/
+    initrd ?<= /tmp/kvm_autotest_root/
 
 # Here are the test sets variants. The variant 'qemu_kvm_windows_quick' is
 # fully commented, the following ones have comments only on noteworthy points
@@ -49,7 +48,7 @@ variants:
         # Operating system choice
         only Win7.64
         # Subtest choice. You can modify that line to add more subtests
-        only unattended_install.cdrom boot shutdown
+        only unattended_install.cdrom, boot, shutdown
 
     # Runs qemu, f14 64 bit guest OS, install, boot, shutdown
     - @qemu_f14_quick:
@@ -65,7 +64,7 @@ variants:
         only no_pci_assignable
         only smallpages
         only Fedora.14.64
-        only unattended_install.cdrom boot shutdown
+        only unattended_install.cdrom, boot, shutdown
         # qemu needs -enable-kvm on the cmdline
         extra_params += ' -enable-kvm'
 
@@ -81,7 +80,7 @@ variants:
         only no_pci_assignable
         only smallpages
         only Fedora.14.64
-        only unattended_install.cdrom boot shutdown
+        only unattended_install.cdrom, boot, shutdown
 
 # You may provide information about the DTM server for WHQL tests here:
 #whql:
diff --git a/client/tests/kvm/tests_base.cfg.sample b/client/tests/kvm/tests_base.cfg.sample
index 80362db..e65bed2 100644
--- a/client/tests/kvm/tests_base.cfg.sample
+++ b/client/tests/kvm/tests_base.cfg.sample
@@ -1722,8 +1722,8 @@ variants:
 
     # Windows section
     - @Windows:
-        no autotest linux_s3 vlan ioquit unattended_install.(url|nfs|remote_ks)
-        no jumbo nicdriver_unload nic_promisc multicast mac_change ethtool clock_getres
+        no autotest, linux_s3, vlan, ioquit, unattended_install.url, unattended_install.nfs, unattended_install.remote_ks
+        no jumbo, nicdriver_unload, nic_promisc, multicast, mac_change, ethtool, clock_getres
 
         shutdown_command = shutdown /s /f /t 0
         reboot_command = shutdown /r /f /t 0
@@ -1747,7 +1747,7 @@ variants:
         mem_chk_cmd = wmic memphysical
         mem_chk_cur_cmd = wmic memphysical
 
-        unattended_install.cdrom|whql.support_vm_install:
+        unattended_install.cdrom, whql.support_vm_install:
             timeout = 7200
             finish_program = deps/finish.exe
             cdroms += " winutils"
@@ -1857,7 +1857,7 @@ variants:
                             steps = WinXP-32.steps
                         setup:
                             steps = WinXP-32-rss.steps
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/WindowsXP-sp2-vlk.iso
                             md5sum_cd1 = 743450644b1d9fe97b3cf379e22dceb0
                             md5sum_1m_cd1 = b473bf75af2d1269fec8958cf0202bfd
@@ -1890,7 +1890,7 @@ variants:
                             steps = WinXP-64.steps
                         setup:
                             steps = WinXP-64-rss.steps
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/WindowsXP-64.iso
                             md5sum_cd1 = 8d3f007ec9c2060cec8a50ee7d7dc512
                             md5sum_1m_cd1 = e812363ff427effc512b7801ee70e513
@@ -1928,7 +1928,7 @@ variants:
                             steps = Win2003-32.steps
                         setup:
                             steps = Win2003-32-rss.steps
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/Windows2003_r2_VLK.iso
                             md5sum_cd1 = 03e921e9b4214773c21a39f5c3f42ef7
                             md5sum_1m_cd1 = 37c2fdec15ac4ec16aa10fdfdb338aa3
@@ -1960,7 +1960,7 @@ variants:
                             steps = Win2003-64.steps
                         setup:
                             steps = Win2003-64-rss.steps
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/Windows2003-x64.iso
                             md5sum_cd1 = 5703f87c9fd77d28c05ffadd3354dbbd
                             md5sum_1m_cd1 = 439393c384116aa09e08a0ad047dcea8
@@ -2008,7 +2008,7 @@ variants:
                                     steps = Win-Vista-32.steps
                                 setup:
                                     steps = WinVista-32-rss.steps
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/WindowsVista-32.iso
                                     md5sum_cd1 = 1008f323d5170c8e614e52ccb85c0491
                                     md5sum_1m_cd1 = c724e9695da483bc0fd59e426eaefc72
@@ -2025,7 +2025,7 @@ variants:
 
                             - sp2:
                                 image_name += -sp2-32
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/en_windows_vista_with_sp2_x86_dvd_342266.iso
                                     md5sum_cd1 = 19ca90a425667812977bab6f4ce24175
                                     md5sum_1m_cd1 = 89c15020e0e6125be19acf7a2e5dc614
@@ -2059,7 +2059,7 @@ variants:
                                     steps = Win-Vista-64.steps
                                 setup:
                                     steps = WinVista-64-rss.steps
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/WindowsVista-64.iso
                                     md5sum_cd1 = 11e2010d857fffc47813295e6be6d58d
                                     md5sum_1m_cd1 = 0947bcd5390546139e25f25217d6f165
@@ -2076,7 +2076,7 @@ variants:
 
                             - sp2:
                                 image_name += -sp2-64
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/en_windows_vista_sp2_x64_dvd_342267.iso
                                     md5sum_cd1 = a1c024d7abaf34bac3368e88efbc2574
                                     md5sum_1m_cd1 = 3d84911a80f3df71d1026f7adedc2181
@@ -2112,7 +2112,7 @@ variants:
                                     steps = Win2008-32.steps
                                 setup:
                                     steps = Win2008-32-rss.steps
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/Windows2008-x86.iso
                                     md5sum=0bfca49f0164de0a8eba236ced47007d
                                     md5sum_1m=07d7f5006393f74dc76e6e2e943e2440
@@ -2127,7 +2127,7 @@ variants:
 
                             - sp2:
                                 image_name += -sp2-32
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/en_windows_server_2008_datacenter_enterprise_standard_sp2_x86_dvd_342333.iso
                                     md5sum_cd1 = b9201aeb6eef04a3c573d036a8780bdf
                                     md5sum_1m_cd1 = b7a9d42e55ea1e85105a3a6ad4da8e04
@@ -2156,7 +2156,7 @@ variants:
                                     passwd = 1q2w3eP
                                 setup:
                                     steps = Win2008-64-rss.steps
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/Windows2008-x64.iso
                                     md5sum=27c58cdb3d620f28c36333a5552f271c
                                     md5sum_1m=efdcc11d485a1ef9afa739cb8e0ca766
@@ -2171,7 +2171,7 @@ variants:
 
                             - sp2:
                                 image_name += -sp2-64
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/en_windows_server_2008_datacenter_enterprise_standard_sp2_x64_dvd_342336.iso
                                     md5sum_cd1 = e94943ef484035b3288d8db69599a6b5
                                     md5sum_1m_cd1 = ee55506823d0efffb5532ddd88a8e47b
@@ -2188,7 +2188,7 @@ variants:
 
                             - r2:
                                 image_name += -r2-64
-                                unattended_install.cdrom|whql.support_vm_install:
+                                unattended_install.cdrom, whql.support_vm_install:
                                     cdrom_cd1 = isos/windows/en_windows_server_2008_r2_standard_enterprise_datacenter_and_web_x64_dvd_x15-59754.iso
                                     md5sum_cd1 = 0207ef392c60efdda92071b0559ca0f9
                                     md5sum_1m_cd1 = a5a22ce25008bd7109f6d830d627e3ed
@@ -2216,7 +2216,7 @@ variants:
                 variants:
                     - 32:
                         image_name += -32
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/en_windows_7_ultimate_x86_dvd_x15-65921.iso
                             md5sum_cd1 = d0b8b407e8a3d4b75ee9c10147266b89
                             md5sum_1m_cd1 = 2b0c2c22b1ae95065db08686bf83af93
@@ -2249,7 +2249,7 @@ variants:
                             steps = Win7-64.steps
                         setup:
                             steps = Win7-64-rss.steps
-                        unattended_install.cdrom|whql.support_vm_install:
+                        unattended_install.cdrom, whql.support_vm_install:
                             cdrom_cd1 = isos/windows/en_windows_7_ultimate_x64_dvd_x15-65922.iso
                             md5sum_cd1 = f43d22e4fb07bf617d573acd8785c028
                             md5sum_1m_cd1 = b44d8cf99dbed2a5cb02765db8dfd48f
@@ -2329,7 +2329,7 @@ variants:
                 md5sum_cd1 = 9fae22f2666369968a76ef59e9a81ced
 
 
-whql.support_vm_install|whql.client_install.support_vm:
+whql.support_vm_install, whql.client_install.support_vm:
     image_name += -supportvm
 
 
@@ -2352,7 +2352,7 @@ variants:
         drive_format=virtio
 
 
-virtio_net|virtio_blk|e1000|balloon_check:
+virtio_net, virtio_blk, e1000, balloon_check:
     only Fedora.11 Fedora.12 Fedora.13 Fedora.14 RHEL.5 RHEL.6 OpenSUSE.11 SLES.11 Ubuntu-8.10-server
     # only WinXP Win2003 Win2008 WinVista Win7 Fedora.11 Fedora.12 Fedora.13 Fedora.14 RHEL.5 RHEL.6 OpenSUSE.11 SLES.11 Ubuntu-8.10-server
 
@@ -2365,15 +2365,9 @@ variants:
         check_image = yes
     - vmdk:
         no ioquit
-        only Fedora Ubuntu Windows
-        only smp2
-        only rtl8139
         image_format = vmdk
     - raw:
         no ioquit
-        only Fedora Ubuntu Windows
-        only smp2
-        only rtl8139
         image_format = raw
 
 
-- 
1.7.3.4

--
To unsubscribe from this list: send the line "unsubscribe kvm" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [KVM ARM]     [KVM ia64]     [KVM ppc]     [Virtualization Tools]     [Spice Development]     [Libvirt]     [Libvirt Users]     [Linux USB Devel]     [Linux Audio Users]     [Yosemite Questions]     [Linux Kernel]     [Linux SCSI]     [XFree86]
  Powered by Linux