sunbeam-charms/charms/tempest-k8s/tests/unit/test_tempest_charm.py
Hemanth Nakkina 8ca8d63e3a
[tempest-k8s] Add tempest config overrides
Add tempest config override for object storage.
Ceph reef supports SHA1 for hash tempurl related APIs
where as tempest uses SHA256 by default. Override
tempest config to use SHA1 for object-storage hash
calulation.

Change-Id: I618bc55bc91c4f7b24803918574cac7fcc4cd718
2024-08-17 06:43:21 +05:30

657 lines
24 KiB
Python

#!/usr/bin/env python3
# Copyright 2024 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.
"""Unit tests for Tempest operator."""
import json
import pathlib
from unittest.mock import (
MagicMock,
Mock,
call,
patch,
)
import charm
import ops_sunbeam.test_utils as test_utils
import utils
import yaml
from utils.constants import (
CONTAINER,
TEMPEST_ADHOC_OUTPUT,
TEMPEST_HOME,
TEMPEST_PERIODIC_OUTPUT,
TEMPEST_READY_KEY,
get_tempest_concurrency,
)
from utils.types import (
TempestEnvVariant,
)
TEST_TEMPEST_ENV = {
"OS_REGION_NAME": "RegionOne",
"OS_IDENTITY_API_VERSION": "3",
"OS_AUTH_VERSION": "3",
"OS_AUTH_URL": "http://10.6.0.23/openstack-keystone/v3",
"OS_USERNAME": "tempest",
"OS_PASSWORD": "password",
"OS_USER_DOMAIN_NAME": "tempest",
"OS_PROJECT_NAME": "CloudValidation-tempest",
"OS_PROJECT_DOMAIN_NAME": "tempest",
"OS_DOMAIN_NAME": "tempest",
"OS_PROJECT_DOMAIN_ID": "tempest-domain-id",
"OS_USER_DOMAIN_ID": "tempest-domain-id",
"OS_DOMAIN_ID": "tempest-domain-id",
"TEMPEST_CONCURRENCY": "4",
"TEMPEST_ACCOUNTS_COUNT": "8",
"TEMPEST_CONF": "/var/lib/tempest/workspace/etc/tempest.conf",
"TEMPEST_EXCLUDE_LIST": "/var/lib/tempest/tempest_exclude_list.txt",
"TEMPEST_HOME": "/var/lib/tempest",
"TEMPEST_LIST_DIR": "/tempest_test_lists",
"TEMPEST_OUTPUT": "/var/lib/tempest/workspace/tempest-validation.log",
"TEMPEST_TEST_ACCOUNTS": "/var/lib/tempest/workspace/test_accounts.yaml",
"TEMPEST_WORKSPACE": "tempest",
"TEMPEST_WORKSPACE_PATH": "/var/lib/tempest/workspace",
"TEMPEST_CONFIG_OVERRIDES": "object-storage-feature-enabled.tempurl_digest_hashlib sha1",
}
charmcraft = (
pathlib.Path(__file__).parents[2] / "charmcraft.yaml"
).read_text()
config = yaml.dump(yaml.safe_load(charmcraft)["config"])
actions = yaml.dump(yaml.safe_load(charmcraft)["actions"])
class _TempestTestOperatorCharm(charm.TempestOperatorCharm):
"""Test Operator Charm for Tempest operator."""
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)
class TestTempestOperatorCharm(test_utils.CharmTestCase):
"""Classes for testing tempest charms."""
def setUp(self):
"""Setup Placement tests."""
super().setUp(charm, [])
self.harness = test_utils.get_harness(
_TempestTestOperatorCharm,
container_calls=self.container_calls,
charm_metadata=charmcraft,
charm_config=config,
charm_actions=actions,
)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
self.harness.set_leader()
self.patch_obj(utils.cleanup, "Connection")
self.patch_obj(
utils.cleanup, "_get_exclusion_resources"
).return_value = {"projects": set(), "users": set()}
def add_identity_ops_relation(self, harness):
"""Add identity resource relation."""
self.harness.charm.set_tempest_ready = Mock()
rel_id = harness.add_relation("identity-ops", "keystone")
harness.add_relation_unit(rel_id, "keystone/0")
harness.charm.user_id_ops.callback_f = Mock()
harness.charm.user_id_ops.get_user_credential = Mock(
return_value={
"username": "tempest",
"password": "password",
"domain-name": "tempest",
"domain-id": "tempest-domain-id",
"project-name": "CloudValidation-tempest",
"auth-url": "http://10.6.0.23/openstack-keystone/v3",
},
)
# Only show the list_endpoint ops for simplicity
harness.update_relation_data(
rel_id,
"keystone",
{
"response": json.dumps(
{
"id": "c8e02ce67f57057d1a0d6660c6571361eea1a03d749d021d33e13ea4b0a7982a",
"tag": "setup_tempest_resource",
"ops": [
{
"name": "some_other_ops",
"return-code": 0,
"value": "",
},
{
"name": "list_endpoint",
"return-code": 0,
"value": [
{
"id": "68c4eba8b01f41829d30cf2519998883",
"service_id": "b2a08eea7699460e838f7cce97529e55",
"interface": "admin",
"region": "RegionOne",
"url": "http://10.152.183.48:5000/v3",
"enabled": True,
}
],
},
],
}
)
},
)
return rel_id
def add_logging_relation(self, harness):
"""Add logging relation."""
rel_id = test_utils.add_complete_logging_relation(harness)
harness.charm.logging.interface = Mock()
harness.charm.logging.interface._promtail_config = Mock()
return rel_id
def add_grafana_dashboard_relation(self, harness):
"""Add grafana dashboard relation."""
rel_id = harness.add_relation("grafana-dashboard", "grafana")
harness.add_relation_unit(rel_id, "grafana/0")
harness.charm.grafana.interface = Mock()
return rel_id
def test_pebble_ready_handler(self):
"""Test Pebble ready event is captured."""
self.assertEqual(self.harness.charm.seen_events, [])
test_utils.set_all_pebbles_ready(self.harness)
self.assertEqual(self.harness.charm.seen_events, ["PebbleReadyEvent"])
def test_all_relations(self):
"""Test all integrations ready and okay for operator."""
test_utils.set_all_pebbles_ready(self.harness)
logging_rel_id = self.add_logging_relation(self.harness)
identity_ops_rel_id = self.add_identity_ops_relation(self.harness)
grafana_dashboard_rel_id = self.add_grafana_dashboard_relation(
self.harness
)
self.harness.charm.is_tempest_ready = Mock(return_value=True)
self.harness.update_config({"schedule": "0 0 */7 * *"})
config_files = [
"/etc/crontab",
"/usr/local/sbin/tempest-run-wrapper",
"/usr/local/sbin/tempest-init",
]
for f in config_files:
self.check_file(charm.CONTAINER, f)
self.assertEqual(self.harness.charm.status.message(), "")
self.assertEqual(self.harness.charm.status.status.name, "active")
self.harness.remove_relation(logging_rel_id)
self.harness.remove_relation(identity_ops_rel_id)
self.harness.remove_relation(grafana_dashboard_rel_id)
def test_config_context_schedule(self):
"""Test config context contains the schedule as expected."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
# schedule is disabled if it's not ready, so set it ready for testing
self.harness.charm.is_tempest_ready = Mock(return_value=True)
# ok schedule
schedule = "0 0 */7 * *"
self.harness.update_config({"schedule": schedule})
self.assertEqual(
self.harness.charm.contexts().tempest.schedule, schedule
)
# too frequent
schedule = "* * * * *"
self.harness.update_config({"schedule": schedule})
self.assertEqual(self.harness.charm.contexts().tempest.schedule, "")
# disabled
schedule = ""
self.harness.update_config({"schedule": schedule})
self.assertEqual(self.harness.charm.contexts().tempest.schedule, "")
# tempest init not ready
self.harness.charm.is_tempest_ready = Mock(return_value=False)
self.harness.charm.peers = Mock()
schedule = "0 0 */7 * *"
self.harness.update_config({"schedule": schedule})
self.assertEqual(self.harness.charm.contexts().tempest.schedule, "")
def test_validate_action_invalid_regex(self):
"""Test validate action with invalid regex provided."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
action_event = Mock()
action_event.params = {
"serial": False,
"regex": "test(",
"exclude-regex": "",
"test-list": "",
}
self.harness.charm._on_validate_action(action_event)
action_event.fail.assert_called_once()
self.assertEqual(
"'test(' is an invalid regex: missing ), unterminated subpattern at position 4",
action_event.set_results.call_args.args[0]["error"],
)
def test_validate_action_invalid_list(self):
"""Test validate action with invalid list provided."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
file1 = Mock()
file1.name = "file_1"
file2 = Mock()
file2.name = "file_2"
self.harness.charm.pebble_handler().container.list_files = Mock(
return_value=[file1, file2]
)
action_event = Mock()
action_event.params = {
"serial": False,
"regex": "",
"exclude-regex": "",
"test-list": "nonexistent",
}
self.harness.charm._on_validate_action(action_event)
action_event.fail.assert_called_once()
self.assertEqual(
"'nonexistent' is not a known test list. Please run list-tests action to view available lists.",
action_event.set_results.call_args.args[0]["error"],
)
@patch("charm.TEMPEST_CONCURRENCY", "4")
def test_validate_action_success(self):
"""Test validate action with default params."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
file1 = Mock()
file1.name = "file_1"
file2 = Mock()
file2.name = "file_2"
self.harness.charm.pebble_handler().container.list_files = Mock(
return_value=[file1, file2]
)
exec_mock = Mock()
self.harness.charm.pebble_handler().execute = exec_mock
action_event = Mock()
action_event.params = {
"serial": False,
"regex": "smoke",
"exclude-regex": "",
"test-list": "",
}
self.harness.charm._on_validate_action(action_event)
action_event.fail.assert_not_called()
exec_mock.assert_called_with(
["tempest-run-wrapper", "--parallel", "--regex", "smoke"],
user="tempest",
group="tempest",
working_dir=TEMPEST_HOME,
exception_on_error=True,
environment=TEST_TEMPEST_ENV,
)
@patch("charm.TEMPEST_CONCURRENCY", "4")
def test_validate_action_params(self):
"""Test validate action with more params."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
file1 = Mock()
file1.name = "file_1"
file2 = Mock()
file2.name = "file_2"
self.harness.charm.pebble_handler().container.list_files = Mock(
return_value=[file1, file2]
)
exec_mock = Mock()
self.harness.charm.pebble_handler().execute = exec_mock
action_event = Mock()
action_event.params = {
"serial": True,
"regex": "re1 re2",
"exclude-regex": "excludethis",
"test-list": "file_1",
}
self.harness.charm._on_validate_action(action_event)
action_event.fail.assert_not_called()
exec_mock.assert_called_with(
[
"tempest-run-wrapper",
"--serial",
"--regex",
"re1 re2",
"--exclude-regex",
"excludethis",
"--load-list",
"/tempest_test_lists/file_1",
],
user="tempest",
group="tempest",
working_dir=TEMPEST_HOME,
exception_on_error=True,
environment=TEST_TEMPEST_ENV,
)
def test_validate_action_no_params(self):
"""Test validate action with no filter params."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
exec_mock = Mock()
self.harness.charm.pebble_handler().execute = exec_mock
action_event = Mock()
action_event.params = {
"serial": True,
"regex": "",
"exclude-regex": "",
"test-list": "",
}
self.harness.charm._on_validate_action(action_event)
action_event.fail.assert_called_once()
self.assertIn(
"No filter parameters provided",
action_event.set_results.call_args.args[0]["error"],
)
exec_mock.assert_not_called()
def test_get_list_action(self):
"""Test get-list action."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
file1 = Mock()
file1.name = "file_1"
file2 = Mock()
file2.name = "file_2"
self.harness.charm.pebble_handler().container.list_files = Mock(
return_value=[file1, file2]
)
action_event = Mock()
self.harness.charm._on_get_lists_action(action_event)
action_event.fail.assert_not_called()
def test_get_list_action_not_ready(self):
"""Test get-list action when pebble is not ready."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
file1 = Mock()
file1.name = "file_1"
file2 = Mock()
file2.name = "file_2"
self.harness.charm.unit.get_container(CONTAINER).can_connect = Mock(
return_value=False
)
action_event = Mock()
self.harness.charm._on_get_lists_action(action_event)
action_event.fail.assert_called_with("pebble is not ready")
def test_blocked_status_invalid_schedule(self):
"""Test to verify blocked status with invalid schedule config."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
self.harness.charm.is_tempest_ready = Mock(return_value=True)
# invalid schedule should make charm in blocked status
self.harness.update_config({"schedule": "* *"})
self.assertIn("invalid schedule", self.harness.charm.status.message())
self.assertEqual(self.harness.charm.status.status.name, "blocked")
# updating the schedule to something valid should unblock it
self.harness.update_config({"schedule": "*/20 * * * *"})
self.assertEqual(self.harness.charm.status.message(), "")
self.assertEqual(self.harness.charm.status.status.name, "active")
def test_error_initing_tempest(self):
"""Test to verify blocked status if tempest init fails."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
self.harness.charm.peers = Mock()
self.harness.charm.peers.interface.peers_rel.data = MagicMock()
self.harness.charm.peers.interface.peers_rel.data.__getitem__.return_value = {
TEMPEST_READY_KEY: ""
}
mock_pebble = Mock()
mock_pebble.init_tempest = Mock(side_effect=RuntimeError)
self.harness.charm.pebble_handler = Mock(return_value=mock_pebble)
self.harness.charm.is_tempest_ready = Mock(return_value=False)
self.harness.update_config({"schedule": "*/21 * * * *"})
self.harness.charm.set_tempest_ready.assert_has_calls(
[call(False), call(False)]
)
self.assertEqual(self.harness.charm.set_tempest_ready.call_count, 2)
self.assertIn(
"tempest init failed", self.harness.charm.status.message()
)
self.assertEqual(self.harness.charm.status.status.name, "blocked")
def test_is_tempest_ready(self):
"""Test the tempest ready check method."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
# simulate tempest ready
self.harness.charm.peers = Mock()
self.harness.charm.peers.interface.peers_rel.data = MagicMock()
self.harness.charm.peers.interface.peers_rel.data.__getitem__.return_value = {
TEMPEST_READY_KEY: "true"
}
self.assertTrue(self.harness.charm.is_tempest_ready())
def test_is_tempest_ready_false(self):
"""Test the tempest ready check method."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_logging_relation(self.harness)
self.add_identity_ops_relation(self.harness)
self.add_grafana_dashboard_relation(self.harness)
# simulate tempest not ready
self.harness.charm.peers = Mock()
self.harness.charm.peers.interface.peers_rel.data = MagicMock()
self.harness.charm.peers.interface.peers_rel.data.__getitem__.return_value = {
TEMPEST_READY_KEY: ""
}
self.assertFalse(self.harness.charm.is_tempest_ready())
def test_set_tempest_ready(self):
"""Test the tempest ready set method."""
test_utils.set_all_pebbles_ready(self.harness)
self.harness.charm.peers = Mock()
self.harness.charm.set_tempest_ready(True)
self.harness.charm.peers.set_unit_data.assert_called_with(
{TEMPEST_READY_KEY: "true"}
)
self.harness.charm.peers = Mock()
self.harness.charm.set_tempest_ready(False)
self.harness.charm.peers.set_unit_data.assert_called_with(
{TEMPEST_READY_KEY: ""}
)
def test_init_tempest_fail(self):
"""Test the tempest init method logic."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_identity_ops_relation(self.harness)
# tempest init not run yet, pebble init tempest fails
pebble_mock = Mock()
pebble_mock.init_tempest = Mock(side_effect=RuntimeError)
self.harness.charm.pebble_handler = Mock(return_value=pebble_mock)
self.harness.charm.is_tempest_ready = Mock(return_value=False)
self.harness.charm.set_tempest_ready = Mock()
self.harness.charm.init_tempest()
self.harness.charm.set_tempest_ready.assert_called_once_with(False)
def test_init_tempest_success(self):
"""Test the tempest init method logic."""
test_utils.set_all_pebbles_ready(self.harness)
self.add_identity_ops_relation(self.harness)
# tempest init succeeds
pebble_mock = Mock()
pebble_mock.init_tempest = Mock()
self.harness.charm.pebble_handler = Mock(return_value=pebble_mock)
self.harness.charm.is_tempest_ready = Mock(return_value=False)
self.harness.charm.set_tempest_ready = Mock()
self.harness.charm.init_tempest()
self.harness.charm.set_tempest_ready.assert_called_once_with(True)
def test_init_tempest_already_run(self):
"""Test the tempest init method logic."""
test_utils.set_all_pebbles_ready(self.harness)
# tempest init already run
pebble_mock = Mock()
pebble_mock.init_tempest = Mock()
self.harness.charm.pebble_handler = Mock(return_value=pebble_mock)
self.harness.charm.is_tempest_ready = Mock(return_value=True)
self.harness.charm.set_tempest_ready = Mock()
self.harness.charm.init_tempest()
self.harness.charm.set_tempest_ready.assert_not_called()
def test_start(self):
"""Test start charm updates things as required."""
test_utils.set_all_pebbles_ready(self.harness)
self.harness.charm.set_tempest_ready = Mock()
self.harness.charm._on_start(Mock())
self.harness.charm.set_tempest_ready.assert_called_once_with(False)
def test_upgrade_charm(self):
"""Test upgrade charm updates things as required."""
test_utils.set_all_pebbles_ready(self.harness)
self.harness.charm.set_tempest_ready = Mock()
self.harness.charm._on_upgrade_charm(Mock())
self.harness.charm.set_tempest_ready.assert_called_once_with(False)
def test_tempest_env_variant(self):
"""Test env variant for tempest returns correct path."""
self.assertEqual(
TempestEnvVariant.PERIODIC.output_path(), TEMPEST_PERIODIC_OUTPUT
)
self.assertEqual(
TempestEnvVariant.ADHOC.output_path(), TEMPEST_ADHOC_OUTPUT
)
def test_remove_identity_triggers_tempest_no_longer_ready(self):
"""Removing the keystone relation causes tempest no longer ready."""
test_utils.set_all_pebbles_ready(self.harness)
identity_ops_rel_id = self.add_identity_ops_relation(self.harness)
self.harness.charm.set_tempest_ready = Mock()
self.harness.remove_relation(identity_ops_rel_id)
self.harness.charm.set_tempest_ready.assert_called_once_with(False)
@patch("utils.constants.cpu_count", Mock(return_value=2))
def test_concurrency_calculation_less_cpus(self):
"""Test concurrency is calculated correctly with only 2 cpus."""
self.assertEqual(get_tempest_concurrency(), "2")
@patch("utils.constants.cpu_count", Mock(return_value=8))
def test_concurrency_calculation_more_cpus(self):
"""Test concurrency is bounded to 4."""
self.assertEqual(get_tempest_concurrency(), "4")
def test_logging_ready(self):
"""Test logging relation ready."""
rel_id = self.add_logging_relation(self.harness)
# client endpoints found
self.harness.charm.logging.interface._promtail_config.return_value = {
"clients": [
{
"url": "http://grafana-agent-k8s-endpoints:3500/loki/api/v1/push"
}
],
"other_key": "other_values",
}
self.assertEqual(self.harness.charm.logging.ready, True)
# empty client endpoints
self.harness.charm.logging.interface._promtail_config.return_value = {
"clients": [],
"other_key": "other_values",
}
self.assertEqual(self.harness.charm.logging.ready, False)
# empty promtail config
self.harness.remove_relation(rel_id)
self.harness.charm.logging.interface._promtail_config.return_value = {}
self.assertEqual(self.harness.charm.logging.ready, False)