diff --git a/linter_exclusions.yml b/linter_exclusions.yml index ce4aaa82bca..650aedfdffa 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -3504,3 +3504,11 @@ neon postgres organization: neon postgres project: rule_exclusions: - require_wait_command_if_no_wait + +cosmosdb sql softdeleted-database list: + rule_exclusions: + - no_ids_for_list_commands + +cosmosdb sql softdeleted-collection list: + rule_exclusions: + - no_ids_for_list_commands \ No newline at end of file diff --git a/src/cosmosdb-preview/HISTORY.rst b/src/cosmosdb-preview/HISTORY.rst index a31383adcba..8ad38372aad 100644 --- a/src/cosmosdb-preview/HISTORY.rst +++ b/src/cosmosdb-preview/HISTORY.rst @@ -2,6 +2,13 @@ Release History =============== +1.7.0 ++++++ +* Add support for soft-deleted resource operations for SQL API +* New command group `az cosmosdb sql softdeleted-account` to list, show, delete (purge), and recover soft-deleted accounts +* New command group `az cosmosdb sql softdeleted-database` to list, show, delete (purge), and recover soft-deleted databases +* New command group `az cosmosdb sql softdeleted-collection` to list, show, delete (purge), and recover soft-deleted containers + 1.6.1 +++++ * Fix SQL container throughput update to preserve existing throughput buckets when not explicitly specified. diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py index 4afa56f268f..0e6703fe3ae 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py @@ -119,3 +119,16 @@ def cf_fleetspace_account(cli_ctx, _): def cf_fleet_analytics(cli_ctx, _): return cf_cosmosdb_preview(cli_ctx).fleet_analytics + + +# soft-deleted resources +def cf_softdeleted_database_accounts(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_database_accounts + + +def cf_softdeleted_sql_databases(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_sql_databases + + +def cf_softdeleted_sql_containers(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_sql_containers diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py index 7e5576bee00..d9795b3aaa3 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py @@ -569,6 +569,8 @@ text: az cosmosdb update --capabilities EnableGremlin --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup - name: Update an Azure Cosmos DB database account to enable materialized views. text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enable-materialized-views true + - name: Enable soft deletion with 1440 minutes (24 hours) retention. + text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enable-soft-deletion true --sd-retention 1440 --min-purge-minutes 60 """ # restore account @@ -1844,3 +1846,129 @@ type: command short-summary: Delete a Fleet Analytics resource from a Fleet. """ + +helps['cosmosdb sql softdeleted-account'] = """ +type: group +short-summary: Manage soft-deleted Azure Cosmos DB accounts. +""" + +helps['cosmosdb sql softdeleted-account list'] = """ +type: command +short-summary: List soft-deleted Azure Cosmos DB accounts. +examples: + - name: List all soft-deleted Azure Cosmos DB accounts in a subscription. + text: | + az cosmosdb sql softdeleted-account list + - name: List soft-deleted Azure Cosmos DB accounts in a specific location. + text: | + az cosmosdb sql softdeleted-account list --location westus +""" + +helps['cosmosdb sql softdeleted-account show'] = """ +type: command +short-summary: Show details of a soft-deleted Azure Cosmos DB account. +examples: + - name: Show details of a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account show --location westus --account-name MyAccount +""" + +helps['cosmosdb sql softdeleted-account delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted Azure Cosmos DB account. +examples: + - name: Permanently delete a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account delete --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-account recover'] = """ +type: command +short-summary: Recover a soft-deleted Azure Cosmos DB account. +examples: + - name: Recover a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account recover --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database'] = """ +type: group +short-summary: Manage soft-deleted databases for Azure Cosmos DB SQL API. +""" + +helps['cosmosdb sql softdeleted-database list'] = """ +type: command +short-summary: List all soft-deleted databases for an Azure Cosmos DB account. +examples: + - name: List all soft-deleted databases for an Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-database list --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database show'] = """ +type: command +short-summary: Show details of a soft-deleted database. +examples: + - name: Show details of a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database show --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted database. +examples: + - name: Permanently delete a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database delete --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database recover'] = """ +type: command +short-summary: Recover a soft-deleted database. +examples: + - name: Recover a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database recover --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection'] = """ +type: group +short-summary: Manage soft-deleted collections for Azure Cosmos DB SQL API. +""" + +helps['cosmosdb sql softdeleted-collection list'] = """ +type: command +short-summary: List all soft-deleted collections in a database. +examples: + - name: List all soft-deleted collections in a database. + text: | + az cosmosdb sql softdeleted-collection list --location westus --account-name MyAccount --database-name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection show'] = """ +type: command +short-summary: Show details of a soft-deleted collection. +examples: + - name: Show details of a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection show --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted collection. +examples: + - name: Permanently delete a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection delete --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection recover'] = """ +type: command +short-summary: Recover a soft-deleted collection. +examples: + - name: Recover a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection recover --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py index b89c8ac3e2c..164ea224b1f 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py @@ -486,6 +486,9 @@ def load_arguments(self, _): with self.argument_context('cosmosdb update') as c: c.argument('key_uri', help="The URI of the key vault", is_preview=True) + c.argument('enable_soft_deletion', arg_type=get_three_state_flag(), help="Flag to enable or disable soft deletion on the account.", is_preview=True, arg_group='Soft Delete') + c.argument('soft_deletion_retention_period_in_minutes', options_list=['--soft-deletion-retention-period-in-minutes', '--sd-retention'], type=int, help="Soft deletion retention period in minutes. Must be at least equal to min_minutes_before_permanent_deletion_allowed.", is_preview=True, arg_group='Soft Delete') + c.argument('min_minutes_before_permanent_deletion_allowed', options_list=['--min-minutes-before-permanent-deletion-allowed', '--min-purge-minutes'], type=int, help="Minimum minutes before permanent deletion is allowed for soft-deleted resources.", is_preview=True, arg_group='Soft Delete') with self.argument_context('cosmosdb restore') as c: c.argument('target_database_account_name', options_list=['--target-database-account-name', '-n'], help='Name of the new target Cosmos DB database account after the restore') @@ -829,6 +832,78 @@ def load_arguments(self, _): c.argument('scope', options_list=['--scope', '-s'], help="Data plane resource path at which this Role Assignment is being granted.") c.argument('principal_id', options_list=['--principal-id', '-p'], help="AAD Object ID of the principal to which this Role Assignment is being granted.") + # Soft-deleted Account + with self.argument_context('cosmosdb sql softdeleted-account list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=False) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + # Soft-deleted Database + with self.argument_context('cosmosdb sql softdeleted-database list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + # Soft-deleted Collection + with self.argument_context('cosmosdb sql softdeleted-collection list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + # Cosmos DB Fleet with self.argument_context('cosmosdb fleet') as c: c.argument('resource_group', options_list=['--resource-group', '-g'], help='Name of the resource group.', required=True) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py b/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py index 2acf5462bad..7bcdb010192 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py @@ -31,7 +31,10 @@ cf_fleet, cf_fleetspace, cf_fleetspace_account, - cf_fleet_analytics + cf_fleet_analytics, + cf_softdeleted_database_accounts, + cf_softdeleted_sql_databases, + cf_softdeleted_sql_containers ) @@ -220,6 +223,19 @@ def load_command_table(self, _): operations_tmpl='azure.mgmt.cosmosdb.operations#RestorableDatabaseAccountsOperations.{}', client_factory=cf_restorable_database_accounts) + # Soft-deleted resources SDK types + cosmosdb_softdeleted_accounts_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedDatabaseAccountsOperations.{}', + client_factory=cf_softdeleted_database_accounts) + + cosmosdb_softdeleted_sql_databases_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedSqlDatabasesOperations.{}', + client_factory=cf_softdeleted_sql_databases) + + cosmosdb_softdeleted_sql_containers_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedSqlContainersOperations.{}', + client_factory=cf_softdeleted_sql_containers) + # define commands # Restorable apis for sql,mongodb,gremlin and table # Provisioning/migrate Continuous 7 days accounts @@ -367,6 +383,36 @@ def load_command_table(self, _): with self.command_group('cosmosdb table', cosmosdb_table_sdk, client_factory=cf_table_resources) as g: g.custom_command('restore', 'cli_cosmosdb_table_restore', is_preview=True) + # Soft-deleted Account commands + with self.command_group('cosmosdb sql softdeleted-account', + cosmosdb_softdeleted_accounts_sdk, + client_factory=cf_softdeleted_database_accounts, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_account_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_account_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_account_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_account_recover') + + # Soft-deleted Database commands + with self.command_group('cosmosdb sql softdeleted-database', + cosmosdb_softdeleted_sql_databases_sdk, + client_factory=cf_softdeleted_sql_databases, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_database_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_database_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_database_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_database_recover') + + # Soft-deleted Collection commands + with self.command_group('cosmosdb sql softdeleted-collection', + cosmosdb_softdeleted_sql_containers_sdk, + client_factory=cf_softdeleted_sql_containers, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_collection_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_collection_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_collection_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_collection_recover') + setup_mongocluster_commands(self) setup_fleet_commands(self) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py index 55d2c3bef6b..453affe2df8 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py @@ -955,7 +955,10 @@ def cli_cosmosdb_update(client, default_priority_level=None, enable_prpp_autoscale=None, enable_partition_merge=None, - capacity_mode=None): + capacity_mode=None, + enable_soft_deletion=None, + soft_deletion_retention_period_in_minutes=None, + min_minutes_before_permanent_deletion_allowed=None): """Update an existing Azure Cosmos DB database account. """ existing = client.get(resource_group_name, account_name) @@ -1022,6 +1025,17 @@ def cli_cosmosdb_update(client, analytical_storage_configuration = AnalyticalStorageConfiguration() analytical_storage_configuration.schema_type = analytical_storage_schema_type + soft_delete_configuration = None + if enable_soft_deletion is not None or \ + soft_deletion_retention_period_in_minutes is not None or \ + min_minutes_before_permanent_deletion_allowed is not None: + from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration + soft_delete_configuration = SoftDeleteConfiguration( + enable_soft_deletion=enable_soft_deletion, + soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, + min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed + ) + params = DatabaseAccountUpdateParameters( locations=locations, tags=tags, @@ -1047,7 +1061,8 @@ def cli_cosmosdb_update(client, default_priority_level=default_priority_level, enable_per_region_per_partition_autoscale=enable_prpp_autoscale, enable_partition_merge=enable_partition_merge, - capacity_mode=capacity_mode) + capacity_mode=capacity_mode, + soft_delete_configuration=soft_delete_configuration) async_docdb_update = client.begin_update(resource_group_name, account_name, params) docdb_account = async_docdb_update.result() @@ -1200,7 +1215,10 @@ def _create_database_account(client, enable_prpp_autoscale=None, disable_ttl=None, enable_partition_merge=None, - capacity_mode=None): + capacity_mode=None, + enable_soft_deletion=None, + soft_deletion_retention_period_in_minutes=None, + min_minutes_before_permanent_deletion_allowed=None): consistency_policy = None if default_consistency_level is not None: consistency_policy = ConsistencyPolicy(default_consistency_level=default_consistency_level, @@ -1280,6 +1298,17 @@ def _create_database_account(client, analytical_storage_configuration = AnalyticalStorageConfiguration() analytical_storage_configuration.schema_type = analytical_storage_schema_type + soft_delete_configuration = None + if enable_soft_deletion is not None or \ + soft_deletion_retention_period_in_minutes is not None or \ + min_minutes_before_permanent_deletion_allowed is not None: + from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration + soft_delete_configuration = SoftDeleteConfiguration( + enabled_soft_deletion=enable_soft_deletion, + soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, + min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed + ) + create_mode = CreateMode.restore.value if is_restore_request else CreateMode.default.value params = None restore_parameters = None @@ -1340,7 +1369,8 @@ def _create_database_account(client, default_priority_level=default_priority_level, enable_per_region_per_partition_autoscale=enable_prpp_autoscale, enable_partition_merge=enable_partition_merge, - capacity_mode=capacity_mode + capacity_mode=capacity_mode, + soft_delete_configuration=soft_delete_configuration ) async_docdb_create = client.begin_create_or_update(resource_group_name, account_name, params) @@ -3377,3 +3407,113 @@ def cli_cosmosdb_fleetspace_account_create(client, fleetspace_account_name=fleetspace_account_name, body=fleetspace_account_body ) + + +# Soft-deleted Account operations +def cli_cosmosdb_sql_softdeleted_account_list(client, + resource_group, + location=None): + """List soft-deleted Cosmos DB accounts.""" + if location is not None: + return client.list_by_location(location) + return client.list() + + +def cli_cosmosdb_sql_softdeleted_account_show(client, + resource_group, + location, + account_name): + """Get a soft-deleted Cosmos DB account.""" + return client.get_by_location(location, account_name) + + +def cli_cosmosdb_sql_softdeleted_account_delete(client, + resource_group, + location, + account_name): + """Purge a soft-deleted Cosmos DB account.""" + return client.begin_purge(resource_group, account_name) + + +def cli_cosmosdb_sql_softdeleted_account_recover(client, + resource_group, + location, + account_name): + """Recover a soft-deleted Cosmos DB account.""" + return client.begin_restore(resource_group, account_name) + + +# Soft-deleted Database operations +def cli_cosmosdb_sql_softdeleted_database_list(client, + resource_group, + location, + account_name): + """List soft-deleted databases in a Cosmos DB account.""" + return client.list(resource_group, location, account_name) + + +def cli_cosmosdb_sql_softdeleted_database_show(client, + resource_group, + location, + account_name, + database_name): + """Get a soft-deleted database.""" + return client.get(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_database_delete(client, + resource_group, + location, + account_name, + database_name): + """Purge a soft-deleted database.""" + return client.begin_purge(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_database_recover(client, + resource_group, + location, + account_name, + database_name): + """Recover a soft-deleted database.""" + return client.begin_restore(resource_group, location, account_name, database_name) + + +# Soft-deleted Collection operations +def cli_cosmosdb_sql_softdeleted_collection_list(client, + resource_group, + location, + account_name, + database_name): + """List soft-deleted containers in a database.""" + return client.list(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_collection_show(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Get a soft-deleted container.""" + return client.get(resource_group, location, account_name, database_name, container_name) + + +def cli_cosmosdb_sql_softdeleted_collection_delete(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Purge a soft-deleted container.""" + return client.begin_purge(resource_group, location, account_name, database_name, container_name) + + +def cli_cosmosdb_sql_softdeleted_collection_recover(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Recover a soft-deleted container.""" + return client.begin_restore(resource_group, location, account_name, database_name, container_name) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py new file mode 100644 index 00000000000..58dacbcbae0 --- /dev/null +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py @@ -0,0 +1,488 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import unittest +import time +import datetime + +from azure.cli.testsdk.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) +from knack.log import get_logger + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + +logger = get_logger(__name__) + + +class CosmosDBSoftDeleteScenarioTest(ScenarioTest): + """ + Test suite for CosmosDB SQL API soft-delete functionality. + + This test suite validates the soft-delete and recovery operations for: + - Database Accounts + - SQL Databases + - SQL Collections/Containers + + Note: These tests require a CosmosDB account with soft-delete feature enabled. + """ + + def _update_account_with_soft_delete(self, account_name, resource_group, location): + """ + Helper function to create a CosmosDB account and enable soft delete. + Sets retention period and minimum purge minutes to 0 for testing. + """ + self.kwargs.update({ + 'acc': account_name, + 'rg': resource_group, + 'loc': location + }) + + logger.info("Creating CosmosDB account") + self.cmd('az cosmosdb create -n {acc} -g {rg} --locations regionName={loc}') + + logger.info("Enabling soft delete on account") + self.cmd( + 'az cosmosdb update -n {acc} -g {rg} ' + '--enable-soft-deletion true ' + '--sd-retention 0 ' + '--min-purge-minutes 0' + ) + logger.info("Account created with soft delete enabled") + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_acc_recover', location='westus') + def test_cosmosdb_sql_softdeleted_account_recover(self, resource_group): + """ + Test soft-deleted account recovery operation. + + This test validates that soft-deleted accounts can be recovered. + """ + location = "westus" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdacc', length=20), + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Soft-deleting the account") + self.cmd('az cosmosdb delete -n {acc} -g {rg} --yes') + + logger.info("Waiting for account deletion to complete") + time.sleep(120) + + logger.info("Listing soft-deleted accounts") + soft_deleted_accounts = self.cmd( + 'az cosmosdb sql softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + deleted_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts) + assert deleted_account_found, f"Soft-deleted account '{self.kwargs['acc']}' should appear in the list" + + logger.info("Showing soft-deleted account details") + soft_deleted_account = self.cmd( + 'az cosmosdb sql softdeleted-account show ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + assert soft_deleted_account is not None + + logger.info("Recovering the soft-deleted account") + self.cmd( + 'az cosmosdb sql softdeleted-account recover ' + '--location {loc} --account-name {acc} -g {rg}' + ) + + logger.info("Waiting for account recovery to complete") + time.sleep(120) + + logger.info("Verifying account is recovered") + recovered_account = self.cmd('az cosmosdb show -n {acc} -g {rg}').get_output_in_json() + assert recovered_account is not None + + logger.info("Verifying account is no longer in soft-deleted list") + soft_deleted_accounts_after = self.cmd( + 'az cosmosdb sql softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + recovered_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts_after) + assert not recovered_account_found, "Account should not appear in soft-deleted list after recovery" + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_acc_purge', location='westus') + def test_cosmosdb_sql_softdeleted_account_purge(self, resource_group): + """ + Test soft-deleted account purge (permanent deletion) operation. + + This test validates that soft-deleted accounts can be permanently removed. + """ + location = "westus" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Soft-deleting the account") + self.cmd('az cosmosdb delete -n {acc} -g {rg} --yes') + + logger.info("Waiting for account deletion to complete") + time.sleep(120) + + logger.info("Listing soft-deleted accounts") + soft_deleted_accounts = self.cmd( + 'az cosmosdb sql softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + deleted_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts) + assert deleted_account_found, f"Soft-deleted account '{self.kwargs['acc']}' should appear in the list" + + logger.info("Showing soft-deleted account details") + soft_deleted_account = self.cmd( + 'az cosmosdb sql softdeleted-account show ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + assert soft_deleted_account is not None + + logger.info("Purging the soft-deleted account") + self.cmd( + 'az cosmosdb sql softdeleted-account delete ' + '--location {loc} --account-name {acc} -g {rg} --yes' + ) + + logger.info("Waiting for account purge to complete") + time.sleep(120) + + logger.info("Account successfully purged") + + logger.info("Verifying account is no longer in soft-deleted list") + soft_deleted_accounts_after = self.cmd( + 'az cosmosdb sql softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + purged_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts_after) + assert not purged_account_found, "Account should not appear in soft-deleted list after purge" + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_db_recover', location='westus') + def test_cosmosdb_sql_softdeleted_database_recover(self, resource_group): + """ + Test soft-deleted database recovery operations: list, show, and recover. + + This test validates the database soft-delete and recovery workflow. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdddb', length=15) + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisddacc', length=20), + 'db_name': db_name, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + database_create = self.cmd( + 'az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}' + ).get_output_in_json() + assert database_create["name"] == db_name + + logger.info("Waiting for database to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the database") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + time.sleep(60) + + logger.info("Listing soft-deleted databases") + soft_deleted_dbs = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + # Verify the deleted database appears in the list + deleted_db_found = any(db.get('name') == db_name for db in soft_deleted_dbs) + assert deleted_db_found, f"Soft-deleted database '{db_name}' should appear in the list" + + logger.info("Showing soft-deleted database details") + soft_deleted_db = self.cmd( + 'az cosmosdb sql softdeleted-database show ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_db is not None + logger.info(f"Soft-deleted database details retrieved successfully") + + logger.info("Recovering the soft-deleted database") + self.cmd( + 'az cosmosdb sql softdeleted-database recover ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg}' + ) + + time.sleep(120) + + recovered_db = self.cmd( + 'az cosmosdb sql database show -g {rg} -a {acc} -n {db_name}' + ).get_output_in_json() + assert recovered_db["name"] == db_name + logger.info("Database successfully recovered") + + logger.info("Verifying database is no longer soft-deleted") + soft_deleted_dbs_after = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + still_soft_deleted = any(db.get('name') == db_name for db in soft_deleted_dbs_after) + assert not still_soft_deleted, "Database should not appear in soft-deleted list after recovery" + + logger.info("Cleaning up test resources") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_coll_recover', location='westus') + def test_cosmosdb_sql_softdeleted_collection_recover(self, resource_group): + """ + Test soft-deleted collection recovery operations: list, show, and recover. + + This test validates the collection/container soft-delete and recovery workflow. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdddb', length=15) + coll_name = self.create_random_name(prefix='clisddcoll', length=15) + partition_key = "/pk" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisddacc', length=20), + 'db_name': db_name, + 'coll_name': coll_name, + 'part': partition_key, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Creating SQL container") + collection_create = self.cmd( + 'az cosmosdb sql container create -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} -p {part}' + ).get_output_in_json() + assert collection_create["name"] == coll_name + + logger.info("Waiting for container to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the container") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + + time.sleep(60) + + logger.info("Listing soft-deleted collections") + soft_deleted_colls = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + # Verify the deleted collection appears in the list + deleted_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls) + assert deleted_coll_found, f"Soft-deleted collection '{coll_name}' should appear in the list" + + logger.info("Showing soft-deleted collection details") + soft_deleted_coll = self.cmd( + 'az cosmosdb sql softdeleted-collection show ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_coll is not None + logger.info(f"Soft-deleted collection details retrieved successfully") + + logger.info("Recovering the soft-deleted collection") + self.cmd( + 'az cosmosdb sql softdeleted-collection recover ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ) + + time.sleep(120) + + recovered_coll = self.cmd( + 'az cosmosdb sql container show -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name}' + ).get_output_in_json() + assert recovered_coll["name"] == coll_name + logger.info("Collection successfully recovered") + + logger.info("Verifying collection is no longer soft-deleted") + soft_deleted_colls_after = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + still_soft_deleted = any(coll.get('name') == coll_name for coll in soft_deleted_colls_after) + assert not still_soft_deleted, "Collection should not appear in soft-deleted list after recovery" + + logger.info("Cleaning up test resources") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_purge', location='westus') + def test_cosmosdb_sql_softdeleted_database_purge(self, resource_group): + """ + Test soft-deleted database purge (permanent deletion) operation. + + This test validates that soft-deleted databases can be permanently removed. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdpdb', length=15) + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'db_name': db_name, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Waiting for database to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the database") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + time.sleep(60) + + logger.info("Purging the soft-deleted database") + try: + self.cmd( + 'az cosmosdb sql softdeleted-database delete ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg} --yes' + ) + + time.sleep(60) + + logger.info("Database successfully purged") + + soft_deleted_dbs = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + purged_db_found = any(db.get('name') == db_name for db in soft_deleted_dbs) + assert not purged_db_found, "Database should not appear in soft-deleted list after purge" + + except Exception as e: + logger.warning(f"Database purge test skipped or failed: {e}") + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_coll_purge', location='westus') + def test_cosmosdb_sql_softdeleted_collection_purge(self, resource_group): + """ + Test soft-deleted collection purge (permanent deletion) operation. + + This test validates that soft-deleted collections can be permanently removed. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdpdb', length=15) + coll_name = self.create_random_name(prefix='clisdpcoll', length=15) + partition_key = "/pk" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'db_name': db_name, + 'coll_name': coll_name, + 'part': partition_key, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Creating SQL container") + self.cmd( + 'az cosmosdb sql container create -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} -p {part}' + ) + + logger.info("Waiting for container to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the container") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + + time.sleep(60) + + logger.info("Listing soft-deleted collections") + soft_deleted_colls = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + deleted_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls) + assert deleted_coll_found, f"Soft-deleted collection '{coll_name}' should appear in the list" + + logger.info("Showing soft-deleted collection details") + soft_deleted_coll = self.cmd( + 'az cosmosdb sql softdeleted-collection show ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_coll is not None + + logger.info("Purging the soft-deleted collection") + self.cmd( + 'az cosmosdb sql softdeleted-collection delete ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg} --yes' + ) + + time.sleep(60) + + logger.info("Collection successfully purged") + + logger.info("Verifying collection is no longer in soft-deleted list") + soft_deleted_colls_after = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + purged_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls_after) + assert not purged_coll_found, "Collection should not appear in soft-deleted list after purge" + + logger.info("Cleaning up test resources") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/cosmosdb-preview/setup.py b/src/cosmosdb-preview/setup.py index 55fc3ee24f5..4c9b8ae2b6b 100644 --- a/src/cosmosdb-preview/setup.py +++ b/src/cosmosdb-preview/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.6.1' +VERSION = '1.7.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers