Browse Source

add starting point of UI...

main
John-Mark Gurney 4 years ago
parent
commit
3b8ef85052
4 changed files with 147 additions and 30 deletions
  1. +123
    -27
      bitelab/__init__.py
  2. +3
    -2
      bitelab/config.py
  3. +21
    -0
      bitelab/data.py
  4. +0
    -1
      setup.py

+ 123
- 27
bitelab/__init__.py View File

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

+ 3
- 2
bitelab/config.py View File

@@ -1,4 +1,4 @@
from pydantic import BaseSettings
from pydantic import BaseSettings, Field

import asyncio
import aiokq
@@ -6,7 +6,8 @@ import aiokq
# How to deal w/ private vars:
# https://web.archive.org/web/20201113005838/https://github.com/samuelcolvin/pydantic/issues/655
class Settings(BaseSettings):
db_file: str
db_file: str = Field(description='path to SQLite3 database file')
setup_script: str = Field(description='script that will initalize an environment')

class Config:
env_file = ".env"

+ 21
- 0
bitelab/data.py View File

@@ -1,8 +1,29 @@
from typing import Optional, Union, Dict, Any
import databases
from pydantic import BaseModel
from datetime import datetime
import orm
import sqlalchemy

__all__ = [ 'make_orm', 'BoardClassInfo', 'Board', 'Error' ]

class BoardClassInfo(BaseModel):
clsname: str
arch: str

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]

def _issubclass(a, b):
try:
return issubclass(a, b)


+ 0
- 1
setup.py View File

@@ -25,7 +25,6 @@ setup(
'aiokq @ git+https://www.funkthat.com/gitea/jmg/aiokq.git'
'orm',
'databases[sqlite]',
'mock',
],
extras_require = {
'dev': [ 'coverage' ],


Loading…
Cancel
Save