first prefix lookup

This commit is contained in:
Matthias Hannig 2017-06-26 15:35:54 +02:00
parent 5d134e18b9
commit 116fbcb18c
9 changed files with 218 additions and 638 deletions

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,100 @@
import _ from 'underscore'
import React from 'react'
import {connect} from 'react-redux'
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'
class ResultsTable extends React.Component {
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.routeserver.id}>
<td>{route.network}</td>
<td>{route.bgp.as_path.join(" ")}</td>
<td>{route.gateway}</td>
<td>{route.neighbour.description}</td>
<td>{route.neighbour.asn}</td>
<td>{route.routeserver.name}</td>
</tr>
));
_awaitingResults() {
let resultCount = this._countResults();
if (!this.props.finished && resultCount == 0) {
return true;
}
return false;
}
/* 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>
);
}
render() {
if (this._resultSetEmpty()) {
return this.renderEmpty();
}
if (this._awaitingResults) {
return null;
}
// Render Results table
return (
<div className="card card-results">
ROUTES INCOMING!
</div>
);
}
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>
);
}
}
class LookupResults extends React.Component {
render() {
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");
let filteredRoutes = this.props.routes.filtered;
let importedRoutes = this.props.routes.imported;
return (
<div className="lookup-results">
<ResultsTable header={filtdHeader} routes={filteredRoutes} />
<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

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