Skip to content

Commit

Permalink
feat: Block upgrade if vault is in sealed status
Browse files Browse the repository at this point in the history
Add pre-check step as first step before all the steps to make
sure vault is not in sealed.
  • Loading branch information
jneo8 committed Jul 26, 2024
1 parent 6b19bf8 commit b210260
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 8 deletions.
4 changes: 4 additions & 0 deletions cou/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
8 changes: 7 additions & 1 deletion cou/steps/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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))
Expand Down
41 changes: 41 additions & 0 deletions cou/steps/vault.py
Original file line number Diff line number Diff line change
@@ -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")
1 change: 1 addition & 0 deletions tests/functional/tests/smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/mocked_plans/sample_plans/base.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
24 changes: 17 additions & 7 deletions tests/unit/steps/test_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/steps/test_vault.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit b210260

Please sign in to comment.