From d86226cc8465c0fd9b6a789a464c97e8a3ce0cbd Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 03:14:39 +0530 Subject: [PATCH 1/6] Add backup_configuration argument to modify AKS backup datasource parameters --- src/dataprotection/azext_dataprotection/manual/_params.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dataprotection/azext_dataprotection/manual/_params.py b/src/dataprotection/azext_dataprotection/manual/_params.py index 4d230c7a27d..75c847963ac 100644 --- a/src/dataprotection/azext_dataprotection/manual/_params.py +++ b/src/dataprotection/azext_dataprotection/manual/_params.py @@ -134,6 +134,9 @@ def load_arguments(self, _): c.argument('vaulted_blob_container_list', type=validate_file_or_dict, options_list=['--vaulted-blob-container-list', '--container-blob-list'], help="Enter the container list to modify a vaulted blob backup. The output for " "'az dataprotection backup-instance initialize-backupconfig' needs to be provided as input") + c.argument('backup_configuration', type=validate_file_or_dict, + help="Enter the Backup configuration to modify AKS backup datasource parameters. " + "The output for 'az dataprotection backup-instance initialize-backupconfig' needs to be provided as input.") c.argument('use_system_assigned_identity', options_list=['--system-assigned', '--use-system-identity', '--use-system-assigned-identity'], arg_type=get_three_state_flag(), help="Use system assigned identity") c.argument('user_assigned_identity_arm_url', options_list=['--user-assigned', '--user-assigned-identity-arm-url', '--uami'], type=str, help="ARM ID of the User Assigned Managed Identity") From 7c7e4dc3388d3bb03e2396a10b37ba32c3dd033c Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 03:14:45 +0530 Subject: [PATCH 2/6] Add backup_configuration parameter to update backup datasource for AKS and AzureBlob --- .../azext_dataprotection/manual/custom.py | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/dataprotection/azext_dataprotection/manual/custom.py b/src/dataprotection/azext_dataprotection/manual/custom.py index dc49ad79f63..fe690af58b8 100644 --- a/src/dataprotection/azext_dataprotection/manual/custom.py +++ b/src/dataprotection/azext_dataprotection/manual/custom.py @@ -246,7 +246,7 @@ def dataprotection_backup_instance_validate_for_update(cmd, resource_group_name, def dataprotection_backup_instance_update(cmd, resource_group_name, vault_name, backup_instance_name, - vaulted_blob_container_list=None, no_wait=False, + vaulted_blob_container_list=None, backup_configuration=None, no_wait=False, use_system_assigned_identity=None, user_assigned_identity_arm_url=None): from azext_dataprotection.aaz.latest.dataprotection.backup_instance import Show as BackupInstanceShow backup_instance = BackupInstanceShow(cli_ctx=cmd.cli_ctx)(command_args={ @@ -266,10 +266,34 @@ def dataprotection_backup_instance_update(cmd, resource_group_name, vault_name, identity_details = helper.get_identity_details(use_system_assigned_identity, user_assigned_identity_arm_url) backup_instance["properties"]["identityDetails"] = identity_details - # Policy changes - updating the vaulted blob container list for vaulted blob backups - if vaulted_blob_container_list is not None: - backup_instance['properties']['policyInfo']['policyParameters']['backupDatasourceParametersList'] = \ - [vaulted_blob_container_list,] + # Policy changes + # - Updating the vaulted blob container list for vaulted blob backups + # - Updating the backup datasource parameters for AKS backups + datasource_type = backup_instance["properties"]["data_source_info"]["datasource_type"] + + # If user provided any of the datasource parameter update inputs, handle according to datasource type + if vaulted_blob_container_list is not None or backup_configuration is not None: + if datasource_type == "AzureKubernetesService": + if vaulted_blob_container_list is not None: + raise InvalidArgumentValueError('Invalid argument --vaulted-blob-container-list for given datasource type.') + elif backup_configuration is not None: + # Allow passing JSON string or already-parsed object + if isinstance(backup_configuration, str): + import json + try: + backup_configuration = json.loads(backup_configuration) + except Exception: + raise InvalidArgumentValueError("Provided --backup-configuration is not valid JSON") + backup_instance['properties']['policyInfo']['policyParameters']['backupDatasourceParametersList'] = [backup_configuration] + + elif datasource_type == "AzureBlob": + if backup_configuration is not None: + raise InvalidArgumentValueError('Invalid argument --backup-configuration for given datasource type.') + elif vaulted_blob_container_list is not None: + backup_instance['properties']['policyInfo']['policyParameters']['backupDatasourceParametersList'] = [vaulted_blob_container_list,] + + else: + raise InvalidArgumentValueError('Setting backup datasource parameters is not supported for given DataSourceType') backup_instance = helper.convert_backup_instance_show_to_input(backup_instance) From 58c63cc45d917b66ee93ae4bcb0ea03d355b11e5 Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 03:14:51 +0530 Subject: [PATCH 3/6] Add test for updating AKS backup configuration in dataprotection --- ...taprotection_backup_instance_operations.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py index 0430e8cd6a8..f7606aaf2af 100644 --- a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py +++ b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py @@ -9,6 +9,7 @@ from azure.cli.testsdk import ScenarioTest, live_only from azure.cli.testsdk.scenario_tests import AllowLargeResponse import time +import copy def reset_softdelete_base_state(test): @@ -273,3 +274,57 @@ def test_dataprotection_backup_instance_softdelete(test): # Once protection elsewhere is stopped, we can resume protection on the undeleted BI reset_softdelete_base_state(test) + + @AllowLargeResponse() + def test_dataprotection_backup_instance_update_aks_configuration(test): + # Update with AKS backup configuration using simple az CLI commands. + test.kwargs.update({ + 'location': 'eastus2euap', + 'rg': 'clitest-dpp-rg', + 'vaultName': 'clitest-bkp-vault-aks-donotdelete', + 'policyId': '/subscriptions/38304e13-357e-405e-9e9a-220351dcce8c/resourceGroups/clitest-dpp-rg/providers/Microsoft.DataProtection/backupVaults/clitest-bkp-vault-aks-donotdelete/backupPolicies/akspolicy', + 'dataSourceType': 'AzureKubernetesService', + 'aksClusterName': 'clitest-cluster1-donotdelete', + 'aksClusterId': '/subscriptions/38304e13-357e-405e-9e9a-220351dcce8c/resourceGroups/oss-clitest-rg/providers/Microsoft.ContainerService/managedClusters/clitest-cluster1-donotdelete', + 'friendlyName': 'clitest-friendly-aks', + 'backupInstanceName': 'clitestsabidonotdelete-clitestsabidonotdelete-887c3538-0bfc-11ee-acd3-002b670b472e' + }) + + # Fetch original BI backupDatasourceParametersList (if any) to allow resetting later + original_bi = test.cmd('az dataprotection backup-instance show -g "{rg}" --vault-name "{vaultName}" --name "{backupInstanceName}"').get_output_in_json() + original_bi_backup_config_json = original_bi['properties']['policyInfo']['policyParameters'].get('backupDatasourceParametersList')[0] + test.kwargs.update({ + 'backupConfig': original_bi_backup_config_json + }) + + # Generate the AKS backup configuration using the dedicated CLI helper + new_backup_config_json = test.cmd('az dataprotection backup-instance initialize-backupconfig --datasource-type AzureKubernetesService').get_output_in_json() + + # mutate a visible field to make the change observable + new_backup_config_json['included_namespaces'] = ['nsA', 'nsB'] + new_backup_config_json['label_selectors'] = ['app=web'] + new_backup_config_json['excluded_resource_types'] = ['ResourceX'] + new_backup_config_json['include_cluster_scope_resources'] = False + new_backup_config_json['snapshot_volumes'] = False + test.kwargs.update({ + 'tempBackupConfig': new_backup_config_json + }) + + # Apply temp configuration + test.cmd('az dataprotection backup-instance update -g "{rg}" --vault-name "{vaultName}" --backup-instance-name "{backupInstanceName}" --backup-configuration "{tempBackupConfig}"', checks=[ + test.check('name', "{backupInstanceName}") + ]) + + # Fetch the BI and verify that the backupDatasourceParametersList was updated to reflect the AKS config + test.cmd('az dataprotection backup-instance show -g "{rg}" --vault-name "{vaultName}" --name "{backupInstanceName}"', checks=[ + test.check("properties.policyInfo.policyParameters.backupDatasourceParametersList[0].included_namespaces", ['nsA', 'nsB']), + test.check("properties.policyInfo.policyParameters.backupDatasourceParametersList[0].label_selectors", ['app=web']), + test.check("properties.policyInfo.policyParameters.backupDatasourceParametersList[0].excluded_resource_types", ['ResourceX']), + test.check("properties.policyInfo.policyParameters.backupDatasourceParametersList[0].include_cluster_scope_resources", False), + test.check("properties.policyInfo.policyParameters.backupDatasourceParametersList[0].snapshot_volumes", False) + ]) + + # Reset to original configuration + test.cmd('az dataprotection backup-instance update -g "{rg}" --vault-name "{vaultName}" --backup-instance-name "{backupInstanceName}" --backup-configuration "{backupConfig}"', checks=[ + test.check('name', "{backupInstanceName}") + ]) From d8f42e32fbdb25e29e90d65703c9d9e125451df3 Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 13:36:05 +0530 Subject: [PATCH 4/6] Update datasource type references for AKS and AzureBlob in backup instance update logic --- src/dataprotection/azext_dataprotection/manual/custom.py | 6 +++--- .../test_dataprotection_backup_instance_operations.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dataprotection/azext_dataprotection/manual/custom.py b/src/dataprotection/azext_dataprotection/manual/custom.py index fe690af58b8..5713c609e0e 100644 --- a/src/dataprotection/azext_dataprotection/manual/custom.py +++ b/src/dataprotection/azext_dataprotection/manual/custom.py @@ -269,11 +269,11 @@ def dataprotection_backup_instance_update(cmd, resource_group_name, vault_name, # Policy changes # - Updating the vaulted blob container list for vaulted blob backups # - Updating the backup datasource parameters for AKS backups - datasource_type = backup_instance["properties"]["data_source_info"]["datasource_type"] + datasource_type = backup_instance["properties"]["dataSourceInfo"]["datasourceType"] # If user provided any of the datasource parameter update inputs, handle according to datasource type if vaulted_blob_container_list is not None or backup_configuration is not None: - if datasource_type == "AzureKubernetesService": + if datasource_type == "Microsoft.ContainerService/managedClusters": if vaulted_blob_container_list is not None: raise InvalidArgumentValueError('Invalid argument --vaulted-blob-container-list for given datasource type.') elif backup_configuration is not None: @@ -286,7 +286,7 @@ def dataprotection_backup_instance_update(cmd, resource_group_name, vault_name, raise InvalidArgumentValueError("Provided --backup-configuration is not valid JSON") backup_instance['properties']['policyInfo']['policyParameters']['backupDatasourceParametersList'] = [backup_configuration] - elif datasource_type == "AzureBlob": + elif datasource_type == "Microsoft.Storage/storageAccounts/blobServices": if backup_configuration is not None: raise InvalidArgumentValueError('Invalid argument --backup-configuration for given datasource type.') elif vaulted_blob_container_list is not None: diff --git a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py index f7606aaf2af..7bb65c03645 100644 --- a/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py +++ b/src/dataprotection/azext_dataprotection/tests/latest/test_dataprotection_backup_instance_operations.py @@ -301,9 +301,9 @@ def test_dataprotection_backup_instance_update_aks_configuration(test): new_backup_config_json = test.cmd('az dataprotection backup-instance initialize-backupconfig --datasource-type AzureKubernetesService').get_output_in_json() # mutate a visible field to make the change observable - new_backup_config_json['included_namespaces'] = ['nsA', 'nsB'] - new_backup_config_json['label_selectors'] = ['app=web'] - new_backup_config_json['excluded_resource_types'] = ['ResourceX'] + new_backup_config_json['included_namespaces'] = ["nsA", "nsB"] + new_backup_config_json['label_selectors'] = ["app=web"] + new_backup_config_json['excluded_resource_types'] = ["ResourceX"] new_backup_config_json['include_cluster_scope_resources'] = False new_backup_config_json['snapshot_volumes'] = False test.kwargs.update({ From 4b28ab12c9fdf8f4ae82ef3b6072ee129b0ecea0 Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 14:02:41 +0530 Subject: [PATCH 5/6] Bump version to 1.6.1 and update HISTORY.rst with new backup-configuration parameter for AKS datasource --- src/dataprotection/HISTORY.rst | 4 ++++ src/dataprotection/setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dataprotection/HISTORY.rst b/src/dataprotection/HISTORY.rst index db8e00cd394..49766a771ab 100644 --- a/src/dataprotection/HISTORY.rst +++ b/src/dataprotection/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +1.6.1 ++++++ +* `az dataprotection backup-instance update`: New parameters: `--backup-configuration` to update AKS datasource parameters. + 1.6.0 +++++ * Added support for User-Assigned Managed Identities for Backup Instances. diff --git a/src/dataprotection/setup.py b/src/dataprotection/setup.py index 5eb42fd7ce0..b7e3832f08a 100644 --- a/src/dataprotection/setup.py +++ b/src/dataprotection/setup.py @@ -10,7 +10,7 @@ from setuptools import setup, find_packages # HISTORY.rst entry. -VERSION = '1.6.0' +VERSION = '1.6.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From a617b5914c12d04e8c55cc40a4d03584c3322f48 Mon Sep 17 00:00:00 2001 From: PRIYANSH CHOUDHARY Date: Thu, 18 Sep 2025 16:54:06 +0530 Subject: [PATCH 6/6] Fix schema validation error by cleaning up resourceProperties when objectType is null in backup instance conversion --- src/dataprotection/HISTORY.rst | 3 ++- .../azext_dataprotection/manual/helpers.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/dataprotection/HISTORY.rst b/src/dataprotection/HISTORY.rst index 49766a771ab..b960ba8cb1f 100644 --- a/src/dataprotection/HISTORY.rst +++ b/src/dataprotection/HISTORY.rst @@ -6,6 +6,7 @@ Release History 1.6.1 +++++ * `az dataprotection backup-instance update`: New parameters: `--backup-configuration` to update AKS datasource parameters. +* Fix in `helpers.py` to correctly prepare/normalize AKS backup-configuration payloads passed via the CLI. 1.6.0 +++++ @@ -188,4 +189,4 @@ Release History 0.1.0 ++++++ -* Initial release. +* Initial release. \ No newline at end of file diff --git a/src/dataprotection/azext_dataprotection/manual/helpers.py b/src/dataprotection/azext_dataprotection/manual/helpers.py index aee6238e1be..0a984016166 100644 --- a/src/dataprotection/azext_dataprotection/manual/helpers.py +++ b/src/dataprotection/azext_dataprotection/manual/helpers.py @@ -899,6 +899,16 @@ def convert_backup_instance_show_to_input(backup_instance): del backup_instance['properties']['protectionStatus'] if 'provisioningState' in backup_instance['properties']: del backup_instance['properties']['provisioningState'] + # Cleaning up resourceProperties if objectType is null to avoid schema validation error + for datasource_property in ['dataSourceInfo', 'dataSourceSetInfo']: + if datasource_property in backup_instance['properties']: + datasource_info = backup_instance['properties'][datasource_property] + if (isinstance(datasource_info, dict) and + 'resourceProperties' in datasource_info and + isinstance(datasource_info['resourceProperties'], dict)): + if datasource_info['resourceProperties'].get('objectType') is None: + # Set resourceProperties to None when objectType is null to avoid schema validation error + del backup_instance['properties'][datasource_property]['resourceProperties'] return backup_instance