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 typing import Optional, Union, Dict, Any
from dataclasses import dataclass from dataclasses import dataclass
from functools import lru_cache, wraps from functools import lru_cache, wraps
from io import StringIO


from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from httpx import AsyncClient, Auth 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.responses import JSONResponse
from starlette.status import HTTP_200_OK from starlette.status import HTTP_200_OK
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, \ from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, \
HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT


from . import config from . import config
from . import data
from .data import *


import asyncio import asyncio
import gc
import orm import orm
import os
import socket import socket
import sqlite3 import sqlite3
import sys import sys
import tempfile import tempfile
import unittest import unittest
import urllib


# fix up parse_socket_addr for hypercorn # fix up parse_socket_addr for hypercorn
from hypercorn.utils import parse_socket_addr 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) # return await _snmpwrapper('set', host, oid, value=value)


class Attribute: 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 defattrname = None


async def getvalue(self): # pragma: no cover
raise NotImplementedError

async def setvalue(self): # pragma: no cover
raise NotImplementedError

class Power(Attribute): class Power(Attribute):
defattrname = 'power' defattrname = 'power'


@@ -72,10 +85,6 @@ class SNMPPower(Power):
#async def setvalue(self, v): #async def setvalue(self, v):
# pass # pass


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

class BoardImpl: class BoardImpl:
def __init__(self, name, brdclass, options): def __init__(self, name, brdclass, options):
self.name = name self.name = name
@@ -107,19 +116,6 @@ class BoardImpl:
def attrs(self): def attrs(self):
return dict(self.attrcache) 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 @dataclass
class BITEError(Exception): class BITEError(Exception):
errobj: Error errobj: Error
@@ -180,6 +176,9 @@ class BiteAuth(Auth):
def __init__(self, token): def __init__(self, token):
self.token = token self.token = token


def __eq__(self, o):
return self.token == o.token

def auth_flow(self, request): def auth_flow(self, request):
request.headers['Authorization'] = 'Bearer ' + self.token request.headers['Authorization'] = 'Bearer ' + self.token
yield request yield request
@@ -201,7 +200,7 @@ def get_settings(): # pragma: no cover
def get_data(settings: config.Settings = Depends(get_settings)): # pragma: no cover def get_data(settings: config.Settings = Depends(get_settings)): # pragma: no cover
print(repr(settings)) print(repr(settings))
database = data.databases.Database('sqlite:///' + settings.db_file) database = data.databases.Database('sqlite:///' + settings.db_file)
d = data.make_orm(self.database)
d = make_orm(self.database)
return d return d


@unhashable_lru() @unhashable_lru()
@@ -244,11 +243,12 @@ async def get_board_info(board_id, user: str = Depends(lookup_user),


return brd 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), brdmgr: BoardManager = Depends(get_boardmanager),
brdlck: asyncio.Lock = Depends(get_board_lock), brdlck: asyncio.Lock = Depends(get_board_lock),
data: data.DataWrapper = Depends(get_data)): data: data.DataWrapper = Depends(get_data)):
board_id = board_id_or_class
brd = brdmgr.boards[board_id] brd = brdmgr.boards[board_id]


async with brd.lock: async with brd.lock:
@@ -322,6 +322,33 @@ def getApp():
# uvicorn can't call the above function, while hypercorn can # uvicorn can't call the above function, while hypercorn can
#app = getApp() #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): class TestUnhashLRU(unittest.TestCase):
def test_unhashlru(self): def test_unhashlru(self):
lsta = [] lsta = []
@@ -369,12 +396,14 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
self.dbtempfile = tempfile.NamedTemporaryFile() self.dbtempfile = tempfile.NamedTemporaryFile()
self.database = data.databases.Database('sqlite:///' + self.database = data.databases.Database('sqlite:///' +
self.dbtempfile.name) self.dbtempfile.name)
self.data = data.make_orm(self.database)
self.data = make_orm(self.database)


await _setup_data(self.data) await _setup_data(self.data)


# setup settings # 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.app.dependency_overrides[get_settings] = \
self.get_settings_override self.get_settings_override
@@ -581,7 +610,7 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase):


self.database = data.databases.Database('sqlite:///' + self.database = data.databases.Database('sqlite:///' +
self.dbtempfile.name) self.dbtempfile.name)
self.data = data.make_orm(self.database)
self.data = make_orm(self.database)


def tearDown(self): def tearDown(self):
self.data = None self.data = None
@@ -602,3 +631,70 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase):
key='thisisanapikey')).user, 'foo') key='thisisanapikey')).user, 'foo')
self.assertEqual((await data.APIKey.objects.get( self.assertEqual((await data.APIKey.objects.get(
key='anotherlongapikey')).user, 'bar') 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 asyncio
import aiokq import aiokq
@@ -6,7 +6,8 @@ import aiokq
# How to deal w/ private vars: # How to deal w/ private vars:
# https://web.archive.org/web/20201113005838/https://github.com/samuelcolvin/pydantic/issues/655 # https://web.archive.org/web/20201113005838/https://github.com/samuelcolvin/pydantic/issues/655
class Settings(BaseSettings): 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: class Config:
env_file = ".env" env_file = ".env"

+ 21
- 0
bitelab/data.py View File

@@ -1,8 +1,29 @@
from typing import Optional, Union, Dict, Any
import databases import databases
from pydantic import BaseModel
from datetime import datetime from datetime import datetime
import orm import orm
import sqlalchemy 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): def _issubclass(a, b):
try: try:
return issubclass(a, b) 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' 'aiokq @ git+https://www.funkthat.com/gitea/jmg/aiokq.git'
'orm', 'orm',
'databases[sqlite]', 'databases[sqlite]',
'mock',
], ],
extras_require = { extras_require = {
'dev': [ 'coverage' ], 'dev': [ 'coverage' ],


Loading…
Cancel
Save