From 9d8d26b17210265dd58cba56eb5252da611ac3c3 Mon Sep 17 00:00:00 2001 From: Robert Gildein Date: Tue, 28 May 2024 08:22:43 +0200 Subject: [PATCH] Add support for Juju 3.4 (#410) Fix getting results from `run_on_unit` and `run_action`. Also, series was replaced with base in juju status, so the small function to convert base to series was required. --------- Co-authored-by: james_lin --- .github/workflows/check.yaml | 8 +- cou/exceptions.py | 10 +- cou/steps/backup.py | 4 +- cou/utils/app_utils.py | 4 +- cou/utils/juju_utils.py | 57 ++-- setup.cfg | 2 +- tests/functional/tests/model.py | 3 +- tests/unit/jujustatus.json | 373 ++++++++++++-------------- tests/unit/steps/test_backup.py | 7 +- tests/unit/utils/test_app_utils.py | 12 +- tests/unit/utils/test_juju_utils.py | 56 ++-- tests/unit/utils/test_nova_compute.py | 4 +- 12 files changed, 243 insertions(+), 297 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 4058a89f..0407c538 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,7 +5,7 @@ on: workflow_dispatch: pull_request: types: [ opened, synchronize, reopened ] - branches: [ master, main ] + branches: [ main, '*.x' ] paths-ignore: - '**.md' - '**.rst' @@ -109,11 +109,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Setup Juju 2.9/stable environment + - name: Setup Juju 3.4/stable environment uses: charmed-kubernetes/actions-operator@main with: provider: lxd - juju-channel: 2.9/stable + juju-channel: 3.4/stable - name: Remove tox install by actions-operator run: sudo apt remove tox -y - name: Install tox @@ -125,4 +125,4 @@ jobs: with: name: SNAP_FILE - name: Run func tests - run: TEST_SNAP=$GITHUB_WORKSPACE/charmed-openstack-upgrader.snap tox -e func + run: TEST_JUJU3=1 TEST_SNAP=$GITHUB_WORKSPACE/charmed-openstack-upgrader.snap tox -e func diff --git a/cou/exceptions.py b/cou/exceptions.py index 2e3e0ff6..94b93b0d 100644 --- a/cou/exceptions.py +++ b/cou/exceptions.py @@ -30,12 +30,12 @@ def __init__(self, cmd: str, result: dict): :param cmd: Command that was run :type cmd: string :param result: Dict returned by juju containing the output of the command - :type result: dict - {'Code': '0', 'Stdout': '', 'Stderr':''} + :type result: dict - {'return-code': '0', 'stdout': '', 'stderr':''} """ - code = result.get("Code") - output = result.get("Stdout") - err = result.get("Stderr") - msg = f"Command {cmd} failed with code {code}, output {output} and error {err}" + code = result.get("return-code") + stdout = result.get("stdout") + stderr = result.get("stderr") + msg = f"Command {cmd} failed with code {code}, output {stdout} and error {stderr}" super().__init__(msg) diff --git a/cou/steps/backup.py b/cou/steps/backup.py index e1d39947..f27171b4 100644 --- a/cou/steps/backup.py +++ b/cou/steps/backup.py @@ -37,8 +37,8 @@ async def backup(model: Model) -> Path: logger.info("mysqldump mysql-innodb-cluster DBs ...") action = await model.run_action(unit_name, "mysqldump") - remote_file = action.data["results"]["mysqldump-file"] - basedir = action.data["parameters"]["basedir"] + remote_file = Path(action.results["mysqldump-file"]) + basedir = remote_file.parent logger.info("Set permissions to read mysql-innodb-cluster:%s ...", basedir) await model.run_on_unit(unit_name, f"chmod o+rx {basedir}") diff --git a/cou/utils/app_utils.py b/cou/utils/app_utils.py index 5acc3867..c49d65a6 100644 --- a/cou/utils/app_utils.py +++ b/cou/utils/app_utils.py @@ -83,7 +83,7 @@ async def _get_required_osd_release(unit: str, model: Model) -> str: check_option_result = await model.run_on_unit( unit_name=unit, command=check_command, timeout=600 ) - current_require_osd_release = json.loads(check_option_result["Stdout"]).get( + current_require_osd_release = json.loads(check_option_result["stdout"]).get( "require_osd_release", "" ) logger.debug("Current require-osd-release is set to: %s", current_require_osd_release) @@ -109,7 +109,7 @@ async def _get_current_osd_release(unit: str, model: Model) -> str: check_osd_result = await model.run_on_unit(unit_name=unit, command=check_command, timeout=600) - osd_release_output = json.loads(check_osd_result["Stdout"]).get("osd", None) + osd_release_output = json.loads(check_osd_result["stdout"]).get("osd", None) # throw exception if ceph-mon doesn't contain osd release information in `ceph` if not osd_release_output: raise RunUpgradeError(f"Cannot get OSD release information on ceph-mon unit '{unit}'.") diff --git a/cou/utils/juju_utils.py b/cou/utils/juju_utils.py index 6986980e..7760eb51 100644 --- a/cou/utils/juju_utils.py +++ b/cou/utils/juju_utils.py @@ -25,12 +25,13 @@ from juju.action import Action from juju.application import Application as JujuApplication -from juju.client._definitions import FullStatus +from juju.client._definitions import Base, FullStatus from juju.client.connector import NoConnectionException from juju.client.jujudata import FileJujuData from juju.errors import JujuAppError, JujuError, JujuUnitError from juju.model import Model as JujuModel from juju.unit import Unit as JujuUnit +from juju.utils import get_version_series from macaroonbakery.httpbakery import BakeryException from six import wraps @@ -56,37 +57,16 @@ logger = logging.getLogger(__name__) -def _normalize_action_results(results: dict[str, str]) -> dict[str, str]: - """Unify action results format. +def _convert_base_to_series(base: Base) -> str: + """Convert base to series. - :param results: Results dictionary to process. - :type results: dict[str, str] - :returns: { - 'Code': '', - 'Stderr': '', - 'Stdout': '', - 'stderr': '', - 'stdout': ''} - :rtype: dict[str, str] + :param base: Base object + :type base: juju.client._definitions.Base + :return: converted channel to series, e.g. 20.04 -> focal + :rtype: str """ - if results: - # In Juju 2.7 some keys are dropped from the results if their - # value was empty. This breaks some functions downstream, so - # ensure the keys are always present. - for key in ["Stderr", "Stdout", "stderr", "stdout"]: - results[key] = results.get(key, "") - # Juju has started using a lowercase "stdout" key in new action - # commands in recent versions. Ensure the old capatalised keys and - # the new lowercase keys are present and point to the same value to - # avoid breaking functions downstream. - for key in ["stderr", "stdout"]: - old_key = key.capitalize() - if results.get(key) and not results.get(old_key): - results[old_key] = results[key] - elif results.get(old_key) and not results.get(key): - results[key] = results[old_key] - return results - return {} + version, *_ = base.channel.split("/") + return get_version_series(version) def retry( @@ -379,7 +359,7 @@ async def get_applications(self) -> dict[str, Application]: }, model=self, origin=status.charm.split(":")[0], - series=status.series, + series=_convert_base_to_series(status.base), subordinate_to=status.subordinate_to, units={ name: Unit(name, machines[unit.machine], unit.workload_version) @@ -471,7 +451,7 @@ async def run_on_unit( :type command: str :param timeout: How long in seconds to wait for command to complete :type timeout: Optional[int] - :returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''} + :returns: action.results {'return-code': '', 'stderr': '', 'stdout': ''} :rtype: dict[str, str] :raises UnitNotFound: When a valid unit cannot be found. :raises CommandRunFailed: When a command fails to run. @@ -479,15 +459,14 @@ async def run_on_unit( logger.debug("Running '%s' on '%s'", command, unit_name) unit = await self._get_unit(unit_name) - action = await unit.run(command, timeout=timeout) - results = action.data.get("results") - normalize_results = _normalize_action_results(results) + action = await unit.run(command, timeout=timeout, block=True) + results = action.results + logger.debug("results: %s", results) - if str(normalize_results["Code"]) != "0": - raise CommandRunFailed(cmd=command, result=normalize_results) - logger.debug(normalize_results["Stdout"]) + if str(results["return-code"]) != "0": + raise CommandRunFailed(cmd=command, result=results) - return normalize_results + return results @retry(no_retry_exceptions=(ApplicationNotFound,)) async def set_application_config(self, name: str, configuration: dict[str, str]) -> None: diff --git a/setup.cfg b/setup.cfg index 8f7c46aa..83155164 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ python_requires = >=3.10 packages = find: install_requires = oslo.config - juju<3.0 + juju colorama packaging aioconsole diff --git a/tests/functional/tests/model.py b/tests/functional/tests/model.py index dd7307f9..15c5ea6a 100644 --- a/tests/functional/tests/model.py +++ b/tests/functional/tests/model.py @@ -45,12 +45,13 @@ def test_get_status(self): def test_run_action(self): """Test run action.""" action = zaza.sync_wrapper(self.model.run_action)(TESTED_UNIT, "resume") + self.assertEqual(0, action.results["return-code"]) self.assertEqual("completed", action.data["status"]) def test_run_on_unit(self): """Test run command on unit.""" results = zaza.sync_wrapper(self.model.run_on_unit)(TESTED_UNIT, "actions/resume") - self.assertIn("active", results["Stdout"]) + self.assertIn("active", results["stdout"]) def test_scp_from_unit(self): """Test copy file from unit.""" diff --git a/tests/unit/jujustatus.json b/tests/unit/jujustatus.json index 88bbbe9d..3eb8851c 100644 --- a/tests/unit/jujustatus.json +++ b/tests/unit/jujustatus.json @@ -5,7 +5,7 @@ "controller": "serverstack-serverstack", "cloud": "serverstack", "region": "serverstack", - "version": "2.9.42", + "version": "3.4.2", "model-status": { "current": "available", "since": "12 Jul 2023 12:36:01+03:00" @@ -17,7 +17,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:15+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-0", "dns-name": "10.5.0.61", @@ -28,14 +28,17 @@ "instance-id": "03e7d6e2-eb4a-4a09-8fb2-4698d0b5d71b", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:57+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -62,7 +65,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:16+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-1", "dns-name": "10.5.0.119", @@ -73,14 +76,17 @@ "instance-id": "35c00523-2803-4554-a963-b3c609d93308", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:58+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -107,7 +113,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:43+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-10", "dns-name": "10.5.0.38", @@ -118,14 +124,17 @@ "instance-id": "855e53b6-ffa2-47ec-9c77-264cf76f04e9", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:58+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:38:00+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -152,7 +161,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:26+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-11", "dns-name": "10.5.0.6", @@ -163,14 +172,17 @@ "instance-id": "68bc1d80-0c9b-4699-a8c5-80d2e53c5961", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:58+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:38:01+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -197,7 +209,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:32+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-2", "dns-name": "10.5.0.72", @@ -208,14 +220,17 @@ "instance-id": "63de6c52-809c-4c82-a4f9-0e72f5b0f791", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:58+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -242,7 +257,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:38:58+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-3", "dns-name": "10.5.0.40", @@ -253,14 +268,17 @@ "instance-id": "f8e7c796-abce-440a-bf10-b0d6ab83a1f0", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:23+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:58+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -287,7 +305,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:10+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-4", "dns-name": "10.5.0.139", @@ -298,14 +316,17 @@ "instance-id": "9ad440c6-551b-42ae-8294-e11b0c281cb7", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:58+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -332,7 +353,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:38:59+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-5", "dns-name": "10.5.0.157", @@ -343,14 +364,17 @@ "instance-id": "fdeb8ae2-eab6-4c8d-9028-cfb50047e121", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:59+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -377,7 +401,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:21+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-6", "dns-name": "10.5.0.112", @@ -388,14 +412,17 @@ "instance-id": "58cd351c-0669-4cc2-a50c-56f4c40f15de", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:52+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:59+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -422,7 +449,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:23+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-7", "dns-name": "10.5.0.198", @@ -433,14 +460,17 @@ "instance-id": "b32cd7af-1c19-4d53-99a0-c9770668fae2", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:53+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:37:59+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -467,7 +497,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:25+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-8", "dns-name": "10.5.0.131", @@ -478,14 +508,17 @@ "instance-id": "cde7c9d7-8e4d-42a7-b4d1-30bf844ecd81", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:58+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:38:00+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -512,7 +545,7 @@ "juju-status": { "current": "started", "since": "12 Jul 2023 12:39:25+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "hostname": "juju-75b9b3-zaza-0228b5aec1cf-9", "dns-name": "10.5.0.151", @@ -523,14 +556,17 @@ "instance-id": "e10f4232-2ab5-4248-884f-b85754132c3d", "machine-status": { "current": "running", - "message": "ACTIVE", + "message": "Running", "since": "12 Jul 2023 12:38:58+03:00" }, "modification-status": { - "current": "idle", + "current": "applied", "since": "12 Jul 2023 12:38:00+03:00" }, - "series": "focal", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "network-interfaces": { "ens3": { "ip-addresses": [ @@ -557,8 +593,10 @@ "applications": { "cinder": { "charm": "cinder", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "cinder", "charm-rev": 633, @@ -600,7 +638,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 13:01:04+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "7", @@ -618,7 +656,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:58:25+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.198" @@ -649,8 +687,10 @@ }, "cinder-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -684,8 +724,10 @@ }, "glance": { "charm": "glance", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "glance", "charm-rev": 567, @@ -725,7 +767,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:59:18+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "10", @@ -743,7 +785,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:51:14+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.38" @@ -773,8 +815,10 @@ }, "glance-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -808,8 +852,10 @@ }, "keystone": { "charm": "keystone", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "keystone", "charm-rev": 652, @@ -845,7 +891,7 @@ "juju-status": { "current": "idle", "since": "14 Jul 2023 07:25:03+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "11", @@ -863,7 +909,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:51:03+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.6" @@ -894,8 +940,10 @@ }, "keystone-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -929,8 +977,10 @@ }, "mysql": { "charm": "mysql-innodb-cluster", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-innodb-cluster", "charm-rev": 56, @@ -968,7 +1018,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:55:31+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "0", @@ -983,7 +1033,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:55:31+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "machine": "1", "public-address": "10.5.0.119" @@ -997,7 +1047,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:55:52+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "machine": "2", "public-address": "10.5.0.72" @@ -1016,8 +1066,10 @@ }, "neutron-api": { "charm": "neutron-api", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "neutron-api", "charm-rev": 541, @@ -1059,7 +1111,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:59:45+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "4", @@ -1077,7 +1129,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:54:17+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.139" @@ -1111,8 +1163,10 @@ }, "neutron-api-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -1146,8 +1200,10 @@ }, "neutron-gateway": { "charm": "neutron-gateway", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "neutron-gateway", "charm-rev": 522, @@ -1182,7 +1238,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 13:03:57+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "3", @@ -1204,8 +1260,10 @@ }, "neutron-openvswitch": { "charm": "neutron-openvswitch", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "neutron-openvswitch", "charm-rev": 526, @@ -1243,8 +1301,10 @@ }, "nova-cloud-controller": { "charm": "nova-cloud-controller", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "nova-cloud-controller", "charm-rev": 672, @@ -1297,7 +1357,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 13:04:53+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "6", @@ -1316,7 +1376,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:52:07+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.112" @@ -1353,8 +1413,10 @@ }, "nova-cloud-controller-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -1388,8 +1450,10 @@ }, "nova-compute": { "charm": "nova-compute", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "nova-compute", "charm-rev": 669, @@ -1428,7 +1492,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 13:04:07+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "5", @@ -1443,7 +1507,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 13:00:07+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.157" @@ -1474,8 +1538,10 @@ }, "placement": { "charm": "placement", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "placement", "charm-rev": 81, @@ -1511,7 +1577,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:59:25+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "8", @@ -1529,7 +1595,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:51:24+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "public-address": "10.5.0.131" @@ -1554,8 +1620,10 @@ }, "placement-mysql-router": { "charm": "mysql-router", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "mysql-router", "charm-rev": 90, @@ -1589,8 +1657,10 @@ }, "rabbitmq-server": { "charm": "rabbitmq-server", - "series": "focal", - "os": "ubuntu", + "base": { + "name": "ubuntu", + "channel": "20.04" + }, "charm-origin": "charmhub", "charm-name": "rabbitmq-server", "charm-rev": 176, @@ -1625,7 +1695,7 @@ "juju-status": { "current": "idle", "since": "12 Jul 2023 12:54:31+03:00", - "version": "2.9.42" + "version": "3.4.2" }, "leader": true, "machine": "9", @@ -1646,104 +1716,7 @@ } } }, - "storage": { - "storage": { - "block-devices/0": { - "kind": "block", - "life": "alive", - "status": { - "current": "attached", - "since": "12 Jul 2023 12:39:23+03:00" - }, - "persistent": false, - "attachments": { - "units": { - "cinder/0": { - "machine": "7", - "location": "/dev/loop3", - "life": "alive" - } - } - } - }, - "ephemeral-device/1": { - "kind": "block", - "life": "alive", - "status": { - "current": "attached", - "since": "12 Jul 2023 12:39:01+03:00" - }, - "persistent": false, - "attachments": { - "units": { - "nova-compute/0": { - "machine": "5", - "location": "/dev/disk/by-uuid/1f5578a3-0f07-458d-9a65-356289adf618", - "life": "alive" - } - } - } - } - }, - "volumes": { - "5/1": { - "provider-id": "volume-5-1", - "storage": "ephemeral-device/1", - "attachments": { - "machines": { - "5": { - "device": "loop3", - "read-only": false, - "life": "alive" - } - }, - "units": { - "nova-compute/0": { - "machine": "5", - "location": "/dev/disk/by-uuid/1f5578a3-0f07-458d-9a65-356289adf618", - "life": "alive" - } - } - }, - "pool": "loop", - "size": 10240, - "persistent": false, - "life": "alive", - "status": { - "current": "attached", - "since": "12 Jul 2023 12:39:01+03:00" - } - }, - "7/0": { - "provider-id": "volume-7-0", - "storage": "block-devices/0", - "attachments": { - "machines": { - "7": { - "device": "loop3", - "read-only": false, - "life": "alive" - } - }, - "units": { - "cinder/0": { - "machine": "7", - "location": "/dev/loop3", - "life": "alive" - } - } - }, - "pool": "loop", - "size": 10240, - "persistent": false, - "life": "alive", - "status": { - "current": "attached", - "since": "12 Jul 2023 12:39:23+03:00" - } - } - } - }, + "storage": {}, "controller": { "timestamp": "07:25:20+03:00" } diff --git a/tests/unit/steps/test_backup.py b/tests/unit/steps/test_backup.py index 3b35eec5..6eabb9d2 100644 --- a/tests/unit/steps/test_backup.py +++ b/tests/unit/steps/test_backup.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path from unittest.mock import AsyncMock, MagicMock, call, patch import pytest @@ -24,11 +25,11 @@ @patch("cou.steps.backup.get_database_app_unit_name", new_callable=AsyncMock) async def test_backup(database_app_name, model): unit = "test-unit" - dump_file = "dump-file" - basedir = "basedir" + dump_file = Path("/test/dump-file") + basedir = dump_file.parent database_app_name.return_value = unit model.run_action.return_value = action = MagicMock() - action.data = {"results": {"mysqldump-file": dump_file}, "parameters": {"basedir": basedir}} + action.results = {"mysqldump-file": dump_file} await backup(model) diff --git a/tests/unit/utils/test_app_utils.py b/tests/unit/utils/test_app_utils.py index b55f103e..be57a09b 100644 --- a/tests/unit/utils/test_app_utils.py +++ b/tests/unit/utils/test_app_utils.py @@ -22,7 +22,7 @@ @pytest.mark.asyncio async def test_application_upgrade_packages(model): - model.run_on_unit.return_value = {"Code": "0", "Stdout": "Success"} + model.run_on_unit.return_value = {"return-code": "0", "stdout": "Success"} units = ["keystone/0", "keystone/1"] for unit in units: @@ -51,7 +51,7 @@ async def test_application_upgrade_packages(model): @pytest.mark.asyncio async def test_application_upgrade_packages_with_hold(model): - model.run_on_unit.return_value = {"Code": "0", "Stdout": "Success"} + model.run_on_unit.return_value = {"return-code": "0", "stdout": "Success"} units = ["keystone/0", "keystone/1"] for unit in units: @@ -100,7 +100,7 @@ async def test_set_require_osd_release_option_different_releases( ): mock_get_required_osd_release.return_value = current_required_osd_release mock_get_current_osd_release.return_value = current_osd_release - model.run_on_unit.return_value = {"Code": "0", "Stdout": "Success"} + model.run_on_unit.return_value = {"return-code": "0", "stdout": "Success"} await app_utils.set_require_osd_release_option(unit="ceph-mon/0", model=model) @@ -142,7 +142,7 @@ async def test_get_required_osd_release(model): check_result = """ {"crush_version":7,"min_compat_client":"jewel","require_osd_release":"octopus"} """ - model.run_on_unit.return_value = {"Code": "0", "Stdout": check_result} + model.run_on_unit.return_value = {"return-code": "0", "stdout": check_result} actual_current_release = await app_utils._get_required_osd_release( unit="ceph-mon/0", model=model ) @@ -177,7 +177,7 @@ async def test_get_current_osd_release(model): """ % ( expected_osd_release ) - model.run_on_unit.return_value = {"Code": "0", "Stdout": check_output} + model.run_on_unit.return_value = {"return-code": "0", "stdout": check_output} actual_osd_release = await app_utils._get_current_osd_release(unit="ceph-mon/0", model=model) model.run_on_unit.assert_called_once_with( @@ -231,7 +231,7 @@ async def test_get_current_osd_release_unsuccessful(model, osd_release_output, e """ % ( json.dumps(osd_release_output) ) - model.run_on_unit.return_value = {"Code": "0", "Stdout": check_output} + model.run_on_unit.return_value = {"return-code": "0", "stdout": check_output} with pytest.raises(RunUpgradeError, match=error_message): await app_utils._get_current_osd_release(unit="ceph-mon/0", model=model) diff --git a/tests/unit/utils/test_juju_utils.py b/tests/unit/utils/test_juju_utils.py index 518b5fef..a2386c73 100644 --- a/tests/unit/utils/test_juju_utils.py +++ b/tests/unit/utils/test_juju_utils.py @@ -17,7 +17,7 @@ import pytest from juju.action import Action from juju.application import Application -from juju.client._definitions import ApplicationStatus, UnitStatus +from juju.client._definitions import ApplicationStatus, Base, UnitStatus from juju.client.connector import NoConnectionException from juju.machine import Machine from juju.model import Model @@ -35,6 +35,19 @@ from cou.utils import juju_utils +@pytest.mark.parametrize( + "base, exp_series", + [ + (Base("18.04/stable", "ubuntu"), "bionic"), + (Base("20.04/stable", "ubuntu"), "focal"), + (Base("22.04/stable", "ubuntu"), "jammy"), + ], +) +def test_convert_base_to_series(base, exp_series): + """Test helper function to convert base to series.""" + assert juju_utils._convert_base_to_series(base) == exp_series + + @pytest.fixture def mocked_model(mocker): """Fixture providing mocked juju.model.Model object.""" @@ -49,24 +62,6 @@ def mocked_model(mocker): yield model -def test_normalize_action_results(): - results = {"Stderr": "error", "stdout": "output"} - expected = {"Stderr": "error", "Stdout": "output", "stderr": "error", "stdout": "output"} - - normalized_results = juju_utils._normalize_action_results(results) - - assert normalized_results == expected - - -def test_normalize_action_results_empty_results(): - results = {} - expected = {} - - normalized_results = juju_utils._normalize_action_results(results) - - assert normalized_results == expected - - @pytest.mark.asyncio async def test_retry_without_args(): """Test retry as decorator without any arguments.""" @@ -416,38 +411,34 @@ async def test_coumodel_run_action(mock_get_waited_action_object, mocked_model): @pytest.mark.asyncio -@patch("cou.utils.juju_utils._normalize_action_results") -async def test_coumodel_run_on_unit(mock_normalize_action_results, mocked_model): +async def test_coumodel_run_on_unit(mocked_model): """Test Model run on unit.""" command = "test-command" + results = {"return-code": "0", "stdout": "some results"} mocked_model.units.get.return_value = mocked_unit = AsyncMock(Unit) mocked_unit.run.return_value = mocked_action = AsyncMock(Action) - results = mocked_action.data.get.return_value - mock_normalize_action_results.return_value = {"Code": "0", "Stdout": "some results"} + mocked_action.results = results model = juju_utils.Model("test-model") await model.run_on_unit("test-unit/0", command) - mocked_unit.run.assert_awaited_once_with(command, timeout=None) - mock_normalize_action_results.assert_called_once_with(results) + mocked_unit.run.assert_awaited_once_with(command, timeout=None, block=True) @pytest.mark.asyncio -@patch("cou.utils.juju_utils._normalize_action_results") -async def test_coumodel_run_on_unit_failed_command(mock_normalize_action_results, mocked_model): +async def test_coumodel_run_on_unit_failed_command(mocked_model): """Test Model run on unit.""" command = "test-command" + results = {"return-code": "1", "stdout": "some results"} mocked_model.units.get.return_value = mocked_unit = AsyncMock(Unit) mocked_unit.run.return_value = mocked_action = AsyncMock(Action) - results = mocked_action.data.get.return_value - mock_normalize_action_results.return_value = {"Code": "1", "Stderr": "Error!"} + mocked_action.results = results model = juju_utils.Model("test-model") with pytest.raises(CommandRunFailed): await model.run_on_unit("test-unit/0", command) - mocked_unit.run.assert_awaited_once_with(command, timeout=None) - mock_normalize_action_results.assert_called_once_with(results) + mocked_unit.run.assert_awaited_once_with(command, timeout=None, block=True) @pytest.mark.asyncio @@ -619,6 +610,7 @@ def _generate_app_status(units: dict[str, MagicMock]) -> MagicMock: """Generate app status with units.""" status = MagicMock(spec_set=ApplicationStatus)() status.units = units + status.base = Base("20.04/stable", "ubuntu") return status @@ -692,7 +684,7 @@ async def test_get_applications(mock_get_machines, mock_get_status, mocked_model machines={unit.machine.id: exp_machines[unit.machine.id] for unit in exp_units[app]}, model=model, origin=status.charm.split(":")[0], - series=status.series, + series="focal", subordinate_to=status.subordinate_to, units={ name: juju_utils.Unit(name, exp_machines[unit.machine], unit.workload_version) diff --git a/tests/unit/utils/test_nova_compute.py b/tests/unit/utils/test_nova_compute.py index 76114cfa..4f3ddab2 100644 --- a/tests/unit/utils/test_nova_compute.py +++ b/tests/unit/utils/test_nova_compute.py @@ -25,7 +25,7 @@ async def test_get_instance_count(model): expected_count = 1 model.run_action.return_value = mocked_action = AsyncMock(spec_set=Action).return_value - mocked_action.results = {"Code": "0", "instance-count": str(expected_count)} + mocked_action.results = {"return-code": "0", "instance-count": str(expected_count)} actual_count = await nova_compute.get_instance_count(unit="nova-compute/0", model=model) @@ -47,7 +47,7 @@ async def test_get_instance_count(model): ) async def test_get_instance_count_invalid_result(model, result_key, value): model.run_action.return_value = mocked_action = AsyncMock(spec_set=Action).return_value - mocked_action.results = {"Code": "0", result_key: value} + mocked_action.results = {"return-code": "0", result_key: value} with pytest.raises(ValueError): await nova_compute.get_instance_count(unit="nova-compute/0", model=model)