Merge branch 'master' into readme_update

This commit is contained in:
Matthias Hannig 2017-06-26 16:49:55 +02:00
commit d802d3e5a0
27 changed files with 5294 additions and 668 deletions

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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

View File

@ -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
View 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
View 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
}

View File

@ -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"`

View File

@ -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
}

View File

@ -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
}

View 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")
}

View File

@ -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)
}

View File

@ -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
View 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
View 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)
}
}

View File

@ -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

View File

@ -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,
[]
));
});
}
}

View File

@ -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);

View File

@ -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);

View File

@ -1,6 +0,0 @@
export const QUERY_TYPE_UNKNOWN = 'unknown';
export const QUERY_TYPE_PREFIX = 'prefix';
export const QUERY_TYPE_ASN = 'asn';

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View 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>
);
}
}

View File

@ -11,7 +11,7 @@ import{ push } from 'react-router-redux'
import { loadRouteservers } from 'components/routeservers/actions'
// Components
// Components
import Status from './status'

View File

@ -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>
*/
}
}

View File

@ -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

File diff suppressed because it is too large Load Diff