From: Maria Matejka Date: Mon, 3 Apr 2023 15:52:03 +0000 (+0200) Subject: Stub of Python package for CLI parsing X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5e4ab092f38c68668641ded65096dd508467b3b4;p=thirdparty%2Fbird.git Stub of Python package for CLI parsing --- diff --git a/.gitignore b/.gitignore index a50f1fceb..541bfd571 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /sysdep/autoconf.h.in~ /cscope.* *.tar.gz +__pycache__ diff --git a/python/BIRD/Basic.py b/python/BIRD/Basic.py new file mode 100644 index 000000000..73cbd03f2 --- /dev/null +++ b/python/BIRD/Basic.py @@ -0,0 +1,25 @@ +import asyncio + +class BIRDException(Exception): + pass + +class Basic: + def __init__(self, bird): + self.bird = bird + self.data = None + + def __getattr__(self, name): + if self.data is None: + raise BIRDException(f"Call update() to get data") + + if name not in self.data: + raise BIRDException(f"Unknown key {name} in {type(self)}") + + return self.data[name] + + def __repr__(self): + return f"{type(self).__name__}({self.data})" + + async def load(self): + if self.data is None: + await self.update() diff --git a/python/BIRD/Socket.py b/python/BIRD/Socket.py new file mode 100644 index 000000000..67aed16cc --- /dev/null +++ b/python/BIRD/Socket.py @@ -0,0 +1,84 @@ +import asyncio + +class SocketException(Exception): + def __init__(self, socket, msg): + Exception.__init__(self, f"Failed to {msg} BIRD Control Socket at {socket.path}") + +class ReadException(Exception): + def __init__(self, socket, line, msg): + Exception.__init__(self, f"Invalid input on line {line}: {msg}") + +class Socket: + def __init__(self, path): + self.path = path + self.reader = None + self.writer = None + + async def open(self): + assert(self.reader is None) + assert(self.writer is None) + + try: + self.reader, self.writer = await asyncio.open_unix_connection(path=self.path) + except Exception as e: + raise SocketException(self, "connect to") from e + + try: + return await self.read_from_socket() + except ReadException as e: + raise SocketException(self, "read hello from") from e + + async def close(self): + assert(self.reader is not None) + assert(self.writer is not None) + + try: + self.writer.close() + await self.writer.wait_closed() + except Exception as e: + raise SocketException(self, "close") from e + + self.reader = None + self.writer = None + + async def read_from_socket(self): + current_code = None + lines = [] + + while True: + line = (await self.reader.readline()).decode() + + if len(line) == 0: + raise ReadException(self, len(lines)+1, "Connection closed") + + if line[-1] != "\n": + raise ReadException(self, len(lines)+1, "Received partial data") + + if line[0] == " ": + if current_code is None: + raise ReadException(self, len(lines)+1, "First line can't be unnumbered continuation") + lines.append({"code": current_code, "data": line[1:-1]}) + + elif line[4] == "-" or line[4] == " ": + try: + current_code = int(line[:4]) + except ValueError as e: + raise ReadException(self, len(lines)+1, f"Invalid line code: {line[:4]}") from e + + lines.append({"code": current_code, "data": line[5:-1]}) + + if line[4] == " ": + return lines + + async def command(self, cmd): + try: + self.writer.write(f"{cmd}\n".encode()) + await self.writer.drain() + except Exception as e: + raise SocketException(self, f"write command {cmd} to") from e + + try: + return await self.read_from_socket() + except Exception as e: + raise SocketException(self, f"read response for command {cmd} from") from e + diff --git a/python/BIRD/Status.py b/python/BIRD/Status.py new file mode 100644 index 000000000..5d24da1dd --- /dev/null +++ b/python/BIRD/Status.py @@ -0,0 +1,48 @@ +import asyncio +from BIRD.Basic import Basic + +class StatusException(Exception): + def __init__(self, msg): + Exception.__init__(self, "Failed to parse status: " + msg) + +class Status(Basic): + async def update(self): + self.data = {} + + await self.bird.cli.open() + data = await self.bird.cli.socket.command("show status") + + if data[0]["code"] != 1000: + raise StatusException(f"BIRD version not on the first line, got {data[0]['code']}") + + self.data["version"] = data[0]["data"] + + if data[-1]["code"] != 13: + raise StatusException(f"BIRD status not on the last line, got {data[-1]['code']}") + + self.data["status"] = data[-1]["data"] + +# for d in data[1:-1]: + + + +class VersionException(Exception): + def __init__(self, msg): + Exception.__init__(self, "Failed to parse version from socket hello: " + msg) + +class Version(Basic): + async def update(self): + await self.bird.cli.open() + hello = self.bird.cli.hello + + if hello["code"] != 1: + raise VersionException(f"code is {hello['code']}, should be 1") + + s = hello["data"].split(" ") + if len(s) != 3 or s[2] != "ready.": + raise VersionException(f"malformed hello: {hello['data']}") + + self.data = { + "name": s[0], + "version": s[1], + } diff --git a/python/BIRD/__init__.py b/python/BIRD/__init__.py new file mode 100644 index 000000000..26ce4d1ea --- /dev/null +++ b/python/BIRD/__init__.py @@ -0,0 +1,48 @@ +import asyncio +from pathlib import Path + +from BIRD.Basic import BIRDException +from BIRD.Socket import Socket +from BIRD.Status import Status, Version + +class CLI: + def __init__(self, name): + self.socket = Socket(name) + self.connected = False + self.hello = None + + async def open(self): + if self.hello is not None: + return + + h = await self.socket.open() + if len(h) != 1: + raise BIRDException("CLI hello should have 1 line, has {len(h)} lines: {h}") + + self.hello = h[0] + + async def close(self): + if self.hello is None: + return + + await self.socket.close() + self.hello = None + +class BIRD: + def __init__(self, socket=Path("bird.ctl")): + self.cli = CLI(socket) + self.version = Version(self) + self.status = Status(self) + + self.within = False + + async def __aenter__(self): + if self.within: + raise BIRDException("Tried to enter BIRD context (async with) more than once") + + self.within = True + return self + + async def __aexit__(self, *args): + await self.cli.close() + self.within = False diff --git a/python/test.py b/python/test.py new file mode 100644 index 000000000..31131bfcf --- /dev/null +++ b/python/test.py @@ -0,0 +1,12 @@ +import asyncio +from BIRD import BIRD + +async def main(): + async with BIRD("/run/bird/bird.ctl") as b: + await b.version.update() + print(b.version) + + await b.status.update() + print(b.status) + +asyncio.run(main())