Added a new input that allows used to specify a one-time host when
creating a job. The job will be run against that host once, and the
host will not appear in the "Available hosts" selector.

Risk: medium (deleting records from database)
Visibility: medium (adding an input field)


git-svn-id: http://test.kernel.org/svn/autotest/trunk@1768 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/models.py b/frontend/afe/models.py
index b8ec10d..c0ce596 100644
--- a/frontend/afe/models.py
+++ b/frontend/afe/models.py
@@ -86,6 +86,21 @@
     objects = model_logic.ExtendedManager()
     valid_objects = model_logic.ValidObjectsManager()
 
+    @staticmethod
+    def create_one_time_host(hostname):
+        query = Host.objects.filter(hostname=hostname)
+        if query.count() == 0:
+            host = Host(hostname=hostname, invalid=True)
+        else:
+            host = query[0]
+            if not host.invalid:
+                raise model_logic.ValidationError({
+                    'hostname' : '%s already exists!' % hostname
+                    })
+            host.clean_object()
+            host.status = Host.Status.READY
+        host.save()
+        return host
 
     def clean_object(self):
         self.aclgroup_set.clear()
@@ -102,6 +117,12 @@
             everyone = AclGroup.objects.get(name='Everyone')
             everyone.hosts.add(self)
 
+    def delete(self):
+        for queue_entry in self.hostqueueentry_set.all():
+            queue_entry.deleted = True
+            queue_entry.abort()
+        super(Host, self).delete()
+
 
     def enqueue_job(self, job):
         ' Enqueue a job on this host.'
@@ -466,7 +487,8 @@
     def requeue(self, new_owner):
         'Creates a new job identical to this one'
         hosts = [queue_entry.meta_host or queue_entry.host
-                 for queue_entry in self.hostqueueentry_set.all()]
+                 for queue_entry
+                 in self.hostqueueentry_set.filter(deleted=False)]
         new_job = Job.create(
             owner=new_owner, name=self.name, priority=self.priority,
             control_file=self.control_file,
@@ -478,13 +500,7 @@
 
     def abort(self):
         for queue_entry in self.hostqueueentry_set.all():
-            if queue_entry.active:
-                queue_entry.status = Job.Status.ABORT
-            elif not queue_entry.complete:
-                queue_entry.status = Job.Status.ABORTED
-                queue_entry.active = False
-                queue_entry.complete = True
-            queue_entry.save()
+            queue_entry.abort()
 
 
     def user(self):
@@ -528,6 +544,7 @@
                                     db_column='meta_host')
     active = dbmodels.BooleanField(default=False)
     complete = dbmodels.BooleanField(default=False)
+    deleted = dbmodels.BooleanField(default=False)
 
     objects = model_logic.ExtendedManager()
 
@@ -536,6 +553,14 @@
         'True if this is a entry has a meta_host instead of a host.'
         return self.host is None and self.meta_host is not None
 
+    def abort(self):
+        if self.active:
+            self.status = Job.Status.ABORT
+        elif not self.complete:
+            self.status = Job.Status.ABORTED
+            self.active = False
+            self.complete = True
+        self.save()
 
     class Meta:
         db_table = 'host_queue_entries'
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 2e03eff..f482530 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -250,7 +250,8 @@
 
 
 def create_job(name, priority, control_file, control_type, timeout=None,
-               is_synchronous=None, hosts=None, meta_hosts=None):
+               is_synchronous=None, hosts=None, meta_hosts=None,
+               one_time_hosts=None):
     """\
     Create and enqueue a job.
 
@@ -271,10 +272,10 @@
 
     owner = rpc_utils.get_user().login
     # input validation
-    if not hosts and not meta_hosts:
+    if not hosts and not meta_hosts and not one_time_hosts:
         raise model_logic.ValidationError({
-            'arguments' : "You must pass at least one of 'hosts' or "
-                          "'meta_hosts'"
+            'arguments' : "You must pass at least one of 'hosts', "
+                          "'meta_hosts', or 'one_time_hosts'"
             })
 
     requested_host_counts = {}
@@ -289,6 +290,9 @@
         host_objects.append(this_label)
         requested_host_counts.setdefault(this_label.name, 0)
         requested_host_counts[this_label.name] += 1
+    for host in one_time_hosts or []:
+        this_host = models.Host.create_one_time_host(host)
+        host_objects.append(this_host)
 
     # check that each metahost request has enough hosts under the label
     if meta_hosts:
diff --git a/frontend/client/src/autotest/afe/CreateJobView.java b/frontend/client/src/autotest/afe/CreateJobView.java
index 4647b9c..f5d5b06 100644
--- a/frontend/client/src/autotest/afe/CreateJobView.java
+++ b/frontend/client/src/autotest/afe/CreateJobView.java
@@ -619,6 +619,8 @@
                 HostSelector.HostSelection hosts = hostSelector.getSelectedHosts();
                 args.put("hosts", Utils.stringsToJSON(hosts.hosts));
                 args.put("meta_hosts", Utils.stringsToJSON(hosts.metaHosts));
+                args.put("one_time_hosts",
+                    Utils.stringsToJSON(hosts.oneTimeHosts));
                 
                 rpcProxy.rpcCall("create_job", args, new JsonRpcCallback() {
                     @Override
diff --git a/frontend/client/src/autotest/afe/HostSelector.java b/frontend/client/src/autotest/afe/HostSelector.java
index 1cab559..8f74e1c 100644
--- a/frontend/client/src/autotest/afe/HostSelector.java
+++ b/frontend/client/src/autotest/afe/HostSelector.java
@@ -39,10 +39,12 @@
 public class HostSelector {
     public static final int TABLE_SIZE = 10;
     public static final String META_PREFIX = "Any ";
+    public static final String ONE_TIME = "(one-time host)";
     
     static class HostSelection {
         public List<String> hosts = new ArrayList<String>();
         public List<String> metaHosts = new ArrayList<String>();
+        public List<String> oneTimeHosts = new ArrayList<String>();
     }
     
     protected ArrayDataSource<JSONObject> selectedHostData =
@@ -150,6 +152,21 @@
         RootPanel.get("create_meta_select").add(metaLabelSelect);
         RootPanel.get("create_meta_number").add(metaNumber);
         RootPanel.get("create_meta_button").add(metaButton);
+        
+        final TextBox oneTimeHostField = new TextBox();
+        final Button oneTimeHostButton = new Button("Add");
+        oneTimeHostButton.addClickListener(new ClickListener() {
+            public void onClick(Widget sender) {
+                String hostname = oneTimeHostField.getText();
+                JSONObject oneTimeObject = new JSONObject();
+                oneTimeObject.put("hostname", new JSONString(hostname));
+                oneTimeObject.put("platform", new JSONString(ONE_TIME));
+                selectRow(oneTimeObject);
+                selectionRefresh();
+            }
+        });
+        RootPanel.get("create_one_time_field").add(oneTimeHostField);
+        RootPanel.get("create_one_time_button").add(oneTimeHostButton);
     }
     
     protected void selectMetaHost(String label, String number) {
@@ -216,6 +233,14 @@
         return Integer.parseInt(getHostname(row).substring(META_PREFIX.length()));
     }
     
+    protected boolean isOneTimeHost(JSONObject row) {
+        JSONString platform = row.get("platform").isString();
+        if (platform == null) {
+            return false;
+        }
+        return platform.stringValue().equals(ONE_TIME);
+    }
+    
     /**
      * Retrieve the set of selected hosts.
      */
@@ -232,7 +257,11 @@
             }
             else {
                 String hostname = getHostname(row);
-                selection.hosts.add(hostname);
+                if (isOneTimeHost(row)) {
+                    selection.oneTimeHosts.add(hostname);
+                } else {
+                    selection.hosts.add(hostname);
+                }
             }
         }
         
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index d9a8442..540cb50 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -89,6 +89,11 @@
             <td colspan="3" class="box">
               <table><tr>
                 <td>
+                  <div>
+                  <span class="field-name">One-time host:</span>
+                  <span id="create_one_time_field"></span>
+                  <span id="create_one_time_button"></span>
+                  </div>
                   <div class="box-full">
                     Run on any <span id="create_meta_select"></span><br>
                     Number: <span id="create_meta_number"></span>
diff --git a/frontend/migrations/011_support_one_time_hosts.py b/frontend/migrations/011_support_one_time_hosts.py
new file mode 100644
index 0000000..96c666d
--- /dev/null
+++ b/frontend/migrations/011_support_one_time_hosts.py
@@ -0,0 +1,41 @@
+def migrate_up(manager):
+    manager.execute_script(CLEAN_DATABASE)
+    manager.execute(ADD_HOST_QUEUE_DELETED_COLUMN)
+    manager.execute(DROP_DEFAULT)
+
+def migrate_down(manager):
+    manager.execute(DROP_HOST_QUEUE_DELETED_COLUMN)
+
+CLEAN_DATABASE = """DELETE FROM acl_groups_hosts
+                    WHERE host_id IN
+                        (SELECT id FROM hosts WHERE invalid = TRUE);
+
+                    DELETE FROM ineligible_host_queues
+                    WHERE host_id IN
+                        (SELECT id FROM hosts WHERE invalid = TRUE);
+
+                    UPDATE host_queue_entries
+                    SET status = 'Abort'
+                    WHERE host_id IN
+                        (SELECT id FROM hosts WHERE invalid = TRUE)
+                        AND active = TRUE;
+
+                    UPDATE host_queue_entries
+                    SET status = 'Aborted', complete = TRUE
+                    WHERE host_id IN
+                        (SELECT id FROM hosts WHERE invalid = TRUE)
+                        AND active = FALSE AND complete = FALSE;
+
+                    DELETE FROM hosts_labels
+                    WHERE host_id IN
+                        (SELECT id FROM hosts WHERE invalid = TRUE);"""
+
+DROP_HOST_QUEUE_DELETED_COLUMN = """ALTER TABLE host_queue_entries
+                                    DROP COLUMN deleted"""
+
+ADD_HOST_QUEUE_DELETED_COLUMN = """ALTER TABLE host_queue_entries
+                                   ADD COLUMN deleted BOOLEAN
+                                       NOT NULL DEFAULT FALSE"""
+
+DROP_DEFAULT = """ALTER TABLE host_queue_entries
+                  ALTER COLUMN deleted DROP DEFAULT"""
diff --git a/scheduler/monitor_db.py b/scheduler/monitor_db.py
index 410904b..667e4ee 100644
--- a/scheduler/monitor_db.py
+++ b/scheduler/monitor_db.py
@@ -552,9 +552,9 @@
             extra_join +
             # exclude hosts with a running entry
             'WHERE active_hqe.host_id IS NULL '
-            # exclude locked, invalid, and non-Ready hosts
+            # exclude locked and non-Ready hosts
             """
-            AND h.locked=false AND h.invalid=false
+            AND h.locked=false
             AND (h.status IS null OR h.status='Ready')
             """)
         if extra_where:
@@ -600,7 +600,7 @@
         LEFT JOIN ineligible_host_queues AS ihq
         ON (ihq.job_id=queued_hqe.job_id AND ihq.host_id=h.id)
         """
-        block_where = 'ihq.id IS NULL'
+        block_where = 'ihq.id IS NULL AND h.invalid = FALSE'
         extra_join = '\n'.join([labels_join, queued_hqe_join,
                                 acl_join, block_join])
         return self._get_runnable_entries(extra_join,
@@ -1482,7 +1482,7 @@
     @classmethod
     def _fields(cls):
         return ['id', 'job_id', 'host_id', 'priority', 'status',
-                  'meta_host', 'active', 'complete']
+                  'meta_host', 'active', 'complete', 'deleted']
 
 
     def set_host(self, host):