diff --git a/src/dataprotection/HISTORY.rst b/src/dataprotection/HISTORY.rst index 1204567e8b5..dcf5b194e3a 100644 --- a/src/dataprotection/HISTORY.rst +++ b/src/dataprotection/HISTORY.rst @@ -6,6 +6,8 @@ Release History 1.7.1 +++++ * `az dataprotection backup-instance restore initialize-for-item-recovery`: Fixed a bug where the command would fail for AKS Scenarios. +* `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.7.0 +++++ @@ -193,4 +195,4 @@ Release History 0.1.0 ++++++ -* Initial release. +* Initial release. \ No newline at end of file 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") diff --git a/src/dataprotection/azext_dataprotection/manual/custom.py b/src/dataprotection/azext_dataprotection/manual/custom.py index e1857a44b48..da128c5db68 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"]["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 == "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: + # 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 == "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: + 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) diff --git a/src/dataprotection/azext_dataprotection/manual/helpers.py b/src/dataprotection/azext_dataprotection/manual/helpers.py index 4ba090b9e3b..1e392846b01 100644 --- a/src/dataprotection/azext_dataprotection/manual/helpers.py +++ b/src/dataprotection/azext_dataprotection/manual/helpers.py @@ -997,6 +997,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 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 45c281d7ac2..aea7a9109a9 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 @@ -10,6 +10,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): @@ -279,3 +280,57 @@ def test_dataprotection_backup_instance_softdelete(test): # Once protection elsewhere is stopped, we can resume protection on the undeleted BI time.sleep(60) 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}") + ])