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:
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user