Browse Source

add some more documentation, reorder api paths for better flow..

main
John-Mark Gurney 3 years ago
parent
commit
085aee2f23
1 changed files with 81 additions and 27 deletions
  1. +81
    -27
      bitelab/__init__.py

+ 81
- 27
bitelab/__init__.py View File

@@ -154,19 +154,19 @@ async def log_event(tag, board=None, user=None, extra={}):


class TimeOut(Attribute): 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 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. 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 While the board is not activated, it will display the timeout in
seconds. When the board is activated, the getvalue will return 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' defattrname = 'timeout'
@@ -257,6 +257,11 @@ class TimeOut(Attribute):
self._task = asyncio.create_task(self.timeout_coro()) self._task = asyncio.create_task(self.timeout_coro())


class EtherIface(DefROAttribute): 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' defattrname = 'eiface'


async def activate(self, brd): async def activate(self, brd):
@@ -271,6 +276,11 @@ class EtherIface(DefROAttribute):
raise RuntimeError('activate failed: %d' % ret) raise RuntimeError('activate failed: %d' % ret)


class SerialConsole(DefROAttribute): 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' defattrname = 'console'


async def activate(self, brd): async def activate(self, brd):
@@ -328,7 +338,13 @@ class BoardImpl:
return repr(Board.from_orm(self)) return repr(Board.from_orm(self))


async def reserve(self, user, sshpubkey=None): 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(): if not self.lock.locked():
raise RuntimeError('code error, board not locked') raise RuntimeError('code error, board not locked')
@@ -405,10 +421,12 @@ class BoardImpl:
self.attrcache[i] = await self.attrmap[i].getvalue() self.attrcache[i] = await self.attrmap[i].getvalue()


async def activate(self): 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 has been reserved. This is for things like adding an
ethernet interface to the jail's vnet, or starting the ethernet interface to the jail's vnet, or starting the
timeout.'''
timeout.

This is called by the reserve method.'''


if not self.lock.locked(): if not self.lock.locked():
raise RuntimeError('code error, board not locked') raise RuntimeError('code error, board not locked')
@@ -420,9 +438,11 @@ class BoardImpl:
await i.activate(self) await i.activate(self)


async def deactivate(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(): if not self.lock.locked():
raise RuntimeError('code error, board not locked') raise RuntimeError('code error, board not locked')
@@ -456,10 +476,20 @@ class BoardImpl:


@dataclass @dataclass
class BITEError(Exception): class BITEError(Exception):
'''Object representing an error that occured during processing
an API request.'''

errobj: Error errobj: Error
status_code: int status_code: int


class BoardManager(object): 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( _option_map = dict(
etheriface=EtherIface, etheriface=EtherIface,
serialconsole=SerialConsole, serialconsole=SerialConsole,
@@ -497,6 +527,16 @@ class BoardManager(object):
return self.board_class_info return self.board_class_info


def unhashable_lru(): 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): def newwrapper(fun):
cache = {} cache = {}


@@ -523,6 +563,14 @@ def unhashable_lru():
return newwrapper return newwrapper


class BiteAuth(Auth): 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): def __init__(self, token):
self.token = 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 '''This context manager checks to see if the request is authorized
for the board_id. This requires that the board is reserved by for the board_id. This requires that the board is reserved by
the user, or the connection came from the board's jail (TBI). 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] 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), async def lookup_user(token: str = Depends(oauth2_scheme),
data: data.DataWrapper = Depends(get_data)): data: data.DataWrapper = Depends(get_data)):
'''Using the token, look up the user that the token authorizes.'''

try: try:
return (await data.APIKey.objects.get(key=token)).user return (await data.APIKey.objects.get(key=token)).user
except orm.exceptions.NoMatch: except orm.exceptions.NoMatch:
@@ -629,6 +683,20 @@ def board_priority(request: Request):
scope = request.scope scope = request.scope
return scope['server'] 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]) @router.get('/board/classes', response_model=Dict[str, BoardClassInfo])
async def get_board_classes(user: str = Depends(lookup_user), async def get_board_classes(user: str = Depends(lookup_user),
brdmgr: BoardManager = Depends(get_boardmanager)): brdmgr: BoardManager = Depends(get_boardmanager)):
@@ -862,20 +930,6 @@ async def set_board_attrs(


return brd 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(): def getApp():
app = FastAPI() app = FastAPI()
app.include_router(router) app.include_router(router)


Loading…
Cancel
Save