A production-oriented REST API for managing OpenStack virtual machine lifecycle operations, built with FastAPI, async SQLAlchemy, and PostgreSQL. The API models the VM lifecycle after OpenStack Nova's state machine, providing a complete set of operations for creating, managing, and destroying virtual machines along with supporting resources like snapshots, volumes, and networks.
- JWT Authentication -- Register, login, token refresh, and protected endpoints with access + refresh token flow
- Virtual Machine Management -- Full CRUD with async lifecycle operations (create, start, stop, reboot, resize, delete)
- VM State Machine -- Deterministic state transitions modeled after OpenStack Nova, with task state tracking
- Snapshots -- Create, list, and restore VM snapshots with async processing
- Block Storage Volumes -- Create volumes and attach/detach them to VMs
- Networks -- Pre-seeded network catalog with interface attachment (MAC/IP auto-generation)
- Flavors -- Read-only instance type catalog (vCPUs, RAM, disk)
- Pagination -- Consistent paginated list responses with total counts
- Background Task Processing -- Async operations return
202 Acceptedwith state polling - Mock OpenStack Backend -- Pluggable backend abstraction with a mock implementation for local development
- OpenAPI Documentation -- Auto-generated interactive docs at
/api/v1/docs
git clone https://github.com/your-org/openstack-vm-api.git
cd openstack-vm-api
docker-compose up --buildThe API will be available at http://localhost:8000. Interactive API docs are at http://localhost:8000/api/v1/docs.
Database migrations run automatically on startup. The compose stack includes:
- api -- FastAPI application with hot reload
- db -- PostgreSQL 16 (Alpine) with health checks
# Health check
curl http://localhost:8000/api/v1/health
# Register a user
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"username": "demo", "email": "demo@example.com", "password": "SecurePass123"}'
# Login and get tokens
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "demo", "password": "SecurePass123"}'
# Create a VM (use access_token from login response)
curl -X POST http://localhost:8000/api/v1/vms \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"name": "my-server", "flavor_id": "<flavor_uuid>"}'
# Poll VM status until ACTIVE
curl http://localhost:8000/api/v1/vms/<vm_id> \
-H "Authorization: Bearer <access_token>"- Python 3.13+
- uv (package manager)
- PostgreSQL 16+ (or use Docker Compose for the database)
# Install dependencies
uv sync
# Copy and configure environment
cp .env.example .env
# Edit .env with your database URL and secret key
# Run database migrations
uv run alembic upgrade head
# Start the development server
uv run uvicorn app.main:app --reload --port 8000| Variable | Default | Description |
|---|---|---|
APP_NAME |
OpenStack VM API |
Application display name |
DEBUG |
false |
Enable debug mode |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
DATABASE_URL |
postgresql+asyncpg://postgres:postgres@localhost:5432/vmapi |
Async PostgreSQL connection string |
DB_POOL_SIZE |
10 |
SQLAlchemy connection pool size |
DB_MAX_OVERFLOW |
20 |
Max overflow connections beyond pool size |
DB_ECHO |
false |
Log all SQL statements |
SECRET_KEY |
change-me-in-production |
JWT signing secret (change this!) |
ALGORITHM |
HS256 |
JWT signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
30 |
Access token TTL |
REFRESH_TOKEN_EXPIRE_DAYS |
7 |
Refresh token TTL |
MOCK_BUILD_DELAY |
5.0 |
Simulated VM build time (seconds) |
MOCK_START_DELAY |
2.0 |
Simulated VM start time |
MOCK_STOP_DELAY |
2.0 |
Simulated VM stop time |
MOCK_REBOOT_DELAY |
3.0 |
Simulated VM reboot time |
MOCK_RESIZE_DELAY |
5.0 |
Simulated VM resize time |
MOCK_DELETE_DELAY |
2.0 |
Simulated VM delete time |
MOCK_SNAPSHOT_DELAY |
4.0 |
Simulated snapshot creation time |
MOCK_ERROR_RATE |
0.0 |
Probability of simulated operation failure (0.0-1.0) |
DEFAULT_PAGE_SIZE |
20 |
Default pagination page size |
MAX_PAGE_SIZE |
100 |
Maximum allowed page size |
All endpoints are prefixed with /api/v1/. JWT authentication is required unless noted otherwise.
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/health |
Health check | No |
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/auth/register |
Register a new user | No |
POST |
/auth/login |
Login and get JWT tokens | No |
POST |
/auth/refresh |
Refresh access token | No |
GET |
/auth/me |
Get current user profile | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/vms |
Create a VM (returns 202) | Yes |
GET |
/vms |
List VMs (paginated, filterable by status/name) | Yes |
GET |
/vms/{id} |
Get VM details | Yes |
DELETE |
/vms/{id} |
Delete a VM (returns 204) | Yes |
POST |
/vms/{id}/actions/start |
Start a stopped VM (returns 202) | Yes |
POST |
/vms/{id}/actions/stop |
Stop a running VM (returns 202) | Yes |
POST |
/vms/{id}/actions/reboot |
Reboot a VM (returns 202) | Yes |
POST |
/vms/{id}/actions/resize |
Resize a VM to new flavor (returns 202) | Yes |
POST |
/vms/{id}/actions/confirm-resize |
Confirm a pending resize | Yes |
POST |
/vms/{id}/actions/revert-resize |
Revert a pending resize | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/flavors |
List available flavors | Yes |
GET |
/flavors/{id} |
Get flavor details | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/vms/{id}/snapshots |
Create a snapshot (returns 202) | Yes |
GET |
/vms/{id}/snapshots |
List snapshots for a VM (paginated) | Yes |
GET |
/vms/{id}/snapshots/{snap_id} |
Get snapshot details | Yes |
DELETE |
/vms/{id}/snapshots/{snap_id} |
Delete a snapshot | Yes |
POST |
/vms/{id}/snapshots/{snap_id}/restore |
Restore VM from snapshot (returns 202) | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/volumes |
Create a volume | Yes |
GET |
/volumes |
List volumes (paginated) | Yes |
GET |
/volumes/{id} |
Get volume details | Yes |
DELETE |
/volumes/{id} |
Delete a volume | Yes |
POST |
/vms/{id}/volumes |
Attach a volume to a VM | Yes |
DELETE |
/vms/{id}/volumes/{vol_id} |
Detach a volume from a VM | Yes |
GET |
/vms/{id}/volumes |
List volume attachments for a VM | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/networks |
List available networks | Yes |
GET |
/networks/{id} |
Get network details | Yes |
POST |
/vms/{id}/interfaces |
Attach a network interface to a VM | Yes |
DELETE |
/vms/{id}/interfaces/{iface_id} |
Detach a network interface | Yes |
GET |
/vms/{id}/interfaces |
List interfaces for a VM | Yes |
The VM lifecycle follows a deterministic state machine modeled after OpenStack Nova. Async operations set a task_state and return 202 Accepted; clients poll GET /vms/{id} for the final state.
+---------+
+---------->| ERROR |<----------+
| +---------+ |
| build_failed resize_failed |
| |
+---------+ +-----------+
create ----->| BUILDING| | RESIZING |
+---------+ +-----------+
| ^
| build_complete resize |
v |
+---------+ stop +---------+ |
| ACTIVE |----------->| SHUTOFF | |
+---------+ +---------+ |
^ ^ ^ ^ | |
| | | | start | |
| | | +<----------------+ |
| | | |
| | +--- confirm_resize ---+ |
| | | |
| +--- revert_resize ---+ | |
| | | |
| reboot_complete +----------+ |
| | VERIFY |------+
+<---+ | RESIZE |
| +----------+
+-----------+
| REBOOTING |
+-----------+
delete (from ACTIVE, SHUTOFF, ERROR, BUILDING, VERIFY_RESIZE)
|
v
+-----------+ delete_complete +---------+
| DELETING |--------------------->| DELETED |
+-----------+ +---------+
Key concepts:
- status -- The stable VM state (ACTIVE, SHUTOFF, ERROR, etc.)
- task_state -- Transient operation indicator (spawning, powering_on, etc.);
nullwhen the VM is in a stable state - Async operations (create, start, stop, reboot, resize, delete) return immediately with
202 Accepted - Background tasks simulate processing delays, then update
statusand cleartask_state - Invalid transitions return
409 Conflictwith allowed actions listed
# Run the full test suite with coverage
uv run pytest --cov=app
# Run specific test modules
uv run pytest tests/test_auth.py -v
uv run pytest tests/test_vms.py -v
# Run with verbose output
uv run pytest -v --tb=shortThe test suite uses:
- pytest-asyncio for async test support
- httpx.AsyncClient for integration tests against the FastAPI app
- SQLite in-memory database for test isolation
- Session-scoped event loop for performance
# Check for lint errors
uv run ruff check src/ tests/
# Auto-fix lint issues
uv run ruff check --fix src/ tests/
# Check formatting
uv run ruff format --check src/ tests/
# Auto-format
uv run ruff format src/ tests/Ruff is configured in pyproject.toml with select rules for:
E,F-- pycodestyle and pyflakesI-- isort import sortingN-- pep8-namingUP-- pyupgradeB-- bugbearSIM-- simplifyRUF-- Ruff-specific rules
openstack-vm-api/
├── pyproject.toml # Project metadata, dependencies, tool config
├── uv.lock # Lockfile
├── Dockerfile # Multi-stage container build
├── docker-compose.yml # API + PostgreSQL stack
├── alembic.ini # Alembic migration config
├── .env.example # Environment variable template
├── docs/
│ ├── architecture.md # Architecture deep-dive
│ └── adr/ # Architecture Decision Records
│ ├── 001-framework-selection.md
│ ├── 002-mock-backend.md
│ └── 003-background-tasks.md
├── alembic/
│ ├── env.py # Async Alembic environment
│ └── versions/ # Migration scripts
├── src/app/
│ ├── main.py # App factory, lifespan, router registration
│ ├── config.py # Pydantic Settings (env-based config)
│ ├── database.py # Async engine, session factory, get_db
│ ├── auth/ # Authentication domain
│ │ ├── router.py # Auth endpoints
│ │ ├── schemas.py # Pydantic request/response models
│ │ ├── models.py # User SQLAlchemy model
│ │ ├── service.py # Auth business logic (JWT, bcrypt)
│ │ ├── dependencies.py # get_current_user dependency
│ │ └── exceptions.py # Auth-specific errors
│ ├── vms/ # Virtual machine domain
│ │ ├── router.py # VM endpoints
│ │ ├── schemas.py # VM request/response models
│ │ ├── models.py # VirtualMachine model + VMStatus enum
│ │ ├── service.py # VM business logic
│ │ ├── state_machine.py # VMStateMachine transition table
│ │ ├── dependencies.py # Backend injection
│ │ └── exceptions.py # VM-specific errors
│ ├── flavors/ # Instance type catalog
│ ├── snapshots/ # VM snapshots
│ ├── volumes/ # Block storage + attachments
│ ├── networks/ # Networks + interfaces
│ ├── backend/
│ │ ├── base.py # AbstractComputeBackend (ABC)
│ │ └── mock.py # MockOpenStackBackend
│ └── common/
│ ├── models.py # Base SQLAlchemy model (UUID PK, timestamps)
│ ├── schemas.py # Shared Pydantic models (pagination, errors)
│ └── exceptions.py # Exception hierarchy + handler
└── tests/ # Test suite (mirrors src structure)
├── conftest.py # Fixtures (async client, test DB, factories)
└── test_*.py # Test modules
For a detailed architecture writeup covering design decisions, module structure, state machine design, authentication flow, and the production roadmap, see docs/architecture.md.
Architecture Decision Records (ADRs):
MIT