Merge lp:~stylesen/lava-scheduler/multinode into lp:lava-scheduler/multinode
- multinode
- Merge into multinode
Proposed by
Senthil Kumaran S
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Neil Williams | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 248 | ||||
Proposed branch: | lp:~stylesen/lava-scheduler/multinode | ||||
Merge into: | lp:lava-scheduler/multinode | ||||
Diff against target: |
540 lines (+376/-12) 8 files modified
lava_scheduler_app/api.py (+1/-1) lava_scheduler_app/management/commands/scheduler.py (+2/-2) lava_scheduler_app/migrations/0030_auto__add_field_testjob_sub_id.py (+160/-0) lava_scheduler_app/models.py (+14/-4) lava_scheduler_app/views.py (+11/-5) lava_scheduler_daemon/dbjobsource.py (+76/-0) lava_scheduler_daemon/job.py (+79/-0) lava_scheduler_daemon/service.py (+33/-0) |
||||
To merge this branch: | bzr merge lp:~stylesen/lava-scheduler/multinode | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Neil Williams | Approve | ||
Review via email: mp+170590@code.launchpad.net |
Commit message
Description of the change
Initial bits with a job based scheduler for multi-node support.
We do not accept multi-node jobs yet.
To post a comment you must log in.
- 248. By Neil Williams
-
Merge Senthil's initial MultiNode changes for lava-scheduler.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lava_scheduler_app/api.py' |
2 | --- lava_scheduler_app/api.py 2013-05-02 07:35:44 +0000 |
3 | +++ lava_scheduler_app/api.py 2013-06-20 11:53:35 +0000 |
4 | @@ -36,7 +36,7 @@ |
5 | raise xmlrpclib.Fault(404, "Specified device not found.") |
6 | except DeviceType.DoesNotExist: |
7 | raise xmlrpclib.Fault(404, "Specified device type not found.") |
8 | - return job.id |
9 | + return job |
10 | |
11 | def resubmit_job(self, job_id): |
12 | try: |
13 | |
14 | === modified file 'lava_scheduler_app/management/commands/scheduler.py' |
15 | --- lava_scheduler_app/management/commands/scheduler.py 2012-03-27 05:41:09 +0000 |
16 | +++ lava_scheduler_app/management/commands/scheduler.py 2013-06-20 11:53:35 +0000 |
17 | @@ -42,7 +42,7 @@ |
18 | |
19 | from twisted.internet import reactor |
20 | |
21 | - from lava_scheduler_daemon.service import BoardSet |
22 | + from lava_scheduler_daemon.service import BoardSet, JobQueue |
23 | from lava_scheduler_daemon.dbjobsource import DatabaseJobSource |
24 | |
25 | daemon_options = self._configure(options) |
26 | @@ -57,7 +57,7 @@ |
27 | 'fake-dispatcher') |
28 | else: |
29 | dispatcher = options['dispatcher'] |
30 | - service = BoardSet( |
31 | + service = JobQueue( |
32 | source, dispatcher, reactor, daemon_options=daemon_options) |
33 | reactor.callWhenRunning(service.startService) |
34 | reactor.run() |
35 | |
36 | === added file 'lava_scheduler_app/migrations/0030_auto__add_field_testjob_sub_id.py' |
37 | --- lava_scheduler_app/migrations/0030_auto__add_field_testjob_sub_id.py 1970-01-01 00:00:00 +0000 |
38 | +++ lava_scheduler_app/migrations/0030_auto__add_field_testjob_sub_id.py 2013-06-20 11:53:35 +0000 |
39 | @@ -0,0 +1,160 @@ |
40 | +# -*- coding: utf-8 -*- |
41 | +import datetime |
42 | +from south.db import db |
43 | +from south.v2 import SchemaMigration |
44 | +from django.db import models |
45 | + |
46 | + |
47 | +class Migration(SchemaMigration): |
48 | + |
49 | + def forwards(self, orm): |
50 | + # Adding field 'TestJob.sub_id' |
51 | + db.add_column('lava_scheduler_app_testjob', 'sub_id', |
52 | + self.gf('django.db.models.fields.CharField')(default='', max_length=200, blank=True), |
53 | + keep_default=False) |
54 | + |
55 | + |
56 | + def backwards(self, orm): |
57 | + # Deleting field 'TestJob.sub_id' |
58 | + db.delete_column('lava_scheduler_app_testjob', 'sub_id') |
59 | + |
60 | + |
61 | + models = { |
62 | + 'auth.group': { |
63 | + 'Meta': {'object_name': 'Group'}, |
64 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
65 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
66 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
67 | + }, |
68 | + 'auth.permission': { |
69 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
70 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
71 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
72 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
73 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
74 | + }, |
75 | + 'auth.user': { |
76 | + 'Meta': {'object_name': 'User'}, |
77 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
78 | + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), |
79 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
80 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
81 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
82 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
83 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
84 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
85 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
86 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
87 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
88 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
89 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
90 | + }, |
91 | + 'contenttypes.contenttype': { |
92 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
93 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
94 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
95 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
96 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
97 | + }, |
98 | + 'dashboard_app.bundle': { |
99 | + 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'}, |
100 | + '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}), |
101 | + '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}), |
102 | + 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}), |
103 | + 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}), |
104 | + 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}), |
105 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
106 | + 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
107 | + 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}), |
108 | + 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) |
109 | + }, |
110 | + 'dashboard_app.bundlestream': { |
111 | + 'Meta': {'object_name': 'BundleStream'}, |
112 | + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}), |
113 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
114 | + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
115 | + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
116 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), |
117 | + 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}), |
118 | + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), |
119 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) |
120 | + }, |
121 | + 'lava_scheduler_app.device': { |
122 | + 'Meta': {'object_name': 'Device'}, |
123 | + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['lava_scheduler_app.TestJob']", 'blank': 'True', 'unique': 'True'}), |
124 | + 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}), |
125 | + 'device_version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), |
126 | + 'health_status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
127 | + 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}), |
128 | + 'last_health_report_job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['lava_scheduler_app.TestJob']", 'blank': 'True', 'unique': 'True'}), |
129 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), |
130 | + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}) |
131 | + }, |
132 | + 'lava_scheduler_app.devicestatetransition': { |
133 | + 'Meta': {'object_name': 'DeviceStateTransition'}, |
134 | + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), |
135 | + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
136 | + 'device': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transitions'", 'to': "orm['lava_scheduler_app.Device']"}), |
137 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
138 | + 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), |
139 | + 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
140 | + 'new_state': ('django.db.models.fields.IntegerField', [], {}), |
141 | + 'old_state': ('django.db.models.fields.IntegerField', [], {}) |
142 | + }, |
143 | + 'lava_scheduler_app.devicetype': { |
144 | + 'Meta': {'object_name': 'DeviceType'}, |
145 | + 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
146 | + 'health_check_job': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), |
147 | + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True'}) |
148 | + }, |
149 | + 'lava_scheduler_app.jobfailuretag': { |
150 | + 'Meta': {'object_name': 'JobFailureTag'}, |
151 | + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
152 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
153 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}) |
154 | + }, |
155 | + 'lava_scheduler_app.tag': { |
156 | + 'Meta': {'object_name': 'Tag'}, |
157 | + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
158 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
159 | + 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}) |
160 | + }, |
161 | + 'lava_scheduler_app.testjob': { |
162 | + 'Meta': {'object_name': 'TestJob'}, |
163 | + '_results_bundle': ('django.db.models.fields.related.OneToOneField', [], {'null': 'True', 'db_column': "'results_bundle_id'", 'on_delete': 'models.SET_NULL', 'to': "orm['dashboard_app.Bundle']", 'blank': 'True', 'unique': 'True'}), |
164 | + '_results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'db_column': "'results_link'", 'blank': 'True'}), |
165 | + 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), |
166 | + 'definition': ('django.db.models.fields.TextField', [], {}), |
167 | + 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), |
168 | + 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), |
169 | + 'failure_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), |
170 | + 'failure_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'failure_tags'", 'blank': 'True', 'to': "orm['lava_scheduler_app.JobFailureTag']"}), |
171 | + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}), |
172 | + 'health_check': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
173 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
174 | + 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
175 | + 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), |
176 | + 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}), |
177 | + 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), |
178 | + 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}), |
179 | + 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), |
180 | + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), |
181 | + 'sub_id': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}), |
182 | + 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
183 | + 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), |
184 | + 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}), |
185 | + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}), |
186 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) |
187 | + }, |
188 | + 'linaro_django_xmlrpc.authtoken': { |
189 | + 'Meta': {'object_name': 'AuthToken'}, |
190 | + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
191 | + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), |
192 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
193 | + 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), |
194 | + 'secret': ('django.db.models.fields.CharField', [], {'default': "'ynu6yihw337isktsrzd0ocr83r0huox1b5y4qs1c0ktat7s3089d2xgiz3ll0n68fr6q026mep0t5xwg1coxnl2aoknolgowx2779uluan8quez0row0jnk5j2qsxlle'", 'unique': 'True', 'max_length': '128'}), |
195 | + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"}) |
196 | + } |
197 | + } |
198 | + |
199 | + complete_apps = ['lava_scheduler_app'] |
200 | \ No newline at end of file |
201 | |
202 | === modified file 'lava_scheduler_app/models.py' |
203 | --- lava_scheduler_app/models.py 2013-02-10 21:26:06 +0000 |
204 | +++ lava_scheduler_app/models.py 2013-06-20 11:53:35 +0000 |
205 | @@ -2,6 +2,7 @@ |
206 | import os |
207 | import simplejson |
208 | import urlparse |
209 | +import copy |
210 | |
211 | from django.conf import settings |
212 | from django.contrib.auth.models import User |
213 | @@ -249,6 +250,12 @@ |
214 | |
215 | id = models.AutoField(primary_key=True) |
216 | |
217 | + sub_id = models.CharField( |
218 | + verbose_name = _(u"Sub ID"), |
219 | + blank = True, |
220 | + max_length = 200 |
221 | + ) |
222 | + |
223 | submitter = models.ForeignKey( |
224 | User, |
225 | verbose_name = _(u"Submitter"), |
226 | @@ -398,9 +405,13 @@ |
227 | elif 'device_type' in job_data: |
228 | target = None |
229 | device_type = DeviceType.objects.get(name=job_data['device_type']) |
230 | + elif 'device_group' in job_data: |
231 | + target = None |
232 | + device_type = None |
233 | else: |
234 | raise JSONDataError( |
235 | - "Neither 'target' nor 'device_type' found in job data.") |
236 | + "No 'target' or 'device_type' or 'device_group' are found " |
237 | + "in job data.") |
238 | |
239 | priorities = dict([(j.upper(), i) for i, j in cls.PRIORITY_CHOICES]) |
240 | priority = cls.MEDIUM |
241 | @@ -463,15 +474,14 @@ |
242 | tags.append(Tag.objects.get(name=tag_name)) |
243 | except Tag.DoesNotExist: |
244 | raise JSONDataError("tag %r does not exist" % tag_name) |
245 | + |
246 | job = TestJob( |
247 | definition=json_data, submitter=submitter, |
248 | requested_device=target, requested_device_type=device_type, |
249 | description=job_name, health_check=health_check, user=user, |
250 | group=group, is_public=is_public, priority=priority) |
251 | job.save() |
252 | - for tag in tags: |
253 | - job.tags.add(tag) |
254 | - return job |
255 | + return job.id |
256 | |
257 | def _can_admin(self, user): |
258 | """ used to check for things like if the user can cancel or annotate |
259 | |
260 | === modified file 'lava_scheduler_app/views.py' |
261 | --- lava_scheduler_app/views.py 2013-05-02 09:00:04 +0000 |
262 | +++ lava_scheduler_app/views.py 2013-06-20 11:53:35 +0000 |
263 | @@ -74,10 +74,16 @@ |
264 | |
265 | |
266 | def pklink(record): |
267 | + job_id = record.pk |
268 | + try: |
269 | + if record.sub_id: |
270 | + job_id = record.sub_id |
271 | + except: |
272 | + pass |
273 | return mark_safe( |
274 | '<a href="%s">%s</a>' % ( |
275 | record.get_absolute_url(), |
276 | - escape(record.pk))) |
277 | + escape(job_id))) |
278 | |
279 | |
280 | class IDLinkColumn(Column): |
281 | @@ -100,13 +106,13 @@ |
282 | |
283 | |
284 | def all_jobs_with_device_sort(): |
285 | - return TestJob.objects.select_related( |
286 | + jobs = TestJob.objects.select_related( |
287 | "actual_device", "requested_device", "requested_device_type", |
288 | "submitter", "user", "group").extra( |
289 | select={ |
290 | 'device_sort': 'coalesce(actual_device_id, requested_device_id, requested_device_type_id)' |
291 | }).all() |
292 | - |
293 | + return jobs.order_by('submit_time') |
294 | |
295 | |
296 | class JobTable(DataTablesTable): |
297 | @@ -126,7 +132,7 @@ |
298 | else: |
299 | return '' |
300 | |
301 | - id = RestrictedIDLinkColumn() |
302 | + sub_id = RestrictedIDLinkColumn() |
303 | status = Column() |
304 | priority = Column() |
305 | device = Column(accessor='device_sort') |
306 | @@ -137,7 +143,7 @@ |
307 | duration = Column() |
308 | |
309 | datatable_opts = { |
310 | - 'aaSorting': [[0, 'desc']], |
311 | + 'aaSorting': [[6, 'desc']], |
312 | } |
313 | searchable_columns=['description'] |
314 | |
315 | |
316 | === modified file 'lava_scheduler_daemon/dbjobsource.py' |
317 | --- lava_scheduler_daemon/dbjobsource.py 2013-06-13 14:53:25 +0000 |
318 | +++ lava_scheduler_daemon/dbjobsource.py 2013-06-20 11:53:35 +0000 |
319 | @@ -103,6 +103,68 @@ |
320 | def getBoardList(self): |
321 | return self.deferForDB(self.getBoardList_impl) |
322 | |
323 | + def _fix_device(self, device, job): |
324 | + """Associate an available/idle DEVICE to the given JOB. |
325 | + |
326 | + Returns the job with actual_device set to DEVICE. |
327 | + |
328 | + If we are unable to grab the DEVICE then we return None. |
329 | + """ |
330 | + DeviceStateTransition.objects.create( |
331 | + created_by=None, device=device, old_state=device.status, |
332 | + new_state=Device.RUNNING, message=None, job=job).save() |
333 | + device.status = Device.RUNNING |
334 | + device.current_job = job |
335 | + try: |
336 | + # The unique constraint on current_job may cause this to |
337 | + # fail in the case of concurrent requests for different |
338 | + # boards grabbing the same job. If there are concurrent |
339 | + # requests for the *same* board they may both return the |
340 | + # same job -- this is an application level bug though. |
341 | + device.save() |
342 | + except IntegrityError: |
343 | + self.logger.info( |
344 | + "job %s has been assigned to another board -- rolling back", |
345 | + job.id) |
346 | + transaction.rollback() |
347 | + return None |
348 | + else: |
349 | + job.actual_device = device |
350 | + job.save() |
351 | + transaction.commit() |
352 | + return job |
353 | + |
354 | + def getJobList_impl(self): |
355 | + jobs = TestJob.objects.all().filter(status=TestJob.SUBMITTED) |
356 | + job_list = [] |
357 | + devices = None |
358 | + |
359 | + for job in jobs: |
360 | + if job.actual_device: |
361 | + job_list.append(job) |
362 | + elif job.requested_device: |
363 | + self.logger.info("Checking Requested Device") |
364 | + devices = Device.objects.all().filter( |
365 | + hostname=job.requested_device.hostname, |
366 | + status=Device.IDLE) |
367 | + elif job.requested_device_type: |
368 | + self.logger.info("Checking Requested Device Type") |
369 | + devices = Device.objects.all().filter( |
370 | + device_type=job.requested_device_type, |
371 | + status=Device.IDLE) |
372 | + else: |
373 | + continue |
374 | + if devices: |
375 | + device = devices[0] |
376 | + job = self._fix_device(device, job) |
377 | + if job: |
378 | + job_list.append(job) |
379 | + |
380 | + return job_list |
381 | + |
382 | + def getJobList(self): |
383 | + return self.deferForDB(self.getJobList_impl) |
384 | + |
385 | def _get_json_data(self, job): |
386 | json_data = simplejson.loads(job.definition) |
387 | json_data['target'] = job.actual_device.hostname |
388 | @@ -229,6 +291,20 @@ |
389 | def getJobForBoard(self, board_name): |
390 | return self.deferForDB(self.getJobForBoard_impl, board_name) |
391 | |
392 | + def getJobDetails_impl(self, job): |
393 | + job.status = TestJob.RUNNING |
394 | + job.start_time = datetime.datetime.utcnow() |
395 | + shutil.rmtree(job.output_dir, ignore_errors=True) |
396 | + job.log_file.save('job-%s.log' % job.id, ContentFile(''), save=False) |
397 | + job.submit_token = AuthToken.objects.create(user=job.submitter) |
398 | + job.save() |
399 | + json_data = self._get_json_data(job) |
400 | + transaction.commit() |
401 | + return json_data |
402 | + |
403 | + def getJobDetails(self, job): |
404 | + return self.deferForDB(self.getJobDetails_impl, job) |
405 | + |
406 | def getOutputDirForJobOnBoard_impl(self, board_name): |
407 | device = Device.objects.get(hostname=board_name) |
408 | job = device.current_job |
409 | |
410 | === added file 'lava_scheduler_daemon/job.py' |
411 | --- lava_scheduler_daemon/job.py 1970-01-01 00:00:00 +0000 |
412 | +++ lava_scheduler_daemon/job.py 2013-06-20 11:53:35 +0000 |
413 | @@ -0,0 +1,79 @@ |
414 | +# Copyright (C) 2013 Linaro Limited |
415 | +# |
416 | +# Author: Senthil Kumaran <senthil.kumaran@linaro.org> |
417 | +# |
418 | +# This file is part of LAVA Scheduler. |
419 | +# |
420 | +# LAVA Scheduler is free software: you can redistribute it and/or modify it |
421 | +# under the terms of the GNU Affero General Public License version 3 as |
422 | +# published by the Free Software Foundation |
423 | +# |
424 | +# LAVA Scheduler is distributed in the hope that it will be useful, but |
425 | +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
426 | +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
427 | +# more details. |
428 | +# |
429 | +# You should have received a copy of the GNU Affero General Public License |
430 | +# along with LAVA Scheduler. If not, see <http://www.gnu.org/licenses/>. |
431 | + |
432 | +import logging |
433 | + |
434 | +from twisted.internet import defer |
435 | +from lava_scheduler_daemon.board import MonitorJob, catchall_errback |
436 | + |
437 | +class NewJob(object): |
438 | + job_cls = MonitorJob |
439 | + |
440 | + def __init__(self, source, job, dispatcher, reactor, daemon_options, |
441 | + job_cls=None): |
442 | + self.source = source |
443 | + self.dispatcher = dispatcher |
444 | + self.reactor = reactor |
445 | + self.daemon_options = daemon_options |
446 | + self.job = job |
447 | + self.board_name = job.actual_device.hostname |
448 | + if job_cls is not None: |
449 | + self.job_cls = job_cls |
450 | + self.running_job = None |
451 | + self.logger = logging.getLogger(__name__ + '.NewJob.' + str(job.id)) |
452 | + |
453 | + def start(self): |
454 | + self.logger.debug("processing job") |
455 | + if self.job is None: |
456 | + self.logger.debug("no job found for processing") |
457 | + return |
458 | + self.source.getJobDetails(self.job).addCallbacks( |
459 | + self._startJob, self._ebStartJob) |
460 | + |
461 | + def _startJob(self, job_data): |
462 | + if job_data is None: |
463 | + self.logger.debug("no job found") |
464 | + return |
465 | + self.logger.info("starting job %r", job_data) |
466 | + |
467 | + self.running_job = self.job_cls( |
468 | + job_data, self.dispatcher, self.source, self.board_name, |
469 | + self.reactor, self.daemon_options) |
470 | + d = self.running_job.run() |
471 | + d.addCallbacks(self._cbJobFinished, self._ebJobFinished) |
472 | + |
473 | + def _ebStartJob(self, result): |
474 | + self.logger.error( |
475 | + '%s: %s\n%s', result.type.__name__, result.value, |
476 | + result.getTraceback()) |
477 | + return |
478 | + |
479 | + def stop(self): |
480 | + self.logger.debug("stopping") |
481 | + |
482 | + if self.running_job is not None: |
483 | + self.logger.debug("job running; deferring stop") |
484 | + else: |
485 | + self.logger.debug("stopping immediately") |
486 | + return defer.succeed(None) |
487 | + |
488 | + def _ebJobFinished(self, result): |
489 | + self.logger.exception(result.value) |
490 | + |
491 | + def _cbJobFinished(self, result): |
492 | + self.running_job = None |
493 | |
494 | === modified file 'lava_scheduler_daemon/service.py' |
495 | --- lava_scheduler_daemon/service.py 2012-12-03 05:03:38 +0000 |
496 | +++ lava_scheduler_daemon/service.py 2013-06-20 11:53:35 +0000 |
497 | @@ -5,6 +5,7 @@ |
498 | from twisted.internet.task import LoopingCall |
499 | |
500 | from lava_scheduler_daemon.board import Board, catchall_errback |
501 | +from lava_scheduler_daemon.job import NewJob |
502 | |
503 | |
504 | class BoardSet(Service): |
505 | @@ -56,3 +57,35 @@ |
506 | self.logger.info( |
507 | "waiting for %s boards", len(self.boards) - len(dead_boards)) |
508 | return defer.gatherResults(ds) |
509 | + |
510 | + |
511 | +class JobQueue(Service): |
512 | + |
513 | + def __init__(self, source, dispatcher, reactor, daemon_options): |
514 | + self.logger = logging.getLogger(__name__ + '.JobQueue') |
515 | + self.source = source |
516 | + self.dispatcher = dispatcher |
517 | + self.reactor = reactor |
518 | + self.daemon_options = daemon_options |
519 | + self._check_job_call = LoopingCall(self._checkJobs) |
520 | + self._check_job_call.clock = reactor |
521 | + |
522 | + def _checkJobs(self): |
523 | + self.logger.debug("Refreshing jobs") |
524 | + return self.source.getJobList().addCallback( |
525 | + self._cbCheckJobs).addErrback(catchall_errback(self.logger)) |
526 | + |
527 | + def _cbCheckJobs(self, job_list): |
528 | + for job in job_list: |
529 | + self.logger.debug("Found job: %d" % job.id) |
530 | + new_job = NewJob(self.source, job, self.dispatcher, self.reactor, |
531 | + self.daemon_options) |
532 | + self.logger.info("Starting Job: %d " % job.id) |
533 | + new_job.start() |
534 | + |
535 | + def startService(self): |
536 | + self._check_job_call.start(20) |
537 | + |
538 | + def stopService(self): |
539 | + self._check_job_call.stop() |
540 | + return None |
Approved