[moblab] Initial Version of moblab_RunSuite test.

Updated moblab_host with the following functions:
* find_and_add_duts to find DUTs on the subnet and add them if they
  are not already in the AFE.
* run_as_moblab function to run commands as the moblab user.
* wait_afe_up function to gate tasks that rely on the AFE being up.

Added a new MoblabTest class that handles basic Moblab test tasks that
will be common to Moblab tests:
* Installing a boto file.
* Setting the image_storage_server to use.

Added the initial version of Moblab_RunSuite test which runs a suite
on a Moblab.
* Currently just has one control file that kicks off the smoke suite.

BUG=chromium:388462
TEST=Ran via test_that.

Change-Id: I697f5b94afc88633e213d3b35b53f8f7d5a6240b
Reviewed-on: https://chromium-review.googlesource.com/210592
Tested-by: Simran Basi <sbasi@chromium.org>
Reviewed-by: Chris Sosa <sosa@chromium.org>
Reviewed-by: Dan Shi <dshi@chromium.org>
Commit-Queue: Simran Basi <sbasi@chromium.org>
diff --git a/server/cros/moblab_test.py b/server/cros/moblab_test.py
new file mode 100644
index 0000000..b2c0a95
--- /dev/null
+++ b/server/cros/moblab_test.py
@@ -0,0 +1,81 @@
+# 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 os
+import re
+
+from autotest_lib.client.common_lib import error, global_config
+from autotest_lib.server import test
+from autotest_lib.server.hosts import moblab_host
+
+
+DEFAULT_IMAGE_STORAGE_SERVER = global_config.global_config.get_config_value(
+        'CROS', 'image_storage_server')
+MOBLAB_BOTO_FILE_DEST = '/home/moblab/.boto'
+STORAGE_SERVER_REGEX = 'gs://.*/'
+
+
+class MoblabTest(test.test):
+    """Base class for Moblab tests.
+    """
+
+    def initialize(self, host, boto_path='',
+                   image_storage_server=DEFAULT_IMAGE_STORAGE_SERVER):
+        """Initialize the Moblab Host.
+
+        * Installs a boto file.
+        * Sets up the image storage server for this test.
+        * Finds and adds DUTs on the testing subnet.
+
+        @param boto_path: Path to the boto file we want to install.
+        @param image_storage_server: image storage server to use for grabbing
+                                     images from Google Storage.
+        """
+        super(MoblabTest, self).initialize()
+        self._host = host
+        self.install_boto_file(boto_path)
+        self.set_image_storage_server(image_storage_server)
+        self._host.wait_afe_up()
+        self._host.find_and_add_duts()
+
+
+    def install_boto_file(self, boto_path=''):
+        """Install a boto file on the Moblab device.
+
+        @param boto_path: Path to the boto file to install. If None, sends the
+                          boto file in the current HOME directory.
+
+        @raises error.TestError if the boto file does not exist.
+        """
+        if not boto_path:
+            boto_path = os.path.join(os.getenv('HOME'), '.boto')
+        if not os.path.exists(boto_path):
+            raise error.TestError('Boto File:%s does not exist.' % boto_path)
+        self._host.send_file(boto_path, MOBLAB_BOTO_FILE_DEST)
+        self._host.run('chown moblab:moblab %s' % MOBLAB_BOTO_FILE_DEST)
+
+
+    def set_image_storage_server(self, image_storage_server):
+        """Set the image storage server.
+
+        @param image_storage_server: Name of image storage server to use. Must
+                                     follow format or gs://bucket-name/
+                                     (Note trailing slash is required).
+
+        @raises error.TestError if the image_storage_server is incorrectly
+                                formatted.
+        """
+        if not re.match(STORAGE_SERVER_REGEX, image_storage_server):
+            raise error.TestError(
+                    'Image Storage Server supplied is not in the correct '
+                    'format. Must start with gs:// and end with a trailing '
+                    'slash: %s' % image_storage_server)
+        logging.debug('Setting image_storage_server to %s',
+                      image_storage_server)
+        # If the image_storage_server is already set, delete it.
+        self._host.run('sed -i /image_storage_server/d %s' %
+                       moblab_host.SHADOW_CONFIG_PATH, ignore_status=True)
+        self._host.run("sed -i '/\[CROS\]/ a\image_storage_server: "
+                       "%s' %s" %(image_storage_server,
+                                  moblab_host.SHADOW_CONFIG_PATH))
\ No newline at end of file
diff --git a/server/hosts/moblab_host.py b/server/hosts/moblab_host.py
index b191ca6..5555b4d 100644
--- a/server/hosts/moblab_host.py
+++ b/server/hosts/moblab_host.py
@@ -2,14 +2,38 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 import common
+import logging
+import re
 
-from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib import error, global_config
+from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
 from autotest_lib.server.hosts import cros_host
 
+
+AUTOTEST_INSTALL_DIR = global_config.global_config.get_config_value(
+        'SCHEDULER', 'drone_installation_directory')
+#'/usr/local/autotest'
+SHADOW_CONFIG_PATH = '%s/shadow_config.ini' % AUTOTEST_INSTALL_DIR
+ATEST_PATH = '%s/cli/atest' % AUTOTEST_INSTALL_DIR
+SUBNET_DUT_SEARCH_RE = (
+        r'/?.*\((?P<ip>192.168.231.*)\) at '
+        '(?P<mac>[0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])')
+MOBLAB_IMAGE_STORAGE = '/mnt/moblab/static'
+
+
 class MoblabHost(cros_host.CrosHost):
     """Moblab specific host class."""
 
 
+    def _initialize(self, *args, **dargs):
+        super(MoblabHost, self)._initialize(*args, **dargs)
+        self.afe = frontend_wrappers.RetryingAFE(timeout_min=1,
+                                                 server=self.hostname)
+        # Clear the Moblab Image Storage so that staging an image is properly
+        # tested.
+        self.run('rm -rf %s/*' % MOBLAB_IMAGE_STORAGE)
+
+
     @staticmethod
     def check_host(host, timeout=10):
         """
@@ -35,3 +59,57 @@
     def get_autodir(self):
         """Return the directory to install autotest for client side tests."""
         return '/tmp/autotest'
+
+
+    def run_as_moblab(self, command, **kwargs):
+        """Moblab commands should be ran as the moblab user not root.
+
+        @param command: Command to run as user moblab.
+        """
+        command = "su - moblab -c '%s'" % command
+        return self.run(command, **kwargs)
+
+
+    def reboot(self, **dargs):
+        """Reboot the Moblab Host and wait for its services to restart."""
+        super(MoblabHost, self).reboot(**dargs)
+        self.wait_afe_up()
+
+
+    def wait_afe_up(self, timeout_min=5):
+        """Wait till the AFE is up and loaded.
+
+        Attempt to reach the Moblab's AFE and database through its RPC
+        interface.
+
+        @param timeout_min: Minutes to wait for the AFE to respond. Default is
+                            5 minutes.
+
+        @raises TimeoutException if AFE does not respond within the timeout.
+        """
+        # Use a new AFE object with a longer timeout to wait for the AFE to
+        # load.
+        afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
+                                            server=self.hostname)
+        # Verify the AFE can handle a simple request.
+        afe.get_hosts()
+
+
+    def find_and_add_duts(self):
+        """Discover DUTs on the testing subnet and add them to the AFE.
+
+        Runs 'arp -a' on the Moblab host and parses the output to discover DUTs
+        and if they are not already in the AFE, adds them.
+        """
+        existing_hosts = [host.hostname for host in self.afe.get_hosts()]
+        arp_command = self.run('arp -a')
+        for line in arp_command.stdout.splitlines():
+            match = re.match(SUBNET_DUT_SEARCH_RE, line)
+            if match:
+                dut_hostname = match.group('ip')
+                if dut_hostname in existing_hosts:
+                    break
+                result = self.run_as_moblab('%s host create %s' %
+                                            (ATEST_PATH, dut_hostname))
+                logging.debug('atest host create output for host %s:\n%s',
+                              dut_hostname, result.stdout)
\ No newline at end of file
diff --git a/server/site_tests/moblab_RunSuite/control.smoke b/server/site_tests/moblab_RunSuite/control.smoke
new file mode 100644
index 0000000..d2850e8
--- /dev/null
+++ b/server/site_tests/moblab_RunSuite/control.smoke
@@ -0,0 +1,39 @@
+# 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.
+
+AUTHOR = "chromeos-moblab@google.com"
+NAME = "moblab_SmokeSuite"
+PURPOSE = "Test that Moblab can run the smoke suite."
+TIME = "MEDIUM"
+TEST_CATEGORY = "Functional"
+TEST_CLASS = "moblab"
+TEST_TYPE = "server"
+
+DOC = """
+Kicks off the smoke suite on a Moblab host against the DUTs on its subnet
+and ensures the suite completes successfully.
+
+To invole this test locally:
+  test_that -b stumpy_moblab <remote> moblab_SmokeSuite --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.
+"""
+from autotest_lib.client.common_lib import utils
+
+def run(machine):
+    host = hosts.create_host(machine)
+    args_dict = utils.args_to_dict(args)
+    job.run_test('moblab_RunSuite', host=host, suite_name='smoke', **args_dict)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/moblab_RunSuite/moblab_RunSuite.py b/server/site_tests/moblab_RunSuite/moblab_RunSuite.py
new file mode 100644
index 0000000..e63044b
--- /dev/null
+++ b/server/site_tests/moblab_RunSuite/moblab_RunSuite.py
@@ -0,0 +1,45 @@
+# 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
+
+from autotest_lib.client.common_lib import global_config
+from autotest_lib.server.cros import moblab_test
+from autotest_lib.server.hosts import moblab_host
+
+
+class moblab_RunSuite(moblab_test.MoblabTest):
+    """
+    Moblab run suite test. Ensures that a Moblab can run a suite from start
+    to finish by kicking off a suite which will have the Moblab stage an
+    image, provision its DUTs and run the tests.
+    """
+    version = 1
+
+
+    def run_once(self, host, suite_name):
+        """Runs a suite on a Moblab Host against its test DUTS.
+
+        @param host: Moblab Host that will run the suite.
+        @param suite_name: Name of the suite to run.
+
+        @raises AutoservRunError if the suite does not complete successfully.
+        """
+        # Fetch the board of the DUT's assigned to this Moblab. There should
+        # only be one type.
+        board = host.afe.get_hosts()[0].platform
+        # TODO (crbug.com/399132) sbasi - Replace repair version with actual
+        # stable_version.
+        stable_version = global_config.global_config.get_config_value(
+                'CROS', 'stable_cros_version')
+        build_pattern = global_config.global_config.get_config_value(
+                'CROS', 'stable_build_pattern')
+        build = build_pattern % (board, stable_version)
+
+        logging.debug('Running suite: %s.', suite_name)
+        result = host.run_as_moblab(
+                "%s/site_utils/run_suite.py --pool='' "
+                "--board=%s --build=%s --suite_name=%s" %
+                (moblab_host.AUTOTEST_INSTALL_DIR, board, build, suite_name))
+        logging.debug('Suite Run Output:\n%s', result.stdout)
\ No newline at end of file