Merge branch 'master' into readme_update
This commit is contained in:
commit
d802d3e5a0
@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ecix/alice-lg/backend/api"
|
||||
|
||||
@ -89,8 +90,8 @@ func apiRegisterEndpoints(router *httprouter.Router) error {
|
||||
endpoint(apiRoutesList))
|
||||
|
||||
// Querying
|
||||
router.GET("/api/routeservers/:id/lookup/prefix",
|
||||
endpoint(apiLookupPrefix))
|
||||
router.GET("/api/lookup/prefix",
|
||||
endpoint(apiLookupPrefixGlobal))
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -126,9 +127,9 @@ func apiRouteserversList(_req *http.Request, _params httprouter.Params) (api.Res
|
||||
routeservers := []api.Routeserver{}
|
||||
|
||||
sources := AliceConfig.Sources
|
||||
for id, source := range sources {
|
||||
for _, source := range sources {
|
||||
routeservers = append(routeservers, api.Routeserver{
|
||||
Id: id,
|
||||
Id: source.Id,
|
||||
Name: source.Name,
|
||||
})
|
||||
}
|
||||
@ -178,6 +179,26 @@ func validateQueryString(req *http.Request, key string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Helper: Validate prefix query
|
||||
func validatePrefixQuery(value string) (string, error) {
|
||||
|
||||
// We should at least provide 2 chars
|
||||
if len(value) < 2 {
|
||||
return "", fmt.Errorf("Query too short")
|
||||
}
|
||||
|
||||
// Query constraints: Should at least include a dot or colon
|
||||
/* let's try without this :)
|
||||
|
||||
if strings.Index(value, ".") == -1 &&
|
||||
strings.Index(value, ":") == -1 {
|
||||
return "", fmt.Errorf("Query needs at least a ':' or '.'")
|
||||
}
|
||||
*/
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Handle status
|
||||
func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
@ -212,19 +233,27 @@ func apiRoutesList(_req *http.Request, params httprouter.Params) (api.Response,
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Handle lookup
|
||||
func apiLookupPrefix(req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle global lookup
|
||||
func apiLookupPrefixGlobal(req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
// Get prefix to query
|
||||
prefix, err := validateQueryString(req, "q")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source := AliceConfig.Sources[rsId].getInstance()
|
||||
result, err := source.LookupPrefix(prefix)
|
||||
return result, err
|
||||
prefix, err = validatePrefixQuery(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make response
|
||||
t0 := time.Now()
|
||||
routes := AliceRoutesStore.Lookup(prefix)
|
||||
|
||||
queryDuration := time.Since(t0)
|
||||
response := api.LookupResponseGlobal{
|
||||
Routes: routes,
|
||||
Time: float64(queryDuration) / 1000.0 / 1000.0, // nano -> micro -> milli
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
@ -143,6 +143,27 @@ type Route struct {
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
// Lookup Prefixes
|
||||
type LookupRoute struct {
|
||||
Id string `json:"id"`
|
||||
NeighbourId string `json:"neighbour_id"`
|
||||
Neighbour Neighbour `json:"neighbour"`
|
||||
|
||||
State string `json:"state"` // Filtered, Imported, ...
|
||||
|
||||
Routeserver Routeserver `json:"routeserver"`
|
||||
|
||||
Network string `json:"network"`
|
||||
Interface string `json:"interface"`
|
||||
Gateway string `json:"gateway"`
|
||||
Metric int `json:"metric"`
|
||||
Bgp BgpInfo `json:"bgp"`
|
||||
Age time.Duration `json:"age"`
|
||||
Type []string `json:"type"` // [BGP, unicast, univ]
|
||||
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
type Routes []Route
|
||||
|
||||
// Implement sorting interface for routes
|
||||
@ -166,6 +187,11 @@ type RoutesResponse struct {
|
||||
}
|
||||
|
||||
type LookupResponse struct {
|
||||
Api ApiStatus `json:"api"`
|
||||
Routes []Route `json:"routes"`
|
||||
Api ApiStatus `json:"api"`
|
||||
Routes []LookupRoute `json:"routes"`
|
||||
}
|
||||
|
||||
type LookupResponseGlobal struct {
|
||||
Routes []LookupRoute `json:"routes"`
|
||||
Time float64 `json:"query_duration_ms"`
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ type UiConfig struct {
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
Id int
|
||||
Name string
|
||||
Type int
|
||||
|
||||
@ -54,6 +55,8 @@ type Config struct {
|
||||
Ui UiConfig
|
||||
Sources []SourceConfig
|
||||
File string
|
||||
|
||||
instances map[SourceConfig]sources.Source
|
||||
}
|
||||
|
||||
// Get sources keys form ini
|
||||
@ -181,6 +184,7 @@ func getSources(config *ini.File) ([]SourceConfig, error) {
|
||||
sources := []SourceConfig{}
|
||||
|
||||
sourceSections := config.ChildSections("source")
|
||||
sourceId := 0
|
||||
for _, section := range sourceSections {
|
||||
if !isSourceBase(section) {
|
||||
continue
|
||||
@ -209,6 +213,7 @@ func getSources(config *ini.File) ([]SourceConfig, error) {
|
||||
|
||||
// Make config
|
||||
config := SourceConfig{
|
||||
Id: sourceId,
|
||||
Name: section.Key("name").MustString("Unknown Source"),
|
||||
Type: backendType,
|
||||
}
|
||||
@ -217,10 +222,14 @@ func getSources(config *ini.File) ([]SourceConfig, error) {
|
||||
switch backendType {
|
||||
case SOURCE_BIRDWATCHER:
|
||||
backendConfig.MapTo(&config.Birdwatcher)
|
||||
config.Birdwatcher.Id = config.Id
|
||||
config.Birdwatcher.Name = config.Name
|
||||
}
|
||||
|
||||
// Add to list of sources
|
||||
sources = append(sources, config)
|
||||
|
||||
sourceId += 1
|
||||
}
|
||||
|
||||
return sources, nil
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
var AliceConfig *Config
|
||||
var AliceRoutesStore *RoutesStore
|
||||
var AliceNeighboursStore *NeighboursStore
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
@ -32,6 +34,14 @@ func main() {
|
||||
|
||||
log.Println("Using configuration:", AliceConfig.File)
|
||||
|
||||
// Setup local routes store
|
||||
AliceRoutesStore = NewRoutesStore(AliceConfig)
|
||||
AliceRoutesStore.Start()
|
||||
|
||||
// Setup local neighbours store
|
||||
AliceNeighboursStore = NewNeighboursStore(AliceConfig)
|
||||
AliceNeighboursStore.Start()
|
||||
|
||||
// Setup request routing
|
||||
router := httprouter.New()
|
||||
|
||||
|
135
backend/neighbours_store.go
Normal file
135
backend/neighbours_store.go
Normal file
@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ecix/alice-lg/backend/api"
|
||||
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NeighboursIndex map[string]api.Neighbour
|
||||
|
||||
type NeighboursStore struct {
|
||||
neighboursMap map[int]NeighboursIndex
|
||||
configMap map[int]SourceConfig
|
||||
statusMap map[int]StoreStatus
|
||||
}
|
||||
|
||||
func NewNeighboursStore(config *Config) *NeighboursStore {
|
||||
|
||||
// Build source mapping
|
||||
neighboursMap := make(map[int]NeighboursIndex)
|
||||
configMap := make(map[int]SourceConfig)
|
||||
statusMap := make(map[int]StoreStatus)
|
||||
|
||||
for _, source := range config.Sources {
|
||||
sourceId := source.Id
|
||||
configMap[sourceId] = source
|
||||
statusMap[sourceId] = StoreStatus{
|
||||
State: STATE_INIT,
|
||||
}
|
||||
|
||||
neighboursMap[sourceId] = make(NeighboursIndex)
|
||||
}
|
||||
|
||||
store := &NeighboursStore{
|
||||
neighboursMap: neighboursMap,
|
||||
statusMap: statusMap,
|
||||
configMap: configMap,
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) Start() {
|
||||
log.Println("Starting local neighbours store")
|
||||
go self.init()
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) init() {
|
||||
// Perform initial update
|
||||
self.update()
|
||||
|
||||
// Initial logging
|
||||
self.Stats().Log()
|
||||
|
||||
// Periodically update store
|
||||
for {
|
||||
time.Sleep(5 * time.Minute)
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) update() {
|
||||
for sourceId, _ := range self.neighboursMap {
|
||||
// Get current state
|
||||
if self.statusMap[sourceId].State == STATE_UPDATING {
|
||||
continue // nothing to do here. really.
|
||||
}
|
||||
|
||||
// Start updating
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
State: STATE_UPDATING,
|
||||
}
|
||||
|
||||
source := self.configMap[sourceId].getInstance()
|
||||
|
||||
neighboursRes, err := source.Neighbours()
|
||||
neighbours := neighboursRes.Neighbours
|
||||
if err != nil {
|
||||
// That's sad.
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
State: STATE_ERROR,
|
||||
LastError: err,
|
||||
LastRefresh: time.Now(),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Update data
|
||||
// Make neighbours index
|
||||
index := make(NeighboursIndex)
|
||||
for _, neighbour := range neighbours {
|
||||
index[neighbour.Id] = neighbour
|
||||
}
|
||||
|
||||
self.neighboursMap[sourceId] = index
|
||||
// Update state
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
LastRefresh: time.Now(),
|
||||
State: STATE_READY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) GetNeighbourAt(
|
||||
sourceId int,
|
||||
id string,
|
||||
) api.Neighbour {
|
||||
// Lookup neighbour on RS
|
||||
neighbours := self.neighboursMap[sourceId]
|
||||
return neighbours[id]
|
||||
}
|
||||
|
||||
// Build some stats for monitoring
|
||||
func (self *NeighboursStore) Stats() NeighboursStoreStats {
|
||||
totalNeighbours := 0
|
||||
rsStats := []RouteServerNeighboursStats{}
|
||||
|
||||
for sourceId, neighbours := range self.neighboursMap {
|
||||
status := self.statusMap[sourceId]
|
||||
totalNeighbours += len(neighbours)
|
||||
serverStats := RouteServerNeighboursStats{
|
||||
Name: self.configMap[sourceId].Name,
|
||||
State: stateToString(status.State),
|
||||
Neighbours: len(neighbours),
|
||||
UpdatedAt: status.LastRefresh,
|
||||
}
|
||||
rsStats = append(rsStats, serverStats)
|
||||
}
|
||||
|
||||
storeStats := NeighboursStoreStats{
|
||||
TotalNeighbours: totalNeighbours,
|
||||
RouteServers: rsStats,
|
||||
}
|
||||
return storeStats
|
||||
}
|
242
backend/routes_store.go
Normal file
242
backend/routes_store.go
Normal file
@ -0,0 +1,242 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ecix/alice-lg/backend/api"
|
||||
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RoutesStore struct {
|
||||
routesMap map[int]api.RoutesResponse
|
||||
statusMap map[int]StoreStatus
|
||||
configMap map[int]SourceConfig
|
||||
}
|
||||
|
||||
func NewRoutesStore(config *Config) *RoutesStore {
|
||||
|
||||
// Build mapping based on source instances
|
||||
routesMap := make(map[int]api.RoutesResponse)
|
||||
statusMap := make(map[int]StoreStatus)
|
||||
configMap := make(map[int]SourceConfig)
|
||||
|
||||
for _, source := range config.Sources {
|
||||
id := source.Id
|
||||
|
||||
configMap[id] = source
|
||||
routesMap[id] = api.RoutesResponse{}
|
||||
statusMap[id] = StoreStatus{
|
||||
State: STATE_INIT,
|
||||
}
|
||||
}
|
||||
|
||||
store := &RoutesStore{
|
||||
routesMap: routesMap,
|
||||
statusMap: statusMap,
|
||||
configMap: configMap,
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func (self *RoutesStore) Start() {
|
||||
log.Println("Starting local routes store")
|
||||
go self.init()
|
||||
}
|
||||
|
||||
// Service initialization
|
||||
func (self *RoutesStore) init() {
|
||||
// Initial refresh
|
||||
self.update()
|
||||
|
||||
// Initial stats
|
||||
self.Stats().Log()
|
||||
|
||||
// Periodically update store
|
||||
for {
|
||||
// TODO: Add config option
|
||||
time.Sleep(5 * time.Minute)
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
// Update all routes
|
||||
func (self *RoutesStore) update() {
|
||||
for sourceId, _ := range self.routesMap {
|
||||
source := self.configMap[sourceId].getInstance()
|
||||
|
||||
// Get current update state
|
||||
if self.statusMap[sourceId].State == STATE_UPDATING {
|
||||
continue // nothing to do here
|
||||
}
|
||||
|
||||
// Set update state
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
State: STATE_UPDATING,
|
||||
}
|
||||
|
||||
routes, err := source.AllRoutes()
|
||||
if err != nil {
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
State: STATE_ERROR,
|
||||
LastError: err,
|
||||
LastRefresh: time.Now(),
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Update data
|
||||
self.routesMap[sourceId] = routes
|
||||
// Update state
|
||||
self.statusMap[sourceId] = StoreStatus{
|
||||
LastRefresh: time.Now(),
|
||||
State: STATE_READY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate store insights
|
||||
func (self *RoutesStore) Stats() RoutesStoreStats {
|
||||
totalImported := 0
|
||||
totalFiltered := 0
|
||||
|
||||
rsStats := []RouteServerRoutesStats{}
|
||||
|
||||
for sourceId, routes := range self.routesMap {
|
||||
status := self.statusMap[sourceId]
|
||||
|
||||
totalImported += len(routes.Imported)
|
||||
totalFiltered += len(routes.Filtered)
|
||||
|
||||
serverStats := RouteServerRoutesStats{
|
||||
Name: self.configMap[sourceId].Name,
|
||||
|
||||
Routes: RoutesStats{
|
||||
Filtered: len(routes.Filtered),
|
||||
Imported: len(routes.Imported),
|
||||
},
|
||||
|
||||
State: stateToString(status.State),
|
||||
UpdatedAt: status.LastRefresh,
|
||||
}
|
||||
|
||||
rsStats = append(rsStats, serverStats)
|
||||
}
|
||||
|
||||
// Make stats
|
||||
storeStats := RoutesStoreStats{
|
||||
TotalRoutes: RoutesStats{
|
||||
Imported: totalImported,
|
||||
Filtered: totalFiltered,
|
||||
},
|
||||
RouteServers: rsStats,
|
||||
}
|
||||
return storeStats
|
||||
}
|
||||
|
||||
// Routes filter
|
||||
func filterRoutes(
|
||||
config SourceConfig,
|
||||
routes []api.Route,
|
||||
prefix string,
|
||||
state string,
|
||||
) []api.LookupRoute {
|
||||
|
||||
results := []api.LookupRoute{}
|
||||
|
||||
for _, route := range routes {
|
||||
// Naiive filtering:
|
||||
if strings.HasPrefix(route.Network, prefix) {
|
||||
lookup := api.LookupRoute{
|
||||
Id: route.Id,
|
||||
NeighbourId: route.NeighbourId,
|
||||
|
||||
Routeserver: api.Routeserver{
|
||||
Id: config.Id,
|
||||
Name: config.Name,
|
||||
},
|
||||
|
||||
State: state,
|
||||
|
||||
Network: route.Network,
|
||||
Interface: route.Interface,
|
||||
Gateway: route.Gateway,
|
||||
Metric: route.Metric,
|
||||
Bgp: route.Bgp,
|
||||
Age: route.Age,
|
||||
Type: route.Type,
|
||||
}
|
||||
results = append(results, lookup)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func addNeighbour(
|
||||
sourceId int,
|
||||
route api.LookupRoute,
|
||||
) api.LookupRoute {
|
||||
neighbour := AliceNeighboursStore.GetNeighbourAt(
|
||||
sourceId, route.NeighbourId)
|
||||
route.Neighbour = neighbour
|
||||
return route
|
||||
}
|
||||
|
||||
// Single RS lookup
|
||||
func (self *RoutesStore) lookupRs(
|
||||
sourceId int,
|
||||
prefix string,
|
||||
) chan []api.LookupRoute {
|
||||
|
||||
response := make(chan []api.LookupRoute)
|
||||
config := self.configMap[sourceId]
|
||||
routes := self.routesMap[sourceId]
|
||||
|
||||
go func() {
|
||||
result := []api.LookupRoute{}
|
||||
|
||||
filtered := filterRoutes(
|
||||
config,
|
||||
routes.Filtered,
|
||||
prefix,
|
||||
"filtered")
|
||||
imported := filterRoutes(
|
||||
config,
|
||||
routes.Imported,
|
||||
prefix,
|
||||
"imported")
|
||||
|
||||
// Add Neighbours to results
|
||||
for _, route := range filtered {
|
||||
result = append(result, addNeighbour(sourceId, route))
|
||||
}
|
||||
|
||||
for _, route := range imported {
|
||||
result = append(result, addNeighbour(sourceId, route))
|
||||
}
|
||||
|
||||
response <- result
|
||||
}()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (self *RoutesStore) Lookup(prefix string) []api.LookupRoute {
|
||||
result := []api.LookupRoute{}
|
||||
responses := []chan []api.LookupRoute{}
|
||||
|
||||
// Dispatch
|
||||
for sourceId, _ := range self.routesMap {
|
||||
res := self.lookupRs(sourceId, prefix)
|
||||
responses = append(responses, res)
|
||||
}
|
||||
|
||||
// Collect
|
||||
for _, response := range responses {
|
||||
routes := <-response
|
||||
result = append(result, routes...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package birdwatcher
|
||||
|
||||
type Config struct {
|
||||
Id int
|
||||
Name string
|
||||
|
||||
Api string `ini:"api"`
|
||||
Timezone string `ini:"timezone"`
|
||||
ShowLastReboot bool `ini:"show_last_reboot"`
|
||||
|
@ -229,13 +229,9 @@ func mustInt(value interface{}, fallback int) int {
|
||||
return int(fval)
|
||||
}
|
||||
|
||||
// Parse routes response
|
||||
func parseRoutes(bird ClientResponse, config Config) ([]api.Route, error) {
|
||||
// Parse partial routes response
|
||||
func parseRoutesData(birdRoutes []interface{}, config Config) api.Routes {
|
||||
routes := api.Routes{}
|
||||
birdRoutes, ok := bird["routes"].([]interface{})
|
||||
if !ok {
|
||||
return routes, fmt.Errorf("Routes response missing")
|
||||
}
|
||||
|
||||
for _, data := range birdRoutes {
|
||||
rdata := data.(map[string]interface{})
|
||||
@ -261,9 +257,51 @@ func parseRoutes(bird ClientResponse, config Config) ([]api.Route, error) {
|
||||
|
||||
routes = append(routes, route)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// Parse routes response
|
||||
func parseRoutes(bird ClientResponse, config Config) ([]api.Route, error) {
|
||||
birdRoutes, ok := bird["routes"].([]interface{})
|
||||
if !ok {
|
||||
return []api.Route{}, fmt.Errorf("Routes response missing")
|
||||
}
|
||||
|
||||
routes := parseRoutesData(birdRoutes, config)
|
||||
|
||||
// Sort routes
|
||||
sort.Sort(routes)
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func parseRoutesDump(bird ClientResponse, config Config) (api.RoutesResponse, error) {
|
||||
result := api.RoutesResponse{}
|
||||
|
||||
apiStatus, err := parseApiStatus(bird, config)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Api = apiStatus
|
||||
|
||||
// Fetch imported routes
|
||||
importedRoutes, ok := bird["imported"].([]interface{})
|
||||
if !ok {
|
||||
return result, fmt.Errorf("Imported routes missing")
|
||||
}
|
||||
|
||||
// Sort routes by network for faster querying
|
||||
imported := parseRoutesData(importedRoutes, config)
|
||||
sort.Sort(imported)
|
||||
result.Imported = imported
|
||||
|
||||
// Fetch filtered routes
|
||||
filteredRoutes, ok := bird["filtered"].([]interface{})
|
||||
if !ok {
|
||||
return result, fmt.Errorf("Filtered routes missing")
|
||||
}
|
||||
filtered := parseRoutesData(filteredRoutes, config)
|
||||
sort.Sort(filtered)
|
||||
result.Filtered = filtered
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package birdwatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ecix/alice-lg/backend/api"
|
||||
)
|
||||
|
||||
@ -111,5 +110,65 @@ func (self *Birdwatcher) Routes(neighbourId string) (api.RoutesResponse, error)
|
||||
|
||||
// Make routes lookup
|
||||
func (self *Birdwatcher) LookupPrefix(prefix string) (api.LookupResponse, error) {
|
||||
return api.LookupResponse{}, fmt.Errorf("not implemented")
|
||||
// Get RS info
|
||||
rs := api.Routeserver{
|
||||
Id: self.config.Id,
|
||||
Name: self.config.Name,
|
||||
}
|
||||
|
||||
// Query prefix on RS
|
||||
bird, err := self.client.GetJson("/routes/prefix?prefix=" + prefix)
|
||||
if err != nil {
|
||||
return api.LookupResponse{}, err
|
||||
}
|
||||
|
||||
// Parse API status
|
||||
apiStatus, err := parseApiStatus(bird, self.config)
|
||||
if err != nil {
|
||||
return api.LookupResponse{}, err
|
||||
}
|
||||
|
||||
// Parse routes
|
||||
routes, err := parseRoutes(bird, self.config)
|
||||
|
||||
// Add corresponding neighbour and source rs to result
|
||||
results := []api.LookupRoute{}
|
||||
for _, src := range routes {
|
||||
// Okay. This is actually really hacky.
|
||||
// A less bruteforce approach would be highly appreciated
|
||||
route := api.LookupRoute{
|
||||
Id: src.Id,
|
||||
|
||||
Routeserver: rs,
|
||||
|
||||
NeighbourId: src.NeighbourId,
|
||||
|
||||
Network: src.Network,
|
||||
Interface: src.Interface,
|
||||
Gateway: src.Gateway,
|
||||
Metric: src.Metric,
|
||||
Bgp: src.Bgp,
|
||||
Age: src.Age,
|
||||
Type: src.Type,
|
||||
|
||||
Details: src.Details,
|
||||
}
|
||||
results = append(results, route)
|
||||
}
|
||||
|
||||
// Make result
|
||||
response := api.LookupResponse{
|
||||
Api: apiStatus,
|
||||
Routes: results,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (self *Birdwatcher) AllRoutes() (api.RoutesResponse, error) {
|
||||
bird, err := self.client.GetJson("/routes/dump")
|
||||
if err != nil {
|
||||
return api.RoutesResponse{}, err
|
||||
}
|
||||
result, err := parseRoutesDump(bird, self.config)
|
||||
return result, err
|
||||
}
|
||||
|
25
backend/sources/birdwatcher/utils.go
Normal file
25
backend/sources/birdwatcher/utils.go
Normal file
@ -0,0 +1,25 @@
|
||||
package birdwatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ecix/alice-lg/backend/api"
|
||||
)
|
||||
|
||||
/*
|
||||
Helper functions for dealing with birdwatcher API data
|
||||
*/
|
||||
|
||||
// Get neighbour by protocol id
|
||||
func getNeighbourById(neighbours api.Neighbours, id string) (api.Neighbour, error) {
|
||||
for _, n := range neighbours {
|
||||
if n.Id == id {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
unknown := api.Neighbour{
|
||||
Id: "unknown",
|
||||
Description: "Unknown neighbour",
|
||||
}
|
||||
return unknown, fmt.Errorf("Neighbour not found")
|
||||
}
|
@ -8,5 +8,5 @@ type Source interface {
|
||||
Status() (api.StatusResponse, error)
|
||||
Neighbours() (api.NeighboursResponse, error)
|
||||
Routes(neighbourId string) (api.RoutesResponse, error)
|
||||
LookupPrefix(prefix string) (api.LookupResponse, error)
|
||||
AllRoutes() (api.RoutesResponse, error)
|
||||
}
|
||||
|
@ -4,14 +4,28 @@ var version = "unknown"
|
||||
|
||||
// Gather application status information
|
||||
type AppStatus struct {
|
||||
Version string `json:"version"`
|
||||
Version string `json:"version"`
|
||||
Routes RoutesStoreStats `json:"routes"`
|
||||
Neighbours NeighboursStoreStats `json:"neighbours"`
|
||||
}
|
||||
|
||||
// Get application status, perform health checks
|
||||
// on backends.
|
||||
func NewAppStatus() (*AppStatus, error) {
|
||||
routesStatus := RoutesStoreStats{}
|
||||
if AliceRoutesStore != nil {
|
||||
routesStatus = AliceRoutesStore.Stats()
|
||||
}
|
||||
|
||||
neighboursStatus := NeighboursStoreStats{}
|
||||
if AliceRoutesStore != nil {
|
||||
neighboursStatus = AliceNeighboursStore.Stats()
|
||||
}
|
||||
|
||||
status := &AppStatus{
|
||||
Version: version,
|
||||
Version: version,
|
||||
Routes: routesStatus,
|
||||
Neighbours: neighboursStatus,
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
33
backend/store.go
Normal file
33
backend/store.go
Normal file
@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
STATE_INIT = iota
|
||||
STATE_READY
|
||||
STATE_UPDATING
|
||||
STATE_ERROR
|
||||
)
|
||||
|
||||
type StoreStatus struct {
|
||||
LastRefresh time.Time
|
||||
LastError error
|
||||
State int
|
||||
}
|
||||
|
||||
// Helper: stateToString
|
||||
func stateToString(state int) string {
|
||||
switch state {
|
||||
case STATE_INIT:
|
||||
return "INIT"
|
||||
case STATE_READY:
|
||||
return "READY"
|
||||
case STATE_UPDATING:
|
||||
return "UPDATING"
|
||||
case STATE_ERROR:
|
||||
return "ERROR"
|
||||
}
|
||||
return "INVALID"
|
||||
}
|
78
backend/store_stats.go
Normal file
78
backend/store_stats.go
Normal file
@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Routes Store
|
||||
|
||||
type RoutesStats struct {
|
||||
Filtered int `json:"filtered"`
|
||||
Imported int `json:"imported"`
|
||||
}
|
||||
|
||||
type RouteServerRoutesStats struct {
|
||||
Name string `json:"name"`
|
||||
Routes RoutesStats `json:"routes"`
|
||||
|
||||
State string `json:"state"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type RoutesStoreStats struct {
|
||||
TotalRoutes RoutesStats `json:"total_routes"`
|
||||
RouteServers []RouteServerRoutesStats `json:"route_servers"`
|
||||
}
|
||||
|
||||
// Write stats to the log
|
||||
func (stats RoutesStoreStats) Log() {
|
||||
log.Println("Routes store:")
|
||||
|
||||
log.Println(" Routes Imported:",
|
||||
stats.TotalRoutes.Imported,
|
||||
"Filtered:",
|
||||
stats.TotalRoutes.Filtered)
|
||||
log.Println(" Routeservers:")
|
||||
|
||||
for _, rs := range stats.RouteServers {
|
||||
log.Println(" -", rs.Name)
|
||||
log.Println(" State:", rs.State)
|
||||
log.Println(" UpdatedAt:", rs.UpdatedAt)
|
||||
log.Println(" Routes Imported:",
|
||||
rs.Routes.Imported,
|
||||
"Filtered:",
|
||||
rs.Routes.Filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Neighbours Store
|
||||
|
||||
type RouteServerNeighboursStats struct {
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
Neighbours int `json:"neighbours"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NeighboursStoreStats struct {
|
||||
TotalNeighbours int `json:"total_neighbours"`
|
||||
|
||||
RouteServers []RouteServerNeighboursStats `json:"route_servers"`
|
||||
}
|
||||
|
||||
// Print stats
|
||||
func (stats NeighboursStoreStats) Log() {
|
||||
log.Println("Neighbours store:")
|
||||
|
||||
log.Println(" Neighbours:",
|
||||
stats.TotalNeighbours)
|
||||
|
||||
for _, rs := range stats.RouteServers {
|
||||
log.Println(" -", rs.Name)
|
||||
log.Println(" State:", rs.State)
|
||||
log.Println(" UpdatedAt:", rs.UpdatedAt)
|
||||
log.Println(" Neighbours:",
|
||||
rs.Neighbours)
|
||||
}
|
||||
}
|
@ -20,17 +20,17 @@ DIST=birdseye-ui-dist-$(VERSION).tar.gz
|
||||
|
||||
DIST_BUILD=$(addprefix $(DIST_BUILDS)/, $(DIST))
|
||||
|
||||
all: deps client
|
||||
all: client
|
||||
|
||||
deps:
|
||||
@echo "Installing dependencies"
|
||||
npm install
|
||||
yarn install
|
||||
|
||||
client:
|
||||
client: deps
|
||||
@echo "Building alice UI"
|
||||
gulp
|
||||
|
||||
client_prod:
|
||||
client_prod: deps
|
||||
@echo "Building alice UI (production)"
|
||||
DISABLE_LOGGING=1 gulp
|
||||
|
||||
|
@ -1,105 +1,56 @@
|
||||
|
||||
/*
|
||||
* Prefix lookup actions
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export const SET_QUERY_INPUT_VALUE = "@lookup/SET_QUERY_INPUT_VALUE";
|
||||
export const SET_QUERY_VALUE = "@lookup/SET_QUERY_VALUE";
|
||||
export const SET_QUERY_TYPE = "@lookup/SET_QUERY_TYPE";
|
||||
export const LOAD_RESULTS_REQUEST = '@lookup/LOAD_RESULTS_REQUEST';
|
||||
export const LOAD_RESULTS_SUCCESS = '@lookup/LOAD_RESULTS_SUCCESS';
|
||||
export const LOAD_RESULTS_ERROR = '@lookup/LOAD_RESULTS_ERROR';
|
||||
|
||||
export const RESET = "@lookup/RESET";
|
||||
export const EXECUTE = "@lookup/EXECUTE";
|
||||
|
||||
export const LOOKUP_STARTED = "@lookup/LOOKUP_STARTED";
|
||||
export const LOOKUP_RESULTS = "@lookup/LOOKUP_RESULTS";
|
||||
|
||||
|
||||
/*
|
||||
* Action Creators
|
||||
*/
|
||||
|
||||
export function setQueryInputValue(q) {
|
||||
if(!q) { q = ''; }
|
||||
return {
|
||||
type: SET_QUERY_INPUT_VALUE,
|
||||
payload: {
|
||||
queryInput: q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setQueryValue(q) {
|
||||
return {
|
||||
type: SET_QUERY_VALUE,
|
||||
payload: {
|
||||
query: q
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setQueryType(type) {
|
||||
return {
|
||||
type: SET_QUERY_TYPE,
|
||||
payload: {
|
||||
queryType: type
|
||||
}
|
||||
// Action creators
|
||||
export function loadResultsRequest(query) {
|
||||
return {
|
||||
type: LOAD_RESULTS_REQUEST,
|
||||
payload: {
|
||||
query: query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
return {
|
||||
type: RESET
|
||||
export function loadResultsSuccess(query, results) {
|
||||
return {
|
||||
type: LOAD_RESULTS_SUCCESS,
|
||||
payload: {
|
||||
query: query,
|
||||
results: results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function execute() {
|
||||
return {
|
||||
type: EXECUTE
|
||||
export function loadResultsError(query, error) {
|
||||
return {
|
||||
type: LOAD_RESULTS_ERROR,
|
||||
payload: {
|
||||
query: query,
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function loadResults(query) {
|
||||
return (dispatch) => {
|
||||
dispatch(loadResultsRequest(query));
|
||||
|
||||
axios.get(`/api/lookup/prefix?q=${query}`)
|
||||
.then((res) => {
|
||||
dispatch(loadResultsSuccess(query, res.data));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(loadResultsError(query, error));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function lookupStarted(routeserverId, query) {
|
||||
return {
|
||||
type: LOOKUP_STARTED,
|
||||
payload: {
|
||||
routeserverId: routeserverId,
|
||||
query: query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function lookupResults(routeserverId, query, results) {
|
||||
return {
|
||||
type: LOOKUP_RESULTS,
|
||||
payload: {
|
||||
routeserverId: routeserverId,
|
||||
query: query,
|
||||
results: results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function routesSearch(routeserverId, q) {
|
||||
return (dispatch) => {
|
||||
dispatch(lookupStarted(routeserverId, q));
|
||||
axios.get(`/birdseye/api/routeserver/${routeserverId}/routes/lookup?q=${q}`)
|
||||
.then((result) => {
|
||||
let routes = result.data.result.routes;
|
||||
dispatch(lookupResults(
|
||||
routeserverId,
|
||||
q,
|
||||
routes
|
||||
));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(lookupResults(
|
||||
routeserverId,
|
||||
q,
|
||||
[]
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,87 +1,43 @@
|
||||
|
||||
/*
|
||||
* Alice (Prefix-)Lookup
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import SearchInput
|
||||
from 'components/search-input'
|
||||
import {loadResults} from './actions'
|
||||
|
||||
import LoadingIndicator
|
||||
from 'components/loading-indicator/small'
|
||||
import LookupResults from './results'
|
||||
import SearchInput from 'components/search-input/debounced'
|
||||
|
||||
import {setQueryInputValue,
|
||||
execute,
|
||||
routesSearch}
|
||||
from './actions'
|
||||
class Lookup extends React.Component {
|
||||
doLookup(q) {
|
||||
this.props.dispatch(loadResults(q));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="lookup-container">
|
||||
<div className="card">
|
||||
<SearchInput
|
||||
placeholder="Search for prefixes by entering a network address"
|
||||
onChange={(e) => this.doLookup(e.target.value)} />
|
||||
</div>
|
||||
|
||||
import QueryDispatcher
|
||||
from './query-dispatcher'
|
||||
|
||||
import LookupResults
|
||||
from './results'
|
||||
|
||||
import {queryParams}
|
||||
from 'components/utils/query'
|
||||
|
||||
|
||||
class LookupView extends React.Component {
|
||||
|
||||
setQuery(q) {
|
||||
this.props.dispatch(
|
||||
setQueryInputValue(q)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Initial mount: keep query from querystring
|
||||
let params = queryParams();
|
||||
this.props.dispatch(
|
||||
setQueryInputValue(params.q)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(execute());
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="routes-lookup">
|
||||
|
||||
<div className="card lookup-header">
|
||||
<form className="form-lookup" onSubmit={(e) => this.handleFormSubmit(e)}>
|
||||
<SearchInput placeholder="Search for routes by entering a network address"
|
||||
name="q"
|
||||
onChange={(e) => this.setQuery(e.target.value)}
|
||||
disabled={this.props.isSearching}
|
||||
value={this.props.queryInput} />
|
||||
<QueryDispatcher />
|
||||
</form>
|
||||
</div>
|
||||
<LoadingIndicator show={this.props.isRunning} />
|
||||
<div className="lookup-results">
|
||||
<LookupResults results={this.props.results}
|
||||
finished={this.props.isFinished} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<LookupResults />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
isRunning: state.lookup.queryRunning,
|
||||
isFinished: state.lookup.queryFinished,
|
||||
(state) => {
|
||||
return {
|
||||
isLoading: state.lookup.isLoading,
|
||||
error: state.lookup.error
|
||||
}
|
||||
}
|
||||
)(Lookup);
|
||||
|
||||
queryInput: state.lookup.queryInput,
|
||||
|
||||
results: state.lookup.results,
|
||||
search: state.lookup.search,
|
||||
}
|
||||
}
|
||||
)(LookupView);
|
||||
|
||||
|
@ -1,112 +0,0 @@
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {QUERY_TYPE_UNKNOWN,
|
||||
QUERY_TYPE_PREFIX}
|
||||
from './query'
|
||||
|
||||
import {setQueryType,
|
||||
routesSearch}
|
||||
from './actions'
|
||||
|
||||
|
||||
class QueryDispatcher extends React.Component {
|
||||
/*
|
||||
* Check if given query is a valid network address
|
||||
* with a lame regex if format resembles a network address.
|
||||
*/
|
||||
isNetwork(query) {
|
||||
// IPv4:
|
||||
if (query.match(/(\d+\.)(\d+\.)(\d+\.)(\d+)\/(\d+)/)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv6:
|
||||
if (query.match(/([0-9a-fA-F]+:+)+\/\d+/)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if our query is ready
|
||||
*/
|
||||
isQueryReady() {
|
||||
if (this.props.isRunning ||
|
||||
this.props.queryType == QUERY_TYPE_UNKNOWN) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
executeQuery() {
|
||||
// Check if we should dispatch this query now
|
||||
for (let rs of this.props.routeservers) {
|
||||
// Debug: limit to rs20
|
||||
if (rs.id != 20) { continue; }
|
||||
switch (this.props.queryType) {
|
||||
case QUERY_TYPE_PREFIX:
|
||||
this.props.dispatch(
|
||||
routesSearch(rs.id, this.props.input)
|
||||
);
|
||||
default:
|
||||
this.props.dispatch(
|
||||
dummySearch(rs.id, this.props.input)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* handle query input, dispatches queryies to
|
||||
* all routeservers.
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.isRunning) {
|
||||
return null; // Do nothing while a query is being processed
|
||||
}
|
||||
|
||||
if (nextProps.shouldExecute) {
|
||||
this.executeQuery();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine query type
|
||||
let queryType = QUERY_TYPE_UNKNOWN;
|
||||
if (this.isNetwork(nextProps.input)) {
|
||||
queryType = QUERY_TYPE_PREFIX;
|
||||
}
|
||||
|
||||
this.props.dispatch(setQueryType(queryType));
|
||||
}
|
||||
|
||||
/*
|
||||
* Render anything? Nope.
|
||||
*/
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
input: state.lookup.queryInput,
|
||||
|
||||
queryType: state.lookup.queryType,
|
||||
|
||||
isRunning: state.lookup.queryRunning,
|
||||
isFinished: state.lookup.queryFinished,
|
||||
|
||||
shouldExecute: state.lookup.queryDispatch,
|
||||
|
||||
routeserversQueue: state.lookup.routeserversQueue,
|
||||
routeservers: state.routeservers.all
|
||||
};
|
||||
}
|
||||
)(QueryDispatcher);
|
||||
|
@ -1,6 +0,0 @@
|
||||
|
||||
|
||||
export const QUERY_TYPE_UNKNOWN = 'unknown';
|
||||
export const QUERY_TYPE_PREFIX = 'prefix';
|
||||
export const QUERY_TYPE_ASN = 'asn';
|
||||
|
@ -1,119 +1,41 @@
|
||||
/*
|
||||
* Prefix Lookup Reducer
|
||||
*/
|
||||
|
||||
import {SET_QUERY_TYPE,
|
||||
SET_QUERY_VALUE,
|
||||
SET_QUERY_INPUT_VALUE,
|
||||
|
||||
LOOKUP_STARTED,
|
||||
LOOKUP_RESULTS,
|
||||
|
||||
RESET,
|
||||
EXECUTE}
|
||||
from './actions'
|
||||
|
||||
import {QUERY_TYPE_UNKNOWN} from './query'
|
||||
import {LOAD_RESULTS_REQUEST,
|
||||
LOAD_RESULTS_SUCCESS,
|
||||
LOAD_RESULTS_ERROR}
|
||||
from './actions'
|
||||
|
||||
const initialState = {
|
||||
results: {},
|
||||
query: '',
|
||||
|
||||
queue: new Set(),
|
||||
results: [],
|
||||
error: null,
|
||||
queryDurationMs: 0.0,
|
||||
|
||||
queryInput: "",
|
||||
|
||||
query: "",
|
||||
queryType: QUERY_TYPE_UNKNOWN,
|
||||
|
||||
queryRunning: false,
|
||||
queryFinished: false,
|
||||
queryDispatch: false,
|
||||
};
|
||||
|
||||
|
||||
// Action handlers:
|
||||
|
||||
// Handle lookup start
|
||||
function _lookupStarted(state, lookup) {
|
||||
// Enqueue Routeserver
|
||||
let queue = new Set(state.queue);
|
||||
queue.add(lookup.routeserverId);
|
||||
|
||||
// Clear results
|
||||
let results = Object.assign({}, state.results, {
|
||||
[lookup.routeserverId]: []
|
||||
});
|
||||
|
||||
// Make state update
|
||||
return {
|
||||
queue: queue,
|
||||
results: results,
|
||||
|
||||
queryRunning: true,
|
||||
queryFinished: false,
|
||||
};
|
||||
isLoading: false
|
||||
}
|
||||
|
||||
|
||||
// Handle a finished lookup
|
||||
function _lookupResults(state, lookup) {
|
||||
// Dequeue routeserver
|
||||
let queue = new Set(state.queue);
|
||||
let currentQueueSize = queue.size;
|
||||
queue.delete(lookup.routeserverId);
|
||||
|
||||
// Any routeservers left in the queue?
|
||||
let isRunning = true;
|
||||
if (queue.size == 0) {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
let isFinished = false;
|
||||
if (queue.size == 0 && currentQueueSize > 0) {
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
|
||||
// Update results set
|
||||
let results = Object.assign({}, state.results, {
|
||||
[lookup.routeserverId]: lookup.results,
|
||||
});
|
||||
|
||||
// Make state update
|
||||
return {
|
||||
results: results,
|
||||
queue: queue,
|
||||
queryRunning: isRunning,
|
||||
queryFinished: isFinished
|
||||
}
|
||||
}
|
||||
|
||||
// Reducer
|
||||
export default function reducer(state=initialState, action) {
|
||||
let payload = action.payload;
|
||||
switch(action.type) {
|
||||
// Setup
|
||||
case SET_QUERY_TYPE:
|
||||
case SET_QUERY_VALUE:
|
||||
case SET_QUERY_INPUT_VALUE:
|
||||
return Object.assign({}, state, payload);
|
||||
|
||||
// Search
|
||||
case LOOKUP_STARTED:
|
||||
// Update state on lookup started
|
||||
return Object.assign({}, state, _lookupStarted(state, payload), {
|
||||
queryDispatch: false,
|
||||
});
|
||||
|
||||
case LOOKUP_RESULTS:
|
||||
// Update state when we receive results
|
||||
return Object.assign({}, state, _lookupResults(state, payload));
|
||||
|
||||
case EXECUTE:
|
||||
return Object.assign({}, state, {
|
||||
queryDispatch: true,
|
||||
});
|
||||
|
||||
case RESET:
|
||||
return Object.assign({}, state, initialState);
|
||||
}
|
||||
return state;
|
||||
switch(action.type) {
|
||||
case LOAD_RESULTS_REQUEST:
|
||||
return Object.assign({}, state, initialState, {
|
||||
isLoading: true,
|
||||
});
|
||||
case LOAD_RESULTS_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isLoading: false,
|
||||
queryDurationMs: action.payload.results.query_duration_ms,
|
||||
results: action.payload.results.routes,
|
||||
error: null,
|
||||
});
|
||||
case LOAD_RESULTS_ERROR:
|
||||
return Object.assign({}, state, initialState, {
|
||||
error: action.payload.error,
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,61 +1,143 @@
|
||||
|
||||
import _ from 'underscore'
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
export default class LookupResults extends React.Component {
|
||||
import FilterReason
|
||||
from 'components/routeservers/large-communities/filter-reason'
|
||||
|
||||
_countResults() {
|
||||
let count = 0;
|
||||
for (let rs in this.props.results) {
|
||||
let set = this.props.results[rs];
|
||||
count += set.length;
|
||||
}
|
||||
return count;
|
||||
import NoexportReason
|
||||
from 'components/routeservers/large-communities/noexport-reason'
|
||||
|
||||
import {showBgpAttributes}
|
||||
from 'components/routeservers/routes/bgp-attributes-modal-actions'
|
||||
|
||||
import BgpAttributesModal
|
||||
from 'components/routeservers/routes/bgp-attributes-modal'
|
||||
|
||||
import LoadingIndicator
|
||||
from 'components/loading-indicator/small'
|
||||
|
||||
class ResultsTableView extends React.Component {
|
||||
|
||||
showAttributesModal(route) {
|
||||
this.props.dispatch(
|
||||
showBgpAttributes(route)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.routes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_resultSetEmpty() {
|
||||
let resultCount = this._countResults();
|
||||
if (this.props.finished && resultCount == 0){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const routes = this.props.routes.map((route) => (
|
||||
<tr key={route.id + '_' + route.neighbour.id + '_' + route.routeserver.id}>
|
||||
<td onClick={() => this.showAttributesModal(route)}>{route.network}
|
||||
{this.props.display_reasons == "filtered" && <FilterReason route={route} />}
|
||||
</td>
|
||||
<td onClick={() => this.showAttributesModal(route)}>{route.bgp.as_path.join(" ")}</td>
|
||||
<td onClick={() => this.showAttributesModal(route)}>
|
||||
{route.gateway}
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/routeservers/${route.routeserver.id}/protocols/${route.neighbour.id}/routes`}>
|
||||
{route.neighbour.description}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/routeservers/${route.routeserver.id}/protocols/${route.neighbour.id}/routes`}>
|
||||
{route.neighbour.asn}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/routeservers/${route.routeserver.id}`}>
|
||||
{route.routeserver.name}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
{this.props.header}
|
||||
<table className="table table-striped table-routes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th>
|
||||
<th>AS Path</th>
|
||||
<th>Gateway</th>
|
||||
<th>Neighbour</th>
|
||||
<th>ASN</th>
|
||||
<th>RS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{routes}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ResultsTable = connect()(ResultsTableView);
|
||||
|
||||
class LookupResults extends React.Component {
|
||||
|
||||
render() {
|
||||
if(this.props.isLoading) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
_awaitingResults() {
|
||||
let resultCount = this._countResults();
|
||||
if (!this.props.finished && resultCount == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const mkHeader = (color, action) => (
|
||||
<p style={{"color": color, "textTransform": "uppercase"}}>
|
||||
Routes {action}
|
||||
</p>
|
||||
);
|
||||
|
||||
const filtdHeader = mkHeader("orange", "filtered");
|
||||
const recvdHeader = mkHeader("green", "accepted");
|
||||
const noexHeader = mkHeader("red", "not exported");
|
||||
|
||||
/* No Results */
|
||||
renderEmpty() {
|
||||
return (
|
||||
<div className="card card-results card-no-results">
|
||||
The prefix could not be found.
|
||||
Did you specify a network address?
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let filteredRoutes = this.props.routes.filtered;
|
||||
let importedRoutes = this.props.routes.imported;
|
||||
|
||||
render() {
|
||||
if (this._resultSetEmpty()) {
|
||||
return this.renderEmpty();
|
||||
}
|
||||
return (
|
||||
<div className="lookup-results">
|
||||
|
||||
if (this._awaitingResults) {
|
||||
return null;
|
||||
}
|
||||
<BgpAttributesModal />
|
||||
|
||||
// Render Results table
|
||||
return (
|
||||
<div className="card card-results">
|
||||
ROUTES INCOMING!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<ResultsTable header={filtdHeader}
|
||||
routes={filteredRoutes}
|
||||
display_reasons="filtered" />
|
||||
<ResultsTable header={recvdHeader}
|
||||
routes={importedRoutes} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function selectRoutes(routes, state) {
|
||||
return _.where(routes, {state: state});
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
let routes = state.lookup.results;
|
||||
let filteredRoutes = selectRoutes(routes, 'filtered');
|
||||
let importedRoutes = selectRoutes(routes, 'imported');
|
||||
return {
|
||||
routes: {
|
||||
filtered: filteredRoutes,
|
||||
imported: importedRoutes
|
||||
}
|
||||
}
|
||||
}
|
||||
)(LookupResults);
|
||||
|
||||
|
@ -1,191 +0,0 @@
|
||||
import _ from 'underscore'
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
|
||||
import {loadRouteserverRoutes, loadRouteserverRoutesFiltered} from '../actions'
|
||||
import {showBgpAttributes} from './bgp-attributes-modal-actions'
|
||||
|
||||
import LoadingIndicator
|
||||
from 'components/loading-indicator/small'
|
||||
|
||||
|
||||
class FilterReason extends React.Component {
|
||||
render() {
|
||||
const route = this.props.route;
|
||||
|
||||
if (!this.props.reject_reasons || !route || !route.bgp ||
|
||||
!route.bgp.large_communities) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reason = route.bgp.large_communities.filter(elem =>
|
||||
elem[0] == this.props.asn && elem[1] == this.props.reject_id
|
||||
);
|
||||
if (!reason.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <p className="reject-reason">{this.props.reject_reasons[reason[0][2]]}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
FilterReason = connect(
|
||||
state => {
|
||||
return {
|
||||
reject_reasons: state.routeservers.reject_reasons,
|
||||
asn: state.routeservers.asn,
|
||||
reject_id: state.routeservers.reject_id,
|
||||
}
|
||||
}
|
||||
)(FilterReason);
|
||||
|
||||
|
||||
function _filteredRoutes(routes, filter) {
|
||||
let filtered = [];
|
||||
if(filter == "") {
|
||||
return routes; // nothing to do here
|
||||
}
|
||||
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
// Filter protocols
|
||||
filtered = _.filter(routes, (r) => {
|
||||
return (r.network.toLowerCase().indexOf(filter) != -1 ||
|
||||
r.gateway.toLowerCase().indexOf(filter) != -1 ||
|
||||
r.interface.toLowerCase().indexOf(filter) != -1);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
class RoutesTable extends React.Component {
|
||||
showAttributesModal(route) {
|
||||
this.props.dispatch(
|
||||
showBgpAttributes(route)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let routes = this.props.routes;
|
||||
const routes_columns = this.props.routes_columns;
|
||||
|
||||
routes = _filteredRoutes(routes, this.props.filter);
|
||||
if (!routes || !routes.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const _lookup = (r, path) => {
|
||||
const split = path.split(".").reduce((acc, elem) => acc[elem], r);
|
||||
|
||||
if (Array.isArray(split)) {
|
||||
return split.join(" ");
|
||||
}
|
||||
return split;
|
||||
}
|
||||
|
||||
let routesView = routes.map((r) => {
|
||||
return (
|
||||
<tr key={r.network} onClick={() => this.showAttributesModal(r)}>
|
||||
<td>{r.network}{this.props.display_filter && <FilterReason route={r}/>}</td>
|
||||
{Object.keys(routes_columns).map(col => <td key={col}>{_lookup(r, col)}</td>)}
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
{this.props.header}
|
||||
<table className="table table-striped table-routes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Network</th>
|
||||
{Object.values(routes_columns).map(col => <th key={col}>{col}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{routesView}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RoutesTable = connect(
|
||||
(state) => {
|
||||
return {
|
||||
filter: state.routeservers.routesFilterValue,
|
||||
reject_reasons: state.routeservers.reject_reasons,
|
||||
routes_columns: state.config.routes_columns,
|
||||
}
|
||||
}
|
||||
)(RoutesTable);
|
||||
|
||||
|
||||
class RoutesTables extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(
|
||||
loadRouteserverRoutes(this.props.routeserverId, this.props.protocolId)
|
||||
);
|
||||
this.props.dispatch(
|
||||
loadRouteserverRoutesFiltered(this.props.routeserverId,
|
||||
this.props.protocolId)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.props.isLoading) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
const routes = this.props.routes[this.props.protocolId];
|
||||
const filtered = this.props.filtered[this.props.protocolId] || [];
|
||||
|
||||
if((!routes || routes.length == 0) &&
|
||||
(!filtered || filtered.length == 0)) {
|
||||
return(
|
||||
<p className="help-block">
|
||||
No routes matched your filter.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const received = routes.filter(r => filtered.indexOf(r) < 0);
|
||||
|
||||
const mkHeader = (color, action) => (
|
||||
<p style={{"color": color, "textTransform": "uppercase"}}>
|
||||
Routes {action}
|
||||
</p>
|
||||
);
|
||||
|
||||
const filtdHeader = mkHeader("orange", "filtered");
|
||||
const recvdHeader = mkHeader("green", "accepted");
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RoutesTable header={filtdHeader} routes={filtered} display_filter={true}/>
|
||||
<RoutesTable header={recvdHeader} routes={received} display_filter={false}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {
|
||||
isLoading: state.routeservers.routesAreLoading,
|
||||
routes: state.routeservers.routes,
|
||||
filtered: state.routeservers.filtered,
|
||||
}
|
||||
}
|
||||
)(RoutesTables);
|
23
client/components/search-input/debounced.jsx
Normal file
23
client/components/search-input/debounced.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
|
||||
|
||||
export default class DebouncedSearchInput extends React.Component {
|
||||
render() {
|
||||
return(
|
||||
<div className="input-group">
|
||||
<span className="input-group-addon">
|
||||
<i className="fa fa-search"></i>
|
||||
</span>
|
||||
<DebounceInput
|
||||
minLength={2}
|
||||
debounceTimeout={250}
|
||||
className="form-control"
|
||||
{...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ import{ push } from 'react-router-redux'
|
||||
|
||||
import { loadRouteservers } from 'components/routeservers/actions'
|
||||
|
||||
// Components
|
||||
// Components
|
||||
import Status from './status'
|
||||
|
||||
|
||||
|
@ -16,14 +16,12 @@ export default class Welcome extends React.Component {
|
||||
<p>Your friendly bird looking glass</p>
|
||||
</div>
|
||||
|
||||
<div className="col-md-8">
|
||||
<Lookup />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
<div className="col-md-8">
|
||||
<Lookup />
|
||||
</div>
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,16 +34,17 @@
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"history": "^2.1.2",
|
||||
"jquery": "^3.1.1",
|
||||
"jshint": "^2.9.4",
|
||||
"jshint": "^2.9.5",
|
||||
"moment": "^2.15.1",
|
||||
"node-sass": "^3.10.1",
|
||||
"react": "^15.3.2",
|
||||
"react": "^15.6.1",
|
||||
"react-debounce-input": "^3.0.0",
|
||||
"react-dom": "^15.3.2",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.1",
|
||||
"react-router-redux": "^4.0.6",
|
||||
"react-spinkit": "^1.1.11",
|
||||
"redux": "^3.6.0",
|
||||
"redux": "^3.7.1",
|
||||
"redux-logger": "^2.7.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"run-sequence": "^1.2.2",
|
||||
|
4301
client/yarn.lock
Normal file
4301
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user