initial commit
This commit is contained in:
parent
3d9c96d01a
commit
df00939d79
12
StaticRoot/bootstrap.min.css
vendored
Normal file
12
StaticRoot/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
StaticRoot/dn42_logo.png
Normal file
BIN
StaticRoot/dn42_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
228
StaticRoot/grc.js
Normal file
228
StaticRoot/grc.js
Normal file
@ -0,0 +1,228 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 Realtime GRC
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const ExplorerURL='https://explorer.burble.com/#'
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// smaller display components
|
||||
|
||||
Vue.component('reg-asn', {
|
||||
template: '#reg-asn',
|
||||
props: [ 'asn' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
computed: {
|
||||
normal: function() {
|
||||
if (this.asn.startsWith("AS")) {
|
||||
return this.asn;
|
||||
} else {
|
||||
return "AS" + this.asn;
|
||||
}
|
||||
},
|
||||
url: function() {
|
||||
return ExplorerURL + "/aut-num/" + this.normal;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Vue.component('reg-path', {
|
||||
template: '#reg-path',
|
||||
props: [ 'path' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
computed: {
|
||||
list: function() {
|
||||
return this.path.split(" ");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Vue.component('reg-prefix', {
|
||||
template: '#reg-prefix',
|
||||
props: [ 'prefix' ],
|
||||
data() {
|
||||
return { }
|
||||
},
|
||||
computed: {
|
||||
isIPv6: function() {
|
||||
return this.prefix.includes(":")
|
||||
},
|
||||
forced: function() { return this.prefix },
|
||||
url: function() {
|
||||
var prefix = this.prefix.replace("/", "_")
|
||||
if (this.isIPv6) {
|
||||
return ExplorerURL + "/inet6num/" + prefix;
|
||||
}
|
||||
else {
|
||||
return ExplorerURL + "/inetnum/" + prefix;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// flap list component
|
||||
|
||||
Vue.component('app-flaps', {
|
||||
template: '#app-flaps-template',
|
||||
data() {
|
||||
return {
|
||||
filter: '',
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
fields: [
|
||||
{
|
||||
key: 'prefix',
|
||||
label: 'Prefix',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'path',
|
||||
label: 'Path',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'count',
|
||||
label: 'Updates',
|
||||
sortable: true,
|
||||
sortDirection: 'desc'
|
||||
}
|
||||
],
|
||||
flaps: [ ]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rows: function() {
|
||||
return this.flaps.length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function(data) {
|
||||
this.flaps.splice(0);
|
||||
|
||||
this.total = data.total;
|
||||
this.flaps = data.list.map((update) => {
|
||||
var tmp = update.path.split(" ");
|
||||
update.path = tmp.map((asn) => {
|
||||
return "AS" + asn;
|
||||
}).join(" ")
|
||||
return update;
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('flaps-update', data => {
|
||||
this.update(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// roa list component
|
||||
|
||||
Vue.component('app-roa', {
|
||||
template: '#app-roa-template',
|
||||
data() {
|
||||
return {
|
||||
filter4: '',
|
||||
filter6: '',
|
||||
roaFields: [
|
||||
{
|
||||
key: 'prefix',
|
||||
label: 'Prefix',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
key: 'origin',
|
||||
label: 'Origin',
|
||||
sortable: true
|
||||
}
|
||||
],
|
||||
roa4: [ ],
|
||||
roa6: [ ]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update: function(data) {
|
||||
this.roa4.splice(0);
|
||||
this.roa6.splice(0);
|
||||
|
||||
data.list.forEach((roa) => {
|
||||
var nroa = {
|
||||
origin: "AS" + roa.origin,
|
||||
prefix: roa.prefix
|
||||
};
|
||||
if (roa.prefix.includes(":")) { this.roa6.push(nroa) }
|
||||
else { this.roa4.push(nroa) }
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('roa-update', data => {
|
||||
this.update(data)
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// timer to update every minute
|
||||
|
||||
Vue.component('app-timer', {
|
||||
template: '#app-timer',
|
||||
data() {
|
||||
return {
|
||||
seconds: 0,
|
||||
timer: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trigger: function() {
|
||||
if (this.seconds <= 0) {
|
||||
this.seconds = 61;
|
||||
|
||||
axios.get('/api/flaps').then(response => {
|
||||
this.$root.$emit('flaps-update', response.data)
|
||||
});
|
||||
|
||||
axios.get('/api/roa').then(response => {
|
||||
this.$root.$emit('roa-update', response.data)
|
||||
});
|
||||
|
||||
}
|
||||
this.seconds -= 1;
|
||||
this.timer = setTimeout(() => this.trigger(), 1000);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.trigger()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// main vue application starts here
|
||||
|
||||
// initialise the Vue Router
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{ path: '/', component: Vue.component('app-flaps') },
|
||||
{ path: '/flaps', component: Vue.component('app-flaps') },
|
||||
{ path: '/roa', component: Vue.component('app-roa') }
|
||||
]
|
||||
})
|
||||
|
||||
// and the main app instance
|
||||
const vm = new Vue({
|
||||
el: '#grc_realtime',
|
||||
data: { },
|
||||
router
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of code
|
222
StaticRoot/index.html
Normal file
222
StaticRoot/index.html
Normal file
@ -0,0 +1,222 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>DN42 GRC Realtime Data</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<!-- Styles -->
|
||||
<link type="text/css" rel="stylesheet" href="bootstrap.min.css">
|
||||
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
|
||||
<link type="text/css" rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<!-- Style overrides -->
|
||||
<style>
|
||||
.material-icons { display:inline-flex;vertical-align:middle }
|
||||
body { box-shadow: inset 0 2em 10em rgba(0,0,0,0.4); min-height: 100vh }
|
||||
</style>
|
||||
|
||||
<!-- Bootstrap/Vue JS -->
|
||||
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
|
||||
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/vue-router@latest/dist/vue-router.min.js"></script>
|
||||
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
|
||||
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
|
||||
|
||||
<!-- Other JS -->
|
||||
<script src="https://unpkg.com/axios@0.20.0/dist/axios.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="grc_realtime">
|
||||
|
||||
<!-- Top Navbar -->
|
||||
<nav class="navbar navbar-fixed-top navbar-expand-md navbar-dark bg-dark">
|
||||
<div id="navlinks" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto text=center">
|
||||
<li class="nav-item"><a href="/" class="nav-link">Realtime Data</a></li>
|
||||
<li class="nav-item"><a href="https://grc.burble.com" class="nav-link">GRC</a></li>
|
||||
<li class="nav-item"><a href="https://explorer.burble.com" class="nav-link">Registry</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse w-100 ml-auto text-nowrap">
|
||||
<div class="ml-auto"><router-link class="navbar-brand"
|
||||
to="/">Realtime GRC Data</router-link> <a class="pull-right navbar-brand"
|
||||
href="https://dn42.dev/"><img src="/dn42_logo.png" width="173" height="60"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Tabs for content -->
|
||||
<div class="clearfix">
|
||||
<app-timer class="float-right"></app-timer>
|
||||
<b-nav tabs>
|
||||
<b-nav-item to="flaps" exact exact-active-class="active">Flapping Routes</b-nav-item>
|
||||
<b-nav-item to="roa" exact exact-active-class="active">ROA Failures</b-nav-item>
|
||||
</b-nav>
|
||||
</div>
|
||||
|
||||
<!-- content is generated by the router -->
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<!-- footer -->
|
||||
<footer class="page-footer font-small">
|
||||
<div style="margin-top: 20px; padding: 5px">
|
||||
<a href="https://git.burble.com/burble.dn42/dn42grcd">Source Code</a>.
|
||||
Powered by
|
||||
<a href="https://getbootstrap.com/">Bootstrap</a>,
|
||||
<a href="https://vuejs.org">Vue.js</a>,
|
||||
<a href="https://github.com/axios/axios">axios</a>,
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
<!-- flap page template -->
|
||||
|
||||
<script type="text/x-template" id="app-flaps-template">
|
||||
<b-container fluid="sm">
|
||||
<b-row class="justify-content-center py-3">
|
||||
<h2>Most Updated Routes</h2>
|
||||
</b-row>
|
||||
<b-row class="px-3">
|
||||
<b-col>
|
||||
<b-table
|
||||
id="flaps-table"
|
||||
:fields="fields"
|
||||
:items="flaps"
|
||||
:filter="filter"
|
||||
:sort-by="'count'"
|
||||
:sort-desc="true"
|
||||
:per-page="20"
|
||||
>
|
||||
<template v-slot:cell(prefix)="data">
|
||||
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
|
||||
</template>
|
||||
<template v-slot:cell(path)="data">
|
||||
<reg-path v-bind:path="data.value"></reg-path>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-col>
|
||||
<b-col class="text-left pr-5">
|
||||
<p>Total Updates: {{ this.total }}</p>
|
||||
<p><b-pagination class="pb-4"
|
||||
v-model="currentPage"
|
||||
:total-rows="rows"
|
||||
:per-page="20"
|
||||
aria-controls="flaps-table"
|
||||
></b-pagination>
|
||||
</p>
|
||||
<p><b-input-group size="sm">
|
||||
<b-form-input
|
||||
v-model="filter"
|
||||
type="search"
|
||||
id="filterInput"
|
||||
placeholder="Type to filter results"
|
||||
debounce="200"
|
||||
></b-form-input>
|
||||
</b-input-group></p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</script>
|
||||
|
||||
|
||||
<!-- ROA page template -->
|
||||
|
||||
<script type="text/x-template" id="app-roa-template">
|
||||
<div>
|
||||
<b-container fluid="sm">
|
||||
<b-row class="justify-content-center py-3">
|
||||
<h2>IPv4 ROA Failures</h1>
|
||||
</b-row>
|
||||
<b-row class="px-3">
|
||||
<b-col>
|
||||
<b-table
|
||||
:fields="roaFields"
|
||||
:items="roa4"
|
||||
:filter="filter4"
|
||||
>
|
||||
<template v-slot:cell(prefix)="data">
|
||||
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
|
||||
</template>
|
||||
<template v-slot:cell(origin)="data">
|
||||
<reg-asn v-bind:asn="data.value"></reg-asn>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-col>
|
||||
<b-col class="text-left pr-5">
|
||||
<b-input-group size="sm">
|
||||
<b-form-input
|
||||
v-model="filter4"
|
||||
type="search"
|
||||
id="filterInput4"
|
||||
placeholder="Type to filter results"
|
||||
debounce="200"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
<b-container fluid="sm" class="mw-100">
|
||||
<b-row class="justify-content-center py-3">
|
||||
<h2>IPv6 ROA Failures</h1>
|
||||
</b-row>
|
||||
<b-row class="px-3">
|
||||
<b-col>
|
||||
<b-table
|
||||
:fields="roaFields"
|
||||
:items="roa6"
|
||||
:filter="filter6"
|
||||
>
|
||||
<template v-slot:cell(prefix)="data">
|
||||
<reg-prefix v-bind:prefix="data.value"></reg-prefix>
|
||||
</template>
|
||||
<template v-slot:cell(origin)="data">
|
||||
<reg-asn v-bind:asn="data.value"></reg-asn>
|
||||
</template>
|
||||
</b-table>
|
||||
</b-col>
|
||||
<b-col class="text-left pr-5">
|
||||
<b-input-group size="sm">
|
||||
<b-form-input
|
||||
v-model="filter6"
|
||||
type="search"
|
||||
id="filterInput6"
|
||||
placeholder="Type to filter results"
|
||||
debounce="200"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<!-- Element Templates -->
|
||||
|
||||
<script type="text/x-template" id="reg-asn">
|
||||
<b-link v-bind:href="url" v-text="normal"></b-link>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-prefix">
|
||||
<b-link v-bind:href="url" v-text="prefix"></b-link>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="reg-path">
|
||||
<ul class="text-nowrap">
|
||||
<li class="d-inline p-1" v-for="asn in list">
|
||||
<reg-asn v-bind:asn="asn"></reg-asn>
|
||||
</li>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="app-timer">
|
||||
<p class="p-1 pr-3">Update in {{ this.seconds }}s</p>
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="grc.js"></script>
|
||||
</body>
|
||||
</html>
|
284
data.go
Normal file
284
data.go
Normal file
@ -0,0 +1,284 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Define data structures
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data structures for route updates
|
||||
|
||||
type RouteData struct {
|
||||
paths map[string]uint
|
||||
}
|
||||
|
||||
type RouteInfo struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Path string `json:"path"`
|
||||
Count uint `json:"count"`
|
||||
}
|
||||
|
||||
type RouteSnapshot struct {
|
||||
cache []byte `json:"-"`
|
||||
|
||||
Total uint `json:"total"`
|
||||
Unique uint `json:"unique"`
|
||||
List []*RouteInfo `json:"list"`
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data structures for ROA failures
|
||||
|
||||
type ROAData struct {
|
||||
roas map[string]uint
|
||||
}
|
||||
|
||||
type ROAInfo struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Origin string `json:"origin"`
|
||||
}
|
||||
|
||||
type ROASnapshot struct {
|
||||
cache []byte `json:"-"`
|
||||
|
||||
Count uint `json:"count"`
|
||||
List []*ROAInfo `json:"list"`
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// summary structures
|
||||
|
||||
type ActiveData struct {
|
||||
route *RouteData
|
||||
roa *ROAData
|
||||
}
|
||||
|
||||
type SnapshotData struct {
|
||||
route *RouteSnapshot
|
||||
roa *ROASnapshot
|
||||
}
|
||||
|
||||
type DataStruct struct {
|
||||
shutdown context.CancelFunc
|
||||
|
||||
// sets for currently cellecting and previous data
|
||||
active *ActiveData
|
||||
snapshot *SnapshotData
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// initialise the data, and start snapshoting every minute
|
||||
|
||||
func StartData() *DataStruct {
|
||||
|
||||
data := &DataStruct{}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
data.shutdown = cancelFunc
|
||||
|
||||
data.active = data.newActiveData()
|
||||
data.snapshot = data.active.snapshot()
|
||||
|
||||
log.Info("Starting data updates")
|
||||
go data.swap(ctx)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (data *DataStruct) Shutdown() {
|
||||
data.shutdown()
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// snapshot the live data every 60 seconds
|
||||
|
||||
func (data *DataStruct) swap(ctx context.Context) {
|
||||
|
||||
// update data at one minute intervals
|
||||
ticker := time.NewTicker(time.Second * 60)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// shutdown
|
||||
log.Info("Stopping data updates")
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
// tiemr expired
|
||||
log.Debug("Data Update")
|
||||
|
||||
var upp *unsafe.Pointer
|
||||
var up unsafe.Pointer
|
||||
var oldp unsafe.Pointer
|
||||
|
||||
// swap in a new empty data set
|
||||
upp = (*unsafe.Pointer)(unsafe.Pointer(&data.active))
|
||||
up = unsafe.Pointer(data.newActiveData())
|
||||
oldp = atomic.SwapPointer(upp, up)
|
||||
|
||||
// generate the reporting snapshot
|
||||
active := (*ActiveData)(oldp)
|
||||
snapshot := active.snapshot()
|
||||
|
||||
// then swap in the new snapshot
|
||||
upp = (*unsafe.Pointer)(unsafe.Pointer(&data.snapshot))
|
||||
up = unsafe.Pointer(snapshot)
|
||||
atomic.SwapPointer(upp, up)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// data initialisation
|
||||
|
||||
func (data *DataStruct) newActiveData() *ActiveData {
|
||||
return &ActiveData{
|
||||
route: data.newRouteData(),
|
||||
roa: data.newROAData(),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *DataStruct) newRouteData() *RouteData {
|
||||
return &RouteData{
|
||||
paths: make(map[string]uint),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *DataStruct) newROAData() *ROAData {
|
||||
return &ROAData{
|
||||
roas: make(map[string]uint),
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// for updates
|
||||
|
||||
func (route *RouteData) add(path string) {
|
||||
route.paths[path] += 1
|
||||
}
|
||||
|
||||
func (roa *ROAData) add(path string) {
|
||||
roa.roas[path] += 1
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// snapshot the live data
|
||||
|
||||
// all data
|
||||
func (active *ActiveData) snapshot() *SnapshotData {
|
||||
return &SnapshotData{
|
||||
route: active.route.snapshot(),
|
||||
roa: active.roa.snapshot(),
|
||||
}
|
||||
}
|
||||
|
||||
// route data
|
||||
func (route *RouteData) snapshot() *RouteSnapshot {
|
||||
|
||||
count := uint(len(route.paths))
|
||||
|
||||
snapshot := &RouteSnapshot{
|
||||
Unique: count,
|
||||
List: make([]*RouteInfo, 0, count),
|
||||
}
|
||||
|
||||
var total uint
|
||||
|
||||
// populate the ROA list
|
||||
for key, value := range route.paths {
|
||||
space := strings.IndexByte(key, ' ')
|
||||
if space == -1 {
|
||||
log.WithFields(log.Fields{
|
||||
"data": key,
|
||||
}).Debug("Invalid route data")
|
||||
} else {
|
||||
info := &RouteInfo{
|
||||
Prefix: key[:space],
|
||||
Path: key[space+1:],
|
||||
Count: value,
|
||||
}
|
||||
total += value
|
||||
snapshot.List = append(snapshot.List, info)
|
||||
}
|
||||
}
|
||||
snapshot.Total = total
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// roa data
|
||||
func (roa *ROAData) snapshot() *ROASnapshot {
|
||||
|
||||
count := uint(len(roa.roas))
|
||||
|
||||
snapshot := &ROASnapshot{
|
||||
Count: count,
|
||||
List: make([]*ROAInfo, 0, count),
|
||||
}
|
||||
|
||||
// populate the ROA list
|
||||
for key, _ := range roa.roas {
|
||||
space := strings.IndexByte(key, ' ')
|
||||
if space == -1 {
|
||||
log.WithFields(log.Fields{
|
||||
"data": key,
|
||||
}).Debug("Invalid roa data")
|
||||
} else {
|
||||
info := &ROAInfo{
|
||||
Prefix: key[:space],
|
||||
Origin: key[space+1:],
|
||||
}
|
||||
snapshot.List = append(snapshot.List, info)
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// create and cache api responses
|
||||
|
||||
func (r *RouteSnapshot) ToJSON() []byte {
|
||||
if r.cache == nil {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Error("Failed to marshal route json")
|
||||
} else {
|
||||
r.cache = data
|
||||
}
|
||||
}
|
||||
return r.cache
|
||||
}
|
||||
|
||||
func (r *ROASnapshot) ToJSON() []byte {
|
||||
if r.cache == nil {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Error("Failed to marshal ROA json")
|
||||
} else {
|
||||
r.cache = data
|
||||
}
|
||||
}
|
||||
return r.cache
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of file
|
91
dn42grcd.go
Normal file
91
dn42grcd.go
Normal file
@ -0,0 +1,91 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// DN42 GRC Daemon
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
flag "github.com/spf13/pflag"
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// utility function to set the log level
|
||||
|
||||
func setLogLevel(levelStr string) {
|
||||
|
||||
if level, err := log.ParseLevel(levelStr); err != nil {
|
||||
// failed to set the level
|
||||
|
||||
// set a sensible default and, of course, log the error
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.WithFields(log.Fields{
|
||||
"loglevel": levelStr,
|
||||
"error": err,
|
||||
}).Error("Failed to set requested log level")
|
||||
|
||||
} else {
|
||||
|
||||
// set the requested level
|
||||
log.SetLevel(level)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// everything starts here
|
||||
|
||||
func main() {
|
||||
|
||||
// set a default log level, so that logging can be used immediately
|
||||
// the level will be overidden later on once the command line
|
||||
// options are loaded
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Info("DN42 GRC Daemon Starting")
|
||||
|
||||
var (
|
||||
logLevel = flag.StringP("LogLevel", "l", "Info", "Log level")
|
||||
socketPath = flag.StringP("SockPath", "p", "bird.sock", "Path to bird fifo")
|
||||
bindAddress = flag.StringP("BindAddress", "b", "[::]:8050", "Server bind address")
|
||||
staticRoot = flag.StringP("StaticRoot", "s", "StaticRoot", "Static page directory")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// now initialise logging properly based on the cmd line options
|
||||
setLogLevel(*logLevel)
|
||||
|
||||
// start the API server and begin ingesting data
|
||||
data := StartData()
|
||||
server := StartAPIServer(*bindAddress, data, *staticRoot)
|
||||
ingest := StartIngesting(*socketPath, data)
|
||||
|
||||
// graceful shutdown via SIGINT (^C)
|
||||
csig := make(chan os.Signal, 1)
|
||||
signal.Notify(csig, os.Interrupt)
|
||||
|
||||
// and block
|
||||
<-csig
|
||||
|
||||
log.Info("Server shutting down")
|
||||
|
||||
// deadline for server to shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10)
|
||||
defer cancel()
|
||||
|
||||
// shutdown the server and stop ingesting data
|
||||
ingest.Shutdown()
|
||||
server.Shutdown(ctx)
|
||||
data.Shutdown()
|
||||
|
||||
// nothing left to do
|
||||
log.Info("Shutdown complete, all done")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of file
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module burble.dn42/dn42grcd
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
)
|
14
go.sum
Normal file
14
go.sum
Normal file
@ -0,0 +1,14 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
157
ingest.go
Normal file
157
ingest.go
Normal file
@ -0,0 +1,157 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Ingest updates from the bird socket to the internal data structures
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type Ingest struct {
|
||||
shutdown context.CancelFunc
|
||||
socket *os.File
|
||||
data *DataStruct
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// initialise and start collecting data
|
||||
|
||||
func StartIngesting(sockPath string, data *DataStruct) *Ingest {
|
||||
|
||||
ingest := &Ingest{data: data}
|
||||
|
||||
// create cancellation context
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
ingest.shutdown = cancelFunc
|
||||
|
||||
// start reading from the socket
|
||||
go ingest.tail(ctx, sockPath)
|
||||
return ingest
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (ingest *Ingest) Shutdown() {
|
||||
|
||||
// signal that the tail function should close
|
||||
ingest.shutdown()
|
||||
|
||||
// and close the file to ensure it gets kicked out the read loop
|
||||
ingest.socket.Close()
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// continuously read lines from the socket until closed
|
||||
// the function will re-open the socket on failures
|
||||
|
||||
func (ingest *Ingest) tail(ctx context.Context, sockPath string) {
|
||||
|
||||
for {
|
||||
|
||||
// have I been cancelled ?
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("Ingestion tail cancelled")
|
||||
break
|
||||
default:
|
||||
}
|
||||
|
||||
// open the named pipe
|
||||
sock, err := os.OpenFile(sockPath, os.O_RDONLY, os.ModeNamedPipe)
|
||||
|
||||
if err != nil {
|
||||
// if an error occured, sleep for a bit and retry
|
||||
log.WithFields(log.Fields{
|
||||
"path": sockPath,
|
||||
}).Fatal("Failed to open fifo, pausing")
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
} else {
|
||||
|
||||
ingest.socket = sock
|
||||
log.WithFields(log.Fields{
|
||||
"path": sockPath,
|
||||
}).Info("Opened bird socket")
|
||||
|
||||
reader := bufio.NewReader(sock)
|
||||
|
||||
// read from the socket
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
// error occurred, break out the read loop
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
}).Warn("Error reading from socket")
|
||||
break
|
||||
}
|
||||
|
||||
// trim the trailing newline
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
|
||||
if len(line) > 3 {
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"line": line,
|
||||
}).Debug("Received line")
|
||||
|
||||
// action based on first two characters
|
||||
action := line[:2]
|
||||
// data is from char 3 because of the space
|
||||
data := line[3:]
|
||||
|
||||
switch action {
|
||||
case "!1":
|
||||
ingest.route(data)
|
||||
case "!2":
|
||||
ingest.roaFail(data)
|
||||
default:
|
||||
// ignore anything else
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ensure socket is closed
|
||||
ingest.socket.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ingest a route update
|
||||
|
||||
func (ingest *Ingest) route(data string) {
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"data": data,
|
||||
}).Debug("route update")
|
||||
|
||||
ingest.data.active.route.add(data)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// ingest an ROA Fail
|
||||
|
||||
func (ingest *Ingest) roaFail(data string) {
|
||||
|
||||
// update the datastructures
|
||||
log.WithFields(log.Fields{
|
||||
"data": data,
|
||||
}).Debug("roa fail")
|
||||
|
||||
ingest.data.active.roa.add(data)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of file
|
147
server.go
Normal file
147
server.go
Normal file
@ -0,0 +1,147 @@
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// API to serve the internal data structures
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
package main
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type APIServer struct {
|
||||
data *DataStruct
|
||||
server *http.Server
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// http request logger
|
||||
|
||||
func requestLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"URL": r.URL.String(),
|
||||
"Remote": r.RemoteAddr,
|
||||
}).Debug("HTTP Request")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// start the API servers
|
||||
|
||||
func StartAPIServer(bindAddress string, data *DataStruct,
|
||||
staticRoot string) *APIServer {
|
||||
|
||||
api := &APIServer{
|
||||
data: data,
|
||||
router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
// initialise http server
|
||||
api.server = &http.Server{
|
||||
Addr: bindAddress,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
Handler: api.router,
|
||||
}
|
||||
|
||||
// add the api
|
||||
api.router.Use(requestLogger)
|
||||
s := api.router.Methods("GET").PathPrefix("/api").Subrouter()
|
||||
s.HandleFunc("/flaps", api.handleFlaps)
|
||||
s.HandleFunc("/roa", api.handleROA)
|
||||
api.installStaticRoutes(staticRoot)
|
||||
|
||||
// run the server in a non-blocking goroutine
|
||||
log.WithFields(log.Fields{
|
||||
"BindAddress": bindAddress,
|
||||
}).Info("Starting server")
|
||||
|
||||
go func() {
|
||||
if err := api.server.ListenAndServe(); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"BindAddress": bindAddress,
|
||||
}).Fatal("Unable to start server")
|
||||
}
|
||||
}()
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// shutdown the API Server
|
||||
|
||||
func (api *APIServer) Shutdown(ctx context.Context) {
|
||||
api.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// handler funcs
|
||||
|
||||
func (api *APIServer) handleFlaps(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Write(api.data.snapshot.route.ToJSON())
|
||||
}
|
||||
|
||||
func (api *APIServer) handleROA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Write(api.data.snapshot.roa.ToJSON())
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// static route handler
|
||||
|
||||
func (api *APIServer) installStaticRoutes(staticPath string) {
|
||||
|
||||
// an empty path disables static route serving
|
||||
if staticPath == "" {
|
||||
log.Info("Disabling static route serving")
|
||||
return
|
||||
}
|
||||
|
||||
// validate that the staticPath exists
|
||||
stat, err := os.Stat(staticPath)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Unable to find static page directory")
|
||||
}
|
||||
|
||||
// and it is a directory
|
||||
if !stat.IsDir() {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"path": staticPath,
|
||||
}).Fatal("Static path is not a directory")
|
||||
}
|
||||
|
||||
// install a file server for the static route
|
||||
api.router.PathPrefix("/").Handler(http.StripPrefix("/",
|
||||
http.FileServer(http.Dir(staticPath)))).Methods("GET")
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"path": staticPath,
|
||||
}).Info("Static route installed")
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// end of file
|
Loading…
x
Reference in New Issue
Block a user