Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
de2d4e3
docs: add SSH LocalForward help text to settings
saiful-semantic Mar 25, 2026
65e6bbe
feat(profiles): add LXD profile management feature
saiful-semantic Mar 25, 2026
ae6c1e7
feat(ui): add help links for container/VM type selection
saiful-semantic Mar 25, 2026
c306277
refactor(models): replace separate VM/Container defaults with unified…
saiful-semantic Mar 26, 2026
8cf8efd
feat(ui): populate profile dropdowns on form display
saiful-semantic Mar 26, 2026
998049a
feat(ui): initialize connection type fields on page load
saiful-semantic Mar 26, 2026
6717cef
refactor: extract classrooms and LXD profiles to dedicated page
saiful-semantic Mar 26, 2026
3625384
refactor: use classrooms for instance creation configuration
saiful-semantic Mar 26, 2026
85cd303
refactor: change default instance type from virtual-machine to container
saiful-semantic Mar 26, 2026
f70c532
refactor(ui): improve instance type labeling and dark mode support
saiful-semantic Mar 26, 2026
3a75984
refactor(cloud-init): separate VM and container cloud-init templates
saiful-semantic Mar 26, 2026
1c5a274
refactor(classrooms): move cloud-init and SSH template endpoints from…
saiful-semantic Mar 26, 2026
fd3ce7b
feat(ui): reorder cloud-init load buttons and add placeholder documen…
saiful-semantic Mar 26, 2026
51455c9
feat(ui): initialize classrooms tab content on DOM ready
saiful-semantic Mar 26, 2026
c4cc4fa
feat(ui): update LXD terminology to Instance in admin classrooms UI
saiful-semantic Mar 26, 2026
f548b9c
refactor(ui): restructure classrooms form layout for improved organiz…
saiful-semantic Mar 26, 2026
6dacc96
refactor(ui): improve form field spacing and cloud-init layout in cla…
saiful-semantic Mar 26, 2026
7a9a1b8
feat(bulk): add container density and overcommit support to bulk pref…
saiful-semantic Mar 26, 2026
fe8b865
feat(classrooms): make username a required field in classroom forms
saiful-semantic Mar 26, 2026
c1c99bb
feat(cloud-init): use LXD profiles for cloud-init configuration
saiful-semantic Mar 26, 2026
d62b51d
fix(ui): disable SSH config download for localhost IPs
saiful-semantic Mar 27, 2026
0c3ad44
fix(ui): improve disabled delete button UX for default LXD profile
saiful-semantic Mar 27, 2026
d45bd43
fix(classrooms): prevent deletion of LXD profiles in use by classrooms
saiful-semantic Mar 27, 2026
e319a9c
feat(ui): add delete confirmation modals for classroom and profile de…
saiful-semantic Mar 27, 2026
fc1bd5a
docs: add production deployment guide with systemd and update Caddyfile
saiful-semantic Mar 28, 2026
3a996b7
chore(changelog): document Sprint 2 classroom and container features
saiful-semantic Mar 28, 2026
c75f4a3
build(deps): pin dependency versions with ranges for production stabi…
saiful-semantic Mar 28, 2026
709b039
refactor(templates): centralize Jinja2 configuration into dedicated c…
saiful-semantic Mar 28, 2026
df93c5d
refactor(core): import templates from core.templates into main
saiful-semantic Mar 28, 2026
eb5b39b
docs(readme): escape $HOST and $PORT in ExecStart example
saiful-semantic Mar 28, 2026
e7f883a
feat(settings): include aliases in image description in get_available…
saiful-semantic Mar 28, 2026
8366297
refactor(routes): drop cloud_init extraction from lxd_profile
saiful-semantic Mar 28, 2026
9d0e407
feat(cloud_init): support profile-based and instance_type-based templ…
saiful-semantic Mar 28, 2026
04c2a67
chore(screenshots): update dashboard.png
saiful-semantic Mar 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **Classroom Management** - Create reusable configurations with predefined images, LXD profiles, and SSH templates
- **LXD Profile Management** - Full CRUD UI for managing LXD profiles with CPU, RAM, disk, and cloud-init settings
- **Container Support** - Bulk creation now supports both VMs and containers with density-aware resource checks
- **Over-commit Option** - Allow resource over-commitment for high-density container deployments
- **Instance Type Display** - Shows selected instance type (VM/Container) in creation forms
- **Help Links** - Added informational links for LXD container vs VM selection

### Changed
- **Refactored Settings** - Merged VM/Container defaults and SSH templates into unified Classroom model
- **Cloud-init Templates** - Separate default templates for VMs (with swap) and containers (with MOTD)
- **Pre-flight Checks** - Container density factor (4x) applied for more accurate resource estimation
- **Delete Confirmations** - Modal dialogs for deleting classrooms and profiles (consistent with dashboard)
- **Profile Protection** - Cannot delete LXD profiles in use by classrooms or the 'default' profile
- **Username Required** - Default username is now mandatory in classroom configuration

### Removed
- **Swap Setting** - Removed dedicated swap field; swap now configured exclusively via cloud-init templates
- **Standalone Settings** - VM defaults, container defaults, and connection templates tabs removed from Settings

### Security
- **Profile Deletion Protection** - Application-level foreign key constraint prevents deleting profiles in use


## [v0.2.0] - 2026-03-25

### Added
- Pattern-based bulk VM creation (e.g., `vm-{01-05}`)
- Pre-flight resource checks for bulk operations
Expand Down
4 changes: 1 addition & 3 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Prerequisites:
# 1. Install Caddy: https://caddyserver.com/docs/install
# 2. Point your domain to this server (DNS A record)
# 2. Point your domain to this server (DNS A record or CNAME pointing to this server's IP)
# 3. Ensure ports 80 and 443 are open
#
# Usage:
Expand All @@ -15,7 +15,6 @@ sandbox.example.com {
# Automatic HTTPS with Let's Encrypt
reverse_proxy localhost:8000

# Increase request body size for cloud-init ISO uploads
request_body {
max_size 1MB
}
Expand All @@ -34,7 +33,6 @@ sandbox.example.com {
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
Referrer-Policy "strict-origin-when-cross-origin"
}

Expand Down
59 changes: 50 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A web-based interface for managing LXD virtual machines and containers. Designed

## Features

- **Classroom Management** - Create reusable configurations with predefined images, LXD profiles, and SSH templates
- **Bulk VM Creation** - Create multiple VMs at once with pre-flight resource checks
- **One-Click Operations** - Start, stop, or delete all VMs in bulk
- **SSH ProxyJump** - Secure SSH access with auto-generated jump user and SSH configs
Expand All @@ -19,7 +20,7 @@ A web-based interface for managing LXD virtual machines and containers. Designed
## Requirements

- Linux host with [LXD installed](https://canonical.com/lxd/install) and configured (`lxd init`)
- Python 3.10+
- Python 3.10+ with `venv` module
- 50GB+ free disk space (depending on VM count)

## Pre-setup
Expand All @@ -35,35 +36,75 @@ This step will download the `ubuntu:24.04` image. The Sandbox app will only show
## Installation

```bash
git clone https://github.com/semanticlib/sandbox.git
cd /opt
sudo git clone https://github.com/semanticlib/sandbox.git
sudo chown -R $(whoami):$(whoami) sandbox
cd sandbox
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp env.example .env
SECRET_KEY=$(openssl rand -hex 32)
sed -i "s/^SECRET_KEY=.*/SECRET_KEY=$SECRET_KEY/" .env
# Update the LXD Host IP in HOST_SERVER_IP variable in .env file
uvicorn main:app
```

Also update the `HOST_SERVER_IP` variable in `.env` file with your LXD host IP.

**Test run**

```bash
python main.py
```

**Secure Access using SSH Tunnel**

Create an SSH Tunnel to access the app.
From your local machine, create an SSH Tunnel to access the app.

```bash
ssh -L 8000:localhost:8000 user@<host-ip>
```

Open `http://localhost:8000` in your browser
Open `http://localhost:8000` in your browser and proceed with the initial setup.

## Production Deployment (Systemd)

**Custom Port:**
To run the application in production using `systemd`, use the following commands:

To use a different port, set `PORT` in `.env`:
```bash
PORT=9000
export APP_DIR=/opt/sandbox # Adjust if you want to install in a different directory
export APP_USER=$(whoami) # Adjust if you want to run as a different user
sudo tee /etc/systemd/system/sandbox.service > /dev/null <<EOF
[Unit]
Description=Sandbox Manager Application
After=network.target

[Service]
Type=exec
User=${APP_USER}
Group=${APP_USER}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${APP_DIR}/.env

ExecStart=${APP_DIR}/.venv/bin/uvicorn main:app --host \$HOST --port \$PORT
Restart=always
RestartSec=3

# Security hardening
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

EOF

sudo systemctl daemon-reload
sudo systemctl enable sandbox
sudo systemctl start sandbox
```

Troubleshoot: Check logs with `sudo journalctl -u sandbox -f` and fix any issues.

> [!IMPORTANT]
> Auth cookies require HTTPS (`secure=True` flag). The login sessions won't persist without HTTPS.
> Point any FQDN to your server and use Caddy for automatic SSL for your domain. See example [Caddyfile](Caddyfile) for reference.
Expand Down
35 changes: 7 additions & 28 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,16 @@ class LXDSettings(Base):
verify_ssl = Column(Boolean, default=True)


class VMDefaultSettings(Base):
__tablename__ = "vm_default_settings"
class Classroom(Base):
"""Classroom configuration - combines VM/Container defaults, image selection, and SSH config"""
__tablename__ = "classrooms"

id = Column(Integer, primary_key=True, index=True)
username = Column(String, default="ubuntu")
cpu = Column(Integer, default=2)
memory = Column(Integer, default=4)
disk = Column(Integer, default=20)
swap = Column(Integer, default=2)
name = Column(String, unique=True, index=True, nullable=False) # Classroom name (e.g., "CS101", "Data Science Lab")
username = Column(String, default="ubuntu") # Default username for instances
image_type = Column(String, default="container") # "container" or "virtual-machine"
lxd_profile = Column(String, nullable=True) # LXD profile name (references profiles[].name from LXD)
image_fingerprint = Column(String, nullable=True) # LXD image fingerprint
image_alias = Column(String, nullable=True) # Image alias (e.g., "ubuntu/24.04")
image_description = Column(String, nullable=True) # Human-readable description
cloud_init = Column(Text, nullable=True)


class ContainerDefaultSettings(Base):
__tablename__ = "container_default_settings"

id = Column(Integer, primary_key=True, index=True)
username = Column(String, default="root")
cpu = Column(Integer, default=2)
memory = Column(Integer, default=2)
disk = Column(Integer, default=10)
image_fingerprint = Column(String, nullable=True) # LXD image fingerprint
image_alias = Column(String, nullable=True) # Image alias (e.g., "ubuntu/24.04")
image_description = Column(String, nullable=True) # Human-readable description
cloud_init = Column(Text, nullable=True)


class ConnectionTemplate(Base):
__tablename__ = "connection_templates"

id = Column(Integer, primary_key=True, index=True)
ssh_config_template = Column(Text, nullable=True) # SSH config template with placeholders
13 changes: 13 additions & 0 deletions core/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Shared Jinja2 templates configuration"""
from fastapi.templating import Jinja2Templates
from jinja2 import filters

from core.config import settings


# Create a single shared templates instance
templates = Jinja2Templates(directory="templates")

# Configure shared filters and globals
templates.env.globals['app_title'] = settings.APP_TITLE
templates.env.filters['filesizeformat'] = filters.do_filesizeformat
11 changes: 3 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from jinja2 import filters

from core.templates import templates
from core.database import engine, Base
from core.config import settings

Expand All @@ -29,20 +28,16 @@ async def lifespan(app: FastAPI):
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")

# Setup templates
templates = Jinja2Templates(directory="templates")
templates.env.filters['filesizeformat'] = filters.do_filesizeformat
templates.env.globals['app_title'] = settings.APP_TITLE


# ============== Include Routers ==============

from routes import auth, dashboard, instances, settings
from routes import auth, dashboard, instances, settings, classrooms

app.include_router(auth.router)
app.include_router(dashboard.router)
app.include_router(instances.router)
app.include_router(settings.router)
app.include_router(classrooms.router)


# ============== Exception Handlers ==============
Expand Down
35 changes: 27 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
fastapi[standard]
pylxd
sqlalchemy
python-jose[cryptography]
bcrypt
psutil
python-dotenv
uvicorn[standard]
# Core dependencies with pinned major versions for production stability
# Minor and patch versions can be updated for security patches and bug fixes

# Web framework
fastapi[standard]>=0.115.0,<1.0.0
starlette>=0.30.0,<1.0.0 # Required for template rendering compatibility

# LXD API client
pylxd>=2.3.0,<3.0.0

# Database ORM
sqlalchemy>=2.0.0,<3.0.0

# JWT authentication
python-jose[cryptography]>=3.3.0,<4.0.0

# Password hashing
bcrypt>=4.0.0,<5.0.0

# System metrics
psutil>=6.0.0,<7.0.0

# Environment variable loading
python-dotenv>=1.0.0,<2.0.0

# ASGI server
uvicorn[standard]>=0.30.0,<1.0.0
5 changes: 1 addition & 4 deletions routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@
from datetime import datetime
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session

from core.database import get_db
from core.models import AdminUser
from core.templates import templates
from core.security import get_password_hash, verify_password, create_access_token
from core.config import settings
from core.rate_limiter import login_rate_limiter

templates = Jinja2Templates(directory="templates")
templates.env.globals['app_title'] = settings.APP_TITLE

router = APIRouter()


Expand Down
Loading
Loading