[autotest] Add create_suite_job() site RPC.

Add create_suite_job() RPC to autotest using the site_rpc mechanism.
Also add support to atest to call this RPC.

The purpose of this RPC is to stage a build on the dev server (if
necessary), image N machines with it, and then run the desired
suite on those machines.

BUG=chromium-os:25573
TEST=atest suite create -b x86-mario -i x86-mario-release/R19-1675.0.0-a1-b1588 test_suites/control.dummy

Change-Id: I09288fe6ffc675e5b111f7f59e349015f52ebb8e
Reviewed-on: https://gerrit.chromium.org/gerrit/15279
Tested-by: Chris Masone <cmasone@chromium.org>
Commit-Ready: Scott Zawalski <scottz@chromium.org>
Reviewed-by: Scott Zawalski <scottz@chromium.org>
Tested-by: Scott Zawalski <scottz@chromium.org>
diff --git a/frontend/afe/site_rpc_interface.py b/frontend/afe/site_rpc_interface.py
new file mode 100644
index 0000000..6793645
--- /dev/null
+++ b/frontend/afe/site_rpc_interface.py
@@ -0,0 +1,72 @@
+# Copyright (c) 2012 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__ = 'cmasone@chromium.org (Chris Masone)'
+
+import common
+import logging
+import sys
+from autotest_lib.client.common_lib import global_config
+from autotest_lib.client.common_lib.cros import dev_server
+# rpc_utils initializes django, which we can't do in unit tests.
+if 'unittest' not in sys.modules.keys():
+    # So, only load that module if we're not running unit tests.
+    from autotest_lib.frontend.afe import rpc_utils
+from autotest_lib.server.cros import control_file_getter, dynamic_suite
+
+
+class StageBuildFailure(Exception):
+    """Raised when the dev server throws 500 while staging a build."""
+    pass
+
+
+class ControlFileEmpty(Exception):
+    """Raised when the control file exists on the server, but can't be read."""
+    pass
+
+
+def _rpc_utils():
+    """Returns the rpc_utils module.  MUST be mocked for unit tests."""
+    return rpc_utils
+
+
+def create_suite_job(suite_name, board, build):
+    """
+    Create a job to run a test suite on the given device with the given image.
+
+    When the timeout specified in the control file is reached, the
+    job is guaranteed to have completed and results will be available.
+
+    @param suite_name: the test suite to run, e.g. 'control.bvt'.
+    @param board: the kind of device to run the tests on.
+    @param build: unique name by which to refer to the image from now on.
+
+    @throws ControlFileNotFound if a unique suite control file doesn't exist.
+    @throws NoControlFileList if we can't list the control files at all.
+    @throws StageBuildFailure if the dev server throws 500 while staging build.
+    @throws ControlFileEmpty if the control file exists on the server, but
+                             can't be read.
+
+    @return: the job ID of the suite; -1 on error.
+    """
+    # Ensure |build| is staged is on the dev server.
+    ds = dev_server.DevServer.create()
+    if not ds.trigger_download(build):
+        raise StageBuildFailure("Server error while staging " + build)
+
+    getter = control_file_getter.DevServerGetter.create(build, ds)
+    # Get the control file for the suite.
+    control_file_in = getter.get_control_file_contents_by_name(suite_name)
+    if not control_file_in:
+        raise ControlFileEmpty("Fetching %s returned no data." % suite_name)
+
+    # prepend build and board to the control file
+    control_file = dynamic_suite.inject_vars({'board': board, 'build': build},
+                                             control_file_in)
+
+    return _rpc_utils().create_job_common('%s-%s' % (build, suite_name),
+                                          priority='Medium',
+                                          control_type='Server',
+                                          control_file=control_file,
+                                          hostless=True)
diff --git a/frontend/afe/site_rpc_interface_unittest.py b/frontend/afe/site_rpc_interface_unittest.py
new file mode 100644
index 0000000..e784a06
--- /dev/null
+++ b/frontend/afe/site_rpc_interface_unittest.py
@@ -0,0 +1,133 @@
+#!/usr/bin/python
+#
+# Copyright (c) 2012 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.
+
+"""Unit tests for frontend/afe/site_rpc_interface.py."""
+
+import common
+import mox
+import unittest
+from autotest_lib.client.common_lib.cros import dev_server
+from autotest_lib.frontend.afe import site_rpc_interface
+from autotest_lib.server.cros import control_file_getter
+
+
+class SiteRpcInterfaceTest(mox.MoxTestBase):
+    """Unit tests for functions in site_rpc_interface.py.
+
+    @var _NAME: fake suite name.
+    @var _BOARD: fake board to reimage.
+    @var _BUILD: fake build with which to reimage.
+    """
+    _NAME = 'name'
+    _BOARD = 'board'
+    _BUILD = 'build'
+
+
+    class rpc_utils(object):
+        def create_job_common(self, name, **kwargs):
+            pass
+
+
+    def setUp(self):
+        super(SiteRpcInterfaceTest, self).setUp()
+        self.dev_server = self.mox.CreateMock(dev_server.DevServer)
+        self.mox.StubOutWithMock(dev_server.DevServer, 'create')
+        dev_server.DevServer.create().AndReturn(self.dev_server)
+
+
+    def _mockDevServerGetter(self):
+        self.getter = self.mox.CreateMock(control_file_getter.DevServerGetter)
+        self.mox.StubOutWithMock(control_file_getter.DevServerGetter, 'create')
+        control_file_getter.DevServerGetter.create(
+            mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(self.getter)
+
+
+    def _mockRpcUtils(self, to_return):
+        """Fake out the autotest rpc_utils module with a mockable class.
+
+        @param to_return: the value that rpc_utils.create_job_common() should
+                          be mocked out to return.
+        """
+        r = self.mox.CreateMock(SiteRpcInterfaceTest.rpc_utils)
+        r.create_job_common(mox.And(mox.StrContains(self._NAME),
+                                    mox.StrContains(self._BUILD)),
+                            priority='Medium',
+                            control_type='Server',
+                            control_file=mox.And(mox.StrContains(self._BOARD),
+                                                 mox.StrContains(self._BUILD)),
+                            hostless=True).AndReturn(to_return)
+        self.mox.StubOutWithMock(site_rpc_interface, '_rpc_utils')
+        site_rpc_interface._rpc_utils().AndReturn(r)
+
+
+    def testStageBuildFail(self):
+        """Ensure that a failure to stage the desired build fails the RPC."""
+        self.dev_server.trigger_download(self._BUILD).AndReturn(False)
+        self.mox.ReplayAll()
+        self.assertRaises(site_rpc_interface.StageBuildFailure,
+                          site_rpc_interface.create_suite_job,
+                          self._NAME,
+                          self._BOARD,
+                          self._BUILD)
+
+
+    def testGetControlFileFail(self):
+        """Ensure that a failure to get needed control file fails the RPC."""
+        self._mockDevServerGetter()
+        self.dev_server.trigger_download(self._BUILD).AndReturn(True)
+        self.getter.get_control_file_contents_by_name(self._NAME).AndReturn(
+            None)
+        self.mox.ReplayAll()
+        self.assertRaises(site_rpc_interface.ControlFileEmpty,
+                          site_rpc_interface.create_suite_job,
+                          self._NAME,
+                          self._BOARD,
+                          self._BUILD)
+
+
+    def testGetControlFileListFail(self):
+        """Ensure that a failure to get needed control file fails the RPC."""
+        self._mockDevServerGetter()
+        self.dev_server.trigger_download(self._BUILD).AndReturn(True)
+        self.getter.get_control_file_contents_by_name(self._NAME).AndRaise(
+            control_file_getter.NoControlFileList())
+        self.mox.ReplayAll()
+        self.assertRaises(control_file_getter.NoControlFileList,
+                          site_rpc_interface.create_suite_job,
+                          self._NAME,
+                          self._BOARD,
+                          self._BUILD)
+
+
+    def testCreateSuiteJobFail(self):
+        """Ensure that failure to schedule the suite job fails the RPC."""
+        self._mockDevServerGetter()
+        self.dev_server.trigger_download(self._BUILD).AndReturn(True)
+        self.getter.get_control_file_contents_by_name(self._NAME).AndReturn('f')
+        self._mockRpcUtils(-1)
+        self.mox.ReplayAll()
+        self.assertEquals(site_rpc_interface.create_suite_job(self._NAME,
+                                                              self._BOARD,
+                                                              self._BUILD),
+                          -1)
+
+
+    def testCreateSuiteJobSuccess(self):
+        """Ensures that success results in a successful RPC."""
+        self._mockDevServerGetter()
+        self.dev_server.trigger_download(self._BUILD).AndReturn(True)
+        self.getter.get_control_file_contents_by_name(self._NAME).AndReturn('f')
+        job_id = 5
+        self._mockRpcUtils(job_id)
+        self.mox.ReplayAll()
+        self.assertEquals(site_rpc_interface.create_suite_job(self._NAME,
+                                                              self._BOARD,
+                                                              self._BUILD),
+                          job_id)
+
+
+if __name__ == '__main__':
+  unittest.main()