| @@ -2,7 +2,7 @@ MODULES=bitelab | |||
| VIRTUALENV?=virtualenv-3.8 | |||
| 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: | |||
| ($(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 .data import * | |||
| from .abstract import * | |||
| from .snmp import * | |||
| from .mocks import * | |||
| import asyncio | |||
| import json | |||
| @@ -67,57 +70,6 @@ def new_parse_socket_addr(domain, 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: | |||
| def __init__(self, name, brdclass, options): | |||
| 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='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): | |||
| # 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 | |||
| @@ -570,7 +511,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase): | |||
| 'arch': 'arm-armv7', 'clsname': 'cora-z7s', }) }) | |||
| @patch('asyncio.create_subprocess_exec') | |||
| @patch('bitelab.snmpget') | |||
| @patch('bitelab.snmp.snmpget') | |||
| async def test_board_reserve_release(self, sg, cse): | |||
| # that when releasing a board that is not yet reserved | |||
| res = await self.client.post('/board/cora-1/release', | |||
| @@ -583,7 +524,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase): | |||
| sg.return_value = False | |||
| # 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 | |||
| res = await self.client.post('/board/cora-1/reserve', | |||
| @@ -607,7 +548,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase): | |||
| stderr=subprocess.PIPE) | |||
| # 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')) | |||
| # that reserving the board | |||
| @@ -686,7 +627,7 @@ class TestBiteLab(unittest.IsolatedAsyncioTestCase): | |||
| # that it is successful | |||
| self.assertEqual(res.status_code, HTTP_200_OK) | |||
| @patch('bitelab.snmpget') | |||
| @patch('bitelab.snmp.snmpget') | |||
| async def test_board_info(self, sg): | |||
| # that when snmpget returns False | |||
| sg.return_value = False | |||
| @@ -765,57 +706,6 @@ class TestDatabase(unittest.IsolatedAsyncioTestCase): | |||
| self.assertEqual((await data.APIKey.objects.get( | |||
| 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_AUTH='thisisanapikey')) | |||
| 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 | |||