Add config option to disable cinder tests

This commit adds a user config option "roles" that allows the user
to skip certain tests that do not pertain to the Sunbeam node roles.
For instance, if there is no storage role defined, the cinder tests
will be skipped by Tempest rather than appearing as failed tests.

Summary of changes:
charmcraft.yaml
  - add user config option "roles" (default: storage,compute,control)
charm.py
  - append user-set config roles to Tempest configuration overrides
  - trigger a charm rebuild on config change
  - add check that only rebuilds charm if there is a config change,
    avoiding a costly rebuild when it is not necessary
tempest-run-wrapper.j2
  - set discover-tempest-config to consume TEMPEST_CONFIG_OVERRIDES
tempest-init-j2
  - set discover-tempest-config to consume TEMPEST_CONFIG_OVERRIDES
test_tempest_charm.py
  - add unit tests for various role configurations
  - add unit test for hash comparison

Change-Id: I1f14ae77a3bc043b3a194ca9308a22f1e66c7907
This commit is contained in:
Myles Penner
2025-05-05 16:29:38 -07:00
parent 2470b3ded7
commit b158f97936
6 changed files with 228 additions and 5 deletions

View File

@@ -47,6 +47,13 @@ config:
"*/30 * * * *" every 30 minutes
"5 2 * * *" at 2:05am every day
"5 2 * * mon" at 2:05am every Monday
roles:
type: string
default: storage,compute,control
description: |
A comma-separated list of Sunbeam node roles to be used for enabling
tempest tests. Removing a role from the list will disable tempest tests
relating to that role. Valid roles are: storage, compute, control.
actions:
validate:

View File

@@ -19,6 +19,8 @@
This charm provide Tempest as part of an OpenStack deployment
"""
import hashlib
import json
import logging
import os
from typing import (
@@ -71,6 +73,7 @@ from utils.constants import (
)
from utils.overrides import (
get_compute_overrides,
get_role_based_overrides,
get_swift_overrides,
)
from utils.types import (
@@ -112,6 +115,7 @@ class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"""Run the constructor."""
# config for openstack, used by tempest
super().__init__(framework)
self._state.set_default(config_rebuild_hash="")
self.framework.observe(
self.on.validate_action, self._on_validate_action
)
@@ -247,8 +251,9 @@ class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
(
get_swift_overrides(),
get_compute_overrides(),
get_role_based_overrides(self.config["roles"]),
)
)
).strip()
def _get_environment_for_tempest(
self, variant: TempestEnvVariant
@@ -369,6 +374,11 @@ class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
f"invalid schedule config: {schedule.err}"
)
# Only trigger rebuild if config options change
updated_hash = self._current_config_hash()
if updated_hash != self._state.config_rebuild_hash:
self.set_tempest_ready(False)
self.status.set(MaintenanceStatus("tempest init in progress"))
self.init_tempest()
@@ -391,6 +401,7 @@ class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
for relation in self.model.relations[LOKI_RELATION_NAME]:
self.logging.interface._handle_alert_rules(relation)
self._state.config_rebuild_hash = updated_hash
self.status.set(ActiveStatus(""))
logger.info("Finished configuring the tempest environment")
@@ -460,6 +471,19 @@ class TempestOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
# display neatly to the user. This will also end up in the action output results.stdout
print("\n".join(lists))
def _get_relevant_tempest_config_values(self) -> dict:
"""Return values that should trigger a rebuild."""
return {
"roles": self.config.get("roles"),
"region": self.config.get("region"),
}
def _current_config_hash(self) -> str:
data = self._get_relevant_tempest_config_values()
blob = json.dumps(data, sort_keys=True).encode()
logger.info(f"Hashing config data: {blob}")
return hashlib.sha256(blob).hexdigest()
if __name__ == "__main__": # pragma: nocover
ops.main(TempestOperatorCharm)

View File

@@ -14,7 +14,7 @@ rm -rf "${TEMPEST_HOME}/.tempest"
tempest init --name "$TEMPEST_WORKSPACE" "$TEMPEST_WORKSPACE_PATH"
discover-tempest-config --out "$TEMPEST_CONF" --create $TEMPEST_CONFIG_OVERRIDES
discover-tempest-config --out "$TEMPEST_CONF" --create ${TEMPEST_CONFIG_OVERRIDES:+$TEMPEST_CONFIG_OVERRIDES}
tempest account-generator -r "$TEMPEST_ACCOUNTS_COUNT" -c "$TEMPEST_CONF" "$TEMPEST_TEST_ACCOUNTS"

View File

@@ -20,8 +20,7 @@ rm -rf /tmp/tempest-lock/
TMP_FILE="$(mktemp)"
echo ":: discover-tempest-config" >> "$TMP_FILE"
if discover-tempest-config --test-accounts "$TEMPEST_TEST_ACCOUNTS" --out "$TEMPEST_CONF" >> "$TMP_FILE" 2>&1; then
echo ":: tempest run" >> "$TMP_FILE"
if discover-tempest-config --test-accounts "$TEMPEST_TEST_ACCOUNTS" --out "$TEMPEST_CONF" ${TEMPEST_CONFIG_OVERRIDES:+$TEMPEST_CONFIG_OVERRIDES} >> "$TMP_FILE" 2>&1; then
tempest run --exclude-list "$TEMPEST_EXCLUDE_LIST" --workspace "$TEMPEST_WORKSPACE" -w "$TEMPEST_CONCURRENCY" "$@" >> "$TMP_FILE" 2>&1
python3 "$TEMPEST_HOME/cleanup.py" quick
else

View File

@@ -13,6 +13,18 @@
# limitations under the License.
"""Tempest configuration overrides."""
import logging
from typing import (
List,
Set,
)
from ops_sunbeam.guard import (
BlockedExceptionError,
)
logger = logging.getLogger(__name__)
def get_swift_overrides() -> str:
"""Return swift configuration override.
@@ -34,3 +46,57 @@ def get_compute_overrides() -> str:
"compute-feature-enabled.cold_migration false", # lp:2082056
)
)
def get_role_based_overrides(config_roles: str) -> str:
"""Generate tempest.conf overrides based on the configured roles.
:param configured_roles: A set of role strings, e.g., {"compute", "storage"}.
:return: A string of space-separated key-value pairs for overrides.
"""
configured_roles = _parse_roles_config(config_roles)
overrides: List[str] = []
if "storage" not in configured_roles:
logger.info("Storage role not configured, disabling cinder tests.")
overrides.extend(["service_available.cinder", "false"])
if "compute" not in configured_roles:
logger.info("Compute role not configured, disabling nova tests.")
overrides.extend(["service_available.nova", "false"])
return " ".join(overrides)
def _parse_roles_config(config_roles: str) -> Set[str]:
"""Parses the 'roles' config string from the charm's configuration.
:param config_roles: The charm's config object (self.config in the charm).
:return: A set of lower-case role strings.
"""
if not config_roles.strip():
error_message = (
"Config option 'roles' must contain at least one role "
"(compute, control, storage)."
)
logger.error(error_message)
raise BlockedExceptionError(error_message)
parsed_user_roles = {
role.strip().lower()
for role in config_roles.split(",")
if role.strip()
}
valid_roles = {"compute", "control", "storage"}
invalid_user_roles = parsed_user_roles - valid_roles
if invalid_user_roles:
error_message = (
f"Invalid roles specified in 'roles' config: {', '.join(invalid_user_roles)}. "
f"Valid roles are: {', '.join(valid_roles)}."
)
logger.error(error_message)
raise BlockedExceptionError(error_message)
return parsed_user_roles

View File

@@ -29,6 +29,9 @@ import charm
import ops_sunbeam.test_utils as test_utils
import utils
import yaml
from ops_sunbeam.guard import (
BlockedExceptionError,
)
from utils import (
overrides,
)
@@ -489,7 +492,7 @@ class TestTempestOperatorCharm(test_utils.CharmTestCase):
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.assertEqual(self.harness.charm.set_tempest_ready.call_count, 3)
self.assertIn(
"tempest init failed", self.harness.charm.status.message()
)
@@ -659,3 +662,127 @@ class TestTempestOperatorCharm(test_utils.CharmTestCase):
self.harness.remove_relation(rel_id)
self.harness.charm.logging.interface._promtail_config.return_value = {}
self.assertEqual(self.harness.charm.logging.ready, False)
def _check_override_in_string(
self,
key: str,
value: str,
actual_string: str,
should_be_present: bool = True,
):
"""Checks if a specific 'key value' pair is in the override string."""
expected_pair = f"{key} {value}"
if should_be_present:
self.assertIn(
expected_pair,
actual_string,
f"Expected '{expected_pair}' to be in '{actual_string}'",
)
else:
self.assertNotIn(
expected_pair,
actual_string,
f"Expected '{expected_pair}' NOT to be in '{actual_string}'",
)
def test_roles_default_overrides(self):
"""Verify role-based overrides when roles config is default (all roles)."""
self.add_identity_ops_relation(self.harness)
test_utils.set_all_pebbles_ready(self.harness)
self.harness.update_config({"roles": "compute,control,storage"})
env = self.harness.charm._get_environment_for_tempest(
TempestEnvVariant.ADHOC
)
actual_combined_overrides = env.get("TEMPEST_CONFIG_OVERRIDES", "")
self._check_override_in_string(
"service_available.cinder",
"false",
actual_combined_overrides,
should_be_present=False,
)
self._check_override_in_string(
"service_available.nova",
"false",
actual_combined_overrides,
should_be_present=False,
)
def test_roles_compute_only_overrides(self):
"""Verify role-based overrides when roles = 'compute'."""
self.add_identity_ops_relation(self.harness)
test_utils.set_all_pebbles_ready(self.harness)
self.harness.update_config({"roles": "compute"})
env = self.harness.charm._get_environment_for_tempest(
TempestEnvVariant.ADHOC
)
actual_combined_overrides = env.get("TEMPEST_CONFIG_OVERRIDES", "")
self._check_override_in_string(
"service_available.cinder",
"false",
actual_combined_overrides,
should_be_present=True,
)
self._check_override_in_string(
"service_available.nova",
"false",
actual_combined_overrides,
should_be_present=False,
)
def test_roles_storage_only_overrides(self):
"""Verify role-based overrides when roles = 'storage'."""
self.add_identity_ops_relation(self.harness)
test_utils.set_all_pebbles_ready(self.harness)
self.harness.update_config({"roles": "storage"})
env = self.harness.charm._get_environment_for_tempest(
TempestEnvVariant.ADHOC
)
actual_combined_overrides = env.get("TEMPEST_CONFIG_OVERRIDES", "")
self._check_override_in_string(
"service_available.cinder",
"false",
actual_combined_overrides,
should_be_present=False,
)
self._check_override_in_string(
"service_available.nova",
"false",
actual_combined_overrides,
should_be_present=True,
)
def test_roles_blank_raises(self):
"""Blank 'roles' config should raise a BlockedExceptionError."""
with self.assertRaises(BlockedExceptionError):
overrides._parse_roles_config("")
def test_current_config_hash_changes(self):
"""Hash is stable for unchanged config and updates when roles/region change."""
initial_roles = "compute,control"
initial_region = "RegionOne"
self.harness.update_config(
{"roles": initial_roles, "region": initial_region}
)
hash_1 = self.harness.charm._current_config_hash()
self.harness.update_config(
{"roles": initial_roles, "region": initial_region}
)
hash_2 = self.harness.charm._current_config_hash()
self.assertEqual(hash_1, hash_2)
self.harness.update_config({"roles": "compute"})
hash_3 = self.harness.charm._current_config_hash()
self.assertNotEqual(hash_1, hash_3)
self.harness.update_config(
{"roles": initial_roles, "region": "RegionTwo"}
)
hash_4 = self.harness.charm._current_config_hash()
self.assertNotEqual(hash_1, hash_4)