Experimental gui.
This commit is contained in:
45
experimental/daemon/internal/handlers/dnsmasq.go
Normal file
45
experimental/daemon/internal/handlers/dnsmasq.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// GetDnsmasqConfigHandler handles requests to view the dnsmasq configuration
|
||||
func (app *App) GetDnsmasqConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if app.Config == nil || app.Config.IsEmpty() {
|
||||
http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
config := app.DnsmasqManager.Generate(app.Config)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(config))
|
||||
}
|
||||
|
||||
// RestartDnsmasqHandler handles requests to restart the dnsmasq service
|
||||
func (app *App) RestartDnsmasqHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if app.Config == nil || app.Config.IsEmpty() {
|
||||
http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dnsmasq config first
|
||||
paths := app.DataManager.GetPaths()
|
||||
if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
|
||||
log.Printf("Failed to update dnsmasq config: %v", err)
|
||||
http.Error(w, "Failed to update dnsmasq config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Restart dnsmasq service
|
||||
if err := app.DnsmasqManager.RestartService(); err != nil {
|
||||
log.Printf("Failed to restart dnsmasq: %v", err)
|
||||
http.Error(w, "Failed to restart dnsmasq service", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "restarted"})
|
||||
}
|
263
experimental/daemon/internal/handlers/handlers.go
Normal file
263
experimental/daemon/internal/handlers/handlers.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"wild-cloud-central/internal/config"
|
||||
"wild-cloud-central/internal/data"
|
||||
"wild-cloud-central/internal/dnsmasq"
|
||||
)
|
||||
|
||||
// App represents the application with its dependencies
|
||||
type App struct {
|
||||
Config *config.Config
|
||||
StartTime time.Time
|
||||
DataManager *data.Manager
|
||||
DnsmasqManager *dnsmasq.ConfigGenerator
|
||||
}
|
||||
|
||||
// NewApp creates a new application instance
|
||||
func NewApp() *App {
|
||||
return &App{
|
||||
StartTime: time.Now(),
|
||||
DataManager: data.NewManager(),
|
||||
DnsmasqManager: dnsmasq.NewConfigGenerator(),
|
||||
}
|
||||
}
|
||||
|
||||
// HealthHandler handles health check requests
|
||||
func (app *App) HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"status": "healthy",
|
||||
"service": "wild-cloud-central",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// StatusHandler handles status requests for the UI
|
||||
func (app *App) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
uptime := time.Since(app.StartTime)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "running",
|
||||
"version": "1.0.0",
|
||||
"uptime": uptime.String(),
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GetConfigHandler handles configuration retrieval requests
|
||||
func (app *App) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Always reload config from file on each request
|
||||
paths := app.DataManager.GetPaths()
|
||||
cfg, err := config.Load(paths.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load config from file: %v", err)
|
||||
response := map[string]interface{}{
|
||||
"configured": false,
|
||||
"message": "No configuration found. Please POST a configuration to /api/v1/config to get started.",
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the cached config with fresh data
|
||||
app.Config = cfg
|
||||
|
||||
// Check if config is empty/uninitialized
|
||||
if cfg.IsEmpty() {
|
||||
response := map[string]interface{}{
|
||||
"configured": false,
|
||||
"message": "Configuration is incomplete. Please complete the setup.",
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"configured": true,
|
||||
"config": cfg,
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// CreateConfigHandler handles configuration creation requests
|
||||
func (app *App) CreateConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow config creation if no config exists
|
||||
if app.Config != nil && !app.Config.IsEmpty() {
|
||||
http.Error(w, "Configuration already exists. Use PUT to update.", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
var newConfig config.Config
|
||||
if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if newConfig.Server.Port == 0 {
|
||||
newConfig.Server.Port = 5055
|
||||
}
|
||||
if newConfig.Server.Host == "" {
|
||||
newConfig.Server.Host = "0.0.0.0"
|
||||
}
|
||||
|
||||
app.Config = &newConfig
|
||||
|
||||
// Persist config to file
|
||||
paths := app.DataManager.GetPaths()
|
||||
if err := config.Save(app.Config, paths.ConfigFile); err != nil {
|
||||
log.Printf("Failed to save config: %v", err)
|
||||
http.Error(w, "Failed to save config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "created"})
|
||||
}
|
||||
|
||||
// UpdateConfigHandler handles configuration update requests
|
||||
func (app *App) UpdateConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if config exists
|
||||
if app.Config == nil || app.Config.IsEmpty() {
|
||||
http.Error(w, "No configuration exists. Use POST to create initial configuration.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var newConfig config.Config
|
||||
if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
app.Config = &newConfig
|
||||
|
||||
// Persist config to file
|
||||
paths := app.DataManager.GetPaths()
|
||||
if err := config.Save(app.Config, paths.ConfigFile); err != nil {
|
||||
log.Printf("Failed to save config: %v", err)
|
||||
http.Error(w, "Failed to save config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Regenerate and apply dnsmasq config
|
||||
if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
|
||||
log.Printf("Failed to update dnsmasq config: %v", err)
|
||||
http.Error(w, "Failed to update dnsmasq config", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// GetConfigYamlHandler handles raw YAML config file retrieval
|
||||
func (app *App) GetConfigYamlHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
paths := app.DataManager.GetPaths()
|
||||
|
||||
// Read the raw config file
|
||||
yamlContent, err := os.ReadFile(paths.ConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "Configuration file not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Printf("Failed to read config file: %v", err)
|
||||
http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write(yamlContent)
|
||||
}
|
||||
|
||||
// UpdateConfigYamlHandler handles raw YAML config file updates
|
||||
func (app *App) UpdateConfigYamlHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the raw YAML content from request body
|
||||
yamlContent, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read request body: %v", err)
|
||||
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
paths := app.DataManager.GetPaths()
|
||||
|
||||
// Write the raw YAML content to file
|
||||
if err := os.WriteFile(paths.ConfigFile, yamlContent, 0644); err != nil {
|
||||
log.Printf("Failed to write config file: %v", err)
|
||||
http.Error(w, "Failed to write configuration file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to reload the config to validate it and update the in-memory config
|
||||
newConfig, err := config.Load(paths.ConfigFile)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Saved YAML config but failed to parse it: %v", err)
|
||||
// File was written but parsing failed - this is a validation warning
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]interface{}{
|
||||
"status": "saved_with_warnings",
|
||||
"warning": "Configuration saved but contains validation errors: " + err.Error(),
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
// Update in-memory config if parsing succeeded
|
||||
app.Config = newConfig
|
||||
|
||||
// Try to regenerate dnsmasq config if the new config is valid
|
||||
if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
|
||||
log.Printf("Warning: Failed to update dnsmasq config: %v", err)
|
||||
// Config was saved but dnsmasq update failed
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := map[string]interface{}{
|
||||
"status": "saved_with_warnings",
|
||||
"warning": "Configuration saved but failed to update dnsmasq config: " + err.Error(),
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
|
||||
}
|
||||
|
||||
// CORSMiddleware adds CORS headers to responses
|
||||
func (app *App) CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
138
experimental/daemon/internal/handlers/pxe.go
Normal file
138
experimental/daemon/internal/handlers/pxe.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// DownloadPXEAssetsHandler handles requests to download PXE boot assets
|
||||
func (app *App) DownloadPXEAssetsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if app.Config == nil || app.Config.IsEmpty() {
|
||||
http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.downloadTalosAssets(); err != nil {
|
||||
log.Printf("Failed to download PXE assets: %v", err)
|
||||
http.Error(w, "Failed to download PXE assets", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "downloaded"})
|
||||
}
|
||||
|
||||
// downloadTalosAssets downloads Talos Linux PXE assets
|
||||
func (app *App) downloadTalosAssets() error {
|
||||
// Get assets directory from data paths
|
||||
paths := app.DataManager.GetPaths()
|
||||
assetsDir := filepath.Join(paths.AssetsDir, "talos")
|
||||
|
||||
log.Printf("Downloading Talos assets to: %s", assetsDir)
|
||||
if err := os.MkdirAll(filepath.Join(assetsDir, "amd64"), 0755); err != nil {
|
||||
return fmt.Errorf("creating assets directory: %w", err)
|
||||
}
|
||||
|
||||
// Create Talos bare metal configuration (schematic format)
|
||||
bareMetalConfig := `customization:
|
||||
extraKernelArgs:
|
||||
- net.ifnames=0
|
||||
systemExtensions:
|
||||
officialExtensions:
|
||||
- siderolabs/gvisor
|
||||
- siderolabs/intel-ucode`
|
||||
|
||||
// Create Talos schematic
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(bareMetalConfig)
|
||||
|
||||
resp, err := http.Post("https://factory.talos.dev/schematics", "text/yaml", &buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating Talos schematic: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var schematic struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&schematic); err != nil {
|
||||
return fmt.Errorf("decoding schematic response: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Created Talos schematic with ID: %s", schematic.ID)
|
||||
|
||||
// Download kernel
|
||||
kernelURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/kernel-amd64",
|
||||
schematic.ID, app.Config.Cluster.Nodes.Talos.Version)
|
||||
if err := downloadFile(kernelURL, filepath.Join(assetsDir, "amd64", "vmlinuz")); err != nil {
|
||||
return fmt.Errorf("downloading kernel: %w", err)
|
||||
}
|
||||
|
||||
// Download initramfs
|
||||
initramfsURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/initramfs-amd64.xz",
|
||||
schematic.ID, app.Config.Cluster.Nodes.Talos.Version)
|
||||
if err := downloadFile(initramfsURL, filepath.Join(assetsDir, "amd64", "initramfs.xz")); err != nil {
|
||||
return fmt.Errorf("downloading initramfs: %w", err)
|
||||
}
|
||||
|
||||
// Create boot.ipxe file
|
||||
bootScript := fmt.Sprintf(`#!ipxe
|
||||
imgfree
|
||||
kernel http://%s/amd64/vmlinuz talos.platform=metal console=tty0 init_on_alloc=1 slab_nomerge pti=on consoleblank=0 nvme_core.io_timeout=4294967295 printk.devkmsg=on ima_template=ima-ng ima_appraise=fix ima_hash=sha512 selinux=1 net.ifnames=0
|
||||
initrd http://%s/amd64/initramfs.xz
|
||||
boot
|
||||
`, app.Config.Cloud.DNS.IP, app.Config.Cloud.DNS.IP)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(assetsDir, "boot.ipxe"), []byte(bootScript), 0644); err != nil {
|
||||
return fmt.Errorf("writing boot script: %w", err)
|
||||
}
|
||||
|
||||
// Download iPXE bootloaders
|
||||
tftpDir := filepath.Join(paths.AssetsDir, "tftp")
|
||||
if err := os.MkdirAll(tftpDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating tftp directory: %w", err)
|
||||
}
|
||||
|
||||
bootloaders := map[string]string{
|
||||
"http://boot.ipxe.org/ipxe.efi": filepath.Join(tftpDir, "ipxe.efi"),
|
||||
"http://boot.ipxe.org/undionly.kpxe": filepath.Join(tftpDir, "undionly.kpxe"),
|
||||
"http://boot.ipxe.org/arm64-efi/ipxe.efi": filepath.Join(tftpDir, "ipxe-arm64.efi"),
|
||||
}
|
||||
|
||||
for url, path := range bootloaders {
|
||||
if err := downloadFile(url, path); err != nil {
|
||||
return fmt.Errorf("downloading %s: %w", url, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Successfully downloaded PXE assets")
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadFile downloads a file from a URL to a local path
|
||||
func downloadFile(url, filepath string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
Reference in New Issue
Block a user