sunbeam-charms/charms/keystone-k8s/tests/unit/test_keystone_charm.py
Hemanth Nakkina df70e376ff
Add zuuljobs
* Add sunbeam project template to run pep8, py3 tests
* Add zuul.d/zuul.yaml to run pep8, py3, cover tests
* Update charmcraft and requirements for each charm
* Add global tox.ini to invoke fmt, pep8, py3, cover,
  build
* Add gitreview file
* Fix py3 test failures in ciner-ceph-k8s, glance-k8s,
  openstack-exporter
* Add jobs for charm builds using files option so that
  job is invoked if files within the component are
  modified. Add charm builds to both check and gate
  pipeline.
* Make function tests as part of global. Split the function
  tests into core, ceph, caas, misc mainly to accomodate
  function tests to run on 8GB. Add function tests as
  part of check pipeline.
* Add zuul job to publish charms in promote pipeline
  Add charmhub token as secret that can be used to
  publish charms.
  Note: Charmhub token is generated with ttl of 90 days.
* Run tox formatting
* Make .gitignore, .jujuignore, .stestr.conf global and
  remove the files from all charms.
* Make libs and templates global. Split libs to internal
  and external so that internal libs can adhere to
  sunbeam formatting styles.
* Add script to copy common files necessary libs, config
  templates, stestr conf, jujuignore during py3 tests
  and charm builds.
* Tests for keystone-ldap-k8s are commented due to
  intermittent bug LP#2045206

Change-Id: I804ca64182c109d16bd820ac00f129aa6dcf4496
2023-11-30 15:32:39 +05:30

567 lines
22 KiB
Python

#!/usr/bin/env python3
# Copyright 2021 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Define keystone tests."""
import json
import os
import textwrap
from unittest.mock import (
ANY,
MagicMock,
)
import charm
import mock
import ops_sunbeam.test_utils as test_utils
class _KeystoneOperatorCharm(charm.KeystoneOperatorCharm):
"""Create Keystone operator test charm."""
def __init__(self, framework):
self.seen_events = []
super().__init__(framework)
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
@property
def public_ingress_address(self) -> str:
return "10.0.0.10"
class TestKeystoneOperatorCharm(test_utils.CharmTestCase):
"""Test Keystone operator charm."""
PATCHES = [
"manager",
"pwgen",
]
def add_id_relation(self) -> str:
"""Add amqp relation."""
rel_id = self.harness.add_relation("identity-service", "cinder")
self.harness.add_relation_unit(rel_id, "cinder/0")
self.harness.update_relation_data(
rel_id, "cinder/0", {"ingress-address": "10.0.0.13"}
)
interal_url = "http://10.152.183.228:8776"
public_url = "http://10.152.183.228:8776"
self.harness.update_relation_data(
rel_id,
"cinder",
{
"region": "RegionOne",
"service-endpoints": json.dumps(
[
{
"service_name": "cinderv2",
"type": "volumev2",
"description": "Cinder Volume Service v2",
"internal_url": f"{interal_url}/v2/$(tenant_id)s",
"public_url": f"{public_url}/v2/$(tenant_id)s",
"admin_url": f"{interal_url}/v2/$(tenant_id)s",
},
{
"service_name": "cinderv3",
"type": "volumev3",
"description": "Cinder Volume Service v3",
"internal_url": f"{interal_url}/v3/$(tenant_id)s",
"public_url": f"{public_url}/v3/$(tenant_id)s",
"admin_url": f"{interal_url}/v3/$(tenant_id)s",
},
]
),
},
)
return rel_id
def ks_manager_mock(self):
"""Create keystone manager mock."""
def _create_mock(p_name, p_id):
return {"id": p_id, "name": p_name}
def _get_domain_side_effect(name: str):
if name == "admin_domain":
return admin_domain_mock
else:
return service_domain_mock
service_domain_mock = _create_mock("sdomain_name", "sdomain_id")
admin_domain_mock = _create_mock("adomain_name", "adomain_id")
admin_project_mock = _create_mock("aproject_name", "aproject_id")
service_user_mock = _create_mock("suser_name", "suser_id")
admin_user_mock = _create_mock("auser_name", "auser_id")
admin_role_mock = _create_mock("arole_name", "arole_id")
km_mock = mock.MagicMock()
km_mock.ksclient.show_domain.side_effect = _get_domain_side_effect
km_mock.ksclient.show_project.return_value = admin_project_mock
km_mock.ksclient.show_user.return_value = admin_user_mock
km_mock.ksclient.create_user.return_value = service_user_mock
km_mock.ksclient.create_role.return_value = admin_role_mock
km_mock.create_service_account.return_value = service_user_mock
km_mock.read_keys.return_value = {
"0": "Qf4vHdf6XC2dGKpEwtGapq7oDOqUWepcH2tKgQ0qOKc=",
"3": "UK3qzLGvu-piYwau0BFyed8O3WP8lFKH_v1sXYulzhs=",
"4": "YVYUJbQNASbVzzntqj2sG9rbDOV_QQfueDCz0PJEKKw=",
}
return km_mock
def setUp(self):
"""Run test setup."""
super().setUp(charm, self.PATCHES)
# used by _launch_heartbeat.
# value doesn't matter for tests because mocking
os.environ["JUJU_CHARM_DIR"] = "/arbitrary/directory/"
self.pwgen.pwgen.return_value = "randonpassword"
self.km_mock = self.ks_manager_mock()
self.manager.KeystoneManager.return_value = self.km_mock
self.harness = test_utils.get_harness(
_KeystoneOperatorCharm, container_calls=self.container_calls
)
# clean up events that were dynamically defined,
# otherwise we get issues because they'll be redefined,
# which is not allowed.
from charms.data_platform_libs.v0.database_requires import (
DatabaseEvents,
)
for attr in (
"database_database_created",
"database_endpoints_changed",
"database_read_only_endpoints_changed",
):
try:
delattr(DatabaseEvents, attr)
except AttributeError:
pass
self.addCleanup(self.harness.cleanup)
self.harness.begin()
# This function need to be moved to operator
def get_secret_by_label(self, label: str) -> str:
"""Get secret by label from harness class."""
for secret in self.harness._backend._secrets:
if secret.label == label:
return secret.id
return None
def test_pebble_ready_handler(self):
"""Test pebble ready handler."""
self.assertEqual(self.harness.charm.seen_events, [])
self.harness.container_pebble_ready("keystone")
self.assertEqual(self.harness.charm.seen_events, ["PebbleReadyEvent"])
def test_id_client(self):
"""Test responding to an identity client."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
peer_rel_id = self.harness.add_relation("peers", "keystone")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
identity_rel_id = self.add_id_relation()
rel_data = self.harness.get_relation_data(
identity_rel_id, self.harness.charm.unit.app.name
)
secret_svc_cinder = self.get_secret_by_label("credentials_svc_cinder")
self.maxDiff = None
self.assertEqual(
rel_data,
{
"admin-auth-url": "http://10.0.0.10:5000/v3",
"admin-domain-id": "adomain_id",
"admin-domain-name": "adomain_name",
"admin-project-id": "aproject_id",
"admin-project-name": "aproject_name",
"admin-role": "Admin",
"admin-user-id": "auser_id",
"admin-user-name": "auser_name",
"api-version": "v3",
"auth-host": "10.0.0.10",
"auth-port": "5000",
"auth-protocol": "http",
"internal-auth-url": "http://internal-url/v3",
"internal-host": "10.0.0.10",
"internal-port": "5000",
"internal-protocol": "http",
"public-auth-url": "http://public-url/v3",
"service-domain-id": "sdomain_id",
"service-domain-name": "sdomain_name",
"service-host": "10.0.0.10",
"service-credentials": secret_svc_cinder,
"service-port": "5000",
"service-project-id": "aproject_id",
"service-project-name": "aproject_name",
"service-protocol": "http",
"service-user-id": "suser_id",
},
)
peer_data = self.harness.get_relation_data(
peer_rel_id, self.harness.charm.unit.app.name
)
fernet_secret_id = self.get_secret_by_label("fernet-keys")
credential_secret_id = self.get_secret_by_label("credential-keys")
self.assertEqual(
peer_data,
{
"leader_ready": "true",
"fernet-secret-id": fernet_secret_id,
"credential-keys-secret-id": credential_secret_id,
"credentials_svc_cinder": secret_svc_cinder,
},
)
def test_leader_bootstraps(self):
"""Test leader bootstrap."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
self.km_mock.setup_keystone.assert_called_once_with()
self.km_mock.setup_initial_projects_and_users.assert_called_once_with()
peer_data = self.harness.get_relation_data(
rel_id, self.harness.charm.unit.app.name
)
fernet_secret_id = self.get_secret_by_label("fernet-keys")
credential_secret_id = self.get_secret_by_label("credential-keys")
self.assertEqual(
peer_data,
{
"leader_ready": "true",
"fernet-secret-id": fernet_secret_id,
"credential-keys-secret-id": credential_secret_id,
},
)
def test_on_peer_data_changed_no_bootstrap(self):
"""Test peer_relation_changed on no bootstrap."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
event = MagicMock()
self.harness.charm._on_peer_data_changed(event)
self.assertTrue(event.defer.called)
def test_on_peer_data_changed_with_fernet_keys_and_fernet_secret_different(
self,
):
"""Test peer_relation_changed when fernet keys and secret have different content."""
updated_fernet_keys = {
"0": "Qf4vHdf6XC2dGKpEwtGapq7oDOqUWepcH2tKgQ0qOKc=",
"2": "UK3qzLGvu-piYwau0BFyed8O3WP8lFKH_v1sXYulzhs=",
"3": "YVYUJbQNASbVzzntqj2sG9rbDOV_QQfueDCz0PJEKKw=",
}
secret_mock = mock.MagicMock()
secret_mock.id = "test-secret-id"
secret_mock.get_content.return_value = updated_fernet_keys
self.harness.model.app.add_secret = MagicMock()
self.harness.model.app.add_secret.return_value = secret_mock
self.harness.model.get_secret = MagicMock()
self.harness.model.get_secret.return_value = secret_mock
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
event = MagicMock()
self.harness.charm._on_peer_data_changed(event)
self.assertTrue(self.harness.model.get_secret.called)
self.assertTrue(self.km_mock.read_keys.called)
self.assertEqual(self.km_mock.write_keys.call_count, 2)
self.km_mock.write_keys.assert_has_calls(
[
mock.call(
key_repository="/etc/keystone/fernet-keys",
keys=updated_fernet_keys,
),
mock.call(
key_repository="/etc/keystone/credential-keys",
keys=updated_fernet_keys,
),
]
)
def test_on_peer_data_changed_with_fernet_keys_and_fernet_secret_same(
self,
):
"""Test peer_relation_changed when fernet keys and secret have same content."""
secret_mock = mock.MagicMock()
secret_mock.id = "test-secret-id"
secret_mock.get_content.return_value = self.km_mock.read_keys()
self.harness.model.app.add_secret = MagicMock()
self.harness.model.app.add_secret.return_value = secret_mock
self.harness.model.get_secret = MagicMock()
self.harness.model.get_secret.return_value = secret_mock
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
event = MagicMock()
self.harness.charm._on_peer_data_changed(event)
self.assertTrue(self.harness.model.get_secret.called)
self.assertTrue(self.km_mock.read_keys.called)
self.assertFalse(self.km_mock.write_keys.called)
def _test_non_leader_on_secret_rotate(self, label: str):
"""Test secert-rotate event on non leader unit."""
test_utils.add_complete_ingress_relation(self.harness)
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
event = MagicMock()
event.secret.label = label
self.harness.charm._on_secret_rotate(event)
if label == "fernet-keys":
self.assertFalse(self.km_mock.rotate_fernet_keys.called)
elif label == "credential-keys":
self.assertFalse(self.km_mock.rotate_credential_keys.called)
def _test_leader_on_secret_rotate(self, label: str):
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
event = MagicMock()
event.secret.label = label
self.harness.charm._on_secret_rotate(event)
if label == "fernet-keys":
fernet_keys_ = {
f"fernet-{k}": v for k, v in self.km_mock.read_keys().items()
}
self.assertTrue(self.km_mock.rotate_fernet_keys.called)
event.secret.set_content.assert_called_once_with(fernet_keys_)
elif label == "credential-keys":
fernet_keys_ = {
f"fernet-{k}": v for k, v in self.km_mock.read_keys().items()
}
self.assertTrue(self.km_mock.rotate_credential_keys.called)
event.secret.set_content.assert_called_once_with(fernet_keys_)
def test_leader_on_secret_rotate_for_label_fernet_keys(self):
"""Test secret-rotate event for label fernet_keys on leader unit."""
self._test_leader_on_secret_rotate(label="fernet-keys")
def test_leader_on_secret_rotate_for_label_credential_keys(self):
"""Test secret-rotate event for label credential_keys on leader unit."""
self._test_leader_on_secret_rotate(label="credential-keys")
def test_non_leader_on_secret_rotate_for_label_fernet_keys(self):
"""Test secret-rotate event for label fernet_keys on non leader unit."""
self._test_non_leader_on_secret_rotate(label="fernet-keys")
def test_non_leader_on_secret_rotate_for_label_credential_keys(self):
"""Test secret-rotate event for label credential_keys on non leader unit."""
self._test_non_leader_on_secret_rotate(label="credential-keys")
def test_on_secret_changed_with_fernet_keys_and_fernet_secret_same(self):
"""Test secret change event when fernet keys and secret have same content."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
event = MagicMock()
event.secret.label = "fernet-keys"
event.secret.get_content.return_value = self.km_mock.read_keys()
self.harness.charm._on_secret_changed(event)
self.assertTrue(event.secret.get_content.called)
self.assertTrue(self.km_mock.read_keys.called)
self.assertFalse(self.km_mock.write_keys.called)
def test_on_secret_changed_with_fernet_keys_and_fernet_secret_different(
self,
):
"""Test secret change event when fernet keys and secret have different content."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
event = MagicMock()
event.secret.label = "fernet-keys"
event.secret.get_content.return_value = {
"0": "Qf4vHdf6XC2dGKpEwtGapq7oDOqUWepcH2tKgQ0qOKc=",
"4": "UK3qzLGvu-piYwau0BFyed8O3WP8lFKH_v1sXYulzhs=",
"5": "YVYUJbQNASbVzzntqj2sG9rbDOV_QQfueDCz0PJEKKw=",
}
self.harness.charm._on_secret_changed(event)
self.assertTrue(event.secret.get_content.called)
self.assertTrue(self.km_mock.read_keys.called)
self.assertTrue(self.km_mock.write_keys.called)
def test_non_leader_no_bootstraps(self):
"""Test bootstrapping on a non-leader."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader(False)
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
self.assertFalse(self.km_mock.setup_keystone.called)
def test_get_service_account_action(self):
"""Test get_service_account action."""
self.harness.add_relation("peers", "keystone-k8s")
action_event = MagicMock()
action_event.params = {"username": "external_service"}
# Check call on non-lead unit.
self.harness.charm._get_service_account_action(action_event)
action_event.set_results.assert_not_called()
action_event.fail.assert_called()
# Check call on lead unit.
self.harness.set_leader()
self.harness.charm._get_service_account_action(action_event)
action_event.set_results.assert_called_with(
{
"username": "external_service",
"password": "randonpassword",
"user-domain-name": "sdomain_name",
"project-name": "aproject_name",
"project-domain-name": "sdomain_name",
"region": "RegionOne",
"internal-endpoint": "http://10.0.0.10:5000/v3",
"public-endpoint": "http://10.0.0.10:5000/v3",
"api-version": 3,
}
)
def test_get_admin_account_action(self):
"""Test admin account action."""
self.harness.add_relation("peers", "keystone-k8s")
action_event = MagicMock()
self.harness.charm._get_admin_account_action(action_event)
action_event.set_results.assert_not_called()
action_event.fail.assert_called()
self.harness.set_leader()
self.harness.charm._get_admin_account_action(action_event)
action_event.set_results.assert_called_with(
{
"username": "admin",
"password": "randonpassword",
"user-domain-name": "admin_domain",
"project-name": "admin",
"project-domain-name": "admin_domain",
"region": "RegionOne",
"internal-endpoint": "http://10.0.0.10:5000/v3",
"public-endpoint": "http://10.0.0.10:5000/v3",
"api-version": 3,
"openrc": ANY,
}
)
def test_domain_config(self):
"""Test domain config."""
test_utils.add_complete_ingress_relation(self.harness)
self.harness.set_leader()
rel_id = self.harness.add_relation("peers", "keystone-k8s")
self.harness.add_relation_unit(rel_id, "keystone-k8s/1")
self.harness.container_pebble_ready("keystone")
test_utils.add_db_relation_credentials(
self.harness, test_utils.add_base_db_relation(self.harness)
)
dc_id = self.harness.add_relation("domain-config", "keystone-ldap-k8s")
self.harness.add_relation_unit(dc_id, "keystone-ldap-k8s/0")
b64file = (
"W2xkYXBdCmdyb3VwX21lbWJlcl9hdHRyaWJ1dGUgPSBtZW1iZXJVaWQKZ3JvdXBf"
"bWVtYmVyc19hcmVfaWRzID0gdHJ1ZQpncm91cF9uYW1lX2F0dHJpYnV0ZSA9IGNu"
"Cmdyb3VwX29iamVjdGNsYXNzID0gcG9zaXhHcm91cApncm91cF90cmVlX2RuID0g"
"b3U9Z3JvdXBzLGRjPXRlc3QsZGM9Y29tCnBhc3N3b3JkID0gY3JhcHBlcgpzdWZm"
"aXggPSBkYz10ZXN0LGRjPWNvbQp1cmwgPSBsZGFwOi8vMTAuMS4xNzYuMTg0CnVz"
"ZXIgPSBjbj1hZG1pbixkYz10ZXN0LGRjPWNvbQpbaWRlbnRpdHldCmRyaXZlciA9"
"IGxkYXA="
)
domain_config = {
"domain-name": "mydomain",
"config-contents": b64file,
}
self.harness.update_relation_data(
dc_id, "keystone-ldap-k8s", domain_config
)
expect_entries = """
[ldap]
group_member_attribute = memberUid
group_members_are_ids = true
group_name_attribute = cn
group_objectclass = posixGroup
group_tree_dn = ou=groups,dc=test,dc=com
password = crapper
suffix = dc=test,dc=com
url = ldap://10.1.176.184
user = cn=admin,dc=test,dc=com
[identity]
driver = ldap"""
self.maxDiff = None
self.check_file(
"keystone",
"/etc/keystone/domains/keystone.mydomain.conf",
contents=textwrap.dedent(expect_entries).lstrip(),
)