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
=== added file 'lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py'
--- lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py 1970-01-01 00:00:00 +0000
+++ lava_scheduler_app/migrations/0031_auto__add_field_testjob_target_group.py 2013-06-25 06:19:07 +0000
@@ -0,0 +1,161 @@
1# -*- coding: utf-8 -*-
2import datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7
8class Migration(SchemaMigration):
9
10 def forwards(self, orm):
11 # Adding field 'TestJob.target_group'
12 db.add_column('lava_scheduler_app_testjob', 'target_group',
13 self.gf('django.db.models.fields.CharField')(default='', max_length=64, blank=True),
14 keep_default=False)
15
16
17 def backwards(self, orm):
18 # Deleting field 'TestJob.target_group'
19 db.delete_column('lava_scheduler_app_testjob', 'target_group')
20
21
22 models = {
23 'auth.group': {
24 'Meta': {'object_name': 'Group'},
25 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
26 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
27 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
28 },
29 'auth.permission': {
30 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
31 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
32 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
33 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
34 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
35 },
36 'auth.user': {
37 'Meta': {'object_name': 'User'},
38 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
39 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
40 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
41 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
42 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
43 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
44 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
45 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
46 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
47 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
48 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
49 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
50 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
51 },
52 'contenttypes.contenttype': {
53 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
54 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
55 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
56 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
57 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
58 },
59 'dashboard_app.bundle': {
60 'Meta': {'ordering': "['-uploaded_on']", 'object_name': 'Bundle'},
61 '_gz_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'gz_content'"}),
62 '_raw_content': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'db_column': "'content'"}),
63 'bundle_stream': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bundles'", 'to': "orm['dashboard_app.BundleStream']"}),
64 'content_filename': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
65 'content_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40', 'unique': 'True', 'null': 'True'}),
66 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
67 'is_deserialized': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
68 'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'uploaded_bundles'", 'null': 'True', 'to': "orm['auth.User']"}),
69 'uploaded_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
70 },
71 'dashboard_app.bundlestream': {
72 'Meta': {'object_name': 'BundleStream'},
73 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
74 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
76 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
77 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
78 'pathname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
79 'slug': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
80 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
81 },
82 'lava_scheduler_app.device': {
83 'Meta': {'object_name': 'Device'},
84 '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'}),
85 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}),
86 'device_version': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
87 'health_status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
88 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}),
89 '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'}),
90 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
91 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'})
92 },
93 'lava_scheduler_app.devicestatetransition': {
94 'Meta': {'object_name': 'DeviceStateTransition'},
95 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
96 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
97 'device': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'transitions'", 'to': "orm['lava_scheduler_app.Device']"}),
98 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
99 'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
100 'message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
101 'new_state': ('django.db.models.fields.IntegerField', [], {}),
102 'old_state': ('django.db.models.fields.IntegerField', [], {})
103 },
104 'lava_scheduler_app.devicetype': {
105 'Meta': {'object_name': 'DeviceType'},
106 'display': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
107 'health_check_job': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
108 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True'})
109 },
110 'lava_scheduler_app.jobfailuretag': {
111 'Meta': {'object_name': 'JobFailureTag'},
112 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
113 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
114 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'})
115 },
116 'lava_scheduler_app.tag': {
117 'Meta': {'object_name': 'Tag'},
118 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
119 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
120 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
121 },
122 'lava_scheduler_app.testjob': {
123 'Meta': {'object_name': 'TestJob'},
124 '_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'}),
125 '_results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'db_column': "'results_link'", 'blank': 'True'}),
126 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
127 'definition': ('django.db.models.fields.TextField', [], {}),
128 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}),
129 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
130 'failure_comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
131 'failure_tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'failure_tags'", 'blank': 'True', 'to': "orm['lava_scheduler_app.JobFailureTag']"}),
132 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
133 'health_check': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
134 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
135 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
136 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}),
137 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}),
138 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}),
139 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}),
140 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
141 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
142 'sub_id': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
143 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
144 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
145 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['auth.User']"}),
146 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}),
147 'target_group': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
148 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'})
149 },
150 'linaro_django_xmlrpc.authtoken': {
151 'Meta': {'object_name': 'AuthToken'},
152 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
153 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
154 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
155 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
156 'secret': ('django.db.models.fields.CharField', [], {'default': "'1n07jwp73fldk40sk0hshidru6nh5rqlrn40oq4hs5f9wx99o0wemwme43raxx008kyfsbahl56x8wyndgyclbapc43maycile201e1snt6p8a02n4hgyc506fda8umq'", 'unique': 'True', 'max_length': '128'}),
157 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"})
158 }
159 }
160
161 complete_apps = ['lava_scheduler_app']
0\ No newline at end of file162\ No newline at end of file
1163
=== modified file 'lava_scheduler_app/models.py'
--- lava_scheduler_app/models.py 2013-06-20 11:41:39 +0000
+++ lava_scheduler_app/models.py 2013-06-25 06:19:07 +0000
@@ -1,5 +1,7 @@
1import logging1import logging
2import os2import os
3import json
4import uuid
3import simplejson5import simplejson
4import urlparse6import urlparse
5import copy7import copy
@@ -19,6 +21,7 @@
19from dashboard_app.models import Bundle, BundleStream21from dashboard_app.models import Bundle, BundleStream
2022
21from lava_dispatcher.job import validate_job_data23from lava_dispatcher.job import validate_job_data
24from lava_scheduler_daemon import utils
2225
23from linaro_django_xmlrpc.models import AuthToken26from linaro_django_xmlrpc.models import AuthToken
2427
@@ -256,6 +259,12 @@
256 max_length = 200259 max_length = 200
257 )260 )
258261
262 target_group = models.CharField(
263 verbose_name = _(u"Target Group"),
264 blank = True,
265 max_length = 64
266 )
267
259 submitter = models.ForeignKey(268 submitter = models.ForeignKey(
260 User,269 User,
261 verbose_name = _(u"Submitter"),270 verbose_name = _(u"Submitter"),
@@ -475,13 +484,49 @@
475 except Tag.DoesNotExist:484 except Tag.DoesNotExist:
476 raise JSONDataError("tag %r does not exist" % tag_name)485 raise JSONDataError("tag %r does not exist" % tag_name)
477486
478 job = TestJob(487 if 'device_group' in job_data:
479 definition=json_data, submitter=submitter,488 target_group = str(uuid.uuid4())
480 requested_device=target, requested_device_type=device_type,489 node_json, group_json = utils.split_multi_job(job_data,
481 description=job_name, health_check=health_check, user=user,490 target_group)
482 group=group, is_public=is_public, priority=priority)491 job_list = []
483 job.save()492 try:
484 return job.id493 parent_id = (TestJob.objects.latest('id')).id + 1
494 except:
495 parent_id = 1
496 child_id = 0
497 parent_job = str(parent_id) + '.' + str(child_id)
498
499 for role in node_json:
500 role_count = len(node_json[role])
501 for c in range(0, role_count):
502 device_type = DeviceType.objects.get(
503 name=node_json[role][c]["device_type"])
504 sub_id = str(parent_id) + '.' + str(child_id)
505 logger = logging.getLogger("SUBMITLOGGER")
506 logger.info(json.dumps(node_json[role][c]))
507
508 job = TestJob(
509 sub_id=sub_id, submitter=submitter,
510 requested_device=target, description=job_name,
511 requested_device_type=device_type,
512 definition=json.dumps(node_json[role][c]),
513 health_check=health_check, user=user, group=group,
514 is_public=is_public, priority=priority,
515 target_group=target_group)
516 job.save()
517 job_list.append(sub_id)
518 child_id += 1
519 return job_list
520
521 else:
522 job = TestJob(
523 definition=json_data, submitter=submitter,
524 requested_device=target, requested_device_type=device_type,
525 description=job_name, health_check=health_check, user=user,
526 group=group, is_public=is_public, priority=priority,
527 target_group=None)
528 job.save()
529 return job.id
485530
486 def _can_admin(self, user):531 def _can_admin(self, user):
487 """ used to check for things like if the user can cancel or annotate532 """ used to check for things like if the user can cancel or annotate
488533
=== added file 'lava_scheduler_daemon/utils.py'
--- lava_scheduler_daemon/utils.py 1970-01-01 00:00:00 +0000
+++ lava_scheduler_daemon/utils.py 2013-06-25 06:19:07 +0000
@@ -0,0 +1,86 @@
1# Copyright (C) 2013 Linaro Limited
2#
3# Author: Neil Williams <neil.williams@linaro.org>
4# Senthil Kumaran <senthil.kumaran@linaro.org>
5#
6# This file is part of LAVA Scheduler.
7#
8# LAVA Scheduler is free software: you can redistribute it and/or modify it
9# under the terms of the GNU Affero General Public License version 3 as
10# published by the Free Software Foundation
11#
12# LAVA Scheduler is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15# more details.
16#
17# You should have received a copy of the GNU Affero General Public License
18# along with LAVA Scheduler. If not, see <http://www.gnu.org/licenses/>.
19
20import json
21import copy
22from socket import gethostname
23
24
25def split_multi_job(multi_job_data, target_group):
26 group_json = {}
27 node_json = {}
28 all_nodes = {}
29 node_actions = {}
30 hostname = gethostname()
31 port = 3079
32 json_jobdata = multi_job_data
33 if "device_group" in json_jobdata:
34 # multinode start, group stage 1
35 group_json["timeout"] = json_jobdata["timeout"]
36 group_json["group_dispatcher"] = True
37 # group stage 2 - configurable values
38 # all the groupd_dispatcher really needs is the port number to use
39 group_json["logging_level"] = "DEBUG"
40 group_json["port"] = port
41 group_json["hostname"] = hostname
42 # multinode node stage 1
43 for actions in json_jobdata["actions"]:
44 if "parameters" not in actions \
45 or 'role' not in actions["parameters"]:
46 continue
47 role = str(actions["parameters"]["role"])
48 node_actions[role] = []
49 for actions in json_jobdata["actions"]:
50 if "parameters" not in actions \
51 or 'role' not in actions["parameters"]:
52 # add to each node, e.g. submit_results
53 all_nodes[actions["command"]] = actions
54 continue
55 role = str(actions["parameters"]["role"])
56 actions["parameters"].pop('role', None)
57 node_actions[role].append({"command": actions["command"],
58 "parameters": actions["parameters"]})
59 group_count = 0
60 for clients in json_jobdata["device_group"]:
61 group_count += int(clients["count"])
62 for clients in json_jobdata["device_group"]:
63 role = str(clients["role"])
64 count = int(clients["count"])
65 node_json[role] = []
66 for c in range(0, count):
67 node_json[role].append({})
68 node_json[role][c]["timeout"] = json_jobdata["timeout"]
69 node_json[role][c]["job_name"] = json_jobdata["job_name"]
70 node_json[role][c]["tags"] = clients["tags"]
71 node_json[role][c]["group_size"] = group_count
72 node_json[role][c]["target_group"] = target_group
73 node_json[role][c]["actions"] = copy.deepcopy(
74 node_actions[role])
75 for key in all_nodes:
76 node_json[role][c]["actions"].append(all_nodes[key])
77 node_json[role][c]["role"] = role
78 # multinode node stage 2
79 node_json[role][c]["logging_level"] = "DEBUG"
80 node_json[role][c]["port"] = port
81 node_json[role][c]["hostname"] = hostname
82 node_json[role][c]["device_type"] = clients["device_type"]
83
84 return (node_json, group_json)
85
86 return 0

Subscribers

People subscribed via source and target branches

to status/vote changes: