Skip to content

Commit aeb9a42

Browse files
authored
[Backup] az backup: Add support for new AFS Vault Standard Policies (#30531)
1 parent eb4feac commit aeb9a42

File tree

11 files changed

+19648
-1631
lines changed

11 files changed

+19648
-1631
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def load_arguments(self, _):
237237
c.argument('fix_for_inconsistent_items', arg_type=get_three_state_flag(), options_list=['--fix-for-inconsistent-items'], help='Specify whether or not to retry Policy Update for failed items.')
238238
c.argument('backup_management_type', backup_management_type)
239239
c.argument('tenant_id', help='ID of the tenant if the Resource Guard protecting the vault exists in a different tenant.')
240+
c.argument('yes', options_list=['--yes', '-y'], help='Skip confirmation when updating Standard to Enhanced Policies.', action='store_true')
240241

241242
with self.argument_context('backup policy create') as c:
242243
c.argument('policy', type=file_type, help='JSON encoded policy definition. Use the show command with JSON output to obtain a policy object. Modify the values using a file editor and pass the object.', completer=FilesCompleter())

src/azure-cli/azure/cli/command_modules/backup/custom_afs.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
from azure.mgmt.recoveryservicesbackup.activestamp import RecoveryServicesBackupClient
2525
from azure.cli.core.commands.client_factory import get_mgmt_service_client
2626

27+
from knack.log import get_logger
28+
logger = get_logger(__name__)
29+
2730
fabric_name = "Azure"
2831
backup_management_type = "AzureStorage"
2932
workload_type = "AzureFileShare"
@@ -245,9 +248,9 @@ def list_recovery_points(cmd, client, resource_group_name, vault_name, item, sta
245248
Please either remove the flag or query for any other backup-management-type.
246249
""")
247250

248-
if is_ready_for_move is not None or target_tier is not None or tier is not None:
251+
if is_ready_for_move is not None or target_tier is not None:
249252
raise ArgumentUsageError("""Invalid argument has been passed. --is-ready-for-move true, --target-tier
250-
and --tier flags are not supported for --backup-management-type AzureStorage.""")
253+
are not supported for --backup-management-type AzureStorage.""")
251254

252255
if recommended_for_archive is not None:
253256
raise ArgumentUsageError("""--recommended-for-archive is supported by AzureIaasVM backup management
@@ -270,11 +273,57 @@ def list_recovery_points(cmd, client, resource_group_name, vault_name, item, sta
270273
recovery_points = client.list(vault_name, resource_group_name, fabric_name, container_uri, item_uri, filter_string)
271274
paged_recovery_points = helper.get_list_from_paged_response(recovery_points)
272275

276+
if tier:
277+
filtered_recovery_points = []
278+
279+
for rp in paged_recovery_points:
280+
# Prepare to collect tier types
281+
rp_tier_types = []
282+
283+
# Safely grab additional_properties
284+
additional_props = getattr(rp.properties, 'additional_properties', {})
285+
if not isinstance(additional_props, dict):
286+
continue
287+
288+
# Get details list
289+
tier_details_list = additional_props.get("recoveryPointTierDetails", [])
290+
if not isinstance(tier_details_list, list):
291+
continue
292+
293+
for detail in tier_details_list:
294+
if not isinstance(detail, dict):
295+
continue
296+
rp_type = detail.get("type")
297+
if rp_type:
298+
rp_tier_types.append(rp_type)
299+
300+
# Map types to a tier
301+
if 'InstantRP' in rp_tier_types and 'HardenedRP' in rp_tier_types:
302+
rp_tier = 'SnapshotAndVaultStandard'
303+
elif 'InstantRP' in rp_tier_types:
304+
rp_tier = 'Snapshot'
305+
elif 'HardenedRP' in rp_tier_types:
306+
rp_tier = 'VaultStandard'
307+
else:
308+
logger.warning(
309+
"Warning: Unrecognized Recovery Point tier received."
310+
"If you see this message, please contact Microsoft Support."
311+
"The recognized tiers for AzureFileShare are: 'Snapshot', 'VaultStandard', or "
312+
"'SnapshotAndVaultStandard'."
313+
)
314+
rp_tier = None
315+
316+
# Filter by matching tier
317+
if rp_tier == tier:
318+
filtered_recovery_points.append(rp)
319+
320+
return filtered_recovery_points
321+
273322
return paged_recovery_points
274323

275324

276325
def update_policy_for_item(cmd, client, resource_group_name, vault_name, item, policy, tenant_id=None,
277-
is_critical_operation=False):
326+
is_critical_operation=False, yes=False):
278327
if item.properties.backup_management_type != policy.properties.backup_management_type:
279328
raise CLIError(
280329
"""
@@ -302,6 +351,13 @@ def update_policy_for_item(cmd, client, resource_group_name, vault_name, item, p
302351
aux_tenants=[tenant_id]).protected_items
303352
afs_item.properties.resource_guard_operation_requests = [helper.get_resource_guard_operation_request(
304353
cmd.cli_ctx, resource_group_name, vault_name, "updateProtection")]
354+
355+
# Validate existing & new policy
356+
existing_policy_name = item.properties.policy_id.split('/')[-1]
357+
existing_policy = common.show_policy(protection_policies_cf(cmd.cli_ctx), resource_group_name, vault_name,
358+
existing_policy_name)
359+
helper.validate_update_policy_request(existing_policy, policy, yes)
360+
305361
# Update policy
306362
result = client.create_or_update(vault_name, resource_group_name, fabric_name,
307363
container_uri, item_uri, afs_item, cls=helper.get_pipeline_response)
@@ -365,7 +421,7 @@ def _get_storage_account_id(cli_ctx, storage_account_name, storage_account_rg):
365421

366422

367423
def set_policy(cmd, client, resource_group_name, vault_name, policy, policy_name, tenant_id=None,
368-
is_critical_operation=False):
424+
is_critical_operation=False, yes=False):
369425
if policy_name is None:
370426
raise CLIError(
371427
"""
@@ -375,7 +431,8 @@ def set_policy(cmd, client, resource_group_name, vault_name, policy, policy_name
375431
policy_object = helper.get_policy_from_json(client, policy)
376432
policy_object.properties.work_load_type = workload_type
377433
existing_policy = common.show_policy(client, resource_group_name, vault_name, policy_name)
378-
helper.validate_update_policy_request(existing_policy, policy_object)
434+
435+
helper.validate_update_policy_request(existing_policy, policy_object, yes)
379436
if is_critical_operation:
380437
if helper.is_retention_duration_decreased(existing_policy, policy_object, "AzureStorage"):
381438
# update the payload with critical operation and add auxiliary header for cross tenant case

src/azure-cli/azure/cli/command_modules/backup/custom_base.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def update_policy_for_item(cmd, client, resource_group_name, vault_name, contain
230230

231231
if item.properties.backup_management_type.lower() == "azurestorage":
232232
return custom_afs.update_policy_for_item(cmd, client, resource_group_name, vault_name, item, policy, tenant_id,
233-
is_critical_operation)
233+
is_critical_operation, yes)
234234

235235
if item.properties.backup_management_type.lower() == "azureworkload":
236236
return custom_wl.update_policy_for_item(cmd, client, resource_group_name, vault_name, item, policy, tenant_id,
@@ -239,19 +239,18 @@ def update_policy_for_item(cmd, client, resource_group_name, vault_name, contain
239239

240240

241241
def set_policy(cmd, client, resource_group_name, vault_name, policy=None, name=None,
242-
fix_for_inconsistent_items=None, backup_management_type=None, tenant_id=None):
242+
fix_for_inconsistent_items=None, backup_management_type=None, tenant_id=None, yes=False):
243243
if backup_management_type is None and policy is not None:
244244
policy_object = custom_help.get_policy_from_json(client, policy)
245245
backup_management_type = policy_object.properties.backup_management_type.lower()
246246
is_critical_operation = custom_help.has_resource_guard_mapping(cmd.cli_ctx, resource_group_name, vault_name,
247247
"updatePolicy")
248-
249248
if backup_management_type.lower() == "azureiaasvm":
250249
return custom.set_policy(cmd, client, resource_group_name, vault_name, policy, name, tenant_id,
251250
is_critical_operation)
252251
if backup_management_type.lower() == "azurestorage":
253252
return custom_afs.set_policy(cmd, client, resource_group_name, vault_name, policy, name, tenant_id,
254-
is_critical_operation)
253+
is_critical_operation, yes)
255254
if backup_management_type.lower() == "azureworkload":
256255
return custom_wl.set_policy(cmd, client, resource_group_name, vault_name, policy, name,
257256
fix_for_inconsistent_items, tenant_id, is_critical_operation)

src/azure-cli/azure/cli/command_modules/backup/custom_help.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from urllib.parse import urlparse
1212

1313
from knack.log import get_logger
14+
from knack.prompting import prompt_y_n
1415

1516
from azure.mgmt.core.tools import parse_resource_id, is_valid_resource_id
1617

@@ -671,11 +672,45 @@ def validate_and_extract_container_type(container_name, backup_management_type):
671672
return None
672673

673674

674-
def validate_update_policy_request(existing_policy, new_policy):
675+
def validate_update_policy_request(existing_policy, new_policy, yes=False):
675676
existing_backup_management_type = existing_policy.properties.backup_management_type
676677
new_backup_management_type = new_policy.properties.backup_management_type
677678
if existing_backup_management_type != new_backup_management_type:
678679
raise CLIError("BackupManagementType cannot be different than the existing type.")
680+
vault_to_snapshot = (
681+
new_backup_management_type.lower() == 'azurestorage' and
682+
hasattr(existing_policy.properties, 'vault_retention_policy') and
683+
existing_policy.properties.vault_retention_policy is not None and
684+
hasattr(new_policy.properties, 'retention_policy') and
685+
new_policy.properties.retention_policy is not None
686+
)
687+
688+
snapshot_to_vault = (
689+
new_backup_management_type.lower() == 'azurestorage' and
690+
hasattr(existing_policy.properties, 'retention_policy') and
691+
existing_policy.properties.retention_policy is not None and
692+
hasattr(new_policy.properties, 'vault_retention_policy') and
693+
new_policy.properties.vault_retention_policy is not None
694+
)
695+
696+
# vault -> snapshot
697+
if vault_to_snapshot:
698+
raise CLIError(
699+
"""
700+
Switching the backup tier from vaulted backup to snapshot is not possible.
701+
Please create a new policy for snapshot-only backups.
702+
""")
703+
# snapshot -> vault
704+
if snapshot_to_vault:
705+
warning_prompt = ('Changing the backup tier keeps current snapshots as-is under the existing policy.'
706+
'Future backups will be stored in the vault with new retention settings. '
707+
'This action is irreversible and incurs additional costs.'
708+
'Switching from vault to snapshot requires reconfiguration.'
709+
'Learn more at'
710+
'https://learn.microsoft.com/en-us/azure/backup/azure-file-share-backup-overview.'
711+
'Do you want to continue?')
712+
if not yes and not prompt_y_n(warning_prompt):
713+
raise CLIError('Cancelling policy update operation')
679714

680715

681716
def transform_softdelete_parameters(parameter):

src/azure-cli/azure/cli/command_modules/backup/tests/latest/preparers.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def _get_key_url(self, **kwargs):
395395
class AFSPolicyPreparer(AbstractPreparer, SingleValueReplacer):
396396
def __init__(self, name_prefix='clitest-item', parameter_name='policy_name', vault_parameter_name='vault_name',
397397
resource_group_parameter_name='resource_group',
398-
instant_rp_days=None):
398+
backup_tier="Snapshot"):
399399
super().__init__(name_prefix, 24)
400400
from azure.cli.core.mock import DummyCli
401401
self.cli_ctx = DummyCli()
@@ -404,7 +404,7 @@ def __init__(self, name_prefix='clitest-item', parameter_name='policy_name', vau
404404
self.resource_group_parameter_name = resource_group_parameter_name
405405
self.vault = None
406406
self.vault_parameter_name = vault_parameter_name
407-
self.instant_rp_days = instant_rp_days
407+
self.backup_tier = backup_tier
408408

409409
def create_resource(self, name, **kwargs):
410410
if not os.environ.get('AZURE_CLI_TEST_DEV_BACKUP_POLICY_NAME', None):
@@ -413,10 +413,46 @@ def create_resource(self, name, **kwargs):
413413

414414
policy_json = execute(self.cli_ctx, 'az backup policy show -g {} -v {} -n {}'
415415
.format(self.resource_group, self.vault, 'DefaultPolicy')).get_output_in_json()
416+
417+
# Remove unwanted keys from default AzureVM policy
418+
keys_to_remove = [
419+
'instantRpDetails',
420+
'instantRpRetentionRangeInDays',
421+
'policyType',
422+
'snapshotConsistencyType',
423+
'tieringPolicy'
424+
]
425+
426+
for key in keys_to_remove:
427+
policy_json['properties'].pop(key, None)
428+
416429
policy_json['name'] = name
417-
if self.instant_rp_days:
418-
policy_json['properties']['instantRpRetentionRangeInDays'] = self.instant_rp_days
430+
419431
policy_json['properties']['backupManagementType'] = "AzureStorage"
432+
policy_json['properties']['workLoadType'] = "AzureFileShare"
433+
434+
# Modify the policy based on the backup tier
435+
if self.backup_tier.lower() == 'vaultstandard':
436+
# Set retentionPolicy to null
437+
policy_json['properties'].pop('retentionPolicy', None)
438+
439+
# Add vaultRetentionPolicy with the required properties
440+
policy_json['properties']['vaultRetentionPolicy'] = {
441+
"snapshotRetentionInDays": 5,
442+
"vaultRetention": {
443+
"dailySchedule": {
444+
"retentionDuration": {
445+
"count": 30,
446+
"durationType": "Days"
447+
},
448+
"retentionTimes": policy_json['properties']['schedulePolicy']['scheduleRunTimes']
449+
},
450+
"monthlySchedule": None,
451+
"retentionPolicyType": "LongTermRetentionPolicy",
452+
"weeklySchedule": None,
453+
"yearlySchedule": None
454+
}
455+
}
420456
policy_json = json.dumps(policy_json)
421457

422458
command_string = 'az backup policy create -g {} -v {} --policy \'{}\' -n {} --backup-management-type {}'

0 commit comments

Comments
 (0)