diff --git a/cou/apps/base.py b/cou/apps/base.py index 68d5defd..69e31d2e 100644 --- a/cou/apps/base.py +++ b/cou/apps/base.py @@ -624,6 +624,58 @@ def _get_disable_action_managed_plan(self, parallel: bool = False) -> UpgradeSte ) return UpgradeStep() + def _get_enable_action_managed_plan(self, parallel: bool = False) -> UpgradeStep: + """Get plan to enable action-managed-upgrade. + + This is used to upgrade as "paused-single-unit" strategy. + + :param parallel: Parallel running, defaults to False + :type parallel: bool, optional + :return: Plan to enable action-managed-upgrade + :rtype: UpgradeStep + """ + if self.config.get("action-managed-upgrade", {}).get("value", False): + return UpgradeStep() + return UpgradeStep( + description=( + f"Change charm config of '{self.name}' 'action-managed-upgrade' to True." + ), + parallel=parallel, + coro=self.model.set_application_config(self.name, {"action-managed-upgrade": True}), + ) + + def _get_pause_unit(self, unit: ApplicationUnit, parallel: bool = False) -> UpgradeStep: + """Get the plan to pause a unit to upgrade. + + :param unit: Unit to be paused. + :type unit: ApplicationUnit + :param parallel: Parallel running, defaults to False + :type parallel: bool, optional + :return: Plan to pause a unit. + :rtype: UpgradeStep + """ + return UpgradeStep( + description=f"Pause the unit: '{unit.name}'.", + parallel=parallel, + coro=self.model.run_action(unit_name=unit.name, action_name="pause"), + ) + + def _get_resume_unit(self, unit: ApplicationUnit, parallel: bool = False) -> UpgradeStep: + """Get the plan to resume a unit after upgrading the workload version. + + :param unit: Unit to be resumed. + :type unit: ApplicationUnit + :param parallel: Parallel running, defaults to False + :type parallel: bool, optional + :return: Plan to resume a unit. + :rtype: UpgradeStep + """ + return UpgradeStep( + description=(f"Resume the unit: '{unit.name}'."), + parallel=parallel, + coro=self.model.run_action(unit_name=unit.name, action_name="resume"), + ) + def _get_workload_upgrade_plan( self, target: OpenStackRelease, parallel: bool = False ) -> UpgradeStep: diff --git a/cou/steps/__init__.py b/cou/steps/__init__.py index 1fdb99bc..f17abc59 100644 --- a/cou/steps/__init__.py +++ b/cou/steps/__init__.py @@ -42,11 +42,24 @@ def compare_step_coroutines(coro1: Optional[Coroutine], coro2: Optional[Coroutin # compare two None or one None and one Coroutine return coro1 == coro2 + inspection_coro1 = inspect.getcoroutinelocals(coro1) + inspection_coro2 = inspect.getcoroutinelocals(coro2) + + args1 = list(inspection_coro1.get("args", [])) + kwargs1_values = list(inspection_coro1.get("kwargs", {}).values()) + + args2 = list(inspection_coro2.get("args", [])) + kwargs2_values = list(inspection_coro2.get("kwargs", {}).values()) return ( # check if same coroutine was used coro1.cr_code == coro2.cr_code # check coroutine arguments - and inspect.getcoroutinelocals(coro1) == inspect.getcoroutinelocals(coro2) + and ( + inspection_coro1 == inspection_coro2 + # with this we can compare for e.g: + # run_action("my_app/0", "pause") with run_action(unit="my_app/0", action_name="pause") + or args1 + kwargs1_values == args2 + kwargs2_values + ) ) diff --git a/cou/utils/app_utils.py b/cou/utils/app_utils.py index edd6ac70..6559b8d4 100644 --- a/cou/utils/app_utils.py +++ b/cou/utils/app_utils.py @@ -75,6 +75,30 @@ async def get_instance_count(unit: str, model: COUModel) -> int: ) +async def enable_nova_compute_scheduler(unit: str, model: COUModel) -> None: + """Enable nova-compute scheduler, so the unit can create new VMs. + + :param unit: Name of the nova-compute unit where the action runs on. + :type unit: str + :param model: COUModel object + :type model: COUModel + """ + action_name = "enable" + await model.run_action(unit_name=unit, action_name=action_name) + + +async def disable_nova_compute_scheduler(unit: str, model: COUModel) -> None: + """Disable nova-compute scheduler, so the unit cannot create new VMs. + + :param unit: Name of the nova-compute unit where the action runs on. + :type unit: str + :param model: COUModel object + :type model: COUModel + """ + action_name = "disable" + await model.run_action(unit_name=unit, action_name=action_name) + + async def set_require_osd_release_option(unit: str, model: COUModel) -> None: """Check and set the correct value for require-osd-release on a ceph-mon unit. diff --git a/tests/unit/apps/test_base.py b/tests/unit/apps/test_base.py new file mode 100644 index 00000000..87a558ad --- /dev/null +++ b/tests/unit/apps/test_base.py @@ -0,0 +1,76 @@ +# Copyright 2023 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 + +import pytest +from juju.client._definitions import ApplicationStatus + +from cou.apps.base import ApplicationUnit, OpenStackApplication +from cou.steps import UpgradeStep + + +@pytest.mark.parametrize( + "charm_config", + [{"action-managed-upgrade": {"value": False}}, {"action-managed-upgrade": {"value": True}}], +) +def test_get_enable_action_managed_plan(charm_config, model): + charm = "app" + app_name = "my_app" + status = MagicMock(spec_set=ApplicationStatus()) + status.charm_channel = "ussuri/stable" + + expected_upgrade_step = UpgradeStep( + f"Change charm config of '{app_name}' 'action-managed-upgrade' to True.", + False, + model.set_application_config(app_name, {"action-managed-upgrade": True}), + ) + if charm_config["action-managed-upgrade"]["value"]: + expected_upgrade_step = UpgradeStep() + + app = OpenStackApplication(app_name, status, charm_config, model, charm, {}) + + assert app._get_enable_action_managed_plan() == expected_upgrade_step + + +def test_get_pause_unit(model): + charm = "app" + app_name = "my_app" + status = MagicMock(spec_set=ApplicationStatus()) + status.charm_channel = "ussuri/stable" + + unit = ApplicationUnit("my_app/0", MagicMock(), MagicMock(), MagicMock()) + + expected_upgrade_step = UpgradeStep( + f"Pause the unit: '{unit.name}'.", False, model.run_action("my_app/0", "pause") + ) + + app = OpenStackApplication(app_name, status, {}, model, charm, {}) + assert app._get_pause_unit(unit) == expected_upgrade_step + + +def test_get_resume_unit(model): + charm = "app" + app_name = "my_app" + status = MagicMock(spec_set=ApplicationStatus()) + status.charm_channel = "ussuri/stable" + + unit = ApplicationUnit("my_app/0", MagicMock(), MagicMock(), MagicMock()) + + expected_upgrade_step = UpgradeStep( + f"Resume the unit: '{unit.name}'.", False, model.run_action(unit.name, "resume") + ) + + app = OpenStackApplication(app_name, status, {}, model, charm, {}) + assert app._get_resume_unit(unit) == expected_upgrade_step diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 643d4eb1..4d9ec3d5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -441,8 +441,8 @@ def generate_my_app(): "12.5/stable", "ch:amd64/focal/my-app-638", [], - ["my-app/0"], - ["0/lxd/11"], + MY_APP_UNITS, + MY_APP_MACHINES, "12.5", ) return {"my_app": mock_my_app} @@ -585,6 +585,7 @@ def model(config, apps_machines): model.get_status = AsyncMock(side_effect=get_status) model.get_charm_name = AsyncMock(side_effect=get_charm_name) model.scp_from_unit = AsyncMock() + model.set_application_config = AsyncMock() model.get_application_config = mock_get_app_config = AsyncMock() mock_get_app_config.side_effect = config.get model.get_model_machines = machines @@ -653,8 +654,8 @@ def apps(status, config, model, apps_machines): model, "nova-compute", apps_machines["nova-compute"], - apps_machines["nova-compute"], ) + return { "keystone_focal_ussuri": keystone_ussuri, "keystone_focal_wallaby": keystone_wallaby, diff --git a/tests/unit/steps/test_steps.py b/tests/unit/steps/test_steps.py index 8a015bb3..cf80dc7f 100644 --- a/tests/unit/steps/test_steps.py +++ b/tests/unit/steps/test_steps.py @@ -44,6 +44,7 @@ async def mock_coro(*args, **kwargs): ... (mock_coro(), mock_coro(arg1=True), False), (mock_coro(), mock_coro(), True), (mock_coro(1, 2, 3, kwarg1=True), mock_coro(1, 2, 3, kwarg1=True), True), + (mock_coro(1, 2, kwarg1=3, kwarg2=True), mock_coro(1, 2, 3, True), True), ], ) def test_compare_step_coroutines(coro1, coro2, exp_result): diff --git a/tests/unit/utils/test_app_utils.py b/tests/unit/utils/test_app_utils.py index 6a7e365d..5617240b 100644 --- a/tests/unit/utils/test_app_utils.py +++ b/tests/unit/utils/test_app_utils.py @@ -94,6 +94,34 @@ async def test_get_instance_count(model): assert actual_count == expected_count +@pytest.mark.asyncio +async def test_enable_nova_compute_scheduler(model): + model.run_action.return_value = mocked_action = AsyncMock(spec_set=Action).return_value + mocked_action.results = {} + + result = await app_utils.enable_nova_compute_scheduler(unit="nova-compute/0", model=model) + + model.run_action.assert_called_once_with( + unit_name="nova-compute/0", + action_name="enable", + ) + assert result is None + + +@pytest.mark.asyncio +async def test_disable_nova_compute_scheduler(model): + model.run_action.return_value = mocked_action = AsyncMock(spec_set=Action).return_value + mocked_action.results = {} + + result = await app_utils.disable_nova_compute_scheduler(unit="nova-compute/0", model=model) + + model.run_action.assert_called_once_with( + unit_name="nova-compute/0", + action_name="disable", + ) + assert result is None + + @pytest.mark.asyncio @pytest.mark.parametrize( "result_key, value",