diff --git a/client/tests/kvm/deps/whql_submission_15.cs b/client/tests/kvm/deps/whql_submission_15.cs
index 8fa6856..0928548 100644
--- a/client/tests/kvm/deps/whql_submission_15.cs
+++ b/client/tests/kvm/deps/whql_submission_15.cs
@@ -1,289 +1,373 @@
-// DTM submission automation program
-// Author: Michael Goldish <mgoldish@redhat.com>
-// Based on sample code by Microsoft.
-
-// Note: this program has only been tested with DTM version 1.5.
-// It might fail to work with other versions, specifically because it uses
-// a few undocumented methods/attributes.
-
-using System;
-using System.Collections.Generic;
-using System.Text.RegularExpressions;
-using Microsoft.DistributedAutomation.DeviceSelection;
-using Microsoft.DistributedAutomation.SqlDataStore;
-
-namespace automate0
-{
-    class AutoJob
-    {
-        static int Main(string[] args)
-        {
-            if (args.Length != 5)
-            {
-                Console.WriteLine("Error: incorrect number of command line arguments");
-                Console.WriteLine("Usage: {0} serverName clientName machinePoolName submissionName timeout",
-                    System.Environment.GetCommandLineArgs()[0]);
-                return 1;
-            }
-            string serverName = args[0];
-            string clientName = args[1];
-            string machinePoolName = args[2];
-            string submissionName = args[3];
-            double timeout = Convert.ToDouble(args[4]);
-
-            try
-            {
-                // Initialize DeviceScript and connect to data store
-                Console.WriteLine("Initializing DeviceScript object");
-                DeviceScript script = new DeviceScript();
-                Console.WriteLine("Connecting to data store");
-
-                script.ConnectToNamedDataStore(serverName);
-
-                // Find client machine
-                IResourcePool rootPool = script.GetResourcePoolByName("$");
-                Console.WriteLine("Looking for client machine '{0}'", clientName);
-                IResource machine = null;
-                while (true)
-                {
-                    try
-                    {
-                        machine = rootPool.GetResourceByName(clientName);
-                    }
-                    catch (Exception e)
-                    {
-                        Console.WriteLine("Warning: " + e.Message);
-                    }
-                    // Make sure the machine is valid
-                    if (machine != null &&
-                        machine.OperatingSystem != null &&
-                        machine.OperatingSystem.Length > 0 &&
-                        machine.ProcessorArchitecture != null &&
-                        machine.ProcessorArchitecture.Length > 0 &&
-                        machine.GetDevices().Length > 0)
-                        break;
-                    System.Threading.Thread.Sleep(1000);
-                }
-                Console.WriteLine("Client machine '{0}' found ({1}, {2})",
-                    clientName, machine.OperatingSystem, machine.ProcessorArchitecture);
-
-                // Create machine pool and add client machine to it
-                // (this must be done because jobs cannot be scheduled for machines in the
-                // default pool)
-                try
-                {
-                    script.CreateResourcePool(machinePoolName, rootPool.ResourcePoolId);
-                }
-                catch (Exception e)
-                {
-                    Console.WriteLine("Warning: " + e.Message);
-                }
-                IResourcePool newPool = script.GetResourcePoolByName(machinePoolName);
-                Console.WriteLine("Moving the client machine to pool '{0}'", machinePoolName);
-                machine.ChangeResourcePool(newPool);
-
-                // Reset client machine
-                if (machine.Status != "Ready")
-                {
-                    Console.WriteLine("Changing the client machine's status to 'Reset'");
-                    while (true)
-                    {
-                        try
-                        {
-                            machine = rootPool.GetResourceByName(clientName);
-                            machine.ChangeResourceStatus("Unsafe");
-                            System.Threading.Thread.Sleep(5000);
-                            machine.ChangeResourceStatus("Reset");
-                            break;
-                        }
-                        catch (Exception e)
-                        {
-                            Console.WriteLine("Warning: " + e.Message);
-                        }
-                        System.Threading.Thread.Sleep(5000);
-                    }
-                    Console.WriteLine("Waiting for client machine to be ready");
-                    while (machine.Status != "Ready")
-                    {
-                        try
-                        {
-                            machine = rootPool.GetResourceByName(clientName);
-                        }
-                        catch (Exception e)
-                        {
-                            Console.WriteLine("Warning: " + e.Message);
-                        }
-                        System.Threading.Thread.Sleep(1000);
-                    }
-                }
-                Console.WriteLine("Client machine is ready");
-
-                // Get requested device regex and look for a matching device
-                Console.WriteLine("Device to test: ");
-                Regex deviceRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase);
-                Console.WriteLine("Looking for device '{0}'", deviceRegex);
-                IDevice device;
-                DateTime endTime = DateTime.Now.AddSeconds(120);
-                while (DateTime.Now < endTime)
-                {
-                    machine = rootPool.GetResourceByName(clientName);
-                    Console.WriteLine("(Client machine has {0} devices)", machine.GetDevices().Length);
-                    foreach (IDevice d in machine.GetDevices())
-                    {
-                        if (deviceRegex.IsMatch(d.FriendlyName))
-                        {
-                            device = d;
-                            goto deviceFound;
-                        }
-                    }
-                    System.Threading.Thread.Sleep(5000);
-                }
-                Console.WriteLine("Error: device '{0}' not found", deviceRegex);
-                return 1;
-
-            deviceFound:
-                Console.WriteLine("Found device '{0}'", device.FriendlyName);
-
-                // Get requested jobs regex
-                Console.WriteLine("Jobs to run: ");
-                Regex jobRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase);
-
-                // Create submission
-                Object[] existingSubmissions = script.GetSubmissionByName(submissionName);
-                if (existingSubmissions.Length > 0)
-                {
-                    Console.WriteLine("Submission '{0}' already exists -- removing it",
-                        submissionName);
-                    script.DeleteSubmission(((ISubmission)existingSubmissions[0]).Id);
-                }
-                Console.WriteLine("Creating submission '{0}'", submissionName);
-                ISubmission submission = script.CreateHardwareSubmission(submissionName,
-                    newPool.ResourcePoolId, device.InstanceId);
-
-                // Get DeviceData objects from the user
-                List<Object> deviceDataList = new List<Object>();
-                while (true)
-                {
-                    ISubmissionDeviceData dd = script.CreateNewSubmissionDeviceData();
-                    Console.WriteLine("DeviceData name: ");
-                    dd.Name = Console.ReadLine();
-                    if (dd.Name.Length == 0)
-                        break;
-                    Console.WriteLine("DeviceData data: ");
-                    dd.Data = Console.ReadLine();
-                    deviceDataList.Add(dd);
-                }
-
-                // Set the submission's DeviceData
-                submission.SetDeviceData(deviceDataList.ToArray());
-
-                // Get descriptors from the user
-                List<Object> descriptorList = new List<Object>();
-                while (true)
-                {
-                    Console.WriteLine("Descriptor path: ");
-                    string descriptorPath = Console.ReadLine();
-                    if (descriptorPath.Length == 0)
-                        break;
-                    descriptorList.Add(script.GetDescriptorByPath(descriptorPath));
-                }
-
-                // Set the submission's descriptors
-                submission.SetLogoDescriptors(descriptorList.ToArray());
-
-                // Create a schedule
-                ISchedule schedule = script.CreateNewSchedule();
-                Console.WriteLine("Scheduling jobs:");
-                int jobCount = 0;
-                foreach (IJob j in submission.GetJobs())
-                {
-                    if (jobRegex.IsMatch(j.Name))
-                     {
-                        Console.WriteLine("  " + j.Name);
-                        schedule.AddDeviceJob(device, j);
-                        jobCount++;
-                    }
-                }
-                if (jobCount == 0)
-                {
-                    Console.WriteLine("Error: no submission jobs match pattern '{0}'", jobRegex);
-                    return 1;
-                }
-                schedule.AddSubmission(submission);
-                schedule.SetResourcePool(newPool);
-                script.RunSchedule(schedule);
-
-                // Wait for jobs to complete
-                Console.WriteLine("Waiting for all jobs to complete (timeout={0})", timeout);
-                endTime = DateTime.Now.AddSeconds(timeout);
-                int numCompleted = 0, numFailed = 0;
-                while (numCompleted < submission.GetResults().Length && DateTime.Now < endTime)
-                {
-                    // Sleep for 30 seconds
-                    System.Threading.Thread.Sleep(30000);
-                    // Count completed submission jobs
-                    numCompleted = 0;
-                    foreach (IResult r in submission.GetResults())
-                        if (r.ResultStatus != "InProgress")
-                            numCompleted++;
-                    // Report results in a Python readable format and count failed schedule jobs
-                    // (submission jobs are a subset of schedule jobs)
-                    Console.WriteLine();
-                    Console.WriteLine("---- [");
-                    numFailed = 0;
-                    foreach (IResult r in schedule.GetResults())
-                    {
-                        Console.WriteLine("  {");
-                        Console.WriteLine("    'id': {0}, 'job': r'''{1}''',", r.Job.Id, r.Job.Name);
-                        Console.WriteLine("    'logs': r'''{0}''',", r.LogLocation);
-                        if (r.ResultStatus != "InProgress")
-                            Console.WriteLine("    'report': r'''{0}''',",
-                                submission.GetSubmissionResultReport(r));
-                        Console.WriteLine("    'status': '{0}',", r.ResultStatus);
-                        Console.WriteLine("    'pass': {0}, 'fail': {1}, 'notrun': {2}, 'notapplicable': {3}",
-                            r.Pass, r.Fail, r.NotRun, r.NotApplicable);
-                        Console.WriteLine("  },");
-                        numFailed += r.Fail;
-                    }
-                    Console.WriteLine("] ----");
-                }
-                Console.WriteLine();
-
-                // Cancel incomplete jobs
-                foreach (IResult r in schedule.GetResults())
-                    if (r.ResultStatus == "InProgress")
-                        r.Cancel();
-
-                // Set the machine's status to Unsafe and then Reset
-                try
-                {
-                    machine = rootPool.GetResourceByName(clientName);
-                    machine.ChangeResourceStatus("Unsafe");
-                    System.Threading.Thread.Sleep(5000);
-                    machine.ChangeResourceStatus("Reset");
-                }
-                catch (Exception e)
-                {
-                    Console.WriteLine("Warning: " + e.Message);
-                }
-
-                // Report failures
-                if (numCompleted < submission.GetResults().Length)
-                    Console.WriteLine("Some jobs did not complete on time.");
-                if (numFailed > 0)
-                    Console.WriteLine("Some jobs failed.");
-
-                if (numFailed > 0 || numCompleted < submission.GetResults().Length)
-                    return 1;
-
-                Console.WriteLine("All jobs completed.");
-                return 0;
-            }
-            catch (Exception e)
-            {
-                Console.WriteLine("Error: " + e.Message);
-                return 1;
-            }
-        }
-    }
-}
+// DTM submission automation program
+// Author: Michael Goldish <mgoldish@redhat.com>
+// Based on sample code by Microsoft.
+
+// Note: this program has only been tested with DTM version 1.5.
+// It might fail to work with other versions, specifically because it uses
+// a few undocumented methods/attributes.
+
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using Microsoft.DistributedAutomation.DeviceSelection;
+using Microsoft.DistributedAutomation.SqlDataStore;
+
+namespace automate0
+{
+    class AutoJob
+    {
+        // Wait for a machine to show up in the data store
+        static void FindMachine(IResourcePool rootPool, string machineName)
+        {
+            Console.WriteLine("Looking for machine '{0}'", machineName);
+            IResource machine = null;
+            while (true)
+            {
+                try
+                {
+                    machine = rootPool.GetResourceByName(machineName);
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                }
+                // Make sure the machine is valid
+                if (machine != null &&
+                    machine.OperatingSystem != null &&
+                    machine.OperatingSystem.Length > 0 &&
+                    machine.ProcessorArchitecture != null &&
+                    machine.ProcessorArchitecture.Length > 0 &&
+                    machine.GetDevices().Length > 0)
+                    break;
+                System.Threading.Thread.Sleep(1000);
+            }
+            Console.WriteLine("Client machine '{0}' found ({1}, {2})",
+                machineName, machine.OperatingSystem, machine.ProcessorArchitecture);
+        }
+
+        // Delete a machine pool if it exists
+        static void DeleteResourcePool(IDeviceScript script, string poolName)
+        {
+            while (true)
+            {
+                try
+                {
+                    IResourcePool pool = script.GetResourcePoolByName(poolName);
+                    if (pool != null)
+                        script.DeleteResourcePool(pool);
+                    break;
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                    System.Threading.Thread.Sleep(1000);
+                }
+            }
+        }
+
+        // Set the machine's status to 'Reset' and optionally wait for it to become ready
+        static void ResetMachine(IResourcePool rootPool, string machineName, bool wait)
+        {
+            Console.WriteLine("Resetting machine '{0}'", machineName);
+            IResource machine;
+            while (true)
+            {
+                try
+                {
+                    machine = rootPool.GetResourceByName(machineName);
+                    machine.ChangeResourceStatus("Reset");
+                    break;
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                    System.Threading.Thread.Sleep(5000);
+                }
+            }
+            if (wait)
+            {
+                Console.WriteLine("Waiting for machine '{0}' to be ready", machineName);
+                while (machine.Status != "Ready")
+                {
+                    try
+                    {
+                        machine = rootPool.GetResourceByName(machineName);
+                    }
+                    catch (Exception e)
+                    {
+                        Console.WriteLine("Warning: " + e.Message);
+                    }
+                    System.Threading.Thread.Sleep(1000);
+                }
+                Console.WriteLine("Machine '{0}' is ready", machineName);
+            }
+        }
+
+        // Look for a device in a machine, and if not found, keep trying for 3 minutes
+        static IDevice GetDevice(IResourcePool rootPool, string machineName, string regexStr)
+        {
+            Regex deviceRegex = new Regex(regexStr, RegexOptions.IgnoreCase);
+            int numAttempts = 1;
+            DateTime endTime = DateTime.Now.AddSeconds(180);
+            while (DateTime.Now < endTime)
+            {
+                IResource machine = rootPool.GetResourceByName(machineName);
+                Console.WriteLine("Looking for device '{0}' in machine '{1}' (machine has {2} devices)",
+                    regexStr, machineName, machine.GetDevices().Length);
+                foreach (IDevice d in machine.GetDevices())
+                {
+                    if (deviceRegex.IsMatch(d.FriendlyName))
+                    {
+                        Console.WriteLine("Found device '{0}'", d.FriendlyName);
+                        return d;
+                    }
+                }
+                Console.WriteLine("Device not found");
+                if (numAttempts % 5 == 0)
+                    ResetMachine(rootPool, machineName, true);
+                else
+                    System.Threading.Thread.Sleep(5000);
+                numAttempts++;
+            }
+            Console.WriteLine("Error: device '{0}' not found", deviceRegex);
+            return null;
+        }
+
+        static int Main(string[] args)
+        {
+            if (args.Length < 5)
+            {
+                Console.WriteLine("Error: incorrect number of command line arguments");
+                Console.WriteLine("Usage: {0} serverName machinePoolName submissionName timeout machineName0 machineName1 ...",
+                    System.Environment.GetCommandLineArgs()[0]);
+                return 1;
+            }
+            string serverName = args[0];
+            string machinePoolName = args[1];
+            string submissionName = args[2];
+            double timeout = Convert.ToDouble(args[3]);
+
+            List<string> machines = new List<string>();
+            for (int i = 4; i < args.Length; i++)
+                machines.Add(args[i]);
+
+            try
+            {
+                // Initialize DeviceScript and connect to data store
+                Console.WriteLine("Initializing DeviceScript object");
+                DeviceScript script = new DeviceScript();
+                Console.WriteLine("Connecting to data store");
+                script.ConnectToNamedDataStore(serverName);
+
+                // Wait for client machines to become available
+                IResourcePool rootPool = script.GetResourcePoolByName("$");
+                foreach (string machineName in machines)
+                    FindMachine(rootPool, machineName);
+
+                // Delete the machine pool if it already exists
+                DeleteResourcePool(script, machinePoolName);
+
+                // Create the machine pool and add the client machines to it
+                // (this must be done because jobs cannot be scheduled for machines in the
+                // default pool)
+                try
+                {
+                    script.CreateResourcePool(machinePoolName, rootPool.ResourcePoolId);
+                }
+                catch (Exception e)
+                {
+                    Console.WriteLine("Warning: " + e.Message);
+                }
+                IResourcePool newPool = script.GetResourcePoolByName(machinePoolName);
+                foreach (string machineName in machines)
+                {
+                    Console.WriteLine("Moving machine '{0}' to pool '{1}'", machineName, machinePoolName);
+                    rootPool.GetResourceByName(machineName).ChangeResourcePool(newPool);
+                }
+
+                // Reset client machine
+                foreach (string machineName in machines)
+                    ResetMachine(rootPool, machineName, true);
+
+                // Get requested device regex and look for a matching device in the first machine
+                Console.WriteLine("Device to test:");
+                IDevice device = GetDevice(rootPool, machines[0], Console.ReadLine());
+                if (device == null)
+                    return 1;
+
+                // Get requested jobs regex
+                Console.WriteLine("Jobs to run:");
+                Regex jobRegex = new Regex(Console.ReadLine(), RegexOptions.IgnoreCase);
+
+                // Create a submission
+                Object[] existingSubmissions = script.GetSubmissionByName(submissionName);
+                if (existingSubmissions.Length > 0)
+                {
+                    Console.WriteLine("Submission '{0}' already exists -- removing it",
+                        submissionName);
+                    script.DeleteSubmission(((ISubmission)existingSubmissions[0]).Id);
+                }
+                string hardwareId = device.InstanceId.Remove(device.InstanceId.LastIndexOf("\\"));
+                Console.WriteLine("Creating submission '{0}' (hardware ID: {1})", submissionName, hardwareId);
+                ISubmission submission = script.CreateHardwareSubmission(submissionName, newPool.ResourcePoolId, hardwareId);
+
+                // Set submission DeviceData
+                List<Object> deviceDataList = new List<Object>();
+                while (true)
+                {
+                    ISubmissionDeviceData dd = script.CreateNewSubmissionDeviceData();
+                    Console.WriteLine("DeviceData name:");
+                    dd.Name = Console.ReadLine();
+                    if (dd.Name.Length == 0)
+                        break;
+                    Console.WriteLine("DeviceData data:");
+                    dd.Data = Console.ReadLine();
+                    deviceDataList.Add(dd);
+                }
+                submission.SetDeviceData(deviceDataList.ToArray());
+
+                // Set submission descriptors
+                List<Object> descriptorList = new List<Object>();
+                while (true)
+                {
+                    Console.WriteLine("Descriptor path:");
+                    string descriptorPath = Console.ReadLine();
+                    if (descriptorPath.Length == 0)
+                        break;
+                    descriptorList.Add(script.GetDescriptorByPath(descriptorPath));
+                }
+                submission.SetLogoDescriptors(descriptorList.ToArray());
+
+                // Set machine dimensions
+                foreach (string machineName in machines)
+                {
+                    IResource machine = rootPool.GetResourceByName(machineName);
+                    while (true)
+                    {
+                        Console.WriteLine("Dimension name ({0}):", machineName);
+                        string dimName = Console.ReadLine();
+                        if (dimName.Length == 0)
+                            break;
+                        Console.WriteLine("Dimension value ({0}):", machineName);
+                        machine.SetDimension(dimName, Console.ReadLine());
+                    }
+                    // Set the WDKSubmissionId dimension for all machines
+                    machine.SetDimension("WDKSubmissionId", submission.Id.ToString() + "_" + submission.Name);
+                }
+
+                // Get job parameters
+                List<string> paramNames = new List<string>();
+                List<string> paramValues = new List<string>();
+                foreach (string machineName in machines)
+                {
+                    while (true)
+                    {
+                        Console.WriteLine("Parameter name ({0}):", machineName);
+                        string paramName = Console.ReadLine();
+                        if (paramName.Length == 0)
+                            break;
+                        Console.WriteLine("Device regex ({0}):", machineName);
+                        IDevice d = GetDevice(rootPool, machineName, Console.ReadLine());
+                        if (d == null)
+                            return 1;
+                        string deviceName = d.GetAttribute("name")[0].ToString();
+                        Console.WriteLine("Setting parameter value to '{0}'", deviceName);
+                        paramNames.Add(paramName);
+                        paramValues.Add(deviceName);
+                    }
+                }
+
+                // Find jobs that match the requested pattern
+                Console.WriteLine("Scheduling jobs:");
+                List<IJob> jobs = new List<IJob>();
+                foreach (IJob j in submission.GetJobs())
+                {
+                    if (jobRegex.IsMatch(j.Name))
+                    {
+                        Console.WriteLine("    " + j.Name);
+                        // Set job parameters
+                        for (int i = 0; i < paramNames.Count; i++)
+                        {
+                            IParameter p = j.GetParameterByName(paramNames[i]);
+                            if (p != null)
+                                p.ScheduleValue = paramValues[i];
+                        }
+                        jobs.Add(j);
+                    }
+                }
+                if (jobs.Count == 0)
+                {
+                    Console.WriteLine("Error: no submission jobs match pattern '{0}'", jobRegex);
+                    return 1;
+                }
+
+                // Create a schedule, add jobs to it and run it
+                ISchedule schedule = script.CreateNewSchedule();
+                foreach (IScheduleItem item in submission.ProcessJobs(jobs.ToArray()))
+                {
+                    item.Device = device;
+                    schedule.AddScheduleItem(item);
+                }
+                schedule.AddSubmission(submission);
+                schedule.SetResourcePool(newPool);
+                script.RunSchedule(schedule);
+
+                // Wait for jobs to complete
+                Console.WriteLine("Waiting for all jobs to complete (timeout={0}s)", timeout);
+                DateTime endTime = DateTime.Now.AddSeconds(timeout);
+                int numCompleted, numFailed;
+                do
+                {
+                    System.Threading.Thread.Sleep(30000);
+                    // Report results in a Python readable format and count completed and failed schedule jobs
+                    numCompleted = numFailed = 0;
+                    Console.WriteLine();
+                    Console.WriteLine("---- [");
+                    foreach (IResult r in schedule.GetResults())
+                    {
+                        if (r.ResultStatus != "InProgress") numCompleted++;
+                        if (r.ResultStatus == "Investigate") numFailed++;
+                        Console.WriteLine("  {");
+                        Console.WriteLine("    'id': {0}, 'job': r'''{1}''',", r.Job.Id, r.Job.Name);
+                        Console.WriteLine("    'logs': r'''{0}''',", r.LogLocation);
+                        if (r.ResultStatus != "InProgress")
+                            Console.WriteLine("    'report': r'''{0}''',",
+                                submission.GetSubmissionResultReport(r));
+                        Console.WriteLine("    'status': '{0}',", r.ResultStatus);
+                        Console.WriteLine("    'pass': {0}, 'fail': {1}, 'notrun': {2}, 'notapplicable': {3}",
+                            r.Pass, r.Fail, r.NotRun, r.NotApplicable);
+                        Console.WriteLine("  },");
+                    }
+                    Console.WriteLine("] ----");
+                } while (numCompleted < schedule.GetResults().Length && DateTime.Now < endTime);
+
+                Console.WriteLine();
+
+                // Cancel incomplete jobs
+                foreach (IResult r in schedule.GetResults())
+                    if (r.ResultStatus == "InProgress")
+                        r.Cancel();
+
+                // Reset the machines
+                foreach (string machineName in machines)
+                    ResetMachine(rootPool, machineName, false);
+
+                // Report failures
+                if (numCompleted < schedule.GetResults().Length)
+                    Console.WriteLine("Some jobs did not complete on time.");
+                if (numFailed > 0)
+                    Console.WriteLine("Some jobs failed.");
+                if (numFailed > 0 || numCompleted < schedule.GetResults().Length)
+                    return 1;
+
+                Console.WriteLine("All jobs completed.");
+                return 0;
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine("Error: " + e.Message);
+                return 1;
+            }
+        }
+    }
+}
diff --git a/client/tests/kvm/deps/whql_submission_15.exe b/client/tests/kvm/deps/whql_submission_15.exe
index 4f30aa801c8f200bd96b0de6c10d2b59c2bad268..605e2e3aa1f0761155f4aac5191e3ad0f0ddecc6 100644
GIT binary patch
diff --git a/client/tests/kvm/tests/whql_submission.py b/client/tests/kvm/tests/whql_submission.py
index 8dd48b8..a0ff87c 100644
--- a/client/tests/kvm/tests/whql_submission.py
+++ b/client/tests/kvm/tests/whql_submission.py
@@ -6,20 +6,24 @@ import kvm_subprocess, kvm_test_utils, kvm_utils, rss_file_transfer
 def run_whql_submission(test, params, env):
     """
     WHQL submission test:
-    1) Log into the guest (the client machine) and into a DTM server machine
+    1) Log into the client machines and into a DTM server machine
     2) Copy the automation program binary (dsso_test_binary) to the server machine
     3) Run the automation program
     4) Pass the program all relevant parameters (e.g. device_data)
     5) Wait for the program to terminate
     6) Parse and report job results
-    (logs and HTML reports are placed in test.bindir)
+    (logs and HTML reports are placed in test.debugdir)
 
     @param test: kvm test object
     @param params: Dictionary with the test parameters
     @param env: Dictionary with test environment.
     """
-    vm = kvm_test_utils.get_living_vm(env, params.get("main_vm"))
-    session = kvm_test_utils.wait_for_login(vm, 0, 240)
+    # Log into all client VMs
+    vms = []
+    sessions = []
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vms.append(kvm_test_utils.get_living_vm(env, vm_name))
+        sessions.append(kvm_test_utils.wait_for_login(vms[-1], 0, 240))
 
     # Collect parameters
     server_address = params.get("server_address")
@@ -30,47 +34,54 @@ def run_whql_submission(test, params, env):
     dsso_test_binary = params.get("dsso_test_binary",
                                   "deps/whql_submission_15.exe")
     dsso_test_binary = kvm_utils.get_path(test.bindir, dsso_test_binary)
-    test_device = params.get("test_device")
-    job_filter = params.get("job_filter", ".*")
+    dsso_delete_machine_binary = params.get("dsso_delete_machine_binary",
+                                            "deps/whql_delete_machine_15.exe")
+    dsso_delete_machine_binary = kvm_utils.get_path(test.bindir,
+                                                    dsso_delete_machine_binary)
     test_timeout = float(params.get("test_timeout", 600))
-    wtt_services = params.get("wtt_services")
 
-    # Restart WTT service(s) on the client
-    logging.info("Restarting WTT services on client")
-    for svc in wtt_services.split():
-        kvm_test_utils.stop_windows_service(session, svc)
-    for svc in wtt_services.split():
-        kvm_test_utils.start_windows_service(session, svc)
-
-    # Run whql_pre_command
-    if params.get("whql_pre_command"):
-        session.cmd(params.get("whql_pre_command"),
-                    int(params.get("whql_pre_command_timeout", 600)))
-
-    # Copy dsso_test_binary to the server
-    rss_file_transfer.upload(server_address, server_file_transfer_port,
-                             dsso_test_binary, server_studio_path, timeout=60)
+    # Copy dsso binaries to the server
+    for filename in dsso_test_binary, dsso_delete_machine_binary:
+        rss_file_transfer.upload(server_address, server_file_transfer_port,
+                                 filename, server_studio_path, timeout=60)
 
     # Open a shell session with the server
     server_session = kvm_utils.remote_login("nc", server_address,
                                             server_shell_port, "", "",
-                                            session.prompt, session.linesep)
-    server_session.set_status_test_command(session.status_test_command)
+                                            sessions[0].prompt,
+                                            sessions[0].linesep)
+    server_session.set_status_test_command(sessions[0].status_test_command)
 
-    # Get the computer names of the server and client
+    # Get the computer names of the server and clients
     cmd = "echo %computername%"
     server_name = server_session.cmd_output(cmd).strip()
-    client_name = session.cmd_output(cmd).strip()
-    session.close()
+    client_names = [session.cmd_output(cmd).strip() for session in sessions]
 
-    # Run the automation program on the server
+    # Delete all client machines from the server's data store
     server_session.cmd("cd %s" % server_studio_path)
+    for client_name in client_names:
+        cmd = "%s %s %s" % (os.path.basename(dsso_delete_machine_binary),
+                            server_name, client_name)
+        server_session.cmd(cmd, print_func=logging.debug)
+
+    # Reboot the client machines
+    sessions = kvm_utils.parallel((kvm_test_utils.reboot, (vm, session))
+                                  for vm, session in zip(vms, sessions))
+
+    # Run whql_pre_command and close the sessions
+    if params.get("whql_pre_command"):
+        for session in sessions:
+            session.cmd(params.get("whql_pre_command"),
+                        int(params.get("whql_pre_command_timeout", 600)))
+            session.close()
+
+    # Run the automation program on the server
     cmd = "%s %s %s %s %s %s" % (os.path.basename(dsso_test_binary),
                                  server_name,
-                                 client_name,
-                                 "%s_pool" % client_name,
-                                 "%s_submission" % client_name,
-                                 test_timeout)
+                                 "%s_pool" % client_names[0],
+                                 "%s_submission" % client_names[0],
+                                 test_timeout,
+                                 " ".join(client_names))
     server_session.sendline(cmd)
 
     # Helper function: wait for a given prompt and raise an exception if an
@@ -89,13 +100,13 @@ def run_whql_submission(test, params, env):
 
     # Tell the automation program which device to test
     find_prompt("Device to test:")
-    server_session.sendline(test_device)
+    server_session.sendline(params.get("test_device"))
 
     # Tell the automation program which jobs to run
     find_prompt("Jobs to run:")
-    server_session.sendline(job_filter)
+    server_session.sendline(params.get("job_filter", ".*"))
 
-    # Give the automation program all the device data supplied by the user
+    # Set submission DeviceData
     find_prompt("DeviceData name:")
     for dd in kvm_utils.get_sub_dict_names(params, "device_data"):
         dd_params = kvm_utils.get_sub_dict(params, dd)
@@ -104,8 +115,7 @@ def run_whql_submission(test, params, env):
             server_session.sendline(dd_params.get("dd_data"))
     server_session.sendline()
 
-    # Give the automation program all the descriptor information supplied by
-    # the user
+    # Set submission descriptors
     find_prompt("Descriptor path:")
     for desc in kvm_utils.get_sub_dict_names(params, "descriptors"):
         desc_params = kvm_utils.get_sub_dict(params, desc)
@@ -113,6 +123,31 @@ def run_whql_submission(test, params, env):
             server_session.sendline(desc_params.get("desc_path"))
     server_session.sendline()
 
+    # Set machine dimensions for each client machine
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vm_params = kvm_utils.get_sub_dict(params, vm_name)
+        find_prompt(r"Dimension name\b.*:")
+        for dp in kvm_utils.get_sub_dict_names(vm_params, "dimensions"):
+            dp_params = kvm_utils.get_sub_dict(vm_params, dp)
+            if dp_params.get("dim_name") and dp_params.get("dim_value"):
+                server_session.sendline(dp_params.get("dim_name"))
+                server_session.sendline(dp_params.get("dim_value"))
+        server_session.sendline()
+
+    # Set extra parameters for tests that require them (e.g. NDISTest)
+    for vm_name in kvm_utils.get_sub_dict_names(params, "vms"):
+        vm_params = kvm_utils.get_sub_dict(params, vm_name)
+        find_prompt(r"Parameter name\b.*:")
+        for dp in kvm_utils.get_sub_dict_names(vm_params, "device_params"):
+            dp_params = kvm_utils.get_sub_dict(vm_params, dp)
+            if dp_params.get("dp_name") and dp_params.get("dp_regex"):
+                server_session.sendline(dp_params.get("dp_name"))
+                server_session.sendline(dp_params.get("dp_regex"))
+                # Make sure the prompt appears again (if the device isn't found
+                # the automation program will terminate)
+                find_prompt(r"Parameter name\b.*:")
+        server_session.sendline()
+
     # Wait for the automation program to terminate
     try:
         o = server_session.read_up_to_prompt(print_func=logging.info,
@@ -162,38 +197,63 @@ def run_whql_submission(test, params, env):
                 except (KeyError, OSError):
                     pass
 
-    # Print result summary
-    logging.info("")
-    logging.info("Result summary:")
-    name_length = max(len(r.get("job", "")) for r in results)
-    fmt = "%%-6s %%-%ds %%-15s %%-8s %%-8s %%-8s %%-15s" % name_length
-    logging.info(fmt % ("ID", "Job", "Status", "Pass", "Fail", "NotRun",
-                        "NotApplicable"))
-    logging.info(fmt % ("--", "---", "------", "----", "----", "------",
-                        "-------------"))
-    for r in results:
-        logging.info(fmt % (r.get("id"), r.get("job"), r.get("status"),
-                            r.get("pass"), r.get("fail"), r.get("notrun"),
-                            r.get("notapplicable")))
-    logging.info("(see logs and HTML reports in %s)" % test.debugdir)
-
-    # Kill the VM and fail if the automation program did not terminate on time
+    # Print result summary (both to the regular logs and to a file named
+    # 'summary' in test.debugdir)
+    def print_summary_line(f, line):
+        logging.info(line)
+        f.write(line + "\n")
+    if results:
+        # Make sure all results have the required keys
+        for r in results:
+            r["id"] = str(r.get("id"))
+            r["job"] = str(r.get("job"))
+            r["status"] = str(r.get("status"))
+            r["pass"] = int(r.get("pass", 0))
+            r["fail"] = int(r.get("fail", 0))
+            r["notrun"] = int(r.get("notrun", 0))
+            r["notapplicable"] = int(r.get("notapplicable", 0))
+        # Sort the results by failures and total test count in descending order
+        results = [(r["fail"],
+                    r["pass"] + r["fail"] + r["notrun"] + r["notapplicable"],
+                    r) for r in results]
+        results.sort(reverse=True)
+        results = [r[-1] for r in results]
+        # Print results
+        logging.info("")
+        logging.info("Result summary:")
+        name_length = max(len(r["job"]) for r in results)
+        fmt = "%%-6s %%-%ds %%-15s %%-8s %%-8s %%-8s %%-15s" % name_length
+        f = open(os.path.join(test.debugdir, "summary"), "w")
+        print_summary_line(f, fmt % ("ID", "Job", "Status", "Pass", "Fail",
+                                     "NotRun", "NotApplicable"))
+        print_summary_line(f, fmt % ("--", "---", "------", "----", "----",
+                                     "------", "-------------"))
+        for r in results:
+            print_summary_line(f, fmt % (r["id"], r["job"], r["status"],
+                                         r["pass"], r["fail"], r["notrun"],
+                                         r["notapplicable"]))
+        f.close()
+        logging.info("(see logs and HTML reports in %s)" % test.debugdir)
+
+    # Kill the client VMs and fail if the automation program did not terminate
+    # on time
     if not done:
-        vm.destroy()
+        kvm_utils.parallel(vm.destroy for vm in vms)
         raise error.TestFail("The automation program did not terminate "
                              "on time")
 
-    # Fail if there are failed or incomplete jobs (kill the VM if there are
-    # incomplete jobs)
-    failed_jobs = [r.get("job") for r in results
-                   if r.get("status", "").lower() == "investigate"]
-    running_jobs = [r.get("job") for r in results
-                    if r.get("status", "").lower() == "inprogress"]
+    # Fail if there are failed or incomplete jobs (kill the client VMs if there
+    # are incomplete jobs)
+    failed_jobs = [r["job"] for r in results
+                   if r["status"].lower() == "investigate"]
+    running_jobs = [r["job"] for r in results
+                    if r["status"].lower() == "inprogress"]
     errors = []
     if failed_jobs:
         errors += ["Jobs failed: %s." % failed_jobs]
     if running_jobs:
-        vm.destroy()
+        for vm in vms:
+            vm.destroy()
         errors += ["Jobs did not complete on time: %s." % running_jobs]
     if errors:
         raise error.TestFail(" ".join(errors))
diff --git a/client/tests/kvm/tests_base.cfg.sample b/client/tests/kvm/tests_base.cfg.sample
index 8d533ba..e81d879 100644
--- a/client/tests/kvm/tests_base.cfg.sample
+++ b/client/tests/kvm/tests_base.cfg.sample
@@ -347,11 +347,12 @@ variants:
         server_shell_port = 10022
         server_file_transfer_port = 10023
         server_studio_path = %programfiles%\Microsoft Driver Test Manager\Studio
+        dsso_test_binary = deps/whql_submission_15.exe
+        dsso_delete_machine_binary = deps/whql_delete_machine_15.exe
         wtt_services = wttsvc
         variants:
             - client_install:
                 type = whql_client_install
-                dsso_delete_machine_binary = deps/whql_delete_machine_15.exe
                 # The username and password are required for accessing the DTM client
                 # installer binary shared by the server
                 server_username = administrator
@@ -363,7 +364,7 @@ variants:
             - submission:    client_install
                 type = whql_submission
                 extra_params += " -snapshot"
-                dsso_test_binary = deps/whql_submission_15.exe
+                restart_vm = yes
                 test_timeout = 3600
                 device_data = cat0 cat1 cat2 cat3 logoarch logoos whqlos whqlqual prog desc filter virt
                 descriptors = desc1 desc2 desc3
