0.1 commit

This commit is contained in:
root 2025-11-23 19:02:38 -05:00
commit ab9607b7f1
8 changed files with 655 additions and 44 deletions

173
app.py
View file

@ -7,17 +7,31 @@ import uuid
from flask import Flask, request, render_template_string, flash
app = Flask(__name__)
app.secret_key = "change-me-in-production"
app.secret_key = os.environ.get("MICROCONTAINERS_SECRET_KEY", "dev-only-not-secret")
# --- Config -----------------------------------------------------------------
DOCKER_IMAGE = "micro-debian-dev:latest" # built from the Dockerfile above
DATA_ROOT = "/srv/microcontainers" # where per-tenant dirs live
IMAGES = {
"debian": {
"docker_image": "micro-debian-dev:latest",
"label": "Debian Dev Box (micro-debian-dev)"
},
"fedora": {
"docker_image": "micro-fedora43-dev:latest",
"label": "Fedora 43 Dev Box (micro-fedora43-dev)"
},
}
DEFAULT_IMAGE_KEY = "debian"
DATA_ROOT = "/srv/microcontainers" # per-tenant metadata + disk.img
MOUNT_ROOT = "/mnt/microcontainers" # where disk.img files are mounted
HOSTNAME = "arthur.lan" # or "mentalnet.xyz" if you prefer
PORT_RANGE_START = 20000
PORT_RANGE_END = 21000
os.makedirs(DATA_ROOT, exist_ok=True)
os.makedirs(MOUNT_ROOT, exist_ok=True)
# --- Helpers ----------------------------------------------------------------
@ -49,21 +63,75 @@ def validate_ssh_key(key: str) -> bool:
return any(key.startswith(p) for p in allowed_prefixes)
def run_docker_container(container_name: str, ssh_dir: str, host_port: int):
def is_mounted(mount_point: str) -> bool:
"""Check if a given mount point is already mounted."""
try:
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.split()
if len(parts) >= 2 and parts[1] == mount_point:
return True
except FileNotFoundError:
pass
return False
def ensure_tenant_disk(container_name: str, ssh_key: str, size_gb: int = 3) -> str:
"""
Ensure a per-tenant disk.img exists, is formatted, mounted, and has
.ssh/authorized_keys populated.
Returns the host mount point path, which will be bind-mounted to /home/micro.
"""
tenant_root = os.path.join(DATA_ROOT, container_name)
os.makedirs(tenant_root, exist_ok=True)
disk_img = os.path.join(tenant_root, "disk.img")
mount_point = os.path.join(MOUNT_ROOT, container_name)
os.makedirs(mount_point, exist_ok=True)
# 1) Create sparse disk.img if it doesn't exist
if not os.path.exists(disk_img):
subprocess.run(["fallocate", "-l", f"{size_gb}G", disk_img], check=True)
subprocess.run(["mkfs.ext4", "-F", disk_img], check=True)
# 2) Mount it if not already mounted
if not is_mounted(mount_point):
subprocess.run(["mount", "-o", "loop", disk_img, mount_point], check=True)
# 3) Prepare /home/micro layout inside the filesystem
# (we're mounting this whole thing as /home/micro in the container)
ssh_dir = os.path.join(mount_point, ".ssh")
os.makedirs(ssh_dir, exist_ok=True)
auth_keys_path = os.path.join(ssh_dir, "authorized_keys")
with open(auth_keys_path, "w") as f:
f.write(ssh_key.strip() + "\n")
# Proper perms for SSH
os.chmod(ssh_dir, 0o700)
os.chmod(auth_keys_path, 0o600)
# Make everything in /home/micro look like micro:micro (uid/gid 1000)
subprocess.run(["chown", "-R", "1000:1000", mount_point], check=True)
return mount_point
def run_docker_container(container_name: str, host_port: int, docker_image: str, home_mount: str):
"""
Start the tenant container:
- Binds host_port -> container:22
- Mounts authorized_keys -> /home/micro/.ssh/authorized_keys (read-only)
- Mounts per-tenant 3GB disk as /home/micro
"""
auth_keys_path = os.path.join(ssh_dir, "authorized_keys")
cmd = [
"docker", "run", "-d",
"--name", container_name,
"--memory=512m",
"--memory-swap=512m",
"-p", f"{host_port}:22",
"-v", f"{auth_keys_path}:/home/micro/.ssh/authorized_keys:ro",
DOCKER_IMAGE,
"-v", f"{home_mount}:/home/micro",
docker_image,
]
subprocess.run(cmd, check=True)
@ -94,7 +162,7 @@ INDEX_TEMPLATE = """
border: 1px solid #333;
}
label { display: block; margin-bottom: 8px; font-weight: 600; }
textarea, input[type=text] {
textarea, input[type=text], select {
width: 100%;
box-sizing: border-box;
background: #101010;
@ -127,8 +195,14 @@ INDEX_TEMPLATE = """
<div class="box">
<p>This prototype spins up a small Docker-based "micro VM" and wires in your SSH public key.</p>
<ul>
<li>Base image: <code>{{ docker_image }}</code></li>
<li>Available base images:</li>
<ul>
{% for key, info in images.items() %}
<li><code>{{ key }}</code>: {{ info.label }}</li>
{% endfor %}
</ul>
<li>User: <code>micro</code></li>
<li>Per-tenant /home size: <code>3 GB</code> (loopback ext4)</li>
<li>SSH inside container: <code>port 22</code></li>
<li>Host port: auto-assigned from <code>{{ port_start }}-{{ port_end }}</code></li>
</ul>
@ -147,6 +221,7 @@ INDEX_TEMPLATE = """
{% if result %}
<div class="box">
<h2 class="success">Container created!</h2>
<p><strong>Image:</strong> <code>{{ result.image_key }}</code> {{ images[result.image_key].label }}</p>
<p><strong>Container name:</strong> <code>{{ result.name }}</code></p>
<p><strong>Host port:</strong> <code>{{ result.port }}</code></p>
<p><strong>SSH command:</strong></p>
@ -158,6 +233,15 @@ INDEX_TEMPLATE = """
<div class="box">
<form method="post">
<label for="image">Base image</label>
<select id="image" name="image">
{% for key, info in images.items() %}
<option value="{{ key }}" {% if key == selected_image %}selected{% endif %}>
{{ key }} - {{ info.label }}
</option>
{% endfor %}
</select>
<label for="ssh_key">SSH public key</label>
<textarea id="ssh_key" name="ssh_key" rows="6" placeholder="ssh-ed25519 AAAAC3Nz... user@host" required>{{ ssh_key or "" }}</textarea>
@ -180,87 +264,96 @@ def index():
if request.method == "GET":
return render_template_string(
INDEX_TEMPLATE,
docker_image=DOCKER_IMAGE,
images=IMAGES,
port_start=PORT_RANGE_START,
port_end=PORT_RANGE_END,
result=None,
ssh_key="",
hostname=HOSTNAME,
selected_image=DEFAULT_IMAGE_KEY,
)
# POST: handle provisioning
ssh_key = (request.form.get("ssh_key") or "").strip()
note = (request.form.get("note") or "").strip()
image_key = (request.form.get("image") or DEFAULT_IMAGE_KEY).strip()
if image_key not in IMAGES:
image_key = DEFAULT_IMAGE_KEY
docker_image = IMAGES[image_key]["docker_image"]
if not validate_ssh_key(ssh_key):
flash("Invalid SSH public key format. Please paste a standard OpenSSH public key line.")
return render_template_string(
INDEX_TEMPLATE,
docker_image=DOCKER_IMAGE,
images=IMAGES,
port_start=PORT_RANGE_START,
port_end=PORT_RANGE_END,
result=None,
ssh_key=ssh_key,
hostname=HOSTNAME,
selected_image=image_key,
)
# Generate a container name
suffix = uuid.uuid4().hex[:6]
base_name = "mc"
base_name = f"mc-{image_key}"
if note:
safe_note = "".join(c for c in note.lower().replace(" ", "-") if c.isalnum() or c in "-_")
base_name = f"mc-{safe_note}"
base_name = f"mc-{image_key}-{safe_note}"
container_name = f"{base_name}-{suffix}"
# Tenant SSH dir on host
tenant_root = os.path.join(DATA_ROOT, container_name)
tenant_ssh_dir = os.path.join(tenant_root, "ssh")
os.makedirs(tenant_ssh_dir, exist_ok=True)
# Write authorized_keys
auth_keys_path = os.path.join(tenant_ssh_dir, "authorized_keys")
with open(auth_keys_path, "w") as f:
f.write(ssh_key.strip() + "\n")
# Make it look like micro:micro (uid/gid 1000) to sshd in the container
os.chmod(auth_keys_path, 0o600)
# Ensure per-tenant disk (3 GB ext4) and authorized_keys
try:
os.chown(auth_keys_path, 1000, 1000) # micro:micro inside container
except PermissionError:
# If Flask isn't running as root, this will fail; on your VM it should succeed.
pass
# Choose a free port and start container
host_port = find_free_port()
try:
run_docker_container(container_name, tenant_ssh_dir, host_port)
home_mount = ensure_tenant_disk(container_name, ssh_key, size_gb=3)
except subprocess.CalledProcessError as e:
flash(f"Error starting container: {e}")
flash(f"Error preparing tenant disk: {e}")
return render_template_string(
INDEX_TEMPLATE,
docker_image=DOCKER_IMAGE,
images=IMAGES,
port_start=PORT_RANGE_START,
port_end=PORT_RANGE_END,
result=None,
ssh_key=ssh_key,
hostname=HOSTNAME,
selected_image=image_key,
)
# Choose a free port and start container
host_port = find_free_port()
try:
run_docker_container(container_name, host_port, docker_image, home_mount)
except subprocess.CalledProcessError as e:
flash(f"Error starting container: {e}")
return render_template_string(
INDEX_TEMPLATE,
images=IMAGES,
port_start=PORT_RANGE_START,
port_end=PORT_RANGE_END,
result=None,
ssh_key=ssh_key,
hostname=HOSTNAME,
selected_image=image_key,
)
class Result:
def __init__(self, name, port):
def __init__(self, name, port, image_key):
self.name = name
self.port = port
self.image_key = image_key
result = Result(container_name, host_port)
result = Result(container_name, host_port, image_key)
return render_template_string(
INDEX_TEMPLATE,
docker_image=DOCKER_IMAGE,
images=IMAGES,
port_start=PORT_RANGE_START,
port_end=PORT_RANGE_END,
result=result,
ssh_key="",
hostname=HOSTNAME,
selected_image=image_key,
)