From a1c8130142d8b9a1d729e373ad7d9356ae0544b9 Mon Sep 17 00:00:00 2001 From: Martin Kalcok Date: Wed, 30 Apr 2025 13:38:13 +0200 Subject: [PATCH 1/3] tox: Avoid running setup.py directly Direct calls to setup.py have been deprecated [0] and they recently started to cause functional test to fail with error: NotImplementedError: Support for egg-based install has been removed. This change removes explicit setup.py calls from tox commands, as it shouldn't be necessary to explicitly install local project into the tox virtual environment anyway. [0] https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html Signed-off-by: Martin Kalcok --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 85167558..f6423005 100644 --- a/tox.ini +++ b/tox.ini @@ -52,23 +52,19 @@ commands = sphinx-build -W -b html -d {toxinidir}/doc/build/doctrees . {toxinidi [testenv:func] basepython = python3 commands = - {envdir}/bin/python3 setup.py install functest-run-suite --keep-faulty-model [testenv:func-target] basepython = python3 commands = - {envdir}/bin/python3 setup.py install functest-run-suite --keep-model --bundle {posargs} [testenv:func-target-extended] basepython = python3 commands = - {envdir}/bin/python3 setup.py install functest-run-suite --keep-model --test-directory {toxinidir}/tests-extended --log INFO --bundle {posargs} [testenv:remove-placement] basepython = python3 commands = - {envdir}/bin/python3 setup.py install remove-placement {posargs} From 3bce1a740ceb48c35ef44e72e67b729d0665148e Mon Sep 17 00:00:00 2001 From: Martin Kalcok Date: Wed, 30 Apr 2025 13:46:20 +0200 Subject: [PATCH 2/3] setup.cfg: Replace dashes with underscores. Per deprecation notice from setuptools, multi-word keys that use dashes to separate words are deprecated and will stop working by 2026-Mar-03. The dashes should be replaced with underscores [0]. [0] https://setuptools.pypa.io/en/latest/userguide/declarative_config.html Signed-off-by: Martin Kalcok --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3aaec771..52771e0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,10 +2,10 @@ name = zaza summary = A Python3-only functional test framework for OpenStack Charms version = 0.0.2.dev1 -description-file = +description_file = README.rst author = OpenStack Charmers -author-email = openstack-charmers@lists.ubuntu.com +author_email = openstack-charmers@lists.ubuntu.com url = https://github.com/openstack-charmers/zaza classifier = Development Status :: 2 - Pre-Alpha From a361300cd35daa7d282425375a96fd49119423ff Mon Sep 17 00:00:00 2001 From: Martin Kalcok Date: Fri, 25 Apr 2025 16:43:40 +0200 Subject: [PATCH 3/3] juju_utils: fix assumptions about 'subordinate-to' Few places in the code made assumption that for subordinate charms, the 'subordinate-to' list would contain only principal charms. However that is not the case. If a suboridnate charm is related to other subordinate charms, they show up in each other's 'subordinate-to' lists. This was especially problematic in the 'get_machines_for_application' function which would end up in an infinite loop. Signed-off-by: Martin Kalcok --- unit_tests/test_zaza_model.py | 31 +++++++++++++++++-- .../utilities/test_zaza_utilities_juju.py | 25 +++++++++++++-- zaza/model.py | 14 +++++++-- zaza/utilities/juju.py | 16 ++++++++-- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index 54a37b0f..c9eaa97c 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -245,6 +245,7 @@ def fail_on_use(): self.machine_data = {self.key: self.key_data} self.unit = "app/1" self.application = "app" + self.subordinate_application = "subordinate_application" self.subordinate_application_data = { "subordinate-to": [self.application], @@ -252,18 +253,44 @@ def fail_on_use(): self.subordinate_unit = "subordinate_application/1" self.subordinate_unit_data = { "workload-status": {"status": "active"}} + + # Second subordinate app is related both to the principal application + # and to the first subordinate application + self.second_subordinate_application = "second_subordinate_application" + self.second_subordinate_application_data = { + "subordinate-to": [ + self.subordinate_application, + self.application + ], + "units": None} + self.second_subordinate_unit = "second_subordinate_application/1" + self.second_subordinate_unit_data = { + "workload-status": {"status": "active"}} + # Update the suboridnate list on the first subordinate as well + self.subordinate_application_data["subordinate-to"].insert( + 0, + self.second_subordinate_application, + ) + self.unit_data = { "workload-status": {"status": "active"}, "machine": self.machine, "subordinates": { - self.subordinate_unit: self.subordinate_unit_data}} + self.subordinate_unit: self.subordinate_unit_data, + self.second_subordinate_unit: + self.second_subordinate_unit_data, + } + } self.application_data = {"units": { self.unit1.name: self.subordinate_unit_data, self.unit: self.unit_data}} self.juju_status = mock.MagicMock() self.juju_status.applications = { self.application: self.application_data, - self.subordinate_application: self.subordinate_application_data} + self.subordinate_application: self.subordinate_application_data, + self.second_subordinate_application: + self.second_subordinate_application_data, + } self.juju_status.machines = self.machine_data async def _connect_model(model_name): diff --git a/unit_tests/utilities/test_zaza_utilities_juju.py b/unit_tests/utilities/test_zaza_utilities_juju.py index 0ef6b863..75578a2b 100644 --- a/unit_tests/utilities/test_zaza_utilities_juju.py +++ b/unit_tests/utilities/test_zaza_utilities_juju.py @@ -78,18 +78,39 @@ def fail_on_use(): self.unit2_mock.data = {'machine-id': self.machine2} self.application = "app" + self.subordinate_application = "subordinate_application" self.subordinate_application_unit = "subordinate_application/0" self.subordinate_application_data = { "subordinate-to": [self.application]} + + # Second subordinate app is related both to the principal application + # and to the first subordinate application + self.second_subordinate_application = "second_subordinate" + self.second_subordinate_application_unit = "second_subordinate/0" + self.second_subordinate_application_data = { + "subordinate-to": [self.subordinate_application, self.application]} + # Update the suboridnate list on the first subordinate as well + self.subordinate_application_data['subordinate-to'].insert( + 0, + self.second_subordinate_application + ) + self.application_data = { "units": {self.unit1: self.unit1_data}, - "subordinates": {self.subordinate_application_unit: {}}} + "subordinates": { + self.subordinate_application_unit: {}, + self.second_subordinate_application_unit: {}, + } + } self.juju_status = mock.MagicMock() self.juju_status.name = "juju_status_object" self.juju_status.applications = { self.application: self.application_data, - self.subordinate_application: self.subordinate_application_data} + self.subordinate_application: self.subordinate_application_data, + self.second_subordinate_application: + self.second_subordinate_application_data, + } self.juju_status.machines = { self.machine0: self.machine0_mock, self.machine1: self.machine1_mock, diff --git a/zaza/model.py b/zaza/model.py index ad48e5b4..c0f36fa6 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -40,6 +40,7 @@ from zaza import sync_wrapper import zaza.utilities.generic as generic_utils +import zaza.utilities.juju as juju_utils import zaza.utilities.exceptions as zaza_exceptions from zaza.utilities import deprecate @@ -2570,8 +2571,14 @@ async def _unit_status(): # unit lead_app_name = subordinate_principal if not subordinate_principal: - lead_app_name = model_status.applications[app][ - 'subordinate-to'][0] + for app_ in model_status.applications[app]['subordinate-to']: + app_status = model_status.applications[app_] + if not juju_utils.is_subordinate_application(app_, + app_status, + model_name): + lead_app_name = app_ + break + units = model_status.applications[lead_app_name]['units'] for unit in units.values(): try: @@ -2582,7 +2589,8 @@ async def _unit_status(): pass else: # pragma: no cover raise ValueError('{} does not exist as a subordinate under a ' - 'principal'.format(unit_name)) + 'principal to {}'.format(unit_name, + lead_app_name)) if negate_match: return v != status else: diff --git a/zaza/utilities/juju.py b/zaza/utilities/juju.py index 208130c0..719a79ae 100644 --- a/zaza/utilities/juju.py +++ b/zaza/utilities/juju.py @@ -122,7 +122,12 @@ def get_principle_applications(application_name, application_status=None, status = application_status or get_application_status( application_name, model_name=model_name) - return status.get("subordinate-to") + # A subodrinate application can be related to other + # subordinate applications, such that they both show up in + # each other's 'subordinate-to' list. We need to filter them out + # from the list that we are returning. + return [app for app in status.get("subordinate-to") + if not is_subordinate_application(app, model_name=model_name)] def get_machines_for_application(application, model_name=None): @@ -142,8 +147,15 @@ def get_machines_for_application(application, model_name=None): # libjuju juju status no longer has units for subordinate charms # Use the application it is subordinate-to to find machines if is_subordinate_application(application, model_name=model_name): + principal = get_principle_applications(application, + model_name=model_name) + if not principal: + logging.warn(f"Suboridnate application {application} is not" + " related to any principal application.") + return + yield from get_machines_for_application( - status.get("subordinate-to")[0], + principal[0], model_name=model_name) else: for unit in status.get("units").keys():