diff --git a/flock/ospf-base/dump-0003-ospf-neighbors.yaml b/flock/ospf-base/dump-0003-ospf-neighbors.yaml new file mode 100644 index 00000000..e4ae50bb --- /dev/null +++ b/flock/ospf-base/dump-0003-ospf-neighbors.yaml @@ -0,0 +1,344 @@ +m1: + ospf4: + ospf4: + neighbors: + 192.0.2.1: + interface: ve31 + ip: 192.0.2.1 + position: PtP + priority: '1' + state: Full + 192.0.2.18: + interface: ve12 + ip: 192.0.2.18 + position: PtP + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.1: + interface: ve31 + ip: fe80::ac55:22ff:fe32:3610 + position: PtP + priority: '1' + state: Full + 192.0.2.18: + interface: ve12 + ip: fe80::5c38:8eff:fe25:1d68 + position: PtP + priority: '1' + state: Full +m2: + ospf4: + ospf4: + neighbors: + 192.0.2.1: + interface: ve23 + ip: 192.0.2.34 + position: PtP + priority: '1' + state: Full + 192.0.2.2: + interface: ve12 + ip: 192.0.2.17 + position: PtP + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.1: + interface: ve23 + ip: fe80::7cda:1aff:feae:9831 + position: PtP + priority: '1' + state: Full + 192.0.2.2: + interface: ve12 + ip: fe80::4cfd:f0ff:fe23:167 + position: PtP + priority: '1' + state: Full +m3: + ospf4: + ospf4: + neighbors: + 192.0.2.18: + interface: ve23 + ip: 192.0.2.33 + position: PtP + priority: '1' + state: Full + 192.0.2.2: + interface: ve31 + ip: 192.0.2.2 + position: PtP + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: 192.0.2.99 + position: "Other\t" + priority: '1' + state: 2-Way + 192.0.2.82: + interface: multi + ip: 192.0.2.100 + position: BDR + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: 192.0.2.98 + position: DR + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.18: + interface: ve23 + ip: fe80::8c41:8bff:fe8f:9446 + position: PtP + priority: '1' + state: Full + 192.0.2.2: + interface: ve31 + ip: fe80::5c82:ccff:fe47:46df + position: PtP + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: fe80::5c5b:1eff:fe50:3793 + position: "Other\t" + priority: '1' + state: 2-Way + 192.0.2.82: + interface: multi + ip: fe80::9ce1:eaff:fe4b:b4f9 + position: BDR + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: fe80::2cf6:51ff:feaa:3b4f + position: DR + priority: '1' + state: Full +m4: + ospf4: + ospf4: + neighbors: + 192.0.2.1: + interface: multi + ip: 192.0.2.97 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: 192.0.2.99 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.82: + interface: multi + ip: 192.0.2.100 + position: BDR + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.1: + interface: multi + ip: fe80::dc8f:6bff:fe0f:6ad9 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: fe80::5c5b:1eff:fe50:3793 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.82: + interface: multi + ip: fe80::9ce1:eaff:fe4b:b4f9 + position: BDR + priority: '1' + state: Full +m5: + ospf4: + ospf4: + neighbors: + 192.0.2.1: + interface: multi + ip: 192.0.2.97 + position: "Other\t" + priority: '1' + state: 2-Way + 192.0.2.50: + interface: ve57 + ip: 192.0.2.50 + position: PtP + priority: '1' + state: Full + 192.0.2.82: + interface: multi + ip: 192.0.2.100 + position: BDR + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: 192.0.2.98 + position: DR + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.1: + interface: multi + ip: fe80::dc8f:6bff:fe0f:6ad9 + position: "Other\t" + priority: '1' + state: 2-Way + 192.0.2.50: + interface: ve57 + ip: fe80::9c97:afff:fe62:f695 + position: PtP + priority: '1' + state: Full + 192.0.2.82: + interface: multi + ip: fe80::9ce1:eaff:fe4b:b4f9 + position: BDR + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: fe80::2cf6:51ff:feaa:3b4f + position: DR + priority: '1' + state: Full +m6: + ospf4: + ospf4: + neighbors: + 192.0.2.1: + interface: multi + ip: 192.0.2.97 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: 192.0.2.99 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.66: + interface: ve86 + ip: 192.0.2.81 + position: PtP + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: 192.0.2.98 + position: DR + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.1: + interface: multi + ip: fe80::dc8f:6bff:fe0f:6ad9 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.49: + interface: multi + ip: fe80::5c5b:1eff:fe50:3793 + position: "Other\t" + priority: '1' + state: Full + 192.0.2.66: + interface: ve86 + ip: fe80::6cc9:f2ff:feb9:754f + position: PtP + priority: '1' + state: Full + 192.0.2.98: + interface: multi + ip: fe80::2cf6:51ff:feaa:3b4f + position: DR + priority: '1' + state: Full +m7: + ospf4: + ospf4: + neighbors: + 192.0.2.49: + interface: ve57 + ip: 192.0.2.49 + position: PtP + priority: '1' + state: Full + 192.0.2.66: + interface: ve78 + ip: 192.0.2.66 + position: PtP + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.49: + interface: ve57 + ip: fe80::2c33:44ff:fe0f:454b + position: PtP + priority: '1' + state: Full + 192.0.2.66: + interface: ve78 + ip: fe80::8c67:1aff:fe6f:1193 + position: PtP + priority: '1' + state: Full +m8: + ospf4: + ospf4: + neighbors: + 192.0.2.50: + interface: ve78 + ip: 192.0.2.65 + position: PtP + priority: '1' + state: Full + 192.0.2.82: + interface: ve86 + ip: 192.0.2.82 + position: PtP + priority: '1' + state: Full + ospf6: + ospf6: + neighbors: + 192.0.2.50: + interface: ve78 + ip: fe80::ac21:71ff:fea9:abe + position: PtP + priority: '1' + state: Full + 192.0.2.82: + interface: ve86 + ip: fe80::5cf4:d5ff:fe79:c89 + position: PtP + priority: '1' + state: Full diff --git a/flock/ospf-base/test.py b/flock/ospf-base/test.py index 4e1b680d..2e0481e4 100644 --- a/flock/ospf-base/test.py +++ b/flock/ospf-base/test.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 import asyncio -from python.BIRD.Test import Test, BIRDInstance, DumpRIB, DumpLinuxKRT +from python.BIRD.Test import Test, BIRDInstance, DumpRIB, DumpLinuxKRT, DumpOSPFNeighbors from python.BIRD.LogChecker import LogExpectedStub class ThisTest(Test): @@ -35,4 +35,5 @@ class ThisTest(Test): await asyncio.gather(*[ DumpRIB(self, 30, "rib-startup")(), DumpLinuxKRT(self, 30, "fib-startup")(), + DumpOSPFNeighbors(self, 30, "ospf-neighbors", protocols=["ospf4", "ospf6"])(), ]) diff --git a/python/BIRD/CLI.py b/python/BIRD/CLI.py index 34915294..1458273d 100644 --- a/python/BIRD/CLI.py +++ b/python/BIRD/CLI.py @@ -1,4 +1,5 @@ -from . import ShowRoute +from .ShowRoute import ShowRouteParser +from .ShowOSPF import ShowOSPFNeighborsParser class Transport: pass @@ -24,6 +25,16 @@ class CLI: async def disable(self, proto: str): return await self.transport.send_cmd("disable", proto) + async def cmd_send_parse(self, parser, *cmd): + result = await self.transport.send_cmd(*cmd) + if len(result["err"]): + raise Exception(f"Command {cmd} returned {result['err'].decode()}, stdout={result['out'].decode()}") + + for line in result["out"].decode().split("\n"): + parser = parser.parse(line) + + return parser.parse(None).result + async def show_route(self, table=["all"], args=[]): cmd = [ "show", "route" ] for t in table: @@ -31,9 +42,8 @@ class CLI: cmd.append(t) cmd += args + return await self.cmd_send_parse(ShowRouteParser(), *cmd) - result = await self.transport.send_cmd(*cmd) - if len(result["err"]): - raise Exception(f"Command {cmd} returned {result['err'].decode()}, stdout={result['out'].decode()}") - - return ShowRoute.parse(result["out"].decode()) + async def show_ospf_neighbors(self, proto: str): + return await self.cmd_send_parse(ShowOSPFNeighborsParser(), + "show", "ospf", "neighbors", proto) diff --git a/python/BIRD/CLIParser.py b/python/BIRD/CLIParser.py new file mode 100644 index 00000000..08c98a8b --- /dev/null +++ b/python/BIRD/CLIParser.py @@ -0,0 +1,68 @@ +import re + +class ParserException(Exception): + pass + +def subparser(base): + def decorator(cls): + base.subparsers = { **base.subparsers, cls.entryRegex: cls } + return cls + return decorator + +class CLIParser: + subparsers = {} + def __init__(self, groups=None, parent=None): + self.result = {} + self.parent = parent + self.cur = None + + if parent is None: + assert(groups is None) + return + + self.enter(groups) + + def parse(self, line: str): + assert(self.cur == None) + if line is not None: + for k,v in self.subparsers.items(): + if m := k.match(line): + self.cur = (c := v(groups=m.groups(), parent=self)) + while c.cur is not None: + c = c.cur + return c + elif self.parent is None: + return self + + try: + self.exit() + self.parent.cur = None + return self.parent.parse(line) + + except Exception as e: + print(f"Failed to parse line: {line}") + raise e + + def exit(self): + raise ParserException(f"Failed to match line to all regexes") + +@subparser(CLIParser) +class VersionParser(CLIParser): + entryRegex = re.compile("BIRD ([0-9a-z._-]+) ready.") + def enter(self, groups): + self.version ,= groups + + def exit(self): + if "version" in self.parent.result: + raise ParserException(f"Duplicate version line") + + self.parent.result["version"] = self.version + +@subparser(CLIParser) +class NothingParser(CLIParser): + entryRegex = re.compile("^$") + def enter(self, _): + pass + + def exit(self): + pass diff --git a/python/BIRD/ShowOSPF.py b/python/BIRD/ShowOSPF.py new file mode 100644 index 00000000..742cec20 --- /dev/null +++ b/python/BIRD/ShowOSPF.py @@ -0,0 +1,34 @@ +import re +from .CLIParser import CLIParser, subparser, ParserException + +class ShowOSPFNeighborsParser(CLIParser): + pass + +@subparser(ShowOSPFNeighborsParser) +class ShowOSPFNeighborsProtocolParser(CLIParser): + entryRegex = re.compile("^(.*):$") + def enter(self, groups): + self.name ,= groups + + def exit(self): + self.parent.result[self.name] = self.result + +@subparser(ShowOSPFNeighborsProtocolParser) +class ShowOSPFNeighborsHeaderParser(CLIParser): + entryRegex = re.compile("^Router ID Pri State DTime Interface Router IP$") + def enter(self, _): + pass + + def exit(self): + self.parent.result["neighbors"] = self.result + +@subparser(ShowOSPFNeighborsHeaderParser) +class ShowOSPFNeighborsHeaderParser(CLIParser): + entryRegex = re.compile("([^ ]+)\s+([^ ]+)\s+([^/]+)/([^ ]+)\s+([^ ]+)\s+([^ ]+)\s+([^ ]+)$") + def enter(self, groups): + self.id, *rest = groups + self.result = dict(zip( + ["priority", "state", "position", "timeout", "interface", "ip"], rest)) + + def exit(self): + self.parent.result[self.id] = self.result diff --git a/python/BIRD/ShowRoute.py b/python/BIRD/ShowRoute.py index e330779f..f34e6511 100644 --- a/python/BIRD/ShowRoute.py +++ b/python/BIRD/ShowRoute.py @@ -1,77 +1,11 @@ import re - -class ParserException(Exception): - pass - -def subparser(base): - def decorator(cls): - base.subparsers = { **base.subparsers, cls.entryRegex: cls } - return cls - return decorator - -class CLIParser: - subparsers = {} - def __init__(self, groups=None, parent=None): - self.result = {} - self.parent = parent - self.cur = None - - if parent is None: - assert(groups is None) - return - - self.enter(groups) - - def parse(self, line: str): - assert(self.cur == None) - if line is not None: - for k,v in self.subparsers.items(): - if m := k.match(line): - self.cur = (c := v(groups=m.groups(), parent=self)) - while c.cur is not None: - c = c.cur - return c - elif self.parent is None: - return self - - try: - self.exit() - self.parent.cur = None - return self.parent.parse(line) - - except Exception as e: - print(f"Failed to parse line: {line}") - raise e - - def exit(self): - raise ParserException(f"Failed to match line to all regexes") - -@subparser(CLIParser) -class VersionParser(CLIParser): - entryRegex = re.compile("BIRD ([0-9a-z._-]+) ready.") - def enter(self, groups): - self.version ,= groups - - def exit(self): - if "version" in self.parent.result: - raise ParserException(f"Duplicate version line") - - self.parent.result["version"] = self.version +from .CLIParser import CLIParser, subparser, ParserException class ShowRouteParser(CLIParser): def __init__(self): super().__init__() self.result["tables"] = {} -@subparser(ShowRouteParser) -class NothingParser(CLIParser): - entryRegex = re.compile("^$") - def enter(self, _): - pass - - def exit(self): - pass - @subparser(ShowRouteParser) class TableParser(CLIParser): entryRegex = re.compile("^Table (.*):$") @@ -153,12 +87,3 @@ class InternalRouteHandlingValuesParser(CLIParser): if k in self.parent.result: raise ParserException(f"Duplicate internal value {k} in route") self.parent.result[k] = v - -def parse(data: str): - parser = ShowRouteParser() - for line in data.split("\n"): - parser = parser.parse(line) - - parser = parser.parse(None) - - return parser.result diff --git a/python/BIRD/Test.py b/python/BIRD/Test.py index 258cf041..928cc80c 100644 --- a/python/BIRD/Test.py +++ b/python/BIRD/Test.py @@ -239,6 +239,7 @@ class DumpCheck: seen = [] try: async with asyncio.timeout(self.check_timeout) as to: + i = 0 while True: dump = await self.obtain() try: @@ -252,6 +253,8 @@ class DumpCheck: print(f"Differs at {' -> '.join([str(s) for s in reversed(d.tree)])}: {d.a} != {d.b}") seen.append(dump) + i += 1 + print('\-|/'[i % 4] + "\010", end='', flush=True) await asyncio.sleep(self.check_retry_timeout) except TimeoutError as e: @@ -302,6 +305,27 @@ class DumpRIB(DumpOnMachines): del r[k] return d +class DumpOSPFNeighbors(DumpOnMachines): + def __init__(self, *args, protocols, **kwargs): + super().__init__(*args, **kwargs) + self.protocols = protocols + + async def obtain_on_machine(self, mach): + d = await dict_gather({ + p: mach.show_ospf_neighbors(proto=p) + for p in self.protocols + }) + + for p in d.values(): + assert("version" in p) + del p["version"] + for pp in p.values(): + for n in pp["neighbors"].values(): + assert("timeout" in n) + del n["timeout"] + + return d + class DumpLinuxKRT(DumpOnMachines): def __init__(self, *args, cmdargs=None, **kwargs): super().__init__(*args, **kwargs) @@ -456,35 +480,6 @@ class Test: await self.cleanup() - async def krt_dump(self, timeout, name, *args, full=True, machines=None, check_timeout=10, check_retry_timeout=0.5): - # Collect machines to dump - if machines is None: - machines = self.machine_index.values() - else: - machines = [ - m if isinstance(m, CLI) else self.machine_index[m] - for m in machines - ] - - raw = await dict_gather({ - (mach.mach.name, fam): - mach.mach.hypervisor.run_in(mach.mach.name, "ip", "-j", f"-{fam}", "route", "show", *args) - for mach in machines - for fam in ("4", "6", "M") - }) - - for k,v in raw.items(): - if v["ret"] != 0 or len(v["err"]) != 0: - raise Exception(f"Failed to gather krt dump for {k}: ret={v['ret']}, {v['err']}") - - dump = dict_expand({ k: json.loads(v["out"]) for k,v in raw.items()}) - print(dump) - - name = "krt.yaml" - with open(name, "w") as y: - yaml.dump(dump, y) - - if __name__ == "__main__": name = sys.argv[1] mode = Test.CHECK