mirror of
https://github.com/jlu5/SupyPlugins.git
synced 2025-04-26 04:51:08 -05:00
BirdLGGo: initial release
This commit is contained in:
parent
7fdf95074c
commit
ce56aeaf2b
32
BirdLGGo/README.md
Normal file
32
BirdLGGo/README.md
Normal 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
72
BirdLGGo/__init__.py
Normal 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
57
BirdLGGo/config.py
Normal 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:
|
1
BirdLGGo/local/__init__.py
Normal file
1
BirdLGGo/local/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Stub so local is a module, used for third-party modules
|
58
BirdLGGo/parsebird.py
Normal file
58
BirdLGGo/parsebird.py
Normal 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
102
BirdLGGo/parsebird_test.py
Normal 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
50
BirdLGGo/parsetrace.py
Normal 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
188
BirdLGGo/parsetrace_test.py
Normal 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
122
BirdLGGo/plugin.py
Normal 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
44
BirdLGGo/test.py
Normal 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:
|
Loading…
x
Reference in New Issue
Block a user