From e45fd7f275ee626e8a87972e232596c4a42cc38f Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 23 Feb 2023 10:01:25 +0000 Subject: [PATCH 01/26] Misc fixes for failing github workflows Add separate workflow for testing on Python 3.6 as this requires an older Ubuntu release. Fixup unit tests for compatibility with newer versions of mock. Drop latest/stable functional test targets - this is actually 2.9.x of Juju and is already covered by the 2.9 targets and we want to avoid suddenly picking up a new Juju version because this will break with the new approach to version alignment in the Python module for Juju. Drop 2.8 functional test target - its broken and we don't really support this version any longer. Fixup iptables forwarding issues from LXD containers with a flush and re-create of rules. (cherry picked from commit 9277a94c155da8dabb4d2d60c629bbc7fa9c6ccc) --- .github/workflows/tox.yaml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 92da89ed6..bf40e0ad2 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -5,12 +5,36 @@ on: - pull_request jobs: + build_old_versions: + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: ['3.6'] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install codecov tox tox-gh-actions + - name: Lint with tox + run: tox -e pep8 + - name: Test with tox + run: tox -e py + - name: Codecov + run: | + set -euxo pipefail + codecov --verbose --gcov-glob unit_tests/* + build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.8', '3.10', '3.11'] - steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} From 3e7b2a5ec74b2ada7a2c2286ef57b31f5c0eac53 Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Tue, 31 Jan 2023 13:27:47 +0530 Subject: [PATCH 02/26] pin juju upper constraint to <3.2 Juju is pinned to <3.0 earlier. This patch pins the juju version to <3.2 so that libjuju 3.1 version is used. Modified run_on_unit to wait for completion and update results based on output. --- requirements.txt | 3 +- setup.py | 2 +- unit_tests/test_zaza_model.py | 223 +++++++++++++++++++++++++++++++++- zaza/model.py | 67 +++++++++- 4 files changed, 283 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index d4288cbe6..287f7a657 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ pyparsing<3.0.0 # pin for aodhclient which is held for py35 async_generator kubernetes<18.0.0; python_version < '3.6' # pined, as juju uses kubernetes -# pinned until 3.0 regressions are handled: https://github.com/openstack-charmers/zaza/issues/545 -juju<3.0 +juju<3.2 juju_wait PyYAML>=3.0 pbr==5.6.0 diff --git a/setup.py b/setup.py index 168fb9e54..ff6b77623 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ 'hvac<0.7.0', 'jinja2', - 'juju<3.0', + 'juju<3.2', 'juju-wait', 'PyYAML', 'tenacity>8.2.0', diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index b000fd153..66fd9b1bd 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -149,6 +149,7 @@ async def _inner_is_leader(): self.run_action = mock.MagicMock() self.run_action.wait.side_effect = _wait self.action = mock.MagicMock() + self.action.wait.side_effect = _wait self.action.data = { 'model-uuid': '1a035018-71ff-473e-8aab-d1a8d6b6cda7', 'id': 'e26ffb69-6626-4e93-8840-07f7e041e99d', @@ -162,6 +163,9 @@ async def _inner_is_leader(): 'enqueued': '2018-04-11T23:13:42Z', 'started': '2018-04-11T23:13:42Z', 'completed': '2018-04-11T23:13:43Z'} + self.action.results = { + 'return-code': '0', 'stderr': '', 'stdout': 'RESULT' + } self.machine3 = mock.MagicMock(status='active') self.machine7 = mock.MagicMock(status='active') @@ -787,7 +791,47 @@ def test_run_on_unit(self): expected) self.unit1.run.assert_called_once_with(cmd, timeout=None) + def test_run_on_unit_juju2_x(self): + del self.action.results + self.patch_object(model, 'get_juju_model', return_value='mname') + expected = { + 'Code': '0', + 'Stderr': '', + 'Stdout': 'RESULT', + 'stderr': '', + 'stdout': 'RESULT'} + self.cmd = cmd = 'somecommand someargument' + self.patch_object(model, 'Model') + self.patch_object(model, 'get_unit_from_name') + self.get_unit_from_name.return_value = self.unit1 + self.Model.return_value = self.Model_mock + self.assertEqual(model.run_on_unit('app/2', cmd), + expected) + self.unit1.run.assert_called_once_with(cmd, timeout=None) + def test_run_on_unit_lc_keys(self): + self.patch_object(model, 'get_juju_model', return_value='mname') + self.action.results = { + 'return-code': '0', + 'stdout': 'RESULT', + 'stderr': 'some error'} + expected = { + 'Code': '0', + 'Stderr': 'some error', + 'Stdout': 'RESULT', + 'stderr': 'some error', + 'stdout': 'RESULT'} + self.cmd = cmd = 'somecommand someargument' + self.patch_object(model, 'Model') + self.patch_object(model, 'get_unit_from_name') + self.get_unit_from_name.return_value = self.unit1 + self.Model.return_value = self.Model_mock + self.assertEqual(model.run_on_unit('app/2', cmd), + expected) + self.unit1.run.assert_called_once_with(cmd, timeout=None) + + def test_run_on_unit_lc_keys_juju2_x(self): + del self.action.results self.patch_object(model, 'get_juju_model', return_value='mname') self.action.data['results'] = { 'Code': '0', @@ -809,6 +853,25 @@ def test_run_on_unit_lc_keys(self): self.unit1.run.assert_called_once_with(cmd, timeout=None) def test_run_on_unit_missing_stderr(self): + self.patch_object(model, 'get_juju_model', return_value='mname') + expected = { + 'Code': '0', + 'Stderr': '', + 'Stdout': 'RESULT', + 'stderr': '', + 'stdout': 'RESULT'} + self.action.results = {'return-code': '0', 'stdout': 'RESULT'} + self.cmd = cmd = 'somecommand someargument' + self.patch_object(model, 'Model') + self.patch_object(model, 'get_unit_from_name') + self.get_unit_from_name.return_value = self.unit1 + self.Model.return_value = self.Model_mock + self.assertEqual(model.run_on_unit('app/2', cmd), + expected) + self.unit1.run.assert_called_once_with(cmd, timeout=None) + + def test_run_on_unit_missing_stderr_juju2_x(self): + del self.action.results self.patch_object(model, 'get_juju_model', return_value='mname') expected = { 'Code': '0', @@ -841,6 +904,22 @@ def test_run_on_leader(self): expected) self.unit2.run.assert_called_once_with(cmd, timeout=None) + def test_run_on_leader_juju2_x(self): + del self.action.results + self.patch_object(model, 'get_juju_model', return_value='mname') + expected = { + 'Code': '0', + 'Stderr': '', + 'Stdout': 'RESULT', + 'stderr': '', + 'stdout': 'RESULT'} + self.cmd = cmd = 'somecommand someargument' + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.assertEqual(model.run_on_leader('app', cmd), + expected) + self.unit2.run.assert_called_once_with(cmd, timeout=None) + def test_get_relation_id(self): self.patch_object(model, 'get_juju_model', return_value='mname') self.patch_object(model, 'Model') @@ -1047,7 +1126,7 @@ async def _async_get_unit_from_name(x, *args): return units[x] self.async_get_unit_from_name.side_effect = _async_get_unit_from_name - self.run_action.status = 'completed' + self.run_action.data = {'status': 'completed'} model.run_action_on_units( ['app/1', 'app/2'], 'backup', @@ -1065,7 +1144,7 @@ def test_run_action_on_units_timeout(self): self.Model.return_value = self.Model_mock self.patch_object(model, 'get_unit_from_name') self.get_unit_from_name.return_value = self.unit1 - self.run_action.status = 'running' + self.run_action.data = {'status': 'running'} with self.assertRaises(AsyncTimeoutError): model.run_action_on_units( ['app/2'], @@ -1083,7 +1162,7 @@ async def _fake_get_action_output(_): self.Model.return_value = self.Model_mock self.patch_object(model, 'get_unit_from_name') self.get_unit_from_name.return_value = self.unit1 - self.run_action.status = 'failed' + self.run_action.data = {'status': 'failed'} with self.assertRaises(model.ActionFailed): model.run_action_on_units( ['app/2'], @@ -1540,6 +1619,25 @@ def test_get_current_model(self): self.assertEqual(model.get_current_model(), self.model_name) def test_file_contents_success(self): + self.action.results = { + 'return-code': '0', + 'stderr': '', + 'stdout': 'somestring' + } + + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + contents = model.file_contents( + 'app/2', + '/tmp/src/myfile.txt', + timeout=0.1) + self.unit1.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt', timeout=0.1) + self.assertEqual('somestring', contents) + + def test_file_contents_success_juju2_x(self): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': '', 'Stdout': 'somestring'} } @@ -1556,6 +1654,23 @@ def test_file_contents_success(self): self.assertEqual('somestring', contents) def test_file_contents_fault(self): + self.action.results = { + 'return-code': '0', + 'stderr': 'fault', + 'stdout': '' + } + + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + with self.assertRaises(model.RemoteFileError) as ctxtmgr: + model.file_contents('app/2', '/tmp/src/myfile.txt', timeout=0.1) + self.unit1.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt', timeout=0.1) + self.assertEqual(ctxtmgr.exception.args, ('fault',)) + + def test_file_contents_fault_juju2_x(self): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': 'fault', 'Stdout': ''} } @@ -1570,6 +1685,33 @@ def test_file_contents_fault(self): self.assertEqual(ctxtmgr.exception.args, ('fault',)) def test_block_until_file_has_contents(self): + self.action.results = { + 'return-code': '0', + 'stderr': '', + 'stdout': 'somestring' + } + + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + self.patch("builtins.open", + new_callable=mock.mock_open(), + name="_open") + _fileobj = mock.MagicMock() + _fileobj.__enter__().read.return_value = "somestring" + self._open.return_value = _fileobj + model.block_until_file_has_contents( + 'app', + '/tmp/src/myfile.txt', + 'somestring', + timeout=0.1) + self.unit1.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + self.unit2.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + + def test_block_until_file_has_contents_juju2_x(self): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': '', 'Stdout': 'somestring'} } @@ -1594,6 +1736,29 @@ def test_block_until_file_has_contents(self): 'cat /tmp/src/myfile.txt') def test_block_until_file_has_no_contents(self): + self.action.results = {'return-code': '0', 'stderr': ''} + + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + self.patch("builtins.open", + new_callable=mock.mock_open(), + name="_open") + _fileobj = mock.MagicMock() + _fileobj.__enter__().read.return_value = "" + self._open.return_value = _fileobj + model.block_until_file_has_contents( + 'app', + '/tmp/src/myfile.txt', + '', + timeout=0.1) + self.unit1.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + self.unit2.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + + def test_block_until_file_has_no_contents_juju2_x(self): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': ''} } @@ -1639,6 +1804,19 @@ def test_block_until_file_missing(self): self.patch_object(model, 'Model') self.Model.return_value = self.Model_mock self.patch_object(model, 'get_juju_model', return_value='mname') + self.action.results = {'stdout': "1"} + model.block_until_file_missing( + 'app', + '/tmp/src/myfile.txt', + timeout=0.1) + self.unit1.run.assert_called_once_with( + 'test -e "/tmp/src/myfile.txt"; echo $?') + + def test_block_until_file_missing_juju2_x(self): + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + del self.action.results self.action.data['results']['Stdout'] = "1" model.block_until_file_missing( 'app', @@ -1659,6 +1837,27 @@ def test_block_until_file_missing_isnt_missing(self): timeout=0.1) def test_block_until_file_matches_re(self): + self.action.results = { + 'return-code': '0', + 'stderr': '', + 'stdout': 'somestring' + } + + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + self.patch_object(model, 'get_juju_model', return_value='mname') + model.block_until_file_matches_re( + 'app', + '/tmp/src/myfile.txt', + 's.*string', + timeout=0.1) + self.unit1.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + self.unit2.run.assert_called_once_with( + 'cat /tmp/src/myfile.txt') + + def test_block_until_file_matches_re_juju2_x(self): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': '', 'Stdout': 'somestring'} } @@ -1965,6 +2164,24 @@ async def _run_on_unit( def block_until_oslo_config_entries_match_base(self, file_contents, expected_contents): + self.action.results = { + 'return-code': '0', + 'stderr': '', + 'stdout': file_contents + } + self.patch_object(model, 'Model') + self.patch_object(model, 'get_juju_model', return_value='mname') + self.Model.return_value = self.Model_mock + model.block_until_oslo_config_entries_match( + 'app', + '/tmp/src/myfile.txt', + expected_contents, + timeout=0.1) + + def block_until_oslo_config_entries_match_base_juju2_x(self, + file_contents, + expected_contents): + del self.action.results self.action.data = { 'results': {'Code': '0', 'Stderr': '', 'Stdout': file_contents} } diff --git a/zaza/model.py b/zaza/model.py index ccec8150f..2c7141d04 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -545,6 +545,9 @@ def _normalise_action_results(results): results[old_key] = results[key] elif results.get(old_key) and not results.get(key): results[key] = results[old_key] + if 'return-code' in results: + results['Code'] = results.get('return-code') + del results['return-code'] return results else: return {} @@ -567,7 +570,12 @@ async def async_run_on_unit(unit_name, command, model_name=None, timeout=None): model = await get_model(model_name) unit = await async_get_unit_from_name(unit_name, model) action = await unit.run(command, timeout=timeout) - results = action.data.get('results') + await action.wait() + results = None + try: + results = action.results + except (AttributeError, KeyError): + results = action.data.get('results') return _normalise_action_results(results) run_on_unit = sync_wrapper(async_run_on_unit) @@ -593,7 +601,12 @@ async def async_run_on_leader(application_name, command, model_name=None, is_leader = await unit.is_leader_from_status() if is_leader: action = await unit.run(command, timeout=timeout) - results = action.data.get('results') + await action.wait() + results = None + try: + results = action.results + except (AttributeError, KeyError): + results = action.data.get('results') return _normalise_action_results(results) run_on_leader = sync_wrapper(async_run_on_leader) @@ -1071,6 +1084,32 @@ def __init__(self, action, output=None): super(ActionFailed, self).__init__(message) +def _normalise_action_object(action_obj): + """Put run action results in a consistent format. + + Prior to libjuju 3.x, action status and results are + in obj.data['status'] and obj.data['results']. + From libjuju 3.x, status and results are modified + to obj._status and obj.results. + This functiona normalises the status to + obj.data['status'] and results to obj.data['results'] + + :param action_obj: action object + :type results: juju.action.Action + :returns: Updated action object + :rtype: juju.action.Action + """ + try: + # libjuju 3.x + action_obj.data['status'] = action_obj._status + action_obj.data['results'] = action_obj.results + except (AttributeError, KeyError): + # libjuju 2.x format, no changes needed + pass + + return action_obj + + async def async_run_action(unit_name, action_name, model_name=None, action_params=None, raise_on_failure=False): """Run action on given unit. @@ -1096,6 +1135,7 @@ async def async_run_action(unit_name, action_name, model_name=None, unit = await async_get_unit_from_name(unit_name, model) action_obj = await unit.run_action(action_name, **action_params) await action_obj.wait() + action_obj = _normalise_action_object(action_obj) if raise_on_failure and action_obj.status != 'completed': try: output = await model.get_action_output(action_obj.id) @@ -1136,6 +1176,7 @@ async def async_run_action_on_leader(application_name, action_name, action_obj = await unit.run_action(action_name, **action_params) await action_obj.wait() + action_obj = _normalise_action_object(action_obj) if raise_on_failure and action_obj.status != 'completed': try: output = await model.get_action_output(action_obj.id) @@ -1183,14 +1224,14 @@ async def async_run_action_on_units(units, action_name, action_params=None, async def _check_actions(): for action_obj in actions: - if action_obj.status in ['running', 'pending']: + if action_obj.data['status'] in ['running', 'pending']: return False return True await async_block_until(_check_actions, timeout=timeout) for action_obj in actions: - if raise_on_failure and action_obj.status != 'completed': + if raise_on_failure and action_obj.data['status'] != 'completed': try: output = await model.get_action_output(action_obj.id) except KeyError: @@ -2081,7 +2122,13 @@ async def _check_file(): for unit in units: try: output = await unit.run('cat {}'.format(remote_file)) - contents = output.data.get('results').get('Stdout', '') + await output.wait() + results = {} + try: + results = output.results + except (AttributeError, KeyError): + results = output.data.get('results', {}) + contents = results.get('Stdout', results.get('stdout', '')) if inspect.iscoroutinefunction(check_function): if not await check_function(contents): return False @@ -2216,7 +2263,15 @@ async def _check_for_file(model): for unit in units: try: output = await unit.run('test -e "{}"; echo $?'.format(path)) - contents = output.data.get('results')['Stdout'] + await output.wait() + output_result = {} + try: + output_result = output.results + except (AttributeError, KeyError): + output_result = output.data.get('results', {}) + contents = output_result.get( + 'Stdout', output_result.get('stdout', '') + ) results.append("1" in contents) # libjuju throws a generic error for connection failure. So we # cannot differentiate between a connectivity issue and a From 1eacbff6cbd098c31955a1d9845db8cb06d1c845 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 11:37:48 +0000 Subject: [PATCH 03/26] Switch functional testing to Juju 3.1 Update channel in github workflows to use Juju 3.1. Drop --no-gui flag usage as this is the default now. --- .github/workflows/tox.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index bf40e0ad2..c2d7a3fb3 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -59,7 +59,7 @@ jobs: fail-fast: false matrix: juju_channel: - - 2.9/stable + - 3.1/stable bundle: - first - second @@ -88,7 +88,7 @@ jobs: lxc image copy --alias juju/focal/amd64 --copy-aliases ubuntu-daily:focal local: lxc image copy --alias juju/jammy/amd64 --copy-aliases ubuntu-daily:jammy local: lxc image list - juju bootstrap --no-gui localhost + juju bootstrap localhost - name: Functional test run: | set -euxo pipefail From 496746cd6017eb6b9fcb45597822bc17d176aca7 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 11:56:08 +0000 Subject: [PATCH 04/26] Pin Juju < 3.2 for testing --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 371b1326b..acb2737c4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,8 +15,8 @@ jinja2 keystoneauth1 oslo.config python-novaclient -tenacity>8.2.0 -# pinned until 3.0 regressions are handled: https://github.com/openstack-charmers/zaza/issues/545 -juju<3.0 +tenacity # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/94 macaroonbakery!=1.3.3 +# Fix upper version to ensure compatibility with Juju 3.1 +juju<3.2 From e7a5c91e68c77b4b5dc56f5d0016fdde61920247 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 12:24:47 +0000 Subject: [PATCH 05/26] Misc updates for 3.1 compatibility Update model configuration default-series to focal. Drop --classic flag for Juju installation. --- .github/workflows/tox.yaml | 2 +- zaza/utilities/deployment_env.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index c2d7a3fb3..f519c72bc 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -74,7 +74,7 @@ jobs: set -euxo pipefail python -m pip install --upgrade pip pip install tox tox-gh-actions - sudo snap install --channel ${{ matrix.juju_channel }} --classic juju + sudo snap install --channel ${{ matrix.juju_channel }} juju sudo snap install --classic juju-crashdump sudo lxd init --auto # This is a throw-away CI environment, do not do this at home diff --git a/zaza/utilities/deployment_env.py b/zaza/utilities/deployment_env.py index 81799e99f..d225e596c 100644 --- a/zaza/utilities/deployment_env.py +++ b/zaza/utilities/deployment_env.py @@ -47,7 +47,7 @@ MODEL_DEFAULTS = { # Model defaults from charm-test-infra # https://jujucharms.com/docs/2.1/models-config - 'default-series': 'xenial', + 'default-series': 'focal', 'image-stream': 'daily', 'test-mode': 'true', 'transmit-vendor-metrics': 'false', From bb72f5d83c5880de7b977aef1bcce845ef27b585 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 12:40:27 +0000 Subject: [PATCH 06/26] Generate temporary directory under $HOME When juju is strictly confined, random temp directories under /tmp are not accessible - render any templated bundle files under $HOME instead as this should be readable. --- zaza/charm_lifecycle/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zaza/charm_lifecycle/deploy.py b/zaza/charm_lifecycle/deploy.py index dc2d7b301..5ea880be7 100755 --- a/zaza/charm_lifecycle/deploy.py +++ b/zaza/charm_lifecycle/deploy.py @@ -339,7 +339,7 @@ def deploy_bundle(bundle, model, model_ctxt=None, force=False, trust=False): if trust: cmd.append('--trust') bundle_out = None - with tempfile.TemporaryDirectory() as tmpdirname: + with tempfile.TemporaryDirectory(dir=os.environ['HOME']) as tmpdirname: # Bundle templates should only exist in the bundle directory so # explicitly set the Jinja2 load path. bundle_template = get_template( From 59973772c3a0117e5f334370960adfbc17426c4b Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 15:29:47 -0400 Subject: [PATCH 07/26] Make libjuju an extra dependency. python-libjuju is released in lockstep with juju, hence if zaza uses a 2.9 controller, it should use libjuju-2.9.x, for a 3.1 controller it should use libjuju-3.1 and so on. This change makes libjuju an extra, which means depending on the juju controller version will be used the right extra should be passed at install time. For juju-2.9: pip install zaza[juju-29] For juju-3.1: pip install zaza[juju-31] --- .github/workflows/tox.yaml | 7 ++++++- requirements.txt | 1 - setup.py | 11 +++++++---- test-requirements.txt | 4 +--- tox.ini | 1 + 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index f519c72bc..d5c8fecf1 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -64,6 +64,11 @@ jobs: - first - second - third + include: + - juju_channel: 2.9/stable + extra: juju-29 + - juju_channel: 3.1/stable + extra: juju-31 env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build @@ -93,7 +98,7 @@ jobs: run: | set -euxo pipefail mkdir logs - tox -e func-target -- ${{ matrix.bundle }} | tee logs/tox-output.txt + env EXTRA=${{ matrix.extra }} tox -e func-target -- ${{ matrix.bundle }} | tee logs/tox-output.txt - name: crashdump on failure if: failure() run: | diff --git a/requirements.txt b/requirements.txt index 287f7a657..9668b5355 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ pyparsing<3.0.0 # pin for aodhclient which is held for py35 async_generator kubernetes<18.0.0; python_version < '3.6' # pined, as juju uses kubernetes -juju<3.2 juju_wait PyYAML>=3.0 pbr==5.6.0 diff --git a/setup.py b/setup.py index ff6b77623..49830ae50 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,6 @@ 'hvac<0.7.0', 'jinja2', - 'juju<3.2', 'juju-wait', 'PyYAML', 'tenacity>8.2.0', @@ -50,6 +49,12 @@ 'tox >= 2.3.1', ] +extras_require={ + 'testing': tests_require, + 'juju-29': ['juju<3.0'], + 'juju-31': ['juju>=3.1.0,<3.2.0'], +} + class Tox(TestCommand): """Tox class.""" @@ -113,8 +118,6 @@ def run_tests(self): zip_safe=False, cmdclass={'test': Tox}, install_requires=install_require, - extras_require={ - 'testing': tests_require, - }, tests_require=tests_require, + extras_require=extras_require, ) diff --git a/test-requirements.txt b/test-requirements.txt index acb2737c4..ff5879e3c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -15,8 +15,6 @@ jinja2 keystoneauth1 oslo.config python-novaclient -tenacity +tenacity>8.2.0 # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/94 macaroonbakery!=1.3.3 -# Fix upper version to ensure compatibility with Juju 3.1 -juju<3.2 diff --git a/tox.ini b/tox.ini index 3830b9a1d..a381068a3 100644 --- a/tox.ini +++ b/tox.ini @@ -55,6 +55,7 @@ commands = [testenv:func-target] basepython = python3 +extras = {env:EXTRA:juju-29} deps = -r{toxinidir}/test-requirements.txt commands = {envdir}/bin/python3 setup.py install From 2c03b5e99d6d7e04b2747de6e6e23171b303f324 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 15:56:15 -0400 Subject: [PATCH 08/26] Add juju-3.2 support Summary of changes: - Add juju-3.2 to the github workflow matrix - Add 'juju-32' extra to install juju-3.2.x --- .github/workflows/tox.yaml | 2 ++ setup.py | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index d5c8fecf1..b66c4fc38 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -69,6 +69,8 @@ jobs: extra: juju-29 - juju_channel: 3.1/stable extra: juju-31 + - juju_channel: 3.2/stable + extra: juju-32 env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build diff --git a/setup.py b/setup.py index 49830ae50..5565d73d3 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ 'testing': tests_require, 'juju-29': ['juju<3.0'], 'juju-31': ['juju>=3.1.0,<3.2.0'], + 'juju-32': ['juju>=3.2.0,<3.3.0'], } From cab27ecacf52e0a77073cedf048bb424e0aed839 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 16:06:01 -0400 Subject: [PATCH 09/26] Move 'extras' to [testenv] --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a381068a3..5fe8ff5f7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,9 @@ passenv = CS_* OS_* TEST_* +extras = {env:EXTRA:juju-29} +deps = + -r{toxinidir}/test-requirements.txt commands = pytest --cov=./zaza/ {posargs} {toxinidir}/unit_tests [testenv:py3] @@ -55,7 +58,6 @@ commands = [testenv:func-target] basepython = python3 -extras = {env:EXTRA:juju-29} deps = -r{toxinidir}/test-requirements.txt commands = {envdir}/bin/python3 setup.py install From 278c541ef0b32c5578c52721c999d76836145c26 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 16:10:45 -0400 Subject: [PATCH 10/26] Set skipsdist to False This forces tox to install the zaza python package and honor the 'extras' defined. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 5fe8ff5f7..e943496cf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = pep8,py3 -skipsdist = True +skipsdist = False # NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci minversion = 3.2.0 From dcd22643150d83bdb78a57604cc252efea630866 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 16:21:44 -0400 Subject: [PATCH 11/26] Fix testing matrix --- .github/workflows/tox.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index b66c4fc38..3a7389203 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -59,7 +59,9 @@ jobs: fail-fast: false matrix: juju_channel: + - 2.9/stable - 3.1/stable + - 3.2/stable bundle: - first - second From 25c5f74ca9ba374f699e7a5cfa15c5dac97f37bd Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 14 Jul 2023 16:27:03 -0400 Subject: [PATCH 12/26] Add --classic when install juju 2.9 --- .github/workflows/tox.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 3a7389203..ae100a03c 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -69,10 +69,13 @@ jobs: include: - juju_channel: 2.9/stable extra: juju-29 + snap_install_flags: "--classic" - juju_channel: 3.1/stable extra: juju-31 + snap_install_flags: "" - juju_channel: 3.2/stable extra: juju-32 + snap_install_flags: "" env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build @@ -83,7 +86,7 @@ jobs: set -euxo pipefail python -m pip install --upgrade pip pip install tox tox-gh-actions - sudo snap install --channel ${{ matrix.juju_channel }} juju + sudo snap install ${{ matrix.snap_install_flags }} --channel ${{ matrix.juju_channel }} juju sudo snap install --classic juju-crashdump sudo lxd init --auto # This is a throw-away CI environment, do not do this at home From 5e138cb7f161e91bac9dd2674c9e39da3213b3fb Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Tue, 18 Jul 2023 15:27:14 -0400 Subject: [PATCH 13/26] Depend on 'juju' when no extra is passed. By default depend on 'juju' (no pinning), the pinning only comes into place when an explicit extra is passed. This makes the extras effectively nothing more than a pinning alias, basically `pip install juju<3.0 zaza` becomes equivalent to `pip install zaza[juju-29]`. --- setup.py | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5565d73d3..1dd9e0847 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ extras_require={ 'testing': tests_require, + '': ['juju'], 'juju-29': ['juju<3.0'], 'juju-31': ['juju>=3.1.0,<3.2.0'], 'juju-32': ['juju>=3.2.0,<3.3.0'], diff --git a/tox.ini b/tox.ini index e943496cf..2cb5a78d6 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ passenv = CS_* OS_* TEST_* -extras = {env:EXTRA:juju-29} +extras = {env:EXTRA} deps = -r{toxinidir}/test-requirements.txt commands = pytest --cov=./zaza/ {posargs} {toxinidir}/unit_tests From 5acd3cf0a2a4cd3fcbd434052a4adb822f325689 Mon Sep 17 00:00:00 2001 From: Erhan Sunar Date: Thu, 24 Aug 2023 13:29:15 +0300 Subject: [PATCH 14/26] Fix Code becoming integer. Value of the Code key in the returned Dict is str in master branch. However while trying to get compatible with juju 3.x series it became int. Now it is not conforming to the function signature. Also there are usages in some tests so it reverted back to str. --- zaza/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zaza/model.py b/zaza/model.py index 2c7141d04..4c5b9c0c3 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -546,7 +546,7 @@ def _normalise_action_results(results): elif results.get(old_key) and not results.get(key): results[key] = results[old_key] if 'return-code' in results: - results['Code'] = results.get('return-code') + results['Code'] = str(results.get('return-code')) del results['return-code'] return results else: From 8b1d54c0bfc5aa1e6b5e2ef52331290a71b6d058 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Thu, 5 Oct 2023 09:59:35 -0300 Subject: [PATCH 15/26] Use _normalise_action_object() --- zaza/model.py | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/zaza/model.py b/zaza/model.py index 4c5b9c0c3..05acc22b2 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -571,11 +571,8 @@ async def async_run_on_unit(unit_name, command, model_name=None, timeout=None): unit = await async_get_unit_from_name(unit_name, model) action = await unit.run(command, timeout=timeout) await action.wait() - results = None - try: - results = action.results - except (AttributeError, KeyError): - results = action.data.get('results') + action = _normalise_action_object(action) + results = action.data.get('results') return _normalise_action_results(results) run_on_unit = sync_wrapper(async_run_on_unit) @@ -602,11 +599,8 @@ async def async_run_on_leader(application_name, command, model_name=None, if is_leader: action = await unit.run(command, timeout=timeout) await action.wait() - results = None - try: - results = action.results - except (AttributeError, KeyError): - results = action.data.get('results') + action = _normalise_action_object(action) + results = action.data.get('results') return _normalise_action_results(results) run_on_leader = sync_wrapper(async_run_on_leader) @@ -2264,14 +2258,10 @@ async def _check_for_file(model): try: output = await unit.run('test -e "{}"; echo $?'.format(path)) await output.wait() - output_result = {} - try: - output_result = output.results - except (AttributeError, KeyError): - output_result = output.data.get('results', {}) - contents = output_result.get( - 'Stdout', output_result.get('stdout', '') - ) + output = _normalise_action_object(output) + output_result = _normalise_action_results( + output.data.get('results', {})) + contents = output_result.get('Stdout', '') results.append("1" in contents) # libjuju throws a generic error for connection failure. So we # cannot differentiate between a connectivity issue and a From e4ccfc230967ad40634136fcbd8a8fdf78c7f203 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Thu, 5 Oct 2023 10:00:46 -0300 Subject: [PATCH 16/26] Unpin libjuju Pinning via constraints is left to downstream consumers. --- setup.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 1dd9e0847..7bdd8e157 100644 --- a/setup.py +++ b/setup.py @@ -43,18 +43,15 @@ # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/94 'macaroonbakery != 1.3.3', + 'juju', ] tests_require = [ 'tox >= 2.3.1', ] -extras_require={ +extras_require = { 'testing': tests_require, - '': ['juju'], - 'juju-29': ['juju<3.0'], - 'juju-31': ['juju>=3.1.0,<3.2.0'], - 'juju-32': ['juju>=3.2.0,<3.3.0'], } From 0226e2f9520e244121c9549656249ba3d189cb10 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Thu, 5 Oct 2023 10:04:16 -0300 Subject: [PATCH 17/26] Use action_obj.data['status'] --- zaza/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zaza/model.py b/zaza/model.py index 05acc22b2..04166394d 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -1130,7 +1130,7 @@ async def async_run_action(unit_name, action_name, model_name=None, action_obj = await unit.run_action(action_name, **action_params) await action_obj.wait() action_obj = _normalise_action_object(action_obj) - if raise_on_failure and action_obj.status != 'completed': + if raise_on_failure and action_obj.data['status'] != 'completed': try: output = await model.get_action_output(action_obj.id) except KeyError: @@ -1171,7 +1171,7 @@ async def async_run_action_on_leader(application_name, action_name, **action_params) await action_obj.wait() action_obj = _normalise_action_object(action_obj) - if raise_on_failure and action_obj.status != 'completed': + if raise_on_failure and action_obj.data['status'] != 'completed': try: output = await model.get_action_output(action_obj.id) except KeyError: From de73779be9e47a6a9682d895c4b11ef48758f091 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Wed, 17 Jan 2024 12:01:13 -0300 Subject: [PATCH 18/26] Move libjuju pinning to a constraints file By default libjuju 2.9 will be used, this can overriden by passing the PIP_CONSTRAINTS environment variable Examples: PIP_CONSTRAINTS=./constraints-juju31.txt tox -e pep8 --recreate This allows running functional tests with different versions of juju --- constraints-juju29.txt | 1 + constraints-juju31.txt | 1 + constraints-juju32.txt | 1 + constraints-juju33.txt | 1 + test-requirements.txt | 3 +++ tox.ini | 11 +++-------- 6 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 constraints-juju29.txt create mode 100644 constraints-juju31.txt create mode 100644 constraints-juju32.txt create mode 100644 constraints-juju33.txt diff --git a/constraints-juju29.txt b/constraints-juju29.txt new file mode 100644 index 000000000..0919801a8 --- /dev/null +++ b/constraints-juju29.txt @@ -0,0 +1 @@ +juju>=2.9.0,<3.0.0 diff --git a/constraints-juju31.txt b/constraints-juju31.txt new file mode 100644 index 000000000..f54dea1ec --- /dev/null +++ b/constraints-juju31.txt @@ -0,0 +1 @@ +juju>=3.1.0,<3.2.0 diff --git a/constraints-juju32.txt b/constraints-juju32.txt new file mode 100644 index 000000000..7c6a709f1 --- /dev/null +++ b/constraints-juju32.txt @@ -0,0 +1 @@ +juju>=3.2.0,<3.3.0 diff --git a/constraints-juju33.txt b/constraints-juju33.txt new file mode 100644 index 000000000..67d9ebfdf --- /dev/null +++ b/constraints-juju33.txt @@ -0,0 +1 @@ +juju>=3.3.0,<3.4.0 diff --git a/test-requirements.txt b/test-requirements.txt index ff5879e3c..3c66eb3da 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,5 +16,8 @@ keystoneauth1 oslo.config python-novaclient tenacity>8.2.0 +# To force the installation of an specific version of libjuju use a constraints +# file, e.g.: `env PIP_CONSTRAINTS=./constraints-juju31.txt tox -e func-target` +juju # https://github.com/go-macaroon-bakery/py-macaroon-bakery/issues/94 macaroonbakery!=1.3.3 diff --git a/tox.ini b/tox.ini index 2cb5a78d6..878f0de43 100644 --- a/tox.ini +++ b/tox.ini @@ -17,23 +17,21 @@ passenv = CS_* OS_* TEST_* -extras = {env:EXTRA} + PIP_* deps = - -r{toxinidir}/test-requirements.txt + -c{env:PIP_CONSTRAINTS:{toxinidir}/constraints-juju29.txt} + -r{toxinidir}/test-requirements.txt commands = pytest --cov=./zaza/ {posargs} {toxinidir}/unit_tests [testenv:py3] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt [testenv:pep8] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt commands = flake8 {posargs} zaza unit_tests [testenv:venv] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt commands = /bin/true [flake8] @@ -51,7 +49,6 @@ commands = sphinx-build -W -b html -d {toxinidir}/doc/build/doctrees . {toxinidi [testenv:func] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt commands = {envdir}/bin/python3 setup.py install functest-run-suite --keep-faulty-model @@ -65,14 +62,12 @@ commands = [testenv:func-target-extended] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt 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 -deps = -r{toxinidir}/test-requirements.txt commands = {envdir}/bin/python3 setup.py install remove-placement {posargs} From c42c965102d9ec2eb58e6fdcf68543773e851c2c Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Wed, 17 Jan 2024 12:22:50 -0300 Subject: [PATCH 19/26] Pass a constraint to pin libjuju 3.x in the functional test --- .github/workflows/tox.yaml | 17 +++++++++++++---- tox.ini | 4 ---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index ae100a03c..75b76fd33 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -62,20 +62,24 @@ jobs: - 2.9/stable - 3.1/stable - 3.2/stable + - 3.3/stable bundle: - first - second - third include: - juju_channel: 2.9/stable - extra: juju-29 snap_install_flags: "--classic" + pip_constraints: constraints-juju29.txt - juju_channel: 3.1/stable - extra: juju-31 snap_install_flags: "" + pip_constraints: constraints-juju31.txt - juju_channel: 3.2/stable - extra: juju-32 snap_install_flags: "" + pip_constraints: constraints-juju32.txt + - juju_channel: 3.3/stable + snap_install_flags: "" + pip_constraints: constraints-juju33.txt env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build @@ -84,6 +88,10 @@ jobs: - name: Install dependencies run: | set -euxo pipefail + sudo apt-get update + sudo apt-get install -yq snapd + sudo systemctl enable snapd + sudo systemctl restart snapd python -m pip install --upgrade pip pip install tox tox-gh-actions sudo snap install ${{ matrix.snap_install_flags }} --channel ${{ matrix.juju_channel }} juju @@ -105,7 +113,8 @@ jobs: run: | set -euxo pipefail mkdir logs - env EXTRA=${{ matrix.extra }} tox -e func-target -- ${{ matrix.bundle }} | tee logs/tox-output.txt + export PIP_CONSTRAINTS=$(pwd)/${{ matrix.pip_constraints }} + tox -e func-target -- ${{ matrix.bundle }} | tee logs/tox-output.txt - name: crashdump on failure if: failure() run: | diff --git a/tox.ini b/tox.ini index 878f0de43..323a9cd0f 100644 --- a/tox.ini +++ b/tox.ini @@ -36,15 +36,12 @@ commands = /bin/true [flake8] ignore = E402,E226,W504 -deps = -r{toxinidir}/test-requirements.txt per-file-ignores = unit_tests/**: D [testenv:docs] basepython = python3 changedir = doc/source -deps = - -r{toxinidir}/doc-requirements.txt commands = sphinx-build -W -b html -d {toxinidir}/doc/build/doctrees . {toxinidir}/doc/build/html [testenv:func] @@ -55,7 +52,6 @@ commands = [testenv:func-target] basepython = python3 -deps = -r{toxinidir}/test-requirements.txt commands = {envdir}/bin/python3 setup.py install functest-run-suite --keep-model --bundle {posargs} From 28c13b780238c8019e606c5456a8ab5496e53a70 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Wed, 17 Jan 2024 15:58:19 -0300 Subject: [PATCH 20/26] Deny use of python-libjuju-2.9.45 --- constraints-juju29.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/constraints-juju29.txt b/constraints-juju29.txt index 0919801a8..a8c2cc509 100644 --- a/constraints-juju29.txt +++ b/constraints-juju29.txt @@ -1 +1,3 @@ -juju>=2.9.0,<3.0.0 +# 2.9.45.0 - https://github.com/juju/python-libjuju/pull/975 +# 2.9.46.0 +juju>=2.9.0,<3.0.0,!=2.9.45,!=2.9.46 From d374a08969d8486b2ab909aa6bdf036361babce3 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Thu, 18 Jan 2024 15:30:06 -0300 Subject: [PATCH 21/26] Disabled juju 3.2 and 3.3 from the CI --- .github/workflows/tox.yaml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 75b76fd33..5dd8d8c25 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -61,8 +61,8 @@ jobs: juju_channel: - 2.9/stable - 3.1/stable - - 3.2/stable - - 3.3/stable + # - 3.2/stable + # - 3.3/stable bundle: - first - second @@ -74,12 +74,16 @@ jobs: - juju_channel: 3.1/stable snap_install_flags: "" pip_constraints: constraints-juju31.txt - - juju_channel: 3.2/stable - snap_install_flags: "" - pip_constraints: constraints-juju32.txt - - juju_channel: 3.3/stable - snap_install_flags: "" - pip_constraints: constraints-juju33.txt + # NOTE(freyes): disabled until "RuntimeError: set_wakeup_fd only works + # in main thread of the main interpreter" gets fixed. + # https://pastebin.ubuntu.com/p/GfYKgpx3SP/ + # + # - juju_channel: 3.2/stable + # snap_install_flags: "" + # pip_constraints: constraints-juju32.txt + # - juju_channel: 3.3/stable + # snap_install_flags: "" + # pip_constraints: constraints-juju33.txt env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build From 69ab4faaa0471a002c156a22841a18295054eeb6 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Thu, 18 Jan 2024 15:30:35 -0300 Subject: [PATCH 22/26] Added note to leave a breadcrumb of consumers of the constraints file(s) --- constraints-juju29.txt | 12 ++++++++++-- constraints-juju31.txt | 8 ++++++++ constraints-juju32.txt | 8 ++++++++ constraints-juju33.txt | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/constraints-juju29.txt b/constraints-juju29.txt index a8c2cc509..ba318b495 100644 --- a/constraints-juju29.txt +++ b/constraints-juju29.txt @@ -1,3 +1,11 @@ -# 2.9.45.0 - https://github.com/juju/python-libjuju/pull/975 -# 2.9.46.0 +# NOTE: this constraints file can be (and will be) consumed by downstream users. +# +# Known consumers: +# * zosci-config: job definitions that declare what juju version (snap channel) +# is used in tandem with this constraints file to lockdown python-libjuju +# version. +# * zaza-openstack-tests +# +# Reasons to block the use of a release: +# * 2.9.45, 2.9.46 - https://github.com/juju/python-libjuju/pull/975 juju>=2.9.0,<3.0.0,!=2.9.45,!=2.9.46 diff --git a/constraints-juju31.txt b/constraints-juju31.txt index f54dea1ec..f85e9e1b3 100644 --- a/constraints-juju31.txt +++ b/constraints-juju31.txt @@ -1 +1,9 @@ +# NOTE: this constraints file can be (and will be) consumed by downstream users. +# +# Known consumers: +# * zosci-config: job definitions that declare what juju version (snap channel) +# is used in tandem with this constraints file to lockdown python-libjuju +# version. +# * zaza-openstack-tests +# juju>=3.1.0,<3.2.0 diff --git a/constraints-juju32.txt b/constraints-juju32.txt index 7c6a709f1..76eaa9034 100644 --- a/constraints-juju32.txt +++ b/constraints-juju32.txt @@ -1 +1,9 @@ +# NOTE: this constraints file can be (and will be) consumed by downstream users. +# +# Known consumers: +# * zosci-config: job definitions that declare what juju version (snap channel) +# is used in tandem with this constraints file to lockdown python-libjuju +# version. +# * zaza-openstack-tests +# juju>=3.2.0,<3.3.0 diff --git a/constraints-juju33.txt b/constraints-juju33.txt index 67d9ebfdf..f5263fa0f 100644 --- a/constraints-juju33.txt +++ b/constraints-juju33.txt @@ -1 +1,9 @@ +# NOTE: this constraints file can be (and will be) consumed by downstream users. +# +# Known consumers: +# * zosci-config: job definitions that declare what juju version (snap channel) +# is used in tandem with this constraints file to lockdown python-libjuju +# version. +# * zaza-openstack-tests +# juju>=3.3.0,<3.4.0 From 227f3d58bcd137304319d56de96fd5bff4932aeb Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 19 Jan 2024 11:04:17 -0300 Subject: [PATCH 23/26] Drop py36 testing from the CI The testing runners use Python 3.8 (Focal), hence no need to keep py36 alive. --- .github/workflows/tox.yaml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 5dd8d8c25..c8cccf003 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -5,31 +5,6 @@ on: - pull_request jobs: - build_old_versions: - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: ['3.6'] - - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install codecov tox tox-gh-actions - - name: Lint with tox - run: tox -e pep8 - - name: Test with tox - run: tox -e py - - name: Codecov - run: | - set -euxo pipefail - codecov --verbose --gcov-glob unit_tests/* - build: runs-on: ubuntu-latest strategy: From dfbdbec44f6dd383393dcd3a07b563fdba48503b Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Thu, 7 Dec 2023 15:41:06 +0100 Subject: [PATCH 24/26] utilities: Convert juju base into Ubuntu series Juju 3.x replaced the `series` status key with a `base` key that consists of Distribution type and version number. To avoid maintenance burden we add a Launchpad module that implements functions to look up available Ubuntu series data. Update the `get_machine_series` helper function to determine Ubuntu series from `base` when no `series` key is available. Signed-off-by: Frode Nordahl --- .../utilities/test_zaza_utilities_juju.py | 74 ++++++++++++++++++- .../test_zaza_utilities_launchpad.py | 55 ++++++++++++++ zaza/utilities/juju.py | 24 +++++- zaza/utilities/launchpad.py | 52 +++++++++++++ 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 unit_tests/utilities/test_zaza_utilities_launchpad.py create mode 100644 zaza/utilities/launchpad.py diff --git a/unit_tests/utilities/test_zaza_utilities_juju.py b/unit_tests/utilities/test_zaza_utilities_juju.py index 05ea9cf5b..0ef6b863e 100644 --- a/unit_tests/utilities/test_zaza_utilities_juju.py +++ b/unit_tests/utilities/test_zaza_utilities_juju.py @@ -370,16 +370,86 @@ def test_get_machine_series(self): new_callable=mock.MagicMock(), name='_get_machine_status' ) - self._get_machine_status.return_value = 'xenial' + self._get_machine_status.return_value = {'series': 'xenial'} expected = 'xenial' actual = juju_utils.get_machine_series('6') self._get_machine_status.assert_called_with( machine='6', - key='series', model_name=None ) self.assertEqual(expected, actual) + def test_get_machine_series_juju3x_exceptions(self): + self.patch( + 'zaza.utilities.juju.get_machine_status', + new_callable=mock.MagicMock(), + name='_get_machine_status' + ) + self.patch( + 'zaza.utilities.juju.launchpad.get_ubuntu_series_by_version', + new_callable=mock.MagicMock(), + name='_get_ubuntu_series_by_version' + ) + self._get_ubuntu_series_by_version.return_value = { + '22.04': {'name': 'jammy'}} + + status = mock.MagicMock() + status.__getitem__.side_effect = KeyError + + base = mock.MagicMock() + base.name = 'ubuntu' + base.channel = '22.04/stable' + status.get.return_value = base + self._get_machine_status.return_value = status + + try: + juju_utils.get_machine_series('6') + except KeyError: + self.fail('Did not expect `get_machine_series` ' + 'to raise a KeyError') + self._get_machine_status.reset_mock() + + self._get_machine_status.return_value = {} + self.assertRaises( + ValueError, + juju_utils.get_machine_series, + '6') + + base = mock.MagicMock() + base.name = 'someOtherDistro' + base.channel = '22.04/stable' + self._get_machine_status.return_value = {'base': base} + self.assertRaises( + NotImplementedError, + juju_utils.get_machine_series, + '6') + + def test_get_machine_series_juju3x(self): + self.patch( + 'zaza.utilities.juju.get_machine_status', + new_callable=mock.MagicMock(), + name='_get_machine_status' + ) + self.patch( + 'zaza.utilities.juju.launchpad.get_ubuntu_series_by_version', + new_callable=mock.MagicMock(), + name='_get_ubuntu_series_by_version' + ) + base = mock.MagicMock() + base.name = 'ubuntu' + base.channel = '22.04/stable' + self._get_machine_status.return_value = {'base': base} + self._get_ubuntu_series_by_version.return_value = { + '22.04': {'name': 'jammy'}} + expected = 'jammy' + actual = juju_utils.get_machine_series('6') + self._get_machine_status.assert_called_with( + machine='6', + model_name=None + ) + self._get_ubuntu_series_by_version.assert_called_once_with() + self.assertEqual(expected, actual) + def test_get_subordinate_units(self): juju_status = mock.MagicMock() juju_status.applications = { diff --git a/unit_tests/utilities/test_zaza_utilities_launchpad.py b/unit_tests/utilities/test_zaza_utilities_launchpad.py new file mode 100644 index 000000000..b25e130e9 --- /dev/null +++ b/unit_tests/utilities/test_zaza_utilities_launchpad.py @@ -0,0 +1,55 @@ +# Copyright 2023 Canonical Ltd. +# +# 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. + +import json +import unittest + +import unit_tests.utils as ut_utils +import zaza.utilities.launchpad as launchpad + + +class TestUtilitiesLaunchpad(ut_utils.BaseTestCase): + + def test_get_ubuntu_series(self): + self.patch_object(launchpad.requests, 'get') + expect = {'entries': {}} + r = unittest.mock.MagicMock() + r.text = json.dumps(expect) + self.get.return_value = r + self.assertEquals( + launchpad.get_ubuntu_series(), + expect, + ) + self.get.assert_called_once_with( + 'https://api.launchpad.net/devel/ubuntu/series') + + def test_get_ubuntu_series_by_version(self): + self.patch_object(launchpad, 'get_ubuntu_series') + + self.get_ubuntu_series.return_value = { + 'entries': [{'version': 'fakeVersion'}]} + + self.assertEquals( + launchpad.get_ubuntu_series_by_version(), + {'fakeVersion': {'version': 'fakeVersion'}}) + + def test_get_ubuntu_series_by_name(self): + self.patch_object(launchpad, 'get_ubuntu_series') + + self.get_ubuntu_series.return_value = { + 'entries': [{'name': 'fakeName'}]} + + self.assertEquals( + launchpad.get_ubuntu_series_by_name(), + {'fakeName': {'name': 'fakeName'}}) diff --git a/zaza/utilities/juju.py b/zaza/utilities/juju.py index fea84c980..208130c02 100644 --- a/zaza/utilities/juju.py +++ b/zaza/utilities/juju.py @@ -24,8 +24,9 @@ model, controller, ) -from zaza.utilities import generic as generic_utils from zaza.utilities import exceptions as zaza_exceptions +from zaza.utilities import generic as generic_utils +from zaza.utilities import launchpad KUBERNETES_PROVIDER_NAME = 'kubernetes' @@ -279,11 +280,28 @@ def get_machine_series(machine, model_name=None): :returns: Juju series :rtype: string """ - return get_machine_status( + status = get_machine_status( machine=machine, - key='series', model_name=model_name ) + try: + if 'series' in status: + return status.get('series') + except KeyError: + # libjuju will raise make the above check return KeyError when not + # present... + pass + + base = status.get('base') + if not base: + raise ValueError("Unable to determine distro from status: '{}'" + .format(status)) + if base.name != 'ubuntu': + raise NotImplementedError("Series resolution not implemented for " + "distro: '{}'".format(base.name)) + + version, risk = base.channel.split('/') + return launchpad.get_ubuntu_series_by_version()[version]['name'] def get_machine_uuids_for_application(application, model_name=None): diff --git a/zaza/utilities/launchpad.py b/zaza/utilities/launchpad.py new file mode 100644 index 000000000..4ba7ee429 --- /dev/null +++ b/zaza/utilities/launchpad.py @@ -0,0 +1,52 @@ +# Copyright 2023 Canonical Ltd. +# +# 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. +"""Module for interacting with Launchpad API.""" + +import json +import requests +import typing + + +def get_ubuntu_series( +) -> typing.Dict[str, typing.List[typing.Dict[str, any]]]: + """Contact Launchpad API and retrieve a list of all Ubuntu releases. + + Launchpad documentation for the returned data structure can be found here: + https://launchpad.net/+apidoc/devel.html#distribution + https://launchpad.net/+apidoc/devel.html#distro_series + """ + r = requests.get('https://api.launchpad.net/devel/ubuntu/series') + return json.loads(r.text) + + +def get_ubuntu_series_by_version() -> typing.Dict[str, typing.Dict[str, any]]: + """Get a Dict of distro series information indexed by version number. + + Please refer to the `get_ubuntu_series()` function docstring for docs. + """ + return { + entry['version']: entry + for entry in get_ubuntu_series().get('entries', {}) + } + + +def get_ubuntu_series_by_name() -> typing.Dict[str, typing.Dict[str, any]]: + """Get a Dict of distro series information indexed by version name. + + Please refer to the `get_ubuntu_series()` function docstring for docs. + """ + return { + entry['name']: entry + for entry in get_ubuntu_series().get('entries', {}) + } From 18c179efc0fd4775df58e823df9f943a63b24ba7 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Wed, 14 Feb 2024 17:52:27 -0300 Subject: [PATCH 25/26] Add constraints-juju-default.txt This file aims to hold the default version of juju that it's expected charms to be tested with. It helps to serve as a sane default for tox.ini --- constraints-juju-default.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 constraints-juju-default.txt diff --git a/constraints-juju-default.txt b/constraints-juju-default.txt new file mode 100644 index 000000000..f85e9e1b3 --- /dev/null +++ b/constraints-juju-default.txt @@ -0,0 +1,9 @@ +# NOTE: this constraints file can be (and will be) consumed by downstream users. +# +# Known consumers: +# * zosci-config: job definitions that declare what juju version (snap channel) +# is used in tandem with this constraints file to lockdown python-libjuju +# version. +# * zaza-openstack-tests +# +juju>=3.1.0,<3.2.0 From aec6e76caaf39f4e170978c422f3f09cbcf4eb87 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Tue, 20 Feb 2024 10:48:30 -0300 Subject: [PATCH 26/26] Run `juju-crashdump` instead of `juju crashdump` The juju snap can't run programs that are outside the snap due to the confinement restrictions --- .github/workflows/tox.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index c8cccf003..023c2f3b7 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -101,7 +101,7 @@ jobs: juju models model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/) juju status -m $model | tee logs/juju-status.txt - juju crashdump -m $model -o logs/ + juju-crashdump -m $model -o logs/ - name: upload logs on failure if: failure() uses: actions/upload-artifact@v2