Skip to content

Commit adfd38d

Browse files
committed
Add first iteration of YAML based router definitions
Add healthchecks
1 parent d749817 commit adfd38d

File tree

16 files changed

+382
-52
lines changed

16 files changed

+382
-52
lines changed

Dockerfile

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ RUN cd protobuf \
1515

1616
FROM golang:alpine AS go-builder
1717
ENV CGO_ENABLED=0
18+
ENV VERSION=untracked
1819
WORKDIR /opt
1920
COPY server /opt/server
2021
COPY go.mod go.sum main.go /opt/.
2122
COPY --from=buf-builder /opt/protobuf /opt/protobuf
2223
COPY --from=node-builder /opt/dist /opt/dist
2324
RUN go mod tidy \
24-
&& go build -o /opt/looking-glass
25+
&& go build -ldflags="-X gitlab.as203038.net/AS203038/looking-glass/server/utils.release=${VERSION}" -o /opt/looking-glass
2526

2627
FROM scratch
2728
WORKDIR /

example.config.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ devices: # List
1111
vrf: "vrf1" # VRF name, most platforms use 'default' if no VRF is used (required)
1212

1313
grpc: # gRPC Server Settings
14+
enabled: true # Enable or disable GRPC endpoints
1415
listen: ":8080" # gRPC listener
1516
tls: # TLS Settings often required for h2c
1617
enabled: false # Enable or disable TLS
@@ -23,6 +24,7 @@ web: # WebU
2324
grpc_url: "" # URI of the GRCP server; uses current host as viewed by the browser if not set (optional)
2425
theme: "skeleton" # Theme, possible options are: skeleton, wintry, modern, rocket, seafoam, vintage, sahara, hamlindingo, gold-nouveau, crimson (ref https://www.skeleton.dev/docs/themes)
2526
title: "Example Web Page" # Freetext web page title (recommended)
27+
rt_list_max: 4 # Maximum number of devices to display as list before grouping by location (optional, defaults to 4, use -1 to force grouping by location)
2628
header: # Header navbar definitions (recommended)
2729
text: "Welcome to Example Website" # Freetext header text, will be centered (recommended)
2830
logo: "/path/to/logo" # Header logo path (optional)

protobuf/lookingglass/v0/lookingglass.proto

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
syntax = "proto3";
44

55
package lookingglass.v0;
6+
import "google/protobuf/empty.proto";
7+
import "google/protobuf/timestamp.proto";
68

79
option go_package = "gitlab.as203038.net/AS203038/looking-glass/protobuf/lookingglass/v0;lookingglass";
810
option java_multiple_files = true;
@@ -11,6 +13,7 @@ option java_package = "lookingglass.v0";
1113

1214
// Data Streaming Service
1315
service LookingGlassService {
16+
rpc GetInfo(google.protobuf.Empty) returns (GetInfoResponse) {}
1417
rpc GetRouters(GetRoutersRequest) returns (GetRoutersResponse) {}
1518
rpc Ping(PingRequest) returns (PingResponse) {}
1619
rpc Traceroute(TracerouteRequest) returns (TracerouteResponse) {}
@@ -20,6 +23,13 @@ service LookingGlassService {
2023
rpc BGPASPath(BGPASPathRequest) returns (BGPASPathResponse) {}
2124
}
2225

26+
message RouterHealth {
27+
// Time Checked
28+
google.protobuf.Timestamp timestamp = 1;
29+
// Status
30+
bool healthy = 2;
31+
}
32+
2333
// Router is a router.
2434
message Router {
2535
// The ID of the router.
@@ -28,6 +38,8 @@ message Router {
2838
string name = 2;
2939
// The Location of the router.
3040
string location = 4;
41+
// Health of the router.
42+
RouterHealth health = 5;
3143
}
3244

3345
// BGPCommunity is a BGP community.g
@@ -38,6 +50,14 @@ message BGPCommunity {
3850
int32 value = 2;
3951
}
4052

53+
// GetInfoResponse is the response message for GetInfo.
54+
message GetInfoResponse {
55+
// The hostname of the service.
56+
string hostname = 1;
57+
// The version of the service.
58+
string version = 2;
59+
}
60+
4161
// GetRoutersRequest is the request message for GetRouters.
4262
message GetRoutersRequest {
4363
// The number of routers to return.

server/errs/router.go

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import (
77
var (
88
UnknownRouter = errors.New("router unknown")
99
RouterUnavailable = errors.New("router unavailable")
10+
OperationUnknown = errors.New("operation unknown")
1011
)

server/http/grpc/grpc.go

+27
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,31 @@ func Mux(ctx context.Context, mux *http.ServeMux, rts utils.RouterMap) {
5656
interceptors := connect.WithInterceptors(NewLogInterceptor())
5757
mux.Handle(lookingglassconnect.NewLookingGlassServiceHandler(NewLookingGlassService(ctx, rts), interceptors))
5858
mux.Handle(grpchealth.NewHandler(Health))
59+
Health.SetStatus(lookingglassconnect.LookingGlassServiceName, grpchealth.StatusServing)
60+
go healthcheck(ctx, rts)
61+
}
62+
63+
func healthcheck(ctx context.Context, rts utils.RouterMap) {
64+
ticker := time.NewTicker(time.Minute)
65+
for {
66+
select {
67+
case <-ctx.Done():
68+
return
69+
case <-ticker.C:
70+
for _, r := range rts {
71+
o := r.HealthCheck.Healthy
72+
if err := r.Healthcheck(); err == nil {
73+
if !o {
74+
Health.SetStatus(lookingglassconnect.LookingGlassServiceName+"/"+r.Config.Name, grpchealth.StatusServing)
75+
log.Printf("Router %s is healthy", r.Config.Name)
76+
}
77+
} else {
78+
if o {
79+
Health.SetStatus(lookingglassconnect.LookingGlassServiceName+"/"+r.Config.Name, grpchealth.StatusNotServing)
80+
log.Printf("Router %s is unhealthy: %s", r.Config.Name, err)
81+
}
82+
}
83+
}
84+
}
85+
}
5986
}

server/http/grpc/service.go

+21
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package grpc
22

33
import (
44
"context"
5+
"os"
56
"strconv"
67
"strings"
78

@@ -10,6 +11,8 @@ import (
1011
"gitlab.as203038.net/AS203038/looking-glass/protobuf/lookingglass/v0/lookingglassconnect"
1112
"gitlab.as203038.net/AS203038/looking-glass/server/errs"
1213
"gitlab.as203038.net/AS203038/looking-glass/server/utils"
14+
emptypb "google.golang.org/protobuf/types/known/emptypb"
15+
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
1316
)
1417

1518
type LookingGlassService struct {
@@ -25,6 +28,17 @@ func NewLookingGlassService(ctx context.Context, rts utils.RouterMap) lookinggla
2528
}
2629
}
2730

31+
func (s *LookingGlassService) GetInfo(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[pb.GetInfoResponse], error) {
32+
h, err := os.Hostname()
33+
if err != nil {
34+
h = "unknown"
35+
}
36+
return connect.NewResponse(&pb.GetInfoResponse{
37+
Hostname: h,
38+
Version: utils.Version(),
39+
}), nil
40+
}
41+
2842
func (s *LookingGlassService) GetRouters(ctx context.Context, req *connect.Request[pb.GetRoutersRequest]) (*connect.Response[pb.GetRoutersResponse], error) {
2943
var ret []*pb.Router
3044
lim := req.Msg.GetLimit()
@@ -49,6 +63,13 @@ func (s *LookingGlassService) GetRouters(ctx context.Context, req *connect.Reque
4963
Name: v.Config.Name,
5064
Location: v.Config.Location,
5165
Id: int64(k + 1),
66+
Health: &pb.RouterHealth{
67+
Healthy: v.HealthCheck.Healthy,
68+
Timestamp: &timestamppb.Timestamp{
69+
Seconds: v.HealthCheck.Checked.Unix(),
70+
Nanos: int32(v.HealthCheck.Checked.Nanosecond()),
71+
},
72+
},
5273
})
5374
}
5475
var nextPage uint32

server/http/http.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ func SecurityTxtInjector(cfg utils.SecurityTxtConfig) http.Handler {
7777

7878
func ListenAndServe(ctx context.Context, cfg *utils.Config, rts utils.RouterMap, webfs fs.FS) error {
7979
mux := http.NewServeMux()
80-
grpc.Mux(ctx, mux, rts)
80+
if cfg.Grpc.Enabled {
81+
grpc.Mux(ctx, mux, rts)
82+
}
8183
if cfg.SecurityTxt.Enabled {
8284
mux.Handle("/.well-known/security.txt", HTTPHandler(SecurityTxtInjector(cfg.SecurityTxt)))
8385
}

server/http/webui/webui.go

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type EnvJS struct {
1717
FooterLinks string `json:"PUBLIC_FOOTER_LINKS"`
1818
FooterLogo string `json:"PUBLIC_FOOTER_LOGO"`
1919
GrpcURL string `json:"PUBLIC_GRPC_URL"`
20+
LGVersion string `json:"PUBLIC_LG_VERSION"`
21+
RtListMax int `json:"PUBLIC_RT_LIST_MAX"`
2022
}
2123

2224
func ConfigInjector(cfg utils.WebConfig) http.Handler {
@@ -30,6 +32,8 @@ func ConfigInjector(cfg utils.WebConfig) http.Handler {
3032
FooterLinks: cfg.Footer.LinksString(),
3133
FooterLogo: cfg.Footer.Logo,
3234
GrpcURL: cfg.GrpcURL,
35+
LGVersion: utils.Version(),
36+
RtListMax: cfg.RtListMax,
3337
})
3438
if err != nil {
3539
panic(err)

server/routers/frrouting.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: frrouting
2+
3+
ping:
4+
# any:
5+
# - ping -n -c5 {{.IP.IP}}
6+
ipv4:
7+
- ping -n -4 -c5 -I {{.Cfg.Source4.IP}} {{.IP.IP}}
8+
ipv6:
9+
- ping -n -6 -c5 -I {{.Cfg.Source6.IP}} {{.IP.IP}}
10+
11+
traceroute:
12+
# any:
13+
# - traceroute -w 1 -q1 -I --back --mtu -e {{.IP.IP}}
14+
ipv4:
15+
- traceroute -4 -w 1 -q1 -I --back --mtu -e -s {{.Cfg.Source4.IP}} {{.IP.IP}}
16+
ipv6:
17+
- traceroute -6 -w 1 -q1 -I --back --mtu -e -s {{.Cfg.Source6.IP}} {{.IP.IP}}
18+
19+
bgp:
20+
route:
21+
- vtysh -c 'show bgp vrf {{.Cfg.VRF}} {{.IP.Family}} unicast {{.IP.IP}}'
22+
community:
23+
- vtysh -c 'show bgp vrf {{.Cfg.VRF}} ipv4 unicast community {{.Community}}'
24+
- vtysh -c 'show bgp vrf {{.Cfg.VRF}} ipv6 unicast community {{.Community}}'
25+
aspath:
26+
- vtysh -c 'show bgp vrf {{.Cfg.VRF}} ipv4 unicast regexp {{.ASPath}}'
27+
- vtysh -c 'show bgp vrf {{.Cfg.VRF}} ipv6 unicast regexp {{.ASPath}}'
28+

server/routers/routers.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ func CreateRouterMap(cfg *utils.Config) utils.RouterMap {
3333
log.Printf("Router Type %s not found (%s)\n", v.Type, v.Name)
3434
continue
3535
}
36-
rm = append(rm, utils.RouterInstance{
37-
Config: &v,
38-
Router: Get(v.Type),
39-
})
36+
ri := &utils.RouterInstance{
37+
Config: &v,
38+
Router: Get(v.Type),
39+
HealthCheck: &utils.HealthCheck{},
40+
}
41+
go ri.Healthcheck()
42+
rm = append(rm, ri)
4043
}
4144
return rm
4245
}

server/routers/yaml.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package routers
2+
3+
import (
4+
"bytes"
5+
"embed"
6+
"fmt"
7+
"text/template"
8+
9+
"gitlab.as203038.net/AS203038/looking-glass/server/errs"
10+
"gitlab.as203038.net/AS203038/looking-glass/server/utils"
11+
yaml "gopkg.in/yaml.v2"
12+
)
13+
14+
type _tpl_data struct {
15+
Cfg *utils.RouterConfig
16+
IP *utils.IPNet
17+
Community string
18+
ASPath string
19+
}
20+
21+
type Yaml struct {
22+
Path string
23+
Template struct {
24+
Name string `yaml:"name"`
25+
Ping struct {
26+
Any []string `yaml:"any"`
27+
IPv4 []string `yaml:"ipv4"`
28+
IPv6 []string `yaml:"ipv6"`
29+
} `yaml:"ping"`
30+
Traceroute struct {
31+
Any []string `yaml:"any"`
32+
IPv4 []string `yaml:"ipv4"`
33+
IPv6 []string `yaml:"ipv6"`
34+
} `yaml:"traceroute"`
35+
BGP struct {
36+
Route []string `yaml:"route"`
37+
Community []string `yaml:"community"`
38+
ASPath []string `yaml:"aspath"`
39+
} `yaml:"bgp"`
40+
}
41+
}
42+
43+
//go:embed all:*.yml
44+
var compiledRouters embed.FS
45+
46+
func init() {
47+
// Load all routers
48+
files, err := compiledRouters.ReadDir(".")
49+
if err != nil {
50+
panic(err)
51+
}
52+
for _, file := range files {
53+
y := &Yaml{}
54+
y.Path = file.Name()
55+
yamlFile, err := compiledRouters.ReadFile(y.Path)
56+
if err != nil {
57+
panic(err)
58+
}
59+
err = yaml.Unmarshal(yamlFile, &y.Template)
60+
if err != nil {
61+
panic(err)
62+
}
63+
register(y.Template.Name, y)
64+
fmt.Printf("Router %s registered\n", y.Template.Name)
65+
}
66+
}
67+
68+
func (rt *Yaml) _tpl(name string, data _tpl_data) ([]string, error) {
69+
var tpl []string
70+
var ret []string
71+
switch name {
72+
case "ping":
73+
if rt.Template.Ping.Any != nil {
74+
tpl = rt.Template.Ping.Any
75+
} else if data.IP.IsIPv4() {
76+
tpl = rt.Template.Ping.IPv4
77+
} else if data.IP.IsIPv6() {
78+
tpl = rt.Template.Ping.IPv6
79+
}
80+
case "traceroute":
81+
if rt.Template.Traceroute.Any != nil {
82+
tpl = rt.Template.Traceroute.Any
83+
} else if data.IP.IsIPv4() {
84+
tpl = rt.Template.Traceroute.IPv4
85+
} else if data.IP.IsIPv6() {
86+
tpl = rt.Template.Traceroute.IPv6
87+
}
88+
case "bgp.route":
89+
tpl = rt.Template.BGP.Route
90+
case "bgp.community":
91+
tpl = rt.Template.BGP.Community
92+
case "bgp.aspath":
93+
tpl = rt.Template.BGP.ASPath
94+
}
95+
if tpl == nil {
96+
return nil, errs.OperationUnknown
97+
}
98+
for _, t := range tpl {
99+
var buf bytes.Buffer
100+
tt, err := template.New(t).Parse(t)
101+
if err != nil {
102+
return nil, err
103+
}
104+
err = tt.Execute(&buf, data)
105+
if err != nil {
106+
return nil, err
107+
}
108+
ret = append(ret, buf.String())
109+
}
110+
return ret, nil
111+
}
112+
113+
func (rt *Yaml) Ping(cfg *utils.RouterConfig, ip *utils.IPNet) ([]string, error) {
114+
return rt._tpl("ping", _tpl_data{Cfg: cfg, IP: ip})
115+
}
116+
117+
func (rt *Yaml) Traceroute(cfg *utils.RouterConfig, ip *utils.IPNet) ([]string, error) {
118+
return rt._tpl("traceroute", _tpl_data{Cfg: cfg, IP: ip})
119+
}
120+
121+
func (rt *Yaml) BGPRoute(cfg *utils.RouterConfig, ip *utils.IPNet) ([]string, error) {
122+
return rt._tpl("bgp.route", _tpl_data{Cfg: cfg, IP: ip})
123+
}
124+
125+
func (rt *Yaml) BGPCommunity(cfg *utils.RouterConfig, community string) ([]string, error) {
126+
return rt._tpl("bgp.community", _tpl_data{Cfg: cfg, Community: community})
127+
}
128+
129+
func (rt *Yaml) BGPASPath(cfg *utils.RouterConfig, aspath string) ([]string, error) {
130+
return rt._tpl("bgp.aspath", _tpl_data{Cfg: cfg, ASPath: aspath})
131+
}

0 commit comments

Comments
 (0)