From 6c77be6d1067f4f772fbfee7f5c89556eb8c72cc Mon Sep 17 00:00:00 2001 From: Roger Nhan Date: Thu, 23 Jul 2026 17:54:52 -0400 Subject: [PATCH] [AKS] `az aks create/update`: Add distributedcache storage option to --enable/--disable-azure-container-storage --- src/aks-preview/HISTORY.rst | 1 + src/aks-preview/azext_aks_preview/_params.py | 13 +- .../azurecontainerstorage/_consts.py | 5 + .../azurecontainerstorage/_helpers.py | 10 + .../azurecontainerstorage/_validators.py | 109 ++++++++++ .../azurecontainerstorage/acstor_ops.py | 109 ++++++++++ .../managed_cluster_decorator.py | 203 ++++++++++++++++++ .../tests/latest/test_validators.py | 126 +++++++++++ 8 files changed, 571 insertions(+), 5 deletions(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 1622ab34e2d..f62c7960b3b 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,6 +11,7 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +* `az aks create/update`: Add `distributedcache` storage option to `--enable-azure-container-storage` and `--disable-azure-container-storage` to install/uninstall the Azure Container Storage distributed cache controller. 21.0.0b10 ++++++++ diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 5db313112ca..5c63079ea9b 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -267,6 +267,7 @@ CONST_STORAGE_POOL_TYPE_AZURE_DISK, CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK, CONST_STORAGE_POOL_TYPE_ELASTIC_SAN, + CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, CONST_STORAGE_POOL_SKU_PREMIUM_LRS, CONST_STORAGE_POOL_SKU_STANDARD_LRS, CONST_STORAGE_POOL_SKU_STANDARDSSD_LRS, @@ -480,12 +481,14 @@ CONST_STORAGE_POOL_TYPE_AZURE_DISK, CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK, CONST_STORAGE_POOL_TYPE_ELASTIC_SAN, + CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, ] disable_storage_pool_types = [ CONST_STORAGE_POOL_TYPE_AZURE_DISK, CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK, CONST_STORAGE_POOL_TYPE_ELASTIC_SAN, + CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, CONST_ACSTOR_ALL, ] @@ -1198,7 +1201,7 @@ def load_arguments(self, _): "enable_azure_container_storage", arg_type=_get_container_storage_enum_type(storage_pool_types), help="enable azure container storage. Can be used as a flag (defaults to True) or with a" - " storage pool type value: (azureDisk, ephemeralDisk, elasticSan)", + " storage pool type value: (azureDisk, ephemeralDisk, elasticSan, distributedcache)", ) c.argument( "container_storage_version", @@ -1907,14 +1910,14 @@ def load_arguments(self, _): "enable_azure_container_storage", arg_type=_get_container_storage_enum_type(storage_pool_types), help="enable azure container storage. Can be used as a flag (defaults to True) or with a" - " storage pool type value: (azureDisk, ephemeralDisk, elasticSan)", + " storage pool type value: (azureDisk, ephemeralDisk, elasticSan, distributedcache)", ) c.argument( "disable_azure_container_storage", arg_type=_get_container_storage_enum_type(disable_storage_pool_types), - help="disable azure container storage or any one of the storage pool types." - " Can be used as a flag (defaults to True) or with a storagepool type value:" - " azureDisk, ephemeralDisk, elasticSan, all (to disable all storage pools).", + help="disable azure container storage or any one of the storage types." + " Can be used as a flag (defaults to True) or with a storage type value:" + " azureDisk, ephemeralDisk, elasticSan, distributedcache, all (to disable all storage types).", ) c.argument( "container_storage_version", diff --git a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_consts.py b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_consts.py index 3019cdbd640..fe634e345d7 100644 --- a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_consts.py +++ b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_consts.py @@ -17,6 +17,10 @@ CONST_ACSTOR_EXT_INSTALLATION_NAME = "acstor" CONST_ACSTOR_EXT_INSTALLATION_NAMESPACE = "kube-system" CONST_ACSTOR_K8S_EXTENSION_NAME = "microsoft.azurecontainerstoragev2" +# Distributed cache (Distributed Accelerator for Cloud Storage) install controller. +CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME = "distributedcache" +CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAMESPACE = "kube-system" +CONST_DISTRIBUTED_CACHE_K8S_EXTENSION_NAME = "microsoft.dacs" CONST_K8S_EXTENSION_CLIENT_FACTORY_MOD_NAME = "azext_k8s_extension._client_factory" CONST_K8S_EXTENSION_CUSTOM_MOD_NAME = "azext_k8s_extension.custom" CONST_K8S_EXTENSION_NAME = "k8s-extension" @@ -35,3 +39,4 @@ CONST_STORAGE_POOL_TYPE_AZURE_DISK = "azureDisk" CONST_STORAGE_POOL_TYPE_ELASTIC_SAN = "elasticSan" CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK = "ephemeralDisk" +CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE = "distributedcache" diff --git a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_helpers.py b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_helpers.py index 6d2e110d939..21e7ea655fe 100644 --- a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_helpers.py +++ b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_helpers.py @@ -23,6 +23,7 @@ CONST_STORAGE_POOL_TYPE_AZURE_DISK, CONST_STORAGE_POOL_TYPE_ELASTIC_SAN, CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK, + CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, CONST_ACSTOR_V1_K8S_EXTENSION_NAME, CONST_ACSTOR_V1_EXT_INSTALLATION_NAME, ) @@ -304,6 +305,15 @@ def should_delete_extension(storage_options_to_remove) -> bool: ) +def is_distributed_cache_requested(storage_options) -> bool: + # Detect whether distributed cache is among the requested storage options. + if storage_options is None or storage_options is True: + return False + if isinstance(storage_options, list): + return CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE in storage_options + return storage_options == CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE + + def get_container_storage_extension_installed( cmd, resource_group, diff --git a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_validators.py b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_validators.py index d4f71d622a5..c0f1749a4ec 100644 --- a/src/aks-preview/azext_aks_preview/azurecontainerstorage/_validators.py +++ b/src/aks-preview/azext_aks_preview/azurecontainerstorage/_validators.py @@ -21,6 +21,7 @@ CONST_STORAGE_POOL_TYPE_AZURE_DISK, CONST_STORAGE_POOL_TYPE_ELASTIC_SAN, CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK, + CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, ) from azext_aks_preview.azurecontainerstorage._helpers import ( get_vm_sku_details @@ -571,6 +572,114 @@ def validate_disable_azure_container_storage_params( ) +def validate_enable_distributed_cache_params( + enablement_option, + is_extension_installed, + storage_pool_name, + storage_pool_sku, + storage_pool_option, + storage_pool_size, + ephemeral_disk_volume_type, + ephemeral_disk_nvme_perf_tier, + container_storage_version=None, +): + # Distributed cache has no storage pool construct, so none of the storage + # pool parameters are supported. + enablement_option_arr = enablement_option if isinstance(enablement_option, list) else [enablement_option] + other_pool_types = [ + opt for opt in enablement_option_arr + if opt != CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE + ] + if other_pool_types: + options_display = "', '".join(other_pool_types) + raise InvalidArgumentValueError( + f"'{CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}' cannot be combined with other storage " + f"options ('{options_display}'). Distributed cache is enabled independently. " + f"Please run --enable-azure-container-storage {CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE} " + "on its own." + ) + + if is_extension_installed: + raise InvalidArgumentValueError( + 'Cannot enable distributed cache as it is already enabled on the cluster.' + ) + + unsupported_params = [] + if storage_pool_name is not None: + unsupported_params.append('--storage-pool-name') + if storage_pool_sku is not None: + unsupported_params.append('--storage-pool-sku') + if storage_pool_option is not None: + unsupported_params.append('--storage-pool-option') + if storage_pool_size is not None: + unsupported_params.append('--storage-pool-size') + if ephemeral_disk_volume_type is not None: + unsupported_params.append('--ephemeral-disk-volume-type') + if ephemeral_disk_nvme_perf_tier is not None: + unsupported_params.append('--ephemeral-disk-nvme-perf-tier') + if container_storage_version is not None: + unsupported_params.append('--container-storage-version') + + if unsupported_params: + params_defined = ', '.join(unsupported_params) + raise InvalidArgumentValueError( + f'{params_defined} cannot be used with ' + f'--enable-azure-container-storage {CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + + +def validate_disable_distributed_cache_params( + disablement_option, + is_extension_installed, + storage_pool_name, + storage_pool_sku, + storage_pool_option, + storage_pool_size, + container_storage_version=None, +): + disablement_option_arr = disablement_option if isinstance(disablement_option, list) else [disablement_option] + other_pool_types = [ + opt for opt in disablement_option_arr + if opt != CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE + ] + if other_pool_types: + options_display = "', '".join(other_pool_types) + raise InvalidArgumentValueError( + f"'{CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}' cannot be combined with other storage " + f"options ('{options_display}'). Distributed cache is disabled independently. " + f"Please run --disable-azure-container-storage {CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE} " + "on its own." + ) + + if not is_extension_installed: + raise InvalidArgumentValueError( + 'Cannot disable distributed cache as it could not be found on the cluster.' + ) + + unsupported_params = [] + if storage_pool_name is not None: + unsupported_params.append('--storage-pool-name') + if storage_pool_sku is not None: + unsupported_params.append('--storage-pool-sku') + if storage_pool_option is not None: + unsupported_params.append('--storage-pool-option') + if storage_pool_size is not None: + unsupported_params.append('--storage-pool-size') + if container_storage_version is not None: + unsupported_params.append('--container-storage-version') + + if unsupported_params: + params_defined = ', '.join(unsupported_params) + raise InvalidArgumentValueError( + f'{params_defined} cannot be used with ' + f'--disable-azure-container-storage {CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + + # _Validate_storage_pool_size validates that the storage_pool_size is # string of a combination of a float number immediately followed by # Ti or Gi e.g. 2Ti, 512Gi, 1.5Ti. The function also validates that the diff --git a/src/aks-preview/azext_aks_preview/azurecontainerstorage/acstor_ops.py b/src/aks-preview/azext_aks_preview/azurecontainerstorage/acstor_ops.py index 4369e6e4e1a..9a5920e58e2 100644 --- a/src/aks-preview/azext_aks_preview/azurecontainerstorage/acstor_ops.py +++ b/src/aks-preview/azext_aks_preview/azurecontainerstorage/acstor_ops.py @@ -12,6 +12,9 @@ CONST_ACSTOR_EXT_INSTALLATION_NAME, CONST_ACSTOR_EXT_INSTALLATION_NAMESPACE, CONST_ACSTOR_K8S_EXTENSION_NAME, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAMESPACE, + CONST_DISTRIBUTED_CACHE_K8S_EXTENSION_NAME, CONST_DISK_TYPE_EPHEMERAL_VOLUME_ONLY, CONST_DISK_TYPE_PV_WITH_ANNOTATION, CONST_EPHEMERAL_NVME_PERF_TIER_STANDARD, @@ -797,3 +800,109 @@ def perform_azure_container_storage_update( raise UnknownError( "Failed to disable Azure Container Storage with error: %s" % delete_ex ) from delete_ex + + +def perform_enable_distributed_cache( + cmd, + resource_group, + cluster_name, + is_extension_installed=False, + is_called_from_extension=False, +): + # This will be set true only when aks-preview extension is used + # and we want the aks-preview ManagedClusterDecorator to call the + # perform_enable_distributed_cache function. + if not is_called_from_extension: + return + + # Enabling only installs the install controller. Cache components are + # deployed later, driven by the creation of the corresponding CR. + client_factory = get_k8s_extension_module(CONST_K8S_EXTENSION_CLIENT_FACTORY_MOD_NAME) + client = client_factory.cf_k8s_extension_operation(cmd.cli_ctx) + k8s_extension_custom_mod = get_k8s_extension_module(CONST_K8S_EXTENSION_CUSTOM_MOD_NAME) + + if is_extension_installed: + logger.warning("Distributed cache is already enabled on the cluster.") + return + + try: + result = k8s_extension_custom_mod.create_k8s_extension( + cmd, + client, + resource_group, + cluster_name, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, + "managedClusters", + CONST_DISTRIBUTED_CACHE_K8S_EXTENSION_NAME, + auto_upgrade_minor_version=True, + release_train="stable", + scope="cluster", + release_namespace=CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAMESPACE, + ) + long_op_result = LongRunningOperation(cmd.cli_ctx)(result) + if long_op_result.provisioning_state == "Succeeded": + logger.warning("Distributed cache successfully installed") + except Exception as ex: # pylint: disable=broad-except + logger.error("Distributed cache failed to install.\nError: %s", ex) + logger.warning("Cleaning up the cluster by disabling distributed cache") + try: + delete_op_result = k8s_extension_custom_mod.delete_k8s_extension( + cmd, + client, + resource_group, + cluster_name, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, + "managedClusters", + yes=True, + ) + LongRunningOperation(cmd.cli_ctx)(delete_op_result) + logger.warning( + "Please retry enabling distributed cache by running " + "`az aks update` along with " + "`--enable-azure-container-storage distributedcache`" + ) + except Exception as delete_ex: # pylint: disable=broad-except + raise UnknownError( + "Failed to clean up distributed cache with error: %s" % delete_ex + ) from delete_ex + + +def perform_disable_distributed_cache( + cmd, + resource_group, + cluster_name, + is_extension_installed=False, + is_called_from_extension=False, +): + # This will be set true only when aks-preview extension is used + # and we want the aks-preview ManagedClusterDecorator to call the + # perform_disable_distributed_cache function. + if not is_called_from_extension: + return + + client_factory = get_k8s_extension_module(CONST_K8S_EXTENSION_CLIENT_FACTORY_MOD_NAME) + client = client_factory.cf_k8s_extension_operation(cmd.cli_ctx) + k8s_extension_custom_mod = get_k8s_extension_module(CONST_K8S_EXTENSION_CUSTOM_MOD_NAME) + + if not is_extension_installed: + logger.warning("Distributed cache is not enabled on the cluster.") + return + + # Deleting the install controller triggers removal of the remaining cache + # components from the cluster. + try: + delete_op_result = k8s_extension_custom_mod.delete_k8s_extension( + cmd, + client, + resource_group, + cluster_name, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, + "managedClusters", + yes=True, + ) + LongRunningOperation(cmd.cli_ctx)(delete_op_result) + logger.warning("Distributed cache has been disabled.") + except Exception as delete_ex: # pylint: disable=broad-except + raise UnknownError( + "Failed to disable distributed cache with error: %s" % delete_ex + ) from delete_ex diff --git a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py index cbf374a13e2..8ba0f3efebd 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -60,6 +60,7 @@ CONST_ACSTOR_EXT_INSTALLATION_NAME, CONST_ACSTOR_V1_EXT_INSTALLATION_NAME, CONST_ACSTOR_VERSION_V1, + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, ) from azext_aks_preview._helpers import ( check_is_apiserver_vnet_integration_cluster, @@ -94,6 +95,8 @@ perform_disable_azure_container_storage_v1, perform_enable_azure_container_storage_v1, perform_azure_container_storage_update, + perform_enable_distributed_cache, + perform_disable_distributed_cache, ) from azext_aks_preview.azuremonitormetrics.azuremonitorprofile import ( ensure_azure_monitor_profile_prerequisites, @@ -290,6 +293,12 @@ def external_functions(self) -> SimpleNamespace: "perform_disable_azure_container_storage_v1" ] = perform_disable_azure_container_storage_v1 external_functions["perform_azure_container_storage_update"] = perform_azure_container_storage_update + external_functions[ + "perform_enable_distributed_cache" + ] = perform_enable_distributed_cache + external_functions[ + "perform_disable_distributed_cache" + ] = perform_disable_distributed_cache external_functions["sanitize_loganalytics_ws_resource_id"] = sanitize_loganalytics_ws_resource_id # Override base module function with preview version that uses REST API to avoid # "Request Header Fields Too Large" errors @@ -5163,6 +5172,32 @@ def set_up_azure_container_storage(self, mc: ManagedCluster) -> ManagedCluster: enable_azure_container_storage_param = self.context.raw_param.get("enable_azure_container_storage") if enable_azure_container_storage_param: + from azext_aks_preview.azurecontainerstorage._helpers import is_distributed_cache_requested + + # Distributed cache installs the install controller only and does + # not go through the v1/v2 storage pool logic below. + if is_distributed_cache_requested(enable_azure_container_storage_param): + from azext_aks_preview.azurecontainerstorage._validators import ( + validate_enable_distributed_cache_params, + ) + validate_enable_distributed_cache_params( + enable_azure_container_storage_param, + False, + self.context.raw_param.get("storage_pool_name"), + self.context.raw_param.get("storage_pool_sku"), + self.context.raw_param.get("storage_pool_option"), + self.context.raw_param.get("storage_pool_size"), + self.context.raw_param.get("ephemeral_disk_volume_type"), + self.context.raw_param.get("ephemeral_disk_nvme_perf_tier"), + self.context.raw_param.get("container_storage_version"), + ) + self.context.set_intermediate( + "enable_distributed_cache", + True, + overwrite_exists=True, + ) + return mc + self.context.set_intermediate( "enable_azure_container_storage", enable_azure_container_storage_param, @@ -5830,6 +5865,10 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: "enable_azure_container_storage", default_value=False ) + enable_distributed_cache = self.context.get_intermediate( + "enable_distributed_cache", + default_value=False + ) enable_backup = self.context.raw_param.get("enable_backup", False) # pylint: disable=too-many-boolean-expressions @@ -5841,6 +5880,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: (enable_managed_identity and attach_acr) or need_grant_vnet_permission_to_cluster_identity or enable_azure_container_storage or + enable_distributed_cache or enable_backup ): return True @@ -6030,6 +6070,20 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: # enable azure container storage enable_azure_container_storage = self.context.get_intermediate("enable_azure_container_storage") container_storage_version = self.context.get_intermediate("container_storage_version") + + # enable distributed cache (independent of the storage pool flow) + enable_distributed_cache = self.context.get_intermediate( + "enable_distributed_cache" + ) + if enable_distributed_cache: + self.context.external_functions.perform_enable_distributed_cache( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + False, + is_called_from_extension=True, + ) + if enable_azure_container_storage: if container_storage_version is not None and container_storage_version == CONST_ACSTOR_VERSION_V1: if cluster.identity_profile is None or cluster.identity_profile["kubeletidentity"] is None: @@ -6566,6 +6620,123 @@ def update_azure_container_storage(self, mc: ManagedCluster) -> ManagedCluster: ) # pylint: disable=too-many-nested-blocks if enable_azure_container_storage_param is not None or disable_azure_container_storage_param is not None: + from azext_aks_preview.azurecontainerstorage._helpers import ( + is_distributed_cache_requested, + should_delete_extension, + get_container_storage_extension_installed, + ) + + # Distributed cache is enabled/disabled independently of the storage + # pool flow. Disable happens explicitly (`distributedcache`) or + # implicitly on a full teardown (bare `--disable...` or `all`). + dc_enable_requested = is_distributed_cache_requested(enable_azure_container_storage_param) + dc_disable_requested_explicit = is_distributed_cache_requested(disable_azure_container_storage_param) + disable_all_or_flag = should_delete_extension(disable_azure_container_storage_param) + + if dc_enable_requested or dc_disable_requested_explicit or disable_all_or_flag: + if enable_azure_container_storage_param is not None and \ + disable_azure_container_storage_param is not None: + raise MutuallyExclusiveArgumentError( + 'Conflicting flags. Cannot set --enable-azure-container-storage ' + 'and --disable-azure-container-storage together.' + ) + + try: + is_distributed_cache_installed, _ = get_container_storage_extension_installed( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + CONST_DISTRIBUTED_CACHE_EXT_INSTALLATION_NAME, + ) + except Exception as ex: + raise UnknownError( + f"An error occurred while checking if distributed cache " + f"is installed on the cluster: {str(ex)}" + ) from ex + + storage_pool_name = self.context.raw_param.get("storage_pool_name") + pool_sku = self.context.raw_param.get("storage_pool_sku") + pool_option = self.context.raw_param.get("storage_pool_option") + pool_size = self.context.raw_param.get("storage_pool_size") + + if dc_enable_requested: + from azext_aks_preview.azurecontainerstorage._validators import ( + validate_enable_distributed_cache_params, + ) + validate_enable_distributed_cache_params( + enable_azure_container_storage_param, + is_distributed_cache_installed, + storage_pool_name, + pool_sku, + pool_option, + pool_size, + self.context.raw_param.get("ephemeral_disk_volume_type"), + self.context.raw_param.get("ephemeral_disk_nvme_perf_tier"), + self.context.raw_param.get("container_storage_version"), + ) + self.context.set_intermediate( + "enable_distributed_cache", True, overwrite_exists=True + ) + elif dc_disable_requested_explicit: + from azext_aks_preview.azurecontainerstorage._validators import ( + validate_disable_distributed_cache_params, + ) + validate_disable_distributed_cache_params( + disable_azure_container_storage_param, + is_distributed_cache_installed, + storage_pool_name, + pool_sku, + pool_option, + pool_size, + self.context.raw_param.get("container_storage_version"), + ) + self.context.set_intermediate( + "disable_distributed_cache", True, overwrite_exists=True + ) + elif disable_all_or_flag and is_distributed_cache_installed: + # Bare `--disable...` / `all` also tears down distributed cache. + self.context.set_intermediate( + "disable_distributed_cache", True, overwrite_exists=True + ) + + self.context.set_intermediate( + "is_distributed_cache_installed", is_distributed_cache_installed, overwrite_exists=True + ) + + # Explicit distributed cache operations do not touch the storage + # pool flow. + if dc_enable_requested or dc_disable_requested_explicit: + return mc + + # For a bare/all disable, only continue into the storage pool + # disable flow if a storage extension (v1 or v2) is installed. + try: + is_storage_v1_installed, _ = get_container_storage_extension_installed( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + CONST_ACSTOR_V1_EXT_INSTALLATION_NAME, + ) + is_storage_v2_installed, _ = get_container_storage_extension_installed( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + CONST_ACSTOR_EXT_INSTALLATION_NAME, + ) + except Exception as ex: + raise UnknownError( + f"An error occurred while checking if Azure Container Storage " + f"is installed on the cluster: {str(ex)}" + ) from ex + if not (is_storage_v1_installed or is_storage_v2_installed): + # Distributed-cache-only cluster: teardown already queued, + # nothing more to disable. + if is_distributed_cache_installed: + return mc + raise InvalidArgumentValueError( + 'Cannot disable Azure Container Storage as it could not be found on the cluster.' + ) + self.context.set_intermediate("container_storage_version", container_storage_version, overwrite_exists=True) enable_azure_container_storage_v1 = ( @@ -8856,6 +9027,12 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: disable_azure_container_storage = self.context.get_intermediate( "disable_azure_container_storage", default_value=False ) + enable_distributed_cache = self.context.get_intermediate( + "enable_distributed_cache", default_value=False + ) + disable_distributed_cache = self.context.get_intermediate( + "disable_distributed_cache", default_value=False + ) keyvault_id = self.context.get_keyvault_id() enable_azure_keyvault_secrets_provider_addon = self.context.get_enable_kv() or ( mc.addon_profiles and mc.addon_profiles.get(CONST_AZURE_KEYVAULT_SECRETS_PROVIDER_ADDON_NAME) @@ -8867,6 +9044,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: # Note: monitoring_addon_disable_postprocessing_required is no longer used - cleanup is done upfront # pylint: disable=too-many-boolean-expressions if (enable_azure_container_storage or disable_azure_container_storage) or \ + (enable_distributed_cache or disable_distributed_cache) or \ (keyvault_id and enable_azure_keyvault_secrets_provider_addon) or \ (monitoring_addon_postprocessing_required) or \ enable_backup: @@ -8951,6 +9129,31 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: existing_ephemeral_nvme_perf_tier = self.context.get_intermediate("current_ephemeral_nvme_perf_tier") pool_option = self.context.raw_param.get("storage_pool_option") + # enable/disable distributed cache (independent of the storage pool flow) + enable_distributed_cache = self.context.get_intermediate( + "enable_distributed_cache" + ) + disable_distributed_cache = self.context.get_intermediate( + "disable_distributed_cache" + ) + is_distributed_cache_installed = self.context.get_intermediate("is_distributed_cache_installed") + if enable_distributed_cache: + self.context.external_functions.perform_enable_distributed_cache( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + is_distributed_cache_installed, + is_called_from_extension=True, + ) + if disable_distributed_cache: + self.context.external_functions.perform_disable_distributed_cache( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + is_distributed_cache_installed, + is_called_from_extension=True, + ) + # enable azure container storage if enable_azure_container_storage: if container_storage_version is not None and container_storage_version == CONST_ACSTOR_VERSION_V1: diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_validators.py b/src/aks-preview/azext_aks_preview/tests/latest/test_validators.py index 5ebffe2db4c..d1e04991d2d 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_validators.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_validators.py @@ -1972,6 +1972,132 @@ def test_enable_with_storagepool_size(self): self.assertEqual(str(cm.exception), err) +class TestValidateEnableDistributedCache(unittest.TestCase): + def test_enable_distributed_cache(self): + acstor_validator.validate_enable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + False, None, None, None, None, None, None, None, + ) + + def test_enable_when_already_installed(self): + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_enable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + True, None, None, None, None, None, None, None, + ) + err = ( + 'Cannot enable distributed cache as it is already enabled on the cluster.' + ) + self.assertEqual(str(cm.exception), err) + + def test_enable_combined_with_other_type(self): + other_type = acstor_consts.CONST_STORAGE_POOL_TYPE_EPHEMERAL_DISK + storage_types = [acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, other_type] + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_enable_distributed_cache_params( + storage_types, False, None, None, None, None, None, None, None, + ) + err = ( + f"'{acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}' cannot be combined with other storage " + f"options ('{other_type}'). Distributed cache is enabled independently. " + f"Please run --enable-azure-container-storage {acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE} " + "on its own." + ) + self.assertEqual(str(cm.exception), err) + + def test_enable_with_storage_pool_name(self): + storage_pool_name = "valid-name" + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_enable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + False, storage_pool_name, None, None, None, None, None, None, + ) + err = ( + '--storage-pool-name cannot be used with ' + '--enable-azure-container-storage distributedcache. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + self.assertEqual(str(cm.exception), err) + + def test_enable_with_container_storage_version(self): + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_enable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + False, None, None, None, None, None, None, "1", + ) + err = ( + '--container-storage-version cannot be used with ' + '--enable-azure-container-storage distributedcache. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + self.assertEqual(str(cm.exception), err) + + +class TestValidateDisableDistributedCache(unittest.TestCase): + def test_disable_distributed_cache(self): + acstor_validator.validate_disable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + True, None, None, None, None, None, + ) + + def test_disable_when_not_installed(self): + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_disable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + False, None, None, None, None, None, + ) + err = ( + 'Cannot disable distributed cache as it could not be found on the cluster.' + ) + self.assertEqual(str(cm.exception), err) + + def test_disable_combined_with_other_type(self): + other_type = acstor_consts.CONST_ACSTOR_ALL + storage_types = [acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, other_type] + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_disable_distributed_cache_params( + storage_types, True, None, None, None, None, None, + ) + err = ( + f"'{acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE}' cannot be combined with other storage " + f"options ('{other_type}'). Distributed cache is disabled independently. " + f"Please run --disable-azure-container-storage {acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE} " + "on its own." + ) + self.assertEqual(str(cm.exception), err) + + def test_disable_with_storage_pool_name(self): + storage_pool_name = "valid-name" + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_disable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + True, storage_pool_name, None, None, None, None, + ) + err = ( + '--storage-pool-name cannot be used with ' + '--disable-azure-container-storage distributedcache. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + self.assertEqual(str(cm.exception), err) + + def test_disable_with_container_storage_version(self): + with self.assertRaises(InvalidArgumentValueError) as cm: + acstor_validator.validate_disable_distributed_cache_params( + acstor_consts.CONST_STORAGE_POOL_TYPE_DISTRIBUTED_CACHE, + True, None, None, None, None, "1", + ) + err = ( + '--container-storage-version cannot be used with ' + '--disable-azure-container-storage distributedcache. ' + 'Distributed cache does not require or support any storage pool configuration. ' + 'Please remove these parameters and try again.' + ) + self.assertEqual(str(cm.exception), err) + + class GatewayPrefixSizeSpace: def __init__(self, gateway_prefix_size=None, mode=None): self.gateway_prefix_size = gateway_prefix_size