Skip to content

Commit da66134

Browse files
committed
Added soft delete feature
1 parent 8c484bc commit da66134

13 files changed

+6072
-6
lines changed

src/azure-cli/azure/cli/command_modules/sql/_help.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,10 @@
16011601
text: az sql server create -l westus -g mygroup -n myserver -u myadminuser -p myadminpassword --tags key1=value1 key2=value2
16021602
- name: Create a server with disabled public network access to server.
16031603
text: az sql server create -l westus -g mygroup -n myserver -u myadminuser -p myadminpassword -e false
1604+
- name: Create a server with soft delete enabled and 7-day retention period.
1605+
text: az sql server create -l westus -g mygroup -n myserver -u myadminuser -p myadminpassword --enable-soft-delete --soft-delete-retention-days 7
1606+
- name: Create a server with minimal TLS version and soft delete protection.
1607+
text: az sql server create -l westus -g mygroup -n myserver -u myadminuser -p myadminpassword --minimal-tls-version 1.2 --enable-soft-delete --soft-delete-retention-days 3
16041608
- name: Create a server without SQL Admin, with AD admin and AD Only enabled.
16051609
text: az sql server create --enable-ad-only-auth --external-admin-principal-type User --external-admin-name myUserName --external-admin-sid c5e964e2-6bb2-1111-1111-3b16ec0e1234 -g myResourceGroup -n myServer
16061610
- name: Create a server without SQL Admin, with AD admin, AD Only enabled, User ManagedIdenties and Identity Type is SystemAssigned,UserAssigned.
@@ -1615,6 +1619,13 @@
16151619
--identity-type UserAssigned --pid /subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/testumi
16161620
"""
16171621

1622+
helps['sql server restore'] = """
1623+
type: command
1624+
short-summary: Restore a deleted SQL server.
1625+
examples:
1626+
- name: Restore a deleted server.
1627+
text: az sql server restore -g mygroup -n myserver -l westus2
1628+
"""
16181629
helps['sql server dns-alias'] = """
16191630
type: group
16201631
short-summary: Manage a server's DNS aliases.
@@ -1799,7 +1810,12 @@
17991810
examples:
18001811
- name: Update a server. (autogenerated)
18011812
text: az sql server update --admin-password myadminpassword --name MyAzureSQLServer --resource-group MyResourceGroup
1802-
crafted: true
1813+
- name: Enable soft delete protection with 7-day retention.
1814+
text: az sql server update --name MyAzureSQLServer --resource-group MyResourceGroup --enable-soft-delete
1815+
- name: Modify soft delete retention period.
1816+
text: az sql server update --name MyAzureSQLServer --resource-group MyResourceGroup --enable-soft-delete --soft-delete-retention-days 5
1817+
- name: Disable soft delete protection.
1818+
text: az sql server update --name MyAzureSQLServer --resource-group MyResourceGroup --enable-soft-delete false
18031819
- name: Update a server with User Managed Identies and Identity Type is SystemAssigned,UserAssigned.
18041820
text: az sql server update -g myResourceGroup -n myServer -i \\
18051821
--user-assigned-identity-id /subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/testumi \\

src/azure-cli/azure/cli/command_modules/sql/_params.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@
7373
create_args_for_complex_type,
7474
validate_managed_instance_storage_size,
7575
validate_backup_storage_redundancy,
76-
validate_subnet
76+
validate_subnet,
77+
validate_soft_delete_parameters
7778
)
7879

7980
#####
@@ -1912,6 +1913,17 @@ def _configure_security_policy_storage_params(arg_ctx):
19121913
options_list=['--federated-client-id', '--fid'],
19131914
help='The federated client id used in cross tenant CMK scenario.')
19141915

1916+
c.argument('enable_soft_delete',
1917+
options_list=['--enable-soft-delete'],
1918+
arg_type=get_three_state_flag(),
1919+
help='Set whether soft delete is enabled or not. When true,'
1920+
'the soft delete is enabled for 7 days by default.')
1921+
1922+
c.argument('soft_delete_retention_days',
1923+
options_list=['--soft-delete-retention-days'],
1924+
help='The number of days to retain soft deleted resources.',
1925+
validator=validate_soft_delete_parameters)
1926+
19151927
with self.argument_context('sql server create') as c:
19161928
c.argument('location',
19171929
arg_type=get_location_type_with_default_from_resource_group(self.cli_ctx))
@@ -1923,7 +1935,9 @@ def _configure_security_policy_storage_params(arg_ctx):
19231935
'administrator_login_password',
19241936
'location',
19251937
'minimal_tls_version',
1926-
'tags'
1938+
'tags',
1939+
'enable_soft_delete',
1940+
'soft_delete_retention_days'
19271941
])
19281942

19291943
c.argument('administrator_login',
@@ -1963,6 +1977,12 @@ def _configure_security_policy_storage_params(arg_ctx):
19631977
options_list=['--expand-ad-admin'],
19641978
help='Expand the Active Directory Administrator for the server.')
19651979

1980+
with self.argument_context('sql server restore') as c:
1981+
c.argument('location',
1982+
arg_type=get_location_type_with_default_from_resource_group(self.cli_ctx),
1983+
required=True,
1984+
help='Location where the deleted server was originally located.')
1985+
19661986
with self.argument_context('sql server list') as c:
19671987
c.argument('expand_ad_admin',
19681988
options_list=['--expand-ad-admin'],

src/azure-cli/azure/cli/command_modules/sql/_util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,7 @@ def get_sql_managed_database_ledger_digest_uploads_operations(cli_ctx, _):
262262

263263
def get_sql_managed_database_move_operations(cli_ctx, _):
264264
return get_sql_management_client(cli_ctx).managed_database_move_operations
265+
266+
267+
def get_sql_deleted_servers_operations(cli_ctx, _):
268+
return get_sql_management_client(cli_ctx).deleted_servers

src/azure-cli/azure/cli/command_modules/sql/_validators.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# --------------------------------------------------------------------------------------------
55

66
from azure.cli.core.util import CLIError
7+
from azure.cli.core.azclierror import (
8+
RequiredArgumentMissingError,
9+
InvalidArgumentValueError
10+
)
711

812
# Important note: if cmd validator exists, then individual param validators will not be
913
# executed. See C:\git\azure-cli\env\lib\site-packages\knack\invocation.py `def _validation`
@@ -138,3 +142,48 @@ def validate_managed_instance_storage_size(namespace):
138142
pass
139143
else:
140144
raise CLIError('incorrect usage: --storage must be specified in increments of 32 GB')
145+
146+
147+
###############################################
148+
# sql server #
149+
###############################################
150+
151+
152+
def validate_soft_delete_parameters(namespace):
153+
enable_soft_delete = getattr(namespace, 'enable_soft_delete', None)
154+
soft_delete_retention_days = getattr(namespace, 'soft_delete_retention_days', None)
155+
156+
# Check if soft_delete_retention_days is specified without enable_soft_delete
157+
if soft_delete_retention_days is not None and enable_soft_delete is None:
158+
raise RequiredArgumentMissingError(
159+
'The --soft-delete-retention-days parameter requires --enable-soft-delete to be specified.',
160+
'Please specify both --enable-soft-delete and --soft-delete-retention-days together.')
161+
162+
# Validate soft_delete_retention_days value when specified
163+
if soft_delete_retention_days is not None:
164+
try:
165+
retention_days = int(soft_delete_retention_days)
166+
namespace.soft_delete_retention_days = retention_days
167+
except (ValueError, TypeError) as exc:
168+
raise InvalidArgumentValueError(
169+
'The value for --soft-delete-retention-days must be a valid integer.') from exc
170+
171+
# Validate range based on enable_soft_delete value
172+
if enable_soft_delete is True:
173+
# When enable_soft_delete is true, retention days must be 1-7
174+
if not 1 <= retention_days <= 7:
175+
raise InvalidArgumentValueError(
176+
'The --soft-delete-retention-days value must be between 1 and 7 (inclusive) '
177+
'when --enable-soft-delete is true.',
178+
'Please specify a value between 1 and 7 days.')
179+
elif enable_soft_delete is False:
180+
# When enable_soft_delete is false, retention days must be 0
181+
if retention_days != 0:
182+
raise InvalidArgumentValueError(
183+
'The --soft-delete-retention-days value must be 0 when --enable-soft-delete is false.',
184+
'Please set --soft-delete-retention-days to 0 or remove it when disabling soft delete.')
185+
else:
186+
# This shouldn't happen since we check above, but for safety
187+
raise RequiredArgumentMissingError(
188+
'The --soft-delete-retention-days parameter requires --enable-soft-delete to be specified.',
189+
'Please specify both --enable-soft-delete and --soft-delete-retention-days together.')

src/azure-cli/azure/cli/command_modules/sql/commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,9 @@ def load_command_table(self, _):
572572
g.custom_command('create', 'server_create',
573573
table_transformer=server_table_format,
574574
supports_no_wait=True)
575+
g.custom_command('restore', 'server_restore',
576+
table_transformer=server_table_format,
577+
supports_no_wait=True)
575578
g.command('delete', 'begin_delete',
576579
confirmation=True)
577580
g.custom_show_command('show', 'server_get',

src/azure-cli/azure/cli/command_modules/sql/custom.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
CLIError,
1414
sdk_no_wait,
1515
)
16+
from azure.cli.core.azclierror import (
17+
ResourceNotFoundError,
18+
ValidationError,
19+
AzCLIError
20+
)
1621

1722
from azure.mgmt.sql.models import (
1823
AdministratorName,
@@ -58,6 +63,7 @@
5863
SensitivityLabelSource,
5964
ServerAzureADOnlyAuthentication,
6065
ServerConnectionPolicy,
66+
ServerCreateMode,
6167
ServerExternalAdministrator,
6268
ServerInfo,
6369
ServerKey,
@@ -92,6 +98,7 @@
9298
get_sql_replication_links_operations,
9399
get_sql_elastic_pools_operations,
94100
get_sql_databases_operations,
101+
get_sql_deleted_servers_operations,
95102
)
96103

97104

@@ -179,6 +186,58 @@ def _is_serverless_slo(sku_name):
179186
return "_S_" in sku_name
180187

181188

189+
def _check_server_exists(client, resource_group_name, server_name):
190+
'''
191+
Checks if a server already exists and raises ValidationError if it does.
192+
Returns False if server doesn't exist (ResourceNotFound/NotFound),
193+
raises the original exception for other errors.
194+
'''
195+
try:
196+
existing_server = client.get(resource_group_name, server_name)
197+
if existing_server:
198+
raise ValidationError(
199+
f'Server "{server_name}" already exists in resource group "{resource_group_name}". '
200+
'Cannot restore to an existing server name.',
201+
'Please specify a different server name for the restore operation.')
202+
except ValidationError:
203+
# Re-raise ValidationError as-is
204+
raise
205+
except Exception as ex:
206+
# If server doesn't exist, that's what we want - continue with restore
207+
if 'ResourceNotFound' not in str(ex) and 'NotFound' not in str(ex):
208+
# Re-raise if it's not a "not found" error
209+
raise
210+
return False
211+
212+
213+
def _validate_deleted_server_exists(cmd, location, server_name):
214+
"""
215+
Validates that a deleted server exists and can be restored.
216+
"""
217+
try:
218+
deleted_servers_client = get_sql_deleted_servers_operations(cmd.cli_ctx, None)
219+
deleted_server = deleted_servers_client.get(location, server_name)
220+
221+
if deleted_server:
222+
logger.info('Found deleted server: %s', deleted_server.name)
223+
return True
224+
225+
except Exception as ex:
226+
# If server doesn't exist in deleted servers, raise appropriate error
227+
if 'ResourceNotFound' in str(ex) or 'NotFound' in str(ex):
228+
raise ResourceNotFoundError(
229+
f'No deleted server found with name "{server_name}" in location "{location}".',
230+
'Please verify the server was deleted with soft delete enabled '
231+
'and is within the retention period.') from ex
232+
# Handle any other unexpected errors
233+
raise AzCLIError(f'Failed to validate deleted server "{server_name}": {str(ex)}') from ex
234+
235+
# This shouldn't be reached if the API call succeeds but returns None
236+
raise ResourceNotFoundError(
237+
f'No deleted server found with name "{server_name}" in location "{location}".',
238+
'Please verify the server was deleted with soft delete enabled and is within the retention period.')
239+
240+
182241
def _get_default_server_version(location_capabilities):
183242
'''
184243
Gets the default server version capability from the full location
@@ -4369,6 +4428,8 @@ def server_create(
43694428
external_admin_principal_type=None,
43704429
external_admin_sid=None,
43714430
external_admin_name=None,
4431+
enable_soft_delete=None,
4432+
soft_delete_retention_days=None,
43724433
**kwargs):
43734434
'''
43744435
Creates a server.
@@ -4414,13 +4475,58 @@ def server_create(
44144475
azure_ad_only_authentication=ad_only,
44154476
tenant_id=tenant_id)
44164477

4478+
kwargs['create_mode'] = ServerCreateMode.NORMAL
4479+
4480+
if enable_soft_delete is not None:
4481+
if soft_delete_retention_days is not None:
4482+
kwargs['retention_days'] = soft_delete_retention_days
4483+
else:
4484+
kwargs['retention_days'] = 7
4485+
44174486
# Create
44184487
return sdk_no_wait(no_wait, client.begin_create_or_update,
44194488
server_name=server_name,
44204489
resource_group_name=resource_group_name,
44214490
parameters=kwargs)
44224491

44234492

4493+
def server_restore(
4494+
cmd,
4495+
client,
4496+
resource_group_name,
4497+
server_name,
4498+
location,
4499+
no_wait=False,
4500+
**kwargs):
4501+
'''
4502+
Restores a deleted server.
4503+
'''
4504+
# Validate that we have enough information to perform the restore
4505+
if not server_name or not resource_group_name or not location:
4506+
logger.info('Not all required parameters were provided. '
4507+
'Please specify server name, resource group name, and location.')
4508+
return None
4509+
4510+
# Check if server already exists
4511+
_check_server_exists(client, resource_group_name, server_name)
4512+
4513+
# Validate deleted server exists
4514+
_validate_deleted_server_exists(cmd, location, server_name)
4515+
4516+
# Set required parameters for restore
4517+
kwargs['location'] = location
4518+
kwargs['create_mode'] = ServerCreateMode.RESTORE
4519+
4520+
logger.info('Attempting to restore server "%s" in location "%s"',
4521+
server_name, location)
4522+
4523+
# Create/restore the server
4524+
return sdk_no_wait(no_wait, client.begin_create_or_update,
4525+
server_name=server_name,
4526+
resource_group_name=resource_group_name,
4527+
parameters=kwargs)
4528+
4529+
44244530
def server_list(
44254531
client,
44264532
resource_group_name=None,
@@ -4469,7 +4575,9 @@ def server_update(
44694575
key_id=None,
44704576
federated_client_id=None,
44714577
identity_type=None,
4472-
user_assigned_identity_id=None):
4578+
user_assigned_identity_id=None,
4579+
enable_soft_delete=None,
4580+
soft_delete_retention_days=None):
44734581
'''
44744582
Updates a server. Custom update function to apply parameters to instance.
44754583
'''
@@ -4507,6 +4615,17 @@ def server_update(
45074615
instance.key_id = (key_id or instance.key_id)
45084616
instance.federated_client_id = (federated_client_id or instance.federated_client_id)
45094617

4618+
# Handle soft delete parameters
4619+
if enable_soft_delete is not None:
4620+
if soft_delete_retention_days is not None:
4621+
instance.retention_days = soft_delete_retention_days
4622+
elif enable_soft_delete:
4623+
# Set default to 7 days when enabling soft delete without specifying retention
4624+
instance.retention_days = 7
4625+
else:
4626+
# When disabling soft delete, clear retention days
4627+
instance.retention_days = 0
4628+
45104629
return instance
45114630

45124631

0 commit comments

Comments
 (0)