0
0
mirror of https://gitlab.nic.cz/labs/bird.git synced 2024-11-08 12:18:42 +00:00

Stub of Python package for CLI parsing

This commit is contained in:
Maria Matejka 2023-04-03 17:52:03 +02:00
parent ca0f239c72
commit 5e4ab092f3
6 changed files with 218 additions and 0 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@
/sysdep/autoconf.h.in~
/cscope.*
*.tar.gz
__pycache__

25
python/BIRD/Basic.py Normal file
View File

@ -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()

84
python/BIRD/Socket.py Normal file
View File

@ -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

48
python/BIRD/Status.py Normal file
View File

@ -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],
}

48
python/BIRD/__init__.py Normal file
View File

@ -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

12
python/test.py Normal file
View File

@ -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())