Complete documentation suite for KiteStacks covering all 11 services across 2-host active-active architecture. Includes beginner track (with AI, 8 files) and advanced track (without AI, 7 files) with time estimates, real troubleshooting cases, and command-by-command explanations. Updates certifications roadmap to reflect July 7 2026 A+ Core 2 exam goal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9 KiB
Without AI — Part 3: Python Basics
Track: Advanced (No AI)
Time for this section: 1–2 weeks
Python is used in this homelab for:
- Database operations — copying SQLite databases safely between machines
- HTTP requests — hitting APIs to configure services
- The metrics API — the Python FastAPI service that feeds live stats to the portal
- One-off automation — scripts that are too complex for Bash
You do not need to be a Python developer. You need to read Python code, understand what it does, modify it for your situation, and write simple scripts.
Installing Python
Ubuntu 24.04 comes with Python 3 already installed:
python3 --version # Should show 3.12.x or similar
pip3 --version # Package manager for Python
Install the packages used in this homelab:
pip3 install requests fastapi uvicorn psutil
Python Syntax Basics
Python uses indentation (spaces) to define blocks of code instead of {} like many
other languages. This is critical — wrong indentation causes errors.
# This is a comment
name = "kenpat" # string
port = 3000 # integer
price = 4.99 # float
is_running = True # boolean
print(name) # prints: kenpat
print(f"Port is {port}") # f-string: prints: Port is 3000
print(f"{name!r}") # repr: prints: 'kenpat' (with quotes)
Data Structures
# List (ordered, mutable)
services = ["forgejo", "grafana", "authentik"]
services.append("portainer") # add to end
services[0] # "forgejo" (zero-indexed)
services[-1] # "portainer" (last item)
len(services) # 4
for service in services:
print(service)
# Dictionary (key-value pairs, like JSON)
monitor = {
"name": "Forgejo",
"url": "https://gitforge.kitestacks.com",
"id": 16,
"active": True
}
monitor["name"] # "Forgejo"
monitor.get("missing", "default") # "default" (safe get with fallback)
monitor.keys() # dict_keys(["name", "url", "id", "active"])
for key, value in monitor.items():
print(f"{key}: {value}")
# List of dicts (very common in API responses)
monitors = [
{"id": 16, "name": "Forgejo"},
{"id": 17, "name": "Grafana"},
]
for m in monitors:
print(m["id"], m["name"])
Functions and Conditionals
def check_service(name, url):
"""Check if a service URL is reachable."""
if not url.startswith("https://"):
return False
print(f"Checking {name} at {url}")
return True
result = check_service("Grafana", "https://grafana.kitestacks.com")
print(result) # True
Conditionals:
status = 200
if status == 200:
print("OK")
elif status in (301, 302):
print("Redirect")
elif status >= 500:
print("Server error")
else:
print(f"Unexpected status: {status}")
Working with JSON
Almost every API in this homelab sends and receives JSON (JavaScript Object Notation).
Python's json module converts between JSON strings and Python dicts/lists:
import json
# JSON string to Python dict
data = json.loads('{"name": "Forgejo", "id": 16}')
print(data["name"]) # Forgejo
# Python dict to JSON string
obj = {"monitors": [1, 2, 3]}
json_str = json.dumps(obj, indent=2)
print(json_str)
# {
# "monitors": [1, 2, 3]
# }
# Read JSON from a file
with open("/tmp/kuma.meta.json") as f:
kuma_data = json.load(f)
# Parse Uptime Kuma heartbeat data
for monitor_id, heartbeats in kuma_data.get("heartbeatList", {}).items():
if heartbeats:
last = heartbeats[-1]
status = "UP" if last["status"] == 1 else "DOWN"
print(f"Monitor {monitor_id}: {status}")
HTTP Requests with requests
The requests library makes HTTP calls easy:
import requests
# GET request
response = requests.get("https://gitforge.kitestacks.com/api/v1/repos/search",
headers={"Authorization": "token your-api-token"},
timeout=5)
print(response.status_code) # 200
data = response.json() # Parse JSON response body
print(data["data"][0]["name"]) # First repo name
# POST request with JSON body
response = requests.post(
"https://auth.kitestacks.com/api/v3/core/tokens/",
headers={"Authorization": "Bearer your-admin-token"},
json={"identifier": "my-token", "user": "kenpat"},
timeout=5
)
if response.ok: # True for 2xx status codes
print("Token created:", response.json()["key"])
else:
print(f"Failed: {response.status_code} {response.text}")
SQLite — The Key Database Skill in This Homelab
SQLite is a database that lives in a single file. Uptime Kuma, Kavita, and other services
use SQLite. You used Python's sqlite3 module to copy databases safely between machines.
import sqlite3
# Connect to a database file
conn = sqlite3.connect("/path/to/kuma.db")
# Run a query
cursor = conn.execute("SELECT id, name, url FROM monitor ORDER BY id")
rows = cursor.fetchall() # Get all results
for row in rows:
print(row[0], row[1], row[2])
# Insert data
conn.execute(
"INSERT INTO monitor (name, type, url, active) VALUES (?, ?, ?, ?)",
("BookStack", "http", "https://wiki.kitestacks.com", 1)
)
conn.commit() # Save changes (without commit, nothing is written)
# Use a transaction explicitly (safer for multiple changes)
conn.execute("BEGIN")
conn.execute("UPDATE monitor SET active=1 WHERE id=26")
conn.execute("UPDATE monitor SET active=1 WHERE id=27")
conn.execute("COMMIT")
conn.close()
The backup() Method — Copying Databases Safely
SQLite databases in WAL mode (write-ahead log) cannot be copied with a plain file copy
while they are in use. The Connection.backup() method creates a consistent snapshot:
import sqlite3
def safe_backup(source_path, dest_path):
"""Copy a SQLite database safely, even if it's in use."""
src = sqlite3.connect(source_path)
dst = sqlite3.connect(dest_path)
src.backup(dst) # Creates a consistent copy
dst.close()
src.close()
print(f"Backed up {source_path} to {dest_path}")
safe_backup("/src/kuma.db", "/out/kuma.db.backup")
Why a plain cp would fail: SQLite in WAL mode has two extra files:
kuma.db-wal (uncommitted changes) and kuma.db-shm (shared memory). If you copy
the main file without those, or in the wrong order, you get a corrupted database.
Connection.backup() handles all of this correctly.
Writing a Simple FastAPI Service
The kitestacks-metrics-api is a Python FastAPI service. Understanding it helps you modify or extend it:
from fastapi import FastAPI
import psutil
app = FastAPI()
@app.get("/api/health")
def health():
return {"ok": True}
@app.get("/api/metrics")
def metrics():
return {
"cpu_percent": psutil.cpu_percent(interval=1),
"ram_percent": psutil.virtual_memory().percent,
"ram_used_gb": psutil.virtual_memory().used / 1e9,
"disk_percent": psutil.disk_usage("/").percent,
}
Run it:
uvicorn myapi:app --host 0.0.0.0 --port 8000
psutil reads these values from the host's /proc filesystem. When running inside
a Docker container with pid: host, it reads the HOST's stats.
Environment Variables in Python
import os
token = os.environ.get("FORGEJO_TOKEN") # None if not set
token = os.environ.get("FORGEJO_TOKEN", "") # Empty string if not set
token = os.environ["FORGEJO_TOKEN"] # KeyError if not set (explicit)
# Check and fail clearly
token = os.environ.get("FORGEJO_TOKEN")
if not token:
raise ValueError("FORGEJO_TOKEN environment variable is required")
File Operations
import os
# Read a file
with open("/tmp/kuma.json") as f:
content = f.read()
# Write a file
with open("/tmp/output.sql", "w") as f:
f.write("UPDATE ServerSetting SET Value='test' WHERE \"Key\"=40;\n")
# Check if a file exists
if os.path.exists("/data/kuma.db"):
print("Database found")
# Delete a file safely
for fname in ["/data/kuma.db-shm", "/data/kuma.db-wal"]:
if os.path.exists(fname):
os.remove(fname)
print(f"Removed {fname}")
# List files in a directory
for filename in os.listdir("/app/data"):
print(filename)
Practice Exercises
-
Write a Python script that reads
monitors.jsonfrom Uptime Kuma's API response and prints each monitor's name and status -
Write a script that connects to a SQLite database, lists all tables, and prints the first 5 rows of the
monitortable -
Write a script that uses
requeststo check if all 11 KiteStacks URLs return a status code between 200 and 399, and prints a summary -
Read the kitestacks-metrics-api source code and understand what each endpoint does
-
Modify the
safe_backup()function to also delete-shmand-walfiles from the destination before writing (prevents WAL conflicts after restore)