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