Skip to main content

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

  1. Validates no active attempt exists
  2. Creates LXD container from exam's container_image (blocking → wrapped in asyncio.to_thread)
  3. Saves container to Container table
  4. Creates ExamAttempt record with status in_progress
  5. 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

  1. Looks up the active ExamAttempt
  2. For each ExamTask, runs the verification_script via SSH in the container
  3. Records PASS/FAIL + stdout in ExamTaskResult
  4. Calculates total score, marks attempt passed if score ≥ passing_score
  5. Deletes the LXD container
  6. 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 PASS or FAIL
  • 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 aliasBased onIncludes
platform-exam-linux-basicsubuntu/22.04Basic Linux tools
platform-exam-linux-servicesubuntu/22.04nginx, systemd tools

Troubleshooting

Exam container not created

make lxd-list   # Confirm LXD is reachable
docker-compose logs backend | grep "exam"

Verification script always fails

  1. Test the script manually: ssh root@65.109.236.163 "lxc exec {container} -- bash -c '...script...'"
  2. Check script permissions inside the container
  3. Ensure last line is exactly PASS or FAIL (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;