1
0
Fork 0
mirror of https://github.com/alice-lg/birdwatcher.git synced 2025-03-09 00:00:05 +01:00
birdwatcher/bird/bird.go

552 lines
13 KiB
Go

package bird
import (
"bytes"
"io"
"log"
"reflect"
"strconv"
"strings"
"sync"
"time"
"os/exec"
)
type Cache interface {
Set(key string, val Parsed, ttl int) error
Get(key string) (Parsed, error)
Expire() int
}
var ClientConf BirdConfig
var StatusConf StatusConfig
var IPVersion = "4"
var BirdVersion = 0
var cache Cache // stores parsed birdc output
var CacheConf CacheConfig
var RateLimitConf struct {
sync.RWMutex
Conf RateLimitConfig
}
var RunQueue sync.Map // queue birdc commands before execution
var NilParse Parsed = (Parsed)(nil) // special Parsed values
var BirdError Parsed = Parsed{"error": "bird unreachable"}
func IsSpecial(ret Parsed) bool { // test for special Parsed values
return reflect.DeepEqual(ret, NilParse) || reflect.DeepEqual(ret, BirdError)
}
// intitialize the Cache once during setup with either a MemoryCache or
// RedisCache implementation.
// TODO implement singleton pattern
func InitializeCache() {
var err error
if CacheConf.UseRedis {
cache, err = NewRedisCache(CacheConf)
if err != nil {
log.Println("Could not initialize redis cache, falling back to memory cache:", err)
}
} else { // initialize the MemoryCache
maxKeys := CacheConf.MaxKeys
maxKeysDefault := 60
if maxKeys == 0 {
log.Println("MaxKeys not set, using default value:", maxKeysDefault)
maxKeys = maxKeysDefault
}
cache = NewMemoryCache(maxKeys)
log.Println("Initialized MemoryCache with maxKeys:", maxKeys)
}
}
// ExpireCache is a convenience method to expire the cache.
func ExpireCache() int {
return cache.Expire()
}
/* Convenience method to make new entries in the cache.
* Abstracts over the specific caching implementation and the ability to set
* individual TTL values for entries. Always use the default TTL value from the
* config.
*/
func toCache(key string, val Parsed) bool {
var ttl int
if ClientConf.CacheTtl >= 0 {
ttl = ClientConf.CacheTtl
} else {
ttl = 5 // five minutes
}
if err := cache.Set(key, val, ttl); err != nil {
log.Println(err)
return false
}
return true
}
/* Convenience method to retrieve entries from the cache.
* Abstracts over the specific caching implementations.
* If err returned by cache.Get(key) is set, the value from the cache is not
* used. There is either a fault e.g. missing entry or the ttl is expired.
* Handling of specific error conditions e.g. ttl expired but entry present is
* possible but currently not implemented.
*/
func fromCache(key string) (Parsed, bool) {
val, err := cache.Get(key)
if err == nil {
return val, true
} else {
return val, false
}
//DEBUG log.Println(err)
}
// Determines the key in the cache, where the result of specific functions are stored.
// Eliminates the need to know what command was executed by that function.
func GetCacheKey(fname string, fargs ...interface{}) string {
key := strings.ToLower(fname)
for _, arg := range fargs {
switch arg.(type) {
case string:
key += "_" + strings.ToLower(arg.(string))
}
}
return key
}
func Run(args string) (io.Reader, error) {
args = "-r " + "show " + args // enforce birdc in restricted mode with "-r" argument
argsList := strings.Split(args, " ")
// Allow for arguments in the config
cmdArgs := strings.Split(ClientConf.BirdCmd, " ")
birdc := cmdArgs[0]
cmdArgs = cmdArgs[1:]
cmd := []string{}
cmd = append(cmd, cmdArgs...)
cmd = append(cmd, argsList...)
out, err := exec.Command(birdc, cmd...).Output()
if err != nil {
return nil, err
}
return bytes.NewReader(out), nil
}
func InstallRateLimitReset() {
go func() {
c := time.Tick(time.Second)
for _ = range c {
RateLimitConf.Lock()
RateLimitConf.Conf.Reqs = RateLimitConf.Conf.Max
RateLimitConf.Unlock()
}
}()
}
func checkRateLimit() bool {
RateLimitConf.RLock()
check := !RateLimitConf.Conf.Enabled
RateLimitConf.RUnlock()
if check {
return true
}
RateLimitConf.RLock()
check = RateLimitConf.Conf.Reqs < 1
RateLimitConf.RUnlock()
if check {
return false
}
RateLimitConf.Lock()
RateLimitConf.Conf.Reqs -= 1
RateLimitConf.Unlock()
return true
}
func RunAndParse(useCache bool, key string, cmd string, parser func(io.Reader) Parsed, updateCache func(*Parsed)) (Parsed, bool) {
var wg sync.WaitGroup
if useCache {
if val, ok := fromCache(cmd); ok {
return val, true
}
}
wg.Add(1)
if queueGroup, queueLoaded := RunQueue.LoadOrStore(cmd, &wg); queueLoaded {
(*queueGroup.(*sync.WaitGroup)).Wait()
if val, ok := fromCache(cmd); ok {
return val, true
} else {
// TODO BirdError should also be signaled somehow
return NilParse, false
}
}
if !checkRateLimit() {
wg.Done()
RunQueue.Delete(cmd)
return NilParse, false
}
out, err := Run(cmd)
if err != nil {
// ignore errors for now
wg.Done()
RunQueue.Delete(cmd)
return BirdError, false
}
parsed := parser(out)
if updateCache != nil {
updateCache(&parsed)
}
toCache(cmd, parsed)
wg.Done()
RunQueue.Delete(cmd)
return parsed, false
}
func Status(useCache bool) (Parsed, bool) {
updateParsedCache := func(p *Parsed) {
status := (*p)["status"].(Parsed)
// Last Reconfig Timestamp source:
var lastReconfig string
switch StatusConf.ReconfigTimestampSource {
case "bird":
lastReconfig = status["last_reconfig"].(string)
break
case "config_modified":
lastReconfig = lastReconfigTimestampFromFileStat(
ClientConf.ConfigFilename,
)
case "config_regex":
lastReconfig = lastReconfigTimestampFromFileContent(
ClientConf.ConfigFilename,
StatusConf.ReconfigTimestampMatch,
)
}
status["last_reconfig"] = lastReconfig
// Filter fields
for _, field := range StatusConf.FilterFields {
status[field] = nil
}
}
birdStatus, from_cache := RunAndParse(useCache, GetCacheKey("Status"), "status", parseStatus, updateParsedCache)
return birdStatus, from_cache
}
func ProtocolsShort(useCache bool) (Parsed, bool) {
res, from_cache := RunAndParse(useCache, GetCacheKey("ProtocolsShort"), "protocols", parseProtocolsShort, nil)
return res, from_cache
}
func Protocols(useCache bool) (Parsed, bool) {
createMetaCache := func(p *Parsed) {
metaProtocol := Parsed{"protocols": Parsed{"bird_protocol": Parsed{}}}
for key, _ := range (*p)["protocols"].(Parsed) {
parsed := (*p)["protocols"].(Parsed)[key].(Parsed)
protocol := parsed["protocol"].(string)
birdProtocol := parsed["bird_protocol"].(string)
// Check if the structure for the current birdProtocol already exists inside the metaProtocol cache, if not create it (BGP|Pipe|etc)
if _, ok := metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol]; !ok {
metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol] = Parsed{}
}
metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol].(Parsed)[protocol] = &parsed
}
toCache(GetCacheKey("metaProtocol"), metaProtocol)
}
res, from_cache := RunAndParse(useCache, GetCacheKey("Protocols"), "protocols all", parseProtocols, createMetaCache)
return res, from_cache
}
func ProtocolsBgp(useCache bool) (Parsed, bool) {
protocols, from_cache := Protocols(useCache)
if IsSpecial(protocols) {
return protocols, from_cache
}
protocolsMeta, _ := fromCache(GetCacheKey("metaProtocol"))
metaProtocol := protocolsMeta["protocols"].(Parsed)
bgpProtocols := Parsed{}
for key, protocol := range metaProtocol["bird_protocol"].(Parsed)["BGP"].(Parsed) {
bgpProtocols[key] = *(protocol.(*Parsed))
}
return Parsed{"protocols": bgpProtocols,
"ttl": protocols["ttl"],
"cached_at": protocols["cached_at"]}, from_cache
}
func Symbols(useCache bool) (Parsed, bool) {
return RunAndParse(useCache, GetCacheKey("Symbols"), "symbols", parseSymbols, nil)
}
func routesQuery(filter string) string {
cmd := "route " + filter
if getBirdVersion() < 2 || ClientConf.Dualstack {
return cmd
}
return cmd + " where net.type = NET_IP" + IPVersion
}
func remapTable(table string) string {
if v := getBirdVersion(); v < 2 {
return table // Nothing to do for bird1
}
if table != "master" {
return table // Nothing to do here
}
// Rewrite master table
if IPVersion == "4" {
return "master4"
}
return "master6"
}
func RoutesPrefixed(useCache bool, prefix string) (Parsed, bool) {
cmd := routesQuery(prefix + " all")
return RunAndParse(
useCache,
GetCacheKey("RoutesPrefixed", prefix),
cmd,
parseRoutes,
nil)
}
func RoutesProto(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("all protocol '" + protocol + "'")
return RunAndParse(
useCache,
GetCacheKey("RoutesProto", protocol),
cmd,
parseRoutes,
nil)
}
func RoutesPeer(useCache bool, peer string) (Parsed, bool) {
cmd := "route all where from=" + peer
return RunAndParse(
useCache,
GetCacheKey("RoutesPeer", peer),
cmd,
parseRoutes,
nil)
}
func RoutesTableAndPeer(useCache bool, table string, peer string) (Parsed, bool) {
table = remapTable(table)
cmd := "route table '" + table + "' all where from=" + peer
return RunAndParse(
useCache,
GetCacheKey("RoutesTableAndPeer", table, peer),
cmd,
parseRoutes,
nil)
}
func RoutesProtoCount(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("protocol '" + protocol + "' count")
return RunAndParse(
useCache,
GetCacheKey("RoutesProtoCount", protocol),
cmd,
parseRoutesCount,
nil)
}
func RoutesProtoPrimaryCount(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("primary protocol '" + protocol + "' count")
return RunAndParse(
useCache,
GetCacheKey("RoutesProtoPrimaryCount", protocol),
cmd,
parseRoutesCount,
nil)
}
func PipeRoutesFilteredCount(useCache bool, pipe string, table string, neighborAddress string) (Parsed, bool) {
table = remapTable(table)
cmd := "route table '" + table +
"' noexport '" + pipe +
"' where from=" + neighborAddress + " count"
return RunAndParse(
useCache,
GetCacheKey("PipeRoutesFilteredCount", table, pipe, neighborAddress),
cmd,
parseRoutesCount,
nil)
}
func PipeRoutesFiltered(useCache bool, pipe string, table string) (Parsed, bool) {
table = remapTable(table)
cmd := routesQuery("table '" + table + "' noexport '" + pipe + "' all")
return RunAndParse(
useCache,
GetCacheKey("PipeRoutesFiltered", table, pipe),
cmd,
parseRoutes,
nil)
}
func RoutesFiltered(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("all filtered protocol '" + protocol + "'")
return RunAndParse(
useCache,
GetCacheKey("RoutesFiltered", protocol),
cmd,
parseRoutes,
nil)
}
func RoutesExport(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("all export '" + protocol + "'")
return RunAndParse(
useCache,
GetCacheKey("RoutesExport", protocol),
cmd,
parseRoutes,
nil)
}
func RoutesNoExport(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("all noexport '" + protocol + "'")
return RunAndParse(
useCache,
GetCacheKey("RoutesNoExport", protocol),
cmd,
parseRoutes,
nil)
}
func RoutesExportCount(useCache bool, protocol string) (Parsed, bool) {
cmd := routesQuery("export '" + protocol + "' count")
return RunAndParse(
useCache,
GetCacheKey("RoutesExportCount", protocol),
cmd,
parseRoutesCount,
nil)
}
func RoutesTable(useCache bool, table string) (Parsed, bool) {
table = remapTable(table)
cmd := routesQuery("table '" + table + "' all")
return RunAndParse(
useCache,
GetCacheKey("RoutesTable", table),
cmd,
parseRoutes,
nil)
}
func RoutesTableFiltered(useCache bool, table string) (Parsed, bool) {
table = remapTable(table)
cmd := routesQuery("table '" + table + "' all filtered")
return RunAndParse(
useCache,
GetCacheKey("RoutesTableFiltered", table),
cmd,
parseRoutes,
nil)
}
func RoutesTableCount(useCache bool, table string) (Parsed, bool) {
table = remapTable(table)
cmd := routesQuery("table '" + table + "' count")
return RunAndParse(
useCache,
GetCacheKey("RoutesTableCount", table),
cmd,
parseRoutesCount,
nil,
)
}
func RoutesLookupTable(useCache bool, net string, table string) (Parsed, bool) {
table = remapTable(table)
cmd := routesQuery("for " + net + " table '" + table + "' all")
return RunAndParse(
useCache,
GetCacheKey("RoutesLookupTable", net, table),
cmd,
parseRoutes,
nil)
}
func RoutesLookupProtocol(useCache bool, net string, protocol string) (Parsed, bool) {
cmd := routesQuery("for " + net + " protocol '" + protocol + "' all")
return RunAndParse(
useCache,
GetCacheKey("RoutesLookupProtocol", net, protocol),
cmd,
parseRoutes,
nil)
}
func getBirdVersion() int {
// We assume the bird major version does not change during
// the time the birdwatcher is running.
//
// However, this requires now a restart when going
// from bird1 to bird2.
if BirdVersion != 0 {
return BirdVersion
}
// This method is a bit hacky.
status, _ := Status(false) // Get status without cache
if IsSpecial(status) {
return 0
}
birdStatus, ok := status["status"].(Parsed)
if !ok {
return 0
}
version, ok := birdStatus["version"].(string)
if !ok {
return 0
}
v, err := strconv.Atoi(string(version[0]))
if err != nil {
return 0
}
BirdVersion = v
return v
}