#!/usr/bin/env python3 import os import socket import subprocess import uuid from flask import Flask, request, render_template_string, flash app = Flask(__name__) app.secret_key = os.environ.get("MICROCONTAINERS_SECRET_KEY", "dev-only-not-secret") # --- Config ----------------------------------------------------------------- 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)" }, "alpine": { "docker_image": "micro-alpine-dev:latest", "label": "Alpine Linux Dev Box (micro-alpine-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 = "localhost" # 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 ---------------------------------------------------------------- def find_free_port(start=PORT_RANGE_START, end=PORT_RANGE_END): """Find a free TCP port on the host in the given range.""" for port in range(start, end): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.2) try: s.bind(("", port)) return port except OSError: continue raise RuntimeError("No free ports available in range") def setup_tenant_environment(home_mount: str): """ Create default .bashrc, fastfetch config, and other user environment files. Runs AFTER the home mount is created. """ bashrc = os.path.join(home_mount, ".bashrc") config_dir = os.path.join(home_mount, ".config", "fastfetch") os.makedirs(config_dir, exist_ok=True) # Copy fastfetch config source_cfg = os.path.join(os.path.dirname(__file__), "fastfetch_config.jsonc") dest_cfg = os.path.join(config_dir, "config.jsonc") if os.path.exists(source_cfg): subprocess.run(["cp", source_cfg, dest_cfg], check=True) # Write a curated .bashrc with open(bashrc, "a") as f: f.write("\n# ----- MicroContainers defaults -----\n") f.write("if command -v fastfetch >/dev/null 2>&1; then fastfetch; fi\n") f.write('alias fastfetch="fastfetch --config $HOME/.config/fastfetch/config.jsonc"\n') # Give proper ownership subprocess.run(["chown", "-R", "1000:1000", home_mount], check=True) def validate_ssh_key(key: str) -> bool: """Very basic SSH public key validation.""" key = key.strip() if not key: return False allowed_prefixes = ( "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-", "sk-ssh-", ) return any(key.startswith(p) for p in allowed_prefixes) 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_home_dir(container_name: str, ssh_key: str) -> str: home_dir = os.path.join(DATA_ROOT, container_name, "home") ssh_dir = os.path.join(home_dir, ".ssh") os.makedirs(ssh_dir, exist_ok=True) # Write authorized_keys auth_keys_path = os.path.join(ssh_dir, "authorized_keys") with open(auth_keys_path, "w") as f: f.write(ssh_key.strip() + "\n") os.chmod(ssh_dir, 0o700) os.chmod(auth_keys_path, 0o600) # Make it owned by micro:micro (uid 1000) subprocess.run(["chown", "-R", "1000:1000", home_dir], check=True) return home_dir 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 per-tenant 3GB disk as /home/micro """ cmd = [ "docker", "run", "-d", "--name", container_name, "--memory=512m", "--memory-swap=512m", "-p", f"{host_port}:22", "-v", f"{home_mount}:/home/micro", docker_image, ] subprocess.run(cmd, check=True) # --- Template --------------------------------------------------------------- INDEX_TEMPLATE = """ MicroContainer Provisioning

MicroContainer Provisioning (Prototype)

This prototype spins up a small Docker-based "micro VM" and wires in your SSH public key.

{% with messages = get_flashed_messages() %} {% if messages %}
{% for msg in messages %} {{ msg }}
{% endfor %}
{% endif %} {% endwith %} {% if result %}

Container created!

Image: {{ result.image_key }} — {{ images[result.image_key].label }}

Container name: {{ result.name }}

Host port: {{ result.port }}

SSH command:

ssh micro@{{ hostname }} -p {{ result.port }}

You can change the password for micro after logging in with:

passwd
{% endif %}
""" # --- Routes ----------------------------------------------------------------- @app.route("/", methods=["GET", "POST"]) def index(): if request.method == "GET": return render_template_string( INDEX_TEMPLATE, 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, 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 = 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-{image_key}-{safe_note}" container_name = f"{base_name}-{suffix}" # Ensure per-tenant disk (1 GB ext4) and authorized_keys try: home_mount = ensure_tenant_home_dir(container_name, ssh_key) setup_tenant_environment(home_mount) except subprocess.CalledProcessError as e: flash(f"Error preparing tenant disk: {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, ) # 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, image_key): self.name = name self.port = port self.image_key = image_key result = Result(container_name, host_port, image_key) return render_template_string( INDEX_TEMPLATE, images=IMAGES, port_start=PORT_RANGE_START, port_end=PORT_RANGE_END, result=result, ssh_key="", hostname=HOSTNAME, selected_image=image_key, ) if __name__ == "__main__": # For dev/testing only app.run(host="0.0.0.0", port=5000, debug=True)