commit e20f691de5b1107c4e9039a63df722198d998f79 Author: Mayuresh Gaitonde Date: Mon Oct 22 19:03:42 2018 -0700 Initial commit for gocast diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4aa9a24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:alpine as builder +RUN apk update && \ + apk upgrade && \ + apk add --no-cache git && \ + apk add make +RUN mkdir -p /opt/gocast +RUN mkdir -p /go/src/github.com/mayuresh82 +RUN cd /go/src/github.com/mayuresh82 && \ + git clone --branch dev https://github.com/mayuresh82/gocast +WORKDIR /go/src/github.com/mayuresh82/gocast +RUN make +RUN cp gocast /opt/gocast/ + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /opt/gocast/gocast . + +EXPOSE 8080/tcp + +ENTRYPOINT ["./gocast"] diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..f6e5388 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,355 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + digest = "1:c47f4964978e211c6e566596ec6246c329912ea92e9bb99c00798bb4564c5b09" + name = "github.com/armon/go-radix" + packages = ["."] + pruneopts = "UT" + revision = "1a2de0c21c94309923825da3df33a4381872c795" + +[[projects]] + branch = "master" + digest = "1:8583eab935e3d99d3a7ac489cd2ee7c8e95eecd7c64ab1fc8382746dacaf8563" + name = "github.com/dgryski/go-farm" + packages = ["."] + pruneopts = "UT" + revision = "2de33835d10275975374b37b2dcfd22c9020a1f5" + +[[projects]] + digest = "1:975a4480c40f2d0b95e1f83d3ec1aa29a2774e80179e08a9a4ba2aab86721b23" + name = "github.com/eapache/channels" + packages = ["."] + pruneopts = "UT" + revision = "47238d5aae8c0fefd518ef2bee46290909cf8263" + version = "v1.1.0" + +[[projects]] + digest = "1:444b82bfe35c83bbcaf84e310fb81a1f9ece03edfed586483c869e2c046aef69" + name = "github.com/eapache/queue" + packages = ["."] + pruneopts = "UT" + revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98" + version = "v1.1.0" + +[[projects]] + digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" + name = "github.com/fsnotify/fsnotify" + packages = ["."] + pruneopts = "UT" + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + branch = "master" + digest = "1:1ba1d79f2810270045c328ae5d674321db34e3aae468eb4233883b473c5c0467" + name = "github.com/golang/glog" + packages = ["."] + pruneopts = "UT" + revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" + +[[projects]] + branch = "master" + digest = "1:c095e448622bb061a09cb718e3dddc59acb624a67756392282374fa091c83343" + name = "github.com/golang/protobuf" + packages = [ + "proto", + "ptypes", + "ptypes/any", + "ptypes/duration", + "ptypes/empty", + "ptypes/timestamp", + ] + pruneopts = "UT" + revision = "7be3631955993a734965532f776bad7093f6fc9d" + +[[projects]] + digest = "1:c0d19ab64b32ce9fe5cf4ddceba78d5bc9807f0016db6b1183599da3dcc24d10" + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token", + ] + pruneopts = "UT" + revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" + version = "v1.0.0" + +[[projects]] + digest = "1:65b0a07f85f7b5cc019c26775efc278a155a5dea0a8aa617c980e8308d16bc55" + name = "github.com/influxdata/influxdb" + packages = [ + "client/v2", + "models", + "pkg/escape", + ] + pruneopts = "UT" + revision = "c75cdfdfa6f71a08473fefcec71f6cbcbdef1ff4" + version = "v1.6.4" + +[[projects]] + digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8" + name = "github.com/konsorten/go-windows-terminal-sequences" + packages = ["."] + pruneopts = "UT" + revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" + version = "v1.0.1" + +[[projects]] + digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7" + name = "github.com/magiconair/properties" + packages = ["."] + pruneopts = "UT" + revision = "c2353362d570a7bfa228149c62842019201cfb71" + version = "v1.8.0" + +[[projects]] + digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + pruneopts = "UT" + revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" + version = "v1.1.2" + +[[projects]] + branch = "master" + digest = "1:7261338473c27eea9593dceecf6a949c689f6dbd2226cad46139ab9d5011bc4b" + name = "github.com/osrg/gobgp" + packages = [ + "api", + "internal/pkg/apiutil", + "internal/pkg/config", + "internal/pkg/table", + "internal/pkg/zebra", + "pkg/packet/bgp", + "pkg/packet/bmp", + "pkg/packet/mrt", + "pkg/packet/rtr", + "pkg/server", + ] + pruneopts = "UT" + revision = "329c2d316efecfed0331e30114d7086aa58e247e" + +[[projects]] + digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" + name = "github.com/pelletier/go-toml" + packages = ["."] + pruneopts = "UT" + revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" + version = "v1.2.0" + +[[projects]] + branch = "master" + digest = "1:dd6ba1917df517806c9dcee5c87f15643c9b1ca6260d5b3f25eb863c6fe092ce" + name = "github.com/satori/go.uuid" + packages = ["."] + pruneopts = "UT" + revision = "8ccf5352a842c034b1a69f28c863aff9b1cdb116" + +[[projects]] + digest = "1:3f53e9e4dfbb664cd62940c9c4b65a2171c66acd0b7621a1a6b8e78513525a52" + name = "github.com/sirupsen/logrus" + packages = ["."] + pruneopts = "UT" + revision = "ad15b42461921f1fb3529b058c6786c6a45d5162" + version = "v1.1.1" + +[[projects]] + digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd" + name = "github.com/spf13/afero" + packages = [ + ".", + "mem", + ] + pruneopts = "UT" + revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd" + version = "v1.1.2" + +[[projects]] + digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f" + name = "github.com/spf13/cast" + packages = ["."] + pruneopts = "UT" + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" + +[[projects]] + digest = "1:68ea4e23713989dc20b1bded5d9da2c5f9be14ff9885beef481848edd18c26cb" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + pruneopts = "UT" + revision = "4a4406e478ca629068e7768fc33f3f044173c0a6" + version = "v1.0.0" + +[[projects]] + digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" + name = "github.com/spf13/pflag" + packages = ["."] + pruneopts = "UT" + revision = "298182f68c66c05229eb03ac171abe6e309ee79a" + version = "v1.0.3" + +[[projects]] + branch = "master" + digest = "1:748519c76ecc7b5d673d7ee8924ace736ec1717b93011c77171b8bd961ac280c" + name = "github.com/spf13/viper" + packages = ["."] + pruneopts = "UT" + revision = "62edee319679b6ceaec16de03b966102d2dea709" + +[[projects]] + branch = "master" + digest = "1:b046e193dd6bb64f4df01dcc12eec2ec89ba32565f025f9cb9f1d54bc3945be9" + name = "github.com/vishvananda/netlink" + packages = [ + ".", + "nl", + ] + pruneopts = "UT" + revision = "d3a23fd178f1a0d9cf1f194af62864b1dfe02be5" + +[[projects]] + branch = "master" + digest = "1:e4e30678fb2560b5c62f6308c5023d6c294fc7713216fa379411cc74465e866f" + name = "github.com/vishvananda/netns" + packages = ["."] + pruneopts = "UT" + revision = "13995c7128ccc8e51e9a6bd2b551020a27180abd" + +[[projects]] + branch = "master" + digest = "1:3f3a05ae0b95893d90b9b3b5afdb79a9b3d96e4e36e099d841ae602e4aca0da8" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + pruneopts = "UT" + revision = "0c41d7ab0a0ee717d4590a44bcb987dfd9e183eb" + +[[projects]] + branch = "master" + digest = "1:505dbee0833715a72a529bb57c354826ad42a4496fad787fa143699b4de1a6d0" + name = "golang.org/x/net" + packages = [ + "context", + "http/httpguts", + "http2", + "http2/hpack", + "idna", + "internal/timeseries", + "trace", + ] + pruneopts = "UT" + revision = "04a2e542c03f1d053ab3e4d6e5abcd4b66e2be8e" + +[[projects]] + branch = "master" + digest = "1:cfc31002d1ab36060fcd4a29d9f6bad6f9eeeab1dc6f5be78d37a0f825ba6dc1" + name = "golang.org/x/sys" + packages = [ + "unix", + "windows", + ] + pruneopts = "UT" + revision = "eda9bb28ed513021f3e6a2a361031adc3d8a6301" + +[[projects]] + digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable", + ] + pruneopts = "UT" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" + +[[projects]] + branch = "master" + digest = "1:56b0bca90b7e5d1facf5fbdacba23e4e0ce069d25381b8e2f70ef1e7ebfb9c1a" + name = "google.golang.org/genproto" + packages = ["googleapis/rpc/status"] + pruneopts = "UT" + revision = "94acd270e44e65579b9ee3cdab25034d33fed608" + +[[projects]] + digest = "1:ab8e92d746fb5c4c18846b0879842ac8e53b3d352449423d0924a11f1020ae1b" + name = "google.golang.org/grpc" + packages = [ + ".", + "balancer", + "balancer/base", + "balancer/roundrobin", + "codes", + "connectivity", + "credentials", + "encoding", + "encoding/proto", + "grpclog", + "internal", + "internal/backoff", + "internal/channelz", + "internal/envconfig", + "internal/grpcrand", + "internal/transport", + "keepalive", + "metadata", + "naming", + "peer", + "resolver", + "resolver/dns", + "resolver/passthrough", + "stats", + "status", + "tap", + ] + pruneopts = "UT" + revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1" + version = "v1.15.0" + +[[projects]] + branch = "v2" + digest = "1:5bb148b78468350091db2ffbb2370f35cc6dcd74d9378a31b1c7b86ff7528f08" + name = "gopkg.in/tomb.v2" + packages = ["."] + pruneopts = "UT" + revision = "d5d1b5820637886def9eef33e03a27a9f166942c" + +[[projects]] + digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" + name = "gopkg.in/yaml.v2" + packages = ["."] + pruneopts = "UT" + revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" + version = "v2.2.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = [ + "github.com/golang/glog", + "github.com/golang/protobuf/ptypes", + "github.com/golang/protobuf/ptypes/any", + "github.com/osrg/gobgp/api", + "github.com/osrg/gobgp/pkg/server", + ] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..f215d19 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,42 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "github.com/golang/glog" + +[[constraint]] + branch = "master" + name = "github.com/golang/protobuf" + +[[constraint]] + branch = "master" + name = "github.com/osrg/gobgp" + +[prune] + go-tests = true + unused-packages = true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50d742e --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: all gocast test + +all: + $(MAKE) deps + $(MAKE) gocast + +deps: + go get -u golang.org/x/lint/golint + go get -u github.com/golang/dep/cmd/dep + dep ensure + +gocast: + go build . + +debug: + dep ensure + go build -race . + +test: + go test -v -race -short -failfast ./... + +linux: + GOOS=linux GOARCH=amd64 go build -o gocast_linux . diff --git a/controller/app.go b/controller/app.go new file mode 100644 index 0000000..b6235b3 --- /dev/null +++ b/controller/app.go @@ -0,0 +1,64 @@ +package controller + +import ( + "fmt" + "github.com/golang/glog" + "net" + "strings" +) + +type MonitorType int + +const ( + Monitor_PORT MonitorType = 1 + Monitor_EXEC MonitorType = 2 +) + +var Monitors = map[string]MonitorType{"port": Monitor_PORT, "exec": Monitor_EXEC} + +func (m MonitorType) String() string { + for str, mtr := range Monitors { + if m == mtr { + return str + } + } + return "unknown" +} + +type Monitor struct { + Type MonitorType + Port string + Protocol string + Cmd string +} + +type App struct { + Name string + Vip *net.IPNet + Monitor Monitor +} + +func NewApp(appName, vip, monitor, monitorType string) (*App, error) { + if appName == "" { + return nil, fmt.Errorf("Invalid app name") + } + app := &App{Name: appName} + _, ipnet, err := net.ParseCIDR(vip) + if err != nil { + return nil, fmt.Errorf("Invalid VIP specified, need ip/mask") + } + app.Vip = ipnet + m := Monitor{Type: Monitors[monitorType]} + switch monitorType { + case "port": + parts := strings.Split(monitor, ":") + m.Protocol = parts[0] + m.Port = parts[1] + case "exec": + m.Cmd = monitor + default: + glog.V(2).Infof("No monitor specified") + } + app.Monitor = m + return app, nil +} diff --git a/controller/bgp.go b/controller/bgp.go new file mode 100644 index 0000000..1bf8006 --- /dev/null +++ b/controller/bgp.go @@ -0,0 +1,136 @@ +package controller + +import ( + "context" + "fmt" + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/any" + api "github.com/osrg/gobgp/api" + gobgp "github.com/osrg/gobgp/pkg/server" + "net" +) + +type Controller struct { + peerAS int + localIP, peerIP net.IP + s *gobgp.BgpServer +} + +func NewController(localAS, peerAS int, peerIP string) (*Controller, error) { + c := &Controller{} + if peerIP == "" { + gw, err := gateway() + if err != nil { + return nil, err + } + c.peerIP = gw + } else { + c.peerIP = net.ParseIP(peerIP) + } + if c.peerIP == nil { + return nil, fmt.Errorf("Unable to get peer IP") + } + s := gobgp.NewBgpServer() + go s.Serve() + localAddr, err := localAddress(c.peerIP) + if err != nil { + return nil, err + } + c.localIP = localAddr + if err := s.StartBgp(context.Background(), &api.StartBgpRequest{ + Global: &api.Global{ + As: uint32(localAS), + RouterId: localAddr.String(), + ListenPort: -1, // gobgp won't listen on tcp:179 + }, + }); err != nil { + return nil, fmt.Errorf("Unable to start bgp: %v", err) + } + c.s = s + c.peerAS = peerAS + return c, nil +} + +func (c *Controller) AddPeer(peer string) error { + n := &api.Peer{ + Conf: &api.PeerConf{ + NeighborAddress: peer, + PeerAs: uint32(c.peerAS), + }, + } + return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n}) +} + +func (c *Controller) getApiPath(route *net.IPNet) *api.Path { + afi := api.Family_AFI_IP + if route.IP.To4() == nil { + afi = api.Family_AFI_IP6 + } + prefixlen, _ := route.Mask.Size() + nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{ + Prefix: route.IP.String(), + PrefixLen: uint32(prefixlen), + }) + a1, _ := ptypes.MarshalAny(&api.OriginAttribute{ + Origin: 0, + }) + a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{ + NextHop: c.localIP.String(), + }) + attrs := []*any.Any{a1, a2} + return &api.Path{ + Family: &api.Family{Afi: afi, Safi: api.Family_SAFI_UNICAST}, + AnyNlri: nlri, + AnyPattrs: attrs, + } +} + +func (c *Controller) Announce(route *net.IPNet) error { + peers, err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}) + if err != nil { + return err + } + var found bool + for _, p := range peers { + if p.Conf.NeighborAddress == c.peerIP.String() { + found = true + break + } + } + if !found { + if err := c.AddPeer(c.peerIP.String()); err != nil { + return err + } + } + _, err = c.s.AddPath(context.Background(), &api.AddPathRequest{Path: c.getApiPath(route)}) + return err +} + +func (c *Controller) Withdraw(route *net.IPNet) error { + return c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: c.getApiPath(route)}) +} + +func (c *Controller) PeerInfo() (*api.Peer, error) { + peers, err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}) + if err != nil { + return nil, err + } + for _, p := range peers { + if p.Conf.NeighborAddress == c.peerIP.String() { + return p, nil + } + } + return nil, nil +} + +func (c *Controller) Shutdown() error { + if err := c.s.ShutdownPeer(context.Background(), &api.ShutdownPeerRequest{ + Address: c.peerIP.String(), + }); err != nil { + return err + } + if err := c.s.StopBgp(context.Background(), &api.StopBgpRequest{}); err != nil { + return err + } + return nil +} diff --git a/controller/monitor.go b/controller/monitor.go new file mode 100644 index 0000000..fde4fe3 --- /dev/null +++ b/controller/monitor.go @@ -0,0 +1,195 @@ +package controller + +import ( + "fmt" + "github.com/golang/glog" + api "github.com/osrg/gobgp/api" + "net" + "os/exec" + "sync" + "time" +) + +func portMonitor(protocol, port string) bool { + switch protocol { + case "tcp": + conn, err := net.Listen(protocol, ":"+port) + if err != nil { + glog.V(4).Infof("Monitor tcp port up") + return true + } + defer conn.Close() + case "udp": + conn, err := net.ListenPacket(protocol, ":"+port) + if err != nil { + glog.V(4).Infof("Monitor udp port up") + return true + } + defer conn.Close() + } + return false +} + +func execMonitor(cmd string) bool { + out := exec.Command("bash", "-c", cmd) + if err := out.Start(); err != nil { + glog.V(2).Infof("Cannot exec cmd: %s : %v", cmd, err) + return false + } + if err := out.Wait(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + glog.V(4).Infof("Monitor cmd failed") + return false + } + } + return true +} + +type appMon struct { + app *App + done chan bool + announced bool + checkOn bool +} + +type MonitorMgr struct { + monitors map[string]*appMon + cleanups map[string]chan bool + c *Controller + monitorInterval time.Duration + cleanupTimer time.Duration + sync.Mutex +} + +func NewMonitor(localAS, peerAS int, monitorInterval time.Duration, peerIP string, cleanup time.Duration) *MonitorMgr { + c, err := NewController(localAS, peerAS, peerIP) + if err != nil { + glog.Exitf("Failed to start BGP controller: %v", err) + } + return &MonitorMgr{ + c: c, + monitors: make(map[string]*appMon), + cleanups: make(map[string]chan bool), + monitorInterval: monitorInterval, + cleanupTimer: cleanup, + } +} + +func (m *MonitorMgr) Add(app *App) { + // stop and start a new one if one already running + m.Remove(app.Name) + appMon := &appMon{app: app, done: make(chan bool)} + m.Lock() + m.monitors[app.Name] = appMon + m.Unlock() + go m.runLoop(appMon) + glog.Infof("Registered a new app: %v", app) +} + +func (m *MonitorMgr) Remove(appName string) { + m.Lock() + defer m.Unlock() + if a, ok := m.monitors[appName]; ok { + if a.checkOn { + a.done <- true + } + if a.announced { + if err := m.c.Withdraw(a.app.Vip); err != nil { + glog.Errorf("Failed to withdraw route: %v", err) + } + } + deleteLoopback(appName) + } + delete(m.monitors, appName) +} + +func (m *MonitorMgr) checkCond(am *appMon) error { + var cond bool + app := am.app + switch app.Monitor.Type { + case Monitor_PORT: + cond = portMonitor(app.Monitor.Protocol, app.Monitor.Port) + case Monitor_EXEC: + cond = execMonitor(app.Monitor.Cmd) + } + m.Lock() + defer m.Unlock() + if cond { + glog.V(2).Infof("%s Monitor for app: %s succeeded", app.Monitor.Type.String(), app.Name) + if !am.announced { + if err := addLoopback(app.Name, app.Vip); err != nil { + return err + } + if err := m.c.Announce(app.Vip); err != nil { + return fmt.Errorf("Failed to announce route: %v", err) + } + am.announced = true + if exit, ok := m.cleanups[app.Name]; ok { + exit <- true + } + } + } else { + glog.V(2).Infof("%s Monitor for app: %s Failed", app.Monitor.Type.String(), app.Name) + if am.announced { + if err := m.c.Withdraw(app.Vip); err != nil { + return fmt.Errorf("Failed to withdraw route: %v", err) + } + am.announced = false + exit := make(chan bool) + go m.Cleanup(app.Name, exit) + m.cleanups[app.Name] = exit + } + } + return nil +} + +func (m *MonitorMgr) runLoop(am *appMon) { + am.checkOn = true + if err := m.checkCond(am); err != nil { + glog.Errorln(err) + } + t := time.NewTicker(m.monitorInterval) + defer t.Stop() + for { + select { + case <-t.C: + if err := m.checkCond(am); err != nil { + glog.Errorln(err) + } + case <-am.done: + glog.V(2).Infof("Exit run-loop for app: %s", am.app.Name) + return + } + } +} + +func (m *MonitorMgr) CloseAll() { + glog.Infof("Shutting down all open bgp sessions") + if err := m.c.Shutdown(); err != nil { + glog.Errorf("Failed to shut-down BGP: %v", err) + } + for name, am := range m.monitors { + if am.checkOn { + am.done <- true + } + deleteLoopback(name) + } +} + +func (m *MonitorMgr) Cleanup(app string, exit chan bool) { + t := time.NewTimer(m.cleanupTimer) + defer t.Stop() + for { + select { + case <-t.C: + glog.Infof("Cleaning up app %s", app) + m.Remove(app) + case <-exit: + return + } + } +} + +func (m *MonitorMgr) GetInfo() (*api.Peer, error) { + return m.c.PeerInfo() +} diff --git a/controller/system.go b/controller/system.go new file mode 100644 index 0000000..1a4b74f --- /dev/null +++ b/controller/system.go @@ -0,0 +1,52 @@ +package controller + +import ( + "fmt" + "net" + "os/exec" + "strings" +) + +func gateway() (net.IP, error) { + cmd := `ip route | grep "^default" | cut -d" " -f3` + out, err := exec.Command("bash", "-c", cmd).Output() + if err != nil { + return nil, fmt.Errorf("Failed to execute command: %s", cmd) + } + return net.ParseIP(strings.TrimSpace(string(out))), nil +} + +func localAddress(gw net.IP) (net.IP, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + if v.Contains(gw) { + return v.IP, nil + } + } + } + return nil, fmt.Errorf("Unable to find local address") +} + +func addLoopback(name string, addr *net.IPNet) error { + mask := fmt.Sprintf("%d.%d.%d.%d", addr.Mask[0], addr.Mask[1], addr.Mask[2], addr.Mask[3]) + cmd := fmt.Sprintf("ifconfig lo:%s %s netmask %s up", name, addr.IP.String(), mask) + _, err := exec.Command("bash", "-c", cmd).Output() + if err != nil { + return fmt.Errorf("Failed to Add loopback command: %v", err) + } + return nil +} + +func deleteLoopback(name string) error { + cmd := fmt.Sprintf("ifconfig lo:%s down", name) + _, err := exec.Command("bash", "-c", cmd).Output() + if err != nil { + return fmt.Errorf("Failed to delete loopback command: %v", err) + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..3f4c0ac --- /dev/null +++ b/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "flag" + "github.com/mayuresh82/gocast/controller" + "github.com/mayuresh82/gocast/server" + "os" + "os/signal" + "syscall" + "time" +) + +var ( + serverAddr = flag.String("serverAddr", ":8080", "Addr for http service") + localAS = flag.Int("localAS", 65000, "Local ASN of the gocast host") + peerAS = flag.Int("peerAS", 65254, "AS to peer with") + monitorInterval = flag.Duration("monitorInterval", 5*time.Second, "Interval for health check") + peerIP = flag.String("peerIP", "", "Override the IP to peer with. Default: gateway ip") + cleanupTimer = flag.Duration("cleanup", 15*time.Minute, "Time to flush out inactive apps") +) + +func main() { + flag.Parse() + mon := controller.NewMonitor(*localAS, *peerAS, *monitorInterval, *peerIP, *cleanupTimer) + srv := server.NewServer(*serverAddr, mon) + + ctx, cancel := context.WithCancel(context.Background()) + // catch interrupt + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM) + go func() { + for { + sig := <-signalChan + if sig == os.Interrupt || sig == syscall.SIGTERM { + mon.CloseAll() + cancel() + return + } + } + }() + srv.Serve(ctx) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..2c9b09c --- /dev/null +++ b/server/server.go @@ -0,0 +1,75 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "github.com/golang/glog" + "github.com/mayuresh82/gocast/controller" + "net/http" +) + +// Server is the main entrypoint into the app and serves app requests +type Server struct { + ListenAddr string + mon *controller.MonitorMgr +} + +func NewServer(addr string, mon *controller.MonitorMgr) *Server { + return &Server{ + ListenAddr: addr, + mon: mon, + } +} + +func (s *Server) Serve(ctx context.Context) { + glog.Infof("Starting http server on %s", s.ListenAddr) + http.HandleFunc("/register", s.registerHandler) + http.HandleFunc("/unregister", s.unregisterHandler) + http.HandleFunc("/info", s.infoHandler) + srv := &http.Server{Addr: s.ListenAddr} + idleConnsClosed := make(chan struct{}) + go func() { + <-ctx.Done() + if err := srv.Shutdown(context.Background()); err != nil { + // Error from closing listeners, or context timeout: + glog.Errorf("HTTP server Shutdown Error: %v", err) + } + close(idleConnsClosed) + }() + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + // Error starting or closing listener + glog.Errorf("HTTP server ListenAndServe Error: %v", err) + } + <-idleConnsClosed +} + +func (s *Server) registerHandler(w http.ResponseWriter, r *http.Request) { + queries := r.URL.Query() + app, err := controller.NewApp(queries["name"][0], queries["vip"][0], queries["monitor"][0], queries["type"][0]) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest) + return + } + s.mon.Add(app) +} + +func (s *Server) unregisterHandler(w http.ResponseWriter, r *http.Request) { + queries := r.URL.Query() + appName, ok := queries["name"] + if !ok { + http.Error(w, "Invalid request, need app name specified", http.StatusBadRequest) + return + } + s.mon.Remove(appName[0]) +} + +func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) { + peer, err := s.mon.GetInfo() + if err != nil { + http.Error(w, fmt.Sprintf("Internal error getting peers: %v", err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(peer) +}