Merge lp:~mwhudson/lava-scheduler/device-tags into lp:lava-scheduler
- device-tags
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Support tags in the scheduler
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Linaro Validation Team | Pending | ||
Review via email: mp+85788@code.launchpad.net |
Commit message
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: |