[autotest] Expand DevServer class to support control-file related calls

Add the ability to call the control_files/ endpoint on the dev server.

BUG=chromium-os:24985
TEST=dev_server_unittest.py

Change-Id: If20e7c6d1f13813cd10511c34d64e9ffddfb37f4
Reviewed-on: https://gerrit.chromium.org/gerrit/14302
Commit-Ready: Chris Masone <cmasone@chromium.org>
Reviewed-by: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
diff --git a/client/common_lib/cros/dev_server.py b/client/common_lib/cros/dev_server.py
index a57194e..8cd977e 100644
--- a/client/common_lib/cros/dev_server.py
+++ b/client/common_lib/cros/dev_server.py
@@ -22,15 +22,32 @@
 
 class DevServer(object):
     """Helper class for interacting with the Dev Server via http."""
-    def __init__(self, dev_host):
+    def __init__(self, dev_host=None):
         """Constructor.
 
         Args:
-        @param host: Address of the Dev Server.
+        @param dev_host: Address of the Dev Server.
+                         Defaults to None.  If not set, CROS.dev_server is used.
         """
         self._dev_server = dev_host if dev_host else _get_dev_server()
 
 
+    def _build_call(self, method, **kwargs):
+        """Build a URL that calls |method|, passing |kwargs|.
+
+        Build a URL that calls |method| on the dev server, passing a set
+        of key/value pairs built from the dict |kwargs|.
+
+        @param method: the dev server method to call.
+        @param kwargs: a dict mapping arg names to arg values
+        @return the URL string
+        """
+        argstr = '&'.join(map(lambda x: "%s=%s" % x, kwargs.iteritems()))
+        return "%(host)s/%(method)s?%(args)s" % {'host': self._dev_server,
+                                                 'method': method,
+                                                 'args': argstr}
+
+
     def trigger_download(self, image):
         """Tell the dev server to download and stage |image|.
 
@@ -40,12 +57,12 @@
         @param image: the image to fetch and stage.
         @return True if the remote call returns HTTP OK, False if it returns
                 an internal server error.
-        @throws urllib2.HTTPError upon any return code that's not 200 or 500
+        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
         """
         try:
             call = self._build_call(
-                method='download',
-                named_args={'archive_url': _get_image_storage_server() + image})
+                'download',
+                archive_url=_get_image_storage_server() + image)
             response = urllib2.urlopen(call)
             return response.read() == 'Success'
         except urllib2.HTTPError as e:
@@ -56,17 +73,50 @@
                 raise
 
 
-    def _build_call(self, method, named_args):
-        """Build a URL that calls |method|, passing |named_args|.
+    def list_control_files(self, build):
+        """Ask the dev server to list all control files for |build|.
 
-        Build a URL that calls |method| on the dev server, passing a set
-        of key/value pairs built from the dict |named_args|.
+        Ask the dev server at |self._dev_server| to list all control files
+        for |build|.
 
-        @param method: the dev server method to call.
-        @param named_args: a dict mapping arg names to arg values
-        @return the URL string
+        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
+                      whose control files the caller wants listed.
+        @return None on failure, or a list of control file paths
+                (e.g. server/site_tests/autoupdate/control)
+        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
         """
-        argstr = '&'.join(map(lambda x: "%s=%s" % x, named_args.iteritems()))
-        return "%(host)s/%(method)s?%(args)s" % { 'host': self._dev_server,
-                                                  'method': method,
-                                                  'args': argstr }
+        try:
+            call = self._build_call('controlfiles', build=build)
+            response = urllib2.urlopen(call)
+            return [line.rstrip() for line in response]
+        except urllib2.HTTPError as e:
+            if e.code == httplib.INTERNAL_SERVER_ERROR:
+                return None
+            else:
+                logging.debug(e)
+                raise
+
+
+    def get_control_file(self, build, control_path):
+        """Ask the dev server for the contents of a control file.
+
+        Ask the dev server at |self._dev_server|for the contents of the
+        control file at |control_path| for |build|.
+
+        @param build: The build (e.g. x86-mario-release/R18-1586.0.0-a1-b1514)
+                      whose control files the caller wants listed.
+        @param control_path: The file to list
+                             (e.g. server/site_tests/autoupdate/control)
+        @return The contents of the desired file, or None
+        @throws urllib2.HTTPError upon any return code that's not 200 or 500.
+        """
+        try:
+            call = self._build_call('controlfiles',
+                                    build=build, control_path=control_path)
+            return urllib2.urlopen(call).read()
+        except urllib2.HTTPError as e:
+            if e.code == httplib.INTERNAL_SERVER_ERROR:
+                return None
+            else:
+                logging.debug(e)
+                raise
diff --git a/client/common_lib/cros/dev_server_unittest.py b/client/common_lib/cros/dev_server_unittest.py
index 22ef818..e5b593e 100644
--- a/client/common_lib/cros/dev_server_unittest.py
+++ b/client/common_lib/cros/dev_server_unittest.py
@@ -23,12 +23,32 @@
     """
 
     _HOST = 'http://nothing'
+    _500 = urllib2.HTTPError(url='',
+                             code=httplib.INTERNAL_SERVER_ERROR,
+                             msg='',
+                             hdrs=None,
+                             fp=None)
+    _403 = urllib2.HTTPError(url='',
+                             code=httplib.FORBIDDEN,
+                             msg='',
+                             hdrs=None,
+                             fp=None)
 
     def setUp(self):
         super(DevServerTest, self).setUp()
         self.dev_server = dev_server.DevServer(self._HOST)
 
 
+    def _returnHttpServerError(self):
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        urllib2.urlopen(mox.IgnoreArg()).AndRaise(self._500)
+
+
+    def _returnHttpForbidden(self):
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        urllib2.urlopen(mox.IgnoreArg()).AndRaise(self._403)
+
+
     def testSuccessfulTriggerDownload(self):
         """Should successfully call the dev server's download method."""
         name = 'fake/image'
@@ -42,27 +62,77 @@
 
     def testFailedTriggerDownload(self):
         """Should call the dev server's download method, fail gracefully."""
-        self.mox.StubOutWithMock(urllib2, 'urlopen')
-        to_raise = urllib2.HTTPError(url='',
-                                     code=httplib.INTERNAL_SERVER_ERROR,
-                                     msg='',
-                                     hdrs=None,
-                                     fp=None)
-        urllib2.urlopen(mox.IgnoreArg()).AndRaise(to_raise)
+        self._returnHttpServerError()
         self.mox.ReplayAll()
         self.assertFalse(self.dev_server.trigger_download(''))
 
 
     def testExplodingTriggerDownload(self):
         """Should call the dev server's download method, get exception."""
-        self.mox.StubOutWithMock(urllib2, 'urlopen')
-        to_raise = urllib2.HTTPError(url='',
-                                     code=httplib.FORBIDDEN,
-                                     msg='',
-                                     hdrs=None,
-                                     fp=None)
-        urllib2.urlopen(mox.IgnoreArg()).AndRaise(to_raise)
+        self._returnHttpForbidden()
         self.mox.ReplayAll()
         self.assertRaises(urllib2.HTTPError,
                           self.dev_server.trigger_download,
                           '')
+
+
+    def testListControlFiles(self):
+        """Should successfully list control files from the dev server."""
+        name = 'fake/build'
+        control_files = ['file/one', 'file/two']
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        to_return = StringIO.StringIO('\n'.join(control_files))
+        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
+                                mox.StrContains(name))).AndReturn(to_return)
+        self.mox.ReplayAll()
+        paths = self.dev_server.list_control_files(name)
+        self.assertEquals(len(paths), 2)
+        for f in control_files:
+            self.assertTrue(f in paths)
+
+
+    def testFailedListControlFiles(self):
+        """Should call the dev server's list-files method, fail gracefully."""
+        self._returnHttpServerError()
+        self.mox.ReplayAll()
+        self.assertEquals(self.dev_server.list_control_files(''), None)
+
+
+    def testExplodingListControlFiles(self):
+        """Should call the dev server's list-files method, get exception."""
+        self._returnHttpForbidden()
+        self.mox.ReplayAll()
+        self.assertRaises(urllib2.HTTPError,
+                          self.dev_server.list_control_files,
+                          '')
+
+
+    def testGetControlFile(self):
+        """Should successfully list control files from the dev server."""
+        name = 'fake/build'
+        file = 'file/one'
+        contents = 'Multi-line\nControl File Contents\n'
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        to_return = StringIO.StringIO(contents)
+        urllib2.urlopen(mox.And(mox.StrContains(self._HOST),
+                                mox.StrContains(name),
+                                mox.StrContains(file))).AndReturn(to_return)
+        self.mox.ReplayAll()
+        self.assertEquals(self.dev_server.get_control_file(name, file),
+                          contents)
+
+
+    def testFailedGetControlFile(self):
+        """Should try to get the contents of a control file, fail gracefully."""
+        self._returnHttpServerError()
+        self.mox.ReplayAll()
+        self.assertEquals(self.dev_server.get_control_file('', ''), None)
+
+
+    def testExplodingGetControlFile(self):
+        """Should try to get the contents of a control file, get exception."""
+        self._returnHttpForbidden()
+        self.mox.ReplayAll()
+        self.assertRaises(urllib2.HTTPError,
+                          self.dev_server.get_control_file,
+                          '', '')
diff --git a/server/cros/dynamic_suite.py b/server/cros/dynamic_suite.py
index 0104416..3cb0abe 100644
--- a/server/cros/dynamic_suite.py
+++ b/server/cros/dynamic_suite.py
@@ -14,6 +14,20 @@
 CONFIG = global_config.global_config
 
 
+def inject_vars(vars, control_file_in):
+    """
+    Inject the contents of |vars| into |control_file_in|
+
+    @param vars: a dict to shoehorn into the provided control file string.
+    @param control_file_in: the contents of a control file to munge.
+    @return the modified control file string.
+    """
+    control_file = ''
+    for key, value in vars.iteritems():
+        control_file += "%s='%s'\n" % (key, value)
+    return control_file + control_file_in
+
+
 def _image_url_pattern():
     return CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
 
@@ -103,20 +117,6 @@
             self._afe.create_label(name=name)
 
 
-    def _inject_vars(self, vars, control_file_in):
-        """
-        Inject the contents of |vars| into |control_file_in|
-
-        @param vars: a dict to shoehorn into the provided control file string.
-        @param control_file_in: the contents of a control file to munge.
-        @return the modified control file string.
-        """
-        control_file = ''
-        for key, value in vars.iteritems():
-            control_file += "%s='%s'\n" % (key, value)
-        return control_file + control_file_in
-
-
     def _schedule_reimage_job(self, name, num_machines, board):
         """
         Schedules the reimaging of |num_machines| |board| devices with |image|.
@@ -129,7 +129,7 @@
         @param board: which kind of devices to reimage.
         @return a frontend.Job object for the reimaging job we scheduled.
         """
-        control_file = self._inject_vars(
+        control_file = inject_vars(
             { 'image_url': _image_url_pattern() % name,
               'image_name': name },
             self._cf_getter.get_control_file_contents_by_name('autoupdate'))
diff --git a/server/cros/dynamic_suite_unittest.py b/server/cros/dynamic_suite_unittest.py
index 696ce31..b96ab92 100755
--- a/server/cros/dynamic_suite_unittest.py
+++ b/server/cros/dynamic_suite_unittest.py
@@ -71,8 +71,8 @@
             return reduce(lambda b,i: "%s='%s'\n" % i in s, d.iteritems(), True)
 
         v = {'v1': 'one', 'v2': 'two'}
-        self.assertTrue(find_all_in(v, self.reimager._inject_vars(v, '')))
-        self.assertTrue(find_all_in(v, self.reimager._inject_vars(v, 'ctrl')))
+        self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, '')))
+        self.assertTrue(find_all_in(v, dynamic_suite.inject_vars(v, 'ctrl')))
 
 
     def testReportResultsGood(self):