diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml new file mode 100644 index 0000000..83c83f6 --- /dev/null +++ b/.github/workflows/unittests.yaml @@ -0,0 +1,28 @@ +name: unittests + +on: + workflow_dispatch: + push: + branches: + - 'stable/1.0' + tags: + - 'v*' + pull_request: + branches: + - 'stable/1.0' + +jobs: + unuttest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y tox + - name: Run tox -e py3 + run: tox -e py3 diff --git a/staffeln/common/openstack.py b/staffeln/common/openstack.py index 988b847..68ed953 100644 --- a/staffeln/common/openstack.py +++ b/staffeln/common/openstack.py @@ -49,6 +49,7 @@ def set_project(self, project): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_user_id(self): user_name = self.conn.config.auth["username"] @@ -65,6 +66,7 @@ def get_user_id(self): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_projects(self): return self.conn.list_projects() @@ -72,6 +74,7 @@ def get_projects(self): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_servers(self, project_id=None, all_projects=True, details=True): if project_id is not None: @@ -84,6 +87,7 @@ def get_servers(self, project_id=None, all_projects=True, details=True): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_volume(self, uuid, project_id): return self.conn.get_volume_by_id(uuid) @@ -91,6 +95,7 @@ def get_volume(self, uuid, project_id): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_backup(self, uuid, project_id=None): # return conn.block_storage.get_backup( @@ -115,6 +120,7 @@ def create_backup(self, volume_id, project_id, force=True, wait=False): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def delete_backup(self, uuid, project_id=None, force=False): # Note(Alex): v3 is not supporting force delete? @@ -130,6 +136,7 @@ def delete_backup(self, uuid, project_id=None, force=False): @tenacity.retry( retry=RetryHTTPError(), wait=tenacity.wait_exponential(max=30), + reraise=True, stop=tenacity.stop_after_delay(CONF.conductor.retry_timeout)) def get_backup_quota(self, project_id): # quota = conn.get_volume_quotas(project_id) diff --git a/staffeln/tests/common/__init__.py b/staffeln/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/staffeln/tests/common/test_openstacksdk.py b/staffeln/tests/common/test_openstacksdk.py new file mode 100644 index 0000000..b69739c --- /dev/null +++ b/staffeln/tests/common/test_openstacksdk.py @@ -0,0 +1,304 @@ +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +from unittest import mock + +from openstack import exceptions as openstack_exc +import tenacity + +from staffeln.common import openstack as s_openstack +from staffeln.tests import base + + +class OpenstackSDKTest(base.TestCase): + + def setUp(self): + super(OpenstackSDKTest, self).setUp() + self.m_c = mock.MagicMock() + with mock.patch("openstack.connect", return_value=self.m_c): + self.openstack = s_openstack.OpenstackSDK() + self.m_sleep = mock.Mock() + func_list = [ + "get_user_id", + "get_projects", + "get_servers", + "get_volume", + "get_backup", + "delete_backup", + "get_backup_quota", + ] + for i in func_list: + getattr(self.openstack, i).retry.sleep = ( # pylint: disable=E1101 + self.m_sleep + ) + getattr(self.openstack, i).retry.stop = ( # pylint: disable=E1101 + tenacity.stop_after_attempt(2) + ) + + self.fake_user = mock.MagicMock(id="foo", email="foo@foo.com") + self.fake_volume = mock.MagicMock(id="fake_volume") + self.fake_backup = mock.MagicMock(id="fake_backup") + self.fake_role_assignment = mock.MagicMock(user="foo") + self.fake_role_assignment2 = mock.MagicMock(user={"id": "bar"}) + + def _test_http_error( + self, m_func, retry_func, status_code, call_count=1, **kwargs + ): + m_func.side_effect = openstack_exc.HttpException( + http_status=status_code + ) + exc = self.assertRaises( + openstack_exc.HttpException, + getattr(self.openstack, retry_func), + **kwargs, + ) + self.assertEqual(status_code, exc.status_code) + if status_code != 404: + if call_count == 1: + self.m_sleep.assert_called_once_with(1.0) + else: + self.m_sleep.assert_has_calls( + [mock.call(1.0) for c in range(call_count)] + ) + else: + self.m_sleep.assert_not_called() + + def _test_non_http_error(self, m_func, retry_func, **kwargs): + m_func.side_effect = KeyError + self.assertRaises( + KeyError, getattr(self.openstack, retry_func), **kwargs + ) + self.m_sleep.assert_not_called() + + def test_get_servers(self): + self.m_c.compute.servers = mock.MagicMock(return_value=[]) + self.assertEqual(self.openstack.get_servers(), []) + self.m_c.compute.servers.assert_called_once_with( + details=True, all_projects=True + ) + + def test_get_servers_non_http_error(self): + self._test_non_http_error(self.m_c.compute.servers, "get_servers") + + def test_get_servers_404_http_error(self): + self._test_http_error( + self.m_c.compute.servers, "get_servers", status_code=404 + ) + + def test_get_servers_500_http_error(self): + self._test_http_error( + self.m_c.compute.servers, "get_servers", status_code=500 + ) + + def test_get_projects(self): + self.m_c.list_projects = mock.MagicMock(return_value=[]) + self.assertEqual(self.openstack.get_projects(), []) + self.m_c.list_projects.assert_called_once_with() + + def test_get_projects_non_http_error(self): + self._test_non_http_error(self.m_c.list_projects, "get_projects") + + def test_get_projects_404_http_error(self): + self._test_http_error( + self.m_c.list_projects, "get_projects", status_code=404 + ) + + def test_get_projects_500_http_error(self): + self._test_http_error( + self.m_c.list_projects, "get_projects", status_code=500 + ) + + def test_get_user_id(self): + self.m_c.get_user = mock.MagicMock(return_value=self.fake_user) + self.assertEqual(self.openstack.get_user_id(), "foo") + self.m_c.get_user.assert_called_once_with(name_or_id=mock.ANY) + + def test_get_user_id_non_http_error(self): + self._test_non_http_error(self.m_c.get_user, "get_user_id") + + def test_get_user_id_404_http_error(self): + self._test_http_error( + self.m_c.get_user, "get_user_id", status_code=404 + ) + + def test_get_user_id_500_http_error(self): + self._test_http_error( + self.m_c.get_user, "get_user_id", status_code=500 + ) + + def test_get_volume(self): + self.m_c.get_volume_by_id = mock.MagicMock( + return_value=self.fake_volume + ) + self.assertEqual( + self.openstack.get_volume( + uuid=self.fake_volume.id, project_id="bar" + ), + self.fake_volume, + ) + self.m_c.get_volume_by_id.assert_called_once_with(self.fake_volume.id) + + def test_get_volume_non_http_error(self): + self._test_non_http_error( + self.m_c.get_volume_by_id, + "get_volume", + uuid="foo", + project_id="bar", + ) + + def test_get_volume_404_http_error(self): + self._test_http_error( + self.m_c.get_volume_by_id, + "get_volume", + status_code=404, + uuid="foo", + project_id="bar", + ) + + def test_get_volume_500_http_error(self): + self._test_http_error( + self.m_c.get_volume_by_id, + "get_volume", + status_code=500, + uuid="foo", + project_id="bar", + ) + + def test_get_backup(self): + self.m_c.get_volume_backup = mock.MagicMock( + return_value=self.fake_backup + ) + self.assertEqual( + self.openstack.get_backup( + uuid=self.fake_backup.id, project_id="bar" + ), + self.fake_backup, + ) + self.m_c.get_volume_backup.assert_called_once_with(self.fake_backup.id) + + def test_get_backup_not_found(self): + self.m_c.get_volume_backup = mock.MagicMock( + side_effect=openstack_exc.ResourceNotFound + ) + self.assertEqual( + self.openstack.get_backup( + uuid=self.fake_backup.id, project_id="bar" + ), + None, + ) + self.m_c.get_volume_backup.assert_called_once_with(self.fake_backup.id) + + def test_get_backup_non_http_error(self): + self._test_non_http_error( + self.m_c.get_volume_backup, + "get_backup", + uuid="foo", + project_id="bar", + ) + + def test_get_backup_404_http_error(self): + self._test_http_error( + self.m_c.get_volume_backup, + "get_backup", + status_code=404, + uuid="foo", + project_id="bar", + ) + + def test_get_backup_500_http_error(self): + self._test_http_error( + self.m_c.get_volume_backup, + "get_backup", + status_code=500, + uuid="foo", + project_id="bar", + ) + + def test_delete_backup(self): + self.m_c.delete_volume_backup = mock.MagicMock( + return_value=self.fake_backup + ) + self.assertEqual( + self.openstack.delete_backup( + uuid=self.fake_backup.id, project_id="bar" + ), + None, + ) + self.m_c.delete_volume_backup.assert_called_once_with( + self.fake_backup.id, force=False + ) + + def test_delete_backup_not_found(self): + self.m_c.delete_volume_backup = mock.MagicMock( + side_effect=openstack_exc.ResourceNotFound + ) + self.assertEqual( + self.openstack.delete_backup( + uuid=self.fake_backup.id, project_id="bar" + ), + None, + ) + self.m_c.delete_volume_backup.assert_called_once_with( + self.fake_backup.id, force=False + ) + + def test_delete_backup_non_http_error(self): + self._test_non_http_error( + self.m_c.delete_volume_backup, + "delete_backup", + uuid="foo", + project_id="bar", + ) + + def test_delete_backup_404_http_error(self): + self._test_http_error( + self.m_c.delete_volume_backup, + "delete_backup", + status_code=404, + uuid="foo", + project_id="bar", + ) + + def test_delete_backup_500_http_error(self): + self._test_http_error( + self.m_c.delete_volume_backup, + "delete_backup", + status_code=500, + uuid="foo", + project_id="bar", + ) + + @mock.patch("openstack.proxy._json_response") + def test_get_backup_quota(self, m_j_r): + self.m_c.block_storage.get = mock.MagicMock(status_code=200) + self.m_gam = mock.MagicMock() + self.m_c._get_and_munchify = self.m_gam + self.m_gam.return_value = mock.MagicMock(backups=[self.fake_backup.id]) + self.assertEqual( + [self.fake_backup.id], + self.openstack.get_backup_quota(project_id="bar"), + ) + self.m_c.block_storage.get.assert_called_once_with( + "/os-quota-sets/bar?usage=True" + ) + + def test_get_backup_quota_non_http_error(self): + self._test_non_http_error( + self.m_c.block_storage.get, "get_backup_quota", project_id="bar" + ) + + def test_get_backup_quota_404_http_error(self): + self._test_http_error( + self.m_c.block_storage.get, + "get_backup_quota", + status_code=404, + project_id="bar", + ) + + def test_get_backup_quota_500_http_error(self): + self._test_http_error( + self.m_c.block_storage.get, + "get_backup_quota", + status_code=500, + project_id="bar", + ) diff --git a/tox.ini b/tox.ini index 4812539..47da115 100755 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,pep8 +envlist = py37,py38,py39,py310,pep8 skipsdist = True sitepackages = False skip_missing_interpreters = True @@ -15,7 +15,6 @@ deps = flake8 -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} install_commands = pip install {opts} {packages} @@ -23,11 +22,12 @@ install_commands = [testenv:py3] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} [testenv:pep8] -commands = +commands = flake8 [testenv:cover]