[autotest] Hosts locked manually by users should not count as usable.

We need to ignore them in Reimager._count_usable_hosts(), just like we
do 'Repairing' and 'Repair Failed' hosts.

The trick is _not_ painting hosts correctly locked by the test infrastructure
with the same brush.  These hosts will become available in due time.

BUG=chromium-os:33623
TEST=unit
TEST=run_suite while all usable devices are locked manually.  Should fail.
TEST=run_suite while all usable devices are locked for reimaging.  Should run.

Change-Id: I7bf001d9503cdd027741760493cce53947f95e45
Reviewed-on: https://gerrit.chromium.org/gerrit/30768
Commit-Ready: Chris Masone <cmasone@chromium.org>
Reviewed-by: Chris Masone <cmasone@chromium.org>
Tested-by: Chris Masone <cmasone@chromium.org>
diff --git a/global_config.ini b/global_config.ini
index dcfc703..b6da726 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -137,6 +137,7 @@
 dev_server: http://172.22.50.2:8082,http://172.22.50.205:8082
 crash_server: http://172.22.50.2:8082
 sharding_factor: 1
+infrastructure_users: chromeos-test
 
 image_url_pattern: %s/update/%s
 package_url_pattern: %s/static/archive/%s/autotest/packages
diff --git a/server/cros/dynamic_suite/fakes.py b/server/cros/dynamic_suite/fakes.py
index 87df4e2..f0a5db3 100644
--- a/server/cros/dynamic_suite/fakes.py
+++ b/server/cros/dynamic_suite/fakes.py
@@ -32,9 +32,11 @@
 
 class FakeHost(object):
     """Faked out RPC-client-side Host object."""
-    def __init__(self, hostname='', status='Ready'):
+    def __init__(self, hostname='', status='Ready', locked=False, locked_by=''):
         self.hostname = hostname
         self.status = status
+        self.locked = locked
+        self.locked_by = locked_by
 
 
 class FakeLabel(object):
diff --git a/server/cros/dynamic_suite/reimager.py b/server/cros/dynamic_suite/reimager.py
index 9d135c0..b50ca27 100644
--- a/server/cros/dynamic_suite/reimager.py
+++ b/server/cros/dynamic_suite/reimager.py
@@ -162,6 +162,47 @@
                 'Too few hosts with %r' % labels)
 
 
+    def _count_usable_hosts(self, host_spec):
+        """
+        Given a set of host labels, count the live hosts that have them all.
+
+        @param host_spec: list of labels specifying a set of hosts.
+        @return the number of live hosts that satisfy |host_spec|.
+        """
+        count = 0
+        for host in self._afe.get_hosts(multiple_labels=host_spec):
+            if self._alive(host) and not self._incorrectly_locked(host):
+                count += 1
+        return count
+
+
+    def _alive(self, host):
+        """
+        Given a host, determine if the host is alive.
+
+        @param host: Host instance (as in server/frontend.py)
+        @return True if host is not under, or in need of, repair.  Else, False.
+        """
+        return host.status not in ['Repair Failed', 'Repairing']
+
+
+    def _incorrectly_locked(self, host):
+        """
+        Given a host, determine if the host is locked by some user.
+
+        If the host is unlocked, or locked by the test infrastructure,
+        this will return False.  Usernames defined as 'part of the test
+        infrastructure' are listed in global_config.ini under the [CROS]
+        section in the 'infrastructure_users' field.
+
+        @param host: Host instance (as in server/frontend.py)
+        @return False if the host is not locked, or locked by the infra.
+                True if the host is locked by someone we haven't blessed.
+        """
+        return (host.locked and
+                host.locked_by not in tools.infrastructure_user_list())
+
+
     def clear_reimaged_host_state(self, build):
         """
         Clear per-host state created in the autotest DB for this job.
@@ -204,20 +245,6 @@
                 {hashlib.md5(test_name).hexdigest(): job_id_owner})
 
 
-    def _count_usable_hosts(self, host_spec):
-        """
-        Given a set of host labels, count the live hosts that have them all.
-
-        @param host_spec: list of labels specifying a set of hosts.
-        @return the number of live hosts that satisfy |host_spec|.
-        """
-        count = 0
-        for h in self._afe.get_hosts(multiple_labels=host_spec):
-            if h.status not in ['Repair Failed', 'Repairing']:
-                count += 1
-        return count
-
-
     def _ensure_version_label(self, name):
         """
         Ensure that a label called |name| exists in the autotest DB.
diff --git a/server/cros/dynamic_suite/reimager_unittest.py b/server/cros/dynamic_suite/reimager_unittest.py
index 91afcfb..3d887ca 100644
--- a/server/cros/dynamic_suite/reimager_unittest.py
+++ b/server/cros/dynamic_suite/reimager_unittest.py
@@ -74,6 +74,23 @@
         self.reimager._ensure_version_label(name)
 
 
+    def testIncorrectlyLocked(self):
+        """Should detect hosts locked by random users."""
+        host = FakeHost(locked=True)
+        host.locked_by = 'some guy'
+        self.assertTrue(self.reimager._incorrectly_locked(host))
+
+
+    def testNotIncorrectlyLocked(self):
+        """Should accept hosts locked by the infrastructure."""
+        infra_user = 'an infra user'
+        self.mox.StubOutWithMock(tools, 'infrastructure_user_list')
+        tools.infrastructure_user_list().AndReturn([infra_user])
+        host = FakeHost(locked=True, locked_by=infra_user)
+        self.mox.ReplayAll()
+        self.assertFalse(self.reimager._incorrectly_locked(host))
+
+
     def testCountHostsByBoardAndPool(self):
         """Should count available hosts by board and pool."""
         spec = [self._BOARD, 'pool:bvt']
@@ -98,6 +115,27 @@
         self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
 
 
+    def testCountAllHostsIncorrectlyLockedByBoard(self):
+        """Should count the available hosts, by board, getting a locked host."""
+        spec = [self._BOARD]
+        badly_locked_host = FakeHost(locked=True, locked_by = 'some guy')
+        self.afe.get_hosts(multiple_labels=spec).AndReturn([badly_locked_host])
+        self.mox.ReplayAll()
+        self.assertEquals(self.reimager._count_usable_hosts(spec), 0)
+
+
+    def testCountAllHostsInfraLockedByBoard(self):
+        """Should count the available hosts, get a host locked by infra."""
+        infra_user = 'an infra user'
+        self.mox.StubOutWithMock(tools, 'infrastructure_user_list')
+        spec = [self._BOARD]
+        self.afe.get_hosts(multiple_labels=spec).AndReturn(
+            [FakeHost(locked=True, locked_by=infra_user)])
+        tools.infrastructure_user_list().AndReturn([infra_user])
+        self.mox.ReplayAll()
+        self.assertEquals(self.reimager._count_usable_hosts(spec), 1)
+
+
     def testScheduleJob(self):
         """Should be able to create a job with the AFE."""
         # Fake out getting the autoupdate control file contents.
diff --git a/server/cros/dynamic_suite/tools.py b/server/cros/dynamic_suite/tools.py
index 08205ad..8ab674c 100644
--- a/server/cros/dynamic_suite/tools.py
+++ b/server/cros/dynamic_suite/tools.py
@@ -18,6 +18,11 @@
     return _CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
 
 
+def infrastructure_user_list():
+    return _CONFIG.get_config_value('CROS', 'infrastructure_users', type=list,
+                                    default=[])
+
+
 def package_url_pattern():
     return _CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)