From d6c0d85cd0e120b5d13433c8a94e2a7d9bc69f68 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 23 Feb 2023 10:01:25 +0000 Subject: [PATCH 1/9] 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 | 39 +++++++++++++------ unit_tests/test_zaza_model.py | 8 +++- .../utilities/test_zaza_utilities_generic.py | 2 +- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 9379c9f94..fdd0ee956 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -5,11 +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.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v1 @@ -35,21 +60,11 @@ jobs: fail-fast: false matrix: juju_channel: - - latest/stable - 2.9/stable - - 2.8/stable bundle: - first - second - third - exclude: - # disable 'first' and 'second' bundles for juju 2.8 since 'magpie' - # is not a promulgated charm in the charmstore, only on charmhub - # which 2.8 can't talk to. - - juju_channel: 2.8/stable - bundle: first - - juju_channel: 2.8/stable - bundle: second env: TEST_ZAZA_BUG_LP1987332: "on" # http://pad.lv/1987332 needs: build @@ -67,6 +82,8 @@ jobs: sudo chmod 666 /var/snap/lxd/common/lxd/unix.socket # until Juju provides stable IPv6-support we unfortunately need this lxc network set lxdbr0 ipv6.address none + sudo iptables -F FORWARD + sudo iptables -P FORWARD ACCEPT # pull images lxc image copy --alias juju/bionic/amd64 --copy-aliases ubuntu-daily:bionic local: lxc image copy --alias juju/focal/amd64 --copy-aliases ubuntu-daily:focal local: diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index 0c8cf962e..23af70b77 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -493,7 +493,9 @@ def test_block_until_auto_reconnect_model_disconnected_sync(self): with mock.patch.object(zaza, 'RUN_LIBJUJU_IN_THREAD', new=False): model.sync_wrapper(self._wrapper)() self.Model_mock.disconnect.assert_has_calls([mock.call()]) - self.Model_mock.connect_model.has_calls([mock.call('modelname')]) + self.Model_mock.connect_model.assert_has_calls( + [mock.call('testmodel')] + ) def test_block_until_auto_reconnect_model_disconnected_async(self): self._mocks_for_block_until_auto_reconnect_model( @@ -506,7 +508,9 @@ async def _async_true(): with mock.patch.object(zaza, 'RUN_LIBJUJU_IN_THREAD', new=False): model.sync_wrapper(self._wrapper)() self.Model_mock.disconnect.assert_has_calls([mock.call()]) - self.Model_mock.connect_model.has_calls([mock.call('modelname')]) + self.Model_mock.connect_model.assert_has_calls( + [mock.call('testmodel')] + ) def test_block_until_auto_reconnect_model_blocks_till_true(self): self._mocks_for_block_until_auto_reconnect_model(True, True) diff --git a/unit_tests/utilities/test_zaza_utilities_generic.py b/unit_tests/utilities/test_zaza_utilities_generic.py index 5d3fb7759..038d53e0b 100644 --- a/unit_tests/utilities/test_zaza_utilities_generic.py +++ b/unit_tests/utilities/test_zaza_utilities_generic.py @@ -254,7 +254,7 @@ def test_series_upgrade(self): _unit, _machine_num, origin=_origin, to_series=_to_series, from_series=_from_series, workaround_script=_workaround_script, files=_files) - self.block_until_all_units_idle.called_with() + self.block_until_all_units_idle.assert_called_with() self.prepare_series_upgrade.assert_called_once_with( _machine_num, to_series=_to_series) self.wrap_do_release_upgrade.assert_called_once_with( From 76c6eeed651a405f866ccc2056d6970d4781e70e Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Tue, 31 Jan 2023 13:27:47 +0530 Subject: [PATCH 2/9] 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 faffe82cc..c4c46c09a 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 87445e411..00e19cdfb 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', diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index 23af70b77..cc1ae1cac 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -142,6 +142,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', @@ -155,6 +156,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') @@ -779,7 +783,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', @@ -801,6 +845,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', @@ -833,6 +896,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') @@ -1009,7 +1088,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', @@ -1027,7 +1106,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'], @@ -1045,7 +1124,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'], @@ -1502,6 +1581,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'} } @@ -1518,6 +1616,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': ''} } @@ -1532,6 +1647,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'} } @@ -1556,6 +1698,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': ''} } @@ -1601,6 +1766,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', @@ -1621,6 +1799,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'} } @@ -1902,6 +2101,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 e5539e15e..0d33b7931 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: @@ -2049,7 +2090,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 @@ -2184,7 +2231,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 c052eac9f8e97033e448256c88f2906c26a116e8 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 11:37:48 +0000 Subject: [PATCH 3/9] 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 fdd0ee956..78a0dd3b3 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -60,7 +60,7 @@ jobs: fail-fast: false matrix: juju_channel: - - 2.9/stable + - 3.1/stable bundle: - first - second @@ -89,7 +89,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 243807716de9fce176edae5ec3f04cfd0db4a98e Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 11:56:08 +0000 Subject: [PATCH 4/9] Pin Juju < 3.2 for testing --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a4a067b23..e93a02978 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -17,5 +17,5 @@ keystoneauth1 oslo.config python-novaclient tenacity -# pinned until 3.0 regressions are handled: https://github.com/openstack-charmers/zaza/issues/545 -juju<3.0 +# Fix upper version to ensure compatibility with Juju 3.1 +juju<3.2 From 3ea1de684e03d1ad99b104569f03812a3fc5ad1f Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 12:04:15 +0000 Subject: [PATCH 5/9] Drop testing with Pythons < 3.8 --- .github/workflows/tox.yaml | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 78a0dd3b3..0b7346bfa 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -5,36 +5,11 @@ 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.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v1 From af53cf6fb983af9d9bd3b384064967b72bcc9483 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 12:24:47 +0000 Subject: [PATCH 6/9] 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 0b7346bfa..efb9e5698 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -50,7 +50,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 543051c33..f08dc63cf 100644 --- a/zaza/utilities/deployment_env.py +++ b/zaza/utilities/deployment_env.py @@ -46,7 +46,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 92dffe8442f5dd997196041abbde1a55d3643bf2 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 27 Feb 2023 12:40:27 +0000 Subject: [PATCH 7/9] 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 9747fc257b19e96f6c3f05ea5086be7c1542a2bc Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 6 Mar 2023 14:57:32 +0000 Subject: [PATCH 8/9] Add helper for scaling k8s charms Add a wrapper to the scale command so k8s applications can be scaled in and out. (cherry picked from commit 05cd81d1e0915bf82a32fe678c24329c6a72a5e9) --- unit_tests/test_zaza_model.py | 34 ++++++++++++++++++++++++++++++++++ zaza/model.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/unit_tests/test_zaza_model.py b/unit_tests/test_zaza_model.py index cc1ae1cac..8fa8f3316 100644 --- a/unit_tests/test_zaza_model.py +++ b/unit_tests/test_zaza_model.py @@ -134,6 +134,9 @@ async def _add_unit(count=1, to=None): async def _destroy_unit(*unitnames): return + async def _scale(scale=None, scale_change=None): + return + def _is_leader(leader): async def _inner_is_leader(): return leader @@ -213,6 +216,7 @@ def fail_on_use(): _units.destroy_relation.side_effect = _destroy_relation _units.add_unit.side_effect = _add_unit _units.destroy_unit.side_effect = _destroy_unit + _units.scale.side_effect = _scale self.mymodel = mock.MagicMock() self.mymodel.applications = { @@ -974,6 +978,36 @@ def test_destroy_unit_wait(self): self.async_block_until_unit_count.assert_called_once_with( 'app', 1, model_name=None) + def test_scale_out(self): + self.patch_object(model, 'get_juju_model', return_value='mname') + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + model.scale('app', scale=3) + self.mymodel.applications['app'].scale.assert_called_once_with( + scale=3, scale_change=None) + + def test_scale_wait(self): + self.patch_object(model, 'async_block_until_unit_count') + self.patch_object(model, 'get_juju_model', return_value='mname') + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + model.scale('app', scale=3, wait=True) + self.mymodel.applications['app'].scale.assert_called_once_with( + scale=3, scale_change=None) + self.async_block_until_unit_count.assert_called_once_with( + 'app', 3, model_name=None) + + def test_scale_back_wait(self): + self.patch_object(model, 'async_block_until_unit_count') + self.patch_object(model, 'get_juju_model', return_value='mname') + self.patch_object(model, 'Model') + self.Model.return_value = self.Model_mock + model.scale('app', scale_change=-1, wait=True) + self.mymodel.applications['app'].scale.assert_called_once_with( + scale=None, scale_change=-1) + self.async_block_until_unit_count.assert_called_once_with( + 'app', 1, model_name=None) + def test_get_relation_id_interface(self): self.patch_object(model, 'get_juju_model', return_value='mname') self.patch_object(model, 'Model') diff --git a/zaza/model.py b/zaza/model.py index 0d33b7931..01048b3d6 100644 --- a/zaza/model.py +++ b/zaza/model.py @@ -2780,6 +2780,40 @@ async def async_destroy_unit(application_name, *unit_names, model_name=None, destroy_unit = sync_wrapper(async_destroy_unit) +async def async_scale(application_name, scale=None, scale_change=None, + wait=False, model_name=None): + """ + Set or adjust the scale of this (K8s) application. + + :param application_name: Name of application to add unit(s) to + :type application_name: str + :param scale: Scale to which to set this application. + :type scale: int + :param scale_change: Amount by which to adjust the scale of this + application (can be positive or negative). + :type scale_change: int + :param wait: Whether to wait for the unit change to appear in juju + status + :type wait: bool + :param model_name: Name of model to operate on. + :type model_name: str + """ + model = await get_model(model_name) + app = model.applications[application_name] + await app.scale(scale=scale, scale_change=scale_change) + if wait: + if scale: + target_count = scale + else: + target_count = len(app.units) + scale_change + await async_block_until_unit_count( + application_name, + target_count, + model_name=model_name) + +scale = sync_wrapper(async_scale) + + def set_model_constraints(constraints, model_name=None): """ Set constraints on a model. From 53971e825f9affcc9ae35bf0fed262696a58951e Mon Sep 17 00:00:00 2001 From: Erhan Sunar Date: Thu, 24 Aug 2023 13:29:15 +0300 Subject: [PATCH 9/9] 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 01048b3d6..002edbc1a 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: