Skip to content

Commit

Permalink
Introduce nova-compute actions to be able to upgrade unit by unit
Browse files Browse the repository at this point in the history
- enable nova-compute unit scheduler
- disable nova-compute unit scheduler
- pause OpenStack application unit
- resume OpenStack application unit

others:
- compare_step_coroutines able to compare two coroutines if the
  sequence of parameters respect the assignament of the function
  • Loading branch information
gabrielcocenza committed Feb 1, 2024
1 parent 0cb655a commit 309b8ca
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 4 deletions.
52 changes: 52 additions & 0 deletions cou/apps/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 14 additions & 1 deletion cou/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)


Expand Down
24 changes: 24 additions & 0 deletions cou/utils/app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
76 changes: 76 additions & 0 deletions tests/unit/apps/test_base.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions tests/unit/steps/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/utils/test_app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 309b8ca

Please sign in to comment.