From 1f5fda8ddffcddc422a5062ee0d06884ad41fea9 Mon Sep 17 00:00:00 2001 From: Ray Chan Date: Thu, 24 Oct 2024 07:34:31 +0800 Subject: [PATCH] Verify nova_cloud_controller_scheduler_default_filters (#594) Add verify step for scheduler_default_filters for nova-cloud-controller during antelope to bobcat upgrade ref. https://docs.openstack.org/charm-guide/latest/release-notes/2023.2-bobcat.html#nova-availabilityzonefilter-removal-in-2023-2 --- cou/steps/plan.py | 48 +++++++++++++++- tests/unit/conftest.py | 8 +++ tests/unit/steps/test_plan.py | 105 ++++++++++++++++++++++++++++++---- tests/unit/utils.py | 1 + 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/cou/steps/plan.py b/cou/steps/plan.py index e20d71b5..d26aa254 100644 --- a/cou/steps/plan.py +++ b/cou/steps/plan.py @@ -41,6 +41,7 @@ from cou.commands import CONTROL_PLANE, DATA_PLANE, HYPERVISORS, CLIargs from cou.exceptions import ( ApplicationError, + ApplicationNotFound, COUException, DataPlaneMachineFilterError, HaltUpgradePlanGeneration, @@ -55,7 +56,12 @@ from cou.steps.nova_cloud_controller import archive, purge from cou.steps.vault import verify_vault_is_unsealed from cou.utils import print_and_debug -from cou.utils.juju_utils import DEFAULT_TIMEOUT, Machine, Unit +from cou.utils.juju_utils import ( + DEFAULT_TIMEOUT, + Machine, + Unit, + get_applications_by_charm_name, +) from cou.utils.nova_compute import get_empty_hypervisors from cou.utils.openstack import LTS_TO_OS_RELEASE, OpenStackRelease @@ -110,6 +116,7 @@ async def verify_cloud(analysis_result: Analysis, args: CLIargs) -> None: _verify_highest_release_achieved(analysis_result) _verify_data_plane_ready_to_upgrade(args, analysis_result) _verify_hypervisors_cli_input(args, analysis_result) + _verify_nova_cloud_controller_scheduler_default_filters(args, analysis_result) await _verify_vault_is_unsealed(analysis_result) await _verify_osd_noout_unset(analysis_result) await _verify_model_idle(analysis_result) @@ -237,6 +244,45 @@ def _verify_highest_release_achieved(analysis_result: Analysis) -> None: ) +def _verify_nova_cloud_controller_scheduler_default_filters( + args: CLIargs, analysis_result: Analysis +) -> None: + """Verify scheduler_default_filters option is set correctly for each OpenStack release. + + :param args: CLI arguments + :type args: CLIargs + :param analysis_result: Analysis result. + :type analysis_result: Analysis + """ + if args.upgrade_group not in {CONTROL_PLANE, None}: + return + + try: + nova_cloud_controllers = get_applications_by_charm_name( + analysis_result.apps_control_plane, "nova-cloud-controller" + ) + except ApplicationNotFound: + PlanStatus.add_message( + "Cannot find nova-cloud-controller apps. Is this a valid OpenStack cloud?", + MessageType.WARNING, + ) + return + + curr_release = analysis_result.current_cloud_o7k_release + for nova_cloud_controller in nova_cloud_controllers: + config = nova_cloud_controller.config.get("scheduler-default-filters", "") + if curr_release == "antelope" and "AvailabilityZoneFilter" in config: + PlanStatus.add_message( + f"Upgrade from '{curr_release}' to 'bobcat' is not possible " + "if `AvailabilityZoneFilter` is in `scheduler-default-filters` option for app: " + f"{nova_cloud_controller.name}. \n" + "This is because `AvailabilityZoneFilter` is removed from the filters: " + "https://docs.openstack.org/charm-guide/latest/release-notes/2023.2-bobcat.html" + "#nova-availabilityzonefilter-removal-in-2023-2", + MessageType.ERROR, + ) + + def _verify_data_plane_ready_to_upgrade(args: CLIargs, analysis_result: Analysis) -> None: """Verify if data plane is ready to upgrade. diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f0a654a3..759b9762 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -17,6 +17,7 @@ import pytest from cou.commands import CLIargs +from cou.steps.plan import PlanStatus from cou.utils.juju_utils import Model from tests.unit.utils import get_charm_name, get_status @@ -58,3 +59,10 @@ def cli_args() -> MagicMock: """ # spec_set needs an instantiated class to be strict with the fields. return MagicMock(spec_set=CLIargs(command="plan"))() + + +@pytest.fixture(autouse=True) +def plan_status() -> None: + """Get an empty PlanStatus for every test case.""" + PlanStatus.error_messages = [] + PlanStatus.warning_messages = [] diff --git a/tests/unit/steps/test_plan.py b/tests/unit/steps/test_plan.py index 1e9e690d..5d894b3b 100644 --- a/tests/unit/steps/test_plan.py +++ b/tests/unit/steps/test_plan.py @@ -23,6 +23,7 @@ from cou.commands import CONTROL_PLANE, DATA_PLANE, HYPERVISORS, CLIargs from cou.exceptions import ( ApplicationError, + ApplicationNotFound, COUException, DataPlaneMachineFilterError, HaltUpgradePlanGeneration, @@ -49,7 +50,7 @@ from cou.utils import app_utils from cou.utils.juju_utils import Machine, Unit from cou.utils.openstack import OpenStackRelease -from tests.unit.utils import dedent_plan, generate_cou_machine +from tests.unit.utils import dedent_plan, generate_cou_machine, get_applications def generate_expected_upgrade_plan_principal(app, target, model): @@ -487,7 +488,8 @@ async def test_generate_plan_with_warning_messages(mock_filter_hypervisors, mode upgrade_plan = await cou_plan.generate_plan(analysis_result, cli_args) assert str(upgrade_plan) == exp_plan - assert len(cou_plan.PlanStatus.warning_messages) == 2 # keystone warning and set_noout + assert len(cou_plan.PlanStatus.error_messages) == 1 # set_noout error + assert len(cou_plan.PlanStatus.warning_messages) == 1 # keystone mismatch warning def test_PlanStatus_warnings_property(): @@ -510,7 +512,9 @@ def test_PlanStatus_warnings_property(): @patch("cou.steps.plan._verify_supported_series") @patch("cou.steps.plan._verify_highest_release_achieved") @patch("cou.steps.plan._verify_data_plane_ready_to_upgrade") +@patch("cou.steps.plan._verify_nova_cloud_controller_scheduler_default_filters") async def test_pre_plan_sanity_checks( + mock_verify_nova_cloud_controller_scheduler_default_filters, mock_verify_data_plane_ready_to_upgrade, mock_verify_highest_release_achieved, mock_verify_supported_series, @@ -528,6 +532,9 @@ async def test_pre_plan_sanity_checks( mock_verify_supported_series.assert_called_once_with(mock_analysis_result) mock_verify_data_plane_ready_to_upgrade.assert_called_once_with(cli_args, mock_analysis_result) mock_verify_hypervisors_cli_input.assert_called_once_with(cli_args, mock_analysis_result) + mock_verify_nova_cloud_controller_scheduler_default_filters.assert_called_once_with( + cli_args, mock_analysis_result + ) mock_verify_vault_is_unsealed.assert_awaited_once_with(mock_analysis_result) mock_verify_osd_noout_unset.assert_awaited_once_with(mock_analysis_result) mock_verify_model_idle.assert_awaited_once_with(mock_analysis_result) @@ -539,14 +546,12 @@ async def test_pre_plan_sanity_checks( ( OpenStackRelease("caracal"), "noble", - "Cloud series 'noble' is not a Ubuntu LTS series supported by COU. " - "The supporting series are: focal, jammy", + "Cloud series 'noble' is not a Ubuntu LTS series supported by COU. ", ), ( OpenStackRelease("train"), "bionic", - "Cloud series 'bionic' is not a Ubuntu LTS series supported by COU. " - "The supporting series are: focal, jammy", + "Cloud series 'bionic' is not a Ubuntu LTS series supported by COU. ", ), ], ) @@ -555,7 +560,7 @@ def test_verify_supported_series(o7k_release, current_series, exp_error_msg): mock_analysis_result.current_cloud_o7k_release = o7k_release mock_analysis_result.current_cloud_series = current_series cou_plan._verify_supported_series(mock_analysis_result) - cou_plan.PlanStatus.error_messages[0] == exp_error_msg + assert exp_error_msg in cou_plan.PlanStatus.error_messages[0] @pytest.mark.parametrize( @@ -579,7 +584,87 @@ def test_verify_highest_release_achieved(o7k_release, series): "- https://docs.openstack.org/charm-guide/latest/admin/upgrades/series-openstack.html" ) cou_plan._verify_highest_release_achieved(mock_analysis_result) - cou_plan.PlanStatus.error_messages[0] == exp_error_msg + assert cou_plan.PlanStatus.error_messages[0] == exp_error_msg + + +@pytest.mark.parametrize( + "upgrade_group", + [CONTROL_PLANE, None], +) +@patch("cou.steps.plan.get_applications_by_charm_name") +def test_verify_nova_cloud_controller_scheduler_default_filters_failed( + mock_get_applications_by_charm_name, upgrade_group, cli_args +): + app_count = 2 + cli_args.upgrade_group = upgrade_group + nova_cloud_controllers = get_applications("nova-cloud-controller", app_count=app_count) + for nova_cloud_controller in nova_cloud_controllers: + nova_cloud_controller.config = {"scheduler-default-filters": "AvailabilityZoneFilter"} + mock_get_applications_by_charm_name.return_value = nova_cloud_controllers + mock_analysis_result = MagicMock(spec=Analysis)() + mock_analysis_result.current_cloud_o7k_release = OpenStackRelease("antelope") + exp_error_msg = ( + "Upgrade from 'antelope' to 'bobcat' is not possible " + "if `AvailabilityZoneFilter` is in `scheduler-default-filters` option" + ) + cou_plan._verify_nova_cloud_controller_scheduler_default_filters( + cli_args, mock_analysis_result + ) + for i in range(app_count): + assert exp_error_msg in cou_plan.PlanStatus.error_messages[i] + + +@pytest.mark.parametrize( + "upgrade_group", + [CONTROL_PLANE, None], +) +@patch("cou.steps.plan.get_applications_by_charm_name") +def test_verify_nova_cloud_controller_scheduler_default_filters_missing_app( + mock_get_applications_by_charm_name, upgrade_group, cli_args +): + cli_args.upgrade_group = upgrade_group + mock_get_applications_by_charm_name.side_effect = ApplicationNotFound + mock_analysis_result = MagicMock(spec=Analysis)() + mock_analysis_result.current_cloud_o7k_release = OpenStackRelease("antelope") + exp_error_msg = "Cannot find nova-cloud-controller apps" + cou_plan._verify_nova_cloud_controller_scheduler_default_filters( + cli_args, mock_analysis_result + ) + assert exp_error_msg in cou_plan.PlanStatus.warning_messages[0] + + +@patch("cou.steps.plan.get_applications_by_charm_name") +def test_verify_nova_cloud_controller_scheduler_default_filters_skip_upgrade_group( + mock_get_applications_by_charm_name, cli_args +): + app_count = 2 + cli_args.upgrade_group = DATA_PLANE + mock_get_applications_by_charm_name.return_value = get_applications( + "nova-cloud-controller", app_count=app_count + ) + mock_analysis_result = MagicMock(spec=Analysis)() + mock_analysis_result.current_cloud_o7k_release = OpenStackRelease("antelope") + cou_plan._verify_nova_cloud_controller_scheduler_default_filters( + cli_args, mock_analysis_result + ) + assert not cou_plan.PlanStatus.error_messages + + +@patch("cou.steps.plan.get_applications_by_charm_name") +def test_verify_nova_cloud_controller_scheduler_default_filters_skip_not_antelope( + mock_get_applications_by_charm_name, cli_args +): + app_count = 2 + cli_args.upgrade_group = CONTROL_PLANE + mock_get_applications_by_charm_name.return_value = get_applications( + "nova-cloud-controller", app_count=app_count + ) + mock_analysis_result = MagicMock(spec=Analysis)() + mock_analysis_result.current_cloud_o7k_release = OpenStackRelease("yoga") # not antelope + cou_plan._verify_nova_cloud_controller_scheduler_default_filters( + cli_args, mock_analysis_result + ) + assert not cou_plan.PlanStatus.error_messages @pytest.mark.parametrize( @@ -588,7 +673,7 @@ def test_verify_highest_release_achieved(o7k_release, series): ( OpenStackRelease("ussuri"), OpenStackRelease("ussuri"), - "Please, upgrade control-plane before data-plane", + "Please upgrade control-plane before data-plane", ), ( OpenStackRelease("ussuri"), @@ -606,7 +691,7 @@ def test_verify_data_plane_ready_to_upgrade_error( mock_analysis_result.min_o7k_version_control_plane = min_o7k_version_control_plane mock_analysis_result.min_o7k_version_data_plane = min_o7k_version_data_plane cou_plan._verify_data_plane_ready_to_upgrade(cli_args, mock_analysis_result) - cou_plan.PlanStatus.error_messages[0] == exp_error_msg + assert exp_error_msg in cou_plan.PlanStatus.error_messages[0] @pytest.mark.parametrize("upgrade_group", [DATA_PLANE, HYPERVISORS]) diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 9c408aca..0bf179a9 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -54,6 +54,7 @@ def get_applications( app = MagicMock(spec_set=Application)() app.name = f"{charm_name}-{i}" app.charm = charm_name + app.config = {} units = {} for j in range(unit_count): unit = MagicMock(spec_set=Unit)()