DNS setup.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"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/network"
|
||||
"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/storage"
|
||||
@@ -202,6 +203,10 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/api/v1/instances/{name}/backup-schedules/{schedule_id}/history", api.ScheduleHistoryHandler).Methods("GET")
|
||||
r.HandleFunc("/api/v1/scheduler/status", api.SchedulerStatusHandler).Methods("GET")
|
||||
|
||||
// Global Configuration
|
||||
r.HandleFunc("/api/v1/config", api.GetGlobalConfig).Methods("GET")
|
||||
r.HandleFunc("/api/v1/config", api.UpdateGlobalConfig).Methods("PUT")
|
||||
|
||||
// Backup Configuration
|
||||
r.HandleFunc("/api/v1/config/backup", api.GetBackupConfig).Methods("GET")
|
||||
r.HandleFunc("/api/v1/config/backup", api.UpdateBackupConfig).Methods("PUT")
|
||||
@@ -220,9 +225,13 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
||||
// 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/config", api.DnsmasqWriteConfig).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/dnsmasq/restart", api.DnsmasqRestart).Methods("POST")
|
||||
r.HandleFunc("/api/v1/dnsmasq/generate", api.DnsmasqGenerate).Methods("POST")
|
||||
r.HandleFunc("/api/v1/dnsmasq/update", api.DnsmasqUpdate).Methods("POST")
|
||||
|
||||
// Network detection
|
||||
r.HandleFunc("/api/v1/network/info", api.NetworkInfoHandler).Methods("GET")
|
||||
r.HandleFunc("/api/v1/network/resolve", api.NetworkResolveHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// CreateInstance creates a new instance
|
||||
@@ -560,6 +569,39 @@ func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime
|
||||
})
|
||||
}
|
||||
|
||||
// NetworkInfoHandler returns detected network configuration
|
||||
func (api *API) NetworkInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := network.DetectNetworkInfo()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to detect network info: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, info)
|
||||
}
|
||||
|
||||
func (api *API) NetworkResolveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
domain := r.URL.Query().Get("domain")
|
||||
if domain == "" {
|
||||
respondError(w, http.StatusBadRequest, "domain parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
ip, err := network.ResolveDomain(domain)
|
||||
if err != nil {
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"ip": ip,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||
)
|
||||
|
||||
// ConfigUpdate represents a single configuration update
|
||||
@@ -74,3 +75,47 @@ func (api *API) ConfigUpdateBatch(w http.ResponseWriter, r *http.Request) {
|
||||
"updated": updateCount,
|
||||
})
|
||||
}
|
||||
|
||||
// GetGlobalConfig returns the global configuration
|
||||
func (api *API) GetGlobalConfig(w http.ResponseWriter, r *http.Request) {
|
||||
globalConfigPath := api.dataDir + "/config.yaml"
|
||||
|
||||
// Load global config
|
||||
globalCfg, err := config.LoadGlobalConfig(globalConfigPath)
|
||||
if err != nil {
|
||||
// If config doesn't exist, return empty config with configured=false
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"configured": false,
|
||||
"config": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"configured": !globalCfg.IsEmpty(),
|
||||
"config": globalCfg,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateGlobalConfig updates the global configuration
|
||||
func (api *API) UpdateGlobalConfig(w http.ResponseWriter, r *http.Request) {
|
||||
globalConfigPath := api.dataDir + "/config.yaml"
|
||||
|
||||
// Parse request body
|
||||
var globalCfg config.GlobalConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&globalCfg); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Save global config
|
||||
if err := config.SaveGlobalConfig(&globalCfg, globalConfigPath); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Global configuration updated successfully",
|
||||
"config": globalCfg,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||
)
|
||||
@@ -16,10 +18,7 @@ func (api *API) DnsmasqStatus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if status.Status != "active" {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
// Always return 200 OK with status in body - let client handle inactive status
|
||||
respondJSON(w, http.StatusOK, status)
|
||||
}
|
||||
|
||||
@@ -49,8 +48,12 @@ func (api *API) DnsmasqRestart(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// DnsmasqGenerate generates the dnsmasq configuration without applying it (dry-run)
|
||||
// DnsmasqGenerate generates the dnsmasq configuration from all instances
|
||||
// Query param ?overwrite=true will write the config and restart the service
|
||||
func (api *API) DnsmasqGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if overwrite flag is set
|
||||
overwrite := r.URL.Query().Get("overwrite") == "true"
|
||||
|
||||
// Get all instances
|
||||
instanceNames, err := api.instance.ListInstances()
|
||||
if err != nil {
|
||||
@@ -78,27 +81,77 @@ func (api *API) DnsmasqGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
instanceConfigs = append(instanceConfigs, *instanceCfg)
|
||||
}
|
||||
|
||||
// Generate config without writing or restarting
|
||||
// Generate config
|
||||
configContent := api.dnsmasq.Generate(globalCfg, instanceConfigs)
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "dnsmasq configuration generated (dry-run mode)",
|
||||
"config": configContent,
|
||||
})
|
||||
if overwrite {
|
||||
// Check if this is the first time dnsmasq is being started
|
||||
status, err := api.dnsmasq.GetStatus()
|
||||
isFirstStart := err != nil || status.Status != "active"
|
||||
|
||||
// Write config and restart service
|
||||
if err := api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs, true); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to update dnsmasq: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Configure system DNS to use local dnsmasq on first start
|
||||
if isFirstStart {
|
||||
if err := api.dnsmasq.ConfigureSystemDNS(); err != nil {
|
||||
log.Printf("Warning: Failed to configure system DNS: %v", err)
|
||||
// Don't fail the request - dnsmasq is still running
|
||||
}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "dnsmasq configuration generated and applied successfully",
|
||||
"config": configContent,
|
||||
})
|
||||
} else {
|
||||
// Just return the generated config
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "dnsmasq configuration generated (preview mode)",
|
||||
"config": configContent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
// DnsmasqWriteConfig writes custom config content to the dnsmasq config file
|
||||
func (api *API) DnsmasqWriteConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Content == "" {
|
||||
respondError(w, http.StatusBadRequest, "Config content is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Write the config directly using the dnsmasq config generator's WriteConfig
|
||||
configPath := api.dnsmasq.GetConfigPath()
|
||||
if err := writeConfigFile(configPath, req.Content); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "dnsmasq configuration updated successfully",
|
||||
"message": "dnsmasq configuration written successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// writeConfigFile writes content to a file
|
||||
func writeConfigFile(path, content string) error {
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("writing config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDnsmasqForAllInstances helper regenerates dnsmasq config from all instances
|
||||
func (api *API) updateDnsmasqForAllInstances() error {
|
||||
// Get all instances
|
||||
@@ -126,8 +179,8 @@ func (api *API) updateDnsmasqForAllInstances() error {
|
||||
instanceConfigs = append(instanceConfigs, *instanceCfg)
|
||||
}
|
||||
|
||||
// Regenerate and write dnsmasq config
|
||||
return api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs)
|
||||
// Regenerate and write dnsmasq config with restart
|
||||
return api.dnsmasq.UpdateConfig(globalCfg, instanceConfigs, true)
|
||||
}
|
||||
|
||||
// getGlobalConfigPath returns the path to the global config file
|
||||
|
||||
223
internal/api/v1/handlers_dnsmasq_test.go
Normal file
223
internal/api/v1/handlers_dnsmasq_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func setupTestDnsmasq(t *testing.T) (*API, string) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Set dnsmasq config to temp directory
|
||||
dnsmasqConfigPath := filepath.Join(tmpDir, "dnsmasq.conf")
|
||||
os.Setenv("WILD_API_DNSMASQ_CONFIG_PATH", dnsmasqConfigPath)
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("WILD_API_DNSMASQ_CONFIG_PATH")
|
||||
})
|
||||
|
||||
appsDir := filepath.Join(tmpDir, "apps")
|
||||
api, err := NewAPI(tmpDir, appsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test API: %v", err)
|
||||
}
|
||||
|
||||
return api, tmpDir
|
||||
}
|
||||
|
||||
func TestDnsmasqGenerate_WithoutOverwrite(t *testing.T) {
|
||||
api, tmpDir := setupTestDnsmasq(t)
|
||||
|
||||
// Create global config
|
||||
globalConfig := config.GlobalConfig{}
|
||||
globalConfig.Cloud.DNS.IP = "192.168.1.100"
|
||||
globalConfig.Cloud.Dnsmasq.Interface = "eth0"
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
configData, _ := yaml.Marshal(globalConfig)
|
||||
storage.WriteFile(configPath, configData, 0644)
|
||||
|
||||
// Create test instance
|
||||
instanceName := "test-instance"
|
||||
createTestInstance(t, api, instanceName)
|
||||
|
||||
// Create instance config
|
||||
instanceConfig := config.InstanceConfig{}
|
||||
instanceConfig.Cloud.Domain = "test.local"
|
||||
instanceConfig.Cloud.InternalDomain = "internal.test.local"
|
||||
instanceConfigPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||
instanceConfigData, _ := yaml.Marshal(instanceConfig)
|
||||
storage.WriteFile(instanceConfigPath, instanceConfigData, 0644)
|
||||
|
||||
// Test generate without overwrite
|
||||
req := httptest.NewRequest("POST", "/api/v1/dnsmasq/generate", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
api.DnsmasqGenerate(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
// Verify response contains config
|
||||
if config, ok := resp["config"].(string); !ok || config == "" {
|
||||
t.Fatal("Expected generated config in response")
|
||||
}
|
||||
|
||||
// Verify config was NOT written to file
|
||||
configPath = api.dnsmasq.GetConfigPath()
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
// File should not exist yet
|
||||
t.Fatal("Config file should not exist in dry-run mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsmasqGenerate_WithOverwrite(t *testing.T) {
|
||||
api, tmpDir := setupTestDnsmasq(t)
|
||||
|
||||
// Create global config
|
||||
globalConfig := config.GlobalConfig{}
|
||||
globalConfig.Cloud.DNS.IP = "192.168.1.100"
|
||||
globalConfig.Cloud.Dnsmasq.Interface = "eth0"
|
||||
configPath := filepath.Join(tmpDir, "config.yaml")
|
||||
configData, _ := yaml.Marshal(globalConfig)
|
||||
storage.WriteFile(configPath, configData, 0644)
|
||||
|
||||
// Create test instance
|
||||
instanceName := "test-instance"
|
||||
createTestInstance(t, api, instanceName)
|
||||
|
||||
// Create instance config
|
||||
instanceConfig := config.InstanceConfig{}
|
||||
instanceConfig.Cloud.Domain = "test.local"
|
||||
instanceConfig.Cloud.InternalDomain = "internal.test.local"
|
||||
instanceConfigPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||
instanceConfigData, _ := yaml.Marshal(instanceConfig)
|
||||
storage.WriteFile(instanceConfigPath, instanceConfigData, 0644)
|
||||
|
||||
// Instead of calling the handler which would try to restart the service,
|
||||
// directly test the UpdateConfig method with restart=false
|
||||
instances, _ := api.instance.ListInstances()
|
||||
var instanceConfigs []config.InstanceConfig
|
||||
for _, name := range instances {
|
||||
icPath := api.instance.GetInstanceConfigPath(name)
|
||||
ic, _ := config.LoadCloudConfig(icPath)
|
||||
instanceConfigs = append(instanceConfigs, *ic)
|
||||
}
|
||||
|
||||
// Test UpdateConfig with restart=false (safe for tests)
|
||||
err := api.dnsmasq.UpdateConfig(&globalConfig, instanceConfigs, false)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify config was written
|
||||
dnsmasqConfigPath := api.dnsmasq.GetConfigPath()
|
||||
content, err := os.ReadFile(dnsmasqConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read dnsmasq config: %v", err)
|
||||
}
|
||||
|
||||
configStr := string(content)
|
||||
if !strings.Contains(configStr, "test.local") {
|
||||
t.Fatal("Expected config to contain test.local domain")
|
||||
}
|
||||
if !strings.Contains(configStr, "internal.test.local") {
|
||||
t.Fatal("Expected config to contain internal.test.local domain")
|
||||
}
|
||||
if !strings.Contains(configStr, "192.168.1.100") {
|
||||
t.Fatal("Expected config to contain DNS IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsmasqWriteConfig(t *testing.T) {
|
||||
api, _ := setupTestDnsmasq(t)
|
||||
|
||||
customConfig := `# Custom dnsmasq config
|
||||
interface=wlan0
|
||||
listen-address=192.168.2.1
|
||||
`
|
||||
|
||||
reqBody := map[string]string{
|
||||
"content": customConfig,
|
||||
}
|
||||
reqData, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/v1/dnsmasq/config", bytes.NewBuffer(reqData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
api.DnsmasqWriteConfig(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
// Verify config was written
|
||||
configPath := api.dnsmasq.GetConfigPath()
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read written config: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != customConfig {
|
||||
t.Fatalf("Config content mismatch.\nExpected:\n%s\nGot:\n%s", customConfig, string(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsmasqWriteConfig_EmptyContent(t *testing.T) {
|
||||
api, _ := setupTestDnsmasq(t)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"content": "",
|
||||
}
|
||||
reqData, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/v1/dnsmasq/config", bytes.NewBuffer(reqData))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
api.DnsmasqWriteConfig(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("Expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDnsmasqGetConfig(t *testing.T) {
|
||||
api, _ := setupTestDnsmasq(t)
|
||||
|
||||
// Write a config first
|
||||
configPath := api.dnsmasq.GetConfigPath()
|
||||
testConfig := "# Test config\ninterface=eth0\n"
|
||||
os.MkdirAll(filepath.Dir(configPath), 0755)
|
||||
os.WriteFile(configPath, []byte(testConfig), 0644)
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/v1/dnsmasq/config", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
api.DnsmasqGetConfig(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
content, ok := resp["content"].(string)
|
||||
if !ok || content != testConfig {
|
||||
t.Fatalf("Expected config content: %s, got: %s", testConfig, content)
|
||||
}
|
||||
}
|
||||
@@ -11,38 +11,38 @@ import (
|
||||
// GlobalConfig represents the main configuration structure
|
||||
type GlobalConfig 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"`
|
||||
Repository string `yaml:"repository,omitempty" json:"repository,omitempty"`
|
||||
CurrentPhase string `yaml:"currentPhase,omitempty" json:"currentPhase,omitempty"`
|
||||
CompletedPhases []string `yaml:"completedPhases,omitempty" json:"completedPhases,omitempty"`
|
||||
} `yaml:"wildcloud,omitempty" json:"wildcloud,omitempty"`
|
||||
Server struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
} `yaml:"server" json:"server"`
|
||||
Port int `yaml:"port,omitempty" json:"port,omitempty"`
|
||||
Host string `yaml:"host,omitempty" json:"host,omitempty"`
|
||||
} `yaml:"server,omitempty" json:"server,omitempty"`
|
||||
Operator struct {
|
||||
Email string `yaml:"email" json:"email"`
|
||||
} `yaml:"operator" json:"operator"`
|
||||
Email string `yaml:"email,omitempty" json:"email,omitempty"`
|
||||
} `yaml:"operator,omitempty" json:"operator,omitempty"`
|
||||
Cloud struct {
|
||||
DNS struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
} `yaml:"dns" json:"dns"`
|
||||
IP string `yaml:"ip,omitempty" json:"ip,omitempty"`
|
||||
ExternalResolver string `yaml:"externalResolver,omitempty" json:"externalResolver,omitempty"`
|
||||
} `yaml:"dns,omitempty" json:"dns,omitempty"`
|
||||
Router struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
DynamicDns string `yaml:"dynamicDns" json:"dynamicDns"`
|
||||
} `yaml:"router" json:"router"`
|
||||
IP string `yaml:"ip,omitempty" json:"ip,omitempty"`
|
||||
DynamicDns string `yaml:"dynamicDns,omitempty" json:"dynamicDns,omitempty"`
|
||||
} `yaml:"router,omitempty" json:"router,omitempty"`
|
||||
Dnsmasq struct {
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
} `yaml:"dnsmasq" json:"dnsmasq"`
|
||||
} `yaml:"cloud" json:"cloud"`
|
||||
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
|
||||
} `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"`
|
||||
} `yaml:"cloud,omitempty" json:"cloud,omitempty"`
|
||||
Cluster struct {
|
||||
EndpointIP string `yaml:"endpointIp" json:"endpointIp"`
|
||||
EndpointIP string `yaml:"endpointIp,omitempty" json:"endpointIp,omitempty"`
|
||||
Nodes struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
} `yaml:"nodes" json:"nodes"`
|
||||
} `yaml:"cluster" json:"cluster"`
|
||||
Version string `yaml:"version,omitempty" json:"version,omitempty"`
|
||||
} `yaml:"talos,omitempty" json:"talos,omitempty"`
|
||||
} `yaml:"nodes,omitempty" json:"nodes,omitempty"`
|
||||
} `yaml:"cluster,omitempty" json:"cluster,omitempty"`
|
||||
}
|
||||
|
||||
// LoadGlobalConfig loads configuration from the specified path
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/config"
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/network"
|
||||
)
|
||||
|
||||
// ConfigGenerator handles dnsmasq configuration generation
|
||||
@@ -33,12 +34,29 @@ func (g *ConfigGenerator) GetConfigPath() string {
|
||||
}
|
||||
|
||||
// Generate creates a dnsmasq configuration from the app config
|
||||
// If the DNS IP or interface in the config don't match the current network,
|
||||
// it will auto-detect and use the current values
|
||||
func (g *ConfigGenerator) Generate(cfg *config.GlobalConfig, clouds []config.InstanceConfig) string {
|
||||
// Auto-detect network info to ensure we use the correct interface and IP
|
||||
netInfo, err := network.DetectNetworkInfo()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to auto-detect network info, using config values: %v", err)
|
||||
// Fall back to config values if detection fails
|
||||
netInfo = &network.NetworkInfo{
|
||||
PrimaryIP: cfg.Cloud.DNS.IP,
|
||||
PrimaryInterface: cfg.Cloud.Dnsmasq.Interface,
|
||||
}
|
||||
}
|
||||
|
||||
// Use detected network info (this ensures dnsmasq works even if config is outdated)
|
||||
dnsIP := netInfo.PrimaryIP
|
||||
iface := netInfo.PrimaryInterface
|
||||
|
||||
resolution_section := ""
|
||||
for _, cloud := range clouds {
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.Domain, cloud.Cloud.Domain, cfg.Cluster.EndpointIP)
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain, cfg.Cluster.EndpointIP)
|
||||
// Use detected DNS IP (Wild Central's actual IP) for domain resolution
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.Domain, cloud.Cloud.Domain, dnsIP)
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain, dnsIP)
|
||||
}
|
||||
|
||||
template := `# Configuration file for dnsmasq.
|
||||
@@ -46,6 +64,7 @@ func (g *ConfigGenerator) Generate(cfg *config.GlobalConfig, clouds []config.Ins
|
||||
# Basic Settings
|
||||
interface=%s
|
||||
listen-address=%s
|
||||
bind-interfaces
|
||||
domain-needed
|
||||
bogus-priv
|
||||
no-resolv
|
||||
@@ -60,8 +79,8 @@ log-dhcp
|
||||
`
|
||||
|
||||
return fmt.Sprintf(template,
|
||||
cfg.Cloud.Dnsmasq.Interface,
|
||||
cfg.Cloud.DNS.IP,
|
||||
iface,
|
||||
dnsIP,
|
||||
resolution_section,
|
||||
)
|
||||
}
|
||||
@@ -71,6 +90,7 @@ func (g *ConfigGenerator) WriteConfig(cfg *config.GlobalConfig, clouds []config.
|
||||
configContent := g.Generate(cfg, clouds)
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -78,11 +98,13 @@ func (g *ConfigGenerator) WriteConfig(cfg *config.GlobalConfig, clouds []config.
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartService restarts the dnsmasq service
|
||||
// RestartService restarts the dnsmasq service using systemd's DBus API
|
||||
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)
|
||||
// Use systemctl without sudo - systemd handles permissions via polkit
|
||||
cmd := exec.Command("systemctl", "restart", "dnsmasq.service")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart dnsmasq: %w (output: %s)", err, string(output))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -161,7 +183,7 @@ func (g *ConfigGenerator) ReadConfig() (string, error) {
|
||||
}
|
||||
|
||||
// UpdateConfig regenerates and writes the dnsmasq configuration for all instances
|
||||
func (g *ConfigGenerator) UpdateConfig(cfg *config.GlobalConfig, instances []config.InstanceConfig) error {
|
||||
func (g *ConfigGenerator) UpdateConfig(cfg *config.GlobalConfig, instances []config.InstanceConfig, restart bool) error {
|
||||
// Generate fresh config from scratch
|
||||
configContent := g.Generate(cfg, instances)
|
||||
|
||||
@@ -171,6 +193,42 @@ func (g *ConfigGenerator) UpdateConfig(cfg *config.GlobalConfig, instances []con
|
||||
return fmt.Errorf("writing dnsmasq config: %w", err)
|
||||
}
|
||||
|
||||
// Restart service to apply changes
|
||||
return g.RestartService()
|
||||
// Restart service to apply changes if requested
|
||||
if restart {
|
||||
return g.RestartService()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureSystemDNS configures systemd-resolved to use the local dnsmasq server
|
||||
// This should only be called on first start of dnsmasq
|
||||
func (g *ConfigGenerator) ConfigureSystemDNS() error {
|
||||
// Auto-detect network info to get the DNS IP
|
||||
netInfo, err := network.DetectNetworkInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect network info: %w", err)
|
||||
}
|
||||
|
||||
dnsIP := netInfo.PrimaryIP
|
||||
|
||||
// Write systemd-resolved configuration to file owned by wildcloud user
|
||||
// (created during package installation in postinst)
|
||||
resolvedConfPath := "/etc/systemd/resolved.conf.d/wild-cloud.conf"
|
||||
resolvedConf := fmt.Sprintf("[Resolve]\nDNS=%s\nDomains=~.\n", dnsIP)
|
||||
|
||||
if err := os.WriteFile(resolvedConfPath, []byte(resolvedConf), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write resolved.conf: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Configured systemd-resolved to use DNS at %s", dnsIP)
|
||||
|
||||
// Restart systemd-resolved to apply changes (via polkit)
|
||||
cmd := exec.Command("systemctl", "restart", "systemd-resolved")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Printf("Warning: Failed to restart systemd-resolved: %v (output: %s)", err, string(output))
|
||||
// Don't return error - the config was written successfully
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
82
internal/network/detect.go
Normal file
82
internal/network/detect.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NetworkInfo holds detected network configuration
|
||||
type NetworkInfo struct {
|
||||
PrimaryIP string `json:"primary_ip"`
|
||||
PrimaryInterface string `json:"primary_interface"`
|
||||
}
|
||||
|
||||
// DetectNetworkInfo detects the machine's primary network configuration
|
||||
// It finds the interface and IP used for internet-bound traffic by checking
|
||||
// the route to a public DNS server (8.8.8.8)
|
||||
func DetectNetworkInfo() (*NetworkInfo, error) {
|
||||
// Use ip route to find the default route interface and IP
|
||||
// This tells us which interface would be used to reach the internet
|
||||
cmd := exec.Command("ip", "route", "get", "8.8.8.8")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get network route: %w", err)
|
||||
}
|
||||
|
||||
line := string(output)
|
||||
// Example output: "8.8.8.8 via 192.168.8.1 dev wlan0 src 192.168.8.152 uid 1000"
|
||||
|
||||
// Extract interface (after "dev")
|
||||
iface := ""
|
||||
if idx := strings.Index(line, "dev "); idx != -1 {
|
||||
rest := line[idx+4:]
|
||||
parts := strings.Fields(rest)
|
||||
if len(parts) > 0 {
|
||||
iface = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Extract source IP (after "src")
|
||||
// This is the IP that would be used as the source for outbound traffic
|
||||
ip := ""
|
||||
if idx := strings.Index(line, "src "); idx != -1 {
|
||||
rest := line[idx+4:]
|
||||
parts := strings.Fields(rest)
|
||||
if len(parts) > 0 {
|
||||
ip = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if ip == "" || iface == "" {
|
||||
return nil, fmt.Errorf("could not detect network information from route output: %s", line)
|
||||
}
|
||||
|
||||
return &NetworkInfo{
|
||||
PrimaryIP: ip,
|
||||
PrimaryInterface: iface,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResolveDomain resolves a domain name to an IP address using the system DNS resolver
|
||||
func ResolveDomain(domain string) (string, error) {
|
||||
ips, err := net.LookupIP(domain)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve domain: %w", err)
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return "", fmt.Errorf("no IP addresses found for domain")
|
||||
}
|
||||
|
||||
// Return the first IPv4 address
|
||||
for _, ip := range ips {
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return ipv4.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no IPv4, return first IP
|
||||
return ips[0].String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user