From d7a8d7b5fb65c4443d1350d04b3544d7d8a8f383 Mon Sep 17 00:00:00 2001 From: Paul Cuzner Date: Mon, 30 Nov 2020 12:06:00 +1300 Subject: [PATCH] cephadm: add unit tests for cephadm-exporter mode Added unit tests to the test_cephadm module to cover key functionality in the exporter. In addition, since the tests are using fixtures, a new fixture module has been added to with code structure going forward. The unit tests cover object properties, http responses and thread exception handling. Signed-off-by: Paul Cuzner --- src/cephadm/tests/fixtures.py | 41 ++++++ src/cephadm/tests/test_cephadm.py | 221 ++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 src/cephadm/tests/fixtures.py diff --git a/src/cephadm/tests/fixtures.py b/src/cephadm/tests/fixtures.py new file mode 100644 index 0000000000000..71ccfb2422788 --- /dev/null +++ b/src/cephadm/tests/fixtures.py @@ -0,0 +1,41 @@ + +import mock +from mock import patch +import pytest + +import os +import time + +with patch('builtins.open', create=True): + from importlib.machinery import SourceFileLoader + cd = SourceFileLoader('cephadm', 'cephadm').load_module() + + +def _daemon_path(): + return os.getcwd() + + +def _mock_scrape_host(obj, interval): + try: + raise ValueError("wah") + except Exception as e: + obj._handle_thread_exception(e, 'host') + + +def _mock_run(obj): + t = obj._create_thread(obj._scrape_host_facts, 'host', 5) + time.sleep(1) + if not t.is_alive(): + obj.cephadm_cache.update_health('host', "inactive", "host thread stopped") + + +@pytest.fixture +def exporter(): + with mock.patch('cephadm.CephadmDaemon.daemon_path', _daemon_path()), \ + mock.patch('cephadm.CephadmDaemon.can_run', return_value=True), \ + mock.patch('cephadm.CephadmDaemon.run', _mock_run), \ + mock.patch('cephadm.CephadmDaemon._scrape_host_facts', _mock_scrape_host): + + exporter = cd.CephadmDaemon(fsid='foobar', daemon_id='test') + assert exporter.token == 'MyAccessToken' + yield exporter diff --git a/src/cephadm/tests/test_cephadm.py b/src/cephadm/tests/test_cephadm.py index c9e2769ed33ca..0a8829779f82c 100644 --- a/src/cephadm/tests/test_cephadm.py +++ b/src/cephadm/tests/test_cephadm.py @@ -4,9 +4,16 @@ from mock import patch import os import sys import unittest +import threading +import time +from http.server import HTTPServer +from urllib.request import Request, urlopen +from urllib.error import HTTPError import pytest +from .fixtures import exporter + with patch('builtins.open', create=True): from importlib.machinery import SourceFileLoader cd = SourceFileLoader('cephadm', 'cephadm').load_module() @@ -361,3 +368,217 @@ class TestCustomContainer(unittest.TestCase): 'ro=true' ] ]) + + +class TestCephadmExporter(object): + exporter: cd.CephadmDaemon + files_created = [] + crt = """-----BEGIN CERTIFICATE----- +MIIC1zCCAb8CEFHoZE2MfUVzo53fzzBKAT0wDQYJKoZIhvcNAQENBQAwKjENMAsG +A1UECgwEQ2VwaDEZMBcGA1UECwwQY2VwaGFkbS1leHBvcnRlcjAeFw0yMDExMjUy +MzEwNTVaFw0zMDExMjMyMzEwNTVaMCoxDTALBgNVBAoMBENlcGgxGTAXBgNVBAsM +EGNlcGhhZG0tZXhwb3J0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCsTfcJcXbREqfx1zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmF +BTdAcNl7V0U+z4EsGJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9A +B5fNswrUXViku5Y2jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7k +O5+0VbdzcOdu37DFpoE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGX +ZTTG/Zf/a+wuCjtMG3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAk +IOz4SbP6Q25K99Czm1K+3kMLAgMBAAEwDQYJKoZIhvcNAQENBQADggEBACmtvZb8 +dJGHx/WC0/JHxnEJCJM2qnn87ELzbbIQL1w1Yb/I6JQYPgq+WiQPaHaLL9eYsm0l +dFwvrh+WC0JpXDfADnUnkTSB/WpZ2nC+2JxBptrQEuIcqNXpcJd0bKDiHunv04JI +uEVpTAK05dBV38qNmIlu4HyB4OEnuQpyOr9xpIhdxuJ95O9K0j5BIw98ZaEwYNUP +Rm3YlQwfS6R5xaBvL9kyfxyAD2joNj44q6w/5zj4egXVIA5VpkQm8DmMtu0Pd2NG +dzfYRmqrDolh+rty8HiyIxzeDJQ5bj6LKbUkmABvX50nDySVyMfHmt461/n7W65R +CHFLoOmfJJik+Uc=\n-----END CERTIFICATE----- +""" + key = """-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsTfcJcXbREqfx +1zTUuEmK+lJn9WWjk0URRF1Z+QgPkascNdkX16PnvhbGwXmFBTdAcNl7V0U+z4Es +GJ7hJsB7qTq6Rb6wNl7r0OxjeWOmB9xbF4Q/KR5yrbM1DA9AB5fNswrUXViku5Y2 +jlOAz+ZMBhYxMx0edqhxSn297j04Z6RF4Mvkc43v0FH7Ju7kO5+0VbdzcOdu37DF +poE4Ll2MZ/GuAHcJ8SD06sEdzFEjRCraav976743XcUlhZGXZTTG/Zf/a+wuCjtM +G3od7vRFfuRrM5oTE133DuQ5deR7ybcZNDyopDjHF8xB1bAkIOz4SbP6Q25K99Cz +m1K+3kMLAgMBAAECggEASnAwToMXWsGdjqxzpYasNv9oBIOO0nk4OHp5ffpJUjiT +XM+ip1tA80g7HMjPD/mt4gge3NtaDgWlf4Bve0O7mnEE7x5cgFIs9eG/jkYOF9eD +ilMBjivcfJywNDWujPH60iIMhqyBNEHaZl1ck+S9UJC8m6rCZLvMj40n/5riFfBy +1sjf2uOwcfWrjSj9Ju4wlMI6khSSz2aYC7glQQ/fo2+YArbEUcy60iloPQ6wEgZK +okoVWZA9AehwLcnRjkwd9EVmMMtRGPE/AcP4s/kKA0tRDRicPLN727Ke/yxv+Ppo +hbIZIcOn7soOFAENcodJ4YRSCd++QfCNaVAi7vwWWQKBgQDeBY4vvr+H0brbSjQg +O7Fpqub/fxZY3UoHWDqWs2X4o3qhDqaTQODpuYtCm8YQE//55JoLWKAD0evq5dLS +YLrtC1Vyxf+TA7opCUjWBe+liyndbJdB5q0zF7qdWUtQKGVSWyUWhK8gHa6M64fP +oi83DD7F0OGusTWGtfbceErk/wKBgQDGrJLRo/5xnAH5VmPfNu+S6h0M2qM6CYwe +Y5wHFG2uQQct73adf53SkhvZVmOzJsWQbVnlDOKMhqazcs+7VWRgO5X3naWVcctE +Hggw9MgpbXAWFOI5sNYsCYE58E+fTHjE6O4A3MhMCsze+CIC3sKuPQBBiL9bWSOX +8POswqfl9QKBgDe/nVxPwTgRaaH2l/AgDQRDbY1qE+psZlJBzTRaB5jPM9ONIjaH +a/JELLuk8a7H1tagmC2RK1zKMTriSnWY5FbxKZuQLAR2QyBavHdBNlOTBggbZD+f +9I2Hv8wSx95wxkBPsphc6Lxft5ya55czWjewU3LIaGK9DHuu5TWm3udxAoGBAJGP +PsJ59KIoOwoDUYjpJv3sqPwR9CVBeXeKY3aMcQ+KdUgiejVKmsb8ZYsG0GUhsv3u +ID7BAfsTbG9tXuVR2wjmnymcRwUHKnXtyvKTZVN06vpCsryx4zjAff2FI9ECpjke +r8HSAK41+4QhKEoSC3C9IMLi/dBfrsRTtTSOKZVBAoGBAI2dl5HEIFpufaI4toWM +LO5HFrlXgRDGoc/+Byr5/8ZZpYpU115Ol/q6M+l0koV2ygJ9jeJJEllFWykIDS6F +XxazFI74swAqobHb2ZS/SLhoVxE82DdSeXrjkTvUjNtrW5zs1gIMKBR4nD6H8AqL +iMN28C2bKGao5UHvdER1rGy7 +-----END PRIVATE KEY----- +""" + token = "MyAccessToken" + + @classmethod + def setup_class(cls): + # create the ssl files + fname = os.path.join(os.getcwd(), 'crt') + with open(fname, 'w') as crt: + crt.write(cls.crt) + cls.files_created.append(fname) + fname = os.path.join(os.getcwd(), 'key') + with open(fname, 'w') as crt: + crt.write(cls.key) + cls.files_created.append(fname) + fname = os.path.join(os.getcwd(), 'token') + with open(fname, 'w') as crt: + crt.write(cls.token) + cls.files_created.append(fname) + # start a simple http instance to test the requesthandler + cls.server = HTTPServer(('0.0.0.0', 9443), cd.CephadmDaemonHandler) + cls.server.cephadm_cache = cd.CephadmCache() + cls.server.token = cls.token + t = threading.Thread(target=cls.server.serve_forever) + t.daemon = True + t.start() + + @classmethod + def teardown_class(cls): + cls.server.shutdown() + assert len(cls.files_created) > 0 + for f in cls.files_created: + os.remove(f) + + def setup_method(self): + # re-init the cache for every test + TestCephadmExporter.server.cephadm_cache = cd.CephadmCache() + + def teardown_method(self): + pass + + def test_files_ready(self): + assert os.path.exists(os.path.join(os.getcwd(), 'crt')) + assert os.path.exists(os.path.join(os.getcwd(), 'key')) + assert os.path.exists(os.path.join(os.getcwd(), 'token')) + + def test_can_run(self, exporter): + assert exporter.can_run + + def test_token_valid(self, exporter): + assert exporter.token == self.token + + def test_unit_name(self,exporter): + assert exporter.unit_name + assert exporter.unit_name == "ceph-foobar-cephadm-exporter.test.service" + + def test_unit_run(self,exporter): + assert exporter.unit_run + lines = exporter.unit_run.split('\n') + assert len(lines) == 2 + assert "/var/lib/ceph/foobar/cephadm exporter --fsid foobar --id test --port 9443 &" in lines[1] + + def test_binary_path(self, exporter): + # fsid = foobar + args = cd._parse_args([]) + cd.args = args + assert exporter.binary_path == "/var/lib/ceph/foobar/cephadm" + + def test_systemd_unit(self, exporter): + assert exporter.unit_file + + def test_validate_passes(self, exporter): + config = { + "crt": self.crt, + "key": self.key, + "token": self.token, + } + cd.CephadmDaemon.validate_config(config) + + def test_validate_fails(self, exporter): + config = { + "key": self.key, + "token": self.token, + } + with pytest.raises(cd.Error): + cd.CephadmDaemon.validate_config(config) + + def test_port_active(self, exporter): + assert exporter.port_active == True + + def test_rqst_health_200(self): + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata/health",headers=hdrs) + r = urlopen(req) + assert r.status == 200 + + def test_rqst_all_inactive_500(self): + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata",headers=hdrs) + try: + r = urlopen(req) + except HTTPError as e: + assert e.code == 500 + + def test_rqst_no_auth_401(self): + req=Request("http://localhost:9443/v1/metadata") + try: + urlopen(req) + except HTTPError as e: + assert e.code == 401 + + def test_rqst_bad_auth_401(self): + hdrs={"Authorization":f"Bearer BogusAuthToken"} + req=Request("http://localhost:9443/v1/metadata",headers=hdrs) + try: + urlopen(req) + except HTTPError as e: + assert e.code == 401 + + def test_rqst_badURL_404(self): + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metazoic",headers=hdrs) + try: + urlopen(req) + except HTTPError as e: + assert e.code == 404 + + def test_rqst_inactive_task_204(self): + # all tasks initialise as inactive, and then 'go' active as their thread starts + # so we can pick any task to check for an inactive response (no content) + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs) + r = urlopen(req) + assert r.status == 204 + + def test_rqst_active_task_200(self): + TestCephadmExporter.server.cephadm_cache.tasks['host'] = 'active' + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata/host",headers=hdrs) + r = urlopen(req) + assert r.status == 200 + + def test_rqst_all_206(self): + TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active' + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata",headers=hdrs) + r = urlopen(req) + assert r.status == 206 + + def test_rqst_disks_200(self): + TestCephadmExporter.server.cephadm_cache.tasks['disks'] = 'active' + hdrs={"Authorization":f"Bearer {TestCephadmExporter.token}"} + req=Request("http://localhost:9443/v1/metadata/disks",headers=hdrs) + r = urlopen(req) + assert r.status == 200 + + def test_thread_exception(self, exporter): + # run is patched to invoke a mocked scrape_host thread that will raise so + # we check here that the exception handler updates the cache object as we'd + # expect with the error + exporter.run() + assert exporter.cephadm_cache.host['scrape_errors'] + assert exporter.cephadm_cache.host['scrape_errors'] == ['ValueError exception: wah'] + assert exporter.cephadm_cache.errors == ['host thread stopped'] -- 2.39.5