From ace6a579f6fb6a1a3abc65b42e2c4ddfebf35b45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= <sybren@stuvel.eu>
Date: Fri, 27 Jan 2017 14:02:10 +0100
Subject: [PATCH] Server: allow re-queueing of individual tasks.

This is quite useful if one task failed (for example when a worker times
out) and you want to re-queue it without cancelling any currently active
tasks.
---
 packages/flamenco/flamenco/tasks/__init__.py  | 11 ++++++
 packages/flamenco/flamenco/tasks/routes.py    | 28 +++++++++++++-
 packages/flamenco/flamenco/tasks/sdk.py       |  3 +-
 .../flamenco/src/scripts/tutti/10_tasks.js    | 38 ++++++++++++++++++-
 .../flamenco/tasks/view_task_embed.jade       | 22 +++++++----
 5 files changed, 92 insertions(+), 10 deletions(-)

diff --git a/packages/flamenco/flamenco/tasks/__init__.py b/packages/flamenco/flamenco/tasks/__init__.py
index f4ece6cc..81049d11 100644
--- a/packages/flamenco/flamenco/tasks/__init__.py
+++ b/packages/flamenco/flamenco/tasks/__init__.py
@@ -20,6 +20,8 @@ COLOR_FOR_TASK_STATUS = {
     'completed': '#bbe151',
 }
 
+REQUEABLE_TASK_STATES = {'completed', 'canceled', 'failed'}
+
 
 @attr.s
 class TaskManager(object):
@@ -90,6 +92,15 @@ class TaskManager(object):
 
         return tasks
 
+    def web_set_task_status(self, task_id, new_status):
+        """Web-level call to updates the task status."""
+        from .sdk import Task
+
+        api = pillar_api()
+        task = Task({'_id': task_id})
+        task.patch({'op': 'set-task-status',
+                    'status': new_status}, api=api)
+
 
 def setup_app(app):
     from . import eve_hooks, patch
diff --git a/packages/flamenco/flamenco/tasks/routes.py b/packages/flamenco/flamenco/tasks/routes.py
index 51928332..e4a05043 100644
--- a/packages/flamenco/flamenco/tasks/routes.py
+++ b/packages/flamenco/flamenco/tasks/routes.py
@@ -12,6 +12,9 @@ from flamenco import current_flamenco, ROLES_REQUIRED_TO_VIEW_ITEMS, ROLES_REQUI
 
 TASK_LOG_PAGE_SIZE = 10
 
+# The task statuses that can be set from the web-interface.
+ALLOWED_TASK_STATUSES_FROM_WEB = {'cancel-requested', 'queued'}
+
 perjob_blueprint = Blueprint('flamenco.tasks.perjob', __name__,
                              url_prefix='/<project_url>/jobs/<job_id>')
 perproject_blueprint = Blueprint('flamenco.tasks.perproject', __name__,
@@ -60,11 +63,34 @@ def view_task(project, flamenco_props, task_id):
         raise wz_exceptions.Forbidden()
 
     task = Task.find(task_id, api=api)
+
+    from . import REQUEABLE_TASK_STATES
+    write_access = current_flamenco.current_user_is_flamenco_admin()
+
     return render_template('flamenco/tasks/view_task_embed.html',
                            task=task,
                            project=project,
                            flamenco_props=flamenco_props.to_dict(),
-                           flamenco_context=request.args.get('context'))
+                           flamenco_context=request.args.get('context'),
+                           can_requeue_task=write_access and task['status'] in REQUEABLE_TASK_STATES)
+
+
+@perproject_blueprint.route('/<task_id>/set-status', methods=['POST'])
+@flask_login.login_required
+@flamenco_project_view(extension_props=False)
+def set_task_status(project, task_id):
+    from flask_login import current_user
+
+    new_status = request.form['status']
+    if new_status not in ALLOWED_TASK_STATUSES_FROM_WEB:
+        log.warning('User %s tried to set status of task %s to disallowed status "%s"; denied.',
+                    current_user.objectid, task_id, new_status)
+        raise wz_exceptions.UnprocessableEntity('Status "%s" not allowed' % new_status)
+
+    log.info('User %s set status of task %s to "%s"', current_user.objectid, task_id, new_status)
+    current_flamenco.task_manager.web_set_task_status(task_id, new_status)
+
+    return '', 204
 
 
 @perproject_blueprint.route('/<task_id>/log')
diff --git a/packages/flamenco/flamenco/tasks/sdk.py b/packages/flamenco/flamenco/tasks/sdk.py
index 270ff645..2c8bf68f 100644
--- a/packages/flamenco/flamenco/tasks/sdk.py
+++ b/packages/flamenco/flamenco/tasks/sdk.py
@@ -1,8 +1,9 @@
 from pillarsdk.resource import List
 from pillarsdk.resource import Find
+from pillarsdk.resource import Patch
 
 
-class Task(List, Find):
+class Task(List, Find, Patch):
     """Task class wrapping the REST nodes endpoint
     """
     path = 'flamenco/tasks'
diff --git a/packages/flamenco/src/scripts/tutti/10_tasks.js b/packages/flamenco/src/scripts/tutti/10_tasks.js
index 7c8e50f7..9e6e5953 100644
--- a/packages/flamenco/src/scripts/tutti/10_tasks.js
+++ b/packages/flamenco/src/scripts/tutti/10_tasks.js
@@ -157,7 +157,7 @@ $(function() {
  */
 function setJobStatus(job_id, new_status) {
     if (typeof job_id === 'undefined' || typeof new_status === 'undefined') {
-        if (console) console.log("cancelJob(" + job_id + ", " + new_status + ") called");
+        if (console) console.log("setJobStatus(" + job_id + ", " + new_status + ") called");
         return;
     }
 
@@ -186,3 +186,39 @@ function setJobStatus(job_id, new_status) {
         $('#job-action-panel .action-result-panel').html(show_html);
     });
 }
+
+/**
+ * Request cancellation or re-queueing of the given task ID.
+ */
+function setTaskStatus(task_id, new_status) {
+    if (typeof task_id === 'undefined' || typeof new_status === 'undefined') {
+        if (console) console.log("setTaskStatus(" + task_id + ", " + new_status + ") called");
+        return;
+    }
+
+    project_url = ProjectUtils.projectUrl();
+    return $.post('/flamenco/' + project_url + '/tasks/' + task_id + '/set-status', {status: new_status})
+    .done(function(data) {
+        if(console) console.log('Job set-status request OK');
+        // Reload the entire page, since both the view-embed and the task list need refreshing.
+        location.reload(true);
+    })
+    .fail(function(xhr) {
+        if (console) {
+            console.log('Error setting task status');
+            console.log('XHR:', xhr);
+        }
+
+        statusBarSet('error', 'Error requesting task status change', 'pi-error');
+
+        var show_html;
+        if (xhr.status) {
+            show_html = xhr.responseText;
+        } else {
+            show_html = $('<p>').addClass('text-danger').text(
+              'Setting task status failed. There possibly was an error connecting to the server. ' +
+              'Please check your network connection and try again.');
+        }
+        $('#task-action-panel .action-result-panel').html(show_html);
+    });
+}
diff --git a/packages/flamenco/src/templates/flamenco/tasks/view_task_embed.jade b/packages/flamenco/src/templates/flamenco/tasks/view_task_embed.jade
index 23b333df..fd263c79 100644
--- a/packages/flamenco/src/templates/flamenco/tasks/view_task_embed.jade
+++ b/packages/flamenco/src/templates/flamenco/tasks/view_task_embed.jade
@@ -28,17 +28,17 @@
 		.table.item-properties
 			.table-body
 				.table-row
-					.table-cell Belongs to job
+					.table-cell Belongs to task
 					.table-cell
 						a(
-							href="{{ url_for('flamenco.jobs.perproject.view_job', project_url=project.url, job_id=task.job) }}"
-							data-job-id="{{ task.job }}",
-							class="job-link"
+							href="{{ url_for('flamenco.tasks.perproject.view_task', project_url=project.url, task_id=task._id) }}"
+							data-task-id="{{ task._id }}",
+							class="task-link"
 						)
-							| {{ task.job }}
+							| {{ task.task }}
 				.table-row
-					.table-cell Job Type
-					.table-cell {{ task.job_type | undertitle }}
+					.table-cell task Type
+					.table-cell {{ task.task_type | undertitle }}
 				.table-row.properties-status.js-help(
 					data-url="{{ url_for('flamenco.help', project_url=project.url) }}")
 					.table-cell Status
@@ -57,6 +57,14 @@
 					.table-cell Worker
 					.table-cell {{ task.worker }}
 
+	#task-action-panel
+		| {% if can_requeue_task %}
+		button.btn.btn-success.requeue-task(onclick="setTaskStatus('{{ task._id }}', 'queued')")
+			i.pi-refresh
+			| Re-queue task
+		| {% endif %}
+		.action-result-panel
+
 #item-view-feed
 	#activities
 	#comments-embed
-- 
GitLab