]> git.ipfire.org Git - thirdparty/bird.git/commitdiff
Stub of Python package for CLI parsing
authorMaria Matejka <mq@ucw.cz>
Mon, 3 Apr 2023 15:52:03 +0000 (17:52 +0200)
committerMaria Matejka <mq@ucw.cz>
Tue, 23 May 2023 11:44:47 +0000 (13:44 +0200)
.gitignore
python/BIRD/Basic.py [new file with mode: 0644]
python/BIRD/Socket.py [new file with mode: 0644]
python/BIRD/Status.py [new file with mode: 0644]
python/BIRD/__init__.py [new file with mode: 0644]
python/test.py [new file with mode: 0644]

index a50f1fceb2ab049f37e02da89cfa171472f4c55f..541bfd571c3d9900feac394cde50e2c1eb58ab3f 100644 (file)
@@ -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 (file)
index 0000000..73cbd03
--- /dev/null
@@ -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 (file)
index 0000000..67aed16
--- /dev/null
@@ -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 (file)
index 0000000..5d24da1
--- /dev/null
@@ -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 (file)
index 0000000..26ce4d1
--- /dev/null
@@ -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 (file)
index 0000000..31131bf
--- /dev/null
@@ -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())