From 085aee2f23628fc4e34a7d3327de5809fbd74992 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Tue, 22 Dec 2020 17:32:16 -0800 Subject: [PATCH] add some more documentation, reorder api paths for better flow.. --- bitelab/__init__.py | 108 +++++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/bitelab/__init__.py b/bitelab/__init__.py index 9fcdaed..f2ac26f 100644 --- a/bitelab/__init__.py +++ b/bitelab/__init__.py @@ -154,19 +154,19 @@ async def log_event(tag, board=None, user=None, extra={}): class TimeOut(Attribute): ''' - Implement a TimeOut functionality. The argument val (first and + Implement timeout functionality. The argument val (first and only) to __init__ is a number of seconds for the timeout to - last. This will start the time ticking on activation. If it - is not deactivated before the timer expires, it will deactivate + last. This will start the timer on activation. If it is not + deactivated before the timer expires, it will release the board itself. - Not that this uses the asyncio loop timescale and NOT UTC. This - means that over large durations, the clock will drift. This means - that over time, the "expired" time will change. + Note that this uses the asyncio loop timescale and NOT UTC. This + means that over large durations, the clock likely will drift, + such that the returned "expired" time will change. While the board is not activated, it will display the timeout in seconds. When the board is activated, the getvalue will return - the time the board will be deactivated. + the time the board will be deactivated in (approximate) UTC. ''' defattrname = 'timeout' @@ -257,6 +257,11 @@ class TimeOut(Attribute): self._task = asyncio.create_task(self.timeout_coro()) class EtherIface(DefROAttribute): + '''Add the specified ethernet interface to the board when + activated. This is for adding specific interfaces, such as + the vlan interface that maps to the board. + ''' + defattrname = 'eiface' async def activate(self, brd): @@ -271,6 +276,11 @@ class EtherIface(DefROAttribute): raise RuntimeError('activate failed: %d' % ret) class SerialConsole(DefROAttribute): + '''Specify a device to add to the board jail. This is primarily + designed for use with a hard wired serial device, one that will + not change on board restart, nor system reboot. + ''' + defattrname = 'console' async def activate(self, brd): @@ -328,7 +338,13 @@ class BoardImpl: return repr(Board.from_orm(self)) async def reserve(self, user, sshpubkey=None): - '''Reserve the board.''' + '''Reserve the board. It requires the user to be + specified and optionally the ssh public key that will be + installed for the freebsd user in the jail. + + The user and optional sshpubkey will be passed to the + setup script. + ''' if not self.lock.locked(): raise RuntimeError('code error, board not locked') @@ -405,10 +421,12 @@ class BoardImpl: self.attrcache[i] = await self.attrmap[i].getvalue() async def activate(self): - '''Activate the attributes. This means that the board + '''Activate the attributes. This requires that the board has been reserved. This is for things like adding an ethernet interface to the jail's vnet, or starting the - timeout.''' + timeout. + + This is called by the reserve method.''' if not self.lock.locked(): raise RuntimeError('code error, board not locked') @@ -420,9 +438,11 @@ class BoardImpl: await i.activate(self) async def deactivate(self): - '''Deactivate the attributes. This should be called - just before release. This is to clean up anything like - scheduled tasks.''' + '''Deactivate the attributes. This is called just before + release. This is to clean up anything like scheduled + tasks. + + This is called by the release method.''' if not self.lock.locked(): raise RuntimeError('code error, board not locked') @@ -456,10 +476,20 @@ class BoardImpl: @dataclass class BITEError(Exception): + '''Object representing an error that occured during processing + an API request.''' + errobj: Error status_code: int class BoardManager(object): + '''Object that acts as a repository for the boards and their + classes. + + This will normally be instantiated via the from_settings or + from_ucl methods. + ''' + _option_map = dict( etheriface=EtherIface, serialconsole=SerialConsole, @@ -497,6 +527,16 @@ class BoardManager(object): return self.board_class_info def unhashable_lru(): + '''A decorator to implement an LRU (least recently used) + cache. Currently, there is no limit. + + It implements handling unhashable arguments via checking + if two objects are the same with is. It also uses the id + property to more quickly look up the arguments. This + does mean that this keeps a copy to any objects passed in + as arguments, so do not expect them to be GC'd. + ''' + def newwrapper(fun): cache = {} @@ -523,6 +563,14 @@ def unhashable_lru(): return newwrapper class BiteAuth(Auth): + '''Implement a bearer token authentication for use with the + httpx client. + + Example usage: + client = AsyncClient(base_url='https://example.com') + res = await client.get('/board/classes', auth=BiteAuth('anapikey')) + ''' + def __init__(self, token): self.token = token @@ -587,6 +635,10 @@ async def validate_board_params(board_id, data, brdmgr, user=None, token=None): '''This context manager checks to see if the request is authorized for the board_id. This requires that the board is reserved by the user, or the connection came from the board's jail (TBI). + + This should only be used for operations on a board that do not + involve reserve or release, that is setting attributes and other + operations. ''' brd = brdmgr.boards[board_id] @@ -613,6 +665,8 @@ async def validate_board_params(board_id, data, brdmgr, user=None, token=None): async def lookup_user(token: str = Depends(oauth2_scheme), data: data.DataWrapper = Depends(get_data)): + '''Using the token, look up the user that the token authorizes.''' + try: return (await data.APIKey.objects.get(key=token)).user except orm.exceptions.NoMatch: @@ -629,6 +683,20 @@ def board_priority(request: Request): scope = request.scope return scope['server'] +@router.get('/') +async def root_test(board_prio: dict = Depends(board_priority), + settings: config.Settings = Depends(get_settings)): + return { 'foo': 'bar', 'board': board_prio } + +@router.get('/board/',response_model=Dict[str, Board]) +async def get_boards(user: str = Depends(lookup_user), + brdmgr: BoardManager = Depends(get_boardmanager)): + brds = brdmgr.boards + for i in brds: + await brds[i].update() + + return brds + @router.get('/board/classes', response_model=Dict[str, BoardClassInfo]) async def get_board_classes(user: str = Depends(lookup_user), brdmgr: BoardManager = Depends(get_boardmanager)): @@ -862,20 +930,6 @@ async def set_board_attrs( return brd -@router.get('/board/',response_model=Dict[str, Board]) -async def get_boards(user: str = Depends(lookup_user), - brdmgr: BoardManager = Depends(get_boardmanager)): - brds = brdmgr.boards - for i in brds: - await brds[i].update() - - return brds - -@router.get('/') -async def root_test(board_prio: dict = Depends(board_priority), - settings: config.Settings = Depends(get_settings)): - return { 'foo': 'bar', 'board': board_prio } - def getApp(): app = FastAPI() app.include_router(router)