Merge lp:~mwhudson/lava-scheduler/device-tags into lp:lava-scheduler

Proposed by Michael Hudson-Doyle
Status: Merged
Merged at revision: 109
Proposed branch: lp:~mwhudson/lava-scheduler/device-tags
Merge into: lp:lava-scheduler
Diff against target: 359 lines (+252/-3)
7 files modified
lava_scheduler_app/admin.py (+2/-1)
lava_scheduler_app/migrations/0011_auto__add_tag.py (+122/-0)
lava_scheduler_app/models.py (+16/-0)
lava_scheduler_app/templates/lava_scheduler_app/device.html (+7/-0)
lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html (+11/-0)
lava_scheduler_app/tests.py (+83/-1)
lava_scheduler_daemon/dbjobsource.py (+11/-1)
To merge this branch: bzr merge lp:~mwhudson/lava-scheduler/device-tags
Reviewer Review Type Date Requested Status
Linaro Validation Team Pending
Review via email: mp+85788@code.launchpad.net

Description of the change

Hey,

This branch adds support for tags to the scheduler. The idea is that a job will not be dispatched to a board if the job has an tag the board does not. The core query change is pretty hard to understand and convince oneself of correctness of, which is why it is extensively tested.

The UI aspects are very very simple -- we can fix that up later.

Cheers,
mwh

To post a comment you must log in.
120. By Michael Hudson-Doyle

merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lava_scheduler_app/admin.py'
2--- lava_scheduler_app/admin.py 2011-06-17 03:55:23 +0000
3+++ lava_scheduler_app/admin.py 2011-12-15 20:59:23 +0000
4@@ -1,6 +1,7 @@
5 from django.contrib import admin
6-from lava_scheduler_app.models import Device, DeviceType, TestJob
7+from lava_scheduler_app.models import Device, DeviceType, TestJob, Tag
8
9 admin.site.register(Device)
10 admin.site.register(DeviceType)
11 admin.site.register(TestJob)
12+admin.site.register(Tag)
13
14=== added file 'lava_scheduler_app/migrations/0011_auto__add_tag.py'
15--- lava_scheduler_app/migrations/0011_auto__add_tag.py 1970-01-01 00:00:00 +0000
16+++ lava_scheduler_app/migrations/0011_auto__add_tag.py 2011-12-15 20:59:23 +0000
17@@ -0,0 +1,122 @@
18+# encoding: utf-8
19+import datetime
20+from south.db import db
21+from south.v2 import SchemaMigration
22+from django.db import models
23+
24+class Migration(SchemaMigration):
25+
26+ def forwards(self, orm):
27+
28+ # Adding model 'Tag'
29+ db.create_table('lava_scheduler_app_tag', (
30+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
31+ ('name', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=50, db_index=True)),
32+ ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
33+ ))
34+ db.send_create_signal('lava_scheduler_app', ['Tag'])
35+
36+ # Adding M2M table for field tags on 'Device'
37+ db.create_table('lava_scheduler_app_device_tags', (
38+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
39+ ('device', models.ForeignKey(orm['lava_scheduler_app.device'], null=False)),
40+ ('tag', models.ForeignKey(orm['lava_scheduler_app.tag'], null=False))
41+ ))
42+ db.create_unique('lava_scheduler_app_device_tags', ['device_id', 'tag_id'])
43+
44+ # Adding M2M table for field tags on 'TestJob'
45+ db.create_table('lava_scheduler_app_testjob_tags', (
46+ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
47+ ('testjob', models.ForeignKey(orm['lava_scheduler_app.testjob'], null=False)),
48+ ('tag', models.ForeignKey(orm['lava_scheduler_app.tag'], null=False))
49+ ))
50+ db.create_unique('lava_scheduler_app_testjob_tags', ['testjob_id', 'tag_id'])
51+
52+
53+ def backwards(self, orm):
54+
55+ # Deleting model 'Tag'
56+ db.delete_table('lava_scheduler_app_tag')
57+
58+ # Removing M2M table for field tags on 'Device'
59+ db.delete_table('lava_scheduler_app_device_tags')
60+
61+ # Removing M2M table for field tags on 'TestJob'
62+ db.delete_table('lava_scheduler_app_testjob_tags')
63+
64+
65+ models = {
66+ 'auth.group': {
67+ 'Meta': {'object_name': 'Group'},
68+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
69+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
70+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
71+ },
72+ 'auth.permission': {
73+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
74+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
75+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
76+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
77+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
78+ },
79+ 'auth.user': {
80+ 'Meta': {'object_name': 'User'},
81+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
82+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
83+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
84+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
85+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
86+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
87+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
88+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
89+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
90+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
91+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
92+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
93+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
94+ },
95+ 'contenttypes.contenttype': {
96+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
97+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
98+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
100+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
101+ },
102+ 'lava_scheduler_app.device': {
103+ 'Meta': {'object_name': 'Device'},
104+ 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'unique': 'True', 'null': 'True', 'blank': 'True'}),
105+ 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}),
106+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}),
107+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
108+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'})
109+ },
110+ 'lava_scheduler_app.devicetype': {
111+ 'Meta': {'object_name': 'DeviceType'},
112+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True', 'db_index': 'True'})
113+ },
114+ 'lava_scheduler_app.tag': {
115+ 'Meta': {'object_name': 'Tag'},
116+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
117+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
118+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
119+ },
120+ 'lava_scheduler_app.testjob': {
121+ 'Meta': {'object_name': 'TestJob'},
122+ 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
123+ 'definition': ('django.db.models.fields.TextField', [], {}),
124+ 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
125+ 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
126+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
127+ 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}),
128+ 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
129+ 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}),
130+ 'results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'blank': 'True'}),
131+ 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
132+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
133+ 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
134+ 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
135+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'})
136+ }
137+ }
138+
139+ complete_apps = ['lava_scheduler_app']
140
141=== modified file 'lava_scheduler_app/models.py'
142--- lava_scheduler_app/models.py 2011-10-28 00:24:13 +0000
143+++ lava_scheduler_app/models.py 2011-12-15 20:59:23 +0000
144@@ -9,6 +9,16 @@
145 """Error raised when JSON is syntactically valid but ill-formed."""
146
147
148+class Tag(models.Model):
149+
150+ name = models.SlugField(unique=True)
151+
152+ description = models.TextField(null=True, blank=True)
153+
154+ def __unicode__(self):
155+ return self.name
156+
157+
158 class DeviceType(models.Model):
159 """
160 A class of device, for example a pandaboard or a snowball.
161@@ -51,6 +61,8 @@
162 current_job = models.ForeignKey(
163 "TestJob", blank=True, unique=True, null=True)
164
165+ tags = models.ManyToManyField(Tag, blank=True)
166+
167 status = models.IntegerField(
168 choices = STATUS_CHOICES,
169 default = IDLE,
170@@ -137,6 +149,8 @@
171 requested_device_type = models.ForeignKey(
172 DeviceType, null=True, default=None, related_name='+', blank=True)
173
174+ tags = models.ManyToManyField(Tag, blank=True)
175+
176 # This is set once the job starts.
177 actual_device = models.ForeignKey(
178 Device, null=True, default=None, related_name='+', blank=True)
179@@ -206,6 +220,8 @@
180 definition=json_data, submitter=user, requested_device=target,
181 requested_device_type=device_type, description=job_name)
182 job.save()
183+ for tag_name in job_data.get('device_tags', []):
184+ job.tags.add(Tag.objects.get_or_create(name=tag_name)[0])
185 return job
186
187 def can_cancel(self, user):
188
189=== modified file 'lava_scheduler_app/templates/lava_scheduler_app/device.html'
190--- lava_scheduler_app/templates/lava_scheduler_app/device.html 2011-10-28 00:24:13 +0000
191+++ lava_scheduler_app/templates/lava_scheduler_app/device.html 2011-12-15 20:59:23 +0000
192@@ -37,6 +37,13 @@
193
194 <dt>Device type:</dt>
195 <dd>{{ device.device_type }}</dd>
196+
197+ <dt>Device Tags</dt>
198+ {% for tag in device.tags.all %}
199+ <dd>{{ tag.name }}</dd>
200+ {% empty %}
201+ <dd><i>None</i></dd>
202+ {% endfor %}
203 </div>
204 <div class="column">
205 <dt>Status:</dt>
206
207=== modified file 'lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html'
208--- lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2011-12-09 03:55:33 +0000
209+++ lava_scheduler_app/templates/lava_scheduler_app/job_sidebar.html 2011-12-15 20:59:23 +0000
210@@ -25,6 +25,17 @@
211 <dd>{{ job.requested_device_type }}</dd>
212 {% endif %}
213
214+ {% for tag in job.tags.all %}
215+ {% if forloop.first %}
216+ {% if forloop.revcounter > 1 %}
217+ <dt>Required Device Tags</dt>
218+ {% else %}
219+ <dt>Required Device Tag</dt>
220+ {% endif %}
221+ {% endif %}
222+ <dd>{{ tag.name }}</dd>
223+ {% endfor %}
224+
225 {% if job.description %}
226 <dt>Description:</dt>
227 <dd>{{ job.description }}</dd>
228
229=== modified file 'lava_scheduler_app/tests.py'
230--- lava_scheduler_app/tests.py 2011-12-14 02:10:04 +0000
231+++ lava_scheduler_app/tests.py 2011-12-15 20:59:23 +0000
232@@ -9,7 +9,7 @@
233
234 from django_testscenarios.ubertest import TestCase
235
236-from lava_scheduler_app.models import Device, DeviceType, TestJob
237+from lava_scheduler_app.models import Device, DeviceType, Tag, TestJob
238
239
240
241@@ -135,6 +135,41 @@
242 json.dumps({'device_type':'panda'}), self.factory.make_user())
243 self.assertEqual(job.status, TestJob.SUBMITTED)
244
245+ def test_from_json_and_user_sets_no_tags_if_no_tags(self):
246+ self.factory.ensure_device_type(name='panda')
247+ job = TestJob.from_json_and_user(
248+ json.dumps({'device_type':'panda', 'device_tags':[]}),
249+ self.factory.make_user())
250+ self.assertEqual(set(job.tags.all()), set([]))
251+
252+ def test_from_json_and_user_sets_tag_from_device_tags(self):
253+ self.factory.ensure_device_type(name='panda')
254+ job = TestJob.from_json_and_user(
255+ json.dumps({'device_type':'panda', 'device_tags':['tag']}),
256+ self.factory.make_user())
257+ self.assertEqual(
258+ set(tag.name for tag in job.tags.all()), set(['tag']))
259+
260+ def test_from_json_and_user_sets_multiple_tag_from_device_tags(self):
261+ self.factory.ensure_device_type(name='panda')
262+ job = TestJob.from_json_and_user(
263+ json.dumps({'device_type':'panda', 'device_tags':['tag1', 'tag2']}),
264+ self.factory.make_user())
265+ self.assertEqual(
266+ set(tag.name for tag in job.tags.all()), set(['tag1', 'tag2']))
267+
268+ def test_from_json_and_user_reuses_tag_objects(self):
269+ self.factory.ensure_device_type(name='panda')
270+ job1 = TestJob.from_json_and_user(
271+ json.dumps({'device_type':'panda', 'device_tags':['tag']}),
272+ self.factory.make_user())
273+ job2 = TestJob.from_json_and_user(
274+ json.dumps({'device_type':'panda', 'device_tags':['tag']}),
275+ self.factory.make_user())
276+ self.assertEqual(
277+ set(tag.pk for tag in job1.tags.all()),
278+ set(tag.pk for tag in job2.tags.all()))
279+
280
281 class TestSchedulerAPI(TestCaseWithFactory):
282
283@@ -298,6 +333,53 @@
284 None,
285 DatabaseJobSource().getJobForBoard_impl('panda02'))
286
287+ def _makeBoardWithTags(self, tags):
288+ board = self.factory.make_device()
289+ for tag_name in tags:
290+ board.tags.add(Tag.objects.get_or_create(name=tag_name)[0])
291+ return board
292+
293+ def _makeJobWithTagsForBoard(self, tags, board):
294+ job = self.factory.make_testjob(requested_device=board)
295+ for tag_name in tags:
296+ job.tags.add(Tag.objects.get_or_create(name=tag_name)[0])
297+ return job
298+
299+ def assertBoardWithTagsGetsJobWithTags(self, board_tags, job_tags):
300+ board = self._makeBoardWithTags(board_tags)
301+ self._makeJobWithTagsForBoard(job_tags, board)
302+ self.assertEqual(
303+ board.hostname,
304+ DatabaseJobSource().getJobForBoard_impl(board.hostname)['target'])
305+
306+ def assertBoardWithTagsDoesNotGetJobWithTags(self, board_tags, job_tags):
307+ board = self._makeBoardWithTags(board_tags)
308+ self._makeJobWithTagsForBoard(job_tags, board)
309+ self.assertEqual(
310+ None,
311+ DatabaseJobSource().getJobForBoard_impl(board.hostname))
312+
313+ def test_getJobForBoard_does_not_return_job_if_board_lacks_tag(self):
314+ self.assertBoardWithTagsDoesNotGetJobWithTags([], ['tag'])
315+
316+ def test_getJobForBoard_returns_job_if_board_has_tag(self):
317+ self.assertBoardWithTagsGetsJobWithTags(['tag'], ['tag'])
318+
319+ def test_getJobForBoard_returns_job_if_board_has_both_tags(self):
320+ self.assertBoardWithTagsGetsJobWithTags(['tag1', 'tag2'], ['tag1', 'tag2'])
321+
322+ def test_getJobForBoard_returns_job_if_board_has_extra_tags(self):
323+ self.assertBoardWithTagsGetsJobWithTags(['tag1', 'tag2'], ['tag1'])
324+
325+ def test_getJobForBoard_does_not_return_job_if_board_has_only_one_tag(self):
326+ self.assertBoardWithTagsDoesNotGetJobWithTags(['tag1'], ['tag1', 'tag2'])
327+
328+ def test_getJobForBoard_does_not_return_job_if_board_has_unrelated_tag(self):
329+ self.assertBoardWithTagsDoesNotGetJobWithTags(['tag1'], ['tag2'])
330+
331+ def test_getJobForBoard_does_not_return_job_if_only_one_tag_matches(self):
332+ self.assertBoardWithTagsDoesNotGetJobWithTags(['tag1', 'tag2'], ['tag1', 'tag3'])
333+
334 def test_getJobForBoard_sets_start_time(self):
335 device = self.factory.make_device(hostname='panda01')
336 job = self.factory.make_testjob(requested_device=device)
337
338=== modified file 'lava_scheduler_daemon/dbjobsource.py'
339--- lava_scheduler_daemon/dbjobsource.py 2011-12-01 03:58:08 +0000
340+++ lava_scheduler_daemon/dbjobsource.py 2011-12-15 20:59:23 +0000
341@@ -87,7 +87,17 @@
342 | Q(requested_device_type=device.device_type),
343 status=TestJob.SUBMITTED)
344 jobs_for_device = jobs_for_device.extra(
345- select={'is_targeted': 'requested_device_id is not NULL'},
346+ select={
347+ 'is_targeted': 'requested_device_id is not NULL',
348+ 'missing_tags': '''
349+ select count(*) from lava_scheduler_app_testjob_tags
350+ where testjob_id = lava_scheduler_app_testjob.id
351+ and tag_id not in (select tag_id
352+ from lava_scheduler_app_device_tags
353+ where device_id = '%s')
354+ ''' % device.hostname,
355+ },
356+ where=['missing_tags = 0'],
357 order_by=['-is_targeted', 'submit_time'])
358 jobs = jobs_for_device[:1]
359 if jobs:

Subscribers

People subscribed via source and target branches