Merge branch 'feature/related-peers' into develop
This commit is contained in:
commit
63c8b7478c
@ -30,8 +30,8 @@ import (
|
||||
// Routes /api/v1/routeservers/:id/neighbors/:neighborId/routes
|
||||
//
|
||||
// Querying
|
||||
// LookupPrefix /api/v1/routeservers/:id/lookup/prefix?q=<prefix>
|
||||
//
|
||||
// LookupPrefix /api/v1/lookup/prefix?q=<prefix>
|
||||
// LookupNeighbor /api/v1/lookup/neighbor?asn=1235
|
||||
|
||||
type apiEndpoint func(*http.Request, httprouter.Params) (api.Response, error)
|
||||
|
||||
@ -111,6 +111,8 @@ func apiRegisterEndpoints(router *httprouter.Router) error {
|
||||
if AliceConfig.Server.EnablePrefixLookup == true {
|
||||
router.GET("/api/v1/lookup/prefix",
|
||||
endpoint(apiLookupPrefixGlobal))
|
||||
router.GET("/api/v1/lookup/neighbors",
|
||||
endpoint(apiLookupNeighborsGlobal))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -22,6 +23,7 @@ type Neighbour struct {
|
||||
RoutesAccepted int `json:"routes_accepted"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
LastError string `json:"last_error"`
|
||||
RouteServerId string `json:"routeserver_id"`
|
||||
|
||||
// Original response
|
||||
Details map[string]interface{} `json:"details"`
|
||||
@ -45,6 +47,34 @@ type NeighboursResponse struct {
|
||||
Neighbours Neighbours `json:"neighbours"`
|
||||
}
|
||||
|
||||
// Implement Filterable interface
|
||||
func (self *Neighbour) MatchSourceId(id string) bool {
|
||||
return self.RouteServerId == id
|
||||
}
|
||||
|
||||
func (self *Neighbour) MatchAsn(asn int) bool {
|
||||
return self.Asn == asn
|
||||
}
|
||||
|
||||
func (self *Neighbour) MatchCommunity(_community Community) bool {
|
||||
return true // Ignore
|
||||
}
|
||||
|
||||
func (self *Neighbour) MatchExtCommunity(_community Community) bool {
|
||||
return true // Ignore
|
||||
}
|
||||
|
||||
func (self *Neighbour) MatchLargeCommunity(_community Community) bool {
|
||||
return true // Ignore
|
||||
}
|
||||
|
||||
func (self *Neighbour) MatchName(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
neighName := strings.ToLower(self.Description)
|
||||
|
||||
return strings.Contains(neighName, name)
|
||||
}
|
||||
|
||||
// Neighbours response is cacheable
|
||||
func (self *NeighboursResponse) CacheTtl() time.Duration {
|
||||
now := time.Now().UTC()
|
||||
@ -53,7 +83,6 @@ func (self *NeighboursResponse) CacheTtl() time.Duration {
|
||||
|
||||
type NeighboursLookupResults map[string]Neighbours
|
||||
|
||||
|
||||
type NeighboursStatus []*NeighbourStatus
|
||||
|
||||
type NeighbourStatus struct {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -541,3 +542,56 @@ func (self *SearchFilters) MergeProperties(other *SearchFilters) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The above filters apply for now for routes.
|
||||
// We are using a slightly simpler solution for neighbor queries.
|
||||
// At least for the time beeing.
|
||||
type NeighborFilter struct {
|
||||
name string
|
||||
asn int
|
||||
}
|
||||
|
||||
/*
|
||||
Get neighbor filters from query parameters.
|
||||
Right now we support filtering by name (partial match)
|
||||
and ASN.
|
||||
|
||||
The latter is used to find related peers on all route servers.
|
||||
*/
|
||||
func NeighborFilterFromQuery(q url.Values) *NeighborFilter {
|
||||
asn := 0
|
||||
name := q.Get("name")
|
||||
asnVal := q.Get("asn")
|
||||
if asnVal != "" {
|
||||
asn, _ = strconv.Atoi(asnVal)
|
||||
}
|
||||
|
||||
filter := &NeighborFilter{
|
||||
name: name,
|
||||
asn: asn,
|
||||
}
|
||||
return filter
|
||||
}
|
||||
|
||||
/*
|
||||
Decode query values from string.
|
||||
This is intendet as a helper method to make testing easier.
|
||||
*/
|
||||
func NeighborFilterFromQueryString(q string) *NeighborFilter {
|
||||
values, _ := url.ParseQuery(q)
|
||||
return NeighborFilterFromQuery(values)
|
||||
}
|
||||
|
||||
/*
|
||||
Match neighbor with filter: Check if the neighbor
|
||||
in question has the required parameters.
|
||||
*/
|
||||
func (self *NeighborFilter) Match(neighbor *Neighbour) bool {
|
||||
if self.name != "" && neighbor.MatchName(self.name) {
|
||||
return true
|
||||
}
|
||||
if self.asn > 0 && neighbor.MatchAsn(self.asn) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -594,3 +594,63 @@ func TestSearchFiltersMergeProperties(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNeighborFilterMatch(t *testing.T) {
|
||||
n1 := &Neighbour{
|
||||
Asn: 2342,
|
||||
Description: "Foo Networks AB",
|
||||
}
|
||||
n2 := &Neighbour{
|
||||
Asn: 42,
|
||||
Description: "Bar Communications Inc.",
|
||||
}
|
||||
|
||||
filter := &NeighborFilter{
|
||||
asn: 42,
|
||||
}
|
||||
if filter.Match(n1) != false {
|
||||
t.Error("Expected n1 not to match filter")
|
||||
}
|
||||
if filter.Match(n2) == false {
|
||||
t.Error("Expected n2 to match filter")
|
||||
}
|
||||
|
||||
filter = &NeighborFilter{
|
||||
name: "network",
|
||||
}
|
||||
if filter.Match(n1) == false {
|
||||
t.Error("Expected n1 to match filter")
|
||||
}
|
||||
if filter.Match(n2) != false {
|
||||
t.Error("Expected n2 not to match filter")
|
||||
}
|
||||
|
||||
filter = &NeighborFilter{
|
||||
asn: 42,
|
||||
name: "network",
|
||||
}
|
||||
|
||||
if filter.Match(n1) == false || filter.Match(n2) == false {
|
||||
t.Error("Expected filter to match both neighbors.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeighborFilterFromQuery(t *testing.T) {
|
||||
query := "asn=2342&name=foo"
|
||||
filter := NeighborFilterFromQueryString(query)
|
||||
|
||||
if filter.asn != 2342 {
|
||||
t.Error("Unexpected asn filter:", filter.asn)
|
||||
}
|
||||
if filter.name != "foo" {
|
||||
t.Error("Unexpected name filter:", filter.name)
|
||||
}
|
||||
|
||||
filter = NeighborFilterFromQueryString(values)
|
||||
if filter.asn != 0 {
|
||||
t.Error("Unexpected asn:", filter.asn)
|
||||
}
|
||||
if filter.name != "" {
|
||||
t.Error("Unexpected name:", filter.name)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ import (
|
||||
)
|
||||
|
||||
// Handle get neighbors on routeserver
|
||||
func apiNeighborsList(_req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
func apiNeighborsList(
|
||||
_req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
rsId, err := validateSourceId(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -14,7 +14,7 @@ func apiLookupPrefixGlobal(
|
||||
req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
// TODO: This function is too long
|
||||
// TODO: This function is way too long
|
||||
|
||||
// Get prefix to query
|
||||
q, err := validateQueryString(req, "q")
|
||||
@ -134,3 +134,27 @@ func apiLookupPrefixGlobal(
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func apiLookupNeighborsGlobal(
|
||||
req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
// Query neighbors store
|
||||
filter := api.NeighborFilterFromQuery(req.URL.Query())
|
||||
neighbors := AliceNeighboursStore.FilterNeighbors(filter)
|
||||
|
||||
sort.Sort(neighbors)
|
||||
|
||||
// Make response
|
||||
response := &api.NeighboursResponse{
|
||||
Api: api.ApiStatus{
|
||||
CacheStatus: api.CacheStatus{
|
||||
CachedAt: AliceNeighboursStore.CachedAt(),
|
||||
},
|
||||
ResultFromCache: true, // You would not have guessed.
|
||||
Ttl: AliceNeighboursStore.CacheTtl(),
|
||||
},
|
||||
Neighbours: neighbors,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ type NeighboursStore struct {
|
||||
statusMap map[string]StoreStatus
|
||||
refreshInterval time.Duration
|
||||
refreshNeighborStatus bool
|
||||
lastRefresh time.Time
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
@ -52,10 +53,10 @@ func NewNeighboursStore(config *Config) *NeighboursStore {
|
||||
refreshNeighborStatus := config.Server.EnableNeighborsStatusRefresh
|
||||
|
||||
store := &NeighboursStore{
|
||||
neighboursMap: neighboursMap,
|
||||
statusMap: statusMap,
|
||||
configMap: configMap,
|
||||
refreshInterval: refreshInterval,
|
||||
neighboursMap: neighboursMap,
|
||||
statusMap: statusMap,
|
||||
configMap: configMap,
|
||||
refreshInterval: refreshInterval,
|
||||
refreshNeighborStatus: refreshNeighborStatus,
|
||||
}
|
||||
return store
|
||||
@ -153,6 +154,7 @@ func (self *NeighboursStore) update() {
|
||||
LastRefresh: time.Now(),
|
||||
State: STATE_READY,
|
||||
}
|
||||
self.lastRefresh = time.Now().UTC()
|
||||
self.Unlock()
|
||||
successCount++
|
||||
}
|
||||
@ -257,6 +259,46 @@ func (self *NeighboursStore) LookupNeighbours(
|
||||
return results
|
||||
}
|
||||
|
||||
/*
|
||||
Filter neighbors from a single route server.
|
||||
*/
|
||||
func (self *NeighboursStore) FilterNeighborsAt(
|
||||
sourceId string,
|
||||
filter *api.NeighborFilter,
|
||||
) api.Neighbours {
|
||||
results := []*api.Neighbour{}
|
||||
|
||||
self.RLock()
|
||||
neighbors := self.neighboursMap[sourceId]
|
||||
self.RUnlock()
|
||||
|
||||
// Apply filters
|
||||
for _, neighbor := range neighbors {
|
||||
if filter.Match(neighbor) {
|
||||
results = append(results, neighbor)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/*
|
||||
Filter neighbors by name or by ASN.
|
||||
Collect results from all routeservers.
|
||||
*/
|
||||
func (self *NeighboursStore) FilterNeighbors(
|
||||
filter *api.NeighborFilter,
|
||||
) api.Neighbours {
|
||||
results := []*api.Neighbour{}
|
||||
|
||||
// Get neighbors from all routeservers
|
||||
for sourceId, _ := range self.neighboursMap {
|
||||
rsResults := self.FilterNeighborsAt(sourceId, filter)
|
||||
results = append(results, rsResults...)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Build some stats for monitoring
|
||||
func (self *NeighboursStore) Stats() NeighboursStoreStats {
|
||||
totalNeighbours := 0
|
||||
@ -282,3 +324,11 @@ func (self *NeighboursStore) Stats() NeighboursStoreStats {
|
||||
}
|
||||
return storeStats
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) CachedAt() time.Time {
|
||||
return self.lastRefresh
|
||||
}
|
||||
|
||||
func (self *NeighboursStore) CacheTtl() time.Time {
|
||||
return self.lastRefresh.Add(self.refreshInterval)
|
||||
}
|
||||
|
@ -25,27 +25,37 @@ func makeTestNeighboursStore() *NeighboursStore {
|
||||
// Populate neighbours
|
||||
rs1 := NeighboursIndex{
|
||||
"ID2233_AS2342": &api.Neighbour{
|
||||
Id: "ID2233_AS2342",
|
||||
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
|
||||
Id: "ID2233_AS2342",
|
||||
Asn: 2342,
|
||||
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
|
||||
RouteServerId: "rs1",
|
||||
},
|
||||
"ID2233_AS2343": &api.Neighbour{
|
||||
Id: "ID2233_AS2343",
|
||||
Description: "PEER AS2343 192.9.23.43 Different Peer 1",
|
||||
Id: "ID2233_AS2343",
|
||||
Asn: 2343,
|
||||
Description: "PEER AS2343 192.9.23.43 Different Peer 1",
|
||||
RouteServerId: "rs1",
|
||||
},
|
||||
"ID2233_AS2344": &api.Neighbour{
|
||||
Id: "ID2233_AS2344",
|
||||
Description: "PEER AS2344 192.9.23.44 3rd Peer from the sun",
|
||||
Id: "ID2233_AS2344",
|
||||
Asn: 2344,
|
||||
Description: "PEER AS2344 192.9.23.44 3rd Peer from the sun",
|
||||
RouteServerId: "rs1",
|
||||
},
|
||||
}
|
||||
|
||||
rs2 := NeighboursIndex{
|
||||
"ID2233_AS2342": &api.Neighbour{
|
||||
Id: "ID2233_AS2342",
|
||||
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
|
||||
Id: "ID2233_AS2342",
|
||||
Asn: 2342,
|
||||
Description: "PEER AS2342 192.9.23.42 Customer Peer 1",
|
||||
RouteServerId: "rs2",
|
||||
},
|
||||
"ID2233_AS4223": &api.Neighbour{
|
||||
Id: "ID2233_AS4223",
|
||||
Description: "PEER AS4223 192.9.42.23 Cloudfoo Inc.",
|
||||
Id: "ID2233_AS4223",
|
||||
Asn: 4223,
|
||||
Description: "PEER AS4223 192.9.42.23 Cloudfoo Inc.",
|
||||
RouteServerId: "rs2",
|
||||
},
|
||||
}
|
||||
|
||||
@ -158,3 +168,19 @@ func TestNeighbourLookup(t *testing.T) {
|
||||
t.Error("Wrong peer in lookup response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeighborFilter(t *testing.T) {
|
||||
store := makeTestNeighboursStore()
|
||||
filter := api.NeighborFilterFromQueryString("asn=2342")
|
||||
neighbors := store.FilterNeighbors(filter)
|
||||
if len(neighbors) != 2 {
|
||||
t.Error("Expected two results")
|
||||
}
|
||||
|
||||
filter = api.NeighborFilterFromQueryString("")
|
||||
neighbors = store.FilterNeighbors(filter)
|
||||
if len(neighbors) != 0 {
|
||||
t.Error("Expected empty result set")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -152,6 +152,7 @@ func parseRelativeServerTime(uptime interface{}, config Config) time.Duration {
|
||||
|
||||
// Parse neighbours response
|
||||
func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error) {
|
||||
rsId := config.Id
|
||||
neighbours := api.Neighbours{}
|
||||
protocols := bird["protocols"].(map[string]interface{})
|
||||
|
||||
@ -176,11 +177,12 @@ func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error)
|
||||
neighbour := &api.Neighbour{
|
||||
Id: protocolId,
|
||||
|
||||
Address: mustString(protocol["neighbor_address"], "error"),
|
||||
Asn: mustInt(protocol["neighbor_as"], 0),
|
||||
State: strings.ToLower(mustString(protocol["state"], "unknown")),
|
||||
Address: mustString(protocol["neighbor_address"], "error"),
|
||||
Asn: mustInt(protocol["neighbor_as"], 0),
|
||||
State: strings.ToLower(
|
||||
mustString(protocol["state"], "unknown")),
|
||||
Description: mustString(protocol["description"], "no description"),
|
||||
//TODO make these changes configurable
|
||||
|
||||
RoutesReceived: mustInt(routesReceived, 0),
|
||||
RoutesAccepted: mustInt(routes["imported"], 0),
|
||||
RoutesFiltered: mustInt(routes["filtered"], 0),
|
||||
@ -190,6 +192,8 @@ func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error)
|
||||
Uptime: uptime,
|
||||
LastError: lastError,
|
||||
|
||||
RouteServerId: rsId,
|
||||
|
||||
Details: protocol,
|
||||
}
|
||||
|
||||
|
@ -167,6 +167,7 @@ func (gobgp *GoBGP) Neighbours() (*api.NeighboursResponse, error) {
|
||||
neigh.Description = _resp.Peer.Conf.Description
|
||||
|
||||
neigh.Id = PeerHash(_resp.Peer)
|
||||
neigh.RouteServerId = gobgp.config.Id
|
||||
|
||||
response.Neighbours = append(response.Neighbours, &neigh)
|
||||
for _, afiSafi := range _resp.Peer.AfiSafis {
|
||||
|
@ -54,4 +54,3 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -311,6 +311,100 @@ $labelOffsetEnd: -70px;
|
||||
}
|
||||
}
|
||||
|
||||
// Related Peers Box
|
||||
.card-related-peers {
|
||||
h2 {
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
|
||||
margin-top: 8px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 10px 0px 0px 0px;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.related-peers-rs-peer {
|
||||
margin: 5px 0px;
|
||||
width: 100%;
|
||||
td {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.card-related-peers {
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
.uptime {
|
||||
text-align: right;
|
||||
}
|
||||
.peer-stats {
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
.routes-received {
|
||||
color: green;
|
||||
}
|
||||
.routes-accepted {
|
||||
color: green;
|
||||
}
|
||||
.routes-filtered {
|
||||
color: orange;
|
||||
}
|
||||
.routes-exported {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.atooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: default;
|
||||
}
|
||||
.atooltip i {
|
||||
position: absolute;
|
||||
font-style: normal;
|
||||
width:140px;
|
||||
color: #ffffff;
|
||||
background: #000000;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
visibility: hidden;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.atooltip i:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
width: 0; height: 0;
|
||||
border-top: 8px solid #000000;
|
||||
border-right: 8px solid transparent;
|
||||
border-left: 8px solid transparent;
|
||||
}
|
||||
.atooltip:hover i {
|
||||
visibility: visible;
|
||||
opacity: 0.8;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
margin-left: -76px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
|
||||
|
@ -20,6 +20,10 @@ export const FETCH_ROUTES_NOT_EXPORTED_REQUEST = "@routes/FETCH_ROUTES_NOT_EXPOR
|
||||
export const FETCH_ROUTES_NOT_EXPORTED_SUCCESS = "@routes/FETCH_ROUTES_NOT_EXPORTED_SUCCESS";
|
||||
export const FETCH_ROUTES_NOT_EXPORTED_ERROR = "@routes/FETCH_ROUTES_NOT_EXPORTED_ERROR";
|
||||
|
||||
export const FETCH_RELATED_PEERS_REQUEST = "@related-peers/FETCH_REQUEST";
|
||||
export const FETCH_RELATED_PEERS_SUCCESS = "@related-peers/FETCH_SUCCESS";
|
||||
export const FETCH_RELATED_PEERS_ERROR = "@related-peers/FETCH_ERROR";
|
||||
|
||||
export const SET_FILTER_QUERY_VALUE = "@routes/SET_FILTER_QUERY_VALUE";
|
||||
|
||||
// Url helper
|
||||
@ -136,3 +140,48 @@ export function setFilterQueryValue(value) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function fetchRelatedPeersRequest(asn) {
|
||||
return {
|
||||
type: FETCH_RELATED_PEERS_REQUEST,
|
||||
payload: {
|
||||
asn: asn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchRelatedPeersSuccess(asn, result) {
|
||||
return {
|
||||
type: FETCH_RELATED_PEERS_SUCCESS,
|
||||
payload: {
|
||||
neighbors: result.neighbours, // TODO: fix inconsistency.
|
||||
asn: asn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchRelatedPeersError(asn, error) {
|
||||
return {
|
||||
type: FETCH_RELATED_PEERS_ERROR,
|
||||
payload: {
|
||||
error: error,
|
||||
asn: asn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function fetchRelatedPeers(asn) {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchRelatedPeersRequest(asn));
|
||||
axios.get(`/api/v1/lookup/neighbors?asn=${asn}`)
|
||||
.then(
|
||||
({data}) => {
|
||||
dispatch(fetchRelatedPeersSuccess(asn, data));
|
||||
},
|
||||
(error) => {
|
||||
dispatch(fetchRelatedPeersError(asn, error));
|
||||
// No global error handling if this fails.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ import SearchInput from 'components/search-input'
|
||||
|
||||
import RoutesView from './view'
|
||||
import QuickLinks from './quick-links'
|
||||
import RelatedPeers from './related-peers'
|
||||
import {RelatedPeersTabs,
|
||||
RelatedPeersCard} from './related-peers'
|
||||
|
||||
import BgpAttributesModal
|
||||
from './bgp-attributes-modal'
|
||||
@ -37,7 +38,7 @@ import {mergeFilters} from 'components/filters/state'
|
||||
import {makeLinkProps} from './urls'
|
||||
|
||||
// Actions
|
||||
import {setFilterQueryValue}
|
||||
import {setFilterQueryValue, fetchRelatedPeers}
|
||||
from './actions'
|
||||
import {loadRouteserverProtocol}
|
||||
from 'components/routeservers/actions'
|
||||
@ -122,6 +123,20 @@ class RoutesPage extends React.Component {
|
||||
this.props.dispatch(
|
||||
loadRouteserverProtocol(this.props.params.routeserverId)
|
||||
);
|
||||
|
||||
if (this.props.neighbor) {
|
||||
this.props.dispatch(
|
||||
fetchRelatedPeers(this.props.neighbor.asn)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.neighbor && this.props.neighbor != prevProps.neighbor) {
|
||||
this.props.dispatch(
|
||||
fetchRelatedPeers(this.props.neighbor.asn)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -133,7 +148,7 @@ class RoutesPage extends React.Component {
|
||||
// We have to shift the layout a bit, to make room for
|
||||
// the related peers tabs
|
||||
let pageClass = "routeservers-page";
|
||||
if (this.props.relatedPeers.length > 1) {
|
||||
if (this.props.localRelatedPeers.length > 1) {
|
||||
pageClass += " has-related-peers";
|
||||
}
|
||||
|
||||
@ -161,9 +176,10 @@ class RoutesPage extends React.Component {
|
||||
<div className="col-main col-lg-9 col-md-12">
|
||||
|
||||
<div className="card">
|
||||
<RelatedPeers peers={this.props.relatedPeers}
|
||||
protocolId={this.props.params.protocolId}
|
||||
routeserverId={this.props.params.routeserverId} />
|
||||
<RelatedPeersTabs
|
||||
peers={this.props.localRelatedPeers}
|
||||
protocolId={this.props.params.protocolId}
|
||||
routeserverId={this.props.params.routeserverId} />
|
||||
<SearchInput
|
||||
value={this.props.filterValue}
|
||||
placeholder={filterPlaceholder}
|
||||
@ -206,6 +222,10 @@ class RoutesPage extends React.Component {
|
||||
linkProps={this.props.linkProps}
|
||||
filtersApplied={this.props.filtersApplied}
|
||||
filtersAvailable={this.props.filtersAvailable} />
|
||||
<RelatedPeersCard
|
||||
neighbors={this.props.allRelatedPeers}
|
||||
rsId={this.props.params.routeserverId}
|
||||
protocolId={this.props.params.protocolId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -215,6 +235,8 @@ class RoutesPage extends React.Component {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
(state, props) => {
|
||||
const protocolId = props.params.protocolId;
|
||||
@ -223,10 +245,10 @@ export default connect(
|
||||
const neighbor = _.findWhere(neighbors, {id: protocolId});
|
||||
|
||||
// Find related peers. Peers belonging to the same AS.
|
||||
let relatedPeers = [];
|
||||
let localRelatedPeers = [];
|
||||
if (neighbor) {
|
||||
relatedPeers = _.where(neighbors, {asn: neighbor.asn,
|
||||
state: "up"});
|
||||
localRelatedPeers = _.where(
|
||||
neighbors, {asn: neighbor.asn, state: "up"});
|
||||
}
|
||||
|
||||
const received = {
|
||||
@ -261,9 +283,10 @@ export default connect(
|
||||
);
|
||||
|
||||
return({
|
||||
neighbor: neighbor,
|
||||
filterValue: state.routes.filterValue,
|
||||
routes: {
|
||||
[ROUTES_RECEIVED]: received,
|
||||
[ROUTES_RECEIVED]: received,
|
||||
[ROUTES_FILTERED]: filtered,
|
||||
[ROUTES_NOT_EXPORTED]: notExported
|
||||
},
|
||||
@ -294,7 +317,8 @@ export default connect(
|
||||
filtersApplied: filtersApplied,
|
||||
},
|
||||
|
||||
relatedPeers: relatedPeers,
|
||||
localRelatedPeers: localRelatedPeers,
|
||||
allRelatedPeers: state.routes.allRelatedPeers,
|
||||
|
||||
// Loding indicator helper
|
||||
receivedLoading: state.routes.receivedLoading,
|
||||
|
@ -9,7 +9,11 @@ import {FETCH_ROUTES_RECEIVED_REQUEST,
|
||||
|
||||
FETCH_ROUTES_NOT_EXPORTED_REQUEST,
|
||||
FETCH_ROUTES_NOT_EXPORTED_SUCCESS,
|
||||
FETCH_ROUTES_NOT_EXPORTED_ERROR} from './actions'
|
||||
FETCH_ROUTES_NOT_EXPORTED_ERROR,
|
||||
|
||||
FETCH_RELATED_PEERS_REQUEST,
|
||||
FETCH_RELATED_PEERS_SUCCESS,
|
||||
FETCH_RELATED_PEERS_ERROR} from './actions'
|
||||
|
||||
import {ROUTES_RECEIVED,
|
||||
ROUTES_FILTERED,
|
||||
@ -63,6 +67,9 @@ const initialState = {
|
||||
// Derived state from location
|
||||
loadNotExported: false,
|
||||
|
||||
// Global related peers
|
||||
allRelatedPeers: [],
|
||||
|
||||
filterValue: "",
|
||||
filterQuery: "",
|
||||
}
|
||||
@ -167,6 +174,25 @@ function _handleFilterQueryValueChange(state, payload) {
|
||||
}
|
||||
|
||||
|
||||
// Related Peers
|
||||
function _handleFetchRelatedPeersRequest(state, payload) {
|
||||
return Object.assign({}, state, {
|
||||
allRelatedPeers: [],
|
||||
});
|
||||
}
|
||||
|
||||
function _handleFetchRelatedPeersSuccess(state, payload) {
|
||||
return Object.assign({}, state, {
|
||||
allRelatedPeers: payload.neighbors,
|
||||
});
|
||||
}
|
||||
|
||||
function _handleFetchRelatedPeersError(state, payload) {
|
||||
return Object.assign({}, state, {
|
||||
allRelatedPeers: [],
|
||||
});
|
||||
}
|
||||
|
||||
export default function reducer(state=initialState, action) {
|
||||
|
||||
switch(action.type) {
|
||||
@ -217,6 +243,14 @@ export default function reducer(state=initialState, action) {
|
||||
return _handleFetchRoutesError(ROUTES_NOT_EXPORTED,
|
||||
state,
|
||||
action.payload);
|
||||
|
||||
// Related Peers
|
||||
case FETCH_RELATED_PEERS_REQUEST:
|
||||
return _handleFetchRelatedPeersRequest(state, action.payload);
|
||||
case FETCH_RELATED_PEERS_SUCCESS:
|
||||
return _handleFetchRelatedPeersSuccess(state, action.payload);
|
||||
case FETCH_RELATED_PEERS_ERROR:
|
||||
return _handleFetchRelatedPeersError(state, action.payload);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
@ -1,14 +1,18 @@
|
||||
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import {makePeerLinkProps} from './urls'
|
||||
|
||||
import RelativeTimestamp
|
||||
from 'components/datetime/relative-timestamp'
|
||||
|
||||
/*
|
||||
* Render related peers as tabs
|
||||
*/
|
||||
export default function RelatedPeers(props) {
|
||||
export function RelatedPeersTabs(props) {
|
||||
if (props.peers.length < 2) {
|
||||
return null; // Nothing to do here.
|
||||
}
|
||||
@ -31,3 +35,125 @@ export default function RelatedPeers(props) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display a link to a peer. If the peer state is up.
|
||||
*/
|
||||
function PeerLink(props) {
|
||||
const neighbor = props.to;
|
||||
if (!neighbor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pid = neighbor.id;
|
||||
const rid = neighbor.routeserver_id;
|
||||
let peerUrl;
|
||||
|
||||
if (neighbor.state == "up") {
|
||||
peerUrl = `/routeservers/${rid}/protocols/${pid}/routes`;
|
||||
} else {
|
||||
peerUrl = `/routeservers/${rid}#sessions-down`;
|
||||
}
|
||||
// Render link
|
||||
return (
|
||||
<a href={peerUrl}>{props.children}</a>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Show routes received, accepted, filtered, exported
|
||||
*/
|
||||
function RoutesStats(props) {
|
||||
const {peer} = props;
|
||||
|
||||
if (peer.state != "up") {
|
||||
return null; // Nothing to render
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="related-peers-routes-stats">
|
||||
<span className="atooltip routes-received">
|
||||
{peer.routes_received}
|
||||
<i>Routes Received</i>
|
||||
</span> / <span className="atooltip routes-accepted">
|
||||
{peer.routes_accepted}
|
||||
<i>Routes Accepted</i>
|
||||
</span> / <span className="atooltip routes-filtered">
|
||||
{peer.routes_filtered}
|
||||
<i>Routes Filtered</i>
|
||||
</span> / <span className="atooltip routes-exported">
|
||||
{peer.routes_exported}
|
||||
<i>Routes Exported</i>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Render a card with related peers for the sidebar.
|
||||
*
|
||||
* This provides quick links to the same peer on other
|
||||
* routeservers.
|
||||
*/
|
||||
function RelatedPeersCardView(props) {
|
||||
let neighbors = props.neighbors;
|
||||
if (!neighbors || neighbors.length < 2) {
|
||||
return null; // nothing to render here.
|
||||
}
|
||||
|
||||
// Exclude own neighbor and group peers by routeserver
|
||||
let related = {};
|
||||
for (let neighbor of neighbors) {
|
||||
if (neighbor.routeserver_id == props.rsId &&
|
||||
neighbor.id == props.protocolId) {
|
||||
continue; // Skip current peer.
|
||||
}
|
||||
|
||||
if (!related[neighbor.routeserver_id]) {
|
||||
related[neighbor.routeserver_id] = [];
|
||||
}
|
||||
related[neighbor.routeserver_id].push(neighbor);
|
||||
}
|
||||
|
||||
// Get routeserver info for routeserver id as key in object.
|
||||
let relatedRs = [];
|
||||
for (let rsId in related) {
|
||||
relatedRs.push(props.routeservers[rsId]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-related-peers">
|
||||
<h2 className="card-header">Related Neighbors</h2>
|
||||
{relatedRs.map(rs => (
|
||||
<div key={rs.id} className="related-peers-rs-group">
|
||||
<h3>{rs.name}</h3>
|
||||
<table className="related-peers-rs-peer">
|
||||
<tbody>
|
||||
{related[rs.id].map(peer => (
|
||||
<tr key={peer.id}>
|
||||
<td className="peer-address">
|
||||
<PeerLink to={peer}>{peer.address}</PeerLink>
|
||||
</td>
|
||||
<td className="peer-stats">
|
||||
<RoutesStats peer={peer} />
|
||||
</td>
|
||||
<td className="uptime">
|
||||
{peer.state} for <RelativeTimestamp
|
||||
value={peer.uptime}
|
||||
suffix={true} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export let RelatedPeersCard = connect(
|
||||
(state) => ({
|
||||
routeservers: state.routeservers.byId
|
||||
})
|
||||
)(RelatedPeersCardView);
|
||||
|
Loading…
x
Reference in New Issue
Block a user