init testarena backend service
This commit is contained in:
44
asf-pc-server/testarena_pc_backend/deploy.sh
Normal file
44
asf-pc-server/testarena_pc_backend/deploy.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TestArena Deployment Script
|
||||
# Run this script with sudo: sudo ./deploy.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting TestArena Deployment..."
|
||||
|
||||
# 1. Install Dependencies
|
||||
echo "📦 Installing dependencies..."
|
||||
apt-get update
|
||||
apt-get install -y nginx python3-pip python3-venv
|
||||
|
||||
# 2. Set up Python Virtual Environment
|
||||
echo "🐍 Setting up Python environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install fastapi uvicorn sqlalchemy
|
||||
|
||||
# 3. Configure Nginx
|
||||
echo "🌐 Configuring Nginx..."
|
||||
cp nginx/testarena.conf /etc/nginx/sites-available/testarena
|
||||
ln -sf /etc/nginx/sites-available/testarena /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# 4. Create Data Directory
|
||||
echo "📁 Creating data directory..."
|
||||
mkdir -p /home/asf/testarena
|
||||
chown -R asf:asf /home/asf/testarena
|
||||
chmod -R 755 /home/asf/testarena
|
||||
|
||||
# 5. Restart Nginx
|
||||
echo "🔄 Restarting Nginx..."
|
||||
nginx -t
|
||||
systemctl restart nginx
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
echo "--------------------------------------------------"
|
||||
echo "Dashboard: http://asf-server.duckdns.org:8080/"
|
||||
echo "Results: http://asf-server.duckdns.org:8080/results/"
|
||||
echo "--------------------------------------------------"
|
||||
echo "To start the app: source venv/bin/activate && uvicorn testarena_app.main:app --host 0.0.0.0 --port 8000"
|
||||
echo "To start the worker: source venv/bin/activate && python3 -m testarena_app.worker"
|
||||
80
asf-pc-server/testarena_pc_backend/deployment_guide.md
Normal file
80
asf-pc-server/testarena_pc_backend/deployment_guide.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# TestArena Deployment & Testing Guide
|
||||
|
||||
This guide explains how to deploy and test the TestArena backend application on your Ubuntu Server.
|
||||
|
||||
## 🚀 Deployment Steps
|
||||
|
||||
### 1. Clone the Repository
|
||||
Ensure you have the code on your server in a directory like `/home/asf/testarena_pc_backend`.
|
||||
|
||||
### 2. Run the Deployment Script
|
||||
The deployment script automates Nginx configuration and dependency installation.
|
||||
```bash
|
||||
sudo chmod +x deploy.sh
|
||||
sudo ./deploy.sh
|
||||
```
|
||||
|
||||
### 3. Start the Application Services
|
||||
You should run these in the background or using a process manager like `pm2` or `systemd`.
|
||||
|
||||
**Start the API Server:**
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
uvicorn testarena_app.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
**Start the Background Worker:**
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python3 -m testarena_app.worker
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing the System
|
||||
|
||||
### 1. Verify Dashboard Access
|
||||
Open your browser and navigate to:
|
||||
`http://asf-server.duckdns.org:8080/`
|
||||
You should see the modern, colorful TestArena dashboard.
|
||||
|
||||
### 2. Verify Results Browsing
|
||||
Navigate to:
|
||||
`http://asf-server.duckdns.org:8080/results/`
|
||||
You should see an automatic directory listing of `/home/asf/testarena/`.
|
||||
|
||||
### 3. Test the Queue API
|
||||
Run the following `curl` command to queue a test task:
|
||||
```bash
|
||||
curl -X POST http://asf-server.duckdns.org:8080/api/queue \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"test_queue_001": [
|
||||
"staging",
|
||||
{
|
||||
"task_1": "/home/asf/scenarios/test1.py",
|
||||
"task_2": "/home/asf/scenarios/test2.py"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Verify Worker Execution
|
||||
- Check the dashboard; you should see the new queue appear and its status change from `Waiting` to `Running` and then `Finished`.
|
||||
- Check the filesystem:
|
||||
```bash
|
||||
ls -R /home/asf/testarena/test_queue_001
|
||||
```
|
||||
You should see `queue_status.json` and any results generated by `tpf_execution.py`.
|
||||
|
||||
### 5. Test Abortion
|
||||
Queue another task and click the **Abort** button on the dashboard. Verify that the status changes to `Aborted` in both the dashboard and the `queue_status.json` file.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
- **Nginx Errors**: Check logs with `sudo tail -f /var/log/nginx/error.log`.
|
||||
- **FastAPI Errors**: Check the terminal where `uvicorn` is running.
|
||||
- **Permission Issues**: Ensure `/home/asf/testarena` is writable by the user running the app.
|
||||
- **Port 8080 Blocked**: Ensure your firewall (ufw) allows traffic on port 8080: `sudo ufw allow 8080`.
|
||||
58
asf-pc-server/testarena_pc_backend/nginx/testarena.conf
Normal file
58
asf-pc-server/testarena_pc_backend/nginx/testarena.conf
Normal file
@@ -0,0 +1,58 @@
|
||||
# TestArena Nginx Configuration
|
||||
# This file should be placed in /etc/nginx/sites-available/testarena
|
||||
# and symlinked to /etc/nginx/sites-enabled/testarena
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
# Security: Prevent directory traversal and restrict symlinks
|
||||
disable_symlinks on;
|
||||
|
||||
# Root directory for the results (autoindex)
|
||||
location /results/ {
|
||||
alias /home/asf/testarena/;
|
||||
|
||||
# Enable autoindex with requested features
|
||||
autoindex on;
|
||||
autoindex_exact_size off; # Human-readable sizes
|
||||
autoindex_localtime on; # Local time
|
||||
|
||||
# Read-only access
|
||||
limit_except GET {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Prevent execution of scripts
|
||||
location ~* \.(php|pl|py|sh|cgi)$ {
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
|
||||
# Proxy requests to the FastAPI application
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support (if needed in future)
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# Custom error pages
|
||||
error_page 404 /404.html;
|
||||
location = /404.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
}
|
||||
16
asf-pc-server/testarena_pc_backend/testarena_app/database.py
Normal file
16
asf-pc-server/testarena_pc_backend/testarena_app/database.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import create_all_engines, create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import os
|
||||
|
||||
# Using SQLite for simplicity as requested
|
||||
DATABASE_URL = "sqlite:///d:/ASF - course/ASF_01/ASF_tools/asf-pc-server/testarena_pc_backend/testarena_app/testarena.db"
|
||||
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
139
asf-pc-server/testarena_pc_backend/testarena_app/main.py
Normal file
139
asf-pc-server/testarena_pc_backend/testarena_app/main.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
from typing import Dict, List
|
||||
from . import models, database
|
||||
|
||||
app = FastAPI(title="TestArena API")
|
||||
|
||||
# Mount static files
|
||||
static_dir = os.path.join(os.path.dirname(__file__), "static")
|
||||
os.makedirs(static_dir, exist_ok=True)
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
# Base directory for data as requested
|
||||
BASE_DATA_DIR = "/home/asf/testarena"
|
||||
# For local development on Windows, we might need to adjust this,
|
||||
# but I'll stick to the user's requirement for the final version.
|
||||
if os.name == 'nt':
|
||||
BASE_DATA_DIR = "d:/ASF - course/ASF_01/ASF_tools/asf-pc-server/testarena_pc_backend/testarena_data"
|
||||
|
||||
# Ensure base directory exists
|
||||
os.makedirs(BASE_DATA_DIR, exist_ok=True)
|
||||
|
||||
# Initialize database
|
||||
models.Base.metadata.create_all(bind=database.engine)
|
||||
|
||||
@app.post("/api/queue")
|
||||
async def queue_task(payload: Dict, db: Session = Depends(database.get_db)):
|
||||
"""
|
||||
Input json contain {<queue_ID> :[environment, "<TASK_ID>" : "<path to scenario>],}
|
||||
"""
|
||||
try:
|
||||
queue_id = list(payload.keys())[0]
|
||||
data = payload[queue_id]
|
||||
environment = data[0]
|
||||
tasks_data = data[1] # This is a dict {"TASK_ID": "path"}
|
||||
|
||||
# 1. Create folder
|
||||
queue_dir = os.path.join(BASE_DATA_DIR, queue_id)
|
||||
os.makedirs(queue_dir, exist_ok=True)
|
||||
|
||||
# 2. Create queue_status.json
|
||||
status_file = os.path.join(queue_dir, "queue_status.json")
|
||||
queue_status = {
|
||||
"queue_id": queue_id,
|
||||
"status": "Waiting",
|
||||
"tasks": {}
|
||||
}
|
||||
|
||||
# 3. Save to database and prepare status file
|
||||
new_queue = models.Queue(id=queue_id, environment=environment, status="Waiting")
|
||||
db.add(new_queue)
|
||||
|
||||
for task_id, scenario_path in tasks_data.items():
|
||||
new_task = models.Task(id=task_id, queue_id=queue_id, scenario_path=scenario_path, status="Waiting")
|
||||
db.add(new_task)
|
||||
queue_status["tasks"][task_id] = "Waiting"
|
||||
|
||||
with open(status_file, 'w') as f:
|
||||
json.dump(queue_status, f, indent=4)
|
||||
|
||||
db.commit()
|
||||
return {"status": "Queue OK", "queue_id": queue_id}
|
||||
except Exception as e:
|
||||
return {"status": "Error", "message": str(e)}
|
||||
|
||||
@app.get("/api/status/{id}")
|
||||
async def get_status(id: str, db: Session = Depends(database.get_db)):
|
||||
# Check if it's a queue ID
|
||||
queue = db.query(models.Queue).filter(models.Queue.id == id).first()
|
||||
if queue:
|
||||
return {"id": id, "type": "queue", "status": queue.status}
|
||||
|
||||
# Check if it's a task ID
|
||||
task = db.query(models.Task).filter(models.Task.id == id).first()
|
||||
if task:
|
||||
return {"id": id, "type": "task", "status": task.status}
|
||||
|
||||
raise HTTPException(status_code=404, detail="ID not found")
|
||||
|
||||
@app.post("/api/abort/{id}")
|
||||
async def abort_task(id: str, db: Session = Depends(database.get_db)):
|
||||
# Abort queue
|
||||
queue = db.query(models.Queue).filter(models.Queue.id == id).first()
|
||||
if queue:
|
||||
queue.status = "Aborted"
|
||||
# Abort all tasks in queue
|
||||
tasks = db.query(models.Task).filter(models.Task.queue_id == id).all()
|
||||
for t in tasks:
|
||||
if t.status in ["Waiting", "Running"]:
|
||||
t.status = "Aborted"
|
||||
|
||||
# Update queue_status.json
|
||||
queue_dir = os.path.join(BASE_DATA_DIR, id)
|
||||
status_file = os.path.join(queue_dir, "queue_status.json")
|
||||
if os.path.exists(status_file):
|
||||
with open(status_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
data["status"] = "Aborted"
|
||||
for tid in data["tasks"]:
|
||||
if data["tasks"][tid] in ["Waiting", "Running"]:
|
||||
data["tasks"][tid] = "Aborted"
|
||||
with open(status_file, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
db.commit()
|
||||
return {"id": id, "status": "Aborted"}
|
||||
|
||||
# Abort single task
|
||||
task = db.query(models.Task).filter(models.Task.id == id).first()
|
||||
if task:
|
||||
task.status = "Aborted"
|
||||
# Update queue_status.json
|
||||
queue_dir = os.path.join(BASE_DATA_DIR, task.queue_id)
|
||||
status_file = os.path.join(queue_dir, "queue_status.json")
|
||||
if os.path.exists(status_file):
|
||||
with open(status_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
data["tasks"][id] = "Aborted"
|
||||
with open(status_file, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
db.commit()
|
||||
return {"id": id, "status": "Aborted"}
|
||||
|
||||
raise HTTPException(status_code=404, detail="ID not found")
|
||||
|
||||
@app.get("/api/queues")
|
||||
async def list_queues(db: Session = Depends(database.get_db)):
|
||||
queues = db.query(models.Queue).order_by(models.Queue.created_at.desc()).all()
|
||||
return queues
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return FileResponse(os.path.join(static_dir, "index.html"))
|
||||
27
asf-pc-server/testarena_pc_backend/testarena_app/models.py
Normal file
27
asf-pc-server/testarena_pc_backend/testarena_app/models.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Queue(Base):
|
||||
__tablename__ = "queues"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
status = Column(String, default="Waiting") # Finished, Waiting, Running, Aborted
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
environment = Column(String)
|
||||
|
||||
tasks = relationship("Task", back_populates="queue", cascade="all, delete-orphan")
|
||||
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
queue_id = Column(String, ForeignKey("queues.id"))
|
||||
scenario_path = Column(String)
|
||||
status = Column(String, default="Waiting") # Finished, Waiting, Running, Aborted
|
||||
result = Column(JSON, nullable=True)
|
||||
|
||||
queue = relationship("Queue", back_populates="tasks")
|
||||
@@ -0,0 +1,407 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TestArena | Modern Dashboard</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-glow: rgba(99, 102, 241, 0.5);
|
||||
--secondary: #ec4899;
|
||||
--accent: #8b5cf6;
|
||||
--bg: #0f172a;
|
||||
--card-bg: rgba(30, 41, 59, 0.7);
|
||||
--text: #f8fafc;
|
||||
--text-muted: #94a3b8;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--glass: rgba(255, 255, 255, 0.05);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
background: radial-gradient(circle at top right, #1e1b4b, #0f172a);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Decorative blobs */
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: var(--primary-glow);
|
||||
filter: blur(100px);
|
||||
border-radius: 50%;
|
||||
z-index: -1;
|
||||
animation: move 20s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from {
|
||||
transform: translate(-10%, -10%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate(20%, 20%);
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 1.5rem;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(to right, var(--primary), var(--secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--warning);
|
||||
box-shadow: 0 0 10px var(--warning);
|
||||
}
|
||||
|
||||
.dot.online {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 10px var(--success);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 1.5rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.75rem;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
border-radius: 1rem 0 0 1rem;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-radius: 0 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #818cf8;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.status-aborted {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.btn-abort {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-abort:hover {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #020617;
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: var(--primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-msg {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--glass-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="blob"></div>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||
</svg>
|
||||
TestArena
|
||||
</div>
|
||||
<nav class="nav-links">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/results/" target="_blank">Browse Results</a>
|
||||
</nav>
|
||||
<div id="connection-status" class="status-badge">
|
||||
<div class="dot"></div>
|
||||
<span>Connecting...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</svg>
|
||||
Queue Monitor
|
||||
</h2>
|
||||
<table id="queue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Queue ID</th>
|
||||
<th>Environment</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- Dynamic content -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
Live System Logs
|
||||
</h2>
|
||||
<div id="logs" class="log-container">
|
||||
<div class="log-entry">
|
||||
<span class="log-time">23:34:52</span>
|
||||
<span class="log-msg">System initialized. Waiting for connection...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/queues');
|
||||
const queues = await response.json();
|
||||
|
||||
const tbody = document.querySelector('#queue-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
queues.forEach(q => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td style="font-weight: 600;">${q.id}</td>
|
||||
<td><span style="opacity: 0.8;">${q.environment}</span></td>
|
||||
<td><span class="status-pill status-${q.status.toLowerCase()}">${q.status}</span></td>
|
||||
<td>
|
||||
<button class="btn-abort" onclick="abortQueue('${q.id}')">Abort</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
const badge = document.getElementById('connection-status');
|
||||
badge.querySelector('.dot').classList.add('online');
|
||||
badge.querySelector('span').textContent = 'System Online';
|
||||
} catch (e) {
|
||||
const badge = document.getElementById('connection-status');
|
||||
badge.querySelector('.dot').classList.remove('online');
|
||||
badge.querySelector('span').textContent = 'Connection Lost';
|
||||
}
|
||||
}
|
||||
|
||||
async function abortQueue(id) {
|
||||
if (confirm(`Are you sure you want to abort queue ${id}?`)) {
|
||||
try {
|
||||
await fetch(`/api/abort/${id}`, { method: 'POST' });
|
||||
addLog(`Aborted queue: ${id}`, 'danger');
|
||||
fetchStatus();
|
||||
} catch (e) {
|
||||
addLog(`Failed to abort queue: ${id}`, 'danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addLog(msg, type = 'info') {
|
||||
const logs = document.getElementById('logs');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
const time = new Date().toLocaleTimeString([], { hour12: false });
|
||||
entry.innerHTML = `
|
||||
<span class="log-time">${time}</span>
|
||||
<span class="log-msg">${msg}</span>
|
||||
`;
|
||||
logs.appendChild(entry);
|
||||
logs.scrollTop = logs.scrollHeight;
|
||||
}
|
||||
|
||||
// Initial fetch and poll
|
||||
fetchStatus();
|
||||
setInterval(fetchStatus, 3000);
|
||||
|
||||
// Simulate some system logs
|
||||
setTimeout(() => addLog("Database connection established."), 1000);
|
||||
setTimeout(() => addLog("Background worker is polling for tasks..."), 2000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
98
asf-pc-server/testarena_pc_backend/testarena_app/worker.py
Normal file
98
asf-pc-server/testarena_pc_backend/testarena_app/worker.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import time
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, database
|
||||
|
||||
# Base directory for data
|
||||
BASE_DATA_DIR = "/home/asf/testarena"
|
||||
if os.name == 'nt':
|
||||
BASE_DATA_DIR = "d:/ASF - course/ASF_01/ASF_tools/asf-pc-server/testarena_pc_backend/testarena_data"
|
||||
|
||||
def update_json_status(queue_id, task_id, status, result=None):
|
||||
queue_dir = os.path.join(BASE_DATA_DIR, queue_id)
|
||||
status_file = os.path.join(queue_dir, "queue_status.json")
|
||||
if os.path.exists(status_file):
|
||||
with open(status_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
if task_id:
|
||||
data["tasks"][task_id] = status
|
||||
else:
|
||||
data["status"] = status
|
||||
|
||||
if result:
|
||||
data["results"] = data.get("results", {})
|
||||
data["results"][task_id] = result
|
||||
|
||||
with open(status_file, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
def run_worker():
|
||||
print("Worker started...")
|
||||
while True:
|
||||
db = database.SessionLocal()
|
||||
try:
|
||||
# Get next waiting queue
|
||||
queue = db.query(models.Queue).filter(models.Queue.status == "Waiting").order_by(models.Queue.created_at).first()
|
||||
|
||||
if queue:
|
||||
print(f"Processing queue: {queue.id}")
|
||||
queue.status = "Running"
|
||||
update_json_status(queue.id, None, "Running")
|
||||
db.commit()
|
||||
|
||||
tasks = db.query(models.Task).filter(models.Task.queue_id == queue.id, models.Task.status == "Waiting").all()
|
||||
|
||||
for task in tasks:
|
||||
# Check if queue was aborted mid-way
|
||||
db.refresh(queue)
|
||||
if queue.status == "Aborted":
|
||||
break
|
||||
|
||||
print(f"Running task: {task.id}")
|
||||
task.status = "Running"
|
||||
update_json_status(queue.id, task.id, "Running")
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
# Run tpf_execution.py [queue_id, scenario_path, task_id]
|
||||
# Assuming tpf_execution.py is in the parent directory or accessible
|
||||
script_path = "tpf_execution.py"
|
||||
# For testing, let's assume it's in the same dir as the app or parent
|
||||
cmd = ["python", script_path, queue.id, task.scenario_path, task.id]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
# Parse result if it returns json
|
||||
try:
|
||||
execution_result = json.loads(result.stdout)
|
||||
except:
|
||||
execution_result = {"output": result.stdout, "error": result.stderr}
|
||||
|
||||
task.status = "Finished"
|
||||
task.result = execution_result
|
||||
update_json_status(queue.id, task.id, "Finished", execution_result)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running task {task.id}: {e}")
|
||||
task.status = "Error"
|
||||
update_json_status(queue.id, task.id, "Error")
|
||||
|
||||
db.commit()
|
||||
|
||||
if queue.status != "Aborted":
|
||||
queue.status = "Finished"
|
||||
update_json_status(queue.id, None, "Finished")
|
||||
db.commit()
|
||||
|
||||
time.sleep(5) # Poll every 5 seconds
|
||||
except Exception as e:
|
||||
print(f"Worker error: {e}")
|
||||
time.sleep(10)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_worker()
|
||||
32
asf-pc-server/testarena_pc_backend/tpf_execution.py
Normal file
32
asf-pc-server/testarena_pc_backend/tpf_execution.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python tpf_execution.py <queue_id> <scenario_path> <task_id>")
|
||||
sys.exit(1)
|
||||
|
||||
queue_id = sys.argv[1]
|
||||
scenario_path = sys.argv[2]
|
||||
task_id = sys.argv[3]
|
||||
|
||||
print(f"Starting execution for Task: {task_id} in Queue: {queue_id}")
|
||||
print(f"Scenario: {scenario_path}")
|
||||
|
||||
# Simulate work
|
||||
duration = random.randint(2, 5)
|
||||
time.sleep(duration)
|
||||
|
||||
result = {
|
||||
"task_id": task_id,
|
||||
"status": "Success",
|
||||
"duration": duration,
|
||||
"details": f"Scenario {scenario_path} executed successfully."
|
||||
}
|
||||
|
||||
print(json.dumps(result))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user