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