From patchwork Thu Feb 9 19:20:16 2012 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Michael-Doyle Hudson X-Patchwork-Id: 6727 Return-Path: X-Original-To: patchwork@peony.canonical.com Delivered-To: patchwork@peony.canonical.com Received: from fiordland.canonical.com (fiordland.canonical.com [91.189.94.145]) by peony.canonical.com (Postfix) with ESMTP id 2E92023E01 for ; Thu, 9 Feb 2012 19:20:19 +0000 (UTC) Received: from mail-iy0-f180.google.com (mail-iy0-f180.google.com [209.85.210.180]) by fiordland.canonical.com (Postfix) with ESMTP id B219FA18175 for ; Thu, 9 Feb 2012 19:20:18 +0000 (UTC) Received: by iabz7 with SMTP id z7so3939241iab.11 for ; Thu, 09 Feb 2012 11:20:18 -0800 (PST) Received: by 10.50.15.231 with SMTP id a7mr5664435igd.8.1328815218047; Thu, 09 Feb 2012 11:20:18 -0800 (PST) X-Forwarded-To: linaro-patchwork@canonical.com X-Forwarded-For: patch@linaro.org linaro-patchwork@canonical.com Delivered-To: patches@linaro.org Received: by 10.231.12.131 with SMTP id x3cs51228ibx; Thu, 9 Feb 2012 11:20:17 -0800 (PST) Received: by 10.216.138.13 with SMTP id z13mr1225417wei.41.1328815216925; Thu, 09 Feb 2012 11:20:16 -0800 (PST) Received: from indium.canonical.com (indium.canonical.com. [91.189.90.7]) by mx.google.com with ESMTPS id v2si3018955wid.20.2012.02.09.11.20.16 (version=TLSv1/SSLv3 cipher=OTHER); Thu, 09 Feb 2012 11:20:16 -0800 (PST) Received-SPF: pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) client-ip=91.189.90.7; Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of bounces@canonical.com designates 91.189.90.7 as permitted sender) smtp.mail=bounces@canonical.com Received: from ackee.canonical.com ([91.189.89.26]) by indium.canonical.com with esmtp (Exim 4.71 #1 (Debian)) id 1RvZXU-0006KA-DM for ; Thu, 09 Feb 2012 19:20:16 +0000 Received: from ackee.canonical.com (localhost [127.0.0.1]) by ackee.canonical.com (Postfix) with ESMTP id 57A18E0180 for ; Thu, 9 Feb 2012 19:20:16 +0000 (UTC) MIME-Version: 1.0 X-Launchpad-Project: lava-scheduler X-Launchpad-Branch: ~linaro-validation/lava-scheduler/trunk X-Launchpad-Message-Rationale: Subscriber X-Launchpad-Branch-Revision-Number: 127 X-Launchpad-Notification-Type: branch-revision To: Linaro Patch Tracker From: noreply@launchpad.net Subject: [Branch ~linaro-validation/lava-scheduler/trunk] Rev 127: * record device status transitions Message-Id: <20120209192016.30327.2628.launchpad@ackee.canonical.com> Date: Thu, 09 Feb 2012 19:20:16 -0000 Reply-To: noreply@launchpad.net Sender: bounces@canonical.com Errors-To: bounces@canonical.com Precedence: bulk X-Generated-By: Launchpad (canonical.com); Revision="14763"; Instance="launchpad-lazr.conf" X-Launchpad-Hash: 55f94dc9704b1172f5dee96598ff53fa7573c30c X-Gm-Message-State: ALoCoQn7hMo1KDPNhgUns9hAYXHIikarmZ7C4+O4BCpn/slVtjY9KDyy0B0wnWkuVTQ6NNdG0/wc Merge authors: Michael Hudson-Doyle (mwhudson) Related merge proposals: https://code.launchpad.net/~mwhudson/lava-scheduler/board-transition-log/+merge/91955 proposed by: Michael Hudson-Doyle (mwhudson) review: Approve - Zygmunt Krynicki (zkrynicki) ------------------------------------------------------------ revno: 127 [merge] committer: Michael Hudson-Doyle branch nick: trunk timestamp: Thu 2012-02-09 11:18:18 -0800 message: * record device status transitions * ask for a reason when offlining/onlining a board * display transitions on the device page added: lava_scheduler_app/migrations/0013_auto__add_devicestatetransition.py lava_scheduler_app/static/js/jquery.details.min.js modified: lava_scheduler_app/admin.py lava_scheduler_app/models.py lava_scheduler_app/templates/lava_scheduler_app/device.html lava_scheduler_app/views.py lava_scheduler_daemon/dbjobsource.py --- lp:lava-scheduler https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk You are subscribed to branch lp:lava-scheduler. To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-scheduler/trunk/+edit-subscription === modified file 'lava_scheduler_app/admin.py' --- lava_scheduler_app/admin.py 2011-12-14 01:07:33 +0000 +++ lava_scheduler_app/admin.py 2012-02-08 23:31:37 +0000 @@ -1,7 +1,29 @@ from django.contrib import admin -from lava_scheduler_app.models import Device, DeviceType, TestJob, Tag - -admin.site.register(Device) +from lava_scheduler_app.models import ( + Device, DeviceStateTransition, DeviceType, TestJob, Tag, + ) + +# XXX These actions should really go to another screen that asks for a reason. +# Sounds tedious to implement though. + +def offline_action(modeladmin, request, queryset): + for device in queryset: + if device.can_admin(request.user): + device.put_into_maintenance_mode(request.user, "admin action") +offline_action.short_description = "take offline" + +def online_action(modeladmin, request, queryset): + for device in queryset: + if device.can_admin(request.user): + device.put_into_online_mode(request.user, "admin action") +online_action.short_description = "take online" + +class DeviceAdmin(admin.ModelAdmin): + actions = [online_action, offline_action] + list_filter = ['device_type', 'status'] + +admin.site.register(Device, DeviceAdmin) +admin.site.register(DeviceStateTransition) admin.site.register(DeviceType) admin.site.register(TestJob) admin.site.register(Tag) === added file 'lava_scheduler_app/migrations/0013_auto__add_devicestatetransition.py' --- lava_scheduler_app/migrations/0013_auto__add_devicestatetransition.py 1970-01-01 00:00:00 +0000 +++ lava_scheduler_app/migrations/0013_auto__add_devicestatetransition.py 2012-02-08 01:23:38 +0000 @@ -0,0 +1,124 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'DeviceStateTransition' + db.create_table('lava_scheduler_app_devicestatetransition', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('created_on', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('device', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['lava_scheduler_app.Device'])), + ('job', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['lava_scheduler_app.TestJob'], null=True, blank=True)), + ('old_state', self.gf('django.db.models.fields.IntegerField')()), + ('new_state', self.gf('django.db.models.fields.IntegerField')()), + ('message', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('lava_scheduler_app', ['DeviceStateTransition']) + + + def backwards(self, orm): + + # Deleting model 'DeviceStateTransition' + db.delete_table('lava_scheduler_app_devicestatetransition') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'lava_scheduler_app.device': { + 'Meta': {'object_name': 'Device'}, + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.TestJob']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'device_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.DeviceType']"}), + 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '200', 'primary_key': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'lava_scheduler_app.devicestatetransition': { + 'Meta': {'object_name': 'DeviceStateTransition'}, + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'device': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['lava_scheduler_app.Device']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'new_state': ('django.db.models.fields.IntegerField', [], {}) + }, + 'lava_scheduler_app.devicetype': { + 'Meta': {'object_name': 'DeviceType'}, + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True', 'db_index': 'True'}) + }, + 'lava_scheduler_app.tag': { + 'Meta': {'object_name': 'Tag'}, + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}) + }, + 'lava_scheduler_app.testjob': { + 'Meta': {'object_name': 'TestJob'}, + 'actual_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), + 'definition': ('django.db.models.fields.TextField', [], {}), + 'description': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'end_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'log_file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'requested_device': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.Device']"}), + 'requested_device_type': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'+'", 'null': 'True', 'blank': 'True', 'to': "orm['lava_scheduler_app.DeviceType']"}), + 'results_link': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '400', 'null': 'True', 'blank': 'True'}), + 'start_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'submit_time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'submit_token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['linaro_django_xmlrpc.AuthToken']", 'null': 'True'}), + 'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['lava_scheduler_app.Tag']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'linaro_django_xmlrpc.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created_on': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_used_on': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'secret': ('django.db.models.fields.CharField', [], {'default': "'7omt7ki208qrgeoltltir1le0c9xka9u70j8guza8i8j1zjmpibxnwyzor8w3d07kngkzfiqo7fo80gx0x3yxxhxm9byfb00ylso2odh53odiz35u0djul4qhp2i0hew'", 'unique': 'True', 'max_length': '128'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['lava_scheduler_app'] === modified file 'lava_scheduler_app/models.py' --- lava_scheduler_app/models.py 2012-02-08 22:05:30 +0000 +++ lava_scheduler_app/models.py 2012-02-09 19:18:18 +0000 @@ -92,15 +92,25 @@ def can_admin(self, user): return user.has_perm('lava_scheduler_app.change_device') - def put_into_maintenance_mode(self): - if self.status == self.RUNNING: - self.status = self.OFFLINING + def put_into_maintenance_mode(self, user, reason): + if self.status in [self.RUNNING, self.OFFLINING]: + new_status = self.OFFLINING else: - self.status = self.OFFLINE + new_status = self.OFFLINE + DeviceStateTransition.objects.create( + created_by=user, device=self, old_state=self.status, + new_state=new_status, message=reason, job=None).save() + self.status = new_status self.save() - def put_into_online_mode(self): - self.status = self.IDLE + def put_into_online_mode(self, user, reason): + if self.status not in [Device.OFFLINE, Device.OFFLINING]: + return + new_status = self.IDLE + DeviceStateTransition.objects.create( + created_by=user, device=self, old_state=self.status, + new_state=new_status, message=reason, job=None).save() + self.status = new_status self.save() #@classmethod @@ -242,3 +252,13 @@ else: self.status = TestJob.CANCELED self.save() + + +class DeviceStateTransition(models.Model): + created_on = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(User, null=True, blank=True) + device = models.ForeignKey(Device, related_name='transitions') + job = models.ForeignKey(TestJob, null=True, blank=True) + old_state = models.IntegerField(choices=Device.STATUS_CHOICES) + new_state = models.IntegerField(choices=Device.STATUS_CHOICES) + message = models.TextField(null=True, blank=True) === added file 'lava_scheduler_app/static/js/jquery.details.min.js' --- lava_scheduler_app/static/js/jquery.details.min.js 1970-01-01 00:00:00 +0000 +++ lava_scheduler_app/static/js/jquery.details.min.js 2012-02-07 23:58:06 +0000 @@ -0,0 +1,4 @@ +/*! http://mths.be/details v0.0.1 by @mathias */ +(function(a,$){var c=$.fn,b,d=(function(i){var g=i.createElement("details"),f,e,h;if(!("open" in g)){return false}e=i.body||(function(){var j=i.documentElement;f=true;return j.insertBefore(i.createElement("body"),j.firstElementChild||j.firstChild)}());g.innerHTML="ab";g.style.display="block";e.appendChild(g);h=g.offsetHeight;g.open=true;h=h!=g.offsetHeight;e.removeChild(g);if(f){e.parentNode.removeChild(e)}return h}(a)); +/*! http://mths.be/noselect v1.0.2 by @mathias */ +c.noSelect=function(){var e="none";return this.bind("selectstart dragstart mousedown",function(){return false}).css({MozUserSelect:e,WebkitUserSelect:e,userSelect:e})};if(d){b=c.details=function(){return this};b.support=d}else{b=c.details=function(){return this.each(function(){var e=$(this),h=$("summary",e),g=e.children(":not(summary)"),i=e.contents(":not(summary)"),f=this.getAttribute("open");if(!h.length){h=$(a.createElement("summary")).text("Details").prependTo(e)}if(g.length!=i.length){i.filter(function(){return(this.nodeType===3)&&(/[^ \t\n\f\r]/.test(this.data))}).wrap("");g=e.children(":not(summary)")}if(typeof f=="string"||(typeof f=="boolean"&&f)){e.addClass("open");g.show()}else{g.hide()}h.noSelect().attr("tabIndex",0).click(function(){h.focus();typeof e.attr("open")!="undefined"?e.removeAttr("open"):e.attr("open","open");g.toggle(0);e.toggleClass("open")}).keyup(function(j){if(13===j.keyCode||32===j.keyCode){if(!($.browser.opera&&13===j.keyCode)){j.preventDefault();h.click()}}})})};b.support=d}}(document,jQuery)); \ No newline at end of file === modified file 'lava_scheduler_app/templates/lava_scheduler_app/device.html' --- lava_scheduler_app/templates/lava_scheduler_app/device.html 2011-12-15 03:49:36 +0000 +++ lava_scheduler_app/templates/lava_scheduler_app/device.html 2012-02-09 18:37:46 +0000 @@ -10,6 +10,17 @@ padding-bottom: 1em; } + + {% endblock %} {% block content %} @@ -21,6 +32,12 @@ {% csrf_token %} + {% endif %} {% if show_online %}
Put online
+ {% endif %}
@@ -47,7 +70,12 @@
Status:
-
{{ device.get_status_display }}
+
+ {{ device.get_status_display }} + {% if transition %} + (reason: {{ transition }}) + {% endif %} +
{% if device.current_job %}
Currently running:
- +
@@ -82,18 +110,85 @@
ID
+
+ See status transitions + + + + + + + + + + + {% for tr in transition_list %} + + + + + + + {% endfor %} + +
WhenTransitionByReason
+ {{ tr.0|date:"Y-m-d H:i" }} + {% if tr.1 %} + (after {{ tr.1|timesince:tr.0 }}) + {% endif %} + {{ tr.2 }} → {{ tr.3 }}{{ tr.4 }} + {% if tr.5 %} + {{ tr.5 }} + {% endif %} +
+
=== modified file 'lava_scheduler_app/views.py' --- lava_scheduler_app/views.py 2012-01-11 22:20:27 +0000 +++ lava_scheduler_app/views.py 2012-02-09 18:29:39 +0000 @@ -28,7 +28,7 @@ getDispatcherErrors, getDispatcherLogMessages ) -from lava_scheduler_app.models import Device, TestJob +from lava_scheduler_app.models import Device, DeviceStateTransition, TestJob def post_only(func): @@ -239,10 +239,32 @@ @BreadCrumb("Device {pk}", parent=index, needs=['pk']) def device_detail(request, pk): device = get_object_or_404(Device, pk=pk) + if device.status in [Device.OFFLINE, Device.OFFLINING]: + try: + transition = device.transitions.filter(message__isnull=False).latest('created_on').message + except DeviceStateTransition.DoesNotExist: + transition = None + else: + transition = None + transition_models = device.transitions.order_by('created_on').select_related('created_by') + transition_list = [] + if transition_models: + for i, t in enumerate(transition_models): + if i > 0: + before = transition_models[i-1].created_on + else: + before = None + transition_list.append( + (t.created_on, before, + t.get_old_state_display(), t.get_new_state_display(), + t.created_by, t.message)) + transition_list.reverse() return render_to_response( "lava_scheduler_app/device.html", { 'device': device, + 'transition': transition, + 'transition_list': transition_list, 'recent_job_list': device.recent_jobs, 'show_maintenance': device.can_admin(request.user) and \ device.status in [Device.IDLE, Device.RUNNING], @@ -257,7 +279,7 @@ def device_maintenance_mode(request, pk): device = Device.objects.get(pk=pk) if device.can_admin(request.user): - device.put_into_maintenance_mode() + device.put_into_maintenance_mode(request.user, request.POST.get('reason')) return redirect(device) else: return HttpResponseForbidden( @@ -268,7 +290,7 @@ def device_online(request, pk): device = Device.objects.get(pk=pk) if device.can_admin(request.user): - device.put_into_online_mode() + device.put_into_online_mode(request.user, request.POST.get('reason')) return redirect(device) else: return HttpResponseForbidden( === modified file 'lava_scheduler_daemon/dbjobsource.py' --- lava_scheduler_daemon/dbjobsource.py 2012-01-20 03:15:19 +0000 +++ lava_scheduler_daemon/dbjobsource.py 2012-02-09 18:20:33 +0000 @@ -15,7 +15,7 @@ from zope.interface import implements -from lava_scheduler_app.models import Device, TestJob +from lava_scheduler_app.models import Device, DeviceStateTransition, TestJob from lava_scheduler_daemon.jobsource import IJobSource @@ -140,6 +140,9 @@ jobs = jobs_for_device[:1] if jobs: job = jobs[0] + DeviceStateTransition.objects.create( + created_by=None, device=device, old_state=device.status, + new_state=Device.RUNNING, message=None, job=job).save() job.status = TestJob.RUNNING job.start_time = datetime.datetime.utcnow() job.actual_device = device @@ -186,6 +189,7 @@ def jobCompleted_impl(self, board_name, exit_code): self.logger.debug('marking job as complete on %s', board_name) device = Device.objects.get(hostname=board_name) + old_device_status = device.status if device.status == Device.RUNNING: device.status = Device.IDLE elif device.status == Device.OFFLINING: @@ -207,6 +211,9 @@ self.logger.error( "Unexpected job state in jobCompleted: %s" % job.status) job.status = TestJob.COMPLETE + DeviceStateTransition.objects.create( + created_by=None, device=device, old_state=old_device_status, + new_state=device.status, message=None, job=job).save() job.end_time = datetime.datetime.utcnow() token = job.submit_token job.submit_token = None