diff --git a/cou/commands.py b/cou/commands.py index 2c572c19..ce7fadd9 100644 --- a/cou/commands.py +++ b/cou/commands.py @@ -93,6 +93,21 @@ def __call__( setattr(namespace, self.dest, cli_input) +def batch_size_arg(value: str) -> int: + """Type converter for argparse. + + :param value: input arg value to validate and convert + :type value: str + :return: the input value converted to an int + :rtype: int + :raises argparse.ArgumentTypeError: if integer is an invalid batch size + """ + batch_size = int(value) + if batch_size <= 0: + raise argparse.ArgumentTypeError("batch size must be greater than 0") + return batch_size + + def get_subcommand_common_opts_parser() -> argparse.ArgumentParser: """Create a shared parser for options specific to subcommands. @@ -124,6 +139,20 @@ def get_subcommand_common_opts_parser() -> argparse.ArgumentParser: action=argparse.BooleanOptionalAction, default=argparse.SUPPRESS, ) + subcommand_common_opts_parser.add_argument( + "--archive", + help="Archive old database data (nova) before cloud upgrade.\n" + "Default to enabling archive.", + action=argparse.BooleanOptionalAction, + default=argparse.SUPPRESS, + ) + subcommand_common_opts_parser.add_argument( + "--archive-batch-size", + help="Batch size for nova old database data archiving.\n" + "Decrease the batch size if performance issues are detected.\n(default: 1000)", + type=batch_size_arg, + default=1000, + ) subcommand_common_opts_parser.add_argument( "--force", action="store_true", @@ -382,6 +411,8 @@ class CLIargs: command: str verbosity: int = 0 backup: bool = True + archive: bool = True + archive_batch_size: int = 1000 quiet: bool = False force: bool = False auto_approve: bool = False diff --git a/cou/steps/nova_cloud_controller.py b/cou/steps/nova_cloud_controller.py new file mode 100644 index 00000000..fe90cec0 --- /dev/null +++ b/cou/steps/nova_cloud_controller.py @@ -0,0 +1,89 @@ +# 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 nova.""" +import logging + +from cou.exceptions import ApplicationNotFound, COUException, UnitNotFound +from cou.utils.juju_utils import Model + +logger = logging.getLogger(__name__) + + +async def archive(model: Model, *, batch_size: int) -> None: + """Archive data on a nova-cloud-controller unit. + + The archive-data action only runs a single batch, + so we run it in a loop until completion. + See also: + https://charmhub.io/nova-cloud-controller/actions#archive-data + https://docs.openstack.org/project-deploy-guide/charm-deployment-guide/wallaby/upgrade-openstack.html#archive-old-database-data + :param model: juju model to work with + :type model: Model + :param batch_size: batch-size to pass to the archive-data action + (default is 1000; decrease if performance issues seen) + :type batch_size: int + :raises COUException: if action returned unexpected output + """ # noqa: E501 line too long + unit_name: str = await _get_nova_cloud_controller_unit_name(model) + # The archive-data action only archives a single batch, + # so we must run it in a loop until everything is archived. + while True: + logger.debug("Running action 'archive-data' on %s", unit_name) + # https://charmhub.io/nova-cloud-controller/actions#archive-data + action = await model.run_action( + unit_name=unit_name, + action_name="archive-data", + raise_on_failure=True, + action_params={"batch-size": batch_size}, + ) + logger.info("action output: %s", action.data) + output = action.data["results"].get("archive-deleted-rows") + if output is None: + raise COUException( + "Expected to find output in action results.'archive-deleted-rows', " + "but it was not present." + ) + # The command will contain this string if there is nothing left to archive. + # This means we don't need to run the command any more. + if "Nothing was archived" in output: + logger.debug("Archiving complete.") + break + logger.debug("Potentially more data to archive...") + + +async def _get_nova_cloud_controller_unit_name(model: Model) -> str: + """Get nova-cloud-controller application's first unit's name. + + Assumes only a single nova-cloud-controller application is deployed. + + :param model: juju model to work with + :type model: Model + :return: unit name + :rtype: str + :raises UnitNotFound: When cannot find a valid unit for 'nova-cloud-controller' + :raises ApplicationNotFound: When cannot find a 'nova-cloud-controller' application + """ + status = await model.get_status() + for app_name, app_config in status.applications.items(): + charm_name = await model.get_charm_name(app_name) + if charm_name == "nova-cloud-controller": + units = list(app_config.units.keys()) + if units: + return units[0] + raise UnitNotFound( + f"Cannot find unit for 'nova-cloud-controller' in model '{model.name}'." + ) + + raise ApplicationNotFound(f"Cannot find 'nova-cloud-controller' in model '{model.name}'.") diff --git a/cou/steps/plan.py b/cou/steps/plan.py index 64f21a8c..258457e6 100644 --- a/cou/steps/plan.py +++ b/cou/steps/plan.py @@ -51,6 +51,7 @@ from cou.steps.analyze import Analysis from cou.steps.backup import backup from cou.steps.hypervisor import HypervisorUpgradePlanner +from cou.steps.nova_cloud_controller import archive 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 @@ -386,6 +387,16 @@ def _get_pre_upgrade_steps(analysis_result: Analysis, args: CLIargs) -> list[Pre ) ) + # Add a pre-upgrade step to archive old database data. + # This is a performance optimisation. + if args.archive: + steps.append( + PreUpgradeStep( + description="Archive old database data on nova-cloud-controller", + coro=archive(analysis_result.model, batch_size=args.archive_batch_size), + ) + ) + return steps diff --git a/docs/how-to/archive-old-data.rst b/docs/how-to/archive-old-data.rst new file mode 100644 index 00000000..50af2219 --- /dev/null +++ b/docs/how-to/archive-old-data.rst @@ -0,0 +1,62 @@ +========================================== +Archive old data +========================================== + +By default, **COU** plans for and runs an archive step +before proceeding to actual upgrade steps. +This can be turned off with the `--no-archive` flag. + +This archive step is a performance optimisation, +moving data for soft deleted nova instances into a shadow table. + +The archiving is run in batches. +The default batch size is 1000. +On some clouds, it may be desirable to reduce the batch size to reduce database load. +The batch size can be configured with `--archive-batch-size N`, where `N` is a positive integer. + +Usage examples +-------------- + +With a custom batch size: + +.. terminal:: + :input: cou plan --archive-batch-size 200 + + Full execution log: '/home/ubuntu/.local/share/cou/log/cou-20231215211717.log' + Connected to 'test-model' ✔ + Analyzing cloud... ✔ + Generating upgrade plan... ✔ + Upgrade cloud from 'ussuri' to 'victoria' + Verify that all OpenStack applications are in idle state + Back up MySQL databases + Archive old database data on nova-cloud-controller + Control Plane principal(s) upgrade plan + ... + +Disabling the archive step: + +.. terminal:: + :input: cou plan --no-archive + + Full execution log: '/home/ubuntu/.local/share/cou/log/cou-20231215211717.log' + Connected to 'test-model' ✔ + Analyzing cloud... ✔ + Generating upgrade plan... ✔ + Upgrade cloud from 'ussuri' to 'victoria' + Verify that all OpenStack applications are in idle state + Back up MySQL databases + Control Plane principal(s) upgrade plan + ... + +More information +---------------- + +- `nova-cloud-controller charm actions`_ +- `nova-manage reference`_ - see `archive_deleted_rows` subcommand +- OpenStack upgrade guide information on `archiving old database data`_ + + +.. LINKS +.. _nova-cloud-controller charm actions: https://charmhub.io/nova-cloud-controller/actions +.. _nova-manage reference: https://docs.openstack.org/nova/rocky/cli/nova-manage.html +.. _archiving old database data: https://docs.openstack.org/project-deploy-guide/charm-deployment-guide/wallaby/upgrade-openstack.html#archive-old-database-data diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 16cbe19d..cef21457 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -15,4 +15,5 @@ are possible with **COU**. interruption configure-connection no-backup + archive-old-data verbosity diff --git a/docs/how-to/no-backup.rst b/docs/how-to/no-backup.rst index 774f29fe..968b4c50 100644 --- a/docs/how-to/no-backup.rst +++ b/docs/how-to/no-backup.rst @@ -20,6 +20,7 @@ Plan: Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state # note that there's no backup step planned + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'rabbitmq-server' to 'victoria' Upgrade software packages of 'rabbitmq-server' from the current APT repositories diff --git a/docs/how-to/plan-upgrade.rst b/docs/how-to/plan-upgrade.rst index 87736caf..beb3a2bb 100644 --- a/docs/how-to/plan-upgrade.rst +++ b/docs/how-to/plan-upgrade.rst @@ -29,6 +29,7 @@ Output example Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'keystone' to 'victoria' Upgrade software packages of 'keystone' from the current APT repositories diff --git a/docs/how-to/upgrade-cloud.rst b/docs/how-to/upgrade-cloud.rst index f2c7e43c..76231806 100644 --- a/docs/how-to/upgrade-cloud.rst +++ b/docs/how-to/upgrade-cloud.rst @@ -122,6 +122,7 @@ Usage example Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'rabbitmq-server' to 'victoria' Upgrade software packages of 'rabbitmq-server' from the current APT repositories diff --git a/tests/functional/tests/smoke.py b/tests/functional/tests/smoke.py index 500f7abc..92be2549 100644 --- a/tests/functional/tests/smoke.py +++ b/tests/functional/tests/smoke.py @@ -136,6 +136,7 @@ def generate_expected_plan(self, backup: bool = True) -> str: "Upgrade cloud from 'ussuri' to 'victoria'\n" "\tVerify that all OpenStack applications are in idle state\n" f"{backup_plan}" + "\tArchive old database data on nova-cloud-controller\n" "\tControl Plane principal(s) upgrade plan\n" "\t\tUpgrade plan for 'designate-bind' to 'victoria'\n" "\t\t\tUpgrade software packages of 'designate-bind' " @@ -194,7 +195,14 @@ def test_upgrade(self) -> None: "Upgrade plan for 'mysql-innodb-cluster' to 'victoria'", ] result_before_upgrade = self.cou( - ["upgrade", "--model", self.model_name, "--no-backup", "--auto-approve"] + [ + "upgrade", + "--model", + self.model_name, + "--no-backup", + "--no-archive", + "--auto-approve", + ] ).stdout for expected_msg in expected_msgs_before_upgrade: with self.subTest(expected_msg): @@ -205,7 +213,9 @@ def test_upgrade(self) -> None: "Upgrade plan for 'designate-bind' to 'wallaby'", "Upgrade plan for 'mysql-innodb-cluster' to 'wallaby'", ] - result_after_upgrade = self.cou(["plan", "--model", self.model_name, "--no-backup"]).stdout + result_after_upgrade = self.cou( + ["plan", "--model", self.model_name, "--no-backup", "--no-archive"] + ).stdout for expected_msg in expected_msg_after_upgrade: with self.subTest(expected_msg): self.assertIn(expected_msg, result_after_upgrade) 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 c38691bb..d9c9c4c4 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 @@ -2,6 +2,7 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'rabbitmq-server' to 'victoria' Upgrade software packages of 'rabbitmq-server' from the current APT repositories 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 9aade41b..45f30d77 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 @@ -2,6 +2,7 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'rabbitmq-server' to 'victoria' Upgrade software packages of 'rabbitmq-server' from the current APT repositories diff --git a/tests/mocked_plans/sample_plans/base.yaml b/tests/mocked_plans/sample_plans/base.yaml index 5b17c52f..33133329 100644 --- a/tests/mocked_plans/sample_plans/base.yaml +++ b/tests/mocked_plans/sample_plans/base.yaml @@ -2,6 +2,7 @@ plan: | Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'keystone' to 'victoria' Upgrade software packages of 'keystone' from the current APT repositories diff --git a/tests/unit/steps/test_nova_cloud_controller.py b/tests/unit/steps/test_nova_cloud_controller.py new file mode 100644 index 00000000..37ab8c2a --- /dev/null +++ b/tests/unit/steps/test_nova_cloud_controller.py @@ -0,0 +1,103 @@ +# 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 AsyncMock, MagicMock, call + +import pytest + +from cou.exceptions import ApplicationNotFound, COUException, UnitNotFound +from cou.steps.nova_cloud_controller import archive +from tests.unit.utils import get_status + + +@pytest.mark.asyncio +async def test_archive_succeeds(model): + model.get_charm_name.side_effect = lambda x: x + model.run_action.return_value = action = MagicMock() + action.data = {"results": {"archive-deleted-rows": "Nothing was archived."}} + + await archive(model, batch_size=999) + + model.run_action.assert_awaited_once_with( + unit_name="nova-cloud-controller/0", + action_name="archive-data", + raise_on_failure=True, + action_params={"batch-size": 999}, + ) + + +@pytest.mark.asyncio +async def test_archive_with_broken_charm_action(model): + model.get_charm_name.side_effect = lambda x: x + model.run_action.return_value = action = MagicMock() + # simulate the expected archive-deleted-rows key missing + action.data = {"results": {}} + + # It should raise an expected exception + # (this will be more graceful than a KeyError for example). + with pytest.raises(COUException, match="archive-deleted-rows"): + await archive(model, batch_size=999) + + model.run_action.assert_awaited_once_with( + unit_name="nova-cloud-controller/0", + action_name="archive-data", + raise_on_failure=True, + action_params={"batch-size": 999}, + ) + + +@pytest.mark.asyncio +async def test_archive_app_not_found(model): + # Update the mocked status so a nova-cloud-controller charm doesn't exist + status = get_status() + del status.applications["nova-cloud-controller"] + model.get_status = AsyncMock(return_value=status) + + with pytest.raises(ApplicationNotFound): + await archive(model, batch_size=999) + + model.run_action.assert_not_called() + + +@pytest.mark.asyncio +async def test_archive_unit_not_found(model): + # Update the mocked status so nova-cloud-controller doesn't have any units + status = get_status() + status.applications["nova-cloud-controller"].units = {} + model.get_status = AsyncMock(return_value=status) + + with pytest.raises(UnitNotFound): + await archive(model, batch_size=999) + + model.run_action.assert_not_called() + + +@pytest.mark.asyncio +async def test_archive_handles_multiple_batches(model): + model.get_charm_name.side_effect = lambda x: x + model.run_action.side_effect = [ + MagicMock(data={"results": {"archive-deleted-rows": "placeholder 25 rows"}}), + MagicMock(data={"results": {"archive-deleted-rows": "Nothing was archived."}}), + ] + + await archive(model, batch_size=999) + + assert model.run_action.call_count == 2 + expected_call = call( + unit_name="nova-cloud-controller/0", + action_name="archive-data", + raise_on_failure=True, + action_params={"batch-size": 999}, + ) + model.run_action.assert_has_awaits([expected_call, expected_call]) diff --git a/tests/unit/steps/test_plan.py b/tests/unit/steps/test_plan.py index ab13270f..69ce6bfe 100644 --- a/tests/unit/steps/test_plan.py +++ b/tests/unit/steps/test_plan.py @@ -44,6 +44,7 @@ from cou.steps.analyze import Analysis from cou.steps.backup import backup from cou.steps.hypervisor import HypervisorGroup, HypervisorUpgradePlanner +from cou.steps.nova_cloud_controller import archive from cou.utils import app_utils from cou.utils.juju_utils import Machine, Unit from cou.utils.openstack import OpenStackRelease @@ -149,6 +150,7 @@ async def test_generate_plan(mock_filter_hypervisors, model, cli_args): Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane principal(s) upgrade plan Upgrade plan for 'keystone' to 'victoria' Upgrade software packages of 'keystone' from the current APT repositories @@ -315,6 +317,7 @@ async def test_generate_plan_with_warning_messages(mock_filter_hypervisors, mode Upgrade cloud from 'ussuri' to 'victoria' Verify that all OpenStack applications are in idle state Back up MySQL databases + Archive old database data on nova-cloud-controller Control Plane subordinate(s) upgrade plan Upgrade plan for 'keystone-ldap' to 'victoria' Refresh 'keystone-ldap' to the latest revision of 'ussuri/stable' @@ -1012,6 +1015,7 @@ async def test_get_upgradable_hypervisors_machines( @pytest.mark.parametrize("cli_backup", [True, False]) def test_get_pre_upgrade_steps(cli_backup, cli_args, model): cli_args.backup = cli_backup + cli_args.archive_batch_size = 998 mock_analysis_result = MagicMock(spec=Analysis)() mock_analysis_result.current_cloud_o7k_release = OpenStackRelease("ussuri") mock_analysis_result.model = model @@ -1035,6 +1039,14 @@ def test_get_pre_upgrade_steps(cli_backup, cli_args, model): ) ) + expected_steps.append( + PreUpgradeStep( + description="Archive old database data on nova-cloud-controller", + parallel=False, + coro=archive(model, batch_size=998), + ) + ) + pre_upgrade_steps = cou_plan._get_pre_upgrade_steps(mock_analysis_result, cli_args) assert pre_upgrade_steps == expected_steps diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 833e6d72..d0464645 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -81,6 +81,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -93,6 +95,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=False, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -105,6 +109,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=True, backup=False, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -117,6 +123,50 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=True, backup=False, force=True, + archive_batch_size=1000, + archive=True, + **{"upgrade_group": None} + ), + ), + ( + ["plan", "--no-archive"], + CLIargs( + command="plan", + model_name=None, + verbosity=0, + quiet=False, + backup=True, + force=False, + archive_batch_size=1000, + archive=False, + **{"upgrade_group": None} + ), + ), + ( + ["plan", "--archive"], + CLIargs( + command="plan", + model_name=None, + verbosity=0, + quiet=False, + backup=True, + force=False, + archive_batch_size=1000, + archive=True, + **{"upgrade_group": None} + ), + ), + ( + ["plan", "--archive-batch-size", "564"], + CLIargs( + command="plan", + model_name=None, + verbosity=0, + quiet=False, + backup=True, + force=False, + archive_batch_size=564, + archive=True, **{"upgrade_group": None} ), ), @@ -129,6 +179,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -141,6 +193,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -153,6 +207,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -167,6 +223,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -181,6 +239,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -195,6 +255,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -209,6 +271,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -221,6 +285,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -235,6 +301,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -249,6 +317,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=True, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones={"1", "2", "3"}, **{"upgrade_group": "hypervisors"} @@ -263,6 +333,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=True, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines=None, availability_zones={"1", "2", "3"}, **{"upgrade_group": "hypervisors"} @@ -278,6 +350,8 @@ def test_parse_args_quiet_verbose_exclusive(args): quiet=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3", "4"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -305,6 +379,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -318,6 +394,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=False, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -331,6 +409,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=False, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -344,6 +424,53 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=False, force=True, + archive_batch_size=1000, + archive=True, + **{"upgrade_group": None} + ), + ), + ( + ["upgrade", "--no-archive"], + CLIargs( + command="upgrade", + model_name=None, + verbosity=0, + quiet=False, + auto_approve=False, + backup=True, + force=False, + archive_batch_size=1000, + archive=False, + **{"upgrade_group": None} + ), + ), + ( + ["upgrade", "--archive"], + CLIargs( + command="upgrade", + model_name=None, + verbosity=0, + quiet=False, + auto_approve=False, + backup=True, + force=False, + archive_batch_size=1000, + archive=True, + **{"upgrade_group": None} + ), + ), + ( + ["upgrade", "--archive-batch-size", "564"], + CLIargs( + command="upgrade", + model_name=None, + verbosity=0, + quiet=False, + auto_approve=False, + backup=True, + force=False, + archive_batch_size=564, + archive=True, **{"upgrade_group": None} ), ), @@ -357,6 +484,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": None} ), ), @@ -370,6 +499,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -383,6 +514,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -396,6 +529,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -413,6 +548,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -428,6 +565,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=False, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -443,6 +582,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones=None, **{"upgrade_group": "data-plane"} @@ -458,6 +599,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -471,6 +614,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, **{"upgrade_group": "control-plane"} ), ), @@ -484,6 +629,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -499,6 +646,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -514,6 +663,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones={"1", "2", "3"}, **{"upgrade_group": "hypervisors"} @@ -529,6 +680,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=False, + archive_batch_size=1000, + archive=True, machines=None, availability_zones={"1", "2", "3"}, **{"upgrade_group": "hypervisors"} @@ -544,6 +697,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=False, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines=None, availability_zones={"1", "2", "3"}, **{"upgrade_group": "hypervisors"} @@ -567,6 +722,8 @@ def test_parse_args_plan(args, expected_CLIargs): auto_approve=True, backup=True, force=True, + archive_batch_size=1000, + archive=True, machines={"1", "2", "3", "4"}, availability_zones=None, **{"upgrade_group": "hypervisors"} @@ -594,6 +751,23 @@ def test_parse_args_hypervisors_exclusive_options(args): commands.parse_args(args) +@pytest.mark.parametrize( + "args", + [ + ["upgrade", "--archive-batch-size", "asdf"], + ["plan", "--archive-batch-size", "asdf"], + ["upgrade", "--archive-batch-size", "-4"], + ["plan", "--archive-batch-size", "-4"], + ["upgrade", "--archive-batch-size", "0"], + ["plan", "--archive-batch-size", "0"], + ], +) +def test_parse_invalid_args(args): + """Generic test for various invalid sets of args.""" + with pytest.raises(SystemExit, match="2"): + commands.parse_args(args) + + @pytest.mark.parametrize( "args", [