Merge branch 'alice-lg:master' into github-action-docker
This commit is contained in:
commit
e36a1bc5e4
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,6 +1,16 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## 5.0.0 (2021-10-09)
|
||||
|
||||
* OpenBGPD support! Thanks to the Route Server Support Foundation
|
||||
for sponsoring this feature!
|
||||
|
||||
* Backend cleanup and restructured go codebase.
|
||||
This should improve a bit working with containers.
|
||||
|
||||
* Fixed links to the IRR Explorer.
|
||||
|
||||
## 4.3.0 (2021-04-15)
|
||||
|
||||
* Added configurable main table
|
||||
|
40
LICENSE
40
LICENSE
@ -1,33 +1,31 @@
|
||||
BSD License
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2016-2018, Peering GmbH / ECIX
|
||||
Copyright (c) 2018-present, Matthias Hannig
|
||||
Copyright (c) 2018-present, Annika Hannig
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGE.
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
11
Makefile
11
Makefile
@ -35,14 +35,11 @@ client_dev:
|
||||
client_prod:
|
||||
$(MAKE) -C client/ client_prod
|
||||
|
||||
backend_dev: client_dev
|
||||
$(MAKE) -C cmd/alice-lg/
|
||||
|
||||
dev:
|
||||
$(MAKE) -C cmd/alice-lg/ osx-dev
|
||||
|
||||
backend_prod: client_prod
|
||||
$(MAKE) -C cmd/alice-lg/
|
||||
$(MAKE) -C cmd/alice-lg/ linux
|
||||
|
||||
backend:
|
||||
$(MAKE) -C cmd/alice-lg/ linux
|
||||
|
||||
alice: backend_prod
|
||||
cp cmd/alice-lg/alice-lg-* bin/
|
||||
|
46
README.md
46
README.md
@ -2,9 +2,13 @@
|
||||
__"No, no! The adventures first, explanations take such a dreadful time."__
|
||||
_Lewis Carroll, Alice's Adventures in Wonderland & Through the Looking-Glass_
|
||||
|
||||
Take a look at an Alice-LG production examples at:
|
||||
Take a look at Alice-LG production examples at:
|
||||
- https://lg.de-cix.net/
|
||||
- https://lg.ecix.net/
|
||||
- https://lg.ams-ix.net
|
||||
- https://lg.bcix.de/
|
||||
- https://lg.megaport.com/
|
||||
- https://lg.netnod.se/
|
||||
- https://alice-rs.linx.net/
|
||||
|
||||
And checkout the API at:
|
||||
- https://lg.de-cix.net/api/v1/config
|
||||
@ -33,12 +37,17 @@ Major thanks to Barry O'Donovan who built the original [INEX Bird's Eye](https:/
|
||||
### GoBGP
|
||||
Alice-LG supports direct integration with GoBGP instances using gRPC. See the configuration section for more detail.
|
||||
|
||||
### OpenBGPD
|
||||
|
||||
Alice-LG supports OpenBGP via [`bgplgd`](https://github.com/cjeker/bgplgd)
|
||||
and [`openbgpd-state-server`](https://github.com/alice-lg/openbgpd-state-server).
|
||||
|
||||
## Building Alice-LG from scratch
|
||||
__These examples include setting up your Go environment, if you already have set that up then you can obviously skip that__
|
||||
|
||||
In case you have trouble with `npm` and `gulp` you can try using `yarn`.
|
||||
|
||||
### CentOS 7:
|
||||
### CentOS:
|
||||
First add the following lines at the end of your `~/.bash_profile`:
|
||||
```bash
|
||||
GOPATH=$HOME/go
|
||||
@ -70,7 +79,7 @@ Your Alice-LG source will now be located at `~/go/src/github.com/alice-lg/alice-
|
||||
## Configuration
|
||||
|
||||
An example configuration can be found at
|
||||
[etc/alice-lg/alice.example.conf](https://github.com/alice-lg/alice-lg/blob/readme_update/etc/alice-lg/alice.example.conf).
|
||||
[etc/alice-lg/alice.example.conf](https://github.com/alice-lg/alice-lg/blob/master/etc/alice-lg/alice.example.conf).
|
||||
|
||||
You can copy it to any of the following locations:
|
||||
|
||||
@ -115,6 +124,32 @@ host = rs2.example.com:50051
|
||||
processing_timeout = 300
|
||||
```
|
||||
|
||||
[OpenBGPD](https://www.openbgpd.org/) via `openbgpd-state-server`:
|
||||
```ini
|
||||
[source.rs-example]
|
||||
name = rs-example.openbgpd-state-server
|
||||
|
||||
[source.rs-example.openbgpd-state-server]
|
||||
api = http://rs23.example.net:29111/api
|
||||
|
||||
# Optional response cache time in seconds
|
||||
# Default: disabled (0)
|
||||
cache_ttl = 100
|
||||
```
|
||||
|
||||
[OpenBGPD](https://www.openbgpd.org/) via `bgplgd`:
|
||||
```ini
|
||||
[source.rs-example]
|
||||
name = rs-example.openbgpd-bgplgd
|
||||
|
||||
[source.rs-example.openbgpd-bgplgd]
|
||||
api = http://rs23.example.net/bgplgd
|
||||
|
||||
# Optional response cache time in seconds
|
||||
# Default: disabled (0)
|
||||
cache_ttl = 100
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
Launch the server by running
|
||||
@ -226,3 +261,6 @@ The development of Alice is now sponsored by
|
||||
</p>
|
||||
|
||||
Many thanks go out to [ECIX](https://www.ecix.net), where this project originated and was backed over the last two years.
|
||||
|
||||
Support for **OpenBGPD** was sponsored by the [Route Server Support Foundation](https://www.rssf.nl/).
|
||||
|
||||
|
@ -21,7 +21,7 @@ class FilterReason extends React.Component {
|
||||
const cls = `reject-reason reject-reason-${community[1]}-${community[2]}`;
|
||||
return (
|
||||
<p key={key} className={cls}>
|
||||
<a href={`http://irrexplorer.nlnog.net/search/${route.network}`}
|
||||
<a href={`https://irrexplorer.nlnog.net/prefix/${route.network}`}
|
||||
target="_blank" >{reason}</a>
|
||||
</p>
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ class NoExportReason extends React.Component {
|
||||
const cls = `noexport-reason noexport-reason-${community[1]}-${community[2]}`;
|
||||
return (
|
||||
<p key={key} className={cls}>
|
||||
<a href={`http://irrexplorer.nlnog.net/search/${route.network}`}
|
||||
<a href={`https://irrexplorer.nlnog.net/prefix/${route.network}`}
|
||||
target="_blank" >{reason}</a>
|
||||
</p>
|
||||
);
|
||||
|
@ -108,11 +108,17 @@ function _sortNeighbors(neighbors, sort, order) {
|
||||
return neighbors.sort(_sortOrder(cmp, order));
|
||||
}
|
||||
|
||||
function isUpState(s) {
|
||||
if (!s) { return false; }
|
||||
s = s.toLowerCase();
|
||||
return (s.includes("up") || s.includes("established"));
|
||||
}
|
||||
|
||||
|
||||
class RoutesLink extends React.Component {
|
||||
render() {
|
||||
let url = `/routeservers/${this.props.routeserverId}/protocols/${this.props.protocol}/routes`;
|
||||
if (this.props.state.toLowerCase() != 'up') {
|
||||
if (!isUpState(this.props.state)) {
|
||||
return (<span>{this.props.children}</span>);
|
||||
}
|
||||
return (
|
||||
@ -189,7 +195,7 @@ const ColDescription = function(props) {
|
||||
protocol={neighbour.id}
|
||||
state={neighbour.state}>
|
||||
{neighbour.description}
|
||||
{neighbour.state.toLowerCase() != "up" &&
|
||||
{!isUpState(neighbour.state) &&
|
||||
neighbour.last_error &&
|
||||
<span className="protocol-state-error">
|
||||
{neighbour.last_error}
|
||||
@ -230,6 +236,12 @@ const ColPlain = function(props) {
|
||||
);
|
||||
}
|
||||
|
||||
const ColNotAvailable = function(props) {
|
||||
return (
|
||||
<td>-</td>
|
||||
);
|
||||
}
|
||||
|
||||
// Column:
|
||||
const NeighbourColumn = function(props) {
|
||||
const neighbour = props.neighbour;
|
||||
@ -244,6 +256,11 @@ const NeighbourColumn = function(props) {
|
||||
"Description": ColDescription,
|
||||
};
|
||||
|
||||
// For openbgpd the value is ommitted
|
||||
if (props.rsType == "openbgpd") {
|
||||
widgets["routes_not_exported"] = ColNotAvailable;
|
||||
}
|
||||
|
||||
// Get render function
|
||||
let Widget = widgets[column] || ColLinked;
|
||||
return (
|
||||
@ -256,8 +273,11 @@ const NeighbourColumn = function(props) {
|
||||
class NeighboursTableView extends React.Component {
|
||||
|
||||
render() {
|
||||
const rs = this.props.routeserver;
|
||||
if(!rs) { return null; } // We wait until we have a routeserver
|
||||
|
||||
const columns = this.props.neighboursColumns;
|
||||
const columnsOrder = this.props.neighboursColumnsOrder;
|
||||
let columnsOrder = this.props.neighboursColumnsOrder;
|
||||
|
||||
const sortedNeighbors = _sortNeighbors(this.props.neighbours,
|
||||
this.props.sortColumn,
|
||||
@ -267,6 +287,7 @@ class NeighboursTableView extends React.Component {
|
||||
return (
|
||||
<NeighborColumnHeader key={col}
|
||||
rsId={this.props.routeserverId}
|
||||
rsType={rs.type}
|
||||
columns={columns} column={col}
|
||||
sort={this.props.sortColumn}
|
||||
order={this.props.sortOrder}
|
||||
@ -278,6 +299,7 @@ class NeighboursTableView extends React.Component {
|
||||
let neighbourColumns = columnsOrder.map((col) => {
|
||||
return <NeighbourColumn key={col}
|
||||
rsId={this.props.routeserverId}
|
||||
rsType={rs.type}
|
||||
column={col}
|
||||
neighbour={n} />
|
||||
});
|
||||
@ -328,14 +350,21 @@ class NeighboursTableView extends React.Component {
|
||||
}
|
||||
|
||||
const NeighboursTable = connect(
|
||||
(state) => ({
|
||||
neighboursColumns: state.config.neighbours_columns,
|
||||
neighboursColumnsOrder: state.config.neighbours_columns_order,
|
||||
(state, ownProps) => {
|
||||
const rsId = ownProps.routeserverId;
|
||||
const rs = state.routeservers.byId[rsId];
|
||||
|
||||
return {
|
||||
routeserver: rs,
|
||||
|
||||
sortColumn: state.neighbors.sortColumn,
|
||||
sortOrder: state.neighbors.sortOrder,
|
||||
filterQuery: state.neighbors.filterQuery,
|
||||
})
|
||||
neighboursColumns: state.config.neighbours_columns,
|
||||
neighboursColumnsOrder: state.config.neighbours_columns_order,
|
||||
|
||||
sortColumn: state.neighbors.sortColumn,
|
||||
sortOrder: state.neighbors.sortOrder,
|
||||
filterQuery: state.neighbors.filterQuery,
|
||||
};
|
||||
}
|
||||
)(NeighboursTableView);
|
||||
|
||||
|
||||
@ -386,25 +415,20 @@ class Protocols extends React.Component {
|
||||
let neighboursDown = [];
|
||||
let neighboursIdle = [];
|
||||
|
||||
for (let id in protocol) {
|
||||
let n = protocol[id];
|
||||
switch(n.state.toLowerCase()) {
|
||||
case 'up':
|
||||
neighboursUp.push(n);
|
||||
break;
|
||||
case 'down':
|
||||
neighboursDown.push(n);
|
||||
break;
|
||||
case 'start':
|
||||
neighboursIdle.push(n);
|
||||
break;
|
||||
default:
|
||||
neighboursUp.push(n);
|
||||
console.error("Couldn't classify neighbour by state:", n);
|
||||
for (let n of protocol) {
|
||||
let s = n.state.toLowerCase();
|
||||
if (s.includes("up") || s.includes("established") ) {
|
||||
neighboursUp.push(n);
|
||||
} else if (s.includes("down")) {
|
||||
neighboursDown.push(n);
|
||||
} else if (s.includes("start") || s.includes("active")) {
|
||||
neighboursIdle.push(n);
|
||||
} else {
|
||||
console.error("Couldn't classify neighbour by state:", n);
|
||||
neighboursUp.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Render tables
|
||||
let tables = [];
|
||||
if (neighboursUp.length) {
|
||||
@ -437,7 +461,7 @@ export default connect(
|
||||
protocols: state.routeservers.protocols,
|
||||
|
||||
filterQuery: state.neighbors.filterQuery,
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
routing: state.routing.locationBeforeTransitions,
|
||||
}
|
||||
}
|
||||
)(Protocols);
|
||||
|
@ -68,6 +68,9 @@ const makeQueryLinkProps = function(routing, query, loadNotExported) {
|
||||
* loading) and show info screen.
|
||||
*/
|
||||
const RoutesViewEmpty = (props) => {
|
||||
const hasContent = props.routes.received.totalResults > 0 ||
|
||||
props.routes.filtered.totalResults > 0 ||
|
||||
props.routes.notExported.totalResults > 0;
|
||||
const isLoading = props.routes.received.loading ||
|
||||
props.routes.filtered.loading ||
|
||||
props.routes.notExported.loading;
|
||||
@ -75,24 +78,24 @@ const RoutesViewEmpty = (props) => {
|
||||
if (isLoading) {
|
||||
return null; // We are not a loading indicator.
|
||||
}
|
||||
|
||||
if (!props.loadNotExported) {
|
||||
return null; // There may be routes matching the query in there!
|
||||
|
||||
// Maybe this has something to do with a filter
|
||||
if (!hasContent && props.hasQuery) {
|
||||
return (
|
||||
<div className="card info-result-empty">
|
||||
<h4>No routes matching your query.</h4>
|
||||
<p>Please check if your query is too restrictive.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const hasContent = props.routes.received.totalResults > 0 ||
|
||||
props.routes.filtered.totalResults > 0 ||
|
||||
props.routes.notExported.totalResults > 0;
|
||||
if (hasContent) {
|
||||
return null; // Nothing to do then.
|
||||
}
|
||||
|
||||
|
||||
// Show info screen
|
||||
return (
|
||||
<div className="card info-result-empty">
|
||||
<h4>No routes found matching your query.</h4>
|
||||
<p>Please check if your query is too restrictive.</p>
|
||||
<p className="card-body">There are <b>no routes</b> to display for this neighbor.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -156,8 +159,7 @@ class RoutesPage extends React.Component {
|
||||
const filterPlaceholder = "Filter by " +
|
||||
filterableColumnsText(
|
||||
this.props.routesColumns,
|
||||
this.props.routesColumnsOrder
|
||||
);
|
||||
this.props.routesColumnsOrder);
|
||||
|
||||
return(
|
||||
<div className={pageClass}>
|
||||
@ -189,9 +191,8 @@ class RoutesPage extends React.Component {
|
||||
<QuickLinks routes={this.props.routes} />
|
||||
|
||||
<RoutesViewEmpty routes={this.props.routes}
|
||||
hasQuery={!!this.props.filterValue}
|
||||
loadNotExported={this.props.loadNotExported} />
|
||||
|
||||
|
||||
<RoutesView
|
||||
type={ROUTES_FILTERED}
|
||||
routeserverId={this.props.params.routeserverId}
|
||||
@ -239,6 +240,7 @@ class RoutesPage extends React.Component {
|
||||
|
||||
export default connect(
|
||||
(state, props) => {
|
||||
const query = props.params.query;
|
||||
const protocolId = props.params.protocolId;
|
||||
const rsId = props.params.routeserverId;
|
||||
const neighbors = state.routeservers.protocols[rsId];
|
||||
@ -266,6 +268,11 @@ export default connect(
|
||||
totalResults: state.routes.notExportedTotalResults,
|
||||
apiStatus: state.routes.notExportedApiStatus
|
||||
};
|
||||
|
||||
const totalResults = state.routes.receivedTotalResults +
|
||||
state.routes.filteredTotalResults +
|
||||
state.routes.notExportedTotalResults;
|
||||
|
||||
const anyLoading = state.routes.receivedLoading ||
|
||||
state.routes.filteredLoading ||
|
||||
state.routes.notExportedLoading;
|
||||
@ -286,9 +293,9 @@ export default connect(
|
||||
neighbor: neighbor,
|
||||
filterValue: state.routes.filterValue,
|
||||
routes: {
|
||||
[ROUTES_RECEIVED]: received,
|
||||
[ROUTES_FILTERED]: filtered,
|
||||
[ROUTES_NOT_EXPORTED]: notExported
|
||||
[ROUTES_RECEIVED]: received,
|
||||
[ROUTES_FILTERED]: filtered,
|
||||
[ROUTES_NOT_EXPORTED]: notExported
|
||||
},
|
||||
routesColumns: state.config.routes_columns,
|
||||
routesColumnsOrder: state.config.routes_columns_order,
|
||||
@ -297,8 +304,10 @@ export default connect(
|
||||
loadNotExported: state.routes.loadNotExported ||
|
||||
!state.config.noexport_load_on_demand,
|
||||
|
||||
totalResults: totalResults,
|
||||
anyLoading: anyLoading,
|
||||
|
||||
filterQuery: state.routes.filterQuery,
|
||||
filtersApplied: filtersApplied,
|
||||
filtersAvailable: filtersAvailable,
|
||||
|
||||
|
@ -31,7 +31,6 @@ const QuickLinks = function(props) {
|
||||
|
||||
// Is there anything to show?
|
||||
if (!isLoading &&
|
||||
!showNotExported &&
|
||||
props.routes.notExported.totalResults == 0 &&
|
||||
props.routes.received.totalResults == 0 &&
|
||||
props.routes.filtered.totalResults == 0) {
|
||||
@ -50,7 +49,8 @@ const QuickLinks = function(props) {
|
||||
props.routes.received.totalResults > 0) &&
|
||||
<li className="received">
|
||||
<a href="#routes-received">Accepted</a></li>}
|
||||
{showNotExported &&
|
||||
{(!props.routes.notExported.loading &&
|
||||
props.routes.notExported.totalResults > 0) &&
|
||||
<li className="not-exported">
|
||||
<a href="#routes-not-exported">Not Exported</a></li>}
|
||||
</ul>
|
||||
|
@ -48,7 +48,7 @@ function PeerLink(props) {
|
||||
const rid = neighbor.routeserver_id;
|
||||
let peerUrl;
|
||||
|
||||
if (neighbor.state == "up") {
|
||||
if (neighbor.state == "up" || neighbor.state.includes("established")) {
|
||||
peerUrl = `/routeservers/${rid}/protocols/${pid}/routes`;
|
||||
} else {
|
||||
peerUrl = `/routeservers/${rid}#sessions-down`;
|
||||
|
@ -54,7 +54,7 @@ export const ColNetwork = function(props) {
|
||||
// Special AS Path Widget
|
||||
export const ColAsPath = function(props) {
|
||||
const asns = _lookup(props.route, "bgp.as_path");
|
||||
const baseUrl = "http://irrexplorer.nlnog.net/search/"
|
||||
const baseUrl = "https://irrexplorer.nlnog.net/asn/AS"
|
||||
|
||||
let asnLinks = asns.map((asn, i) => {
|
||||
return (<a key={`${asn}_${i}`} href={baseUrl + asn} target="_blank">{asn} </a>);
|
||||
@ -91,6 +91,7 @@ export default function(props) {
|
||||
"flags": ColFlags,
|
||||
"bgp.as_path": ColAsPath,
|
||||
|
||||
"Flags": ColFlags,
|
||||
"ASPath": ColAsPath,
|
||||
};
|
||||
|
||||
|
@ -158,6 +158,9 @@ export const RpkiIndicator = connect(
|
||||
class _RejectCandidateIndicator extends React.Component {
|
||||
|
||||
render() {
|
||||
if (!this.props.candidateCommunities) {
|
||||
return null;
|
||||
}
|
||||
if (!isRejectCandidate(this.props.candidateCommunities, this.props.route)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -183,6 +183,7 @@ class RoutesView extends React.Component {
|
||||
}
|
||||
|
||||
renderLoadTrigger() {
|
||||
const rs = this.props.routeserver;
|
||||
const type = this.props.type;
|
||||
const state = this.props.routes[type];
|
||||
const name = {
|
||||
@ -191,6 +192,10 @@ class RoutesView extends React.Component {
|
||||
[ROUTES_NOT_EXPORTED]: "routes-not-exported",
|
||||
}[type];
|
||||
|
||||
// We do not support this with openbgpd based route servers.
|
||||
if (rs && rs.type == "openbgpd") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is an artificial delay, to make the user wait until
|
||||
// filtered and recieved routes are fetched
|
||||
@ -234,7 +239,8 @@ class RoutesView extends React.Component {
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => {
|
||||
(state, ownProps) => {
|
||||
const rs = state.routeservers.byId[ownProps.routeserverId];
|
||||
const received = {
|
||||
routes: state.routes.received,
|
||||
requested: state.routes.receivedRequested,
|
||||
@ -278,6 +284,7 @@ export default connect(
|
||||
state.routes.notExportedFiltersApplied
|
||||
);
|
||||
return({
|
||||
routeserver: rs,
|
||||
filterQuery: state.routes.filterQuery,
|
||||
routes: {
|
||||
[ROUTES_RECEIVED]: received,
|
||||
|
@ -46,6 +46,15 @@ class Details extends React.Component {
|
||||
];
|
||||
};
|
||||
|
||||
let lastReconfig = rsStatus.last_reconfig;
|
||||
|
||||
// We have some capabilities: openbgpd does not support
|
||||
// last reboot or last reconfig
|
||||
if (rs.type == "openbgpd") {
|
||||
lastReboot = null;
|
||||
lastReconfig = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="routeserver-status">
|
||||
<tbody>
|
||||
@ -54,10 +63,11 @@ class Details extends React.Component {
|
||||
<td><i className="fa fa-clock-o"></i></td>
|
||||
<td>Last Reboot: <b><Datetime value={lastReboot} /></b></td>
|
||||
</tr>}
|
||||
{lastReconfig &&
|
||||
<tr>
|
||||
<td><i className="fa fa-clock-o"></i></td>
|
||||
<td>Last Reconfig: <b><Datetime value={rsStatus.last_reconfig} /></b></td>
|
||||
</tr>
|
||||
<td>Last Reconfig: <b><Datetime value={lastReconfig} /></b></td>
|
||||
</tr>}
|
||||
|
||||
<tr>
|
||||
<td><i className="fa fa-thumbs-up"></i></td>
|
||||
|
@ -120,8 +120,10 @@ Description = Description
|
||||
routes_received = Routes Received
|
||||
routes_filtered = Filtered
|
||||
|
||||
#
|
||||
|
||||
[routes_columns]
|
||||
flags =
|
||||
network = Network
|
||||
gateway = Gateway
|
||||
interface = Interface
|
||||
@ -130,6 +132,7 @@ bgp.as_path = AS Path
|
||||
|
||||
|
||||
[lookup_columns]
|
||||
flags =
|
||||
network = Network
|
||||
gateway = Gateway
|
||||
neighbour.asn = ASN
|
||||
@ -193,3 +196,16 @@ servertime_ext = Mon, 02 Jan 2006 15:04:05 -0700
|
||||
# Default: 300
|
||||
# processing_timeout = 300
|
||||
|
||||
# [source.rs0-example]
|
||||
# name = rs-example.openbgpd-state-server
|
||||
# [source.rs0-example.openbgpd-state-server]
|
||||
# api = http://165.22.27.105:29111/api
|
||||
|
||||
# Cache results from openbgpd for n seconds, 0 disables the cache.
|
||||
# cache_ttl = 30
|
||||
# routes_cache_size = 1024 # Neighbors
|
||||
|
||||
# [source.rs0-example-bgplgd]
|
||||
# name = rs-example.bgplgd
|
||||
# [source.rs0-example-bgplgd.openbgpd-bgplgd]
|
||||
# api = http://165.22.27.105:29111/api
|
||||
|
@ -5,13 +5,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// General api response
|
||||
// Response is a general API response
|
||||
type Response interface{}
|
||||
|
||||
// Details, usually the original backend response
|
||||
// Details are usually the original backend response
|
||||
type Details map[string]interface{}
|
||||
|
||||
// Error Handling
|
||||
// ErrorResponse encodes an error message and code
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code"`
|
||||
@ -19,12 +19,12 @@ type ErrorResponse struct {
|
||||
RouteserverId string `json:"routeserver_id"`
|
||||
}
|
||||
|
||||
// Cache aware api response
|
||||
// CacheableResponse is a cache aware API response
|
||||
type CacheableResponse interface {
|
||||
CacheTTL() time.Duration
|
||||
}
|
||||
|
||||
// Config
|
||||
// ConfigResponse is a response with client runtime configuration
|
||||
type ConfigResponse struct {
|
||||
Asn int `json:"asn"`
|
||||
|
||||
@ -51,6 +51,7 @@ type ConfigResponse struct {
|
||||
PrefixLookupEnabled bool `json:"prefix_lookup_enabled"`
|
||||
}
|
||||
|
||||
// Noexport options
|
||||
type Noexport struct {
|
||||
LoadOnDemand bool `json:"load_on_demand"`
|
||||
}
|
||||
@ -95,9 +96,10 @@ type StatusResponse struct {
|
||||
Status Status `json:"status"`
|
||||
}
|
||||
|
||||
// Routeservers
|
||||
// A Routeserver is a datasource with attributes.
|
||||
type Routeserver struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Group string `json:"group"`
|
||||
Blackholes []string `json:"blackholes"`
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -29,6 +30,13 @@ type Neighbour struct {
|
||||
Details map[string]interface{} `json:"details"`
|
||||
}
|
||||
|
||||
// String encodes a neighbor as json. This is
|
||||
// more readable than the golang default represenation.
|
||||
func (n *Neighbour) String() string {
|
||||
repr, _ := json.Marshal(n)
|
||||
return string(repr)
|
||||
}
|
||||
|
||||
// Implement sorting interface for routes
|
||||
func (neighbours Neighbours) Len() int {
|
||||
return len(neighbours)
|
||||
@ -85,6 +93,8 @@ type NeighboursLookupResults map[string]Neighbours
|
||||
|
||||
type NeighboursStatus []*NeighbourStatus
|
||||
|
||||
// NeighbourStatus contains only the neighbor state and
|
||||
// uptime.
|
||||
type NeighbourStatus struct {
|
||||
Id string `json:"id"`
|
||||
State string `json:"state"`
|
||||
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -21,6 +22,11 @@ type Route struct {
|
||||
Details Details `json:"details"`
|
||||
}
|
||||
|
||||
func (r *Route) String() string {
|
||||
s, _ := json.Marshal(r)
|
||||
return string(s)
|
||||
}
|
||||
|
||||
// Implement Filterable interface for routes
|
||||
func (self *Route) MatchSourceId(id string) bool {
|
||||
return true // A route has no source info so we exclude this filter
|
||||
|
212
pkg/backend/_testdata/alice.conf
Normal file
212
pkg/backend/_testdata/alice.conf
Normal file
@ -0,0 +1,212 @@
|
||||
# ======================================
|
||||
# Alice-LG configuration example
|
||||
# ======================================
|
||||
|
||||
[server]
|
||||
# configures the built-in webserver and provides global application settings
|
||||
listen_http = 127.0.0.1:7340
|
||||
# enable the prefix-lookup endpoint / the global search feature
|
||||
enable_prefix_lookup = true
|
||||
# Try to refresh the neighbor status on every request to /neighbors
|
||||
enable_neighbors_status_refresh = false
|
||||
asn = 9033
|
||||
# this ASN is used as a fallback value in the RPKI feature and for route
|
||||
# filtering evaluation with large BGP communities
|
||||
|
||||
[housekeeping]
|
||||
# Interval for the housekeeping routine in minutes
|
||||
interval = 5
|
||||
# Try to release memory via a forced GC/SCVG run on every housekeeping run
|
||||
force_release_memory = true
|
||||
|
||||
[theme]
|
||||
path = /path/to/my/alice/theme/files
|
||||
# Optional:
|
||||
url_base = /theme
|
||||
|
||||
[pagination]
|
||||
# Routes tables can be paginated, which comes in handy with
|
||||
# peers announcing a lot of routes. Set to 0 to disable
|
||||
# pagination.
|
||||
routes_filtered_page_size = 250
|
||||
routes_accepted_page_size = 250
|
||||
routes_not_exported_page_size = 250
|
||||
|
||||
[rejection_reasons]
|
||||
# a pair of a large BGP community value and a string to signal the processing
|
||||
# results of route filtering
|
||||
9033:65666:1 = An IP Bogon was detected
|
||||
9033:65666:2 = Prefix is longer than 64
|
||||
9033:65666:3 = Prefix is longer than 24
|
||||
9033:65666:4 = AS path contains a bogon AS
|
||||
9033:65666:5 = AS path length is longer than 64
|
||||
9033:65666:6 = First AS in path is not the same as the Peer AS
|
||||
9033:65666:7 = ECIX prefix hijack
|
||||
9033:65666:8 = Origin AS not found in IRRDB for Peer AS-SET
|
||||
9033:65666:9 = Prefix not found in IRRDB for Origin AS
|
||||
9033:65666:10 = Advertised nexthop address is not the same as the peer
|
||||
|
||||
23:42:1 = Some made up reason
|
||||
|
||||
#
|
||||
# Optional: Define communities which might be filtered
|
||||
# in the future.
|
||||
[rejection_candidates]
|
||||
communities = 6695:1102:14, 6695:1102:15, 23:42:46
|
||||
|
||||
[noexport]
|
||||
load_on_demand = true # Default: false
|
||||
|
||||
[noexport_reasons]
|
||||
# a pair of a large BGP community value and a string to signal the processing
|
||||
# results of route distribution and the distribution policy applied to a route
|
||||
9033:65667:1 = The target peer policy is Fairly-open and the sender ASN is an exception
|
||||
9033:65667:2 = The target peer policy is Selective and the sender ASN is no exception
|
||||
9033:65667:3 = The target peer policy is set to restrictive
|
||||
9033:65667:4 = The sender has specifically refused export to the target peer, either through sending 65000:AS, or through the portal
|
||||
9033:65667:5 = The sender has refused export to all peers and the target is no exception, either through sending 65000:0, or through the portal
|
||||
9033:65667:6 = The Sender has set (peerRTTHigherDeny:ms) and the targets RTT ms >= then the ms in the community
|
||||
9033:65667:7 = The Sender has set (peerRTTLowerDeny:ms) and the targets RTT ms <= then the ms in the community
|
||||
|
||||
23:46:1 = Some other made up reason
|
||||
|
||||
|
||||
[rpki]
|
||||
# shows rpki validation status in the client, based on the presence of a large
|
||||
# BGP community on the route
|
||||
enabled = true
|
||||
|
||||
# Optional, falling back to defaults as defined in:
|
||||
# https://www.euro-ix.net/en/forixps/large-bgp-communities/
|
||||
valid = 23042:1000:1
|
||||
unknown = 23042:1000:2
|
||||
# not_checked = 23042:1000:3
|
||||
invalid = 23042:1000:4-*
|
||||
|
||||
|
||||
# Define other known bgp communities
|
||||
[bgp_communities]
|
||||
1:23 = some tag
|
||||
9033:65666:1 = ip bogon detected
|
||||
# Wildcards are supported aswell:
|
||||
0:* = do not redistribute to AS$1
|
||||
|
||||
#
|
||||
# Define columns for neighbours and routes table,
|
||||
# with <key> = <Table Header>
|
||||
#
|
||||
# and <key> := <object.path> Implicitly referencing the object,
|
||||
# e.g. route.bgp.as_path -> bgp.as_path)
|
||||
# |= <Widget> A widget with special rendering features,
|
||||
# to which the object is applied. E.g.
|
||||
# Uptime, which will be rendered as
|
||||
# Uptime(neighbour).
|
||||
#
|
||||
# As per convention: Widgets are in Uppercase, object properties are
|
||||
# in lowercase.
|
||||
#
|
||||
# Available Widgets for Neighbours:
|
||||
#
|
||||
# Uptime Displays the relative uptime of this neighbour
|
||||
# Description The neighbour's description with link to routes page
|
||||
#
|
||||
|
||||
[neighbours_columns]
|
||||
address = Neighbour
|
||||
asn = ASN
|
||||
state = State
|
||||
Uptime = Uptime
|
||||
Description = Description
|
||||
routes_received = Routes Received
|
||||
routes_filtered = Filtered
|
||||
|
||||
|
||||
[routes_columns]
|
||||
network = Network
|
||||
gateway = Gateway
|
||||
interface = Interface
|
||||
metric = Metric
|
||||
bgp.as_path = AS Path
|
||||
|
||||
|
||||
[lookup_columns]
|
||||
network = Network
|
||||
gateway = Gateway
|
||||
neighbour.asn = ASN
|
||||
neighbour.description = Description
|
||||
bgp.as_path = AS Path
|
||||
routeserver.name = RS
|
||||
|
||||
|
||||
# Routeservers
|
||||
# Birdwatcher Example
|
||||
[source.rs0-example-v4]
|
||||
name = rs1.example.com (IPv4)
|
||||
# Optional: a group for the routeservers list
|
||||
group = FRA
|
||||
blackholes = 10.23.6.666, 10.23.6.665
|
||||
|
||||
[source.rs0-example-v4.birdwatcher]
|
||||
api = http://rs1.example.com:29184/
|
||||
# single_table / multi_table
|
||||
type = multi_table
|
||||
peer_table_prefix = T
|
||||
pipe_protocol_prefix = M
|
||||
# Timeout in seconds to wait for the status data (only required if enable_neighbors_status_refresh is true)
|
||||
neighbors_refresh_timeout = 2
|
||||
|
||||
# Optional:
|
||||
show_last_reboot = true
|
||||
|
||||
[source.rs1-example-v6]
|
||||
name = rs1.example.com (IPv6)
|
||||
[source.rs1-example-v6.birdwatcher]
|
||||
timezone = Europe/Brussels
|
||||
api = http://rs1.example.com:29186/
|
||||
# single_table / multi_table
|
||||
type = multi_table
|
||||
peer_table_prefix = T
|
||||
pipe_protocol_prefix = M
|
||||
# Timeout in seconds to wait for the status data (only required if enable_neighbors_status_refresh is true)
|
||||
neighbors_refresh_timeout = 2
|
||||
|
||||
# Optional: Examples for time format
|
||||
# Please see https://golang.org/pkg/time/#pkg-constants for an
|
||||
# explanation on how time parsing in go works.
|
||||
servertime = 2006-01-02T15:04:05Z07:00
|
||||
servertime_short = 02.01.2006
|
||||
servertime_ext = Mon, 02 Jan 2006 15:04:05 -0700
|
||||
|
||||
|
||||
# Routeservers
|
||||
# GoBGP Example
|
||||
[source.rs2-example]
|
||||
name = rs2.example.com
|
||||
group = AMS
|
||||
|
||||
[source.rs2-example.gobgp]
|
||||
# host is the IP (or DNS name) and port for the remote GoBGP daemon
|
||||
host = rs2.example.com:50051
|
||||
# processing_timeout is a timeout in seconds configured per gRPC call to a given GoBGP daemon
|
||||
processing_timeout = 300
|
||||
type = multi_table
|
||||
peer_table_prefix = T
|
||||
pipe_protocol_prefix = M
|
||||
neighbors_refresh_timeout = 2
|
||||
|
||||
[source.rs3-example.openbgpd-state-server]
|
||||
name = rs-example.openbgpd-state-server
|
||||
[source.rs3-example.openbgpd-state-server]
|
||||
api = http://165.22.27.105:29111/api
|
||||
|
||||
# Cache results from openbgpd for n seconds, 0 disables the cache.
|
||||
cache_ttl = 30
|
||||
routes_cache_size = 1024 # Neighbors
|
||||
|
||||
[source.rs4-example-bgplgd]
|
||||
name = rs-example.bgplgd
|
||||
[source.rs4-example-bgplgd.openbgpd-bgplgd]
|
||||
api = http://165.22.27.105:29111/api
|
||||
cache_ttl = 30
|
||||
routes_cache_size = 1024 # Neighbors
|
||||
|
@ -17,19 +17,19 @@ func apiStatusShow(_req *http.Request, _params httprouter.Params) (api.Response,
|
||||
|
||||
// Handle status
|
||||
func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.Status()
|
||||
if err != nil {
|
||||
apiLogSourceError("status", rsId, err)
|
||||
apiLogSourceError("status", rsID, err)
|
||||
}
|
||||
|
||||
return result, err
|
||||
@ -39,22 +39,22 @@ func apiStatus(_req *http.Request, params httprouter.Params) (api.Response, erro
|
||||
func apiConfigShow(_req *http.Request, _params httprouter.Params) (api.Response, error) {
|
||||
result := api.ConfigResponse{
|
||||
Asn: AliceConfig.Server.Asn,
|
||||
BgpCommunities: AliceConfig.Ui.BgpCommunities,
|
||||
RejectReasons: AliceConfig.Ui.RoutesRejections.Reasons,
|
||||
BgpCommunities: AliceConfig.UI.BgpCommunities,
|
||||
RejectReasons: AliceConfig.UI.RoutesRejections.Reasons,
|
||||
Noexport: api.Noexport{
|
||||
LoadOnDemand: AliceConfig.Ui.RoutesNoexports.LoadOnDemand,
|
||||
LoadOnDemand: AliceConfig.UI.RoutesNoexports.LoadOnDemand,
|
||||
},
|
||||
NoexportReasons: AliceConfig.Ui.RoutesNoexports.Reasons,
|
||||
NoexportReasons: AliceConfig.UI.RoutesNoexports.Reasons,
|
||||
RejectCandidates: api.RejectCandidates{
|
||||
Communities: AliceConfig.Ui.RoutesRejectCandidates.Communities,
|
||||
Communities: AliceConfig.UI.RoutesRejectCandidates.Communities,
|
||||
},
|
||||
Rpki: api.Rpki(AliceConfig.Ui.Rpki),
|
||||
RoutesColumns: AliceConfig.Ui.RoutesColumns,
|
||||
RoutesColumnsOrder: AliceConfig.Ui.RoutesColumnsOrder,
|
||||
NeighboursColumns: AliceConfig.Ui.NeighboursColumns,
|
||||
NeighboursColumnsOrder: AliceConfig.Ui.NeighboursColumnsOrder,
|
||||
LookupColumns: AliceConfig.Ui.LookupColumns,
|
||||
LookupColumnsOrder: AliceConfig.Ui.LookupColumnsOrder,
|
||||
Rpki: api.Rpki(AliceConfig.UI.Rpki),
|
||||
RoutesColumns: AliceConfig.UI.RoutesColumns,
|
||||
RoutesColumnsOrder: AliceConfig.UI.RoutesColumnsOrder,
|
||||
NeighboursColumns: AliceConfig.UI.NeighboursColumns,
|
||||
NeighboursColumnsOrder: AliceConfig.UI.NeighboursColumnsOrder,
|
||||
LookupColumns: AliceConfig.UI.LookupColumns,
|
||||
LookupColumnsOrder: AliceConfig.UI.LookupColumnsOrder,
|
||||
PrefixLookupEnabled: AliceConfig.Server.EnablePrefixLookup,
|
||||
}
|
||||
return result, nil
|
||||
|
@ -14,7 +14,7 @@ func apiNeighborsList(
|
||||
_req *http.Request,
|
||||
params httprouter.Params,
|
||||
) (api.Response, error) {
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -23,9 +23,9 @@ func apiNeighborsList(
|
||||
|
||||
// Try to fetch neighbors from store, only fall back
|
||||
// to RS query if store is not ready yet
|
||||
sourceStatus := AliceNeighboursStore.SourceStatus(rsId)
|
||||
sourceStatus := AliceNeighboursStore.SourceStatus(rsID)
|
||||
if sourceStatus.State == STATE_READY {
|
||||
neighbors := AliceNeighboursStore.GetNeighborsAt(rsId)
|
||||
neighbors := AliceNeighboursStore.GetNeighborsAt(rsID)
|
||||
// Make response
|
||||
neighborsResponse = &api.NeighboursResponse{
|
||||
Api: api.ApiStatus{
|
||||
@ -41,14 +41,14 @@ func apiNeighborsList(
|
||||
Neighbours: neighbors,
|
||||
}
|
||||
} else {
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
neighborsResponse, err = source.Neighbours()
|
||||
if err != nil {
|
||||
apiLogSourceError("neighbors", rsId, err)
|
||||
apiLogSourceError("neighbors", rsID, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -11,20 +11,20 @@ import (
|
||||
|
||||
// Handle routes
|
||||
func apiRoutesList(_req *http.Request, params httprouter.Params) (api.Response, error) {
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
neighborId := params.ByName("neighborId")
|
||||
neighborID := params.ByName("neighborId")
|
||||
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.Routes(neighborId)
|
||||
result, err := source.Routes(neighborID)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes", rsId, neighborId, err)
|
||||
apiLogSourceError("routes", rsID, neighborID, err)
|
||||
}
|
||||
|
||||
return result, err
|
||||
@ -38,20 +38,20 @@ func apiRoutesListReceived(
|
||||
// Measure response time
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
neighborID := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesReceived(neighborId)
|
||||
result, err := source.RoutesReceived(neighborID)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_received", rsId, neighborId, err)
|
||||
apiLogSourceError("routes_received", rsID, neighborID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ func apiRoutesListReceived(
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesAcceptedPageSize
|
||||
pageSize := AliceConfig.UI.Pagination.RoutesAcceptedPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
@ -111,20 +111,20 @@ func apiRoutesListFiltered(
|
||||
) (api.Response, error) {
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
neighborID := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesFiltered(neighborId)
|
||||
result, err := source.RoutesFiltered(neighborID)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_filtered", rsId, neighborId, err)
|
||||
apiLogSourceError("routes_filtered", rsID, neighborID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -153,7 +153,7 @@ func apiRoutesListFiltered(
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesFilteredPageSize
|
||||
pageSize := AliceConfig.UI.Pagination.RoutesFilteredPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
@ -184,20 +184,20 @@ func apiRoutesListNotExported(
|
||||
) (api.Response, error) {
|
||||
t0 := time.Now()
|
||||
|
||||
rsId, err := validateSourceID(params.ByName("id"))
|
||||
rsID, err := validateSourceID(params.ByName("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
neighborId := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceById(rsId)
|
||||
neighborID := params.ByName("neighborId")
|
||||
source := AliceConfig.SourceInstanceByID(rsID)
|
||||
if source == nil {
|
||||
return nil, SOURCE_NOT_FOUND_ERROR
|
||||
}
|
||||
|
||||
result, err := source.RoutesNotExported(neighborId)
|
||||
result, err := source.RoutesNotExported(neighborID)
|
||||
if err != nil {
|
||||
apiLogSourceError("routes_not_exported", rsId, neighborId, err)
|
||||
apiLogSourceError("routes_not_exported", rsID, neighborID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ func apiRoutesListNotExported(
|
||||
|
||||
// Paginate results
|
||||
page := apiQueryMustInt(req, "page", 0)
|
||||
pageSize := AliceConfig.Ui.Pagination.RoutesNotExportedPageSize
|
||||
pageSize := AliceConfig.UI.Pagination.RoutesNotExportedPageSize
|
||||
routes, pagination := apiPaginateRoutes(routes, page, pageSize)
|
||||
|
||||
// Calculate query duration
|
||||
|
@ -17,7 +17,8 @@ func apiRouteserversList(_req *http.Request, _params httprouter.Params) (api.Res
|
||||
sources := AliceConfig.Sources
|
||||
for _, source := range sources {
|
||||
routeservers = append(routeservers, api.Routeserver{
|
||||
Id: source.Id,
|
||||
Id: source.ID,
|
||||
Type: source.Type,
|
||||
Name: source.Name,
|
||||
Group: source.Group,
|
||||
Blackholes: source.Blackholes,
|
||||
|
@ -89,13 +89,13 @@ func apiLookupPrefixGlobal(
|
||||
|
||||
// Paginate results
|
||||
pageImported := apiQueryMustInt(req, "page_imported", 0)
|
||||
pageSizeImported := AliceConfig.Ui.Pagination.RoutesAcceptedPageSize
|
||||
pageSizeImported := AliceConfig.UI.Pagination.RoutesAcceptedPageSize
|
||||
routesImported, paginationImported := apiPaginateLookupRoutes(
|
||||
imported, pageImported, pageSizeImported,
|
||||
)
|
||||
|
||||
pageFiltered := apiQueryMustInt(req, "page_filtered", 0)
|
||||
pageSizeFiltered := AliceConfig.Ui.Pagination.RoutesFilteredPageSize
|
||||
pageSizeFiltered := AliceConfig.UI.Pagination.RoutesFilteredPageSize
|
||||
routesFiltered, paginationFiltered := apiPaginateLookupRoutes(
|
||||
filtered, pageFiltered, pageSizeFiltered,
|
||||
)
|
||||
|
@ -12,7 +12,7 @@ func apiLogSourceError(module string, sourceID string, params ...interface{}) {
|
||||
args := []string{}
|
||||
|
||||
// Get source configuration
|
||||
source := AliceConfig.SourceById(sourceID)
|
||||
source := AliceConfig.SourceByID(sourceID)
|
||||
sourceName := "unknown"
|
||||
if source != nil {
|
||||
sourceName = source.Name
|
||||
|
@ -11,7 +11,7 @@ func TestApiLogSourceError(t *testing.T) {
|
||||
conf := &Config{
|
||||
Sources: []*SourceConfig{
|
||||
&SourceConfig{
|
||||
Id: "rs1v4",
|
||||
ID: "rs1v4",
|
||||
Name: "rs1.example.net (IPv4)",
|
||||
},
|
||||
},
|
||||
|
@ -15,7 +15,7 @@ func StartHTTPServer() {
|
||||
router := httprouter.New()
|
||||
|
||||
// Serve static content
|
||||
if err := webRegisterAssets(AliceConfig.Ui, router); err != nil {
|
||||
if err := webRegisterAssets(AliceConfig.UI, router); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,10 @@ package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -130,3 +133,44 @@ func (c BgpCommunities) Set(community string, label string) {
|
||||
slookup := lookup.(BgpCommunities)
|
||||
slookup[path[len(path)-1]] = label
|
||||
}
|
||||
|
||||
// APICommunities enumerates all bgp communities into
|
||||
// a set of api.Communities.
|
||||
// CAVEAT: Wildcards are substituted by 0 and ** ARE NOT ** expanded.
|
||||
func (c BgpCommunities) APICommunities() api.Communities {
|
||||
communities := api.Communities{}
|
||||
// We could do this recursive, or assume that
|
||||
// the max depth is 3.
|
||||
for uVal, c1 := range c {
|
||||
u, err := strconv.Atoi(uVal)
|
||||
if err != nil {
|
||||
u = 0
|
||||
}
|
||||
for vVal, c2 := range c1.(BgpCommunities) {
|
||||
v, err := strconv.Atoi(vVal)
|
||||
if err != nil {
|
||||
v = 0
|
||||
}
|
||||
|
||||
com2, ok := c2.(BgpCommunities)
|
||||
if !ok {
|
||||
// we only have labels here
|
||||
communities = append(
|
||||
communities, api.Community{u, v})
|
||||
continue
|
||||
}
|
||||
|
||||
for wVal := range com2 {
|
||||
w, err := strconv.Atoi(wVal)
|
||||
if err != nil {
|
||||
w = 0
|
||||
}
|
||||
|
||||
communities = append(
|
||||
communities, api.Community{u, v, w})
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return communities
|
||||
}
|
||||
|
@ -82,3 +82,11 @@ func TestWildcardLookup(t *testing.T) {
|
||||
t.Error("Unexpected label for key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPICommunities(t *testing.T) {
|
||||
c := MakeWellKnownBgpCommunities()
|
||||
comm := c.APICommunities()
|
||||
if len(comm) != 14 {
|
||||
t.Error("unexpected len(communities) = ", len(comm))
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,55 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/sources"
|
||||
"github.com/alice-lg/alice-lg/pkg/sources/birdwatcher"
|
||||
"github.com/alice-lg/alice-lg/pkg/sources/gobgp"
|
||||
"github.com/alice-lg/alice-lg/pkg/sources/openbgpd"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrSourceTypeUnknown will be used if the type could
|
||||
// not be identified from the section.
|
||||
ErrSourceTypeUnknown = errors.New("source type unknown")
|
||||
)
|
||||
|
||||
// Config Source Types
|
||||
const (
|
||||
SOURCE_UNKNOWN = 0
|
||||
SOURCE_BIRDWATCHER = 1
|
||||
SOURCE_GOBGP = 2
|
||||
// SourceTypeBird is used for either bird 1x and 2x
|
||||
// based route servers with a birdwatcher backend.
|
||||
SourceTypeBird = "bird"
|
||||
|
||||
// SourceTypeGoBGP indicates a GoBGP based source.
|
||||
SourceTypeGoBGP = "gobgp"
|
||||
|
||||
// SourceTypeOpenBGPD is used for an OpenBGPD source.
|
||||
SourceTypeOpenBGPD = "openbgpd"
|
||||
)
|
||||
|
||||
const (
|
||||
// SourceBackendBirdwatcher is used to indicate that
|
||||
// the source is using a birdwatcher interface.
|
||||
SourceBackendBirdwatcher = "birdwatcher"
|
||||
|
||||
// SourceBackendGoBGP is used when the source is consuming
|
||||
// a GoBGP daemon via grpc API.
|
||||
SourceBackendGoBGP = "gobgp"
|
||||
|
||||
// SourceBackendOpenBGPDStateServer is used when the openbgpd
|
||||
// is exported using the openbgpd-state-server.
|
||||
SourceBackendOpenBGPDStateServer = "openbgpd-state-server"
|
||||
|
||||
// SourceBackendOpenBGPDBgplgd is used when the openbgpd
|
||||
// state is exported through the bgplgd.
|
||||
SourceBackendOpenBGPDBgplgd = "openbgpd-bgplgd"
|
||||
)
|
||||
|
||||
// A ServerConfig holds the runtime configuration
|
||||
@ -70,8 +102,8 @@ type RpkiConfig struct {
|
||||
Invalid []string `ini:"invalid"`
|
||||
}
|
||||
|
||||
// UiConfig holds runtime settings for the web client
|
||||
type UiConfig struct {
|
||||
// UIConfig holds runtime settings for the web client
|
||||
type UIConfig struct {
|
||||
RoutesColumns map[string]string
|
||||
RoutesColumnsOrder []string
|
||||
|
||||
@ -108,7 +140,7 @@ type PaginationConfig struct {
|
||||
|
||||
// A SourceConfig is a generic source configuration
|
||||
type SourceConfig struct {
|
||||
Id string
|
||||
ID string
|
||||
Order int
|
||||
Name string
|
||||
Group string
|
||||
@ -117,9 +149,11 @@ type SourceConfig struct {
|
||||
Blackholes []string
|
||||
|
||||
// Source configurations
|
||||
Type int
|
||||
Type string
|
||||
Backend string
|
||||
Birdwatcher birdwatcher.Config
|
||||
GoBGP gobgp.Config
|
||||
OpenBGPD openbgpd.Config
|
||||
|
||||
// Source instance
|
||||
instance sources.Source
|
||||
@ -129,24 +163,24 @@ type SourceConfig struct {
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Housekeeping HousekeepingConfig
|
||||
Ui UiConfig
|
||||
UI UIConfig
|
||||
Sources []*SourceConfig
|
||||
File string
|
||||
}
|
||||
|
||||
// SourceById returns a source from the config by id
|
||||
func (cfg *Config) SourceById(sourceId string) *SourceConfig {
|
||||
// SourceByID returns a source from the config by id
|
||||
func (cfg *Config) SourceByID(id string) *SourceConfig {
|
||||
for _, sourceConfig := range cfg.Sources {
|
||||
if sourceConfig.Id == sourceId {
|
||||
if sourceConfig.ID == id {
|
||||
return sourceConfig
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SourceInstanceById returns an instance by id
|
||||
func (cfg *Config) SourceInstanceById(sourceId string) sources.Source {
|
||||
sourceConfig := cfg.SourceById(sourceId)
|
||||
// SourceInstanceByID returns an instance by id
|
||||
func (cfg *Config) SourceInstanceByID(id string) sources.Source {
|
||||
sourceConfig := cfg.SourceByID(id)
|
||||
if sourceConfig == nil {
|
||||
return nil // Nothing to do here.
|
||||
}
|
||||
@ -172,15 +206,36 @@ func isSourceBase(section *ini.Section) bool {
|
||||
}
|
||||
|
||||
// Get backend configuration type
|
||||
func getBackendType(section *ini.Section) int {
|
||||
func sourceBackendTypeFromConfig(section *ini.Section) (string, error) {
|
||||
name := section.Name()
|
||||
if strings.HasSuffix(name, "birdwatcher") {
|
||||
return SOURCE_BIRDWATCHER
|
||||
return SourceBackendBirdwatcher, nil
|
||||
} else if strings.HasSuffix(name, "gobgp") {
|
||||
return SOURCE_GOBGP
|
||||
return SourceBackendGoBGP, nil
|
||||
} else if strings.HasSuffix(name, "openbgpd-bgplgd") {
|
||||
return SourceBackendOpenBGPDBgplgd, nil
|
||||
} else if strings.HasSuffix(name, "openbgpd-state-server") {
|
||||
return SourceBackendOpenBGPDStateServer, nil
|
||||
}
|
||||
|
||||
return SOURCE_UNKNOWN
|
||||
return "", ErrSourceTypeUnknown
|
||||
}
|
||||
|
||||
// sourceTypeFromBackendType will return the backend source type
|
||||
// for a given backend type
|
||||
func sourceTypeFromBackendType(t string) string {
|
||||
switch t {
|
||||
case SourceBackendBirdwatcher:
|
||||
return SourceTypeBird
|
||||
case SourceBackendGoBGP:
|
||||
return SourceTypeGoBGP
|
||||
case SourceBackendOpenBGPDStateServer:
|
||||
return SourceTypeOpenBGPD
|
||||
case SourceBackendOpenBGPDBgplgd:
|
||||
return SourceTypeOpenBGPD
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Get UI config: Routes Columns Default
|
||||
@ -191,9 +246,7 @@ func getRoutesColumnsDefaults() (map[string]string, []string, error) {
|
||||
"gateway": "Gateway",
|
||||
"interface": "Interface",
|
||||
}
|
||||
|
||||
order := []string{"network", "bgp.as_path", "gateway", "interface"}
|
||||
|
||||
return columns, order, nil
|
||||
}
|
||||
|
||||
@ -502,8 +555,8 @@ func getPaginationConfig(config *ini.File) PaginationConfig {
|
||||
}
|
||||
|
||||
// Get the UI configuration from the config file
|
||||
func getUiConfig(config *ini.File) (UiConfig, error) {
|
||||
uiConfig := UiConfig{}
|
||||
func getUIConfig(config *ini.File) (UIConfig, error) {
|
||||
uiConfig := UIConfig{}
|
||||
|
||||
// Get route columns
|
||||
routesColumns, routesColumnsOrder, err := getRoutesColumns(config)
|
||||
@ -553,7 +606,7 @@ func getUiConfig(config *ini.File) (UiConfig, error) {
|
||||
paginationConfig := getPaginationConfig(config)
|
||||
|
||||
// Make config
|
||||
uiConfig = UiConfig{
|
||||
uiConfig = UIConfig{
|
||||
RoutesColumns: routesColumns,
|
||||
RoutesColumnsOrder: routesColumnsOrder,
|
||||
|
||||
@ -589,47 +642,49 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
||||
}
|
||||
|
||||
// Derive source-id from name
|
||||
sourceId := section.Name()[len("source:"):]
|
||||
sourceID := section.Name()[len("source:"):]
|
||||
|
||||
// Try to get child configs and determine
|
||||
// Source type
|
||||
sourceConfigSections := section.ChildSections()
|
||||
if len(sourceConfigSections) == 0 {
|
||||
// This source has no configured backend
|
||||
return sources, fmt.Errorf("%s has no backend configuration", section.Name())
|
||||
return nil, fmt.Errorf("%s has no backend configuration", section.Name())
|
||||
}
|
||||
|
||||
if len(sourceConfigSections) > 1 {
|
||||
// The source is ambiguous
|
||||
return sources, fmt.Errorf("%s has ambigous backends", section.Name())
|
||||
return nil, fmt.Errorf("%s has ambigous backends", section.Name())
|
||||
}
|
||||
|
||||
// Configure backend
|
||||
backendConfig := sourceConfigSections[0]
|
||||
backendType := getBackendType(backendConfig)
|
||||
|
||||
if backendType == SOURCE_UNKNOWN {
|
||||
return sources, fmt.Errorf("%s has an unsupported backend", section.Name())
|
||||
backendType, err := sourceBackendTypeFromConfig(backendConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s has an unsupported backend", section.Name())
|
||||
}
|
||||
|
||||
sourceType := sourceTypeFromBackendType(backendType)
|
||||
|
||||
// Make config
|
||||
sourceName := section.Key("name").MustString("Unknown Source")
|
||||
sourceGroup := section.Key("group").MustString("")
|
||||
sourceBlackholes := TrimmedStringList(
|
||||
section.Key("blackholes").MustString(""))
|
||||
|
||||
config := &SourceConfig{
|
||||
Id: sourceId,
|
||||
srcCfg := &SourceConfig{
|
||||
ID: sourceID,
|
||||
Order: order,
|
||||
Name: sourceName,
|
||||
Group: sourceGroup,
|
||||
Blackholes: sourceBlackholes,
|
||||
Type: backendType,
|
||||
Backend: backendType,
|
||||
Type: sourceType,
|
||||
}
|
||||
|
||||
// Set backend
|
||||
switch backendType {
|
||||
case SOURCE_BIRDWATCHER:
|
||||
case SourceBackendBirdwatcher:
|
||||
sourceType := backendConfig.Key("type").MustString("")
|
||||
mainTable := backendConfig.Key("main_table").MustString("master")
|
||||
peerTablePrefix := backendConfig.Key("peer_table_prefix").MustString("T")
|
||||
@ -645,8 +700,8 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
||||
"and pipe_protocol_prefix", pipeProtocolPrefix)
|
||||
|
||||
c := birdwatcher.Config{
|
||||
Id: config.Id,
|
||||
Name: config.Name,
|
||||
ID: srcCfg.ID,
|
||||
Name: srcCfg.Name,
|
||||
|
||||
Timezone: "UTC",
|
||||
ServerTime: "2006-01-02T15:04:05.999999999Z07:00",
|
||||
@ -660,12 +715,12 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
||||
}
|
||||
|
||||
backendConfig.MapTo(&c)
|
||||
config.Birdwatcher = c
|
||||
srcCfg.Birdwatcher = c
|
||||
|
||||
case SOURCE_GOBGP:
|
||||
case SourceBackendGoBGP:
|
||||
c := gobgp.Config{
|
||||
Id: config.Id,
|
||||
Name: config.Name,
|
||||
Id: srcCfg.ID,
|
||||
Name: srcCfg.Name,
|
||||
}
|
||||
|
||||
backendConfig.MapTo(&c)
|
||||
@ -675,11 +730,51 @@ func getSources(config *ini.File) ([]*SourceConfig, error) {
|
||||
c.ProcessingTimeout = 300
|
||||
}
|
||||
|
||||
config.GoBGP = c
|
||||
srcCfg.GoBGP = c
|
||||
|
||||
case SourceBackendOpenBGPDStateServer:
|
||||
// Get cache TTL and reject communities from the config
|
||||
cacheTTL := time.Second * time.Duration(backendConfig.Key("cache_ttl").MustInt(300))
|
||||
routesCacheSize := backendConfig.Key("routes_cache_size").MustInt(1024)
|
||||
rc, err := getRoutesRejections(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rejectComms := rc.Reasons.APICommunities()
|
||||
|
||||
c := openbgpd.Config{
|
||||
ID: srcCfg.ID,
|
||||
Name: srcCfg.Name,
|
||||
CacheTTL: cacheTTL,
|
||||
RoutesCacheSize: routesCacheSize,
|
||||
RejectCommunities: rejectComms,
|
||||
}
|
||||
backendConfig.MapTo(&c)
|
||||
srcCfg.OpenBGPD = c
|
||||
|
||||
case SourceBackendOpenBGPDBgplgd:
|
||||
// Get cache TTL from the config
|
||||
cacheTTL := time.Second * time.Duration(backendConfig.Key("cache_ttl").MustInt(300))
|
||||
routesCacheSize := backendConfig.Key("routes_cache_size").MustInt(1024)
|
||||
rc, err := getRoutesRejections(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rejectComms := rc.Reasons.APICommunities()
|
||||
|
||||
c := openbgpd.Config{
|
||||
ID: srcCfg.ID,
|
||||
Name: srcCfg.Name,
|
||||
CacheTTL: cacheTTL,
|
||||
RoutesCacheSize: routesCacheSize,
|
||||
RejectCommunities: rejectComms,
|
||||
}
|
||||
backendConfig.MapTo(&c)
|
||||
srcCfg.OpenBGPD = c
|
||||
}
|
||||
|
||||
// Add to list of sources
|
||||
sources = append(sources, config)
|
||||
sources = append(sources, srcCfg)
|
||||
order++
|
||||
}
|
||||
|
||||
@ -728,7 +823,7 @@ func loadConfig(file string) (*Config, error) {
|
||||
}
|
||||
|
||||
// Get UI configurations
|
||||
ui, err := getUiConfig(parsedConfig)
|
||||
ui, err := getUIConfig(parsedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -736,7 +831,7 @@ func loadConfig(file string) (*Config, error) {
|
||||
config := &Config{
|
||||
Server: server,
|
||||
Housekeeping: housekeeping,
|
||||
Ui: ui,
|
||||
UI: ui,
|
||||
Sources: sources,
|
||||
File: file,
|
||||
}
|
||||
@ -751,11 +846,15 @@ func (cfg *SourceConfig) getInstance() sources.Source {
|
||||
}
|
||||
|
||||
var instance sources.Source
|
||||
switch cfg.Type {
|
||||
case SOURCE_BIRDWATCHER:
|
||||
switch cfg.Backend {
|
||||
case SourceBackendBirdwatcher:
|
||||
instance = birdwatcher.NewBirdwatcher(cfg.Birdwatcher)
|
||||
case SOURCE_GOBGP:
|
||||
case SourceBackendGoBGP:
|
||||
instance = gobgp.NewGoBGP(cfg.GoBGP)
|
||||
case SourceBackendOpenBGPDStateServer:
|
||||
instance = openbgpd.NewStateServerSource(&cfg.OpenBGPD)
|
||||
case SourceBackendOpenBGPDBgplgd:
|
||||
instance = openbgpd.NewBgplgdSource(&cfg.OpenBGPD)
|
||||
}
|
||||
|
||||
cfg.instance = instance
|
||||
|
@ -3,8 +3,8 @@ package backend
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alice-lg/alice-lg/backend/sources/birdwatcher"
|
||||
"github.com/alice-lg/alice-lg/backend/sources/gobgp"
|
||||
"github.com/alice-lg/alice-lg/pkg/sources/birdwatcher"
|
||||
"github.com/alice-lg/alice-lg/pkg/sources/gobgp"
|
||||
)
|
||||
|
||||
// Test configuration loading and parsing
|
||||
@ -12,25 +12,25 @@ import (
|
||||
|
||||
func TestLoadConfigs(t *testing.T) {
|
||||
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
if config.Server.Listen == "" {
|
||||
t.Error("Listen string not present.")
|
||||
}
|
||||
|
||||
if len(config.Ui.RoutesColumns) == 0 {
|
||||
if len(config.UI.RoutesColumns) == 0 {
|
||||
t.Error("Route columns settings missing")
|
||||
}
|
||||
|
||||
if len(config.Ui.RoutesRejections.Reasons) == 0 {
|
||||
if len(config.UI.RoutesRejections.Reasons) == 0 {
|
||||
t.Error("Rejection reasons missing")
|
||||
}
|
||||
|
||||
// Check communities
|
||||
label, err := config.Ui.BgpCommunities.Lookup("1:23")
|
||||
label, err := config.UI.BgpCommunities.Lookup("1:23")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -43,10 +43,9 @@ func TestLoadConfigs(t *testing.T) {
|
||||
// TestSourceConfig checks that the proper backend type was identified for each
|
||||
// example routeserver
|
||||
func TestSourceConfig(t *testing.T) {
|
||||
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
// Get sources
|
||||
@ -77,10 +76,9 @@ func TestSourceConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
// Get sources
|
||||
@ -117,13 +115,13 @@ func TestSourceConfigDefaultsOverride(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRejectAndNoexportReasons(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
// Rejection reasons
|
||||
description, err := config.Ui.RoutesRejections.Reasons.Lookup("23:42:1")
|
||||
description, err := config.UI.RoutesRejections.Reasons.Lookup("23:42:1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -133,7 +131,7 @@ func TestRejectAndNoexportReasons(t *testing.T) {
|
||||
}
|
||||
|
||||
// Noexport reasons
|
||||
description, err = config.Ui.RoutesNoexports.Reasons.Lookup("23:46:1")
|
||||
description, err = config.UI.RoutesNoexports.Reasons.Lookup("23:46:1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -144,9 +142,9 @@ func TestRejectAndNoexportReasons(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBlackholeParsing(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
// Get first source
|
||||
@ -163,9 +161,9 @@ func TestBlackholeParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOwnASN(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
if config.Server.Asn != 9033 {
|
||||
@ -174,45 +172,43 @@ func TestOwnASN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRpkiConfig(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
if len(config.Ui.Rpki.Valid) != 3 {
|
||||
t.Error("Unexpected RPKI:VALID,", config.Ui.Rpki.Valid)
|
||||
if len(config.UI.Rpki.Valid) != 3 {
|
||||
t.Error("Unexpected RPKI:VALID,", config.UI.Rpki.Valid)
|
||||
}
|
||||
if len(config.Ui.Rpki.Invalid) != 4 {
|
||||
t.Error("Unexpected RPKI:INVALID,", config.Ui.Rpki.Invalid)
|
||||
return // We would fail hard later
|
||||
if len(config.UI.Rpki.Invalid) != 4 {
|
||||
t.Fatal("Unexpected RPKI:INVALID,", config.UI.Rpki.Invalid)
|
||||
}
|
||||
|
||||
// Check fallback
|
||||
if config.Ui.Rpki.NotChecked[0] != "9033" {
|
||||
if config.UI.Rpki.NotChecked[0] != "9033" {
|
||||
t.Error(
|
||||
"Expected NotChecked to fall back to defaults, got:",
|
||||
config.Ui.Rpki.NotChecked,
|
||||
config.UI.Rpki.NotChecked,
|
||||
)
|
||||
}
|
||||
|
||||
// Check range postprocessing
|
||||
if config.Ui.Rpki.Invalid[3] != "*" {
|
||||
if config.UI.Rpki.Invalid[3] != "*" {
|
||||
t.Error("Missing wildcard from config")
|
||||
}
|
||||
|
||||
t.Log(config.Ui.Rpki)
|
||||
t.Log(config.UI.Rpki)
|
||||
}
|
||||
|
||||
func TestRejectCandidatesConfig(t *testing.T) {
|
||||
config, err := loadConfig("../etc/alice-lg/alice.example.conf")
|
||||
config, err := loadConfig("_testdata/alice.conf")
|
||||
if err != nil {
|
||||
t.Error("Could not load test config:", err)
|
||||
return
|
||||
t.Fatal("Could not load test config:", err)
|
||||
}
|
||||
|
||||
t.Log(config.Ui.RoutesRejectCandidates.Communities)
|
||||
t.Log(config.UI.RoutesRejectCandidates.Communities)
|
||||
|
||||
description, err := config.Ui.RoutesRejectCandidates.Communities.Lookup("23:42:46")
|
||||
description, err := config.UI.RoutesRejectCandidates.Communities.Lookup("23:42:46")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ type NeighboursStore struct {
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// NewNeighboursStore creates a new store for neighbors
|
||||
func NewNeighboursStore(config *Config) *NeighboursStore {
|
||||
|
||||
// Build source mapping
|
||||
@ -33,13 +34,13 @@ func NewNeighboursStore(config *Config) *NeighboursStore {
|
||||
statusMap := make(map[string]StoreStatus)
|
||||
|
||||
for _, source := range config.Sources {
|
||||
sourceId := source.Id
|
||||
configMap[sourceId] = source
|
||||
statusMap[sourceId] = StoreStatus{
|
||||
id := source.ID
|
||||
configMap[id] = source
|
||||
statusMap[id] = StoreStatus{
|
||||
State: STATE_INIT,
|
||||
}
|
||||
|
||||
neighboursMap[sourceId] = make(NeighboursIndex)
|
||||
neighboursMap[id] = make(NeighboursIndex)
|
||||
}
|
||||
|
||||
// Set refresh interval, default to 5 minutes when
|
||||
@ -62,6 +63,7 @@ func NewNeighboursStore(config *Config) *NeighboursStore {
|
||||
return store
|
||||
}
|
||||
|
||||
// Start the store's housekeeping.
|
||||
func (self *NeighboursStore) Start() {
|
||||
log.Println("Starting local neighbours store")
|
||||
log.Println("Neighbours Store refresh interval set to:", self.refreshInterval)
|
||||
@ -121,7 +123,7 @@ func (self *NeighboursStore) update() {
|
||||
if err != nil {
|
||||
log.Println(
|
||||
"Refreshing the neighbors store failed for:",
|
||||
sourceConfig.Name, "(", sourceConfig.Id, ")",
|
||||
sourceConfig.Name, "(", sourceConfig.ID, ")",
|
||||
"with:", err,
|
||||
"- NEXT STATE: ERROR",
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ func NewRoutesStore(config *Config) *RoutesStore {
|
||||
configMap := make(map[string]*SourceConfig)
|
||||
|
||||
for _, source := range config.Sources {
|
||||
id := source.Id
|
||||
id := source.ID
|
||||
|
||||
configMap[id] = source
|
||||
routesMap[id] = &api.RoutesResponse{}
|
||||
@ -109,7 +109,7 @@ func (rs *RoutesStore) update() {
|
||||
if err != nil {
|
||||
log.Println(
|
||||
"Refreshing the routes store failed for:", sourceConfig.Name,
|
||||
"(", sourceConfig.Id, ")",
|
||||
"(", sourceConfig.ID, ")",
|
||||
"with:", err,
|
||||
"- NEXT STATE: ERROR",
|
||||
)
|
||||
@ -207,7 +207,7 @@ func routeToLookupRoute(
|
||||
) *api.LookupRoute {
|
||||
|
||||
// Get neighbour
|
||||
neighbour := AliceNeighboursStore.GetNeighbourAt(source.Id, route.NeighbourId)
|
||||
neighbour := AliceNeighboursStore.GetNeighbourAt(source.ID, route.NeighbourId)
|
||||
|
||||
// Make route
|
||||
lookup := &api.LookupRoute{
|
||||
@ -217,7 +217,7 @@ func routeToLookupRoute(
|
||||
Neighbour: neighbour,
|
||||
|
||||
Routeserver: api.Routeserver{
|
||||
Id: source.Id,
|
||||
Id: source.ID,
|
||||
Name: source.Name,
|
||||
},
|
||||
|
||||
|
@ -79,12 +79,12 @@ func makeTestRoutesStore() *RoutesStore {
|
||||
|
||||
configMap := map[string]*SourceConfig{
|
||||
"rs1": &SourceConfig{
|
||||
Id: "rs1",
|
||||
ID: "rs1",
|
||||
Name: "rs1.test",
|
||||
Type: SOURCE_BIRDWATCHER,
|
||||
Type: SourceTypeBird,
|
||||
|
||||
Birdwatcher: birdwatcher.Config{
|
||||
Api: "http://localhost:2342",
|
||||
API: "http://localhost:2342",
|
||||
Timezone: "UTC",
|
||||
ServerTime: "2006-01-02T15:04:05",
|
||||
ServerTimeShort: "2006-01-02",
|
||||
|
@ -34,7 +34,7 @@ func webPrepareClientHTML(html string) string {
|
||||
|
||||
// Register assets handler and index handler
|
||||
// at /static and /
|
||||
func webRegisterAssets(ui UiConfig, router *httprouter.Router) error {
|
||||
func webRegisterAssets(ui UIConfig, router *httprouter.Router) error {
|
||||
log.Println("Preparing and installing assets")
|
||||
|
||||
// Prepare client html: Rewrite paths
|
||||
|
@ -5,12 +5,14 @@ import (
|
||||
)
|
||||
|
||||
/*
|
||||
Use a least recently used caching strategy:
|
||||
LRUMap is a cache map which uses
|
||||
a least recently used caching strategy:
|
||||
Store last access in map, retrieve least recently
|
||||
used key.
|
||||
*/
|
||||
type LRUMap map[string]time.Time
|
||||
|
||||
// LRU retrievs the least recently used key
|
||||
func (lrumap LRUMap) LRU() string {
|
||||
t := time.Now()
|
||||
key := ""
|
||||
|
@ -14,11 +14,13 @@ birdwatcher, we keep a local cache. (This comes in handy
|
||||
when we are paginating the results for better client performance.)
|
||||
*/
|
||||
|
||||
// NeighborsCache implements a cache to store neighbors
|
||||
type NeighborsCache struct {
|
||||
response *api.NeighboursResponse
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// NewNeighborsCache initializes a cache for neighbor responses.
|
||||
func NewNeighborsCache(disabled bool) *NeighborsCache {
|
||||
cache := &NeighborsCache{
|
||||
response: nil,
|
||||
@ -28,26 +30,29 @@ func NewNeighborsCache(disabled bool) *NeighborsCache {
|
||||
return cache
|
||||
}
|
||||
|
||||
func (self *NeighborsCache) Get() *api.NeighboursResponse {
|
||||
if self.disabled {
|
||||
// Get retrievs the neighbors response from the cache, if present,
|
||||
// and makes sure the information is still up to date.
|
||||
func (cache *NeighborsCache) Get() *api.NeighboursResponse {
|
||||
if cache.disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.response == nil {
|
||||
if cache.response == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if self.response.CacheTTL() < 0 {
|
||||
if cache.response.CacheTTL() < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.response
|
||||
return cache.response
|
||||
}
|
||||
|
||||
func (self *NeighborsCache) Set(response *api.NeighboursResponse) {
|
||||
if self.disabled {
|
||||
// Set updates the neighbors cache with a new response retrieved
|
||||
// from a backend source.
|
||||
func (cache *NeighborsCache) Set(response *api.NeighboursResponse) {
|
||||
if cache.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
self.response = response
|
||||
cache.response = response
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
/*
|
||||
Routes Cache:
|
||||
RoutesCache stores routes responses from the backend.
|
||||
|
||||
Keep a kv map with neighborId <-> api.RoutesResponse
|
||||
TTL is derived from the api.RoutesResponse.
|
||||
|
||||
@ -24,6 +25,7 @@ type RoutesCache struct {
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// NewRoutesCache initializes a new cache for route responses.
|
||||
func NewRoutesCache(disabled bool, size int) *RoutesCache {
|
||||
cache := &RoutesCache{
|
||||
responses: make(map[string]*api.RoutesResponse),
|
||||
@ -35,15 +37,16 @@ func NewRoutesCache(disabled bool, size int) *RoutesCache {
|
||||
return cache
|
||||
}
|
||||
|
||||
func (self *RoutesCache) Get(neighborId string) *api.RoutesResponse {
|
||||
if self.disabled {
|
||||
// Get retrievs all routes for a given neighbor
|
||||
func (cache *RoutesCache) Get(neighborID string) *api.RoutesResponse {
|
||||
if cache.disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.Lock()
|
||||
defer self.Unlock()
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
response, ok := self.responses[neighborId]
|
||||
response, ok := cache.responses[neighborID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@ -52,43 +55,45 @@ func (self *RoutesCache) Get(neighborId string) *api.RoutesResponse {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.accessedAt[neighborId] = time.Now()
|
||||
cache.accessedAt[neighborID] = time.Now()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func (self *RoutesCache) Set(neighborId string, response *api.RoutesResponse) {
|
||||
if self.disabled {
|
||||
// Set the routes response for a given neighbor
|
||||
func (cache *RoutesCache) Set(neighborID string, response *api.RoutesResponse) {
|
||||
if cache.disabled {
|
||||
return
|
||||
}
|
||||
|
||||
self.Lock()
|
||||
defer self.Unlock()
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
if len(self.responses) > self.size {
|
||||
if len(cache.responses) > cache.size {
|
||||
// delete LRU
|
||||
lru := self.accessedAt.LRU()
|
||||
delete(self.accessedAt, lru)
|
||||
delete(self.responses, lru)
|
||||
leastRecentNeighbor := cache.accessedAt.LRU()
|
||||
delete(cache.accessedAt, leastRecentNeighbor)
|
||||
delete(cache.responses, leastRecentNeighbor)
|
||||
}
|
||||
|
||||
self.accessedAt[neighborId] = time.Now()
|
||||
self.responses[neighborId] = response
|
||||
cache.accessedAt[neighborID] = time.Now()
|
||||
cache.responses[neighborID] = response
|
||||
}
|
||||
|
||||
func (self *RoutesCache) Expire() int {
|
||||
self.Lock()
|
||||
defer self.Unlock()
|
||||
// Expire will flush expired keys. (TODO: naming could be better.)
|
||||
func (cache *RoutesCache) Expire() int {
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
expiredKeys := []string{}
|
||||
for key, response := range self.responses {
|
||||
for key, response := range cache.responses {
|
||||
if response.CacheTTL() < 0 {
|
||||
expiredKeys = append(expiredKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range expiredKeys {
|
||||
delete(self.responses, key)
|
||||
delete(cache.responses, key)
|
||||
}
|
||||
|
||||
return len(expiredKeys)
|
||||
|
27
pkg/decoders/json_response.go
Normal file
27
pkg/decoders/json_response.go
Normal file
@ -0,0 +1,27 @@
|
||||
package decoders
|
||||
|
||||
// Helper for decoding json bodies from responses
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ReadJSONResponse reads a json blob from a
|
||||
// http response and decodes it into a map
|
||||
func ReadJSONResponse(res *http.Response) (map[string]interface{}, error) {
|
||||
// Read body
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
payload := make(map[string]interface{})
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
31
pkg/decoders/maps.go
Normal file
31
pkg/decoders/maps.go
Normal file
@ -0,0 +1,31 @@
|
||||
package decoders
|
||||
|
||||
// MapGet retrievs a key from an expected map
|
||||
// it falls back if the input is not a map
|
||||
// or the key was not found.
|
||||
func MapGet(m interface{}, key string, fallback interface{}) interface{} {
|
||||
smap, ok := m.(map[string]interface{})
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
val, ok := smap[key]
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// MapGetString retrievs a key from a map and
|
||||
// asserts its type is a string. Otherwise fallback
|
||||
// will be returned.
|
||||
func MapGetString(m interface{}, key string, fallback string) string {
|
||||
val := MapGet(m, key, fallback)
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
// MapGetBool will retrieve a boolean value
|
||||
// for a given key.
|
||||
func MapGetBool(m interface{}, key string, fallback bool) bool {
|
||||
val := MapGet(m, key, fallback)
|
||||
return val.(bool)
|
||||
}
|
153
pkg/decoders/types.go
Normal file
153
pkg/decoders/types.go
Normal file
@ -0,0 +1,153 @@
|
||||
package decoders
|
||||
|
||||
// Decode interfaces into expected types
|
||||
// with a fallback.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// String asserts a string, provided a default
|
||||
func String(value interface{}, fallback string) string {
|
||||
sval, ok := value.(string)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return sval
|
||||
}
|
||||
|
||||
// StringList decodes a list of strings
|
||||
func StringList(data interface{}) []string {
|
||||
ldata, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
list := make([]string, 0, len(ldata))
|
||||
for _, e := range ldata {
|
||||
s, ok := e.(string)
|
||||
if ok {
|
||||
list = append(list, s)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// IntList decodes a list of integers
|
||||
func IntList(data interface{}) []int {
|
||||
sdata := StringList(data)
|
||||
list := make([]int, 0, len(sdata))
|
||||
for _, e := range sdata {
|
||||
val, err := strconv.Atoi(e)
|
||||
if err == nil {
|
||||
list = append(list, val)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// IntListFromStrings decodes a list of strings
|
||||
// into a list of integers.
|
||||
func IntListFromStrings(strs []string) []int {
|
||||
list := make([]int, 0, len(strs))
|
||||
for _, s := range strs {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
continue // skip this
|
||||
}
|
||||
list = append(list, v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Int decodes an integer value
|
||||
func Int(value interface{}, fallback int) int {
|
||||
fval, ok := value.(float64)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return int(fval)
|
||||
}
|
||||
|
||||
// IntFromString decodes an integer from a string
|
||||
func IntFromString(s string, fallback int) int {
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Bool decodes a boolean value
|
||||
func Bool(value interface{}, fallback bool) bool {
|
||||
val, ok := value.(bool)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Duration decodes a time.Duration
|
||||
func Duration(value interface{}, fallback time.Duration) time.Duration {
|
||||
val, ok := value.(time.Duration)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// DurationTimeframe decodes a duration: Bgpctl encodes
|
||||
// this using fmt_timeframe, whiuch outputs a format similar
|
||||
// to that being understood by time.ParseDuration - however
|
||||
// the time unit "w" (weeks) is not supported.
|
||||
// According to https://github.com/openbgpd-portable/openbgpd-openbsd/blob/master/src/usr.sbin/bgpctl/bgpctl.c#L586-L591
|
||||
// we have to parse %02lluw%01ud%02uh, %01ud%02uh%02um and %02u:%02u:%02u.
|
||||
// This yields three formats:
|
||||
// 01w3d01h
|
||||
// 1d02h03m
|
||||
// 01:02:03
|
||||
func DurationTimeframe(value interface{}, fallback time.Duration) time.Duration {
|
||||
var sec, min, hour, day uint
|
||||
var week uint64
|
||||
sval := String(value, "")
|
||||
if sval == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
n, _ := fmt.Sscanf(sval, "%02dw%01dd%02dh", &week, &day, &hour)
|
||||
if n == 3 {
|
||||
return time.Duration(week)*7*24*time.Hour +
|
||||
time.Duration(day)*24*time.Hour +
|
||||
time.Duration(hour)*time.Hour
|
||||
}
|
||||
|
||||
n, _ = fmt.Sscanf(sval, "%01dd%02dh%02dm", &day, &hour, &min)
|
||||
if n == 3 {
|
||||
return time.Duration(day)*24*time.Hour +
|
||||
time.Duration(hour)*time.Hour +
|
||||
time.Duration(min)*time.Minute
|
||||
}
|
||||
|
||||
n, _ = fmt.Sscanf(sval, "%02d:%02d:%02d", &hour, &min, &sec)
|
||||
if n == 3 {
|
||||
return time.Duration(hour)*time.Hour +
|
||||
time.Duration(min)*time.Minute +
|
||||
time.Duration(sec)*time.Second
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// TimeUTC returns the time expecting an UTC timestamp
|
||||
func TimeUTC(value interface{}, fallback time.Time) time.Time {
|
||||
sval := String(value, "")
|
||||
if sval == "" {
|
||||
return fallback
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, sval)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return t
|
||||
}
|
24
pkg/decoders/types_test.go
Normal file
24
pkg/decoders/types_test.go
Normal file
@ -0,0 +1,24 @@
|
||||
package decoders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDurationTimeframe(t *testing.T) {
|
||||
d := DurationTimeframe("01w1d5h", 0)
|
||||
if d != 197*time.Hour {
|
||||
t.Error("unexpected", d)
|
||||
}
|
||||
|
||||
d = DurationTimeframe("5d20h05m", 0)
|
||||
if d != 140*time.Hour+5*time.Minute {
|
||||
t.Error("unexpected", d)
|
||||
}
|
||||
|
||||
d = DurationTimeframe("01:02:03", 0)
|
||||
if d != 1*time.Hour+2*time.Minute+3*time.Second {
|
||||
t.Error("unexpected", d)
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
package birdwatcher
|
||||
|
||||
// Config contains all configuration attributes
|
||||
// for a birdwatcher based source.
|
||||
type Config struct {
|
||||
Id string
|
||||
ID string
|
||||
Name string
|
||||
|
||||
Api string `ini:"api"`
|
||||
API string `ini:"api"`
|
||||
Timezone string `ini:"timezone"`
|
||||
ServerTime string `ini:"servertime"`
|
||||
ServerTimeShort string `ini:"servertime_short"`
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
// Convert server time string to time
|
||||
@ -136,9 +137,9 @@ func parseBirdwatcherStatus(bird ClientResponse, config Config) (api.Status, err
|
||||
LastReboot: lastReboot,
|
||||
LastReconfig: lastReconfig,
|
||||
Backend: "bird",
|
||||
Version: mustString(birdStatus["version"], "unknown"),
|
||||
Message: mustString(birdStatus["message"], "unknown"),
|
||||
RouterId: mustString(birdStatus["router_id"], "unknown"),
|
||||
Version: decoders.String(birdStatus["version"], "unknown"),
|
||||
Message: decoders.String(birdStatus["message"], "unknown"),
|
||||
RouterId: decoders.String(birdStatus["router_id"], "unknown"),
|
||||
}
|
||||
|
||||
return status, nil
|
||||
@ -152,7 +153,7 @@ func parseRelativeServerTime(uptime interface{}, config Config) time.Duration {
|
||||
|
||||
// Parse neighbours response
|
||||
func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error) {
|
||||
rsId := config.Id
|
||||
rsId := config.ID
|
||||
neighbours := api.Neighbours{}
|
||||
protocols := bird["protocols"].(map[string]interface{})
|
||||
|
||||
@ -162,7 +163,7 @@ func parseNeighbours(bird ClientResponse, config Config) (api.Neighbours, error)
|
||||
routes := protocol["routes"].(map[string]interface{})
|
||||
|
||||
uptime := parseRelativeServerTime(protocol["state_changed"], config)
|
||||
lastError := mustString(protocol["last_error"], "")
|
||||
lastError := decoders.String(protocol["last_error"], "")
|
||||
|
||||
routesReceived := float64(0)
|
||||
if routes != nil {
|
||||
@ -177,17 +178,17 @@ 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),
|
||||
Address: decoders.String(protocol["neighbor_address"], "error"),
|
||||
Asn: decoders.Int(protocol["neighbor_as"], 0),
|
||||
State: strings.ToLower(
|
||||
mustString(protocol["state"], "unknown")),
|
||||
Description: mustString(protocol["description"], "no description"),
|
||||
decoders.String(protocol["state"], "unknown")),
|
||||
Description: decoders.String(protocol["description"], "no description"),
|
||||
|
||||
RoutesReceived: mustInt(routesReceived, 0),
|
||||
RoutesAccepted: mustInt(routes["imported"], 0),
|
||||
RoutesFiltered: mustInt(routes["filtered"], 0),
|
||||
RoutesExported: mustInt(routes["exported"], 0), //TODO protocol_exported?
|
||||
RoutesPreferred: mustInt(routes["preferred"], 0),
|
||||
RoutesReceived: decoders.Int(routesReceived, 0),
|
||||
RoutesAccepted: decoders.Int(routes["imported"], 0),
|
||||
RoutesFiltered: decoders.Int(routes["filtered"], 0),
|
||||
RoutesExported: decoders.Int(routes["exported"], 0), //TODO protocol_exported?
|
||||
RoutesPreferred: decoders.Int(routes["preferred"], 0),
|
||||
|
||||
Uptime: uptime,
|
||||
LastError: lastError,
|
||||
@ -218,7 +219,7 @@ func parseNeighboursShort(bird ClientResponse, config Config) (api.NeighboursSta
|
||||
|
||||
neighbour := &api.NeighbourStatus{
|
||||
Id: protocolId,
|
||||
State: mustString(protocol["state"], "unknown"),
|
||||
State: decoders.String(protocol["state"], "unknown"),
|
||||
Since: uptime,
|
||||
}
|
||||
|
||||
@ -238,18 +239,18 @@ func parseRouteBgpInfo(data interface{}) api.BgpInfo {
|
||||
return api.BgpInfo{}
|
||||
}
|
||||
|
||||
asPath := mustIntList(bgpData["as_path"])
|
||||
asPath := decoders.IntList(bgpData["as_path"])
|
||||
communities := parseBgpCommunities(bgpData["communities"])
|
||||
largeCommunities := parseBgpCommunities(bgpData["large_communities"])
|
||||
extCommunities := parseExtBgpCommunities(bgpData["ext_communities"])
|
||||
|
||||
localPref, _ := strconv.Atoi(mustString(bgpData["local_pref"], "0"))
|
||||
med, _ := strconv.Atoi(mustString(bgpData["med"], "0"))
|
||||
localPref, _ := strconv.Atoi(decoders.String(bgpData["local_pref"], "0"))
|
||||
med, _ := strconv.Atoi(decoders.String(bgpData["med"], "0"))
|
||||
|
||||
bgp := api.BgpInfo{
|
||||
Origin: mustString(bgpData["origin"], "unknown"),
|
||||
Origin: decoders.String(bgpData["origin"], "unknown"),
|
||||
AsPath: asPath,
|
||||
NextHop: mustString(bgpData["next_hop"], "unknown"),
|
||||
NextHop: decoders.String(bgpData["next_hop"], "unknown"),
|
||||
LocalPref: localPref,
|
||||
Med: med,
|
||||
Communities: communities,
|
||||
@ -312,18 +313,18 @@ func parseRoutesData(birdRoutes []interface{}, config Config) api.Routes {
|
||||
rdata := data.(map[string]interface{})
|
||||
|
||||
age := parseRelativeServerTime(rdata["age"], config)
|
||||
rtype := mustStringList(rdata["type"])
|
||||
rtype := decoders.StringList(rdata["type"])
|
||||
bgpInfo := parseRouteBgpInfo(rdata["bgp"])
|
||||
|
||||
route := &api.Route{
|
||||
Id: mustString(rdata["network"], "unknown"),
|
||||
NeighbourId: mustString(rdata["from_protocol"], "unknown neighbour"),
|
||||
Id: decoders.String(rdata["network"], "unknown"),
|
||||
NeighbourId: decoders.String(rdata["from_protocol"], "unknown neighbour"),
|
||||
|
||||
Network: mustString(rdata["network"], "unknown net"),
|
||||
Interface: mustString(rdata["interface"], "unknown interface"),
|
||||
Gateway: mustString(rdata["gateway"], "unknown gateway"),
|
||||
Metric: mustInt(rdata["metric"], -1),
|
||||
Primary: mustBool(rdata["primary"], false),
|
||||
Network: decoders.String(rdata["network"], "unknown net"),
|
||||
Interface: decoders.String(rdata["interface"], "unknown interface"),
|
||||
Gateway: decoders.String(rdata["gateway"], "unknown gateway"),
|
||||
Metric: decoders.Int(rdata["metric"], -1),
|
||||
Primary: decoders.Bool(rdata["primary"], false),
|
||||
Age: age,
|
||||
Type: rtype,
|
||||
Bgp: bgpInfo,
|
||||
|
@ -30,7 +30,7 @@ type GenericBirdwatcher struct {
|
||||
}
|
||||
|
||||
func NewBirdwatcher(config Config) Birdwatcher {
|
||||
client := NewClient(config.Api)
|
||||
client := NewClient(config.API)
|
||||
|
||||
// Cache settings:
|
||||
// TODO: Maybe read from config file
|
||||
@ -248,7 +248,7 @@ func (self *GenericBirdwatcher) NeighboursStatus() (*api.NeighboursStatusRespons
|
||||
func (self *GenericBirdwatcher) LookupPrefix(prefix string) (*api.RoutesLookupResponse, error) {
|
||||
// Get RS info
|
||||
rs := api.Routeserver{
|
||||
Id: self.config.Id,
|
||||
Id: self.config.ID,
|
||||
Name: self.config.Name,
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
type MultiTableBirdwatcher struct {
|
||||
@ -238,7 +239,7 @@ func (self *MultiTableBirdwatcher) fetchRequiredRoutes(neighborId string) (*api.
|
||||
importedRoutes := api.Routes{}
|
||||
if len(receivedRoutes) > 0 {
|
||||
peer := receivedRoutes[0].Gateway
|
||||
learntFrom := mustString(receivedRoutes[0].Details["learnt_from"], peer)
|
||||
learntFrom := decoders.String(receivedRoutes[0].Details["learnt_from"], peer)
|
||||
|
||||
filteredRoutes = self.filterRoutesByPeerOrLearntFrom(filteredRoutes, peer, learntFrom)
|
||||
importedRoutes = self.filterRoutesByDuplicates(receivedRoutes, filteredRoutes)
|
||||
@ -507,7 +508,7 @@ func (self *MultiTableBirdwatcher) AllRoutes() (*api.RoutesResponse, error) {
|
||||
protocolsBgp := self.filterProtocolsBgp(birdProtocols)
|
||||
for protocolId, protocolsData := range protocolsBgp["protocols"].(map[string]interface{}) {
|
||||
peer := protocolsData.(map[string]interface{})["neighbor_address"].(string)
|
||||
learntFrom := mustString(protocolsData.(map[string]interface{})["learnt_from"], peer)
|
||||
learntFrom := decoders.String(protocolsData.(map[string]interface{})["learnt_from"], peer)
|
||||
|
||||
// Fetch filtered routes
|
||||
_, filtered, err := self.fetchFilteredRoutes(protocolId)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
type SingleTableBirdwatcher struct {
|
||||
@ -119,7 +120,7 @@ func (self *SingleTableBirdwatcher) fetchRequiredRoutes(neighborId string) (*api
|
||||
importedRoutes := api.Routes{}
|
||||
if len(receivedRoutes) > 0 {
|
||||
peer := receivedRoutes[0].Gateway
|
||||
learntFrom := mustString(receivedRoutes[0].Details["learnt_from"], peer)
|
||||
learntFrom := decoders.String(receivedRoutes[0].Details["learnt_from"], peer)
|
||||
|
||||
filteredRoutes = self.filterRoutesByPeerOrLearntFrom(filteredRoutes, peer, learntFrom)
|
||||
importedRoutes = self.filterRoutesByDuplicates(receivedRoutes, filteredRoutes)
|
||||
|
@ -1,61 +0,0 @@
|
||||
package birdwatcher
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
/*
|
||||
* Types helper for parser
|
||||
*/
|
||||
|
||||
// Assert string, provide default
|
||||
func mustString(value interface{}, fallback string) string {
|
||||
sval, ok := value.(string)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return sval
|
||||
}
|
||||
|
||||
// Assert list of strings
|
||||
func mustStringList(data interface{}) []string {
|
||||
list := []string{}
|
||||
ldata, ok := data.([]interface{})
|
||||
if !ok {
|
||||
return []string{}
|
||||
}
|
||||
for _, e := range ldata {
|
||||
s, ok := e.(string)
|
||||
if ok {
|
||||
list = append(list, s)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// Convert list of strings to int
|
||||
func mustIntList(data interface{}) []int {
|
||||
list := []int{}
|
||||
sdata := mustStringList(data)
|
||||
for _, e := range sdata {
|
||||
val, _ := strconv.Atoi(e)
|
||||
list = append(list, val)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func mustInt(value interface{}, fallback int) int {
|
||||
fval, ok := value.(float64)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return int(fval)
|
||||
}
|
||||
|
||||
func mustBool(value interface{}, fallback bool) bool {
|
||||
val, ok := value.(bool)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
384
pkg/sources/openbgpd/bgplgd_source.go
Normal file
384
pkg/sources/openbgpd/bgplgd_source.go
Normal file
@ -0,0 +1,384 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/caches"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
const (
|
||||
// BgplgdSourceVersion is currently fixed at 1.0
|
||||
BgplgdSourceVersion = "1.0"
|
||||
)
|
||||
|
||||
// BgplgdSource implements a source for Alice, consuming
|
||||
// the openbgp bgplgd.
|
||||
type BgplgdSource struct {
|
||||
// cfg is the source configuration retrieved
|
||||
// from the alice config file.
|
||||
cfg *Config
|
||||
|
||||
// Store the neighbor responses from the server here
|
||||
neighborsCache *caches.NeighborsCache
|
||||
|
||||
// Store the routes responses from the server
|
||||
// here identified by neighborID
|
||||
routesCache *caches.RoutesCache
|
||||
routesReceivedCache *caches.RoutesCache
|
||||
routesFilteredCache *caches.RoutesCache
|
||||
}
|
||||
|
||||
// NewBgplgdSource creates a new source instance with a configuration.
|
||||
func NewBgplgdSource(cfg *Config) *BgplgdSource {
|
||||
cacheDisabled := cfg.CacheTTL == 0
|
||||
|
||||
// Initialize caches
|
||||
nc := caches.NewNeighborsCache(cacheDisabled)
|
||||
rc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
rrc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
rfc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
|
||||
return &BgplgdSource{
|
||||
cfg: cfg,
|
||||
neighborsCache: nc,
|
||||
routesCache: rc,
|
||||
routesReceivedCache: rrc,
|
||||
routesFilteredCache: rfc,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpireCaches ... will flush the cache.
|
||||
func (src *BgplgdSource) ExpireCaches() int {
|
||||
totalExpired := src.routesReceivedCache.Expire()
|
||||
return totalExpired
|
||||
}
|
||||
|
||||
// Requests
|
||||
// ========
|
||||
|
||||
// ShowNeighborsRequest makes an all neighbors request
|
||||
func (src *BgplgdSource) ShowNeighborsRequest(ctx context.Context) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/neighbors")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowNeighborsSummaryRequest builds an neighbors status request
|
||||
func (src *BgplgdSource) ShowNeighborsSummaryRequest(
|
||||
ctx context.Context,
|
||||
) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/summary")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowNeighborRIBRequest retrives the routes accepted from the neighbor
|
||||
// identified by bgp-id.
|
||||
func (src *BgplgdSource) ShowNeighborRIBRequest(
|
||||
ctx context.Context,
|
||||
neighborID string,
|
||||
) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/rib?neighbor=%s", neighborID)
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowRIBRequest makes a request for retrieving all routes imported
|
||||
// from all peers
|
||||
func (src *BgplgdSource) ShowRIBRequest(ctx context.Context) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/rib")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// Datasource
|
||||
// ==========
|
||||
|
||||
// makeCacheStatus will create a new api status with cache infos
|
||||
func (src *BgplgdSource) makeCacheStatus() api.ApiStatus {
|
||||
return api.ApiStatus{
|
||||
CacheStatus: api.CacheStatus{
|
||||
CachedAt: time.Now().UTC(),
|
||||
},
|
||||
Version: BgplgdSourceVersion,
|
||||
ResultFromCache: false,
|
||||
Ttl: time.Now().UTC().Add(src.cfg.CacheTTL),
|
||||
}
|
||||
}
|
||||
|
||||
// Status returns an API status response. In our case
|
||||
// this is pretty much only that the service is available.
|
||||
func (src *BgplgdSource) Status() (*api.StatusResponse, error) {
|
||||
// Make API request and read response. We do not cache the result.
|
||||
response := &api.StatusResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Status: api.Status{
|
||||
Version: "openbgpd",
|
||||
Message: "openbgpd up and running",
|
||||
},
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Neighbours retrievs a full list of all neighbors
|
||||
func (src *BgplgdSource) Neighbours() (*api.NeighboursResponse, error) {
|
||||
// Query cache and see if we have a hit
|
||||
response := src.neighborsCache.Get()
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Make API request and read response
|
||||
req, err := src.ShowNeighborsRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nb, err := decodeNeighbors(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set route server id (sourceID) for all neighbors and
|
||||
// calculate the filtered routes.
|
||||
for _, n := range nb {
|
||||
n.RouteServerId = src.cfg.ID
|
||||
rejectedRes, err := src.RoutesFiltered(n.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rejectCount := len(rejectedRes.Filtered)
|
||||
n.RoutesFiltered = rejectCount
|
||||
|
||||
}
|
||||
response = &api.NeighboursResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Neighbours: nb,
|
||||
}
|
||||
src.neighborsCache.Set(response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// NeighboursStatus retrives the status summary
|
||||
// for all neightbors
|
||||
func (src *BgplgdSource) NeighboursStatus() (*api.NeighboursStatusResponse, error) {
|
||||
// Make API request and read response
|
||||
req, err := src.ShowNeighborsSummaryRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nb, err := decodeNeighborsStatus(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &api.NeighboursStatusResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Neighbours: nb,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Routes retrieves the routes for a specific neighbor
|
||||
// identified by ID.
|
||||
func (src *BgplgdSource) Routes(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
src.routesCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesReceived returns the routes exported by the neighbor.
|
||||
func (src *BgplgdSource) RoutesReceived(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesReceivedCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: api.Routes{},
|
||||
}
|
||||
src.routesReceivedCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesFiltered retrieves the routes filtered / not valid
|
||||
func (src *BgplgdSource) RoutesFiltered(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesFilteredCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: api.Routes{},
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
src.routesFilteredCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesNotExported retrievs the routes not exported
|
||||
// from the rs for a neighbor.
|
||||
func (src *BgplgdSource) RoutesNotExported(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
|
||||
Imported: api.Routes{},
|
||||
NotExported: api.Routes{},
|
||||
Filtered: api.Routes{},
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// AllRoutes retrievs the entire RIB from the source. This is never
|
||||
// cached as it is processed by the store.
|
||||
func (src *BgplgdSource) AllRoutes() (*api.RoutesResponse, error) {
|
||||
req, err := src.ShowRIBRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response := &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
return response, nil
|
||||
}
|
32
pkg/sources/openbgpd/config.go
Normal file
32
pkg/sources/openbgpd/config.go
Normal file
@ -0,0 +1,32 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
)
|
||||
|
||||
// Config is a OpenBGPD source config
|
||||
type Config struct {
|
||||
ID string
|
||||
Name string
|
||||
|
||||
CacheTTL time.Duration
|
||||
RoutesCacheSize int
|
||||
|
||||
API string `ini:"api"`
|
||||
|
||||
RejectCommunities api.Communities
|
||||
}
|
||||
|
||||
// APIURL creates an url from the config
|
||||
func (cfg *Config) APIURL(path string, params ...interface{}) string {
|
||||
u := cfg.API
|
||||
if strings.HasSuffix(u, "/") {
|
||||
u = u[:len(u)-1]
|
||||
}
|
||||
u += fmt.Sprintf(path, params...)
|
||||
return u
|
||||
}
|
16
pkg/sources/openbgpd/config_test.go
Normal file
16
pkg/sources/openbgpd/config_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigAPIURL(t *testing.T) {
|
||||
cfg := &Config{
|
||||
API: "http://a",
|
||||
}
|
||||
|
||||
url := cfg.APIURL("/%d/bgpd", 42)
|
||||
if url != "http://a/42/bgpd" {
|
||||
t.Error("unexpected url:", url)
|
||||
}
|
||||
}
|
239
pkg/sources/openbgpd/decoders.go
Normal file
239
pkg/sources/openbgpd/decoders.go
Normal file
@ -0,0 +1,239 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
// Decode the api status response from the openbgpd
|
||||
// state server.
|
||||
func decodeAPIStatus(res map[string]interface{}) api.Status {
|
||||
now := time.Now().UTC()
|
||||
uptime := decoders.Duration(res["server_uptime"], 0)
|
||||
|
||||
// This is an approximation and maybe wrong
|
||||
lastReboot := now.Add(-uptime)
|
||||
s := api.Status{
|
||||
ServerTime: decoders.TimeUTC(res["server_time_utc"], time.Time{}),
|
||||
LastReboot: lastReboot,
|
||||
LastReconfig: time.Time{},
|
||||
Message: "openbgpd up and running",
|
||||
Version: "",
|
||||
Backend: "openbgpd",
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// decodeNeighbor decodes a single neighbor in a
|
||||
// bgpctl response.
|
||||
func decodeNeighbor(n interface{}) (*api.Neighbour, error) {
|
||||
nb, ok := n.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("decode neighbor failed, interface is not a map")
|
||||
}
|
||||
|
||||
stats := decoders.MapGet(nb, "stats", map[string]interface{}{})
|
||||
prefixes := decoders.MapGet(stats, "prefixes", map[string]interface{}{})
|
||||
|
||||
neighbor := &api.Neighbour{
|
||||
Id: decoders.MapGetString(nb, "remote_addr", "invalid_id"),
|
||||
Address: decoders.MapGetString(nb, "remote_addr", "invalid_address"),
|
||||
Asn: decoders.IntFromString(decoders.MapGetString(nb, "remote_as", ""), -1),
|
||||
State: decodeState(decoders.MapGetString(nb, "state", "unknown")),
|
||||
Description: describeNeighbor(nb),
|
||||
RoutesReceived: int(decoders.MapGet(prefixes, "received", -1).(float64)),
|
||||
// TODO: RoutesFiltered
|
||||
RoutesExported: int(decoders.MapGet(prefixes, "sent", -1).(float64)),
|
||||
// TODO: RoutesPreferred
|
||||
// TODO: RoutesAccepted
|
||||
Uptime: decoders.DurationTimeframe(decoders.MapGet(nb, "last_updown", ""), 0),
|
||||
}
|
||||
return neighbor, nil
|
||||
}
|
||||
|
||||
// describeNeighbor creates a neighbor description
|
||||
func describeNeighbor(nb interface{}) string {
|
||||
addr := decoders.MapGetString(nb, "remote_addr", "invalid_address")
|
||||
asn := decoders.MapGetString(nb, "remote_as", "")
|
||||
return fmt.Sprintf("PEER AS%s %s", asn, addr)
|
||||
}
|
||||
|
||||
// decodeNeighbors retrievs neighbors data from
|
||||
// the bgpctl response.
|
||||
func decodeNeighbors(res map[string]interface{}) (api.Neighbours, error) {
|
||||
nbs := decoders.MapGet(res, "neighbors", nil)
|
||||
if nbs == nil {
|
||||
return nil, fmt.Errorf("missing neighbors in response body")
|
||||
}
|
||||
neighbors, ok := nbs.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no a list of neighbors")
|
||||
}
|
||||
all := make(api.Neighbours, 0, len(neighbors))
|
||||
for _, nb := range neighbors {
|
||||
n, err := decodeNeighbor(nb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, n)
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// decodeNeighborsStatus retrievs a neighbors summary
|
||||
// and decodes the status.
|
||||
func decodeNeighborsStatus(res map[string]interface{}) (api.NeighboursStatus, error) {
|
||||
nbs := decoders.MapGet(res, "neighbors", nil)
|
||||
if nbs == nil {
|
||||
return nil, fmt.Errorf("missing neighbors in response body")
|
||||
}
|
||||
neighbors, ok := nbs.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no a list of interfaces")
|
||||
}
|
||||
|
||||
all := make(api.NeighboursStatus, 0, len(neighbors))
|
||||
for _, nb := range neighbors {
|
||||
status := decodeNeighborStatus(nb)
|
||||
all = append(all, status)
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// decodeNeighborStatus decodes a single status from a
|
||||
// list of neighbor summaries.
|
||||
func decodeNeighborStatus(nb interface{}) *api.NeighbourStatus {
|
||||
id := decoders.MapGetString(nb, "bgpid", "undefined")
|
||||
state := decodeState(decoders.MapGetString(nb, "state", "Down"))
|
||||
uptime := decoders.DurationTimeframe(decoders.MapGet(nb, "last_updown", ""), 0)
|
||||
return &api.NeighbourStatus{
|
||||
Id: id,
|
||||
State: state,
|
||||
Since: uptime,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeRoutes decodes a response with a rib query.
|
||||
// The toplevel element is expected to be "rib".
|
||||
func decodeRoutes(res interface{}) (api.Routes, error) {
|
||||
r := decoders.MapGet(res, "rib", nil)
|
||||
if r == nil {
|
||||
// The response was a valid json but empty. So no
|
||||
// routes are present.
|
||||
return api.Routes{}, nil
|
||||
}
|
||||
rib, ok := r.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a list of interfaces")
|
||||
}
|
||||
routes := make(api.Routes, 0, len(rib))
|
||||
for _, details := range rib {
|
||||
route, err := decodeRoute(details.(map[string]interface{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes = append(routes, route)
|
||||
}
|
||||
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// decodeRoute decodes a single route received from the source
|
||||
func decodeRoute(details map[string]interface{}) (*api.Route, error) {
|
||||
prefix := decoders.MapGetString(details, "prefix", "")
|
||||
origin := decoders.MapGetString(details, "origin", "")
|
||||
neighbor := decoders.MapGet(details, "neighbor", nil)
|
||||
neighborID := "unknown"
|
||||
if neighbor != nil {
|
||||
neighborID = decoders.MapGetString(neighbor, "remote_addr", neighborID)
|
||||
}
|
||||
trueNextHop := decoders.MapGetString(details, "true_nexthop", "")
|
||||
lastUpdate := decoders.DurationTimeframe(
|
||||
decoders.MapGet(details, "last_update", nil), 0)
|
||||
|
||||
asPath := decodeASPath(decoders.MapGetString(details, "aspath", ""))
|
||||
localPref := int(decoders.MapGet(details, "localpref", 0).(float64))
|
||||
|
||||
// Decode BGP communities
|
||||
communities := decodeCommunities(
|
||||
decoders.MapGet(details, "communities", nil))
|
||||
largeCommunities := decodeCommunities(
|
||||
decoders.MapGet(details, "large_communities", nil))
|
||||
extendedCommunities := decodeExtendedCommunities(
|
||||
decoders.MapGet(details, "extended_communities", nil))
|
||||
|
||||
// Is preferred route
|
||||
isPrimary := decoders.MapGetBool(details, "best", false)
|
||||
|
||||
// Make bgp info
|
||||
bgpInfo := api.BgpInfo{
|
||||
Origin: origin,
|
||||
AsPath: asPath,
|
||||
NextHop: trueNextHop,
|
||||
Communities: communities,
|
||||
ExtCommunities: extendedCommunities,
|
||||
LargeCommunities: largeCommunities,
|
||||
LocalPref: localPref,
|
||||
}
|
||||
|
||||
r := &api.Route{
|
||||
Id: prefix,
|
||||
NeighbourId: neighborID,
|
||||
Network: prefix,
|
||||
Gateway: trueNextHop,
|
||||
Bgp: bgpInfo,
|
||||
Age: lastUpdate,
|
||||
Type: []string{origin},
|
||||
Primary: isPrimary,
|
||||
Details: api.Details(details),
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// decodeState will decode the state into a canonical form
|
||||
// used by the looking glass.
|
||||
func decodeState(s string) string {
|
||||
s = strings.ToLower(s) // todo elaborate
|
||||
return s
|
||||
}
|
||||
|
||||
// decodeASPath decodes a space separated list of
|
||||
// string encoded ASNs into a list of integers.
|
||||
func decodeASPath(path string) []int {
|
||||
tokens := strings.Split(path, " ")
|
||||
return decoders.IntListFromStrings(tokens)
|
||||
}
|
||||
|
||||
// decodeCommunities decodes communities into a list of
|
||||
// list of ints.
|
||||
func decodeCommunities(c interface{}) api.Communities {
|
||||
details := decoders.StringList(c)
|
||||
comms := make(api.Communities, 0, len(details))
|
||||
for _, com := range details {
|
||||
tokens := strings.Split(com, ":")
|
||||
comms = append(comms, decoders.IntListFromStrings(tokens))
|
||||
}
|
||||
return comms
|
||||
}
|
||||
|
||||
// decodeExtendedCommunities decodes extended communties
|
||||
// into a list of (str, int, int).
|
||||
func decodeExtendedCommunities(c interface{}) api.ExtCommunities {
|
||||
details := decoders.StringList(c)
|
||||
comms := make(api.ExtCommunities, 0, len(details))
|
||||
for _, com := range details {
|
||||
tokens := strings.SplitN(com, " ", 2)
|
||||
if len(tokens) != 2 {
|
||||
continue
|
||||
}
|
||||
nums := decoders.IntListFromStrings(
|
||||
strings.SplitN(tokens[1], ":", 2))
|
||||
comms = append(comms, []interface{}{tokens[0], nums[0], nums[1]})
|
||||
}
|
||||
return comms
|
||||
}
|
81
pkg/sources/openbgpd/decoders_test.go
Normal file
81
pkg/sources/openbgpd/decoders_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func readTestData(filename string) map[string]interface{} {
|
||||
data, _ := ioutil.ReadFile(filepath.Join("testdata", filename))
|
||||
payload := make(map[string]interface{})
|
||||
json.Unmarshal(data, &payload)
|
||||
return payload
|
||||
}
|
||||
|
||||
func TestDecodeAPIStatus(t *testing.T) {
|
||||
res := readTestData("status.json")
|
||||
s := decodeAPIStatus(res)
|
||||
t.Log(s.ServerTime)
|
||||
t.Log(s.LastReboot)
|
||||
}
|
||||
|
||||
func TestDecodeNeighbors(t *testing.T) {
|
||||
res := readTestData("show.neighbor.json")
|
||||
n, err := decodeNeighbors(res)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(n[0])
|
||||
}
|
||||
|
||||
func TestDecodeNeighborsStatus(t *testing.T) {
|
||||
res := readTestData("show.summary.json")
|
||||
n, err := decodeNeighborsStatus(res)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(n) != 3 {
|
||||
t.Error("unexpected length:", len(n))
|
||||
}
|
||||
t.Log(*n[0])
|
||||
}
|
||||
|
||||
func TestDecodeRoutes(t *testing.T) {
|
||||
res := readTestData("rib.json")
|
||||
routes, err := decodeRoutes(res)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 2 {
|
||||
t.Error("unexpected length:", len(routes))
|
||||
}
|
||||
|
||||
// Check first route
|
||||
r := routes[0]
|
||||
if r.Network != "23.42.1.0/24" {
|
||||
t.Error("unexpected network:", r.Network)
|
||||
}
|
||||
// Community decoding
|
||||
if r.Bgp.Communities[0][0] != 20119 {
|
||||
t.Error("unexpected community:", r.Bgp.Communities[0])
|
||||
}
|
||||
if r.Bgp.Communities[0][1] != 3 {
|
||||
t.Error("unexpected community:", r.Bgp.Communities[0])
|
||||
}
|
||||
if r.Bgp.ExtCommunities[1][0] != "rt" {
|
||||
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
|
||||
}
|
||||
if r.Bgp.ExtCommunities[1][1] != 65000 {
|
||||
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
|
||||
}
|
||||
if r.Bgp.ExtCommunities[1][2] != 11000 {
|
||||
t.Error("unexpected community:", r.Bgp.ExtCommunities[0])
|
||||
}
|
||||
|
||||
if r.Bgp.AsPath[0] != 1111 {
|
||||
t.Error("unexpected as_path:", r.Bgp.AsPath)
|
||||
}
|
||||
t.Log(r.Age)
|
||||
}
|
45
pkg/sources/openbgpd/filters.go
Normal file
45
pkg/sources/openbgpd/filters.go
Normal file
@ -0,0 +1,45 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
)
|
||||
|
||||
func filterReceivedRoutes(
|
||||
rejectCommunities api.Communities,
|
||||
routes api.Routes,
|
||||
) api.Routes {
|
||||
filtered := make(api.Routes, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
received := true
|
||||
for _, c := range rejectCommunities {
|
||||
if r.Bgp.HasLargeCommunity(c) {
|
||||
received = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if received {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterRejectedRoutes(
|
||||
rejectCommunities api.Communities,
|
||||
routes api.Routes,
|
||||
) api.Routes {
|
||||
filtered := make(api.Routes, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
rejected := false
|
||||
for _, c := range rejectCommunities {
|
||||
if r.Bgp.HasLargeCommunity(c) {
|
||||
rejected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if rejected {
|
||||
filtered = append(filtered, r)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
95
pkg/sources/openbgpd/filters_test.go
Normal file
95
pkg/sources/openbgpd/filters_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
)
|
||||
|
||||
func TestFilterReceivedRoutes(t *testing.T) {
|
||||
routes := api.Routes{
|
||||
&api.Route{
|
||||
Id: "1.2.3.4",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 666, 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
&api.Route{
|
||||
Id: "5.6.6.6",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 5, 42},
|
||||
api.Community{9999, 666, 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
&api.Route{
|
||||
Id: "5.6.7.8",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 5, 42},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
c := api.Communities{
|
||||
api.Community{9999, 666, 1},
|
||||
api.Community{9999, 666, 2},
|
||||
}
|
||||
filtered := filterReceivedRoutes(c, routes)
|
||||
|
||||
if filtered[0].Id != "5.6.7.8" {
|
||||
t.Error("unexpected route:", filtered[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterRejectedRoutes(t *testing.T) {
|
||||
routes := api.Routes{
|
||||
&api.Route{
|
||||
Id: "5.6.7.8",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 5, 42},
|
||||
},
|
||||
},
|
||||
},
|
||||
&api.Route{
|
||||
Id: "1.2.3.4",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 666, 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
&api.Route{
|
||||
Id: "5.6.6.6",
|
||||
Bgp: api.BgpInfo{
|
||||
LargeCommunities: api.Communities{
|
||||
api.Community{9999, 23, 23},
|
||||
api.Community{9999, 5, 42},
|
||||
api.Community{9999, 666, 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
c := api.Communities{
|
||||
api.Community{9999, 666, 1},
|
||||
api.Community{9999, 666, 2},
|
||||
}
|
||||
filtered := filterRejectedRoutes(c, routes)
|
||||
|
||||
if len(filtered) != 2 {
|
||||
t.Error("expected two filtered routes")
|
||||
}
|
||||
|
||||
if filtered[0].Id != "1.2.3.4" {
|
||||
t.Error("unexpected route:", filtered[0])
|
||||
}
|
||||
}
|
403
pkg/sources/openbgpd/state_server_source.go
Normal file
403
pkg/sources/openbgpd/state_server_source.go
Normal file
@ -0,0 +1,403 @@
|
||||
package openbgpd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alice-lg/alice-lg/pkg/api"
|
||||
"github.com/alice-lg/alice-lg/pkg/caches"
|
||||
"github.com/alice-lg/alice-lg/pkg/decoders"
|
||||
)
|
||||
|
||||
const (
|
||||
// StateServerSourceVersion is currently fixed at 1.0
|
||||
StateServerSourceVersion = "1.0"
|
||||
)
|
||||
|
||||
// StateServerSource implements the OpenBGPD source for Alice.
|
||||
// It is intendet to consume structured bgpctl output
|
||||
// queried over HTTP using the:
|
||||
//
|
||||
// openbgpd-state-server
|
||||
// https://github.com/alice-lg/openbgpd-state-server
|
||||
//
|
||||
type StateServerSource struct {
|
||||
// cfg is the source configuration retrieved
|
||||
// from the alice config file.
|
||||
cfg *Config
|
||||
|
||||
// Store the neighbor responses from the server here
|
||||
neighborsCache *caches.NeighborsCache
|
||||
|
||||
// Store the routes responses from the server
|
||||
// here identified by neighborID
|
||||
routesCache *caches.RoutesCache
|
||||
routesReceivedCache *caches.RoutesCache
|
||||
routesFilteredCache *caches.RoutesCache
|
||||
}
|
||||
|
||||
// NewStateServerSource creates a new source instance with a
|
||||
// configuration.
|
||||
func NewStateServerSource(cfg *Config) *StateServerSource {
|
||||
cacheDisabled := cfg.CacheTTL == 0
|
||||
|
||||
// Initialize caches
|
||||
nc := caches.NewNeighborsCache(cacheDisabled)
|
||||
rc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
rrc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
rfc := caches.NewRoutesCache(cacheDisabled, cfg.RoutesCacheSize)
|
||||
|
||||
return &StateServerSource{
|
||||
cfg: cfg,
|
||||
neighborsCache: nc,
|
||||
routesCache: rc,
|
||||
routesReceivedCache: rrc,
|
||||
routesFilteredCache: rfc,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpireCaches ... will flush the cache. Seriously this needs
|
||||
// a renaming.
|
||||
func (src *StateServerSource) ExpireCaches() int {
|
||||
totalExpired := src.routesCache.Expire()
|
||||
return totalExpired
|
||||
}
|
||||
|
||||
// Requests
|
||||
// ========
|
||||
|
||||
// StatusRequest makes status request from source
|
||||
func (src *StateServerSource) StatusRequest(ctx context.Context) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/v1/status")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowNeighborsRequest makes an all neighbors request
|
||||
func (src *StateServerSource) ShowNeighborsRequest(ctx context.Context) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/v1/bgpd/show/neighbor")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowNeighborsSummaryRequest builds an neighbors status request
|
||||
func (src *StateServerSource) ShowNeighborsSummaryRequest(
|
||||
ctx context.Context,
|
||||
) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/v1/bgpd/show/summary")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowNeighborRIBRequest retrives the routes accepted from the neighbor
|
||||
// identified by bgp-id.
|
||||
func (src *StateServerSource) ShowNeighborRIBRequest(
|
||||
ctx context.Context,
|
||||
neighborID string,
|
||||
) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/v1/bgpd/show/rib/neighbor/%s/detail", neighborID)
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// ShowRIBRequest makes a request for retrieving all routes imported
|
||||
// from all peers
|
||||
func (src *StateServerSource) ShowRIBRequest(ctx context.Context) (*http.Request, error) {
|
||||
url := src.cfg.APIURL("/v1/bgpd/show/rib/detail")
|
||||
return http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
}
|
||||
|
||||
// Datasource
|
||||
// ==========
|
||||
|
||||
// makeCacheStatus will create a new api status with cache infos
|
||||
func (src *StateServerSource) makeCacheStatus() api.ApiStatus {
|
||||
return api.ApiStatus{
|
||||
CacheStatus: api.CacheStatus{
|
||||
CachedAt: time.Now().UTC(),
|
||||
},
|
||||
Version: StateServerSourceVersion,
|
||||
ResultFromCache: false,
|
||||
Ttl: time.Now().UTC().Add(src.cfg.CacheTTL),
|
||||
}
|
||||
}
|
||||
|
||||
// Status returns an API status response. In our case
|
||||
// this is pretty much only that the service is available.
|
||||
func (src *StateServerSource) Status() (*api.StatusResponse, error) {
|
||||
// Make API request and read response. We do not cache the result.
|
||||
req, err := src.StatusRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := decodeAPIStatus(body)
|
||||
response := &api.StatusResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Status: status,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Neighbours retrievs a full list of all neighbors
|
||||
func (src *StateServerSource) Neighbours() (*api.NeighboursResponse, error) {
|
||||
// Query cache and see if we have a hit
|
||||
response := src.neighborsCache.Get()
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Make API request and read response
|
||||
req, err := src.ShowNeighborsRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nb, err := decodeNeighbors(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set route server id (sourceID) for all neighbors
|
||||
for _, n := range nb {
|
||||
n.RouteServerId = src.cfg.ID
|
||||
|
||||
rejectedRes, err := src.RoutesFiltered(n.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rejectCount := len(rejectedRes.Filtered)
|
||||
n.RoutesFiltered = rejectCount
|
||||
|
||||
}
|
||||
response = &api.NeighboursResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Neighbours: nb,
|
||||
}
|
||||
src.neighborsCache.Set(response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// NeighboursStatus retrives the status summary
|
||||
// for all neightbors
|
||||
func (src *StateServerSource) NeighboursStatus() (*api.NeighboursStatusResponse, error) {
|
||||
// Make API request and read response
|
||||
req, err := src.ShowNeighborsSummaryRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nb, err := decodeNeighborsStatus(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &api.NeighboursStatusResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Neighbours: nb,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Routes retrieves the routes for a specific neighbor
|
||||
// identified by ID.
|
||||
func (src *StateServerSource) Routes(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
src.routesCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesReceived returns the routes exported by the neighbor.
|
||||
func (src *StateServerSource) RoutesReceived(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesReceivedCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: api.Routes{},
|
||||
}
|
||||
src.routesReceivedCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesFiltered retrieves the routes filtered / not valid
|
||||
func (src *StateServerSource) RoutesFiltered(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := src.routesFilteredCache.Get(neighborID)
|
||||
if response != nil {
|
||||
response.Api.ResultFromCache = true
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Query RIB for routes received
|
||||
req, err := src.ShowNeighborRIBRequest(context.Background(), neighborID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response = &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: api.Routes{},
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
src.routesFilteredCache.Set(neighborID, response)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// RoutesNotExported retrievs the routes not exported
|
||||
// from the rs for a neighbor.
|
||||
func (src *StateServerSource) RoutesNotExported(neighborID string) (*api.RoutesResponse, error) {
|
||||
response := &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
|
||||
Imported: api.Routes{},
|
||||
NotExported: api.Routes{},
|
||||
Filtered: api.Routes{},
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// AllRoutes retrievs the entire RIB from the source. This is never
|
||||
// cached as it is processed by the store.
|
||||
func (src *StateServerSource) AllRoutes() (*api.RoutesResponse, error) {
|
||||
req, err := src.ShowRIBRequest(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read and decode response
|
||||
body, err := decoders.ReadJSONResponse(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := decodeRoutes(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filtered routes are marked with a large BGP community
|
||||
// as defined in the reject reasons.
|
||||
received := filterReceivedRoutes(src.cfg.RejectCommunities, routes)
|
||||
rejected := filterRejectedRoutes(src.cfg.RejectCommunities, routes)
|
||||
|
||||
response := &api.RoutesResponse{
|
||||
Api: src.makeCacheStatus(),
|
||||
Imported: received,
|
||||
NotExported: api.Routes{},
|
||||
Filtered: rejected,
|
||||
}
|
||||
return response, nil
|
||||
}
|
55
pkg/sources/openbgpd/testdata/rib.json
vendored
Normal file
55
pkg/sources/openbgpd/testdata/rib.json
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"rib": [
|
||||
{
|
||||
"prefix": "23.42.1.0/24",
|
||||
"aspath": "1111",
|
||||
"exit_nexthop": "200.100.25.7",
|
||||
"true_nexthop": "200.100.25.7",
|
||||
"neighbor": {
|
||||
"remote_addr": "200.100.25.7",
|
||||
"bgp_id": "200.100.25.7"
|
||||
},
|
||||
"valid": true,
|
||||
"source": "external",
|
||||
"ovs": "not-found",
|
||||
"origin": "IGP",
|
||||
"metric": 50,
|
||||
"localpref": 100,
|
||||
"weight": 0,
|
||||
"last_update": "05w3d19h",
|
||||
"communities": [
|
||||
"20119:3"
|
||||
],
|
||||
"large_communities": [
|
||||
"54987:1:1"
|
||||
],
|
||||
"extended_communities": [
|
||||
"[0] 11000:0",
|
||||
"rt 65000:11000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"prefix": "fc29:bac0:167::/48",
|
||||
"aspath": "1111",
|
||||
"exit_nexthop": "2212:100:f::1:1",
|
||||
"true_nexthop": "2212:100:f::1:1",
|
||||
"neighbor": {
|
||||
"remote_addr": "2212:100:f::1:1",
|
||||
"bgp_id": "172.11.111.1"
|
||||
},
|
||||
"valid": true,
|
||||
"source": "external",
|
||||
"ovs": "valid",
|
||||
"origin": "IGP",
|
||||
"metric": 0,
|
||||
"localpref": 100,
|
||||
"weight": 0,
|
||||
"last_update": "05w3d19h",
|
||||
"communities": [
|
||||
"13335:10167",
|
||||
"13335:19000",
|
||||
"13335:31000"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
224
pkg/sources/openbgpd/testdata/show.neighbor.json
vendored
Normal file
224
pkg/sources/openbgpd/testdata/show.neighbor.json
vendored
Normal file
@ -0,0 +1,224 @@
|
||||
{
|
||||
"neighbors": [
|
||||
{
|
||||
"remote_as": "123",
|
||||
"group": "clients",
|
||||
"remote_addr": "200.100.25.7",
|
||||
"bgpid": "206.100.25.7",
|
||||
"state": "Established",
|
||||
"last_updown": "01w0d16h",
|
||||
"config": {
|
||||
"template": false,
|
||||
"cloned": false,
|
||||
"passive": true,
|
||||
"down": false,
|
||||
"multihop": false,
|
||||
"max_prefix": 577,
|
||||
"max_prefix_restart": 15,
|
||||
"ttl_security": false,
|
||||
"holdtime": 0,
|
||||
"min_holdtime": 0,
|
||||
"announce_capabilities": true,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": true,
|
||||
"multiprotocol": [
|
||||
"IPv4 unicast"
|
||||
],
|
||||
"graceful_restart": {
|
||||
"eor": true,
|
||||
"restart": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"last_read": "00:00:08",
|
||||
"last_write": "00:00:08",
|
||||
"prefixes": {
|
||||
"sent": 864,
|
||||
"received": 1
|
||||
},
|
||||
"message": {
|
||||
"sent": {
|
||||
"open": 1,
|
||||
"notifications": 0,
|
||||
"updates": 460,
|
||||
"keepalives": 22152,
|
||||
"route_refresh": 0,
|
||||
"total": 22613
|
||||
},
|
||||
"received": {
|
||||
"open": 1,
|
||||
"notifications": 0,
|
||||
"updates": 2,
|
||||
"keepalives": 22158,
|
||||
"route_refresh": 0,
|
||||
"total": 22161
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"sent": {
|
||||
"updates": 897,
|
||||
"withdraws": 7,
|
||||
"eor": 0
|
||||
},
|
||||
"received": {
|
||||
"updates": 1,
|
||||
"withdraws": 0,
|
||||
"eor": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"holdtime": 90,
|
||||
"keepalive": 30,
|
||||
"local": {
|
||||
"address": "206.126.225.254",
|
||||
"port": 179,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": true,
|
||||
"multiprotocol": [
|
||||
"IPv4 unicast"
|
||||
],
|
||||
"graceful_restart": {
|
||||
"eor": true,
|
||||
"restart": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"address": "206.126.225.7",
|
||||
"port": 37237,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": false,
|
||||
"multiprotocol": [
|
||||
"IPv4 unicast",
|
||||
"IPv6 unicast",
|
||||
"IPv4 vpn",
|
||||
"IPv6 vpn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": false,
|
||||
"multiprotocol": [
|
||||
"IPv4 unicast"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"remote_as": "12346",
|
||||
"group": "clients",
|
||||
"remote_addr": "2000:1:2::3",
|
||||
"bgpid": "1.2.3.4",
|
||||
"state": "Established",
|
||||
"last_updown": "01w0d16h",
|
||||
"config": {
|
||||
"template": false,
|
||||
"cloned": false,
|
||||
"passive": true,
|
||||
"down": false,
|
||||
"multihop": false,
|
||||
"max_prefix": 747,
|
||||
"max_prefix_restart": 15,
|
||||
"ttl_security": false,
|
||||
"holdtime": 0,
|
||||
"min_holdtime": 0,
|
||||
"announce_capabilities": true,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": true,
|
||||
"multiprotocol": [
|
||||
"IPv6 unicast"
|
||||
],
|
||||
"graceful_restart": {
|
||||
"eor": true,
|
||||
"restart": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"last_read": "00:00:08",
|
||||
"last_write": "00:00:07",
|
||||
"prefixes": {
|
||||
"sent": 127,
|
||||
"received": 0
|
||||
},
|
||||
"message": {
|
||||
"sent": {
|
||||
"open": 1,
|
||||
"notifications": 0,
|
||||
"updates": 133,
|
||||
"keepalives": 22155,
|
||||
"route_refresh": 0,
|
||||
"total": 22289
|
||||
},
|
||||
"received": {
|
||||
"open": 1,
|
||||
"notifications": 0,
|
||||
"updates": 1,
|
||||
"keepalives": 22158,
|
||||
"route_refresh": 0,
|
||||
"total": 22160
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"sent": {
|
||||
"updates": 131,
|
||||
"withdraws": 2,
|
||||
"eor": 0
|
||||
},
|
||||
"received": {
|
||||
"updates": 0,
|
||||
"withdraws": 0,
|
||||
"eor": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"holdtime": 90,
|
||||
"keepalive": 30,
|
||||
"local": {
|
||||
"address": "fd42:1:2::3:4:5",
|
||||
"port": 179,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": true,
|
||||
"multiprotocol": [
|
||||
"IPv6 unicast"
|
||||
],
|
||||
"graceful_restart": {
|
||||
"eor": true,
|
||||
"restart": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"address": "fd42:1:2::3:4:6",
|
||||
"port": 43025,
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": false,
|
||||
"multiprotocol": [
|
||||
"IPv4 unicast",
|
||||
"IPv6 unicast",
|
||||
"IPv4 vpn",
|
||||
"IPv6 vpn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"as4byte": true,
|
||||
"refresh": false,
|
||||
"multiprotocol": [
|
||||
"IPv6 unicast"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
28
pkg/sources/openbgpd/testdata/show.summary.json
vendored
Normal file
28
pkg/sources/openbgpd/testdata/show.summary.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"neighbors": [
|
||||
{
|
||||
"remote_as": "12345",
|
||||
"group": "clients",
|
||||
"remote_addr": "200.100.50.25",
|
||||
"bgpid": "200.100.50.1",
|
||||
"state": "Established",
|
||||
"last_updown": "5d20h05m"
|
||||
},
|
||||
{
|
||||
"remote_as": "12345",
|
||||
"group": "clients",
|
||||
"remote_addr": "fd23:42:1::23:1",
|
||||
"bgpid": "100.200.50.25",
|
||||
"state": "Active",
|
||||
"last_updown": "01w2d15h"
|
||||
},
|
||||
{
|
||||
"remote_as": "62993",
|
||||
"group": "clients",
|
||||
"remote_addr": "200.100.50.25",
|
||||
"bgpid": "200.100.50.1",
|
||||
"state": "None",
|
||||
"last_updown": "10:11:12"
|
||||
}
|
||||
]
|
||||
}
|
1
pkg/sources/openbgpd/testdata/status.json
vendored
Normal file
1
pkg/sources/openbgpd/testdata/status.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"service":"openbgpd-state-server","version":"HEAD","build":"df4a695","server_time_utc":"2021-03-23T06:15:44.563675318Z","server_uptime":20574589140}
|
Loading…
x
Reference in New Issue
Block a user