Dnsmasq management endpoints.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/context"
|
"github.com/wild-cloud/wild-central/daemon/internal/context"
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/dnsmasq"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/instance"
|
"github.com/wild-cloud/wild-central/daemon/internal/instance"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
"github.com/wild-cloud/wild-central/daemon/internal/operations"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
"github.com/wild-cloud/wild-central/daemon/internal/secrets"
|
||||||
@@ -27,6 +29,7 @@ type API struct {
|
|||||||
secrets *secrets.Manager
|
secrets *secrets.Manager
|
||||||
context *context.Manager
|
context *context.Manager
|
||||||
instance *instance.Manager
|
instance *instance.Manager
|
||||||
|
dnsmasq *dnsmasq.ConfigGenerator
|
||||||
broadcaster *operations.Broadcaster // SSE broadcaster for operation output
|
broadcaster *operations.Broadcaster // SSE broadcaster for operation output
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +42,13 @@ func NewAPI(dataDir, appsDir string) (*API, error) {
|
|||||||
return nil, fmt.Errorf("failed to create instances directory: %w", err)
|
return nil, fmt.Errorf("failed to create instances directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine dnsmasq config path
|
||||||
|
dnsmasqConfigPath := "/etc/dnsmasq.d/wild-cloud.conf"
|
||||||
|
if os.Getenv("WILD_API_DNSMASQ_CONFIG_PATH") != "" {
|
||||||
|
dnsmasqConfigPath = os.Getenv("WILD_API_DNSMASQ_CONFIG_PATH")
|
||||||
|
log.Printf("Using custom dnsmasq config path: %s", dnsmasqConfigPath)
|
||||||
|
}
|
||||||
|
|
||||||
return &API{
|
return &API{
|
||||||
dataDir: dataDir,
|
dataDir: dataDir,
|
||||||
appsDir: appsDir,
|
appsDir: appsDir,
|
||||||
@@ -46,6 +56,7 @@ func NewAPI(dataDir, appsDir string) (*API, error) {
|
|||||||
secrets: secrets.NewManager(),
|
secrets: secrets.NewManager(),
|
||||||
context: context.NewManager(dataDir),
|
context: context.NewManager(dataDir),
|
||||||
instance: instance.NewManager(dataDir),
|
instance: instance.NewManager(dataDir),
|
||||||
|
dnsmasq: dnsmasq.NewConfigGenerator(dnsmasqConfigPath),
|
||||||
broadcaster: operations.NewBroadcaster(),
|
broadcaster: operations.NewBroadcaster(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -145,6 +156,12 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET")
|
r.HandleFunc("/api/v1/utilities/controlplane/ip", api.UtilitiesControlPlaneIP).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST")
|
r.HandleFunc("/api/v1/utilities/secrets/{secret}/copy", api.UtilitiesSecretCopy).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/utilities/version", api.UtilitiesVersion).Methods("GET")
|
r.HandleFunc("/api/v1/utilities/version", api.UtilitiesVersion).Methods("GET")
|
||||||
|
|
||||||
|
// dnsmasq management
|
||||||
|
r.HandleFunc("/api/v1/dnsmasq/status", api.DnsmasqStatus).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/dnsmasq/config", api.DnsmasqGetConfig).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/dnsmasq/restart", api.DnsmasqRestart).Methods("POST")
|
||||||
|
r.HandleFunc("/api/v1/dnsmasq/update", api.DnsmasqUpdate).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateInstance creates a new instance
|
// CreateInstance creates a new instance
|
||||||
@@ -168,10 +185,19 @@ func (api *API) CreateInstance(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusCreated, map[string]string{
|
// Attempt to update dnsmasq configuration with all instances
|
||||||
|
// This is non-critical - include warning in response if it fails
|
||||||
|
response := map[string]interface{}{
|
||||||
"name": req.Name,
|
"name": req.Name,
|
||||||
"message": "Instance created successfully",
|
"message": "Instance created successfully",
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if err := api.updateDnsmasqForAllInstances(); err != nil {
|
||||||
|
log.Printf("Warning: Could not update dnsmasq configuration: %v", err)
|
||||||
|
response["warning"] = fmt.Sprintf("dnsmasq update failed: %v. Use POST /api/v1/dnsmasq/update to retry.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusCreated, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListInstances lists all instances
|
// ListInstances lists all instances
|
||||||
|
|||||||
99
internal/api/v1/handlers_dnsmasq.go
Normal file
99
internal/api/v1/handlers_dnsmasq.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DnsmasqStatus returns the status of the dnsmasq service
|
||||||
|
func (api *API) DnsmasqStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
status, err := api.dnsmasq.GetStatus()
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get dnsmasq status: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status != "active" {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DnsmasqGetConfig returns the current dnsmasq configuration
|
||||||
|
func (api *API) DnsmasqGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
configContent, err := api.dnsmasq.ReadConfig()
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to read dnsmasq config: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"config_file": api.dnsmasq.GetConfigPath(),
|
||||||
|
"content": configContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DnsmasqRestart restarts the dnsmasq service
|
||||||
|
func (api *API) DnsmasqRestart(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := api.dnsmasq.RestartService(); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to restart dnsmasq: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"message": "dnsmasq service restarted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DnsmasqUpdate regenerates and updates the dnsmasq configuration with all instances
|
||||||
|
func (api *API) DnsmasqUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := api.updateDnsmasqForAllInstances(); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update dnsmasq: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"message": "dnsmasq configuration updated successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDnsmasqForAllInstances helper regenerates dnsmasq config from all instances
|
||||||
|
func (api *API) updateDnsmasqForAllInstances() error {
|
||||||
|
// Get all instances
|
||||||
|
instanceNames, err := api.instance.ListInstances()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listing instances: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load global config
|
||||||
|
globalConfigPath := api.getGlobalConfigPath()
|
||||||
|
globalCfg, err := config.LoadGlobalConfig(globalConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all instance configs
|
||||||
|
var instanceConfigs []config.InstanceConfig
|
||||||
|
for _, name := range instanceNames {
|
||||||
|
instanceConfigPath := api.instance.GetInstanceConfigPath(name)
|
||||||
|
instanceCfg, err := config.LoadCloudConfig(instanceConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Could not load instance config for %s: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instanceConfigs = append(instanceConfigs, *instanceCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate and write dnsmasq config
|
||||||
|
return api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGlobalConfigPath returns the path to the global config file
|
||||||
|
func (api *API) getGlobalConfigPath() string {
|
||||||
|
// This should match the structure from data.Paths
|
||||||
|
return api.dataDir + "/config.yaml"
|
||||||
|
}
|
||||||
@@ -5,16 +5,31 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigGenerator handles dnsmasq configuration generation
|
// ConfigGenerator handles dnsmasq configuration generation
|
||||||
type ConfigGenerator struct{}
|
type ConfigGenerator struct {
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
// NewConfigGenerator creates a new dnsmasq config generator
|
// NewConfigGenerator creates a new dnsmasq config generator
|
||||||
func NewConfigGenerator() *ConfigGenerator {
|
func NewConfigGenerator(configPath string) *ConfigGenerator {
|
||||||
return &ConfigGenerator{}
|
if configPath == "" {
|
||||||
|
configPath = "/etc/dnsmasq.d/wild-cloud.conf"
|
||||||
|
}
|
||||||
|
return &ConfigGenerator{
|
||||||
|
configPath: configPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigPath returns the dnsmasq config file path
|
||||||
|
func (g *ConfigGenerator) GetConfigPath() string {
|
||||||
|
return g.configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates a dnsmasq configuration from the app config
|
// Generate creates a dnsmasq configuration from the app config
|
||||||
@@ -71,3 +86,91 @@ func (g *ConfigGenerator) RestartService() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceStatus represents the status of the dnsmasq service
|
||||||
|
type ServiceStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
PID int `json:"pid"`
|
||||||
|
ConfigFile string `json:"config_file"`
|
||||||
|
InstancesConfigured int `json:"instances_configured"`
|
||||||
|
LastRestart time.Time `json:"last_restart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus checks the status of the dnsmasq service
|
||||||
|
func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) {
|
||||||
|
status := &ServiceStatus{
|
||||||
|
ConfigFile: g.configPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service is active
|
||||||
|
cmd := exec.Command("systemctl", "is-active", "dnsmasq.service")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
status.Status = "inactive"
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
statusStr := strings.TrimSpace(string(output))
|
||||||
|
status.Status = statusStr
|
||||||
|
|
||||||
|
// Get PID if running
|
||||||
|
if statusStr == "active" {
|
||||||
|
cmd = exec.Command("systemctl", "show", "dnsmasq.service", "--property=MainPID")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
parts := strings.Split(strings.TrimSpace(string(output)), "=")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
if pid, err := strconv.Atoi(parts[1]); err == nil {
|
||||||
|
status.PID = pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last restart time
|
||||||
|
cmd = exec.Command("systemctl", "show", "dnsmasq.service", "--property=ActiveEnterTimestamp")
|
||||||
|
output, err = cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
parts := strings.Split(strings.TrimSpace(string(output)), "=")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
// Parse systemd timestamp format
|
||||||
|
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", parts[1]); err == nil {
|
||||||
|
status.LastRestart = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count instances in config
|
||||||
|
if data, err := os.ReadFile(g.configPath); err == nil {
|
||||||
|
// Count "local=/" occurrences (each instance has multiple)
|
||||||
|
count := strings.Count(string(data), "local=/")
|
||||||
|
// Each instance creates 2 "local=/" entries (domain and internal domain)
|
||||||
|
status.InstancesConfigured = count / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig reads the current dnsmasq configuration
|
||||||
|
func (g *ConfigGenerator) ReadConfig() (string, error) {
|
||||||
|
data, err := os.ReadFile(g.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading dnsmasq config: %w", err)
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConfig regenerates and writes the dnsmasq configuration for all instances
|
||||||
|
func (g *ConfigGenerator) UpdateConfig(cfg *config.GlobalConfig, instances []config.InstanceConfig) error {
|
||||||
|
// Generate fresh config from scratch
|
||||||
|
configContent := g.Generate(cfg, instances)
|
||||||
|
|
||||||
|
// Write config
|
||||||
|
log.Printf("Writing dnsmasq config to: %s", g.configPath)
|
||||||
|
if err := os.WriteFile(g.configPath, []byte(configContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("writing dnsmasq config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart service to apply changes
|
||||||
|
return g.RestartService()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user