From 0615d83134d9ce698b0dec321fe47dc9c1469799 Mon Sep 17 00:00:00 2001 From: malmert Date: Sat, 22 Nov 2025 23:25:39 +0100 Subject: [PATCH] feat: add netplan configuration models and backend API for applying netplan --- backend/src/Models/netplan.py | 36 ++++++++++++++ backend/src/main.py | 34 ++++++++++++- start_dev.sh | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 backend/src/Models/netplan.py create mode 100755 start_dev.sh diff --git a/backend/src/Models/netplan.py b/backend/src/Models/netplan.py new file mode 100644 index 0000000..abe9b7a --- /dev/null +++ b/backend/src/Models/netplan.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field +from typing import List, Dict, Optional + + +class Nameservers(BaseModel): + addresses: List[str] = Field(default_factory=list) + search: List[str] = Field(default_factory=list) + + +class EthernetConfig(BaseModel): + dhcp4: Optional[bool] = None + dhcp6: Optional[bool] = None + addresses: Optional[List[str]] = None + gateway4: Optional[str] = None + gateway6: Optional[str] = None + nameservers: Optional[Nameservers] = None + optional: Optional[bool] = None + + +class BridgeConfig(BaseModel): + interfaces: List[str] = Field(default_factory=list) # ["eth1", "eth2"] + dhcp4: Optional[bool] = None + dhcp6: Optional[bool] = None + addresses: Optional[List[str]] = None + gateway4: Optional[str] = None + gateway6: Optional[str] = None + nameservers: Optional[Nameservers] = None + parameters: Optional[dict] = None # allows spanning-tree, port-priority, forward-delay, etc. + optional: Optional[bool] = None + + +class NetworkConfig(BaseModel): + version: int = 2 + renderer: Optional[str] = "networkd" + ethernets: Dict[str, EthernetConfig] = Field(default_factory=dict) + bridges: Dict[str, BridgeConfig] = Field(default_factory=dict) diff --git a/backend/src/main.py b/backend/src/main.py index e58cd31..0f980bf 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,3 +1,6 @@ +from asyncio import subprocess +from http.client import HTTPException +import src.Models.netplan as netplan from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import os @@ -25,4 +28,33 @@ def read_root(): @app.get("/api/nft/ruleset") def read_root(): message = os.popen("sudo nft --json list ruleset").read().strip() - return {"message": message} \ No newline at end of file + return {"message": message} + + +NETPLAN_FILE = "/etc/netplan/99-app.yaml" + +@app.post("/netplan/apply") +def apply_netplan(config: netplan.NetworkConfig): + try: + data = {"network": config.dict(exclude_none=True)} + + with open(NETPLAN_FILE, "w") as f: + yaml.dump(data, f, sort_keys=False) + + # Safety: rollback if config breaks connectivity + result = subprocess.run( + ["netplan", "try", "--timeout", "10"], + capture_output=True, + text=True, + ) + + if result.returncode != 0: + raise HTTPException( + status_code=500, + detail=f"Netplan failed: {result.stderr}" + ) + + return {"status": "ok", "output": result.stdout} + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/start_dev.sh b/start_dev.sh new file mode 100755 index 0000000..eb61bbe --- /dev/null +++ b/start_dev.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +set -e + +# Kill children on exit +cleanup() { + echo + echo "🛑 Stopping dev environment…" + kill 0 2>/dev/null +} +trap cleanup EXIT + +START_BE=true +START_FE=true + +case "$1" in + backend|be) + START_FE=false + ;; + frontend|fe) + START_BE=false + ;; + "" ) + # default → both + ;; + * ) + echo "❌ Unknown argument: $1" + echo "Usage:" + echo " ./dev.sh → start both" + echo " ./dev.sh be → start backend only" + echo " ./dev.sh fe → start frontend only" + exit 1 + ;; +esac + + +############################### +# Backend +############################### +if [ "$START_BE" = true ]; then +( + echo "🚀 Starting backend…" + cd backend || { echo "❌ backend folder missing"; exit 1; } + + if [ ! -d venv ]; then + echo "⚙️ Creating virtual environment…" + python3 -m venv venv + ./venv/bin/pip install -r requirements.txt + fi + + echo "📡 Backend at http://localhost:8000" + ./venv/bin/uvicorn src.main:app --reload --host 0.0.0.0 --port 8000 +) & +BE_PID=$! +else + echo "⏭ Skipping backend" +fi + + +############################### +# Frontend +############################### +if [ "$START_FE" = true ]; then +( + echo "🚀 Starting frontend…" + cd frontend || { echo "❌ frontend folder missing"; exit 1; } + + if [ ! -d node_modules ]; then + echo "📦 Installing frontend dependencies…" + npm install + fi + + echo "🌍 Frontend at http://localhost:5173" + npm run dev-local +) & +FE_PID=$! +else + echo "⏭ Skipping frontend" +fi + + +############################### +# Wait for both +############################### +echo +echo "💡 Dev environment running." +[ "$START_BE" = true ] && echo " Backend PID: $BE_PID" +[ "$START_FE" = true ] && echo " Frontend PID: $FE_PID" +echo " Press CTRL+C to stop everything." +echo + +wait