Allow lab status to block based on a build regex.

Previously, the lab status could contain a string to block testing
based on the board.  With this change, the status can block any
build that matches a regex in the status.  This allows closing the
lab to either a build or a milestone.  It also allows closing the
lab for other more specific conditions, for anyone desperate enough
to want to.

BUG=chromium:220934
TEST=unit tests

Change-Id: I8a44f5e9be504415a7c5bd73c714c778bf684d2c
Reviewed-on: https://chromium-review.googlesource.com/179544
Tested-by: Richard Barnette <jrbarnette@chromium.org>
Reviewed-by: Alex Miller <milleral@chromium.org>
Commit-Queue: Richard Barnette <jrbarnette@chromium.org>
diff --git a/server/lab_status_unittest.py b/server/lab_status_unittest.py
index fad4285..1299a66 100644
--- a/server/lab_status_unittest.py
+++ b/server/lab_status_unittest.py
@@ -13,6 +13,8 @@
 from autotest_lib.client.common_lib import global_config
 from autotest_lib.server import site_utils
 
+_DEADBUILD = 'deadboard-release/R33-4966.0.0'
+_LIVEBUILD = 'liveboard-release/R32-4920.14.0'
 
 _OPEN_STATUS_VALUES = [
     '''
@@ -44,6 +46,16 @@
       "general_state": "open"
     }
     ''',
+
+    '''
+    {
+      "username": "fizzbin@google.com",
+      "date": "2013-11-16 00:25:23.511208",
+      "message": "Lab is up despite R33-4966.0.0",
+      "can_commit_freely": true,
+      "general_state": "open"
+    }
+    ''',
 ]
 
 _CLOSED_STATUS_VALUES = [
@@ -61,19 +73,19 @@
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "Lab is down even for [deadboard]",
+      "message": "Lab is down even for [liveboard-release/R32-4920.14.0]",
       "can_commit_freely": false,
       "general_state": "closed"
     }
     ''',
 ]
 
-_DEADBOARD_STATUS_VALUES = [
+_DEADBUILD_STATUS_VALUES = [
     '''
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "Lab is up except for [deadboard]",
+      "message": "Lab is up except for [deadboard-]",
       "can_commit_freely": false,
       "general_state": "open"
     }
@@ -83,7 +95,7 @@
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "liveboard is good, but [deadboard] is bad",
+      "message": "Lab is up except for [R33-]",
       "can_commit_freely": false,
       "general_state": "open"
     }
@@ -93,7 +105,7 @@
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "Lab is up [deadboard otherboard]",
+      "message": "Lab is up except for [deadboard-.*/R33-]",
       "can_commit_freely": false,
       "general_state": "open"
     }
@@ -103,7 +115,7 @@
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "Lab is up [otherboard deadboard]",
+      "message": "Lab is up except for [ deadboard-]",
       "can_commit_freely": false,
       "general_state": "open"
     }
@@ -113,7 +125,47 @@
     {
       "username": "fizzbin@google.com",
       "date": "2013-11-16 00:25:23.511208",
-      "message": "Lab is up [first deadboard last]",
+      "message": "Lab is up except for [deadboard- ]",
+      "can_commit_freely": false,
+      "general_state": "open"
+    }
+    ''',
+
+    '''
+    {
+      "username": "fizzbin@google.com",
+      "date": "2013-11-16 00:25:23.511208",
+      "message": "Lab is up [first R33- last]",
+      "can_commit_freely": false,
+      "general_state": "open"
+    }
+    ''',
+
+    '''
+    {
+      "username": "fizzbin@google.com",
+      "date": "2013-11-16 00:25:23.511208",
+      "message": "liveboard is good, but [deadboard-] is bad",
+      "can_commit_freely": false,
+      "general_state": "open"
+    }
+    ''',
+
+    '''
+    {
+      "username": "fizzbin@google.com",
+      "date": "2013-11-16 00:25:23.511208",
+      "message": "Lab is up [deadboard- otherboard-]",
+      "can_commit_freely": false,
+      "general_state": "open"
+    }
+    ''',
+
+    '''
+    {
+      "username": "fizzbin@google.com",
+      "date": "2013-11-16 00:25:23.511208",
+      "message": "Lab is up [otherboard- deadboard-]",
       "can_commit_freely": false,
       "general_state": "open"
     }
@@ -249,10 +301,10 @@
      1. Lab is up.  All calls to _decode_lab_status() will
         succeed without raising an exception.
      2. Lab is down.  All calls to _decode_lab_status() will
-        fail with LabIsDownException.
-     3. Board disabled.  Calls to _decode_lab_status() will
-        succeed, except that board 'deadboard' will raise
-        BoardIsDisabledException.
+        fail with TestLabException.
+     3. Build disabled.  Calls to _decode_lab_status() will
+        succeed, except that board `_DEADBUILD` will raise
+        TestLabException.
 
     """
 
@@ -265,42 +317,37 @@
         @param lab_status JSON value describing lab status.
 
         """
-        site_utils._decode_lab_status(lab_status, None)
-        site_utils._decode_lab_status(lab_status, 'liveboard')
-        site_utils._decode_lab_status(lab_status, 'deadboard')
+        site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+        site_utils._decode_lab_status(lab_status, _DEADBUILD)
 
 
     def _assert_lab_closed(self, lab_status):
         """Test that closed status values are handled properly.
 
-        Test that _decode_lab_status() raises LabIsDownException
+        Test that _decode_lab_status() raises TestLabException
         when the lab status is down.
 
         @param lab_status JSON value describing lab status.
 
         """
-        with self.assertRaises(site_utils.LabIsDownException):
-            site_utils._decode_lab_status(lab_status, None)
-        with self.assertRaises(site_utils.LabIsDownException):
-            site_utils._decode_lab_status(lab_status, 'liveboard')
-        with self.assertRaises(site_utils.LabIsDownException):
-            site_utils._decode_lab_status(lab_status, 'deadboard')
+        with self.assertRaises(site_utils.TestLabException):
+            site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+        with self.assertRaises(site_utils.TestLabException):
+            site_utils._decode_lab_status(lab_status, _DEADBUILD)
 
 
-    def _assert_lab_deadboard(self, lab_status):
-        """Test that disabled boards are handled properly.
+    def _assert_lab_deadbuild(self, lab_status):
+        """Test that disabled builds are handled properly.
 
-        Test that _decode_lab_status() raises
-        BoardIsDisabledException for board 'deadboard' and
-        succeeds otherwise.
+        Test that _decode_lab_status() raises TestLabException
+        for build `_DEADBUILD` and succeeds otherwise.
 
         @param lab_status JSON value describing lab status.
 
         """
-        site_utils._decode_lab_status(lab_status, None)
-        site_utils._decode_lab_status(lab_status, 'liveboard')
-        with self.assertRaises(site_utils.BoardIsDisabledException):
-            site_utils._decode_lab_status(lab_status, 'deadboard')
+        site_utils._decode_lab_status(lab_status, _LIVEBUILD)
+        with self.assertRaises(site_utils.TestLabException):
+            site_utils._decode_lab_status(lab_status, _DEADBUILD)
 
 
     def _assert_lab_status(self, test_values, checker):
@@ -333,10 +380,10 @@
                                 self._assert_lab_closed)
 
 
-    def test_dead_board(self):
-        """Test that disabled boards are handled correctly."""
-        self._assert_lab_status(_DEADBOARD_STATUS_VALUES,
-                                self._assert_lab_deadboard)
+    def test_dead_build(self):
+        """Test that disabled builds are handled correctly."""
+        self._assert_lab_status(_DEADBUILD_STATUS_VALUES,
+                                self._assert_lab_deadbuild)
 
 
 class CheckStatusTest(mox.MoxTestBase):
@@ -389,100 +436,51 @@
         site_utils._get_lab_status(_FAKE_URL).AndReturn(json_value)
 
 
-    def _try_check_no_board(self):
-        """Test calling check_lab_status() with no board."""
+    def _try_check_status(self, build):
+        """Test calling check_lab_status() with `build`."""
         try:
             self.mox.ReplayAll()
-            site_utils.check_lab_status()
+            site_utils.check_lab_status(build)
         finally:
             self.mox.VerifyAll()
 
 
-    def _try_check_dead_board(self):
-        """Test calling check_lab_status() with 'deadboard'."""
-        try:
-            self.mox.ReplayAll()
-            site_utils.check_lab_status('deadboard')
-        finally:
-            self.mox.VerifyAll()
-
-
-    def _try_check_live_board(self):
-        """Test calling check_lab_status() with 'liveboard'."""
-        try:
-            self.mox.ReplayAll()
-            site_utils.check_lab_status('liveboard')
-        finally:
-            self.mox.VerifyAll()
-
-
-    def test_non_cautotest_no_board(self):
-        """Test a call with no board when the host isn't cautotest."""
+    def test_non_cautotest(self):
+        """Test a call with a build when the host isn't cautotest."""
         self._setup_not_cautotest()
-        self._try_check_no_board()
+        self._try_check_status(_LIVEBUILD)
 
 
-    def test_non_cautotest_with_board(self):
-        """Test a call with a board when the host isn't cautotest."""
-        self._setup_not_cautotest()
-        self._try_check_live_board()
-
-
-    def test_no_status_no_board(self):
-        """Test without a board when `_get_lab_status()` returns `None`."""
+    def test_no_lab_status(self):
+        """Test with a build when `_get_lab_status()` returns `None`."""
         self._setup_no_status()
-        self._try_check_no_board()
+        self._try_check_status(_LIVEBUILD)
 
 
-    def test_no_lab_status_with_board(self):
-        """Test with a board when `_get_lab_status()` returns `None`."""
-        self._setup_no_status()
-        self._try_check_live_board()
-
-
-    def test_lab_up_no_board(self):
-        """Test lab open with no board specified."""
+    def test_lab_up_live_build(self):
+        """Test lab open with a build specified."""
         self._setup_lab_status(_OPEN_STATUS_VALUES[0])
-        self._try_check_no_board()
+        self._try_check_status(_LIVEBUILD)
 
 
-    def test_lab_up_live_board(self):
-        """Test lab open with a board specified."""
-        self._setup_lab_status(_OPEN_STATUS_VALUES[0])
-        self._try_check_live_board()
-
-
-    def test_lab_down_no_board(self):
-        """Test lab closed with no board specified."""
+    def test_lab_down_live_build(self):
+        """Test lab closed with a build specified."""
         self._setup_lab_status(_CLOSED_STATUS_VALUES[0])
-        with self.assertRaises(site_utils.LabIsDownException):
-            self._try_check_no_board()
+        with self.assertRaises(site_utils.TestLabException):
+            self._try_check_status(_LIVEBUILD)
 
 
-    def test_lab_down_live_board(self):
-        """Test lab closed with a board specified."""
-        self._setup_lab_status(_CLOSED_STATUS_VALUES[0])
-        with self.assertRaises(site_utils.LabIsDownException):
-            self._try_check_live_board()
+    def test_build_disabled_live_build(self):
+        """Test build disabled with a live build specified."""
+        self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
+        self._try_check_status(_LIVEBUILD)
 
 
-    def test_board_disabled_no_board(self):
-        """Test board disabled with no board specified."""
-        self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
-        self._try_check_no_board()
-
-
-    def test_board_disabled_live_board(self):
-        """Test board disabled with a live board specified."""
-        self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
-        self._try_check_live_board()
-
-
-    def test_board_disabled_dead_board(self):
-        """Test board disabled with the disabled board specified."""
-        self._setup_lab_status(_DEADBOARD_STATUS_VALUES[0])
-        with self.assertRaises(site_utils.BoardIsDisabledException):
-            self._try_check_dead_board()
+    def test_build_disabled_dead_build(self):
+        """Test build disabled with the disabled build specified."""
+        self._setup_lab_status(_DEADBUILD_STATUS_VALUES[0])
+        with self.assertRaises(site_utils.TestLabException):
+            self._try_check_status(_DEADBUILD)
 
 
 if __name__ == '__main__':
diff --git a/server/site_utils.py b/server/site_utils.py
index daa0a6b..d27aff3 100644
--- a/server/site_utils.py
+++ b/server/site_utils.py
@@ -25,13 +25,8 @@
 LAB_GOOD_STATES = ('open', 'throttled')
 
 
-class LabIsDownException(Exception):
-    """Raised when the Lab is Down"""
-    pass
-
-
-class BoardIsDisabledException(Exception):
-    """Raised when a certain board is disabled in the Lab"""
+class TestLabException(Exception):
+    """Exception raised when the Test Lab blocks a test or suite."""
     pass
 
 
@@ -178,50 +173,48 @@
     return None
 
 
-def _decode_lab_status(lab_status, board):
+def _decode_lab_status(lab_status, build):
     """Decode lab status, and report exceptions as needed.
 
-    Takes a deserialized JSON object from the lab status page, and
-    interprets it to determine the actual lab status.  Raises
+    Take a deserialized JSON object from the lab status page, and
+    interpret it to determine the actual lab status.  Raise
     exceptions as required to report when the lab is down.
 
-    @param board: board name that we want to check the status of.
+    @param build: build name that we want to check the status of.
 
-    @raises LabIsDownException if the lab is not up.
-    @raises BoardIsDisabledException if the desired board is currently
-                                           disabled.
+    @raises TestLabException Raised if a request to test for the given
+                             status and build should be blocked.
     """
     # First check if the lab is up.
     if not lab_status['general_state'] in LAB_GOOD_STATES:
-        raise LabIsDownException('Chromium OS Lab is currently not up: '
-                                 '%s.' % lab_status['message'])
+        raise TestLabException('Chromium OS Test Lab is closed: '
+                               '%s.' % lab_status['message'])
 
-    # Check if the board we wish to use is disabled.
+    # Check if the build we wish to use is disabled.
     # Lab messages should be in the format of:
-    # Lab is 'status' [boards not to be ran] (comment). Example:
-    # Lab is Open [stumpy, kiev, x86-alex] (power_resume rtc causing duts to go
-    # down)
-    boards_are_disabled = re.search('\[(.*)\]', lab_status['message'])
-    if board and boards_are_disabled:
-        if board in boards_are_disabled.group(1):
-            raise BoardIsDisabledException('Chromium OS Lab is '
-                    'currently not allowing suites to be scheduled on board '
-                    '%s: %s' % (board, lab_status['message']))
+    #    Lab is 'status' [regex ...] (comment)
+    # If the build name matches any regex, it will be blocked.
+    build_exceptions = re.search('\[(.*)\]', lab_status['message'])
+    if not build_exceptions:
+        return
+    for build_pattern in build_exceptions.group(1).split():
+        if re.search(build_pattern, build):
+            raise TestLabException('Chromium OS Test Lab is closed: '
+                                   '%s matches %s.' % (
+                                           build, build_pattern))
     return
 
 
-def check_lab_status(board=None):
-    """Check if the lab status allows us to schedule suites.
+def check_lab_status(build):
+    """Check if the lab status allows us to schedule for a build.
 
-    Also checks if the lab is disabled for that particular board, and if so
-    will raise an error to prevent new suites from being scheduled for that
-    board.
+    Checks if the lab is down, or if testing for the requested build
+    should be blocked.
 
-    @param board: board name that we want to check the status of.
+    @param build: Name of the build to be scheduled for testing.
 
-    @raises LabIsDownException if the lab is not up.
-    @raises BoardIsDisabledException if the desired board is currently
-                                           disabled.
+    @raises TestLabException Raised if a request to test for the given
+                             status and build should be blocked.
 
     """
     # Ensure we are trying to schedule on the actual lab.
@@ -238,4 +231,4 @@
         # We go ahead and say the lab is open if we can't get the status.
         logging.warn('Could not get a status from %s', status_url)
         return
-    _decode_lab_status(json_status, board)
+    _decode_lab_status(json_status, build)