From ecf5f1e1083b914dbd99976401156f9e3edf5194 Mon Sep 17 00:00:00 2001 From: hernandanielg Date: Mon, 26 Sep 2022 15:26:18 +0000 Subject: [PATCH 01/26] initial commit --- setup.py | 1 + .../charm_tests/cloudkitty/__init__.py | 17 +++++++ .../openstack/charm_tests/cloudkitty/setup.py | 26 ++++++++++ .../openstack/charm_tests/cloudkitty/tests.py | 47 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 zaza/openstack/charm_tests/cloudkitty/__init__.py create mode 100644 zaza/openstack/charm_tests/cloudkitty/setup.py create mode 100644 zaza/openstack/charm_tests/cloudkitty/tests.py diff --git a/setup.py b/setup.py index 6ae126b..9cf78b9 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ install_require = [ 'gnocchiclient>=7.0.5,<8.0.0', 'pika>=1.1.0,<2.0.0', 'python-barbicanclient>=4.0.1,<5.0.0', + 'python-cloudkittyclient', 'python-designateclient>=1.5,<3.0.0', 'python-heatclient<2.0.0', 'python-ironicclient', diff --git a/zaza/openstack/charm_tests/cloudkitty/__init__.py b/zaza/openstack/charm_tests/cloudkitty/__init__.py new file mode 100644 index 0000000..288ce37 --- /dev/null +++ b/zaza/openstack/charm_tests/cloudkitty/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +# Copyright 2019 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 cloudkitty.""" diff --git a/zaza/openstack/charm_tests/cloudkitty/setup.py b/zaza/openstack/charm_tests/cloudkitty/setup.py new file mode 100644 index 0000000..86e697f --- /dev/null +++ b/zaza/openstack/charm_tests/cloudkitty/setup.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# Copyright 2019 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. + +"""Code for configuring Cloudkitty.""" + + +def basic_setup(): + """Run setup for testing Cloudkitty. + + Setup for testing Cloudkitty is currently part of functional + tests. + """ + pass diff --git a/zaza/openstack/charm_tests/cloudkitty/tests.py b/zaza/openstack/charm_tests/cloudkitty/tests.py new file mode 100644 index 0000000..7f14151 --- /dev/null +++ b/zaza/openstack/charm_tests/cloudkitty/tests.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +# Copyright 2019 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 Cloudkitty testing.""" + +import logging + +import zaza.openstack.charm_tests.test_utils as test_utils +import zaza.openstack.utilities.openstack as openstack_utils + +from cloudkittyclient import client + + +class CloudkittyTest(test_utils.OpenStackBaseTest): + """Encapsulate Cloudkitty tests.""" + + CONF_FILE = '/etc/cloudkitty/cloudkitty.conf' + + @classmethod + def setUpClass(cls): + """Run class setup for running Cloudkitty tests.""" + super(CloudkittyTest, cls).setUpClass() + cls.current_release = openstack_utils.get_os_release() + + def test_400_api_connection(self): + """Simple api calls to check service is up and responding.""" + logging.info('Instantiating cloudkitty client...') + client_version = '1' + cloudkitty = client.Client( + client_version, + session=openstack_utils.get_overcloud_keystone_session() + ) + cloudkitty.report.get_summary() + pass From a6a540aebbce24bf5348d291115302dacfd2028a Mon Sep 17 00:00:00 2001 From: Samuel Walladge Date: Fri, 30 Sep 2022 08:50:53 +0930 Subject: [PATCH 02/26] Pin pyOpenSSL in setup.py It was pinned in requirements.txt, but this isn't picked up when installing this via pip. So, it needs to be pinned in the setup.py install_require list too. --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 6ae126b..8e963c1 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,11 @@ install_require = [ 'async_generator', 'boto3', + # pyopenssl depends on a newer version of cryptography since 22.1.0 + # TypeError: deprecated() got an unexpected keyword argument 'name' + # https://github.com/pyca/pyopenssl/commit/a145fc3bc6d2e943434beb2f04bbf9b18930296f + 'pyopenssl<22.1.0', + # Newer versions require a Rust compiler to build, see # * https://github.com/openstack-charmers/zaza/issues/421 # * https://mail.python.org/pipermail/cryptography-dev/2021-January/001003.html From 627e2d82b1d1a8341e9b7962032bbbb720ffbee0 Mon Sep 17 00:00:00 2001 From: hernandanielg Date: Fri, 30 Sep 2022 00:36:29 +0000 Subject: [PATCH 03/26] added module_enable test --- .../openstack/charm_tests/cloudkitty/tests.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/zaza/openstack/charm_tests/cloudkitty/tests.py b/zaza/openstack/charm_tests/cloudkitty/tests.py index 7f14151..9dc29f6 100644 --- a/zaza/openstack/charm_tests/cloudkitty/tests.py +++ b/zaza/openstack/charm_tests/cloudkitty/tests.py @@ -28,6 +28,7 @@ class CloudkittyTest(test_utils.OpenStackBaseTest): """Encapsulate Cloudkitty tests.""" CONF_FILE = '/etc/cloudkitty/cloudkitty.conf' + API_VERSION = '1' @classmethod def setUpClass(cls): @@ -35,13 +36,21 @@ class CloudkittyTest(test_utils.OpenStackBaseTest): super(CloudkittyTest, cls).setUpClass() cls.current_release = openstack_utils.get_os_release() + logging.info('Instantiating cloudkitty client...') + cls.cloudkitty = client.Client( + CloudkittyTest.API_VERSION, + session=cls.keystone_session + ) + def test_400_api_connection(self): """Simple api calls to check service is up and responding.""" - logging.info('Instantiating cloudkitty client...') - client_version = '1' - cloudkitty = client.Client( - client_version, - session=openstack_utils.get_overcloud_keystone_session() - ) - cloudkitty.report.get_summary() - pass + tenants_list = self.cloudkitty.report.get_tenants() + assert tenants_list == [] + + def test_401_module_enable(self): + """Test enabling hashmap module via API.""" + logging.info('Enabling hashmap module') + self.cloudkitty.rating.update_module(module_id='hashmap', enabled=True) + + hashmap = self.cloudkitty.rating.get_module(module_id='hashmap') + assert hashmap.get('enabled') From 84c58b44a42b761555a3e87243326194efcaba2d Mon Sep 17 00:00:00 2001 From: hernandanielg Date: Fri, 30 Sep 2022 06:05:37 +0000 Subject: [PATCH 04/26] added services field and mapping tests --- .../openstack/charm_tests/cloudkitty/tests.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/zaza/openstack/charm_tests/cloudkitty/tests.py b/zaza/openstack/charm_tests/cloudkitty/tests.py index 9dc29f6..ae22d00 100644 --- a/zaza/openstack/charm_tests/cloudkitty/tests.py +++ b/zaza/openstack/charm_tests/cloudkitty/tests.py @@ -50,7 +50,47 @@ class CloudkittyTest(test_utils.OpenStackBaseTest): def test_401_module_enable(self): """Test enabling hashmap module via API.""" logging.info('Enabling hashmap module') - self.cloudkitty.rating.update_module(module_id='hashmap', enabled=True) + rating = self.cloudkitty.rating + rating.update_module(module_id='hashmap', enabled=True) - hashmap = self.cloudkitty.rating.get_module(module_id='hashmap') + hashmap = rating.get_module(module_id='hashmap') assert hashmap.get('enabled') + + def test_402_service_create_and_delete(self): + """Test service create and delete via API.""" + hashmap = self.cloudkitty.rating.hashmap + service = hashmap.create_service(name='test') + + assert service['name'] == 'test' + + service_id = service['service_id'] + hashmap.delete_service(service_id=service_id) + + def test_403_field_create_and_delete(self): + """Test field create and delete via API.""" + hashmap = self.cloudkitty.rating.hashmap + service = hashmap.create_service(name='test') + + service_id = service['service_id'] + field = hashmap.create_field(name='test', service_id=service_id) + + field_id = field['field_id'] + hashmap.delete_field(field_id=field_id) + hashmap.delete_service(service_id=service_id) + + def test_404_mapping_create_and_delete(self): + """Test mapping create and delete via API.""" + hashmap = self.cloudkitty.rating.hashmap + service = hashmap.create_service(name='test') + + service_id = service['service_id'] + field = hashmap.create_field(name='test', service_id=service_id) + + field_id = field['field_id'] + mapping = hashmap.create_mapping( + type='flat', field_id=field_id, value='test', cost=0.1) + + mapping_id = mapping['mapping_id'] + hashmap.delete_mapping(mapping_id=mapping_id) + hashmap.delete_field(field_id=field_id) + hashmap.delete_service(service_id=service_id) From 8919e508c081019b745050b170b12c18686ea380 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Fri, 30 Sep 2022 17:08:48 +0000 Subject: [PATCH 05/26] Add jammy image to octavia ensure_lts_images() --- zaza/openstack/charm_tests/octavia/setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zaza/openstack/charm_tests/octavia/setup.py b/zaza/openstack/charm_tests/octavia/setup.py index 0fc1ba3..49c60d5 100644 --- a/zaza/openstack/charm_tests/octavia/setup.py +++ b/zaza/openstack/charm_tests/octavia/setup.py @@ -31,9 +31,10 @@ import zaza.openstack.charm_tests.nova.utils as nova_utils def ensure_lts_images(): - """Ensure that bionic and focal images are available for the tests.""" + """Ensure LTS images are available for the tests.""" glance_setup.add_lts_image(image_name='bionic', release='bionic') glance_setup.add_lts_image(image_name='focal', release='focal') + glance_setup.add_lts_image(image_name='jammy', release='jammy') def add_amphora_image(image_url=None): From ace6e6d70bb7ca1569b4440310b9d7a861e6409a Mon Sep 17 00:00:00 2001 From: hernandanielg Date: Fri, 30 Sep 2022 07:20:58 +0000 Subject: [PATCH 06/26] added group create and delete test --- .../openstack/charm_tests/cloudkitty/tests.py | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/zaza/openstack/charm_tests/cloudkitty/tests.py b/zaza/openstack/charm_tests/cloudkitty/tests.py index ae22d00..8e16f67 100644 --- a/zaza/openstack/charm_tests/cloudkitty/tests.py +++ b/zaza/openstack/charm_tests/cloudkitty/tests.py @@ -27,7 +27,6 @@ from cloudkittyclient import client class CloudkittyTest(test_utils.OpenStackBaseTest): """Encapsulate Cloudkitty tests.""" - CONF_FILE = '/etc/cloudkitty/cloudkitty.conf' API_VERSION = '1' @classmethod @@ -42,55 +41,77 @@ class CloudkittyTest(test_utils.OpenStackBaseTest): session=cls.keystone_session ) + def tearDown(self): + """Run teardown for test class.""" + rating = self.cloudkitty.rating + + if not rating.get_module(module_id='hashmap').get('enabled'): + rating.update_module(module_id='hashmap', enabled=True) + + hashmap = rating.hashmap + for service in hashmap.get_service().get('services'): + + service_id = service.get('service_id') + + fields = hashmap.get_field(service_id=service_id) + for field in fields.get('fields'): + hashmap.delete_field(field_id=field.get('field_id')) + + mappings = hashmap.get_mapping(service_id=service_id) + for mapping in mappings.get('mappings'): + hashmap.delete_mapping(mapping_id=mapping.get('mapping_id')) + + hashmap.delete_service(service_id=service_id) + + for group in hashmap.get_group().get('groups'): + hashmap.delete_group(group_id=group.get('group_id')) + def test_400_api_connection(self): """Simple api calls to check service is up and responding.""" - tenants_list = self.cloudkitty.report.get_tenants() + report = self.cloudkitty.report + tenants_list = report.get_tenants() assert tenants_list == [] - def test_401_module_enable(self): - """Test enabling hashmap module via API.""" - logging.info('Enabling hashmap module') + def test_401_module_enable_and_disable(self): + """Test enable and disable module via API.""" rating = self.cloudkitty.rating - rating.update_module(module_id='hashmap', enabled=True) + modules = rating.get_module() - hashmap = rating.get_module(module_id='hashmap') - assert hashmap.get('enabled') + for module in modules.get('modules'): + module_id = module.get('module_id') - def test_402_service_create_and_delete(self): - """Test service create and delete via API.""" - hashmap = self.cloudkitty.rating.hashmap - service = hashmap.create_service(name='test') + # noop module can't be disabled + if module_id == 'noop': + continue - assert service['name'] == 'test' + logging.info('Enabling {} module'.format(module_id)) + rating.update_module(module_id=module_id, enabled=True) + module = rating.get_module(module_id=module_id) + assert module.get('enabled') - service_id = service['service_id'] - hashmap.delete_service(service_id=service_id) + logging.info('Disabling {} module'.format(module_id)) + rating.update_module(module_id=module_id, enabled=False) + module = rating.get_module(module_id=module_id) + assert not module.get('enabled') - def test_403_field_create_and_delete(self): - """Test field create and delete via API.""" - hashmap = self.cloudkitty.rating.hashmap - service = hashmap.create_service(name='test') + def test_402_create_mapping(self): + """Test mapping create via API.""" + rating = self.cloudkitty.rating - service_id = service['service_id'] - field = hashmap.create_field(name='test', service_id=service_id) + if not rating.get_module(module_id='hashmap').get('enabled'): + rating.update_module(module_id='hashmap', enabled=True) - field_id = field['field_id'] - hashmap.delete_field(field_id=field_id) - hashmap.delete_service(service_id=service_id) + hashmap = rating.hashmap - def test_404_mapping_create_and_delete(self): - """Test mapping create and delete via API.""" - hashmap = self.cloudkitty.rating.hashmap - service = hashmap.create_service(name='test') + service = hashmap.create_service(name='test-service') + service_id = service.get('service_id') - service_id = service['service_id'] - field = hashmap.create_field(name='test', service_id=service_id) + field = hashmap.create_field(name='test-field', service_id=service_id) + field_id = field.get('field_id') - field_id = field['field_id'] - mapping = hashmap.create_mapping( - type='flat', field_id=field_id, value='test', cost=0.1) + group = hashmap.create_group(name='test-group') + group_id = group.get('group_id') - mapping_id = mapping['mapping_id'] - hashmap.delete_mapping(mapping_id=mapping_id) - hashmap.delete_field(field_id=field_id) - hashmap.delete_service(service_id=service_id) + hashmap.create_mapping( + type='flat', field_id=field_id, + group_id=group_id, value='test-value', cost=0.1) From 65cc55a5dbe50571ff99a704301381c1be3e5be7 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 3 Oct 2022 12:52:30 +0100 Subject: [PATCH 07/26] Remove liberty special case (#944) * Remove liberty special case * Update unit test --- unit_tests/utilities/test_zaza_utilities_openstack.py | 7 ------- zaza/openstack/utilities/openstack.py | 10 +--------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index 190437b..c380115 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -180,13 +180,6 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): network_msg) def test_get_keystone_scope(self): - self.patch_object(openstack_utils, "get_current_os_versions") - - # <= Liberty - self.get_current_os_versions.return_value = {"keystone": "liberty"} - self.assertEqual(openstack_utils.get_keystone_scope(), "DOMAIN") - # > Liberty - self.get_current_os_versions.return_value = {"keystone": "mitaka"} self.assertEqual(openstack_utils.get_keystone_scope(), "PROJECT") def _test_get_overcloud_auth(self, tls_relation=False, ssl_cert=False, diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index d9eb9bf..da6a0ef 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -487,15 +487,7 @@ def get_keystone_scope(model_name=None): :returns: String keystone scope :rtype: string """ - os_version = get_current_os_versions("keystone", - model_name=model_name)["keystone"] - # Keystone policy.json shipped the charm with liberty requires a domain - # scoped token. Bug #1649106 - if os_version == "liberty": - scope = "DOMAIN" - else: - scope = "PROJECT" - return scope + return "PROJECT" def get_keystone_session(openrc_creds, scope='PROJECT', verify=None): From 99186a6651a1c77087c270443a013659374ee40c Mon Sep 17 00:00:00 2001 From: Liam Young Date: Mon, 3 Oct 2022 18:27:56 +0100 Subject: [PATCH 08/26] Add k8s compatible method for keystone auth (#945) * Add k8s compatible method for keystone auth * Mock out is_k8s_deployment --- .../test_zaza_utilities_openstack.py | 2 + zaza/openstack/utilities/openstack.py | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index c380115..476152f 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -190,6 +190,8 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.patch_object(openstack_utils, "get_current_os_versions") self.patch_object(openstack_utils, "get_remote_ca_cert_file") self.patch_object(openstack_utils.juju_utils, 'leader_get') + self.patch_object(openstack_utils.juju_utils, 'is_k8s_deployment') + self.is_k8s_deployment.return_value = False if tls_relation: self.patch_object(openstack_utils.model, "scp_from_unit") self.patch_object(openstack_utils.model, "get_first_unit_name") diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index da6a0ef..16da37e 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -2094,6 +2094,56 @@ def get_keystone_api_version(model_name=None): def get_overcloud_auth(address=None, model_name=None): """Get overcloud OpenStack authentication from the environment. + :param model_name: Name of model to query. + :type model_name: str + :returns: Dictionary of authentication settings + :rtype: dict + """ + if juju_utils.is_k8s_deployment(): + return _get_overcloud_auth_k8s(address=address, model_name=None) + else: + return _get_overcloud_auth(address=address, model_name=None) + + +def _get_overcloud_auth_k8s(address=None, model_name=None): + """Get overcloud OpenStack authentication from the k8s environment. + + :param model_name: Name of model to query. + :type model_name: str + :returns: Dictionary of authentication settings + :rtype: dict + """ + logging.warning('Assuming http keystone endpoint') + transport = 'http' + port = 5000 + if not address: + address = zaza.model.get_status()[ + 'applications']['keystone'].public_address + address = network_utils.format_addr(address) + + # This is hard-coded in the charm at the moment + logging.warning('Using hardcoded keystone password') + password = 'abc123' + + # V3 or later + logging.info('Using keystone API V3 (or later) for overcloud auth') + auth_settings = { + 'OS_AUTH_URL': '%s://%s:%i/v3' % (transport, address, port), + 'OS_USERNAME': 'admin', + 'OS_PASSWORD': password, + 'OS_REGION_NAME': 'RegionOne', + 'OS_DOMAIN_NAME': 'admin_domain', + 'OS_USER_DOMAIN_NAME': 'admin_domain', + 'OS_PROJECT_NAME': 'admin', + 'OS_PROJECT_DOMAIN_NAME': 'admin_domain', + 'API_VERSION': 3, + } + return auth_settings + + +def _get_overcloud_auth(address=None, model_name=None): + """Get overcloud OpenStack authentication from the environment. + :param model_name: Name of model to query. :type model_name: str :returns: Dictionary of authentication settings From a55f320c2a4f2933b053e3749ffaff9e90389b5f Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Wed, 5 Oct 2022 09:34:18 -0300 Subject: [PATCH 09/26] Add test for keystone-openidc (#925) * Add keystone-openidc setup code. The keystone-openidc charm requires 2 configuration steps: 1) Configure the oidc-client-id, oidc-client-secret and oidc-provider-metadata-url, this information is tightly related to the Identity Provider configured, which for testing purposes this is the openidc-test-fixture charm, the setup function zaza.openstack.charm_tests.openidc.setup.configure_keystone_openidc takes care of setting these values once the fixture charm is ready for service. 2) Create the OpenStack objects to correctly configure the federation, this is made by the setup function zaza.openstack.charm_tests.openidc.setup.keystone_federation_setup_site1 which will create and configure the following resources: - Create a domain named 'federated_domain'. - Create a group named 'federated_users'. - Grant the 'Member' role to users in the 'federated_users' group. - Create an identity provider named 'openid'. - Create a mapping named 'openid_mapping'. - Create a federation protocol named 'openid' that relates the mapping and the identity provider. * Add support for v3oidcpassword auth plugin. get_keystone_session() uses the v3.OidcPassword class when the OS_AUTH_TYPE is set to v3oidcpassword, this class expects the following extra configuration options: - OS_IDENTITY_PROVIDER - OS_PROTOCOL - OS_CLIENT_ID - OS_CLIENT_SECRET - OS_ACCESS_TOKEN_ENDPOINT (optional) - OS_DISCOVERY_ENDPOINT (optional) * Add test for keystone-openidc This patch introduces a new testing class named CharmKeystoneOpenIDCTest which interacts with keystone using users provided by openidc-test-fixture via OpenID Connect. * Add keystone_session argument to launch instances. Adding the option to pass a keystone session allows callers to use credentials different from the ones provided by get_overcloud_keystone_session(), this is helpful when testing non default keystone configurations (e.g. Federation). * Add zaza.openstack.charm_tests.openidc.tests.TestLaunchInstance This testing class configures a private network in the user's project defined by the mapping rules during the setUpClass stage. Specifically this test performs the following steps: - Create keypair named 'zaza' in the user's project - Create a router for the project - Attach the router to the external network - Create a network - Create a subnet attached to the previously create network - Connect the subnet to the project's router The testing method launches an instance using a keystone session associated with a user backed by OpenID Connect. --- .../keystone_federation/__init__.py | 14 ++ .../charm_tests/keystone_federation/utils.py | 101 ++++++++++ .../openstack/charm_tests/openidc/__init__.py | 15 ++ zaza/openstack/charm_tests/openidc/setup.py | 178 ++++++++++++++++++ zaza/openstack/charm_tests/openidc/tests.py | 174 +++++++++++++++++ zaza/openstack/charm_tests/test_utils.py | 8 +- zaza/openstack/configure/guest.py | 9 +- zaza/openstack/utilities/openstack.py | 23 ++- 8 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 zaza/openstack/charm_tests/keystone_federation/__init__.py create mode 100644 zaza/openstack/charm_tests/keystone_federation/utils.py create mode 100644 zaza/openstack/charm_tests/openidc/__init__.py create mode 100644 zaza/openstack/charm_tests/openidc/setup.py create mode 100644 zaza/openstack/charm_tests/openidc/tests.py diff --git a/zaza/openstack/charm_tests/keystone_federation/__init__.py b/zaza/openstack/charm_tests/keystone_federation/__init__.py new file mode 100644 index 0000000..2efd6af --- /dev/null +++ b/zaza/openstack/charm_tests/keystone_federation/__init__.py @@ -0,0 +1,14 @@ +# 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 to setup Keystone Federation.""" diff --git a/zaza/openstack/charm_tests/keystone_federation/utils.py b/zaza/openstack/charm_tests/keystone_federation/utils.py new file mode 100644 index 0000000..12b5a10 --- /dev/null +++ b/zaza/openstack/charm_tests/keystone_federation/utils.py @@ -0,0 +1,101 @@ +# 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. +"""Code for setting up a Keystone Federation Provider.""" + +import json +import logging + +import keystoneauth1 + +from zaza.openstack.utilities import ( + cli as cli_utils, + openstack as openstack_utils, +) + + +def keystone_federation_setup(federated_domain: str, + federated_group: str, + idp_name: str, + idp_remote_id: str, + protocol_name: str, + map_template: str, + role_name: str = 'Member', + ): + """Configure Keystone Federation.""" + cli_utils.setup_logging() + keystone_session = openstack_utils.get_overcloud_keystone_session() + keystone_client = openstack_utils.get_keystone_session_client( + keystone_session) + + try: + domain = keystone_client.domains.find(name=federated_domain) + logging.info('Reusing domain %s with id %s', + federated_domain, domain.id) + except keystoneauth1.exceptions.http.NotFound: + logging.info('Creating domain %s', federated_domain) + domain = keystone_client.domains.create( + federated_domain, + description="Federated Domain", + enabled=True) + + try: + group = keystone_client.groups.find( + name=federated_group, domain=domain) + logging.info('Reusing group %s with id %s', federated_group, group.id) + except keystoneauth1.exceptions.http.NotFound: + logging.info('Creating group %s', federated_group) + group = keystone_client.groups.create( + federated_group, + domain=domain, + enabled=True) + + role = keystone_client.roles.find(name=role_name) + assert role is not None, 'Role %s not found' % role_name + logging.info('Granting %s role to group %s on domain %s', + role.name, group.name, domain.name) + keystone_client.roles.grant(role, group=group, domain=domain) + + try: + idp = keystone_client.federation.identity_providers.get(idp_name) + logging.info('Reusing identity provider %s with id %s', + idp_name, idp.id) + except keystoneauth1.exceptions.http.NotFound: + logging.info('Creating identity provider %s', idp_name) + idp = keystone_client.federation.identity_providers.create( + idp_name, + remote_ids=[idp_remote_id], + domain_id=domain.id, + enabled=True) + + JSON_RULES = json.loads(map_template.format( + domain_id=domain.id, group_id=group.id, role_name=role_name)) + + map_name = "{}_mapping".format(idp_name) + try: + keystone_client.federation.mappings.get(map_name) + logging.info('Reusing mapping %s', map_name) + except keystoneauth1.exceptions.http.NotFound: + logging.info('Creating mapping %s', map_name) + keystone_client.federation.mappings.create( + map_name, rules=JSON_RULES) + + try: + keystone_client.federation.protocols.get(idp_name, protocol_name) + logging.info('Reusing protocol %s from identity provider %s', + protocol_name, idp_name) + except keystoneauth1.exceptions.http.NotFound: + logging.info(('Creating protocol %s for identity provider %s with ' + 'mapping %s'), protocol_name, idp_name, map_name) + keystone_client.federation.protocols.create( + protocol_name, mapping=map_name, identity_provider=idp) diff --git a/zaza/openstack/charm_tests/openidc/__init__.py b/zaza/openstack/charm_tests/openidc/__init__.py new file mode 100644 index 0000000..6633926 --- /dev/null +++ b/zaza/openstack/charm_tests/openidc/__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 Keystone OpenID Connect.""" diff --git a/zaza/openstack/charm_tests/openidc/setup.py b/zaza/openstack/charm_tests/openidc/setup.py new file mode 100644 index 0000000..f8793ee --- /dev/null +++ b/zaza/openstack/charm_tests/openidc/setup.py @@ -0,0 +1,178 @@ +# 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. + +"""Code for setting up Keystone OpenID Connect federation.""" + +import logging + +import zaza.model + +from zaza.charm_lifecycle import utils as lifecycle_utils +from zaza.openstack.charm_tests.keystone_federation.utils import ( + keystone_federation_setup, +) +from zaza.openstack.utilities import ( + cli as cli_utils, + openstack as openstack_utils, +) + +APP_NAME = 'keystone-openidc' +FEDERATED_DOMAIN = "federated_domain" +FEDERATED_GROUP = "federated_users" +MEMBER = "Member" +IDP = "openid" +LOCAL_IDP_REMOTE_ID = 'https://{}:8443/realms/demorealm' +REMOTE_ID = "http://openidc" +PROTOCOL_NAME = "openid" +MAP_TEMPLATE = ''' +[{{ + "local": [ + {{ + "user": {{ + "name": "{{1}}", + "email": "{{2}}" + }}, + "group": {{ + "name": "{group_id}", + "domain": {{ + "id": "{domain_id}" + }} + }}, + "projects": [ + {{ + "name": "{{1}}_project", + "roles": [ + {{ + "name": "{role_name}" + }} + ] + }} + ] + }} + ], + "remote": [ + {{ + "type": "HTTP_OIDC_SUB" + }}, + {{ + "type": "HTTP_OIDC_USERNAME" + }}, + {{ + "type": "HTTP_OIDC_EMAIL" + }} + ] +}}] +''' +REQUIRED_KEYS_MSG = 'required keys: oidc_client_id, oidc_provider_metadata_url' +# Default objects created by openidc-test-fixture charm +DEFAULT_CLIENT_ID = 'keystone' +DEFAULT_CLIENT_SECRET = 'ubuntu11' +DEFAULT_REALM = 'demorealm' +OPENIDC_TEST_FIXTURE = 'openidc-test-fixture' # app's name + + +# NOTE(freyes): workaround for bug http://pad.lv/1982948 +def relate_keystone_openidc(): + """Add relation between keystone and keystone-openidc. + + .. note: This is a workaround for the bug http://pad.lv/1982948 + """ + cli_utils.setup_logging() + relations_added = False + if not zaza.model.get_relation_id(APP_NAME, 'keystone'): + logging.info('Adding relation keystone-openidc -> keystone') + zaza.model.add_relation(APP_NAME, + 'keystone-fid-service-provider', + 'keystone:keystone-fid-service-provider') + relations_added = True + if not zaza.model.get_relation_id(APP_NAME, 'openstack-dashboard'): + logging.info('Adding relation keystone-openidc -> openstack-dashboard') + zaza.model.add_relation( + APP_NAME, + 'websso-fid-service-provider', + 'openstack-dashboard:websso-fid-service-provider' + ) + relations_added = True + + if relations_added: + zaza.model.wait_for_agent_status() + + # NOTE: the test bundle has been deployed with a non-related + # keystone-opendic subordinate application, and thus Zaza is expecting no + # unit from this application. We are now relating it to a principal + # keystone application with 3 units. We now need to make sure we wait for + # the units to get fully deployed before proceeding: + test_config = lifecycle_utils.get_charm_config(fatal=False) + target_deploy_status = test_config.get('target_deploy_status', {}) + try: + # this is a HA deployment + target_deploy_status['keystone-openidc']['num-expected-units'] = 3 + opts = { + 'workload-status-message-prefix': REQUIRED_KEYS_MSG, + 'workload-status': 'blocked', + } + target_deploy_status['keystone-openidc'].update(opts) + except KeyError: + # num-expected-units wasn't set to 0, no expectation to be + # fixed, let's move on. + pass + + zaza.model.wait_for_application_states( + states=target_deploy_status) + + +def configure_keystone_openidc(): + """Configure OpenIDC testing fixture certificate.""" + units = zaza.model.get_units(OPENIDC_TEST_FIXTURE) + assert len(units) > 0, 'openidc-test-fixture units not found' + ip = zaza.model.get_unit_public_address(units[0]) + url = 'https://{ip}:8443/realms/{realm}/.well-known/openid-configuration' + cfg = {'oidc-client-id': DEFAULT_CLIENT_ID, + 'oidc-client-secret': DEFAULT_CLIENT_SECRET, + 'oidc-provider-metadata-url': url.format(ip=ip, + realm=DEFAULT_REALM)} + zaza.model.set_application_config(APP_NAME, cfg) + zaza.model.wait_for_agent_status() + test_config = lifecycle_utils.get_charm_config(fatal=False) + target_deploy_status = test_config.get('target_deploy_status', {}) + target_deploy_status.update({ + 'keystone-openidc': { + 'workload-status': 'active', + 'workload-status-message': 'Unit is ready' + }, + }) + zaza.model.wait_for_application_states(states=target_deploy_status) + + +def keystone_federation_setup_site1(): + """Configure Keystone Federation for the local IdP #1.""" + idp_unit = zaza.model.get_units("openidc-test-fixture")[0] + idp_remote_id = LOCAL_IDP_REMOTE_ID.format( + zaza.model.get_unit_public_address(idp_unit)) + + keystone_session = openstack_utils.get_overcloud_keystone_session() + keystone_client = openstack_utils.get_keystone_session_client( + keystone_session) + role = keystone_client.roles.find(name=MEMBER) + logging.info('Using role name %s with id %s', role.name, role.id) + + keystone_federation_setup( + federated_domain=FEDERATED_DOMAIN, + federated_group=FEDERATED_GROUP, + idp_name=IDP, + idp_remote_id=idp_remote_id, + protocol_name=PROTOCOL_NAME, + map_template=MAP_TEMPLATE, + role_name=role.name, + ) diff --git a/zaza/openstack/charm_tests/openidc/tests.py b/zaza/openstack/charm_tests/openidc/tests.py new file mode 100644 index 0000000..afa20f0 --- /dev/null +++ b/zaza/openstack/charm_tests/openidc/tests.py @@ -0,0 +1,174 @@ +# 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. + +"""Keystone OpenID Connect Testing.""" +import copy +import logging +import pprint + +import zaza.model + +from zaza.openstack.charm_tests.glance.setup import CIRROS_IMAGE_NAME +from zaza.openstack.charm_tests.keystone import BaseKeystoneTest +from zaza.openstack.charm_tests.neutron.setup import ( + OVERCLOUD_NETWORK_CONFIG, + DEFAULT_UNDERCLOUD_NETWORK_CONFIG, +) +from zaza.openstack.charm_tests.nova.setup import manage_ssh_key +from zaza.openstack.charm_tests.openidc.setup import ( + FEDERATED_DOMAIN, + IDP, + PROTOCOL_NAME, +) +from zaza.openstack.utilities import ( + generic as generic_utils, + openstack as openstack_utils, +) + +# static users created by openidc-test-fixture charm +OIDC_TEST_USER = 'johndoe' +OIDC_TEST_USER_PASSWORD = 'f00bar' + + +class BaseCharmKeystoneOpenIDC(BaseKeystoneTest): + """Charm Keystone OpenID Connect tests.""" + + run_resource_cleanup = True + RESOURCE_PREFIX = 'zaza-openidc' + + @classmethod + def setUpClass(cls): + """Define openrc credentials for OIDC_TEST_USER.""" + super().setUpClass() + charm_config = zaza.model.get_application_config('keystone-openidc') + client_id = charm_config['oidc-client-id']['value'] + client_secret = charm_config['oidc-client-secret']['value'] + metadata_url = charm_config['oidc-provider-metadata-url']['value'] + cls.oidc_test_openrc = { + 'API_VERSION': 3, + 'OS_USERNAME': OIDC_TEST_USER, + 'OS_PASSWORD': OIDC_TEST_USER_PASSWORD, + # using the first keystone ip by default, for environments with + # HA+TLS enabled this is the virtual IP, otherwise it will be one + # of the keystone units. + 'OS_AUTH_URL': 'https://{}:5000/v3'.format(cls.keystone_ips[0]), + 'OS_PROJECT_DOMAIN_NAME': FEDERATED_DOMAIN, + 'OS_PROJECT_NAME': '{}_project'.format(OIDC_TEST_USER), + 'OS_CACERT': openstack_utils.get_cacert(), + # openid specific info + 'OS_AUTH_TYPE': 'v3oidcpassword', + 'OS_DISCOVERY_ENDPOINT': metadata_url, + 'OS_OPENID_SCOPE': 'openid email profile', + 'OS_CLIENT_ID': client_id, + 'OS_CLIENT_SECRET': client_secret, + 'OS_IDENTITY_PROVIDER': IDP, + 'OS_PROTOCOL': PROTOCOL_NAME, + } + logging.info('openrc: %s', pprint.pformat(cls.oidc_test_openrc)) + + +class TestToken(BaseCharmKeystoneOpenIDC): + """Test tokens for user's backed by OpenID Connect via Federation.""" + + def test_token_issue(self): + """Test token issue with a federated user via openidc.""" + openrc = copy.deepcopy(self.oidc_test_openrc) + with self.v3_keystone_preferred(): + for ip in self.keystone_ips: + logging.info('keystone IP %s', ip) + openrc['AUTH_URL'] = 'https://{}:5000/v3'.format(ip) + keystone_session = openstack_utils.get_keystone_session( + openrc, scope='PROJECT') + logging.info('Retrieving token for federated user') + token = keystone_session.get_token() + logging.info('Token: %s', token) + self.assertIsNotNone(token) + logging.info('OK') + + +class TestLaunchInstance(BaseCharmKeystoneOpenIDC): + """Test instance launching in a project defined by Federation mapping.""" + + @classmethod + def setUpClass(cls): + """Configure user's project network backed by OpenID Connect.""" + super().setUpClass() + # Get network configuration settings + network_config = {"private_net_cidr": "192.168.21.0/24"} + # Declared overcloud settings + network_config.update(OVERCLOUD_NETWORK_CONFIG) + # Default undercloud settings + network_config.update(DEFAULT_UNDERCLOUD_NETWORK_CONFIG) + # Environment specific settings + network_config.update(generic_utils.get_undercloud_env_vars()) + ip_version = network_config.get("ip_version") or 4 + + keystone_session = openstack_utils.get_keystone_session( + cls.oidc_test_openrc, scope='PROJECT') + # find user's project id + project_id = keystone_session.get_project_id() + + # Get authenticated clients + neutron_client = openstack_utils.get_neutron_session_client( + keystone_session) + nova_client = openstack_utils.get_nova_session_client( + keystone_session) + + # create 'zaza' key in user's project + manage_ssh_key(nova_client) + + # create a router attached to the external network + ext_net_name = network_config["external_net_name"] + networks = neutron_client.list_networks(name=ext_net_name) + ext_network = networks['networks'][0] + provider_router = openstack_utils.create_provider_router( + neutron_client, project_id) + openstack_utils.plug_extnet_into_router( + neutron_client, + provider_router, + ext_network) + + # create project's private network + project_network = openstack_utils.create_project_network( + neutron_client, + project_id, + shared=False, + network_type=network_config["network_type"], + net_name=network_config["project_net_name"]) + project_subnet = openstack_utils.create_project_subnet( + neutron_client, + project_id, + project_network, + network_config["private_net_cidr"], + ip_version=ip_version, + subnet_name=network_config["project_subnet_name"]) + openstack_utils.update_subnet_dns( + neutron_client, + project_subnet, + network_config["external_dns"]) + openstack_utils.plug_subnet_into_router( + neutron_client, + provider_router['name'], + project_network, + project_subnet) + openstack_utils.add_neutron_secgroup_rules(neutron_client, project_id) + + def test_20_launch_instance(self): + """Test launching an instance in a project defined by mapping rules.""" + keystone_session = openstack_utils.get_keystone_session( + self.oidc_test_openrc, scope='PROJECT') + + self.launch_guest('test-42', + instance_key=CIRROS_IMAGE_NAME, + keystone_session=keystone_session) diff --git a/zaza/openstack/charm_tests/test_utils.py b/zaza/openstack/charm_tests/test_utils.py index 7fa2261..ff96da1 100644 --- a/zaza/openstack/charm_tests/test_utils.py +++ b/zaza/openstack/charm_tests/test_utils.py @@ -752,7 +752,8 @@ class OpenStackBaseTest(BaseCharmTest): def launch_guest(self, guest_name, userdata=None, use_boot_volume=False, instance_key=None, flavor_name=None, - attach_to_external_network=False): + attach_to_external_network=False, + keystone_session=None): """Launch one guest to use in tests. Note that it is up to the caller to have set the RESOURCE_PREFIX class @@ -772,6 +773,8 @@ class OpenStackBaseTest(BaseCharmTest): :param attach_to_external_network: Attach instance directly to external network. :type attach_to_external_network: bool + :param keystone_session: Keystone session to use. + :type keystone_session: Optional[keystoneauth1.session.Session] :returns: Nova instance objects :rtype: Server """ @@ -801,7 +804,8 @@ class OpenStackBaseTest(BaseCharmTest): use_boot_volume=use_boot_volume, userdata=userdata, flavor_name=flavor_name, - attach_to_external_network=attach_to_external_network) + attach_to_external_network=attach_to_external_network, + keystone_session=keystone_session) def launch_guests(self, userdata=None, attach_to_external_network=False, flavor_name=None): diff --git a/zaza/openstack/configure/guest.py b/zaza/openstack/configure/guest.py index 1c1e929..1b09dfd 100644 --- a/zaza/openstack/configure/guest.py +++ b/zaza/openstack/configure/guest.py @@ -94,7 +94,8 @@ def launch_instance_retryer(instance_key, **kwargs): def launch_instance(instance_key, use_boot_volume=False, vm_name=None, private_network_name=None, image_name=None, flavor_name=None, external_network_name=None, meta=None, - userdata=None, attach_to_external_network=False): + userdata=None, attach_to_external_network=False, + keystone_session=None): """Launch an instance. :param instance_key: Key to collect associated config data with. @@ -120,10 +121,14 @@ def launch_instance(instance_key, use_boot_volume=False, vm_name=None, :param attach_to_external_network: Attach instance directly to external network. :type attach_to_external_network: bool + :param keystone_session: Keystone session to use. + :type keystone_session: Optional[keystoneauth1.session.Session] :returns: the created instance :rtype: novaclient.Server """ - keystone_session = openstack_utils.get_overcloud_keystone_session() + if not keystone_session: + keystone_session = openstack_utils.get_overcloud_keystone_session() + nova_client = openstack_utils.get_nova_session_client(keystone_session) neutron_client = openstack_utils.get_neutron_session_client( keystone_session) diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index 16da37e..d8c03c6 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -299,10 +299,26 @@ def get_ks_creds(cloud_creds, scope='PROJECT'): 'username': cloud_creds['OS_USERNAME'], 'password': cloud_creds['OS_PASSWORD'], 'auth_url': cloud_creds['OS_AUTH_URL'], - 'user_domain_name': cloud_creds['OS_USER_DOMAIN_NAME'], 'project_domain_name': cloud_creds['OS_PROJECT_DOMAIN_NAME'], 'project_name': cloud_creds['OS_PROJECT_NAME'], } + # the FederationBaseAuth class doesn't support the + # 'user_domain_name' argument, so only setting it in the 'auth' + # dict when it's passed in the cloud_creds. + if cloud_creds.get('OS_USER_DOMAIN_NAME'): + auth['user_domain_name'] = cloud_creds['OS_USER_DOMAIN_NAME'] + + if cloud_creds.get('OS_AUTH_TYPE') == 'v3oidcpassword': + auth.update({ + 'identity_provider': cloud_creds['OS_IDENTITY_PROVIDER'], + 'protocol': cloud_creds['OS_PROTOCOL'], + 'client_id': cloud_creds['OS_CLIENT_ID'], + 'client_secret': cloud_creds['OS_CLIENT_SECRET'], + # optional configuration options: + 'access_token_endpoint': cloud_creds.get( + 'OS_ACCESS_TOKEN_ENDPOINT'), + 'discovery_endpoint': cloud_creds.get('OS_DISCOVERY_ENDPOINT') + }) return auth @@ -511,7 +527,10 @@ def get_keystone_session(openrc_creds, scope='PROJECT', verify=None): if openrc_creds.get('API_VERSION', 2) == 2: auth = v2.Password(**keystone_creds) else: - auth = v3.Password(**keystone_creds) + if openrc_creds.get('OS_AUTH_TYPE') == 'v3oidcpassword': + auth = v3.OidcPassword(**keystone_creds) + else: + auth = v3.Password(**keystone_creds) return session.Session(auth=auth, verify=verify) From 98faf678465c805fb754ac1f4d31e4d2b4363eed Mon Sep 17 00:00:00 2001 From: Guilherme Maluf Balzana Date: Fri, 7 Oct 2022 20:35:36 +0200 Subject: [PATCH 10/26] Add glance-simplestreams-sync set_latest_property config functest (#853) When creating Openstack VMs the user has to specify the image it wants to use. sstream-mirror-glance adds a date to the image name, so they always have to recheck which is the current latest image. This commit tests the usage of the `set_latest_property` configuration. When --set-latest-property is given sstream-mirror-glance will set the recently synced image with the `latest=true` property and then remove the `latest` property from all the os_version/architecture matching images. Closes-bug: LP #1933130 --- .../glance_simplestreams_sync/setup.py | 15 ++++++++++++++ .../glance_simplestreams_sync/tests.py | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/zaza/openstack/charm_tests/glance_simplestreams_sync/setup.py b/zaza/openstack/charm_tests/glance_simplestreams_sync/setup.py index 96648af..b728d3c 100644 --- a/zaza/openstack/charm_tests/glance_simplestreams_sync/setup.py +++ b/zaza/openstack/charm_tests/glance_simplestreams_sync/setup.py @@ -21,6 +21,7 @@ import tenacity import pprint import zaza.model as zaza_model +import zaza.charm_lifecycle.utils as lifecycle_utils import zaza.openstack.utilities.generic as generic_utils import zaza.openstack.utilities.openstack as openstack_utils @@ -77,3 +78,17 @@ def sync_images(): logging.info('Contents of Keystone service catalog: "{}"' .format(pprint.pformat(catalog))) raise + + +def set_latest_property_config(): + """Enable set_latest_property config. + + This config adds `latest=true` to new synced images. + """ + logging.info("Change config `set_latest_property=true`") + zaza_model.set_application_config('glance-simplestreams-sync', + {'set_latest_property': 'true', + 'snap-channel': 'edge'}) + test_config = lifecycle_utils.get_charm_config(fatal=False) + zaza_model.wait_for_application_states( + states=test_config.get('target_deploy_status', {})) diff --git a/zaza/openstack/charm_tests/glance_simplestreams_sync/tests.py b/zaza/openstack/charm_tests/glance_simplestreams_sync/tests.py index 0c0d472..c19339f 100644 --- a/zaza/openstack/charm_tests/glance_simplestreams_sync/tests.py +++ b/zaza/openstack/charm_tests/glance_simplestreams_sync/tests.py @@ -125,3 +125,23 @@ class GlanceSimpleStreamsSyncTest(test_utils.OpenStackBaseTest): _check_local_product_streams(expected_images) logging.debug("Local product stream successful") + + +class GlanceSimpleStreamsSyncWithPropertiesTest(GlanceSimpleStreamsSyncTest): + """Glance Simple Streams Sync Test with Image property. + + `setup.py:set_latest_property_config()` is required by this test and it is + called during charm-glance-simplestreams-sync/tests/tests.yaml:configure + phase. + """ + + # TODO(guimalufb) test if the latest property gets removed from old images + def test_200_check_image_latest_property(self): + """Verify that images had metadata property set.""" + logging.debug("Checking images with latest=true property...") + + filter_properties = {'filters': {'latest': 'true'}} + images = self.glance_client.images.list(**filter_properties) + self.assertTrue(len(list(images)) > 0, + "'latest=true' property not found in glance images" + " list") From bab65e8c2eb537a16f05e015e22c2b36b13bf890 Mon Sep 17 00:00:00 2001 From: James Page Date: Tue, 11 Oct 2022 11:43:01 +0100 Subject: [PATCH 11/26] k8s: use action to retrieve admin password The Keystone K8S operator now uses generated passwords; make use of the helper action to retrieve the admin password. --- zaza/openstack/utilities/openstack.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index d8c03c6..4cd403a 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -2140,9 +2140,13 @@ def _get_overcloud_auth_k8s(address=None, model_name=None): 'applications']['keystone'].public_address address = network_utils.format_addr(address) - # This is hard-coded in the charm at the moment - logging.warning('Using hardcoded keystone password') - password = 'abc123' + logging.info('Retrieving admin password from keystone') + action = zaza.model.run_action_on_leader( + 'keystone', + 'get-admin-password', + action_params={} + ) + password = action.data['results']['password'] # V3 or later logging.info('Using keystone API V3 (or later) for overcloud auth') From 98a1c8948aa121c1be6db68407d014113b8193b4 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Wed, 12 Oct 2022 14:34:14 +0000 Subject: [PATCH 12/26] [ceph] remove white-box testing migrated from amulet (#951) --- zaza/openstack/charm_tests/ceph/tests.py | 44 ------------------------ 1 file changed, 44 deletions(-) diff --git a/zaza/openstack/charm_tests/ceph/tests.py b/zaza/openstack/charm_tests/ceph/tests.py index 68662b7..29a74e0 100644 --- a/zaza/openstack/charm_tests/ceph/tests.py +++ b/zaza/openstack/charm_tests/ceph/tests.py @@ -141,50 +141,6 @@ class CephRelationTest(test_utils.OpenStackBaseTest): # The private address in relation should match ceph-mon/0 address self.assertEqual(rel_private_ip, remote_ip) - def _ceph_to_ceph_osd_relation(self, remote_unit_name): - """Verify the cephX to ceph-osd relation data. - - Helper function to test the relation. - """ - logging.info('Checking {}:ceph-osd mon relation data...'. - format(remote_unit_name)) - unit_name = 'ceph-osd/0' - relation_name = 'osd' - remote_unit = zaza_model.get_unit_from_name(remote_unit_name) - 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() - expected = { - 'private-address': remote_ip, - 'ceph-public-address': remote_ip, - 'fsid': fsid, - } - relation = juju_utils.get_relation_from_unit( - unit_name, - remote_unit_name, - relation_name - ) - for e_key, e_value in expected.items(): - a_value = relation[e_key] - self.assertEqual(e_value, a_value) - self.assertTrue(relation['osd_bootstrap_key'] is not None) - - def test_ceph0_to_ceph_osd_relation(self): - """Verify the ceph0 to ceph-osd relation data.""" - remote_unit_name = 'ceph-mon/0' - self._ceph_to_ceph_osd_relation(remote_unit_name) - - def test_ceph1_to_ceph_osd_relation(self): - """Verify the ceph1 to ceph-osd relation data.""" - remote_unit_name = 'ceph-mon/1' - self._ceph_to_ceph_osd_relation(remote_unit_name) - - def test_ceph2_to_ceph_osd_relation(self): - """Verify the ceph2 to ceph-osd relation data.""" - remote_unit_name = 'ceph-mon/2' - self._ceph_to_ceph_osd_relation(remote_unit_name) - class CephTest(test_utils.OpenStackBaseTest): """Ceph common functional tests.""" From 89fd74c5dbce066e3eb10aebc8717a1d823cd155 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Fri, 14 Oct 2022 12:59:26 +0100 Subject: [PATCH 13/26] Add method to check OpenStack endpoints (#953) * Add method to check OpenStack endpoints Add method to check OpenStack endpoints are returning acceptable http codes. This should be used with caution as a charm whould indicate if its payload is not ready via workload status and workload status messages * Fix dox string --- zaza/openstack/charm_tests/keystone/setup.py | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/zaza/openstack/charm_tests/keystone/setup.py b/zaza/openstack/charm_tests/keystone/setup.py index 73264cd..22d6143 100644 --- a/zaza/openstack/charm_tests/keystone/setup.py +++ b/zaza/openstack/charm_tests/keystone/setup.py @@ -15,6 +15,8 @@ """Code for setting up keystone.""" import logging +import requests +import tenacity import keystoneauth1 @@ -167,3 +169,43 @@ def add_tempest_roles(): :rtype: None """ _add_additional_roles(TEMPEST_ROLES) + + +def wait_for_url(url, ok_codes=None): + """Wait for url to return acceptable return code. + + :param url: url to test + :type url: str + :param ok_codes: HTTP codes that are acceptable + :type ok_codes: Optional[List[int]] + :raises: AssertionError + """ + if not ok_codes: + ok_codes = [requests.codes.ok] + for attempt in tenacity.Retrying( + stop=tenacity.stop_after_attempt(10), + wait=tenacity.wait_exponential( + multiplier=1, min=2, max=60)): + with attempt: + r = requests.get(url) + logging.info("{} returned {}".format(url, r.status_code)) + assert r.status_code in ok_codes + + +def wait_for_all_endpoints(interface='public'): + """Check all endpoints are returning an acceptable return code. + + :param interface: Endpoint type to check. public, admin or internal + :type interface: str + :raises: AssertionError + """ + overcloud_auth = openstack_utils.get_overcloud_auth() + wait_for_url(overcloud_auth['OS_AUTH_URL']) + session = openstack_utils.get_overcloud_keystone_session() + keystone_client = openstack_utils.get_keystone_session_client(session) + for service in keystone_client.services.list(): + for ep in keystone_client.endpoints.list(service=service, + interface=interface): + wait_for_url( + ep.url, + [requests.codes.ok, requests.codes.multiple_choices]) From 9eac7470a7665837646bd74f51672f2b30a1c777 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Tue, 18 Oct 2022 19:25:36 +0000 Subject: [PATCH 14/26] Update masakari test to launch jammy instance --- zaza/openstack/charm_tests/masakari/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zaza/openstack/charm_tests/masakari/tests.py b/zaza/openstack/charm_tests/masakari/tests.py index 6f27d17..1170e9c 100644 --- a/zaza/openstack/charm_tests/masakari/tests.py +++ b/zaza/openstack/charm_tests/masakari/tests.py @@ -71,7 +71,7 @@ class MasakariTest(test_utils.OpenStackBaseTest): except novaclient.exceptions.NotFound: logging.info('Launching new guest') guest = zaza.openstack.configure.guest.launch_instance( - 'bionic', + 'jammy', use_boot_volume=True, meta={'HA_Enabled': 'True'}, vm_name=vm_name) From 827d81fc12201e525d6d57d2908ee9be71f02649 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Thu, 20 Oct 2022 11:18:04 +0100 Subject: [PATCH 15/26] Allow tempest to run in minimal mode (#956) Add test class that runs tempest in miminal mode. This is useful for testing that the OpenStack apis are responding but there are components not configured like an external network. In addtion some missing doc strings were added and support for neutron tempest configuration pre ussuri removed as there is a branch for that. --- zaza/openstack/charm_tests/tempest/tests.py | 17 +++ zaza/openstack/charm_tests/tempest/utils.py | 113 ++++++++++++-------- 2 files changed, 88 insertions(+), 42 deletions(-) diff --git a/zaza/openstack/charm_tests/tempest/tests.py b/zaza/openstack/charm_tests/tempest/tests.py index b7fc859..da8078b 100644 --- a/zaza/openstack/charm_tests/tempest/tests.py +++ b/zaza/openstack/charm_tests/tempest/tests.py @@ -122,6 +122,23 @@ class TempestTestWithKeystoneV3(TempestTestBase): return super().run() +class TempestTestWithKeystoneMinimal(TempestTestBase): + """Tempest test class to validate an OpenStack setup with Keystone V2.""" + + def run(self): + """Run tempest tests as specified in tests/tests.yaml. + + Allow test to run even if some components are missing (like + external network setup). + See TempestTestBase.run() for the available test options. + + :returns: Status of tempest run + :rtype: bool + """ + tempest_utils.render_tempest_config_keystone_v3(minimal=True) + return super().run() + + class TempestTest(TempestTestBase): """Tempest test class. diff --git a/zaza/openstack/charm_tests/tempest/utils.py b/zaza/openstack/charm_tests/tempest/utils.py index fa6c051..7463e11 100644 --- a/zaza/openstack/charm_tests/tempest/utils.py +++ b/zaza/openstack/charm_tests/tempest/utils.py @@ -21,6 +21,8 @@ import shutil import subprocess import urllib.parse +from neutronclient.common import exceptions as neutronexceptions + import zaza.model as model import zaza.utilities.deployment_env as deployment_env import zaza.openstack.utilities.juju as juju_utils @@ -51,13 +53,18 @@ def render_tempest_config_keystone_v2(): _setup_tempest('tempest_v2.j2', 'accounts.j2') -def render_tempest_config_keystone_v3(): +def render_tempest_config_keystone_v3(minimal=False): """Render tempest config for Keystone V3 API. + :param minimal: Run in minimal mode eg ignore missing setup + :type minimal: bool :returns: None :rtype: None """ - _setup_tempest('tempest_v3.j2', 'accounts.j2') + _setup_tempest( + 'tempest_v3.j2', + 'accounts.j2', + minimal=minimal) def get_workspace(): @@ -105,20 +112,22 @@ def _init_workspace(workspace_path): pass -def _setup_tempest(tempest_template, accounts_template): +def _setup_tempest(tempest_template, accounts_template, minimal=False): """Initialize tempest and render tempest config. :param tempest_template: tempest.conf template :type tempest_template: module :param accounts_template: accounts.yaml template :type accounts_template: module + :param minimal: Run in minimal mode eg ignore missing setup + :type minimal: bool :returns: None :rtype: None """ workspace_name, workspace_path = get_workspace() destroy_workspace(workspace_name, workspace_path) _init_workspace(workspace_path) - context = _get_tempest_context(workspace_path) + context = _get_tempest_context(workspace_path, missing_fatal=not minimal) _render_tempest_config( os.path.join(workspace_path, 'etc/tempest.conf'), context, @@ -129,9 +138,13 @@ def _setup_tempest(tempest_template, accounts_template): accounts_template) -def _get_tempest_context(workspace_path): +def _get_tempest_context(workspace_path, missing_fatal=True): """Generate the tempest config context. + :param workspace_path: path to workspace directory + :type workspace_path: str + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: Context dictionary :rtype: dict """ @@ -153,8 +166,14 @@ def _get_tempest_context(workspace_path): _add_application_ips(ctxt) for svc_name, ctxt_func in ctxt_funcs.items(): if svc_name in ctxt['enabled_services']: - ctxt_func(ctxt, keystone_session) - _add_environment_var_config(ctxt, ctxt['enabled_services']) + ctxt_func( + ctxt, + keystone_session, + missing_fatal=missing_fatal) + _add_environment_var_config( + ctxt, + ctxt['enabled_services'], + missing_fatal=missing_fatal) _add_auth_config(ctxt) if 'octavia' in ctxt['enabled_services']: _add_octavia_config(ctxt) @@ -194,13 +213,15 @@ def _add_application_ips(ctxt): ctxt['ncc'] = juju_utils.get_application_ip('nova-cloud-controller') -def _add_nova_config(ctxt, keystone_session): +def _add_nova_config(ctxt, keystone_session, missing_fatal=True): """Add nova config to context. :param ctxt: Context dictionary :type ctxt: dict :param keystone_session: keystoneauth1.session.Session object :type: keystoneauth1.session.Session + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ @@ -213,54 +234,52 @@ def _add_nova_config(ctxt, keystone_session): ctxt['flavor_ref_alt'] = flavor.id -def _add_neutron_config(ctxt, keystone_session): +def _add_neutron_config(ctxt, keystone_session, missing_fatal=True): """Add neutron config to context. :param ctxt: Context dictionary :type ctxt: dict :param keystone_session: keystoneauth1.session.Session object :type: keystoneauth1.session.Session + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ - current_release = openstack_utils.get_os_release() - focal_ussuri = openstack_utils.get_os_release('focal_ussuri') neutron_client = openstack_utils.get_neutron_session_client( keystone_session) - net = neutron_client.find_resource("network", "ext_net") - ctxt['ext_net'] = net['id'] - router = neutron_client.find_resource("router", "provider-router") - ctxt['provider_router_id'] = router['id'] - # For focal+ with OVN, we use the same settings as upstream gate. - # This is because the l3_agent_scheduler extension is only - # applicable for OVN when conventional layer-3 agent enabled: - # https://docs.openstack.org/networking-ovn/2.0.1/features.html - # This enables test_list_show_extensions to run successfully. - if current_release >= focal_ussuri: - extensions = ('address-scope,agent,allowed-address-pairs,' - 'auto-allocated-topology,availability_zone,' - 'binding,default-subnetpools,external-net,' - 'extra_dhcp_opt,multi-provider,net-mtu,' - 'network_availability_zone,network-ip-availability,' - 'port-security,provider,quotas,rbac-address-scope,' - 'rbac-policies,standard-attr-revisions,security-group,' - 'standard-attr-description,subnet_allocation,' - 'standard-attr-tag,standard-attr-timestamp,trunk,' - 'quota_details,router,extraroute,ext-gw-mode,' - 'fip-port-details,pagination,sorting,project-id,' - 'dns-integration,qos') - ctxt['neutron_api_extensions'] = extensions - else: - ctxt['neutron_api_extensions'] = 'all' + try: + net = neutron_client.find_resource("network", "ext_net") + ctxt['ext_net'] = net['id'] + router = neutron_client.find_resource("router", "provider-router") + ctxt['provider_router_id'] = router['id'] + except neutronexceptions.NotFound: + if missing_fatal: + raise + extensions = ('address-scope,agent,allowed-address-pairs,' + 'auto-allocated-topology,availability_zone,' + 'binding,default-subnetpools,external-net,' + 'extra_dhcp_opt,multi-provider,net-mtu,' + 'network_availability_zone,network-ip-availability,' + 'port-security,provider,quotas,rbac-address-scope,' + 'rbac-policies,standard-attr-revisions,security-group,' + 'standard-attr-description,subnet_allocation,' + 'standard-attr-tag,standard-attr-timestamp,trunk,' + 'quota_details,router,extraroute,ext-gw-mode,' + 'fip-port-details,pagination,sorting,project-id,' + 'dns-integration,qos') + ctxt['neutron_api_extensions'] = extensions -def _add_glance_config(ctxt, keystone_session): +def _add_glance_config(ctxt, keystone_session, missing_fatal=True): """Add glance config to context. :param ctxt: Context dictionary :type ctxt: dict :param keystone_session: keystoneauth1.session.Session object :type: keystoneauth1.session.Session + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ @@ -276,13 +295,15 @@ def _add_glance_config(ctxt, keystone_session): ctxt['image_alt_id'] = image_alt[0].id -def _add_cinder_config(ctxt, keystone_session): +def _add_cinder_config(ctxt, keystone_session, missing_fatal=True): """Add cinder config to context. :param ctxt: Context dictionary :type ctxt: dict :param keystone_session: keystoneauth1.session.Session object :type: keystoneauth1.session.Session + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ @@ -297,13 +318,15 @@ def _add_cinder_config(ctxt, keystone_session): break -def _add_keystone_config(ctxt, keystone_session): +def _add_keystone_config(ctxt, keystone_session, missing_fatal=True): """Add keystone config to context. :param ctxt: Context dictionary :type ctxt: dict :param keystone_session: keystoneauth1.session.Session object :type: keystoneauth1.session.Session + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ @@ -313,11 +336,13 @@ def _add_keystone_config(ctxt, keystone_session): ctxt['default_domain_id'] = domain.id -def _add_octavia_config(ctxt): +def _add_octavia_config(ctxt, missing_fatal=True): """Add octavia config to context. :param ctxt: Context dictionary :type ctxt: dict + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None :raises: subprocess.CalledProcessError @@ -334,11 +359,15 @@ def _add_octavia_config(ctxt): ]) -def _add_environment_var_config(ctxt, services): +def _add_environment_var_config(ctxt, services, missing_fatal=True): """Add environment variable config to context. :param ctxt: Context dictionary :type ctxt: dict + :param services: List of services + :type services: List[str] + :param missing_fatal: Raise an exception if a resource is missing + :type missing_fatal: bool :returns: None :rtype: None """ @@ -353,7 +382,7 @@ def _add_environment_var_config(ctxt, services): else: if var not in IGNORABLE_VARS: missing_vars.append(var) - if missing_vars: + if missing_vars and missing_fatal: raise ValueError( ("Environment variables [{}] must all be set to run this" " test").format(', '.join(missing_vars))) From 0fe0f0167cccc0169e146fde654aa599a01c8555 Mon Sep 17 00:00:00 2001 From: Corey Bryant Date: Tue, 25 Oct 2022 13:12:35 -0400 Subject: [PATCH 16/26] Drop unrelated Python versions Python 3.5, 3.6, and 3.7 are no longer needed on this branch. --- .github/workflows/tox.yaml | 4 ++-- tox.ini | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index 754ce29..ddfeff1 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.8, 3.9] steps: - uses: actions/checkout@v1 @@ -24,4 +24,4 @@ jobs: - name: Lint with tox run: tox -e pep8 - name: Test with tox - run: tox -e py${{ matrix.python-version }} \ No newline at end of file + run: tox -e py${{ matrix.python-version }} diff --git a/tox.ini b/tox.ini index 890b193..2a8ae4b 100644 --- a/tox.ini +++ b/tox.ini @@ -30,18 +30,6 @@ commands = nosetests --with-coverage --cover-package=zaza.openstack {posargs} {t basepython = python3 deps = -r{toxinidir}/requirements.txt -[testenv:py3.5] -basepython = python3.5 -deps = -r{toxinidir}/requirements.txt - -[testenv:py3.6] -basepython = python3.6 -deps = -r{toxinidir}/requirements.txt - -[testenv:py3.7] -basepython = python3.7 -deps = -r{toxinidir}/requirements.txt - [testenv:py3.8] basepython = python3.8 deps = -r{toxinidir}/requirements.txt From 7b65b7301059c7ccfba36c9370b7c4e22bebc5db Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Tue, 25 Oct 2022 15:13:15 -0300 Subject: [PATCH 17/26] Migrate from nosetest to pytest nosetest doesn't support python 3.10 and over the years the python community has been settling on pytest as test runner, this change introduces pytest and adds py3.10 to the gh workflow testing matrix. --- .github/workflows/tox.yaml | 3 ++- requirements.txt | 3 ++- tox.ini | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml index ddfeff1..ce816f9 100644 --- a/.github/workflows/tox.yaml +++ b/.github/workflows/tox.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9] + python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v1 @@ -19,6 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + sudo apt-get install -q --yes libxml2-dev libxslt1-dev python -m pip install --upgrade pip pip install tox tox-gh-actions - name: Lint with tox diff --git a/requirements.txt b/requirements.txt index a9629a0..82f136a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,8 @@ flake8-per-file-ignores pydocstyle<4.0.0 coverage<6.0.0 # coverage 6.0+ drops support for py3.5/py2.7 mock>=1.2 -nose>=1.3.7 +pytest +pytest-cov pbr>=1.8.0,<1.9.0 simplejson>=2.2.0 netifaces>=0.10.4 diff --git a/tox.ini b/tox.ini index 2a8ae4b..c1e4a91 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ setenv = VIRTUAL_ENV={envdir} install_command = {toxinidir}/pip.sh install {opts} {packages} -commands = nosetests --with-coverage --cover-package=zaza.openstack {posargs} {toxinidir}/unit_tests +commands = pytest --cov=zaza.openstack {posargs} {toxinidir}/unit_tests [testenv:py3] basepython = python3 @@ -38,6 +38,10 @@ deps = -r{toxinidir}/requirements.txt basepython = python3.9 deps = -r{toxinidir}/requirements.txt +[testenv:py3.10] +basepython = python3.10 +deps = -r{toxinidir}/requirements.txt + [testenv:pep8] basepython = python3 deps = -r{toxinidir}/requirements.txt From ad215019e4aa4fe7838641dba469d2f3e2d1c45b Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Tue, 25 Oct 2022 15:33:56 -0300 Subject: [PATCH 18/26] Unpin PyYAML for python>=3.10 PyYAML<5.1 fails with the error below on Python 3.10 AttributeError: module 'collections' has no attribute 'Hashable' The fix got available by this commit https://github.com/yaml/pyyaml/commit/9959328b41ee577c6f14e3a0184b07dd0ecadb8c --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82f136a..6168614 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,8 @@ async_generator pyopenssl<22.1.0 boto3<1.25 -PyYAML<=4.2,>=3.0 +PyYAML<=4.2,>=3.0; python_version < '3.10' +PyYAML>=5.1; python_version >= '3.10' flake8>=2.2.4 flake8-docstrings flake8-per-file-ignores From 788fd771375e46a0e274f883662117b87894dec9 Mon Sep 17 00:00:00 2001 From: jneo8 Date: Thu, 18 Aug 2022 06:19:30 +0000 Subject: [PATCH 19/26] test(zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py): Add tests for mysql prometheus exporter --- .../mysql/test_prometheus_mysql_exporter.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py diff --git a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py new file mode 100644 index 0000000..fff3bb1 --- /dev/null +++ b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py @@ -0,0 +1,110 @@ +# 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. + +"""MySQL Prometheus Exporter Testing.""" + +import json + +import zaza.model as zaza_model +from zaza.openstack.charm_tests.mysql.tests import MySQLBaseTest + + +class PrometheusMySQLExporterTest(MySQLBaseTest): + """Functional tests check prometheus exporter.""" + + @classmethod + def setUpClass(cls, application_name=None): + """Run class setup for running mysql tests.""" + super().setUpClass(application_name="mysql-innodb-cluster") + cls.application = "mysql-innodb-cluster" + + def _exporter_http_check( + self, + cmd, + expected, + ): + """Exec check cmd on each unit in the application. + + :param str cmd: The check command run on unit. + :param str expected: Expected result code. + """ + for unit in zaza_model.get_units(self.application): + result = zaza_model.run_on_unit(unit.name, cmd) + self.assertEqual(result.get("Code"), expected) + + def _check_service_status_is( + self, + active=True, + ): + cmd = "systemctl is-active {}".format( + "snap.mysql-prometheus-exporter.mysqld-exporter.service" + ) + excepted = "active\n" + if not active: + excepted = "inactive\n" + for unit in zaza_model.get_units(self.application): + result = zaza_model.run_on_unit(unit.name, cmd) + self.assertEqual(result.get("stdout"), excepted) + + def test_01_exporter_http_check(self): + """Check exporter endpoint is working.""" + self._exporter_http_check( + cmd="curl http://localhost:9104", + expected="0", + ) + + def test_02_exporter_metrics_http_check(self): + """Check metrics API is working.""" + cmd = "curl http://localhost:9104/metrics" + self._exporter_http_check(cmd=cmd, expected="0") + + def test_03_exporter_service_relation_trigger(self): + """Relation trigger exporter service start/stop.""" + zaza_model.remove_relation( + self.application, + "prometheus2:target", + "mysql-innodb-cluster:prometheus", + ) + for unit in zaza_model.get_units(self.application): + zaza_model.block_until_unit_wl_status(unit.name, "active") + zaza_model.block_until_all_units_idle() + self._check_service_status_is(active=False) + + # Recover + zaza_model.add_relation( + self.application, + "prometheus2:target", + "mysql-innodb-cluster:prometheus", + ) + for unit in zaza_model.get_units(self.application): + zaza_model.block_until_unit_wl_status(unit.name, "active") + zaza_model.block_until_all_units_idle() + self._check_service_status_is(active=True) + + def test_04_snap_config(self): + """Check snap set config is working.""" + cmd = "sudo snap get mysql-prometheus-exporter mysql -d" + for unit in zaza_model.get_units(self.application): + result = zaza_model.run_on_unit(unit.name, cmd) + json_mysql_config = json.loads( + result.get("stdout")).get("mysql") + json_mysql_config.pop("pwd") + self.assertEqual( + json_mysql_config, + { + "host": unit.public_address, + "port": 3306, + "user": "prom_exporter" + } + ) From f39ea121a4a59201d5a3b72fbf18fbf0a82764e4 Mon Sep 17 00:00:00 2001 From: jneo8 Date: Mon, 22 Aug 2022 07:48:14 +0000 Subject: [PATCH 20/26] style(zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py): Update docstring style --- .../charm_tests/mysql/test_prometheus_mysql_exporter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py index fff3bb1..c1f377e 100644 --- a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py +++ b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py @@ -36,8 +36,10 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): ): """Exec check cmd on each unit in the application. - :param str cmd: The check command run on unit. - :param str expected: Expected result code. + :param cmd: The check command run on unit + :type cmd: str + :param expected: Expected result code + :type expected: str """ for unit in zaza_model.get_units(self.application): result = zaza_model.run_on_unit(unit.name, cmd) From f079b9cddb9b6d046cfa6e5030d9f382bd8d2394 Mon Sep 17 00:00:00 2001 From: jneo8 Date: Thu, 25 Aug 2022 08:35:43 +0000 Subject: [PATCH 21/26] fix(zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py): Fix snap naming and configure to fit new snap release --- .../charm_tests/mysql/test_prometheus_mysql_exporter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py index c1f377e..23403fc 100644 --- a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py +++ b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py @@ -28,6 +28,8 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): """Run class setup for running mysql tests.""" super().setUpClass(application_name="mysql-innodb-cluster") cls.application = "mysql-innodb-cluster" + cls.snap_name = "mysqld-exporter" + cls.service_name = "snap.mysqld-exporter.mysqld-exporter.service" def _exporter_http_check( self, @@ -50,7 +52,7 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): active=True, ): cmd = "systemctl is-active {}".format( - "snap.mysql-prometheus-exporter.mysqld-exporter.service" + self.service_name ) excepted = "active\n" if not active: @@ -96,12 +98,12 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): def test_04_snap_config(self): """Check snap set config is working.""" - cmd = "sudo snap get mysql-prometheus-exporter mysql -d" + cmd = "sudo snap get {} mysql -d".format(self.snap_name) for unit in zaza_model.get_units(self.application): result = zaza_model.run_on_unit(unit.name, cmd) json_mysql_config = json.loads( result.get("stdout")).get("mysql") - json_mysql_config.pop("pwd") + json_mysql_config.pop("password") self.assertEqual( json_mysql_config, { From db21233890bdcf6da3042b07b0a17d1d51c28b74 Mon Sep 17 00:00:00 2001 From: jneo8 Date: Mon, 29 Aug 2022 10:03:10 +0000 Subject: [PATCH 22/26] test(zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py): Add HTTP metrics query check --- .../mysql/test_prometheus_mysql_exporter.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py index 23403fc..723bea9 100644 --- a/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py +++ b/zaza/openstack/charm_tests/mysql/test_prometheus_mysql_exporter.py @@ -15,6 +15,7 @@ """MySQL Prometheus Exporter Testing.""" import json +import urllib.request import zaza.model as zaza_model from zaza.openstack.charm_tests.mysql.tests import MySQLBaseTest @@ -68,12 +69,22 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): expected="0", ) - def test_02_exporter_metrics_http_check(self): - """Check metrics API is working.""" - cmd = "curl http://localhost:9104/metrics" - self._exporter_http_check(cmd=cmd, expected="0") + for unit in zaza_model.get_units(self.application): + url = "http://{}:9104/metrics".format( + unit.public_address) + with urllib.request.urlopen(url) as resp: + metrics = resp.read().decode("utf-8") + if not any( + str(line) == "mysql_up 1" + for line in metrics.split("\n") + ): + self.fail( + "Exporter permission not correct on {}".format( + unit.public_address + ) + ) - def test_03_exporter_service_relation_trigger(self): + def test_02_exporter_service_relation_trigger(self): """Relation trigger exporter service start/stop.""" zaza_model.remove_relation( self.application, @@ -96,7 +107,7 @@ class PrometheusMySQLExporterTest(MySQLBaseTest): zaza_model.block_until_all_units_idle() self._check_service_status_is(active=True) - def test_04_snap_config(self): + def test_03_snap_config(self): """Check snap set config is working.""" cmd = "sudo snap get {} mysql -d".format(self.snap_name) for unit in zaza_model.get_units(self.application): From c8ae6cdc55193d5f8b33de72ad22830b20bbcfff Mon Sep 17 00:00:00 2001 From: coreycb Date: Thu, 27 Oct 2022 07:19:34 -0400 Subject: [PATCH 23/26] Add retry decorator to masakari segment creation (#972) The create_segment() function is often failing in the gate due to being unable to establish a connection to the masakari endpoint. This will allow some more time for the endpoint to become available when this error occurs. --- zaza/openstack/configure/masakari.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zaza/openstack/configure/masakari.py b/zaza/openstack/configure/masakari.py index 707416f..2f780f4 100644 --- a/zaza/openstack/configure/masakari.py +++ b/zaza/openstack/configure/masakari.py @@ -21,6 +21,7 @@ and recovery. import logging import openstack.exceptions as ostack_except import tenacity +import urllib3 import zaza.model import zaza.openstack.utilities.openstack as openstack_utils @@ -57,6 +58,10 @@ HOST_ASSIGNMENT_METHODS = { } +@tenacity.retry( + wait=tenacity.wait_exponential(multiplier=2, max=60), + reraise=True, stop=tenacity.stop_after_attempt(10), + retry=tenacity.retry_if_exception_type(urllib3.connection.HTTPSConnection)) def create_segments(segment_number=1, host_assignment_method=None): """Create a masakari segment and populate it with hypervisors. From 74bca90a6f149b63965e2771efb4be619e43f65f Mon Sep 17 00:00:00 2001 From: Peter Sabaini Date: Thu, 27 Oct 2022 13:46:08 +0200 Subject: [PATCH 24/26] Ceph: also check ceph-mgr (#973) In the ceph lowlevel test, also check for ceph-mgr process and service. --- zaza/openstack/charm_tests/ceph/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zaza/openstack/charm_tests/ceph/tests.py b/zaza/openstack/charm_tests/ceph/tests.py index 29a74e0..9fbfad9 100644 --- a/zaza/openstack/charm_tests/ceph/tests.py +++ b/zaza/openstack/charm_tests/ceph/tests.py @@ -62,6 +62,7 @@ class CephLowLevelTest(test_utils.OpenStackBaseTest): # Process name and quantity of processes to expect on each unit ceph_mon_processes = { 'ceph-mon': 1, + 'ceph-mgr': 1, } ceph_osd_processes = { @@ -90,7 +91,7 @@ class CephLowLevelTest(test_utils.OpenStackBaseTest): """ logging.info('Checking ceph-osd and ceph-mon services...') services = {} - ceph_services = ['ceph-mon'] + ceph_services = ['ceph-mon', 'ceph-mgr'] services['ceph-osd/0'] = ['ceph-osd'] services['ceph-mon/0'] = ceph_services From d083dde10c772a1fd9b1915707167d7723e245a5 Mon Sep 17 00:00:00 2001 From: Felipe Reyes Date: Fri, 28 Oct 2022 12:30:26 -0300 Subject: [PATCH 25/26] Retry check of service IP configured (#970) There are scenarios where the config-changed hook can complete, yet the service IP get configured many seconds after, because a relation-changed hook execution needs to be triggered on the hacluster side of the relation. This change adds a retry to the check (10 times with a 2 seconds wait time). This issue was found at the gate https://review.opendev.org/c/openstack/charm-designate-bind/+/861417 --- .../charm_tests/designate_bind/tests.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/zaza/openstack/charm_tests/designate_bind/tests.py b/zaza/openstack/charm_tests/designate_bind/tests.py index 7526fc7..77da133 100644 --- a/zaza/openstack/charm_tests/designate_bind/tests.py +++ b/zaza/openstack/charm_tests/designate_bind/tests.py @@ -16,6 +16,13 @@ import logging import os +from tenacity import ( + Retrying, + retry_if_exception_type, + stop_after_attempt, + wait_fixed, +) + import zaza.model as zaza_model import zaza.openstack.charm_tests.test_utils as test_utils @@ -43,8 +50,14 @@ class DesignateBindServiceIPsTest(test_utils.OpenStackBaseTest): zaza_model.set_application_config(self.APPLICATION, config) zaza_model.wait_for_application_states() - configured_ips = zaza_model.run_on_unit(self.UNIT, "ip addr") - self.assertIn(self.VIP, configured_ips["Stdout"]) + for attempt in Retrying(wait=wait_fixed(2), + retry=retry_if_exception_type(AssertionError), + reraise=True, + stop=stop_after_attempt(10)): + with attempt: + configured_ips = zaza_model.run_on_unit(self.UNIT, + "ip addr") + self.assertIn(self.VIP, configured_ips["Stdout"]) logging.info("Removing service IP configuration from %s unit.", self.UNIT) From d0193e38170ae8baa515c5ac368f6d967db6e761 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Mon, 21 Nov 2022 14:31:24 +0000 Subject: [PATCH 26/26] Add new required dns.nameservers config --- zaza/openstack/charm_tests/tempest/templates/tempest_v3.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zaza/openstack/charm_tests/tempest/templates/tempest_v3.j2 b/zaza/openstack/charm_tests/tempest/templates/tempest_v3.j2 index adbe0ef..c7ba8a5 100644 --- a/zaza/openstack/charm_tests/tempest/templates/tempest_v3.j2 +++ b/zaza/openstack/charm_tests/tempest/templates/tempest_v3.j2 @@ -121,3 +121,6 @@ test_with_ipv6 = false test_server_path = {{ workspace_path }}/test_server.bin provider = amphora {% endif %} + +[dns] +nameservers = {{ test_name_server }}