initial
This commit is contained in:
0
tasklogger/__init__.py
Normal file
0
tasklogger/__init__.py
Normal file
3
tasklogger/admin.py
Normal file
3
tasklogger/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
tasklogger/apps.py
Normal file
6
tasklogger/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TaskloggerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'tasklogger'
|
||||
0
tasklogger/migrations/__init__.py
Normal file
0
tasklogger/migrations/__init__.py
Normal file
88
tasklogger/models.py
Normal file
88
tasklogger/models.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from django.db import models
|
||||
from django_middleware_global_request.middleware import get_request
|
||||
|
||||
from lib.db import DateAwareMixin
|
||||
|
||||
|
||||
class TaskFactory:
|
||||
def __new__(self, task_uuid=None, request=None, *args, **kwargs):
|
||||
request = request or get_request()
|
||||
if not request:
|
||||
logging.error('No request found while creating task')
|
||||
else:
|
||||
session_uuid = request.session.get('current_task_uuid')
|
||||
form_uuid = request.POST.get('task_uuid', request.GET.get('task_uuid'))
|
||||
if form_uuid:
|
||||
task_uuid = task_uuid or form_uuid
|
||||
request.session['current_task_uuid'] = task_uuid
|
||||
elif session_uuid:
|
||||
task_uuid = task_uuid or session_uuid
|
||||
task_uuid = task_uuid or kwargs.pop('uuid', None)
|
||||
if not task_uuid:
|
||||
task = Task.objects.create()
|
||||
else:
|
||||
task, _created = Task.objects.get_or_create(uuid=task_uuid)
|
||||
if request:
|
||||
request.session['current_task_uuid'] = str(task.uuid)
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def reset_current_task(request=None):
|
||||
request = request or get_request()
|
||||
if request:
|
||||
request.session.pop('current_task_uuid', None)
|
||||
return True
|
||||
|
||||
|
||||
class Task(DateAwareMixin):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
|
||||
proxmox_upid = models.CharField(max_length=150, null=True, blank=True)
|
||||
status = models.CharField(max_length=150, null=True, blank=True, default='running')
|
||||
|
||||
current_task = None
|
||||
|
||||
def __del__(self):
|
||||
self.unset_as_current()
|
||||
|
||||
def unset_as_current(self, request=None):
|
||||
request = request or get_request()
|
||||
if request:
|
||||
uuid = request.session.get('current_task_uuid')
|
||||
if uuid == self.uuid:
|
||||
request.session.pop('current_task_uuid', None)
|
||||
return True
|
||||
else:
|
||||
logging.warning(f'Tried to unset {self.uuid} as current, but it is not the current task ({uuid}')
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return str(self.uuid)
|
||||
|
||||
def add_entry(self, message):
|
||||
TaskEntry.objects.create(task=self, message=message)
|
||||
return self
|
||||
|
||||
def task_is_stopped(self, result):
|
||||
return result.get('status', 'running').lower() in ('ok', 'error', 'stopped')
|
||||
|
||||
def wrap_proxmox_function(self, proxmox_function, *args, **kwargs):
|
||||
from lib.proxmox import Proxmox
|
||||
self.proxmox_upid = proxmox_function(*args, **kwargs)
|
||||
self.save()
|
||||
if not self.proxmox_upid:
|
||||
logging.warning(f'Could not get upid for {proxmox_function} - {args} {kwargs}')
|
||||
return self
|
||||
while True:
|
||||
with Proxmox() as pm:
|
||||
for message in pm.get_task_status(taskhash=self.proxmox_upid):
|
||||
self.add_entry(message)
|
||||
if self.task_is_stopped(message):
|
||||
return self
|
||||
|
||||
|
||||
class TaskEntry(DateAwareMixin):
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='entries')
|
||||
message = models.JSONField(null=True, blank=True)
|
||||
83
tasklogger/templates/tasklogger/task_detail.html
Normal file
83
tasklogger/templates/tasklogger/task_detail.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Task Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-10">
|
||||
<h4>Task Details</h4>
|
||||
<small class="text-muted">Task UUID: {{ task.uuid }}</small>
|
||||
</div>
|
||||
<div class="col-12 col-md-2 text-end">
|
||||
<a href="{% url 'tasklogger:task_list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Tasks
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3"><strong>Status:</strong></div>
|
||||
<div class="col-md-9">
|
||||
<span class="badge {% if task.status == 'completed' %}bg-success{% elif task.status == 'error' %}bg-danger{% else %}bg-primary{% endif %}">
|
||||
{{ task.status|default:"running" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3"><strong>Created:</strong></div>
|
||||
<div class="col-md-9">{{ task.created_at }}</div>
|
||||
</div>
|
||||
{% if task.proxmox_upid %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3"><strong>Proxmox UPID:</strong></div>
|
||||
<div class="col-md-9"><code>{{ task.proxmox_upid }}</code></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>Task Log Entries</h5>
|
||||
{% if entries %}
|
||||
<div class="log-container" style="max-height: 500px; overflow-y: auto; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px;">
|
||||
{% for entry in entries %}
|
||||
<div class="log-entry mb-2" style="padding: 8px; border-left: 3px solid #007bff; background: white; border-radius: 4px;">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="log-content flex-grow-1">
|
||||
{% if entry.message %}
|
||||
{% if entry.message|length > 200 %}
|
||||
<pre style="font-size: 13px; margin: 0; white-space: pre-wrap;">{{ entry.message }}</pre>
|
||||
{% else %}
|
||||
<span style="font-family: 'Courier New', monospace; font-size: 13px;">{{ entry.message }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em class="text-muted">No message</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="log-timestamp text-muted" style="font-size: 11px; margin-left: 15px; white-space: nowrap;">
|
||||
{{ entry.created_at|date:"H:i:s" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No log entries found for this task.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.log-container {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
76
tasklogger/templates/tasklogger/task_list.html
Normal file
76
tasklogger/templates/tasklogger/task_list.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% extends 'frontend/base.html' %}
|
||||
|
||||
{% block title %} - Task List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4>Recent Tasks</h4>
|
||||
<small class="text-muted">Last 50 tasks</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if tasks %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>UUID</th>
|
||||
<th>Status</th>
|
||||
<th>Proxmox UPID</th>
|
||||
<th>Created</th>
|
||||
<th>Latest Entry</th>
|
||||
<th style="width: 100px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for task in tasks %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>
|
||||
<code style="font-size: 11px;">{{ task.uuid|truncatechars:13 }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if task.status == 'completed' %}bg-success{% elif task.status == 'error' %}bg-danger{% else %}bg-primary{% endif %}">
|
||||
{{ task.status|default:"running" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if task.proxmox_upid %}
|
||||
<code style="font-size: 11px;">{{ task.proxmox_upid|truncatechars:20 }}</code>
|
||||
{% else %}
|
||||
<em class="text-muted">-</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small>{{ task.created_at|date:"d.m.Y H:i" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% with latest_entry=task.entries.last %}
|
||||
{% if latest_entry %}
|
||||
<small style="font-family: 'Courier New', monospace; font-size: 11px;">
|
||||
{{ latest_entry.message|truncatechars:50 }}
|
||||
</small>
|
||||
{% else %}
|
||||
<em class="text-muted">No entries</em>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'tasklogger:task_detail' task.uuid %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> No tasks found.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
3
tasklogger/tests.py
Normal file
3
tasklogger/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
tasklogger/urls.py
Normal file
10
tasklogger/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'tasklogger'
|
||||
|
||||
urlpatterns = [
|
||||
path('api/task-status/', views.task_status, name='task_status'),
|
||||
path('task/<uuid:task_uuid>/', views.task_detail, name='task_detail'),
|
||||
path('tasks/', views.task_list, name='task_list'),
|
||||
]
|
||||
71
tasklogger/views.py
Normal file
71
tasklogger/views.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from .models import Task
|
||||
|
||||
|
||||
def task_response(data, status=200):
|
||||
"""Helper function to return a task response"""
|
||||
return HttpResponse(json.dumps(data, default=str), status=status, content_type='application/json')
|
||||
|
||||
|
||||
@login_required
|
||||
def task_status(request):
|
||||
"""API endpoint to check task status"""
|
||||
task_uuid = request.GET.get('task_uuid')
|
||||
|
||||
if not task_uuid:
|
||||
return task_response({
|
||||
'status': 'error',
|
||||
'message': 'task_uuid parameter required'
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
task = Task.objects.get(uuid=task_uuid)
|
||||
|
||||
# Get latest entries
|
||||
latest_entries = list(task.entries.order_by('-created_at')[:10].values(
|
||||
'message', 'created_at'
|
||||
))
|
||||
|
||||
return task_response({
|
||||
'status': 'success',
|
||||
'task': {
|
||||
'uuid': str(task.uuid),
|
||||
'proxmox_upid': task.proxmox_upid,
|
||||
'status': task.status,
|
||||
'created_at': task.created_at.isoformat(),
|
||||
'entries': latest_entries
|
||||
}
|
||||
}, status=200)
|
||||
|
||||
except Task.DoesNotExist:
|
||||
return task_response({
|
||||
'status': 'error',
|
||||
'message': f'Task {task_uuid} not found'
|
||||
}, status=404)
|
||||
|
||||
|
||||
@login_required
|
||||
def task_detail(request, task_uuid):
|
||||
"""View to show task details and log entries"""
|
||||
task = get_object_or_404(Task, uuid=task_uuid)
|
||||
entries = task.entries.order_by('created_at')
|
||||
|
||||
return render(request, 'tasklogger/task_detail.html', {
|
||||
'task': task,
|
||||
'entries': entries
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def task_list(request):
|
||||
"""View to list recent tasks"""
|
||||
tasks = Task.objects.order_by('-created_at')[:50]
|
||||
|
||||
return render(request, 'tasklogger/task_list.html', {
|
||||
'tasks': tasks
|
||||
})
|
||||
Reference in New Issue
Block a user