Exam System
Timed exams run inside isolated LXD containers. Bash verification scripts auto-grade each task immediately on submission.
Data Models
Exam
├── title, description
├── container_image (e.g. "platform-exam-linux-basics")
├── time_limit_minutes
├── passing_score
├── module_id
└── ExamTask[]
├── title, description
├── points
└── verification_script (bash)
ExamAttempt
├── exam_id, user_id
├── container_name
├── started_at, submitted_at
├── score, max_score, passed
└── ExamTaskResult[]
├── task_id
├── passed, points_earned
└── output (script stdout)
Exam Lifecycle
1. Student Starts Exam
POST /api/exams/{id}/start
- Validates no active attempt exists
- Creates LXD container from exam's
container_image(blocking → wrapped inasyncio.to_thread) - Saves container to
Containertable - Creates
ExamAttemptrecord with statusin_progress - Returns attempt ID + container name
2. Student Works in Terminal
The exam page shows a WebSocket terminal connected to the exam container. Students run Linux commands to complete each task.
3. Student Submits
POST /api/exams/{id}/submit
- Looks up the active
ExamAttempt - For each
ExamTask, runs theverification_scriptvia SSH in the container - Records
PASS/FAIL+ stdout inExamTaskResult - Calculates total score, marks attempt
passedif score ≥passing_score - Deletes the LXD container
- Returns full results
Verification Scripts
A bash script that prints PASS or FAIL as its last line:
#!/bin/bash
# Check if /home/student/projects directory exists
if [ -d /home/student/projects ]; then
echo "Directory found"
echo "PASS"
else
echo "Directory not found"
echo "FAIL"
fi
Contract:
- Last line must be exactly
PASSorFAIL - Must complete within 30 seconds
- Can emit diagnostic output before the verdict
- Script is base64-encoded before SSH execution (prevents injection)
Container Naming
exam-{exam_id}-user-{user_id}-{unix_timestamp}
# e.g.: exam-3-user-12-1769968425
One exam container per active attempt. Deleted on submit or abandon.
Admin — Creating an Exam
Via admin panel or API:
# Create exam
POST /api/admin/exams
{
"title": "Linux File System Basics",
"container_image": "platform-exam-linux-basics",
"time_limit_minutes": 45,
"passing_score": 70,
"module_id": 1
}
# Add task
POST /api/admin/exams/{id}/tasks
{
"title": "Create the projects directory",
"description": "Create /home/student/projects directory",
"points": 25,
"verification_script": "#!/bin/bash\n[ -d /home/student/projects ] && echo PASS || echo FAIL"
}
LXD Exam Images
Exam images are custom LXD images with SSH pre-installed and any required tools/packages. See LXD Architecture for how to build them.
Built-in exam images:
| Image alias | Based on | Includes |
|---|---|---|
platform-exam-linux-basics | ubuntu/22.04 | Basic Linux tools |
platform-exam-linux-services | ubuntu/22.04 | nginx, systemd tools |
Troubleshooting
Exam container not created
make lxd-list # Confirm LXD is reachable
docker-compose logs backend | grep "exam"
Verification script always fails
- Test the script manually:
ssh root@65.109.236.163 "lxc exec {container} -- bash -c '...script...'" - Check script permissions inside the container
- Ensure last line is exactly
PASSorFAIL(no trailing spaces/newlines)
Score not updating after submit
Check ExamAttempt.status in DB — if still in_progress, submission may have failed mid-way.
make db-shell
SELECT * FROM exam_attempts WHERE status = 'in_progress' ORDER BY started_at DESC LIMIT 5;