Compare commits
2 Commits
multiple_p
...
iptables_n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
256afcbd97 | ||
|
|
fe399e2f03 |
25
README.md
25
README.md
@@ -63,6 +63,31 @@ Alternatively, if `gocast_nat=protocol:port` is specified, then GoCast will crea
|
|||||||
|
|
||||||
Example: `gocast_nat=tcp:53` and `gocast_nat=udp:53`
|
Example: `gocast_nat=tcp:53` and `gocast_nat=udp:53`
|
||||||
|
|
||||||
|
## Configuration Reload
|
||||||
|
|
||||||
|
GoCast supports dynamic configuration reloading without service restart. Send a `SIGHUP` signal to reload the configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kill -HUP $(pidof gocast)
|
||||||
|
```
|
||||||
|
|
||||||
|
**What gets reloaded:**
|
||||||
|
- BGP configuration (peers, AS numbers, MD5 passwords, communities)
|
||||||
|
- Application definitions (add/remove/update apps)
|
||||||
|
- Agent settings (Consul, timers, intervals, iptables binary)
|
||||||
|
|
||||||
|
**Important:** Reloading BGP configuration causes existing BGP sessions to be restarted, resulting in brief routing interruption. Routes are automatically re-announced after reload.
|
||||||
|
Consul-discovered apps are not removed during reload.
|
||||||
|
|
||||||
|
## Iptables Configuration
|
||||||
|
|
||||||
|
On modern Linux systems using nftables, you need to configure gocast to use `iptables-nft` instead of the legacy `iptables` binary (default):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
agent:
|
||||||
|
iptables_binary: iptables-nft
|
||||||
|
```
|
||||||
|
|
||||||
## Docker support
|
## Docker support
|
||||||
The docker image at mayuresh82/gocast can be used to run GoCast inside a container. In order for GoCast to manipulate the host network stack correctly, the container needs to run with NET_ADMIN capablity and host mode networking. For example:
|
The docker image at mayuresh82/gocast can be used to run GoCast inside a container. In order for GoCast to manipulate the host network stack correctly, the container needs to run with NET_ADMIN capablity and host mode networking. For example:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ agent:
|
|||||||
consul_query_interval: 5m
|
consul_query_interval: 5m
|
||||||
# token to authenticate client if consul requires it
|
# token to authenticate client if consul requires it
|
||||||
consul_token: 00000000-0000-0000-0000-000000000000
|
consul_token: 00000000-0000-0000-0000-000000000000
|
||||||
|
# iptables binary to use for NAT rules (default: iptables)
|
||||||
|
# Use "iptables-nft" on modern systems with nftables
|
||||||
|
# iptables_binary: iptables-nft
|
||||||
|
|
||||||
bgp:
|
bgp:
|
||||||
local_as: 12345
|
local_as: 12345
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type AgentConfig struct {
|
|||||||
ConsulAddr string `yaml:"consul_addr"`
|
ConsulAddr string `yaml:"consul_addr"`
|
||||||
ConsulQueryInterval time.Duration `yaml:"consul_query_interval"`
|
ConsulQueryInterval time.Duration `yaml:"consul_query_interval"`
|
||||||
ConsulToken string `yaml:"consul_token"`
|
ConsulToken string `yaml:"consul_token"`
|
||||||
|
IptablesBinary string `yaml:"iptables_binary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerConfig struct {
|
type PeerConfig struct {
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ func NewMonitor(config *c.Config) *MonitorMgr {
|
|||||||
if config.Agent.CleanupTimer == 0 {
|
if config.Agent.CleanupTimer == 0 {
|
||||||
config.Agent.CleanupTimer = defaultCleanupTimer
|
config.Agent.CleanupTimer = defaultCleanupTimer
|
||||||
}
|
}
|
||||||
|
// Set iptables binary (defaults to "iptables" if not specified)
|
||||||
|
if config.Agent.IptablesBinary == "" {
|
||||||
|
config.Agent.IptablesBinary = "iptables"
|
||||||
|
}
|
||||||
|
SetIptablesBinary(config.Agent.IptablesBinary)
|
||||||
mon.config = config
|
mon.config = config
|
||||||
// add apps defined in config
|
// add apps defined in config
|
||||||
for _, a := range config.Apps {
|
for _, a := range config.Apps {
|
||||||
@@ -339,6 +344,240 @@ func (m *MonitorMgr) CloseAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload re-reads the configuration file and applies changes
|
||||||
|
func (m *MonitorMgr) Reload(configPath string) error {
|
||||||
|
glog.Infof("Reloading configuration from %s", configPath)
|
||||||
|
|
||||||
|
// Read new configuration
|
||||||
|
newConfig := c.GetConfig(configPath)
|
||||||
|
if newConfig == nil {
|
||||||
|
return fmt.Errorf("Failed to load configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults if not specified
|
||||||
|
if newConfig.Agent.MonitorInterval == 0 {
|
||||||
|
newConfig.Agent.MonitorInterval = defaultMonitorInterval
|
||||||
|
}
|
||||||
|
if newConfig.Agent.CleanupTimer == 0 {
|
||||||
|
newConfig.Agent.CleanupTimer = defaultCleanupTimer
|
||||||
|
}
|
||||||
|
if newConfig.Agent.IptablesBinary == "" {
|
||||||
|
newConfig.Agent.IptablesBinary = "iptables"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update iptables binary if changed
|
||||||
|
if m.config.Agent.IptablesBinary != newConfig.Agent.IptablesBinary {
|
||||||
|
glog.Infof("Iptables binary changed from %s to %s", m.config.Agent.IptablesBinary, newConfig.Agent.IptablesBinary)
|
||||||
|
SetIptablesBinary(newConfig.Agent.IptablesBinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if BGP configuration has changed
|
||||||
|
bgpChanged := m.bgpConfigChanged(m.config.Bgp, newConfig.Bgp)
|
||||||
|
|
||||||
|
if bgpChanged {
|
||||||
|
glog.Infof("BGP configuration changed, restarting BGP controller")
|
||||||
|
|
||||||
|
// Withdraw all current routes before shutting down
|
||||||
|
m.monMu.Lock()
|
||||||
|
for _, am := range m.monitors {
|
||||||
|
if am.announced {
|
||||||
|
if err := m.ctrl.Withdraw(am.app.Vip); err != nil {
|
||||||
|
glog.Errorf("Failed to withdraw route during reload: %v", err)
|
||||||
|
}
|
||||||
|
am.announced = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.monMu.Unlock()
|
||||||
|
|
||||||
|
// Shutdown old BGP controller
|
||||||
|
if err := m.ctrl.Shutdown(); err != nil {
|
||||||
|
glog.Errorf("Failed to shutdown BGP controller: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new BGP controller
|
||||||
|
ctrl, err := NewController(newConfig.Bgp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to start new BGP controller: %v", err)
|
||||||
|
}
|
||||||
|
m.ctrl = ctrl
|
||||||
|
|
||||||
|
// Re-announce all routes with new BGP config
|
||||||
|
m.monMu.Lock()
|
||||||
|
for _, am := range m.monitors {
|
||||||
|
// Only re-announce if the app was previously announced and is still healthy
|
||||||
|
if m.runMonitors(am.app) {
|
||||||
|
if err := m.ctrl.Announce(am.app.Vip); err != nil {
|
||||||
|
glog.Errorf("Failed to re-announce route %s: %v", am.app.Vip.Net.String(), err)
|
||||||
|
} else {
|
||||||
|
am.announced = true
|
||||||
|
glog.Infof("Re-announced route %s", am.app.Vip.Net.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.monMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle consul configuration changes
|
||||||
|
if m.config.Agent.ConsulAddr != newConfig.Agent.ConsulAddr ||
|
||||||
|
m.config.Agent.ConsulToken != newConfig.Agent.ConsulToken {
|
||||||
|
glog.Infof("Consul configuration changed")
|
||||||
|
if newConfig.Agent.ConsulAddr != "" {
|
||||||
|
cmon, err := NewConsulMon(newConfig.Agent.ConsulAddr, newConfig.Agent.ConsulToken)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Failed to start consul monitor: %v", err)
|
||||||
|
} else {
|
||||||
|
m.consul = cmon
|
||||||
|
// Start consul monitoring in background if not already running
|
||||||
|
if m.config.Agent.ConsulAddr == "" {
|
||||||
|
go m.consulMon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update agent configuration
|
||||||
|
oldConfig := m.config
|
||||||
|
m.config = newConfig
|
||||||
|
|
||||||
|
// Handle app configuration changes
|
||||||
|
m.reloadApps(oldConfig.Apps, newConfig.Apps)
|
||||||
|
|
||||||
|
glog.Infof("Configuration reloaded successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bgpConfigChanged checks if BGP configuration has changed
|
||||||
|
func (m *MonitorMgr) bgpConfigChanged(old, new c.BgpConfig) bool {
|
||||||
|
// Check basic parameters
|
||||||
|
if old.LocalAS != new.LocalAS || old.LocalIP != new.LocalIP || old.Origin != new.Origin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check legacy peer config
|
||||||
|
if old.PeerAS != new.PeerAS || old.PeerIP != new.PeerIP {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check peers
|
||||||
|
if len(old.Peers) != len(new.Peers) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare each peer
|
||||||
|
for i := range old.Peers {
|
||||||
|
if old.Peers[i].PeerIP != new.Peers[i].PeerIP ||
|
||||||
|
old.Peers[i].PeerAS != new.Peers[i].PeerAS ||
|
||||||
|
old.Peers[i].MD5Password != new.Peers[i].MD5Password ||
|
||||||
|
old.Peers[i].MD5EnvVar != new.Peers[i].MD5EnvVar {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check multi-hop
|
||||||
|
if (old.Peers[i].MultiHop == nil) != (new.Peers[i].MultiHop == nil) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if old.Peers[i].MultiHop != nil && new.Peers[i].MultiHop != nil {
|
||||||
|
if *old.Peers[i].MultiHop != *new.Peers[i].MultiHop {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check communities
|
||||||
|
if len(old.Peers[i].Communities) != len(new.Peers[i].Communities) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for j := range old.Peers[i].Communities {
|
||||||
|
if old.Peers[i].Communities[j] != new.Peers[i].Communities[j] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check global communities
|
||||||
|
if len(old.Communities) != len(new.Communities) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := range old.Communities {
|
||||||
|
if old.Communities[i] != new.Communities[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// reloadApps compares old and new app configurations and applies changes
|
||||||
|
func (m *MonitorMgr) reloadApps(oldApps, newApps []c.AppConfig) {
|
||||||
|
// Build maps for easy comparison
|
||||||
|
oldAppMap := make(map[string]c.AppConfig)
|
||||||
|
for _, app := range oldApps {
|
||||||
|
oldAppMap[app.Name] = app
|
||||||
|
}
|
||||||
|
|
||||||
|
newAppMap := make(map[string]c.AppConfig)
|
||||||
|
for _, app := range newApps {
|
||||||
|
newAppMap[app.Name] = app
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove apps that are no longer in config
|
||||||
|
for name := range oldAppMap {
|
||||||
|
if _, exists := newAppMap[name]; !exists {
|
||||||
|
m.monMu.Lock()
|
||||||
|
if am, ok := m.monitors[name]; ok {
|
||||||
|
if am.app.Source == "config" {
|
||||||
|
glog.Infof("Removing app %s (no longer in config)", name)
|
||||||
|
m.monMu.Unlock()
|
||||||
|
m.Remove(name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.monMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new apps or update existing ones
|
||||||
|
for name, newAppConfig := range newAppMap {
|
||||||
|
oldAppConfig, existed := oldAppMap[name]
|
||||||
|
|
||||||
|
// Check if app configuration changed
|
||||||
|
configChanged := !existed ||
|
||||||
|
oldAppConfig.Vip != newAppConfig.Vip ||
|
||||||
|
!equalStringSlices(oldAppConfig.Monitors, newAppConfig.Monitors) ||
|
||||||
|
!equalStringSlices(oldAppConfig.Nats, newAppConfig.Nats) ||
|
||||||
|
!equalStringSlices(oldAppConfig.VipConfig.BgpCommunities, newAppConfig.VipConfig.BgpCommunities)
|
||||||
|
|
||||||
|
if configChanged {
|
||||||
|
if existed {
|
||||||
|
glog.Infof("App %s configuration changed, reloading", name)
|
||||||
|
m.Remove(name)
|
||||||
|
} else {
|
||||||
|
glog.Infof("Adding new app %s from config", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := NewApp(newAppConfig.Name, newAppConfig.Vip, newAppConfig.VipConfig,
|
||||||
|
newAppConfig.Monitors, newAppConfig.Nats, "config")
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Failed to add app %s: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.Add(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// equalStringSlices compares two string slices
|
||||||
|
func equalStringSlices(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// CleanUp periodically monitors for stale apps and cleans them up
|
// CleanUp periodically monitors for stale apps and cleans them up
|
||||||
func (m *MonitorMgr) Cleanup(app string, exit chan bool) {
|
func (m *MonitorMgr) Cleanup(app string, exit chan bool) {
|
||||||
t := time.NewTimer(m.config.Agent.CleanupTimer)
|
t := time.NewTimer(m.config.Agent.CleanupTimer)
|
||||||
|
|||||||
293
controller/reload_test.go
Normal file
293
controller/reload_test.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
config "github.com/mayuresh82/gocast/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBgpConfigChanged(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
mon := &MonitorMgr{}
|
||||||
|
|
||||||
|
// Test 1: No changes
|
||||||
|
cfg1 := config.BgpConfig{
|
||||||
|
LocalAS: 12345,
|
||||||
|
LocalIP: "192.168.1.100",
|
||||||
|
Origin: "igp",
|
||||||
|
Peers: []config.PeerConfig{
|
||||||
|
{PeerIP: "10.10.10.1", PeerAS: 6789},
|
||||||
|
},
|
||||||
|
Communities: []string{"100:100"},
|
||||||
|
}
|
||||||
|
cfg2 := cfg1
|
||||||
|
a.False(mon.bgpConfigChanged(cfg1, cfg2), "Identical configs should not be considered changed")
|
||||||
|
|
||||||
|
// Test 2: LocalAS changed
|
||||||
|
cfg3 := cfg1
|
||||||
|
cfg3.LocalAS = 54321
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg3), "LocalAS change should be detected")
|
||||||
|
|
||||||
|
// Test 3: Peer IP changed
|
||||||
|
cfg4 := cfg1
|
||||||
|
cfg4.Peers = []config.PeerConfig{
|
||||||
|
{PeerIP: "10.10.10.2", PeerAS: 6789},
|
||||||
|
}
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg4), "Peer IP change should be detected")
|
||||||
|
|
||||||
|
// Test 4: MD5 password changed
|
||||||
|
cfg5 := cfg1
|
||||||
|
cfg5.Peers = []config.PeerConfig{
|
||||||
|
{PeerIP: "10.10.10.1", PeerAS: 6789, MD5Password: "secret"},
|
||||||
|
}
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg5), "MD5 password change should be detected")
|
||||||
|
|
||||||
|
// Test 5: Community added
|
||||||
|
cfg6 := cfg1
|
||||||
|
cfg6.Communities = []string{"100:100", "200:200"}
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg6), "Community addition should be detected")
|
||||||
|
|
||||||
|
// Test 6: Peer added
|
||||||
|
cfg7 := cfg1
|
||||||
|
cfg7.Peers = []config.PeerConfig{
|
||||||
|
{PeerIP: "10.10.10.1", PeerAS: 6789},
|
||||||
|
{PeerIP: "10.10.10.2", PeerAS: 6789},
|
||||||
|
}
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg7), "Peer addition should be detected")
|
||||||
|
|
||||||
|
// Test 7: MultiHop changed
|
||||||
|
multiHopTrue := true
|
||||||
|
cfg8 := cfg1
|
||||||
|
cfg8.Peers = []config.PeerConfig{
|
||||||
|
{PeerIP: "10.10.10.1", PeerAS: 6789, MultiHop: &multiHopTrue},
|
||||||
|
}
|
||||||
|
a.True(mon.bgpConfigChanged(cfg1, cfg8), "MultiHop change should be detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqualStringSlices(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
a.True(equalStringSlices([]string{}, []string{}), "Empty slices should be equal")
|
||||||
|
a.True(equalStringSlices([]string{"a", "b"}, []string{"a", "b"}), "Identical slices should be equal")
|
||||||
|
a.False(equalStringSlices([]string{"a"}, []string{"a", "b"}), "Different length slices should not be equal")
|
||||||
|
a.False(equalStringSlices([]string{"a", "b"}, []string{"a", "c"}), "Different content should not be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReload(t *testing.T) {
|
||||||
|
if os.Getenv("CI") != "" {
|
||||||
|
t.Skip("Skipping reload test in CI environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
// Create initial config file
|
||||||
|
initialConfig := `
|
||||||
|
agent:
|
||||||
|
listen_addr: :8080
|
||||||
|
monitor_interval: 10s
|
||||||
|
cleanup_timer: 15m
|
||||||
|
|
||||||
|
bgp:
|
||||||
|
local_as: 12345
|
||||||
|
peer_as: 6789
|
||||||
|
local_ip: 192.168.1.100
|
||||||
|
origin: igp
|
||||||
|
communities:
|
||||||
|
- 100:100
|
||||||
|
|
||||||
|
apps:
|
||||||
|
- name: test-app
|
||||||
|
vip: 1.1.1.1/32
|
||||||
|
monitors:
|
||||||
|
- exec:echo
|
||||||
|
`
|
||||||
|
|
||||||
|
// Create temporary config file
|
||||||
|
tmpfile, err := ioutil.TempFile("", "gocast-test-*.yaml")
|
||||||
|
a.NoError(err)
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
_, err = tmpfile.Write([]byte(initialConfig))
|
||||||
|
a.NoError(err)
|
||||||
|
tmpfile.Close()
|
||||||
|
|
||||||
|
// Initialize monitor with initial config
|
||||||
|
conf := config.GetConfig(tmpfile.Name())
|
||||||
|
mon := NewMonitor(conf)
|
||||||
|
defer mon.CloseAll()
|
||||||
|
|
||||||
|
// Wait a bit for initialization
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
a.Equal(12345, mon.ctrl.localAS)
|
||||||
|
m := mon.monitors["test-app"]
|
||||||
|
a.NotNil(m, "Initial app should be loaded")
|
||||||
|
|
||||||
|
// Update config file with new BGP AS and remove app
|
||||||
|
updatedConfig := `
|
||||||
|
agent:
|
||||||
|
listen_addr: :8080
|
||||||
|
monitor_interval: 10s
|
||||||
|
cleanup_timer: 15m
|
||||||
|
|
||||||
|
bgp:
|
||||||
|
local_as: 54321
|
||||||
|
peer_as: 6789
|
||||||
|
local_ip: 192.168.1.100
|
||||||
|
origin: igp
|
||||||
|
communities:
|
||||||
|
- 200:200
|
||||||
|
`
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(tmpfile.Name(), []byte(updatedConfig), 0644)
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Reload configuration
|
||||||
|
err = mon.Reload(tmpfile.Name())
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Wait for reload to complete
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify new state
|
||||||
|
a.Equal(54321, mon.ctrl.localAS)
|
||||||
|
a.Equal([]string{"200:200"}, mon.ctrl.communities)
|
||||||
|
|
||||||
|
// Verify app was removed
|
||||||
|
mon.monMu.Lock()
|
||||||
|
_, exists := mon.monitors["test-app"]
|
||||||
|
mon.monMu.Unlock()
|
||||||
|
a.False(exists, "App should be removed after reload")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadAddApp(t *testing.T) {
|
||||||
|
if os.Getenv("CI") != "" {
|
||||||
|
t.Skip("Skipping reload test in CI environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
// Create initial config without apps
|
||||||
|
initialConfig := `
|
||||||
|
agent:
|
||||||
|
listen_addr: :8080
|
||||||
|
monitor_interval: 10s
|
||||||
|
|
||||||
|
bgp:
|
||||||
|
local_as: 12345
|
||||||
|
peer_as: 6789
|
||||||
|
local_ip: 192.168.1.100
|
||||||
|
origin: igp
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpfile, err := ioutil.TempFile("", "gocast-test-*.yaml")
|
||||||
|
a.NoError(err)
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
_, err = tmpfile.Write([]byte(initialConfig))
|
||||||
|
a.NoError(err)
|
||||||
|
tmpfile.Close()
|
||||||
|
|
||||||
|
conf := config.GetConfig(tmpfile.Name())
|
||||||
|
mon := NewMonitor(conf)
|
||||||
|
defer mon.CloseAll()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify no apps initially
|
||||||
|
mon.monMu.Lock()
|
||||||
|
initialCount := len(mon.monitors)
|
||||||
|
mon.monMu.Unlock()
|
||||||
|
a.Equal(0, initialCount)
|
||||||
|
|
||||||
|
// Add app to config
|
||||||
|
updatedConfig := `
|
||||||
|
agent:
|
||||||
|
listen_addr: :8080
|
||||||
|
monitor_interval: 10s
|
||||||
|
|
||||||
|
bgp:
|
||||||
|
local_as: 12345
|
||||||
|
peer_as: 6789
|
||||||
|
local_ip: 192.168.1.100
|
||||||
|
origin: igp
|
||||||
|
|
||||||
|
apps:
|
||||||
|
- name: new-app
|
||||||
|
vip: 2.2.2.2/32
|
||||||
|
monitors:
|
||||||
|
- exec:echo
|
||||||
|
`
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(tmpfile.Name(), []byte(updatedConfig), 0644)
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
err = mon.Reload(tmpfile.Name())
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify app was added
|
||||||
|
mon.monMu.Lock()
|
||||||
|
_, exists := mon.monitors["new-app"]
|
||||||
|
mon.monMu.Unlock()
|
||||||
|
a.True(exists, "New app should be added after reload")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReloadMD5Change(t *testing.T) {
|
||||||
|
if os.Getenv("CI") != "" {
|
||||||
|
t.Skip("Skipping reload test in CI environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
// Set environment variable for MD5 password
|
||||||
|
os.Setenv("BGP_TEST_PASSWORD", "initial_secret")
|
||||||
|
defer os.Unsetenv("BGP_TEST_PASSWORD")
|
||||||
|
|
||||||
|
initialConfig := `
|
||||||
|
agent:
|
||||||
|
listen_addr: :8080
|
||||||
|
|
||||||
|
bgp:
|
||||||
|
local_as: 12345
|
||||||
|
local_ip: 192.168.1.100
|
||||||
|
peers:
|
||||||
|
- peer_ip: 10.10.10.1
|
||||||
|
peer_as: 6789
|
||||||
|
md5_env_var: BGP_TEST_PASSWORD
|
||||||
|
origin: igp
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpfile, err := ioutil.TempFile("", "gocast-test-*.yaml")
|
||||||
|
a.NoError(err)
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
_, err = tmpfile.Write([]byte(initialConfig))
|
||||||
|
a.NoError(err)
|
||||||
|
tmpfile.Close()
|
||||||
|
|
||||||
|
conf := config.GetConfig(tmpfile.Name())
|
||||||
|
mon := NewMonitor(conf)
|
||||||
|
defer mon.CloseAll()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Update environment variable
|
||||||
|
os.Setenv("BGP_TEST_PASSWORD", "updated_secret")
|
||||||
|
|
||||||
|
// Reload (MD5 env var change should trigger BGP reload)
|
||||||
|
err = mon.Reload(tmpfile.Name())
|
||||||
|
a.NoError(err)
|
||||||
|
|
||||||
|
// Note: We can't easily verify the MD5 password changed without
|
||||||
|
// actually establishing BGP sessions, but we can verify reload succeeded
|
||||||
|
a.NotNil(mon.ctrl)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var execCmd = "bash"
|
var execCmd = "bash"
|
||||||
|
var iptablesBinary = "iptables"
|
||||||
|
|
||||||
func getCmdList(mainCmd string) []string {
|
func getCmdList(mainCmd string) []string {
|
||||||
cmdList := []string{}
|
cmdList := []string{}
|
||||||
@@ -88,8 +89,8 @@ func deleteLoopback(addr *net.IPNet) error {
|
|||||||
|
|
||||||
func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error {
|
func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error {
|
||||||
cmd := fmt.Sprintf(
|
cmd := fmt.Sprintf(
|
||||||
"iptables -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
|
"%s -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
|
||||||
op, protocol, vip.String(), lport, localAddr.String(), dport,
|
iptablesBinary, op, protocol, vip.String(), lport, localAddr.String(), dport,
|
||||||
)
|
)
|
||||||
cmdList := getCmdList(cmd)
|
cmdList := getCmdList(cmd)
|
||||||
_, err := exec.Command(execCmd, cmdList...).Output()
|
_, err := exec.Command(execCmd, cmdList...).Output()
|
||||||
@@ -98,3 +99,10 @@ func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) er
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIptablesBinary sets the iptables binary to use for NAT rules
|
||||||
|
func SetIptablesBinary(binary string) {
|
||||||
|
if binary != "" {
|
||||||
|
iptablesBinary = binary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,71 @@ func TestAddLoopback(t *testing.T) {
|
|||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetIptablesBinary(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
// Save original value
|
||||||
|
originalBinary := iptablesBinary
|
||||||
|
defer func() {
|
||||||
|
iptablesBinary = originalBinary
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Test setting custom binary
|
||||||
|
SetIptablesBinary("iptables-nft")
|
||||||
|
a.Equal("iptables-nft", iptablesBinary)
|
||||||
|
|
||||||
|
// Test setting back to default
|
||||||
|
SetIptablesBinary("iptables")
|
||||||
|
a.Equal("iptables", iptablesBinary)
|
||||||
|
|
||||||
|
// Test that empty string doesn't change the value
|
||||||
|
SetIptablesBinary("iptables-custom")
|
||||||
|
a.Equal("iptables-custom", iptablesBinary)
|
||||||
|
SetIptablesBinary("")
|
||||||
|
a.Equal("iptables-custom", iptablesBinary, "Empty string should not change the binary")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNatRuleCommandFormat(t *testing.T) {
|
||||||
|
a := assert.New(t)
|
||||||
|
|
||||||
|
// Save original values
|
||||||
|
originalBinary := iptablesBinary
|
||||||
|
originalExecCmd := execCmd
|
||||||
|
defer func() {
|
||||||
|
iptablesBinary = originalBinary
|
||||||
|
execCmd = originalExecCmd
|
||||||
|
}()
|
||||||
|
|
||||||
|
vip := net.ParseIP("192.0.2.1")
|
||||||
|
localAddr := net.ParseIP("10.0.0.1")
|
||||||
|
|
||||||
|
// Test with default iptables
|
||||||
|
SetIptablesBinary("iptables")
|
||||||
|
err := natRule("A", vip, localAddr, "tcp", "80", "8080")
|
||||||
|
// We expect this to fail in test environment, but we can check the error message
|
||||||
|
// contains our command
|
||||||
|
if err != nil {
|
||||||
|
a.Contains(err.Error(), "iptables -t nat -A PREROUTING")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with iptables-nft
|
||||||
|
SetIptablesBinary("iptables-nft")
|
||||||
|
err = natRule("A", vip, localAddr, "tcp", "80", "8080")
|
||||||
|
if err != nil {
|
||||||
|
a.Contains(err.Error(), "iptables-nft -t nat -A PREROUTING")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with custom binary path
|
||||||
|
SetIptablesBinary("/usr/sbin/iptables")
|
||||||
|
err = natRule("D", vip, localAddr, "udp", "53", "5353")
|
||||||
|
if err != nil {
|
||||||
|
a.Contains(err.Error(), "/usr/sbin/iptables -t nat -D PREROUTING")
|
||||||
|
a.Contains(err.Error(), "udp")
|
||||||
|
a.Contains(err.Error(), "53")
|
||||||
|
a.Contains(err.Error(), "5353")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
switch os.Getenv("test_name") {
|
switch os.Getenv("test_name") {
|
||||||
case "test_gateway":
|
case "test_gateway":
|
||||||
|
|||||||
11
main.go
11
main.go
@@ -33,7 +33,16 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
sig := <-signalChan
|
sig := <-signalChan
|
||||||
if sig == os.Interrupt || sig == syscall.SIGTERM {
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
log.Info("Received SIGHUP, reloading configuration")
|
||||||
|
if err := mon.Reload(*config); err != nil {
|
||||||
|
log.Errorf("Failed to reload configuration: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Configuration reloaded successfully")
|
||||||
|
}
|
||||||
|
case os.Interrupt, syscall.SIGTERM:
|
||||||
|
log.Info("Received shutdown signal, cleaning up")
|
||||||
mon.CloseAll()
|
mon.CloseAll()
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user