|
|
@@ -1,28 +1,29 @@ |
|
|
|
from typing import Optional, Union, Dict, Any |
|
|
|
from dataclasses import dataclass |
|
|
|
from functools import lru_cache, wraps |
|
|
|
from io import StringIO |
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request |
|
|
|
from fastapi.security import OAuth2PasswordBearer |
|
|
|
from httpx import AsyncClient, Auth |
|
|
|
from mock import patch, AsyncMock, Mock |
|
|
|
from pydantic import BaseModel |
|
|
|
from unittest.mock import patch, AsyncMock, Mock, PropertyMock |
|
|
|
from starlette.responses import JSONResponse |
|
|
|
from starlette.status import HTTP_200_OK |
|
|
|
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, \ |
|
|
|
HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT |
|
|
|
|
|
|
|
from . import config |
|
|
|
from . import data |
|
|
|
from .data import * |
|
|
|
|
|
|
|
import asyncio |
|
|
|
import gc |
|
|
|
import orm |
|
|
|
import os |
|
|
|
import socket |
|
|
|
import sqlite3 |
|
|
|
import sys |
|
|
|
import tempfile |
|
|
|
import unittest |
|
|
|
import urllib |
|
|
|
|
|
|
|
# fix up parse_socket_addr for hypercorn |
|
|
|
from hypercorn.utils import parse_socket_addr |
|
|
@@ -54,8 +55,20 @@ async def snmpget(host, oid, type): |
|
|
|
# return await _snmpwrapper('set', host, oid, value=value) |
|
|
|
|
|
|
|
class Attribute: |
|
|
|
'''Base class for board attributes. This is for both read-only |
|
|
|
and read-write attributes for a board. |
|
|
|
|
|
|
|
The defattrname should be set. |
|
|
|
''' |
|
|
|
|
|
|
|
defattrname = None |
|
|
|
|
|
|
|
async def getvalue(self): # pragma: no cover |
|
|
|
raise NotImplementedError |
|
|
|
|
|
|
|
async def setvalue(self): # pragma: no cover |
|
|
|
raise NotImplementedError |
|
|
|
|
|
|
|
class Power(Attribute): |
|
|
|
defattrname = 'power' |
|
|
|
|
|
|
@@ -72,10 +85,6 @@ class SNMPPower(Power): |
|
|
|
#async def setvalue(self, v): |
|
|
|
# pass |
|
|
|
|
|
|
|
class BoardClassInfo(BaseModel): |
|
|
|
clsname: str |
|
|
|
arch: str |
|
|
|
|
|
|
|
class BoardImpl: |
|
|
|
def __init__(self, name, brdclass, options): |
|
|
|
self.name = name |
|
|
@@ -107,19 +116,6 @@ class BoardImpl: |
|
|
|
def attrs(self): |
|
|
|
return dict(self.attrcache) |
|
|
|
|
|
|
|
class Board(BaseModel): |
|
|
|
name: str |
|
|
|
brdclass: str |
|
|
|
reserved: bool |
|
|
|
attrs: Dict[str, Any] |
|
|
|
|
|
|
|
class Config: |
|
|
|
orm_mode = True |
|
|
|
|
|
|
|
class Error(BaseModel): |
|
|
|
error: str |
|
|
|
board: Optional[Board] |
|
|
|
|
|
|
|
@dataclass |
|
|
|
class BITEError(Exception): |
|
|
|
errobj: Error |
|
|
@@ -180,6 +176,9 @@ class BiteAuth(Auth): |
|
|
|
def __init__(self, token): |
|
|
|
self.token = token |
|
|
|
|
|
|
|
def __eq__(self, o): |
|
|
|
return self.token == o.token |
|
|
|
|
|
|
|
def auth_flow(self, request): |
|
|
|
request.headers['Authorization'] = 'Bearer ' + self.token |
|
|
|
yield request |
|
|
@@ -201,7 +200,7 @@ def get_settings(): # pragma: no cover |
|
|
|
def get_data(settings: config.Settings = Depends(get_settings)): # pragma: no cover |
|
|
|
print(repr(settings)) |
|
|
|
database = data.databases.Database('sqlite:///' + settings.db_file) |
|
|
|
d = data.make_orm(self.database) |
|
|
|
d = make_orm(self.database) |
|
|
|
return d |
|
|
|
|
|
|
|
@unhashable_lru() |
|
|
@@ -244,11 +243,12 @@ async def get_board_info(board_id, user: str = Depends(lookup_user), |
|
|
|
|
|
|
|
return brd |
|
|
|
|
|
|
|
@router.post('/board/{board_id}/reserve', response_model=Union[Board, Error]) |
|
|
|
async def reserve_board(board_id, user: str = Depends(lookup_user), |
|
|
|
@router.post('/board/{board_id_or_class}/reserve', response_model=Union[Board, Error]) |
|
|
|
async def reserve_board(board_id_or_class, user: str = Depends(lookup_user), |
|
|
|
brdmgr: BoardManager = Depends(get_boardmanager), |
|
|
|
brdlck: asyncio.Lock = Depends(get_board_lock), |
|
|
|
data: data.DataWrapper = Depends(get_data)): |
|
|
|
board_id = board_id_or_class |
|
|
|
brd = brdmgr.boards[board_id] |
|
|
|
|
|
|
|
async with brd.lock: |
|
|
@@ -322,6 +322,33 @@ def getApp(): |
|
|
|
# uvicorn can't call the above function, while hypercorn can |
|
|
|
#app = getApp() |
|
|
|
|
|
|
|
async def real_main(): |
|
|
|
baseurl = os.environ['BITELAB_URL'] |
|
|
|
authkey = os.environ['BITELAB_AUTH'] |
|
|
|
|
|
|
|
client = AsyncClient(base_url=baseurl) |
|
|
|
|
|
|
|
if sys.argv[1] == 'list': |
|
|
|
res = await client.get('board/classes', auth=BiteAuth(authkey)) |
|
|
|
|
|
|
|
print('Classes:') |
|
|
|
for i in res.json(): |
|
|
|
print('\t' + i) |
|
|
|
elif sys.argv[1] == 'reserve': |
|
|
|
res = await client.get('board/%s/reserve' % |
|
|
|
urllib.parse.quote(sys.argv[2], safe=''), |
|
|
|
auth=BiteAuth(authkey)) |
|
|
|
|
|
|
|
brd = Board.parse_obj(res.json()) |
|
|
|
print('Name:\t%s' % brd.name) |
|
|
|
print('Class:\t%s' % brd.brdclass) |
|
|
|
print('Attributes:') |
|
|
|
for i in brd.attrs: |
|
|
|
print('\t%s\t%s' % (i, brd.attrs[i])) |
|
|
|
|
|
|
|
def main(): |
|
|
|
asyncio.run(real_main()) |
|
|
|
|
|
|
|
class TestUnhashLRU(unittest.TestCase): |
|
|
|
def test_unhashlru(self): |
|
|
|
lsta = [] |
|
|
@@ -369,12 +396,14 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase): |
|
|
|
self.dbtempfile = tempfile.NamedTemporaryFile() |
|
|
|
self.database = data.databases.Database('sqlite:///' + |
|
|
|
self.dbtempfile.name) |
|
|
|
self.data = data.make_orm(self.database) |
|
|
|
self.data = make_orm(self.database) |
|
|
|
|
|
|
|
await _setup_data(self.data) |
|
|
|
|
|
|
|
# setup settings |
|
|
|
self.settings = config.Settings(db_file=self.dbtempfile.name) |
|
|
|
self.settings = config.Settings(db_file=self.dbtempfile.name, |
|
|
|
setup_script='somesetupscript', |
|
|
|
) |
|
|
|
|
|
|
|
self.app.dependency_overrides[get_settings] = \ |
|
|
|
self.get_settings_override |
|
|
@@ -581,7 +610,7 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase): |
|
|
|
|
|
|
|
self.database = data.databases.Database('sqlite:///' + |
|
|
|
self.dbtempfile.name) |
|
|
|
self.data = data.make_orm(self.database) |
|
|
|
self.data = make_orm(self.database) |
|
|
|
|
|
|
|
def tearDown(self): |
|
|
|
self.data = None |
|
|
@@ -602,3 +631,70 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase): |
|
|
|
key='thisisanapikey')).user, 'foo') |
|
|
|
self.assertEqual((await data.APIKey.objects.get( |
|
|
|
key='anotherlongapikey')).user, 'bar') |
|
|
|
|
|
|
|
@patch.dict(os.environ, dict(BITELAB_URL='http://someserver/')) |
|
|
|
@patch.dict(os.environ, dict(BITELAB_AUTH='thisisanapikey')) |
|
|
|
class TestClient(unittest.TestCase): |
|
|
|
def setUp(self): |
|
|
|
self.ac_patcher = patch(__name__ + '.AsyncClient') |
|
|
|
self.ac = self.ac_patcher.start() |
|
|
|
self.addCleanup(self.ac_patcher.stop) |
|
|
|
|
|
|
|
self.acg = self.ac.return_value.get = AsyncMock() |
|
|
|
self.acgr = self.acg.return_value = Mock() |
|
|
|
|
|
|
|
def runMain(self): |
|
|
|
stdout = StringIO() |
|
|
|
with patch.dict(sys.__dict__, dict(stdout=stdout)): |
|
|
|
main() |
|
|
|
|
|
|
|
return stdout.getvalue() |
|
|
|
|
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'list' ])) |
|
|
|
def test_list(self): |
|
|
|
ac = self.ac |
|
|
|
acg = self.acg |
|
|
|
acg.return_value.status_code = HTTP_200_OK |
|
|
|
acg.return_value.json.return_value = { 'cora-z7s': { |
|
|
|
'arch': 'arm-armv7', 'clsname': 'cora-z7s', }} |
|
|
|
|
|
|
|
stdout = self.runMain() |
|
|
|
|
|
|
|
output = '''Classes: |
|
|
|
cora-z7s |
|
|
|
''' |
|
|
|
|
|
|
|
self.assertEqual(stdout, output) |
|
|
|
|
|
|
|
ac.assert_called_with(base_url='http://someserver/') |
|
|
|
|
|
|
|
acg.assert_called_with('board/classes', auth=BiteAuth('thisisanapikey')) |
|
|
|
|
|
|
|
# XXX - add error cases for UI |
|
|
|
|
|
|
|
@patch.dict(sys.__dict__, dict(argv=[ '', 'reserve', 'cora-z7s' ])) |
|
|
|
def test_reserve(self): |
|
|
|
ac = self.ac |
|
|
|
acg = self.acg |
|
|
|
acg.return_value.status_code = HTTP_200_OK |
|
|
|
acg.return_value.json.return_value = Board(name='cora-1', |
|
|
|
brdclass='cora-z7s', reserved=True, |
|
|
|
attrs={ |
|
|
|
'power': False, |
|
|
|
'ip': '172.20.20.5', |
|
|
|
}).dict() |
|
|
|
|
|
|
|
stdout = self.runMain() |
|
|
|
|
|
|
|
output = '''Name:\tcora-1 |
|
|
|
Class:\tcora-z7s |
|
|
|
Attributes: |
|
|
|
\tpower\tFalse |
|
|
|
\tip\t172.20.20.5 |
|
|
|
''' |
|
|
|
|
|
|
|
self.assertEqual(stdout, output) |
|
|
|
|
|
|
|
ac.assert_called_with(base_url='http://someserver/') |
|
|
|
|
|
|
|
acg.assert_called_with('board/cora-z7s/reserve', auth=BiteAuth('thisisanapikey')) |