diff mbox

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

Message ID 1297216207-28375-1-git-send-email-mgoldish@redhat.com (mailing list archive)
State New, archived
Headers show

Commit Message

Michael Goldish Feb. 9, 2011, 1:50 a.m. UTC
None
diff mbox

Patch

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