From d9a356207afa9219794eec2adadd8c6395f55f32 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 25 May 2026 12:37:32 +0530 Subject: [PATCH 01/19] [aks-preview] Add --enable-backup to az aks create and az aks update Adds three new arguments to both 'az aks create' and 'az aks update' that orchestrate the same flow as 'az dataprotection enable-backup trigger': --enable-backup --backup-strategy {Week|Month|DisasterRecovery|Custom} --backup-configuration-file The backup orchestration (vault, policy, storage account, k8s extension install, trusted access, role assignments, backup instance creation) is delegated to the 'dataprotection' CLI extension via a thin shim (aks_backup.py) that imports the helper lazily. If the dataprotection extension is not installed, an actionable error is raised. Wired into both create and update decorators via the postprocessing_after_mc_created path so backup runs only after the cluster mutation succeeds. --- src/aks-preview/azext_aks_preview/_help.py | 6 ++ src/aks-preview/azext_aks_preview/_params.py | 56 +++++++++++++++++++ .../azext_aks_preview/aks_backup.py | 52 +++++++++++++++++ src/aks-preview/azext_aks_preview/custom.py | 8 +++ .../managed_cluster_decorator.py | 33 ++++++++++- 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/aks-preview/azext_aks_preview/aks_backup.py diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 0450cd679f2..124c868d632 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -822,6 +822,8 @@ text: az aks create -g MyResourceGroup -n MyManagedCluster --control-plane-scaling-size H4 - name: Create an automatic cluster with hosted system components enabled. text: az aks create -g MyResourceGroup -n MyManagedCluster --sku automatic --enable-hosted-system + - name: Create a kubernetes cluster with Azure Backup enabled (default Week strategy). Requires the 'dataprotection' extension. Implicitly waits for cluster creation. + text: az aks create -g MyResourceGroup -n MyManagedCluster --generate-ssh-keys --enable-backup --yes """ @@ -1563,6 +1565,10 @@ text: az aks update -g MyResourceGroup -n MyManagedCluster --safeguards-level Warning --safeguards-excluded-ns ns1,ns2 - name: Enable Azure Monitor logs for a kubernetes cluster text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs + - name: Enable Azure Backup for a kubernetes cluster (default Week strategy). Requires the 'dataprotection' extension. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --yes + - name: Enable Azure Backup with a custom strategy using an existing vault and policy + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --backup-strategy Custom --backup-configuration-file @config.json --yes - name: Disable Azure Monitor logs for a kubernetes cluster text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-azure-monitor-logs - name: Update a kubernetes cluster to clear any namespaces excluded from safeguards. Assumes azure policy addon is already enabled diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index f4deb15bca9..000393c6521 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -33,6 +33,7 @@ tags_type, zones_type, ) +from azure.cli.core.commands.validators import validate_file_or_dict from azext_aks_preview._validators import ( validate_nat_gateway_managed_outbound_ipv6_count, validate_nat_gateway_v2_params, @@ -1279,6 +1280,34 @@ def load_arguments(self, _): is_preview=True, help="Enable continuous control plane and addon monitor for the cluster.", ) + # Backup (delegates to the dataprotection extension) + c.argument( + "enable_backup", + action="store_true", + is_preview=True, + help="Enable Azure Backup for this AKS cluster. Orchestrates the same flow as " + "'az dataprotection enable-backup trigger' (requires the 'dataprotection' extension). " + "Implicitly waits for cluster creation to complete (ignores --no-wait).", + ) + c.argument( + "backup_strategy", + arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), + is_preview=True, + help="Backup strategy preset. Week (default, 7-day operational retention), Month " + "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " + "retention), Custom (bring your own vault and policy via --backup-configuration-file). " + "Only valid with --enable-backup.", + ) + c.argument( + "backup_configuration_file", + options_list=["--backup-configuration-file"], + type=validate_file_or_dict, + is_preview=True, + help="Path to a JSON backup configuration file (@file.json) or inline JSON string. " + "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " + "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " + "for Custom strategy. Only valid with --enable-backup.", + ) with self.argument_context("aks update") as c: # managed cluster paramerters @@ -1918,6 +1947,33 @@ def load_arguments(self, _): is_preview=True, help="Disable continuous control plane and addon monitor for the cluster.", ) + # Backup (delegates to the dataprotection extension) + c.argument( + "enable_backup", + action="store_true", + is_preview=True, + help="Enable Azure Backup for this AKS cluster. Orchestrates the same flow as " + "'az dataprotection enable-backup trigger' (requires the 'dataprotection' extension).", + ) + c.argument( + "backup_strategy", + arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), + is_preview=True, + help="Backup strategy preset. Week (default, 7-day operational retention), Month " + "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " + "retention), Custom (bring your own vault and policy via --backup-configuration-file). " + "Only valid with --enable-backup.", + ) + c.argument( + "backup_configuration_file", + options_list=["--backup-configuration-file"], + type=validate_file_or_dict, + is_preview=True, + help="Path to a JSON backup configuration file (@file.json) or inline JSON string. " + "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " + "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " + "for Custom strategy. Only valid with --enable-backup.", + ) with self.argument_context("aks delete") as c: c.argument("if_match") diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py new file mode 100644 index 00000000000..7d4766c9b1f --- /dev/null +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -0,0 +1,52 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Helpers that delegate AKS backup enablement to the dataprotection CLI extension. + +The actual orchestration (vault, policy, storage account, extension install, +trusted access, role assignments, backup instance) lives in the +``dataprotection`` extension. This module is a thin shim that: + +* loads that extension's path lazily (so ``az aks`` works without it), +* raises an actionable error if the extension is not installed, +* derives the AKS datasource ARM id from the resource group + cluster name. +""" + +from azure.cli.core.azclierror import UnknownError +from azure.cli.core.commands.client_factory import get_subscription_id + + +def enable_aks_backup(cmd, resource_group_name, cluster_name, + backup_strategy, backup_configuration_file, yes): + """Enable Azure Backup for an AKS cluster by delegating to the + ``dataprotection`` extension. + + Raises ``UnknownError`` if the extension is not installed. + """ + try: + from azure.cli.core.extension.operations import add_extension_to_path + add_extension_to_path("dataprotection") + from azext_dataprotection.manual.aks.aks_helper import ( + dataprotection_enable_backup_helper, + ) + except ImportError: + raise UnknownError( # pylint: disable=raise-missing-from + "Please add CLI extension `dataprotection` to use --enable-backup with " + "'az aks create' / 'az aks update'.\n" + "Run command `az extension add --name dataprotection`." + ) + + subscription_id = get_subscription_id(cmd.cli_ctx) + datasource_id = ( + f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.ContainerService/managedClusters/{cluster_name}" + ) + dataprotection_enable_backup_helper( + cmd, + datasource_id, + backup_strategy or "Week", + backup_configuration_file or {}, + yes=yes, + ) diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 77ab483c1ad..af27b8dee23 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1185,6 +1185,10 @@ def aks_create( control_plane_scaling_size=None, # health monitor enable_continuous_control_plane_and_addon_monitor=False, + # backup (delegates to the dataprotection extension) + enable_backup=False, + backup_strategy=None, + backup_configuration_file=None, ): # DO NOT MOVE: get all the original parameters and save them as a dictionary raw_parameters = locals() @@ -1439,6 +1443,10 @@ def aks_update( # health monitor enable_continuous_control_plane_and_addon_monitor=False, disable_continuous_control_plane_and_addon_monitor=False, + # backup (delegates to the dataprotection extension) + enable_backup=False, + backup_strategy=None, + backup_configuration_file=None, ): # DO NOT MOVE: get all the original parameters and save them as a dictionary raw_parameters = locals() 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 60f22700aaa..b10d8af4e88 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -296,6 +296,9 @@ def external_functions(self) -> SimpleNamespace: external_functions["ensure_container_insights_for_monitoring"] = ( ensure_container_insights_for_monitoring_preview ) + # AKS backup (delegates to the dataprotection extension) + from azext_aks_preview.aks_backup import enable_aks_backup + external_functions["enable_aks_backup"] = enable_aks_backup self.__external_functions = SimpleNamespace(**external_functions) return self.__external_functions @@ -5293,6 +5296,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: "enable_azure_container_storage", default_value=False ) + enable_backup = self.context.raw_param.get("enable_backup", False) # pylint: disable=too-many-boolean-expressions if ( @@ -5302,7 +5306,8 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: azuremonitormetrics_addon_enabled or (enable_managed_identity and attach_acr) or need_grant_vnet_permission_to_cluster_identity or - enable_azure_container_storage + enable_azure_container_storage or + enable_backup ): return True return False @@ -5560,6 +5565,17 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: resolve_assignee=False, ) + # Enable Azure Backup for the AKS cluster (delegates to dataprotection extension) + if self.context.raw_param.get("enable_backup", False): + self.context.external_functions.enable_aks_backup( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.raw_param.get("backup_strategy"), + self.context.raw_param.get("backup_configuration_file"), + self.context.raw_param.get("yes", False), + ) + def put_mc(self, mc: ManagedCluster) -> ManagedCluster: etag, match_condition = _get_etag_match_condition( self.context.get_if_match(), self.context.get_if_none_match() @@ -8191,10 +8207,12 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: monitoring_addon_postprocessing_required = self.context.get_intermediate( "monitoring_addon_postprocessing_required", default_value=False ) + enable_backup = self.context.raw_param.get("enable_backup", False) # Note: monitoring_addon_disable_postprocessing_required is no longer used - cleanup is done upfront if (enable_azure_container_storage or disable_azure_container_storage) or \ (keyvault_id and enable_azure_keyvault_secrets_provider_addon) or \ - (monitoring_addon_postprocessing_required): + (monitoring_addon_postprocessing_required) or \ + enable_backup: return True return postprocessing_required @@ -8427,6 +8445,17 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: else: raise CLIError('Keyvault secrets provider addon must be enabled to attach keyvault.\n') + # Enable Azure Backup for the AKS cluster (delegates to dataprotection extension) + if self.context.raw_param.get("enable_backup", False): + self.context.external_functions.enable_aks_backup( + self.cmd, + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.raw_param.get("backup_strategy"), + self.context.raw_param.get("backup_configuration_file"), + self.context.raw_param.get("yes", False), + ) + def put_mc(self, mc: ManagedCluster) -> ManagedCluster: etag, match_condition = _get_etag_match_condition( self.context.get_if_match(), self.context.get_if_none_match() From d1bb93b59e3cd684881682bd932bda4723a0c975 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 25 May 2026 12:41:52 +0530 Subject: [PATCH 02/19] [aks-preview] HISTORY: note --enable-backup under Pending --- src/aks-preview/HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index eedb4f39f93..27f08f8d5ca 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -12,6 +12,7 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ * `az aks create` and `az aks nodepool add`: Add `--enable-osdisk-full-caching` (preview) to enable the full-cache ephemeral OS disk feature for a node pool. Requires AFEC registration `Microsoft.ContainerService/FullCachePreview`. Property is immutable after node pool creation. +* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration-file` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. 21.0.0b1 ++++++ From 08bc672de86956c5f9e1fcbf3316cd46c20d6cfb Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 25 May 2026 12:48:45 +0530 Subject: [PATCH 03/19] [aks-preview] aks_backup: align continuation indent + silence too-many-positional --- src/aks-preview/azext_aks_preview/aks_backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index 7d4766c9b1f..8d8fe63d833 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -18,8 +18,8 @@ from azure.cli.core.commands.client_factory import get_subscription_id -def enable_aks_backup(cmd, resource_group_name, cluster_name, - backup_strategy, backup_configuration_file, yes): +def enable_aks_backup(cmd, resource_group_name, cluster_name, # pylint: disable=too-many-positional-arguments + backup_strategy, backup_configuration_file, yes): """Enable Azure Backup for an AKS cluster by delegating to the ``dataprotection`` extension. From e2739c7cdf0647adaa77667c9a115d7611f0b847 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 25 May 2026 15:33:45 +0530 Subject: [PATCH 04/19] [aks-preview] _params: note backup-strategy enum mirrors dataprotection --- src/aks-preview/azext_aks_preview/_params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 000393c6521..8d72c64421c 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -1291,6 +1291,7 @@ def load_arguments(self, _): ) c.argument( "backup_strategy", + # NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " @@ -1957,6 +1958,7 @@ def load_arguments(self, _): ) c.argument( "backup_strategy", + # NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " From 237444b69370982a00924856636292ef285809a6 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 25 May 2026 16:01:02 +0530 Subject: [PATCH 05/19] [aks-preview] Address CI lint feedback - Rename --backup-configuration-file to --backup-configuration (drops the misleading -file suffix; the validator accepts both @file.json and inline JSON). Also resolves option_length_too_long (HIGH). - Silence too-many-boolean-expressions in the update decorator's check_is_postprocessing_required, matching the existing pylint-disable already used in the create decorator's equivalent block. --- src/aks-preview/azext_aks_preview/_help.py | 2 +- src/aks-preview/azext_aks_preview/_params.py | 12 ++++++------ .../azext_aks_preview/managed_cluster_decorator.py | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 124c868d632..553e5692531 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -1568,7 +1568,7 @@ - name: Enable Azure Backup for a kubernetes cluster (default Week strategy). Requires the 'dataprotection' extension. text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --yes - name: Enable Azure Backup with a custom strategy using an existing vault and policy - text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --backup-strategy Custom --backup-configuration-file @config.json --yes + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-backup --backup-strategy Custom --backup-configuration @config.json --yes - name: Disable Azure Monitor logs for a kubernetes cluster text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-azure-monitor-logs - name: Update a kubernetes cluster to clear any namespaces excluded from safeguards. Assumes azure policy addon is already enabled diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 8d72c64421c..09d79b22923 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -1296,15 +1296,15 @@ def load_arguments(self, _): is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " - "retention), Custom (bring your own vault and policy via --backup-configuration-file). " + "retention), Custom (bring your own vault and policy via --backup-configuration). " "Only valid with --enable-backup.", ) c.argument( "backup_configuration_file", - options_list=["--backup-configuration-file"], + options_list=["--backup-configuration"], type=validate_file_or_dict, is_preview=True, - help="Path to a JSON backup configuration file (@file.json) or inline JSON string. " + help="Backup configuration as inline JSON string or @file.json. " "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " "for Custom strategy. Only valid with --enable-backup.", @@ -1963,15 +1963,15 @@ def load_arguments(self, _): is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " - "retention), Custom (bring your own vault and policy via --backup-configuration-file). " + "retention), Custom (bring your own vault and policy via --backup-configuration). " "Only valid with --enable-backup.", ) c.argument( "backup_configuration_file", - options_list=["--backup-configuration-file"], + options_list=["--backup-configuration"], type=validate_file_or_dict, is_preview=True, - help="Path to a JSON backup configuration file (@file.json) or inline JSON string. " + help="Backup configuration as inline JSON string or @file.json. " "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " "for Custom strategy. Only valid with --enable-backup.", 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 b10d8af4e88..719d91a44a2 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -8209,6 +8209,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: ) enable_backup = self.context.raw_param.get("enable_backup", False) # 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 \ (keyvault_id and enable_azure_keyvault_secrets_provider_addon) or \ (monitoring_addon_postprocessing_required) or \ From 2ce8293850e0d133ea54a641676b46915655e694 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 26 May 2026 08:30:27 +0530 Subject: [PATCH 06/19] [aks-preview] Extract backup-strategy enum into a shared constant Per PR review: define aks_backup_strategies once at module scope in _params.py and reference it from both 'aks create' and 'aks update' argument contexts instead of inlining the list twice. --- src/aks-preview/azext_aks_preview/_params.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 09d79b22923..5496b52de5b 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -562,6 +562,10 @@ CONST_UPGRADE_STRATEGY_BLUE_GREEN, ] +# AKS backup strategy presets exposed by --backup-strategy. +# NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. +aks_backup_strategies = ["Week", "Month", "DisasterRecovery", "Custom"] + def load_arguments(self, _): acr_arg_type = CLIArgumentType(metavar="ACR_NAME_OR_RESOURCE_ID") @@ -1291,8 +1295,7 @@ def load_arguments(self, _): ) c.argument( "backup_strategy", - # NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. - arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), + arg_type=get_enum_type(aks_backup_strategies), is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " @@ -1958,8 +1961,7 @@ def load_arguments(self, _): ) c.argument( "backup_strategy", - # NOTE: must mirror CONST_AKS_BACKUP_STRATEGIES in azext_dataprotection.manual._consts. - arg_type=get_enum_type(["Week", "Month", "DisasterRecovery", "Custom"]), + arg_type=get_enum_type(aks_backup_strategies), is_preview=True, help="Backup strategy preset. Week (default, 7-day operational retention), Month " "(30-day operational retention), DisasterRecovery (7-day operational + 90-day vault " From 938d597f2ac782c464cba196cef9b6a257dab13f Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 1 Jun 2026 09:54:32 +0530 Subject: [PATCH 07/19] [aks-preview] aks_backup: prompt to install dataprotection extension on demand Per PR review: when --enable-backup is used and the dataprotection extension is not installed, prompt the user (or install silently when --yes is passed) instead of just raising UnknownError. Falls back to a clean UnknownError on NoTTYException (CI) or user decline, with the actionable install command spelled out. --- .../azext_aks_preview/aks_backup.py | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index 8d8fe63d833..f4a023619f2 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -10,34 +10,76 @@ ``dataprotection`` extension. This module is a thin shim that: * loads that extension's path lazily (so ``az aks`` works without it), -* raises an actionable error if the extension is not installed, +* offers to install the extension on-demand if it is missing (and installs + silently when ``--yes`` is passed), * derives the AKS datasource ARM id from the resource group + cluster name. """ +from knack.log import get_logger +from knack.prompting import prompt_y_n, NoTTYException + from azure.cli.core.azclierror import UnknownError -from azure.cli.core.commands.client_factory import get_subscription_id +DATAPROTECTION_EXTENSION_NAME = "dataprotection" + +logger = get_logger(__name__) -def enable_aks_backup(cmd, resource_group_name, cluster_name, # pylint: disable=too-many-positional-arguments - backup_strategy, backup_configuration_file, yes): - """Enable Azure Backup for an AKS cluster by delegating to the - ``dataprotection`` extension. - Raises ``UnknownError`` if the extension is not installed. +def _ensure_dataprotection_extension(cmd, yes): + """Make ``azext_dataprotection.manual.aks.aks_helper`` importable. + + If the ``dataprotection`` extension is not installed, prompt the user to + install it (or install silently when ``yes`` is True). Raises + ``UnknownError`` if the user declines or the install fails. """ + from azure.cli.core.extension.operations import add_extension_to_path + try: - from azure.cli.core.extension.operations import add_extension_to_path - add_extension_to_path("dataprotection") - from azext_dataprotection.manual.aks.aks_helper import ( + add_extension_to_path(DATAPROTECTION_EXTENSION_NAME) + from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=unused-import dataprotection_enable_backup_helper, ) + return except ImportError: - raise UnknownError( # pylint: disable=raise-missing-from - "Please add CLI extension `dataprotection` to use --enable-backup with " - "'az aks create' / 'az aks update'.\n" - "Run command `az extension add --name dataprotection`." + pass + + install_msg = ( + f"The '{DATAPROTECTION_EXTENSION_NAME}' extension is required for " + "--enable-backup but is not installed. Install it now?" + ) + proceed = yes + if not proceed: + try: + proceed = prompt_y_n(install_msg, default="y") + except NoTTYException: + proceed = False + if not proceed: + raise UnknownError( + f"The '{DATAPROTECTION_EXTENSION_NAME}' extension is required for " + "--enable-backup with 'az aks create' / 'az aks update'.\n" + f"Run `az extension add --name {DATAPROTECTION_EXTENSION_NAME}` " + "and retry, or rerun with --yes to auto-install." ) + logger.warning("Installing extension '%s'...", DATAPROTECTION_EXTENSION_NAME) + from azure.cli.core.extension.operations import add_extension + add_extension(cmd=cmd, extension_name=DATAPROTECTION_EXTENSION_NAME) + add_extension_to_path(DATAPROTECTION_EXTENSION_NAME) + + +def enable_aks_backup(cmd, resource_group_name, cluster_name, # pylint: disable=too-many-positional-arguments + backup_strategy, backup_configuration_file, yes): + """Enable Azure Backup for an AKS cluster by delegating to the + ``dataprotection`` extension. + """ + from azure.cli.core.commands.client_factory import get_subscription_id + + _ensure_dataprotection_extension(cmd, yes) + + from azext_dataprotection.manual.aks.aks_helper import ( + dataprotection_enable_backup_helper, + ) + subscription_id = get_subscription_id(cmd.cli_ctx) datasource_id = ( f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}" From b70c8ae64cdf7b7e18904b6741b6db7d0035f3ba Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 1 Jun 2026 14:40:18 +0530 Subject: [PATCH 08/19] [aks-preview] Add live tests for aks create/update --enable-backup Two new @live_only scenario tests under AzureKubernetesServiceScenarioTest that exercise the full orchestration end-to-end: * test_aks_create_with_enable_backup - greenfield create with --enable-backup --backup-strategy Week, validates k8s extension, vault, policy, and backup instance reach ProtectionConfigured. * test_aks_update_with_enable_backup - brownfield: plain cluster then aks update --enable-backup; same validations. Shared helpers (_setup_backup_test / _validate_backup / _cleanup_backup) install the dataprotection + k8s-extension extensions, locate the regional backup vault by datasource resourceID, poll for ProtectionConfigured, and best-effort drain backup instances + policies + delete the vault before the preparer reaps the cluster RG. --- .../tests/latest/test_aks_commands.py | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 46769d53699..cf83ca7b080 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -14769,6 +14769,271 @@ def test_aks_create_with_azuremonitorappmonitoring( ], ) + # ------------------------------------------------------------------ + # --enable-backup helpers + tests + # ------------------------------------------------------------------ + + def _setup_backup_test(self, resource_group, resource_group_location, aks_name): + """Common setup: install sibling extensions used by --enable-backup + orchestration (`dataprotection`, `k8s-extension`) and seed kwargs.""" + self.test_resources_count = 0 + node_vm_size = "standard_d2s_v3" + self.kwargs.update( + { + "resource_group": resource_group, + "name": aks_name, + "location": resource_group_location, + "ssh_key_value": self.generate_ssh_keys(), + "node_vm_size": node_vm_size, + } + ) + self.cmd("extension add --name dataprotection") + self.cmd("extension add --name k8s-extension") + + def _validate_backup(self, resource_group, aks_name, resource_group_location): + """Validate k8s extension, vault, policy, backup instance. + Returns (vault_name, instance_name, policy_name) for cleanup. + """ + vault_name = None + instance_name = None + policy_name = None + + # 1. k8s extension + self.cmd( + "k8s-extension show --resource-group {resource_group} " + "--cluster-name {name} --cluster-type managedClusters " + "--name azure-aks-backup", + checks=[self.check("provisioningState", "Succeeded")], + ) + + sub_id = self.cmd("account show --query id -o tsv").output.strip() + cluster_id = ( + f"/subscriptions/{sub_id}/resourceGroups/{resource_group}" + f"/providers/Microsoft.ContainerService/managedClusters/" + f"{aks_name}" + ) + backup_rg = f"AKSAzureBackup_{resource_group_location}" + self.kwargs["backup_rg"] = backup_rg + + # 2. vault exists + vaults = self.cmd( + "dataprotection backup-vault list -g {backup_rg}" + ).get_output_in_json() + self.assertGreaterEqual( + len(vaults), 1, + f"Expected at least one backup vault in {backup_rg}" + ) + + # 3. find the vault hosting OUR backup instance + policy + backup_vault_name = None + backup_policy_count = 0 + for vault in vaults: + v_name = vault["name"] + self.kwargs["vault_name"] = v_name + instances = self.cmd( + "dataprotection backup-instance list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for inst in instances: + ds_id = ( + inst.get("properties", {}) + .get("dataSourceInfo", {}) + .get("resourceID", "") + ) + if ds_id.lower() == cluster_id.lower(): + backup_vault_name = v_name + instance_name = inst["name"] + vault_name = v_name + break + if backup_vault_name: + policies = self.cmd( + "dataprotection backup-policy list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + backup_policy_count = len(policies) + if policies: + policy_id = ( + inst.get("properties", {}) + .get("policyInfo", {}) + .get("policyId", "") + ) + for pol in policies: + if pol["id"].lower() == policy_id.lower(): + policy_name = pol["name"] + break + if policy_name is None: + policy_name = policies[0]["name"] + break + self.assertIsNotNone( + backup_vault_name, + f"No backup vault hosting a backup instance for " + f"cluster {aks_name} was found in {backup_rg}" + ) + self.assertGreaterEqual( + backup_policy_count, 1, + f"Expected at least one backup policy on vault " + f"{backup_vault_name}" + ) + + # 4. poll until ProtectionConfigured + self.kwargs["vault_name"] = backup_vault_name + self.kwargs["instance_name"] = instance_name + final_status = None + for _ in range(8): + inst = self.cmd( + "dataprotection backup-instance show " + "-g {backup_rg} --vault-name {vault_name} " + "--backup-instance-name {instance_name}" + ).get_output_in_json() + final_status = ( + inst.get("properties", {}) + .get("protectionStatus", {}) + .get("status") + ) + if final_status == "ProtectionConfigured": + break + time.sleep(30) + self.assertEqual( + final_status, "ProtectionConfigured", + f"Backup instance {instance_name} did not reach " + f"ProtectionConfigured (last status: {final_status})" + ) + + return vault_name, instance_name, policy_name + + def _cleanup_backup(self, vault_name): + """Best-effort cleanup of the shared regional backup vault: drain + all backup instances and policies, then delete the vault. The + cluster + cluster RG are reaped by `AKSCustomResourceGroupPreparer`. + """ + backup_rg = self.kwargs.get("backup_rg") + if not (vault_name and backup_rg): + return + + # 1. Disable immutability + soft-delete on the vault so that BIs + # with active recovery points can be force-deleted. + try: + self.cmd( + "dataprotection backup-vault update " + "-g {backup_rg} --vault-name {vault_name} " + "--set properties.securitySettings.immutabilitySettings.state=Disabled " + "properties.securitySettings.softDeleteSettings.state=Off" + ) + except Exception: # pylint: disable=broad-except + pass + + # 2. Delete ALL backup instances on the vault. + try: + all_instances = self.cmd( + "dataprotection backup-instance list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for bi in all_instances: + self.kwargs["_bi"] = bi["name"] + try: + self.cmd( + "dataprotection backup-instance delete " + "-g {backup_rg} --vault-name {vault_name} " + "--backup-instance-name {_bi} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + except Exception: # pylint: disable=broad-except + pass + + # 3. Delete ALL backup policies on the vault. + try: + all_policies = self.cmd( + "dataprotection backup-policy list " + "-g {backup_rg} --vault-name {vault_name}" + ).get_output_in_json() + for pol in all_policies: + self.kwargs["_pol"] = pol["name"] + try: + self.cmd( + "dataprotection backup-policy delete " + "-g {backup_rg} --vault-name {vault_name} " + "--name {_pol} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + except Exception: # pylint: disable=broad-except + pass + + # 4. Delete the vault (now empty). + try: + self.cmd( + "dataprotection backup-vault delete " + "-g {backup_rg} --vault-name {vault_name} --yes" + ) + except Exception: # pylint: disable=broad-except + pass + + # ---- aks create --enable-backup ---- + + @live_only() + @AllowLargeResponse(999999) + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westcentralus" + ) + def test_aks_create_with_enable_backup(self, resource_group, resource_group_location): + """aks create --enable-backup: full 8-step orchestration after + cluster LRO, then validate extension / vault / policy / BI.""" + aks_name = self.create_random_name("cliakstest", 16) + self._setup_backup_test(resource_group, resource_group_location, aks_name) + vault_name = None + try: + self.cmd( + "aks create --resource-group={resource_group} --name={name} " + "--location={location} --ssh-key-value={ssh_key_value} " + "--node-vm-size={node_vm_size} --node-count 3 " + "--enable-managed-identity " + "--enable-backup --backup-strategy Week " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + vault_name, _, _ = self._validate_backup( + resource_group, aks_name, resource_group_location + ) + finally: + self._cleanup_backup(vault_name) + + # ---- aks update --enable-backup ---- + + @live_only() + @AllowLargeResponse(999999) + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="westcentralus" + ) + def test_aks_update_with_enable_backup(self, resource_group, resource_group_location): + """aks update --enable-backup on a brownfield cluster: create the + cluster first without backup, then update with --enable-backup.""" + aks_name = self.create_random_name("cliakstest", 16) + self._setup_backup_test(resource_group, resource_group_location, aks_name) + vault_name = None + try: + # 1. Create a plain cluster (no backup). + self.cmd( + "aks create --resource-group={resource_group} --name={name} " + "--location={location} --ssh-key-value={ssh_key_value} " + "--node-vm-size={node_vm_size} --node-count 3 " + "--enable-managed-identity " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + # 2. Update: enable backup on the existing cluster. + self.cmd( + "aks update --resource-group={resource_group} --name={name} " + "--enable-backup --backup-strategy Week " + "--yes --output=json", + checks=[self.check("provisioningState", "Succeeded")], + ) + vault_name, _, _ = self._validate_backup( + resource_group, aks_name, resource_group_location + ) + finally: + self._cleanup_backup(vault_name) + # live only due to downloading k8s-extension extension @live_only() @AllowLargeResponse(999999) From cdf039a34048eb62b0e804d5ac8141fe1696a205 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 1 Jun 2026 14:56:07 +0530 Subject: [PATCH 09/19] [aks-preview] Fix error type and release notes for --enable-backup - aks_backup.py: Change UnknownError to RequiredArgumentMissingError when dataprotection extension is missing and user declines install. - HISTORY.rst: Fix --backup-configuration-file -> --backup-configuration to match the actual CLI option name. --- src/aks-preview/azext_aks_preview/aks_backup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index f4a023619f2..39024404d3f 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -18,7 +18,7 @@ from knack.log import get_logger from knack.prompting import prompt_y_n, NoTTYException -from azure.cli.core.azclierror import UnknownError +from azure.cli.core.azclierror import RequiredArgumentMissingError DATAPROTECTION_EXTENSION_NAME = "dataprotection" @@ -30,7 +30,7 @@ def _ensure_dataprotection_extension(cmd, yes): If the ``dataprotection`` extension is not installed, prompt the user to install it (or install silently when ``yes`` is True). Raises - ``UnknownError`` if the user declines or the install fails. + ``RequiredArgumentMissingError`` if the user declines or the install fails. """ from azure.cli.core.extension.operations import add_extension_to_path @@ -54,7 +54,7 @@ def _ensure_dataprotection_extension(cmd, yes): except NoTTYException: proceed = False if not proceed: - raise UnknownError( + raise RequiredArgumentMissingError( f"The '{DATAPROTECTION_EXTENSION_NAME}' extension is required for " "--enable-backup with 'az aks create' / 'az aks update'.\n" f"Run `az extension add --name {DATAPROTECTION_EXTENSION_NAME}` " @@ -76,7 +76,7 @@ def enable_aks_backup(cmd, resource_group_name, cluster_name, # pylint: disable _ensure_dataprotection_extension(cmd, yes) - from azext_dataprotection.manual.aks.aks_helper import ( + from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=import-error dataprotection_enable_backup_helper, ) From 1d6194aa3d622a104559c9518269a70d2dc7a68d Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 1 Jun 2026 15:23:04 +0530 Subject: [PATCH 10/19] [aks-preview][dataprotection] Auto-install dependent CLI extensions on demand - aks_backup.py: Fix _ensure_dataprotection_extension to catch ExtensionNotInstalledException (was only catching ImportError, so auto-install never triggered). - aks_helper.py: Add _ensure_k8s_extension() to auto-install the k8s-extension CLI extension when needed for backup extension creation, mirroring the dataprotection auto-install pattern. Thread yes parameter through the call chain. --- .../azext_aks_preview/aks_backup.py | 2 +- .../manual/aks/aks_helper.py | 65 +++++++++++++++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index 39024404d3f..d6f5f4970e3 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -40,7 +40,7 @@ def _ensure_dataprotection_extension(cmd, yes): dataprotection_enable_backup_helper, ) return - except ImportError: + except Exception: # pylint: disable=broad-except pass install_msg = ( diff --git a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py index 0392e4e6377..4e0b4ea5140 100644 --- a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py +++ b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py @@ -5,19 +5,64 @@ # -------------------------------------------------------------------------------------------- import json -from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.core.tools import parse_resource_id from knack.log import get_logger +from knack.prompting import prompt_y_n, NoTTYException logger = get_logger(__name__) +K8S_EXTENSION_NAME = "k8s-extension" + # Tag used to identify storage accounts created for AKS backup # Format: AKSAzureBackup: AKS_BACKUP_TAG_KEY = "AKSAzureBackup" +def _ensure_k8s_extension(cmd, yes=False): + """Ensure the k8s-extension CLI extension is installed and importable. + + If the extension is not installed, prompt the user to install it + (or install silently when ``yes`` is True). Raises + ``RequiredArgumentMissingError`` if the user declines. + """ + from azure.cli.core.extension.operations import add_extension_to_path + + try: + add_extension_to_path(K8S_EXTENSION_NAME) + from importlib import import_module + import_module("azext_k8s_extension.custom") # verify importable + return + except (ImportError, Exception): # pylint: disable=broad-except + pass + + install_msg = ( + f"The '{K8S_EXTENSION_NAME}' CLI extension is required to install " + "the data protection extension on the AKS cluster but is not " + "installed. Install it now?" + ) + proceed = yes + if not proceed: + try: + proceed = prompt_y_n(install_msg, default="y") + except NoTTYException: + proceed = False + if not proceed: + raise RequiredArgumentMissingError( + f"The '{K8S_EXTENSION_NAME}' CLI extension is required for " + "AKS backup operations.\n" + f"Run `az extension add --name {K8S_EXTENSION_NAME}` " + "and retry, or rerun with --yes to auto-install." + ) + + logger.warning("Installing CLI extension '%s'...", K8S_EXTENSION_NAME) + from azure.cli.core.extension.operations import add_extension + add_extension(cmd=cmd, extension_name=K8S_EXTENSION_NAME) + add_extension_to_path(K8S_EXTENSION_NAME) + + def _check_and_assign_role(cmd, role, assignee, scope, identity_name="identity", max_retries=3, retry_delay=10): """ Check if a role assignment already exists, and create it if not. @@ -506,7 +551,7 @@ def _install_backup_extension(cmd, cluster_subscription_id, backup_storage_account_name, backup_storage_account_container_name, backup_resource_group_name, - backup_storage_account): + backup_storage_account, yes=False): """Install backup extension on the cluster.""" backup_extension = _create_backup_extension( cmd, @@ -516,7 +561,8 @@ def _install_backup_extension(cmd, cluster_subscription_id, backup_storage_account_name, backup_storage_account_container_name, backup_resource_group_name, - cluster_subscription_id) + cluster_subscription_id, + yes=yes) _check_and_assign_role( cmd, @@ -1016,7 +1062,8 @@ def _show_plan_and_confirm(cluster_subscription_id, cluster_name, def _setup_extension_and_storage( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, storage_account_id, blob_container_name, - backup_resource_group_name, cluster_location, resource_tags): + backup_resource_group_name, cluster_location, resource_tags, + yes=False): """Setup backup extension and storage account (steps 3 & 4). If the extension is already installed, reuses its configured storage @@ -1074,7 +1121,8 @@ def _setup_extension_and_storage( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, sa_result[1], sa_result[2], - backup_resource_group_name, backup_storage_account) + backup_resource_group_name, backup_storage_account, + yes=yes) return backup_storage_account @@ -1141,7 +1189,8 @@ def dataprotection_enable_backup_helper( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, configuration_params.get("storageAccountResourceId"), configuration_params.get("blobContainerName"), - backup_resource_group_name, cluster_location, resource_tags) + backup_resource_group_name, cluster_location, resource_tags, + yes=yes) # Step 5: Setup backup vault logger.warning("[5/8] Setting up backup vault...") @@ -1503,7 +1552,7 @@ def _create_backup_extension( cmd, subscription_id, resource_group_name, cluster_name, storage_account_name, storage_account_container_name, storage_account_resource_group, - storage_account_subscription_id): + storage_account_subscription_id, yes=False): """Create or reuse the data protection k8s extension.""" from azext_dataprotection.vendored_sdks.azure_mgmt_kubernetesconfiguration import SourceControlConfigurationClient k8s_configuration_client = get_mgmt_service_client( @@ -1555,6 +1604,8 @@ def _create_backup_extension( logger.warning("Installing data protection extension (azure-aks-backup)...") + _ensure_k8s_extension(cmd, yes=yes) + from azure.cli.core.extension.operations import add_extension_to_path from importlib import import_module add_extension_to_path("k8s-extension") From 61d86c378b3a7389772ece024692c94d9006d756 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Mon, 1 Jun 2026 15:27:41 +0530 Subject: [PATCH 11/19] [aks-preview] Fix HISTORY.rst: --backup-configuration-file -> --backup-configuration in 21.0.0b3 --- src/aks-preview/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index b96bdd617e5..2ea2dd26e6d 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -15,7 +15,7 @@ Pending 21.0.0b3 +++++++ * Migrate code from Azure SDK to AAZ based commands for compute operations (VM). -* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration-file` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. +* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. 21.0.0b2 ++++++++ From de88d3efdb660bf0e869bec888b8f7801da8185d Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 2 Jun 2026 11:18:24 +0530 Subject: [PATCH 12/19] [aks-preview] Release enable-backup preview support --- src/aks-preview/HISTORY.rst | 5 ++++- src/aks-preview/azext_aks_preview/aks_backup.py | 2 +- src/aks-preview/setup.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 2ea2dd26e6d..58da1001dfc 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -12,10 +12,13 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +21.0.0b4 ++++++++ +* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. + 21.0.0b3 +++++++ * Migrate code from Azure SDK to AAZ based commands for compute operations (VM). -* `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. 21.0.0b2 ++++++++ diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index d6f5f4970e3..c1e02b10e50 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -36,7 +36,7 @@ def _ensure_dataprotection_extension(cmd, yes): try: add_extension_to_path(DATAPROTECTION_EXTENSION_NAME) - from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=unused-import + from azext_dataprotection.manual.aks.aks_helper import ( # pylint: disable=unused-import,import-error dataprotection_enable_backup_helper, ) return diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 95cee0fffe4..96714c013ee 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "21.0.0b3" +VERSION = "21.0.0b4" CLASSIFIERS = [ "Development Status :: 4 - Beta", From 9e69b8a3b8f75c186ce3a9bcdca01ac61658c2d3 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 2 Jun 2026 11:25:00 +0530 Subject: [PATCH 13/19] [aks-preview] Fix flake8 indentation in backup parameter --- src/aks-preview/azext_aks_preview/_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index dbbdba22ba3..5a1e11ffbe6 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -1989,7 +1989,7 @@ def load_arguments(self, _): "Supports storageAccountResourceId, blobContainerName, backupResourceGroupId, " "backupVaultId, backupPolicyId, tags. backupVaultId and backupPolicyId are required " "for Custom strategy. Only valid with --enable-backup.", - ) + ) c.argument( "control_plane_scaling_size", options_list=["--control-plane-scaling-size", "--cp-scaling-size"], From 526082678275d71cce4f47b2a2aa9342430f8cb8 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 2 Jun 2026 11:40:26 +0530 Subject: [PATCH 14/19] [dataprotection] Remove redundant prompt import in AKS helper --- src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py index 4e0b4ea5140..ebbe5f27d67 100644 --- a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py +++ b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py @@ -1055,7 +1055,6 @@ def _show_plan_and_confirm(cluster_subscription_id, cluster_name, logger.warning(" RBAC role assignments listed above.") logger.warning("") - from knack.prompting import prompt_y_n return prompt_y_n("Do you want to proceed?", default='y') From 0b4a1336c76b1a4d624a19b56407c7f17c5cf862 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Wed, 3 Jun 2026 11:20:37 +0530 Subject: [PATCH 15/19] Refactor docstring in aks_backup.py to simplify description of AKS backup enablement helpers Signed-off-by: Anshul Ahuja --- src/aks-preview/azext_aks_preview/aks_backup.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/aks_backup.py b/src/aks-preview/azext_aks_preview/aks_backup.py index c1e02b10e50..845ca013c45 100644 --- a/src/aks-preview/azext_aks_preview/aks_backup.py +++ b/src/aks-preview/azext_aks_preview/aks_backup.py @@ -3,17 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -"""Helpers that delegate AKS backup enablement to the dataprotection CLI extension. - -The actual orchestration (vault, policy, storage account, extension install, -trusted access, role assignments, backup instance) lives in the -``dataprotection`` extension. This module is a thin shim that: - -* loads that extension's path lazily (so ``az aks`` works without it), -* offers to install the extension on-demand if it is missing (and installs - silently when ``--yes`` is passed), -* derives the AKS datasource ARM id from the resource group + cluster name. -""" +"""Helpers that delegate AKS backup enablement to the dataprotection extension.""" from knack.log import get_logger from knack.prompting import prompt_y_n, NoTTYException From b0c335bb520be106144773b681d5acfee89ec8de Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Wed, 3 Jun 2026 12:43:46 +0530 Subject: [PATCH 16/19] [aks-preview] Move pending notes into 21.0.0b4 release --- src/aks-preview/HISTORY.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index d23891f8e3d..1c6d63f79f7 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -11,13 +11,13 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ + +21.0.0b4 ++++++++ * `az aks nodepool add`: Add `--secondary-network-interfaces`/`--secondary-nics` (preview) to configure secondary network interfaces on agent pool nodes. Accepts inline JSON or `@file`. Property is immutable after node pool creation. * `az aks update`: Add `--control-plane-scaling-size` parameter to update the control plane scaling size on an existing cluster with available sizes 'H2', 'H4', and 'H8'. * `az aks bastion`: Fix `--subscription` not being passed to internal `az network bastion tunnel` and bastion discovery commands. * `az aks update`: Add `--node-disruption-policy` (preview) to update the node disruption policy for a cluster. Requires AFEC registration `Microsoft.ContainerService/NodeDisruptionProfile`. This is a cluster-level property that applies to all node pools in the cluster. - -21.0.0b4 -+++++++ * `az aks create` and `az aks update`: Add `--enable-backup` (preview) to configure Azure Backup for the AKS cluster in a single command. Supports `--backup-strategy` presets (Week, Month, DisasterRecovery, Custom) and an optional `--backup-configuration` for bring-your-own vault/policy/storage. Requires the `dataprotection` CLI extension. 21.0.0b3 From 2e020dd32e270dc22f276cbd0a4203d813fe8077 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Wed, 3 Jun 2026 14:13:46 +0530 Subject: [PATCH 17/19] [aks-preview] Remove dataprotection helper changes --- .../manual/aks/aks_helper.py | 66 +++---------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py index ebbe5f27d67..0392e4e6377 100644 --- a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py +++ b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py @@ -5,64 +5,19 @@ # -------------------------------------------------------------------------------------------- import json -from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError +from azure.cli.core.azclierror import InvalidArgumentValueError from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.core.tools import parse_resource_id from knack.log import get_logger -from knack.prompting import prompt_y_n, NoTTYException logger = get_logger(__name__) -K8S_EXTENSION_NAME = "k8s-extension" - # Tag used to identify storage accounts created for AKS backup # Format: AKSAzureBackup: AKS_BACKUP_TAG_KEY = "AKSAzureBackup" -def _ensure_k8s_extension(cmd, yes=False): - """Ensure the k8s-extension CLI extension is installed and importable. - - If the extension is not installed, prompt the user to install it - (or install silently when ``yes`` is True). Raises - ``RequiredArgumentMissingError`` if the user declines. - """ - from azure.cli.core.extension.operations import add_extension_to_path - - try: - add_extension_to_path(K8S_EXTENSION_NAME) - from importlib import import_module - import_module("azext_k8s_extension.custom") # verify importable - return - except (ImportError, Exception): # pylint: disable=broad-except - pass - - install_msg = ( - f"The '{K8S_EXTENSION_NAME}' CLI extension is required to install " - "the data protection extension on the AKS cluster but is not " - "installed. Install it now?" - ) - proceed = yes - if not proceed: - try: - proceed = prompt_y_n(install_msg, default="y") - except NoTTYException: - proceed = False - if not proceed: - raise RequiredArgumentMissingError( - f"The '{K8S_EXTENSION_NAME}' CLI extension is required for " - "AKS backup operations.\n" - f"Run `az extension add --name {K8S_EXTENSION_NAME}` " - "and retry, or rerun with --yes to auto-install." - ) - - logger.warning("Installing CLI extension '%s'...", K8S_EXTENSION_NAME) - from azure.cli.core.extension.operations import add_extension - add_extension(cmd=cmd, extension_name=K8S_EXTENSION_NAME) - add_extension_to_path(K8S_EXTENSION_NAME) - - def _check_and_assign_role(cmd, role, assignee, scope, identity_name="identity", max_retries=3, retry_delay=10): """ Check if a role assignment already exists, and create it if not. @@ -551,7 +506,7 @@ def _install_backup_extension(cmd, cluster_subscription_id, backup_storage_account_name, backup_storage_account_container_name, backup_resource_group_name, - backup_storage_account, yes=False): + backup_storage_account): """Install backup extension on the cluster.""" backup_extension = _create_backup_extension( cmd, @@ -561,8 +516,7 @@ def _install_backup_extension(cmd, cluster_subscription_id, backup_storage_account_name, backup_storage_account_container_name, backup_resource_group_name, - cluster_subscription_id, - yes=yes) + cluster_subscription_id) _check_and_assign_role( cmd, @@ -1055,14 +1009,14 @@ def _show_plan_and_confirm(cluster_subscription_id, cluster_name, logger.warning(" RBAC role assignments listed above.") logger.warning("") + from knack.prompting import prompt_y_n return prompt_y_n("Do you want to proceed?", default='y') def _setup_extension_and_storage( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, storage_account_id, blob_container_name, - backup_resource_group_name, cluster_location, resource_tags, - yes=False): + backup_resource_group_name, cluster_location, resource_tags): """Setup backup extension and storage account (steps 3 & 4). If the extension is already installed, reuses its configured storage @@ -1120,8 +1074,7 @@ def _setup_extension_and_storage( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, sa_result[1], sa_result[2], - backup_resource_group_name, backup_storage_account, - yes=yes) + backup_resource_group_name, backup_storage_account) return backup_storage_account @@ -1188,8 +1141,7 @@ def dataprotection_enable_backup_helper( cmd, cluster_subscription_id, cluster_resource_group_name, cluster_name, configuration_params.get("storageAccountResourceId"), configuration_params.get("blobContainerName"), - backup_resource_group_name, cluster_location, resource_tags, - yes=yes) + backup_resource_group_name, cluster_location, resource_tags) # Step 5: Setup backup vault logger.warning("[5/8] Setting up backup vault...") @@ -1551,7 +1503,7 @@ def _create_backup_extension( cmd, subscription_id, resource_group_name, cluster_name, storage_account_name, storage_account_container_name, storage_account_resource_group, - storage_account_subscription_id, yes=False): + storage_account_subscription_id): """Create or reuse the data protection k8s extension.""" from azext_dataprotection.vendored_sdks.azure_mgmt_kubernetesconfiguration import SourceControlConfigurationClient k8s_configuration_client = get_mgmt_service_client( @@ -1603,8 +1555,6 @@ def _create_backup_extension( logger.warning("Installing data protection extension (azure-aks-backup)...") - _ensure_k8s_extension(cmd, yes=yes) - from azure.cli.core.extension.operations import add_extension_to_path from importlib import import_module add_extension_to_path("k8s-extension") From c047a503d7ccfa5654407d5946fcced32eb69517 Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 9 Jun 2026 12:23:33 +0530 Subject: [PATCH 18/19] Fix AKS backup storage account create payload --- .../manual/aks/aks_helper.py | 17 ++++--- .../test_dataprotection_enable_backup.py | 51 +++++++++++++++++++ 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py index 0392e4e6377..3c586f06565 100644 --- a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py +++ b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py @@ -436,6 +436,7 @@ def _setup_storage_account(cmd, cluster_subscription_id, storage_account_id, cluster_resource_group_name, resource_tags): """Create or use storage account.""" from azure.mgmt.storage import StorageManagementClient + from azure.mgmt.storage.models import Sku, StorageAccountCreateParameters storage_client = get_mgmt_service_client( cmd.cli_ctx, StorageManagementClient, @@ -475,14 +476,14 @@ def _setup_storage_account(cmd, cluster_subscription_id, storage_account_id, if resource_tags: sa_tags.update(resource_tags) - storage_params = { - "location": cluster_location, - "kind": "StorageV2", - "sku": {"name": "Standard_LRS"}, - "allow_blob_public_access": False, - "allow_shared_key_access": False, - "tags": sa_tags - } + storage_params = StorageAccountCreateParameters( + location=cluster_location, + kind="StorageV2", + sku=Sku(name="Standard_LRS"), + allow_blob_public_access=False, + allow_shared_key_access=False, + tags=sa_tags, + ) backup_storage_account = storage_client.storage_accounts.begin_create( resource_group_name=backup_resource_group_name, account_name=backup_storage_account_name, diff --git a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py index 9ee5ef09757..775a6ee31ee 100644 --- a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py +++ b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py @@ -26,6 +26,7 @@ _generate_trusted_access_role_binding_name, _generate_arm_id, _check_and_assign_role, + _setup_storage_account, _find_existing_backup_resource_group, _find_existing_backup_storage_account, _check_existing_backup_instance, @@ -342,6 +343,56 @@ def test_returns_none_when_no_match(self): self.assertIsNone(result_sa) +# --------------------------------------------------------------------------- +# _setup_storage_account +# --------------------------------------------------------------------------- +class TestSetupStorageAccount(unittest.TestCase): + """Tests for backup storage account setup.""" + + @patch("azext_dataprotection.manual.aks.aks_helper._generate_backup_storage_account_name", return_value="aksbkpeastus123") + @patch("azext_dataprotection.manual.aks.aks_helper.get_mgmt_service_client") + def test_create_storage_account_uses_sdk_model_payload(self, mock_get_client, _): + from azure.mgmt.storage.models import StorageAccountCreateParameters + + storage_client = MagicMock() + storage_client.storage_accounts.list.return_value = [] + created_storage_account = MagicMock() + created_storage_account.id = ( + f"/subscriptions/{SUB_ID}/resourceGroups/backup-rg" + "/providers/Microsoft.Storage/storageAccounts/aksbkpeastus123" + ) + storage_client.storage_accounts.begin_create.return_value.result.return_value = created_storage_account + mock_get_client.return_value = storage_client + + cmd = MagicMock() + cmd.cli_ctx = MagicMock() + + result = _setup_storage_account( + cmd, + SUB_ID, + None, + None, + "backup-rg", + "eastus", + CLUSTER_NAME, + CLUSTER_RG, + {"env": "test"}, + ) + + self.assertEqual(result[0], created_storage_account) + _, kwargs = storage_client.storage_accounts.begin_create.call_args + storage_params = kwargs["parameters"] + self.assertIsInstance(storage_params, StorageAccountCreateParameters) + self.assertEqual(storage_params.location, "eastus") + self.assertEqual(storage_params.kind, "StorageV2") + self.assertEqual(storage_params.sku.name, "Standard_LRS") + self.assertFalse(storage_params.allow_blob_public_access) + self.assertFalse(storage_params.allow_shared_key_access) + self.assertEqual(storage_params.tags[AKS_BACKUP_TAG_KEY], "eastus") + self.assertEqual(storage_params.tags["env"], "test") + storage_client.blob_containers.create.assert_called_once() + + # --------------------------------------------------------------------------- # _check_existing_backup_instance # --------------------------------------------------------------------------- From f31e6902b4ce4e1573173c5f2d698ee449d4ba6c Mon Sep 17 00:00:00 2001 From: Anshul Ahuja Date: Tue, 9 Jun 2026 14:56:05 +0530 Subject: [PATCH 19/19] Revert "Fix AKS backup storage account create payload" This reverts commit c047a503d7ccfa5654407d5946fcced32eb69517. --- .../manual/aks/aks_helper.py | 17 +++---- .../test_dataprotection_enable_backup.py | 51 ------------------- 2 files changed, 8 insertions(+), 60 deletions(-) diff --git a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py index 3c586f06565..0392e4e6377 100644 --- a/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py +++ b/src/dataprotection/azext_dataprotection/manual/aks/aks_helper.py @@ -436,7 +436,6 @@ def _setup_storage_account(cmd, cluster_subscription_id, storage_account_id, cluster_resource_group_name, resource_tags): """Create or use storage account.""" from azure.mgmt.storage import StorageManagementClient - from azure.mgmt.storage.models import Sku, StorageAccountCreateParameters storage_client = get_mgmt_service_client( cmd.cli_ctx, StorageManagementClient, @@ -476,14 +475,14 @@ def _setup_storage_account(cmd, cluster_subscription_id, storage_account_id, if resource_tags: sa_tags.update(resource_tags) - storage_params = StorageAccountCreateParameters( - location=cluster_location, - kind="StorageV2", - sku=Sku(name="Standard_LRS"), - allow_blob_public_access=False, - allow_shared_key_access=False, - tags=sa_tags, - ) + storage_params = { + "location": cluster_location, + "kind": "StorageV2", + "sku": {"name": "Standard_LRS"}, + "allow_blob_public_access": False, + "allow_shared_key_access": False, + "tags": sa_tags + } backup_storage_account = storage_client.storage_accounts.begin_create( resource_group_name=backup_resource_group_name, account_name=backup_storage_account_name, diff --git a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py index 775a6ee31ee..9ee5ef09757 100644 --- a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py +++ b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_enable_backup.py @@ -26,7 +26,6 @@ _generate_trusted_access_role_binding_name, _generate_arm_id, _check_and_assign_role, - _setup_storage_account, _find_existing_backup_resource_group, _find_existing_backup_storage_account, _check_existing_backup_instance, @@ -343,56 +342,6 @@ def test_returns_none_when_no_match(self): self.assertIsNone(result_sa) -# --------------------------------------------------------------------------- -# _setup_storage_account -# --------------------------------------------------------------------------- -class TestSetupStorageAccount(unittest.TestCase): - """Tests for backup storage account setup.""" - - @patch("azext_dataprotection.manual.aks.aks_helper._generate_backup_storage_account_name", return_value="aksbkpeastus123") - @patch("azext_dataprotection.manual.aks.aks_helper.get_mgmt_service_client") - def test_create_storage_account_uses_sdk_model_payload(self, mock_get_client, _): - from azure.mgmt.storage.models import StorageAccountCreateParameters - - storage_client = MagicMock() - storage_client.storage_accounts.list.return_value = [] - created_storage_account = MagicMock() - created_storage_account.id = ( - f"/subscriptions/{SUB_ID}/resourceGroups/backup-rg" - "/providers/Microsoft.Storage/storageAccounts/aksbkpeastus123" - ) - storage_client.storage_accounts.begin_create.return_value.result.return_value = created_storage_account - mock_get_client.return_value = storage_client - - cmd = MagicMock() - cmd.cli_ctx = MagicMock() - - result = _setup_storage_account( - cmd, - SUB_ID, - None, - None, - "backup-rg", - "eastus", - CLUSTER_NAME, - CLUSTER_RG, - {"env": "test"}, - ) - - self.assertEqual(result[0], created_storage_account) - _, kwargs = storage_client.storage_accounts.begin_create.call_args - storage_params = kwargs["parameters"] - self.assertIsInstance(storage_params, StorageAccountCreateParameters) - self.assertEqual(storage_params.location, "eastus") - self.assertEqual(storage_params.kind, "StorageV2") - self.assertEqual(storage_params.sku.name, "Standard_LRS") - self.assertFalse(storage_params.allow_blob_public_access) - self.assertFalse(storage_params.allow_shared_key_access) - self.assertEqual(storage_params.tags[AKS_BACKUP_TAG_KEY], "eastus") - self.assertEqual(storage_params.tags["env"], "test") - storage_client.blob_containers.create.assert_called_once() - - # --------------------------------------------------------------------------- # _check_existing_backup_instance # ---------------------------------------------------------------------------