Compare commits
2 Commits
config_rel
...
ipv6_suppo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc61f713a | ||
|
|
645b10548a |
19
README.md
19
README.md
@@ -5,9 +5,6 @@ The most common use case for this is anycast (vip) based load balancing for infr
|
|||||||
|
|
||||||
For some practical examples and more details, check out this blog post : https://mayuresh82.github.io/2020/11/28/automatic_service_discovery_anycast/
|
For some practical examples and more details, check out this blog post : https://mayuresh82.github.io/2020/11/28/automatic_service_discovery_anycast/
|
||||||
|
|
||||||
# Looking for code reviewers
|
|
||||||
If you are interested in being a reviewer and/or co-maintainer, please reach out to @mayuresh82 !
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Use the docker container at mayuresh82/gocast or compile from source:
|
Use the docker container at mayuresh82/gocast or compile from source:
|
||||||
|
|
||||||
@@ -63,22 +60,6 @@ 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)
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## 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:
|
||||||
```
|
```
|
||||||
|
|||||||
29
config.yaml
29
config.yaml
@@ -11,33 +11,25 @@ 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
|
||||||
|
# this tag must be present in consul for Gocast to pickup the service
|
||||||
|
# by default its enable_gocast
|
||||||
|
consul_match_tag: enable_gocast
|
||||||
|
|
||||||
bgp:
|
bgp:
|
||||||
local_as: 12345
|
local_as: 12345
|
||||||
remote_as: 6789
|
remote_as: 6789
|
||||||
# override the peer IP to use instead of auto discovering
|
# override the peer IP to use instead of auto discovering
|
||||||
peer_ip: 10.10.10.1
|
peer_ip: 10.10.10.1
|
||||||
|
|
||||||
# Alternatively, define multiple BGP peers for redundancy
|
|
||||||
#peers:
|
|
||||||
# - peer_ip: 10.10.10.1
|
|
||||||
# peer_as: 6789
|
|
||||||
# communities:
|
|
||||||
# - 100:100
|
|
||||||
# - 200:200
|
|
||||||
# md5_env_var: GOCAST_BGP_PEER1_PASSWORD # optional. Set via: export GOCAST_BGP_PEER1_PASSWORD="secret"
|
|
||||||
# - peer_ip: 10.10.10.2
|
|
||||||
# peer_as: 6789
|
|
||||||
# communities:
|
|
||||||
# - 100:101
|
|
||||||
# - 200:201
|
|
||||||
# multi_hop: true # optional
|
|
||||||
# md5_password: "secret123" # optional
|
|
||||||
|
|
||||||
communities:
|
communities:
|
||||||
- asn:nnnn
|
- asn:nnnn
|
||||||
- asn:nnnn
|
- asn:nnnn
|
||||||
origin: igp
|
origin: igp
|
||||||
|
# family will determine whether we want to peer over an ipv4 or ipv6 session
|
||||||
|
# one gocast instance can only peer over one session. Multiple sessions are not
|
||||||
|
# supported. The default is 4
|
||||||
|
family: 4
|
||||||
|
# router ID is required when family 6 is specified
|
||||||
|
router_id: 1.1.1.1
|
||||||
|
|
||||||
# optional list of apps to register on startup
|
# optional list of apps to register on startup
|
||||||
apps:
|
apps:
|
||||||
@@ -46,5 +38,4 @@ apps:
|
|||||||
vip_config:
|
vip_config:
|
||||||
# additional per VIP BGP communities
|
# additional per VIP BGP communities
|
||||||
bgp_communities: [ aaaa:bbbb ]
|
bgp_communities: [ aaaa:bbbb ]
|
||||||
monitors:
|
monitor: port:tcp:5000
|
||||||
- port:tcp:5000
|
|
||||||
|
|||||||
@@ -16,27 +16,18 @@ 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"`
|
||||||
}
|
ConsulMatchTag string `yaml:"consul_match_tag"`
|
||||||
|
|
||||||
type PeerConfig struct {
|
|
||||||
PeerIP string `yaml:"peer_ip"`
|
|
||||||
PeerAS int `yaml:"peer_as"`
|
|
||||||
MultiHop *bool `yaml:"multi_hop,omitempty"`
|
|
||||||
Communities []string `yaml:"communities,omitempty"`
|
|
||||||
MD5Password string `yaml:"md5_password,omitempty"`
|
|
||||||
MD5EnvVar string `yaml:"md5_env_var,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BgpConfig struct {
|
type BgpConfig struct {
|
||||||
LocalAS int `yaml:"local_as"`
|
LocalAS int `yaml:"local_as"`
|
||||||
|
PeerAS int `yaml:"peer_as"`
|
||||||
LocalIP string `yaml:"local_ip"`
|
LocalIP string `yaml:"local_ip"`
|
||||||
// Legacy single-peer config (deprecated but supported for backward compatibility)
|
PeerIP string `yaml:"peer_ip"`
|
||||||
PeerAS int `yaml:"peer_as,omitempty"`
|
|
||||||
PeerIP string `yaml:"peer_ip,omitempty"`
|
|
||||||
// New multi-peer config
|
|
||||||
Peers []PeerConfig `yaml:"peers,omitempty"`
|
|
||||||
Communities []string
|
Communities []string
|
||||||
Origin string
|
Origin string
|
||||||
|
Family int
|
||||||
|
RouterID string `yaml:"router_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VipConfig struct {
|
type VipConfig struct {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -21,134 +20,92 @@ type Route struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
localAS int
|
peerAS int
|
||||||
localIP net.IP
|
localIP, peerIP net.IP
|
||||||
peers []c.PeerConfig
|
|
||||||
communities []string
|
communities []string
|
||||||
origin uint32
|
origin uint32
|
||||||
|
multiHop bool
|
||||||
s *gobgp.BgpServer
|
s *gobgp.BgpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(config c.BgpConfig) (*Controller, error) {
|
func NewController(config c.BgpConfig) (*Controller, error) {
|
||||||
ctrl := &Controller{}
|
c := &Controller{}
|
||||||
var gw net.IP
|
var gw net.IP
|
||||||
var err error
|
var err error
|
||||||
|
if config.Family == 0 {
|
||||||
// Normalize config: convert legacy single-peer to new multi-peer format
|
config.Family = 4
|
||||||
peers := config.Peers
|
}
|
||||||
if len(peers) == 0 {
|
if config.PeerIP == "" {
|
||||||
// Backward compatibility: convert legacy config
|
gw, err := gateway(config.Family)
|
||||||
if config.PeerIP != "" {
|
if err != nil {
|
||||||
// Explicit peer IP configured
|
return nil, fmt.Errorf("unable to get gw ip: %v", err)
|
||||||
peers = []c.PeerConfig{{
|
}
|
||||||
PeerIP: config.PeerIP,
|
c.peerIP = gw
|
||||||
PeerAS: config.PeerAS,
|
|
||||||
}}
|
|
||||||
} else {
|
} else {
|
||||||
// No peer IP configured - use default gateway
|
c.peerIP = net.ParseIP(config.PeerIP)
|
||||||
gw, err = gateway()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to get gateway ip: %v", err)
|
|
||||||
}
|
}
|
||||||
peers = []c.PeerConfig{{
|
|
||||||
PeerIP: gw.String(),
|
|
||||||
PeerAS: config.PeerAS,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine local IP
|
|
||||||
if config.LocalIP == "" {
|
if config.LocalIP == "" {
|
||||||
// Use first peer to determine local IP
|
gw, err = via(c.peerIP)
|
||||||
firstPeerIP := net.ParseIP(peers[0].PeerIP)
|
|
||||||
if firstPeerIP == nil {
|
|
||||||
gw, err = gateway()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to get gw ip: %v", err)
|
return nil, fmt.Errorf("unable to get gw ip: %v", err)
|
||||||
}
|
}
|
||||||
firstPeerIP = gw
|
c.localIP, err = localAddress(gw)
|
||||||
}
|
|
||||||
gw, err = via(firstPeerIP)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to get gw ip: %v", err)
|
|
||||||
}
|
|
||||||
ctrl.localIP, err = localAddress(gw)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctrl.localIP = net.ParseIP(config.LocalIP)
|
c.localIP = net.ParseIP(config.LocalIP)
|
||||||
}
|
}
|
||||||
|
c.communities = config.Communities
|
||||||
ctrl.localAS = config.LocalAS
|
|
||||||
ctrl.peers = peers
|
|
||||||
ctrl.communities = config.Communities
|
|
||||||
|
|
||||||
switch config.Origin {
|
switch config.Origin {
|
||||||
case "igp":
|
case "igp":
|
||||||
ctrl.origin = 0
|
c.origin = 0
|
||||||
case "egp":
|
case "egp":
|
||||||
ctrl.origin = 1
|
c.origin = 1
|
||||||
case "unknown":
|
case "unknown":
|
||||||
ctrl.origin = 2
|
c.origin = 2
|
||||||
|
}
|
||||||
|
var rid string
|
||||||
|
if config.Family == 4 {
|
||||||
|
rid = c.localIP.String()
|
||||||
|
}
|
||||||
|
if config.RouterID != "" {
|
||||||
|
rid = config.RouterID
|
||||||
}
|
}
|
||||||
|
|
||||||
s := gobgp.NewBgpServer()
|
s := gobgp.NewBgpServer()
|
||||||
go s.Serve()
|
go s.Serve()
|
||||||
if err := s.StartBgp(context.Background(), &api.StartBgpRequest{
|
if err := s.StartBgp(context.Background(), &api.StartBgpRequest{
|
||||||
Global: &api.Global{
|
Global: &api.Global{
|
||||||
As: uint32(config.LocalAS),
|
As: uint32(config.LocalAS),
|
||||||
RouterId: ctrl.localIP.String(),
|
RouterId: rid,
|
||||||
ListenPort: -1, // gobgp won't listen on tcp:179
|
ListenPort: -1, // gobgp won't listen on tcp:179
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("Unable to start bgp: %v", err)
|
return nil, fmt.Errorf("unable to start bgp: %v", err)
|
||||||
}
|
}
|
||||||
ctrl.s = s
|
c.s = s
|
||||||
|
c.peerAS = config.PeerAS
|
||||||
return ctrl, nil
|
// set mh by default for all ebgp peers
|
||||||
|
if c.peerAS != config.LocalAS {
|
||||||
|
c.multiHop = true
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) addPeer(peer *c.PeerConfig) error {
|
func (c *Controller) AddPeer(peer string) error {
|
||||||
n := &api.Peer{
|
n := &api.Peer{
|
||||||
Conf: &api.PeerConf{
|
Conf: &api.PeerConf{
|
||||||
NeighborAddress: peer.PeerIP,
|
NeighborAddress: peer,
|
||||||
PeerAs: uint32(peer.PeerAS),
|
PeerAs: uint32(c.peerAS),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if c.multiHop {
|
||||||
// Enable multihop only if explicitly configured
|
|
||||||
if peer.MultiHop != nil && *peer.MultiHop {
|
|
||||||
n.EbgpMultihop = &api.EbgpMultihop{Enabled: true, MultihopTtl: uint32(255)}
|
n.EbgpMultihop = &api.EbgpMultihop{Enabled: true, MultihopTtl: uint32(255)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure MD5 authentication if specified
|
|
||||||
md5Password := c.getMD5Password(peer)
|
|
||||||
if md5Password != "" {
|
|
||||||
n.Conf.AuthPassword = md5Password
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n})
|
return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMD5Password retrieves the MD5 password from config or environment variable
|
func (c *Controller) getApiPath(route *Route) *api.Path {
|
||||||
func (c *Controller) getMD5Password(peer *c.PeerConfig) string {
|
|
||||||
// Priority 1: Check for environment variable
|
|
||||||
if peer.MD5EnvVar != "" {
|
|
||||||
if password := os.Getenv(peer.MD5EnvVar); password != "" {
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Use password from config file
|
|
||||||
if peer.MD5Password != "" {
|
|
||||||
return peer.MD5Password
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
|
|
||||||
afi := api.Family_AFI_IP
|
afi := api.Family_AFI_IP
|
||||||
if route.Net.IP.To4() == nil {
|
if route.Net.IP.To4() == nil {
|
||||||
afi = api.Family_AFI_IP6
|
afi = api.Family_AFI_IP6
|
||||||
@@ -164,15 +121,8 @@ func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
|
|||||||
a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{
|
a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{
|
||||||
NextHop: c.localIP.String(),
|
NextHop: c.localIP.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Merge communities: global + per-peer + per-route
|
|
||||||
var allCommunities []string
|
|
||||||
allCommunities = append(allCommunities, c.communities...)
|
|
||||||
allCommunities = append(allCommunities, peer.Communities...)
|
|
||||||
allCommunities = append(allCommunities, route.Communities...)
|
|
||||||
|
|
||||||
var communities []uint32
|
var communities []uint32
|
||||||
for _, comm := range allCommunities {
|
for _, comm := range append(c.communities, route.Communities...) {
|
||||||
communities = append(communities, convertCommunity(comm))
|
communities = append(communities, convertCommunity(comm))
|
||||||
}
|
}
|
||||||
a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
|
a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
|
||||||
@@ -187,105 +137,49 @@ func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) Announce(route *Route) error {
|
func (c *Controller) Announce(route *Route) error {
|
||||||
var errs []error
|
|
||||||
|
|
||||||
for i := range c.peers {
|
|
||||||
peer := &c.peers[i]
|
|
||||||
|
|
||||||
// Check if peer exists
|
|
||||||
var found bool
|
var found bool
|
||||||
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
||||||
if p.Conf.NeighborAddress == peer.PeerIP {
|
if p.Conf.NeighborAddress == c.peerIP.String() {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("peer %s: list error: %v", peer.PeerIP, err))
|
return err
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add peer if not found
|
|
||||||
if !found {
|
if !found {
|
||||||
if err := c.addPeer(peer); err != nil {
|
if err := c.AddPeer(c.peerIP.String()); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("peer %s: add error: %v", peer.PeerIP, err))
|
return err
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_, err = c.s.AddPath(context.Background(), &api.AddPathRequest{Path: c.getApiPath(route)})
|
||||||
// Announce route to this peer
|
return err
|
||||||
path := c.getApiPath(route, peer)
|
|
||||||
if _, err := c.s.AddPath(context.Background(), &api.AddPathRequest{Path: path}); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("peer %s: announce error: %v", peer.PeerIP, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return aggregated errors if any peer failed
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("announcement errors: %v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) Withdraw(route *Route) error {
|
func (c *Controller) Withdraw(route *Route) error {
|
||||||
var errs []error
|
return c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: c.getApiPath(route)})
|
||||||
|
|
||||||
for i := range c.peers {
|
|
||||||
peer := &c.peers[i]
|
|
||||||
path := c.getApiPath(route, peer)
|
|
||||||
if err := c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: path}); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("peer %s: withdraw error: %v", peer.PeerIP, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return aggregated errors if any peer failed
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("withdrawal errors: %v", errs)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) PeerInfo() ([]*api.Peer, error) {
|
func (c *Controller) PeerInfo() (*api.Peer, error) {
|
||||||
var peers []*api.Peer
|
var peer *api.Peer
|
||||||
peerMap := make(map[string]bool)
|
|
||||||
|
|
||||||
// Build map of configured peer IPs
|
|
||||||
for _, peer := range c.peers {
|
|
||||||
peerMap[peer.PeerIP] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect info for all configured peers
|
|
||||||
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
||||||
if peerMap[p.Conf.NeighborAddress] {
|
if p.Conf.NeighborAddress == c.peerIP.String() {
|
||||||
peers = append(peers, p)
|
peer = p
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return peers, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) Shutdown() error {
|
func (c *Controller) Shutdown() error {
|
||||||
var errs []error
|
|
||||||
|
|
||||||
// Shutdown all peer sessions
|
|
||||||
for _, peer := range c.peers {
|
|
||||||
if err := c.s.ShutdownPeer(context.Background(), &api.ShutdownPeerRequest{
|
if err := c.s.ShutdownPeer(context.Background(), &api.ShutdownPeerRequest{
|
||||||
Address: peer.PeerIP,
|
Address: c.peerIP.String(),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("peer %s: shutdown error: %v", peer.PeerIP, err))
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Stop BGP server
|
|
||||||
if err := c.s.StopBgp(context.Background(), &api.StopBgpRequest{}); err != nil {
|
if err := c.s.StopBgp(context.Background(), &api.StopBgpRequest{}); err != nil {
|
||||||
errs = append(errs, fmt.Errorf("stop bgp error: %v", err))
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) > 0 {
|
|
||||||
return fmt.Errorf("shutdown errors: %v", errs)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,578 +102,3 @@ func TestBgpNew(t *testing.T) {
|
|||||||
a.Equal("20.30.40.0/24", path)
|
a.Equal("20.30.40.0/24", path)
|
||||||
ctrl.Shutdown()
|
ctrl.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLegacyConfigConversion(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Test legacy single-peer config
|
|
||||||
legacyConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
PeerAS: 22222,
|
|
||||||
PeerIP: "10.10.10.1",
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
Communities: []string{"100:100"},
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(legacyConfig)
|
|
||||||
if err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Verify legacy config was converted to multi-peer format
|
|
||||||
a.Equal(1, len(ctrl.peers), "Should have exactly 1 peer")
|
|
||||||
a.Equal("10.10.10.1", ctrl.peers[0].PeerIP)
|
|
||||||
a.Equal(22222, ctrl.peers[0].PeerAS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiPeerConfig(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Test new multi-peer config
|
|
||||||
multiPeerConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
Peers: []config.PeerConfig{
|
|
||||||
{PeerIP: "10.10.10.1", PeerAS: 22222},
|
|
||||||
{PeerIP: "10.10.10.2", PeerAS: 22222},
|
|
||||||
},
|
|
||||||
Communities: []string{"100:100"},
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(multiPeerConfig)
|
|
||||||
if err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Verify both peers are configured
|
|
||||||
a.Equal(2, len(ctrl.peers), "Should have exactly 2 peers")
|
|
||||||
a.Equal("10.10.10.1", ctrl.peers[0].PeerIP)
|
|
||||||
a.Equal("10.10.10.2", ctrl.peers[1].PeerIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultGatewayPeer(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Test config with no peer_ip - should use default gateway
|
|
||||||
defaultGatewayConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
PeerAS: 22222,
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(defaultGatewayConfig)
|
|
||||||
a.NoError(err, "Should not error when peer_ip is not specified")
|
|
||||||
if err == nil {
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Verify a peer was configured using gateway
|
|
||||||
a.Equal(1, len(ctrl.peers), "Should have exactly 1 peer")
|
|
||||||
a.NotEmpty(ctrl.peers[0].PeerIP, "Peer IP should be set from gateway")
|
|
||||||
a.Equal(22222, ctrl.peers[0].PeerAS, "Peer AS should match config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommunityMerging(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
ctrl := &Controller{
|
|
||||||
localAS: 11111,
|
|
||||||
localIP: net.ParseIP("192.168.1.100"),
|
|
||||||
communities: []string{"1000:1000", "2000:2000"}, // Global
|
|
||||||
origin: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
peer := &config.PeerConfig{
|
|
||||||
PeerIP: "10.10.10.1",
|
|
||||||
PeerAS: 22222,
|
|
||||||
Communities: []string{"100:100", "200:200"}, // Per-peer
|
|
||||||
}
|
|
||||||
|
|
||||||
route := &Route{
|
|
||||||
Net: &net.IPNet{
|
|
||||||
IP: net.ParseIP("20.30.40.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
Communities: []string{"5000:5000"}, // Per-route
|
|
||||||
}
|
|
||||||
|
|
||||||
path := ctrl.getApiPath(route, peer)
|
|
||||||
|
|
||||||
// Extract communities from path
|
|
||||||
var commAttr *api.CommunitiesAttribute
|
|
||||||
for _, attr := range path.Pattrs {
|
|
||||||
var dynAny ptypes.DynamicAny
|
|
||||||
if err := ptypes.UnmarshalAny(attr, &dynAny); err == nil {
|
|
||||||
if c, ok := dynAny.Message.(*api.CommunitiesAttribute); ok {
|
|
||||||
commAttr = c
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a.NotNil(commAttr, "Should have communities attribute")
|
|
||||||
a.Equal(5, len(commAttr.Communities), "Should have 5 communities (2 global + 2 peer + 1 route)")
|
|
||||||
|
|
||||||
// Verify community values (converted to uint32)
|
|
||||||
expectedCommunities := []uint32{
|
|
||||||
convertCommunity("1000:1000"), // Global
|
|
||||||
convertCommunity("2000:2000"), // Global
|
|
||||||
convertCommunity("100:100"), // Per-peer
|
|
||||||
convertCommunity("200:200"), // Per-peer
|
|
||||||
convertCommunity("5000:5000"), // Per-route
|
|
||||||
}
|
|
||||||
a.Equal(expectedCommunities, commAttr.Communities)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiHopConfiguration(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
localAS int
|
|
||||||
peerAS int
|
|
||||||
multiHopPtr *bool
|
|
||||||
expectMH bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "default - multihop not enabled",
|
|
||||||
localAS: 11111,
|
|
||||||
peerAS: 22222,
|
|
||||||
multiHopPtr: nil,
|
|
||||||
expectMH: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit disable",
|
|
||||||
localAS: 11111,
|
|
||||||
peerAS: 22222,
|
|
||||||
multiHopPtr: boolPtr(false),
|
|
||||||
expectMH: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit enable",
|
|
||||||
localAS: 11111,
|
|
||||||
peerAS: 22222,
|
|
||||||
multiHopPtr: boolPtr(true),
|
|
||||||
expectMH: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
ctrl := &Controller{
|
|
||||||
localAS: tc.localAS,
|
|
||||||
localIP: net.ParseIP("192.168.1.100"),
|
|
||||||
s: gobgp.NewBgpServer(),
|
|
||||||
}
|
|
||||||
go ctrl.s.Serve()
|
|
||||||
|
|
||||||
if err := ctrl.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
|
||||||
Global: &api.Global{
|
|
||||||
As: uint32(tc.localAS),
|
|
||||||
RouterId: ctrl.localIP.String(),
|
|
||||||
ListenPort: -1,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.s.StopBgp(context.Background(), &api.StopBgpRequest{})
|
|
||||||
|
|
||||||
peer := &config.PeerConfig{
|
|
||||||
PeerIP: "10.10.10.1",
|
|
||||||
PeerAS: tc.peerAS,
|
|
||||||
MultiHop: tc.multiHopPtr,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := ctrl.addPeer(peer)
|
|
||||||
a.NoError(err)
|
|
||||||
|
|
||||||
// Verify multihop setting by checking peer config
|
|
||||||
var foundPeer *api.Peer
|
|
||||||
ctrl.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
|
||||||
if p.Conf.NeighborAddress == peer.PeerIP {
|
|
||||||
foundPeer = p
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
a.NotNil(foundPeer, "Peer should be added")
|
|
||||||
if tc.expectMH {
|
|
||||||
a.NotNil(foundPeer.EbgpMultihop, "Should have multihop configured")
|
|
||||||
a.True(foundPeer.EbgpMultihop.Enabled, "Multihop should be enabled")
|
|
||||||
} else {
|
|
||||||
if foundPeer.EbgpMultihop != nil {
|
|
||||||
a.False(foundPeer.EbgpMultihop.Enabled, "Multihop should not be enabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to create bool pointer
|
|
||||||
func boolPtr(b bool) *bool {
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiPeerAnnouncement(t *testing.T) {
|
|
||||||
if os.Getenv("CI") != "" {
|
|
||||||
t.Skip("Skipping testing in CI environment")
|
|
||||||
}
|
|
||||||
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Create two BGP listeners
|
|
||||||
listener1, err := NewBgpListener(22222)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer listener1.Shutdown()
|
|
||||||
|
|
||||||
listener2, err := NewBgpListener(33333)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer listener2.Shutdown()
|
|
||||||
|
|
||||||
// Create controller with two peers
|
|
||||||
multiPeerConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
Peers: []config.PeerConfig{
|
|
||||||
{PeerIP: "127.0.0.1", PeerAS: 22222},
|
|
||||||
{PeerIP: "127.0.0.1", PeerAS: 33333},
|
|
||||||
},
|
|
||||||
Communities: []string{"100:100"},
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(multiPeerConfig)
|
|
||||||
if err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Announce a route
|
|
||||||
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
|
|
||||||
r := &Route{Net: ipnet}
|
|
||||||
if err := ctrl.Announce(r); err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify both listeners received the route
|
|
||||||
path1 := <-listener1.recvdPaths
|
|
||||||
a.Equal("20.30.40.0/24", path1)
|
|
||||||
|
|
||||||
path2 := <-listener2.recvdPaths
|
|
||||||
a.Equal("20.30.40.0/24", path2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBestEffortAnnouncement(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Create controller with two peers
|
|
||||||
mixedConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
Peers: []config.PeerConfig{
|
|
||||||
{PeerIP: "127.0.0.1", PeerAS: 22222},
|
|
||||||
{PeerIP: "127.0.0.2", PeerAS: 33333},
|
|
||||||
},
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(mixedConfig)
|
|
||||||
if err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Announce a route - both peers will be added successfully
|
|
||||||
// (they won't have actual BGP sessions established, but peers are added to GoBGP)
|
|
||||||
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
|
|
||||||
r := &Route{Net: ipnet}
|
|
||||||
|
|
||||||
// The announcement should succeed for both peers being added
|
|
||||||
err = ctrl.Announce(r)
|
|
||||||
a.NoError(err, "Announcement should succeed for both peers")
|
|
||||||
|
|
||||||
// Verify both peers were added
|
|
||||||
peers, err := ctrl.PeerInfo()
|
|
||||||
a.NoError(err)
|
|
||||||
a.Equal(2, len(peers), "Should have both peers configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithdrawMultiplePeers(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
ctrl := &Controller{
|
|
||||||
localAS: 11111,
|
|
||||||
localIP: net.ParseIP("192.168.1.100"),
|
|
||||||
peers: []config.PeerConfig{
|
|
||||||
{PeerIP: "10.10.10.1", PeerAS: 22222},
|
|
||||||
{PeerIP: "10.10.10.2", PeerAS: 22222},
|
|
||||||
},
|
|
||||||
origin: 0,
|
|
||||||
s: gobgp.NewBgpServer(),
|
|
||||||
}
|
|
||||||
go ctrl.s.Serve()
|
|
||||||
|
|
||||||
if err := ctrl.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
|
||||||
Global: &api.Global{
|
|
||||||
As: uint32(11111),
|
|
||||||
RouterId: ctrl.localIP.String(),
|
|
||||||
ListenPort: -1,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.s.StopBgp(context.Background(), &api.StopBgpRequest{})
|
|
||||||
|
|
||||||
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
|
|
||||||
r := &Route{Net: ipnet}
|
|
||||||
|
|
||||||
// Withdraw should iterate through all peers
|
|
||||||
// This will fail because peers aren't established, but it should try both
|
|
||||||
err := ctrl.Withdraw(r)
|
|
||||||
// We expect an error but it should have tried both peers
|
|
||||||
if err != nil {
|
|
||||||
a.Contains(err.Error(), "withdrawal errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPeerInfoMultiplePeers(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
ctrl := &Controller{
|
|
||||||
localAS: 11111,
|
|
||||||
localIP: net.ParseIP("192.168.1.100"),
|
|
||||||
peers: []config.PeerConfig{
|
|
||||||
{PeerIP: "10.10.10.1", PeerAS: 22222},
|
|
||||||
{PeerIP: "10.10.10.2", PeerAS: 33333},
|
|
||||||
},
|
|
||||||
origin: 0,
|
|
||||||
s: gobgp.NewBgpServer(),
|
|
||||||
}
|
|
||||||
go ctrl.s.Serve()
|
|
||||||
|
|
||||||
if err := ctrl.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
|
||||||
Global: &api.Global{
|
|
||||||
As: uint32(11111),
|
|
||||||
RouterId: ctrl.localIP.String(),
|
|
||||||
ListenPort: -1,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.s.StopBgp(context.Background(), &api.StopBgpRequest{})
|
|
||||||
|
|
||||||
// Add peers
|
|
||||||
for i := range ctrl.peers {
|
|
||||||
ctrl.addPeer(&ctrl.peers[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get peer info
|
|
||||||
peers, err := ctrl.PeerInfo()
|
|
||||||
a.NoError(err)
|
|
||||||
a.Equal(2, len(peers), "Should return info for both peers")
|
|
||||||
|
|
||||||
// Verify peer addresses
|
|
||||||
peerAddrs := make(map[string]bool)
|
|
||||||
for _, p := range peers {
|
|
||||||
peerAddrs[p.Conf.NeighborAddress] = true
|
|
||||||
}
|
|
||||||
a.True(peerAddrs["10.10.10.1"], "Should have first peer")
|
|
||||||
a.True(peerAddrs["10.10.10.2"], "Should have second peer")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMD5Authentication(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
md5Password string
|
|
||||||
md5EnvVar string
|
|
||||||
envValue string
|
|
||||||
expectedAuth string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "MD5 password from config",
|
|
||||||
md5Password: "secret123",
|
|
||||||
md5EnvVar: "",
|
|
||||||
envValue: "",
|
|
||||||
expectedAuth: "secret123",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "MD5 password from environment variable",
|
|
||||||
md5Password: "",
|
|
||||||
md5EnvVar: "BGP_PEER_PASSWORD",
|
|
||||||
envValue: "env_secret456",
|
|
||||||
expectedAuth: "env_secret456",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Environment variable takes priority over config",
|
|
||||||
md5Password: "config_password",
|
|
||||||
md5EnvVar: "BGP_PEER_PASSWORD",
|
|
||||||
envValue: "env_password",
|
|
||||||
expectedAuth: "env_password",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No authentication when neither is set",
|
|
||||||
md5Password: "",
|
|
||||||
md5EnvVar: "",
|
|
||||||
envValue: "",
|
|
||||||
expectedAuth: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Config password used when env var is set but empty",
|
|
||||||
md5Password: "fallback_password",
|
|
||||||
md5EnvVar: "EMPTY_VAR",
|
|
||||||
envValue: "",
|
|
||||||
expectedAuth: "fallback_password",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Set environment variable if specified
|
|
||||||
if tc.md5EnvVar != "" && tc.envValue != "" {
|
|
||||||
os.Setenv(tc.md5EnvVar, tc.envValue)
|
|
||||||
defer os.Unsetenv(tc.md5EnvVar)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl := &Controller{
|
|
||||||
localAS: 11111,
|
|
||||||
localIP: net.ParseIP("192.168.1.100"),
|
|
||||||
s: gobgp.NewBgpServer(),
|
|
||||||
}
|
|
||||||
go ctrl.s.Serve()
|
|
||||||
|
|
||||||
if err := ctrl.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
|
||||||
Global: &api.Global{
|
|
||||||
As: uint32(11111),
|
|
||||||
RouterId: ctrl.localIP.String(),
|
|
||||||
ListenPort: -1,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.s.StopBgp(context.Background(), &api.StopBgpRequest{})
|
|
||||||
|
|
||||||
peer := &config.PeerConfig{
|
|
||||||
PeerIP: "10.10.10.1",
|
|
||||||
PeerAS: 22222,
|
|
||||||
MD5Password: tc.md5Password,
|
|
||||||
MD5EnvVar: tc.md5EnvVar,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := ctrl.addPeer(peer)
|
|
||||||
a.NoError(err)
|
|
||||||
|
|
||||||
// Verify MD5 authentication is configured correctly
|
|
||||||
var foundPeer *api.Peer
|
|
||||||
ctrl.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
|
||||||
if p.Conf.NeighborAddress == peer.PeerIP {
|
|
||||||
foundPeer = p
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
a.NotNil(foundPeer, "Peer should be added")
|
|
||||||
a.Equal(tc.expectedAuth, foundPeer.Conf.AuthPassword, "MD5 password should match expected")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetMD5Password(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
ctrl := &Controller{}
|
|
||||||
|
|
||||||
// Test 1: Environment variable takes priority
|
|
||||||
os.Setenv("TEST_BGP_PASS", "env_password")
|
|
||||||
defer os.Unsetenv("TEST_BGP_PASS")
|
|
||||||
|
|
||||||
peer := &config.PeerConfig{
|
|
||||||
MD5Password: "config_password",
|
|
||||||
MD5EnvVar: "TEST_BGP_PASS",
|
|
||||||
}
|
|
||||||
a.Equal("env_password", ctrl.getMD5Password(peer))
|
|
||||||
|
|
||||||
// Test 2: Config password when env var not set
|
|
||||||
peer2 := &config.PeerConfig{
|
|
||||||
MD5Password: "config_only",
|
|
||||||
MD5EnvVar: "NONEXISTENT_VAR",
|
|
||||||
}
|
|
||||||
a.Equal("config_only", ctrl.getMD5Password(peer2))
|
|
||||||
|
|
||||||
// Test 3: Empty string when nothing is set
|
|
||||||
peer3 := &config.PeerConfig{}
|
|
||||||
a.Equal("", ctrl.getMD5Password(peer3))
|
|
||||||
|
|
||||||
// Test 4: Only env var specified
|
|
||||||
os.Setenv("ANOTHER_PASS", "another_env_password")
|
|
||||||
defer os.Unsetenv("ANOTHER_PASS")
|
|
||||||
|
|
||||||
peer4 := &config.PeerConfig{
|
|
||||||
MD5EnvVar: "ANOTHER_PASS",
|
|
||||||
}
|
|
||||||
a.Equal("another_env_password", ctrl.getMD5Password(peer4))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiPeerWithMD5(t *testing.T) {
|
|
||||||
a := assert.New(t)
|
|
||||||
|
|
||||||
// Set environment variables for testing
|
|
||||||
os.Setenv("PEER1_PASSWORD", "peer1_secret")
|
|
||||||
os.Setenv("PEER2_PASSWORD", "peer2_secret")
|
|
||||||
defer os.Unsetenv("PEER1_PASSWORD")
|
|
||||||
defer os.Unsetenv("PEER2_PASSWORD")
|
|
||||||
|
|
||||||
// Create controller with multiple peers using different MD5 configurations
|
|
||||||
multiPeerConfig := config.BgpConfig{
|
|
||||||
LocalAS: 11111,
|
|
||||||
LocalIP: "192.168.1.100",
|
|
||||||
Peers: []config.PeerConfig{
|
|
||||||
{
|
|
||||||
PeerIP: "10.10.10.1",
|
|
||||||
PeerAS: 22222,
|
|
||||||
MD5EnvVar: "PEER1_PASSWORD",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PeerIP: "10.10.10.2",
|
|
||||||
PeerAS: 33333,
|
|
||||||
MD5Password: "peer2_config_password",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PeerIP: "10.10.10.3",
|
|
||||||
PeerAS: 44444,
|
|
||||||
// No MD5 authentication
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Origin: "igp",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl, err := NewController(multiPeerConfig)
|
|
||||||
if err != nil {
|
|
||||||
a.FailNow(err.Error())
|
|
||||||
}
|
|
||||||
defer ctrl.Shutdown()
|
|
||||||
|
|
||||||
// Trigger peer addition by announcing a route (peers are added lazily)
|
|
||||||
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
|
|
||||||
r := &Route{Net: ipnet}
|
|
||||||
ctrl.Announce(r)
|
|
||||||
|
|
||||||
// Verify all peers have correct MD5 configuration
|
|
||||||
peers := make(map[string]string) // map[peerIP]authPassword
|
|
||||||
ctrl.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
|
||||||
peers[p.Conf.NeighborAddress] = p.Conf.AuthPassword
|
|
||||||
})
|
|
||||||
|
|
||||||
a.Equal(3, len(peers), "Should have all three peers")
|
|
||||||
a.Equal("peer1_secret", peers["10.10.10.1"], "First peer should use env var password")
|
|
||||||
a.Equal("peer2_config_password", peers["10.10.10.2"], "Second peer should use config password")
|
|
||||||
a.Equal("", peers["10.10.10.3"], "Third peer should have no authentication")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const (
|
|||||||
consulNodeEnv = "CONSUL_NODE"
|
consulNodeEnv = "CONSUL_NODE"
|
||||||
consulToken = "CONSUL_TOKEN"
|
consulToken = "CONSUL_TOKEN"
|
||||||
allowStale = "CONSUL_STALE"
|
allowStale = "CONSUL_STALE"
|
||||||
matchTag = "enable_gocast"
|
defaultmatchTag = "enable_gocast"
|
||||||
nodeURL = "/catalog/node"
|
nodeURL = "/catalog/node"
|
||||||
remoteHealthCheckurl = "/health/checks"
|
remoteHealthCheckurl = "/health/checks"
|
||||||
localHealthCheckurl = "/agent/checks"
|
localHealthCheckurl = "/agent/checks"
|
||||||
@@ -35,6 +35,7 @@ type ConsulMon struct {
|
|||||||
addr string
|
addr string
|
||||||
token string
|
token string
|
||||||
node string
|
node string
|
||||||
|
matchTag string
|
||||||
client Clienter
|
client Clienter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +56,15 @@ func contains(inp []string, elem string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConsulMon(addr string, token string) (*ConsulMon, error) {
|
func NewConsulMon(addr string, token string, matchTag string) (*ConsulMon, error) {
|
||||||
node := os.Getenv(consulNodeEnv)
|
node := os.Getenv(consulNodeEnv)
|
||||||
if node == "" {
|
if node == "" {
|
||||||
return nil, fmt.Errorf("%s env variable not set", consulNodeEnv)
|
return nil, fmt.Errorf("%s env variable not set", consulNodeEnv)
|
||||||
}
|
}
|
||||||
return &ConsulMon{addr: addr, token: token, node: node, client: &http.Client{Timeout: 10 * time.Second}}, nil
|
if matchTag == "" {
|
||||||
|
matchTag = defaultmatchTag
|
||||||
|
}
|
||||||
|
return &ConsulMon{addr: addr, token: token, node: node, client: &http.Client{Timeout: 10 * time.Second}, matchTag: matchTag}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHTTPReq(httpMethod string, addr string, tokenFrmCfg string) (*http.Request, error) {
|
func getHTTPReq(httpMethod string, addr string, tokenFrmCfg string) (*http.Request, error) {
|
||||||
@@ -95,10 +99,10 @@ func (c *ConsulMon) queryServices() ([]*App, error) {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
var consulData ConsulServiceData
|
var consulData ConsulServiceData
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&consulData); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&consulData); err != nil {
|
||||||
return apps, fmt.Errorf("Unable to decode consul data: %v", err)
|
return apps, fmt.Errorf("unable to decode consul data: %v", err)
|
||||||
}
|
}
|
||||||
for _, service := range consulData.Services {
|
for _, service := range consulData.Services {
|
||||||
if !contains(service.Tags, matchTag) {
|
if !contains(service.Tags, c.matchTag) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
@@ -170,7 +174,7 @@ func (c *ConsulMon) healthCheckLocal(service string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("No local healthcheck info found for service %s on node %s in consul", service, c.node)
|
return false, fmt.Errorf("no local healthcheck info found for service %s on node %s in consul", service, c.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthCheckRemote queries the consul cluster's healthcheck endpoint to perform service healthchecks
|
// healthCheckRemote queries the consul cluster's healthcheck endpoint to perform service healthchecks
|
||||||
@@ -202,7 +206,7 @@ func (c *ConsulMon) healthCheckRemote(service string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("No healthcheck info found for node %s in consul", c.node)
|
return false, fmt.Errorf("no healthcheck info found for node %s in consul", c.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// healthCheck determines if we should use the local agent
|
// healthCheck determines if we should use the local agent
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -119,13 +119,13 @@ func TestQueryServices(t *testing.T) {
|
|||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
client := &MockClient{}
|
client := &MockClient{}
|
||||||
cm := &ConsulMon{
|
cm := &ConsulMon{
|
||||||
addr: "foo", node: "test", client: client,
|
addr: "foo", node: "test", client: client, matchTag: "enable_gocast",
|
||||||
}
|
}
|
||||||
|
|
||||||
// test valid app
|
// test valid app
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulData["single-app"]))
|
b := bytes.NewBuffer([]byte(mockConsulData["single-app"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
apps, err := cm.queryServices()
|
apps, err := cm.queryServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,7 +140,7 @@ func TestQueryServices(t *testing.T) {
|
|||||||
// test no match
|
// test no match
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-match"]))
|
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-match"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
apps, err = cm.queryServices()
|
apps, err = cm.queryServices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -151,7 +151,7 @@ func TestQueryServices(t *testing.T) {
|
|||||||
// test missing vip
|
// test missing vip
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-vip"]))
|
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-vip"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
apps, _ = cm.queryServices()
|
apps, _ = cm.queryServices()
|
||||||
a.Equal(0, len(apps))
|
a.Equal(0, len(apps))
|
||||||
@@ -160,13 +160,13 @@ func TestQueryServices(t *testing.T) {
|
|||||||
func TestHealthCheck(t *testing.T) {
|
func TestHealthCheck(t *testing.T) {
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
client := &MockClient{}
|
client := &MockClient{}
|
||||||
cm := &ConsulMon{node: "test-node1", client: client}
|
cm := &ConsulMon{node: "test-node1", client: client, matchTag: "enable_gocast"}
|
||||||
|
|
||||||
// test remote checks
|
// test remote checks
|
||||||
cm.addr = "http://remote/check"
|
cm.addr = "http://remote/check"
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-pass"]))
|
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-pass"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
check, err := cm.healthCheck("test-service")
|
check, err := cm.healthCheck("test-service")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,7 +175,7 @@ func TestHealthCheck(t *testing.T) {
|
|||||||
a.True(check)
|
a.True(check)
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-fail"]))
|
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-fail"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
check, _ = cm.healthCheck("test-service")
|
check, _ = cm.healthCheck("test-service")
|
||||||
a.False(check)
|
a.False(check)
|
||||||
@@ -184,7 +184,7 @@ func TestHealthCheck(t *testing.T) {
|
|||||||
cm.addr = "http://localhost/check"
|
cm.addr = "http://localhost/check"
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-pass"]))
|
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-pass"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
check, _ = cm.healthCheck("test-service")
|
check, _ = cm.healthCheck("test-service")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -194,7 +194,7 @@ func TestHealthCheck(t *testing.T) {
|
|||||||
cm.addr = "http://127.0.0.1/check"
|
cm.addr = "http://127.0.0.1/check"
|
||||||
client.do = func(*http.Request) (*http.Response, error) {
|
client.do = func(*http.Request) (*http.Response, error) {
|
||||||
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-fail"]))
|
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-fail"]))
|
||||||
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
|
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
|
||||||
}
|
}
|
||||||
check, _ = cm.healthCheck("test-service")
|
check, _ = cm.healthCheck("test-service")
|
||||||
a.False(check)
|
a.False(check)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func NewMonitor(config *c.Config) *MonitorMgr {
|
|||||||
cleanups: make(map[string]chan bool),
|
cleanups: make(map[string]chan bool),
|
||||||
}
|
}
|
||||||
if config.Agent.ConsulAddr != "" {
|
if config.Agent.ConsulAddr != "" {
|
||||||
cmon, err := NewConsulMon(config.Agent.ConsulAddr, config.Agent.ConsulToken)
|
cmon, err := NewConsulMon(config.Agent.ConsulAddr, config.Agent.ConsulToken, config.Agent.ConsulMatchTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to start consul monitor: %v", err)
|
glog.Errorf("Failed to start consul monitor: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -184,7 +184,7 @@ func (m *MonitorMgr) Add(app *App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove removes an app from monitor manager, stops BGP
|
// Remove removes an app from monitor manager, stops BGP
|
||||||
/// announcement and cleans up state
|
// / announcement and cleans up state
|
||||||
func (m *MonitorMgr) Remove(appName string) {
|
func (m *MonitorMgr) Remove(appName string) {
|
||||||
m.monMu.Lock()
|
m.monMu.Lock()
|
||||||
defer m.monMu.Unlock()
|
defer m.monMu.Unlock()
|
||||||
@@ -339,231 +339,6 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -581,6 +356,6 @@ func (m *MonitorMgr) Cleanup(app string, exit chan bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInfo returns basic BGP info for established peers
|
// GetInfo returns basic BGP info for established peers
|
||||||
func (m *MonitorMgr) GetInfo() ([]*api.Peer, error) {
|
func (m *MonitorMgr) GetInfo() (*api.Peer, error) {
|
||||||
return m.ctrl.PeerInfo()
|
return m.ctrl.PeerInfo()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -18,22 +18,30 @@ func getCmdList(mainCmd string) []string {
|
|||||||
return cmdList
|
return cmdList
|
||||||
}
|
}
|
||||||
|
|
||||||
func gateway() (net.IP, error) {
|
func gateway(family int) (net.IP, error) {
|
||||||
cmd := `ip route | grep "^default" | cut -d" " -f3`
|
prefix := "ip"
|
||||||
|
if family == 6 {
|
||||||
|
prefix = "ip -6"
|
||||||
|
}
|
||||||
|
cmd := fmt.Sprintf(`%s route | grep "^default" | cut -d" " -f3`, prefix)
|
||||||
cmdList := getCmdList(cmd)
|
cmdList := getCmdList(cmd)
|
||||||
out, err := exec.Command(execCmd, cmdList...).Output()
|
out, err := exec.Command(execCmd, cmdList...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
|
return nil, fmt.Errorf("failed to execute command: %s: %v", cmd, err)
|
||||||
}
|
}
|
||||||
return net.ParseIP(strings.TrimSpace(string(out))), nil
|
return net.ParseIP(strings.TrimSpace(string(out))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func via(dest net.IP) (net.IP, error) {
|
func via(dest net.IP) (net.IP, error) {
|
||||||
cmd := fmt.Sprintf(`ip route get %s | grep via | cut -d" " -f3`, dest.String())
|
prefix := "ip"
|
||||||
|
if dest.To4() == nil {
|
||||||
|
prefix = "ip -6"
|
||||||
|
}
|
||||||
|
cmd := fmt.Sprintf(`%s route get %s | grep via | cut -d" " -f3`, prefix, dest.String())
|
||||||
cmdList := getCmdList(cmd)
|
cmdList := getCmdList(cmd)
|
||||||
out, err := exec.Command(execCmd, cmdList...).Output()
|
out, err := exec.Command(execCmd, cmdList...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
|
return nil, fmt.Errorf("failed to execute command: %s: %v", cmd, err)
|
||||||
}
|
}
|
||||||
if string(out) == "" {
|
if string(out) == "" {
|
||||||
// assume the provided dest is the next hop
|
// assume the provided dest is the next hop
|
||||||
@@ -55,7 +63,7 @@ func localAddress(gw net.IP) (net.IP, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Unable to find local address")
|
return nil, fmt.Errorf("unable to find local address")
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLoopback(name string, addr *net.IPNet) error {
|
func addLoopback(name string, addr *net.IPNet) error {
|
||||||
@@ -66,30 +74,42 @@ func addLoopback(name string, addr *net.IPNet) error {
|
|||||||
if len(label) > 15 {
|
if len(label) > 15 {
|
||||||
label = label[:15]
|
label = label[:15]
|
||||||
}
|
}
|
||||||
cmd := fmt.Sprintf("ip address add %s/%d dev lo label %s", addr.IP.String(), prefixLen, label)
|
prefix := "ip"
|
||||||
|
if addr.IP.To4() == nil {
|
||||||
|
prefix = "ip -6"
|
||||||
|
}
|
||||||
|
cmd := fmt.Sprintf("%s address add %s/%d dev lo label %s", prefix, addr.IP.String(), prefixLen, label)
|
||||||
cmdList := getCmdList(cmd)
|
cmdList := getCmdList(cmd)
|
||||||
_, err := exec.Command(execCmd, cmdList...).Output()
|
_, err := exec.Command(execCmd, cmdList...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to Add loopback command: %s: %v", cmd, err)
|
return fmt.Errorf("failed to Add loopback command: %s: %v", cmd, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteLoopback(addr *net.IPNet) error {
|
func deleteLoopback(addr *net.IPNet) error {
|
||||||
|
prefix := "ip"
|
||||||
|
if addr.IP.To4() == nil {
|
||||||
|
prefix = "ip -6"
|
||||||
|
}
|
||||||
prefixLen, _ := addr.Mask.Size()
|
prefixLen, _ := addr.Mask.Size()
|
||||||
cmd := fmt.Sprintf("ip address delete %s/%d dev lo", addr.IP.String(), prefixLen)
|
cmd := fmt.Sprintf("%s address delete %s/%d dev lo", prefix, addr.IP.String(), prefixLen)
|
||||||
cmdList := getCmdList(cmd)
|
cmdList := getCmdList(cmd)
|
||||||
_, err := exec.Command(execCmd, cmdList...).Output()
|
_, err := exec.Command(execCmd, cmdList...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to delete loopback command: %s: %v", cmd, err)
|
return fmt.Errorf("failed to delete loopback command: %s: %v", cmd, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
prefix := "iptables"
|
||||||
|
if vip.To4() == nil {
|
||||||
|
prefix = "ip6tables"
|
||||||
|
}
|
||||||
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,
|
prefix, 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()
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import (
|
|||||||
func TestGateway(t *testing.T) {
|
func TestGateway(t *testing.T) {
|
||||||
execCmd = os.Args[0]
|
execCmd = os.Args[0]
|
||||||
os.Setenv("test_name", "test_gateway")
|
os.Setenv("test_name", "test_gateway")
|
||||||
gw, err := gateway()
|
gw, err := gateway(4)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, "10.1.1.1", gw.String())
|
assert.Equal(t, "10.1.1.1", gw.String())
|
||||||
|
|
||||||
|
os.Setenv("test_name", "test_gateway_v6")
|
||||||
|
gw, err = gateway(6)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "2001:dead:beef::1", gw.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVia(t *testing.T) {
|
func TestVia(t *testing.T) {
|
||||||
@@ -28,6 +33,11 @@ func TestVia(t *testing.T) {
|
|||||||
ip, err = via(net.ParseIP("10.1.4.1"))
|
ip, err = via(net.ParseIP("10.1.4.1"))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, "10.1.4.1", ip.String())
|
assert.Equal(t, "10.1.4.1", ip.String())
|
||||||
|
|
||||||
|
os.Setenv("test_name", "test_via_v6")
|
||||||
|
ip, err = via(net.ParseIP("2001:dead:beef::100"))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "2001:dead:beef::1", ip.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddLoopback(t *testing.T) {
|
func TestAddLoopback(t *testing.T) {
|
||||||
@@ -41,14 +51,23 @@ func TestAddLoopback(t *testing.T) {
|
|||||||
_, ipnet, _ = net.ParseCIDR("1.1.1.1/32")
|
_, ipnet, _ = net.ParseCIDR("1.1.1.1/32")
|
||||||
err = addLoopback("test_app", ipnet)
|
err = addLoopback("test_app", ipnet)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
os.Setenv("test_name", "test_add_v6")
|
||||||
|
_, ipnet, _ = net.ParseCIDR("2001:dead:beef:1001::100/64")
|
||||||
|
err = addLoopback("test_app", ipnet)
|
||||||
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
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":
|
||||||
fmt.Println("10.1.1.1")
|
fmt.Println("10.1.1.1")
|
||||||
|
case "test_gateway_v6":
|
||||||
|
fmt.Println("2001:dead:beef::1")
|
||||||
case "test_via":
|
case "test_via":
|
||||||
fmt.Println("10.1.2.1")
|
fmt.Println("10.1.2.1")
|
||||||
|
case "test_via_v6":
|
||||||
|
fmt.Println("2001:dead:beef::1")
|
||||||
case "test_via_none":
|
case "test_via_none":
|
||||||
break
|
break
|
||||||
case "test_add_fail":
|
case "test_add_fail":
|
||||||
|
|||||||
11
main.go
11
main.go
@@ -33,16 +33,7 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
sig := <-signalChan
|
sig := <-signalChan
|
||||||
switch sig {
|
if sig == os.Interrupt || sig == syscall.SIGTERM {
|
||||||
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
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ func (s *Server) unregisterHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
peers, err := s.mon.GetInfo()
|
peer, err := s.mon.GetInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, fmt.Sprintf("Internal error getting peers: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("Internal error getting peers: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(peers)
|
json.NewEncoder(w).Encode(peer)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user