Experimental gui.
This commit is contained in:
90
experimental/daemon/Makefile
Normal file
90
experimental/daemon/Makefile
Normal file
@@ -0,0 +1,90 @@
|
||||
# Default target
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# Build configuration
|
||||
BINARY_NAME := wild-api
|
||||
VERSION ?= 0.1.1
|
||||
BUILD_DIR := build
|
||||
|
||||
# Go build configuration
|
||||
GO_VERSION := $(shell go version | cut -d' ' -f3)
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
LDFLAGS := -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT) -X main.BuildTime=$(BUILD_TIME)
|
||||
|
||||
.PHONY: help build clean test run install check fmt vet lint deps-check version
|
||||
|
||||
# Usage: $(call package_deb,architecture,binary_name)
|
||||
help:
|
||||
@echo "🏗️ Wild Cloud API Build System"
|
||||
@echo ""
|
||||
@echo "📦 Build targets (compile binaries):"
|
||||
@echo " build - Build for current architecture"
|
||||
@echo ""
|
||||
@echo "🔍 Quality assurance:"
|
||||
@echo " check - Run all checks (fmt + vet + test)"
|
||||
@echo " fmt - Format Go code"
|
||||
@echo " vet - Run go vet"
|
||||
@echo " test - Run tests"
|
||||
@echo ""
|
||||
@echo "🛠️ Development:"
|
||||
@echo " run - Run application locally"
|
||||
@echo " clean - Remove all build artifacts"
|
||||
@echo " deps-check - Verify and tidy dependencies"
|
||||
@echo " version - Show build information"
|
||||
@echo " install - Install to system"
|
||||
@echo ""
|
||||
@echo "📁 Directory structure:"
|
||||
@echo " build/ - Intermediate build artifacts"
|
||||
|
||||
build:
|
||||
@echo "Building $(BINARY_NAME) for current architecture..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) .
|
||||
@echo "✅ Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||
|
||||
clean:
|
||||
@echo "🧹 Cleaning build artifacts..."
|
||||
@rm -rf $(BUILD_DIR) $(DIST_DIR) $(DEB_DIR)-* $(DEB_DIR)
|
||||
@go clean
|
||||
@echo "✅ Clean complete"
|
||||
|
||||
test:
|
||||
@echo "🧪 Running tests..."
|
||||
@go test -v ./...
|
||||
|
||||
run:
|
||||
@echo "🚀 Running $(BINARY_NAME)..."
|
||||
@go run -ldflags="$(LDFLAGS)" .
|
||||
|
||||
# Code quality targets
|
||||
fmt:
|
||||
@echo "🎨 Formatting code..."
|
||||
@go fmt ./...
|
||||
@echo "✅ Format complete"
|
||||
|
||||
vet:
|
||||
@echo "🔍 Running go vet..."
|
||||
@go vet ./...
|
||||
@echo "✅ Vet complete"
|
||||
|
||||
check: fmt vet test
|
||||
@echo "✅ All checks passed"
|
||||
|
||||
# Dependency management
|
||||
deps-check:
|
||||
@echo "📦 Checking dependencies..."
|
||||
@go mod verify
|
||||
@go mod tidy
|
||||
@echo "✅ Dependencies verified"
|
||||
|
||||
# Version information
|
||||
version:
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Git Commit: $(GIT_COMMIT)"
|
||||
@echo "Build Time: $(BUILD_TIME)"
|
||||
@echo "Go Version: $(GO_VERSION)"
|
||||
|
||||
dev:
|
||||
go run . &
|
||||
echo "Server started on http://localhost:5055"
|
1
experimental/daemon/README.md
Normal file
1
experimental/daemon/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Wild-cloud API Backend Service
|
BIN
experimental/daemon/build/wild-api
Executable file
BIN
experimental/daemon/build/wild-api
Executable file
Binary file not shown.
8
experimental/daemon/go.mod
Normal file
8
experimental/daemon/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module wild-cloud-central
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
6
experimental/daemon/go.sum
Normal file
6
experimental/daemon/go.sum
Normal file
@@ -0,0 +1,6 @@
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
94
experimental/daemon/internal/config/config.go
Normal file
94
experimental/daemon/internal/config/config.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config represents the main configuration structure
|
||||
type Config struct {
|
||||
Wildcloud struct {
|
||||
Repository string `yaml:"repository" json:"repository"`
|
||||
CurrentPhase string `yaml:"currentPhase" json:"currentPhase"`
|
||||
CompletedPhases []string `yaml:"completedPhases" json:"completedPhases"`
|
||||
} `yaml:"wildcloud" json:"wildcloud"`
|
||||
Server struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
} `yaml:"server" json:"server"`
|
||||
Cloud struct {
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
InternalDomain string `yaml:"internalDomain" json:"internalDomain"`
|
||||
DNS struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
} `yaml:"dns" json:"dns"`
|
||||
Router struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
} `yaml:"router" json:"router"`
|
||||
DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"`
|
||||
Dnsmasq struct {
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
} `yaml:"dnsmasq" json:"dnsmasq"`
|
||||
} `yaml:"cloud" json:"cloud"`
|
||||
Cluster struct {
|
||||
EndpointIP string `yaml:"endpointIp" json:"endpointIp"`
|
||||
Nodes struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
} `yaml:"nodes" json:"nodes"`
|
||||
} `yaml:"cluster" json:"cluster"`
|
||||
}
|
||||
|
||||
// Load loads configuration from the specified path
|
||||
func Load(configPath string) (*Config, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return nil, fmt.Errorf("parsing config file: %w", err)
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if config.Server.Port == 0 {
|
||||
config.Server.Port = 5055
|
||||
}
|
||||
if config.Server.Host == "" {
|
||||
config.Server.Host = "0.0.0.0"
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Save saves the configuration to the specified path
|
||||
func Save(config *Config, configPath string) error {
|
||||
// Ensure the directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||
return fmt.Errorf("creating config directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling config: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// IsEmpty checks if the configuration is empty or uninitialized
|
||||
func (c *Config) IsEmpty() bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any essential fields are empty
|
||||
return c.Cloud.Domain == "" ||
|
||||
c.Cloud.DNS.IP == "" ||
|
||||
c.Cluster.Nodes.Talos.Version == ""
|
||||
}
|
130
experimental/daemon/internal/data/paths.go
Normal file
130
experimental/daemon/internal/data/paths.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Paths represents the data directory paths configuration
|
||||
type Paths struct {
|
||||
ConfigFile string
|
||||
DataDir string
|
||||
LogsDir string
|
||||
AssetsDir string
|
||||
DnsmasqConf string
|
||||
}
|
||||
|
||||
// Manager handles data directory management
|
||||
type Manager struct {
|
||||
dataDir string
|
||||
isDev bool
|
||||
}
|
||||
|
||||
// NewManager creates a new data manager
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
// Initialize sets up the data directory structure
|
||||
func (m *Manager) Initialize() error {
|
||||
// Detect environment: development vs production
|
||||
m.isDev = m.isDevelopmentMode()
|
||||
|
||||
var dataDir string
|
||||
if m.isDev {
|
||||
// Development mode: use .wildcloud in current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
}
|
||||
dataDir = filepath.Join(cwd, ".wildcloud")
|
||||
log.Printf("Running in development mode, using data directory: %s", dataDir)
|
||||
} else {
|
||||
// Production mode: use standard Linux directories
|
||||
dataDir = "/var/lib/wild-cloud-central"
|
||||
log.Printf("Running in production mode, using data directory: %s", dataDir)
|
||||
}
|
||||
|
||||
m.dataDir = dataDir
|
||||
|
||||
// Create directory structure
|
||||
paths := m.GetPaths()
|
||||
|
||||
// Create all necessary directories
|
||||
for _, dir := range []string{paths.DataDir, paths.LogsDir, paths.AssetsDir} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Data directory structure initialized at: %s", dataDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDevelopmentMode detects if we're running in development mode
|
||||
func (m *Manager) isDevelopmentMode() bool {
|
||||
// Check multiple indicators for development mode
|
||||
|
||||
// 1. Check if GO_ENV is set to development
|
||||
if env := os.Getenv("GO_ENV"); env == "development" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 2. Check if running as systemd service (has INVOCATION_ID)
|
||||
if os.Getenv("INVOCATION_ID") != "" {
|
||||
return false // Running under systemd
|
||||
}
|
||||
|
||||
// 3. Check if running from a typical development location
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
// If executable is in current directory or contains "wild-central" without being in /usr/bin
|
||||
if strings.Contains(exe, "/usr/bin") || strings.Contains(exe, "/usr/local/bin") {
|
||||
return false
|
||||
}
|
||||
if filepath.Base(exe) == "wild-central" && !strings.HasPrefix(exe, "/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check if we can write to /var/lib (if not, probably development)
|
||||
if _, err := os.Stat("/var/lib"); err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 5. Default to development if uncertain
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPaths returns the appropriate paths for the current environment
|
||||
func (m *Manager) GetPaths() Paths {
|
||||
if m.isDev {
|
||||
return Paths{
|
||||
ConfigFile: filepath.Join(m.dataDir, "config.yaml"),
|
||||
DataDir: m.dataDir,
|
||||
LogsDir: filepath.Join(m.dataDir, "logs"),
|
||||
AssetsDir: filepath.Join(m.dataDir, "assets"),
|
||||
DnsmasqConf: filepath.Join(m.dataDir, "dnsmasq.conf"),
|
||||
}
|
||||
} else {
|
||||
return Paths{
|
||||
ConfigFile: "/etc/wild-cloud-central/config.yaml",
|
||||
DataDir: m.dataDir,
|
||||
LogsDir: "/var/log/wild-cloud-central",
|
||||
AssetsDir: "/var/www/html/wild-central",
|
||||
DnsmasqConf: "/etc/dnsmasq.conf",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir returns the current data directory
|
||||
func (m *Manager) GetDataDir() string {
|
||||
return m.dataDir
|
||||
}
|
||||
|
||||
// IsDevelopment returns true if running in development mode
|
||||
func (m *Manager) IsDevelopment() bool {
|
||||
return m.isDev
|
||||
}
|
97
experimental/daemon/internal/dnsmasq/config.go
Normal file
97
experimental/daemon/internal/dnsmasq/config.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"wild-cloud-central/internal/config"
|
||||
)
|
||||
|
||||
// ConfigGenerator handles dnsmasq configuration generation
|
||||
type ConfigGenerator struct{}
|
||||
|
||||
// NewConfigGenerator creates a new dnsmasq config generator
|
||||
func NewConfigGenerator() *ConfigGenerator {
|
||||
return &ConfigGenerator{}
|
||||
}
|
||||
|
||||
// Generate creates a dnsmasq configuration from the app config
|
||||
func (g *ConfigGenerator) Generate(cfg *config.Config) string {
|
||||
template := `# Configuration file for dnsmasq.
|
||||
|
||||
# Basic Settings
|
||||
interface=%s
|
||||
listen-address=%s
|
||||
domain-needed
|
||||
bogus-priv
|
||||
no-resolv
|
||||
|
||||
# DNS Local Resolution - Central server handles these domains authoritatively
|
||||
local=/%s/
|
||||
address=/%s/%s
|
||||
local=/%s/
|
||||
address=/%s/%s
|
||||
server=1.1.1.1
|
||||
server=8.8.8.8
|
||||
|
||||
# --- DHCP Settings ---
|
||||
dhcp-range=%s,12h
|
||||
dhcp-option=3,%s
|
||||
dhcp-option=6,%s
|
||||
|
||||
# --- PXE Booting ---
|
||||
enable-tftp
|
||||
tftp-root=/var/ftpd
|
||||
|
||||
dhcp-match=set:efi-x86_64,option:client-arch,7
|
||||
dhcp-boot=tag:efi-x86_64,ipxe.efi
|
||||
dhcp-boot=tag:!efi-x86_64,undionly.kpxe
|
||||
|
||||
dhcp-match=set:efi-arm64,option:client-arch,11
|
||||
dhcp-boot=tag:efi-arm64,ipxe-arm64.efi
|
||||
|
||||
dhcp-userclass=set:ipxe,iPXE
|
||||
dhcp-boot=tag:ipxe,http://%s/boot.ipxe
|
||||
|
||||
log-queries
|
||||
log-dhcp
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template,
|
||||
cfg.Cloud.Dnsmasq.Interface,
|
||||
cfg.Cloud.DNS.IP,
|
||||
cfg.Cloud.Domain,
|
||||
cfg.Cloud.Domain,
|
||||
cfg.Cluster.EndpointIP,
|
||||
cfg.Cloud.InternalDomain,
|
||||
cfg.Cloud.InternalDomain,
|
||||
cfg.Cluster.EndpointIP,
|
||||
cfg.Cloud.DHCPRange,
|
||||
cfg.Cloud.Router.IP,
|
||||
cfg.Cloud.DNS.IP,
|
||||
cfg.Cloud.DNS.IP,
|
||||
)
|
||||
}
|
||||
|
||||
// WriteConfig writes the dnsmasq configuration to the specified path
|
||||
func (g *ConfigGenerator) WriteConfig(cfg *config.Config, configPath string) error {
|
||||
configContent := g.Generate(cfg)
|
||||
|
||||
log.Printf("Writing dnsmasq config to: %s", configPath)
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
return fmt.Errorf("writing dnsmasq config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartService restarts the dnsmasq service
|
||||
func (g *ConfigGenerator) RestartService() error {
|
||||
cmd := exec.Command("sudo", "/usr/bin/systemctl", "restart", "dnsmasq.service")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to restart dnsmasq: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
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
|
||||
}
|
74
experimental/daemon/main.go
Normal file
74
experimental/daemon/main.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"wild-cloud-central/internal/config"
|
||||
"wild-cloud-central/internal/handlers"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create application instance
|
||||
app := handlers.NewApp()
|
||||
|
||||
// Initialize data directory
|
||||
if err := app.DataManager.Initialize(); err != nil {
|
||||
log.Fatalf("Failed to initialize data directory: %v", err)
|
||||
}
|
||||
|
||||
// Load configuration if it exists
|
||||
paths := app.DataManager.GetPaths()
|
||||
if cfg, err := config.Load(paths.ConfigFile); err != nil {
|
||||
log.Printf("No configuration found, starting with empty config: %v", err)
|
||||
} else {
|
||||
app.Config = cfg
|
||||
log.Printf("Configuration loaded successfully")
|
||||
}
|
||||
|
||||
// Set up HTTP router
|
||||
router := mux.NewRouter()
|
||||
setupRoutes(app, router)
|
||||
|
||||
// Use default server settings if config is empty
|
||||
host := "0.0.0.0"
|
||||
port := 5055
|
||||
if app.Config != nil && app.Config.Server.Host != "" {
|
||||
host = app.Config.Server.Host
|
||||
}
|
||||
if app.Config != nil && app.Config.Server.Port != 0 {
|
||||
port = app.Config.Server.Port
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
log.Printf("Starting wild-cloud-central server on %s", addr)
|
||||
|
||||
if err := http.ListenAndServe(addr, router); err != nil {
|
||||
log.Fatal("Server failed to start:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupRoutes(app *handlers.App, router *mux.Router) {
|
||||
// Add CORS middleware
|
||||
router.Use(app.CORSMiddleware)
|
||||
|
||||
// API v1 routes
|
||||
router.HandleFunc("/api/v1/health", app.HealthHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/config", app.GetConfigHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/config", app.UpdateConfigHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/v1/config", app.CreateConfigHandler).Methods("POST")
|
||||
router.HandleFunc("/api/v1/config/yaml", app.GetConfigYamlHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/config/yaml", app.UpdateConfigYamlHandler).Methods("PUT")
|
||||
router.HandleFunc("/api/v1/dnsmasq/config", app.GetDnsmasqConfigHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/dnsmasq/restart", app.RestartDnsmasqHandler).Methods("POST")
|
||||
router.HandleFunc("/api/v1/pxe/assets", app.DownloadPXEAssetsHandler).Methods("POST")
|
||||
|
||||
// UI-specific endpoints
|
||||
router.HandleFunc("/api/status", app.StatusHandler).Methods("GET")
|
||||
|
||||
// Serve static files
|
||||
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))
|
||||
}
|
39
experimental/daemon/tests/integration/debug-container.sh
Executable file
39
experimental/daemon/tests/integration/debug-container.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🐳 Starting wild-cloud-central debug container..."
|
||||
|
||||
# Build the Docker image if it doesn't exist
|
||||
if ! docker images | grep -q wild-cloud-central-test; then
|
||||
echo "🔨 Building Docker image..."
|
||||
docker build -t wild-cloud-central-test .
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔧 Starting container with shell access..."
|
||||
echo ""
|
||||
echo "📍 Access points:"
|
||||
echo " - Management UI: http://localhost:9080"
|
||||
echo " - API directly: http://localhost:9081"
|
||||
echo ""
|
||||
echo "💡 Inside the container you can:"
|
||||
echo " - Start services manually: /test-installation.sh"
|
||||
echo " - Check logs: journalctl or service status"
|
||||
echo " - Test APIs: curl http://localhost:5055/api/v1/health"
|
||||
echo " - Modify config: nano /etc/wild-cloud-central/config.yaml"
|
||||
echo " - View web files: ls /var/www/html/wild-central/"
|
||||
echo ""
|
||||
|
||||
# Run container with shell access
|
||||
docker run --rm -it \
|
||||
-p 127.0.0.1:9081:5055 \
|
||||
-p 127.0.0.1:9080:80 \
|
||||
-p 127.0.0.1:9053:53/udp \
|
||||
-p 127.0.0.1:9067:67/udp \
|
||||
-p 127.0.0.1:9069:69/udp \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_BIND_SERVICE \
|
||||
--name wild-central-debug \
|
||||
wild-cloud-central-test \
|
||||
/bin/bash
|
69
experimental/daemon/tests/integration/start-background.sh
Executable file
69
experimental/daemon/tests/integration/start-background.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting wild-cloud-central in background..."
|
||||
|
||||
# Build the Docker image if it doesn't exist
|
||||
if ! docker images | grep -q wild-cloud-central-test; then
|
||||
echo "🔨 Building Docker image..."
|
||||
docker build -t wild-cloud-central-test .
|
||||
fi
|
||||
|
||||
# Stop any existing container
|
||||
docker rm -f wild-central-bg 2>/dev/null || true
|
||||
|
||||
echo "🌐 Starting services in background..."
|
||||
|
||||
# Start container in background
|
||||
docker run -d \
|
||||
--name wild-central-bg \
|
||||
-p 127.0.0.1:9081:5055 \
|
||||
-p 127.0.0.1:9080:80 \
|
||||
-p 127.0.0.1:9053:53/udp \
|
||||
-p 127.0.0.1:9067:67/udp \
|
||||
-p 127.0.0.1:9069:69/udp \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_BIND_SERVICE \
|
||||
wild-cloud-central-test \
|
||||
/bin/bash -c '
|
||||
# Start nginx
|
||||
nginx &
|
||||
|
||||
# Start dnsmasq
|
||||
dnsmasq --keep-in-foreground --log-facility=- &
|
||||
|
||||
# Start wild-cloud-central
|
||||
/usr/bin/wild-cloud-central &
|
||||
|
||||
# Wait indefinitely
|
||||
tail -f /dev/null
|
||||
'
|
||||
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 5
|
||||
|
||||
# Test if services are running
|
||||
if curl -s http://localhost:9081/api/v1/health > /dev/null 2>&1; then
|
||||
echo "✅ Services started successfully!"
|
||||
echo ""
|
||||
echo "📍 Access points (localhost only):"
|
||||
echo " - Management UI: http://localhost:9080"
|
||||
echo " - API: http://localhost:9081/api/v1/health"
|
||||
echo " - DNS: localhost:9053 (for testing)"
|
||||
echo " - DHCP: localhost:9067 (for testing)"
|
||||
echo " - TFTP: localhost:9069 (for testing)"
|
||||
echo ""
|
||||
echo "🔧 Container management:"
|
||||
echo " - View logs: docker logs wild-central-bg"
|
||||
echo " - Stop services: docker stop wild-central-bg"
|
||||
echo " - Remove container: docker rm wild-central-bg"
|
||||
echo ""
|
||||
echo "💡 Test commands:"
|
||||
echo " curl http://localhost:9081/api/v1/health"
|
||||
echo " dig @localhost -p 9053 wildcloud.local"
|
||||
echo " curl http://localhost:9081/api/v1/dnsmasq/config"
|
||||
else
|
||||
echo "❌ Services failed to start. Check logs with: docker logs wild-central-bg"
|
||||
exit 1
|
||||
fi
|
92
experimental/daemon/tests/integration/start-interactive.sh
Executable file
92
experimental/daemon/tests/integration/start-interactive.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting wild-cloud-central for interactive testing..."
|
||||
|
||||
# Build the Docker image if it doesn't exist
|
||||
if ! docker images | grep -q wild-cloud-central-test; then
|
||||
echo "🔨 Building Docker image..."
|
||||
docker build -t wild-cloud-central-test .
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🌐 Starting services... This will take a few seconds."
|
||||
echo ""
|
||||
echo "📍 Access points:"
|
||||
echo " - Management UI: http://localhost:9080"
|
||||
echo " - API directly: http://localhost:9081"
|
||||
echo " - Health check: http://localhost:9081/api/v1/health"
|
||||
echo ""
|
||||
echo "🔧 Available API endpoints:"
|
||||
echo " - GET /api/v1/health"
|
||||
echo " - GET /api/v1/config"
|
||||
echo " - PUT /api/v1/config"
|
||||
echo " - GET /api/v1/dnsmasq/config"
|
||||
echo " - POST /api/v1/dnsmasq/restart"
|
||||
echo " - POST /api/v1/pxe/assets"
|
||||
echo ""
|
||||
echo "💡 Example commands to try:"
|
||||
echo " curl http://localhost:9081/api/v1/health"
|
||||
echo " curl http://localhost:9081/api/v1/config"
|
||||
echo " curl http://localhost:9081/api/v1/dnsmasq/config"
|
||||
echo " curl -X POST http://localhost:9081/api/v1/pxe/assets"
|
||||
echo ""
|
||||
echo "🛑 Press Ctrl+C to stop all services"
|
||||
echo ""
|
||||
|
||||
# Create a custom startup script that keeps services running
|
||||
docker run --rm -it \
|
||||
-p 127.0.0.1:9081:5055 \
|
||||
-p 127.0.0.1:9080:80 \
|
||||
-p 127.0.0.1:9053:53/udp \
|
||||
-p 127.0.0.1:9067:67/udp \
|
||||
-p 127.0.0.1:9069:69/udp \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_BIND_SERVICE \
|
||||
--name wild-central-interactive \
|
||||
wild-cloud-central-test \
|
||||
/bin/bash -c '
|
||||
echo "🔧 Starting all services..."
|
||||
|
||||
# Start nginx
|
||||
nginx &
|
||||
NGINX_PID=$!
|
||||
|
||||
# Start dnsmasq
|
||||
dnsmasq --keep-in-foreground --log-facility=- &
|
||||
DNSMASQ_PID=$!
|
||||
|
||||
# Start wild-cloud-central
|
||||
/usr/bin/wild-cloud-central &
|
||||
SERVICE_PID=$!
|
||||
|
||||
# Wait for services to start
|
||||
sleep 3
|
||||
|
||||
echo "✅ All services started!"
|
||||
echo " - nginx (PID: $NGINX_PID)"
|
||||
echo " - dnsmasq (PID: $DNSMASQ_PID)"
|
||||
echo " - wild-cloud-central (PID: $SERVICE_PID)"
|
||||
echo ""
|
||||
echo "🌐 Services are now available:"
|
||||
echo " - Web UI: http://localhost:9080"
|
||||
echo " - API: http://localhost:9081"
|
||||
echo ""
|
||||
|
||||
# Function to handle shutdown
|
||||
shutdown() {
|
||||
echo ""
|
||||
echo "🛑 Shutting down services..."
|
||||
kill $SERVICE_PID $DNSMASQ_PID $NGINX_PID 2>/dev/null || true
|
||||
echo "✅ Shutdown complete."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Set up signal handlers
|
||||
trap shutdown SIGTERM SIGINT
|
||||
|
||||
# Keep container running and wait for signals
|
||||
echo "✨ Container is ready! Press Ctrl+C to stop."
|
||||
wait
|
||||
'
|
11
experimental/daemon/tests/integration/stop-background.sh
Executable file
11
experimental/daemon/tests/integration/stop-background.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🛑 Stopping wild-cloud-central background services..."
|
||||
|
||||
if docker ps | grep -q wild-central-bg; then
|
||||
docker stop wild-central-bg
|
||||
docker rm wild-central-bg
|
||||
echo "✅ Services stopped and container removed."
|
||||
else
|
||||
echo "ℹ️ No background services running."
|
||||
fi
|
20
experimental/daemon/tests/integration/test-docker.sh
Executable file
20
experimental/daemon/tests/integration/test-docker.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🧪 Testing wild-cloud-central Docker installation..."
|
||||
|
||||
# Change to project root directory
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
# Build the Docker image
|
||||
echo "🔨 Building Docker image..."
|
||||
docker build -t wild-cloud-central-test .
|
||||
|
||||
# Run the container to test installation
|
||||
echo "🚀 Running installation test..."
|
||||
echo "Access points after container starts:"
|
||||
echo " - Management UI: http://localhost:9080"
|
||||
echo " - API directly: http://localhost:9055"
|
||||
echo ""
|
||||
docker run --rm -p 9055:5055 -p 9080:80 wild-cloud-central-test
|
146
experimental/daemon/tests/test-installation.sh
Executable file
146
experimental/daemon/tests/test-installation.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Testing wild-cloud-central installation..."
|
||||
|
||||
# Verify the binary was installed
|
||||
echo "✅ Checking binary installation..."
|
||||
if [ -f "/usr/bin/wild-cloud-central" ]; then
|
||||
echo " Binary installed at /usr/bin/wild-cloud-central"
|
||||
else
|
||||
echo "❌ Binary not found at /usr/bin/wild-cloud-central"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify config was installed
|
||||
echo "✅ Checking configuration..."
|
||||
if [ -f "/etc/wild-cloud-central/config.yaml" ]; then
|
||||
echo " Config installed at /etc/wild-cloud-central/config.yaml"
|
||||
else
|
||||
echo "❌ Config not found at /etc/wild-cloud-central/config.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify systemd service file was installed
|
||||
echo "✅ Checking systemd service..."
|
||||
if [ -f "/etc/systemd/system/wild-cloud-central.service" ]; then
|
||||
echo " Service file installed at /etc/systemd/system/wild-cloud-central.service"
|
||||
else
|
||||
echo "❌ Service file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify nginx config was installed
|
||||
echo "✅ Checking nginx configuration..."
|
||||
if [ -f "/etc/nginx/sites-available/wild-central" ]; then
|
||||
echo " Nginx config installed at /etc/nginx/sites-available/wild-central"
|
||||
# Enable the site for testing
|
||||
ln -sf /etc/nginx/sites-available/wild-central /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
else
|
||||
echo "❌ Nginx config not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify web assets were installed
|
||||
echo "✅ Checking web assets..."
|
||||
if [ -f "/var/www/html/wild-central/index.html" ]; then
|
||||
echo " Web assets installed at /var/www/html/wild-central/"
|
||||
else
|
||||
echo "❌ Web assets not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start nginx (simulating systemd)
|
||||
echo "🔧 Starting nginx..."
|
||||
nginx &
|
||||
NGINX_PID=$!
|
||||
|
||||
# Start dnsmasq (simulating systemd)
|
||||
echo "🔧 Starting dnsmasq..."
|
||||
dnsmasq --keep-in-foreground --log-facility=- &
|
||||
DNSMASQ_PID=$!
|
||||
|
||||
# Start wild-cloud-central service (simulating systemd)
|
||||
echo "🔧 Starting wild-cloud-central service..."
|
||||
/usr/bin/wild-cloud-central &
|
||||
SERVICE_PID=$!
|
||||
|
||||
# Wait for service to start
|
||||
echo "⏳ Waiting for services to start..."
|
||||
sleep 5
|
||||
|
||||
# Test health endpoint
|
||||
echo "🩺 Testing health endpoint..."
|
||||
if curl -s http://localhost:5055/api/v1/health | grep -q "healthy"; then
|
||||
echo " ✅ Health check passed"
|
||||
else
|
||||
echo " ❌ Health check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test configuration endpoint
|
||||
echo "🔧 Testing configuration endpoint..."
|
||||
CONFIG_RESPONSE=$(curl -s http://localhost:5055/api/v1/config)
|
||||
if echo "$CONFIG_RESPONSE" | grep -q "Server"; then
|
||||
echo " ✅ Configuration endpoint working"
|
||||
else
|
||||
echo " ❌ Configuration endpoint failed"
|
||||
echo " Response: $CONFIG_RESPONSE"
|
||||
echo " Checking if service is still running..."
|
||||
if kill -0 $SERVICE_PID 2>/dev/null; then
|
||||
echo " Service is running"
|
||||
else
|
||||
echo " Service has died"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test dnsmasq config generation
|
||||
echo "🔧 Testing dnsmasq config generation..."
|
||||
if curl -s http://localhost:5055/api/v1/dnsmasq/config | grep -q "interface"; then
|
||||
echo " ✅ Dnsmasq config generation working"
|
||||
else
|
||||
echo " ❌ Dnsmasq config generation failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test web interface accessibility (through nginx)
|
||||
echo "🌐 Testing web interface..."
|
||||
if curl -s http://localhost:80/ | grep -q "Wild Cloud Central"; then
|
||||
echo " ✅ Web interface accessible through nginx"
|
||||
else
|
||||
echo " ❌ Web interface not accessible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 All installation tests passed!"
|
||||
echo ""
|
||||
echo "Services running:"
|
||||
echo " - wild-cloud-central: http://localhost:5055"
|
||||
echo " - Web interface: http://localhost:80"
|
||||
echo " - API health: http://localhost:5055/api/v1/health"
|
||||
echo ""
|
||||
echo "Installation simulation successful! 🚀"
|
||||
|
||||
# Keep services running for manual testing
|
||||
echo "Services will continue running. Press Ctrl+C to stop."
|
||||
|
||||
# Function to handle shutdown
|
||||
shutdown() {
|
||||
echo ""
|
||||
echo "🛑 Shutting down services..."
|
||||
kill $SERVICE_PID 2>/dev/null || true
|
||||
kill $DNSMASQ_PID 2>/dev/null || true
|
||||
kill $NGINX_PID 2>/dev/null || true
|
||||
echo "Shutdown complete."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Set up signal handlers
|
||||
trap shutdown SIGTERM SIGINT
|
||||
|
||||
# Wait for signals
|
||||
wait
|
Reference in New Issue
Block a user