BirdLGGo: initial release

This commit is contained in:
James Lu 2021-10-18 00:30:19 -07:00
parent 7fdf95074c
commit ce56aeaf2b
10 changed files with 726 additions and 0 deletions

32
BirdLGGo/README.md Normal file
View File

@ -0,0 +1,32 @@
This is a [bird-lg-go](https://github.com/xddxdd/bird-lg-go/) API client for Limnoria.
## Configure
First configure the following variables:
- `plugins.birdlggo.lgserver` - point this to your looking glass server (e.g. `http://lg.highdef.dn42/`)
- `plugins.birdlggo.nodes` - a space separated list of nodes to query. These should use internal server names (as shown in request URLs to the frontend), not the display names that the frontend shows in the navigation bar.
## Usage
Currently this provides a slimmed down UI for traceroute and BIRD's `show route`:
### Show route
This will only show the first / preferred route, with an AS path for BGP routes.
```
21:58 <@jlu5> @showroute 172.20.66.67
21:58 <atlas> 172.20.229.114 -> 172.20.66.67: unicast via 192.168.88.113 on igp-us-chi01 [Type: BGP univ] [Pref: 100/165] [AS path: 76190 4242420101]
21:58 <atlas> 172.20.229.113 -> 172.20.66.67: unicast via 192.168.88.123 on igp-us-nyc02 [Type: BGP univ] [Pref: 100/108] [AS path: 4242422601 4242420101]
21:58 <atlas> 172.20.229.117 -> 172.20.66.67: unicast via 172.23.235.1 on dn42fsn-tbspace [Type: BGP univ] [Pref: 100] [AS path: 76190 4242420101]
```
### Traceroute
```
22:02 <@jlu5> @traceroute wiki.dn42
22:02 <atlas> 172.20.229.114 -> wiki.dn42: 33.310 ms | 172.20.229.122 172.20.129.165 169.254.64.2 169.254.64.2 172.23.0.80
22:02 <atlas> 172.20.229.113 -> wiki.dn42: 29.169 ms | 172.20.229.123 172.20.129.167 169.254.64.2 169.254.64.2 172.23.0.80
22:02 <atlas> 172.20.229.117 -> wiki.dn42: 4.907 ms | 172.20.129.169 169.254.64.2 169.254.64.2 172.23.0.80
```

72
BirdLGGo/__init__.py Normal file
View File

@ -0,0 +1,72 @@
###
# Copyright (c) 2021, James Lu
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * 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.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# 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 OWNER 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.
###
"""
BirdLGGo: API client (show route / traceroute) for Bird-lg-go
"""
import sys
import supybot
from supybot import world
# Use this for the version of this plugin.
__version__ = ""
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.authors.unknown
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = ''
from . import config
from . import plugin
from importlib import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
from . import parsebird, parsetrace
reload(parsebird)
reload(parsetrace)
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:

57
BirdLGGo/config.py Normal file
View File

@ -0,0 +1,57 @@
###
# Copyright (c) 2021, James Lu
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * 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.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# 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 OWNER 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.
###
from supybot import conf, registry
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('BirdLGGo')
except:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified themself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('BirdLGGo', True)
BirdLGGo = conf.registerPlugin('BirdLGGo')
conf.registerChannelValue(BirdLGGo, 'lgServer',
registry.String("", _("""Looking glass server to query, including the scheme (http(s)://)""")))
conf.registerChannelValue(BirdLGGo, 'nodes',
registry.SpaceSeparatedListOfStrings([], _("""List of nodes to query (space separated list)""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=120:

View File

@ -0,0 +1 @@
# Stub so local is a module, used for third-party modules

58
BirdLGGo/parsebird.py Normal file
View File

@ -0,0 +1,58 @@
import re
from dataclasses import dataclass, field
@dataclass
class BirdRouteResult:
prefix: str
protocol_name: str
route_preference: str
route_type: str
route_origin: str = field(default=None)
via: str = field(default=None)
bgp_as_path: list[str] = field(default_factory=list)
bgp_community: str = field(default=None)
bgp_large_community: str = field(default=None)
class BirdParseError(ValueError):
pass
_ROUTE_INFO_RE = re.compile(r'(?P<prefix>[0-9a-fA-F.:]+\/[0-9]+).*?(?P<type>[a-z]+) \[(?P<protocol_name>\w+).*?\] \* \((?P<preference>.*?)\)')
def parse_bird(text):
"""
Return details of the first route in bird's "show route ... all" output.
This is most useful if you specify a query that only returns one route - e.g. by writing "show route ... primary all"
"""
text = text.strip()
lines = text.splitlines()
if not text.startswith("Table"):
# Unexpected format - probably an error like "Route not found"
raise BirdParseError(text)
if len(lines) < 3:
raise BirdParseError("Not enough data (expected at least 3 lines, got %d" % len(lines))
m = _ROUTE_INFO_RE.match(lines[1])
if not m:
raise BirdParseError("Failed to match route info against regex")
result = BirdRouteResult(
prefix=m.group("prefix"),
protocol_name=m.group("protocol_name"),
route_preference=m.group("preference"),
route_type=m.group("type"),
# Most routes except unreachable etc. should specify an interface or IP next hop.
# For unreachable routes, this seems to fall back to a "Type:" field instead
via=None if "Type:" in lines[2] else lines[2].strip())
for line in lines[2:]:
parts = line.strip().split(" ", 1)
if parts[0] == "Type:":
result.route_origin = parts[1]
if parts[0] == "BGP.as_path:":
result.bgp_as_path = parts[1].split()
if parts[0] == "BGP.community:":
result.bgp_community = parts[1]
if parts[0] == "BGP.large_community:":
result.bgp_large_community = parts[1]
return result

102
BirdLGGo/parsebird_test.py Normal file
View File

@ -0,0 +1,102 @@
import unittest
from parsebird import parse_bird, BirdRouteResult, BirdParseError
class ParseBirdRouteTestCase(unittest.TestCase):
maxDiff = None
def testShowRouteBGP(self):
s = """Table master4:
172.20.0.53/32 unicast [jrb0001 2021-10-06 from fe80::119] * (100) [AS4242420119i]
via 172.20.1.10 on dn42sea-jrb0001
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242420119
BGP.next_hop: 172.20.1.10
BGP.local_pref: 300
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242420119, 2000, 10) (4242421080, 101, 44) (4242421080, 103, 114)
"""
self.assertEqual(
BirdRouteResult(
prefix="172.20.0.53/32",
protocol_name="jrb0001",
route_preference="100",
route_type="unicast",
route_origin="BGP univ",
via="via 172.20.1.10 on dn42sea-jrb0001",
bgp_as_path=["4242420119"],
bgp_community="(64511,1) (64511,24) (64511,34)",
bgp_large_community="(4242420119, 2000, 10) (4242421080, 101, 44) (4242421080, 103, 114)"
), parse_bird(s)
)
def testShowRouteBGP6(self):
s = """Table master6:
fd42:1145:1419:5::/64 unicast [ibgp_us_lax01 03:41:43.904 from fd86:bad:11b7:22::1] * (100/21) [AS4242422464i]
via fe80::122 on igp-us-lax01
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242421288 4242421306 4242421331 4242422464
BGP.next_hop: fd86:bad:11b7:22::1
BGP.local_pref: 150
BGP.community: (64511,1) (64511,24) (64511,33) (64511,44)
BGP.large_community: (207268, 1, 44) (4242421080, 101, 44) (4242421080, 103, 122) (4242422464, 1, 500)"""
self.assertEqual(
BirdRouteResult(
prefix="fd42:1145:1419:5::/64",
protocol_name="ibgp_us_lax01",
route_preference="100/21",
route_type="unicast",
route_origin="BGP univ",
via="via fe80::122 on igp-us-lax01",
bgp_as_path=['4242421288', '4242421306', '4242421331', '4242422464'],
bgp_community="(64511,1) (64511,24) (64511,33) (64511,44)",
bgp_large_community="(207268, 1, 44) (4242421080, 101, 44) (4242421080, 103, 122) (4242422464, 1, 500)"
), parse_bird(s)
)
def testShowStaticUnreachable(self):
s = """Table master4:
172.22.108.0/26 unreachable [static1 2021-09-23] * (200)
Type: static univ
"""
self.assertEqual(
BirdRouteResult(
prefix="172.22.108.0/26",
protocol_name="static1",
route_preference="200",
route_type="unreachable",
route_origin="static univ"
), parse_bird(s)
)
def testShowRouteBabel6(self):
s = """Table master6:
fd86:bad:11b7:53::1/128 unicast [int_babel 20:04:48.769] * (130/18) [00:00:00:00:ac:14:e5:7a]
via fe80::122 on igp-us-lax01
Type: Babel univ
Babel.metric: 18
Babel.router_id: 00:00:00:00:ac:14:e5:7a
"""
self.assertEqual(
BirdRouteResult(
prefix="fd86:bad:11b7:53::1/128",
protocol_name="int_babel",
route_preference="130/18",
route_type="unicast",
route_origin="Babel univ",
via="via fe80::122 on igp-us-lax01"
), parse_bird(s)
)
def testNetworkNotFound(self):
s = "Network not found"
self.assertRaises(BirdParseError, lambda: parse_bird(s))
def testSyntaxError(self):
s = "syntax error, unexpected CF_SYM_UNDEFINED, expecting IP4 or IP6 or VPN_RD or CF_SYM_KNOWN"
self.assertRaises(BirdParseError, lambda: parse_bird(s))
if __name__ == '__main__':
unittest.main()

50
BirdLGGo/parsetrace.py Normal file
View File

@ -0,0 +1,50 @@
import re
import sys
import subprocess
from dataclasses import dataclass, field
@dataclass
class TraceResult:
ips: list[str]
latency: str = field(default=None)
notes: list[str] = field(default_factory=list)
class TraceParseError(ValueError):
pass
# bird-lg-go enables DNS lookups and sends one query by default, but we should be a bit more flexible with what we accept
# This will grab IPs from whichever format is used, and report the first latency / error code of the last line if there are multiple queries
_TRACEROUTE_RE = re.compile(r'\s*\d+\s+(?P<line>(?:[^() ]+ \((?P<IPdns>[0-9a-fA-F.:]+)\)|(?P<IPbare>[0-9a-fA-F.:]+)).*? (?P<latency>[0-9.]+ ms( \![A-Za-z0-9]+)?)|\*)')
def parse_traceroute(text):
lines = text.strip().splitlines()
if len(lines) < 2 or not lines[1].lstrip().startswith("1"):
# Assume error condition if 2nd line doesn't start with "1" (first hop)
raise TraceParseError(' '.join(lines) or "traceroute returned empty output")
else:
ips = []
notes = []
latency = None
for line in lines[1:]:
if not line.strip():
continue
m = _TRACEROUTE_RE.match(line)
if not m:
notes.append(line)
continue
ips.append(m.group("IPdns") or m.group("IPbare") or m.group("line"))
latency = m.group("latency")
# bird-lg-go specific truncation
if "hops not responding" in ''.join(notes):
latency = None
return TraceResult(ips, latency, notes)
if __name__ == '__main__':
proc = subprocess.run(['traceroute', *sys.argv[1:]], encoding='utf-8', stdout=subprocess.PIPE)
print(parse_traceroute(proc.stdout))
if proc.returncode:
print("traceroute exited with code", proc.returncode)
sys.exit(proc.returncode)

188
BirdLGGo/parsetrace_test.py Normal file
View File

@ -0,0 +1,188 @@
import unittest
from parsetrace import parse_traceroute, TraceResult
class ParseTraceTestCase(unittest.TestCase):
maxDiff = None
def testTracerouteSingleQuery(self):
s = """traceroute to ipv6.google.com (2607:f8b0:4009:809::200e), 30 hops max, 80 byte packets
1 2605:4840:3::1 (2605:4840:3::1) 1.269 ms
2 2604:6600:2700:11::1 (2604:6600:2700:11::1) 0.442 ms
3 ce-0-7-0-2.r07.chcgil09.us.bb.gin.ntt.net (2001:418:0:5000::ec0) 1.083 ms
4 eqix-ch-200g-1.google.com (2001:504:0:4:0:1:5169:1) 62.029 ms
5 *
6 2001:4860:0:1::5737 (2001:4860:0:1::5737) 0.870 ms
7 ord37s33-in-x0e.1e100.net (2607:f8b0:4009:809::200e) 46.306 ms
"""
self.assertEqual(TraceResult(ips=[
"2605:4840:3::1",
"2604:6600:2700:11::1",
"2001:418:0:5000::ec0",
"2001:504:0:4:0:1:5169:1",
"*",
"2001:4860:0:1::5737",
"2607:f8b0:4009:809::200e"
], latency="46.306 ms"), parse_traceroute(s))
def testTracerouteMultiQuery(self):
s = """traceroute to 1.1.1.1 (1.1.1.1), 30 hops max, 60 byte packets
1 205.185.112.1 (205.185.112.1) 0.418 ms 0.360 ms 0.310 ms
2 172.18.0.29 (172.18.0.29) 0.648 ms 0.517 ms 0.405 ms
3 100ge3-2.core1.slc1.he.net (184.104.194.81) 20.144 ms 20.177 ms 20.161 ms
4 cloudflare.slix.net (149.112.13.27) 9.057 ms 9.060 ms 9.504 ms
5 one.one.one.one (1.1.1.1) 8.567 ms 8.688 ms 8.764 ms
"""
self.assertEqual(TraceResult(ips=[
'205.185.112.1',
'172.18.0.29',
'184.104.194.81',
'149.112.13.27',
'1.1.1.1'
], latency="8.567 ms"), parse_traceroute(s))
def testTracerouteMultiQueryDifferentPaths(self):
s = """traceroute to google.com (142.250.185.174), 30 hops max, 60 byte packets
1 172.31.1.1 (172.31.1.1) 5.918 ms 5.974 ms 5.959 ms
2 17476.your-cloud.host (49.12.142.82) 0.339 ms 0.408 ms 0.405 ms
3 * * *
4 static.237.3.47.78.clients.your-server.de (78.47.3.237) 2.441 ms static.233.3.47.78.clients.your-server.de (78.47.3.233) 0.973 ms static.237.3.47.78.clients.your-server.de (78.47.3.237) 2.269 ms
5 static.85.10.239.169.clients.your-server.de (85.10.239.169) 0.914 ms 0.908 ms 1.246 ms
6 static.85-10-228-85.clients.your-server.de (85.10.228.85) 2.445 ms core11.nbg1.hetzner.com (85.10.250.209) 1.037 ms core11.nbg1.hetzner.com (213.239.208.221) 0.972 ms
7 core1.fra.hetzner.com (213.239.245.250) 3.951 ms core0.fra.hetzner.com (213.239.252.25) 3.374 ms core1.fra.hetzner.com (213.239.245.250) 3.497 ms
8 core8.fra.hetzner.com (213.239.224.217) 9.705 ms 3.626 ms core8.fra.hetzner.com (213.239.245.126) 3.697 ms
9 142.250.169.172 (142.250.169.172) 3.822 ms 3.587 ms 3.624 ms
10 * * *
11 142.250.226.148 (142.250.226.148) 3.881 ms 142.250.46.248 (142.250.46.248) 3.701 ms 172.253.50.150 (172.253.50.150) 5.400 ms
12 142.250.210.209 (142.250.210.209) 3.812 ms 108.170.252.18 (108.170.252.18) 3.763 ms 142.250.210.209 (142.250.210.209) 3.685 ms
13 fra16s51-in-f14.1e100.net (142.250.185.174) 3.895 ms 3.871 ms 3.945 ms
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'*',
'78.47.3.237',
'85.10.239.169',
'85.10.228.85',
'213.239.245.250',
'213.239.224.217',
'142.250.169.172',
'*',
'142.250.226.148',
'142.250.210.209',
'142.250.185.174'], latency="3.895 ms"), parse_traceroute(s))
def testTracerouteTimedOut(self):
s = """
traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
1 172.31.1.1 (172.31.1.1) 9.411 ms 9.405 ms 9.404 ms
2 17476.your-cloud.host (49.12.142.82) 0.341 ms 0.189 ms 0.086 ms
3 * * *
4 static.237.3.47.78.clients.your-server.de (78.47.3.237) 0.822 ms static.233.3.47.78.clients.your-server.de (78.47.3.233) 0.944 ms 0.876 ms
5 static.85.10.248.221.clients.your-server.de (85.10.248.221) 0.923 ms 1.164 ms 1.086 ms
6 core11.nbg1.hetzner.com (213.239.208.221) 0.596 ms 0.449 ms core12.nbg1.hetzner.com (85.10.250.213) 2.050 ms
7 core5.fra.hetzner.com (213.239.224.238) 3.574 ms core1.fra.hetzner.com (213.239.245.250) 3.480 ms 3.491 ms
8 ae72-0.fra-96cbe-1b.ntwk.msn.net (104.44.37.193) 3.748 ms hetzner.fra-96cbe-1a.ntwk.msn.net (104.44.197.103) 3.741 ms 3.709 ms
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 * * *
19 * * *
20 * * *
21 * * *
22 * * *
23 * * *
24 * * *
25 * * *
26 * * *
27 * * *
28 * * *
29 * * *
30 * * *
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'*',
'78.47.3.237',
'85.10.248.221',
'213.239.208.221',
'213.239.224.238',
'104.44.37.193'] + ["*"]*(30-9+1)), parse_traceroute(s))
def testTracerouteTimedOutTruncated(self):
# bird-lg-go specific - set latency to None in this case
s = """
traceroute to azure.microsoft.com (13.107.42.16), 30 hops max, 60 byte packets
1 172.31.1.1 (172.31.1.1) 6.362 ms
2 17476.your-cloud.host (49.12.142.82) 0.331 ms
4 static.237.3.47.78.clients.your-server.de (78.47.3.237) 1.309 ms
5 static.85.10.248.217.clients.your-server.de (85.10.248.217) 0.940 ms
6 core11.nbg1.hetzner.com (85.10.250.209) 2.706 ms
7 core1.fra.hetzner.com (213.239.245.254) 3.809 ms
8 hetzner.fra-96cbe-1a.ntwk.msn.net (104.44.197.103) 8.055 ms
23 hops not responding.
"""
self.assertEqual(TraceResult(ips=[
'172.31.1.1',
'49.12.142.82',
'78.47.3.237',
'85.10.248.217',
'85.10.250.209',
'213.239.245.254',
'104.44.197.103'], notes=["23 hops not responding."]), parse_traceroute(s))
def testTracerouteNoDNS(self):
s = """
traceroute to irc.hackint.dn42 (172.20.66.67), 30 hops max, 60 byte packets
1 172.20.229.114 15.386 ms 15.408 ms 15.409 ms
2 172.20.229.113 59.462 ms 59.485 ms 59.480 ms
3 172.20.229.123 80.929 ms * *
4 * * *
5 * * 172.20.129.187 152.446 ms
6 172.20.129.169 165.155 ms 165.375 ms 165.610 ms
7 172.23.96.1 165.631 ms 182.696 ms 182.695 ms
8 172.20.66.67 182.695 ms 167.117 ms 187.094 ms"""
self.assertEqual(TraceResult(ips=[
'172.20.229.114',
'172.20.229.113',
'172.20.229.123',
'*',
'*',
'172.20.129.169',
'172.23.96.1',
'172.20.66.67'], latency="182.695 ms"), parse_traceroute(s))
def testTracerouteNoDNSv6(self):
s = """traceroute to map.dn42 (fd42:4242:2189:e9::1), 30 hops max, 80 byte packets
1 fd86:bad:11b7:34::1 33.947 ms 33.932 ms 33.916 ms
2 fd86:bad:11b7:22::1 43.463 ms 43.467 ms 43.463 ms
3 fd42:4242:2189:ef::1 45.676 ms 45.665 ms 50.619 ms
4 fd42:4242:2189:e9::1 208.400 ms 208.398 ms 208.543 ms
"""
self.assertEqual(TraceResult(ips=[
'fd86:bad:11b7:34::1',
'fd86:bad:11b7:22::1',
'fd42:4242:2189:ef::1',
'fd42:4242:2189:e9::1'], latency="208.400 ms"), parse_traceroute(s))
def testTracerouteError(self):
s = """traceroute to 192.168.123.123 (192.168.123.123), 30 hops max, 60 byte packets
1 192.168.123.1 (192.168.123.1) 3079.329 ms !H 3079.304 ms !H 3079.299 ms !H
"""
self.assertEqual(TraceResult(ips=[
'192.168.123.1'], latency="3079.329 ms !H"), parse_traceroute(s))
if __name__ == '__main__':
unittest.main()

122
BirdLGGo/plugin.py Normal file
View File

@ -0,0 +1,122 @@
###
# Copyright (c) 2021, James Lu
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * 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.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# 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 OWNER 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.
###
import json
import urllib.parse
# local modules
from . import parsebird, parsetrace
# 3rd party
import requests
from supybot import utils, plugins, callbacks
from supybot.commands import wrap
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('BirdLGGo')
except ImportError:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x: x
class BirdLGGo(callbacks.Plugin):
"""API client (show route / traceroute) for Bird-lg-go"""
threaded = True
def lg_post_request(self, irc, msg, query):
url = urllib.parse.urljoin(self.registryValue("lgServer", network=irc.network, channel=msg.channel), "/api/")
if not url:
raise irc.error("No looking glass server specified - please set plugins.birdlggo.lgserver", Raise=True)
elif not query.get("servers"):
raise irc.error("No target nodes specified - please set plugins.birdlggo.nodes", Raise=True)
req = requests.post(url, json=query)
resp = req.json()
if resp.get("error"):
raise irc.error("Error from looking glass: " + resp["error"], Raise=True)
else:
return resp["result"]
@wrap(['something'])
def traceroute(self, irc, msg, args, target):
"""<target>
Sends a traceroute to the target host or IP.
"""
nodes = self.registryValue("nodes", network=irc.network, channel=msg.channel)
query = {
"servers": nodes,
"type": "traceroute",
"args": target
}
results = self.lg_post_request(irc, msg, query)
for result in results:
parsed_result = parsetrace.parse_traceroute(result["data"])
server = result["server"]
ips = " ".join(parsed_result.ips)
latency = parsed_result.latency or "(timed out)"
notes = ""
if parsed_result.notes:
notes = "- " + ", ".join(parsed_result.notes)
irc.reply(f"{server} -> {target}: {latency} | {ips} {notes}")
@wrap(['something'])
def showroute(self, irc, msg, args, target):
"""<target>
Shows the preferred BIRD route for the given target.
"""
nodes = self.registryValue("nodes", network=irc.network, channel=msg.channel)
query = {
"servers": nodes,
"type": "bird",
"args": f"show route for {target} all primary"
}
results = self.lg_post_request(irc, msg, query)
for result in results:
parsed_result = parsebird.parse_bird(result["data"])
server = result["server"]
s = f"{server} -> {target}: {parsed_result.route_type} {parsed_result.via} [Type: {parsed_result.route_origin}] [Pref: {parsed_result.route_preference}]"
if parsed_result.bgp_as_path:
path = " ".join(parsed_result.bgp_as_path)
s += f" [AS path: {path}]"
irc.reply(s)
Class = BirdLGGo
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=120:

44
BirdLGGo/test.py Normal file
View File

@ -0,0 +1,44 @@
###
# Copyright (c) 2021, James Lu
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * 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.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# 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 OWNER 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.
###
import os
import sys
from supybot.test import *
# Hack to allow relative imports
sys.path.append(os.path.join(os.path.dirname(__file__)))
from parsebird_test import *
from parsetrace_test import *
class BirdLGGoTestCase(PluginTestCase):
plugins = ('BirdLGGo',)
# TODO: integration tests
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: