Merge lp:~stylesen/lava-scheduler/multinode into lp:lava-scheduler/multinode

Proposed by Senthil Kumaran S
Status: Merged
Approved by: Neil Williams
Approved revision: no longer in the source branch.
Merged at revision: 249
Proposed branch: lp:~stylesen/lava-scheduler/multinode
Merge into: lp:lava-scheduler/multinode
Diff against target: 347 lines (+299/-7)
3 files modified
lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py (+161/-0)
lava_scheduler_app/models.py (+52/-7)
lava_scheduler_daemon/utils.py (+86/-0)
To merge this branch: bzr merge lp:~stylesen/lava-scheduler/multinode
Reviewer Review Type Date Requested Status
Neil Williams Approve
Review via email: mp+171218@code.launchpad.net

Description of the change

Support for submitting multinode jobs to the scheduler and have sub-ids and unique target-groups.

To post a comment you must log in.
Revision history for this message
Neil Williams (codehelp) wrote :

OK, there are a few debug bits left in the split_multi_job function. The group_json doesn't need to know the hostname once the code from translate.py is migrated into the scheduler. Firstly because that will be the hostname of the machine running the scheduler daemon, not the machine running the GroupDispatcher. During debugging, translate.py was run on the same machine as the dispatcher. As the comment notes, the only thing the GroupDispatcher needs from group_json is the group_dispatcher = true flag and the port number. (In some ways, group_json is possibly redundant and the port number could be passed as an argument to __init__.)

However, this is harmless, so I'm happy to approve this merge, we will need to remove the hostname call (and therefore the import of hostname from socket) before the branch is finally merged.

review: Approve
249. By Neil Williams

Merge Senthil's branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py'
2--- lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py 1970-01-01 00:00:00 +0000
3+++ lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py 2013-06-25 06:19:07 +0000
4@@ -0,0 +1,161 @@
5+# -*- coding: utf-8 -*-
6+import datetime
7+from south.db import db
8+from south.v2 import SchemaMigration
9+from django.db import models
10+
11+
12+class Migration(SchemaMigration):
13+
14+ def forwards(self, orm):
15+ # Adding field 'TestJob.target_group'
16+ db.add_column('lava_scheduler_app_testjob', 'target_group',
17+ self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True),
18+ keep_default=False)
19+
20+
21+ def backwards(self, orm):
22+ # Deleting field 'TestJob.target_group'
23+ db.delete_column('lava_scheduler_app_testjob', 'target_group')
24+
25+
26+ models = {
27+ 'auth.group': {
28+ 'Meta': {'object_name': 'Group'},
29+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
30+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
31+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
32+ },
33+ 'auth.permission': {
34+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
35+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
36+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
37+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
38+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
39+ },
40+ 'auth.user': {
41+ 'Meta': {'object_name': 'User'},
42+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
43+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
44+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
45+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
46+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
47+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
48+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
49+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
50+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
51+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
52+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
53+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
54+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
55+ },
56+ 'contenttypes.contenttype': {
57+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
58+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
59+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
60+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
61+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
62+ },
63+ 'dashboard_app.bundle': {
64+ 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
65+ '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
66+ '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
67+ 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
68+ 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
69+ 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
70+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
71+ 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
72+ 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
73+ 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
74+ },
75+ 'dashboard_app.bundlestream': {
76+ 'Meta': {'object_name': 'BundleStream'},
77+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
78+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
80+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
81+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
82+ 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
83+ 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
84+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
85+ },
86+ 'lava_scheduler_app.device': {
87+ 'Meta': {'object_name': 'Device'},
88+ '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'}),
89+ 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}),
90+ 'device_version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
91+ 'health_status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
92+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}),
93+ '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'}),
94+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
95+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'})
96+ },
97+ 'lava_scheduler_app.devicestatetransition': {
98+ 'Meta': {'object_name': 'DeviceStateTransition'},
99+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
100+ 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
101+ 'device': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transitions'", 'to': "orm['lava_scheduler_app.Device']"}),
102+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
103+ 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
104+ 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
105+ 'new_state': ('django.db.models.fields.IntegerField', [], {}),
106+ 'old_state': ('django.db.models.fields.IntegerField', [], {})
107+ },
108+ 'lava_scheduler_app.devicetype': {
109+ 'Meta': {'object_name': 'DeviceType'},
110+ 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
111+ 'health_check_job': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
112+ 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True'})
113+ },
114+ 'lava_scheduler_app.jobfailuretag': {
115+ 'Meta': {'object_name': 'JobFailureTag'},
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.CharField', [], {'unique': 'True', 'max_length': '256'})
119+ },
120+ 'lava_scheduler_app.tag': {
121+ 'Meta': {'object_name': 'Tag'},
122+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
123+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
124+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
125+ },
126+ 'lava_scheduler_app.testjob': {
127+ 'Meta': {'object_name': 'TestJob'},
128+ '_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'}),
129+ '_results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'db_column': "'results_link'", 'blank': 'True'}),
130+ 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
131+ 'definition': ('django.db.models.fields.TextField', [], {}),
132+ 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
133+ 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
134+ 'failure_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
135+ 'failure_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'failure_tags'", 'blank': 'True', 'to': "orm['lava_scheduler_app.JobFailureTag']"}),
136+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
137+ 'health_check': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
138+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
140+ 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}),
141+ 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}),
142+ 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
143+ 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}),
144+ 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
145+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
146+ 'sub_id': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
147+ 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
148+ 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
149+ 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}),
150+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}),
151+ 'target_group': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
152+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
153+ },
154+ 'linaro_django_xmlrpc.authtoken': {
155+ 'Meta': {'object_name': 'AuthToken'},
156+ 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
157+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
158+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
159+ 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
160+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'1n07jwp73fldk40sk0hshidru6nh5rqlrn40oq4hs5f9wx99o0wemwme43raxx008kyfsbahl56x8wyndgyclbapc43maycile201e1snt6p8a02n4hgyc506fda8umq'", 'unique': 'True', 'max_length': '128'}),
161+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"})
162+ }
163+ }
164+
165+ complete_apps = ['lava_scheduler_app']
166\ No newline at end of file
167
168=== modified file 'lava_scheduler_app/models.py'
169--- lava_scheduler_app/models.py 2013-06-20 11:41:39 +0000
170+++ lava_scheduler_app/models.py 2013-06-25 06:19:07 +0000
171@@ -1,5 +1,7 @@
172 import logging
173 import os
174+import json
175+import uuid
176 import simplejson
177 import urlparse
178 import copy
179@@ -19,6 +21,7 @@
180 from dashboard_app.models import Bundle, BundleStream
181
182 from lava_dispatcher.job import validate_job_data
183+from lava_scheduler_daemon import utils
184
185 from linaro_django_xmlrpc.models import AuthToken
186
187@@ -256,6 +259,12 @@
188 max_length = 200
189 )
190
191+ target_group = models.CharField(
192+ verbose_name = _(u"Target Group"),
193+ blank = True,
194+ max_length = 64
195+ )
196+
197 submitter = models.ForeignKey(
198 User,
199 verbose_name = _(u"Submitter"),
200@@ -475,13 +484,49 @@
201 except Tag.DoesNotExist:
202 raise JSONDataError("tag %r does not exist" % tag_name)
203
204- job = TestJob(
205- definition=json_data, submitter=submitter,
206- requested_device=target, requested_device_type=device_type,
207- description=job_name, health_check=health_check, user=user,
208- group=group, is_public=is_public, priority=priority)
209- job.save()
210- return job.id
211+ if 'device_group' in job_data:
212+ target_group = str(uuid.uuid4())
213+ node_json, group_json = utils.split_multi_job(job_data,
214+ target_group)
215+ job_list = []
216+ try:
217+ parent_id = (TestJob.objects.latest('id')).id + 1
218+ except:
219+ parent_id = 1
220+ child_id = 0
221+ parent_job = str(parent_id) + '.' + str(child_id)
222+
223+ for role in node_json:
224+ role_count = len(node_json[role])
225+ for c in range(0, role_count):
226+ device_type = DeviceType.objects.get(
227+ name=node_json[role][c]["device_type"])
228+ sub_id = str(parent_id) + '.' + str(child_id)
229+ logger = logging.getLogger("SUBMITLOGGER")
230+ logger.info(json.dumps(node_json[role][c]))
231+
232+ job = TestJob(
233+ sub_id=sub_id, submitter=submitter,
234+ requested_device=target, description=job_name,
235+ requested_device_type=device_type,
236+ definition=json.dumps(node_json[role][c]),
237+ health_check=health_check, user=user, group=group,
238+ is_public=is_public, priority=priority,
239+ target_group=target_group)
240+ job.save()
241+ job_list.append(sub_id)
242+ child_id += 1
243+ return job_list
244+
245+ else:
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+ target_group=None)
252+ job.save()
253+ return job.id
254
255 def _can_admin(self, user):
256 """ used to check for things like if the user can cancel or annotate
257
258=== added file 'lava_scheduler_daemon/utils.py'
259--- lava_scheduler_daemon/utils.py 1970-01-01 00:00:00 +0000
260+++ lava_scheduler_daemon/utils.py 2013-06-25 06:19:07 +0000
261@@ -0,0 +1,86 @@
262+# Copyright (C) 2013 Linaro Limited
263+#
264+# Author: Neil Williams <neil.williams@linaro.org>
265+# Senthil Kumaran <senthil.kumaran@linaro.org>
266+#
267+# This file is part of LAVA Scheduler.
268+#
269+# LAVA Scheduler is free software: you can redistribute it and/or modify it
270+# under the terms of the GNU Affero General Public License version 3 as
271+# published by the Free Software Foundation
272+#
273+# LAVA Scheduler is distributed in the hope that it will be useful, but
274+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
275+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
276+# more details.
277+#
278+# You should have received a copy of the GNU Affero General Public License
279+# along with LAVA Scheduler. If not, see <http://www.gnu.org/licenses/>.
280+
281+import json
282+import copy
283+from socket import gethostname
284+
285+
286+def split_multi_job(multi_job_data, target_group):
287+ group_json = {}
288+ node_json = {}
289+ all_nodes = {}
290+ node_actions = {}
291+ hostname = gethostname()
292+ port = 3079
293+ json_jobdata = multi_job_data
294+ if "device_group" in json_jobdata:
295+ # multinode start, group stage 1
296+ group_json["timeout"] = json_jobdata["timeout"]
297+ group_json["group_dispatcher"] = True
298+ # group stage 2 - configurable values
299+ # all the groupd_dispatcher really needs is the port number to use
300+ group_json["logging_level"] = "DEBUG"
301+ group_json["port"] = port
302+ group_json["hostname"] = hostname
303+ # multinode node stage 1
304+ for actions in json_jobdata["actions"]:
305+ if "parameters" not in actions \
306+ or 'role' not in actions["parameters"]:
307+ continue
308+ role = str(actions["parameters"]["role"])
309+ node_actions[role] = []
310+ for actions in json_jobdata["actions"]:
311+ if "parameters" not in actions \
312+ or 'role' not in actions["parameters"]:
313+ # add to each node, e.g. submit_results
314+ all_nodes[actions["command"]] = actions
315+ continue
316+ role = str(actions["parameters"]["role"])
317+ actions["parameters"].pop('role', None)
318+ node_actions[role].append({"command": actions["command"],
319+ "parameters": actions["parameters"]})
320+ group_count = 0
321+ for clients in json_jobdata["device_group"]:
322+ group_count += int(clients["count"])
323+ for clients in json_jobdata["device_group"]:
324+ role = str(clients["role"])
325+ count = int(clients["count"])
326+ node_json[role] = []
327+ for c in range(0, count):
328+ node_json[role].append({})
329+ node_json[role][c]["timeout"] = json_jobdata["timeout"]
330+ node_json[role][c]["job_name"] = json_jobdata["job_name"]
331+ node_json[role][c]["tags"] = clients["tags"]
332+ node_json[role][c]["group_size"] = group_count
333+ node_json[role][c]["target_group"] = target_group
334+ node_json[role][c]["actions"] = copy.deepcopy(
335+ node_actions[role])
336+ for key in all_nodes:
337+ node_json[role][c]["actions"].append(all_nodes[key])
338+ node_json[role][c]["role"] = role
339+ # multinode node stage 2
340+ node_json[role][c]["logging_level"] = "DEBUG"
341+ node_json[role][c]["port"] = port
342+ node_json[role][c]["hostname"] = hostname
343+ node_json[role][c]["device_type"] = clients["device_type"]
344+
345+ return (node_json, group_json)
346+
347+ return 0

Subscribers

People subscribed via source and target branches

to status/vote changes: