Browse Source

refactor things a bit.. break out from one large file..

main
John-Mark Gurney 4 years ago
parent
commit
12a6162d4b
6 changed files with 217 additions and 118 deletions
  1. +1
    -1
      Makefile
  2. +7
    -117
      bitelab/__init__.py
  3. +18
    -0
      bitelab/abstract.py
  4. +40
    -0
      bitelab/mocks.py
  5. +120
    -0
      bitelab/snmp.py
  6. +31
    -0
      bitelab/testing.py

+ 1
- 1
Makefile View File

@@ -2,7 +2,7 @@ MODULES=bitelab
VIRTUALENV?=virtualenv-3.8 VIRTUALENV?=virtualenv-3.8


test: test:
(ls $(MODULES)/*.py | entr sh -c 'python -m coverage run -m unittest $(basename $(MODULES)) && coverage report --omit=p/\* -m -i')
(ls $(MODULES)/*.py | entr sh -c 'python -m coverage run -m unittest $(basename $(MODULES)).testing && coverage report --omit=p/\* -m -i')


env: env:
($(VIRTUALENV) p && . ./p/bin/activate && pip install -r requirements.txt) ($(VIRTUALENV) p && . ./p/bin/activate && pip install -r requirements.txt)


+ 7
- 117
bitelab/__init__.py View File

@@ -43,6 +43,9 @@ from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR


from . import config from . import config
from .data import * from .data import *
from .abstract import *
from .snmp import *
from .mocks import *


import asyncio import asyncio
import json import json
@@ -67,57 +70,6 @@ def new_parse_socket_addr(domain, addr):


tcp_server.parse_socket_addr = new_parse_socket_addr tcp_server.parse_socket_addr = new_parse_socket_addr


async def snmpget(host, oid, type):
p = await asyncio.create_subprocess_exec('snmpget', '-Oqv', host, oid,
stdout=subprocess.PIPE)

res = (await p.communicate())[0].strip()

if type == 'bool':
if res == b'true':
return True
elif res == b'false':
return False

raise RuntimeError('unknown results for bool: %s' % repr(res))

raise RuntimeError('unknown type: %s' % repr(type))

async def snmpset(host, oid, value):
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, v): # pragma: no cover
raise NotImplementedError

class Power(Attribute):
defattrname = 'power'

class SNMPPower(Power):
def __init__(self, host, port):
self.host = host
self.port = port

# Future - add caching + invalidation on set
async def getvalue(self):
return await snmpget(self.host,
'pethPsePortAdminEnable.1.%d' % self.port, 'bool')

async def setvalue(self, v):
return await snmpset(self.host,
'pethPsePortAdminEnable.1.%d' % self.port, v)

class BoardImpl: class BoardImpl:
def __init__(self, name, brdclass, options): def __init__(self, name, brdclass, options):
self.name = name self.name = name
@@ -485,17 +437,6 @@ async def _setup_data(data):
await data.APIKey.objects.create(user='foo', key='thisisanapikey') await data.APIKey.objects.create(user='foo', key='thisisanapikey')
await data.APIKey.objects.create(user='bar', key='anotherlongapikey') await data.APIKey.objects.create(user='bar', key='anotherlongapikey')


def _wrap_subprocess_exec(mockobj, stdout=b'', stderr=b'', retcode=0):
assert isinstance(stdout, bytes)
assert isinstance(stderr, bytes)
proc = Mock()
proc.communicate = AsyncMock()
proc.communicate.return_value = (stdout, stderr)
proc.wait = AsyncMock()
proc.wait.return_value = retcode
proc.returncode = retcode
mockobj.return_value = proc

# Per RFC 5737 (https://tools.ietf.org/html/rfc5737): # Per RFC 5737 (https://tools.ietf.org/html/rfc5737):
# The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), # The blocks 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2),
# and 203.0.113.0/24 (TEST-NET-3) are provided for use in # and 203.0.113.0/24 (TEST-NET-3) are provided for use in
@@ -570,7 +511,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
'arch': 'arm-armv7', 'clsname': 'cora-z7s', }) }) 'arch': 'arm-armv7', 'clsname': 'cora-z7s', }) })


@patch('asyncio.create_subprocess_exec') @patch('asyncio.create_subprocess_exec')
@patch('bitelab.snmpget')
@patch('bitelab.snmp.snmpget')
async def test_board_reserve_release(self, sg, cse): async def test_board_reserve_release(self, sg, cse):
# that when releasing a board that is not yet reserved # that when releasing a board that is not yet reserved
res = await self.client.post('/board/cora-1/release', res = await self.client.post('/board/cora-1/release',
@@ -583,7 +524,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
sg.return_value = False sg.return_value = False


# that when the setup script will fail # that when the setup script will fail
_wrap_subprocess_exec(cse, stderr=b'error', retcode=1)
wrap_subprocess_exec(cse, stderr=b'error', retcode=1)


# that reserving the board # that reserving the board
res = await self.client.post('/board/cora-1/reserve', res = await self.client.post('/board/cora-1/reserve',
@@ -607,7 +548,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
stderr=subprocess.PIPE) stderr=subprocess.PIPE)


# that when the setup script returns # that when the setup script returns
_wrap_subprocess_exec(cse,
wrap_subprocess_exec(cse,
json.dumps(dict(ip='192.0.2.10')).encode('utf-8')) json.dumps(dict(ip='192.0.2.10')).encode('utf-8'))


# that reserving the board # that reserving the board
@@ -686,7 +627,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase):
# that it is successful # that it is successful
self.assertEqual(res.status_code, HTTP_200_OK) self.assertEqual(res.status_code, HTTP_200_OK)


@patch('bitelab.snmpget')
@patch('bitelab.snmp.snmpget')
async def test_board_info(self, sg): async def test_board_info(self, sg):
# that when snmpget returns False # that when snmpget returns False
sg.return_value = False sg.return_value = False
@@ -765,57 +706,6 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase):
self.assertEqual((await data.APIKey.objects.get( self.assertEqual((await data.APIKey.objects.get(
key='anotherlongapikey')).user, 'bar') key='anotherlongapikey')).user, 'bar')


class TestSNMPWrapper(unittest.IsolatedAsyncioTestCase):
@patch('asyncio.create_subprocess_exec')
async def test_snmpwrapper(self, cse):
_wrap_subprocess_exec(cse, b'false\n')

r = await snmpget('somehost', 'snmpoid', 'bool')

self.assertEqual(r, False)

cse.assert_called_with('snmpget', '-Oqv', 'somehost',
'snmpoid', stdout=subprocess.PIPE)

_wrap_subprocess_exec(cse, b'true\n')
r = await snmpget('somehost', 'snmpoid', 'bool')

self.assertEqual(r, True)

# that a bogus return value
_wrap_subprocess_exec(cse, b'bogus\n')

# raises an error
with self.assertRaises(RuntimeError):
await snmpget('somehost', 'snmpoid', 'bool')

# that an unknown type, raises an error
with self.assertRaises(RuntimeError):
await snmpget('somehost', 'snmpoid', 'randomtype')


class TestSNMPPower(unittest.IsolatedAsyncioTestCase):
@patch('bitelab.snmpset')
@patch('bitelab.snmpget')
async def test_snmppower(self, sg, ss):
sp = SNMPPower('host', 5)

# that when snmpget returns False
sg.return_value = False

self.assertFalse(await sp.getvalue())

# calls snmpget w/ the correct args
sg.assert_called_with('host', 'pethPsePortAdminEnable.1.5',
'bool')

# that when setvalue is called
await sp.setvalue(True)

# calls snmpset w/ the correct args
ss.assert_called_with('host', 'pethPsePortAdminEnable.1.5',
True)

@patch.dict(os.environ, dict(BITELAB_URL='http://someserver/')) @patch.dict(os.environ, dict(BITELAB_URL='http://someserver/'))
@patch.dict(os.environ, dict(BITELAB_AUTH='thisisanapikey')) @patch.dict(os.environ, dict(BITELAB_AUTH='thisisanapikey'))
class TestClient(unittest.TestCase): class TestClient(unittest.TestCase):


+ 18
- 0
bitelab/abstract.py View File

@@ -0,0 +1,18 @@
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, v): # pragma: no cover
raise NotImplementedError

class Power(Attribute):
defattrname = 'power'


+ 40
- 0
bitelab/mocks.py View File

@@ -0,0 +1,40 @@
#
# Copyright (c) 2020 The FreeBSD Foundation
#
# This software1 was developed by John-Mark Gurney under sponsorship
# from the FreeBSD Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

from unittest.mock import AsyncMock, Mock

def wrap_subprocess_exec(mockobj, stdout=b'', stderr=b'', retcode=0):
assert isinstance(stdout, bytes)
assert isinstance(stderr, bytes)
proc = Mock()
proc.communicate = AsyncMock()
proc.communicate.return_value = (stdout, stderr)
proc.wait = AsyncMock()
proc.wait.return_value = retcode
proc.returncode = retcode
mockobj.return_value = proc

+ 120
- 0
bitelab/snmp.py View File

@@ -0,0 +1,120 @@
#
# Copyright (c) 2020 The FreeBSD Foundation
#
# This software1 was developed by John-Mark Gurney under sponsorship
# from the FreeBSD Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

from unittest.mock import patch

from .abstract import *
from .mocks import *

import asyncio
import subprocess
import unittest

async def snmpget(host, oid, type):
p = await asyncio.create_subprocess_exec('snmpget', '-Oqv', host, oid,
stdout=subprocess.PIPE)

res = (await p.communicate())[0].strip()

if type == 'bool':
if res == b'true':
return True
elif res == b'false':
return False

raise RuntimeError('unknown results for bool: %s' % repr(res))

raise RuntimeError('unknown type: %s' % repr(type))

async def snmpset(host, oid, value):
return await _snmpwrapper('set', host, oid, value=value)

class SNMPPower(Power):
def __init__(self, host, port):
self.host = host
self.port = port

# Future - add caching + invalidation on set
async def getvalue(self):
return await snmpget(self.host,
'pethPsePortAdminEnable.1.%d' % self.port, 'bool')

async def setvalue(self, v):
return await snmpset(self.host,
'pethPsePortAdminEnable.1.%d' % self.port, v)


class TestSNMPWrapper(unittest.IsolatedAsyncioTestCase):
@patch('asyncio.create_subprocess_exec')
async def test_snmpwrapper(self, cse):
wrap_subprocess_exec(cse, b'false\n')

r = await snmpget('somehost', 'snmpoid', 'bool')

self.assertEqual(r, False)

cse.assert_called_with('snmpget', '-Oqv', 'somehost',
'snmpoid', stdout=subprocess.PIPE)

wrap_subprocess_exec(cse, b'true\n')
r = await snmpget('somehost', 'snmpoid', 'bool')

self.assertEqual(r, True)

# that a bogus return value
wrap_subprocess_exec(cse, b'bogus\n')

# raises an error
with self.assertRaises(RuntimeError):
await snmpget('somehost', 'snmpoid', 'bool')

# that an unknown type, raises an error
with self.assertRaises(RuntimeError):
await snmpget('somehost', 'snmpoid', 'randomtype')

class TestSNMPPower(unittest.IsolatedAsyncioTestCase):
@patch('bitelab.snmp.snmpset')
@patch('bitelab.snmp.snmpget')
async def test_snmppower(self, sg, ss):
sp = SNMPPower('host', 5)

# that when snmpget returns False
sg.return_value = False

self.assertFalse(await sp.getvalue())

# calls snmpget w/ the correct args
sg.assert_called_with('host', 'pethPsePortAdminEnable.1.5',
'bool')

# that when setvalue is called
await sp.setvalue(True)

# calls snmpset w/ the correct args
ss.assert_called_with('host', 'pethPsePortAdminEnable.1.5',
True)

+ 31
- 0
bitelab/testing.py View File

@@ -0,0 +1,31 @@
#
# Copyright (c) 2020 The FreeBSD Foundation
#
# This software1 was developed by John-Mark Gurney under sponsorship
# from the FreeBSD Foundation.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

# Module that includes all the test cases.

from . import TestClient, TestSNMPPower, TestSNMPWrapper, TestDatabase, TestBiteLab, TestUnhashLRU

Loading…
Cancel
Save