Merge remote-tracking branch 'upstream/master' into ovn-downscale
This commit is contained in:
@@ -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", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@@ -19,9 +19,10 @@ 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
|
||||
run: tox -e pep8
|
||||
- name: Test with tox
|
||||
run: tox -e py${{ matrix.python-version }}
|
||||
run: tox -e py${{ matrix.python-version }}
|
||||
|
||||
+4
-2
@@ -11,14 +11,16 @@ 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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -46,6 +51,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',
|
||||
|
||||
@@ -24,24 +24,12 @@ 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
|
||||
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
|
||||
@@ -50,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
|
||||
|
||||
@@ -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,
|
||||
@@ -197,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")
|
||||
|
||||
@@ -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
|
||||
@@ -141,50 +142,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."""
|
||||
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/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."""
|
||||
|
||||
API_VERSION = '1'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Run class setup for running Cloudkitty tests."""
|
||||
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 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."""
|
||||
report = self.cloudkitty.report
|
||||
tenants_list = report.get_tenants()
|
||||
assert tenants_list == []
|
||||
|
||||
def test_401_module_enable_and_disable(self):
|
||||
"""Test enable and disable module via API."""
|
||||
rating = self.cloudkitty.rating
|
||||
modules = rating.get_module()
|
||||
|
||||
for module in modules.get('modules'):
|
||||
module_id = module.get('module_id')
|
||||
|
||||
# noop module can't be disabled
|
||||
if module_id == 'noop':
|
||||
continue
|
||||
|
||||
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')
|
||||
|
||||
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_402_create_mapping(self):
|
||||
"""Test mapping create via API."""
|
||||
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
|
||||
|
||||
service = hashmap.create_service(name='test-service')
|
||||
service_id = service.get('service_id')
|
||||
|
||||
field = hashmap.create_field(name='test-field', service_id=service_id)
|
||||
field_id = field.get('field_id')
|
||||
|
||||
group = hashmap.create_group(name='test-group')
|
||||
group_id = group.get('group_id')
|
||||
|
||||
hashmap.create_mapping(
|
||||
type='flat', field_id=field_id,
|
||||
group_id=group_id, value='test-value', cost=0.1)
|
||||
@@ -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)
|
||||
|
||||
@@ -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', {}))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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 urllib.request
|
||||
|
||||
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"
|
||||
cls.snap_name = "mysqld-exporter"
|
||||
cls.service_name = "snap.mysqld-exporter.mysqld-exporter.service"
|
||||
|
||||
def _exporter_http_check(
|
||||
self,
|
||||
cmd,
|
||||
expected,
|
||||
):
|
||||
"""Exec check cmd on each unit in the application.
|
||||
|
||||
: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)
|
||||
self.assertEqual(result.get("Code"), expected)
|
||||
|
||||
def _check_service_status_is(
|
||||
self,
|
||||
active=True,
|
||||
):
|
||||
cmd = "systemctl is-active {}".format(
|
||||
self.service_name
|
||||
)
|
||||
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",
|
||||
)
|
||||
|
||||
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_02_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_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):
|
||||
result = zaza_model.run_on_unit(unit.name, cmd)
|
||||
json_mysql_config = json.loads(
|
||||
result.get("stdout")).get("mysql")
|
||||
json_mysql_config.pop("password")
|
||||
self.assertEqual(
|
||||
json_mysql_config,
|
||||
{
|
||||
"host": unit.public_address,
|
||||
"port": 3306,
|
||||
"user": "prom_exporter"
|
||||
}
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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."""
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -121,3 +121,6 @@ test_with_ipv6 = false
|
||||
test_server_path = {{ workspace_path }}/test_server.bin
|
||||
provider = amphora
|
||||
{% endif %}
|
||||
|
||||
[dns]
|
||||
nameservers = {{ test_name_server }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -487,15 +503,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):
|
||||
@@ -519,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)
|
||||
|
||||
|
||||
@@ -2102,6 +2113,60 @@ 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)
|
||||
|
||||
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')
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user