diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 472b35b..cc5fa68 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -345,6 +345,20 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): openstack_utils.get_images_by_name(glance_client, 'frank'), []) + def test_get_volumes_by_name(self): + volume_mock1 = mock.MagicMock() + volume_mock1.name = 'bob' + volume_mock2 = mock.MagicMock() + volume_mock2.name = 'bill' + cinder_client = mock.MagicMock() + cinder_client.volumes.list.return_value = [volume_mock1, volume_mock2] + self.assertEqual( + openstack_utils.get_volumes_by_name(cinder_client, 'bob'), + [volume_mock1]) + self.assertEqual( + openstack_utils.get_volumes_by_name(cinder_client, 'frank'), + []) + def test_find_cirros_image(self): urllib_opener_mock = mock.MagicMock() self.patch_object(openstack_utils, "get_urllib_opener") @@ -942,6 +956,34 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.assertEqual(expected, result) self._get_os_rel_pair.assert_called_once_with(application='myapp') + def test_get_keystone_ip__vip(self): + self.patch_object(openstack_utils, "get_application_config_option") + self.patch_object(openstack_utils.model, "get_units") + unit1 = mock.Mock(public_address='5.6.7.8') + self.get_application_config_option.return_value = "1.2.3.4" + self.get_units.return_value = [unit1] + + self.assertEqual( + openstack_utils.get_keystone_ip(model_name='some-model'), + '1.2.3.4') + self.get_application_config_option.assert_called_once_with( + 'keystone', 'vip', model_name='some-model') + self.get_application_config_option.return_value = " 1.2.3.4 11" + self.assertEqual(openstack_utils.get_keystone_ip(), '1.2.3.4') + + def test_get_keystone_ip__from_unit(self): + self.patch_object(openstack_utils, "get_application_config_option") + self.patch_object(openstack_utils.model, "get_units") + self.patch_object(openstack_utils.model, 'get_unit_public_address') + mock_unit1 = mock.Mock() + self.get_unit_public_address.return_value = '5.6.7.8' + self.get_application_config_option.return_value = None + self.get_units.return_value = [mock_unit1] + + self.assertEqual(openstack_utils.get_keystone_ip(), '5.6.7.8') + self.get_units.assert_called_once_with('keystone', model_name=None) + self.get_unit_public_address.assert_called_once_with(mock_unit1) + def test_get_keystone_api_version(self): self.patch_object(openstack_utils, "get_current_os_versions") self.patch_object(openstack_utils, "get_application_config_option") diff --git a/unit_tests/utilities/test_zaza_utilities_swift.py b/unit_tests/utilities/test_zaza_utilities_swift.py index c5fc3da..f0e330b 100644 --- a/unit_tests/utilities/test_zaza_utilities_swift.py +++ b/unit_tests/utilities/test_zaza_utilities_swift.py @@ -91,12 +91,20 @@ class TestSwiftUtils(ut_utils.BaseTestCase): self.patch_object(juju_utils, 'get_full_juju_status') self.patch_object(zaza.model, 'get_application_config') self.patch_object(zaza.model, 'get_units') + self.patch_object(zaza.model, 'get_unit_public_address') + + def _get_unit_public_address(u): + return u.public_address + + self.get_unit_public_address.side_effect = _get_unit_public_address + juju_status = mock.MagicMock() juju_status.applications = {} self.get_full_juju_status.return_value = juju_status for app_name, units in app_units.items(): - expected_topology[units[0].public_address]['unit'] = units[0] + ip = units[0].public_address + expected_topology[ip]['unit'] = units[0] app_config = {} for app_name in app_units.keys(): diff --git a/unit_tests/utilities/test_zaza_utilities_upgrade_utils.py b/unit_tests/utilities/test_zaza_utilities_upgrade_utils.py index f42ba5a..986ba38 100644 --- a/unit_tests/utilities/test_zaza_utilities_upgrade_utils.py +++ b/unit_tests/utilities/test_zaza_utilities_upgrade_utils.py @@ -116,6 +116,21 @@ class TestUpgradeUtils(ut_utils.BaseTestCase): self.assertEqual( actual, expected) + # test that, at focal, there are no database services. + expected = [ + ('Database Services', []), + ('Stateful Services', []), + ('Core Identity', []), + ('Control Plane', ['cinder']), + ('Data Plane', ['nova-compute']), + ('sweep_up', ['ntp'])] + actual = openstack_upgrade.get_series_upgrade_groups( + target_series='focal') + pprint.pprint(expected) + pprint.pprint(actual) + self.assertEqual( + actual, + expected) def test_extract_charm_name_from_url(self): self.assertEqual( diff --git a/zaza/openstack/charm_tests/ceilometer/tests.py b/zaza/openstack/charm_tests/ceilometer/tests.py index c460cdb..dde2eb8 100644 --- a/zaza/openstack/charm_tests/ceilometer/tests.py +++ b/zaza/openstack/charm_tests/ceilometer/tests.py @@ -160,5 +160,10 @@ class CeilometerTest(test_utils.OpenStackBaseTest): Pause service and check services are stopped then resume and check they are started. """ + if self.application_name == 'ceilometer-agent': + logging.info("ceilometer-agent doesn't have pause/resume actions " + "anymore, skipping") + return + with self.pause_resume(self.restartable_services): logging.info("Testing pause and resume") diff --git a/zaza/openstack/charm_tests/ceph/dashboard/setup.py b/zaza/openstack/charm_tests/ceph/dashboard/setup.py index 7a2488f..5c9e5d6 100644 --- a/zaza/openstack/charm_tests/ceph/dashboard/setup.py +++ b/zaza/openstack/charm_tests/ceph/dashboard/setup.py @@ -48,4 +48,5 @@ def set_grafana_url(model_name=None): 'ceph-dashboard', { 'grafana-api-url': "https://{}:3000".format( - unit.public_address)}) + zaza.model.get_unit_public_address(unit)) + }) diff --git a/zaza/openstack/charm_tests/ceph/dashboard/tests.py b/zaza/openstack/charm_tests/ceph/dashboard/tests.py index 50cdbaf..5b88c42 100644 --- a/zaza/openstack/charm_tests/ceph/dashboard/tests.py +++ b/zaza/openstack/charm_tests/ceph/dashboard/tests.py @@ -103,11 +103,13 @@ class CephDashboardTest(test_utils.BaseCharmTest): units = zaza.model.get_units(self.application_name) for unit in units: r = self._run_request_get( - 'https://{}:8443'.format(unit.public_address), + 'https://{}:8443'.format( + zaza.model.get_unit_public_address(unit)), verify=self.local_ca_cert, allow_redirects=False) if r.status_code == requests.codes.ok: - return 'https://{}:8443'.format(unit.public_address) + return 'https://{}:8443'.format( + zaza.model.get_unit_public_address(unit)) def test_dashboard_units(self): """Check dashboard units are configured correctly.""" @@ -116,10 +118,11 @@ class CephDashboardTest(test_utils.BaseCharmTest): rcs = collections.defaultdict(list) for unit in units: r = self._run_request_get( - 'https://{}:8443'.format(unit.public_address), + 'https://{}:8443'.format( + zaza.model.get_unit_public_address(unit)), verify=verify, allow_redirects=False) - rcs[r.status_code].append(unit.public_address) + rcs[r.status_code].append(zaza.model.get_unit_public_address(unit)) self.assertEqual(len(rcs[requests.codes.ok]), 1) self.assertEqual(len(rcs[requests.codes.see_other]), len(units) - 1) diff --git a/zaza/openstack/charm_tests/ceph/iscsi/tests.py b/zaza/openstack/charm_tests/ceph/iscsi/tests.py index 0766e4a..c64b081 100644 --- a/zaza/openstack/charm_tests/ceph/iscsi/tests.py +++ b/zaza/openstack/charm_tests/ceph/iscsi/tests.py @@ -79,7 +79,7 @@ class CephISCSIGatewayTest(test_utils.BaseCharmTest): ctxt['gateway_units'] = [ { 'entity_id': u.entity_id, - 'ip': u.public_address, + 'ip': zaza.model.get_unit_public_address(u), 'hostname': host_names[u.entity_id]} for u in zaza.model.get_units('ceph-iscsi')] ctxt['gw_ip'] = sorted([g['ip'] for g in ctxt['gateway_units']])[0] diff --git a/zaza/openstack/charm_tests/ceph/tests.py b/zaza/openstack/charm_tests/ceph/tests.py index 6ee2113..543fc22 100644 --- a/zaza/openstack/charm_tests/ceph/tests.py +++ b/zaza/openstack/charm_tests/ceph/tests.py @@ -35,7 +35,6 @@ import zaza.openstack.utilities.exceptions as zaza_exceptions import zaza.openstack.utilities.generic as zaza_utils import zaza.utilities.juju as juju_utils import zaza.openstack.utilities.openstack as zaza_openstack -import zaza.openstack.utilities.juju as zaza_juju class CephLowLevelTest(test_utils.OpenStackBaseTest): @@ -124,7 +123,7 @@ class CephRelationTest(test_utils.OpenStackBaseTest): remote_unit_name = 'ceph-mon/0' relation_name = 'osd' remote_unit = zaza_model.get_unit_from_name(remote_unit_name) - remote_ip = remote_unit.public_address + remote_ip = zaza_model.get_unit_public_address(remote_unit) relation = juju_utils.get_relation_from_unit( unit_name, remote_unit_name, @@ -145,7 +144,7 @@ class CephRelationTest(test_utils.OpenStackBaseTest): unit_name = 'ceph-osd/0' relation_name = 'osd' remote_unit = zaza_model.get_unit_from_name(remote_unit_name) - remote_ip = remote_unit.public_address + remote_ip = zaza_model.get_unit_public_address(remote_unit) cmd = 'leader-get fsid' result = zaza_model.run_on_unit(remote_unit_name, cmd) fsid = result.get('Stdout').strip() @@ -546,20 +545,28 @@ class CephTest(test_utils.OpenStackBaseTest): ) logging.debug('OK') - def _get_local_osd_id(self, unit): + def get_local_osd_id(self, unit): + """Get the OSD id for a unit.""" ret = zaza_model.run_on_unit(unit, 'ceph-volume lvm list --format=json') local = list(json.loads(ret['Stdout']))[-1] return local if local.startswith('osd.') else 'osd.' + local + def get_num_osds(self, osd): + """Compute the number of active OSD's.""" + result = zaza_model.run_on_unit(osd, 'ceph osd stat --format=json') + result = json.loads(result['Stdout']) + return int(result['num_osds']) + def test_cache_device(self): """Test replacing a disk in use.""" logging.info('Running add-disk action with a caching device') + mon = next(iter(zaza_model.get_units('ceph-mon'))).entity_id osds = [x.entity_id for x in zaza_model.get_units('ceph-osd')] params = [] for unit in osds: - zaza_juju.add_storage(unit, 'cache-devices', 'cinder', 10) - loop_dev = zaza_juju.add_loop_device(unit, 10).get('Stdout') + zaza_model.add_storage(unit, 'cache-devices', 'cinder', 10) + loop_dev = zaza_utils.add_loop_device(unit, 10) params.append({'unit': unit}) action_obj = zaza_model.run_action( unit_name=unit, @@ -572,13 +579,13 @@ class CephTest(test_utils.OpenStackBaseTest): logging.info('Removing previously added disks') for param in params: - osd_id = self._get_local_osd_id(param['unit']) + osd_id = self.get_local_osd_id(param['unit']) param.update({'osd-id': osd_id}) action_obj = zaza_model.run_action( unit_name=param['unit'], action_name='remove-disk', action_params={'osd-ids': osd_id, 'timeout': 5, - 'format': 'json', 'purge': True} + 'format': 'json', 'purge': False} ) zaza_utils.assertActionRanOK(action_obj) results = json.loads(action_obj.data['results']['message']) @@ -599,6 +606,7 @@ class CephTest(test_utils.OpenStackBaseTest): ) zaza_utils.assertActionRanOK(action_obj) zaza_model.wait_for_application_states() + self.assertEqual(len(osds) * 2, self.get_num_osds(mon)) class CephRGWTest(test_utils.OpenStackBaseTest): @@ -861,7 +869,9 @@ class CephPrometheusTest(unittest.TestCase): unit = zaza_model.get_unit_from_name( zaza_model.get_lead_unit_name('prometheus2')) self.assertEqual( - '3', _get_mon_count_from_prometheus(unit.public_address)) + '3', + _get_mon_count_from_prometheus( + zaza_model.get_unit_public_address(unit))) class CephPoolConfig(Exception): diff --git a/zaza/openstack/charm_tests/cinder_backend/__init__.py b/zaza/openstack/charm_tests/cinder_backend/__init__.py new file mode 100644 index 0000000..bc8ad6a --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_backend/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2022 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. + +"""Collection of code for setting up and testing cinder backends.""" diff --git a/zaza/openstack/charm_tests/cinder_backend/tests.py b/zaza/openstack/charm_tests/cinder_backend/tests.py new file mode 100644 index 0000000..358d6a8 --- /dev/null +++ b/zaza/openstack/charm_tests/cinder_backend/tests.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# Copyright 2021 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. + +"""Encapsulate cinder backend tests.""" + +import uuid + +import zaza.model +import zaza.openstack.charm_tests.test_utils as test_utils +import zaza.openstack.utilities.openstack as openstack_utils + + +class CinderBackendTest(test_utils.OpenStackBaseTest): + """Encapsulate cinder backend tests.""" + + expected_config_content = {} + backend_name = "" + + @classmethod + def setUpClass(cls): + """Run class setup for running tests.""" + super(CinderBackendTest, cls).setUpClass() + cls.keystone_session = openstack_utils.get_overcloud_keystone_session() + cls.model_name = zaza.model.get_juju_model() + cls.cinder_client = openstack_utils.get_cinder_session_client( + cls.keystone_session) + + def test_cinder_config(self): + """Test that configuration options match our expectations.""" + zaza.model.run_on_leader( + 'cinder', + 'sudo cp /etc/cinder/cinder.conf /tmp/') + zaza.model.block_until_oslo_config_entries_match( + 'cinder', + '/tmp/cinder.conf', + self.expected_config_content, + timeout=2) + + def test_create_volume(self): + """Test creating a volume with basic configuration.""" + test_vol_name = "zaza{}".format(uuid.uuid1().fields[0]) + vol_new = self.cinder_client.volumes.create( + name=test_vol_name, + size='1') + try: + openstack_utils.resource_reaches_status( + self.cinder_client.volumes, + vol_new.id, + wait_iteration_max_time=12000, + stop_after_attempt=5, + expected_status='available', + msg='Volume status wait') + test_vol = self.cinder_client.volumes.find(name=test_vol_name) + self.assertEqual( + getattr(test_vol, 'os-vol-host-attr:host').split('#')[0], + 'cinder@{}'.format(self.backend_name)) + finally: + self.cinder_client.volumes.delete(vol_new) diff --git a/zaza/openstack/charm_tests/cinder_netapp/tests.py b/zaza/openstack/charm_tests/cinder_netapp/tests.py index 41456ba..9e1ebc1 100644 --- a/zaza/openstack/charm_tests/cinder_netapp/tests.py +++ b/zaza/openstack/charm_tests/cinder_netapp/tests.py @@ -16,62 +16,19 @@ """Encapsulate cinder-netapp testing.""" -import uuid - -import zaza.model -import zaza.openstack.charm_tests.test_utils as test_utils -import zaza.openstack.utilities.openstack as openstack_utils +from zaza.openstack.charm_tests.cinder_backend.tests import CinderBackendTest -class CinderNetAppTest(test_utils.OpenStackBaseTest): +class CinderNetAppTest(CinderBackendTest): """Encapsulate netapp tests.""" - @classmethod - def setUpClass(cls): - """Run class setup for running tests.""" - super(CinderNetAppTest, cls).setUpClass() - cls.keystone_session = openstack_utils.get_overcloud_keystone_session() - cls.model_name = zaza.model.get_juju_model() - cls.cinder_client = openstack_utils.get_cinder_session_client( - cls.keystone_session) + backend_name = 'cinder-netapp' - def test_cinder_config(self): - """Test that configuration options match our expectations.""" - expected_contents = { - 'cinder-netapp': { - 'netapp_storage_family': ['ontap_cluster'], - 'netapp_storage_protocol': ['iscsi'], - 'volume_backend_name': ['cinder_netapp'], - 'volume_driver': + expected_config_content = { + 'cinder-netapp': { + 'netapp_storage_family': ['ontap_cluster'], + 'netapp_storage_protocol': ['iscsi'], + 'volume_backend_name': ['cinder_netapp'], + 'volume_driver': ['cinder.volume.drivers.netapp.common.NetAppDriver'], - }} - - zaza.model.run_on_leader( - 'cinder', - 'sudo cp /etc/cinder/cinder.conf /tmp/') - zaza.model.block_until_oslo_config_entries_match( - 'cinder', - '/tmp/cinder.conf', - expected_contents, - timeout=2) - - def test_create_volume(self): - """Test creating a volume with basic configuration.""" - test_vol_name = "zaza{}".format(uuid.uuid1().fields[0]) - vol_new = self.cinder_client.volumes.create( - name=test_vol_name, - size='1') - try: - openstack_utils.resource_reaches_status( - self.cinder_client.volumes, - vol_new.id, - wait_iteration_max_time=12000, - stop_after_attempt=5, - expected_status='available', - msg='Volume status wait') - test_vol = self.cinder_client.volumes.find(name=test_vol_name) - self.assertEqual( - getattr(test_vol, 'os-vol-host-attr:host').split('#')[0], - 'cinder@cinder-netapp') - finally: - self.cinder_client.volumes.delete(vol_new) + }} diff --git a/zaza/openstack/charm_tests/glance/tests.py b/zaza/openstack/charm_tests/glance/tests.py index a6da291..0f505df 100644 --- a/zaza/openstack/charm_tests/glance/tests.py +++ b/zaza/openstack/charm_tests/glance/tests.py @@ -17,6 +17,7 @@ """Encapsulate glance testing.""" import logging +import math import boto3 import zaza.model as model @@ -222,3 +223,46 @@ class GlanceExternalS3Test(test_utils.OpenStackBaseTest): ) self.assertEqual(image["size"], response["ContentLength"]) openstack_utils.delete_image(self.glance_client, image["id"]) + + +class GlanceCinderBackendTest(test_utils.OpenStackBaseTest): + """Encapsulate glance tests using cinder backend.""" + + @classmethod + def setUpClass(cls): + """Run class setup for running glance tests with cinder backend.""" + super(GlanceCinderBackendTest, cls).setUpClass() + cls.glance_client = openstack_utils.get_glance_session_client( + cls.keystone_session) + cls.cinder_client = openstack_utils.get_cinder_session_client( + cls.keystone_session) + + def test_100_create_delete_image(self): + """Create an image and do a simple validation of it. + + Validate the size of the image in both Glance API and Cinder API. + """ + image_name = "zaza-cinder-test-image" + openstack_utils.create_image( + glance=self.glance_client, + image_url=openstack_utils.find_cirros_image(arch="x86_64"), + image_name=image_name, + backend="cinder", + ) + images = openstack_utils.get_images_by_name( + self.glance_client, image_name) + self.assertEqual(len(images), 1) + image = images[0] + + volume_name = 'image-'+image["id"] + volumes = openstack_utils.get_volumes_by_name( + self.cinder_client, volume_name) + self.assertEqual(len(volumes), 1) + volume = volumes[0] + + logging.info( + "Checking glance image size {} matches volume size {} " + "GB".format(image["size"], volume.size)) + image_size_in_gb = int(math.ceil(float(image["size"]) / 1024 ** 3)) + self.assertEqual(image_size_in_gb, volume.size) + openstack_utils.delete_image(self.glance_client, image["id"]) diff --git a/zaza/openstack/charm_tests/manila_ganesha/setup.py b/zaza/openstack/charm_tests/manila_ganesha/setup.py index a804958..c4edd97 100644 --- a/zaza/openstack/charm_tests/manila_ganesha/setup.py +++ b/zaza/openstack/charm_tests/manila_ganesha/setup.py @@ -42,5 +42,5 @@ def setup_ganesha_share_type(manila_client=None): extra_specs={ 'vendor_name': 'Ceph', 'storage_protocol': 'NFS', - 'snapshot_support': False, + 'snapshot_support': True, }) diff --git a/zaza/openstack/charm_tests/mysql/tests.py b/zaza/openstack/charm_tests/mysql/tests.py index fbc6494..f040a87 100644 --- a/zaza/openstack/charm_tests/mysql/tests.py +++ b/zaza/openstack/charm_tests/mysql/tests.py @@ -90,7 +90,7 @@ class MySQLBaseTest(test_utils.OpenStackBaseTest): _primary_ip = _primary_ip.split(':')[0] units = zaza.model.get_units(self.application_name) for unit in units: - if _primary_ip in unit.public_address: + if _primary_ip in zaza.model.get_unit_public_address(unit): return unit def get_blocked_mysql_routers(self): @@ -802,6 +802,7 @@ class MySQLInnoDBClusterScaleTest(MySQLBaseTest): leader, nons = generic_utils.get_leaders_and_non_leaders( self.application_name) leader_unit = zaza.model.get_unit_from_name(leader) + leader_unit_ip = zaza.model.get_unit_public_address(leader_unit) # Wait until we are idle in the hopes clients are not running # update-status hooks @@ -821,12 +822,12 @@ class MySQLInnoDBClusterScaleTest(MySQLBaseTest): logging.info( "Removing old unit from cluster: {} " - .format(leader_unit.public_address)) + .format(leader_unit_ip)) action = zaza.model.run_action( nons[0], "remove-instance", action_params={ - "address": leader_unit.public_address, + "address": leader_unit_ip, "force": True}) assert action.data.get("results") is not None, ( "Remove instance action failed: No results: {}" @@ -877,6 +878,8 @@ class MySQLInnoDBClusterScaleTest(MySQLBaseTest): leader, nons = generic_utils.get_leaders_and_non_leaders( self.application_name) non_leader_unit = zaza.model.get_unit_from_name(nons[0]) + non_leader_unit_ip = zaza.model.get_unit_public_address( + non_leader_unit) # Wait until we are idle in the hopes clients are not running # update-status hooks @@ -897,12 +900,12 @@ class MySQLInnoDBClusterScaleTest(MySQLBaseTest): logging.info( "Removing old unit from cluster: {} " - .format(non_leader_unit.public_address)) + .format(non_leader_unit_ip)) action = zaza.model.run_action( leader, "remove-instance", action_params={ - "address": non_leader_unit.public_address, + "address": non_leader_unit_ip, "force": True}) assert action.data.get("results") is not None, ( "Remove instance action failed: No results: {}" @@ -925,7 +928,7 @@ class MySQLInnoDBClusterPartitionTest(MySQLBaseTest): no_of_units = len(mysql_units) for index, unit in enumerate(mysql_units): next_unit = mysql_units[(index+1) % no_of_units] - ip_address = next_unit.public_address + ip_address = zaza.model.get_unit_public_address(next_unit) cmd = "sudo iptables -A INPUT -s {} -j DROP".format(ip_address) zaza.model.async_run_on_unit(unit, cmd) @@ -949,7 +952,7 @@ class MySQLInnoDBClusterPartitionTest(MySQLBaseTest): leader_unit.entity_id, "force-quorum-using-partition-of", action_params={ - "address": leader_unit.public_address, + "address": zaza.model.get_unit_public_address(leader_unit), 'i-really-mean-it': True }) diff --git a/zaza/openstack/charm_tests/neutron/setup.py b/zaza/openstack/charm_tests/neutron/setup.py index 174c51d..290483a 100644 --- a/zaza/openstack/charm_tests/neutron/setup.py +++ b/zaza/openstack/charm_tests/neutron/setup.py @@ -23,10 +23,11 @@ from zaza.openstack.configure import ( from zaza.openstack.utilities import ( cli as cli_utils, generic as generic_utils, - juju as juju_utils, openstack as openstack_utils, ) +import zaza.utilities.juju as juju_utils + import zaza.charm_lifecycle.utils as lifecycle_utils diff --git a/zaza/openstack/charm_tests/neutron/tests.py b/zaza/openstack/charm_tests/neutron/tests.py index b3c8906..5ea8b76 100644 --- a/zaza/openstack/charm_tests/neutron/tests.py +++ b/zaza/openstack/charm_tests/neutron/tests.py @@ -492,14 +492,11 @@ class NeutronApiTest(NeutronCreateNetworkTest): Pause service and check services are stopped then resume and check they are started """ - bionic_stein = openstack_utils.get_os_release('bionic_stein') - if openstack_utils.get_os_release() >= bionic_stein: - pgrep_full = True - else: - pgrep_full = False with self.pause_resume( - ["neutron-server", "apache2", "haproxy"], - pgrep_full=pgrep_full): + ["/usr/bin/neutron-server", + "/usr/sbin/apache2", + "/usr/sbin/haproxy"], + pgrep_full=True): logging.info("Testing pause resume") diff --git a/zaza/openstack/charm_tests/neutron_arista/utils.py b/zaza/openstack/charm_tests/neutron_arista/utils.py index 6fb77b3..6ded31b 100644 --- a/zaza/openstack/charm_tests/neutron_arista/utils.py +++ b/zaza/openstack/charm_tests/neutron_arista/utils.py @@ -25,7 +25,8 @@ PLUGIN_APP_NAME = 'neutron-api-plugin-arista' def fixture_ip_addr(): """Return the public IP address of the Arista test fixture.""" - return zaza.model.get_units(FIXTURE_APP_NAME)[0].public_address + return zaza.model.get_unit_public_address( + zaza.model.get_units(FIXTURE_APP_NAME)[0]) _FIXTURE_LOGIN = 'admin' diff --git a/zaza/openstack/charm_tests/nova/tests.py b/zaza/openstack/charm_tests/nova/tests.py index 66ca957..00ca643 100644 --- a/zaza/openstack/charm_tests/nova/tests.py +++ b/zaza/openstack/charm_tests/nova/tests.py @@ -461,7 +461,7 @@ class NovaCloudControllerActionTest(test_utils.OpenStackBaseTest): if juju_az: zone = juju_az - juju_units_az_map[unit.public_address] = zone + juju_units_az_map[zaza.model.get_unit_public_address(unit)] = zone continue session = openstack_utils.get_overcloud_keystone_session() diff --git a/zaza/openstack/charm_tests/octavia/setup.py b/zaza/openstack/charm_tests/octavia/setup.py index b04b696..9560e43 100644 --- a/zaza/openstack/charm_tests/octavia/setup.py +++ b/zaza/openstack/charm_tests/octavia/setup.py @@ -15,6 +15,7 @@ """Code for configuring octavia.""" import os +import re import base64 import logging @@ -141,6 +142,63 @@ def configure_octavia(): 'octavia', 'configure-resources', action_params={}) + # When bug #1964117 is fix released for all affected releases this call can + # be removed. + bug_1964117_workaround() + + +def disable_ohm_port_security(): + """Disable port security on the health manager ports on octavia units.""" + keystone_session = openstack.get_overcloud_keystone_session() + neutron_client = openstack.get_neutron_session_client( + keystone_session) + ports = [ + p + for p in neutron_client.list_ports()['ports'] + if re.match('octavia-health-manager-.*-listen-port', p['name'])] + for port in ports: + neutron_client.update_port( + port['id'], + { + 'port': + { + 'port_security_enabled': False, + 'security_groups': []}}) + + +def bug_1964117_workaround(): + """Apply Bug #1964117 if allowed.""" + if openstack.ovn_present(): + # Issue only known to affect ml2 ovs so if do not apply work around + # to ovn deploys. + return + allow_pkg_list = ['2.16.0-0ubuntu2.1~cloud0'] + allow_release_list = ['focal_xena'] + _allow_release_list = [ + openstack.get_os_release(r) + for r in allow_release_list + ] + current_release = openstack.get_os_release() + if current_release in _allow_release_list: + cmd_out = zaza.model.run_on_leader( + 'octavia', + """dpkg -l | awk '/openvswitch-switch/ {print $3;}'""") + pkg_version = cmd_out['Stdout'].strip() + if pkg_version in allow_pkg_list: + logging.info('Disabling port security to work around bug #1964117') + disable_ohm_port_security() + else: + msg = ( + "Detected Xena deploy and package version {} is not in the " + "allow list {}. If you believe that bug #1964117 has been " + "resolved please remove the call to this function. If the " + "new package does not resolve bug #1964117 then please add " + "the new package version to the 'allow_pkg_list' defined at " + "the start of this function. If changes are required please " + "raise a PR againt " + "https://github.com/openstack-charmers/zaza-openstack-tests" + "".format(pkg_version, allow_pkg_list)) + raise Exception(msg) def centralized_fip_network(): diff --git a/zaza/openstack/charm_tests/octavia/tests.py b/zaza/openstack/charm_tests/octavia/tests.py index 6158375..086932b 100644 --- a/zaza/openstack/charm_tests/octavia/tests.py +++ b/zaza/openstack/charm_tests/octavia/tests.py @@ -29,6 +29,10 @@ import zaza.openstack.utilities.openstack as openstack_utils from zaza.openstack.utilities import generic as generic_utils from zaza.openstack.utilities import ObjectRetrierWraps +from zaza.openstack.utilities.exceptions import ( + LoadBalancerUnexpectedState, + LoadBalancerUnrecoverableError, +) LBAAS_ADMIN_ROLE = 'load-balancer_admin' @@ -279,24 +283,35 @@ class LBAASv2Test(test_utils.OpenStackBaseTest): super(LBAASv2Test, self).resource_cleanup() @staticmethod - @tenacity.retry(retry=tenacity.retry_if_exception_type(AssertionError), - wait=tenacity.wait_fixed(1), reraise=True, - stop=tenacity.stop_after_delay(900)) + @tenacity.retry( + retry=tenacity.retry_if_exception_type(LoadBalancerUnexpectedState), + wait=tenacity.wait_fixed(1), reraise=True, + stop=tenacity.stop_after_delay(900)) def wait_for_lb_resource(octavia_show_func, resource_id, provisioning_status=None, operating_status=None): """Wait for loadbalancer resource to reach expected status.""" provisioning_status = provisioning_status or 'ACTIVE' resp = octavia_show_func(resource_id) - logging.info(resp['provisioning_status']) - assert resp['provisioning_status'] == provisioning_status, ( - 'load balancer resource has not reached ' - 'expected provisioning status: {}' - .format(resp)) + logging.info("Current provisioning status: {}, waiting for {}" + .format(resp['provisioning_status'], provisioning_status)) + + msg = ('load balancer resource has not reached ' + 'expected provisioning status: {}'.format(resp)) + + # ERROR is a final state, once it's reached there is no reason to keep + # retrying and delaying the failure. + if resp['provisioning_status'] == 'ERROR': + raise LoadBalancerUnrecoverableError(msg) + elif resp['provisioning_status'] != provisioning_status: + raise LoadBalancerUnexpectedState(msg) + if operating_status: - logging.info(resp['operating_status']) - assert resp['operating_status'] == operating_status, ( - 'load balancer resource has not reached ' - 'expected operating status: {}'.format(resp)) + logging.info('Current operating status: {}, waiting for {}' + .format(resp['operating_status'], operating_status)) + if not resp['operating_status'] == operating_status: + raise LoadBalancerUnexpectedState(( + 'load balancer resource has not reached ' + 'expected operating status: {}'.format(resp))) return resp diff --git a/zaza/openstack/charm_tests/openstack_dashboard/tests.py b/zaza/openstack/charm_tests/openstack_dashboard/tests.py index 60ac7eb..d689581 100644 --- a/zaza/openstack/charm_tests/openstack_dashboard/tests.py +++ b/zaza/openstack/charm_tests/openstack_dashboard/tests.py @@ -178,7 +178,7 @@ class OpenStackDashboardBase(): else: unit = zaza_model.get_unit_from_name( zaza_model.get_lead_unit_name(self.application_name)) - ip = unit.public_address + ip = zaza_model.get_unit_public_address(unit) logging.debug("Dashboard ip is:{}".format(ip)) scheme = 'http' @@ -272,7 +272,8 @@ class OpenStackDashboardTests(test_utils.OpenStackBaseTest, logging.info('Checking dashboard HAProxy settings...') unit = zaza_model.get_unit_from_name( zaza_model.get_lead_unit_name(self.application_name)) - logging.debug("... dashboard_ip is:{}".format(unit.public_address)) + logging.debug("... dashboard_ip is:{}".format( + zaza_model.get_unit_public_address(unit))) conf = '/etc/haproxy/haproxy.cfg' port = '8888' set_alternate = { @@ -280,13 +281,18 @@ class OpenStackDashboardTests(test_utils.OpenStackBaseTest, } request = urllib.request.Request( - 'http://{}:{}'.format(unit.public_address, port)) + 'http://{}:{}'.format( + zaza_model.get_unit_public_address(unit), port)) output = str(generic_utils.get_file_contents(unit, conf)) + password = None for line in output.split('\n'): if "stats auth" in line: password = line.split(':')[1] + break + else: + raise ValueError("'stats auth' not found in output'") base64string = base64.b64encode( bytes('{}:{}'.format('admin', password), 'ascii')) request.add_header( @@ -493,7 +499,8 @@ class OpenStackDashboardPolicydTests(policyd.BasePolicydSpecialization, """ unit = zaza_model.get_unit_from_name( zaza_model.get_lead_unit_name(self.application_name)) - logging.info("Dashboard is at {}".format(unit.public_address)) + logging.info("Dashboard is at {}".format( + zaza_model.get_unit_public_address(unit))) overcloud_auth = openstack_utils.get_overcloud_auth() password = overcloud_auth['OS_PASSWORD'] logging.info("admin password is {}".format(password)) @@ -504,7 +511,7 @@ class OpenStackDashboardPolicydTests(policyd.BasePolicydSpecialization, self.get_horizon_url(), domain, username, password, cafile=self.cacert) # now attempt to get the domains page - _url = self.url.format(unit.public_address) + _url = self.url.format(zaza_model.get_unit_public_address(unit)) logging.info("URL is {}".format(_url)) result = client.get(_url) if result.status_code == 403: diff --git a/zaza/openstack/charm_tests/rabbitmq_server/tests.py b/zaza/openstack/charm_tests/rabbitmq_server/tests.py index fed72df..027ef95 100644 --- a/zaza/openstack/charm_tests/rabbitmq_server/tests.py +++ b/zaza/openstack/charm_tests/rabbitmq_server/tests.py @@ -26,8 +26,8 @@ import zaza.model import zaza.openstack.charm_tests.test_utils as test_utils import zaza.openstack.utilities.generic as generic_utils -from charmhelpers.core.host import CompareHostReleases from zaza.openstack.utilities.generic import get_series +from zaza.openstack.utilities.os_versions import CompareHostReleases from . import utils as rmq_utils from .utils import RmqNoMessageException @@ -49,6 +49,7 @@ class RmqTests(test_utils.OpenStackBaseTest): return '[{}-{}]'.format(uuid.uuid4(), time.time()) @tenacity.retry( + reraise=True, retry=tenacity.retry_if_exception_type(RmqNoMessageException), wait=tenacity.wait_fixed(10), stop=tenacity.stop_after_attempt(2)) @@ -57,6 +58,40 @@ class RmqTests(test_utils.OpenStackBaseTest): ssl=ssl, port=port) + def _search_for_message(self, amqp_msg, check_unit, ssl, port, + amqp_msg_counter): + """Search for message in message queue. + + WARNING: This will consume messages until it finds the target message. + + :param amqp_msg: Message to search for + :type amqp_msg: string + :param check_unit: Unit to retrieve messages from + :type check_unit: juju.unit.Unit + :param ssl: Whether to use SSL when connecting to rabbit + :type ssl: bool + :param port: Port to use when connecting to rabbit + :type port: Union[int, None] + :param amqp_msg_counter: Number in test sequence of this message. + :type amqp_msg: int + :raises: RmqNoMessageException + """ + for i in range(100): + amqp_msg_rcvd = self._retry_get_amqp_message( + check_unit, + ssl=ssl, + port=port) + if amqp_msg == amqp_msg_rcvd: + logging.info( + 'Message {} received OK.'.format(amqp_msg_counter)) + break + else: + logging.info('Expected: {}'.format(amqp_msg)) + logging.info('Actual: {}'.format(amqp_msg_rcvd)) + else: + msg = 'Message {} not found.'.format(amqp_msg_counter) + raise RmqNoMessageException(msg) + def _test_rmq_amqp_messages_all_units(self, units, ssl=False, port=None): """Reusable test to send/check amqp messages to every listed rmq unit. @@ -85,7 +120,7 @@ class RmqTests(test_utils.OpenStackBaseTest): for dest_unit in units: dest_unit_name = dest_unit.entity_id - dest_unit_host = dest_unit.public_address + dest_unit_host = zaza.model.get_unit_public_address(dest_unit) dest_unit_host_name = host_names[dest_unit_name] for check_unit in units: @@ -93,7 +128,8 @@ class RmqTests(test_utils.OpenStackBaseTest): if dest_unit_name == check_unit_name: logging.info("Skipping check for this unit to itself.") continue - check_unit_host = check_unit.public_address + check_unit_host = zaza.model.get_unit_public_address( + check_unit) check_unit_host_name = host_names[check_unit_name] amqp_msg_stamp = self._get_uuid_epoch_stamp() @@ -111,25 +147,22 @@ class RmqTests(test_utils.OpenStackBaseTest): port=port) # Get amqp message - logging.info('Get message from: {} ' + logging.info('Get messages from: {} ' '({} {})'.format(check_unit_host, check_unit_name, check_unit_host_name)) - amqp_msg_rcvd = self._retry_get_amqp_message(check_unit, - ssl=ssl, - port=port) - - # Validate amqp message content - if amqp_msg == amqp_msg_rcvd: - logging.info('Message {} received ' - 'OK.'.format(amqp_msg_counter)) - else: - logging.error('Expected: {}'.format(amqp_msg)) - logging.error('Actual: {}'.format(amqp_msg_rcvd)) - msg = 'Message {} mismatch.'.format(amqp_msg_counter) + try: + self._search_for_message( + amqp_msg, + check_unit, + ssl, + port, + amqp_msg_counter) + except RmqNoMessageException: + msg = 'Failed to retrieve message {}.'.format( + amqp_msg_counter) raise Exception(msg) - amqp_msg_counter += 1 # Delete the test user diff --git a/zaza/openstack/charm_tests/rabbitmq_server/utils.py b/zaza/openstack/charm_tests/rabbitmq_server/utils.py index d00f288..a0ced05 100644 --- a/zaza/openstack/charm_tests/rabbitmq_server/utils.py +++ b/zaza/openstack/charm_tests/rabbitmq_server/utils.py @@ -351,7 +351,7 @@ def configure_ssl_off(units, model_name=None, max_wait=60): def is_ssl_enabled_on_unit(unit, port=None): """Check a single juju rmq unit for ssl and port in the config file.""" - host = unit.public_address + host = zaza.model.get_unit_public_address(unit) unit_name = unit.entity_id conf_file = '/etc/rabbitmq/rabbitmq.conf' @@ -406,7 +406,7 @@ def connect_amqp_by_unit(unit, ssl=False, :param password: amqp user password :returns: pika amqp connection pointer or None if failed and non-fatal """ - host = unit.public_address + host = zaza.model.get_unit_public_address(unit) unit_name = unit.entity_id if ssl: @@ -506,9 +506,9 @@ def get_amqp_message_by_unit(unit, queue="test", password=password) channel = connection.channel() method_frame, _, body = channel.basic_get(queue) - body = body.decode() if method_frame: + body = body.decode() logging.debug('Retreived message from {} queue:\n{}'.format(queue, body)) channel.basic_ack(method_frame.delivery_tag) diff --git a/zaza/openstack/charm_tests/saml_mellon/setup.py b/zaza/openstack/charm_tests/saml_mellon/setup.py index 20b8cbd..0a7e159 100644 --- a/zaza/openstack/charm_tests/saml_mellon/setup.py +++ b/zaza/openstack/charm_tests/saml_mellon/setup.py @@ -228,7 +228,7 @@ def keystone_federation_setup_idp1(): """Configure Keystone Federation for the local IdP #1.""" test_saml_idp_unit = zaza.model.get_units("test-saml-idp1")[0] idp_remote_id = LOCAL_IDP_REMOTE_ID.format( - test_saml_idp_unit.public_address) + zaza.model.get_unit_public_address(test_saml_idp_unit)) keystone_federation_setup( federated_domain="federated_domain_idp1", @@ -241,7 +241,7 @@ def keystone_federation_setup_idp2(): """Configure Keystone Federation for the local IdP #2.""" test_saml_idp_unit = zaza.model.get_units("test-saml-idp2")[0] idp_remote_id = LOCAL_IDP_REMOTE_ID.format( - test_saml_idp_unit.public_address) + zaza.model.get_unit_public_address(test_saml_idp_unit)) keystone_federation_setup( federated_domain="federated_domain_idp2", diff --git a/zaza/openstack/charm_tests/saml_mellon/tests.py b/zaza/openstack/charm_tests/saml_mellon/tests.py index 962fa92..64e9be7 100644 --- a/zaza/openstack/charm_tests/saml_mellon/tests.py +++ b/zaza/openstack/charm_tests/saml_mellon/tests.py @@ -54,7 +54,7 @@ class CharmKeystoneSAMLMellonTest(BaseKeystoneTest): if self.vip: ip = self.vip else: - ip = unit.public_address + ip = zaza.model.get_unit_public_address(unit) action = zaza.model.run_action(unit.entity_id, self.action) if "failed" in action.data["status"]: @@ -81,7 +81,7 @@ class CharmKeystoneSAMLMellonTest(BaseKeystoneTest): keystone_ip = self.vip else: unit = zaza.model.get_units(self.application_name)[0] - keystone_ip = unit.public_address + keystone_ip = zaza.model.get_unit_public_address(unit) horizon = "openstack-dashboard" horizon_vip = (zaza.model.get_application_config(horizon) @@ -90,7 +90,7 @@ class CharmKeystoneSAMLMellonTest(BaseKeystoneTest): horizon_ip = horizon_vip else: unit = zaza.model.get_units("openstack-dashboard")[0] - horizon_ip = unit.public_address + horizon_ip = zaza.model.get_unit_public_address(unit) if self.tls_rid: proto = "https" @@ -258,7 +258,7 @@ class BaseCharmKeystoneSAMLMellonTest(BaseKeystoneTest): def test_run_get_sp_metadata_action(self): """Validate the get-sp-metadata action.""" unit = zaza.model.get_units(self.application_name)[0] - ip = self.vip if self.vip else unit.public_address + ip = self.vip if self.vip else zaza.model.get_unit_public_address(unit) action = zaza.model.run_action(unit.entity_id, self.action) self.assertNotIn( @@ -283,14 +283,16 @@ class BaseCharmKeystoneSAMLMellonTest(BaseKeystoneTest): def test_saml_mellon_redirects(self): """Validate the horizon -> keystone -> IDP redirects.""" unit = zaza.model.get_units(self.application_name)[0] - keystone_ip = self.vip if self.vip else unit.public_address + keystone_ip = self.vip if self.vip else ( + zaza.model.get_unit_public_address(unit)) horizon = "openstack-dashboard" horizon_config = zaza.model.get_application_config(horizon) horizon_vip = horizon_config.get("vip").get("value") unit = zaza.model.get_units("openstack-dashboard")[0] - horizon_ip = horizon_vip if horizon_vip else unit.public_address + horizon_ip = horizon_vip if horizon_vip else ( + zaza.model.get_unit_public_address(unit)) proto = "https" if self.tls_rid else "http" # Use Keystone URL for < Focal @@ -299,8 +301,8 @@ class BaseCharmKeystoneSAMLMellonTest(BaseKeystoneTest): else: region = "default" - idp_address = zaza.model.get_units( - self.test_saml_idp_app_name)[0].public_address + idp_address = zaza.model.get_unit_public_address( + zaza.model.get_units(self.test_saml_idp_app_name)[0]) horizon_url = "{}://{}/horizon/auth/login/".format(proto, horizon_ip) horizon_expect = ''.format( diff --git a/zaza/openstack/charm_tests/series_upgrade/parallel_tests.py b/zaza/openstack/charm_tests/series_upgrade/parallel_tests.py index e524fa2..4dd2432 100644 --- a/zaza/openstack/charm_tests/series_upgrade/parallel_tests.py +++ b/zaza/openstack/charm_tests/series_upgrade/parallel_tests.py @@ -73,7 +73,8 @@ class ParallelSeriesUpgradeTest(unittest.TestCase): # Set Feature Flag os.environ["JUJU_DEV_FEATURE_FLAGS"] = "upgrade-series" upgrade_groups = upgrade_utils.get_series_upgrade_groups( - extra_filters=[_filter_etcd, _filter_easyrsa]) + extra_filters=[_filter_etcd, _filter_easyrsa], + target_series=self.to_series) from_series = self.from_series to_series = self.to_series completed_machines = [] diff --git a/zaza/openstack/charm_tests/series_upgrade/tests.py b/zaza/openstack/charm_tests/series_upgrade/tests.py index 122af11..3bb37ed 100644 --- a/zaza/openstack/charm_tests/series_upgrade/tests.py +++ b/zaza/openstack/charm_tests/series_upgrade/tests.py @@ -193,7 +193,8 @@ class ParallelSeriesUpgradeTest(unittest.TestCase): os.environ["JUJU_DEV_FEATURE_FLAGS"] = "upgrade-series" upgrade_groups = upgrade_utils.get_series_upgrade_groups( extra_filters=[upgrade_utils._filter_etcd, - upgrade_utils._filter_easyrsa]) + upgrade_utils._filter_easyrsa], + target_series=self.to_series) applications = model.get_status().applications completed_machines = [] for group_name, group in upgrade_groups: diff --git a/zaza/openstack/charm_tests/swift/tests.py b/zaza/openstack/charm_tests/swift/tests.py index 3cb34df..7c731b6 100644 --- a/zaza/openstack/charm_tests/swift/tests.py +++ b/zaza/openstack/charm_tests/swift/tests.py @@ -72,6 +72,12 @@ class SwiftImageCreateTest(test_utils.OpenStackBaseTest): class SwiftProxyTests(test_utils.OpenStackBaseTest): """Tests specific to swift proxy.""" + TEST_SEARCH_TARGET = 'd0' + TEST_REMOVE_TARGET = 'd1' + TEST_EXPECTED_RING_HOSTS = 1 + TEST_WEIGHT_TARGET = 999 + TEST_WEIGHT_INITIAL = 100 + def test_901_pause_resume(self): """Run pause and resume tests. @@ -91,6 +97,59 @@ class SwiftProxyTests(test_utils.OpenStackBaseTest): action_params={}) self.assertEqual(action.status, "completed") + def test_904_set_weight_action_and_validate_rebalance(self): + """Set weight of device in object ring.""" + logging.info('Running set-weight action on leader') + action = zaza.model.run_action_on_leader( + 'swift-proxy', + 'set-weight', + action_params={'ring': 'object', + 'search-value': self.TEST_SEARCH_TARGET, + 'weight': self.TEST_WEIGHT_TARGET}) + self.assertEqual(action.status, "completed") + + logging.info('Validating builder updated as expected') + result = swift_utils.search_builder('swift-proxy', 'object', + self.TEST_SEARCH_TARGET) + # disk weight is the 9th field of the second line and is a float + disk_weight = int(result.split('\n')[1].split()[8].split('.')[0]) + self.assertEqual(disk_weight, self.TEST_WEIGHT_TARGET) + self.assertTrue(swift_utils.is_proxy_ring_up_to_date('swift-proxy', + 'object')) + + logging.info('Running set-weight on leader to reset weight back') + action = zaza.model.run_action_on_leader( + 'swift-proxy', + 'set-weight', + action_params={'ring': 'object', + 'search-value': self.TEST_SEARCH_TARGET, + 'weight': self.TEST_WEIGHT_INITIAL}) + self.assertEqual(action.status, "completed") + self.assertTrue( + swift_utils.is_ring_synced('swift-proxy', 'object', + self.TEST_EXPECTED_RING_HOSTS)) + + def test_905_remove_device_action_and_validate_rebalance(self): + """Remove device from object ring.""" + logging.info('Running remove-devices action on leader') + action = zaza.model.run_action_on_leader( + 'swift-proxy', + 'remove-devices', + action_params={'ring': 'object', + 'search-value': self.TEST_REMOVE_TARGET}) + self.assertEqual(action.status, "completed") + + logging.info('Validating builder updated as expected') + result = swift_utils.search_builder('swift-proxy', 'object', + self.TEST_REMOVE_TARGET) + expected = 'No matching devices found' + self.assertEqual(result.strip('\n'), expected) + self.assertTrue(swift_utils.is_proxy_ring_up_to_date('swift-proxy', + 'object')) + self.assertTrue( + swift_utils.is_ring_synced('swift-proxy', 'object', + self.TEST_EXPECTED_RING_HOSTS)) + class SwiftProxyMultiZoneTests(test_utils.OpenStackBaseTest): """Tests specific to swift proxy in multi zone environment.""" diff --git a/zaza/openstack/charm_tests/test_utils.py b/zaza/openstack/charm_tests/test_utils.py index be47dd9..a7c522d 100644 --- a/zaza/openstack/charm_tests/test_utils.py +++ b/zaza/openstack/charm_tests/test_utils.py @@ -1043,6 +1043,21 @@ class BaseDeferredRestartTest(BaseCharmTest): # clear status message. self.clear_hooks() + def get_service_timestamps(self, service): + """For units of self.application_name get start time of service. + + :param service: Service to check, must be a systemd service + :type service: str + :returns: A dict timestamps keyed on unit name. + :rtype: dict + """ + timestamps = {} + for unit in model.get_units(self.application_name): + timestamps[unit.entity_id] = model.get_systemd_service_active_time( + unit.entity_id, + service) + return timestamps + def run_package_change_test(self, restart_package, restart_package_svc): """Trigger a deferred restart by updating a package. @@ -1055,8 +1070,32 @@ class BaseDeferredRestartTest(BaseCharmTest): after restart_package has changed. :type restart_package_service: str """ + pre_timestamps = self.get_service_timestamps( + restart_package_svc) self.trigger_deferred_restart_via_package(restart_package) - + post_timestamps = self.get_service_timestamps( + restart_package_svc) + broken_units = [] + for unit_name in post_timestamps.keys(): + if pre_timestamps[unit_name] != post_timestamps[unit_name]: + logging.error( + "Service {} on unit {} should have start time of {} but" + " it has {}".format( + restart_package_svc, + unit_name, + pre_timestamps[unit_name], + post_timestamps[unit_name])) + broken_units.append(unit_name) + if broken_units: + msg = ( + "Units {} restarted service {} when disallowed by " + "deferred_restarts").format( + ','.join(broken_units), + restart_package_svc) + raise Exception(msg) + else: + logging.info( + "Service was {} not restarted.".format(restart_package_svc)) self.check_show_deferred_restarts_wlm(restart_package_svc) self.check_show_deferred_events_action_restart( restart_package_svc, diff --git a/zaza/openstack/configure/network.py b/zaza/openstack/configure/network.py index 5d3a2f8..c0bbe1c 100755 --- a/zaza/openstack/configure/network.py +++ b/zaza/openstack/configure/network.py @@ -85,10 +85,11 @@ import sys from zaza.openstack.utilities import ( cli as cli_utils, generic as generic_utils, - juju as juju_utils, openstack as openstack_utils, ) +import zaza.utilities.juju as juju_utils + def setup_sdn(network_config, keystone_session=None): """Perform setup for Software Defined Network. diff --git a/zaza/openstack/utilities/exceptions.py b/zaza/openstack/utilities/exceptions.py index 7f73be4..c638c2d 100644 --- a/zaza/openstack/utilities/exceptions.py +++ b/zaza/openstack/utilities/exceptions.py @@ -208,3 +208,15 @@ class CACERTNotFound(Exception): """Could not find cacert.""" pass + + +class LoadBalancerUnexpectedState(Exception): + """The LoadBalancer is in a unexpected state.""" + + pass + + +class LoadBalancerUnrecoverableError(Exception): + """The LoadBalancer has reached to an unrecoverable error state.""" + + pass diff --git a/zaza/openstack/utilities/generic.py b/zaza/openstack/utilities/generic.py index be19c31..126ef82 100644 --- a/zaza/openstack/utilities/generic.py +++ b/zaza/openstack/utilities/generic.py @@ -622,7 +622,7 @@ def port_knock_units(units, port=22, expect_success=True): :returns: None if successful, Failure message otherwise """ for u in units: - host = u.public_address + host = model.get_unit_public_address(u) connected = is_port_open(port, host) if not connected and expect_success: return 'Socket connect failed.' @@ -723,3 +723,22 @@ def get_leaders_and_non_leaders(application_name): else: non_leaders.append(unit) return leader, non_leaders + + +def add_loop_device(unit, size=10): + """Add a loopback device to a Juju unit. + + :param unit: The unit name on which to create the device. + :type unit: str + + :param size: The size in GB of the device. + :type size: int + + :returns: The device name. + """ + loop_name = '/home/ubuntu/loop.img' + truncate = 'truncate --size {}GB {}'.format(size, loop_name) + losetup = 'losetup --find {}'.format(loop_name) + lofind = 'losetup -a | grep {} | cut -f1 -d ":"'.format(loop_name) + cmd = "sudo sh -c '{} && {} && {}'".format(truncate, losetup, lofind) + return model.run_on_unit(unit, cmd) diff --git a/zaza/openstack/utilities/juju.py b/zaza/openstack/utilities/juju.py index e4646a5..073a26f 100644 --- a/zaza/openstack/utilities/juju.py +++ b/zaza/openstack/utilities/juju.py @@ -17,9 +17,7 @@ import logging import functools -import subprocess -import zaza.model import zaza.utilities.juju @@ -312,67 +310,3 @@ def get_subordinate_units(unit_list, charm_name=None, status=None, charm_name=charm_name, status=status, model_name=model_name) - - -def add_storage(unit, label, pool, size): - """Add storage to a Juju unit. - - :param unit: The unit name (i.e: ceph-osd/0) - :type unit: str - - :param label: The storage label (i.e: osd-devices) - :type label: str - - :param pool: The pool on which to allocate the storage (i.e: cinder) - :type pool: str - - :size: The size in GB of the storage to attach. - :type size: int - - :returns: The name of the allocated storage. - """ - rv = subprocess.check_output(['juju', 'add-storage', unit, - '{}={},{}'.format(label, pool, - str(size) + 'GB')], - stderr=subprocess.STDOUT) - return rv.decode('UTF-8').replace('added storage ', '').split(' ')[0] - - -def detach_storage(storage_name): - """Detach previously allocated Juju storage.""" - subprocess.check_call(['juju', 'detach-storage', storage_name]) - - -def remove_storage(storage_name, force=False): - """Remove Juju storage. - - :param storage_name: The name of the previously allocated Juju storage. - :type storage_name: str - - :param force: If False (default), require that the storage be detached - before it can be removed. - :type force: bool - """ - cmd = ['juju', 'remove-storage', storage_name] - if force: - cmd.append('--force') - subprocess.check_call(cmd) - - -def add_loop_device(unit, size=10): - """Add a loopback device to a Juju unit. - - :param unit: The unit name on which to create the device. - :type unit: str - - :param size: The size in GB of the device. - :type size: int - - :returns: The device name. - """ - loop_name = '/home/ubuntu/loop.img' - truncate = 'truncate --size {}GB {}'.format(size, loop_name) - losetup = 'losetup --find {}'.format(loop_name) - lofind = 'losetup -a | grep {} | cut -f1 -d ":"'.format(loop_name) - cmd = "sudo sh -c '{} && {} && {}'".format(truncate, losetup, lofind) - return zaza.model.run_on_unit(unit, cmd) diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index 5359992..a12a1ac 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -1554,7 +1554,7 @@ def create_bgp_peer(neutron_client, peer_application_name='quagga', :rtype: dict """ peer_unit = model.get_units(peer_application_name)[0] - peer_ip = peer_unit.public_address + peer_ip = model.get_unit_public_address(peer_unit) bgp_peers = neutron_client.list_bgp_peers(name=peer_application_name) if len(bgp_peers['bgp_peers']) == 0: logging.info('Creating BGP Peer') @@ -2019,6 +2019,9 @@ def get_undercloud_auth(): def get_keystone_ip(model_name=None): """Return the IP address to use when communicating with keystone api. + If there are multiple VIP addresses specified in the 'vip' option for the + keystone unit, then ONLY the first one is returned. + :param model_name: Name of model to query. :type model_name: str :returns: IP address @@ -2029,9 +2032,10 @@ def get_keystone_ip(model_name=None): 'vip', model_name=model_name) if vip_option: - return vip_option + # strip the option, splits on whitespace and return the first one. + return vip_option.strip().split()[0] unit = model.get_units('keystone', model_name=model_name)[0] - return unit.public_address + return model.get_unit_public_address(unit) def get_keystone_api_version(model_name=None): @@ -2225,6 +2229,22 @@ def get_images_by_name(glance, image_name): return [i for i in glance.images.list() if image_name == i.name] +def get_volumes_by_name(cinder, volume_name): + """Get all cinder volume objects with the given name. + + :param cinder: Authenticated cinderclient + :type cinder: cinderclient.Client + :param image_name: Name of volume + :type image_name: str + :returns: List of cinder volumes + :rtype: List[cinderclient.v3.volume, ...] + """ + return [i for i in cinder.volumes.list() if volume_name == i.name] + + +@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), + reraise=True, + retry=tenacity.retry_if_exception_type(urllib.error.URLError)) def find_cirros_image(arch): """Return the url for the latest cirros image for the given architecture. @@ -2251,7 +2271,7 @@ def download_image(image_url, target_file): :param image_url: URL to download image from :type image_url: str - :param target_file: Local file to savee image to + :param target_file: Local file to save image to :type target_file: str """ opener = get_urllib_opener() diff --git a/zaza/openstack/utilities/openstack_upgrade.py b/zaza/openstack/utilities/openstack_upgrade.py index f75a05c..0451b8f 100755 --- a/zaza/openstack/utilities/openstack_upgrade.py +++ b/zaza/openstack/utilities/openstack_upgrade.py @@ -17,7 +17,7 @@ This module contains a number of functions for upgrading OpenStack. """ import logging -import zaza.openstack.utilities.juju as juju_utils +import zaza.utilities.juju as juju_utils import zaza.model from zaza import sync_wrapper diff --git a/zaza/openstack/utilities/os_versions.py b/zaza/openstack/utilities/os_versions.py index e67a97f..d69f9cf 100644 --- a/zaza/openstack/utilities/os_versions.py +++ b/zaza/openstack/utilities/os_versions.py @@ -278,3 +278,105 @@ PACKAGE_CODENAMES = { ('4', 'victoria'), ]), } + + +UBUNTU_RELEASES = ( + 'lucid', + 'maverick', + 'natty', + 'oneiric', + 'precise', + 'quantal', + 'raring', + 'saucy', + 'trusty', + 'utopic', + 'vivid', + 'wily', + 'xenial', + 'yakkety', + 'zesty', + 'artful', + 'bionic', + 'cosmic', + 'disco', + 'eoan', + 'focal', + 'groovy', + 'hirsute', + 'impish', +) + + +class BasicStringComparator(object): + """Provides a class that will compare strings from an iterator type object. + + Used to provide > and < comparisons on strings that may not necessarily be + alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the + z-wrap. + """ + + _list = None + + def __init__(self, item): + """Do init.""" + if self._list is None: + raise Exception("Must define the _list in the class definition!") + try: + self.index = self._list.index(item) + except Exception: + raise KeyError("Item '{}' is not in list '{}'" + .format(item, self._list)) + + def __eq__(self, other): + """Do equals.""" + assert isinstance(other, str) or isinstance(other, self.__class__) + return self.index == self._list.index(other) + + def __ne__(self, other): + """Do not equals.""" + return not self.__eq__(other) + + def __lt__(self, other): + """Do less than.""" + assert isinstance(other, str) or isinstance(other, self.__class__) + return self.index < self._list.index(other) + + def __ge__(self, other): + """Do greater than or equal.""" + return not self.__lt__(other) + + def __gt__(self, other): + """Do greater than.""" + assert isinstance(other, str) or isinstance(other, self.__class__) + return self.index > self._list.index(other) + + def __le__(self, other): + """Do less than or equals.""" + return not self.__gt__(other) + + def __str__(self): + """Give back the item at the index. + + This is so it can be used in comparisons like: + + s_mitaka = CompareOpenStack('mitaka') + s_newton = CompareOpenstack('newton') + + assert s_newton > s_mitaka + + :returns: + """ + return self._list[self.index] + + +class CompareHostReleases(BasicStringComparator): + """Provide comparisons of Ubuntu releases. + + Use in the form of + + if CompareHostReleases(release) > 'trusty': + # do something with mitaka + """ + + _list = UBUNTU_RELEASES diff --git a/zaza/openstack/utilities/swift.py b/zaza/openstack/utilities/swift.py index 2b1f46a..b7db584 100644 --- a/zaza/openstack/utilities/swift.py +++ b/zaza/openstack/utilities/swift.py @@ -207,7 +207,7 @@ def get_swift_storage_topology(model_name=None): region = app_config['storage-region']['value'] zone = app_config['zone']['value'] for unit in zaza.model.get_units(app_name, model_name=model_name): - topology[unit.public_address] = { + topology[zaza.model.get_unit_public_address(unit)] = { 'app_name': app_name, 'unit': unit, 'region': region, @@ -298,3 +298,69 @@ def create_object(swift_client, proxy_app, storage_topology, resource_prefix, storage_topology, model_name=model_name) return container_name, object_name, obj_replicas + + +def search_builder(proxy_app, ring, search_target, model_name=None): + """Run a swift-ring-builder search. + + :param proxy_app: Name of proxy application + :type proxy_app: str + :param ring: Name of ring (one of: object, account, container) + :type ring: str + :param search_target: device search string (see: man swift-ring-builder) + :type search_target: str + :param model_name: Model to point environment at + :type model_name: str + :returns: stdout - full stdout output from swift-ring-builder cmd + :rtype: str + """ + cmd = ('swift-ring-builder /etc/swift/{}.builder search {}' + ''.format(ring, search_target)) + result = zaza.model.run_on_leader(proxy_app, cmd, + model_name=model_name) + return result['Stdout'] + + +def is_proxy_ring_up_to_date(proxy_app, ring, model_name=None): + """Check if the ring file is up-to-date with changes of the builder. + + :param proxy_app: Name of proxy application + :type proxy_app: str + :param ring: Name of ring (one of: object, account, container) + :type ring: str + :param model_name: Model to point environment at + :type model_name: str + :returns: True if swift-ring-builder denotes ring.gz file is up-to-date + :rtype: str + """ + logging.info('Checking ring file matches builder file') + cmd = ('swift-ring-builder /etc/swift/{}.builder | ' + 'grep "Ring file .* is"'.format(ring)) + result = zaza.model.run_on_leader(proxy_app, cmd, model_name=model_name) + expected = ('Ring file /etc/swift/{}.ring.gz is up-to-date' + ''.format(ring)) + return bool(result['Stdout'].strip('\n') == expected) + + +def is_ring_synced(proxy_app, ring, expected_hosts, model_name=None): + """Check if md5sums of rings on swift-storage are synced to this proxy. + + :param proxy_app: Name of proxy application + :type proxy_app: str + :param ring: Name of ring (one of: object, account, container) + :type ring: str + :param expoected_hosts: Number of swift-storage hosts in test environment + :type search_target: int + :param model_name: Model to point environment at + :type model_name: str + :returns: True if all expected_hosts matched md5sum of proxy ring file + :rtype: bool + """ + logging.info('Checking ring md5sums on storage unit(s) against proxy') + zaza.model.block_until_all_units_idle() + cmd = ('swift-recon {} --md5 | ' + 'grep -A1 "ring md5" | tail -1'.format(ring)) + result = zaza.model.run_on_leader(proxy_app, cmd, model_name=model_name) + expected = ('{num}/{num} hosts matched, 0 error[s] while checking hosts.' + ''.format(num=expected_hosts)) + return bool(result['Stdout'].strip('\n') == expected) diff --git a/zaza/openstack/utilities/upgrade_utils.py b/zaza/openstack/utilities/upgrade_utils.py index ecc0b8b..3add346 100644 --- a/zaza/openstack/utilities/upgrade_utils.py +++ b/zaza/openstack/utilities/upgrade_utils.py @@ -24,6 +24,7 @@ from zaza.openstack.utilities.os_versions import ( OPENSTACK_CODENAMES, UBUNTU_OPENSTACK_RELEASE, OPENSTACK_RELEASES_PAIRS, + CompareHostReleases, ) """ @@ -48,7 +49,11 @@ SERVICE_GROUPS = ( 'nova-compute', 'ceph-osd', 'swift-proxy', 'swift-storage'])) -UPGRADE_EXCLUDE_LIST = ['rabbitmq-server', 'percona-cluster'] +UPGRADE_EXCLUDE_LIST = [ + 'rabbitmq-server', + 'percona-cluster', + 'glance-simplestreams-sync', +] def get_upgrade_candidates(model_name=None, filters=None): @@ -96,6 +101,25 @@ def _filter_openstack_upgrade_list(app, app_config, model_name=None): return False +def _make_filter_percona_cluster_at(target_series): + def _filter_percona_cluster(app, app_config, model_name=None): + charm_name = extract_charm_name_from_url(app_config['charm']) + if charm_name == "percona-cluster": + logging.warning( + "Excluding percona-cluster from upgrade, " + "as no candidate in %s", target_series) + return True + return False + + def _noop_filter(*args, **kwargs): + return False + + if target_series and CompareHostReleases(target_series) >= "focal": + return _filter_percona_cluster + + return _noop_filter + + def _filter_non_openstack_services(app, app_config, model_name=None): charm_options = zaza.model.get_application_config( app, model_name=model_name).keys() @@ -168,7 +192,8 @@ def get_upgrade_groups(model_name=None, extra_filters=None): return _build_service_groups(apps_in_model) -def get_series_upgrade_groups(model_name=None, extra_filters=None): +def get_series_upgrade_groups(model_name=None, extra_filters=None, + target_series=None): """Place apps in the model into their upgrade groups. Place apps in the model into their upgrade groups. If an app is deployed @@ -176,10 +201,17 @@ def get_series_upgrade_groups(model_name=None, extra_filters=None): :param model_name: Name of model to query. :type model_name: str + :param extra_filters: filters to apply to the upgrade groups + :type extra_filters: Callable + :param target_series: The series that will be series upgraded to. + :type target_series: Optional[str] :returns: List of tuples(group name, applications) :rtype: List[Tuple[str, Dict[str, ANY]]] """ - filters = [_filter_subordinates] + filters = [ + _filter_subordinates, + _make_filter_percona_cluster_at(target_series), + ] filters = _apply_extra_filters(filters, extra_filters) apps_in_model = get_upgrade_candidates( model_name=model_name,