[moblab] add moblab_StorageQual test

Create a test that will flex the moblab provisioning and scheduling
functionality required to run hardware_storagequal suite. Storage qual
requires that tests run in the correct priority order on the correct
DUTs as determined by their labels. This is a new moblab suite, which
runs a new non-destructive storage qual suite 'hardware_storagequal_cq'
on the DUTs. This test is intended to be added to CQ. Before adding
to CQ, we will need to label 2 DUTs on each moblab as 'storage_qual_cq_1'
and 'storage_qual_cq_2'.

BUG=chromium:878009
TEST=run_suite.py --pool='' --board=caroline --build=caroline-release/R69-10755.0.0 --suite_name=hardware_storagequal_cq --retry=True --max_retries=42

test_that -b guado_moblab 100.90.25.159 moblab_StorageQual --args="image_storage_server=gs://chromeos-moblab-peng-staging/ clear_devserver_cache=False"

Change-Id: I2846200d2ac90664c2d9d87ba825698ed9a06386
Reviewed-on: https://chromium-review.googlesource.com/1225892
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Matt Mallett <mattmallett@chromium.org>
Reviewed-by: Keith Haddow <haddowk@chromium.org>
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_after b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_after
new file mode 100644
index 0000000..f84a35f
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_after
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualBase_storage_qual_cq_1_after"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualBase_storage_qual_cq_1_after"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 70
+DEPENDENCIES = "storage_qual_cq_1"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualBase_storage_qual_cq_1_after"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualBase", host=hosts.create_host(machine),
+            client_ip=machine, client_tag='after', tag='after', cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_before b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_before
new file mode 100644
index 0000000..a8bb211
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_1_before
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualBase_storage_qual_cq_1_before"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualBase_storage_qual_cq_1_before"
+TIME = "lengthy"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 100
+DEPENDENCIES = "storage_qual_cq_1"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualBase_storage_qual_cq_1_before"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualBase", host=hosts.create_host(machine),
+            client_ip=machine, client_tag='before', tag='before', cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_after b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_after
new file mode 100644
index 0000000..c2b8134
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_after
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualBase_storage_qual_cq_2_after"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualBase_storage_qual_cq_2_after"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 70
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualBase_storage_qual_cq_2_after"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualBase", host=hosts.create_host(machine),
+            client_ip=machine, client_tag='after', tag='after', cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_before b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_before
new file mode 100644
index 0000000..43fb1ed
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualBase_storage_qual_cq_2_before
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualBase_storage_qual_cq_2_before"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualBase_storage_qual_cq_2_before"
+TIME = "lengthy"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 100
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualBase_storage_qual_cq_2_before"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualBase", host=hosts.create_host(machine),
+            client_ip=machine, client_tag='before', tag='before', cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_0 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_0
new file mode 100644
index 0000000..666c9fd
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_0
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualTrimStress_storage_qual_cq_2_0"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualTrimStress_storage_qual_cq_2_0"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 80
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualTrimStress_storage_qual_cq_2_0"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualTrimStress", host=hosts.create_host(machine),
+            client_ip=machine, duration=1800, cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_1 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_1
new file mode 100644
index 0000000..566a2b3
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageQualTrimStress_storage_qual_cq_2_1
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageQualTrimStress_storage_qual_cq_2_1"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageQualTrimStress_storage_qual_cq_2_1"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 80
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageQualTrimStress_storage_qual_cq_2_1"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageQualTrimStress", host=hosts.create_host(machine),
+            client_ip=machine, duration=1800, cq=True)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_0 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_0
new file mode 100644
index 0000000..fadf017
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_0
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageStress_storage_qual_cq_1_soak_0"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageStress_storage_qual_cq_1_soak_0"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 90
+DEPENDENCIES = "storage_qual_cq_1"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageStress_storage_qual_cq_1_soak_0"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageStress", host=hosts.create_host(machine),
+            client_ip=machine, storage_test_command='full_write', suspend_duration=300, tag='soak', duration=14400, cq=True, power_command='wait')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_1 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_1
new file mode 100644
index 0000000..6754193
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_soak_1
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageStress_storage_qual_cq_1_soak_1"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageStress_storage_qual_cq_1_soak_1"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 90
+DEPENDENCIES = "storage_qual_cq_1"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageStress_storage_qual_cq_1_soak_1"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageStress", host=hosts.create_host(machine),
+            client_ip=machine, storage_test_command='full_write', suspend_duration=300, tag='soak', duration=14400, cq=True, power_command='wait')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_suspend b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_suspend
new file mode 100644
index 0000000..950ff1b
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_1_suspend
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageStress_storage_qual_cq_1_suspend"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageStress_storage_qual_cq_1_suspend"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 80
+DEPENDENCIES = "storage_qual_cq_1"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageStress_storage_qual_cq_1_suspend"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageStress", host=hosts.create_host(machine),
+            client_ip=machine, storage_test_command='full_write', suspend_duration=120, tag='suspend', duration=1800, cq=True, power_command='suspend')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_0 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_0
new file mode 100644
index 0000000..deacfce
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_0
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageStress_storage_qual_cq_2_soak_0"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageStress_storage_qual_cq_2_soak_0"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 90
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageStress_storage_qual_cq_2_soak_0"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageStress", host=hosts.create_host(machine),
+            client_ip=machine, storage_test_command='full_write', suspend_duration=300, tag='soak', duration=14400, cq=True, power_command='wait')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_1 b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_1
new file mode 100644
index 0000000..4a89bd2
--- /dev/null
+++ b/server/site_tests/hardware_StorageQual/control.storage_qual_cq_hardware_StorageStress_storage_qual_cq_2_soak_1
@@ -0,0 +1,36 @@
+
+# Copyright (c) 2018 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This control file was auto-generated by generate_storage_qual_control_files.py
+# Do not edit this file!
+
+from autotest_lib.client.common_lib import utils
+
+AUTHOR = "Chrome OS Team"
+NAME = "hardware_StorageStress_storage_qual_cq_2_soak_1"
+ATTRIBUTES = "suite:storage_qual_cq"
+PURPOSE = "hardware_StorageStress_storage_qual_cq_2_soak_1"
+TIME = "long"
+TEST_CATEGORY = "Stress"
+TEST_CLASS = "Hardware"
+TEST_TYPE = "server"
+REQUIRE_SSP = False
+PRIORITY = 90
+DEPENDENCIES = "storage_qual_cq_2"
+JOB_RETRIES = 0
+
+DOC = "hardware_StorageStress_storage_qual_cq_2_soak_1"
+
+keyval = dict()
+keyval['storage_qual_version'] = 1
+keyval['bug_id'] = bug_id
+keyval['part_id'] = part_id
+utils.write_keyval(job.resultdir, keyval)
+
+def run(machine):
+    job.run_test("hardware_StorageStress", host=hosts.create_host(machine),
+            client_ip=machine, storage_test_command='full_write', suspend_duration=300, tag='soak', duration=14400, cq=True, power_command='wait')
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/hardware_StorageQual/generate_storage_qual_control_files.py b/server/site_tests/hardware_StorageQual/generate_storage_qual_control_files.py
index 2d38836..73e2c14 100644
--- a/server/site_tests/hardware_StorageQual/generate_storage_qual_control_files.py
+++ b/server/site_tests/hardware_StorageQual/generate_storage_qual_control_files.py
@@ -61,10 +61,6 @@
     'length': 'long'
 }
 
-SOAK_QUICK = copy.deepcopy(SOAK)
-SOAK_QUICK['iterations'] = 2
-SOAK_QUICK['args']['duration'] = HOUR_IN_SECS
-
 BASE_AFTER = {
     'test': 'hardware_StorageQualBase',
     'args': {'tag': 'after', 'client_tag': 'after'},
@@ -72,6 +68,18 @@
     'length': 'long'
 }
 
+SOAK_QUICK = copy.deepcopy(SOAK)
+SOAK_QUICK['iterations'] = 2
+SOAK_QUICK['args']['duration'] = HOUR_IN_SECS
+
+BASE_BEFORE_CQ = copy.deepcopy(BASE_BEFORE)
+BASE_BEFORE_CQ['args']['cq'] = True
+SOAK_CQ = copy.deepcopy(SOAK)
+SOAK_CQ['args']['cq'] = True
+SOAK_CQ['iterations'] = 2
+BASE_AFTER_CQ = copy.deepcopy(BASE_AFTER)
+BASE_AFTER_CQ['args']['cq'] = True
+
 SUITES = {
     'storage_qual': [
         {
@@ -179,12 +187,50 @@
                 BASE_AFTER
             ]
         }
+    ],
+    'storage_qual_cq': [
+        {
+            'label': 'storage_qual_cq_1',
+            'tests': [
+                BASE_BEFORE_CQ,
+                SOAK_CQ,
+                {
+                    'test': 'hardware_StorageStress',
+                    'args': {'tag': 'suspend', 'power_command': 'suspend',
+                        'storage_test_command': 'full_write',
+                        'suspend_duration': 120,
+                        'duration': HOUR_IN_SECS / 2,
+                        'cq': True
+                    },
+                    'priority': 80,
+                    'length': 'long'
+                },
+                BASE_AFTER_CQ
+            ]
+        },
+
+        {
+            'label': 'storage_qual_cq_2',
+            'tests': [
+                BASE_BEFORE_CQ,
+                SOAK_CQ,
+                {
+                    'test': 'hardware_StorageQualTrimStress',
+                    'args': {'duration': HOUR_IN_SECS / 2, 'cq': True},
+                    'iterations': 2,
+                    'priority': 80,
+                    'length': 'long'
+                },
+                BASE_AFTER_CQ
+            ]
+        }
     ]
 }
 
 SUITE_ATTRIBUTES = {
     'storage_qual': 'suite:storage_qual',
-    'storage_qual_quick': 'suite:storage_qual_quick'
+    'storage_qual_quick': 'suite:storage_qual_quick',
+    'storage_qual_cq': 'suite:storage_qual_cq'
 }
 
 TEMPLATE = """
diff --git a/server/site_tests/hardware_StorageQualBase/hardware_StorageQualBase.py b/server/site_tests/hardware_StorageQualBase/hardware_StorageQualBase.py
index 9fe90ec..b3d015d 100644
--- a/server/site_tests/hardware_StorageQualBase/hardware_StorageQualBase.py
+++ b/server/site_tests/hardware_StorageQualBase/hardware_StorageQualBase.py
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import time
 from autotest_lib.server import autotest
 from autotest_lib.server import hosts
 from autotest_lib.server import test
@@ -47,7 +48,17 @@
     ]
 
 
-    def run_once(self, client_ip, client_tag='', crypto_runtime=CRYPTO_RUNTIME):
+    def run_once(self, client_ip, client_tag='', crypto_runtime=CRYPTO_RUNTIME,
+            cq=False):
+
+        # in a cq run, do not execute the test, just output
+        # the order that the test would have run in
+        if cq:
+            self.write_test_keyval(
+                {'storage_qual_cq': ('%f hardware_StorageQualBase_%s'
+                    % (time.time(), client_tag))})
+            return
+
         client = hosts.create_host(client_ip)
         client_at = autotest.Autotest(client)
         for test_name, argv in self.CLIENT_FUNCTIONAL_TESTS:
diff --git a/server/site_tests/hardware_StorageQualTrimStress/hardware_StorageQualTrimStress.py b/server/site_tests/hardware_StorageQualTrimStress/hardware_StorageQualTrimStress.py
index ba711de..3b76450 100644
--- a/server/site_tests/hardware_StorageQualTrimStress/hardware_StorageQualTrimStress.py
+++ b/server/site_tests/hardware_StorageQualTrimStress/hardware_StorageQualTrimStress.py
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import time
 from autotest_lib.server import autotest
 from autotest_lib.server import hosts
 from autotest_lib.server import test
@@ -10,7 +11,15 @@
     """Do traffic and trim while suspending aggressively."""
 
     version = 1
-    def run_once(self, client_ip, duration):
+    def run_once(self, client_ip, duration, cq=False):
+
+        # in a cq run, do not execute the test, just output
+        # the order that the test would have run in
+        if cq:
+            self.write_test_keyval({'storage_qual_cq':
+                '%f hardware_StorageQualTrimStress' % time.time()})
+            return
+
         client = hosts.create_host(client_ip)
         client_at = autotest.Autotest(client)
         control = """job.parallel(
diff --git a/server/site_tests/hardware_StorageStress/hardware_StorageStress.py b/server/site_tests/hardware_StorageStress/hardware_StorageStress.py
index f8e612b..6d699a5 100644
--- a/server/site_tests/hardware_StorageStress/hardware_StorageStress.py
+++ b/server/site_tests/hardware_StorageStress/hardware_StorageStress.py
@@ -25,7 +25,8 @@
 
     def run_once(self, client_ip, gap=_TEST_GAP, duration=_TEST_DURATION,
                  power_command='reboot', storage_test_command='integrity',
-                 suspend_duration=_SUSPEND_DURATION, storage_test_argument=''):
+                 suspend_duration=_SUSPEND_DURATION, storage_test_argument='',
+                 cq=False):
         """
         Run the Storage stress test
         Use hardwareStorageFio to run some test_command repeatedly for a long
@@ -44,8 +45,20 @@
                                             to determine which disk to write
         @param suspend_duration: if power_command is suspend, how long the DUT
                               is suspended.
+        @param cq:            Indicates that this test is being run as part of
+                              the cq. This is not used to test a component for
+                              qualification, but to test the storage qual suite
         """
 
+        # in a cq run, do not execute the test, just output
+        # the order that the test would have run in
+        if cq:
+            label = 'suspend' if power_command is 'suspend' else 'soak'
+            self.write_test_keyval(
+                {'storage_qual_cq': ('%f hardware_StorageStress_%s'
+                    % (time.time(), label))})
+            return
+
         # init test
         if not client_ip:
             error.TestError("Must provide client's IP address to test")
diff --git a/server/site_tests/moblab_StorageQual/control b/server/site_tests/moblab_StorageQual/control
new file mode 100644
index 0000000..15949bb
--- /dev/null
+++ b/server/site_tests/moblab_StorageQual/control
@@ -0,0 +1,71 @@
+# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = "chromeos-moblab@google.com"
+NAME = "moblab_StorageQual"
+PURPOSE = "Test that Moblab can run the Storage Qual suite."
+ATTRIBUTES = "suite:moblab_storage_qual"
+TIME = "MEDIUM"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "moblab"
+TEST_TYPE = "server"
+
+DOC = """
+Kicks off the storage qual suite on a Moblab host against the DUTs on its
+subnet and ensures the suite completes successfully. The suite tests that
+moblab correctly provisions and runs the storage qual suite, but does not
+perform any disk operations.
+
+To invole this test locally:
+  test_that -b stumpy_moblab <remote> moblab_StorageQual
+  --args="<ARGLIST>"
+
+where ARGLIST is a whitespace separated list of the following key=value pairs.
+Values pertaining to the test case include:
+
+  boto_path=<boto_path>                path to the boto file to be installed on
+                                       the Moblab DUT. If not specified, the
+                                       boto file in the current home directory
+                                       will be installed if it exists.
+  image_storage_server=<server_name>   Google Storage Bucket from which to
+                                       fetch test images from. If not
+                                       specified, the value will be fetched
+                                       from global_config.
+  service_init_timeout_m=<int>         Timeout (in minutes) to wait for upstart
+                                       services to start on the moblab host.
+                                       This can take ~5 minutes on a physical
+                                       devices and ~10 minutes on a VM.
+  test_timeout_hint_m=<int>            The overall timeout to expect for the
+                                       test run. For this test, it is very
+                                       important to collect post failure data
+                                       from the moblab device. If the overall
+                                       timeout is provided, the test will try to
+                                       fail early to save some time for log
+                                       collection from the DUT.
+  clear_devserver_cache=<boolean>      If True, image cache of the devserver
+                                       running on moblab is cleared before
+                                       running the test to validate devserver
+                                       imaging staging flow.
+"""
+from autotest_lib.client.bin import sysinfo
+from autotest_lib.client.common_lib import utils
+
+MOBLAB_AUTOTEST_FOLDERS = ['/usr/local/autotest/results',
+                           '/usr/local/autotest/logs']
+
+
+def run(machine):
+    host = hosts.create_host(machine)
+    args_dict = utils.args_to_dict(args)
+
+    logging.info('Logs from moblab\'s instance of autotest will be collected '
+                 'under the sysinfo/ folder in results.')
+    for folder in MOBLAB_AUTOTEST_FOLDERS:
+        logging.info('  Will collect %s', folder)
+        job.sysinfo.add_logdir(sysinfo.logdir(folder, excludes=()))
+
+    job.run_test('moblab_StorageQual', host=host,
+                 moblab_suite_max_retries=1, **args_dict)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/moblab_StorageQual/moblab_StorageQual.py b/server/site_tests/moblab_StorageQual/moblab_StorageQual.py
new file mode 100644
index 0000000..8aac8d3
--- /dev/null
+++ b/server/site_tests/moblab_StorageQual/moblab_StorageQual.py
@@ -0,0 +1,272 @@
+# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import re
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.server.cros import moblab_test
+from autotest_lib.server.hosts import moblab_host
+from autotest_lib.utils import labellib
+
+
+_CLEANUP_TIME_M = 5
+_MOBLAB_IMAGE_STORAGE = '/mnt/moblab/static'
+
+class moblab_StorageQual(moblab_test.MoblabTest):
+    """
+    Moblab storage qual suite test. Ensures that moblab can run the storage
+    qual tests on the correct DUTs in the correct order. This test does not
+    perform any destructive disk operations.
+
+    The test requires 2 duts, labeled 'storage_qual_cq_1', 'storage_qual_cq_2'.
+    Each DUT will run a sequence of tests, and the test will then verify
+    that the correct tests ran on the correctly labeled DUT, in the correct
+    order.
+    """
+    version = 1
+
+    # Moblab expects to have 1 dut with each of these labels
+    REQUIRED_LABELS = {'storage_qual_cq_1', 'storage_qual_cq_2'}
+
+    EXPECTED_RESULTS = {
+        'storage_qual_cq_1': [
+            'hardware_StorageQualBase_before',
+            'hardware_StorageStress_soak',
+            'hardware_StorageStress_soak',
+            'hardware_StorageStress_suspend',
+            'hardware_StorageQualBase_after'
+        ],
+        'storage_qual_cq_2': [
+            'hardware_StorageQualBase_before',
+            'hardware_StorageStress_soak',
+            'hardware_StorageStress_soak',
+            'hardware_StorageQualTrimStress',
+            'hardware_StorageQualTrimStress',
+            'hardware_StorageQualBase_after'
+        ]
+    }
+
+    def run_once(self, host, moblab_suite_max_retries,
+                 target_build='', clear_devserver_cache=True,
+                 test_timeout_hint_m=None):
+        """Runs a suite on a Moblab Host against its test DUTS.
+
+        @param host: Moblab Host that will run the suite.
+        @param moblab_suite_max_retries: The maximum number of test retries
+                allowed within the suite launched on moblab.
+        @param target_build: Optional build to be use in the run_suite
+                call on moblab. This argument is passed as is to run_suite. It
+                must be a sensible build target for the board of the sub-DUTs
+                attached to the moblab.
+        @param clear_devserver_cache: If True, image cache of the devserver
+                running on moblab is cleared before running the test to validate
+                devserver imaging staging flow.
+        @param test_timeout_hint_m: (int) Optional overall timeout for the test.
+                For this test, it is very important to collect post failure data
+                from the moblab device. If the overall timeout is provided, the
+                test will try to fail early to save some time for log collection
+                from the DUT.
+
+        @raises AutoservRunError if the suite does not complete successfully.
+        """
+        self._host = host
+        self._maybe_clear_devserver_cache(clear_devserver_cache)
+
+        duts = host.afe.get_hosts()
+        if len(duts) == 0:
+            raise error.TestFail('All hosts for this MobLab are down. Please '
+                                 'request the lab admins to take a look.')
+
+        board = None
+        dut_to_label = {}
+        for dut in duts:
+            # Fetch the board of the DUT's assigned to this Moblab. There should
+            # only be one type.
+            board = labellib.LabelsMapping(dut.labels)['board']
+            for label in dut.labels:
+                if label in self.REQUIRED_LABELS:
+                    dut_to_label[dut.hostname] = label
+
+        if not set(dut_to_label.values()) == self.REQUIRED_LABELS:
+            raise error.TestFail(
+                'Missing required labels on hosts %s, are some hosts down?'
+                    % self.REQUIRED_LABELS - set(dut_to_label.values()))
+
+        if not board:
+            raise error.TestFail('Could not determine board from hosts.')
+
+        if not target_build:
+            stable_version_map = host.afe.get_stable_version_map(
+                    host.afe.CROS_IMAGE_TYPE)
+            target_build = stable_version_map.get_image_name(board)
+
+        logging.info('Running suite: hardware_storagequal_cq')
+        cmd = ("%s/site_utils/run_suite.py --pool='' --board=%s --build=%s "
+               "--suite_name=hardware_storagequal_cq --retry=True "
+               "--max_retries=%d" %
+               (moblab_host.AUTOTEST_INSTALL_DIR, board, target_build,
+               moblab_suite_max_retries))
+        cmd, run_suite_timeout_s = self._append_run_suite_timeout(
+                cmd,
+                test_timeout_hint_m,
+        )
+
+        logging.debug('Run suite command: %s', cmd)
+        try:
+            result = host.run_as_moblab(cmd, timeout=run_suite_timeout_s)
+        except error.AutoservRunError as e:
+            if _is_run_suite_error_critical(e.result_obj.exit_status):
+                raise
+
+        logging.debug('Suite Run Output:\n%s', result.stderr)
+
+        job_ids = self._get_job_ids_from_suite_output(result.stderr)
+
+        logging.debug('Suite job ids %s', job_ids)
+
+        keyvals_per_host = self._get_keyval_files_per_host(host, job_ids)
+
+        logging.debug('Keyvals grouped by host %s', keyvals_per_host)
+
+        failed_test = False
+        for hostname in keyvals_per_host:
+            label = dut_to_label[hostname]
+            expected = self.EXPECTED_RESULTS[label]
+            actual = self._get_test_execution_order(
+                host, keyvals_per_host[hostname])
+
+            logging.info('Comparing test order for %s from host %s',
+                label, hostname)
+            logging.info('%-37s %s', 'Expected', 'Actual')
+            for i in range(max(len(expected), len(actual))):
+                expected_i = expected[i] if i < len(expected) else None
+                actual_i = actual[i] if i < len(actual) else None
+                check_fail = expected_i != actual_i
+                check_text = 'X' if check_fail else ' '
+                logging.info('%s %-35s %s', check_text, expected_i, actual_i)
+                failed_test = failed_test or check_fail
+
+        # Cache directory can contain large binaries like CTS/CTS zip files
+        # no need to offload those in the results.
+        # The cache is owned by root user
+        host.run('rm -fR /mnt/moblab/results/shared/cache',
+                    timeout=600)
+
+        if failed_test:
+            raise error.TestFail(
+                'Actual test execution order did not match expected')
+
+    def _append_run_suite_timeout(self, cmd, test_timeout_hint_m):
+        """Modify given run_suite command with timeout.
+
+        @param cmd: run_suite command str.
+        @param test_timeout_hint_m: (int) timeout for the test, or None.
+        @return cmd, run_suite_timeout_s: cmd is the updated command str,
+                run_suite_timeout_s is the timeout to use for the run_suite
+                call, in seconds.
+        """
+        if test_timeout_hint_m is None:
+            return cmd, 10800
+
+        # Arguments passed in via test_args may be all str, depending on how
+        # they're passed in.
+        test_timeout_hint_m = int(test_timeout_hint_m)
+        elasped_m = self.elapsed.total_seconds() / 60
+        run_suite_timeout_m = (
+                test_timeout_hint_m - elasped_m - _CLEANUP_TIME_M)
+        logging.info('Overall test timeout hint provided (%d minutes)',
+                     test_timeout_hint_m)
+        logging.info('%d minutes have already elasped', elasped_m)
+        logging.info(
+                'Keeping %d minutes for cleanup, will allow %d minutes for '
+                'the suite to run.', _CLEANUP_TIME_M, run_suite_timeout_m)
+        cmd += ' --timeout_mins %d' % run_suite_timeout_m
+        return cmd, run_suite_timeout_m * 60
+
+    def _maybe_clear_devserver_cache(self, clear_devserver_cache):
+        # When passed in via test_args, all arguments are str
+        if not isinstance(clear_devserver_cache, bool):
+            clear_devserver_cache = (clear_devserver_cache.lower() == 'true')
+        if clear_devserver_cache:
+            self._host.run('rm -rf %s/*' % _MOBLAB_IMAGE_STORAGE)
+
+    def _get_job_ids_from_suite_output(self, suite_output):
+        """Parse the set of job ids from run_suite output
+
+        @param suite_output (str) output from run_suite command
+        @return (set<int>) job ids contained in the suite
+        """
+        job_ids = set()
+        job_id_pattern = re.compile('(\d+)-moblab')
+        for line in suite_output.splitlines():
+            match = job_id_pattern.search(line)
+            logging.debug('suite line %s match %s', line, match)
+            if match is None:
+                continue
+            job_ids.add(int(match.groups()[0]))
+        return job_ids
+
+    def _get_keyval_files_per_host(self, host, job_ids):
+        """Find the result keyval files for the given job ids and
+        group them by host
+
+        @param host (moblab_host)
+        @param job_ids (set<int>) set of job ids to find keyvals for
+        @return (dict<str, list<str>>) map of hosts and the keyval
+            file locations
+        @throws AutoservRunError if the command fails to run on moblab
+        """
+        keyvals_per_host = {}
+        keyvals = host.run_as_moblab(
+            'find /mnt/moblab/results '
+            '-wholename *-moblab/192.168*/hardware_Storage*/keyval')
+        pattern = re.compile('(\d+)-moblab/(192.168.\d+.\d+)')
+        for line in keyvals.stdout.splitlines():
+            match = pattern.search(line)
+            if match is None:
+                continue
+            job_id, dut = match.groups()
+            if int(job_id) not in job_ids:
+                continue
+            if dut not in keyvals_per_host:
+                keyvals_per_host[dut] = []
+            keyvals_per_host[dut].append(line)
+
+        return keyvals_per_host
+
+    def _get_test_execution_order(self, host, keyvals):
+        """Determines the test execution order for the given list
+        of storage qual test result keyvals
+
+        @param host (moblab_host)
+        @param keyvals (list<str>) location of keyval files to order
+        @return (list<str>) test names in the order they executed
+        @throws AutoservRunError if the command fails to run on moblab
+        """
+        tests = host.run_as_moblab(
+            'FILES=(%s); for FILE in ${FILES[@]}; do cat $FILE '
+            '| grep storage_qual_cq; done '
+            '| sort | cut -d " " -f 2'
+            % ' '.join(keyvals)
+        )
+        test_execution_order = []
+        pattern = re.compile('hardware_\w+')
+        logging.debug(tests.stdout)
+        for line in tests.stdout.splitlines():
+            match = pattern.search(line)
+            if match:
+                test_execution_order.append(match.group(0))
+        return test_execution_order
+
+def _is_run_suite_error_critical(return_code):
+    # We can't actually import run_suite here because importing run_suite pulls
+    # in certain MySQLdb dependencies that fail to load in the context of a
+    # test.
+    # OTOH, these return codes are unlikely to change because external users /
+    # builders depend on them.
+    return return_code not in (
+            0,  # run_suite.RETURN_CODES.OK
+            2,  # run_suite.RETURN_CODES.WARNING
+    )
diff --git a/site_utils/attribute_whitelist.txt b/site_utils/attribute_whitelist.txt
index 04bbbec..a682c8a 100644
--- a/site_utils/attribute_whitelist.txt
+++ b/site_utils/attribute_whitelist.txt
@@ -151,6 +151,7 @@
 suite:mmc_qual
 suite:moblab
 suite:moblab_quick
+suite:moblab_storage_qual
 suite:network_nightly
 suite:network_ui
 suite:partners
@@ -181,6 +182,7 @@
 suite:something_else
 suite:ssdqual
 suite:storage_qual
+suite:storage_qual_cq
 suite:storage_qual_quick
 suite:storage_qual_temp
 suite:storagequal
diff --git a/test_suites/control.hardware_storagequal_cq b/test_suites/control.hardware_storagequal_cq
new file mode 100644
index 0000000..406456a
--- /dev/null
+++ b/test_suites/control.hardware_storagequal_cq
@@ -0,0 +1,38 @@
+# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = 'mattmallett'
+NAME = 'storage_qual_cq'
+TIME = 'SHORT'
+TEST_CATEGORY = 'General'
+TEST_CLASS = 'suite'
+TEST_TYPE = 'Server'
+
+DOC = """
+Test the moblab mechanisms that run the storage qual suite in the cq
+
+@param build: The name of the image to test.
+          Ex: x86-mario-release/R17-1412.33.0-a1-b29
+@param board: The board to test on. Ex: x86-mario
+@param pool: The pool of machines to utilize for scheduling. If pool=None
+             board is used.
+@param check_hosts: require appropriate live hosts to exist in the lab.
+@param SKIP_IMAGE: (optional) If present and True, don't re-image devices.
+@param file_bugs: If True your suite will file bugs on failures.
+"""
+
+import common
+from autotest_lib.server.cros.dynamic_suite import dynamic_suite
+
+args_dict['add_experimental'] = True
+args_dict['timeout_mins'] = 30
+args_dict['max_runtime_mins'] = 30
+args_dict['name'] = NAME
+args_dict['job'] = job
+args_dict['test_args'] = {
+    'bug_id': '',
+    'part_id': ''
+}
+
+dynamic_suite.reimage_and_run(**args_dict)
diff --git a/test_suites/control.moblab_storage_qual b/test_suites/control.moblab_storage_qual
new file mode 100644
index 0000000..9413818
--- /dev/null
+++ b/test_suites/control.moblab_storage_qual
@@ -0,0 +1,33 @@
+# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = "Chrome OS Team"
+NAME = "moblab_storage_qual"
+PURPOSE = "Quickly test basic moblab functionality."
+
+TIME = "MEDIUM"
+TEST_CATEGORY = "General"
+TEST_CLASS = "suite"
+TEST_TYPE = "Server"
+
+DOC = """
+Moblab Functionality Tests
+
+@param build: The name of the image to test.
+              Ex: x86-mario-release/R17-1412.33.0-a1-b29
+@param board: The board to test on. Ex: x86-mario
+@param pool: The pool of machines to utilize for scheduling. If pool=None
+             board is used.
+@param check_hosts: require appropriate live hosts to exist in the lab.
+@param SKIP_IMAGE: (optional) If present and True, don't re-image devices.
+"""
+
+import common
+from autotest_lib.server.cros.dynamic_suite import dynamic_suite
+
+args_dict['add_experimental'] = True
+args_dict['name'] = 'moblab_quick'
+args_dict['job'] = job
+
+dynamic_suite.reimage_and_run(**args_dict)