=== modified file 'lava_scheduler_app/api.py'
@@ -13,3 +13,12 @@
if not self.user.has_perm('lava_scheduler_app.add_testjob'):
raise xmlrpclib.Fault(403, "Permission denied.")
return TestJob.from_json_and_user(job_data, self.user).id
+
+ def cancel_job(self, job_id):
+ if not self.user:
+ raise xmlrpclib.Fault(401, "Authentication required.")
+ job = TestJob.objects.get(pk=job_id)
+ if not job.can_cancel(self.user):
+ raise xmlrpclib.Fault(403, "Permission denied.")
+ job.cancel()
+ return True
=== modified file 'lava_scheduler_app/models.py'
@@ -69,6 +69,7 @@
COMPLETE = 2
INCOMPLETE = 3
CANCELED = 4
+ CANCELING = 5
STATUS_CHOICES = (
(SUBMITTED, 'Submitted'),
@@ -76,6 +77,7 @@
(COMPLETE, 'Complete'),
(INCOMPLETE, 'Incomplete'),
(CANCELED, 'Canceled'),
+ (CANCELING, 'Canceling'),
)
id = models.AutoField(primary_key=True)
@@ -92,13 +94,13 @@
# Only one of these two should be non-null.
requested_device = models.ForeignKey(
- Device, null=True, default=None, related_name='+')
+ Device, null=True, default=None, related_name='+', blank=True)
requested_device_type = models.ForeignKey(
- DeviceType, null=True, default=None, related_name='+')
+ DeviceType, null=True, default=None, related_name='+', blank=True)
# This is set once the job starts.
actual_device = models.ForeignKey(
- Device, null=True, default=None, related_name='+')
+ Device, null=True, default=None, related_name='+', blank=True)
#priority = models.IntegerField(
# verbose_name = _(u"Priority"),
@@ -133,7 +135,7 @@
editable = False,
)
log_file = models.FileField(
- upload_to='lava-logs', default=None, null=True)
+ upload_to='lava-logs', default=None, null=True, blank=True)
results_link = models.CharField(
max_length=400, default=None, null=True, blank=True)
@@ -158,3 +160,13 @@
requested_device_type=device_type)
job.save()
return job
+
+ def can_cancel(self, user):
+ return user.is_superuser or user == self.submitter
+
+ def cancel(self):
+ if self.status == TestJob.RUNNING:
+ self.status = TestJob.CANCELING
+ else:
+ self.status = TestJob.CANCELED
+ self.save()
=== modified file 'lava_scheduler_app/templates/lava_scheduler_app/job.html'
@@ -21,6 +21,14 @@
{% block content %}
<h2>Job {{ job.pk }}</h2>
+{% if show_cancel %}
+<form style="display:inline; float:right" method="POST"
+ action="{% url lava_scheduler_app.views.job_cancel job.pk %}">
+ {% csrf_token %}
+ <button id="cancel-button">Cancel</button>
+</form>
+{% endif %}
+
<div id="columns">
<div class="column">
<dt>Submitted by:</dt>
@@ -155,6 +163,7 @@
}
}
);
+ $("#cancel-button").button();
}
);
</script>
=== modified file 'lava_scheduler_app/tests.py'
@@ -66,10 +66,11 @@
device.save()
return device
- def make_testjob(self, definition=None, **kwargs):
+ def make_testjob(self, definition=None, submitter=None, **kwargs):
if definition is None:
definition = json.dumps({})
- submitter = self.make_user()
+ if submitter is None:
+ submitter = self.make_user()
testjob = TestJob(
definition=definition, submitter=submitter, **kwargs)
testjob.save()
@@ -139,7 +140,7 @@
'http://localhost/RPC2/',
transport=TestTransport(user=user, password=password))
- def test_api_rejects_anonymous(self):
+ def test_submit_job_rejects_anonymous(self):
server = self.server_proxy()
try:
server.scheduler.submit_job("{}")
@@ -148,7 +149,7 @@
else:
self.fail("fault not raised")
- def test_api_rejects_unpriv_user(self):
+ def test_submit_job_rejects_unpriv_user(self):
User.objects.create_user('test', 'e@mail.invalid', 'test').save()
server = self.server_proxy('test', 'test')
try:
@@ -158,7 +159,7 @@
else:
self.fail("fault not raised")
- def test_sets_definition(self):
+ def test_submit_job_sets_definition(self):
user = User.objects.create_user('test', 'e@mail.invalid', 'test')
user.user_permissions.add(
Permission.objects.get(codename='add_testjob'))
@@ -170,6 +171,36 @@
job = TestJob.objects.get(id=job_id)
self.assertEqual(definition, job.definition)
+ def test_cancel_job_rejects_anonymous(self):
+ job = self.factory.make_testjob()
+ server = self.server_proxy()
+ try:
+ server.scheduler.cancel_job(job.id)
+ except xmlrpclib.Fault as f:
+ self.assertEqual(401, f.faultCode)
+ else:
+ self.fail("fault not raised")
+
+ def test_cancel_job_rejects_unpriv_user(self):
+ job = self.factory.make_testjob()
+ User.objects.create_user('test', 'e@mail.invalid', 'test').save()
+ server = self.server_proxy('test', 'test')
+ try:
+ server.scheduler.cancel_job(job.id)
+ except xmlrpclib.Fault as f:
+ self.assertEqual(403, f.faultCode)
+ else:
+ self.fail("fault not raised")
+
+ def test_cancel_job_cancels_job(self):
+ user = User.objects.create_user('test', 'e@mail.invalid', 'test')
+ user.save()
+ job = self.factory.make_testjob(submitter=user)
+ server = self.server_proxy('test', 'test')
+ server.scheduler.cancel_job(job.id)
+ job = TestJob.objects.get(pk=job.pk)
+ self.assertEqual(TestJob.CANCELED, job.status)
+
from django.test import TransactionTestCase
=== modified file 'lava_scheduler_app/urls.py'
@@ -6,4 +6,5 @@
url(r'^alljobs$', 'alljobs'),
url(r'^job/(?P<pk>[0-9]+)$', 'job'),
url(r'^job/(?P<pk>[0-9]+)/output$', 'job_output'),
+ url(r'^job/(?P<pk>[0-9]+)/cancel$', 'job_cancel'),
)
=== modified file 'lava_scheduler_app/views.py'
@@ -1,8 +1,8 @@
import os
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseForbidden
from django.template import RequestContext
-from django.shortcuts import render_to_response
+from django.shortcuts import redirect, render_to_response
from lava_scheduler_app.models import Device, TestJob
@@ -33,6 +33,7 @@
{
'log_file_present': bool(job.log_file),
'job': TestJob.objects.get(pk=pk),
+ 'show_cancel': job.status <= TestJob.RUNNING and job.can_cancel(request.user),
},
RequestContext(request))
@@ -69,3 +70,13 @@
if job.status != TestJob.RUNNING:
response['X-Is-Finished'] = '1'
return response
+
+
+def job_cancel(request, pk):
+ job = TestJob.objects.get(pk=pk)
+ if job.can_cancel(request.user):
+ job.cancel()
+ return redirect('lava_scheduler_app.views.job', pk=job.pk)
+ else:
+ return HttpResponseForbidden(
+ "you cannot cancel this job", content_type="text/plain")
=== modified file 'lava_scheduler_daemon/board.py'
@@ -1,10 +1,11 @@
import json
import os
+import signal
import tempfile
import logging
from twisted.internet.protocol import ProcessProtocol
-from twisted.internet import defer
+from twisted.internet import defer, task
from twisted.protocols.basic import LineReceiver
@@ -69,6 +70,17 @@
self.board_name = board_name
self.reactor = reactor
self._json_file = None
+ self._source_lock = defer.DeferredLock()
+ self._checkCancel_call = task.LoopingCall(self._checkCancel)
+
+ def _checkCancel(self):
+ return self._source_lock.run(
+ self.source.jobCheckForCancellation, self.board_name).addCallback(
+ self._maybeCancel)
+
+ def _maybeCancel(self, cancel):
+ if cancel:
+ self._protocol.transport.signalProcess(signal.SIGINT)
def run(self):
d = self.source.getLogFileForJobOnBoard(self.board_name)
@@ -81,12 +93,13 @@
fd, self._json_file = tempfile.mkstemp()
with os.fdopen(fd, 'wb') as f:
json.dump(json_data, f)
+ self._protocol = DispatcherProcessProtocol(
+ d, log_file, self.source, self.board_name)
self.reactor.spawnProcess(
- DispatcherProcessProtocol(
- d, log_file, self.source, self.board_name),
- self.dispatcher, args=[
+ self._protocol, self.dispatcher, args=[
self.dispatcher, self._json_file, '--oob-fd', '3'],
childFDs={0:0, 1:'r', 2:'r', 3:'r'}, env=None)
+ self._checkCancel_call.start(10)
d.addBoth(self._exited)
return d
@@ -95,7 +108,9 @@
if self._json_file is not None:
os.unlink(self._json_file)
self.logger.info("reporting job completed")
- return self.source.jobCompleted(self.board_name).addCallback(
+ self._source_lock.run(self._checkCancel_call.stop)
+ return self._source_lock.run(
+ self.source.jobCompleted, self.board_name).addCallback(
lambda r:result)
@@ -128,7 +143,7 @@
SimplePP(d), 'lava-scheduler-monitor', childFDs={0:0, 1:1, 2:2},
env=None, args=[
'lava-scheduler-monitor', self.dispatcher,
- self.board_name, self._json_file])
+ str(self.board_name), self._json_file])
d.addBoth(self._exited)
return d
=== modified file 'lava_scheduler_daemon/dbjobsource.py'
@@ -127,7 +127,14 @@
device.status = Device.IDLE
job = device.current_job
device.current_job = None
- job.status = TestJob.COMPLETE
+ if job.status == TestJob.RUNNING:
+ job.status = TestJob.COMPLETE
+ elif job.status == TestJob.CANCELING:
+ job.status = TestJob.CANCELED
+ else:
+ self.logger.error(
+ "Unexpected job state in jobCompleted: %s" % job.status)
+ job.status = TestJob.COMPLETE
job.end_time = datetime.datetime.utcnow()
device.save()
job.save()
@@ -146,3 +153,12 @@
def jobOobData(self, board_name, key, value):
return self.deferForDB(self.jobOobData_impl, board_name, key, value)
+
+ def jobCheckForCancellation_impl(self, board_name):
+ device = Device.objects.get(hostname=board_name)
+ device.status = Device.IDLE
+ job = device.current_job
+ return job.status != TestJob.RUNNING
+
+ def jobCheckForCancellation(self, board_name):
+ return self.deferForDB(self.jobCheckForCancellation_impl, board_name)