[autotest] create_job should fail if the metahost cannot run the job

If one passes a specific list of hosts into `create_job`, then those
hosts are vetted to make sure they satisfy the dependencies of the job.

If one passes in a metahost, no checking is done.  This is unfortunate,
as it means one needs to be very careful when scheduling jobs with
metahosts to make sure that it is actually possible for the job to run.
Instead, let's just do the checking in the RPC and raise an
easy-to-identify Exception if we do hit this case.

BUG=chromium:250586
DEPLOY=apache
TEST=unit, Scheduled a job with a metahost and DEPENDENCIES that did not
exist.  Got an error.NoEligibleHostException.  Both dummy and bvt still
run (testing provisionable labels and tests with unsatisfyable
dependencies).

Change-Id: I020be2607867fafd04194b9c17d3052b006f60e0
Reviewed-on: https://chromium-review.googlesource.com/66603
Tested-by: Alexander Miller <milleral@chromium.org>
Reviewed-by: Dan Shi <dshi@chromium.org>
Reviewed-by: Prashanth Balasubramanian <beeps@chromium.org>
Commit-Queue: Alexander Miller <milleral@chromium.org>
diff --git a/frontend/afe/rpc_utils.py b/frontend/afe/rpc_utils.py
index 3d2b9e8..24af4c0 100644
--- a/frontend/afe/rpc_utils.py
+++ b/frontend/afe/rpc_utils.py
@@ -9,7 +9,7 @@
 import datetime, os, inspect
 import django.http
 from autotest_lib.frontend.afe import models, model_logic
-from autotest_lib.client.common_lib import control_data
+from autotest_lib.client.common_lib import control_data, error
 from autotest_lib.server.cros import provision
 
 NULL_DATETIME = datetime.datetime.max
@@ -245,6 +245,24 @@
                        (', '.join(failing_hosts))})
 
 
+def check_job_metahost_dependencies(metahost_objects, job_dependencies):
+    """
+    Check that at least one machine within the metahost spec satisfies the job's
+    dependencies.
+
+    @param metahost_objects A list of label objects representing the metahosts.
+    @param job_dependencies A list of strings of the required label names.
+    @raises NoEligibleHostException If a metahost cannot run the job.
+    """
+    for metahost in metahost_objects:
+        hosts = models.Host.objects.filter(labels=metahost)
+        for label_name in job_dependencies:
+            if not provision.can_provision(label_name):
+                hosts = hosts.filter(labels__name=label_name)
+        if not any(hosts):
+            raise error.NoEligibleHostException("No hosts within %s satisfy %s."
+                    % (metahost.name, ', '.join(job_dependencies)))
+
 
 def _execution_key_for(host_queue_entry):
     return (host_queue_entry.job.id, host_queue_entry.execution_subdir)
@@ -489,10 +507,6 @@
 
     check_for_duplicate_hosts(host_objects)
 
-    check_job_dependencies(host_objects, dependencies)
-
-    # There may be provisionable labels in the dependencies list
-    # that do not yet exist in the database. If so, create them.
     for label_name in dependencies:
         if provision.can_provision(label_name):
             # TODO: We could save a few queries
@@ -500,6 +514,10 @@
             # a bulk .get() call. The win is probably very small.
             _ensure_label_exists(label_name)
 
+    # This only checks targeted hosts, not hosts eligible due to the metahost
+    check_job_dependencies(host_objects, dependencies)
+    check_job_metahost_dependencies(metahost_objects, dependencies)
+
     options['dependencies'] = list(
             models.Label.objects.filter(name__in=dependencies))