From b2102600b7319bfb5c1729b86c4d5147ddacf513 Mon Sep 17 00:00:00 2001 From: jneo8 Date: Fri, 19 Jul 2024 17:12:14 +0800 Subject: [PATCH] feat: Block upgrade if vault is in sealed status Add pre-check step as first step before all the steps to make sure vault is not in sealed. --- cou/exceptions.py | 4 ++ cou/steps/plan.py | 8 ++- cou/steps/vault.py | 41 +++++++++++++ tests/functional/tests/smoke.py | 1 + .../018346c5-f95c-46df-a34e-9a78bdec0018.yaml | 1 + .../9eb9af6a-b919-4cf9-8f2f-9df16a1556be.yaml | 1 + tests/mocked_plans/sample_plans/base.yaml | 1 + tests/unit/steps/test_plan.py | 24 +++++--- tests/unit/steps/test_vault.py | 59 +++++++++++++++++++ 9 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 cou/steps/vault.py create mode 100644 tests/unit/steps/test_vault.py diff --git a/cou/exceptions.py b/cou/exceptions.py index cf6b4c81..e8f18242 100644 --- a/cou/exceptions.py +++ b/cou/exceptions.py @@ -147,3 +147,7 @@ def __init__(self, message: str, exit_code: int) -> None: class ApplicationNotSupported(COUException): """COU exception when the application is known but not supported by COU.""" + + +class VaultSealed(COUException): + """COU exception when the application vault is sealed.""" diff --git a/cou/steps/plan.py b/cou/steps/plan.py index 09eedd69..975c4e58 100644 --- a/cou/steps/plan.py +++ b/cou/steps/plan.py @@ -52,6 +52,7 @@ from cou.steps.backup import backup from cou.steps.hypervisor import HypervisorUpgradePlanner from cou.steps.nova_cloud_controller import archive, purge +from cou.steps.vault import check_vault_status from cou.utils.app_utils import set_require_osd_release_option from cou.utils.juju_utils import DEFAULT_TIMEOUT, Machine, Unit from cou.utils.nova_compute import get_empty_hypervisors @@ -369,6 +370,11 @@ def _get_pre_upgrade_steps(analysis_result: Analysis, args: CLIargs) -> list[Pre :rtype: list[PreUpgradeStep] """ steps = [ + PreUpgradeStep( + description="Check application vault is not sealed", + parallel=False, + coro=check_vault_status(analysis_result.model), + ), PreUpgradeStep( description="Verify that all OpenStack applications are in idle state", parallel=False, @@ -381,7 +387,7 @@ def _get_pre_upgrade_steps(analysis_result: Analysis, args: CLIargs) -> list[Pre idle_period=10, raise_on_blocked=True, ), - ) + ), ] steps.extend(_get_backup_steps(analysis_result, args)) steps.extend(_get_archive_data_steps(analysis_result, args)) diff --git a/cou/steps/vault.py b/cou/steps/vault.py new file mode 100644 index 00000000..b9150feb --- /dev/null +++ b/cou/steps/vault.py @@ -0,0 +1,41 @@ +# Copyright 2024 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Functions for prereq steps relating to vault.""" +import logging + +from cou.exceptions import ApplicationNotFound, VaultSealed +from cou.utils.juju_utils import Model + +logger = logging.getLogger(__name__) + + +async def check_vault_status(model: Model) -> None: + """Make sure vault is not in sealed status. + + :param model: juju model to work with + :type model: Model + :raises VaultSealed: if application in sealed status + """ + try: + app_names = await model.get_application_names(charm_name="vault") + for app_name in app_names: + app = await model.get_application_status(app_name=app_name) + if app.status.info == "Unit is sealed" and app.status.status == "blocked": + raise VaultSealed( + "Vault is in sealed, please follow the steps on " + "https://charmhub.io/vault to unseal the vault manually before upgrade" + ) + except ApplicationNotFound: + logger.warning("Application vault not found, skip") + logger.debug("Vault not in sealed status") diff --git a/tests/functional/tests/smoke.py b/tests/functional/tests/smoke.py index 92be2549..2702fa25 100644 --- a/tests/functional/tests/smoke.py +++ b/tests/functional/tests/smoke.py @@ -134,6 +134,7 @@ def generate_expected_plan(self, backup: bool = True) -> str: backup_plan = "\tBack up MySQL databases\n" if backup else "" return ( "Upgrade cloud from 'ussuri' to 'victoria'\n" + "\tCheck application vault is not sealed\n" "\tVerify that all OpenStack applications are in idle state\n" f"{backup_plan}" "\tArchive old database data on nova-cloud-controller\n" diff --git a/tests/mocked_plans/sample_plans/018346c5-f95c-46df-a34e-9a78bdec0018.yaml b/tests/mocked_plans/sample_plans/018346c5-f95c-46df-a34e-9a78bdec0018.yaml index 99eb74b0..a8719de4 100644 --- a/tests/mocked_plans/sample_plans/018346c5-f95c-46df-a34e-9a78bdec0018.yaml +++ b/tests/mocked_plans/sample_plans/018346c5-f95c-46df-a34e-9a78bdec0018.yaml @@ -1,5 +1,6 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' + Check application vault is not sealed Verify that all OpenStack applications are in idle state Back up MySQL databases Archive old database data on nova-cloud-controller diff --git a/tests/mocked_plans/sample_plans/9eb9af6a-b919-4cf9-8f2f-9df16a1556be.yaml b/tests/mocked_plans/sample_plans/9eb9af6a-b919-4cf9-8f2f-9df16a1556be.yaml index 3f4977c4..73b5cb27 100644 --- a/tests/mocked_plans/sample_plans/9eb9af6a-b919-4cf9-8f2f-9df16a1556be.yaml +++ b/tests/mocked_plans/sample_plans/9eb9af6a-b919-4cf9-8f2f-9df16a1556be.yaml @@ -1,5 +1,6 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' + Check application vault is not sealed Verify that all OpenStack applications are in idle state Back up MySQL databases Archive old database data on nova-cloud-controller diff --git a/tests/mocked_plans/sample_plans/base.yaml b/tests/mocked_plans/sample_plans/base.yaml index cc245604..3a1ad734 100644 --- a/tests/mocked_plans/sample_plans/base.yaml +++ b/tests/mocked_plans/sample_plans/base.yaml @@ -1,5 +1,6 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' + Check application vault is not sealed Verify that all OpenStack applications are in idle state Back up MySQL databases Archive old database data on nova-cloud-controller diff --git a/tests/unit/steps/test_plan.py b/tests/unit/steps/test_plan.py index de4e10cc..b8fdbf3f 100644 --- a/tests/unit/steps/test_plan.py +++ b/tests/unit/steps/test_plan.py @@ -45,6 +45,7 @@ from cou.steps.backup import backup from cou.steps.hypervisor import HypervisorGroup, HypervisorUpgradePlanner from cou.steps.nova_cloud_controller import archive, purge +from cou.steps.vault import check_vault_status from cou.utils import app_utils from cou.utils.juju_utils import Machine, SubordinateUnit, Unit from cou.utils.openstack import OpenStackRelease @@ -154,6 +155,7 @@ async def test_generate_plan(mock_filter_hypervisors, model, cli_args): exp_plan = dedent_plan( """\ Upgrade cloud from 'ussuri' to 'victoria' + Check application vault is not sealed Verify that all OpenStack applications are in idle state Back up MySQL databases Archive old database data on nova-cloud-controller @@ -326,6 +328,7 @@ async def test_generate_plan_with_warning_messages(mock_filter_hypervisors, mode exp_plan = dedent_plan( """\ Upgrade cloud from 'ussuri' to 'victoria' + Check application vault is not sealed Verify that all OpenStack applications are in idle state Back up MySQL databases Archive old database data on nova-cloud-controller @@ -1059,14 +1062,21 @@ def test_get_pre_upgrade_steps( mock_analysis_result.model = model expected_steps = [] - expected_steps.append( - PreUpgradeStep( - description="Verify that all OpenStack applications are in idle state", - parallel=False, - coro=mock_analysis_result.model.wait_for_idle( - timeout=120, idle_period=10, raise_on_blocked=True + expected_steps.extend( + [ + PreUpgradeStep( + description="Check application vault is not sealed", + parallel=False, + coro=check_vault_status(mock_analysis_result.model), ), - ) + PreUpgradeStep( + description="Verify that all OpenStack applications are in idle state", + parallel=False, + coro=mock_analysis_result.model.wait_for_idle( + timeout=120, idle_period=10, raise_on_blocked=True + ), + ), + ] ) expected_steps.append("backup_step") diff --git a/tests/unit/steps/test_vault.py b/tests/unit/steps/test_vault.py new file mode 100644 index 00000000..b7e6a34d --- /dev/null +++ b/tests/unit/steps/test_vault.py @@ -0,0 +1,59 @@ +# Copyright 2024 Canonical Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest.mock import MagicMock, patch + +import pytest + +from cou.exceptions import ApplicationNotFound, VaultSealed +from cou.steps.vault import check_vault_status + + +@pytest.mark.asyncio +async def test_check_vault_status_sealed(model) -> None: + model.get_application_names.return_value = ["app1", "app2"] + model.get_application_status.return_value = MagicMock() + model.get_application_status.return_value.status = MagicMock() + model.get_application_status.return_value.status.info = "Unit is sealed" + model.get_application_status.return_value.status.status = "blocked" + err_msg = ( + "Vault is in sealed, please follow the steps on " + "https://charmhub.io/vault to unseal the vault manually before upgrade" + ) + with pytest.raises(VaultSealed, match=err_msg): + await check_vault_status(model) + + +@pytest.mark.parametrize( + "case,info,status", + [ + ["wrong info", "wrong info msg", "blocked"], + ["wrong status", "Unit is sealed", "wrong status"], + ], +) +@pytest.mark.asyncio +async def test_check_vault_status_unseal(case, info, status, model) -> None: + model.get_application_names.return_value = ["app1", "app2"] + model.get_application_status.return_value = MagicMock() + model.get_application_status.return_value.status = MagicMock() + model.get_application_status.return_value.status.info = info + model.get_application_status.return_value.status.status = status + await check_vault_status(model) + + +@pytest.mark.asyncio +@patch("cou.steps.vault.logger") +async def test_check_vault_status_vault_not_exists(mock_logger, model) -> None: + model.get_application_names.side_effect = ApplicationNotFound + await check_vault_status(model) + mock_logger.warning.assert_called_once_with("Application vault not found, skip")