Add ability to associate drone sets with jobs. This restricts a job to
running on a specified set of drones.
Signed-off-by: James Ren <[email protected]>
git-svn-id: http://test.kernel.org/svn/autotest/trunk@4439 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/admin.py b/frontend/afe/admin.py
index df5d9a7..0b92670 100644
--- a/frontend/afe/admin.py
+++ b/frontend/afe/admin.py
@@ -136,6 +136,14 @@
admin.site.register(models.AclGroup, AclGroupAdmin)
+class DroneSetAdmin(SiteAdmin):
+ filter_horizontal = ('drones',)
+
+admin.site.register(models.DroneSet, DroneSetAdmin)
+
+admin.site.register(models.Drone)
+
+
if settings.FULL_ADMIN:
class JobAdmin(SiteAdmin):
list_display = ('id', 'owner', 'name', 'control_type')
diff --git a/frontend/afe/doctests/001_rpc_test.txt b/frontend/afe/doctests/001_rpc_test.txt
index 8b0f1d7..7e33a86 100644
--- a/frontend/afe/doctests/001_rpc_test.txt
+++ b/frontend/afe/doctests/001_rpc_test.txt
@@ -30,6 +30,10 @@
>>> from autotest_lib.client.common_lib import logging_manager
>>> logging_manager.logger.setLevel(100)
+>>> drone_set = models.DroneSet.default_drone_set_name()
+>>> if drone_set:
+... _ = models.DroneSet.objects.create(name=drone_set)
+
# basic interface test
######################
@@ -184,6 +188,7 @@
... 'access_level': 1,
... 'reboot_before': 'If dirty',
... 'reboot_after': 'Always',
+... 'drone_set': None,
... 'show_experimental': False}]
True
>>> rpc_interface.delete_user('showard')
@@ -538,7 +543,8 @@
... 'email_list': '',
... 'reboot_before': 'If dirty',
... 'reboot_after': 'Always',
-... 'parse_failed_repair': True}
+... 'parse_failed_repair': True,
+... 'drone_set': drone_set}
True
# get_host_queue_entries returns a lot of data, so let's only check a couple
diff --git a/frontend/afe/frontend_test_utils.py b/frontend/afe/frontend_test_utils.py
index 637c286..5aba4f4 100644
--- a/frontend/afe/frontend_test_utils.py
+++ b/frontend/afe/frontend_test_utils.py
@@ -8,6 +8,10 @@
class FrontendTestMixin(object):
def _fill_in_test_data(self):
"""Populate the test database with some hosts and labels."""
+ if models.DroneSet.drone_sets_enabled():
+ models.DroneSet.objects.create(
+ name=models.DroneSet.default_drone_set_name())
+
acl_group = models.AclGroup.objects.create(name='my_acl')
acl_group.users.add(models.User.current_user())
@@ -72,7 +76,8 @@
def _create_job(self, hosts=[], metahosts=[], priority=0, active=False,
- synchronous=False, atomic_group=None, hostless=False):
+ synchronous=False, atomic_group=None, hostless=False,
+ drone_set=None):
"""
Create a job row in the test database.
@@ -93,6 +98,10 @@
@returns A Django frontend.afe.models.Job instance.
"""
+ if not drone_set:
+ drone_set = (models.DroneSet.default_drone_set_name()
+ and models.DroneSet.get_default())
+
assert not (atomic_group and active) # TODO(gps): support this
synch_count = synchronous and 2 or 1
created_on = datetime.datetime(2008, 1, 1)
@@ -102,7 +111,8 @@
job = models.Job.objects.create(
name='test', owner='autotest_system', priority=priority,
synch_count=synch_count, created_on=created_on,
- reboot_before=model_attributes.RebootBefore.NEVER)
+ reboot_before=model_attributes.RebootBefore.NEVER,
+ drone_set=drone_set)
for host_id in hosts:
models.HostQueueEntry.objects.create(job=job, host_id=host_id,
status=status,
@@ -126,11 +136,12 @@
def _create_job_simple(self, hosts, use_metahost=False,
- priority=0, active=False):
+ priority=0, active=False, drone_set=None):
"""An alternative interface to _create_job"""
args = {'hosts' : [], 'metahosts' : []}
if use_metahost:
args['metahosts'] = hosts
else:
args['hosts'] = hosts
- return self._create_job(priority=priority, active=active, **args)
+ return self._create_job(priority=priority, active=active,
+ drone_set=drone_set, **args)
diff --git a/frontend/afe/models.py b/frontend/afe/models.py
index eceb1f5..3370442 100644
--- a/frontend/afe/models.py
+++ b/frontend/afe/models.py
@@ -116,6 +116,119 @@
return unicode(self.name)
+class Drone(dbmodels.Model, model_logic.ModelExtensions):
+ """
+ A scheduler drone
+
+ hostname: the drone's hostname
+ """
+ hostname = dbmodels.CharField(max_length=255, unique=True)
+
+ name_field = 'hostname'
+ objects = model_logic.ExtendedManager()
+
+
+ def save(self, *args, **kwargs):
+ if not User.current_user().is_superuser():
+ raise Exception('Only superusers may edit drones')
+ super(Drone, self).save(*args, **kwargs)
+
+
+ def delete(self):
+ if not User.current_user().is_superuser():
+ raise Exception('Only superusers may delete drones')
+ super(Drone, self).delete()
+
+
+ class Meta:
+ db_table = 'afe_drones'
+
+ def __unicode__(self):
+ return unicode(self.hostname)
+
+
+class DroneSet(dbmodels.Model, model_logic.ModelExtensions):
+ """
+ A set of scheduler drones
+
+ These will be used by the scheduler to decide what drones a job is allowed
+ to run on.
+
+ name: the drone set's name
+ drones: the drones that are part of the set
+ """
+ DRONE_SETS_ENABLED = global_config.global_config.get_config_value(
+ 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False)
+ DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value(
+ 'SCHEDULER', 'default_drone_set_name', default=None)
+
+ name = dbmodels.CharField(max_length=255, unique=True)
+ drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones')
+
+ name_field = 'name'
+ objects = model_logic.ExtendedManager()
+
+
+ def save(self, *args, **kwargs):
+ if not User.current_user().is_superuser():
+ raise Exception('Only superusers may edit drone sets')
+ super(DroneSet, self).save(*args, **kwargs)
+
+
+ def delete(self):
+ if not User.current_user().is_superuser():
+ raise Exception('Only superusers may delete drone sets')
+ super(DroneSet, self).delete()
+
+
+ @classmethod
+ def drone_sets_enabled(cls):
+ return cls.DRONE_SETS_ENABLED
+
+
+ @classmethod
+ def default_drone_set_name(cls):
+ return cls.DEFAULT_DRONE_SET_NAME
+
+
+ @classmethod
+ def get_default(cls):
+ return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME)
+
+
+ @classmethod
+ def resolve_name(cls, drone_set_name):
+ """
+ Returns the name of one of these, if not None, in order of preference:
+ 1) the drone set given,
+ 2) the current user's default drone set, or
+ 3) the global default drone set
+
+ or returns None if drone sets are disabled
+ """
+ if not cls.drone_sets_enabled():
+ return None
+
+ user = User.current_user()
+ user_drone_set_name = user.drone_set and user.drone_set.name
+
+ return drone_set_name or user_drone_set_name or cls.get_default().name
+
+
+ def get_drone_hostnames(self):
+ """
+ Gets the hostnames of all drones in this drone set
+ """
+ return set(self.drones.all().values_list('hostname', flat=True))
+
+
+ class Meta:
+ db_table = 'afe_drone_sets'
+
+ def __unicode__(self):
+ return unicode(self.name)
+
+
class User(dbmodels.Model, model_logic.ModelExtensions):
"""\
Required:
@@ -140,6 +253,7 @@
reboot_after = dbmodels.SmallIntegerField(
choices=model_attributes.RebootAfter.choices(), blank=True,
default=DEFAULT_REBOOT_AFTER)
+ drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
show_experimental = dbmodels.BooleanField(default=False)
name_field = 'login'
@@ -644,6 +758,7 @@
reboot_after: Never, If all tests passed, or Always
parse_failed_repair: if True, a failed repair launched by this job will have
its results parsed as part of the job.
+ drone_set: The set of drones to run this job on
"""
DEFAULT_TIMEOUT = global_config.global_config.get_config_value(
'AUTOTEST_WEB', 'job_timeout_default', default=240)
@@ -682,6 +797,7 @@
parse_failed_repair = dbmodels.BooleanField(
default=DEFAULT_PARSE_FAILED_REPAIR)
max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS)
+ drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True)
# custom manager
@@ -706,6 +822,8 @@
if options.get('reboot_after') is None:
options['reboot_after'] = user.get_reboot_after_display()
+ drone_set = DroneSet.resolve_name(options.get('drone_set'))
+
job = cls.add_object(
owner=owner,
name=options['name'],
@@ -720,7 +838,8 @@
reboot_before=options.get('reboot_before'),
reboot_after=options.get('reboot_after'),
parse_failed_repair=options.get('parse_failed_repair'),
- created_on=datetime.now())
+ created_on=datetime.now(),
+ drone_set=drone_set)
job.dependency_labels = options['dependencies']
@@ -995,7 +1114,7 @@
host = dbmodels.ForeignKey(Host, blank=False, null=False)
task = dbmodels.CharField(max_length=64, choices=Task.choices(),
blank=False, null=False)
- requested_by = dbmodels.ForeignKey(User, blank=True, null=True)
+ requested_by = dbmodels.ForeignKey(User)
time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False,
null=False)
is_active = dbmodels.BooleanField(default=False, blank=False, null=False)
diff --git a/frontend/afe/models_test.py b/frontend/afe/models_test.py
index 244aa47..c01c9cd 100755
--- a/frontend/afe/models_test.py
+++ b/frontend/afe/models_test.py
@@ -77,7 +77,8 @@
def _create_task(self):
return models.SpecialTask.objects.create(
- host=self.hosts[0], task=models.SpecialTask.Task.VERIFY)
+ host=self.hosts[0], task=models.SpecialTask.Task.VERIFY,
+ requested_by=models.User.current_user())
def test_execution_path(self):
diff --git a/frontend/afe/resources.py b/frontend/afe/resources.py
index b47c5f5..eaeeea0 100644
--- a/frontend/afe/resources.py
+++ b/frontend/afe/resources.py
@@ -643,9 +643,11 @@
rep = super(Job, self).full_representation()
queue_entries = QueueEntryCollection(self._request)
queue_entries.set_query_parameters(job=self.instance.id)
+ drone_set = self.instance.drone_set and self.instance.drone_set.name
rep.update({'email_list': self.instance.email_list,
'parse_failed_repair':
bool(self.instance.parse_failed_repair),
+ 'drone_set': drone_set,
'execution_info':
ExecutionInfo.execution_info_from_job(self.instance),
'queue_entries': queue_entries.link(),
@@ -686,6 +688,7 @@
reboot_before=execution_info.get('cleanup_before_job'),
reboot_after=execution_info.get('cleanup_after_job'),
parse_failed_repair=input_dict.get('parse_failed_repair', None),
+ drone_set=input_dict.get('drone_set', None),
keyvals=input_dict.get('keyvals', None))
host_objects, metahost_label_objects, atomic_group = [], [], None
diff --git a/frontend/afe/resources_test.py b/frontend/afe/resources_test.py
index e3551b4..9120335 100644
--- a/frontend/afe/resources_test.py
+++ b/frontend/afe/resources_test.py
@@ -350,6 +350,7 @@
'execution_info': {'control_file': self.CONTROL_FILE_CONTENTS,
'is_server': True},
'owner': owner,
+ 'drone_set': models.DroneSet.default_drone_set_name(),
'queue_entries':
[{'host': {'href': self.URI_PREFIX + '/hosts/host1'}},
{'host': {'href': self.URI_PREFIX + '/hosts/host2'}}]}
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index ab2596a..fc60a75 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -403,7 +403,7 @@
timeout=None, max_runtime_hrs=None, run_verify=True,
email_list='', dependencies=(), reboot_before=None,
reboot_after=None, parse_failed_repair=None, hostless=False,
- keyvals=None):
+ keyvals=None, drone_set=None):
"""\
Create and enqueue a job.
@@ -432,6 +432,7 @@
one host will be chosen from that label to run the job on.
@param one_time_hosts List of hosts not in the database to run the job on.
@param atomic_group_name The name of an atomic group to schedule the job on.
+ @param drone_set The name of the drone set to run this test on.
@returns The created Job id number.
@@ -529,7 +530,8 @@
reboot_before=reboot_before,
reboot_after=reboot_after,
parse_failed_repair=parse_failed_repair,
- keyvals=keyvals)
+ keyvals=keyvals,
+ drone_set=drone_set)
return rpc_utils.create_new_job(owner=owner,
options=options,
host_objects=host_objects,
@@ -656,6 +658,7 @@
else:
info['atomic_group_name'] = None
info['hostless'] = job_info['hostless']
+ info['drone_set'] = job.drone_set and job.drone_set.name
return rpc_utils.prepare_for_serialization(info)
@@ -802,6 +805,11 @@
"""
job_fields = models.Job.get_field_dict()
+ default_drone_set_name = models.DroneSet.default_drone_set_name()
+ drone_sets = ([default_drone_set_name] +
+ sorted(drone_set.name for drone_set in
+ models.DroneSet.objects.exclude(
+ name=default_drone_set_name)))
result = {}
result['priorities'] = models.Job.Priority.choices()
@@ -824,6 +832,8 @@
result['reboot_before_options'] = model_attributes.RebootBefore.names
result['reboot_after_options'] = model_attributes.RebootAfter.names
result['motd'] = rpc_utils.get_motd()
+ result['drone_sets_enabled'] = models.DroneSet.drone_sets_enabled()
+ result['drone_sets'] = drone_sets
result['status_dictionary'] = {"Aborted": "Aborted",
"Verifying": "Verifying Host",
diff --git a/frontend/afe/rpc_interface_unittest.py b/frontend/afe/rpc_interface_unittest.py
index 9ba13cb..900f46c 100755
--- a/frontend/afe/rpc_interface_unittest.py
+++ b/frontend/afe/rpc_interface_unittest.py
@@ -212,13 +212,14 @@
self.task1 = models.SpecialTask.objects.create(
host=host, task=models.SpecialTask.Task.VERIFY,
time_started=datetime.datetime(2009, 1, 1), # ran before job 1
- is_complete=True)
+ is_complete=True, requested_by=models.User.current_user())
self.task2 = models.SpecialTask.objects.create(
host=host, task=models.SpecialTask.Task.VERIFY,
queue_entry=entry2, # ran with job 2
- is_active=True)
+ is_active=True, requested_by=models.User.current_user())
self.task3 = models.SpecialTask.objects.create(
- host=host, task=models.SpecialTask.Task.VERIFY) # not yet run
+ host=host, task=models.SpecialTask.Task.VERIFY,
+ requested_by=models.User.current_user()) # not yet run
def test_get_special_tasks(self):
diff --git a/frontend/client/src/autotest/afe/AfeUtils.java b/frontend/client/src/autotest/afe/AfeUtils.java
index 691c549..f5124db 100644
--- a/frontend/client/src/autotest/afe/AfeUtils.java
+++ b/frontend/client/src/autotest/afe/AfeUtils.java
@@ -16,6 +16,9 @@
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ListBox;
import java.util.ArrayList;
import java.util.Collection;
@@ -312,6 +315,28 @@
chooser.addChoice(Utils.jsonToString(jsonOption));
}
}
+
+ public static void popualateListBox(ListBox box, String staticDataKey) {
+ JSONArray options = staticData.getData(staticDataKey).isArray();
+ for (JSONString jsonOption : new JSONArrayList<JSONString>(options)) {
+ box.addItem(Utils.jsonToString(jsonOption));
+ }
+ }
+
+ public static void setSelectedItem(ListBox box, String item) {
+ box.setSelectedIndex(0);
+ for (int i = 0; i < box.getItemCount(); i++) {
+ if (box.getItemText(i).equals(item)) {
+ box.setSelectedIndex(i);
+ break;
+ }
+ }
+ }
+
+ public static void removeElement(String id) {
+ Element element = DOM.getElementById(id);
+ element.getParentElement().removeChild(element);
+ }
public static int parsePositiveIntegerInput(String input, String fieldName) {
final int parsedInt;
diff --git a/frontend/client/src/autotest/afe/CreateJobView.java b/frontend/client/src/autotest/afe/CreateJobView.java
index 74249d5..4b43e86 100644
--- a/frontend/client/src/autotest/afe/CreateJobView.java
+++ b/frontend/client/src/autotest/afe/CreateJobView.java
@@ -179,6 +179,7 @@
new CheckBoxPanel<CheckBox>(TEST_COLUMNS);
private CheckBox runNonProfiledIteration =
new CheckBox("Run each test without profilers first");
+ private ListBox droneSet = new ListBox();
protected TextArea controlFile = new TextArea();
protected DisclosurePanel controlFilePanel = new DisclosurePanel();
protected ControlTypeSelect controlTypeSelect;
@@ -238,6 +239,9 @@
if (hostless.getValue()) {
hostSelector.setEnabled(false);
}
+ if (cloneObject.get("drone_set").isNull() == null) {
+ AfeUtils.setSelectedItem(droneSet, Utils.jsonToString(cloneObject.get("drone_set")));
+ }
controlTypeSelect.setControlType(
jobObject.get("control_type").isString().stringValue());
@@ -455,8 +459,9 @@
@Override
public void initialize() {
super.initialize();
+
populatePriorities(staticData.getData("priorities").isArray());
-
+
BlurHandler kernelBlurHandler = new BlurHandler() {
public void onBlur(BlurEvent event) {
generateControlFile(false);
@@ -617,6 +622,13 @@
addWidget(createTemplateJobButton, "create_template_job");
addWidget(resetButton, "create_reset");
+ if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
+ AfeUtils.popualateListBox(droneSet, "drone_sets");
+ addWidget(droneSet, "create_drone_set");
+ } else {
+ AfeUtils.removeElement("create_drone_set_wrapper");
+ }
+
testSelector.setListener(this);
}
@@ -693,6 +705,11 @@
args.put("parse_failed_repair",
JSONBoolean.getInstance(parseFailedRepair.getValue()));
args.put("hostless", JSONBoolean.getInstance(hostless.getValue()));
+
+ if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
+ args.put("drone_set",
+ new JSONString(droneSet.getItemText(droneSet.getSelectedIndex())));
+ }
HostSelector.HostSelection hosts = hostSelector.getSelectedHosts();
args.put("hosts", Utils.stringsToJSON(hosts.hosts));
@@ -773,10 +790,17 @@
String defaultOption = Utils.jsonToString(user.get(name));
chooser.setDefaultChoice(defaultOption);
}
-
+
+ private void selectPreferredDroneSet() {
+ JSONObject user = staticData.getData("current_user").isObject();
+ String preference = Utils.jsonToString(user.get("drone_set"));
+ AfeUtils.setSelectedItem(droneSet, preference);
+ }
+
public void onPreferencesChanged() {
setRebootSelectorDefault(rebootBefore, "reboot_before");
setRebootSelectorDefault(rebootAfter, "reboot_after");
+ selectPreferredDroneSet();
testSelector.reset();
}
}
diff --git a/frontend/client/src/autotest/afe/JobDetailView.java b/frontend/client/src/autotest/afe/JobDetailView.java
index c7c804b..7e37585 100644
--- a/frontend/client/src/autotest/afe/JobDetailView.java
+++ b/frontend/client/src/autotest/afe/JobDetailView.java
@@ -76,6 +76,8 @@
private Label controlFile = new Label();
private DisclosurePanel controlFilePanel = new DisclosurePanel("");
+ protected StaticDataRepository staticData = StaticDataRepository.getRepository();
+
public JobDetailView(JobDetailListener listener) {
this.listener = listener;
}
@@ -114,6 +116,10 @@
showField(jobObject, "synch_count", "view_synch_count");
showField(jobObject, "dependencies", "view_dependencies");
+ if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
+ showField(jobObject, "drone_set", "view_drone_set");
+ }
+
String header = Utils.jsonToString(jobObject.get("control_type")) + " control file";
controlFilePanel.getHeaderTextAccessor().setText(header);
controlFile.setText(Utils.jsonToString(jobObject.get("control_file")));
@@ -208,6 +214,10 @@
controlFile.addStyleName("code");
controlFilePanel.setContent(controlFile);
addWidget(controlFilePanel, "view_control_file");
+
+ if (!staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
+ AfeUtils.removeElement("view_drone_set_wrapper");
+ }
}
diff --git a/frontend/client/src/autotest/afe/UserPreferencesView.java b/frontend/client/src/autotest/afe/UserPreferencesView.java
index 217819e..057905d 100644
--- a/frontend/client/src/autotest/afe/UserPreferencesView.java
+++ b/frontend/client/src/autotest/afe/UserPreferencesView.java
@@ -18,6 +18,7 @@
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HTMLTable;
+import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
@@ -35,6 +36,7 @@
private RadioChooser rebootBefore = new RadioChooser();
private RadioChooser rebootAfter = new RadioChooser();
+ private ListBox droneSet = new ListBox();
private CheckBox showExperimental = new CheckBox();
private Button saveButton = new Button("Save preferences");
private HTMLTable preferencesTable = new FlexTable();
@@ -60,6 +62,11 @@
addOption("Reboot before", rebootBefore);
addOption("Reboot after", rebootAfter);
addOption("Show experimental tests", showExperimental);
+ if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
+ AfeUtils.popualateListBox(droneSet, "drone_sets");
+ addOption("Drone set", droneSet);
+ }
+
container.add(preferencesTable);
container.add(saveButton);
addWidget(container, "user_preferences_table");
@@ -81,6 +88,8 @@
private void updateValues() {
rebootBefore.setSelectedChoice(getValue("reboot_before"));
rebootAfter.setSelectedChoice(getValue("reboot_after"));
+ AfeUtils.setSelectedItem(droneSet, getValue("drone_set"));
+
showExperimental.setValue(user.get("show_experimental").isBoolean().booleanValue());
}
@@ -98,6 +107,7 @@
values.put("id", user.get("id"));
values.put("reboot_before", new JSONString(rebootBefore.getSelectedChoice()));
values.put("reboot_after", new JSONString(rebootAfter.getSelectedChoice()));
+ values.put("drone_set", new JSONString(droneSet.getItemText(droneSet.getSelectedIndex())));
values.put("show_experimental", JSONBoolean.getInstance(showExperimental.getValue()));
proxy.rpcCall("modify_user", values, new JsonRpcCallback() {
@Override
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index 303157f..f5f2a9c 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -72,6 +72,12 @@
<span class="field-name">Reboot options:</span>
<span id="view_reboot_before"></span> before job,
<span id="view_reboot_after"></span> after job<br>
+
+ <span id="view_drone_set_wrapper">
+ <span class="field-name">Drone set:</span>
+ <span id="view_drone_set"></span><br>
+ </span>
+
<span class="field-name">Include failed repair results:</span>
<span id="view_parse_failed_repair"></span><br>
<span class="field-name">Dependencies:</span>
@@ -132,6 +138,12 @@
<td id="create_parse_failed_repair"></td><td></td></tr>
<tr><td class="field-name">Hostless:</td>
<td id="create_hostless"></td><td></td></tr>
+
+ <tr id="create_drone_set_wrapper">
+ <td class="field-name">Drone set:</td>
+ <td id="create_drone_set" colspan="2"></td>
+ </tr>
+
<tr><td class="field-name">Tests:</td>
<td id="create_tests" colspan="2"></td></tr>
<tr><td class="field-name">Profilers:</td>
diff --git a/frontend/migrations/058_drone_management.py b/frontend/migrations/058_drone_management.py
new file mode 100644
index 0000000..183e921
--- /dev/null
+++ b/frontend/migrations/058_drone_management.py
@@ -0,0 +1,85 @@
+UP_SQL = """
+CREATE TABLE afe_drones (
+ id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ hostname VARCHAR(255) NOT NULL
+) ENGINE=InnoDB;
+
+ALTER TABLE afe_drones
+ADD CONSTRAINT afe_drones_unique
+UNIQUE KEY (hostname);
+
+
+CREATE TABLE afe_drone_sets (
+ id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL
+) ENGINE=InnoDB;
+
+ALTER TABLE afe_drone_sets
+ADD CONSTRAINT afe_drone_sets_unique
+UNIQUE KEY (name);
+
+
+CREATE TABLE afe_drone_sets_drones (
+ id INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
+ droneset_id INT NOT NULL,
+ drone_id INT NOT NULL
+) ENGINE=InnoDB;
+
+ALTER TABLE afe_drone_sets_drones
+ADD CONSTRAINT afe_drone_sets_drones_droneset_ibfk
+FOREIGN KEY (droneset_id) REFERENCES afe_drone_sets (id);
+
+ALTER TABLE afe_drone_sets_drones
+ADD CONSTRAINT afe_drone_sets_drones_drone_ibfk
+FOREIGN KEY (drone_id) REFERENCES afe_drones (id);
+
+ALTER TABLE afe_drone_sets_drones
+ADD CONSTRAINT afe_drone_sets_drones_unique
+UNIQUE KEY (droneset_id, drone_id);
+
+
+ALTER TABLE afe_jobs
+ADD COLUMN drone_set_id INT;
+
+ALTER TABLE afe_jobs
+ADD CONSTRAINT afe_jobs_drone_set_ibfk
+FOREIGN KEY (drone_set_id) REFERENCES afe_drone_sets (id);
+
+
+ALTER TABLE afe_users
+ADD COLUMN drone_set_id INT;
+
+ALTER TABLE afe_users
+ADD CONSTRAINT afe_users_drone_set_ibfk
+FOREIGN KEY (drone_set_id) REFERENCES afe_drone_sets (id);
+
+
+UPDATE afe_special_tasks SET requested_by_id = (
+ SELECT id FROM afe_users WHERE login = 'autotest_system')
+WHERE requested_by_id IS NULL;
+
+ALTER TABLE afe_special_tasks
+MODIFY COLUMN requested_by_id INT NOT NULL;
+"""
+
+
+DOWN_SQL = """
+ALTER TABLE afe_special_tasks
+MODIFY COLUMN requested_by_id INT DEFAULT NULL;
+
+ALTER TABLE afe_users
+DROP FOREIGN KEY afe_users_drone_set_ibfk;
+
+ALTER TABLE afe_users
+DROP COLUMN drone_set_id;
+
+ALTER TABLE afe_jobs
+DROP FOREIGN KEY afe_jobs_drone_set_ibfk;
+
+ALTER TABLE afe_jobs
+DROP COLUMN drone_set_id;
+
+DROP TABLE IF EXISTS afe_drone_sets_drones;
+DROP TABLE IF EXISTS afe_drone_sets;
+DROP TABLE IF EXISTS afe_drones;
+"""