diff --git a/config.yaml b/config.yaml index fc4ab50dc..a6a4b7340 100644 --- a/config.yaml +++ b/config.yaml @@ -36,3 +36,13 @@ options: default: true type: boolean description: Enable opensearch-knn + + profile: + type: string + default: "production" + description: | + Profile representing the scope of deployment, and used to tune resource allocation. + Allowed values are: "production", "staging" or "testing" + Production will tune opensearch for maximum performance while default will tune for + minimal running performance. + Performance tuning is described on: https://opensearch.org/docs/latest/tuning-your-cluster/performance/ diff --git a/lib/charms/opensearch/v0/constants_charm.py b/lib/charms/opensearch/v0/constants_charm.py index b24aebdc2..1b75ed268 100644 --- a/lib/charms/opensearch/v0/constants_charm.py +++ b/lib/charms/opensearch/v0/constants_charm.py @@ -118,3 +118,5 @@ # User-face Backup ID format OPENSEARCH_BACKUP_ID_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + +PERFORMANCE_PROFILE = "profile" diff --git a/lib/charms/opensearch/v0/helper_conf_setter.py b/lib/charms/opensearch/v0/helper_conf_setter.py index 12876f8d9..a78102b23 100755 --- a/lib/charms/opensearch/v0/helper_conf_setter.py +++ b/lib/charms/opensearch/v0/helper_conf_setter.py @@ -272,7 +272,6 @@ def replace( output_file: Target file for the result config, by default same as config_file """ path = f"{self.base_path}{config_file}" - if not exists(path): raise FileNotFoundError(f"{path} not found.") @@ -290,7 +289,7 @@ def replace( logger.info(data) if output_file is None: - output_file = config_file + output_file = path with open(output_file, "w") as f: f.write(data) diff --git a/lib/charms/opensearch/v0/models.py b/lib/charms/opensearch/v0/models.py index 663af8e68..5474fded8 100644 --- a/lib/charms/opensearch/v0/models.py +++ b/lib/charms/opensearch/v0/models.py @@ -23,6 +23,10 @@ LIBPATCH = 1 +MIN_HEAP_SIZE = 1024 * 1024 # 1GB in KB +MAX_HEAP_SIZE = 32 * MIN_HEAP_SIZE # 32GB in KB + + class Model(ABC, BaseModel): """Base model class.""" @@ -153,6 +157,14 @@ class DeploymentType(BaseStrEnum): OTHER = "other" +class PerformanceType(BaseStrEnum): + """Performance types available.""" + + PRODUCTION = "production" + STAGING = "staging" + TESTING = "testing" + + class StartMode(BaseStrEnum): """Mode of start of units in this deployment.""" @@ -204,6 +216,10 @@ class PeerClusterConfig(Model): cluster_name: str init_hold: bool roles: List[str] + # We have a breaking change in the model + # For older charms, this field will not exist and they will be set in the + # profile called "testing". + profile: Optional[PerformanceType] = PerformanceType.TESTING data_temperature: Optional[str] = None @root_validator @@ -346,3 +362,60 @@ def promote_failover(self) -> None: self.main_app = self.failover_app self.main_rel_id = self.failover_rel_id self.delete("failover") + + +class OpenSearchPerfProfile(Model): + """Generates an immutable description of the performance profile.""" + + typ: PerformanceType + heap_size_in_kb: int = MIN_HEAP_SIZE + opensearch_yml: Dict[str, str] = {} + charmed_index_template: Dict[str, str] = {} + charmed_component_templates: Dict[str, str] = {} + + @root_validator + def set_options(cls, values): # noqa: N805 + """Generate the attributes depending on the input.""" + # Check if PerformanceType has been rendered correctly + # if an user creates the OpenSearchPerfProfile + if "typ" not in values: + raise AttributeError("Missing 'typ' attribute.") + + if values["typ"] == PerformanceType.TESTING: + values["heap_size_in_kb"] = MIN_HEAP_SIZE + return values + + mem_total = OpenSearchPerfProfile.meminfo()["MemTotal"] + mem_percent = 0.50 if values["typ"] == PerformanceType.PRODUCTION else 0.25 + + values["heap_size_in_kb"] = min(int(mem_percent * mem_total), MAX_HEAP_SIZE) + + if values["typ"] != PerformanceType.TESTING: + values["opensearch_yml"] = {"indices.memory.index_buffer_size": "25%"} + + values["charmed_index_template"] = { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": { + "settings": { + "number_of_replicas": "1", + }, + }, + }, + } + + return values + + @staticmethod + def meminfo() -> dict[str, float]: + """Read the /proc/meminfo file and return the values. + + According to the kernel source code, the values are always in kB: + https://github.com/torvalds/linux/blob/ + 2a130b7e1fcdd83633c4aa70998c314d7c38b476/fs/proc/meminfo.c#L31 + """ + with open("/proc/meminfo") as f: + meminfo = f.read().split("\n") + meminfo = [line.split() for line in meminfo if line.strip()] + + return {line[0][:-1]: float(line[1]) for line in meminfo} diff --git a/lib/charms/opensearch/v0/opensearch_base_charm.py b/lib/charms/opensearch/v0/opensearch_base_charm.py index 13ab35d58..0fc091f77 100644 --- a/lib/charms/opensearch/v0/opensearch_base_charm.py +++ b/lib/charms/opensearch/v0/opensearch_base_charm.py @@ -11,6 +11,7 @@ from charms.grafana_agent.v0.cos_agent import COSAgentProvider from charms.opensearch.v0.constants_charm import ( + PERFORMANCE_PROFILE, AdminUser, AdminUserInitProgress, AdminUserNotConfigured, @@ -49,7 +50,11 @@ generate_hashed_password, generate_password, ) -from charms.opensearch.v0.models import DeploymentDescription, DeploymentType +from charms.opensearch.v0.models import ( + DeploymentDescription, + DeploymentType, + PerformanceType, +) from charms.opensearch.v0.opensearch_backups import backup from charms.opensearch.v0.opensearch_config import OpenSearchConfig from charms.opensearch.v0.opensearch_distro import OpenSearchDistribution @@ -74,6 +79,7 @@ OpenSearchProvidedRolesException, StartMode, ) +from charms.opensearch.v0.opensearch_performance_profile import OpenSearchPerformance from charms.opensearch.v0.opensearch_plugin_manager import OpenSearchPluginManager from charms.opensearch.v0.opensearch_plugins import OpenSearchPluginError from charms.opensearch.v0.opensearch_relation_peer_cluster import ( @@ -246,6 +252,8 @@ def __init__(self, *args, distro: Type[OpenSearchDistribution] = None): metrics_rules_dir="./src/alert_rules/prometheus", log_slots=["opensearch:logs"], ) + + self.performance_profile = OpenSearchPerformance(self) # Ensure that only one instance of the `_on_peer_relation_changed` handler exists # in the deferred event queue self._is_peer_rel_changed_deferred = False @@ -657,8 +665,19 @@ def _on_update_status(self, event: UpdateStatusEvent): # noqa: C901 # handle when/if certificates are expired self._check_certs_expiration(event) + def trigger_restart(self): + """Trigger a restart of the service.""" + self._restart_opensearch_event.emit() + def _on_config_changed(self, event: ConfigChangedEvent): # noqa C901 """On config changed event. Useful for IP changes or for user provided config changes.""" + if not self.performance_profile.current: + # We are running (1) install or (2) an upgrade on instance that pre-dates profile + # First, we set this unit's effective profile -> 1G heap and no index templates. + # Our goal is to make sure this value exists once the refresh is finished + # and it represents the accurate value for this unit. + self.performance_profile.current = PerformanceType.TESTING + if self.opensearch_config.update_host_if_needed(): self.status.set(MaintenanceStatus(TLSNewCertsRequested)) self.tls.delete_stored_tls_resources() @@ -680,10 +699,19 @@ def _on_config_changed(self, event: ConfigChangedEvent): # noqa C901 # handle cluster change to main-orchestrator (i.e: init_hold: true -> false) self._handle_change_to_main_orchestrator_if_needed(event, previous_deployment_desc) - # todo: handle gracefully configuration setting at start of the charm - if not self.plugin_manager.check_plugin_manager_ready(): + if self.upgrade_in_progress: + # The following changes in _on_config_changed are not supported during an upgrade + # Therefore, we leave now + logger.warning( + "Changing config during an upgrade is not supported. The charm may be in a broken, " + "unrecoverable state" + ) + event.defer() return + perf_profile_needs_restart = False + plugin_needs_restart = False + try: if not self.plugin_manager.check_plugin_manager_ready(): raise OpenSearchNotFullyReadyError() @@ -691,16 +719,7 @@ def _on_config_changed(self, event: ConfigChangedEvent): # noqa C901 if self.unit.is_leader(): self.status.set(MaintenanceStatus(PluginConfigCheck), app=True) - if self.plugin_manager.run(): - if self.upgrade_in_progress: - logger.warning( - "Changing config during an upgrade is not supported. The charm may be in a broken, " - "unrecoverable state" - ) - event.defer() - return - - self._restart_opensearch_event.emit() + plugin_needs_restart = self.plugin_manager.run() except (OpenSearchNotFullyReadyError, OpenSearchPluginError) as e: if isinstance(e, OpenSearchNotFullyReadyError): logger.warning("Plugin management: cluster not ready yet at config changed") @@ -711,11 +730,16 @@ def _on_config_changed(self, event: ConfigChangedEvent): # noqa C901 # config-changed is called again. if self.unit.is_leader(): self.status.clear(PluginConfigCheck, app=True) - return + else: + if self.unit.is_leader(): + self.status.clear(PluginConfigCheck, app=True) + self.status.clear(PluginConfigChangeError, app=True) - if self.unit.is_leader(): - self.status.clear(PluginConfigCheck, app=True) - self.status.clear(PluginConfigChangeError, app=True) + perf_profile_needs_restart = self.performance_profile.apply( + self.config.get(PERFORMANCE_PROFILE) + ) + if plugin_needs_restart or perf_profile_needs_restart: + self._restart_opensearch_event.emit() def _on_set_password_action(self, event: ActionEvent): """Set new admin password from user input or generate if not passed.""" diff --git a/lib/charms/opensearch/v0/opensearch_config.py b/lib/charms/opensearch/v0/opensearch_config.py index a9ffa2d89..3b330041d 100644 --- a/lib/charms/opensearch/v0/opensearch_config.py +++ b/lib/charms/opensearch/v0/opensearch_config.py @@ -8,7 +8,7 @@ from charms.opensearch.v0.constants_tls import CertType from charms.opensearch.v0.helper_security import normalized_tls_subject -from charms.opensearch.v0.models import App +from charms.opensearch.v0.models import App, OpenSearchPerfProfile from charms.opensearch.v0.opensearch_distro import OpenSearchDistribution # The unique Charmhub library identifier, never change it @@ -69,6 +69,25 @@ def set_client_auth(self): "-Djdk.tls.client.protocols=TLSv1.2", ) + def apply_performance_profile(self, profile: OpenSearchPerfProfile): + """Apply the performance profile to the opensearch config.""" + self._opensearch.config.replace( + self.JVM_OPTIONS, + "-Xms[0-9]+[kmgKMG]", + f"-Xms{str(profile.heap_size_in_kb)}k", + regex=True, + ) + + self._opensearch.config.replace( + self.JVM_OPTIONS, + "-Xmx[0-9]+[kmgKMG]", + f"-Xmx{str(profile.heap_size_in_kb)}k", + regex=True, + ) + + for key, val in profile.opensearch_yml.items(): + self._opensearch.config.put(self.CONFIG_YML, key, val) + def set_admin_tls_conf(self, secrets: Dict[str, any]): """Configures the admin certificate.""" self._opensearch.config.put( diff --git a/lib/charms/opensearch/v0/opensearch_peer_clusters.py b/lib/charms/opensearch/v0/opensearch_peer_clusters.py index 76969701f..11fbd281d 100644 --- a/lib/charms/opensearch/v0/opensearch_peer_clusters.py +++ b/lib/charms/opensearch/v0/opensearch_peer_clusters.py @@ -165,6 +165,7 @@ def _user_config(self): for option in self._charm.config.get("roles", "").split(",") if option ], + profile=self._charm.performance_profile.current.typ.value, ) def _new_cluster_setup(self, config: PeerClusterConfig) -> DeploymentDescription: @@ -222,6 +223,7 @@ def _new_cluster_setup(self, config: PeerClusterConfig) -> DeploymentDescription init_hold=config.init_hold, roles=config.roles, data_temperature=config.data_temperature, + profile=self._charm.performance_profile.current.typ.value, ), start=start_mode, pending_directives=directives, @@ -270,6 +272,7 @@ def _existing_cluster_setup( init_hold=prev_deployment.config.init_hold, roles=config.roles, data_temperature=config.data_temperature, + profile=self._charm.performance_profile.current.typ.value, ), start=start_mode, state=deployment_state, diff --git a/lib/charms/opensearch/v0/opensearch_performance_profile.py b/lib/charms/opensearch/v0/opensearch_performance_profile.py new file mode 100644 index 000000000..1dcae0b66 --- /dev/null +++ b/lib/charms/opensearch/v0/opensearch_performance_profile.py @@ -0,0 +1,178 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Represents the performance profile of the OpenSearch cluster. + +The main goals of this library is to provide a way to manage the performance +profile of the OpenSearch cluster. + +There are two ways the charm can learn about its profile and when it changes: +1) If this is the MAIN_ORCHESTRATOR: config-changed -> the user has switched the profile directly +2) If not the MAIN_ORCHESTRATOR: peer-cluster-relation-changed -> the main orchestrator has + switched the profile + +The charm will then apply the profile and restart the OpenSearch service if needed. +""" +import logging + +import ops +from charms.opensearch.v0.constants_charm import PERFORMANCE_PROFILE +from charms.opensearch.v0.models import ( + DeploymentType, + OpenSearchPerfProfile, + PerformanceType, +) +from charms.opensearch.v0.opensearch_exceptions import OpenSearchHttpError +from charms.opensearch.v0.opensearch_internal_data import Scope +from ops.framework import EventBase, EventSource + +# The unique Charmhub library identifier, never change it +LIBID = "8b7aa39016e748ea908787df1d7fb089" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + + +logger = logging.getLogger(__name__) + + +class _ApplyProfileTemplatesOpenSearch(EventBase): + """Attempt to apply the profile templates. + + The main reason to have a separate event, is to be able to wait for the cluster to restart. + In this case, deferring this event does not defer any major other event. + """ + + +class OpenSearchPerformance(ops.Object): + """Base class for OpenSearch charms.""" + + _apply_profile_templates_event = EventSource(_ApplyProfileTemplatesOpenSearch) + + def __init__(self, charm: ops.charm.CharmBase = None): + super().__init__(charm, None) + self.charm = charm + self.peers_data = self.charm.peers_data + self.framework.observe( + self._apply_profile_templates_event, self._on_apply_profile_templates + ) + self._apply_profile_templates_has_been_called = False + + @property + def current(self) -> OpenSearchPerfProfile | None: + """Return the current performance profile. + + The profile is saved as a string in the charm peer databag. + """ + if not self.peers_data.get(Scope.UNIT, PERFORMANCE_PROFILE): + return None + return OpenSearchPerfProfile.from_dict( + {"typ": self.peers_data.get(Scope.UNIT, PERFORMANCE_PROFILE)} + ) + + @current.setter + def current(self, value: OpenSearchPerfProfile | str): + """Set the current performance profile.""" + if isinstance(value, OpenSearchPerfProfile): + value = value.typ + elif isinstance(value, str): + # Ensure the value is a valid one + value = PerformanceType(value) + + self.peers_data.put(Scope.UNIT, PERFORMANCE_PROFILE, str(value)) + + def apply(self, profile_name: str) -> bool: + """Apply the performance profile. + + If returns True, then the caller must execute a restart. + """ + logger.info(f"Current profile: {str(self.current)}, new proposed profile: {profile_name}") + new_profile = OpenSearchPerfProfile.from_dict( + { + "typ": profile_name, + } + ) + if self.current == new_profile: + # Nothing to do, nothing changes + return False + + self.charm.opensearch_config.apply_performance_profile(new_profile) + self.current = new_profile + + if self.charm.opensearch_peer_cm.deployment_desc().typ == DeploymentType.MAIN_ORCHESTRATOR: + self._apply_profile_templates_event.emit() + return True + + def _on_apply_profile_templates(self, event: EventBase): + """Apply the profile templates. + + The main reason to have a separate event, is to be able to wait for the cluster. It + defers otherwise and only defers the execution of this particular task. + """ + logger.info("Applying profile-templates") + if not self.charm.unit.is_leader(): + # Only the leader can apply the templates + return + + if self._apply_profile_templates_has_been_called: + # we can safely abandon this event as we already had a previous call on the same hook + return + self._apply_profile_templates_has_been_called = True + + if ( + not self.charm.opensearch_peer_cm.deployment_desc() + or not self.charm.opensearch.is_node_up() + ): + logger.info("Applying profile templates but cluster not ready yet.") + event.defer() + return + + # Configure templates if needed + if not self.apply_perf_templates_if_needed(): + logger.debug("Failed to apply templates. Will retry later.") + event.defer() + + def apply_perf_templates_if_needed( # noqa: C901 + self, new_profile: PerformanceType | None = None + ) -> bool: + """Apply performance templates if needed.""" + profile = new_profile or self.current.typ + if not profile: + return False + + if profile == PerformanceType.TESTING: + # We try to remove the index and components' templates + for endpoint in [ + "/_index_template/charmed-index-tpl", + ]: + try: + self.charm.opensearch.request("DELETE", endpoint) + except OpenSearchHttpError as e: + if e.response_code != 404: + logger.warning(f"Failed to delete index template: {e}") + return False + # Nothing to do anymore + return True + + for idx, template in self.current.charmed_index_template.items(): + try: + # We can re-run PUT on the same index template + # It just gets updated and returns "ack: True" + self.charm.opensearch.request("PUT", f"/_index_template/{idx}", template) + except OpenSearchHttpError as e: + logger.error(f"Failed to apply index template: {e}") + return False + + for idx, template in self.current.charmed_component_templates.items(): + try: + # We can re-run PUT on the same template + # It just gets updated and returns "ack: True" + self.charm.opensearch.request("PUT", f"/_component_template/{idx}", template) + except OpenSearchHttpError as e: + logger.error(f"Failed to apply component template: {e}") + return False + return True diff --git a/lib/charms/opensearch/v0/opensearch_plugin_manager.py b/lib/charms/opensearch/v0/opensearch_plugin_manager.py index 9d53e94a0..97171c694 100644 --- a/lib/charms/opensearch/v0/opensearch_plugin_manager.py +++ b/lib/charms/opensearch/v0/opensearch_plugin_manager.py @@ -388,7 +388,7 @@ def _is_enabled(self, plugin: OpenSearchPlugin) -> bool: if any(k in keys_available for k in keys_to_del): return False except (KeyError, OpenSearchPluginError) as e: - logger.warning(f"_is_enabled: error with {e}") + logger.debug(f"_is_enabled: error with {e}") return False return True diff --git a/src/charm.py b/src/charm.py index 0ac3955af..66e572b44 100755 --- a/src/charm.py +++ b/src/charm.py @@ -9,6 +9,7 @@ import ops from charms.opensearch.v0.constants_charm import InstallError, InstallProgress +from charms.opensearch.v0.models import PerformanceType from charms.opensearch.v0.opensearch_base_charm import OpenSearchBaseCharm from charms.opensearch.v0.opensearch_exceptions import OpenSearchInstallError from ops.charm import InstallEvent @@ -136,6 +137,13 @@ def _set_upgrade_status(self): logger.debug(f"Set app status to {self.app.status}") def _on_upgrade_charm(self, _): + if not self.performance_profile.current: + # We are running (1) install or (2) an upgrade on instance that pre-dates profile + # First, we set this unit's effective profile -> 1G heap and no index templates. + # Our goal is to make sure this value exists once the refresh is finished + # and it represents the accurate value for this unit. + self.performance_profile.current = PerformanceType.TESTING + if self._unit_lifecycle.authorized_leader: if not self._upgrade.in_progress: logger.info("Charm upgraded. OpenSearch version unchanged") diff --git a/src/opensearch.py b/src/opensearch.py index 735940938..fb06a8762 100644 --- a/src/opensearch.py +++ b/src/opensearch.py @@ -88,7 +88,7 @@ def is_service_started(self, paused: Optional[bool] = False) -> bool: # Now, we must dig deeper into the actual status of systemd and the JVM process. # First, we want to make sure the process is not stopped, dead or zombie. try: - pid = run_cmd("lsof", args="-ti:9200").rstrip() + pid = run_cmd("lsof", args="-ti:9200").out.rstrip() if not pid or not os.path.exists(f"/proc/{pid}/stat"): return False with open(f"/proc/{pid}/stat") as f: diff --git a/tests/integration/ha/test_backups.py b/tests/integration/ha/test_backups.py index 35e6180b9..cc90238ea 100644 --- a/tests/integration/ha/test_backups.py +++ b/tests/integration/ha/test_backups.py @@ -37,6 +37,7 @@ from ..ha.test_horizontal_scaling import IDLE_PERIOD from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, get_leader_unit_id, @@ -235,7 +236,7 @@ async def test_small_deployment_build_and_deploy( await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ops_test.model.deploy(S3_INTEGRATOR, channel=S3_INTEGRATOR_CHANNEL), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. @@ -293,17 +294,21 @@ async def test_large_deployment_build_and_deploy( application_name="main", num_units=1, series=SERIES, - config=main_orchestrator_conf, + config=main_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name="failover", num_units=2, series=SERIES, - config=failover_orchestrator_conf, + config=failover_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( - my_charm, application_name=APP_NAME, num_units=1, series=SERIES, config=data_hot_conf + my_charm, + application_name=APP_NAME, + num_units=1, + series=SERIES, + config=data_hot_conf | CONFIG_OPTS, ), ) @@ -575,7 +580,7 @@ async def test_restore_to_new_cluster( await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ops_test.model.deploy(S3_INTEGRATOR, channel=S3_INTEGRATOR_CHANNEL), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. @@ -677,7 +682,7 @@ async def test_build_deploy_and_test_status(ops_test: OpsTest) -> None: await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ops_test.model.deploy(S3_INTEGRATOR, channel=S3_INTEGRATOR_CHANNEL), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/ha/test_ha.py b/tests/integration/ha/test_ha.py index 4e77f954f..7df76531e 100644 --- a/tests/integration/ha/test_ha.py +++ b/tests/integration/ha/test_ha.py @@ -11,6 +11,7 @@ from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, check_cluster_formation_successful, @@ -43,6 +44,9 @@ logger = logging.getLogger(__name__) +NUM_HA_UNITS = 3 + + @pytest.mark.runner(["self-hosted", "linux", "X64", "jammy", "large"]) @pytest.mark.group(1) @pytest.mark.abort_on_fail @@ -60,7 +64,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=NUM_HA_UNITS, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. @@ -71,7 +75,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: timeout=1400, idle_period=IDLE_PERIOD, ) - assert len(ops_test.model.applications[APP_NAME].units) == 3 + assert len(ops_test.model.applications[APP_NAME].units) == NUM_HA_UNITS @pytest.mark.runner(["self-hosted", "linux", "X64", "jammy", "large"]) diff --git a/tests/integration/ha/test_ha_multi_clusters.py b/tests/integration/ha/test_ha_multi_clusters.py index 3385cb137..c95ac5a35 100644 --- a/tests/integration/ha/test_ha_multi_clusters.py +++ b/tests/integration/ha/test_ha_multi_clusters.py @@ -10,6 +10,7 @@ from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, app_name, @@ -43,7 +44,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=2, series=SERIES), + ops_test.model.deploy(my_charm, num_units=2, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. @@ -73,7 +74,9 @@ async def test_multi_clusters_db_isolation( # deploy new cluster my_charm = await ops_test.build_charm(".") - await ops_test.model.deploy(my_charm, num_units=1, application_name=SECOND_APP_NAME) + await ops_test.model.deploy( + my_charm, num_units=1, application_name=SECOND_APP_NAME, config=CONFIG_OPTS + ) await ops_test.model.integrate(SECOND_APP_NAME, TLS_CERTIFICATES_APP_NAME) # wait diff --git a/tests/integration/ha/test_ha_networking.py b/tests/integration/ha/test_ha_networking.py index 268bd743c..789b2fdc8 100644 --- a/tests/integration/ha/test_ha_networking.py +++ b/tests/integration/ha/test_ha_networking.py @@ -22,6 +22,7 @@ ) from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, app_name, @@ -58,7 +59,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/ha/test_horizontal_scaling.py b/tests/integration/ha/test_horizontal_scaling.py index 784b6878f..4dc99d258 100644 --- a/tests/integration/ha/test_horizontal_scaling.py +++ b/tests/integration/ha/test_horizontal_scaling.py @@ -21,6 +21,7 @@ ) from ..helpers import ( APP_NAME, + CONFIG_OPTS, IDLE_PERIOD, MODEL_CONFIG, SERIES, @@ -57,7 +58,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=1, series=SERIES), + ops_test.model.deploy(my_charm, num_units=1, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/ha/test_large_deployments_cluster_manager_only_nodes.py b/tests/integration/ha/test_large_deployments_cluster_manager_only_nodes.py index 9f13fa5b2..f6cb64706 100644 --- a/tests/integration/ha/test_large_deployments_cluster_manager_only_nodes.py +++ b/tests/integration/ha/test_large_deployments_cluster_manager_only_nodes.py @@ -10,7 +10,7 @@ from charms.opensearch.v0.constants_charm import PClusterNoDataNode, PClusterNoRelation from pytest_operator.plugin import OpsTest -from ..helpers import MODEL_CONFIG, SERIES, get_leader_unit_ip +from ..helpers import CONFIG_OPTS, MODEL_CONFIG, SERIES, get_leader_unit_ip from ..helpers_deployments import wait_until from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME from .continuous_writes import ContinuousWrites @@ -51,21 +51,23 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: application_name=MAIN_APP, num_units=1, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "roles": "cluster_manager"}, + config={"cluster_name": CLUSTER_NAME, "roles": "cluster_manager"} | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=FAILOVER_APP, num_units=1, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "cluster_manager"}, + config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "cluster_manager"} + | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=DATA_APP, num_units=2, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data"}, + config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data"} + | CONFIG_OPTS, ), ) diff --git a/tests/integration/ha/test_large_deployments_relations.py b/tests/integration/ha/test_large_deployments_relations.py index dcaa98adc..eace6035c 100644 --- a/tests/integration/ha/test_large_deployments_relations.py +++ b/tests/integration/ha/test_large_deployments_relations.py @@ -10,7 +10,7 @@ from charms.opensearch.v0.constants_charm import PClusterNoRelation, TLSRelationMissing from pytest_operator.plugin import OpsTest -from ..helpers import MODEL_CONFIG, SERIES, get_leader_unit_ip +from ..helpers import CONFIG_OPTS, MODEL_CONFIG, SERIES, get_leader_unit_ip from ..helpers_deployments import wait_until from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME from .continuous_writes import ContinuousWrites @@ -53,28 +53,30 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: application_name=MAIN_APP, num_units=3, series=SERIES, - config={"cluster_name": CLUSTER_NAME}, + config={"cluster_name": CLUSTER_NAME} | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=FAILOVER_APP, num_units=3, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "init_hold": True}, + config={"cluster_name": CLUSTER_NAME, "init_hold": True} | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=DATA_APP, num_units=2, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data.hot,ml"}, + config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data.hot,ml"} + | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=INVALID_APP, num_units=1, series=SERIES, - config={"cluster_name": INVALID_CLUSTER_NAME, "init_hold": True, "roles": "data.cold"}, + config={"cluster_name": INVALID_CLUSTER_NAME, "init_hold": True, "roles": "data.cold"} + | CONFIG_OPTS, ), ) diff --git a/tests/integration/ha/test_roles_managements.py b/tests/integration/ha/test_roles_managements.py index 81e5f9454..d62049832 100644 --- a/tests/integration/ha/test_roles_managements.py +++ b/tests/integration/ha/test_roles_managements.py @@ -11,6 +11,7 @@ from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, check_cluster_formation_successful, @@ -45,7 +46,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/ha/test_scale_to_one_and_back.py b/tests/integration/ha/test_scale_to_one_and_back.py index 9bb0a4573..74f86d27d 100644 --- a/tests/integration/ha/test_scale_to_one_and_back.py +++ b/tests/integration/ha/test_scale_to_one_and_back.py @@ -11,6 +11,7 @@ from ..ha.helpers import get_elected_cm_unit_id from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, cluster_health, @@ -53,7 +54,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=3, series=SERIES), + ops_test.model.deploy(my_charm, num_units=3, series=SERIES, config=CONFIG_OPTS), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/ha/test_storage.py b/tests/integration/ha/test_storage.py index 761fc7a7d..9ca2c56b9 100644 --- a/tests/integration/ha/test_storage.py +++ b/tests/integration/ha/test_storage.py @@ -17,7 +17,13 @@ storage_type, ) from ..ha.test_horizontal_scaling import IDLE_PERIOD -from ..helpers import APP_NAME, MODEL_CONFIG, SERIES, get_application_unit_ids +from ..helpers import ( + APP_NAME, + CONFIG_OPTS, + MODEL_CONFIG, + SERIES, + get_application_unit_ids, +) from ..helpers_deployments import wait_until from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME from .continuous_writes import ContinuousWrites @@ -44,7 +50,9 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), - ops_test.model.deploy(my_charm, num_units=1, series=SERIES, storage=storage), + ops_test.model.deploy( + my_charm, num_units=1, series=SERIES, storage=storage, config=CONFIG_OPTS + ), ) # Relate it to OpenSearch to set up TLS. diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index cd4b38178..685a57c67 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -37,6 +37,8 @@ TARBALL_INSTALL_CERTS_DIR = "/etc/opensearch/config/certificates" +CONFIG_OPTS = {"profile": "testing"} + MODEL_CONFIG = { "logging-config": "=INFO;unit=DEBUG", "update-status-hook-interval": "5m", @@ -594,3 +596,44 @@ async def cluster_voting_config_exclusions( .get("cluster_coordination", {}) .get("voting_config_exclusions", {}) ) + + +async def service_start_time(ops_test: OpsTest, app: str, unit_id: int) -> float: + """Get the start date unix timestamp of the opensearch service.""" + unit_name = f"{app}/{unit_id}" + + boot_time_cmd = f"ssh {unit_name} awk '/btime/ {{print $2}}' /proc/stat" + _, unit_boot_time, _ = await ops_test.juju(*boot_time_cmd.split(), check=True) + unit_boot_time = int(unit_boot_time.strip()) + + active_since_cmd = f"exec --unit {unit_name} -- systemctl show snap.opensearch.daemon --property=ActiveEnterTimestampMonotonic --value" + _, active_time_since_boot, _ = await ops_test.juju(*active_since_cmd.split(), check=True) + active_time_since_boot = int(active_time_since_boot.strip()) / 1000000 + + return unit_boot_time + active_time_since_boot + + +async def get_application_unit_ids_start_time(ops_test: OpsTest, app: str) -> Dict[int, float]: + """Get opensearch start time by unit.""" + result = {} + + for u_id in get_application_unit_ids(ops_test, app): + result[u_id] = await service_start_time(ops_test, app, u_id) + return result + + +async def is_each_unit_restarted( + ops_test: OpsTest, app: str, previous_timestamps: Dict[int, float] +) -> bool: + """Check if all units are restarted.""" + try: + for attempt in Retrying(stop=stop_after_attempt(15), wait=wait_fixed(wait=5)): + with attempt: + for u_id, new_timestamp in ( + await get_application_unit_ids_start_time(ops_test, app) + ).items(): + if new_timestamp <= previous_timestamps[u_id]: + raise Exception + return True + except RetryError: + return False diff --git a/tests/integration/plugins/helpers.py b/tests/integration/plugins/helpers.py index 6ecc6a54e..51130e92b 100644 --- a/tests/integration/plugins/helpers.py +++ b/tests/integration/plugins/helpers.py @@ -19,52 +19,11 @@ ) from ..ha.helpers_data import bulk_insert, create_index -from ..helpers import get_application_unit_ids, http_request +from ..helpers import http_request logger = logging.getLogger(__name__) -async def service_start_time(ops_test: OpsTest, app: str, unit_id: int) -> float: - """Get the start date unix timestamp of the opensearch service.""" - unit_name = f"{app}/{unit_id}" - - boot_time_cmd = f"ssh {unit_name} awk '/btime/ {{print $2}}' /proc/stat" - _, unit_boot_time, _ = await ops_test.juju(*boot_time_cmd.split(), check=True) - unit_boot_time = int(unit_boot_time.strip()) - - active_since_cmd = f"exec --unit {unit_name} -- systemctl show snap.opensearch.daemon --property=ActiveEnterTimestampMonotonic --value" - _, active_time_since_boot, _ = await ops_test.juju(*active_since_cmd.split(), check=True) - active_time_since_boot = int(active_time_since_boot.strip()) / 1000000 - - return unit_boot_time + active_time_since_boot - - -async def get_application_unit_ids_start_time(ops_test: OpsTest, app: str) -> Dict[int, float]: - """Get opensearch start time by unit.""" - result = {} - - for u_id in get_application_unit_ids(ops_test, app): - result[u_id] = await service_start_time(ops_test, app, u_id) - return result - - -async def is_each_unit_restarted( - ops_test: OpsTest, app: str, previous_timestamps: Dict[int, float] -) -> bool: - """Check if all units are restarted.""" - try: - for attempt in Retrying(stop=stop_after_attempt(15), wait=wait_fixed(wait=5)): - with attempt: - for u_id, new_timestamp in ( - await get_application_unit_ids_start_time(ops_test, app) - ).items(): - if new_timestamp <= previous_timestamps[u_id]: - raise Exception - return True - except RetryError: - return False - - def generate_bulk_training_data( index_name: str, vector_name: str, diff --git a/tests/integration/plugins/test_plugins.py b/tests/integration/plugins/test_plugins.py index 7276e6990..816c30231 100644 --- a/tests/integration/plugins/test_plugins.py +++ b/tests/integration/plugins/test_plugins.py @@ -15,15 +15,18 @@ from ..ha.test_horizontal_scaling import IDLE_PERIOD from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, check_cluster_formation_successful, get_application_unit_ids_ips, + get_application_unit_ids_start_time, get_application_unit_names, get_leader_unit_id, get_leader_unit_ip, get_secret_by_label, http_request, + is_each_unit_restarted, run_action, set_watermark, ) @@ -31,8 +34,6 @@ from ..plugins.helpers import ( create_index_and_bulk_insert, generate_bulk_training_data, - get_application_unit_ids_start_time, - is_each_unit_restarted, is_knn_training_complete, run_knn_training, ) @@ -77,9 +78,9 @@ async def _set_config(ops_test: OpsTest, deploy_type: str, conf: dict[str, str]) if deploy_type == "small_deployment": await ops_test.model.applications[APP_NAME].set_config(conf) return - await ops_test.model.applications[MAIN_ORCHESTRATOR_NAME].set_config(conf) - await ops_test.model.applications[FAILOVER_ORCHESTRATOR_NAME].set_config(conf) - await ops_test.model.applications[APP_NAME].set_config(conf) + await ops_test.model.applications[MAIN_ORCHESTRATOR_NAME].set_config(conf | CONFIG_OPTS) + await ops_test.model.applications[FAILOVER_ORCHESTRATOR_NAME].set_config(conf | CONFIG_OPTS) + await ops_test.model.applications[APP_NAME].set_config(conf | CONFIG_OPTS) async def _wait_for_units( @@ -161,7 +162,10 @@ async def test_build_and_deploy_small_deployment(ops_test: OpsTest, deploy_type: config = {"ca-common-name": "CN_CA"} await asyncio.gather( ops_test.model.deploy( - my_charm, num_units=3, series=SERIES, config={"plugin_opensearch_knn": True} + my_charm, + num_units=3, + series=SERIES, + config={"plugin_opensearch_knn": True} | CONFIG_OPTS, ), ops_test.model.deploy(TLS_CERTIFICATES_APP_NAME, channel="stable", config=config), ) @@ -170,6 +174,7 @@ async def test_build_and_deploy_small_deployment(ops_test: OpsTest, deploy_type: await ops_test.model.integrate(APP_NAME, TLS_CERTIFICATES_APP_NAME) await _wait_for_units(ops_test, deploy_type) assert len(ops_test.model.applications[APP_NAME].units) == 3 + await set_watermark(ops_test, APP_NAME) @pytest.mark.parametrize("deploy_type", SMALL_DEPLOYMENTS) @@ -244,17 +249,21 @@ async def test_large_deployment_build_and_deploy(ops_test: OpsTest, deploy_type: application_name=MAIN_ORCHESTRATOR_NAME, num_units=1, series=SERIES, - config=main_orchestrator_conf, + config=main_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=FAILOVER_ORCHESTRATOR_NAME, num_units=2, series=SERIES, - config=failover_orchestrator_conf, + config=failover_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( - my_charm, application_name=APP_NAME, num_units=1, series=SERIES, config=data_hot_conf + my_charm, + application_name=APP_NAME, + num_units=1, + series=SERIES, + config=data_hot_conf | CONFIG_OPTS, ), ) diff --git a/tests/integration/relations/test_opensearch_provider.py b/tests/integration/relations/test_opensearch_provider.py index 44e9002f8..def80263a 100644 --- a/tests/integration/relations/test_opensearch_provider.py +++ b/tests/integration/relations/test_opensearch_provider.py @@ -13,6 +13,7 @@ from ..helpers import APP_NAME as OPENSEARCH_APP_NAME from ..helpers import ( + CONFIG_OPTS, MODEL_CONFIG, SERIES, get_application_unit_ids, @@ -84,6 +85,7 @@ async def test_create_relation(ops_test: OpsTest, application_charm, opensearch_ application_name=OPENSEARCH_APP_NAME, num_units=NUM_UNITS, series=SERIES, + config=CONFIG_OPTS, ), ) await ops_test.model.integrate(OPENSEARCH_APP_NAME, TLS_CERTIFICATES_APP_NAME) diff --git a/tests/integration/spaces/test_wrong_space.py b/tests/integration/spaces/test_wrong_space.py index bed184692..4dca5b473 100644 --- a/tests/integration/spaces/test_wrong_space.py +++ b/tests/integration/spaces/test_wrong_space.py @@ -12,6 +12,7 @@ from ..helpers import ( APP_NAME, + CONFIG_OPTS, IDLE_PERIOD, MODEL_CONFIG, SERIES, @@ -49,6 +50,7 @@ async def test_build_and_deploy(ops_test: OpsTest, lxd_spaces) -> None: series=SERIES, constraints="spaces=alpha,client,cluster,backup", bind={"": "cluster"}, + config=CONFIG_OPTS, ) config = {"ca-common-name": "CN_CA"} await ops_test.model.deploy( diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 4e9f36611..5e4de373b 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -23,6 +23,7 @@ ) from .helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, get_application_unit_ids, @@ -54,6 +55,7 @@ async def test_deploy_and_remove_single_unit(ops_test: OpsTest) -> None: my_charm, num_units=1, series=SERIES, + config=CONFIG_OPTS, ) # Deploy TLS Certificates operator. config = {"ca-common-name": "CN_CA"} @@ -94,6 +96,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None: my_charm, num_units=DEFAULT_NUM_UNITS, series=SERIES, + config=CONFIG_OPTS, ) await wait_until( ops_test, @@ -340,6 +343,7 @@ async def test_all_units_have_internal_users_synced(ops_test: OpsTest) -> None: assert leader_conf == unit_conf +@pytest.mark.runner(["self-hosted", "linux", "X64", "jammy", "large"]) @pytest.mark.group(1) @pytest.mark.abort_on_fail async def test_add_users_and_calling_update_status(ops_test: OpsTest) -> None: diff --git a/tests/integration/tls/test_ca_rotation.py b/tests/integration/tls/test_ca_rotation.py index d9b398be6..988bf0ab6 100644 --- a/tests/integration/tls/test_ca_rotation.py +++ b/tests/integration/tls/test_ca_rotation.py @@ -12,6 +12,7 @@ from ..ha.continuous_writes import ContinuousWrites from ..helpers import ( APP_NAME, + CONFIG_OPTS, IDLE_PERIOD, MODEL_CONFIG, SERIES, @@ -74,6 +75,7 @@ async def test_build_and_deploy_active(ops_test: OpsTest) -> None: my_charm, num_units=len(UNIT_IDS), series=SERIES, + config=CONFIG_OPTS, ) # Deploy TLS Certificates operator. @@ -106,7 +108,7 @@ async def test_build_large_deployment(ops_test: OpsTest) -> None: application_name=MAIN_APP, num_units=3, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "roles": "cluster_manager,data"}, + config={"cluster_name": CLUSTER_NAME, "roles": "cluster_manager,data"} | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, @@ -117,14 +119,16 @@ async def test_build_large_deployment(ops_test: OpsTest) -> None: "cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "cluster_manager,data", - }, + } + | CONFIG_OPTS, ), ops_test.model.deploy( my_charm, application_name=DATA_APP, num_units=1, series=SERIES, - config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data"}, + config={"cluster_name": CLUSTER_NAME, "init_hold": True, "roles": "data"} + | CONFIG_OPTS, ), ops_test.model.deploy( TLS_CERTIFICATES_APP_NAME, diff --git a/tests/integration/tls/test_manual_tls.py b/tests/integration/tls/test_manual_tls.py index dc86974d6..a510cab81 100644 --- a/tests/integration/tls/test_manual_tls.py +++ b/tests/integration/tls/test_manual_tls.py @@ -8,7 +8,7 @@ from juju.application import Application from pytest_operator.plugin import OpsTest -from ..helpers import APP_NAME, MODEL_CONFIG, SERIES, UNIT_IDS +from ..helpers import APP_NAME, CONFIG_OPTS, MODEL_CONFIG, SERIES, UNIT_IDS from ..helpers_deployments import wait_until from .helpers_manual_tls import MANUAL_TLS_CERTIFICATES_APP_NAME, ManualTLSAgent @@ -29,6 +29,7 @@ async def test_build_and_deploy_with_manual_tls(ops_test: OpsTest) -> None: num_units=len(UNIT_IDS), series=SERIES, application_name=APP_NAME, + config=CONFIG_OPTS, ) # Deploy TLS Certificates operator. diff --git a/tests/integration/tls/test_tls.py b/tests/integration/tls/test_tls.py index c56680237..4f876be3d 100644 --- a/tests/integration/tls/test_tls.py +++ b/tests/integration/tls/test_tls.py @@ -11,6 +11,7 @@ from ..helpers import ( APP_NAME, + CONFIG_OPTS, MODEL_CONFIG, SERIES, UNIT_IDS, @@ -54,6 +55,7 @@ async def test_build_and_deploy_active(ops_test: OpsTest) -> None: my_charm, num_units=len(UNIT_IDS), series=SERIES, + config=CONFIG_OPTS, ) # Deploy TLS Certificates operator. @@ -191,6 +193,7 @@ async def test_tls_expiration(ops_test: OpsTest) -> None: my_charm, num_units=1, series=SERIES, + config=CONFIG_OPTS, ) await wait_until( diff --git a/tests/integration/upgrades/helpers.py b/tests/integration/upgrades/helpers.py index 77cbecbb1..f37bc370f 100644 --- a/tests/integration/upgrades/helpers.py +++ b/tests/integration/upgrades/helpers.py @@ -34,6 +34,7 @@ async def refresh( switch: Optional[str] = None, channel: Optional[str] = None, path: Optional[str] = None, + config: Optional[dict[str, str]] = None, ) -> None: # due to: https://github.com/juju/python-libjuju/issues/1057 # the following call does not work: @@ -52,12 +53,15 @@ async def refresh( args.append(f"--channel={channel}") if path: args.append(f"--path={path}") + if config: + for key, val in config.items(): + args.extend(["--config", f"{key}={val}"]) for attempt in Retrying(stop=stop_after_attempt(6), wait=wait_fixed(wait=30)): with attempt: cmd = ["juju", "refresh"] - cmd.extend(args) cmd.append(app_name) + cmd.extend(args) subprocess.check_output(cmd) @@ -69,7 +73,6 @@ async def assert_upgrade_to_local( units = await get_application_units(ops_test, app) leader_id = [u.id for u in units if u.is_leader][0] - application = ops_test.model.applications[app] action = await run_action( ops_test, leader_id, @@ -80,7 +83,8 @@ async def assert_upgrade_to_local( async with ops_test.fast_forward(): logger.info("Refresh the charm") - await application.refresh(path=local_charm) + + await refresh(ops_test, app, path=local_charm, config={"profile": "testing"}) await wait_until( ops_test, @@ -90,7 +94,7 @@ async def assert_upgrade_to_local( wait_for_exact_units={ APP_NAME: 3, }, - timeout=1400, + timeout=2800, idle_period=IDLE_PERIOD, ) @@ -114,7 +118,7 @@ async def assert_upgrade_to_local( wait_for_exact_units={ APP_NAME: 3, }, - timeout=1400, + timeout=2800, idle_period=IDLE_PERIOD, ) diff --git a/tests/integration/upgrades/test_manual_large_deployment_upgrades.py b/tests/integration/upgrades/test_manual_large_deployment_upgrades.py index 5aa9e85a8..cf7fc4277 100644 --- a/tests/integration/upgrades/test_manual_large_deployment_upgrades.py +++ b/tests/integration/upgrades/test_manual_large_deployment_upgrades.py @@ -10,7 +10,14 @@ from ..ha.continuous_writes import ContinuousWrites from ..ha.helpers import assert_continuous_writes_consistency -from ..helpers import APP_NAME, IDLE_PERIOD, MODEL_CONFIG, SERIES, run_action +from ..helpers import ( + APP_NAME, + CONFIG_OPTS, + IDLE_PERIOD, + MODEL_CONFIG, + SERIES, + run_action, +) from ..helpers_deployments import get_application_units, wait_until from ..tls.test_tls import TLS_CERTIFICATES_APP_NAME @@ -64,7 +71,7 @@ async def test_large_deployment_deploy_original_charm(ops_test: OpsTest) -> None num_units=WORKLOAD[OPENSEARCH_MAIN_APP_NAME], series=SERIES, channel=OPENSEARCH_INITIAL_CHANNEL, - config=main_orchestrator_conf, + config=main_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( OPENSEARCH_ORIGINAL_CHARM_NAME, @@ -72,7 +79,7 @@ async def test_large_deployment_deploy_original_charm(ops_test: OpsTest) -> None num_units=WORKLOAD[OPENSEARCH_FAILOVER_APP_NAME], series=SERIES, channel=OPENSEARCH_INITIAL_CHANNEL, - config=failover_orchestrator_conf, + config=failover_orchestrator_conf | CONFIG_OPTS, ), ops_test.model.deploy( OPENSEARCH_ORIGINAL_CHARM_NAME, @@ -80,7 +87,7 @@ async def test_large_deployment_deploy_original_charm(ops_test: OpsTest) -> None num_units=WORKLOAD[APP_NAME], series=SERIES, channel=OPENSEARCH_INITIAL_CHANNEL, - config=data_hot_conf, + config=data_hot_conf | CONFIG_OPTS, ), ) diff --git a/tests/integration/upgrades/test_small_deployment_upgrades.py b/tests/integration/upgrades/test_small_deployment_upgrades.py index cee4e6853..8d8ad959e 100644 --- a/tests/integration/upgrades/test_small_deployment_upgrades.py +++ b/tests/integration/upgrades/test_small_deployment_upgrades.py @@ -230,7 +230,7 @@ async def test_upgrade_rollback_from_local( async with ops_test.fast_forward(): logger.info("Refresh the charm") - await refresh(ops_test, app, path=charm) + await refresh(ops_test, app, path=charm, config={"profile": "testing"}) await wait_until( ops_test, diff --git a/tests/unit/helpers.py b/tests/unit/helpers.py index 60b55f939..a3e5078c4 100644 --- a/tests/unit/helpers.py +++ b/tests/unit/helpers.py @@ -15,6 +15,7 @@ def mock_deployment_desc( typ: str, temperature: str | None = None, cluster_name: str = CLUSTER_NAME, + profile: str = "production", ) -> dict[str, str]: return { "app": { @@ -28,6 +29,7 @@ def mock_deployment_desc( "data_temperature": temperature, "init_hold": False, "roles": roles, + "profile": profile, }, "pending_directives": [], "promotion_time": 1721391694.387948, diff --git a/tests/unit/lib/test_backups.py b/tests/unit/lib/test_backups.py index 2d0ec49f4..561ff1978 100644 --- a/tests/unit/lib/test_backups.py +++ b/tests/unit/lib/test_backups.py @@ -68,7 +68,10 @@ def create_deployment_desc(): return DeploymentDescription( config=PeerClusterConfig( - cluster_name="logs", init_hold=False, roles=["cluster_manager", "data"] + cluster_name="logs", + init_hold=False, + roles=["cluster_manager", "data"], + profile="production", ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[], diff --git a/tests/unit/lib/test_ml_plugins.py b/tests/unit/lib/test_ml_plugins.py index 58c57fe48..710fab3d5 100644 --- a/tests/unit/lib/test_ml_plugins.py +++ b/tests/unit/lib/test_ml_plugins.py @@ -58,6 +58,7 @@ def setUp(self) -> None: charms.opensearch.v0.helper_cluster.ClusterTopology.get_cluster_settings = MagicMock( return_value={} ) + self.charm.performance_profile = MagicMock() @patch(f"{BASE_LIB_PATH}.opensearch_config.OpenSearchConfig.update_host_if_needed") @patch(f"{BASE_LIB_PATH}.opensearch_distro.OpenSearchDistribution.is_node_up") @@ -89,6 +90,8 @@ def test_disable_via_config_change( ___, mock_is_node_up, mock_update_host_if_needed, + # mock_deployment_desc, + # ____, ) -> None: """Tests entire config_changed event with KNN plugin.""" mock_status.return_value = PluginState.ENABLED @@ -120,7 +123,6 @@ def test_disable_via_config_change( self.harness.update_config({"plugin_opensearch_knn": False}) self.charm.plugin_manager.check_plugin_manager_ready.assert_called() - self.charm._restart_opensearch_event.emit.assert_called_once() self.plugin_manager._opensearch_config.add_plugin.assert_called_once_with( {"knn.plugin.enabled": "false"} ) diff --git a/tests/unit/lib/test_opensearch_base_charm.py b/tests/unit/lib/test_opensearch_base_charm.py index b944707c6..a0b76fa48 100644 --- a/tests/unit/lib/test_opensearch_base_charm.py +++ b/tests/unit/lib/test_opensearch_base_charm.py @@ -45,7 +45,9 @@ class TestOpenSearchBaseCharm(unittest.TestCase): deployment_descriptions = { "ok": DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -53,7 +55,9 @@ class TestOpenSearchBaseCharm(unittest.TestCase): state=DeploymentState(value=State.ACTIVE), ), "ko": DeploymentDescription( - config=PeerClusterConfig(cluster_name="logs", init_hold=True, roles=["ml"]), + config=PeerClusterConfig( + cluster_name="logs", init_hold=True, roles=["ml"], profile="production" + ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[Directive.WAIT_FOR_PEER_CLUSTER_RELATION], typ=DeploymentType.OTHER, @@ -61,7 +65,9 @@ class TestOpenSearchBaseCharm(unittest.TestCase): state=DeploymentState(value=State.BLOCKED_CANNOT_START_WITH_ROLES, message="error"), ), "cm-only": DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=["cluster-manager"]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=["cluster-manager"], profile="production" + ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -69,7 +75,9 @@ class TestOpenSearchBaseCharm(unittest.TestCase): state=DeploymentState(value=State.ACTIVE), ), "data-only": DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=["data"]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=["data"], profile="production" + ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[], typ=DeploymentType.OTHER, @@ -84,6 +92,7 @@ def setUp(self) -> None: self.harness.begin() self.charm = self.harness.charm + self.charm.opensearch_config.apply_performance_profile = MagicMock() for typ in ["ok", "ko"]: self.deployment_descriptions[typ].app = App( @@ -218,7 +227,12 @@ def test_data_role_only_on_start( _apply_peer_cm_directives_and_check_if_can_start, ): """Test start event for nodes that only have the `data` role.""" - with patch(f"{self.OPENSEARCH_DISTRO}.is_node_up") as is_node_up: + with ( + patch(f"{self.OPENSEARCH_DISTRO}.is_node_up") as is_node_up, + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchConfig.apply_performance_profile" + ), + ): is_node_up.return_value = False _apply_peer_cm_directives_and_check_if_can_start.return_value = True is_fully_configured.return_value = True @@ -228,7 +242,6 @@ def test_data_role_only_on_start( self.harness.set_leader(True) self.charm.on.start.emit() - self.charm._start_opensearch_event.emit.assert_called_once() @patch(f"{BASE_LIB_PATH}.opensearch_locking.OpenSearchNodeLock.acquired") @@ -265,7 +278,15 @@ def test_on_start( lock_acquired, ): """Test on start event.""" - with patch(f"{self.OPENSEARCH_DISTRO}.is_node_up") as is_node_up: + with ( + patch(f"{self.OPENSEARCH_DISTRO}.is_node_up") as is_node_up, + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchConfig.apply_performance_profile" + ), + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchDistribution.is_service_started" + ), + ): # test when setup complete is_node_up.return_value = True self.peers_data.put(Scope.APP, "security_index_initialised", True) @@ -281,24 +302,41 @@ def test_on_start( self.charm.on.start.emit() set_client_auth.assert_not_called() - # when _get_nodes fails - _get_nodes.side_effect = OpenSearchHttpError() - self.charm.on.start.emit() - _set_node_conf.assert_not_called() + with ( + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchConfig.apply_performance_profile" + ) as perf_profile, + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchDistribution.is_service_started" + ), + ): + # when _get_nodes fails + _get_nodes.side_effect = OpenSearchHttpError() + self.charm.on.start.emit() + _set_node_conf.assert_not_called() + perf_profile.assert_not_called() # not called as the profile has been already set - _get_nodes.reset_mock() + _get_nodes.reset_mock() - # _get_nodes succeeds - is_fully_configured.return_value = True - is_admin_user_configured.return_value = True - _get_nodes.side_effect = None - _can_service_start.return_value = False - self.charm.on.start.emit() - _get_nodes.assert_called_once() - _set_node_conf.assert_not_called() - _initialize_security_index.assert_not_called() - - with patch(f"{self.OPENSEARCH_DISTRO}.start") as start: + # _get_nodes succeeds + is_fully_configured.return_value = True + is_admin_user_configured.return_value = True + _get_nodes.side_effect = None + _can_service_start.return_value = False + self.charm.on.start.emit() + _get_nodes.assert_called_once() + _set_node_conf.assert_not_called() + _initialize_security_index.assert_not_called() + + with ( + patch(f"{self.OPENSEARCH_DISTRO}.start") as start, + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchConfig.apply_performance_profile" + ) as perf_profile, + patch( + f"{self.BASE_LIB_PATH}.opensearch_config.OpenSearchDistribution.is_service_started" + ), + ): # initialisation of the security index _get_nodes.reset_mock() _set_node_conf.reset_mock() diff --git a/tests/unit/lib/test_opensearch_config.py b/tests/unit/lib/test_opensearch_config.py index 4be5fcd3e..8038c643d 100644 --- a/tests/unit/lib/test_opensearch_config.py +++ b/tests/unit/lib/test_opensearch_config.py @@ -176,7 +176,10 @@ def test_set_node_and_cleanup_if_bootstrapped(self, mock_deployment_desc): app = App(model_uuid=self.charm.model.uuid, name=self.charm.app.name) mock_deployment_desc.return_value = DeploymentDescription( config=PeerClusterConfig( - cluster_name="logs", init_hold=False, roles=["cluster_manager", "data"] + cluster_name="logs", + init_hold=False, + roles=["cluster_manager", "data"], + profile="production", ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[], diff --git a/tests/unit/lib/test_opensearch_internal_data.py b/tests/unit/lib/test_opensearch_internal_data.py index 90e1586a0..0e8b6142e 100644 --- a/tests/unit/lib/test_opensearch_internal_data.py +++ b/tests/unit/lib/test_opensearch_internal_data.py @@ -159,13 +159,17 @@ def test_put_and_get_complex_obj(self, scope): datetime.now.return_value.timestamp.return_value = 12345788.12 deployment = DeploymentDescription( config=PeerClusterConfig( - cluster_name="logs", init_hold=False, roles=["cluster_manager", "data"] + cluster_name="logs", + init_hold=False, + roles=["cluster_manager", "data"], + profile="production", ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[], app=App(model_uuid="model-uuid", name=self.charm.app.name), typ=DeploymentType.MAIN_ORCHESTRATOR, state=DeploymentState(value=State.ACTIVE), + profile="production", ) self.store.put_object(scope, "deployment", deployment.to_dict()) fetched_deployment = DeploymentDescription.from_dict( diff --git a/tests/unit/lib/test_opensearch_peer_clusters.py b/tests/unit/lib/test_opensearch_peer_clusters.py index c92764312..8e72c5f59 100644 --- a/tests/unit/lib/test_opensearch_peer_clusters.py +++ b/tests/unit/lib/test_opensearch_peer_clusters.py @@ -40,14 +40,27 @@ class TestOpenSearchPeerClustersManager(unittest.TestCase): ) user_configs = { - "default": PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), - "name": PeerClusterConfig(cluster_name="logs", init_hold=False, roles=[]), - "init_hold": PeerClusterConfig(cluster_name="", init_hold=True, roles=[]), + "default": PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), + "name": PeerClusterConfig( + cluster_name="logs", init_hold=False, roles=[], profile="production" + ), + "init_hold": PeerClusterConfig( + cluster_name="", init_hold=True, roles=[], profile="production" + ), "roles_ok": PeerClusterConfig( - cluster_name="", init_hold=False, roles=["cluster_manager", "data"] + cluster_name="", + init_hold=False, + roles=["cluster_manager", "data"], + profile="production", + ), + "roles_ko": PeerClusterConfig( + cluster_name="", init_hold=False, roles=["data"], profile="production" + ), + "roles_temp": PeerClusterConfig( + cluster_name="", init_hold=True, roles=["data.hot"], profile="production" ), - "roles_ko": PeerClusterConfig(cluster_name="", init_hold=False, roles=["data"]), - "roles_temp": PeerClusterConfig(cluster_name="", init_hold=True, roles=["data.hot"]), } p_units = [ @@ -88,13 +101,17 @@ def test_can_start(self, deployment_desc): ]: deployment_desc = DeploymentDescription( config=PeerClusterConfig( - cluster_name="logs", init_hold=False, roles=["cluster_manager", "data"] + cluster_name="logs", + init_hold=False, + roles=["cluster_manager", "data"], + profile="production", ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=directives, app=App(model_uuid=self.charm.model.uuid, name=self.charm.app.name), typ=DeploymentType.MAIN_ORCHESTRATOR, state=DeploymentState(value=State.ACTIVE), + profile="production", ) can_start = self.peer_cm.can_start(deployment_desc) self.assertEqual(can_start, expected) @@ -120,6 +137,7 @@ def test_validate_roles( app=App(model_uuid=self.charm.model.uuid, name="logs"), typ=DeploymentType.MAIN_ORCHESTRATOR, state=DeploymentState(value=State.ACTIVE), + profile="production", ) with self.assertRaises(OpenSearchProvidedRolesException): # on scale up @@ -185,6 +203,7 @@ def test_pre_validate_roles_change( app=App(model_uuid=self.charm.model.uuid, name="logs"), typ=DeploymentType.MAIN_ORCHESTRATOR, state=DeploymentState(value=State.ACTIVE), + profile="production", ) alt_hosts.return_value = [] diff --git a/tests/unit/lib/test_opensearch_performance.py b/tests/unit/lib/test_opensearch_performance.py new file mode 100644 index 000000000..0f8c5adff --- /dev/null +++ b/tests/unit/lib/test_opensearch_performance.py @@ -0,0 +1,185 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +import json +import unittest +from unittest.mock import mock_open, patch + +import pytest +from charms.opensearch.v0.models import ( + MAX_HEAP_SIZE, + MIN_HEAP_SIZE, + OpenSearchPerfProfile, + PerformanceType, +) +from ops.testing import Harness + +from charm import OpenSearchOperatorCharm + + +@pytest.fixture +def mock_meminfo(): + with patch("charms.opensearch.v0.models.OpenSearchPerfProfile.meminfo") as mock: + mock.return_value = {"MemTotal": 8000000} # 8 GB in kB + yield mock + + +def test_production_profile_type(): + """Tests the different formats of creating an object out of a model.""" + OpenSearchPerfProfile.from_dict({"typ": "production"}) + OpenSearchPerfProfile.from_str(json.dumps({"typ": "production"})) + + +def test_invalid_profile_type(): + with pytest.raises(AttributeError): + OpenSearchPerfProfile.from_dict({"typ": "INVALID_TYPE"}) + + +def test_production_profile(mock_meminfo): + profile = OpenSearchPerfProfile(typ=PerformanceType.PRODUCTION) + assert profile.heap_size_in_kb == min(int(0.50 * 8000000), MAX_HEAP_SIZE) + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": { + "settings": { + "number_of_replicas": "1", + }, + }, + }, + } + + +def test_staging_profile(mock_meminfo): + profile = OpenSearchPerfProfile(typ=PerformanceType.STAGING) + assert profile.heap_size_in_kb == min(int(0.25 * 8000000), MAX_HEAP_SIZE) + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": { + "settings": { + "number_of_replicas": "1", + }, + }, + }, + } + + +def test_testing_profile(mock_meminfo): + profile = OpenSearchPerfProfile(typ=PerformanceType.TESTING) + assert profile.heap_size_in_kb == MIN_HEAP_SIZE + assert profile.opensearch_yml == {} + assert profile.charmed_index_template == {} + + +def test_perf_profile_15g(): + """Test the performance profile for a 115GB machine. + + Each profile type must respect their respective values. + """ + with patch("charms.opensearch.v0.models.OpenSearchPerfProfile.meminfo") as mock_perf_profile: + mock_perf_profile.return_value = {"MemTotal": 15360.0 * 1024} + + profile = OpenSearchPerfProfile(typ=PerformanceType.PRODUCTION) + assert profile.typ == PerformanceType.PRODUCTION + assert str(profile.heap_size_in_kb) == "7864320" + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": {"settings": {"number_of_replicas": "1"}}, + } + } + + profile = OpenSearchPerfProfile(typ=PerformanceType.STAGING) + assert profile.typ == PerformanceType.STAGING + assert str(profile.heap_size_in_kb) == "3932160" + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": {"settings": {"number_of_replicas": "1"}}, + } + } + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + + profile = OpenSearchPerfProfile(typ=PerformanceType.TESTING) + assert profile.typ == PerformanceType.TESTING + assert str(profile.heap_size_in_kb) == "1048576" + assert profile.charmed_index_template == {} + assert profile.opensearch_yml == {} + + +def test_perf_profile_5g(): + """Test the performance profile for a 5GB machine. + + In this case, we should expect the on "staging" to be smaller than 1GB, therefore, to select + the 1GB value instead. + """ + with patch("charms.opensearch.v0.models.OpenSearchPerfProfile.meminfo") as mock_perf_profile: + mock_perf_profile.return_value = {"MemTotal": 5120.0 * 1024} + + profile = OpenSearchPerfProfile(typ=PerformanceType.PRODUCTION) + assert profile.typ == PerformanceType.PRODUCTION + assert str(profile.heap_size_in_kb) == "2621440" + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": {"settings": {"number_of_replicas": "1"}}, + } + } + + profile = OpenSearchPerfProfile(typ=PerformanceType.STAGING) + assert profile.typ == PerformanceType.STAGING + assert str(profile.heap_size_in_kb) == "1310720" + assert profile.charmed_index_template == { + "charmed-index-tpl": { + "index_patterns": ["*"], + "template": {"settings": {"number_of_replicas": "1"}}, + } + } + assert profile.opensearch_yml == {"indices.memory.index_buffer_size": "25%"} + + profile = OpenSearchPerfProfile(typ=PerformanceType.TESTING) + assert profile.typ == PerformanceType.TESTING + assert str(profile.heap_size_in_kb) == "1048576" + assert profile.charmed_index_template == {} + assert profile.opensearch_yml == {} + + +# We need to simulate the original value of jvm.options +JVM_OPTIONS = """-Xms1g +-Xmx1g""" + +MEMINFO = """MemTotal: 15728640 kB +MemFree: 1234 kB +NotValid: 0 +""" + + +class TestPerformanceProfile(unittest.TestCase): + + def setUp(self): + with patch("builtins.open", mock_open(read_data=MEMINFO)): + self.harness = Harness(OpenSearchOperatorCharm) + self.addCleanup(self.harness.cleanup) + self.harness.set_leader(True) + self.harness.begin() + self.charm = self.harness.charm + self.opensearch = self.charm.opensearch + self.test_profile = OpenSearchPerfProfile(typ=PerformanceType.PRODUCTION) + + @patch("charms.opensearch.v0.helper_conf_setter.YamlConfigSetter.replace") + @patch("charms.opensearch.v0.helper_conf_setter.YamlConfigSetter.put") + @patch("charms.opensearch.v0.helper_conf_setter.exists") + def test_update_jvm_options(self, _, __, mock_replace): + """Test the update of the JVM options.""" + self.charm.opensearch_config.apply_performance_profile(profile=self.test_profile) + mock_replace.assert_any_call( + "jvm.options", "-Xms[0-9]+[kmgKMG]", "-Xms7864320k", regex=True + ) + mock_replace.assert_any_call( + "jvm.options", "-Xmx[0-9]+[kmgKMG]", "-Xmx7864320k", regex=True + ) diff --git a/tests/unit/lib/test_opensearch_relation_provider.py b/tests/unit/lib/test_opensearch_relation_provider.py index e23405407..bee6b6b30 100644 --- a/tests/unit/lib/test_opensearch_relation_provider.py +++ b/tests/unit/lib/test_opensearch_relation_provider.py @@ -64,7 +64,9 @@ def setUp(self): def mock_deployment_desc(): return DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, diff --git a/tests/unit/lib/test_opensearch_tls.py b/tests/unit/lib/test_opensearch_tls.py index 31d146682..47975bada 100644 --- a/tests/unit/lib/test_opensearch_tls.py +++ b/tests/unit/lib/test_opensearch_tls.py @@ -60,7 +60,9 @@ class TestOpenSearchTLS(unittest.TestCase): deployment_descriptions = { "ok": DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -68,7 +70,9 @@ class TestOpenSearchTLS(unittest.TestCase): state=DeploymentState(value=State.ACTIVE), ), "ko": DeploymentDescription( - config=PeerClusterConfig(cluster_name="logs", init_hold=True, roles=["ml"]), + config=PeerClusterConfig( + cluster_name="logs", init_hold=True, roles=["ml"], profile="production" + ), start=StartMode.WITH_PROVIDED_ROLES, pending_directives=[Directive.WAIT_FOR_PEER_CLUSTER_RELATION], typ=DeploymentType.OTHER, @@ -85,6 +89,7 @@ def setUp(self, _) -> None: self.harness.add_network("1.1.1.1", endpoint=TLS_RELATION) self.harness.begin() self.charm = self.harness.charm + self.rel_id = self.harness.add_relation(PeerRelationName, self.charm.app.name) self.harness.add_relation_unit(self.rel_id, f"{self.charm.app.name}/0") self.harness.add_relation(TLS_RELATION, self.charm.app.name) @@ -162,7 +167,9 @@ def test_find_secret(self): def test_on_relation_created_admin(self, _, __, _request_certificate, deployment_desc): """Test on certificate relation created event.""" deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -193,7 +200,9 @@ def test_on_relation_created_only_main_orchestrator_requests_application_cert( ): """Test on certificate relation created event.""" deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.OTHER, @@ -228,7 +237,9 @@ def test_on_relation_created_only_main_orchestrator_requests_application_cert( def test_on_relation_created_non_admin(self, _, __, _request_certificate, deployment_desc): """Test on certificate relation created event.""" deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -353,7 +364,9 @@ def test_on_certificate_expiring(self, _, deployment_desc, request_certificate_c ) deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -387,7 +400,9 @@ def test_on_certificate_invalidated(self, _, deployment_desc, request_certificat ) deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.MAIN_ORCHESTRATOR, @@ -430,7 +445,9 @@ def test_truststore_password_secret_only_created_by_main_orchestrator( self, _, __, _create_keystore_pwd_if_not_exists, deployment_desc ): deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=DeploymentType.OTHER, @@ -509,7 +526,9 @@ def test_on_certificate_available_leader_app_cert_full_workflow( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -651,7 +670,9 @@ def test_on_certificate_available_any_node_unit_cert_full_workflow( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -775,7 +796,9 @@ def test_on_certificate_available_ca_rotation_first_stage_any_cluster_leader( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -871,7 +894,9 @@ def test_on_certificate_available_ca_rotation_first_stage_any_cluster_non_leader # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1002,7 +1027,9 @@ def test_on_certificate_available_ca_rotation_second_stage_any_cluster_leader( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1161,7 +1188,9 @@ def test_on_certificate_available_ca_rotation_second_stage_any_cluster_non_leade # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1285,7 +1314,9 @@ def mock_stored_ca(alias: str | None = None): # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1459,7 +1490,9 @@ def test_on_certificate_available_ca_rotation_third_stage_any_unit_cert_unit( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1575,7 +1608,9 @@ def test_on_certificate_available_rotation_ongoing_on_this_unit( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, @@ -1671,7 +1706,9 @@ def test_on_certificate_available_rotation_ongoing_on_another_unit( # Applies to ANY deployment type deployment_desc.return_value = DeploymentDescription( - config=PeerClusterConfig(cluster_name="", init_hold=False, roles=[]), + config=PeerClusterConfig( + cluster_name="", init_hold=False, roles=[], profile="production" + ), start=StartMode.WITH_GENERATED_ROLES, pending_directives=[], typ=deployment_type, diff --git a/tests/unit/lib/test_plugins.py b/tests/unit/lib/test_plugins.py index 2a8084574..99461ef8f 100644 --- a/tests/unit/lib/test_plugins.py +++ b/tests/unit/lib/test_plugins.py @@ -202,6 +202,7 @@ def test_failed_install_plugin_missing_dependency(self, _, mock_version) -> None # Check if we had any other exception assert succeeded is True + @patch(f"{BASE_LIB_PATH}.opensearch_performance_profile.OpenSearchPerformance.apply") @patch( f"{BASE_LIB_PATH}.opensearch_peer_clusters.OpenSearchPeerClustersManager.deployment_desc" ) @@ -209,13 +210,12 @@ def test_failed_install_plugin_missing_dependency(self, _, mock_version) -> None "charms.opensearch.v0.opensearch_distro.OpenSearchDistribution.version", new_callable=PropertyMock, ) - def test_check_plugin_called_on_config_changed(self, mock_version, deployment_desc) -> None: + def test_check_plugin_called_on_config_changed(self, mock_version, deployment_desc, _) -> None: """Triggers a config change and should call plugin manager.""" self.harness.set_leader(True) self.peers_data.put(Scope.APP, "security_index_initialised", True) self.harness.set_leader(False) - deployment_desc.return_value = "something" self.plugin_manager.run = MagicMock(return_value=False) self.charm.opensearch_config.update_host_if_needed = MagicMock(return_value=False) self.charm.opensearch.is_started = MagicMock(return_value=True)