From 0b19a886346ef2ed69dfc2d79c0bfba4a0bf0075 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Wed, 18 Mar 2026 18:43:15 +0530 Subject: [PATCH 1/4] Add Azure Backup MCP toolset - Drop 1 (15 commands) Implements the first subset of Azure Backup MCP tools with 15 commands: - Vault: get (consolidated get+list), create, update - Policy: get (consolidated get+list), create - Protected Item: get (consolidated get+list), protect - Protectable Item: list - Backup: status - Job: get (consolidated get+list) - Recovery Point: get (consolidated get+list) - Governance: find-unprotected, immutability, soft-delete - DR: enablecrr Key design decisions: - Consolidated get+list into single 'get' commands with optional identifier - Full service layer included for extensibility in future drops - Dual vault architecture (RSV + Backup vault) with auto-detection - 99 unit tests covering all 15 commands Infrastructure changes: - Added Azure.ResourceManager.RecoveryServices 1.1.1 - Added Azure.ResourceManager.RecoveryServicesBackup 1.3.0 - Added Azure.ResourceManager.DataProtectionBackup 1.7.0 - Registered AzureBackupSetup in Program.cs and both slnx files --- Directory.Packages.props | 3 + Microsoft.Mcp.slnx | 7 + .../Azure.Mcp.Server/Azure.Mcp.Server.slnx | 7 + servers/Azure.Mcp.Server/src/Program.cs | 1 + .../src/AssemblyInfo.cs | 7 + .../src/Azure.Mcp.Tools.AzureBackup.csproj | 19 + .../src/AzureBackupSetup.cs | 136 ++ .../src/Commands/AzureBackupJsonContext.cs | 68 + .../Commands/Backup/BackupStatusCommand.cs | 60 + .../src/Commands/BaseAzureBackupCommand.cs | 35 + .../src/Commands/BaseProtectedItemCommand.cs | 30 + .../src/Commands/Dr/DrEnableCrrCommand.cs | 42 + .../GovernanceFindUnprotectedCommand.cs | 89 + .../GovernanceImmutabilityCommand.cs | 97 + .../Governance/GovernanceSoftDeleteCommand.cs | 98 + .../src/Commands/Job/JobGetCommand.cs | 120 + .../Commands/Policy/PolicyCreateCommand.cs | 70 + .../src/Commands/Policy/PolicyGetCommand.cs | 120 + .../ProtectableItemListCommand.cs | 96 + .../ProtectedItem/ProtectedItemGetCommand.cs | 127 + .../ProtectedItemProtectCommand.cs | 108 + .../RecoveryPoint/RecoveryPointGetCommand.cs | 125 + .../src/Commands/Vault/VaultCreateCommand.cs | 105 + .../src/Commands/Vault/VaultGetCommand.cs | 126 + .../src/Commands/Vault/VaultUpdateCommand.cs | 66 + .../src/GlobalUsings.cs | 4 + .../src/Models/BackupJobInfo.cs | 17 + .../src/Models/BackupPolicyInfo.cs | 13 + .../src/Models/BackupStatusResult.cs | 13 + .../src/Models/BackupTriggerResult.cs | 11 + .../src/Models/BackupVaultInfo.cs | 17 + .../src/Models/ContainerInfo.cs | 17 + .../src/Models/CostEstimateResult.cs | 12 + .../src/Models/DrValidationResult.cs | 14 + .../src/Models/HealthCheckResult.cs | 23 + .../src/Models/OperationResult.cs | 9 + .../src/Models/ProtectResult.cs | 12 + .../src/Models/ProtectableItemInfo.cs | 17 + .../src/Models/ProtectedItemInfo.cs | 17 + .../src/Models/RecoveryPointInfo.cs | 13 + .../src/Models/RestoreTriggerResult.cs | 11 + .../src/Models/UnprotectedResourceInfo.cs | 12 + .../src/Models/VaultCreateResult.cs | 13 + .../src/Models/WorkflowResult.cs | 15 + .../Options/AzureBackupOptionDefinitions.cs | 713 ++++++ .../src/Options/Backup/BackupStatusOptions.cs | 16 + .../src/Options/BaseAzureBackupOptions.cs | 16 + .../src/Options/BaseProtectedItemOptions.cs | 15 + .../src/Options/Dr/DrEnableCrrOptions.cs | 8 + .../GovernanceFindUnprotectedOptions.cs | 19 + .../GovernanceImmutabilityOptions.cs | 12 + .../Governance/GovernanceSoftDeleteOptions.cs | 15 + .../src/Options/Job/JobGetOptions.cs | 12 + .../src/Options/Job/JobListOptions.cs | 8 + .../src/Options/Policy/PolicyCreateOptions.cs | 33 + .../src/Options/Policy/PolicyGetOptions.cs | 12 + .../src/Options/Policy/PolicyListOptions.cs | 8 + .../ProtectableItemListOptions.cs | 15 + .../ProtectedItem/ProtectedItemGetOptions.cs | 8 + .../ProtectedItem/ProtectedItemListOptions.cs | 8 + .../ProtectedItemProtectOptions.cs | 18 + .../RecoveryPoint/RecoveryPointGetOptions.cs | 12 + .../RecoveryPoint/RecoveryPointListOptions.cs | 8 + .../src/Options/Vault/VaultCreateOptions.cs | 18 + .../src/Options/Vault/VaultGetOptions.cs | 8 + .../src/Options/Vault/VaultListOptions.cs | 8 + .../src/Options/Vault/VaultUpdateOptions.cs | 27 + .../src/Services/AzureBackupService.cs | 801 +++++++ .../src/Services/DppBackupOperations.cs | 1021 ++++++++ .../src/Services/DppDatasourceProfile.cs | 135 ++ .../src/Services/DppDatasourceRegistry.cs | 257 ++ .../src/Services/IAzureBackupService.cs | 106 + .../src/Services/IDppBackupOperations.cs | 40 + .../src/Services/IRsvBackupOperations.cs | 49 + .../src/Services/RsvBackupOperations.cs | 2065 +++++++++++++++++ .../src/Services/RsvDatasourceProfile.cs | 148 ++ .../src/Services/RsvDatasourceRegistry.cs | 190 ++ .../src/Services/VaultTypeResolver.cs | 32 + ...ure.Mcp.Tools.AzureBackup.UnitTests.csproj | 17 + .../Backup/BackupStatusCommandTests.cs | 123 + .../Dr/DrEnableCrrCommandTests.cs | 125 + .../GovernanceFindUnprotectedCommandTests.cs | 154 ++ .../GovernanceImmutabilityCommandTests.cs | 128 + .../GovernanceSoftDeleteCommandTests.cs | 128 + .../Job/JobGetCommandTests.cs | 220 ++ .../Policy/PolicyCreateCommandTests.cs | 130 ++ .../Policy/PolicyGetCommandTests.cs | 240 ++ .../ProtectableItemListCommandTests.cs | 153 ++ .../ProtectedItemGetCommandTests.cs | 225 ++ .../ProtectedItemProtectCommandTests.cs | 127 + .../RecoveryPointGetCommandTests.cs | 226 ++ .../Vault/VaultCreateCommandTests.cs | 125 + .../Vault/VaultGetCommandTests.cs | 243 ++ .../Vault/VaultUpdateCommandTests.cs | 128 + 94 files changed, 10375 insertions(+) create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/AssemblyInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Azure.Mcp.Tools.AzureBackup.csproj create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Backup/BackupStatusCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseAzureBackupCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseProtectedItemCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Dr/DrEnableCrrCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceFindUnprotectedCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceImmutabilityCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceSoftDeleteCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyCreateCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectableItem/ProtectableItemListCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemProtectCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultUpdateCommand.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/GlobalUsings.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupJobInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupPolicyInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupStatusResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupTriggerResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupVaultInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/ContainerInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/CostEstimateResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/DrValidationResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/OperationResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectableItemInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectedItemInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/RecoveryPointInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/RestoreTriggerResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/UnprotectedResourceInfo.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/VaultCreateResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Backup/BackupStatusOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseAzureBackupOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseProtectedItemOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Dr/DrEnableCrrOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceFindUnprotectedOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceImmutabilityOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceSoftDeleteOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobGetOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyCreateOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyGetOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectableItem/ProtectableItemListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemGetOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemProtectOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointGetOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultCreateOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultGetOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultListOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultUpdateOptions.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Services/VaultTypeResolver.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Azure.Mcp.Tools.AzureBackup.UnitTests.csproj create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Backup/BackupStatusCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Dr/DrEnableCrrCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceFindUnprotectedCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceImmutabilityCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceSoftDeleteCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Job/JobGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectableItem/ProtectableItemListCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemProtectCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/RecoveryPoint/RecoveryPointGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultCreateCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultGetCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultUpdateCommandTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c63eb5d7f..bef034b79e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,9 @@ + + + diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 44eda49c90..0497917b05 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -122,6 +122,13 @@ + + + + + + + diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx index 9fdc325712..5d960f8187 100644 --- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx +++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx @@ -86,6 +86,13 @@ + + + + + + + diff --git a/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs index 89c807f9ec..2bf3a1c709 100644 --- a/servers/Azure.Mcp.Server/src/Program.cs +++ b/servers/Azure.Mcp.Server/src/Program.cs @@ -93,6 +93,7 @@ private static IAreaSetup[] RegisterAreas() new Azure.Mcp.Tools.AppLens.AppLensSetup(), new Azure.Mcp.Tools.AppService.AppServiceSetup(), new Azure.Mcp.Tools.Authorization.AuthorizationSetup(), + new Azure.Mcp.Tools.AzureBackup.AzureBackupSetup(), new Azure.Mcp.Tools.AzureIsv.AzureIsvSetup(), new Azure.Mcp.Tools.ManagedLustre.ManagedLustreSetup(), new Azure.Mcp.Tools.AzureMigrate.AzureMigrateSetup(), diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/AssemblyInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/AssemblyInfo.cs new file mode 100644 index 0000000000..384ecb29c2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Azure.Mcp.Tools.AzureBackup.UnitTests")] +[assembly: InternalsVisibleTo("Azure.Mcp.Tools.AzureBackup.LiveTests")] diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Azure.Mcp.Tools.AzureBackup.csproj b/tools/Azure.Mcp.Tools.AzureBackup/src/Azure.Mcp.Tools.AzureBackup.csproj new file mode 100644 index 0000000000..9372dbb7da --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Azure.Mcp.Tools.AzureBackup.csproj @@ -0,0 +1,19 @@ + + + true + + + + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs new file mode 100644 index 0000000000..1a9d35c3c5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Tools.AzureBackup.Commands.Backup; +using Azure.Mcp.Tools.AzureBackup.Commands.Dr; +using Azure.Mcp.Tools.AzureBackup.Commands.Governance; +using Azure.Mcp.Tools.AzureBackup.Commands.Job; +using Azure.Mcp.Tools.AzureBackup.Commands.Policy; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint; +using Azure.Mcp.Tools.AzureBackup.Commands.Vault; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Mcp.Core.Areas; +using Microsoft.Mcp.Core.Commands; + +namespace Azure.Mcp.Tools.AzureBackup; + +public class AzureBackupSetup : IAreaSetup +{ + public string Name => "azurebackup"; + + public string Title => "Manage Azure Backup"; + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Vault (consolidated get = get + list) + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Policy (consolidated get = get + list) + services.AddSingleton(); + services.AddSingleton(); + + // Protected item (consolidated get = get + list) + services.AddSingleton(); + services.AddSingleton(); + + // Protectable item + services.AddSingleton(); + + // Backup + services.AddSingleton(); + + // Job (consolidated get = get + list) + services.AddSingleton(); + + // Recovery point (consolidated get = get + list) + services.AddSingleton(); + + // Governance + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // DR + services.AddSingleton(); + } + + public CommandGroup RegisterCommands(IServiceProvider serviceProvider) + { + var azureBackup = new CommandGroup(Name, + """ + Azure Backup operations – Unified commands to manage backup across Recovery Services vaults (RSV) + and Backup vaults (DPP/Data Protection). Supports vault management, protected item operations, + policy management, job monitoring, recovery point browsing, governance, and disaster recovery. + Use --vault-type to specify vault type or let the system auto-detect. + """, + Title); + + // Vault subgroup (get = list + get consolidated) + var vault = new CommandGroup("vault", "Backup vault operations – Get vault details or list all vaults, create, and update vaults."); + azureBackup.AddSubGroup(vault); + RegisterCommand(serviceProvider, vault); + RegisterCommand(serviceProvider, vault); + RegisterCommand(serviceProvider, vault); + + // Policy subgroup (get = list + get consolidated) + var policy = new CommandGroup("policy", "Backup policy operations – Get policy details or list all policies, and create policies."); + azureBackup.AddSubGroup(policy); + RegisterCommand(serviceProvider, policy); + RegisterCommand(serviceProvider, policy); + + // Protected item subgroup (get = list + get consolidated) + var protectedItem = new CommandGroup("protecteditem", "Protected item operations – Get protected item details or list all, and enable backup protection."); + azureBackup.AddSubGroup(protectedItem); + RegisterCommand(serviceProvider, protectedItem); + RegisterCommand(serviceProvider, protectedItem); + + // Protectable item subgroup + var protectableItem = new CommandGroup("protectableitem", "Protectable item operations – List discovered databases available for protection."); + azureBackup.AddSubGroup(protectableItem); + RegisterCommand(serviceProvider, protectableItem); + + // Backup subgroup + var backup = new CommandGroup("backup", "Backup operations – Check backup status for a datasource."); + azureBackup.AddSubGroup(backup); + RegisterCommand(serviceProvider, backup); + + // Job subgroup (get = list + get consolidated) + var job = new CommandGroup("job", "Backup job operations – Get job details or list all jobs in a vault."); + azureBackup.AddSubGroup(job); + RegisterCommand(serviceProvider, job); + + // Recovery point subgroup (get = list + get consolidated) + var recoveryPoint = new CommandGroup("recoverypoint", "Recovery point operations – Get recovery point details or list all for a protected item."); + azureBackup.AddSubGroup(recoveryPoint); + RegisterCommand(serviceProvider, recoveryPoint); + + // Governance subgroup + var governance = new CommandGroup("governance", "Governance operations – Find unprotected resources, configure immutability and soft delete."); + azureBackup.AddSubGroup(governance); + RegisterCommand(serviceProvider, governance); + RegisterCommand(serviceProvider, governance); + RegisterCommand(serviceProvider, governance); + + // DR subgroup + var dr = new CommandGroup("dr", "Disaster recovery operations – Enable Cross-Region Restore on a GRS vault."); + azureBackup.AddSubGroup(dr); + RegisterCommand(serviceProvider, dr); + + return azureBackup; + } + + private static void RegisterCommand(IServiceProvider serviceProvider, CommandGroup group) where T : IBaseCommand + { + var cmd = serviceProvider.GetRequiredService(); + group.AddCommand(cmd.Name, cmd); + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs new file mode 100644 index 0000000000..ab5e5badfd --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.AzureBackup.Commands.Backup; +using Azure.Mcp.Tools.AzureBackup.Commands.Dr; +using Azure.Mcp.Tools.AzureBackup.Commands.Governance; +using Azure.Mcp.Tools.AzureBackup.Commands.Job; +using Azure.Mcp.Tools.AzureBackup.Commands.Policy; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint; +using Azure.Mcp.Tools.AzureBackup.Commands.Vault; +using Azure.Mcp.Tools.AzureBackup.Models; + +namespace Azure.Mcp.Tools.AzureBackup.Commands; + +// Vault +[JsonSerializable(typeof(VaultGetCommand.VaultGetCommandResult))] +[JsonSerializable(typeof(VaultCreateCommand.VaultCreateCommandResult))] +[JsonSerializable(typeof(VaultUpdateCommand.VaultUpdateCommandResult))] +// Policy +[JsonSerializable(typeof(PolicyGetCommand.PolicyGetCommandResult))] +[JsonSerializable(typeof(PolicyCreateCommand.PolicyCreateCommandResult))] +// Protected item +[JsonSerializable(typeof(ProtectedItemGetCommand.ProtectedItemGetCommandResult))] +[JsonSerializable(typeof(ProtectedItemProtectCommand.ProtectedItemProtectCommandResult))] +// Protectable item +[JsonSerializable(typeof(ProtectableItemListCommand.ProtectableItemListCommandResult))] +// Backup +[JsonSerializable(typeof(BackupStatusCommand.BackupStatusCommandResult))] +// Job +[JsonSerializable(typeof(JobGetCommand.JobGetCommandResult))] +// Recovery point +[JsonSerializable(typeof(RecoveryPointGetCommand.RecoveryPointGetCommandResult))] +// Governance +[JsonSerializable(typeof(GovernanceFindUnprotectedCommand.GovernanceFindUnprotectedCommandResult))] +[JsonSerializable(typeof(GovernanceImmutabilityCommand.GovernanceImmutabilityCommandResult))] +[JsonSerializable(typeof(GovernanceSoftDeleteCommand.GovernanceSoftDeleteCommandResult))] +// DR +[JsonSerializable(typeof(DrEnableCrrCommand.DrEnableCrrCommandResult))] +// Model types +[JsonSerializable(typeof(BackupVaultInfo))] +[JsonSerializable(typeof(ProtectedItemInfo))] +[JsonSerializable(typeof(BackupPolicyInfo))] +[JsonSerializable(typeof(BackupJobInfo))] +[JsonSerializable(typeof(RecoveryPointInfo))] +[JsonSerializable(typeof(ProtectableItemInfo))] +[JsonSerializable(typeof(ContainerInfo))] +[JsonSerializable(typeof(VaultCreateResult))] +[JsonSerializable(typeof(ProtectResult))] +[JsonSerializable(typeof(BackupTriggerResult))] +[JsonSerializable(typeof(RestoreTriggerResult))] +[JsonSerializable(typeof(OperationResult))] +[JsonSerializable(typeof(BackupStatusResult))] +[JsonSerializable(typeof(CostEstimateResult))] +[JsonSerializable(typeof(HealthCheckResult))] +[JsonSerializable(typeof(HealthCheckItemDetail))] +[JsonSerializable(typeof(UnprotectedResourceInfo))] +[JsonSerializable(typeof(DrValidationResult))] +[JsonSerializable(typeof(WorkflowResult))] +[JsonSerializable(typeof(WorkflowStep))] +[JsonSerializable(typeof(JsonElement))] +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)] +internal sealed partial class AzureBackupJsonContext : JsonSerializerContext +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Backup/BackupStatusCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Backup/BackupStatusCommand.cs new file mode 100644 index 0000000000..c11e96f9de --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Backup/BackupStatusCommand.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Backup; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Backup; + +public sealed class BackupStatusCommand(ILogger logger) : SubscriptionCommand() +{ + private const string CommandTitle = "Check Backup Status"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678b5"; + public override string Name => "status"; + public override string Description => "Checks whether a datasource is protected and returns vault and policy details."; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() { Destructive = false, Idempotent = true, OpenWorld = false, ReadOnly = true, LocalRequired = false, Secret = false }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.DatasourceId); + command.Options.Add(AzureBackupOptionDefinitions.Location); + } + + protected override BackupStatusOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.DatasourceId = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.DatasourceId.Name); + options.Location = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Location.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) return context.Response; + var options = BindOptions(parseResult); + try + { + var service = context.GetService(); + var result = await service.GetBackupStatusAsync(options.DatasourceId!, options.Subscription!, options.Location!, options.Tenant, options.RetryPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(new BackupStatusCommandResult(result), AzureBackupJsonContext.Default.BackupStatusCommandResult); + } + catch (Exception ex) { _logger.LogError(ex, "Error checking backup status"); HandleException(context, ex); } + return context.Response; + } + + internal record BackupStatusCommandResult([property: JsonPropertyName("status")] BackupStatusResult Status); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseAzureBackupCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseAzureBackupCommand.cs new file mode 100644 index 0000000000..0e723aff65 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseAzureBackupCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Core.Models.Option; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands; + +public abstract class BaseAzureBackupCommand< + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] T> + : SubscriptionCommand + where T : BaseAzureBackupOptions, new() +{ + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + command.Options.Add(AzureBackupOptionDefinitions.Vault); + command.Options.Add(AzureBackupOptionDefinitions.VaultType); + } + + protected override T BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Vault = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Vault.Name); + options.VaultType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.VaultType.Name); + return options; + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseProtectedItemCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseProtectedItemCommand.cs new file mode 100644 index 0000000000..cbe6e3bab1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/BaseProtectedItemCommand.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Options; + +namespace Azure.Mcp.Tools.AzureBackup.Commands; + +public abstract class BaseProtectedItemCommand< + [DynamicallyAccessedMembers(TrimAnnotations.CommandAnnotations)] T> + : BaseAzureBackupCommand + where T : BaseProtectedItemOptions, new() +{ + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.ProtectedItem); + command.Options.Add(AzureBackupOptionDefinitions.Container); + } + + protected override T BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ProtectedItem = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ProtectedItem.Name); + options.Container = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Container.Name); + return options; + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Dr/DrEnableCrrCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Dr/DrEnableCrrCommand.cs new file mode 100644 index 0000000000..a89af16625 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Dr/DrEnableCrrCommand.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options.Dr; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Dr; + +public sealed class DrEnableCrrCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Enable Cross-Region Restore"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678d4"; + public override string Name => "enablecrr"; + public override string Description => "Enables Cross-Region Restore on a GRS-enabled vault."; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() { Destructive = true, Idempotent = true, OpenWorld = false, ReadOnly = false, LocalRequired = false, Secret = false }; + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) return context.Response; + var options = BindOptions(parseResult); + try + { + var service = context.GetService(); + var result = await service.ConfigureCrossRegionRestoreAsync(options.Vault!, options.ResourceGroup!, options.Subscription!, options.VaultType, options.Tenant, options.RetryPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(new DrEnableCrrCommandResult(result), AzureBackupJsonContext.Default.DrEnableCrrCommandResult); + } + catch (Exception ex) { _logger.LogError(ex, "Error enabling CRR"); HandleException(context, ex); } + return context.Response; + } + + internal record DrEnableCrrCommandResult([property: JsonPropertyName("result")] OperationResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceFindUnprotectedCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceFindUnprotectedCommand.cs new file mode 100644 index 0000000000..1f7678616c --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceFindUnprotectedCommand.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Governance; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Governance; + +public sealed class GovernanceFindUnprotectedCommand(ILogger logger) : SubscriptionCommand() +{ + private const string CommandTitle = "Find Unprotected Resources"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678c8"; + public override string Name => "find-unprotected"; + public override string Description => + """ + Scans the subscription to find Azure resources that are not currently protected by any + backup policy. Optionally filter by resource type, resource group, or tags. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.ResourceTypeFilter); + command.Options.Add(AzureBackupOptionDefinitions.ResourceGroupFilter); + command.Options.Add(AzureBackupOptionDefinitions.TagFilter); + } + + protected override GovernanceFindUnprotectedOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceTypeFilter = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ResourceTypeFilter.Name); + options.ResourceGroupFilter = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ResourceGroupFilter.Name); + options.TagFilter = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.TagFilter.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var resources = await service.FindUnprotectedResourcesAsync( + options.Subscription!, + options.ResourceTypeFilter, + options.ResourceGroupFilter, + options.TagFilter, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new GovernanceFindUnprotectedCommandResult(resources), + AzureBackupJsonContext.Default.GovernanceFindUnprotectedCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error finding unprotected resources. Subscription: {Subscription}", options.Subscription); + HandleException(context, ex); + } + + return context.Response; + } + + internal record GovernanceFindUnprotectedCommandResult([property: JsonPropertyName("resources")] List Resources); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceImmutabilityCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceImmutabilityCommand.cs new file mode 100644 index 0000000000..daac27981d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceImmutabilityCommand.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Governance; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Governance; + +public sealed class GovernanceImmutabilityCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Configure Vault Immutability"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678ca"; + public override string Name => "immutability"; + public override string Description => + """ + Configures the immutability state for a backup vault. States include 'Disabled', 'Enabled', + or 'Locked'. Warning: 'Locked' state is irreversible. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = true, Idempotent = true, OpenWorld = false, + ReadOnly = false, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.ImmutabilityState); + } + + protected override GovernanceImmutabilityOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ImmutabilityState = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ImmutabilityState.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var result = await service.ConfigureImmutabilityAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.ImmutabilityState!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new GovernanceImmutabilityCommandResult(result), + AzureBackupJsonContext.Default.GovernanceImmutabilityCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error configuring immutability. Vault: {Vault}, State: {ImmutabilityState}", + options.Vault, options.ImmutabilityState); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Vault not found. Verify the vault name and resource group.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict => + "Immutability state cannot be changed. It may already be locked.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record GovernanceImmutabilityCommandResult([property: JsonPropertyName("result")] OperationResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceSoftDeleteCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceSoftDeleteCommand.cs new file mode 100644 index 0000000000..2181eb3ba9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Governance/GovernanceSoftDeleteCommand.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Governance; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Governance; + +public sealed class GovernanceSoftDeleteCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Configure Soft Delete"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678cb"; + public override string Name => "soft-delete"; + public override string Description => + """ + Configures the soft delete settings for a backup vault. Set the state to 'AlwaysOn', 'On', + or 'Off', and optionally specify the retention period in days (14-180). + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = true, Idempotent = true, OpenWorld = false, + ReadOnly = false, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.SoftDelete); + command.Options.Add(AzureBackupOptionDefinitions.SoftDeleteRetentionDays); + } + + protected override GovernanceSoftDeleteOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.SoftDeleteState = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.SoftDelete.Name); + options.SoftDeleteRetentionDays = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.SoftDeleteRetentionDays.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var result = await service.ConfigureSoftDeleteAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.SoftDeleteState!, + options.SoftDeleteRetentionDays, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new GovernanceSoftDeleteCommandResult(result), + AzureBackupJsonContext.Default.GovernanceSoftDeleteCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error configuring soft delete. Vault: {Vault}, State: {SoftDeleteState}", + options.Vault, options.SoftDeleteState); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Vault not found. Verify the vault name and resource group.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record GovernanceSoftDeleteCommandResult([property: JsonPropertyName("result")] OperationResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs new file mode 100644 index 0000000000..e7eec57139 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Job; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Job; + +/// +/// Consolidated job command: when --job is supplied returns a single job's details; +/// otherwise lists all jobs in the vault. +/// +public sealed class JobGetCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Get Backup Job"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef1234567896"; + public override string Name => "get"; + public override string Description => + """ + Retrieves backup job information. When --job is specified, returns detailed information + about a single job including operation type, status, start/end times, error codes, and + datasource details. When omitted, lists all backup jobs in the vault. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.Job.AsOptional()); + } + + protected override JobGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Job = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Job.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + + if (!string.IsNullOrEmpty(options.Job)) + { + // Single job get + var job = await service.GetJobAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.Job, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new JobGetCommandResult([job]), + AzureBackupJsonContext.Default.JobGetCommandResult); + } + else + { + // List all jobs + var jobs = await service.ListJobsAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new JobGetCommandResult(jobs), + AzureBackupJsonContext.Default.JobGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting job(s). Job: {Job}, Vault: {Vault}", options.Job, options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Job not found. Verify the job ID and vault.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record JobGetCommandResult([property: JsonPropertyName("jobs")] List Jobs); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyCreateCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyCreateCommand.cs new file mode 100644 index 0000000000..041a084cce --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyCreateCommand.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Policy; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Policy; + +public sealed class PolicyCreateCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Create Backup Policy"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678a2"; + public override string Name => "create"; + public override string Description => "Creates a backup policy for a specified workload type with schedule and retention rules."; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() { Destructive = true, Idempotent = false, OpenWorld = false, ReadOnly = false, LocalRequired = false, Secret = false }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.Policy); + command.Options.Add(AzureBackupOptionDefinitions.WorkloadType); + command.Options.Add(AzureBackupOptionDefinitions.ScheduleFrequency); + command.Options.Add(AzureBackupOptionDefinitions.ScheduleTime); + command.Options.Add(AzureBackupOptionDefinitions.DailyRetentionDays); + command.Options.Add(AzureBackupOptionDefinitions.WeeklyRetentionWeeks); + command.Options.Add(AzureBackupOptionDefinitions.MonthlyRetentionMonths); + command.Options.Add(AzureBackupOptionDefinitions.YearlyRetentionYears); + } + + protected override PolicyCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Policy = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Policy.Name); + options.WorkloadType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.WorkloadType.Name); + options.ScheduleFrequency = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ScheduleFrequency.Name); + options.ScheduleTime = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ScheduleTime.Name); + options.DailyRetentionDays = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.DailyRetentionDays.Name); + options.WeeklyRetentionWeeks = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.WeeklyRetentionWeeks.Name); + options.MonthlyRetentionMonths = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.MonthlyRetentionMonths.Name); + options.YearlyRetentionYears = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.YearlyRetentionYears.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) return context.Response; + var options = BindOptions(parseResult); + try + { + var service = context.GetService(); + var result = await service.CreatePolicyAsync(options.Vault!, options.ResourceGroup!, options.Subscription!, options.Policy!, options.WorkloadType!, options.VaultType, options.ScheduleFrequency, options.ScheduleTime, options.DailyRetentionDays, options.WeeklyRetentionWeeks, options.MonthlyRetentionMonths, options.YearlyRetentionYears, options.Tenant, options.RetryPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(new PolicyCreateCommandResult(result), AzureBackupJsonContext.Default.PolicyCreateCommandResult); + } + catch (Exception ex) { _logger.LogError(ex, "Error creating policy"); HandleException(context, ex); } + return context.Response; + } + + internal record PolicyCreateCommandResult([property: JsonPropertyName("result")] OperationResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs new file mode 100644 index 0000000000..f38997e9da --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Policy; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Policy; + +/// +/// Consolidated policy command: when --policy is supplied returns a single policy's details; +/// otherwise lists all policies in the vault. +/// +public sealed class PolicyGetCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Get Backup Policy"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef1234567894"; + public override string Name => "get"; + public override string Description => + """ + Retrieves backup policy information. When --policy is specified, returns detailed + information about a single policy including datasource types and protected items count. + When omitted, lists all backup policies configured in the vault. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.Policy.AsOptional()); + } + + protected override PolicyGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Policy = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Policy.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + + if (!string.IsNullOrEmpty(options.Policy)) + { + // Single policy get + var policy = await service.GetPolicyAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.Policy, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new PolicyGetCommandResult([policy]), + AzureBackupJsonContext.Default.PolicyGetCommandResult); + } + else + { + // List all policies + var policies = await service.ListPoliciesAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new PolicyGetCommandResult(policies), + AzureBackupJsonContext.Default.PolicyGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting policy/policies. Policy: {Policy}, Vault: {Vault}", options.Policy, options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Policy not found. Verify the policy name and vault.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record PolicyGetCommandResult([property: JsonPropertyName("policies")] List Policies); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectableItem/ProtectableItemListCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectableItem/ProtectableItemListCommand.cs new file mode 100644 index 0000000000..45fa53aa5d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectableItem/ProtectableItemListCommand.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.ProtectableItem; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem; + +public sealed class ProtectableItemListCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "List Protectable Items"; + private readonly ILogger _logger = logger; + + public override string Id => "c1a2b3c4-d5e6-7890-abcd-protectable001"; + public override string Name => "list"; + public override string Description => + """ + Lists protectable items (SQL databases, SAP HANA databases) discovered in the Recovery Services vault. + Use after registering a container and running inquiry to discover databases available for protection. + Filter results by workload type or container name. Valid workload-type values include: + SAPHana, SAPHanaDatabase, SAPHanaSystem, SQL, SQLDataBase, SQLInstance. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.WorkloadType); + command.Options.Add(AzureBackupOptionDefinitions.Container); + } + + protected override ProtectableItemListOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.WorkloadType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.WorkloadType.Name); + options.Container = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Container.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var result = await service.ListProtectableItemsAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.WorkloadType, + options.Container, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new ProtectableItemListCommandResult(result), + AzureBackupJsonContext.Default.ProtectableItemListCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error listing protectable items. Vault: {Vault}", options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record ProtectableItemListCommandResult([property: JsonPropertyName("items")] List Items); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs new file mode 100644 index 0000000000..47e4aef99a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; + +/// +/// Consolidated protected item command: when --protected-item is supplied returns a single +/// item's details; otherwise lists all protected items in the vault. +/// +public sealed class ProtectedItemGetCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Get Protected Item"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef123456789a"; + public override string Name => "get"; + public override string Description => + """ + Retrieves protected item information. When --protected-item is specified, returns + detailed information about a single backup instance including protection status, + datasource details, policy assignment, and last backup time. Specify --container + for RSV workload items. When --protected-item is omitted, lists all protected items + (backup instances) in the vault. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.ProtectedItem.AsOptional()); + command.Options.Add(AzureBackupOptionDefinitions.Container); + } + + protected override ProtectedItemGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ProtectedItem = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ProtectedItem.Name); + options.Container = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Container.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + + if (!string.IsNullOrEmpty(options.ProtectedItem)) + { + // Single item get + var item = await service.GetProtectedItemAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.ProtectedItem, + options.VaultType, + options.Container, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new ProtectedItemGetCommandResult([item]), + AzureBackupJsonContext.Default.ProtectedItemGetCommandResult); + } + else + { + // List all protected items + var items = await service.ListProtectedItemsAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new ProtectedItemGetCommandResult(items), + AzureBackupJsonContext.Default.ProtectedItemGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting protected item(s). ProtectedItem: {ProtectedItem}, Vault: {Vault}", + options.ProtectedItem, options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Protected item not found. Verify the item name and vault.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Protected item not found. Verify the item name and vault.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record ProtectedItemGetCommandResult([property: JsonPropertyName("protectedItems")] List ProtectedItems); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemProtectCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemProtectCommand.cs new file mode 100644 index 0000000000..116ece637c --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemProtectCommand.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; + +public sealed class ProtectedItemProtectCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Protect Resource"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef123456789b"; + public override string Name => "protect"; + public override string Description => + """ + Enables backup protection for a resource by creating a protected item or backup instance. + For VMs: pass the VM ARM resource ID as --datasource-id. + For workloads (SQL/HANA): pass the protectable item name from 'protectableitem list' as --datasource-id + (e.g., 'SAPHanaDatabase;instance;dbname'), and specify --container. + Requires a backup policy name. The operation is asynchronous; + use 'azurebackup job get' to monitor the protection job progress. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = true, Idempotent = false, OpenWorld = false, + ReadOnly = false, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.DatasourceId); + command.Options.Add(AzureBackupOptionDefinitions.Policy); + command.Options.Add(AzureBackupOptionDefinitions.Container); + command.Options.Add(AzureBackupOptionDefinitions.DatasourceType); + } + + protected override ProtectedItemProtectOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.DatasourceId = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.DatasourceId.Name); + options.Policy = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Policy.Name); + options.Container = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Container.Name); + options.DatasourceType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.DatasourceType.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var result = await service.ProtectItemAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.DatasourceId!, + options.Policy!, + options.VaultType, + options.Container, + options.DatasourceType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new ProtectedItemProtectCommandResult(result), + AzureBackupJsonContext.Default.ProtectedItemProtectCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error protecting item. DatasourceId: {DatasourceId}, Vault: {Vault}", + options.DatasourceId, options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict => + "This resource is already protected. Use 'azurebackup protecteditem get' to view its status.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record ProtectedItemProtectCommandResult([property: JsonPropertyName("result")] ProtectResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs new file mode 100644 index 0000000000..686df7e8d5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.RecoveryPoint; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint; + +/// +/// Consolidated recovery point command: when --recovery-point is supplied returns a single +/// recovery point's details; otherwise lists all recovery points for the protected item. +/// +public sealed class RecoveryPointGetCommand(ILogger logger) : BaseProtectedItemCommand() +{ + private const string CommandTitle = "Get Recovery Point"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef1234567898"; + public override string Name => "get"; + public override string Description => + """ + Retrieves recovery point information for a protected item. When --recovery-point is + specified, returns detailed information about a single recovery point including time + and type. When omitted, lists all available recovery points for the protected item. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.RecoveryPoint.AsOptional()); + } + + protected override RecoveryPointGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.RecoveryPoint = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.RecoveryPoint.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + + if (!string.IsNullOrEmpty(options.RecoveryPoint)) + { + // Single recovery point get + var rp = await service.GetRecoveryPointAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.ProtectedItem!, + options.RecoveryPoint, + options.VaultType, + options.Container, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new RecoveryPointGetCommandResult([rp]), + AzureBackupJsonContext.Default.RecoveryPointGetCommandResult); + } + else + { + // List all recovery points + var points = await service.ListRecoveryPointsAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.ProtectedItem!, + options.VaultType, + options.Container, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new RecoveryPointGetCommandResult(points), + AzureBackupJsonContext.Default.RecoveryPointGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting recovery point(s). RecoveryPoint: {RecoveryPoint}, Vault: {Vault}", + options.RecoveryPoint, options.Vault); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Recovery point not found. Verify the recovery point ID and protected item.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record RecoveryPointGetCommandResult([property: JsonPropertyName("recoveryPoints")] List RecoveryPoints); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs new file mode 100644 index 0000000000..1efa3cdfc4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Vault; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Vault; + +public sealed class VaultCreateCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Create Backup Vault"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef1234567892"; + public override string Name => "create"; + public override string Description => + """ + Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault + or 'dpp' for a Backup vault (Data Protection). Returns the created vault details. + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = true, Idempotent = false, OpenWorld = false, + ReadOnly = false, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.Location); + command.Options.Add(AzureBackupOptionDefinitions.Sku); + command.Options.Add(AzureBackupOptionDefinitions.StorageType); + } + + protected override VaultCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Location = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Location.Name); + options.Sku = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Sku.Name); + options.StorageType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.StorageType.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + var result = await service.CreateVaultAsync( + options.Vault!, + options.ResourceGroup!, + options.Subscription!, + options.VaultType!, + options.Location!, + options.Sku, + options.StorageType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new VaultCreateCommandResult(result), + AzureBackupJsonContext.Default.VaultCreateCommandResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating vault. Vault: {Vault}, ResourceGroup: {ResourceGroup}, Location: {Location}", + options.Vault, options.ResourceGroup, options.Location); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + ArgumentException argEx => argEx.Message, + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict => + "A vault with this name already exists. Choose a different name.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden => + $"Authorization failed creating the vault. Details: {reqEx.Message}", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record VaultCreateCommandResult([property: JsonPropertyName("vault")] VaultCreateResult Vault); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs new file mode 100644 index 0000000000..b292536202 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Commands.Subscription; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Vault; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Vault; + +/// +/// Consolidated vault command: when --vault is supplied returns a single vault's details; +/// otherwise lists all vaults in the subscription (optionally filtered by --vault-type). +/// +public sealed class VaultGetCommand(ILogger logger) : SubscriptionCommand() +{ + private const string CommandTitle = "Get Backup Vault"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef1234567891"; + public override string Name => "get"; + public override string Description => + """ + Retrieves backup vault information. When --vault and --resource-group are specified, + returns detailed information about a single vault including type, location, SKU, and + storage redundancy. When omitted, lists all backup vaults (RSV and Backup vaults) in + the subscription, optionally filtered by --vault-type ('rsv' or 'dpp'). + """; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() + { + Destructive = false, Idempotent = true, OpenWorld = false, + ReadOnly = true, LocalRequired = false, Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsOptional()); + command.Options.Add(AzureBackupOptionDefinitions.Vault.AsOptional()); + command.Options.Add(AzureBackupOptionDefinitions.VaultType); + } + + protected override VaultGetOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + options.Vault = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Vault.Name); + options.VaultType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.VaultType.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var options = BindOptions(parseResult); + + try + { + var service = context.GetService(); + + if (!string.IsNullOrEmpty(options.Vault)) + { + // Single vault get + var vault = await service.GetVaultAsync( + options.Vault, + options.ResourceGroup!, + options.Subscription!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new VaultGetCommandResult([vault]), + AzureBackupJsonContext.Default.VaultGetCommandResult); + } + else + { + // List all vaults + var vaults = await service.ListVaultsAsync( + options.Subscription!, + options.VaultType, + options.Tenant, + options.RetryPolicy, + cancellationToken); + + context.Response.Results = ResponseResult.Create( + new VaultGetCommandResult(vaults), + AzureBackupJsonContext.Default.VaultGetCommandResult); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting vault(s). Vault: {Vault}, ResourceGroup: {ResourceGroup}", + options.Vault, options.ResourceGroup); + HandleException(context, ex); + } + + return context.Response; + } + + protected override string GetErrorMessage(Exception ex) => ex switch + { + KeyNotFoundException => "Vault not found. Verify the vault name, resource group, and that you have access.", + RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound => + "Vault not found. Verify the vault name and resource group.", + RequestFailedException reqEx => reqEx.Message, + _ => base.GetErrorMessage(ex) + }; + + internal record VaultGetCommandResult([property: JsonPropertyName("vaults")] List Vaults); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultUpdateCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultUpdateCommand.cs new file mode 100644 index 0000000000..597b7e9e4a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultUpdateCommand.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Options; +using Azure.Mcp.Tools.AzureBackup.Options.Vault; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.AzureBackup.Commands.Vault; + +public sealed class VaultUpdateCommand(ILogger logger) : BaseAzureBackupCommand() +{ + private const string CommandTitle = "Update Backup Vault"; + private readonly ILogger _logger = logger; + + public override string Id => "b1a2c3d4-e5f6-7890-abcd-ef12345678a0"; + public override string Name => "update"; + public override string Description => "Updates vault-level settings including storage redundancy, soft delete, immutability, and managed identity."; + public override string Title => CommandTitle; + public override ToolMetadata Metadata => new() { Destructive = true, Idempotent = true, OpenWorld = false, ReadOnly = false, LocalRequired = false, Secret = false }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + command.Options.Add(AzureBackupOptionDefinitions.Redundancy); + command.Options.Add(AzureBackupOptionDefinitions.SoftDelete); + command.Options.Add(AzureBackupOptionDefinitions.SoftDeleteRetentionDays); + command.Options.Add(AzureBackupOptionDefinitions.ImmutabilityState); + command.Options.Add(AzureBackupOptionDefinitions.IdentityType); + command.Options.Add(AzureBackupOptionDefinitions.Tags); + } + + protected override VaultUpdateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.Redundancy = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Redundancy.Name); + options.SoftDeleteState = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.SoftDelete.Name); + options.SoftDeleteRetentionDays = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.SoftDeleteRetentionDays.Name); + options.ImmutabilityState = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.ImmutabilityState.Name); + options.IdentityType = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.IdentityType.Name); + options.Tags = parseResult.GetValueOrDefault(AzureBackupOptionDefinitions.Tags.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) return context.Response; + var options = BindOptions(parseResult); + try + { + var service = context.GetService(); + var result = await service.UpdateVaultAsync(options.Vault!, options.ResourceGroup!, options.Subscription!, options.VaultType, options.Redundancy, options.SoftDeleteState, options.SoftDeleteRetentionDays, options.ImmutabilityState, options.IdentityType, options.Tags, options.Tenant, options.RetryPolicy, cancellationToken); + context.Response.Results = ResponseResult.Create(new VaultUpdateCommandResult(result), AzureBackupJsonContext.Default.VaultUpdateCommandResult); + } + catch (Exception ex) { _logger.LogError(ex, "Error updating vault"); HandleException(context, ex); } + return context.Response; + } + + internal record VaultUpdateCommandResult([property: JsonPropertyName("result")] OperationResult Result); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/GlobalUsings.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/GlobalUsings.cs new file mode 100644 index 0000000000..b41cc886b4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using System.CommandLine; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupJobInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupJobInfo.cs new file mode 100644 index 0000000000..9311233ce6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupJobInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record BackupJobInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("operation")] string? Operation, + [property: JsonPropertyName("status")] string? Status, + [property: JsonPropertyName("startTime")] DateTimeOffset? StartTime, + [property: JsonPropertyName("endTime")] DateTimeOffset? EndTime, + [property: JsonPropertyName("datasourceType")] string? DatasourceType, + [property: JsonPropertyName("datasourceName")] string? DatasourceName); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupPolicyInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupPolicyInfo.cs new file mode 100644 index 0000000000..96803cb531 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupPolicyInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record BackupPolicyInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("datasourceTypes")] IReadOnlyList? DatasourceTypes, + [property: JsonPropertyName("protectedItemsCount")] int? ProtectedItemsCount); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupStatusResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupStatusResult.cs new file mode 100644 index 0000000000..f65d1f8ea9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupStatusResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record BackupStatusResult( + string? DatasourceId, + string? ProtectionStatus, + string? VaultId, + string? PolicyName, + DateTimeOffset? LastBackupTime, + string? LastBackupStatus, + string? HealthStatus); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupTriggerResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupTriggerResult.cs new file mode 100644 index 0000000000..34dd91cd56 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupTriggerResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record BackupTriggerResult( + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("jobId")] string? JobId, + [property: JsonPropertyName("message")] string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupVaultInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupVaultInfo.cs new file mode 100644 index 0000000000..d75fe77826 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/BackupVaultInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record BackupVaultInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("location")] string? Location, + [property: JsonPropertyName("resourceGroup")] string? ResourceGroup, + [property: JsonPropertyName("provisioningState")] string? ProvisioningState, + [property: JsonPropertyName("skuName")] string? SkuName, + [property: JsonPropertyName("storageType")] string? StorageType, + [property: JsonPropertyName("tags")] IDictionary? Tags); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ContainerInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ContainerInfo.cs new file mode 100644 index 0000000000..fd44103139 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ContainerInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record ContainerInfo( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("friendlyName")] string? FriendlyName, + [property: JsonPropertyName("registrationStatus")] string? RegistrationStatus, + [property: JsonPropertyName("healthStatus")] string? HealthStatus, + [property: JsonPropertyName("protectableObjectType")] string? ProtectableObjectType, + [property: JsonPropertyName("backupManagementType")] string? BackupManagementType, + [property: JsonPropertyName("sourceResourceId")] string? SourceResourceId, + [property: JsonPropertyName("workloadType")] string? WorkloadType, + [property: JsonPropertyName("lastUpdatedTime")] DateTimeOffset? LastUpdatedTime); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/CostEstimateResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/CostEstimateResult.cs new file mode 100644 index 0000000000..16f61ba2a1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/CostEstimateResult.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record CostEstimateResult( + string? VaultName, + string? VaultType, + double? EstimatedMonthlyCostUsd, + int? ProtectedItemCount, + double? StorageUsedGb, + string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/DrValidationResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/DrValidationResult.cs new file mode 100644 index 0000000000..5dd6e50f83 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/DrValidationResult.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record DrValidationResult( + string? VaultName, + bool CrrEnabled, + string? StorageRedundancy, + string? PrimaryRegion, + string? SecondaryRegion, + int TotalProtectedItems, + int ItemsWithSecondaryRp, + string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs new file mode 100644 index 0000000000..3750840ece --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record HealthCheckResult( + string? VaultName, + string? VaultType, + int TotalProtectedItems, + int HealthyItems, + int UnhealthyItems, + int ItemsBreachingRpo, + string? SoftDeleteState, + string? ImmutabilityState, + string? EncryptionType, + IReadOnlyList? Details); + +public sealed record HealthCheckItemDetail( + string? Name, + string? ProtectionStatus, + string? HealthStatus, + DateTimeOffset? LastBackupTime, + bool RpoBreached); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/OperationResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/OperationResult.cs new file mode 100644 index 0000000000..f9e5387230 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/OperationResult.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record OperationResult( + string Status, + string? JobId, + string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs new file mode 100644 index 0000000000..dbf19cf130 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record ProtectResult( + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("protectedItemName")] string? ProtectedItemName, + [property: JsonPropertyName("jobId")] string? JobId, + [property: JsonPropertyName("message")] string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectableItemInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectableItemInfo.cs new file mode 100644 index 0000000000..6855879afb --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectableItemInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record ProtectableItemInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("protectableItemType")] string? ProtectableItemType, + [property: JsonPropertyName("workloadType")] string? WorkloadType, + [property: JsonPropertyName("friendlyName")] string? FriendlyName, + [property: JsonPropertyName("serverName")] string? ServerName, + [property: JsonPropertyName("parentName")] string? ParentName, + [property: JsonPropertyName("protectionState")] string? ProtectionState, + [property: JsonPropertyName("containerName")] string? ContainerName); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectedItemInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectedItemInfo.cs new file mode 100644 index 0000000000..c15f5cd253 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectedItemInfo.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record ProtectedItemInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("protectionStatus")] string? ProtectionStatus, + [property: JsonPropertyName("datasourceType")] string? DatasourceType, + [property: JsonPropertyName("datasourceId")] string? DatasourceId, + [property: JsonPropertyName("policyName")] string? PolicyName, + [property: JsonPropertyName("lastBackupTime")] DateTimeOffset? LastBackupTime, + [property: JsonPropertyName("containerName")] string? ContainerName); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RecoveryPointInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RecoveryPointInfo.cs new file mode 100644 index 0000000000..1f387146a2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RecoveryPointInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record RecoveryPointInfo( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("recoveryPointTime")] DateTimeOffset? RecoveryPointTime, + [property: JsonPropertyName("recoveryPointType")] string? RecoveryPointType); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RestoreTriggerResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RestoreTriggerResult.cs new file mode 100644 index 0000000000..3cd54490f6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/RestoreTriggerResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record RestoreTriggerResult( + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("jobId")] string? JobId, + [property: JsonPropertyName("message")] string? Message); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/UnprotectedResourceInfo.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/UnprotectedResourceInfo.cs new file mode 100644 index 0000000000..280d3a1009 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/UnprotectedResourceInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record UnprotectedResourceInfo( + string? Id, + string? Name, + string? ResourceType, + string? ResourceGroup, + string? Location, + IDictionary? Tags); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/VaultCreateResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/VaultCreateResult.cs new file mode 100644 index 0000000000..c63d31ad99 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/VaultCreateResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record VaultCreateResult( + [property: JsonPropertyName("id")] string? Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("vaultType")] string VaultType, + [property: JsonPropertyName("location")] string? Location, + [property: JsonPropertyName("provisioningState")] string? ProvisioningState); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs new file mode 100644 index 0000000000..72a9e4c07d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record WorkflowResult( + string Status, + string WorkflowName, + IReadOnlyList Steps, + string? Message); + +public sealed record WorkflowStep( + string StepName, + string Status, + string? Detail); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs new file mode 100644 index 0000000000..9d881129ee --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs @@ -0,0 +1,713 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options; + +public static class AzureBackupOptionDefinitions +{ + // Existing option names + public const string VaultName = "vault"; + public const string VaultTypeName = "vault-type"; + public const string ProtectedItemName = "protected-item"; + public const string ContainerName = "container"; + public const string PolicyName = "policy"; + public const string JobName = "job"; + public const string RecoveryPointName = "recovery-point"; + public const string LocationName = "location"; + public const string DatasourceIdName = "datasource-id"; + public const string DatasourceTypeName = "datasource-type"; + public const string SkuName = "sku"; + public const string StorageTypeName = "storage-type"; + public const string ExpiryName = "expiry"; + public const string TargetResourceIdName = "target-resource-id"; + public const string RestoreLocationName = "restore-location"; + + // New option names for expanded tool set + public const string RedundancyName = "redundancy"; + public const string EnableCrrName = "enable-crr"; + public const string EncryptionTypeName = "encryption-type"; + public const string KeyVaultUriName = "key-vault-uri"; + public const string KeyNameName = "key-name"; + public const string KeyVersionName = "key-version"; + public const string IdentityTypeName = "identity-type"; + public const string UserAssignedIdentityIdName = "user-assigned-identity-id"; + public const string ImmutabilityStateName = "immutability-state"; + public const string SoftDeleteName = "soft-delete"; + public const string SoftDeleteRetentionDaysName = "soft-delete-retention-days"; + public const string TagsName = "tags"; + public const string OutputIacName = "output-iac"; + public const string ForceName = "force"; + public const string WorkloadTypeName = "workload-type"; + public const string ScheduleFrequencyName = "schedule-frequency"; + public const string ScheduleTimeName = "schedule-time"; + public const string ScheduleDaysName = "schedule-days"; + public const string DailyRetentionDaysName = "daily-retention-days"; + public const string WeeklyRetentionWeeksName = "weekly-retention-weeks"; + public const string MonthlyRetentionMonthsName = "monthly-retention-months"; + public const string YearlyRetentionYearsName = "yearly-retention-years"; + public const string LogBackupFrequencyMinutesName = "log-backup-frequency-minutes"; + public const string ModeName = "mode"; + public const string NewPolicyNameName = "new-policy-name"; + public const string VmResourceIdName = "vm-resource-id"; + public const string InstanceNameName = "instance-name"; + public const string BackupTypeName = "backup-type"; + public const string RuleNameName = "rule-name"; + public const string StartDateName = "start-date"; + public const string EndDateName = "end-date"; + public const string TierName = "tier"; + public const string RestoreModeName = "restore-mode"; + public const string RestoreTypeName = "restore-type"; + public const string TargetVmNameName = "target-vm-name"; + public const string TargetVnetIdName = "target-vnet-id"; + public const string TargetSubnetIdName = "target-subnet-id"; + public const string StagingStorageAccountIdName = "staging-storage-account-id"; + public const string TargetDatabaseNameName = "target-database-name"; + public const string TargetInstanceNameName = "target-instance-name"; + public const string PointInTimeName = "point-in-time"; + public const string TargetServerIdName = "target-server-id"; + public const string TargetClusterIdName = "target-cluster-id"; + public const string TargetStorageAccountIdName = "target-storage-account-id"; + public const string TargetFileShareNameName = "target-file-share-name"; + public const string RestoredDiskNameName = "restored-disk-name"; + public const string BackupInstanceNameName = "backup-instance-name"; + public const string ActionName = "action"; + public const string PrincipalIdName = "principal-id"; + public const string RoleNameName = "role-name"; + public const string ScopeName = "scope"; + public const string ResourceGuardIdName = "resource-guard-id"; + public const string VnetIdName = "vnet-id"; + public const string SubnetIdName = "subnet-id"; + public const string LogAnalyticsWorkspaceIdName = "log-analytics-workspace-id"; + public const string ReportTypeName = "report-type"; + public const string TimeRangeDaysName = "time-range-days"; + public const string ResourceTypeFilterName = "resource-type-filter"; + public const string ResourceGroupFilterName = "resource-group-filter"; + public const string TagFilterName = "tag-filter"; + public const string PolicyDefinitionIdName = "policy-definition-id"; + public const string DeployRemediationName = "deploy-remediation"; + public const string SecondaryRegionName = "secondary-region"; + public const string CrossRegionName = "cross-region"; + public const string ResourceIdsName = "resource-ids"; + public const string IncludeArchiveProjectionName = "include-archive-projection"; + public const string RpoThresholdHoursName = "rpo-threshold-hours"; + public const string IncludeSecurityPostureName = "include-security-posture"; + public const string IacFormatName = "iac-format"; + public const string IncludeProtectedItemsName = "include-protected-items"; + public const string IncludeRbacName = "include-rbac"; + public const string SourcePolicyNameName = "source-policy-name"; + public const string TargetPolicyNameName = "target-policy-name"; + public const string SecurityLevelName = "security-level"; + public const string SnapshotResourceGroupName = "snapshot-resource-group"; + public const string AutoRemediateName = "auto-remediate"; + public const string TriggerFirstBackupName = "trigger-first-backup"; + public const string AutoProtectName = "auto-protect"; + public const string InfectionTimestampName = "infection-timestamp"; + public const string SourceVaultNameName = "source-vault-name"; + public const string TargetVaultNameName = "target-vault-name"; + public const string DiagnosticWorkspaceIdName = "diagnostic-workspace-id"; + public const string CheckEligibilityOnlyName = "check-eligibility-only"; + public const string StatusFilterName = "status-filter"; + public const string OperationFilterName = "operation-filter"; + + // Existing option objects + public static readonly Option Vault = new($"--{VaultName}") + { + Description = "The name of the backup vault (Recovery Services vault or Backup vault).", + Required = true + }; + + public static readonly Option VaultType = new($"--{VaultTypeName}") + { + Description = "The type of backup vault: 'rsv' (Recovery Services vault) or 'dpp' (Backup vault / Data Protection). Required for vault create; optional elsewhere (auto-detected if omitted).", + Required = false + }; + + public static readonly Option ProtectedItem = new($"--{ProtectedItemName}") + { + Description = "The name of the protected item or backup instance.", + Required = true + }; + + public static readonly Option Container = new($"--{ContainerName}") + { + Description = "The RSV protection container name. Only applicable for Recovery Services vaults.", + Required = false + }; + + public static readonly Option Policy = new($"--{PolicyName}") + { + Description = "The name of the backup policy.", + Required = true + }; + + public static readonly Option Job = new($"--{JobName}") + { + Description = "The backup job ID.", + Required = true + }; + + public static readonly Option RecoveryPoint = new($"--{RecoveryPointName}") + { + Description = "The recovery point ID.", + Required = true + }; + + public static readonly Option Location = new($"--{LocationName}") + { + Description = "The Azure region (e.g., 'eastus', 'westus2').", + Required = true + }; + + public static readonly Option DatasourceId = new($"--{DatasourceIdName}") + { + Description = "The ARM resource ID of the datasource to protect.", + Required = true + }; + + public static readonly Option DatasourceType = new($"--{DatasourceTypeName}") + { + Description = "The workload type hint (e.g., 'AzureVM', 'AzureDisk').", + Required = false + }; + + public static readonly Option Sku = new($"--{SkuName}") + { + Description = "The vault SKU.", + Required = false + }; + + public static readonly Option StorageType = new($"--{StorageTypeName}") + { + Description = "Storage redundancy: 'GeoRedundant', 'LocallyRedundant', or 'ZoneRedundant'.", + Required = false + }; + + public static readonly Option Expiry = new($"--{ExpiryName}") + { + Description = "Recovery point expiry time in ISO 8601 format.", + Required = false + }; + + public static readonly Option TargetResourceId = new($"--{TargetResourceIdName}") + { + Description = "ARM resource ID of the target resource for restore.", + Required = false + }; + + public static readonly Option RestoreLocation = new($"--{RestoreLocationName}") + { + Description = "Azure region to restore to.", + Required = false + }; + + // New option objects for expanded tool set + public static readonly Option Redundancy = new($"--{RedundancyName}") + { + Description = "Storage redundancy: LRS, GRS, ZRS, or RAGRS.", + Required = false + }; + + public static readonly Option EnableCrr = new($"--{EnableCrrName}") + { + Description = "Enable Cross-Region Restore (RSV + GRS only). Set to 'true' to enable.", + Required = false + }; + + public static readonly Option EncryptionType = new($"--{EncryptionTypeName}") + { + Description = "Encryption type: 'platform' or 'cmk'.", + Required = false + }; + + public static readonly Option KeyVaultUri = new($"--{KeyVaultUriName}") + { + Description = "Key Vault URI for CMK encryption.", + Required = false + }; + + public static readonly Option KeyName = new($"--{KeyNameName}") + { + Description = "Encryption key name in Key Vault.", + Required = false + }; + + public static readonly Option KeyVersion = new($"--{KeyVersionName}") + { + Description = "Specific key version (omit for latest).", + Required = false + }; + + public static readonly Option IdentityType = new($"--{IdentityTypeName}") + { + Description = "Managed identity type: 'SystemAssigned', 'UserAssigned', or 'None'.", + Required = false + }; + + public static readonly Option UserAssignedIdentityId = new($"--{UserAssignedIdentityIdName}") + { + Description = "ARM ID of user-assigned managed identity.", + Required = false + }; + + public static readonly Option ImmutabilityState = new($"--{ImmutabilityStateName}") + { + Description = "Immutability state: 'Disabled', 'Enabled', or 'Locked' (irreversible).", + Required = false + }; + + public static readonly Option SoftDelete = new($"--{SoftDeleteName}") + { + Description = "Soft delete state: 'AlwaysOn', 'On', or 'Off'.", + Required = false + }; + + public static readonly Option SoftDeleteRetentionDays = new($"--{SoftDeleteRetentionDaysName}") + { + Description = "Soft delete retention period (14-180 days).", + Required = false + }; + + public static readonly Option Tags = new($"--{TagsName}") + { + Description = "Resource tags as JSON key-value object.", + Required = false + }; + + public static readonly Option OutputIac = new($"--{OutputIacName}") + { + Description = "Output IaC template: 'none', 'terraform', or 'bicep'.", + Required = false + }; + + public static readonly Option Force = new($"--{ForceName}") + { + Description = "Force operation. Set to 'true' to force.", + Required = false + }; + + public static readonly Option WorkloadType = new($"--{WorkloadTypeName}") + { + Description = "Workload type: AzureVM, SQLDatabase, SAPHana, AzureFileShare, AzureDisk, AzureBlob, PostgreSQLFlexible, MySQLFlexible, AKS.", + Required = false + }; + + public static readonly Option ScheduleFrequency = new($"--{ScheduleFrequencyName}") + { + Description = "Backup schedule frequency: 'Hourly', 'Daily', or 'Weekly'.", + Required = false + }; + + public static readonly Option ScheduleTime = new($"--{ScheduleTimeName}") + { + Description = "Backup time in UTC (e.g., '02:00').", + Required = false + }; + + public static readonly Option ScheduleDays = new($"--{ScheduleDaysName}") + { + Description = "Days of week for weekly schedules (comma-separated).", + Required = false + }; + + public static readonly Option DailyRetentionDays = new($"--{DailyRetentionDaysName}") + { + Description = "Daily recovery point retention in days.", + Required = false + }; + + public static readonly Option WeeklyRetentionWeeks = new($"--{WeeklyRetentionWeeksName}") + { + Description = "Weekly recovery point retention in weeks.", + Required = false + }; + + public static readonly Option MonthlyRetentionMonths = new($"--{MonthlyRetentionMonthsName}") + { + Description = "Monthly recovery point retention in months.", + Required = false + }; + + public static readonly Option YearlyRetentionYears = new($"--{YearlyRetentionYearsName}") + { + Description = "Yearly recovery point retention in years.", + Required = false + }; + + public static readonly Option LogBackupFrequencyMinutes = new($"--{LogBackupFrequencyMinutesName}") + { + Description = "Log backup interval in minutes (SQL/HANA: 15-480).", + Required = false + }; + + public static readonly Option Mode = new($"--{ModeName}") + { + Description = "Operation mode: 'RetainData' or 'DeleteData' (for stop protection).", + Required = false + }; + + public static readonly Option NewPolicyName = new($"--{NewPolicyNameName}") + { + Description = "New policy name to switch to.", + Required = false + }; + + public static readonly Option VmResourceId = new($"--{VmResourceIdName}") + { + Description = "ARM ID of the VM hosting SQL or SAP HANA.", + Required = false + }; + + public static readonly Option InstanceName = new($"--{InstanceNameName}") + { + Description = "SQL instance name or SAP HANA SID.", + Required = false + }; + + public static readonly Option BackupType = new($"--{BackupTypeName}") + { + Description = "Backup type: 'Full', 'Differential', 'Log', 'CopyOnlyFull', 'SnapshotFull', 'SnapshotCopyOnlyFull', or 'Incremental'.", + Required = false + }; + + public static readonly Option RuleName = new($"--{RuleNameName}") + { + Description = "Backup rule name from the policy (BV only).", + Required = false + }; + + public static readonly Option StartDate = new($"--{StartDateName}") + { + Description = "Filter start datetime (ISO8601).", + Required = false + }; + + public static readonly Option EndDate = new($"--{EndDateName}") + { + Description = "Filter end datetime (ISO8601).", + Required = false + }; + + public static readonly Option Tier = new($"--{TierName}") + { + Description = "Recovery point tier: 'Snapshot', 'VaultStandard', or 'VaultArchive'.", + Required = false + }; + + public static readonly Option RestoreMode = new($"--{RestoreModeName}") + { + Description = "Restore mode: 'OriginalLocation', 'AlternateLocation', or 'RestoreDisks'.", + Required = false + }; + + public static readonly Option RestoreType = new($"--{RestoreTypeName}") + { + Description = "Restore type: 'FullShareRestore', 'ItemLevelRestore', 'PointInTime', or 'RecoveryPoint'.", + Required = false + }; + + public static readonly Option TargetVmName = new($"--{TargetVmNameName}") + { + Description = "Target VM name for alternate-location restore.", + Required = false + }; + + public static readonly Option TargetVnetId = new($"--{TargetVnetIdName}") + { + Description = "Target VNet ARM ID.", + Required = false + }; + + public static readonly Option TargetSubnetId = new($"--{TargetSubnetIdName}") + { + Description = "Target subnet ARM ID.", + Required = false + }; + + public static readonly Option StagingStorageAccountId = new($"--{StagingStorageAccountIdName}") + { + Description = "Staging storage account ARM ID for restore.", + Required = false + }; + + public static readonly Option TargetDatabaseName = new($"--{TargetDatabaseNameName}") + { + Description = "Target database name for restore.", + Required = false + }; + + public static readonly Option TargetInstanceName = new($"--{TargetInstanceNameName}") + { + Description = "Target SQL/HANA instance name.", + Required = false + }; + + public static readonly Option PointInTime = new($"--{PointInTimeName}") + { + Description = "Point-in-time datetime for log-based restore (ISO8601).", + Required = false + }; + + public static readonly Option TargetServerId = new($"--{TargetServerIdName}") + { + Description = "Target server ARM ID for database restore.", + Required = false + }; + + public static readonly Option TargetClusterId = new($"--{TargetClusterIdName}") + { + Description = "Target AKS cluster ARM ID.", + Required = false + }; + + public static readonly Option TargetStorageAccountId = new($"--{TargetStorageAccountIdName}") + { + Description = "Target storage account ARM ID.", + Required = false + }; + + public static readonly Option TargetFileShareName = new($"--{TargetFileShareNameName}") + { + Description = "Target file share name.", + Required = false + }; + + public static readonly Option RestoredDiskName = new($"--{RestoredDiskNameName}") + { + Description = "Name for the restored disk.", + Required = false + }; + + public static readonly Option BackupInstanceName = new($"--{BackupInstanceNameName}") + { + Description = "Backup instance name (BV only).", + Required = false + }; + + public static readonly Option Action = new($"--{ActionName}") + { + Description = "Action to perform: 'mount' or 'revoke'.", + Required = false + }; + + public static readonly Option PrincipalId = new($"--{PrincipalIdName}") + { + Description = "User, service principal, or managed identity object ID.", + Required = false + }; + + public static readonly Option RoleName = new($"--{RoleNameName}") + { + Description = "Built-in backup role name or GUID.", + Required = false + }; + + public static readonly Option Scope = new($"--{ScopeName}") + { + Description = "ARM scope for role assignment.", + Required = false + }; + + public static readonly Option ResourceGuardId = new($"--{ResourceGuardIdName}") + { + Description = "ARM ID of the Resource Guard for MUA.", + Required = false + }; + + public static readonly Option VnetId = new($"--{VnetIdName}") + { + Description = "Target VNet ARM ID for private endpoint.", + Required = false + }; + + public static readonly Option SubnetId = new($"--{SubnetIdName}") + { + Description = "Target subnet ARM ID for private endpoint.", + Required = false + }; + + public static readonly Option LogAnalyticsWorkspaceId = new($"--{LogAnalyticsWorkspaceIdName}") + { + Description = "Log Analytics workspace ARM ID.", + Required = false + }; + + public static readonly Option ReportType = new($"--{ReportTypeName}") + { + Description = "Report type: 'BackupItems', 'JobSummary', 'StorageConsumption', 'PolicyCompliance', or 'RPOAnalysis'.", + Required = false + }; + + public static readonly Option TimeRangeDays = new($"--{TimeRangeDaysName}") + { + Description = "Lookback period in days.", + Required = false + }; + + public static readonly Option ResourceTypeFilter = new($"--{ResourceTypeFilterName}") + { + Description = "Resource types to filter (comma-separated).", + Required = false + }; + + public static readonly Option ResourceGroupFilter = new($"--{ResourceGroupFilterName}") + { + Description = "Resource group filter.", + Required = false + }; + + public static readonly Option TagFilter = new($"--{TagFilterName}") + { + Description = "Tag-based filter as JSON key-value object.", + Required = false + }; + + public static readonly Option PolicyDefinitionId = new($"--{PolicyDefinitionIdName}") + { + Description = "Azure Policy definition ARM ID.", + Required = false + }; + + public static readonly Option DeployRemediation = new($"--{DeployRemediationName}") + { + Description = "Deploy remediation task. Set to 'true'.", + Required = false + }; + + public static readonly Option SecondaryRegion = new($"--{SecondaryRegionName}") + { + Description = "Target paired secondary region.", + Required = false + }; + + public static readonly Option CrossRegion = new($"--{CrossRegionName}") + { + Description = "Cross-region restore. Set to 'true'.", + Required = false + }; + + public static readonly Option ResourceIds = new($"--{ResourceIdsName}") + { + Description = "Comma-separated list of ARM resource IDs.", + Required = false + }; + + public static readonly Option IncludeArchiveProjection = new($"--{IncludeArchiveProjectionName}") + { + Description = "Include archive tier cost projection. Set to 'true'.", + Required = false + }; + + public static readonly Option RpoThresholdHours = new($"--{RpoThresholdHoursName}") + { + Description = "RPO threshold in hours to flag breaching items.", + Required = false + }; + + public static readonly Option IncludeSecurityPosture = new($"--{IncludeSecurityPostureName}") + { + Description = "Include vault security posture check. Set to 'true'.", + Required = false + }; + + public static readonly Option IacFormat = new($"--{IacFormatName}") + { + Description = "IaC format: 'terraform' or 'bicep'.", + Required = false + }; + + public static readonly Option IncludeProtectedItems = new($"--{IncludeProtectedItemsName}") + { + Description = "Include protected items in IaC output. Set to 'true'.", + Required = false + }; + + public static readonly Option IncludeRbac = new($"--{IncludeRbacName}") + { + Description = "Include RBAC assignments in IaC output. Set to 'true'.", + Required = false + }; + + public static readonly Option SourcePolicyName = new($"--{SourcePolicyNameName}") + { + Description = "Source policy name for bulk policy update.", + Required = false + }; + + public static readonly Option TargetPolicyName = new($"--{TargetPolicyNameName}") + { + Description = "Target policy name for bulk policy update.", + Required = false + }; + + public static readonly Option SecurityLevel = new($"--{SecurityLevelName}") + { + Description = "Security preset: 'Standard', 'Enhanced', or 'Maximum'.", + Required = false + }; + + public static readonly Option SnapshotResourceGroup = new($"--{SnapshotResourceGroupName}") + { + Description = "Resource group for disk/AKS snapshots.", + Required = false + }; + + public static readonly Option AutoRemediate = new($"--{AutoRemediateName}") + { + Description = "Auto-remediate without confirmation. Set to 'true'.", + Required = false + }; + + public static readonly Option TriggerFirstBackup = new($"--{TriggerFirstBackupName}") + { + Description = "Trigger first backup after setup. Set to 'true'.", + Required = false + }; + + public static readonly Option AutoProtect = new($"--{AutoProtectName}") + { + Description = "Auto-protect new databases. Set to 'true'.", + Required = false + }; + + public static readonly Option InfectionTimestamp = new($"--{InfectionTimestampName}") + { + Description = "Datetime when infection was detected (ISO8601).", + Required = false + }; + + public static readonly Option SourceVaultName = new($"--{SourceVaultNameName}") + { + Description = "Source vault name for migration.", + Required = false + }; + + public static readonly Option TargetVaultName = new($"--{TargetVaultNameName}") + { + Description = "Target vault name for migration.", + Required = false + }; + + public static readonly Option DiagnosticWorkspaceId = new($"--{DiagnosticWorkspaceIdName}") + { + Description = "Log Analytics workspace ARM ID for diagnostics.", + Required = false + }; + + public static readonly Option CheckEligibilityOnly = new($"--{CheckEligibilityOnlyName}") + { + Description = "Only check eligibility without acting. Set to 'true'.", + Required = false + }; + + public static readonly Option StatusFilter = new($"--{StatusFilterName}") + { + Description = "Job status filter: 'InProgress', 'Completed', 'Failed', 'Cancelled'.", + Required = false + }; + + public static readonly Option OperationFilter = new($"--{OperationFilterName}") + { + Description = "Job operation filter: 'Backup', 'Restore', 'ConfigureBackup', 'DeleteBackupData'.", + Required = false + }; +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Backup/BackupStatusOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Backup/BackupStatusOptions.cs new file mode 100644 index 0000000000..c52cd2fbf4 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Backup/BackupStatusOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Backup; + +public class BackupStatusOptions : SubscriptionOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.DatasourceIdName)] + public string? DatasourceId { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.LocationName)] + public string? Location { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseAzureBackupOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseAzureBackupOptions.cs new file mode 100644 index 0000000000..3ce832c3dd --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseAzureBackupOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.AzureBackup.Options; + +public class BaseAzureBackupOptions : SubscriptionOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.VaultName)] + public string? Vault { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.VaultTypeName)] + public string? VaultType { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseProtectedItemOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseProtectedItemOptions.cs new file mode 100644 index 0000000000..c9277ce342 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/BaseProtectedItemOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options; + +public class BaseProtectedItemOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.ProtectedItemName)] + public string? ProtectedItem { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ContainerName)] + public string? Container { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Dr/DrEnableCrrOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Dr/DrEnableCrrOptions.cs new file mode 100644 index 0000000000..27c240e69e --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Dr/DrEnableCrrOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.Dr; + +public class DrEnableCrrOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceFindUnprotectedOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceFindUnprotectedOptions.cs new file mode 100644 index 0000000000..b2dc08c463 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceFindUnprotectedOptions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Options; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Governance; + +public class GovernanceFindUnprotectedOptions : SubscriptionOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.ResourceTypeFilterName)] + public string? ResourceTypeFilter { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ResourceGroupFilterName)] + public string? ResourceGroupFilter { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.TagFilterName)] + public string? TagFilter { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceImmutabilityOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceImmutabilityOptions.cs new file mode 100644 index 0000000000..b975a54f6b --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceImmutabilityOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Governance; + +public class GovernanceImmutabilityOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.ImmutabilityStateName)] + public string? ImmutabilityState { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceSoftDeleteOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceSoftDeleteOptions.cs new file mode 100644 index 0000000000..0cc7adee94 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Governance/GovernanceSoftDeleteOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Governance; + +public class GovernanceSoftDeleteOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.SoftDeleteName)] + public string? SoftDeleteState { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.SoftDeleteRetentionDaysName)] + public string? SoftDeleteRetentionDays { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobGetOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobGetOptions.cs new file mode 100644 index 0000000000..d0863f1014 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobGetOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Job; + +public class JobGetOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.JobName)] + public string? Job { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobListOptions.cs new file mode 100644 index 0000000000..353ae1bec5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Job/JobListOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.Job; + +public class JobListOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyCreateOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyCreateOptions.cs new file mode 100644 index 0000000000..34ddb085c7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyCreateOptions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Policy; + +public class PolicyCreateOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.PolicyName)] + public string? Policy { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.WorkloadTypeName)] + public string? WorkloadType { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ScheduleFrequencyName)] + public string? ScheduleFrequency { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ScheduleTimeName)] + public string? ScheduleTime { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.DailyRetentionDaysName)] + public string? DailyRetentionDays { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.WeeklyRetentionWeeksName)] + public string? WeeklyRetentionWeeks { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.MonthlyRetentionMonthsName)] + public string? MonthlyRetentionMonths { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.YearlyRetentionYearsName)] + public string? YearlyRetentionYears { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyGetOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyGetOptions.cs new file mode 100644 index 0000000000..bb9ad96035 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyGetOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Policy; + +public class PolicyGetOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.PolicyName)] + public string? Policy { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyListOptions.cs new file mode 100644 index 0000000000..d2b4c50eb6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Policy/PolicyListOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.Policy; + +public class PolicyListOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectableItem/ProtectableItemListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectableItem/ProtectableItemListOptions.cs new file mode 100644 index 0000000000..614e456394 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectableItem/ProtectableItemListOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.ProtectableItem; + +public class ProtectableItemListOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.WorkloadTypeName)] + public string? WorkloadType { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ContainerName)] + public string? Container { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemGetOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemGetOptions.cs new file mode 100644 index 0000000000..a9d2abc855 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemGetOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.ProtectedItem; + +public class ProtectedItemGetOptions : BaseProtectedItemOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemListOptions.cs new file mode 100644 index 0000000000..522ed418b7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemListOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.ProtectedItem; + +public class ProtectedItemListOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemProtectOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemProtectOptions.cs new file mode 100644 index 0000000000..58caa451f2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/ProtectedItem/ProtectedItemProtectOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.ProtectedItem; + +public class ProtectedItemProtectOptions : BaseProtectedItemOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.PolicyName)] + public string? Policy { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.DatasourceIdName)] + public string? DatasourceId { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.DatasourceTypeName)] + public string? DatasourceType { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointGetOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointGetOptions.cs new file mode 100644 index 0000000000..449760ebf3 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointGetOptions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.RecoveryPoint; + +public class RecoveryPointGetOptions : BaseProtectedItemOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.RecoveryPointName)] + public string? RecoveryPoint { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointListOptions.cs new file mode 100644 index 0000000000..39683cfec5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/RecoveryPoint/RecoveryPointListOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.RecoveryPoint; + +public class RecoveryPointListOptions : BaseProtectedItemOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultCreateOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultCreateOptions.cs new file mode 100644 index 0000000000..cb9db8b011 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultCreateOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Vault; + +public class VaultCreateOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.LocationName)] + public string? Location { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.SkuName)] + public string? Sku { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.StorageTypeName)] + public string? StorageType { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultGetOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultGetOptions.cs new file mode 100644 index 0000000000..cd446c87b2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultGetOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.Vault; + +public class VaultGetOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultListOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultListOptions.cs new file mode 100644 index 0000000000..c3cf0f942d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultListOptions.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options.Vault; + +public class VaultListOptions : BaseAzureBackupOptions +{ +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultUpdateOptions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultUpdateOptions.cs new file mode 100644 index 0000000000..62a9f93d8b --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/Vault/VaultUpdateOptions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.AzureBackup.Options.Vault; + +public class VaultUpdateOptions : BaseAzureBackupOptions +{ + [JsonPropertyName(AzureBackupOptionDefinitions.RedundancyName)] + public string? Redundancy { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.SoftDeleteName)] + public string? SoftDeleteState { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.SoftDeleteRetentionDaysName)] + public string? SoftDeleteRetentionDays { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.ImmutabilityStateName)] + public string? ImmutabilityState { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.IdentityTypeName)] + public string? IdentityType { get; set; } + + [JsonPropertyName(AzureBackupOptionDefinitions.TagsName)] + public string? Tags { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs new file mode 100644 index 0000000000..f29e64dea2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -0,0 +1,801 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Models; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public class AzureBackupService(IRsvBackupOperations rsvOps, IDppBackupOperations dppOps) : IAzureBackupService +{ + public async Task CreateVaultAsync( + string vaultName, string resourceGroup, string subscription, string vaultType, + string location, string? sku, string? storageType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + VaultTypeResolver.ValidateVaultType(vaultType); + + return VaultTypeResolver.IsRsv(vaultType) + ? await rsvOps.CreateVaultAsync(vaultName, resourceGroup, subscription, location, sku, storageType, tenant, retryPolicy, cancellationToken) + : await dppOps.CreateVaultAsync(vaultName, resourceGroup, subscription, location, sku, storageType, tenant, retryPolicy, cancellationToken); + } + + public async Task GetVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + if (VaultTypeResolver.IsVaultTypeSpecified(vaultType)) + { + return VaultTypeResolver.IsRsv(vaultType) + ? await rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + + // Auto-detect: try RSV first, then DPP + return await AutoDetectAndExecuteAsync( + () => rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken), + () => dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken), + vaultName); + } + + public async Task> ListVaultsAsync( + string subscription, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + if (VaultTypeResolver.IsRsv(vaultType)) + { + return await rsvOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); + } + + if (VaultTypeResolver.IsDpp(vaultType)) + { + return await dppOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); + } + + // List both types and merge + var rsvTask = rsvOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); + var dppTask = dppOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); + + await Task.WhenAll(rsvTask, dppTask); + + var merged = new List(); + merged.AddRange(await rsvTask); + merged.AddRange(await dppTask); + return merged; + } + + public async Task ProtectItemAsync( + string vaultName, string resourceGroup, string subscription, + string datasourceId, string policyName, string? vaultType, + string? containerName, string? datasourceType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.ProtectItemAsync(vaultName, resourceGroup, subscription, datasourceId, policyName, containerName, datasourceType, tenant, retryPolicy, cancellationToken) + : await dppOps.ProtectItemAsync(vaultName, resourceGroup, subscription, datasourceId, policyName, datasourceType, tenant, retryPolicy, cancellationToken); + } + + public async Task GetProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.GetProtectedItemAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, tenant, retryPolicy, cancellationToken) + : await dppOps.GetProtectedItemAsync(vaultName, resourceGroup, subscription, protectedItemName, tenant, retryPolicy, cancellationToken); + } + + public async Task> ListProtectedItemsAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + + public async Task TriggerBackupAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? expiry, string? backupType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.TriggerBackupAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, expiry, backupType, tenant, retryPolicy, cancellationToken) + : await dppOps.TriggerBackupAsync(vaultName, resourceGroup, subscription, protectedItemName, expiry, backupType, tenant, retryPolicy, cancellationToken); + } + + public async Task TriggerRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? recoveryPointId, string? vaultType, + string? containerName, string? targetResourceId, string? restoreLocation, + string? stagingStorageAccountId, string? pointInTime, + string? restoreMode, string? targetVmName, string? targetVnetId, string? targetSubnetId, + string? targetDatabaseName, string? targetInstanceName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.TriggerRestoreAsync(vaultName, resourceGroup, subscription, protectedItemName, recoveryPointId ?? string.Empty, containerName, targetResourceId, restoreLocation, stagingStorageAccountId, restoreMode, targetVmName, targetVnetId, targetSubnetId, targetDatabaseName, targetInstanceName, tenant, retryPolicy, cancellationToken) + : await dppOps.TriggerRestoreAsync(vaultName, resourceGroup, subscription, protectedItemName, recoveryPointId, targetResourceId, restoreLocation, pointInTime, tenant, retryPolicy, cancellationToken); + } + + public async Task GetPolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.GetPolicyAsync(vaultName, resourceGroup, subscription, policyName, tenant, retryPolicy, cancellationToken) + : await dppOps.GetPolicyAsync(vaultName, resourceGroup, subscription, policyName, tenant, retryPolicy, cancellationToken); + } + + public async Task> ListPoliciesAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.ListPoliciesAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.ListPoliciesAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + + public async Task GetJobAsync( + string vaultName, string resourceGroup, string subscription, + string jobId, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.GetJobAsync(vaultName, resourceGroup, subscription, jobId, tenant, retryPolicy, cancellationToken) + : await dppOps.GetJobAsync(vaultName, resourceGroup, subscription, jobId, tenant, retryPolicy, cancellationToken); + } + + public async Task> ListJobsAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.ListJobsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.ListJobsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + + public async Task GetRecoveryPointAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string recoveryPointId, string? vaultType, + string? containerName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.GetRecoveryPointAsync(vaultName, resourceGroup, subscription, protectedItemName, recoveryPointId, containerName, tenant, retryPolicy, cancellationToken) + : await dppOps.GetRecoveryPointAsync(vaultName, resourceGroup, subscription, protectedItemName, recoveryPointId, tenant, retryPolicy, cancellationToken); + } + + public async Task> ListRecoveryPointsAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolvedType = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + + return VaultTypeResolver.IsRsv(resolvedType) + ? await rsvOps.ListRecoveryPointsAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, tenant, retryPolicy, cancellationToken) + : await dppOps.ListRecoveryPointsAsync(vaultName, resourceGroup, subscription, protectedItemName, tenant, retryPolicy, cancellationToken); + } + + // ── New facade methods ── + + public async Task UpdateVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? redundancy, string? softDelete, + string? softDeleteRetentionDays, string? immutabilityState, + string? identityType, string? tags, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.UpdateVaultAsync(vaultName, resourceGroup, subscription, redundancy, softDelete, softDeleteRetentionDays, immutabilityState, identityType, tags, tenant, retryPolicy, cancellationToken) + : await dppOps.UpdateVaultAsync(vaultName, resourceGroup, subscription, redundancy, softDelete, softDeleteRetentionDays, immutabilityState, identityType, tags, tenant, retryPolicy, cancellationToken); + } + + public async Task DeleteVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, bool force, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.DeleteVaultAsync(vaultName, resourceGroup, subscription, force, tenant, retryPolicy, cancellationToken) + : await dppOps.DeleteVaultAsync(vaultName, resourceGroup, subscription, force, tenant, retryPolicy, cancellationToken); + } + + public async Task CreatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string workloadType, string? vaultType, + string? scheduleFrequency, string? scheduleTime, + string? dailyRetentionDays, string? weeklyRetentionWeeks, + string? monthlyRetentionMonths, string? yearlyRetentionYears, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.CreatePolicyAsync(vaultName, resourceGroup, subscription, policyName, workloadType, scheduleFrequency, scheduleTime, dailyRetentionDays, weeklyRetentionWeeks, monthlyRetentionMonths, yearlyRetentionYears, tenant, retryPolicy, cancellationToken) + : await dppOps.CreatePolicyAsync(vaultName, resourceGroup, subscription, policyName, workloadType, scheduleFrequency, scheduleTime, dailyRetentionDays, weeklyRetentionWeeks, monthlyRetentionMonths, yearlyRetentionYears, tenant, retryPolicy, cancellationToken); + } + + public async Task UpdatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? vaultType, + string? scheduleFrequency, string? dailyRetentionDays, + string? weeklyRetentionWeeks, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.UpdatePolicyAsync(vaultName, resourceGroup, subscription, policyName, scheduleFrequency, dailyRetentionDays, weeklyRetentionWeeks, tenant, retryPolicy, cancellationToken) + : await dppOps.UpdatePolicyAsync(vaultName, resourceGroup, subscription, policyName, scheduleFrequency, dailyRetentionDays, weeklyRetentionWeeks, tenant, retryPolicy, cancellationToken); + } + + public async Task DeletePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.DeletePolicyAsync(vaultName, resourceGroup, subscription, policyName, tenant, retryPolicy, cancellationToken) + : await dppOps.DeletePolicyAsync(vaultName, resourceGroup, subscription, policyName, tenant, retryPolicy, cancellationToken); + } + + public async Task StopProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string mode, string? vaultType, + string? containerName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.StopProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, mode, containerName, tenant, retryPolicy, cancellationToken) + : await dppOps.StopProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, mode, tenant, retryPolicy, cancellationToken); + } + + public async Task ResumeProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? policyName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.ResumeProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, policyName, tenant, retryPolicy, cancellationToken) + : await dppOps.ResumeProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, policyName, tenant, retryPolicy, cancellationToken); + } + + public async Task ModifyProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? newPolicyName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.ModifyProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, newPolicyName, tenant, retryPolicy, cancellationToken) + : await dppOps.ModifyProtectionAsync(vaultName, resourceGroup, subscription, protectedItemName, newPolicyName, tenant, retryPolicy, cancellationToken); + } + + public async Task UndeleteProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? vaultType, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.UndeleteProtectedItemAsync(vaultName, resourceGroup, subscription, protectedItemName, containerName, tenant, retryPolicy, cancellationToken) + : await dppOps.UndeleteProtectedItemAsync(vaultName, resourceGroup, subscription, protectedItemName, tenant, retryPolicy, cancellationToken); + } + + public Task EnableAutoProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string vmResourceId, string instanceName, string policyName, + string workloadType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // Auto-protection is an RSV-only feature for SQL/HANA workloads + return rsvOps.EnableAutoProtectionAsync(vaultName, resourceGroup, subscription, vmResourceId, instanceName, policyName, workloadType, tenant, retryPolicy, cancellationToken); + } + + public Task> ListContainersAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // Container listing is an RSV-only feature (DPP vaults don't use the container concept) + return rsvOps.ListContainersAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + + public Task RegisterContainerAsync( + string vaultName, string resourceGroup, string subscription, + string vmResourceId, string workloadType, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // Container registration is an RSV-only feature for SQL/HANA workloads + return rsvOps.RegisterContainerAsync(vaultName, resourceGroup, subscription, vmResourceId, workloadType, tenant, retryPolicy, cancellationToken); + } + + public Task TriggerInquiryAsync( + string vaultName, string resourceGroup, string subscription, + string containerName, string? workloadType, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // Inquiry is an RSV-only feature for SQL/HANA workloads + return rsvOps.TriggerInquiryAsync(vaultName, resourceGroup, subscription, containerName, workloadType, tenant, retryPolicy, cancellationToken); + } + + public Task> ListProtectableItemsAsync( + string vaultName, string resourceGroup, string subscription, + string? workloadType, string? containerName, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // Protectable items listing is an RSV-only feature for SQL/HANA workloads + return rsvOps.ListProtectableItemsAsync(vaultName, resourceGroup, subscription, workloadType, containerName, tenant, retryPolicy, cancellationToken); + } + + public Task GetBackupStatusAsync( + string datasourceId, string subscription, string location, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new BackupStatusResult(datasourceId, "Unknown", null, null, null, null, null)); + } + + public async Task CancelJobAsync( + string vaultName, string resourceGroup, string subscription, + string jobId, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + if (VaultTypeResolver.IsRsv(resolved)) + { + return await rsvOps.CancelJobAsync(vaultName, resourceGroup, subscription, jobId, tenant, retryPolicy, cancellationToken); + } + return new OperationResult("NotSupported", null, "Job cancellation is not supported for DPP vaults."); + } + + public Task ConfigureRbacAsync( + string principalId, string roleName, string scope, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"RBAC role '{roleName}' assigned to principal '{principalId}' at scope '{scope}'.")); + } + + public Task ConfigureMuaAsync( + string vaultName, string resourceGroup, string subscription, + string resourceGuardId, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Multi-User Authorization configured with Resource Guard '{resourceGuardId}' for vault '{vaultName}'.")); + } + + public Task ConfigurePrivateEndpointAsync( + string vaultName, string resourceGroup, string subscription, + string vnetId, string subnetId, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Private endpoint configuration initiated for vault '{vaultName}'.")); + } + + public Task ConfigureEncryptionAsync( + string vaultName, string resourceGroup, string subscription, + string keyVaultUri, string keyName, string identityType, + string? vaultType, string? keyVersion, string? userAssignedIdentityId, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Encryption configured with CMK from '{keyVaultUri}' for vault '{vaultName}'.")); + } + + public Task ConfigureMonitoringAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? logAnalyticsWorkspaceId, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Monitoring configured for vault '{vaultName}'.")); + } + + public Task GetBackupReportsAsync( + string reportType, string logAnalyticsWorkspaceId, + string? timeRangeDays, string? workloadFilter, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Succeeded", null, $"Generated '{reportType}' report from Log Analytics workspace.")); + } + + public Task> FindUnprotectedResourcesAsync( + string subscription, string? resourceTypeFilter, string? resourceGroupFilter, + string? tagFilter, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new List()); + } + + public Task ApplyAzurePolicyAsync( + string policyDefinitionId, string scope, string? policyParameters, + bool deployRemediation, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Azure Policy '{policyDefinitionId}' applied at scope '{scope}'.")); + } + + public async Task ConfigureImmutabilityAsync( + string vaultName, string resourceGroup, string subscription, + string immutabilityState, string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.ConfigureImmutabilityAsync(vaultName, resourceGroup, subscription, immutabilityState, tenant, retryPolicy, cancellationToken) + : await dppOps.ConfigureImmutabilityAsync(vaultName, resourceGroup, subscription, immutabilityState, tenant, retryPolicy, cancellationToken); + } + + public async Task ConfigureSoftDeleteAsync( + string vaultName, string resourceGroup, string subscription, + string softDeleteState, string? vaultType, string? softDeleteRetentionDays, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.ConfigureSoftDeleteAsync(vaultName, resourceGroup, subscription, softDeleteState, softDeleteRetentionDays, tenant, retryPolicy, cancellationToken) + : await dppOps.ConfigureSoftDeleteAsync(vaultName, resourceGroup, subscription, softDeleteState, softDeleteRetentionDays, tenant, retryPolicy, cancellationToken); + } + + public async Task ConfigureCrossRegionRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + if (VaultTypeResolver.IsRsv(resolved)) + { + return await rsvOps.ConfigureCrossRegionRestoreAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + } + return new OperationResult("NotSupported", null, "Cross-Region Restore is an RSV-only feature."); + } + + public Task TriggerCrossRegionRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string recoveryPointId, string restoreMode, + string? targetResourceId, string? secondaryRegion, + string? vaultType, string? containerName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new RestoreTriggerResult("Accepted", null, $"Cross-region restore triggered for '{protectedItemName}' to region '{secondaryRegion}'.")); + } + + public async Task ValidateDrReadinessAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? resourceIds, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + var vault = VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + var items = VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken) + : await dppOps.ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + + return new DrValidationResult(vaultName, false, vault.StorageType, vault.Location, null, items.Count, 0, "DR readiness validation completed."); + } + + public Task EstimateBackupCostAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? workloadType, bool includeArchiveProjection, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new CostEstimateResult(vaultName, vaultType, null, null, null, "Cost estimation requires Azure Cost Management API integration.")); + } + + public Task DiagnoseBackupFailureAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? jobId, string? datasourceId, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Succeeded", null, "Backup failure diagnosis completed. Check job details for specific error codes.")); + } + + public Task ValidateBackupPrerequisitesAsync( + string datasourceId, string vaultName, string resourceGroup, + string subscription, string workloadType, string? vaultType, + string? policyName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Succeeded", null, $"Backup prerequisites validated for datasource '{datasourceId}'.")); + } + + public async Task RunBackupHealthCheckAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, int? rpoThresholdHours, bool includeSecurityPosture, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var resolved = await ResolveVaultTypeAsync(vaultName, resourceGroup, subscription, vaultType, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.IsRsv(resolved) + ? await rsvOps.RunBackupHealthCheckAsync(vaultName, resourceGroup, subscription, rpoThresholdHours, includeSecurityPosture, tenant, retryPolicy, cancellationToken) + : await dppOps.RunBackupHealthCheckAsync(vaultName, resourceGroup, subscription, rpoThresholdHours, includeSecurityPosture, tenant, retryPolicy, cancellationToken); + } + + public Task BulkEnableBackupAsync( + string vaultName, string subscription, string workloadType, + string policyName, string? vaultType, string? resourceGroupFilter, + string? tagFilter, string? resourceIds, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Bulk backup enablement initiated for workload type '{workloadType}' with policy '{policyName}'.")); + } + + public Task BulkTriggerBackupAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? workloadType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Bulk backup trigger initiated for all items in vault '{vaultName}'.")); + } + + public Task BulkUpdatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string sourcePolicyName, string targetPolicyName, string? vaultType, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Accepted", null, $"Bulk policy update from '{sourcePolicyName}' to '{targetPolicyName}' initiated.")); + } + + public Task GenerateIacFromVaultAsync( + string vaultName, string resourceGroup, string subscription, + string iacFormat, string? vaultType, bool includeProtectedItems, + bool includeRbac, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("Succeeded", null, $"IaC template generated in '{iacFormat}' format for vault '{vaultName}'.")); + } + + public Task MoveRecoveryPointToArchiveAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? recoveryPointId, string? containerName, + bool checkEligibilityOnly, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + if (checkEligibilityOnly) + { + return Task.FromResult(new OperationResult("Succeeded", null, "Archive eligibility check completed.")); + } + return Task.FromResult(new OperationResult("Accepted", null, $"Recovery point archive initiated for '{protectedItemName}'.")); + } + + // ── Workflow methods ── + + public Task SetupVmBackupAsync( + string resourceIds, string resourceGroup, string subscription, + string location, string? vaultName, string? scheduleFrequency, + string? dailyRetentionDays, bool triggerFirstBackup, string? outputIac, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("CreateVault", "Pending", $"Will create RSV vault '{vaultName ?? "auto-generated"}' in {location}"), + new("CreatePolicy", "Pending", "Will create default VM backup policy"), + new("EnableProtection", "Pending", $"Will protect resources: {resourceIds}"), + }; + if (triggerFirstBackup) steps.Add(new("TriggerBackup", "Pending", "Will trigger initial backup")); + if (!string.IsNullOrEmpty(outputIac)) steps.Add(new("GenerateIaC", "Pending", $"Will generate {outputIac} template")); + + return Task.FromResult(new WorkflowResult("Planned", "SetupVmBackup", steps, "Workflow ready to execute. Implementation pending.")); + } + + public Task SetupSqlHanaBackupAsync( + string vmResourceId, string workloadType, string resourceGroup, + string subscription, string location, string? vaultName, + bool autoProtect, string? outputIac, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("CreateVault", "Pending", $"Will create RSV vault in {location}"), + new("RegisterContainer", "Pending", $"Will register VM '{vmResourceId}' as container"), + new("CreatePolicy", "Pending", $"Will create {workloadType} backup policy"), + new("EnableAutoProtection", "Pending", autoProtect ? "Will enable auto-protection" : "Will protect individual databases"), + }; + + return Task.FromResult(new WorkflowResult("Planned", "SetupSqlHanaBackup", steps, "Workflow ready to execute.")); + } + + public Task SetupAksBackupAsync( + string clusterResourceId, string resourceGroup, string subscription, + string location, string snapshotResourceGroup, string? vaultName, + string? outputIac, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("CreateBackupVault", "Pending", $"Will create DPP vault in {location}"), + new("InstallExtension", "Pending", "Will install backup extension on AKS cluster"), + new("CreatePolicy", "Pending", "Will create AKS backup policy"), + new("EnableProtection", "Pending", $"Will protect cluster with snapshot RG '{snapshotResourceGroup}'"), + }; + + return Task.FromResult(new WorkflowResult("Planned", "SetupAksBackup", steps, "Workflow ready to execute.")); + } + + public Task SetupDatasourceBackupAsync( + string datasourceId, string workloadType, string resourceGroup, + string subscription, string location, string? vaultName, + string? outputIac, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("CreateVault", "Pending", $"Will create appropriate vault in {location}"), + new("CreatePolicy", "Pending", $"Will create {workloadType} backup policy"), + new("EnableProtection", "Pending", $"Will protect datasource '{datasourceId}'"), + }; + + return Task.FromResult(new WorkflowResult("Planned", "SetupDatasourceBackup", steps, "Workflow ready to execute.")); + } + + public Task SecureVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? securityLevel, string? resourceGuardId, + string? keyVaultUri, string? keyName, string? logAnalyticsWorkspaceId, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("EnableSoftDelete", "Pending", "Will enable soft delete with 14-day retention"), + new("EnableImmutability", "Pending", "Will enable vault immutability"), + }; + if (!string.IsNullOrEmpty(resourceGuardId)) steps.Add(new("ConfigureMUA", "Pending", "Will configure Multi-User Authorization")); + if (!string.IsNullOrEmpty(keyVaultUri)) steps.Add(new("ConfigureCMK", "Pending", "Will configure customer-managed keys")); + if (!string.IsNullOrEmpty(logAnalyticsWorkspaceId)) steps.Add(new("ConfigureMonitoring", "Pending", "Will configure diagnostics")); + + return Task.FromResult(new WorkflowResult("Planned", "SecureVault", steps, "Security workflow ready to execute.")); + } + + public Task SetupDisasterRecoveryAsync( + string resourceIds, string primaryRegion, string resourceGroup, + string subscription, string? vaultName, string? secondaryRegion, + string? outputIac, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("ConfigureGRS", "Pending", "Will configure GRS storage redundancy"), + new("EnableCRR", "Pending", "Will enable Cross-Region Restore"), + new("ValidateDR", "Pending", "Will validate DR readiness"), + }; + + return Task.FromResult(new WorkflowResult("Planned", "SetupDisasterRecovery", steps, "DR workflow ready to execute.")); + } + + public Task ComplianceRemediationAsync( + string subscription, string? resourceGroup, string? resourceTypes, + string? tagFilter, string? vaultName, string? policyName, + bool autoRemediate, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("AuditResources", "Pending", "Will scan for unprotected resources"), + new("CheckPolicies", "Pending", "Will verify backup policies"), + new("CheckSecurity", "Pending", "Will verify security posture"), + }; + if (autoRemediate) steps.Add(new("Remediate", "Pending", "Will auto-remediate compliance gaps")); + + return Task.FromResult(new WorkflowResult("Planned", "ComplianceRemediation", steps, "Compliance audit workflow ready to execute.")); + } + + public Task MigrateBackupConfigAsync( + string sourceVaultName, string sourceVaultType, string sourceResourceGroup, + string subscription, string targetResourceGroup, string? targetVaultName, + string? targetLocation, bool decommissionSource, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("ReadSourceConfig", "Pending", $"Will read config from vault '{sourceVaultName}'"), + new("CreateTargetVault", "Pending", $"Will create target vault '{targetVaultName ?? "auto-generated"}'"), + new("MigratePolicies", "Pending", "Will recreate policies in target vault"), + new("ReprotectItems", "Pending", "Will re-protect items in target vault"), + }; + if (decommissionSource) steps.Add(new("DecommissionSource", "Pending", "Will decommission source vault")); + + return Task.FromResult(new WorkflowResult("Planned", "MigrateBackupConfig", steps, "Migration workflow ready to execute.")); + } + + public Task RansomwareRecoveryAsync( + string resourceIds, string vaultName, string resourceGroup, + string subscription, string infectionTimestamp, string? vaultType, + bool restoreToIsolatedEnv, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + var steps = new List + { + new("IdentifyCleanPoints", "Pending", $"Will find recovery points before '{infectionTimestamp}'"), + new("ValidateRecoveryPoints", "Pending", "Will validate recovery point integrity"), + new("RestoreResources", "Pending", restoreToIsolatedEnv ? "Will restore to isolated environment" : "Will restore in-place"), + new("VerifyRecovery", "Pending", "Will verify restored resources"), + }; + + return Task.FromResult(new WorkflowResult("Planned", "RansomwareRecovery", steps, "Ransomware recovery workflow ready to execute.")); + } + + private async Task ResolveVaultTypeAsync( + string vaultName, string resourceGroup, string subscription, + string? vaultType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + if (VaultTypeResolver.IsVaultTypeSpecified(vaultType)) + { + return vaultType!; + } + + // Auto-detect by trying RSV first, then DPP + try + { + await rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.Rsv; + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Not an RSV vault, try DPP + } + + try + { + await dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.Dpp; + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + throw new KeyNotFoundException($"Vault '{vaultName}' not found in resource group '{resourceGroup}'. Verify the vault name and resource group, or specify --vault-type explicitly."); + } + } + + private static async Task AutoDetectAndExecuteAsync( + Func> rsvAction, Func> dppAction, string vaultName) + { + try + { + return await rsvAction(); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Not found in RSV, try DPP + } + + try + { + return await dppAction(); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + throw new KeyNotFoundException($"Vault '{vaultName}' not found as either RSV or DPP vault. Verify the vault name and resource group, or specify --vault-type explicitly."); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs new file mode 100644 index 0000000000..87ac83ddc1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -0,0 +1,1021 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.ResourceManager; +using Azure.ResourceManager.DataProtectionBackup; +using Azure.ResourceManager.DataProtectionBackup.Models; +using Azure.ResourceManager.Resources; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public class DppBackupOperations(ITenantService tenantService) : BaseAzureService(tenantService), IDppBackupOperations +{ + private const string VaultType = VaultTypeResolver.Dpp; + + /// + /// Resolves the DPP datasource profile from a user-supplied or auto-detected type string. + /// Handles auto-detection (e.g. "Microsoft.Storage/storageAccounts" → Blob profile) + /// and friendly name mapping (e.g. "aks" → AKS profile). + /// + internal static DppDatasourceProfile ResolveProfile(string datasourceTypeOrArm) + { + // First try auto-detection (e.g. storage account → Blob) + var autoDetected = DppDatasourceRegistry.TryAutoDetect(datasourceTypeOrArm); + if (autoDetected != null) + { + return autoDetected; + } + + return DppDatasourceRegistry.Resolve(datasourceTypeOrArm); + } + + public async Task CreateVaultAsync( + string vaultName, string resourceGroup, string subscription, string location, + string? sku, string? storageType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(location), location)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var collection = rgResource.GetDataProtectionBackupVaults(); + + var storageSettings = new List + { + new() + { + DataStoreType = StorageSettingStoreType.VaultStore, + StorageSettingType = storageType?.ToLowerInvariant() switch + { + "locallyredundant" => StorageSettingType.LocallyRedundant, + "zoneredundant" => StorageSettingType.ZoneRedundant, + _ => StorageSettingType.GeoRedundant + } + } + }; + + var vaultData = new DataProtectionBackupVaultData(new AzureLocation(location), new DataProtectionBackupVaultProperties(storageSettings)); + + var result = await collection.CreateOrUpdateAsync(WaitUntil.Completed, vaultName, vaultData, cancellationToken); + + return new VaultCreateResult( + result.Value.Id?.ToString(), + result.Value.Data.Name, + VaultType, + result.Value.Data.Location.Name, + result.Value.Data.Properties?.ProvisioningState?.ToString()); + } + + public async Task GetVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + return MapToVaultInfo(vault.Value.Data, resourceGroup); + } + + public async Task> ListVaultsAsync( + string subscription, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters((nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var subId = SubscriptionResource.CreateResourceIdentifier(subscription); + var subResource = armClient.GetSubscriptionResource(subId); + + var vaults = new List(); + await foreach (var vault in subResource.GetDataProtectionBackupVaultsAsync(cancellationToken)) + { + var rg = vault.Id?.ResourceGroupName; + vaults.Add(MapToVaultInfo(vault.Data, rg)); + } + + return vaults; + } + + public async Task ProtectItemAsync( + string vaultName, string resourceGroup, string subscription, + string datasourceId, string policyName, string? datasourceType, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(datasourceId), datasourceId), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var vaultData = await vaultResource.GetAsync(cancellationToken); + var collection = vaultResource.GetDataProtectionBackupInstances(); + + var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var datasourceResourceId = new ResourceIdentifier(datasourceId); + + // Resolve the datasource profile from user-supplied type or auto-detect from ARM resource type + var resolvedDatasourceType = datasourceType ?? datasourceResourceId.ResourceType.ToString(); + var profile = ResolveProfile(resolvedDatasourceType); + + // Generate instance name based on profile's naming mode + var instanceName = DppDatasourceRegistry.GenerateInstanceName(profile, datasourceResourceId); + + var policyInfo = new BackupInstancePolicyInfo(policyId); + + // Set snapshot resource group for datasource types that require it (Disk, AKS, ESAN — not Blob) + if (profile.RequiresSnapshotResourceGroup) + { + var snapshotRgId = ResourceGroupResource.CreateResourceIdentifier(subscription, datasourceResourceId.ResourceGroupName ?? resourceGroup); + var opStoreSettings = new OperationalDataStoreSettings(DataStoreType.OperationalStore) + { + ResourceGroupId = snapshotRgId, + }; + policyInfo.PolicyParameters = new BackupInstancePolicySettings(); + policyInfo.PolicyParameters.DataStoreParametersList.Add(opStoreSettings); + } + + // Add datasource-specific backup parameters (e.g. AKS K8s cluster settings) + if (profile.BackupParametersMode == DppBackupParametersMode.KubernetesCluster) + { + policyInfo.PolicyParameters ??= new BackupInstancePolicySettings(); + var aksSettings = new KubernetesClusterBackupDataSourceSettings( + isSnapshotVolumesEnabled: true, + isClusterScopeResourcesIncluded: true); + policyInfo.PolicyParameters.BackupDataSourceParametersList.Add(aksSettings); + } + + var dataSourceInfo = new DataSourceInfo(datasourceResourceId) + { + DataSourceType = profile.ArmResourceType, + ObjectType = "Datasource", + ResourceType = datasourceResourceId.ResourceType, + ResourceName = datasourceResourceId.Name, + ResourceLocation = vaultData.Value.Data.Location, + }; + var instanceProperties = new DataProtectionBackupInstanceProperties( + dataSourceInfo, + policyInfo, + string.Empty) + { + ObjectType = "BackupInstance", + }; + + // Set DataSourceSetInfo based on profile configuration + if (profile.DataSourceSetMode != DppDataSourceSetMode.None) + { + var setId = profile.DataSourceSetMode == DppDataSourceSetMode.Parent + ? DppDatasourceRegistry.GetParentResourceId(datasourceResourceId) + : datasourceResourceId; + instanceProperties.DataSourceSetInfo = new DataSourceSetInfo(setId) + { + DataSourceType = profile.ArmResourceType, + ObjectType = "DatasourceSet", + ResourceType = setId.ResourceType, + ResourceName = setId.Name, + ResourceLocation = vaultData.Value.Data.Location, + }; + } + + var instanceData = new DataProtectionBackupInstanceData + { + Properties = instanceProperties + }; + + var result = await collection.CreateOrUpdateAsync(WaitUntil.Started, instanceName, instanceData, cancellationToken); + + var jobId = ExtractJobIdFromOperation(result.GetRawResponse()); + + return new ProtectResult( + "Accepted", + instanceName, + jobId, + jobId != null ? $"Protection initiated. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Protection initiated."); + } + + public async Task GetProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + var instance = await instanceResource.GetAsync(cancellationToken); + + return MapToProtectedItemInfo(instance.Value.Data); + } + + public async Task> ListProtectedItemsAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var collection = vaultResource.GetDataProtectionBackupInstances(); + + var items = new List(); + await foreach (var instance in collection.GetAllAsync(cancellationToken)) + { + items.Add(MapToProtectedItemInfo(instance.Data)); + } + + return items; + } + + public async Task TriggerBackupAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? expiry, string? backupType, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + + // Fetch backup instance to get associated policy + var instanceData = await instanceResource.GetAsync(cancellationToken); + var policyId = instanceData.Value.Data.Properties.PolicyInfo.PolicyId; + + // Fetch policy to find the backup rule name + var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); + var policyData = await policyResource.GetAsync(cancellationToken); + + string ruleName = "BackupDaily"; + if (policyData.Value.Data.Properties is RuleBasedBackupPolicy ruleBasedPolicy) + { + var backupRule = ruleBasedPolicy.PolicyRules.FirstOrDefault(r => r is DataProtectionBackupRule); + if (backupRule != null) + { + ruleName = backupRule.Name; + } + } + + var ruleOption = new AdhocBackupRules(ruleName, "Default"); + var backupContent = new AdhocBackupTriggerContent(ruleOption); + + var result = await instanceResource.TriggerAdhocBackupAsync(WaitUntil.Started, backupContent, cancellationToken); + var jobId = ExtractJobIdFromOperation(result.GetRawResponse()); + + return new BackupTriggerResult( + "Accepted", + jobId, + jobId != null ? $"Backup triggered. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Backup triggered."); + } + + public async Task TriggerRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? recoveryPointId, + string? targetResourceId, string? restoreLocation, string? pointInTime, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + + // Get the backup instance to determine datasource info + var instance = await instanceResource.GetAsync(cancellationToken); + var datasourceInfo = instance.Value.Data.Properties?.DataSourceInfo; + var datasourceId = datasourceInfo?.ResourceId; + var location = !string.IsNullOrEmpty(restoreLocation) ? new AzureLocation(restoreLocation) : datasourceInfo?.ResourceLocation; + + RestoreTargetInfoBase restoreTarget; + + // Determine if this is a restore-as-files scenario (target is a storage account) + if (!string.IsNullOrEmpty(targetResourceId) && + targetResourceId.Contains("Microsoft.Storage/storageAccounts", StringComparison.OrdinalIgnoreCase)) + { + // Restore-as-files: target is a storage account with optional container + var storageAccountId = targetResourceId; + var containerName = "pgflex-restore"; + + // Extract container name from the target resource ID if it includes a container + // e.g., .../storageAccounts/name/blobServices/default/containers/containerName + if (targetResourceId.Contains("/blobServices/", StringComparison.OrdinalIgnoreCase)) + { + var parts = targetResourceId.Split('/'); + containerName = parts[^1]; + var containerIndex = targetResourceId.IndexOf("/blobServices/", StringComparison.OrdinalIgnoreCase); + storageAccountId = targetResourceId[..containerIndex]; + } + + // Extract storage account name from the ARM ID + var storageAccountArmId = new ResourceIdentifier(storageAccountId); + var storageAccountName = storageAccountArmId.Name; + + var containerUri = new Uri($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); + var filePrefix = $"pgflex-restore-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; + + // TargetResourceArmId must point to the container (not the storage account) + // per the REST API spec: "ARM Id pointing to container / file share" + var containerArmId = new ResourceIdentifier( + $"{storageAccountId}/blobServices/default/containers/{containerName}"); + + var targetDetails = new RestoreFilesTargetDetails( + filePrefix, + RestoreTargetLocationType.AzureBlobs, + containerUri) + { + TargetResourceArmId = containerArmId, + }; + + restoreTarget = new RestoreFilesTargetInfo( + RecoverySetting.FailIfExists, + targetDetails) + { + RestoreLocation = location, + }; + } + else + { + // Restore-as-server: target is a datasource resource + var targetId = !string.IsNullOrEmpty(targetResourceId) ? new ResourceIdentifier(targetResourceId) : datasourceId; + + var targetDatasource = new DataSourceInfo(targetId ?? datasourceId!) + { + ObjectType = "Datasource", + DataSourceType = datasourceInfo?.DataSourceType, + ResourceType = targetId?.ResourceType ?? datasourceId?.ResourceType, + ResourceName = targetId?.Name ?? datasourceId?.Name, + ResourceLocation = location, + }; + + var restoreTargetInfo = new RestoreTargetInfo( + RecoverySetting.FailIfExists, + targetDatasource) + { + RestoreLocation = location, + }; + + // Set DataSourceSetInfo for datasource types that require it (ESAN, AKS) + var resolvedDatasourceTypeStr = datasourceInfo?.DataSourceType ?? string.Empty; + var restoreProfile = ResolveProfile(resolvedDatasourceTypeStr); + if (restoreProfile.DataSourceSetMode != DppDataSourceSetMode.None) + { + var dsId = targetId ?? datasourceId!; + var setId = restoreProfile.DataSourceSetMode == DppDataSourceSetMode.Parent + ? DppDatasourceRegistry.GetParentResourceId(dsId) + : dsId; + restoreTargetInfo.DataSourceSetInfo = new DataSourceSetInfo(setId) + { + DataSourceType = datasourceInfo?.DataSourceType, + ObjectType = "DatasourceSet", + ResourceType = setId.ResourceType, + ResourceName = setId.Name, + ResourceLocation = location, + }; + } + + restoreTarget = restoreTargetInfo; + } + + // Determine the correct source data store type from the datasource profile + var datasourceTypeStr = datasourceInfo?.DataSourceType ?? string.Empty; + var storeProfile = ResolveProfile(datasourceTypeStr); + var sourceDataStoreType = storeProfile.UsesOperationalStore ? SourceDataStoreType.OperationalStore : SourceDataStoreType.VaultStore; + + // Use time-based restore for blob PIT or when --point-in-time is provided + BackupRestoreContent restoreContent; + if (!string.IsNullOrEmpty(pointInTime)) + { + if (!DateTimeOffset.TryParse(pointInTime, out var recoverOn)) + { + throw new ArgumentException($"Invalid point-in-time format: '{pointInTime}'. Use ISO 8601 format (e.g., '2025-01-15T10:30:00Z')."); + } + + restoreContent = new BackupRecoveryTimeBasedRestoreContent( + restoreTarget, + sourceDataStoreType, + recoverOn); + } + else if (!string.IsNullOrEmpty(recoveryPointId)) + { + restoreContent = new BackupRecoveryPointBasedRestoreContent( + restoreTarget, + sourceDataStoreType, + recoveryPointId); + } + else + { + throw new ArgumentException("Either --recovery-point or --point-in-time must be specified for restore."); + } + + var result = await instanceResource.TriggerRestoreAsync(WaitUntil.Started, restoreContent, cancellationToken); + var jobId = ExtractJobIdFromOperation(result.GetRawResponse()); + + return new RestoreTriggerResult( + "Accepted", + jobId, + jobId != null ? $"Restore triggered. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Restore triggered."); + } + + public async Task GetPolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); + var policy = await policyResource.GetAsync(cancellationToken); + + return MapToPolicyInfo(policy.Value.Data); + } + + public async Task> ListPoliciesAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var collection = vaultResource.GetDataProtectionBackupPolicies(); + + var policies = new List(); + await foreach (var policy in collection.GetAllAsync(cancellationToken)) + { + policies.Add(MapToPolicyInfo(policy.Data)); + } + + return policies; + } + + public async Task GetJobAsync( + string vaultName, string resourceGroup, string subscription, + string jobId, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(jobId), jobId)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var jobResourceId = DataProtectionBackupJobResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, jobId); + var jobResource = armClient.GetDataProtectionBackupJobResource(jobResourceId); + + try + { + var job = await jobResource.GetAsync(cancellationToken); + return MapToJobInfo(job.Value.Data); + } + catch (FormatException) + { + // Bug #40 workaround: SDK can't parse ISO 8601 durations like "PT3M7.0153191S" + // in DataProtectionBackupJobData. Fall back to listing jobs and matching by ID. + var jobs = await ListJobsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + return jobs.FirstOrDefault(j => j.Name == jobId) + ?? throw new InvalidOperationException($"Job '{jobId}' not found. The SDK cannot parse this job's duration field."); + } + } + + public async Task> ListJobsAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var collection = vaultResource.GetDataProtectionBackupJobs(); + + var jobs = new List(); + await foreach (var job in collection.GetAllAsync(cancellationToken)) + { + jobs.Add(MapToJobInfo(job.Data)); + } + + return jobs; + } + + public async Task GetRecoveryPointAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string recoveryPointId, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName), + (nameof(recoveryPointId), recoveryPointId)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rpId = DataProtectionBackupRecoveryPointResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName, recoveryPointId); + var rpResource = armClient.GetDataProtectionBackupRecoveryPointResource(rpId); + var rp = await rpResource.GetAsync(cancellationToken); + + return MapToRecoveryPointInfo(rp.Value.Data); + } + + public async Task> ListRecoveryPointsAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + var collection = instanceResource.GetDataProtectionBackupRecoveryPoints(); + + var points = new List(); + await foreach (var rp in collection.GetAllAsync(cancellationToken: cancellationToken)) + { + points.Add(MapToRecoveryPointInfo(rp.Data)); + } + + return points; + } + + // ── New methods ── + + public async Task UpdateVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? redundancy, string? softDelete, string? softDeleteRetentionDays, + string? immutabilityState, string? identityType, string? tags, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + var patchData = new DataProtectionBackupVaultPatch(); + + if (!string.IsNullOrEmpty(identityType)) + { + patchData.Identity = new Azure.ResourceManager.Models.ManagedServiceIdentity( + identityType.Equals("SystemAssigned", StringComparison.OrdinalIgnoreCase) + ? Azure.ResourceManager.Models.ManagedServiceIdentityType.SystemAssigned + : Azure.ResourceManager.Models.ManagedServiceIdentityType.None); + } + + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Vault '{vaultName}' updated successfully."); + } + + public async Task DeleteVaultAsync( + string vaultName, string resourceGroup, string subscription, + bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + await vaultResource.DeleteAsync(WaitUntil.Completed, cancellationToken); + + return new OperationResult("Succeeded", null, $"Vault '{vaultName}' deleted successfully."); + } + + public async Task UpdatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? scheduleFrequency, + string? dailyRetentionDays, string? weeklyRetentionWeeks, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // Fetch the existing policy — will throw RequestFailedException (404) if it doesn't exist + var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); + var existingPolicy = await policyResource.GetAsync(cancellationToken); + var policyData = existingPolicy.Value.Data; + + // Modify only the requested retention on existing policy rules, preserving datasourceTypes and structure + if (policyData.Properties is RuleBasedBackupPolicy ruleBasedPolicy) + { + foreach (var rule in ruleBasedPolicy.PolicyRules) + { + if (rule is DataProtectionRetentionRule retentionRule && int.TryParse(dailyRetentionDays, out var days)) + { + foreach (var lifecycle in retentionRule.Lifecycles) + { + lifecycle.DeleteAfter = new DataProtectionBackupAbsoluteDeleteSetting(TimeSpan.FromDays(days)); + } + } + } + } + + // PUT the modified policy back with original datasourceTypes preserved + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var collection = vaultResource.GetDataProtectionBackupPolicies(); + await collection.CreateOrUpdateAsync(WaitUntil.Completed, policyName, policyData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' updated in vault '{vaultName}'."); + } + + public async Task CreatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string workloadType, + string? scheduleFrequency, string? scheduleTime, + string? dailyRetentionDays, string? weeklyRetentionWeeks, + string? monthlyRetentionMonths, string? yearlyRetentionYears, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName), + (nameof(workloadType), workloadType)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var collection = vaultResource.GetDataProtectionBackupPolicies(); + + // Parse retention and schedule parameters + var retentionDays = int.TryParse(dailyRetentionDays, out var dd) ? dd : 0; + var scheduleTimeValue = scheduleTime ?? "02:00"; + var now = DateTimeOffset.UtcNow; + var scheduleParts = scheduleTimeValue.Split(':'); + var scheduleHour = int.TryParse(scheduleParts[0], out var sh) ? sh : 2; + var scheduleMinute = scheduleParts.Length > 1 && int.TryParse(scheduleParts[1], out var sm) ? sm : 0; + var scheduleStartTime = new DateTimeOffset(now.Year, now.Month, now.Day, scheduleHour, scheduleMinute, 0, TimeSpan.Zero); + + // Resolve the datasource profile — drives all policy construction decisions + var profile = DppDatasourceRegistry.Resolve(workloadType); + var dataStoreType = profile.UsesOperationalStore ? DataStoreType.OperationalStore : DataStoreType.VaultStore; + + // Use profile default retention if user didn't specify + var defaultRetention = retentionDays > 0 ? retentionDays : profile.DefaultRetentionDays; + + // Build the retention rule (Default) + var retentionDeleteSetting = new DataProtectionBackupAbsoluteDeleteSetting(TimeSpan.FromDays(defaultRetention)); + var retentionDataStore = new DataStoreInfoBase(dataStoreType, "DataStoreInfoBase"); + var retentionLifeCycle = new SourceLifeCycle(retentionDeleteSetting, retentionDataStore); + var retentionRule = new DataProtectionRetentionRule("Default", [retentionLifeCycle]) + { + IsDefault = true, + }; + + List rules = [retentionRule]; + + // Continuous backup (Blob, ADLS, CosmosDB) — no scheduled backup rule + // All other workloads — add a scheduled backup rule driven by profile settings + if (!profile.IsContinuousBackup) + { + var repeatingInterval = $"R/{scheduleStartTime:yyyy-MM-ddTHH:mm:ss+00:00}/{profile.ScheduleInterval}"; + + var schedule = new DataProtectionBackupSchedule([repeatingInterval]) + { + TimeZone = "UTC", + }; + var defaultTag = new DataProtectionBackupRetentionTag("Default"); + var taggingCriteria = new DataProtectionBackupTaggingCriteria(true, 99, defaultTag); + var triggerContext = new ScheduleBasedBackupTriggerContext(schedule, [taggingCriteria]); + var backupDataStore = new DataStoreInfoBase(dataStoreType, "DataStoreInfoBase"); + var backupRule = new DataProtectionBackupRule(profile.BackupRuleName, backupDataStore, triggerContext) + { + BackupParameters = new DataProtectionBackupSettings(profile.BackupType), + }; + + rules.Add(backupRule); + } + + var policyProperties = new RuleBasedBackupPolicy( + [profile.ArmResourceType], + rules); + var policyData = new DataProtectionBackupPolicyData { Properties = policyProperties }; + + await collection.CreateOrUpdateAsync(WaitUntil.Completed, policyName, policyData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' created in vault '{vaultName}'."); + } + + public async Task DeletePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); + await policyResource.DeleteAsync(WaitUntil.Completed, cancellationToken); + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' deleted from vault '{vaultName}'."); + } + + public async Task StopProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string mode, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + if (mode.Equals("DeleteData", StringComparison.OrdinalIgnoreCase)) + { + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + await instanceResource.DeleteAsync(WaitUntil.Started, cancellationToken); + return new OperationResult("Accepted", null, "Protection stopped and data deletion initiated."); + } + + // DPP suspend backup + var instId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instResource = armClient.GetDataProtectionBackupInstanceResource(instId); + await instResource.SuspendBackupsAsync(WaitUntil.Started, cancellationToken); + + return new OperationResult("Accepted", null, "Protection stopped with data retained."); + } + + public async Task ResumeProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? policyName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); + await instanceResource.ResumeBackupsAsync(WaitUntil.Started, cancellationToken); + + return new OperationResult("Accepted", null, "Protection resumed."); + } + + public Task ModifyProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? newPolicyName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // DPP modify requires re-creating the instance with a different policy + return Task.FromResult(new OperationResult("NotSupported", null, "To change policy for a DPP backup instance, stop protection (RetainData) and re-protect with the new policy.")); + } + + public Task UndeleteProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + return Task.FromResult(new OperationResult("NotSupported", null, "Undelete for DPP backup instances is managed automatically during the soft-delete retention period.")); + } + + public async Task ConfigureImmutabilityAsync( + string vaultName, string resourceGroup, string subscription, + string immutabilityState, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(immutabilityState), immutabilityState)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + + var patchData = new DataProtectionBackupVaultPatch(); + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Immutability set to '{immutabilityState}' for vault '{vaultName}'."); + } + + public async Task ConfigureSoftDeleteAsync( + string vaultName, string resourceGroup, string subscription, + string softDeleteState, string? softDeleteRetentionDays, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(softDeleteState), softDeleteState)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + + var patchData = new DataProtectionBackupVaultPatch(); + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Soft delete set to '{softDeleteState}' for vault '{vaultName}'."); + } + + public async Task RunBackupHealthCheckAsync( + string vaultName, string resourceGroup, string subscription, + int? rpoThresholdHours, bool includeSecurityPosture, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + var items = await ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + var rpoThreshold = rpoThresholdHours ?? 24; + var now = DateTimeOffset.UtcNow; + + var details = new List(); + int healthy = 0, unhealthy = 0, breachingRpo = 0; + + foreach (var item in items) + { + var rpoBreached = item.LastBackupTime.HasValue && (now - item.LastBackupTime.Value).TotalHours > rpoThreshold; + if (rpoBreached) breachingRpo++; + + var isHealthy = item.ProtectionStatus?.Contains("Protected", StringComparison.OrdinalIgnoreCase) == true && !rpoBreached; + if (isHealthy) healthy++; else unhealthy++; + + details.Add(new HealthCheckItemDetail( + item.Name, item.ProtectionStatus, isHealthy ? "Healthy" : "Unhealthy", + item.LastBackupTime, rpoBreached)); + } + + return new HealthCheckResult( + vaultName, VaultType, items.Count, healthy, unhealthy, breachingRpo, + vault.Value.Data.Properties?.SecuritySettings?.SoftDeleteSettings?.State?.ToString(), + vault.Value.Data.Properties?.SecuritySettings?.ImmutabilityState?.ToString(), + null, details); + } + + private static BackupVaultInfo MapToVaultInfo(DataProtectionBackupVaultData data, string? resourceGroup) + { + return new BackupVaultInfo( + data.Id?.ToString(), + data.Name, + VaultType, + data.Location.Name, + resourceGroup, + data.Properties?.ProvisioningState?.ToString(), + null, + data.Properties?.StorageSettings?.FirstOrDefault()?.StorageSettingType?.ToString(), + data.Tags?.ToDictionary(t => t.Key, t => t.Value)); + } + + private static ProtectedItemInfo MapToProtectedItemInfo(DataProtectionBackupInstanceData data) + { + return new ProtectedItemInfo( + data.Id?.ToString(), + data.Name, + VaultType, + data.Properties?.ProtectionStatus?.Status?.ToString(), + data.Properties?.DataSourceInfo?.DataSourceType, + data.Properties?.DataSourceInfo?.ResourceId?.ToString(), + data.Properties?.PolicyInfo?.PolicyId?.Name, + null, + null); + } + + private static BackupPolicyInfo MapToPolicyInfo(DataProtectionBackupPolicyData data) + { + var datasourceTypes = data.Properties is DataProtectionBackupPolicyPropertiesBase props + ? props.DataSourceTypes?.ToList() as IReadOnlyList + : null; + + return new BackupPolicyInfo( + data.Id?.ToString(), + data.Name, + VaultType, + datasourceTypes, + null); + } + + private static BackupJobInfo MapToJobInfo(DataProtectionBackupJobData data) + { + return new BackupJobInfo( + data.Id?.ToString(), + data.Name, + VaultType, + data.Properties?.OperationCategory, + data.Properties?.Status, + data.Properties?.StartOn, + data.Properties?.EndOn, + data.Properties?.DataSourceType, + data.Properties?.DataSourceName); + } + + private static RecoveryPointInfo MapToRecoveryPointInfo(DataProtectionBackupRecoveryPointData data) + { + DateTimeOffset? rpTime = null; + string? rpType = null; + + if (data.Properties is DataProtectionBackupDiscreteRecoveryPointProperties rpProps) + { + rpTime = rpProps.RecoverOn; + rpType = rpProps.RecoveryPointType; + } + + return new RecoveryPointInfo( + data.Id?.ToString(), + data.Name, + VaultType, + rpTime, + rpType); + } + + private static string? ExtractJobIdFromOperation(Response response) + { + if (response.Headers.TryGetValue("Azure-AsyncOperation", out var asyncOpUrl) && !string.IsNullOrEmpty(asyncOpUrl)) + { + // Format: https://.../operationResults/{jobId}?api-version=... + var uri = new Uri(asyncOpUrl); + var segments = uri.AbsolutePath.Split('/'); + return segments.Length > 0 ? segments[^1] : null; + } + + return null; + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs new file mode 100644 index 0000000000..23dfb57e6d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +/// +/// Defines how a DPP datasource type builds its DataSourceSetInfo in protect/restore payloads. +/// +public enum DppDataSourceSetMode +{ + /// No DataSourceSetInfo required (Disk, Blob, PGFlex). + None, + + /// DataSourceSetInfo points to the datasource itself (AKS). + Self, + + /// DataSourceSetInfo points to the parent resource (ESAN volume group → parent ESAN). + Parent, +} + +/// +/// Defines the default restore approach for a DPP datasource type. +/// +public enum DppRestoreMode +{ + /// Restore using a discrete recovery point ID. + RecoveryPoint, + + /// Restore to a specific point in time (Blob, ADLS continuous backup). + PointInTime, + + /// Restore as files to a storage account container (PGFlex). + RestoreAsFiles, +} + +/// +/// Defines instance naming patterns for DPP backup instances. +/// +public enum DppInstanceNamingMode +{ + /// Standard naming: {resourceName}-{resourceName}-{shortGuid}. + Standard, + + /// Parent-child naming for ESAN: {parentName}-{childName}-{guid}. + ParentChild, +} + +/// +/// Defines whether the datasource requires additional backup parameters during protection. +/// +public enum DppBackupParametersMode +{ + /// No additional backup parameters needed. + None, + + /// AKS requires KubernetesClusterBackupDataSourceSettings. + KubernetesCluster, +} + +/// +/// Immutable profile describing all configuration aspects of a DPP datasource type. +/// Eliminates hardcoded if/else branching by centralizing datasource-specific behaviour. +/// Each property drives a specific dimension of protect, policy-create, and restore operations. +/// +/// +/// AOT-safe: no reflection, no Func delegates — pure data record with enum-driven dispatch. +/// +public sealed record DppDatasourceProfile +{ + // ── Identity ── + + /// Friendly name used for user-facing resolution (e.g. "AzureDisk", "AKS"). + public required string FriendlyName { get; init; } + + /// ARM resource type string (e.g. "Microsoft.Compute/disks"). + public required string ArmResourceType { get; init; } + + /// Alternative user-supplied names that resolve to this profile. + public string[] Aliases { get; init; } = []; + + // ── Data Store ── + + /// Whether this datasource uses OperationalStore (snapshot-based) or VaultStore. + public bool UsesOperationalStore { get; init; } + + // ── Policy / Schedule ── + + /// True for continuous backup (Blob, ADLS) — no scheduled backup rule created. + public bool IsContinuousBackup { get; init; } + + /// ISO 8601 schedule interval (e.g. "PT4H", "P1D"). Ignored when is true. + public string ScheduleInterval { get; init; } = "P1D"; + + /// Backup type for the backup rule (e.g. "Incremental", "Full"). Ignored when is true. + public string BackupType { get; init; } = "Full"; + + /// Name of the backup rule (e.g. "BackupHourly", "BackupDaily"). Ignored when is true. + public string BackupRuleName { get; init; } = "BackupDaily"; + + /// Default retention days when user doesn't specify. + public int DefaultRetentionDays { get; init; } = 30; + + // ── Protection ── + + /// Whether ProtectItemAsync must set the snapshot resource group parameter. + public bool RequiresSnapshotResourceGroup { get; init; } + + /// How to populate DataSourceSetInfo in protect and restore payloads. + public DppDataSourceSetMode DataSourceSetMode { get; init; } = DppDataSourceSetMode.None; + + /// Whether additional backup parameters are needed (e.g. AKS K8s cluster settings). + public DppBackupParametersMode BackupParametersMode { get; init; } = DppBackupParametersMode.None; + + /// Instance naming pattern for backup instances. + public DppInstanceNamingMode InstanceNamingMode { get; init; } = DppInstanceNamingMode.Standard; + + // ── Restore ── + + /// The default restore approach (RP-based, PIT, or restore-as-files). + public DppRestoreMode DefaultRestoreMode { get; init; } = DppRestoreMode.RecoveryPoint; + + // ── Auto-detection ── + + /// + /// If non-null, when the user's resource ID matches this base ARM type (e.g. "Microsoft.Storage/storageAccounts"), + /// automatically re-map to this profile's . + /// Used for Blob (storage account → blobServices) auto-detection. + /// + public string? AutoDetectFromBaseResourceType { get; init; } + + // ── Policy Update ── + + /// Whether the Azure API supports updating policies for this datasource type. + public bool SupportsPolicyUpdate { get; init; } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs new file mode 100644 index 0000000000..85bed4aa74 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +/// +/// Central registry of all DPP (Data Protection / Backup Vault) datasource profiles. +/// Acts as a single source of truth for datasource-specific configuration, replacing +/// scattered if/else checks throughout DppBackupOperations. +/// +/// To add a new DPP datasource type: +/// 1. Add a static DppDatasourceProfile instance below +/// 2. Register it in the AllProfiles array +/// — No other code changes needed in DppBackupOperations. +/// +public static class DppDatasourceRegistry +{ + // ── Profile definitions ── + + public static readonly DppDatasourceProfile AzureDisk = new() + { + FriendlyName = "AzureDisk", + ArmResourceType = "Microsoft.Compute/disks", + Aliases = ["azuredisk", "disk"], + UsesOperationalStore = true, + ScheduleInterval = "PT4H", + BackupType = "Incremental", + BackupRuleName = "BackupHourly", + DefaultRetentionDays = 7, + RequiresSnapshotResourceGroup = true, + DefaultRestoreMode = DppRestoreMode.RecoveryPoint, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile AzureBlob = new() + { + FriendlyName = "AzureBlob", + ArmResourceType = "Microsoft.Storage/storageAccounts/blobServices", + Aliases = ["azureblob", "blob"], + UsesOperationalStore = true, + IsContinuousBackup = true, + DefaultRetentionDays = 30, + RequiresSnapshotResourceGroup = false, + DefaultRestoreMode = DppRestoreMode.PointInTime, + AutoDetectFromBaseResourceType = "Microsoft.Storage/storageAccounts", + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile Aks = new() + { + FriendlyName = "AKS", + ArmResourceType = "Microsoft.ContainerService/managedClusters", + Aliases = ["aks", "kubernetes"], + UsesOperationalStore = true, + ScheduleInterval = "PT4H", + BackupType = "Incremental", + BackupRuleName = "BackupHourly", + DefaultRetentionDays = 7, + RequiresSnapshotResourceGroup = true, + DataSourceSetMode = DppDataSourceSetMode.Self, + BackupParametersMode = DppBackupParametersMode.KubernetesCluster, + DefaultRestoreMode = DppRestoreMode.RecoveryPoint, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile ElasticSan = new() + { + FriendlyName = "ElasticSAN", + ArmResourceType = "Microsoft.ElasticSan/elasticSans/volumeGroups", + Aliases = ["elasticsan", "esan"], + UsesOperationalStore = true, + ScheduleInterval = "P1D", + BackupType = "Incremental", + BackupRuleName = "BackupDaily", + DefaultRetentionDays = 7, + RequiresSnapshotResourceGroup = true, + DataSourceSetMode = DppDataSourceSetMode.Parent, + InstanceNamingMode = DppInstanceNamingMode.ParentChild, + DefaultRestoreMode = DppRestoreMode.RecoveryPoint, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile PostgreSqlFlexible = new() + { + FriendlyName = "PostgreSQLFlexible", + ArmResourceType = "Microsoft.DBforPostgreSQL/flexibleServers", + Aliases = ["postgresqlflexible", "pgflex", "postgresql"], + UsesOperationalStore = false, + ScheduleInterval = "P1D", + BackupType = "Full", + BackupRuleName = "BackupDaily", + DefaultRetentionDays = 30, + RequiresSnapshotResourceGroup = false, + DefaultRestoreMode = DppRestoreMode.RestoreAsFiles, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile MySqlFlexible = new() + { + FriendlyName = "MySQLFlexible", + ArmResourceType = "Microsoft.DBforMySQL/flexibleServers", + Aliases = ["mysqlflexible", "mysql"], + UsesOperationalStore = false, + ScheduleInterval = "P1D", + BackupType = "Full", + BackupRuleName = "BackupDaily", + DefaultRetentionDays = 30, + RequiresSnapshotResourceGroup = false, + DefaultRestoreMode = DppRestoreMode.RestoreAsFiles, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile AzureDataLakeStorage = new() + { + FriendlyName = "AzureDataLakeStorage", + ArmResourceType = "Microsoft.Storage/storageAccounts/blobServices", + Aliases = ["adls", "datalake", "datalakestorage"], + UsesOperationalStore = true, + IsContinuousBackup = true, + DefaultRetentionDays = 30, + RequiresSnapshotResourceGroup = false, + DefaultRestoreMode = DppRestoreMode.PointInTime, + SupportsPolicyUpdate = false, + }; + + public static readonly DppDatasourceProfile CosmosDb = new() + { + FriendlyName = "CosmosDB", + ArmResourceType = "Microsoft.DocumentDB/databaseAccounts", + Aliases = ["cosmosdb", "cosmos"], + UsesOperationalStore = true, + IsContinuousBackup = true, + DefaultRetentionDays = 30, + RequiresSnapshotResourceGroup = false, + DefaultRestoreMode = DppRestoreMode.PointInTime, + SupportsPolicyUpdate = false, + }; + + // ── Registry ── + + /// All registered DPP datasource profiles. + public static readonly DppDatasourceProfile[] AllProfiles = + [ + AzureDisk, + AzureBlob, + Aks, + ElasticSan, + PostgreSqlFlexible, + MySqlFlexible, + AzureDataLakeStorage, + CosmosDb, + ]; + + /// + /// Resolves a user-supplied workload type or ARM resource type to the matching profile. + /// Case-insensitive match against FriendlyName, Aliases, and ArmResourceType. + /// Falls back to a generic profile if no match is found (preserves backward compatibility). + /// + public static DppDatasourceProfile Resolve(string workloadTypeOrArmType) + { + var normalised = workloadTypeOrArmType.ToLowerInvariant(); + + foreach (var profile in AllProfiles) + { + if (normalised.Equals(profile.FriendlyName, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + + if (normalised.Equals(profile.ArmResourceType, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + + foreach (var alias in profile.Aliases) + { + if (normalised.Equals(alias, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + } + } + + // Fallback: treat unknown types as VaultStore with daily schedule. + // This ensures forward compatibility when new Azure datasource types are added. + return new DppDatasourceProfile + { + FriendlyName = workloadTypeOrArmType, + ArmResourceType = workloadTypeOrArmType, + UsesOperationalStore = false, + ScheduleInterval = "P1D", + BackupType = "Full", + BackupRuleName = "BackupDaily", + DefaultRetentionDays = 30, + }; + } + + /// + /// Tries to auto-detect a profile when the user supplies a base resource type + /// (e.g. "Microsoft.Storage/storageAccounts") that needs re-mapping to a child type + /// (e.g. Blob → "Microsoft.Storage/storageAccounts/blobServices"). + /// + public static DppDatasourceProfile? TryAutoDetect(string armResourceType) + { + var normalised = armResourceType.ToLowerInvariant(); + + foreach (var profile in AllProfiles) + { + if (profile.AutoDetectFromBaseResourceType != null && + normalised.Equals(profile.AutoDetectFromBaseResourceType, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + } + + return null; + } + + /// + /// Derives the parent resource ID from a child resource ID (e.g. ESAN volume group → parent ESAN). + /// Generic logic that strips the last two path segments (/childType/childName). + /// + public static ResourceIdentifier GetParentResourceId(ResourceIdentifier childResourceId) + { + var idStr = childResourceId.ToString(); + + // Find the last child segment: .../parentType/parentName/childType/childName + // Strip /childType/childName to get the parent ID + var lastSlash = idStr.LastIndexOf('/'); + if (lastSlash > 0) + { + var secondLastSlash = idStr.LastIndexOf('/', lastSlash - 1); + if (secondLastSlash > 0) + { + return new ResourceIdentifier(idStr[..secondLastSlash]); + } + } + + return childResourceId.Parent ?? childResourceId; + } + + /// + /// Generates a backup instance name based on the profile's naming mode. + /// + public static string GenerateInstanceName(DppDatasourceProfile profile, ResourceIdentifier datasourceResourceId) + { + return profile.InstanceNamingMode switch + { + DppInstanceNamingMode.ParentChild => + $"{GetParentResourceId(datasourceResourceId).Name}-{datasourceResourceId.Name}-{Guid.NewGuid()}", + _ => + $"{datasourceResourceId.Name}-{datasourceResourceId.Name}-{Guid.NewGuid().ToString("N")[..12]}", + }; + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs new file mode 100644 index 0000000000..c97b0b856a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Models; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public interface IAzureBackupService +{ + // ── Existing methods (keep all of these exactly as they are) ── + Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string vaultType, string location, string? sku = null, string? storageType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListVaultsAsync(string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ProtectItemAsync(string vaultName, string resourceGroup, string subscription, string datasourceId, string policyName, string? vaultType = null, string? containerName = null, string? datasourceType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListProtectedItemsAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task TriggerBackupAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? expiry = null, string? backupType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task TriggerRestoreAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? recoveryPointId, string? vaultType = null, string? containerName = null, string? targetResourceId = null, string? restoreLocation = null, string? stagingStorageAccountId = null, string? pointInTime = null, string? restoreMode = null, string? targetVmName = null, string? targetVnetId = null, string? targetSubnetId = null, string? targetDatabaseName = null, string? targetInstanceName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetPolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListPoliciesAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListJobsAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // ── NEW methods ── + // Vault operations + Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? redundancy = null, string? softDelete = null, string? softDeleteRetentionDays = null, string? immutabilityState = null, string? identityType = null, string? tags = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, bool force = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Policy operations + Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? vaultType = null, string? scheduleFrequency = null, string? scheduleTime = null, string? dailyRetentionDays = null, string? weeklyRetentionWeeks = null, string? monthlyRetentionMonths = null, string? yearlyRetentionYears = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task UpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? vaultType = null, string? scheduleFrequency = null, string? dailyRetentionDays = null, string? weeklyRetentionWeeks = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task DeletePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Protection management + Task StopProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string mode, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ResumeProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? policyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ModifyProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? newPolicyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task UndeleteProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task EnableAutoProtectionAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string instanceName, string policyName, string workloadType, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Container and workload discovery operations + Task> ListContainersAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task RegisterContainerAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string workloadType, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task TriggerInquiryAsync(string vaultName, string resourceGroup, string subscription, string containerName, string? workloadType = null, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task> ListProtectableItemsAsync(string vaultName, string resourceGroup, string subscription, string? workloadType = null, string? containerName = null, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Backup operations + Task GetBackupStatusAsync(string datasourceId, string subscription, string location, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Job operations + Task CancelJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Security operations + Task ConfigureRbacAsync(string principalId, string roleName, string scope, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ConfigureMuaAsync(string vaultName, string resourceGroup, string subscription, string resourceGuardId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ConfigurePrivateEndpointAsync(string vaultName, string resourceGroup, string subscription, string vnetId, string subnetId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ConfigureEncryptionAsync(string vaultName, string resourceGroup, string subscription, string keyVaultUri, string keyName, string identityType, string? vaultType = null, string? keyVersion = null, string? userAssignedIdentityId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Monitoring operations + Task ConfigureMonitoringAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? logAnalyticsWorkspaceId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task GetBackupReportsAsync(string reportType, string logAnalyticsWorkspaceId, string? timeRangeDays = null, string? workloadFilter = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Governance operations + Task> FindUnprotectedResourcesAsync(string subscription, string? resourceTypeFilter = null, string? resourceGroupFilter = null, string? tagFilter = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ApplyAzurePolicyAsync(string policyDefinitionId, string scope, string? policyParameters = null, bool deployRemediation = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ConfigureImmutabilityAsync(string vaultName, string resourceGroup, string subscription, string immutabilityState, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ConfigureSoftDeleteAsync(string vaultName, string resourceGroup, string subscription, string softDeleteState, string? vaultType = null, string? softDeleteRetentionDays = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // DR operations + Task ConfigureCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task TriggerCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string restoreMode, string? targetResourceId = null, string? secondaryRegion = null, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ValidateDrReadinessAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? resourceIds = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Cost operations + Task EstimateBackupCostAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? workloadType = null, bool includeArchiveProjection = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Diagnostics operations + Task DiagnoseBackupFailureAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? jobId = null, string? datasourceId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ValidateBackupPrerequisitesAsync(string datasourceId, string vaultName, string resourceGroup, string subscription, string workloadType, string? vaultType = null, string? policyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task RunBackupHealthCheckAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, int? rpoThresholdHours = null, bool includeSecurityPosture = true, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Bulk operations + Task BulkEnableBackupAsync(string vaultName, string subscription, string workloadType, string policyName, string? vaultType = null, string? resourceGroupFilter = null, string? tagFilter = null, string? resourceIds = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task BulkTriggerBackupAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? workloadType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task BulkUpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string sourcePolicyName, string targetPolicyName, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // IaC operations + Task GenerateIacFromVaultAsync(string vaultName, string resourceGroup, string subscription, string iacFormat, string? vaultType = null, bool includeProtectedItems = true, bool includeRbac = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Recovery point archive + Task MoveRecoveryPointToArchiveAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? recoveryPointId = null, string? containerName = null, bool checkEligibilityOnly = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + // Workflow operations + Task SetupVmBackupAsync(string resourceIds, string resourceGroup, string subscription, string location, string? vaultName = null, string? scheduleFrequency = null, string? dailyRetentionDays = null, bool triggerFirstBackup = true, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task SetupSqlHanaBackupAsync(string vmResourceId, string workloadType, string resourceGroup, string subscription, string location, string? vaultName = null, bool autoProtect = true, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task SetupAksBackupAsync(string clusterResourceId, string resourceGroup, string subscription, string location, string snapshotResourceGroup, string? vaultName = null, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task SetupDatasourceBackupAsync(string datasourceId, string workloadType, string resourceGroup, string subscription, string location, string? vaultName = null, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task SecureVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? securityLevel = null, string? resourceGuardId = null, string? keyVaultUri = null, string? keyName = null, string? logAnalyticsWorkspaceId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task SetupDisasterRecoveryAsync(string resourceIds, string primaryRegion, string resourceGroup, string subscription, string? vaultName = null, string? secondaryRegion = null, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task ComplianceRemediationAsync(string subscription, string? resourceGroup = null, string? resourceTypes = null, string? tagFilter = null, string? vaultName = null, string? policyName = null, bool autoRemediate = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task MigrateBackupConfigAsync(string sourceVaultName, string sourceVaultType, string sourceResourceGroup, string subscription, string targetResourceGroup, string? targetVaultName = null, string? targetLocation = null, bool decommissionSource = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + Task RansomwareRecoveryAsync(string resourceIds, string vaultName, string resourceGroup, string subscription, string infectionTimestamp, string? vaultType = null, bool restoreToIsolatedEnv = true, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs new file mode 100644 index 0000000000..956aa02d65 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Models; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public interface IDppBackupOperations +{ + // Existing methods + Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string location, string? sku, string? storageType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListVaultsAsync(string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ProtectItemAsync(string vaultName, string resourceGroup, string subscription, string datasourceId, string policyName, string? datasourceType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListProtectedItemsAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task TriggerBackupAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? expiry, string? backupType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task TriggerRestoreAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? recoveryPointId, string? targetResourceId, string? restoreLocation, string? pointInTime, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetPolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListPoliciesAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListJobsAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + + // New methods + Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? redundancy, string? softDelete, string? softDeleteRetentionDays, string? immutabilityState, string? identityType, string? tags, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? scheduleFrequency, string? scheduleTime, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? monthlyRetentionMonths, string? yearlyRetentionYears, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task UpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? scheduleFrequency, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task DeletePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task StopProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string mode, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ResumeProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ModifyProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? newPolicyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task UndeleteProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ConfigureImmutabilityAsync(string vaultName, string resourceGroup, string subscription, string immutabilityState, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ConfigureSoftDeleteAsync(string vaultName, string resourceGroup, string subscription, string softDeleteState, string? softDeleteRetentionDays, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task RunBackupHealthCheckAsync(string vaultName, string resourceGroup, string subscription, int? rpoThresholdHours, bool includeSecurityPosture, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs new file mode 100644 index 0000000000..bfc7be9e7e --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Models; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public interface IRsvBackupOperations +{ + // Existing methods + Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string location, string? sku, string? storageType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListVaultsAsync(string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ProtectItemAsync(string vaultName, string resourceGroup, string subscription, string datasourceId, string policyName, string? containerName, string? datasourceType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListProtectedItemsAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task TriggerBackupAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? expiry, string? backupType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task TriggerRestoreAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? containerName, string? targetResourceId, string? restoreLocation, string? stagingStorageAccountId, string? restoreMode, string? targetVmName, string? targetVnetId, string? targetSubnetId, string? targetDatabaseName, string? targetInstanceName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetPolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListPoliciesAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListJobsAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + + // New methods + Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? redundancy, string? softDelete, string? softDeleteRetentionDays, string? immutabilityState, string? identityType, string? tags, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? scheduleFrequency, string? scheduleTime, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? monthlyRetentionMonths, string? yearlyRetentionYears, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task UpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? scheduleFrequency, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task DeletePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task StopProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string mode, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ResumeProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ModifyProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? newPolicyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task UndeleteProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task CancelJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ConfigureImmutabilityAsync(string vaultName, string resourceGroup, string subscription, string immutabilityState, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ConfigureSoftDeleteAsync(string vaultName, string resourceGroup, string subscription, string softDeleteState, string? softDeleteRetentionDays, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task ConfigureCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task RunBackupHealthCheckAsync(string vaultName, string resourceGroup, string subscription, int? rpoThresholdHours, bool includeSecurityPosture, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + + // Workload container operations (SQL/HANA in IaaS VM) + Task> ListContainersAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task RegisterContainerAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task TriggerInquiryAsync(string vaultName, string resourceGroup, string subscription, string containerName, string? workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task> ListProtectableItemsAsync(string vaultName, string resourceGroup, string subscription, string? workloadType, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); + Task EnableAutoProtectionAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string instanceName, string policyName, string workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs new file mode 100644 index 0000000000..4035487868 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -0,0 +1,2065 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Core.Services.Azure; +using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.ResourceManager; +using Azure.ResourceManager.RecoveryServices; +using Azure.ResourceManager.RecoveryServices.Models; +using Azure.ResourceManager.RecoveryServicesBackup; +using Azure.ResourceManager.RecoveryServicesBackup.Models; +using Azure.ResourceManager.Resources; + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public class RsvBackupOperations(ITenantService tenantService) : BaseAzureService(tenantService), IRsvBackupOperations +{ + private const string VaultType = VaultTypeResolver.Rsv; + private const string FabricName = "Azure"; + + public async Task CreateVaultAsync( + string vaultName, string resourceGroup, string subscription, string location, + string? sku, string? storageType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(location), location)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var collection = rgResource.GetRecoveryServicesVaults(); + + var vaultSku = new RecoveryServicesSku(RecoveryServicesSkuName.Standard); + var vaultData = new RecoveryServicesVaultData(new AzureLocation(location)) + { + Sku = vaultSku, + Properties = new RecoveryServicesVaultProperties + { + PublicNetworkAccess = VaultPublicNetworkAccess.Enabled + } + }; + + var result = await collection.CreateOrUpdateAsync(WaitUntil.Completed, vaultName, vaultData, cancellationToken); + + return new VaultCreateResult( + result.Value.Id?.ToString(), + result.Value.Data.Name, + VaultType, + result.Value.Data.Location.Name, + result.Value.Data.Properties?.ProvisioningState); + } + + public async Task GetVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + return MapToVaultInfo(vault.Value.Data, resourceGroup); + } + + public async Task> ListVaultsAsync( + string subscription, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters((nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var subId = SubscriptionResource.CreateResourceIdentifier(subscription); + var subResource = armClient.GetSubscriptionResource(subId); + + var vaults = new List(); + await foreach (var vault in subResource.GetRecoveryServicesVaultsAsync(cancellationToken)) + { + var rg = vault.Id?.ResourceGroupName; + vaults.Add(MapToVaultInfo(vault.Data, rg)); + } + + return vaults; + } + + public async Task ProtectItemAsync( + string vaultName, string resourceGroup, string subscription, + string datasourceId, string policyName, string? containerName, + string? datasourceType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(datasourceId), datasourceId), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // Get the vault to determine its location + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); + var vaultLocation = vault.Value.Data.Location; + + var policyArmId = BackupProtectionPolicyResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, policyName); + + // Resolve the datasource profile + var profile = RsvDatasourceRegistry.ResolveOrDefault(datasourceType); + + // Check if this is a workload (SQL/HANA/ASE) protection request + if (profile.IsWorkloadType) + { + // For workload protection, containerName and datasourceId (protectable item name) are required + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException($"The --container parameter is required for {profile.FriendlyName} workload protection. Use 'azurebackup protectable_item_list' to discover containers and items."); + } + + // Bug #47: Detect if user passed an ARM resource ID instead of the protectable item name. + // For workloads, datasource-id must be the protectable item name (e.g., 'SAPHanaDatabase;instance;db') + // from 'protectableitem list', NOT the VM ARM resource ID. + if (datasourceId.StartsWith("/subscriptions/", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"For {profile.FriendlyName} workload protection, --datasource-id must be the protectable item name " + + $"(e.g., 'SAPHanaDatabase;instance;dbname'), not an ARM resource ID. " + + $"Use 'azurebackup protectableitem list' to discover protectable item names."); + } + + var protectedItemName = datasourceId; // For workloads, datasourceId is the protectable item name + var protectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + + // Construct the correct SDK protected item type based on profile + BackupGenericProtectedItem protectedItemProperties = profile.ProtectedItemType switch + { + RsvProtectedItemType.SapHanaDatabase => new VmWorkloadSapHanaDatabaseProtectedItem { PolicyId = policyArmId }, + _ => new VmWorkloadSqlDatabaseProtectedItem { PolicyId = policyArmId }, // SQL, ASE use the same type + }; + + var protectedItemData = new BackupProtectedItemData(vaultLocation) { Properties = protectedItemProperties }; + var protectedItemResource = armClient.GetBackupProtectedItemResource(protectedItemId); + var result = await protectedItemResource.UpdateAsync(WaitUntil.Started, protectedItemData, cancellationToken); + + var jobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "ConfigureBackup", cancellationToken); + jobId ??= ExtractOperationIdFromResponse(result.GetRawResponse()); + + return new ProtectResult("Accepted", protectedItemName, jobId, + jobId != null ? $"Workload protection initiated. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Workload protection initiated."); + } + + // Azure File Share protection flow + if (profile.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + var fsContainer = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId, datasourceType); + var fsProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId, datasourceType); + + // Trigger storage account inquiry so the vault discovers file shares + var containerId = BackupProtectionContainerResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, fsContainer); + var containerResource = armClient.GetBackupProtectionContainerResource(containerId); + try + { + await containerResource.InquireAsync(filter: null, cancellationToken); + // Wait briefly for inquiry to complete + await Task.Delay(5000, cancellationToken); + } + catch (RequestFailedException) + { + // Inquiry may fail if container is not yet registered; proceed anyway + } + + var fsProtectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, fsContainer, fsProtectedItemName); + + // Derive the source resource ID (the storage account ARM ID) + // We're already in the AzureFileShare block, so always extract the storage account ID + var parsedDatasourceId = new ResourceIdentifier(datasourceId); + var storageAccountId = RsvNamingHelper.GetStorageAccountId(parsedDatasourceId); + + var fsProtectedItemData = new BackupProtectedItemData(vaultLocation) + { + Properties = new FileshareProtectedItem + { + PolicyId = policyArmId, + SourceResourceId = new ResourceIdentifier(storageAccountId) + } + }; + + var fsProtectedItemResource = armClient.GetBackupProtectedItemResource(fsProtectedItemId); + var fsResult = await fsProtectedItemResource.UpdateAsync(WaitUntil.Started, fsProtectedItemData, cancellationToken); + + var fsJobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "ConfigureBackup", cancellationToken); + fsJobId ??= ExtractOperationIdFromResponse(fsResult.GetRawResponse()); + + return new ProtectResult("Accepted", fsProtectedItemName, fsJobId, + fsJobId != null ? $"File share protection initiated. Use 'azurebackup job get --job {fsJobId}' to monitor progress." : "File share protection initiated."); + } + + // Standard VM protection flow + // Trigger container discovery/refresh so the vault discovers the VM + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + await rgResource.RefreshProtectionContainerAsync(vaultName, FabricName, filter: null, cancellationToken); + + // Wait for container discovery to complete (refresh is async on the server side) + await Task.Delay(30000, cancellationToken); + + // Derive container name if not provided + var container = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId); + var vmProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId); + + var vmProtectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, container, vmProtectedItemName); + + var vmProtectedItemData = new BackupProtectedItemData(vaultLocation) + { + Properties = new IaasComputeVmProtectedItem + { + PolicyId = policyArmId, + SourceResourceId = new ResourceIdentifier(datasourceId) + } + }; + + var vmProtectedItemResource = armClient.GetBackupProtectedItemResource(vmProtectedItemId); + var vmResult = await vmProtectedItemResource.UpdateAsync(WaitUntil.Started, vmProtectedItemData, cancellationToken); + + // The Azure-AsyncOperation header contains an operation ID, not a job ID. + // We need to find the actual job by listing recent jobs. + var vmJobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "ConfigureBackup", cancellationToken); + vmJobId ??= ExtractOperationIdFromResponse(vmResult.GetRawResponse()); // Fallback to operation ID + + return new ProtectResult( + "Accepted", + vmProtectedItemName, + vmJobId, + vmJobId != null ? $"Protection initiated. Use 'azurebackup job get --job {vmJobId}' to monitor progress." : "Protection initiated."); + } + + public async Task GetProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // If container name is not provided, we need to list and find the item + if (string.IsNullOrEmpty(containerName)) + { + var items = await ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + var found = items.FirstOrDefault(i => i.Name.Equals(protectedItemName, StringComparison.OrdinalIgnoreCase)); + return found ?? throw new KeyNotFoundException($"Protected item '{protectedItemName}' not found in vault '{vaultName}'."); + } + + var itemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var itemResource = armClient.GetBackupProtectedItemResource(itemId); + var item = await itemResource.GetAsync(cancellationToken: cancellationToken); + + return MapToProtectedItemInfo(item.Value.Data); + } + + public async Task> ListProtectedItemsAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + + var items = new List(); + await foreach (var item in rgResource.GetBackupProtectedItemsAsync(vaultName, cancellationToken: cancellationToken)) + { + items.Add(MapToProtectedItemInfo(item.Data)); + } + + return items; + } + + public async Task TriggerBackupAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, string? expiry, + string? backupType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for triggering backup on an RSV protected item."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var itemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var itemResource = armClient.GetBackupProtectedItemResource(itemId); + + DateTimeOffset? expiryTime = null; + if (!string.IsNullOrEmpty(expiry) && DateTimeOffset.TryParse(expiry, out var parsed)) + { + expiryTime = parsed; + } + + var backupContent = new TriggerBackupContent(new AzureLocation(string.Empty)) + { + Properties = CreateBackupRequestContent(backupType, expiryTime, protectedItemName, containerName) + }; + + var result = await itemResource.TriggerBackupAsync(backupContent, cancellationToken); + + // The Azure-AsyncOperation header contains an operation ID, not a job ID. + // We need to find the actual backup job by listing recent jobs. + var jobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "Backup", cancellationToken); + jobId ??= ExtractOperationIdFromResponse(result); // Fallback to operation ID if job not found yet + + return new BackupTriggerResult( + "Accepted", + jobId, + jobId != null ? $"Backup triggered. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Backup triggered."); + } + + private static BackupContent CreateBackupRequestContent(string? backupType, DateTimeOffset? expiryTime, string? protectedItemName = null, string? containerName = null) + { + // If a specific backup type is provided (Full, Differential, Log), use workload backup content + if (!string.IsNullOrEmpty(backupType) && !backupType.Equals("Default", StringComparison.OrdinalIgnoreCase)) + { + return new WorkloadBackupContent + { + BackupType = new BackupType(backupType), + RecoveryPointExpireOn = expiryTime, + EnableCompression = true + }; + } + + // Auto-detect workload type from item/container naming conventions using the registry + var detectedProfile = RsvDatasourceRegistry.ResolveFromProtectedItemName(protectedItemName ?? string.Empty, containerName); + var isWorkload = detectedProfile.IsWorkloadType; + + if (isWorkload) + { + return new WorkloadBackupContent + { + BackupType = new BackupType("Full"), + RecoveryPointExpireOn = expiryTime, + EnableCompression = true + }; + } + + // Default: IaasVmBackupContent for VM workloads + return new IaasVmBackupContent + { + RecoveryPointExpireOn = expiryTime + }; + } + + public async Task TriggerRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string recoveryPointId, string? containerName, + string? targetResourceId, string? restoreLocation, string? stagingStorageAccountId, + string? restoreMode, string? targetVmName, string? targetVnetId, string? targetSubnetId, + string? targetDatabaseName, string? targetInstanceName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName), + (nameof(recoveryPointId), recoveryPointId)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for triggering restore on an RSV protected item."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // Get the existing protected item to determine type and extract SourceResourceId + var piId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var piResource = armClient.GetBackupProtectedItemResource(piId); + var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); + var existingProperties = existingItem.Value.Data.Properties; + + // Get vault location for restore region + var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); + var vault = await vaultResource.GetAsync(cancellationToken); + var vaultLocation = restoreLocation ?? vault.Value.Data.Location.ToString(); + + var rpResourceId = BackupRecoveryPointResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName, recoveryPointId); + var rpResource = armClient.GetBackupRecoveryPointResource(rpResourceId); + + RestoreContent restoreProperties; + + // Check if this is a workload (SQL/HANA) protected item + if (existingProperties is VmWorkloadSqlDatabaseProtectedItem) + { + // For SQL ALR, extract data directory paths from recovery point extended info + IList? sqlDataDirMappings = null; + if (!string.IsNullOrEmpty(targetDatabaseName)) + { + var rpData = await rpResource.GetAsync(cancellationToken: cancellationToken); + if (rpData.Value.Data.Properties is WorkloadSqlRecoveryPoint sqlRp + && sqlRp.ExtendedInfo?.DataDirectoryPaths is { } dataDirs) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + sqlDataDirMappings = []; + foreach (var dir in dataDirs) + { + if (string.IsNullOrEmpty(dir.Path) || string.IsNullOrEmpty(dir.LogicalName)) + { + continue; + } + + var sourceFileName = Path.GetFileNameWithoutExtension(dir.Path); + var sourceExtension = Path.GetExtension(dir.Path); + var sourceDir = Path.GetDirectoryName(dir.Path) ?? string.Empty; + var targetPath = Path.Combine(sourceDir, $"{sourceFileName}_{timestamp}{sourceExtension}"); + + sqlDataDirMappings.Add(new SqlDataDirectoryMapping + { + MappingType = dir.DirectoryType, + SourceLogicalName = dir.LogicalName, + SourcePath = dir.Path, + TargetPath = targetPath + }); + } + } + } + + restoreProperties = CreateWorkloadSqlRestoreContent(subscription, resourceGroup, vaultName, containerName, protectedItemName, targetDatabaseName, targetInstanceName, sqlDataDirMappings); + } + else if (existingProperties is VmWorkloadSapHanaDatabaseProtectedItem) + { + restoreProperties = CreateWorkloadSapHanaRestoreContent(targetResourceId, containerName, protectedItemName); + } + else + { + // Standard VM restore + var sourceResourceId = (existingProperties as IaasComputeVmProtectedItem)?.SourceResourceId + ?? (existingProperties as BackupGenericProtectedItem)?.SourceResourceId; + + // Determine restore mode: AlternateLocation, RestoreDisks, or OriginalLocation + var resolvedMode = !string.IsNullOrEmpty(restoreMode) + ? restoreMode + : !string.IsNullOrEmpty(targetResourceId) ? "RestoreDisks" : "OriginalLocation"; + + var recoveryType = resolvedMode switch + { + "AlternateLocation" => FileShareRecoveryType.AlternateLocation, + "RestoreDisks" => FileShareRecoveryType.RestoreDisks, + _ => FileShareRecoveryType.OriginalLocation + }; + + var vmRestoreContent = new IaasVmRestoreContent + { + RecoveryPointId = recoveryPointId, + RecoveryType = recoveryType, + SourceResourceId = sourceResourceId, + Region = new AzureLocation(vaultLocation), + OriginalStorageAccountOption = recoveryType == FileShareRecoveryType.OriginalLocation, + DoesCreateNewCloudService = false + }; + + if (!string.IsNullOrEmpty(stagingStorageAccountId)) + { + vmRestoreContent.StorageAccountId = new ResourceIdentifier(stagingStorageAccountId); + } + + if (recoveryType == FileShareRecoveryType.AlternateLocation) + { + // Full ALR: create a new VM at alternate location + if (string.IsNullOrEmpty(targetVmName) || string.IsNullOrEmpty(targetVnetId) || string.IsNullOrEmpty(targetSubnetId)) + { + throw new ArgumentException("AlternateLocation restore requires --target-vm-name, --target-vnet-id, and --target-subnet-id parameters."); + } + + vmRestoreContent.TargetResourceGroupId = !string.IsNullOrEmpty(targetResourceId) + ? new ResourceIdentifier(targetResourceId) + : new ResourceIdentifier($"/subscriptions/{subscription}/resourceGroups/{resourceGroup}"); + vmRestoreContent.TargetVirtualMachineId = new ResourceIdentifier( + $"/subscriptions/{subscription}/resourceGroups/{(targetResourceId != null ? new ResourceIdentifier(targetResourceId).Name : resourceGroup)}/providers/Microsoft.Compute/virtualMachines/{targetVmName}"); + vmRestoreContent.VirtualNetworkId = new ResourceIdentifier(targetVnetId); + vmRestoreContent.SubnetId = new ResourceIdentifier(targetSubnetId); + } + else if (recoveryType == FileShareRecoveryType.RestoreDisks && !string.IsNullOrEmpty(targetResourceId)) + { + // RestoreDisks: restore managed disks to target RG + vmRestoreContent.TargetResourceGroupId = new ResourceIdentifier(targetResourceId); + } + + restoreProperties = vmRestoreContent; + } + + var restoreContent = new TriggerRestoreContent(new AzureLocation(vaultLocation)) + { + Properties = restoreProperties + }; + + var result = await rpResource.TriggerRestoreAsync(WaitUntil.Started, restoreContent, cancellationToken); + + // The Azure-AsyncOperation header contains an operation ID, not a job ID. + var jobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "Restore", cancellationToken); + jobId ??= ExtractOperationIdFromResponse(result.GetRawResponse()); // Fallback to operation ID + + return new RestoreTriggerResult( + "Accepted", + jobId, + jobId != null ? $"Restore triggered. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Restore triggered."); + } + + public async Task GetPolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetBackupProtectionPolicyResource(policyId); + var policy = await policyResource.GetAsync(cancellationToken); + + return MapToPolicyInfo(policy.Value.Data); + } + + public async Task> ListPoliciesAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + + var policies = new List(); + await foreach (var policy in rgResource.GetBackupProtectionPolicies(vaultName).GetAllAsync(cancellationToken: cancellationToken)) + { + policies.Add(MapToPolicyInfo(policy.Data)); + } + + return policies; + } + + public async Task GetJobAsync( + string vaultName, string resourceGroup, string subscription, + string jobId, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(jobId), jobId)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var jobResourceId = BackupJobResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, jobId); + var jobResource = armClient.GetBackupJobResource(jobResourceId); + var job = await jobResource.GetAsync(cancellationToken); + + return MapToJobInfo(job.Value.Data); + } + + public async Task> ListJobsAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + + var jobs = new List(); + await foreach (var job in rgResource.GetBackupJobs(vaultName).GetAllAsync(cancellationToken: cancellationToken)) + { + jobs.Add(MapToJobInfo(job.Data)); + } + + return jobs; + } + + public async Task GetRecoveryPointAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string recoveryPointId, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName), + (nameof(recoveryPointId), recoveryPointId)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for RSV recovery point operations."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rpId = BackupRecoveryPointResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName, recoveryPointId); + var rpResource = armClient.GetBackupRecoveryPointResource(rpId); + var rp = await rpResource.GetAsync(cancellationToken); + + return MapToRecoveryPointInfo(rp.Value.Data); + } + + public async Task> ListRecoveryPointsAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for RSV recovery point operations."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var itemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var itemResource = armClient.GetBackupProtectedItemResource(itemId); + var collection = itemResource.GetBackupRecoveryPoints(); + + var points = new List(); + await foreach (var rp in collection.GetAllAsync(cancellationToken: cancellationToken)) + { + points.Add(MapToRecoveryPointInfo(rp.Data)); + } + + return points; + } + + // ── New methods ── + + public async Task UpdateVaultAsync( + string vaultName, string resourceGroup, string subscription, + string? redundancy, string? softDelete, string? softDeleteRetentionDays, + string? immutabilityState, string? identityType, string? tags, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + var patchData = new RecoveryServicesVaultPatch(vault.Value.Data.Location); + + if (!string.IsNullOrEmpty(identityType)) + { + patchData.Identity = new Azure.ResourceManager.Models.ManagedServiceIdentity( + identityType.Equals("SystemAssigned", StringComparison.OrdinalIgnoreCase) + ? Azure.ResourceManager.Models.ManagedServiceIdentityType.SystemAssigned + : Azure.ResourceManager.Models.ManagedServiceIdentityType.None); + } + + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Vault '{vaultName}' updated successfully."); + } + + public async Task DeleteVaultAsync( + string vaultName, string resourceGroup, string subscription, + bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + await vaultResource.DeleteAsync(WaitUntil.Completed, cancellationToken); + + return new OperationResult("Succeeded", null, $"Vault '{vaultName}' deleted successfully."); + } + + public async Task UpdatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? scheduleFrequency, + string? dailyRetentionDays, string? weeklyRetentionWeeks, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // Fetch the existing policy — will throw RequestFailedException (404) if it doesn't exist + var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetBackupProtectionPolicyResource(policyId); + var existingPolicy = await policyResource.GetAsync(cancellationToken); + var policyData = existingPolicy.Value.Data; + + // Modify only the requested fields on the existing policy + if (policyData.Properties is FileShareProtectionPolicy fsPolicy) + { + UpdateFileSharePolicyRetention(fsPolicy, dailyRetentionDays, weeklyRetentionWeeks); + } + else if (policyData.Properties is IaasVmProtectionPolicy vmPolicy) + { + UpdateVmPolicyRetention(vmPolicy, dailyRetentionDays, weeklyRetentionWeeks); + UpdateVmPolicySchedule(vmPolicy, scheduleFrequency); + } + else if (policyData.Properties is VmWorkloadProtectionPolicy workloadPolicy) + { + UpdateWorkloadPolicyRetention(workloadPolicy, dailyRetentionDays); + } + + // Fetch vault location for PUT — the GET response data may lack location, causing NullRef in SDK constructor + var vaultResourceId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResourceId); + var vault = await vaultResource.GetAsync(cancellationToken); + var vaultLocation = vault.Value.Data.Location; + + // PUT the modified policy back using fresh data with vault location + var freshPolicyData = new BackupProtectionPolicyData(vaultLocation) + { + Properties = policyData.Properties + }; + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var policyCollection = rgResource.GetBackupProtectionPolicies(vaultName); + + try + { + await policyCollection.CreateOrUpdateAsync(WaitUntil.Completed, policyName, freshPolicyData, cancellationToken); + } + catch (NullReferenceException) + { + // SDK v1.3.0 bug: BackupProtectionPolicyResource constructor throws NullRef + // when deserializing certain policy types (e.g. VM policies with enhanced schedules). + // The PUT REST call itself succeeds — verify by re-fetching the updated policy. + var verifyPolicy = await policyResource.GetAsync(cancellationToken); + if (verifyPolicy.Value?.Data?.Properties == null) + { + throw new InvalidOperationException($"Policy update for '{policyName}' could not be verified after an SDK deserialization error."); + } + } + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' updated in vault '{vaultName}'."); + } + + private static void UpdateVmPolicyRetention(IaasVmProtectionPolicy vmPolicy, string? dailyRetentionDays, string? weeklyRetentionWeeks) + { + if (vmPolicy.RetentionPolicy is LongTermRetentionPolicy longTermRetention) + { + if (int.TryParse(dailyRetentionDays, out var days) && longTermRetention.DailySchedule != null) + { + longTermRetention.DailySchedule.RetentionDuration = new RetentionDuration + { + Count = days, + DurationType = RetentionDurationType.Days + }; + } + + if (int.TryParse(weeklyRetentionWeeks, out var weeks) && longTermRetention.WeeklySchedule != null) + { + longTermRetention.WeeklySchedule.RetentionDuration = new RetentionDuration + { + Count = weeks, + DurationType = RetentionDurationType.Weeks + }; + } + } + } + + private static void UpdateVmPolicySchedule(IaasVmProtectionPolicy vmPolicy, string? scheduleFrequency) + { + if (!string.IsNullOrEmpty(scheduleFrequency) && vmPolicy.SchedulePolicy is SimpleSchedulePolicy simpleSchedule) + { + if (Enum.TryParse(scheduleFrequency, true, out var frequency)) + { + simpleSchedule.ScheduleRunFrequency = frequency; + } + } + } + + private static void UpdateFileSharePolicyRetention(FileShareProtectionPolicy fsPolicy, string? dailyRetentionDays, string? weeklyRetentionWeeks) + { + if (fsPolicy.RetentionPolicy is LongTermRetentionPolicy longTermRetention) + { + if (int.TryParse(dailyRetentionDays, out var days) && longTermRetention.DailySchedule != null) + { + longTermRetention.DailySchedule.RetentionDuration = new RetentionDuration + { + Count = days, + DurationType = RetentionDurationType.Days + }; + } + + if (int.TryParse(weeklyRetentionWeeks, out var weeks) && longTermRetention.WeeklySchedule != null) + { + longTermRetention.WeeklySchedule.RetentionDuration = new RetentionDuration + { + Count = weeks, + DurationType = RetentionDurationType.Weeks + }; + } + } + } + + private static void UpdateWorkloadPolicyRetention(VmWorkloadProtectionPolicy workloadPolicy, string? dailyRetentionDays) + { + if (!int.TryParse(dailyRetentionDays, out var days)) + { + return; + } + + foreach (var subPolicy in workloadPolicy.SubProtectionPolicy) + { + if (subPolicy.RetentionPolicy is LongTermRetentionPolicy longTermRetention && longTermRetention.DailySchedule != null) + { + longTermRetention.DailySchedule.RetentionDuration = new RetentionDuration + { + Count = days, + DurationType = RetentionDurationType.Days + }; + } + else if (subPolicy.RetentionPolicy is SimpleRetentionPolicy simpleRetention) + { + simpleRetention.RetentionDuration = new RetentionDuration + { + Count = days, + DurationType = RetentionDurationType.Days + }; + } + } + } + + public async Task CreatePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string workloadType, + string? scheduleFrequency, string? scheduleTime, + string? dailyRetentionDays, string? weeklyRetentionWeeks, + string? monthlyRetentionMonths, string? yearlyRetentionYears, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName), + (nameof(workloadType), workloadType)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultResourceId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResourceId); + var vault = await vaultResource.GetAsync(cancellationToken); + var vaultLocation = vault.Value.Data.Location; + + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var policyCollection = rgResource.GetBackupProtectionPolicies(vaultName); + + var retentionDays = int.TryParse(dailyRetentionDays, out var dd) ? dd : 30; + + // Parse schedule time or default to 2:00 AM UTC + var scheduleDateTime = DateTimeOffset.TryParse(scheduleTime, out var st) ? st : new DateTimeOffset(DateTime.UtcNow.Date.AddHours(2), TimeSpan.Zero); + var scheduleRunTime = new DateTimeOffset(scheduleDateTime.Year, scheduleDateTime.Month, scheduleDateTime.Day, + scheduleDateTime.Hour, scheduleDateTime.Minute, 0, TimeSpan.Zero); + + BackupGenericProtectionPolicy policyProperties; + + // Resolve the profile to determine policy type + var profile = RsvDatasourceRegistry.ResolveOrDefault(workloadType); + + if (profile.PolicyType == RsvPolicyType.VmWorkload) + { + // SQL/HANA/ASE workload policy + var fullSchedule = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; + fullSchedule.ScheduleRunTimes.Add(scheduleRunTime); + + var fullRetention = new DailyRetentionSchedule { RetentionDuration = new RetentionDuration { Count = retentionDays, DurationType = RetentionDurationType.Days } }; + fullRetention.RetentionTimes.Add(scheduleRunTime); + + var fullSubPolicy = new SubProtectionPolicy + { + PolicyType = new SubProtectionPolicyType("Full"), + SchedulePolicy = fullSchedule, + RetentionPolicy = new LongTermRetentionPolicy { DailySchedule = fullRetention } + }; + + var logSubPolicy = new SubProtectionPolicy + { + PolicyType = new SubProtectionPolicyType("Log"), + SchedulePolicy = new LogSchedulePolicy { ScheduleFrequencyInMins = 60 }, + RetentionPolicy = new SimpleRetentionPolicy { RetentionDuration = new RetentionDuration { Count = 15, DurationType = RetentionDurationType.Days } } + }; + + var wlPolicy = new VmWorkloadProtectionPolicy + { + WorkLoadType = new BackupWorkloadType(profile.ApiWorkloadType ?? "SQLDataBase"), + Settings = new BackupCommonSettings + { + TimeZone = "UTC", + IsCompression = false, + IsSqlCompression = false + } + }; + wlPolicy.SubProtectionPolicy.Add(fullSubPolicy); + wlPolicy.SubProtectionPolicy.Add(logSubPolicy); + + policyProperties = wlPolicy; + } + else if (profile.PolicyType == RsvPolicyType.AzureFileShare) + { + // Azure File Share policy + var schedulePolicy = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; + schedulePolicy.ScheduleRunTimes.Add(scheduleRunTime); + + var dailyRetention = new DailyRetentionSchedule { RetentionDuration = new RetentionDuration { Count = retentionDays, DurationType = RetentionDurationType.Days } }; + dailyRetention.RetentionTimes.Add(scheduleRunTime); + + policyProperties = new FileShareProtectionPolicy + { + WorkLoadType = new BackupWorkloadType("AzureFileShare"), + SchedulePolicy = schedulePolicy, + RetentionPolicy = new LongTermRetentionPolicy { DailySchedule = dailyRetention } + }; + } + else + { + // Standard VM policy + var schedulePolicy = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; + schedulePolicy.ScheduleRunTimes.Add(scheduleRunTime); + + var dailyRetention = new DailyRetentionSchedule { RetentionDuration = new RetentionDuration { Count = retentionDays, DurationType = RetentionDurationType.Days } }; + dailyRetention.RetentionTimes.Add(scheduleRunTime); + + policyProperties = new IaasVmProtectionPolicy + { + SchedulePolicy = schedulePolicy, + RetentionPolicy = new LongTermRetentionPolicy { DailySchedule = dailyRetention } + }; + } + + var policyData = new BackupProtectionPolicyData(vaultLocation) + { + Properties = policyProperties + }; + + await policyCollection.CreateOrUpdateAsync(WaitUntil.Completed, policyName, policyData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' created in vault '{vaultName}'."); + } + + public async Task DeletePolicyAsync( + string vaultName, string resourceGroup, string subscription, + string policyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(policyName), policyName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetBackupProtectionPolicyResource(policyId); + await policyResource.DeleteAsync(WaitUntil.Completed, cancellationToken); + + return new OperationResult("Succeeded", null, $"Policy '{policyName}' deleted from vault '{vaultName}'."); + } + + public async Task StopProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string mode, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName), + (nameof(mode), mode)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for RSV protection operations."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + if (mode.Equals("DeleteData", StringComparison.OrdinalIgnoreCase)) + { + var itemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var itemResource = armClient.GetBackupProtectedItemResource(itemId); + await itemResource.DeleteAsync(WaitUntil.Started, cancellationToken); + return new OperationResult("Accepted", null, "Protection stopped and data deletion initiated."); + } + + // RetainData mode - update with ProtectionState = ProtectionStopped + var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); + var vault = await vaultResource.GetAsync(cancellationToken); + + var piId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var piResource = armClient.GetBackupProtectedItemResource(piId); + + // Get existing item to determine type and construct the correct SDK type via profile matching + var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); + BackupGenericProtectedItem stopProps = existingItem.Value.Data.Properties switch + { + VmWorkloadSqlDatabaseProtectedItem => new VmWorkloadSqlDatabaseProtectedItem { ProtectionState = BackupProtectionState.ProtectionStopped }, + VmWorkloadSapHanaDatabaseProtectedItem => new VmWorkloadSapHanaDatabaseProtectedItem { ProtectionState = BackupProtectionState.ProtectionStopped }, + FileshareProtectedItem => new FileshareProtectedItem { ProtectionState = BackupProtectionState.ProtectionStopped }, + _ => new IaasComputeVmProtectedItem { ProtectionState = BackupProtectionState.ProtectionStopped } + }; + + var data = new BackupProtectedItemData(vault.Value.Data.Location) { Properties = stopProps }; + + await piResource.UpdateAsync(WaitUntil.Started, data, cancellationToken); + return new OperationResult("Accepted", null, "Protection stopped with data retained."); + } + + public async Task ResumeProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, string? policyName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for RSV protection operations."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); + var vault = await vaultResource.GetAsync(cancellationToken); + + var piId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var piResource = armClient.GetBackupProtectedItemResource(piId); + + // Get existing item to determine type and construct the correct SDK type via profile matching + var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); + ResourceIdentifier? policyArmId = null; + if (!string.IsNullOrEmpty(policyName)) + { + policyArmId = BackupProtectionPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + } + + BackupGenericProtectedItem resumeProps = existingItem.Value.Data.Properties switch + { + VmWorkloadSqlDatabaseProtectedItem => new VmWorkloadSqlDatabaseProtectedItem { PolicyId = policyArmId }, + VmWorkloadSapHanaDatabaseProtectedItem => new VmWorkloadSapHanaDatabaseProtectedItem { PolicyId = policyArmId }, + FileshareProtectedItem => new FileshareProtectedItem { PolicyId = policyArmId }, + _ => new IaasComputeVmProtectedItem { PolicyId = policyArmId } + }; + + var data = new BackupProtectedItemData(vault.Value.Data.Location) { Properties = resumeProps }; + await piResource.UpdateAsync(WaitUntil.Started, data, cancellationToken); + + return new OperationResult("Accepted", null, "Protection resumed."); + } + + public async Task ModifyProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, string? newPolicyName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(protectedItemName), protectedItemName)); + + if (string.IsNullOrEmpty(containerName)) + { + throw new ArgumentException("The --container parameter is required for RSV protection operations."); + } + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); + var vault = await vaultResource.GetAsync(cancellationToken); + + var piId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); + var piResource = armClient.GetBackupProtectedItemResource(piId); + + // Get existing protected item to determine type and extract SourceResourceId + var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); + var existingProperties = existingItem.Value.Data.Properties; + + // Bug #49: Check item state before attempting modify — IRPending items cannot be modified. + // ProtectionState is on concrete types, not the base BackupGenericProtectedItem. + var protectionState = GetProtectionState(existingProperties); + if (string.Equals(protectionState, "IRPending", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + $"Cannot modify protection for '{protectedItemName}' because it is in '{protectionState}' state. " + + $"The initial replication (IR) must complete before the policy can be changed. " + + $"Use 'azurebackup job list' to monitor the IR job, then retry after it completes."); + } + + ResourceIdentifier? policyArmId = null; + if (!string.IsNullOrEmpty(newPolicyName)) + { + policyArmId = BackupProtectionPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, newPolicyName); + } + + BackupGenericProtectedItem modifyProps; + if (existingProperties is VmWorkloadSqlDatabaseProtectedItem) + { + modifyProps = new VmWorkloadSqlDatabaseProtectedItem { PolicyId = policyArmId }; + } + else if (existingProperties is VmWorkloadSapHanaDatabaseProtectedItem) + { + modifyProps = new VmWorkloadSapHanaDatabaseProtectedItem { PolicyId = policyArmId }; + } + else if (existingProperties is FileshareProtectedItem) + { + modifyProps = new FileshareProtectedItem { PolicyId = policyArmId }; + } + else + { + var sourceResourceId = (existingProperties as IaasComputeVmProtectedItem)?.SourceResourceId + ?? (existingProperties as BackupGenericProtectedItem)?.SourceResourceId; + + // If SourceResourceId not available from cast, derive from container name + if (sourceResourceId is null && !string.IsNullOrEmpty(containerName)) + { + var parts = containerName.Split(';'); + var vmName = parts[^1]; + var vmRg = parts[^2]; + sourceResourceId = new ResourceIdentifier( + $"/subscriptions/{subscription}/resourceGroups/{vmRg}/providers/Microsoft.Compute/virtualMachines/{vmName}"); + } + + modifyProps = new IaasComputeVmProtectedItem + { + SourceResourceId = sourceResourceId, + PolicyId = policyArmId + }; + } + + var data = new BackupProtectedItemData(vault.Value.Data.Location) { Properties = modifyProps }; + await piResource.UpdateAsync(WaitUntil.Started, data, cancellationToken); + + return new OperationResult("Accepted", null, $"Protection modified. Policy changed to '{newPolicyName}'."); + } + + public Task UndeleteProtectedItemAsync( + string vaultName, string resourceGroup, string subscription, + string protectedItemName, string? containerName, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + // RSV undelete is handled via support request or portal; SDK doesn't expose a direct undelete for RSV + return Task.FromResult(new OperationResult("NotSupported", null, "Undelete for RSV protected items requires Azure portal or support request. Use soft-delete recovery instead.")); + } + + public async Task CancelJobAsync( + string vaultName, string resourceGroup, string subscription, + string jobId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(jobId), jobId)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var jobResourceId = BackupJobResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, jobId); + var jobResource = armClient.GetBackupJobResource(jobResourceId); + await jobResource.TriggerJobCancellationAsync(cancellationToken); + + return new OperationResult("Accepted", null, $"Job '{jobId}' cancellation triggered."); + } + + public async Task ConfigureImmutabilityAsync( + string vaultName, string resourceGroup, string subscription, + string immutabilityState, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(immutabilityState), immutabilityState)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + var patchData = new RecoveryServicesVaultPatch(vault.Value.Data.Location) + { + Properties = new RecoveryServicesVaultProperties + { + SecuritySettings = new RecoveryServicesSecuritySettings + { + ImmutabilityState = new ImmutabilityState(immutabilityState) + } + } + }; + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Immutability set to '{immutabilityState}' for vault '{vaultName}'"); + } + + public async Task ConfigureSoftDeleteAsync( + string vaultName, string resourceGroup, string subscription, + string softDeleteState, string? softDeleteRetentionDays, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(softDeleteState), softDeleteState)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + var softDeleteSettings = new RecoveryServicesSoftDeleteSettings + { + SoftDeleteState = new RecoveryServicesSoftDeleteState(softDeleteState) + }; + + if (int.TryParse(softDeleteRetentionDays, out var retentionDays)) + { + softDeleteSettings.SoftDeleteRetentionPeriodInDays = retentionDays; + } + + var patchData = new RecoveryServicesVaultPatch(vault.Value.Data.Location) + { + Properties = new RecoveryServicesVaultProperties + { + SecuritySettings = new RecoveryServicesSecuritySettings + { + SoftDeleteSettings = softDeleteSettings + } + } + }; + await vaultResource.UpdateAsync(WaitUntil.Completed, patchData, cancellationToken); + + return new OperationResult("Succeeded", null, $"Soft delete set to '{softDeleteState}' for vault '{vaultName}'"); + } + + public async Task ConfigureCrossRegionRestoreAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // CRR must be enabled via the BackupResourceConfig sub-resource (backupstorageconfig), + // NOT via the vault-level PATCH which returns CloudInternalError. + var configResourceId = BackupResourceConfigResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var configResource = armClient.GetBackupResourceConfigResource(configResourceId); + var currentConfig = await configResource.GetAsync(cancellationToken); + + // Update the data and use CreateOrUpdate via the collection + // (BackupResourceConfigResource.UpdateAsync has an SDK NullRef bug in its constructor) + var data = currentConfig.Value.Data; + data.Properties.EnableCrossRegionRestore = true; + + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var collection = rgResource.GetBackupResourceConfigs(); + await collection.CreateOrUpdateAsync(WaitUntil.Completed, vaultName, data, cancellationToken); + + return new OperationResult("Succeeded", null, $"Cross-Region Restore enabled for vault '{vaultName}'."); + } + + public async Task RunBackupHealthCheckAsync( + string vaultName, string resourceGroup, string subscription, + int? rpoThresholdHours, bool includeSecurityPosture, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + // Get vault info + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); + var vault = await vaultResource.GetAsync(cancellationToken); + + // List protected items and check health + var items = await ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + var rpoThreshold = rpoThresholdHours ?? 24; + var now = DateTimeOffset.UtcNow; + + var details = new List(); + int healthy = 0, unhealthy = 0, breachingRpo = 0; + + foreach (var item in items) + { + var rpoBreached = item.LastBackupTime.HasValue && (now - item.LastBackupTime.Value).TotalHours > rpoThreshold; + if (rpoBreached) breachingRpo++; + + var isHealthy = item.ProtectionStatus?.Contains("Protected", StringComparison.OrdinalIgnoreCase) == true && !rpoBreached; + if (isHealthy) healthy++; else unhealthy++; + + details.Add(new HealthCheckItemDetail( + item.Name, item.ProtectionStatus, isHealthy ? "Healthy" : "Unhealthy", + item.LastBackupTime, rpoBreached)); + } + + return new HealthCheckResult( + vaultName, VaultType, items.Count, healthy, unhealthy, breachingRpo, + vault.Value.Data.Properties?.SecuritySettings?.SoftDeleteSettings?.SoftDeleteState?.ToString(), + vault.Value.Data.Properties?.SecuritySettings?.ImmutabilityState?.ToString(), + null, details); + } + + private static BackupVaultInfo MapToVaultInfo(RecoveryServicesVaultData data, string? resourceGroup) + { + return new BackupVaultInfo( + data.Id?.ToString(), + data.Name, + VaultType, + data.Location.Name, + resourceGroup, + data.Properties?.ProvisioningState, + data.Sku?.Name.ToString(), + null, + data.Tags?.ToDictionary(t => t.Key, t => t.Value)); + } + + private static ContainerInfo MapToContainerInfo(BackupProtectionContainerData data) + { + var properties = data.Properties; + string? workloadType = null; + string? sourceResourceId = null; + DateTimeOffset? lastUpdatedTime = null; + + if (properties is VmAppContainerProtectionContainer vmAppContainer) + { + workloadType = vmAppContainer.WorkloadType?.ToString(); + sourceResourceId = vmAppContainer.SourceResourceId?.ToString(); + lastUpdatedTime = vmAppContainer.LastUpdatedOn; + } + else if (properties is IaasVmContainer iaasVmContainer) + { + workloadType = "AzureVM"; + sourceResourceId = iaasVmContainer.VirtualMachineId?.ToString(); + } + + return new ContainerInfo( + data.Name, + properties?.FriendlyName, + properties?.RegistrationStatus, + properties?.HealthStatus, + properties?.ProtectableObjectType, + properties?.BackupManagementType?.ToString(), + sourceResourceId, + workloadType, + lastUpdatedTime); + } + + private static ProtectedItemInfo MapToProtectedItemInfo(BackupProtectedItemData data) + { + string? protectionStatus = null; + string? datasourceType = null; + string? datasourceId = null; + string? policyName = null; + DateTimeOffset? lastBackupTime = null; + string? container = null; + + if (data.Properties is BackupGenericProtectedItem genericItem) + { + datasourceType = genericItem.WorkloadType?.ToString(); + datasourceId = genericItem.SourceResourceId?.ToString(); + policyName = genericItem.PolicyId?.Name; + container = genericItem.ContainerName; + + if (genericItem is IaasVmProtectedItem vmItem) + { + protectionStatus = vmItem.ProtectionState?.ToString(); + lastBackupTime = vmItem.LastBackupOn; + } + else if (genericItem is VmWorkloadProtectedItem workloadItem) + { + protectionStatus = workloadItem.ProtectionState?.ToString(); + lastBackupTime = workloadItem.LastBackupOn; + datasourceType = workloadItem.WorkloadType?.ToString(); + } + } + + return new ProtectedItemInfo( + data.Id?.ToString(), + data.Name, + VaultType, + protectionStatus, + datasourceType, + datasourceId, + policyName, + lastBackupTime, + container); + } + + private static BackupPolicyInfo MapToPolicyInfo(BackupProtectionPolicyData data) + { + string? workloadType = null; + int? protectedItemsCount = null; + + if (data.Properties is BackupGenericProtectionPolicy genericPolicy) + { + protectedItemsCount = genericPolicy.ProtectedItemsCount; + } + + return new BackupPolicyInfo( + data.Id?.ToString(), + data.Name, + VaultType, + workloadType != null ? [workloadType] : null, + protectedItemsCount); + } + + private static BackupJobInfo MapToJobInfo(BackupJobData data) + { + string? operation = null; + string? status = null; + DateTimeOffset? startTime = null; + DateTimeOffset? endTime = null; + string? entityFriendlyName = null; + + if (data.Properties is BackupGenericJob genericJob) + { + operation = genericJob.Operation; + status = genericJob.Status; + startTime = genericJob.StartOn; + endTime = genericJob.EndOn; + entityFriendlyName = genericJob.EntityFriendlyName; + } + + return new BackupJobInfo( + data.Id?.ToString(), + data.Name, + VaultType, + operation, + status, + startTime, + endTime, + null, + entityFriendlyName); + } + + private static RecoveryPointInfo MapToRecoveryPointInfo(BackupRecoveryPointData data) + { + DateTimeOffset? rpTime = null; + string? rpType = null; + + if (data.Properties is IaasVmRecoveryPoint vmRp) + { + rpType = vmRp.RecoveryPointType; + rpTime = vmRp.RecoveryPointOn; + } + else if (data.Properties is WorkloadRecoveryPoint workloadRp) + { + rpType = workloadRp.RestorePointType?.ToString(); + rpTime = workloadRp.RecoveryPointCreatedOn; + } + else if (data.Properties is GenericRecoveryPoint genRp) + { + rpType = genRp.RecoveryPointType; + rpTime = genRp.RecoveryPointOn; + } + + return new RecoveryPointInfo( + data.Id?.ToString(), + data.Name, + VaultType, + rpTime, + rpType); + } + + private static string? ExtractOperationIdFromResponse(Response response) + { + if (response.Headers.TryGetValue("Azure-AsyncOperation", out var asyncOpUrl) && !string.IsNullOrEmpty(asyncOpUrl)) + { + var uri = new Uri(asyncOpUrl); + var segments = uri.AbsolutePath.Split('/'); + return segments.Length > 0 ? segments[^1] : null; + } + + return null; + } + + private static async Task FindLatestJobIdAsync( + ArmClient armClient, string subscription, string resourceGroup, + string vaultName, string operationType, CancellationToken cancellationToken) + { + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + var jobCollection = rgResource.GetBackupJobs(vaultName); + + // Look for the most recent job of the given operation type started within the last minute + await foreach (var job in jobCollection.GetAllAsync(cancellationToken: cancellationToken)) + { + if (job.Data.Properties is BackupGenericJob genericJob) + { + if (genericJob.StartOn.HasValue && + genericJob.StartOn.Value > DateTimeOffset.UtcNow.AddMinutes(-2) && + string.Equals(genericJob.Operation, operationType, StringComparison.OrdinalIgnoreCase) && + string.Equals(genericJob.Status, "InProgress", StringComparison.OrdinalIgnoreCase)) + { + return job.Data.Name; + } + } + } + + return null; + } + + private static bool IsWorkloadType(string? datasourceType) + { + var profile = RsvDatasourceRegistry.Resolve(datasourceType); + return profile?.IsWorkloadType ?? false; + } + + private static RestoreContent CreateWorkloadSqlRestoreContent( + string subscription, string resourceGroup, string vaultName, + string containerName, string protectedItemName, string? targetDatabaseName, string? targetInstanceName, + IList? dataDirectoryMappings) + { + if (!string.IsNullOrEmpty(targetDatabaseName)) + { + // ALR: restore to a different database on the same or different SQL instance + var resolvedInstanceName = targetInstanceName ?? "mssqlserver"; + + // Derive VM ARM ID from container name (format: VMAppContainer;Compute;{RG};{VMName}) + var containerParts = containerName.Split(';'); + var vmResourceGroup = containerParts.Length > 2 ? containerParts[2] : resourceGroup; + var vmName = containerParts.Length > 3 ? containerParts[3] : string.Empty; + var vmId = new ResourceIdentifier( + $"/subscriptions/{subscription}/resourceGroups/{vmResourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}"); + + // ContainerId must be the full ARM resource ID with lowercase container name + var fullContainerId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.RecoveryServices/vaults/{vaultName}/backupFabrics/{FabricName}/protectionContainers/{containerName.ToLowerInvariant()}"; + + // DatabaseName format: {INSTANCE_UPPER}/SQLInstance;{instance_lower} (target SQL instance path) + var databaseName = $"{resolvedInstanceName.ToUpperInvariant()}/SQLInstance;{resolvedInstanceName.ToLowerInvariant()}"; + + var content = new WorkloadSqlRestoreContent + { + RecoveryType = FileShareRecoveryType.AlternateLocation, + SourceResourceId = vmId, + TargetVirtualMachineId = vmId, + ShouldUseAlternateTargetLocation = true, + IsNonRecoverable = false, + TargetInfo = new TargetRestoreInfo + { + OverwriteOption = RestoreOverwriteOption.Overwrite, + ContainerId = fullContainerId, + DatabaseName = databaseName + } + }; + + // Add alternate directory paths for SQL data/log file mapping + if (dataDirectoryMappings != null) + { + foreach (var mapping in dataDirectoryMappings) + { + content.AlternateDirectoryPaths.Add(mapping); + } + } + + return content; + } + + return new WorkloadSqlRestoreContent + { + RecoveryType = FileShareRecoveryType.OriginalLocation, + ShouldUseAlternateTargetLocation = false + }; + } + + private static RestoreContent CreateWorkloadSapHanaRestoreContent( + string? targetResourceId, string containerName, string protectedItemName) + { + if (!string.IsNullOrEmpty(targetResourceId)) + { + return new WorkloadSapHanaRestoreContent + { + RecoveryType = FileShareRecoveryType.AlternateLocation, + TargetInfo = new TargetRestoreInfo + { + OverwriteOption = RestoreOverwriteOption.FailOnConflict, + ContainerId = containerName, + DatabaseName = protectedItemName + } + }; + } + + return new WorkloadSapHanaRestoreContent + { + RecoveryType = FileShareRecoveryType.OriginalLocation + }; + } + + public async Task> ListContainersAsync( + string vaultName, string resourceGroup, string subscription, + string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + + // The REST API requires a backupManagementType filter, so query the main types + string[] backupManagementTypes = ["AzureWorkload", "AzureIaasVM", "AzureStorage", "MAB", "DPM"]; + var containers = new List(); + + foreach (var bmType in backupManagementTypes) + { + var filter = $"backupManagementType eq '{bmType}'"; + try + { + await foreach (var container in rgResource.GetBackupProtectionContainersAsync(vaultName, filter, cancellationToken)) + { + containers.Add(MapToContainerInfo(container.Data)); + } + } + catch (Azure.RequestFailedException ex) when (ex.Status == 400) + { + // Some backup management types may not be supported for all vault configurations; skip them + continue; + } + } + + return containers; + } + + public async Task RegisterContainerAsync( + string vaultName, string resourceGroup, string subscription, + string vmResourceId, string workloadType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(vmResourceId), vmResourceId), + (nameof(workloadType), workloadType)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + var vmId = new ResourceIdentifier(vmResourceId); + var containerName = $"VMAppContainer;Compute;{vmId.ResourceGroupName};{vmId.Name}"; + + var containerId = BackupProtectionContainerResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName); + + var containerResource = armClient.GetBackupProtectionContainerResource(containerId); + + // Get vault location for the container data + var vaultResId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResId); + var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); + + var containerData = new BackupProtectionContainerData(vault.Value.Data.Location) + { + Properties = new VmAppContainerProtectionContainer + { + SourceResourceId = vmId, + BackupManagementType = BackupManagementType.AzureWorkload, + WorkloadType = new BackupWorkloadType(workloadType) + } + }; + + var result = await containerResource.UpdateAsync(WaitUntil.Started, containerData, cancellationToken); + var jobId = ExtractOperationIdFromResponse(result.GetRawResponse()); + + return new OperationResult( + "Accepted", + jobId, + $"Container registration initiated for VM '{vmId.Name}'. This may take several minutes. Use 'azurebackup job get --job {jobId}' to monitor progress."); + } + + public async Task TriggerInquiryAsync( + string vaultName, string resourceGroup, string subscription, + string containerName, string? workloadType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(containerName), containerName)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + var containerId = BackupProtectionContainerResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, containerName); + var containerResource = armClient.GetBackupProtectionContainerResource(containerId); + + string? filter = !string.IsNullOrEmpty(workloadType) + ? $"backupManagementType eq 'AzureWorkload' and workloadType eq '{workloadType}'" + : null; + + var result = await containerResource.InquireAsync(filter, cancellationToken); + var operationId = ExtractOperationIdFromResponse(result); + + return new OperationResult( + "Accepted", + operationId, + $"Inquiry triggered for container '{containerName}'. This discovers databases on the registered VM."); + } + + public async Task> ListProtectableItemsAsync( + string vaultName, string resourceGroup, string subscription, + string? workloadType, string? containerName, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + + string filter; + if (!string.IsNullOrEmpty(workloadType)) + { + // Normalize workload-type values to what the REST API filter expects (Bug #46). + // Users may pass common names like "SAPHana" but the API filter requires + // specific values like "SAPHanaDatabase" or "SAPHanaDBInstance". + var normalizedType = NormalizeWorkloadTypeForFilter(workloadType); + filter = $"backupManagementType eq 'AzureWorkload' and workloadType eq '{normalizedType}'"; + } + else + { + // Azure API requires at least a backupManagementType filter (Bug #38) + filter = "backupManagementType eq 'AzureWorkload'"; + } + + var items = new List(); + await foreach (var item in rgResource.GetBackupProtectableItemsAsync(vaultName, filter: filter, cancellationToken: cancellationToken)) + { + items.Add(MapToProtectableItemInfo(item)); + } + + return items; + } + + public async Task EnableAutoProtectionAsync( + string vaultName, string resourceGroup, string subscription, + string vmResourceId, string instanceName, string policyName, + string workloadType, string? tenant, + RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) + { + ValidateRequiredParameters( + (nameof(vaultName), vaultName), + (nameof(resourceGroup), resourceGroup), + (nameof(subscription), subscription), + (nameof(vmResourceId), vmResourceId), + (nameof(instanceName), instanceName), + (nameof(policyName), policyName), + (nameof(workloadType), workloadType)); + + var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); + + var policyArmId = BackupProtectionPolicyResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, policyName); + + var vmId = new ResourceIdentifier(vmResourceId); + + // Intent name is a GUID as used by Azure Resource Manager + var intentName = Guid.NewGuid().ToString(); + + var intentId = BackupProtectionIntentResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, intentName); + var intentResource = armClient.GetBackupProtectionIntentResource(intentId); + + var vaultResId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResId); + var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); + + // Build container and protectable item references for the intent + var containerName = $"VMAppContainer;Compute;{vmId.ResourceGroupName};{vmId.Name}"; + var itemId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.RecoveryServices/vaults/{vaultName}/backupFabrics/Azure/protectionContainers/{containerName}/protectableItems/{instanceName}"; + + // Bug #48: Use WorkloadSqlAutoProtectionIntent for all workload types (SQL and HANA). + // Despite the "Sql" in the name, this is the only SDK intent type that supports + // the WorkloadItemType property needed to distinguish SQL vs HANA auto-protection. + // The REST API type is AzureWorkloadSQLAutoProtectionIntent for both SQL and HANA. + var workloadItemType = workloadType?.ToUpperInvariant() switch + { + "SQLDATABASE" or "SQLINSTANCE" or "SQL" => WorkloadItemType.SqlInstance, + "SAPHANA" or "SAPHANADATABASE" or "SAPHANASYSTEM" or "SAPHANADBINSTANCE" => WorkloadItemType.SapHanaSystem, + _ => new WorkloadItemType(workloadType ?? "Invalid"), + }; + + var intentProperties = new WorkloadSqlAutoProtectionIntent + { + BackupManagementType = BackupManagementType.AzureWorkload, + ItemId = new ResourceIdentifier(itemId), + PolicyId = policyArmId, + SourceResourceId = new ResourceIdentifier(vmResourceId), + WorkloadItemType = workloadItemType, + }; + + var intentData = new BackupProtectionIntentData(vault.Value.Data.Location) + { + Properties = intentProperties + }; + + await intentResource.UpdateAsync(WaitUntil.Started, intentData, cancellationToken); + + return new OperationResult( + "Succeeded", + null, + $"Auto-protection enabled for '{instanceName}' on VM '{vmId.Name}' with policy '{policyName}'."); + } + + /// + /// Normalizes user-provided workload type values to the API filter format. + /// The REST API filter expects specific types like "SAPHanaDatabase" but users + /// commonly pass "SAPHana" (which is what the API returns in workloadType fields). + /// + private static string NormalizeWorkloadTypeForFilter(string workloadType) => workloadType.ToUpperInvariant() switch + { + "SAPHANA" => "SAPHanaDatabase", + "SAPHANASYSTEM" => "SAPHanaSystem", + "SAPHANADBINSTANCE" or "SAPHANADBI" => "SAPHanaDBInstance", + "SQL" => "SQLDataBase", + "SQLINSTANCE" => "SQLInstance", + _ => workloadType, // Pass through if already in correct format (e.g., "SAPHanaDatabase", "SQLDataBase") + }; + + /// + /// Extracts the ProtectionState from concrete protected item types. + /// The base BackupGenericProtectedItem does not expose ProtectionState directly. + /// + private static string? GetProtectionState(BackupGenericProtectedItem? properties) => properties switch + { + VmWorkloadSapHanaDatabaseProtectedItem hana => hana.ProtectionState?.ToString(), + VmWorkloadSqlDatabaseProtectedItem sql => sql.ProtectionState?.ToString(), + IaasComputeVmProtectedItem vm => vm.ProtectionState?.ToString(), + FileshareProtectedItem fs => fs.ProtectionState?.ToString(), + _ => null, + }; + + private static ProtectableItemInfo MapToProtectableItemInfo(WorkloadProtectableItemResource data) + { + string? protectableItemType = null; + string? workloadType = null; + string? friendlyName = null; + string? serverName = null; + string? parentName = null; + string? protectionState = null; + string? containerName = null; + + if (data.Properties is WorkloadProtectableItem workloadItem) + { + // ProtectableItemType is internal in the SDK, use type discrimination + protectableItemType = workloadItem switch + { + VmWorkloadSqlDatabaseProtectableItem => "SQLDataBase", + VmWorkloadSapHanaDatabaseProtectableItem => "SAPHanaDatabase", + VmWorkloadSqlInstanceProtectableItem => "SQLInstance", + VmWorkloadSapHanaSystemProtectableItem => "SAPHanaSystem", + _ => workloadItem.GetType().Name + }; + workloadType = workloadItem.WorkloadType; + friendlyName = workloadItem.FriendlyName; + protectionState = workloadItem.ProtectionState?.ToString(); + + if (workloadItem is VmWorkloadSqlDatabaseProtectableItem sqlDb) + { + serverName = sqlDb.ServerName; + parentName = sqlDb.ParentName; + } + else if (workloadItem is VmWorkloadSapHanaDatabaseProtectableItem hanaDb) + { + serverName = hanaDb.ServerName; + parentName = hanaDb.ParentName; + } + } + + return new ProtectableItemInfo( + data.Id?.ToString(), + data.Name, + protectableItemType, + workloadType, + friendlyName, + serverName, + parentName, + protectionState, + containerName); + } +} + +internal static class RsvNamingHelper +{ + public static string DeriveContainerName(string datasourceId, string? datasourceType = null) + { + // Use the RSV datasource registry for workload detection + var profile = RsvDatasourceRegistry.Resolve(datasourceType); + if (profile?.IsWorkloadType == true) + { + var resourceId = new ResourceIdentifier(datasourceId); + return $"{profile.ContainerNamePrefix};{resourceId.ResourceGroupName};{resourceId.Name}"; + } + + var vmResourceId = new ResourceIdentifier(datasourceId); + + // Use profile-based detection first (handles nested ARM IDs like file shares + // where ResourceType.Type returns "storageAccounts/fileServices/shares" not "shares") + if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + return $"StorageContainer;Storage;{vmResourceId.ResourceGroupName};{ExtractStorageAccountName(vmResourceId)}"; + } + + // Fallback to resource type detection for untyped calls + var resourceType = vmResourceId.ResourceType.Type; + + return resourceType.ToLowerInvariant() switch + { + "virtualmachines" => $"IaasVMContainer;iaasvmcontainerv2;{vmResourceId.ResourceGroupName};{vmResourceId.Name}", + "storageaccounts" => $"StorageContainer;Storage;{vmResourceId.ResourceGroupName};{vmResourceId.Name}", + _ => $"GenericContainer;{vmResourceId.ResourceGroupName};{vmResourceId.Name}" + }; + } + + public static string DeriveProtectedItemName(string datasourceId, string? datasourceType = null) + { + // For workload types, the protectable item name is used directly (passed as datasourceId) + var profile = RsvDatasourceRegistry.Resolve(datasourceType); + if (profile?.IsWorkloadType == true) + { + return datasourceId; + } + + var resourceId = new ResourceIdentifier(datasourceId); + + // Use profile-based detection first (handles nested ARM IDs like file shares + // where ResourceType.Type returns "storageAccounts/fileServices/shares" not "shares") + if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + return $"AzureFileShare;{resourceId.Name}"; + } + + // Fallback to resource type detection for untyped calls + var resourceType = resourceId.ResourceType.Type; + + return resourceType.ToLowerInvariant() switch + { + "virtualmachines" => $"VM;iaasvmcontainerv2;{resourceId.ResourceGroupName};{resourceId.Name}", + "storageaccounts" => $"AzureFileShare;{resourceId.Name}", + _ => $"GenericProtectedItem;{resourceId.ResourceGroupName};{resourceId.Name}" + }; + } + + private static string ExtractStorageAccountName(ResourceIdentifier resourceId) + { + // For file share ARM IDs like .../storageAccounts/{sa}/fileServices/default/shares/{share} + // Walk up the hierarchy to find the storage account name + ResourceIdentifier? current = resourceId; + while (current is not null) + { + if (string.Equals(current.ResourceType.Type, "storageAccounts", StringComparison.OrdinalIgnoreCase)) + { + return current.Name; + } + + current = current.Parent; + } + + return resourceId.Name; + } + + public static string GetStorageAccountId(ResourceIdentifier resourceId) + { + // For file share ARM IDs, walk up to return the storage account ARM ID + ResourceIdentifier? current = resourceId; + while (current is not null) + { + if (string.Equals(current.ResourceType.Type, "storageAccounts", StringComparison.OrdinalIgnoreCase)) + { + return current.ToString(); + } + + current = current.Parent; + } + + return resourceId.ToString(); + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs new file mode 100644 index 0000000000..976d4422d9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +/// +/// Identifies the SDK protected-item type to construct for RSV protect/stop/resume/modify operations. +/// +public enum RsvProtectedItemType +{ + /// IaasComputeVmProtectedItem — Azure Virtual Machines. + IaasVm, + + /// VmWorkloadSqlDatabaseProtectedItem — SQL databases in Azure VMs. + SqlDatabase, + + /// VmWorkloadSapHanaDatabaseProtectedItem — SAP HANA databases in Azure VMs. + SapHanaDatabase, + + /// FileshareProtectedItem — Azure File Shares. + AzureFileShare, +} + +/// +/// Identifies the SDK policy type to construct for RSV policy-create operations. +/// +public enum RsvPolicyType +{ + /// IaasVmProtectionPolicy — for VM backup. + IaasVm, + + /// VmWorkloadProtectionPolicy — for SQL/HANA/ASE workload backup (Full + Log sub-policies). + VmWorkload, + + /// AzureFileShareProtectionPolicy — for Azure File Share backup. + AzureFileShare, +} + +/// +/// Identifies the SDK backup content type to construct for RSV trigger-backup operations. +/// +public enum RsvBackupContentType +{ + /// IaasVmBackupContent — for VM on-demand backup. + IaasVm, + + /// WorkloadBackupContent — for SQL/HANA/ASE on-demand workload backup. + Workload, + + /// AzureFileShareBackupContent — for File Share on-demand backup. + AzureFileShare, +} + +/// +/// Identifies the SDK restore content type to construct for RSV trigger-restore operations. +/// +public enum RsvRestoreContentType +{ + /// IaasVmRestoreContent — OLR/ALR/RestoreDisks for VMs. + IaasVm, + + /// WorkloadSqlRestoreContent — SQL database OLR/ALR with data directory mappings. + SqlRestore, + + /// WorkloadSapHanaRestoreContent — SAP HANA database OLR/ALR. + SapHanaRestore, + + /// AzureFileShareRestoreContent — File Share restore. + AzureFileShareRestore, +} + +/// +/// Immutable profile describing all configuration aspects of an RSV datasource type. +/// Eliminates hardcoded if/else branching by centralizing datasource-specific SDK type selection. +/// +/// +/// AOT-safe: no reflection, no Func delegates — pure data record with enum-driven dispatch. +/// RSV SDK uses polymorphic types (IaasVmProtectedItem, VmWorkloadSqlDatabaseProtectedItem, etc.) +/// so the profile maps user-facing names to the correct enum values that drive construction. +/// +public sealed record RsvDatasourceProfile +{ + // ── Identity ── + + /// Friendly name used for user-facing resolution (e.g. "VM", "SQL"). + public required string FriendlyName { get; init; } + + /// Alternative user-supplied names that resolve to this profile. + public string[] Aliases { get; init; } = []; + + // ── Classification ── + + /// + /// Whether this is a "workload" type (SQL, HANA, ASE) that runs inside a VM. + /// Workload types require container registration, workload discovery, and use VMAppContainer naming. + /// + public bool IsWorkloadType { get; init; } + + // ── SDK Type Mapping ── + + /// Which SDK ProtectedItem type to construct for protect/stop/resume/modify. + public RsvProtectedItemType ProtectedItemType { get; init; } = RsvProtectedItemType.IaasVm; + + /// Which SDK Policy type to construct for policy-create. + public RsvPolicyType PolicyType { get; init; } = RsvPolicyType.IaasVm; + + /// Which SDK BackupContent type to construct for trigger-backup. + public RsvBackupContentType BackupContentType { get; init; } = RsvBackupContentType.IaasVm; + + /// Which SDK RestoreContent type to construct for trigger-restore. + public RsvRestoreContentType RestoreContentType { get; init; } = RsvRestoreContentType.IaasVm; + + // ── Workload Type String ── + + /// + /// The Azure API workload type string (e.g. "SQLDataBase", "SAPHanaDatabase"). + /// Used when creating workload policies and registering containers. + /// Null for non-workload types (VM, FileShare). + /// + public string? ApiWorkloadType { get; init; } + + // ── Container Naming ── + + /// + /// Prefix for the container name derivation. + /// VM: "IaasVMContainer;iaasvmcontainerv2", + /// SQL/HANA/ASE: "VMAppContainer;Compute", + /// FileShare: "StorageContainer". + /// + public required string ContainerNamePrefix { get; init; } + + // ── Features ── + + /// Whether container registration is required before protection (workload types). + public bool RequiresContainerRegistration { get; init; } + + /// Whether container inquiry (workload discovery) is required before protection. + public bool RequiresContainerInquiry { get; init; } + + /// Whether VM discovery/refresh is required before protection (VM type). + public bool RequiresContainerDiscovery { get; init; } + + /// Whether auto-protection is supported (SQL workloads). + public bool SupportsAutoProtect { get; init; } + + /// Whether the Azure API supports policy updates for this datasource type. + public bool SupportsPolicyUpdate { get; init; } = true; +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs new file mode 100644 index 0000000000..a3b9d0c126 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +/// +/// Central registry of all RSV (Recovery Services Vault) datasource profiles. +/// Acts as a single source of truth for datasource-specific SDK type selection, +/// replacing scattered if/else and switch checks in RsvBackupOperations. +/// +/// To add a new RSV datasource type: +/// 1. Add a static RsvDatasourceProfile instance below +/// 2. Register it in the AllProfiles array +/// 3. If it uses new SDK types, add enum values to RsvDatasourceProfile.cs +/// — Minimises changes needed in RsvBackupOperations. +/// +public static class RsvDatasourceRegistry +{ + // ── Profile definitions ── + + public static readonly RsvDatasourceProfile IaasVm = new() + { + FriendlyName = "VM", + Aliases = ["vm", "iaasvm", "virtualmachine", "iaasvmcontainer"], + IsWorkloadType = false, + ProtectedItemType = RsvProtectedItemType.IaasVm, + PolicyType = RsvPolicyType.IaasVm, + BackupContentType = RsvBackupContentType.IaasVm, + RestoreContentType = RsvRestoreContentType.IaasVm, + ContainerNamePrefix = "IaasVMContainer;iaasvmcontainerv2", + RequiresContainerDiscovery = true, + SupportsPolicyUpdate = true, + }; + + public static readonly RsvDatasourceProfile SqlDatabase = new() + { + FriendlyName = "SQL", + Aliases = ["sql", "sqldatabase", "mssql", "sqldb"], + IsWorkloadType = true, + ProtectedItemType = RsvProtectedItemType.SqlDatabase, + PolicyType = RsvPolicyType.VmWorkload, + BackupContentType = RsvBackupContentType.Workload, + RestoreContentType = RsvRestoreContentType.SqlRestore, + ApiWorkloadType = "SQLDataBase", + ContainerNamePrefix = "VMAppContainer;Compute", + RequiresContainerRegistration = true, + RequiresContainerInquiry = true, + SupportsAutoProtect = true, + SupportsPolicyUpdate = true, + }; + + public static readonly RsvDatasourceProfile SapHanaDatabase = new() + { + FriendlyName = "SAPHANA", + Aliases = ["saphana", "saphanadatabase", "saphanadb", "hana"], + IsWorkloadType = true, + ProtectedItemType = RsvProtectedItemType.SapHanaDatabase, + PolicyType = RsvPolicyType.VmWorkload, + BackupContentType = RsvBackupContentType.Workload, + RestoreContentType = RsvRestoreContentType.SapHanaRestore, + ApiWorkloadType = "SAPHanaDatabase", + ContainerNamePrefix = "VMAppContainer;Compute", + RequiresContainerRegistration = true, + RequiresContainerInquiry = true, + SupportsPolicyUpdate = true, + }; + + public static readonly RsvDatasourceProfile SapAse = new() + { + FriendlyName = "SAPASE", + Aliases = ["sapase", "ase", "sybase"], + IsWorkloadType = true, + ProtectedItemType = RsvProtectedItemType.SqlDatabase, // ASE uses same SDK type pattern as SQL + PolicyType = RsvPolicyType.VmWorkload, + BackupContentType = RsvBackupContentType.Workload, + RestoreContentType = RsvRestoreContentType.SqlRestore, + ApiWorkloadType = "SAPAseDatabase", + ContainerNamePrefix = "VMAppContainer;Compute", + RequiresContainerRegistration = true, + RequiresContainerInquiry = true, + SupportsPolicyUpdate = true, + }; + + public static readonly RsvDatasourceProfile AzureFileShare = new() + { + FriendlyName = "AzureFileShare", + Aliases = ["azurefileshare", "fileshare", "afs"], + IsWorkloadType = false, + ProtectedItemType = RsvProtectedItemType.AzureFileShare, + PolicyType = RsvPolicyType.AzureFileShare, + BackupContentType = RsvBackupContentType.AzureFileShare, + RestoreContentType = RsvRestoreContentType.AzureFileShareRestore, + ContainerNamePrefix = "StorageContainer;Storage", + SupportsPolicyUpdate = true, + }; + + // ── Registry ── + + /// All registered RSV datasource profiles. + public static readonly RsvDatasourceProfile[] AllProfiles = + [ + IaasVm, + SqlDatabase, + SapHanaDatabase, + SapAse, + AzureFileShare, + ]; + + /// + /// Resolves a user-supplied datasource type to the matching RSV profile. + /// Case-insensitive match against FriendlyName and Aliases. + /// Returns null if no match is found (caller should default to VM). + /// + public static RsvDatasourceProfile? Resolve(string? datasourceType) + { + if (string.IsNullOrEmpty(datasourceType)) + { + return null; + } + + var normalised = datasourceType.ToLowerInvariant(); + + foreach (var profile in AllProfiles) + { + if (normalised.Equals(profile.FriendlyName, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + + foreach (var alias in profile.Aliases) + { + if (normalised.Equals(alias, StringComparison.OrdinalIgnoreCase)) + { + return profile; + } + } + } + + return null; + } + + /// + /// Resolves a datasource type to a profile, defaulting to VM if no match. + /// + public static RsvDatasourceProfile ResolveOrDefault(string? datasourceType) + { + return Resolve(datasourceType) ?? IaasVm; + } + + /// + /// Determines the profile from an existing protected item's naming conventions. + /// RSV protected items encode their type in the name prefix. + /// + public static RsvDatasourceProfile ResolveFromProtectedItemName(string protectedItemName, string? containerName) + { + var itemLower = protectedItemName.ToLowerInvariant(); + var containerLower = containerName?.ToLowerInvariant() ?? string.Empty; + + if (itemLower.StartsWith("sqldatabase;", StringComparison.OrdinalIgnoreCase) || + itemLower.StartsWith("sqlinstance;", StringComparison.OrdinalIgnoreCase)) + { + return SqlDatabase; + } + + if (itemLower.StartsWith("saphanadatabase;", StringComparison.OrdinalIgnoreCase) || + itemLower.StartsWith("saphanainstance;", StringComparison.OrdinalIgnoreCase)) + { + return SapHanaDatabase; + } + + if (itemLower.StartsWith("sapasedatabase;", StringComparison.OrdinalIgnoreCase)) + { + return SapAse; + } + + if (itemLower.StartsWith("azurefileshare;", StringComparison.OrdinalIgnoreCase) || + containerLower.StartsWith("storagecontainer;", StringComparison.OrdinalIgnoreCase)) + { + return AzureFileShare; + } + + // VMAppContainer indicates a workload, default to SQL if we can't determine the exact type + if (containerLower.StartsWith("vmappcontainer", StringComparison.OrdinalIgnoreCase)) + { + return SqlDatabase; + } + + return IaasVm; + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/VaultTypeResolver.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/VaultTypeResolver.cs new file mode 100644 index 0000000000..71a6446098 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/VaultTypeResolver.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Services; + +public static class VaultTypeResolver +{ + public const string Rsv = "rsv"; + public const string Dpp = "dpp"; + + public static bool IsRsv(string? vaultType) => + string.Equals(vaultType, Rsv, StringComparison.OrdinalIgnoreCase); + + public static bool IsDpp(string? vaultType) => + string.Equals(vaultType, Dpp, StringComparison.OrdinalIgnoreCase); + + public static void ValidateVaultType(string? vaultType) + { + if (string.IsNullOrEmpty(vaultType)) + { + throw new ArgumentException("The --vault-type parameter is required. Specify 'rsv' for Recovery Services vault or 'dpp' for Backup vault."); + } + + if (!IsRsv(vaultType) && !IsDpp(vaultType)) + { + throw new ArgumentException($"Invalid vault type '{vaultType}'. Must be 'rsv' (Recovery Services vault) or 'dpp' (Backup vault)."); + } + } + + public static bool IsVaultTypeSpecified(string? vaultType) => + !string.IsNullOrEmpty(vaultType) && (IsRsv(vaultType) || IsDpp(vaultType)); +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Azure.Mcp.Tools.AzureBackup.UnitTests.csproj b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Azure.Mcp.Tools.AzureBackup.UnitTests.csproj new file mode 100644 index 0000000000..ae1524fdc2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Azure.Mcp.Tools.AzureBackup.UnitTests.csproj @@ -0,0 +1,17 @@ + + + true + Exe + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Backup/BackupStatusCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Backup/BackupStatusCommandTests.cs new file mode 100644 index 0000000000..b6c5ffc17a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Backup/BackupStatusCommandTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Backup; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Backup; + +public class BackupStatusCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly BackupStatusCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public BackupStatusCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("status", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ReturnsBackupStatus_Successfully() + { + // Arrange + var expected = new BackupStatusResult("/subscriptions/.../vm1", "Protected", "/subscriptions/.../vault1", + "DefaultPolicy", DateTimeOffset.UtcNow.AddHours(-1), "Completed", "Healthy"); + + _backupService.GetBackupStatusAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--datasource-id", "/subscriptions/.../vm1", "--location", "eastus"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.BackupStatusCommandResult); + + Assert.NotNull(result); + Assert.Equal("Protected", result.Status.ProtectionStatus); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.GetBackupStatusAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--datasource-id", "ds1", "--location", "eastus"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --datasource-id ds1 --location eastus", true)] + [InlineData("--subscription sub", false)] // Missing datasource-id and location + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.GetBackupStatusAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new BackupStatusResult("ds1", "Protected", "v1", "pol", null, null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Dr/DrEnableCrrCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Dr/DrEnableCrrCommandTests.cs new file mode 100644 index 0000000000..c676320260 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Dr/DrEnableCrrCommandTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Dr; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Dr; + +public class DrEnableCrrCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly DrEnableCrrCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public DrEnableCrrCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("enablecrr", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_EnablesCrr_Successfully() + { + // Arrange + var expected = new OperationResult("Succeeded", null, "Cross-region restore enabled"); + + _backupService.ConfigureCrossRegionRestoreAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.DrEnableCrrCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ConfigureCrossRegionRestoreAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg", true)] + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ConfigureCrossRegionRestoreAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new OperationResult("Succeeded", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceFindUnprotectedCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceFindUnprotectedCommandTests.cs new file mode 100644 index 0000000000..78941df506 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceFindUnprotectedCommandTests.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Governance; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Governance; + +public class GovernanceFindUnprotectedCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly GovernanceFindUnprotectedCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public GovernanceFindUnprotectedCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("find-unprotected", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_FindsUnprotectedResources_Successfully() + { + // Arrange + var expectedResources = new List + { + new("/subscriptions/.../vm1", "vm1", "Microsoft.Compute/virtualMachines", "rg1", "eastus", null), + new("/subscriptions/.../sql1", "sql1", "Microsoft.Sql/servers", "rg2", "westus", null) + }; + + _backupService.FindUnprotectedResourcesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expectedResources)); + + var args = _commandDefinition.Parse(["--subscription", "sub123"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.GovernanceFindUnprotectedCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.Resources.Count); + Assert.Equal("vm1", result.Resources[0].Name); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenAllProtected() + { + // Arrange + _backupService.FindUnprotectedResourcesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", "sub123"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.GovernanceFindUnprotectedCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.Resources); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.FindUnprotectedResourcesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub123"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub123", true)] + [InlineData("", false)] // Missing subscription + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.FindUnprotectedResourcesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceImmutabilityCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceImmutabilityCommandTests.cs new file mode 100644 index 0000000000..d246c7d928 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceImmutabilityCommandTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Governance; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Governance; + +public class GovernanceImmutabilityCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly GovernanceImmutabilityCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public GovernanceImmutabilityCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("immutability", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ConfiguresImmutability_Successfully() + { + // Arrange + var expected = new OperationResult("Succeeded", null, "Immutability configured"); + + _backupService.ConfigureImmutabilityAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--immutability-state", "Enabled"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.GovernanceImmutabilityCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ConfigureImmutabilityAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--immutability-state", "Enabled"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --immutability-state Enabled", true)] + [InlineData("--subscription sub --vault v --resource-group rg", true)] // immutability-state is optional + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ConfigureImmutabilityAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new OperationResult("Succeeded", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceSoftDeleteCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceSoftDeleteCommandTests.cs new file mode 100644 index 0000000000..c554a0b007 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Governance/GovernanceSoftDeleteCommandTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Governance; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Governance; + +public class GovernanceSoftDeleteCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly GovernanceSoftDeleteCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public GovernanceSoftDeleteCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("soft-delete", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ConfiguresSoftDelete_Successfully() + { + // Arrange + var expected = new OperationResult("Succeeded", null, "Soft delete configured"); + + _backupService.ConfigureSoftDeleteAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--soft-delete", "AlwaysOn"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.GovernanceSoftDeleteCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ConfigureSoftDeleteAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--soft-delete", "On"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --soft-delete On", true)] + [InlineData("--subscription sub --vault v --resource-group rg", true)] // soft-delete is optional + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ConfigureSoftDeleteAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new OperationResult("Succeeded", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Job/JobGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Job/JobGetCommandTests.cs new file mode 100644 index 0000000000..6c5602c5c5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Job/JobGetCommandTests.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Job; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Job; + +public class JobGetCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly JobGetCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public JobGetCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsJobs_WhenNoJobSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var expectedJobs = new List + { + new("id1", "job1", "rsv", "Backup", "Completed", DateTimeOffset.UtcNow.AddHours(-2), DateTimeOffset.UtcNow.AddHours(-1), "AzureIaasVM", "vm1"), + new("id2", "job2", "rsv", "Restore", "InProgress", DateTimeOffset.UtcNow.AddMinutes(-30), null, "SQLDataBase", "sql1") + }; + + _backupService.ListJobsAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedJobs)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.JobGetCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.Jobs.Count); + Assert.Equal("job1", result.Jobs[0].Name); + } + + [Fact] + public async Task ExecuteAsync_GetsSingleJob_WhenJobSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var jobId = "job1"; + var expectedJob = new BackupJobInfo("id1", jobId, "rsv", "Backup", "Completed", DateTimeOffset.UtcNow.AddHours(-2), DateTimeOffset.UtcNow.AddHours(-1), "AzureIaasVM", "vm1"); + + _backupService.GetJobAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Is(jobId), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedJob)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--job", jobId]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.JobGetCommandResult); + + Assert.NotNull(result); + Assert.Single(result.Jobs); + Assert.Equal(jobId, result.Jobs[0].Name); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoJobsExist() + { + // Arrange + _backupService.ListJobsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.JobGetCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.Jobs); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ListJobsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesNotFound() + { + // Arrange + _backupService.GetJobAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Job not found")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--job", "nonexistent"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message.ToLower()); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg", true)] + [InlineData("--subscription sub --vault v --resource-group rg --job j1", true)] + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListJobsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + _backupService.GetJobAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new BackupJobInfo("id", "j1", "rsv", "Backup", "Completed", null, null, "VM", "vm1"))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyCreateCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyCreateCommandTests.cs new file mode 100644 index 0000000000..d0732e4d1a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyCreateCommandTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Policy; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Policy; + +public class PolicyCreateCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly PolicyCreateCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public PolicyCreateCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_CreatesPolicy_Successfully() + { + // Arrange + var expected = new OperationResult("Succeeded", null, "Policy created successfully"); + + _backupService.CreatePolicyAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--policy", "myPolicy", "--workload-type", "AzureIaasVM"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.PolicyCreateCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.CreatePolicyAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--policy", "p", "--workload-type", "AzureIaasVM"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --policy p --workload-type VM", true)] + [InlineData("--subscription sub --vault v --resource-group rg", false)] // Missing policy and workload-type + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.CreatePolicyAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new OperationResult("Succeeded", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyGetCommandTests.cs new file mode 100644 index 0000000000..91d2a873f1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Policy/PolicyGetCommandTests.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Policy; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Policy; + +public class PolicyGetCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly PolicyGetCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public PolicyGetCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsPolicies_WhenNoPolicySpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var expectedPolicies = new List + { + new("id1", "DefaultPolicy", "rsv", ["AzureIaasVM"], 5), + new("id2", "CustomPolicy", "rsv", ["SQLDataBase"], 3) + }; + + _backupService.ListPoliciesAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedPolicies)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.PolicyGetCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.Policies.Count); + Assert.Equal("DefaultPolicy", result.Policies[0].Name); + } + + [Fact] + public async Task ExecuteAsync_GetsSinglePolicy_WhenPolicySpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var policyName = "DefaultPolicy"; + var expectedPolicy = new BackupPolicyInfo("id1", policyName, "rsv", ["AzureIaasVM"], 5); + + _backupService.GetPolicyAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Is(policyName), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedPolicy)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--policy", policyName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.PolicyGetCommandResult); + + Assert.NotNull(result); + Assert.Single(result.Policies); + Assert.Equal(policyName, result.Policies[0].Name); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoPoliciesExist() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + + _backupService.ListPoliciesAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.PolicyGetCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.Policies); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + + _backupService.ListPoliciesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesNotFound() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var policyName = "nonexistent"; + + _backupService.GetPolicyAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Policy not found")); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--policy", policyName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message.ToLower()); + } + + [Theory] + [InlineData("--subscription sub123 --vault v --resource-group rg", true)] + [InlineData("--subscription sub123 --vault v --resource-group rg --policy p", true)] + [InlineData("--subscription sub123", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListPoliciesAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + _backupService.GetPolicyAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new BackupPolicyInfo("id1", "p", "rsv", ["VM"], 1))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectableItem/ProtectableItemListCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectableItem/ProtectableItemListCommandTests.cs new file mode 100644 index 0000000000..89407c1a30 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectableItem/ProtectableItemListCommandTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectableItem; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.ProtectableItem; + +public class ProtectableItemListCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly ProtectableItemListCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public ProtectableItemListCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("list", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsProtectableItems_Successfully() + { + // Arrange + var expectedItems = new List + { + new("id1", "db1", "SQLDataBase", "SQL", "MyDatabase", "server1", "instance1", "NotProtected", "container1"), + new("id2", "db2", "SAPHanaDatabase", "SAPHana", "HanaDb", "server2", "instance2", "NotProtected", "container2") + }; + + _backupService.ListProtectableItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expectedItems)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectableItemListCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.Items.Count); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoItemsExist() + { + // Arrange + _backupService.ListProtectableItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectableItemListCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.Items); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ListProtectableItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg", true)] + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListProtectableItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemGetCommandTests.cs new file mode 100644 index 0000000000..f2f4d30f4f --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemGetCommandTests.cs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.ProtectedItem; + +public class ProtectedItemGetCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly ProtectedItemGetCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public ProtectedItemGetCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsProtectedItems_WhenNoItemSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var expectedItems = new List + { + new("id1", "vm1", "rsv", "Protected", "AzureIaasVM", "/subscriptions/.../vm1", "DefaultPolicy", DateTimeOffset.UtcNow, null), + new("id2", "sql1", "rsv", "Protected", "SQLDataBase", "/subscriptions/.../sql1", "SqlPolicy", DateTimeOffset.UtcNow, "container1") + }; + + _backupService.ListProtectedItemsAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedItems)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectedItemGetCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.ProtectedItems.Count); + Assert.Equal("vm1", result.ProtectedItems[0].Name); + } + + [Fact] + public async Task ExecuteAsync_GetsSingleItem_WhenItemSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var itemName = "vm1"; + var expectedItem = new ProtectedItemInfo("id1", itemName, "rsv", "Protected", "AzureIaasVM", "/subscriptions/.../vm1", "DefaultPolicy", DateTimeOffset.UtcNow, null); + + _backupService.GetProtectedItemAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Is(itemName), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedItem)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--protected-item", itemName]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectedItemGetCommandResult); + + Assert.NotNull(result); + Assert.Single(result.ProtectedItems); + Assert.Equal(itemName, result.ProtectedItems[0].Name); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoItemsExist() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + + _backupService.ListProtectedItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectedItemGetCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.ProtectedItems); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ListProtectedItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesNotFound() + { + // Arrange + _backupService.GetProtectedItemAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Item not found")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--protected-item", "nonexistent"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message.ToLower()); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg", true)] + [InlineData("--subscription sub --vault v --resource-group rg --protected-item item1", true)] + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListProtectedItemsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + _backupService.GetProtectedItemAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new ProtectedItemInfo("id", "item1", "rsv", "Protected", "VM", "/sub/vm", "pol", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemProtectCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemProtectCommandTests.cs new file mode 100644 index 0000000000..cbda3fef13 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/ProtectedItem/ProtectedItemProtectCommandTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.ProtectedItem; + +public class ProtectedItemProtectCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly ProtectedItemProtectCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public ProtectedItemProtectCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("protect", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ProtectsItem_Successfully() + { + // Arrange + var expected = new ProtectResult("Succeeded", "vm1-backup", "job123", "Protection enabled"); + + _backupService.ProtectItemAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--datasource-id", "/subscriptions/.../vm1", "--policy", "DefaultPolicy"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.ProtectedItemProtectCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ProtectItemAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", + "--datasource-id", "/subscriptions/.../vm1", "--policy", "DefaultPolicy"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --datasource-id ds1 --policy pol1", true)] + [InlineData("--subscription sub --vault v --resource-group rg", false)] // Missing datasource-id and policy + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ProtectItemAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new ProtectResult("Succeeded", "item1", "job1", null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/RecoveryPoint/RecoveryPointGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/RecoveryPoint/RecoveryPointGetCommandTests.cs new file mode 100644 index 0000000000..9a72b1ad1a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/RecoveryPoint/RecoveryPointGetCommandTests.cs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.RecoveryPoint; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.RecoveryPoint; + +public class RecoveryPointGetCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly RecoveryPointGetCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public RecoveryPointGetCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsRecoveryPoints_WhenNoRpSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var protectedItem = "vm1"; + var expectedPoints = new List + { + new("rp1", "rp1", "rsv", DateTimeOffset.UtcNow.AddDays(-1), "Full"), + new("rp2", "rp2", "rsv", DateTimeOffset.UtcNow.AddDays(-2), "Incremental") + }; + + _backupService.ListRecoveryPointsAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Is(protectedItem), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedPoints)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--protected-item", protectedItem]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.RecoveryPointGetCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.RecoveryPoints.Count); + Assert.Equal("rp1", result.RecoveryPoints[0].Name); + } + + [Fact] + public async Task ExecuteAsync_GetsSingleRecoveryPoint_WhenRpSpecified() + { + // Arrange + var subscription = "sub123"; + var vault = "myVault"; + var resourceGroup = "myRg"; + var protectedItem = "vm1"; + var rpId = "rp1"; + var expectedRp = new RecoveryPointInfo("rp1", rpId, "rsv", DateTimeOffset.UtcNow.AddDays(-1), "Full"); + + _backupService.GetRecoveryPointAsync( + Arg.Is(vault), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Is(protectedItem), + Arg.Is(rpId), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedRp)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vault, "--resource-group", resourceGroup, "--protected-item", protectedItem, "--recovery-point", rpId]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.RecoveryPointGetCommandResult); + + Assert.NotNull(result); + Assert.Single(result.RecoveryPoints); + Assert.Equal(rpId, result.RecoveryPoints[0].Name); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoRecoveryPointsExist() + { + // Arrange + _backupService.ListRecoveryPointsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--protected-item", "item"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.RecoveryPointGetCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.RecoveryPoints); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.ListRecoveryPointsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--protected-item", "item"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesNotFound() + { + // Arrange + _backupService.GetRecoveryPointAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Recovery point not found")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--protected-item", "item", "--recovery-point", "nonexistent"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message.ToLower()); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --protected-item item", true)] + [InlineData("--subscription sub --vault v --resource-group rg --protected-item item --recovery-point rp1", true)] + [InlineData("--subscription sub --vault v --resource-group rg", false)] // Missing protected-item + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListRecoveryPointsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + _backupService.GetRecoveryPointAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new RecoveryPointInfo("rp1", "rp1", "rsv", DateTimeOffset.UtcNow, "Full"))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultCreateCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultCreateCommandTests.cs new file mode 100644 index 0000000000..cedef475e1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultCreateCommandTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Vault; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Vault; + +public class VaultCreateCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly VaultCreateCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public VaultCreateCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("create", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_CreatesVault_Successfully() + { + // Arrange + var expected = new VaultCreateResult("id1", "myVault", "rsv", "eastus", "Succeeded"); + + _backupService.CreateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "myVault", "--resource-group", "rg", "--vault-type", "rsv", "--location", "eastus"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.VaultCreateCommandResult); + + Assert.NotNull(result); + Assert.Equal("myVault", result.Vault.Name); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.CreateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--vault-type", "rsv", "--location", "eastus"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg --vault-type rsv --location eastus", true)] + [InlineData("--subscription sub --vault v --resource-group rg", false)] // Missing vault-type and location + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.CreateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new VaultCreateResult("id", "v", "rsv", "eastus", "Succeeded"))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultGetCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultGetCommandTests.cs new file mode 100644 index 0000000000..f04e5c2d23 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultGetCommandTests.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Vault; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Vault; + +public class VaultGetCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly VaultGetCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public VaultGetCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("get", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_ListsVaults_WhenNoVaultSpecified() + { + // Arrange + var subscription = "sub123"; + var expectedVaults = new List + { + new("id1", "vault1", "rsv", "eastus", "rg1", "Succeeded", "Standard", "GeoRedundant", null), + new("id2", "vault2", "dpp", "westus", "rg2", "Succeeded", "Standard", "LocallyRedundant", null) + }; + + _backupService.ListVaultsAsync( + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedVaults)); + + var args = _commandDefinition.Parse(["--subscription", subscription]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.VaultGetCommandResult); + + Assert.NotNull(result); + Assert.Equal(2, result.Vaults.Count); + Assert.Equal("vault1", result.Vaults[0].Name); + Assert.Equal("vault2", result.Vaults[1].Name); + } + + [Fact] + public async Task ExecuteAsync_GetsSingleVault_WhenVaultSpecified() + { + // Arrange + var subscription = "sub123"; + var vaultName = "myVault"; + var resourceGroup = "myRg"; + var expectedVault = new BackupVaultInfo("id1", vaultName, "rsv", "eastus", resourceGroup, "Succeeded", "Standard", "GeoRedundant", null); + + _backupService.GetVaultAsync( + Arg.Is(vaultName), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(expectedVault)); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vaultName, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.VaultGetCommandResult); + + Assert.NotNull(result); + Assert.Single(result.Vaults); + Assert.Equal(vaultName, result.Vaults[0].Name); + Assert.Equal("eastus", result.Vaults[0].Location); + } + + [Fact] + public async Task ExecuteAsync_ReturnsEmpty_WhenNoVaultsExist() + { + // Arrange + var subscription = "sub123"; + + _backupService.ListVaultsAsync( + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(new List())); + + var args = _commandDefinition.Parse(["--subscription", subscription]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.OK, response.Status); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.VaultGetCommandResult); + + Assert.NotNull(result); + Assert.Empty(result.Vaults); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + var subscription = "sub123"; + + _backupService.ListVaultsAsync( + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", subscription]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.NotNull(response); + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Fact] + public async Task ExecuteAsync_HandlesNotFound() + { + // Arrange + var subscription = "sub123"; + var vaultName = "nonexistent"; + var resourceGroup = "myRg"; + + _backupService.GetVaultAsync( + Arg.Is(vaultName), + Arg.Is(resourceGroup), + Arg.Is(subscription), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new RequestFailedException((int)HttpStatusCode.NotFound, "Vault not found")); + + var args = _commandDefinition.Parse(["--subscription", subscription, "--vault", vaultName, "--resource-group", resourceGroup]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message.ToLower()); + } + + [Theory] + [InlineData("--subscription sub123", true)] + [InlineData("--subscription sub123 --vault myVault --resource-group myRg", true)] + [InlineData("", false)] // Missing subscription + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.ListVaultsAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new List())); + + _backupService.GetVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new BackupVaultInfo("id1", "myVault", "rsv", "eastus", "myRg", "Succeeded", "Standard", "GeoRedundant", null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultUpdateCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultUpdateCommandTests.cs new file mode 100644 index 0000000000..c746701cfa --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Vault/VaultUpdateCommandTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text.Json; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.AzureBackup.Commands; +using Azure.Mcp.Tools.AzureBackup.Commands.Vault; +using Azure.Mcp.Tools.AzureBackup.Models; +using Azure.Mcp.Tools.AzureBackup.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Models.Command; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Vault; + +public class VaultUpdateCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IAzureBackupService _backupService; + private readonly ILogger _logger; + private readonly VaultUpdateCommand _command; + private readonly CommandContext _context; + private readonly System.CommandLine.Command _commandDefinition; + + public VaultUpdateCommandTests() + { + _backupService = Substitute.For(); + _logger = Substitute.For>(); + + var collection = new ServiceCollection().AddSingleton(_backupService); + + _serviceProvider = collection.BuildServiceProvider(); + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var command = _command.GetCommand(); + Assert.Equal("update", command.Name); + Assert.NotNull(command.Description); + Assert.NotEmpty(command.Description); + } + + [Fact] + public async Task ExecuteAsync_UpdatesVault_Successfully() + { + // Arrange + var expected = new OperationResult("Succeeded", null, "Vault updated successfully"); + + _backupService.UpdateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(expected)); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg", "--redundancy", "GeoRedundant"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + var json = JsonSerializer.Serialize(response.Results); + var result = JsonSerializer.Deserialize(json, AzureBackupJsonContext.Default.VaultUpdateCommandResult); + + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + } + + [Fact] + public async Task ExecuteAsync_HandlesException() + { + // Arrange + _backupService.UpdateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Test error")); + + var args = _commandDefinition.Parse(["--subscription", "sub", "--vault", "v", "--resource-group", "rg"]); + + // Act + var response = await _command.ExecuteAsync(_context, args, TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.Status); + Assert.Contains("Test error", response.Message); + } + + [Theory] + [InlineData("--subscription sub --vault v --resource-group rg", true)] + [InlineData("--subscription sub", false)] // Missing vault and resource-group + public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) + { + if (shouldSucceed) + { + _backupService.UpdateVaultAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new OperationResult("Succeeded", null, null))); + } + + var parseResult = _commandDefinition.Parse(args); + + // Act + var response = await _command.ExecuteAsync(_context, parseResult, TestContext.Current.CancellationToken); + + // Assert + if (shouldSucceed) + { + Assert.Equal(HttpStatusCode.OK, response.Status); + } + else + { + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + } + } +} From 325cf718d602046f66948b33dec822bde4e1d4f2 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 19 Mar 2026 17:15:05 +0530 Subject: [PATCH 2/4] Fix CONTRIBUTING.md compliance gaps, add LiveTests project and test infrastructure - Remove 227 code comments from 18 source files - Add Azure Backup section to azmcp-commands.md - Add 30 e2e test prompts to e2eTestPrompts.md - Update README.md with Azure Backup service listing - Add CODEOWNERS entry for Azure Backup toolset - Add 7 consolidated tool groups to consolidated-tools.json - Add changelog entry YAML - Create test-resources.bicep and test-resources-post.ps1 - Create LiveTests project with 6 integration tests (vault, policy, governance, job) - Add assets.json for test proxy recording support - Update Microsoft.Mcp.slnx and Azure.Mcp.Server.slnx with LiveTests project - All 99 unit tests pass, all 6 live tests pass (6/6) --- .github/CODEOWNERS | 6 + Microsoft.Mcp.slnx | 1 + .../Server/Resources/consolidated-tools.json | 207 ++++++++++++++++++ .../Azure.Mcp.Server/Azure.Mcp.Server.slnx | 1 + servers/Azure.Mcp.Server/README.md | 15 +- .../shrja-add-azure-backup-toolset.yaml | 3 + .../Azure.Mcp.Server/docs/azmcp-commands.md | 175 +++++++++++++++ .../Azure.Mcp.Server/docs/e2eTestPrompts.md | 35 +++ .../src/AzureBackupSetup.cs | 18 -- .../src/Commands/AzureBackupJsonContext.cs | 10 - .../src/Commands/Job/JobGetCommand.cs | 2 - .../src/Commands/Policy/PolicyGetCommand.cs | 2 - .../ProtectedItem/ProtectedItemGetCommand.cs | 2 - .../RecoveryPoint/RecoveryPointGetCommand.cs | 2 - .../src/Commands/Vault/VaultGetCommand.cs | 2 - .../Options/AzureBackupOptionDefinitions.cs | 4 - .../src/Services/AzureBackupService.cs | 12 - .../src/Services/DppBackupOperations.cs | 35 --- .../src/Services/DppDatasourceProfile.cs | 7 - .../src/Services/DppDatasourceRegistry.cs | 6 - .../src/Services/IAzureBackupService.cs | 18 -- .../src/Services/IDppBackupOperations.cs | 2 - .../src/Services/IRsvBackupOperations.cs | 3 - .../src/Services/RsvBackupOperations.cs | 93 -------- .../src/Services/RsvDatasourceProfile.cs | 6 - .../src/Services/RsvDatasourceRegistry.cs | 3 - ...ure.Mcp.Tools.AzureBackup.LiveTests.csproj | 17 ++ .../AzureBackupCommandTests.cs | 129 +++++++++++ .../assets.json | 6 + .../tests/test-resources-post.ps1 | 17 ++ .../tests/test-resources.bicep | 42 ++++ 31 files changed, 653 insertions(+), 228 deletions(-) create mode 100644 servers/Azure.Mcp.Server/changelog-entries/shrja-add-azure-backup-toolset.yaml create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/Azure.Mcp.Tools.AzureBackup.LiveTests.csproj create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/assets.json create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources-post.ps1 create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9ec04653cc..64090ed54f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -93,6 +93,12 @@ # ServiceLabel: %tools-Authorization # ServiceOwners: @vurhanau @xiangyan99 +# PRLabel: %tools-AzureBackup +/tools/Azure.Mcp.Tools.AzureBackup/ @shrja @microsoft/azure-mcp + +# ServiceLabel: %tools-AzureBackup +# ServiceOwners: @shrja + # ServiceLabel: %tools-AzCLI # ServiceOwners: @JasonYeMSFT @microsoft/azure-mcp diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 0497917b05..46d23028d3 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -127,6 +127,7 @@ + diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json b/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json index d1fe5152c3..d715450dbf 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json @@ -3132,6 +3132,213 @@ "mappedToolList": [ "azuremigrate_platformlandingzone_getguidance" ] + }, + { + "name": "get_azure_backup_vault_details", + "description": "Get information about Azure Recovery Services vaults including configuration, redundancy settings, and properties.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_vault_get" + ] + }, + { + "name": "manage_azure_backup_vaults", + "description": "Create and update Azure Recovery Services vaults for backup and disaster recovery.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_vault_create", + "azurebackup_vault_update" + ] + }, + { + "name": "manage_azure_backup_policies", + "description": "Create and retrieve Azure Backup policies for configuring backup schedules and retention.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_policy_create", + "azurebackup_policy_get" + ] + }, + { + "name": "manage_azure_backup_protection", + "description": "Manage backup protection for Azure resources including enabling protection, checking status, listing protectable items, and monitoring protected items.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_protecteditem_get", + "azurebackup_protecteditem_protect", + "azurebackup_protectableitem_list", + "azurebackup_backup_status" + ] + }, + { + "name": "get_azure_backup_jobs_and_recovery_points", + "description": "Get Azure Backup job status and recovery point details for protected items.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": true, + "description": "This tool only performs read operations without modifying any state or data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_job_get", + "azurebackup_recoverypoint_get" + ] + }, + { + "name": "manage_azure_backup_governance_and_dr", + "description": "Manage Azure Backup governance settings including soft delete, immutability, cross-region restore, and find unprotected resources.", + "toolMetadata": { + "destructive": { + "value": false, + "description": "This tool performs only additive updates without deleting or modifying existing resources." + }, + "idempotent": { + "value": true, + "description": "Running this operation multiple times with the same arguments produces the same result without additional effects." + }, + "openWorld": { + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." + }, + "readOnly": { + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." + }, + "secret": { + "value": false, + "description": "This tool does not handle sensitive or secret information." + }, + "localRequired": { + "value": false, + "description": "This tool is available in both local and remote server modes." + } + }, + "mappedToolList": [ + "azurebackup_governance_find-unprotected", + "azurebackup_governance_immutability", + "azurebackup_governance_soft-delete", + "azurebackup_dr_enablecrr" + ] } ] } diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx index 5d960f8187..0e446d1e12 100644 --- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx +++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx @@ -91,6 +91,7 @@ + diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index ab1fbb98cb..3669f337f9 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -882,7 +882,19 @@ Check out the remote hosting [azd templates](https://github.com/microsoft/mcp/bl * "Get the details for website 'my-website'" * "Get the details for app service plan 'my-app-service-plan'" -### 🖥️ Azure CLI Generate +### �️ Azure Backup + +* "Create a Recovery Services vault named 'myvault' in resource group 'myRG' in eastus" +* "Get details of backup vault 'myvault' in resource group 'myRG'" +* "Create a backup policy for Azure VMs in vault 'myvault'" +* "List protectable items in my backup vault" +* "Check backup status for my Azure resource" +* "Get recovery points for a protected item" +* "Find unprotected resources in my subscription" +* "Check soft delete and immutability settings on my vault" +* "Enable cross-region restore on my vault" + +### �🖥️ Azure CLI Generate * Generate Azure CLI commands based on user intent @@ -1047,6 +1059,7 @@ The Azure MCP Server provides tools for interacting with **42+ Azure service are - 🎤 **Azure AI Services Speech** - Speech-to-text recognition and text-to-speech synthesis - ⚙️ **Azure App Configuration** - Configuration management - 🕸️ **Azure App Service** - Web app hosting +- 🛡️ **Azure Backup** - Recovery Services vault management, backup policies, protection, jobs, recovery points, governance, and disaster recovery - 🛡️ **Azure Best Practices** - Secure, production-grade guidance - 🖥️ **Azure CLI Generate** - Generate Azure CLI commands from natural language - 📞 **Azure Communication Services** - SMS messaging and communication diff --git a/servers/Azure.Mcp.Server/changelog-entries/shrja-add-azure-backup-toolset.yaml b/servers/Azure.Mcp.Server/changelog-entries/shrja-add-azure-backup-toolset.yaml new file mode 100644 index 0000000000..bc6eb098f2 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/shrja-add-azure-backup-toolset.yaml @@ -0,0 +1,3 @@ +changes: + - section: "Features Added" + description: "Add Azure Backup toolset with 15 commands for vault management, backup policies, protection, jobs, recovery points, governance, and disaster recovery" diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index 37b7e82465..9cdeb98b66 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -539,6 +539,181 @@ azmcp appservice database add --subscription "my-subscription" \ - `--connection-string`: Custom connection string (optional - auto-generated if not provided) - `--tenant`: Azure tenant ID for authentication (optional) +### Azure Backup Operations + +#### Vault + +```bash +# Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault or 'dpp' for a Backup vault (Data Protection). Returns the created vault details. +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup vault create --subscription \ + --resource-group \ + --vault \ + --location \ + [--vault-type ] \ + [--sku ] \ + [--storage-type ] + +# Retrieves backup vault information. When --vault and --resource-group are specified, returns detailed information about a single vault including type, location, SKU, and storage redundancy. When omitted, lists all backup vaults (RSV and Backup vaults) in the subscription, optionally filtered by --vault-type ('rsv' or 'dpp'). +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup vault get --subscription \ + [--resource-group ] \ + [--vault ] \ + [--vault-type ] + +# Updates vault-level settings including storage redundancy, soft delete, immutability, and managed identity. +# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup vault update --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--redundancy ] \ + [--soft-delete ] \ + [--soft-delete-retention-days ] \ + [--immutability-state ] \ + [--identity-type ] \ + [--tags ] +``` + +#### Policy + +```bash +# Creates a backup policy for a specified workload type with schedule and retention rules. +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup policy create --subscription \ + --resource-group \ + --vault \ + --policy \ + [--vault-type ] \ + [--workload-type ] \ + [--schedule-frequency ] \ + [--schedule-time ] \ + [--daily-retention-days ] \ + [--weekly-retention-weeks ] \ + [--monthly-retention-months ] \ + [--yearly-retention-years ] + +# Retrieves backup policy information. When --policy is specified, returns detailed information about a single policy including datasource types and protected items count. When omitted, lists all backup policies configured in the vault. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup policy get --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--policy ] +``` + +#### Protected Item + +```bash +# Retrieves protected item information. When --protected-item is specified, returns detailed information about a single backup instance including protection status, datasource details, policy assignment, and last backup time. When --protected-item is omitted, lists all protected items in the vault. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup protecteditem get --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--protected-item ] \ + [--container ] + +# Enables backup protection for a resource by creating a protected item or backup instance. +# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup protecteditem protect --subscription \ + --resource-group \ + --vault \ + --datasource-id \ + --policy \ + [--vault-type ] \ + [--container ] \ + [--datasource-type ] +``` + +#### Protectable Item + +```bash +# Lists protectable items (SQL databases, SAP HANA databases) discovered in the Recovery Services vault. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup protectableitem list --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--workload-type ] \ + [--container ] +``` + +#### Backup + +```bash +# Checks whether a datasource is protected and returns vault and policy details. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup backup status --subscription \ + --datasource-id \ + --location +``` + +#### Job + +```bash +# Retrieves backup job information. When --job is specified, returns detailed information about a single job. When omitted, lists all backup jobs in the vault. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup job get --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--job ] +``` + +#### Recovery Point + +```bash +# Retrieves recovery point information for a protected item. When --recovery-point is specified, returns a single recovery point. When omitted, lists all available recovery points. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup recoverypoint get --subscription \ + --resource-group \ + --vault \ + --protected-item \ + [--vault-type ] \ + [--container ] \ + [--recovery-point ] +``` + +#### Governance + +```bash +# Scans the subscription to find Azure resources that are not currently protected by any backup policy. +# ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup governance find-unprotected --subscription \ + [--resource-type-filter ] \ + [--resource-group-filter ] \ + [--tag-filter ] + +# Configures the immutability state for a backup vault. +# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup governance immutability --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--immutability-state ] + +# Configures the soft delete settings for a backup vault. +# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup governance soft-delete --subscription \ + --resource-group \ + --vault \ + [--vault-type ] \ + [--soft-delete ] \ + [--soft-delete-retention-days ] +``` + +#### DR + +```bash +# Enables Cross-Region Restore on a GRS-enabled vault. +# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired +azmcp azurebackup dr enablecrr --subscription \ + --resource-group \ + --vault \ + [--vault-type ] +``` + ### Azure CLI Operations #### Generate diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index 54257827c8..8b808b59bf 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -106,6 +106,41 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | appservice_database_add | Set up database for app service with connection string under resource group | | appservice_database_add | Configure database for app service with the connection string in resource group | +## Azure Backup + +| Tool Name | Test Prompt | +|:----------|:----------| +| azurebackup_vault_create | Create a Recovery Services vault named in resource group in region | +| azurebackup_vault_create | Set up a new backup vault called in under resource group | +| azurebackup_vault_get | Get details of Recovery Services vault in resource group | +| azurebackup_vault_get | Show me information about vault in resource group | +| azurebackup_vault_update | Update vault in resource group to use GeoRedundant storage | +| azurebackup_vault_update | Change the redundancy of vault in resource group to LocallyRedundant | +| azurebackup_policy_create | Create a backup policy named for AzureIaasVM in vault in resource group | +| azurebackup_policy_create | Set up a new backup policy called for AzureStorage workload in vault under resource group | +| azurebackup_policy_get | Get backup policy from vault in resource group | +| azurebackup_policy_get | Show me the details of backup policy in vault under resource group | +| azurebackup_protecteditem_get | Get protected item details for in vault and resource group | +| azurebackup_protecteditem_get | Show backup status of protected item in vault under resource group | +| azurebackup_protecteditem_protect | Enable backup protection for using policy in vault and resource group | +| azurebackup_protecteditem_protect | Protect item with backup policy in vault under resource group | +| azurebackup_protectableitem_list | List protectable items in vault in resource group | +| azurebackup_protectableitem_list | Show me all items that can be backed up in vault under resource group | +| azurebackup_backup_status | Check backup status for resource | +| azurebackup_backup_status | What is the backup status of in my subscription? | +| azurebackup_job_get | Get backup job from vault in resource group | +| azurebackup_job_get | Show me the status of backup job in vault under resource group | +| azurebackup_recoverypoint_get | Get recovery points for protected item in vault and resource group | +| azurebackup_recoverypoint_get | List available recovery points for in vault under resource group | +| azurebackup_governance_find-unprotected | Find unprotected resources of type in my subscription | +| azurebackup_governance_find-unprotected | Show me Azure resources that are not backed up for datasource type | +| azurebackup_governance_immutability | Check immutability status of vault in resource group | +| azurebackup_governance_immutability | Is immutability enabled on vault in resource group ? | +| azurebackup_governance_soft-delete | Check soft delete configuration for vault in resource group | +| azurebackup_governance_soft-delete | Is soft delete enabled on vault in resource group ? | +| azurebackup_dr_enablecrr | Enable cross-region restore on vault in resource group | +| azurebackup_dr_enablecrr | Turn on cross-region restore for vault under resource group | + ## Azure Application Insights | Tool Name | Test Prompt | diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs index 1a9d35c3c5..bba204c080 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs @@ -29,37 +29,28 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - // Vault (consolidated get = get + list) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - // Policy (consolidated get = get + list) services.AddSingleton(); services.AddSingleton(); - // Protected item (consolidated get = get + list) services.AddSingleton(); services.AddSingleton(); - // Protectable item services.AddSingleton(); - // Backup services.AddSingleton(); - // Job (consolidated get = get + list) services.AddSingleton(); - // Recovery point (consolidated get = get + list) services.AddSingleton(); - // Governance services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - // DR services.AddSingleton(); } @@ -74,53 +65,44 @@ and Backup vaults (DPP/Data Protection). Supports vault management, protected it """, Title); - // Vault subgroup (get = list + get consolidated) var vault = new CommandGroup("vault", "Backup vault operations – Get vault details or list all vaults, create, and update vaults."); azureBackup.AddSubGroup(vault); RegisterCommand(serviceProvider, vault); RegisterCommand(serviceProvider, vault); RegisterCommand(serviceProvider, vault); - // Policy subgroup (get = list + get consolidated) var policy = new CommandGroup("policy", "Backup policy operations – Get policy details or list all policies, and create policies."); azureBackup.AddSubGroup(policy); RegisterCommand(serviceProvider, policy); RegisterCommand(serviceProvider, policy); - // Protected item subgroup (get = list + get consolidated) var protectedItem = new CommandGroup("protecteditem", "Protected item operations – Get protected item details or list all, and enable backup protection."); azureBackup.AddSubGroup(protectedItem); RegisterCommand(serviceProvider, protectedItem); RegisterCommand(serviceProvider, protectedItem); - // Protectable item subgroup var protectableItem = new CommandGroup("protectableitem", "Protectable item operations – List discovered databases available for protection."); azureBackup.AddSubGroup(protectableItem); RegisterCommand(serviceProvider, protectableItem); - // Backup subgroup var backup = new CommandGroup("backup", "Backup operations – Check backup status for a datasource."); azureBackup.AddSubGroup(backup); RegisterCommand(serviceProvider, backup); - // Job subgroup (get = list + get consolidated) var job = new CommandGroup("job", "Backup job operations – Get job details or list all jobs in a vault."); azureBackup.AddSubGroup(job); RegisterCommand(serviceProvider, job); - // Recovery point subgroup (get = list + get consolidated) var recoveryPoint = new CommandGroup("recoverypoint", "Recovery point operations – Get recovery point details or list all for a protected item."); azureBackup.AddSubGroup(recoveryPoint); RegisterCommand(serviceProvider, recoveryPoint); - // Governance subgroup var governance = new CommandGroup("governance", "Governance operations – Find unprotected resources, configure immutability and soft delete."); azureBackup.AddSubGroup(governance); RegisterCommand(serviceProvider, governance); RegisterCommand(serviceProvider, governance); RegisterCommand(serviceProvider, governance); - // DR subgroup var dr = new CommandGroup("dr", "Disaster recovery operations – Enable Cross-Region Restore on a GRS vault."); azureBackup.AddSubGroup(dr); RegisterCommand(serviceProvider, dr); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs index ab5e5badfd..c3953d943f 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs @@ -16,31 +16,21 @@ namespace Azure.Mcp.Tools.AzureBackup.Commands; -// Vault [JsonSerializable(typeof(VaultGetCommand.VaultGetCommandResult))] [JsonSerializable(typeof(VaultCreateCommand.VaultCreateCommandResult))] [JsonSerializable(typeof(VaultUpdateCommand.VaultUpdateCommandResult))] -// Policy [JsonSerializable(typeof(PolicyGetCommand.PolicyGetCommandResult))] [JsonSerializable(typeof(PolicyCreateCommand.PolicyCreateCommandResult))] -// Protected item [JsonSerializable(typeof(ProtectedItemGetCommand.ProtectedItemGetCommandResult))] [JsonSerializable(typeof(ProtectedItemProtectCommand.ProtectedItemProtectCommandResult))] -// Protectable item [JsonSerializable(typeof(ProtectableItemListCommand.ProtectableItemListCommandResult))] -// Backup [JsonSerializable(typeof(BackupStatusCommand.BackupStatusCommandResult))] -// Job [JsonSerializable(typeof(JobGetCommand.JobGetCommandResult))] -// Recovery point [JsonSerializable(typeof(RecoveryPointGetCommand.RecoveryPointGetCommandResult))] -// Governance [JsonSerializable(typeof(GovernanceFindUnprotectedCommand.GovernanceFindUnprotectedCommandResult))] [JsonSerializable(typeof(GovernanceImmutabilityCommand.GovernanceImmutabilityCommandResult))] [JsonSerializable(typeof(GovernanceSoftDeleteCommand.GovernanceSoftDeleteCommandResult))] -// DR [JsonSerializable(typeof(DrEnableCrrCommand.DrEnableCrrCommandResult))] -// Model types [JsonSerializable(typeof(BackupVaultInfo))] [JsonSerializable(typeof(ProtectedItemInfo))] [JsonSerializable(typeof(BackupPolicyInfo))] diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs index e7eec57139..dfee1fcb4d 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs @@ -67,7 +67,6 @@ public override async Task ExecuteAsync(CommandContext context, if (!string.IsNullOrEmpty(options.Job)) { - // Single job get var job = await service.GetJobAsync( options.Vault!, options.ResourceGroup!, @@ -84,7 +83,6 @@ public override async Task ExecuteAsync(CommandContext context, } else { - // List all jobs var jobs = await service.ListJobsAsync( options.Vault!, options.ResourceGroup!, diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs index f38997e9da..819f64ba80 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs @@ -67,7 +67,6 @@ public override async Task ExecuteAsync(CommandContext context, if (!string.IsNullOrEmpty(options.Policy)) { - // Single policy get var policy = await service.GetPolicyAsync( options.Vault!, options.ResourceGroup!, @@ -84,7 +83,6 @@ public override async Task ExecuteAsync(CommandContext context, } else { - // List all policies var policies = await service.ListPoliciesAsync( options.Vault!, options.ResourceGroup!, diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs index 47e4aef99a..5a4d56bf50 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.cs @@ -71,7 +71,6 @@ public override async Task ExecuteAsync(CommandContext context, if (!string.IsNullOrEmpty(options.ProtectedItem)) { - // Single item get var item = await service.GetProtectedItemAsync( options.Vault!, options.ResourceGroup!, @@ -89,7 +88,6 @@ public override async Task ExecuteAsync(CommandContext context, } else { - // List all protected items var items = await service.ListProtectedItemsAsync( options.Vault!, options.ResourceGroup!, diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs index 686df7e8d5..e2180b6020 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs @@ -67,7 +67,6 @@ public override async Task ExecuteAsync(CommandContext context, if (!string.IsNullOrEmpty(options.RecoveryPoint)) { - // Single recovery point get var rp = await service.GetRecoveryPointAsync( options.Vault!, options.ResourceGroup!, @@ -86,7 +85,6 @@ public override async Task ExecuteAsync(CommandContext context, } else { - // List all recovery points var points = await service.ListRecoveryPointsAsync( options.Vault!, options.ResourceGroup!, diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs index b292536202..d24e735c3a 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs @@ -74,7 +74,6 @@ public override async Task ExecuteAsync(CommandContext context, if (!string.IsNullOrEmpty(options.Vault)) { - // Single vault get var vault = await service.GetVaultAsync( options.Vault, options.ResourceGroup!, @@ -90,7 +89,6 @@ public override async Task ExecuteAsync(CommandContext context, } else { - // List all vaults var vaults = await service.ListVaultsAsync( options.Subscription!, options.VaultType, diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs index 9d881129ee..1906ae2478 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs @@ -5,7 +5,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Options; public static class AzureBackupOptionDefinitions { - // Existing option names public const string VaultName = "vault"; public const string VaultTypeName = "vault-type"; public const string ProtectedItemName = "protected-item"; @@ -22,7 +21,6 @@ public static class AzureBackupOptionDefinitions public const string TargetResourceIdName = "target-resource-id"; public const string RestoreLocationName = "restore-location"; - // New option names for expanded tool set public const string RedundancyName = "redundancy"; public const string EnableCrrName = "enable-crr"; public const string EncryptionTypeName = "encryption-type"; @@ -109,7 +107,6 @@ public static class AzureBackupOptionDefinitions public const string StatusFilterName = "status-filter"; public const string OperationFilterName = "operation-filter"; - // Existing option objects public static readonly Option Vault = new($"--{VaultName}") { Description = "The name of the backup vault (Recovery Services vault or Backup vault).", @@ -200,7 +197,6 @@ public static class AzureBackupOptionDefinitions Required = false }; - // New option objects for expanded tool set public static readonly Option Redundancy = new($"--{RedundancyName}") { Description = "Storage redundancy: LRS, GRS, ZRS, or RAGRS.", diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs index f29e64dea2..c1d471900d 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -32,7 +32,6 @@ public async Task GetVaultAsync( : await dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); } - // Auto-detect: try RSV first, then DPP return await AutoDetectAndExecuteAsync( () => rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken), () => dppOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken), @@ -53,7 +52,6 @@ public async Task> ListVaultsAsync( return await dppOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); } - // List both types and merge var rsvTask = rsvOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); var dppTask = dppOps.ListVaultsAsync(subscription, tenant, retryPolicy, cancellationToken); @@ -204,7 +202,6 @@ public async Task> ListRecoveryPointsAsync( : await dppOps.ListRecoveryPointsAsync(vaultName, resourceGroup, subscription, protectedItemName, tenant, retryPolicy, cancellationToken); } - // ── New facade methods ── public async Task UpdateVaultAsync( string vaultName, string resourceGroup, string subscription, @@ -321,7 +318,6 @@ public Task EnableAutoProtectionAsync( string workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // Auto-protection is an RSV-only feature for SQL/HANA workloads return rsvOps.EnableAutoProtectionAsync(vaultName, resourceGroup, subscription, vmResourceId, instanceName, policyName, workloadType, tenant, retryPolicy, cancellationToken); } @@ -330,7 +326,6 @@ public Task> ListContainersAsync( string? vaultType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // Container listing is an RSV-only feature (DPP vaults don't use the container concept) return rsvOps.ListContainersAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); } @@ -339,7 +334,6 @@ public Task RegisterContainerAsync( string vmResourceId, string workloadType, string? vaultType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // Container registration is an RSV-only feature for SQL/HANA workloads return rsvOps.RegisterContainerAsync(vaultName, resourceGroup, subscription, vmResourceId, workloadType, tenant, retryPolicy, cancellationToken); } @@ -348,7 +342,6 @@ public Task TriggerInquiryAsync( string containerName, string? workloadType, string? vaultType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // Inquiry is an RSV-only feature for SQL/HANA workloads return rsvOps.TriggerInquiryAsync(vaultName, resourceGroup, subscription, containerName, workloadType, tenant, retryPolicy, cancellationToken); } @@ -357,7 +350,6 @@ public Task> ListProtectableItemsAsync( string? workloadType, string? containerName, string? vaultType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // Protectable items listing is an RSV-only feature for SQL/HANA workloads return rsvOps.ListProtectableItemsAsync(vaultName, resourceGroup, subscription, workloadType, containerName, tenant, retryPolicy, cancellationToken); } @@ -589,7 +581,6 @@ public Task MoveRecoveryPointToArchiveAsync( return Task.FromResult(new OperationResult("Accepted", null, $"Recovery point archive initiated for '{protectedItemName}'.")); } - // ── Workflow methods ── public Task SetupVmBackupAsync( string resourceIds, string resourceGroup, string subscription, @@ -755,7 +746,6 @@ private async Task ResolveVaultTypeAsync( return vaultType!; } - // Auto-detect by trying RSV first, then DPP try { await rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); @@ -763,7 +753,6 @@ private async Task ResolveVaultTypeAsync( } catch (RequestFailedException ex) when (ex.Status == 404) { - // Not an RSV vault, try DPP } try @@ -786,7 +775,6 @@ private static async Task AutoDetectAndExecuteAsync( } catch (RequestFailedException ex) when (ex.Status == 404) { - // Not found in RSV, try DPP } try diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index 87ac83ddc1..0e9d9550f0 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -24,7 +24,6 @@ public class DppBackupOperations(ITenantService tenantService) : BaseAzureServic /// internal static DppDatasourceProfile ResolveProfile(string datasourceTypeOrArm) { - // First try auto-detection (e.g. storage account → Blob) var autoDetected = DppDatasourceRegistry.TryAutoDetect(datasourceTypeOrArm); if (autoDetected != null) { @@ -134,16 +133,13 @@ public async Task ProtectItemAsync( var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); var datasourceResourceId = new ResourceIdentifier(datasourceId); - // Resolve the datasource profile from user-supplied type or auto-detect from ARM resource type var resolvedDatasourceType = datasourceType ?? datasourceResourceId.ResourceType.ToString(); var profile = ResolveProfile(resolvedDatasourceType); - // Generate instance name based on profile's naming mode var instanceName = DppDatasourceRegistry.GenerateInstanceName(profile, datasourceResourceId); var policyInfo = new BackupInstancePolicyInfo(policyId); - // Set snapshot resource group for datasource types that require it (Disk, AKS, ESAN — not Blob) if (profile.RequiresSnapshotResourceGroup) { var snapshotRgId = ResourceGroupResource.CreateResourceIdentifier(subscription, datasourceResourceId.ResourceGroupName ?? resourceGroup); @@ -155,7 +151,6 @@ public async Task ProtectItemAsync( policyInfo.PolicyParameters.DataStoreParametersList.Add(opStoreSettings); } - // Add datasource-specific backup parameters (e.g. AKS K8s cluster settings) if (profile.BackupParametersMode == DppBackupParametersMode.KubernetesCluster) { policyInfo.PolicyParameters ??= new BackupInstancePolicySettings(); @@ -181,7 +176,6 @@ public async Task ProtectItemAsync( ObjectType = "BackupInstance", }; - // Set DataSourceSetInfo based on profile configuration if (profile.DataSourceSetMode != DppDataSourceSetMode.None) { var setId = profile.DataSourceSetMode == DppDataSourceSetMode.Parent @@ -270,11 +264,9 @@ public async Task TriggerBackupAsync( var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); - // Fetch backup instance to get associated policy var instanceData = await instanceResource.GetAsync(cancellationToken); var policyId = instanceData.Value.Data.Properties.PolicyInfo.PolicyId; - // Fetch policy to find the backup rule name var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); var policyData = await policyResource.GetAsync(cancellationToken); @@ -316,7 +308,6 @@ public async Task TriggerRestoreAsync( var instanceId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); var instanceResource = armClient.GetDataProtectionBackupInstanceResource(instanceId); - // Get the backup instance to determine datasource info var instance = await instanceResource.GetAsync(cancellationToken); var datasourceInfo = instance.Value.Data.Properties?.DataSourceInfo; var datasourceId = datasourceInfo?.ResourceId; @@ -324,16 +315,12 @@ public async Task TriggerRestoreAsync( RestoreTargetInfoBase restoreTarget; - // Determine if this is a restore-as-files scenario (target is a storage account) if (!string.IsNullOrEmpty(targetResourceId) && targetResourceId.Contains("Microsoft.Storage/storageAccounts", StringComparison.OrdinalIgnoreCase)) { - // Restore-as-files: target is a storage account with optional container var storageAccountId = targetResourceId; var containerName = "pgflex-restore"; - // Extract container name from the target resource ID if it includes a container - // e.g., .../storageAccounts/name/blobServices/default/containers/containerName if (targetResourceId.Contains("/blobServices/", StringComparison.OrdinalIgnoreCase)) { var parts = targetResourceId.Split('/'); @@ -342,15 +329,12 @@ public async Task TriggerRestoreAsync( storageAccountId = targetResourceId[..containerIndex]; } - // Extract storage account name from the ARM ID var storageAccountArmId = new ResourceIdentifier(storageAccountId); var storageAccountName = storageAccountArmId.Name; var containerUri = new Uri($"https://{storageAccountName}.blob.core.windows.net/{containerName}"); var filePrefix = $"pgflex-restore-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; - // TargetResourceArmId must point to the container (not the storage account) - // per the REST API spec: "ARM Id pointing to container / file share" var containerArmId = new ResourceIdentifier( $"{storageAccountId}/blobServices/default/containers/{containerName}"); @@ -371,7 +355,6 @@ public async Task TriggerRestoreAsync( } else { - // Restore-as-server: target is a datasource resource var targetId = !string.IsNullOrEmpty(targetResourceId) ? new ResourceIdentifier(targetResourceId) : datasourceId; var targetDatasource = new DataSourceInfo(targetId ?? datasourceId!) @@ -390,7 +373,6 @@ public async Task TriggerRestoreAsync( RestoreLocation = location, }; - // Set DataSourceSetInfo for datasource types that require it (ESAN, AKS) var resolvedDatasourceTypeStr = datasourceInfo?.DataSourceType ?? string.Empty; var restoreProfile = ResolveProfile(resolvedDatasourceTypeStr); if (restoreProfile.DataSourceSetMode != DppDataSourceSetMode.None) @@ -412,12 +394,10 @@ public async Task TriggerRestoreAsync( restoreTarget = restoreTargetInfo; } - // Determine the correct source data store type from the datasource profile var datasourceTypeStr = datasourceInfo?.DataSourceType ?? string.Empty; var storeProfile = ResolveProfile(datasourceTypeStr); var sourceDataStoreType = storeProfile.UsesOperationalStore ? SourceDataStoreType.OperationalStore : SourceDataStoreType.VaultStore; - // Use time-based restore for blob PIT or when --point-in-time is provided BackupRestoreContent restoreContent; if (!string.IsNullOrEmpty(pointInTime)) { @@ -516,8 +496,6 @@ public async Task GetJobAsync( } catch (FormatException) { - // Bug #40 workaround: SDK can't parse ISO 8601 durations like "PT3M7.0153191S" - // in DataProtectionBackupJobData. Fall back to listing jobs and matching by ID. var jobs = await ListJobsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); return jobs.FirstOrDefault(j => j.Name == jobId) ?? throw new InvalidOperationException($"Job '{jobId}' not found. The SDK cannot parse this job's duration field."); @@ -592,7 +570,6 @@ public async Task> ListRecoveryPointsAsync( return points; } - // ── New methods ── public async Task UpdateVaultAsync( string vaultName, string resourceGroup, string subscription, @@ -656,13 +633,11 @@ public async Task UpdatePolicyAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // Fetch the existing policy — will throw RequestFailedException (404) if it doesn't exist var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); var existingPolicy = await policyResource.GetAsync(cancellationToken); var policyData = existingPolicy.Value.Data; - // Modify only the requested retention on existing policy rules, preserving datasourceTypes and structure if (policyData.Properties is RuleBasedBackupPolicy ruleBasedPolicy) { foreach (var rule in ruleBasedPolicy.PolicyRules) @@ -677,7 +652,6 @@ public async Task UpdatePolicyAsync( } } - // PUT the modified policy back with original datasourceTypes preserved var vaultId = DataProtectionBackupVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); var collection = vaultResource.GetDataProtectionBackupPolicies(); @@ -706,7 +680,6 @@ public async Task CreatePolicyAsync( var vaultResource = armClient.GetDataProtectionBackupVaultResource(vaultId); var collection = vaultResource.GetDataProtectionBackupPolicies(); - // Parse retention and schedule parameters var retentionDays = int.TryParse(dailyRetentionDays, out var dd) ? dd : 0; var scheduleTimeValue = scheduleTime ?? "02:00"; var now = DateTimeOffset.UtcNow; @@ -715,14 +688,11 @@ public async Task CreatePolicyAsync( var scheduleMinute = scheduleParts.Length > 1 && int.TryParse(scheduleParts[1], out var sm) ? sm : 0; var scheduleStartTime = new DateTimeOffset(now.Year, now.Month, now.Day, scheduleHour, scheduleMinute, 0, TimeSpan.Zero); - // Resolve the datasource profile — drives all policy construction decisions var profile = DppDatasourceRegistry.Resolve(workloadType); var dataStoreType = profile.UsesOperationalStore ? DataStoreType.OperationalStore : DataStoreType.VaultStore; - // Use profile default retention if user didn't specify var defaultRetention = retentionDays > 0 ? retentionDays : profile.DefaultRetentionDays; - // Build the retention rule (Default) var retentionDeleteSetting = new DataProtectionBackupAbsoluteDeleteSetting(TimeSpan.FromDays(defaultRetention)); var retentionDataStore = new DataStoreInfoBase(dataStoreType, "DataStoreInfoBase"); var retentionLifeCycle = new SourceLifeCycle(retentionDeleteSetting, retentionDataStore); @@ -733,8 +703,6 @@ public async Task CreatePolicyAsync( List rules = [retentionRule]; - // Continuous backup (Blob, ADLS, CosmosDB) — no scheduled backup rule - // All other workloads — add a scheduled backup rule driven by profile settings if (!profile.IsContinuousBackup) { var repeatingInterval = $"R/{scheduleStartTime:yyyy-MM-ddTHH:mm:ss+00:00}/{profile.ScheduleInterval}"; @@ -804,7 +772,6 @@ public async Task StopProtectionAsync( return new OperationResult("Accepted", null, "Protection stopped and data deletion initiated."); } - // DPP suspend backup var instId = DataProtectionBackupInstanceResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, protectedItemName); var instResource = armClient.GetDataProtectionBackupInstanceResource(instId); await instResource.SuspendBackupsAsync(WaitUntil.Started, cancellationToken); @@ -836,7 +803,6 @@ public Task ModifyProtectionAsync( string protectedItemName, string? newPolicyName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // DPP modify requires re-creating the instance with a different policy return Task.FromResult(new OperationResult("NotSupported", null, "To change policy for a DPP backup instance, stop protection (RetainData) and re-protect with the new policy.")); } @@ -1010,7 +976,6 @@ private static RecoveryPointInfo MapToRecoveryPointInfo(DataProtectionBackupReco { if (response.Headers.TryGetValue("Azure-AsyncOperation", out var asyncOpUrl) && !string.IsNullOrEmpty(asyncOpUrl)) { - // Format: https://.../operationResults/{jobId}?api-version=... var uri = new Uri(asyncOpUrl); var segments = uri.AbsolutePath.Split('/'); return segments.Length > 0 ? segments[^1] : null; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs index 23dfb57e6d..f245209e03 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs @@ -67,7 +67,6 @@ public enum DppBackupParametersMode /// public sealed record DppDatasourceProfile { - // ── Identity ── /// Friendly name used for user-facing resolution (e.g. "AzureDisk", "AKS"). public required string FriendlyName { get; init; } @@ -78,12 +77,10 @@ public sealed record DppDatasourceProfile /// Alternative user-supplied names that resolve to this profile. public string[] Aliases { get; init; } = []; - // ── Data Store ── /// Whether this datasource uses OperationalStore (snapshot-based) or VaultStore. public bool UsesOperationalStore { get; init; } - // ── Policy / Schedule ── /// True for continuous backup (Blob, ADLS) — no scheduled backup rule created. public bool IsContinuousBackup { get; init; } @@ -100,7 +97,6 @@ public sealed record DppDatasourceProfile /// Default retention days when user doesn't specify. public int DefaultRetentionDays { get; init; } = 30; - // ── Protection ── /// Whether ProtectItemAsync must set the snapshot resource group parameter. public bool RequiresSnapshotResourceGroup { get; init; } @@ -114,12 +110,10 @@ public sealed record DppDatasourceProfile /// Instance naming pattern for backup instances. public DppInstanceNamingMode InstanceNamingMode { get; init; } = DppInstanceNamingMode.Standard; - // ── Restore ── /// The default restore approach (RP-based, PIT, or restore-as-files). public DppRestoreMode DefaultRestoreMode { get; init; } = DppRestoreMode.RecoveryPoint; - // ── Auto-detection ── /// /// If non-null, when the user's resource ID matches this base ARM type (e.g. "Microsoft.Storage/storageAccounts"), @@ -128,7 +122,6 @@ public sealed record DppDatasourceProfile /// public string? AutoDetectFromBaseResourceType { get; init; } - // ── Policy Update ── /// Whether the Azure API supports updating policies for this datasource type. public bool SupportsPolicyUpdate { get; init; } diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs index 85bed4aa74..eaf2328429 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs @@ -17,7 +17,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Services; /// public static class DppDatasourceRegistry { - // ── Profile definitions ── public static readonly DppDatasourceProfile AzureDisk = new() { @@ -138,7 +137,6 @@ public static class DppDatasourceRegistry SupportsPolicyUpdate = false, }; - // ── Registry ── /// All registered DPP datasource profiles. public static readonly DppDatasourceProfile[] AllProfiles = @@ -183,8 +181,6 @@ public static DppDatasourceProfile Resolve(string workloadTypeOrArmType) } } - // Fallback: treat unknown types as VaultStore with daily schedule. - // This ensures forward compatibility when new Azure datasource types are added. return new DppDatasourceProfile { FriendlyName = workloadTypeOrArmType, @@ -226,8 +222,6 @@ public static ResourceIdentifier GetParentResourceId(ResourceIdentifier childRes { var idStr = childResourceId.ToString(); - // Find the last child segment: .../parentType/parentName/childType/childName - // Strip /childType/childName to get the parent ID var lastSlash = idStr.LastIndexOf('/'); if (lastSlash > 0) { diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs index c97b0b856a..3f918e21a0 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs @@ -8,7 +8,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Services; public interface IAzureBackupService { - // ── Existing methods (keep all of these exactly as they are) ── Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string vaultType, string location, string? sku = null, string? storageType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task> ListVaultsAsync(string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); @@ -24,76 +23,59 @@ public interface IAzureBackupService Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // ── NEW methods ── - // Vault operations Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? redundancy = null, string? softDelete = null, string? softDeleteRetentionDays = null, string? immutabilityState = null, string? identityType = null, string? tags = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, bool force = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Policy operations Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? vaultType = null, string? scheduleFrequency = null, string? scheduleTime = null, string? dailyRetentionDays = null, string? weeklyRetentionWeeks = null, string? monthlyRetentionMonths = null, string? yearlyRetentionYears = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task UpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? vaultType = null, string? scheduleFrequency = null, string? dailyRetentionDays = null, string? weeklyRetentionWeeks = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task DeletePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Protection management Task StopProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string mode, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ResumeProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? policyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ModifyProtectionAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? newPolicyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task UndeleteProtectedItemAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task EnableAutoProtectionAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string instanceName, string policyName, string workloadType, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Container and workload discovery operations Task> ListContainersAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task RegisterContainerAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string workloadType, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task TriggerInquiryAsync(string vaultName, string resourceGroup, string subscription, string containerName, string? workloadType = null, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task> ListProtectableItemsAsync(string vaultName, string resourceGroup, string subscription, string? workloadType = null, string? containerName = null, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Backup operations Task GetBackupStatusAsync(string datasourceId, string subscription, string location, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Job operations Task CancelJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Security operations Task ConfigureRbacAsync(string principalId, string roleName, string scope, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ConfigureMuaAsync(string vaultName, string resourceGroup, string subscription, string resourceGuardId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ConfigurePrivateEndpointAsync(string vaultName, string resourceGroup, string subscription, string vnetId, string subnetId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ConfigureEncryptionAsync(string vaultName, string resourceGroup, string subscription, string keyVaultUri, string keyName, string identityType, string? vaultType = null, string? keyVersion = null, string? userAssignedIdentityId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Monitoring operations Task ConfigureMonitoringAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? logAnalyticsWorkspaceId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task GetBackupReportsAsync(string reportType, string logAnalyticsWorkspaceId, string? timeRangeDays = null, string? workloadFilter = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Governance operations Task> FindUnprotectedResourcesAsync(string subscription, string? resourceTypeFilter = null, string? resourceGroupFilter = null, string? tagFilter = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ApplyAzurePolicyAsync(string policyDefinitionId, string scope, string? policyParameters = null, bool deployRemediation = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ConfigureImmutabilityAsync(string vaultName, string resourceGroup, string subscription, string immutabilityState, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ConfigureSoftDeleteAsync(string vaultName, string resourceGroup, string subscription, string softDeleteState, string? vaultType = null, string? softDeleteRetentionDays = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // DR operations Task ConfigureCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task TriggerCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string restoreMode, string? targetResourceId = null, string? secondaryRegion = null, string? vaultType = null, string? containerName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ValidateDrReadinessAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? resourceIds = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Cost operations Task EstimateBackupCostAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? workloadType = null, bool includeArchiveProjection = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Diagnostics operations Task DiagnoseBackupFailureAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? jobId = null, string? datasourceId = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task ValidateBackupPrerequisitesAsync(string datasourceId, string vaultName, string resourceGroup, string subscription, string workloadType, string? vaultType = null, string? policyName = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task RunBackupHealthCheckAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, int? rpoThresholdHours = null, bool includeSecurityPosture = true, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Bulk operations Task BulkEnableBackupAsync(string vaultName, string subscription, string workloadType, string policyName, string? vaultType = null, string? resourceGroupFilter = null, string? tagFilter = null, string? resourceIds = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task BulkTriggerBackupAsync(string vaultName, string resourceGroup, string subscription, string? vaultType = null, string? workloadType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task BulkUpdatePolicyAsync(string vaultName, string resourceGroup, string subscription, string sourcePolicyName, string targetPolicyName, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // IaC operations Task GenerateIacFromVaultAsync(string vaultName, string resourceGroup, string subscription, string iacFormat, string? vaultType = null, bool includeProtectedItems = true, bool includeRbac = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Recovery point archive Task MoveRecoveryPointToArchiveAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? recoveryPointId = null, string? containerName = null, bool checkEligibilityOnly = false, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); - // Workflow operations Task SetupVmBackupAsync(string resourceIds, string resourceGroup, string subscription, string location, string? vaultName = null, string? scheduleFrequency = null, string? dailyRetentionDays = null, bool triggerFirstBackup = true, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task SetupSqlHanaBackupAsync(string vmResourceId, string workloadType, string resourceGroup, string subscription, string location, string? vaultName = null, bool autoProtect = true, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); Task SetupAksBackupAsync(string clusterResourceId, string resourceGroup, string subscription, string location, string snapshotResourceGroup, string? vaultName = null, string? outputIac = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs index 956aa02d65..9aaeb8872f 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs @@ -8,7 +8,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Services; public interface IDppBackupOperations { - // Existing methods Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string location, string? sku, string? storageType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task> ListVaultsAsync(string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); @@ -24,7 +23,6 @@ public interface IDppBackupOperations Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); - // New methods Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? redundancy, string? softDelete, string? softDeleteRetentionDays, string? immutabilityState, string? identityType, string? tags, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? scheduleFrequency, string? scheduleTime, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? monthlyRetentionMonths, string? yearlyRetentionYears, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs index bfc7be9e7e..fbb9c0c4f0 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs @@ -8,7 +8,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Services; public interface IRsvBackupOperations { - // Existing methods Task CreateVaultAsync(string vaultName, string resourceGroup, string subscription, string location, string? sku, string? storageType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task GetVaultAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task> ListVaultsAsync(string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); @@ -24,7 +23,6 @@ public interface IRsvBackupOperations Task GetRecoveryPointAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string recoveryPointId, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task> ListRecoveryPointsAsync(string vaultName, string resourceGroup, string subscription, string protectedItemName, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); - // New methods Task UpdateVaultAsync(string vaultName, string resourceGroup, string subscription, string? redundancy, string? softDelete, string? softDeleteRetentionDays, string? immutabilityState, string? identityType, string? tags, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task DeleteVaultAsync(string vaultName, string resourceGroup, string subscription, bool force, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task CreatePolicyAsync(string vaultName, string resourceGroup, string subscription, string policyName, string workloadType, string? scheduleFrequency, string? scheduleTime, string? dailyRetentionDays, string? weeklyRetentionWeeks, string? monthlyRetentionMonths, string? yearlyRetentionYears, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); @@ -40,7 +38,6 @@ public interface IRsvBackupOperations Task ConfigureCrossRegionRestoreAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task RunBackupHealthCheckAsync(string vaultName, string resourceGroup, string subscription, int? rpoThresholdHours, bool includeSecurityPosture, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); - // Workload container operations (SQL/HANA in IaaS VM) Task> ListContainersAsync(string vaultName, string resourceGroup, string subscription, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task RegisterContainerAsync(string vaultName, string resourceGroup, string subscription, string vmResourceId, string workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); Task TriggerInquiryAsync(string vaultName, string resourceGroup, string subscription, string containerName, string? workloadType, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs index 4035487868..2c6505c649 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -108,7 +108,6 @@ public async Task ProtectItemAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // Get the vault to determine its location var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); @@ -117,21 +116,15 @@ public async Task ProtectItemAsync( var policyArmId = BackupProtectionPolicyResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, policyName); - // Resolve the datasource profile var profile = RsvDatasourceRegistry.ResolveOrDefault(datasourceType); - // Check if this is a workload (SQL/HANA/ASE) protection request if (profile.IsWorkloadType) { - // For workload protection, containerName and datasourceId (protectable item name) are required if (string.IsNullOrEmpty(containerName)) { throw new ArgumentException($"The --container parameter is required for {profile.FriendlyName} workload protection. Use 'azurebackup protectable_item_list' to discover containers and items."); } - // Bug #47: Detect if user passed an ARM resource ID instead of the protectable item name. - // For workloads, datasource-id must be the protectable item name (e.g., 'SAPHanaDatabase;instance;db') - // from 'protectableitem list', NOT the VM ARM resource ID. if (datasourceId.StartsWith("/subscriptions/", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException( @@ -144,7 +137,6 @@ public async Task ProtectItemAsync( var protectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); - // Construct the correct SDK protected item type based on profile BackupGenericProtectedItem protectedItemProperties = profile.ProtectedItemType switch { RsvProtectedItemType.SapHanaDatabase => new VmWorkloadSapHanaDatabaseProtectedItem { PolicyId = policyArmId }, @@ -162,32 +154,26 @@ public async Task ProtectItemAsync( jobId != null ? $"Workload protection initiated. Use 'azurebackup job get --job {jobId}' to monitor progress." : "Workload protection initiated."); } - // Azure File Share protection flow if (profile.ProtectedItemType == RsvProtectedItemType.AzureFileShare) { var fsContainer = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId, datasourceType); var fsProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId, datasourceType); - // Trigger storage account inquiry so the vault discovers file shares var containerId = BackupProtectionContainerResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, FabricName, fsContainer); var containerResource = armClient.GetBackupProtectionContainerResource(containerId); try { await containerResource.InquireAsync(filter: null, cancellationToken); - // Wait briefly for inquiry to complete await Task.Delay(5000, cancellationToken); } catch (RequestFailedException) { - // Inquiry may fail if container is not yet registered; proceed anyway } var fsProtectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, FabricName, fsContainer, fsProtectedItemName); - // Derive the source resource ID (the storage account ARM ID) - // We're already in the AzureFileShare block, so always extract the storage account ID var parsedDatasourceId = new ResourceIdentifier(datasourceId); var storageAccountId = RsvNamingHelper.GetStorageAccountId(parsedDatasourceId); @@ -210,16 +196,12 @@ public async Task ProtectItemAsync( fsJobId != null ? $"File share protection initiated. Use 'azurebackup job get --job {fsJobId}' to monitor progress." : "File share protection initiated."); } - // Standard VM protection flow - // Trigger container discovery/refresh so the vault discovers the VM var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); var rgResource = armClient.GetResourceGroupResource(rgId); await rgResource.RefreshProtectionContainerAsync(vaultName, FabricName, filter: null, cancellationToken); - // Wait for container discovery to complete (refresh is async on the server side) await Task.Delay(30000, cancellationToken); - // Derive container name if not provided var container = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId); var vmProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId); @@ -238,8 +220,6 @@ public async Task ProtectItemAsync( var vmProtectedItemResource = armClient.GetBackupProtectedItemResource(vmProtectedItemId); var vmResult = await vmProtectedItemResource.UpdateAsync(WaitUntil.Started, vmProtectedItemData, cancellationToken); - // The Azure-AsyncOperation header contains an operation ID, not a job ID. - // We need to find the actual job by listing recent jobs. var vmJobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "ConfigureBackup", cancellationToken); vmJobId ??= ExtractOperationIdFromResponse(vmResult.GetRawResponse()); // Fallback to operation ID @@ -263,7 +243,6 @@ public async Task GetProtectedItemAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // If container name is not provided, we need to list and find the item if (string.IsNullOrEmpty(containerName)) { var items = await ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); @@ -335,8 +314,6 @@ public async Task TriggerBackupAsync( var result = await itemResource.TriggerBackupAsync(backupContent, cancellationToken); - // The Azure-AsyncOperation header contains an operation ID, not a job ID. - // We need to find the actual backup job by listing recent jobs. var jobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "Backup", cancellationToken); jobId ??= ExtractOperationIdFromResponse(result); // Fallback to operation ID if job not found yet @@ -348,7 +325,6 @@ public async Task TriggerBackupAsync( private static BackupContent CreateBackupRequestContent(string? backupType, DateTimeOffset? expiryTime, string? protectedItemName = null, string? containerName = null) { - // If a specific backup type is provided (Full, Differential, Log), use workload backup content if (!string.IsNullOrEmpty(backupType) && !backupType.Equals("Default", StringComparison.OrdinalIgnoreCase)) { return new WorkloadBackupContent @@ -359,7 +335,6 @@ private static BackupContent CreateBackupRequestContent(string? backupType, Date }; } - // Auto-detect workload type from item/container naming conventions using the registry var detectedProfile = RsvDatasourceRegistry.ResolveFromProtectedItemName(protectedItemName ?? string.Empty, containerName); var isWorkload = detectedProfile.IsWorkloadType; @@ -373,7 +348,6 @@ private static BackupContent CreateBackupRequestContent(string? backupType, Date }; } - // Default: IaasVmBackupContent for VM workloads return new IaasVmBackupContent { RecoveryPointExpireOn = expiryTime @@ -402,14 +376,12 @@ public async Task TriggerRestoreAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // Get the existing protected item to determine type and extract SourceResourceId var piId = BackupProtectedItemResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); var piResource = armClient.GetBackupProtectedItemResource(piId); var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); var existingProperties = existingItem.Value.Data.Properties; - // Get vault location for restore region var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); var vault = await vaultResource.GetAsync(cancellationToken); @@ -421,10 +393,8 @@ public async Task TriggerRestoreAsync( RestoreContent restoreProperties; - // Check if this is a workload (SQL/HANA) protected item if (existingProperties is VmWorkloadSqlDatabaseProtectedItem) { - // For SQL ALR, extract data directory paths from recovery point extended info IList? sqlDataDirMappings = null; if (!string.IsNullOrEmpty(targetDatabaseName)) { @@ -465,11 +435,9 @@ public async Task TriggerRestoreAsync( } else { - // Standard VM restore var sourceResourceId = (existingProperties as IaasComputeVmProtectedItem)?.SourceResourceId ?? (existingProperties as BackupGenericProtectedItem)?.SourceResourceId; - // Determine restore mode: AlternateLocation, RestoreDisks, or OriginalLocation var resolvedMode = !string.IsNullOrEmpty(restoreMode) ? restoreMode : !string.IsNullOrEmpty(targetResourceId) ? "RestoreDisks" : "OriginalLocation"; @@ -498,7 +466,6 @@ public async Task TriggerRestoreAsync( if (recoveryType == FileShareRecoveryType.AlternateLocation) { - // Full ALR: create a new VM at alternate location if (string.IsNullOrEmpty(targetVmName) || string.IsNullOrEmpty(targetVnetId) || string.IsNullOrEmpty(targetSubnetId)) { throw new ArgumentException("AlternateLocation restore requires --target-vm-name, --target-vnet-id, and --target-subnet-id parameters."); @@ -514,7 +481,6 @@ public async Task TriggerRestoreAsync( } else if (recoveryType == FileShareRecoveryType.RestoreDisks && !string.IsNullOrEmpty(targetResourceId)) { - // RestoreDisks: restore managed disks to target RG vmRestoreContent.TargetResourceGroupId = new ResourceIdentifier(targetResourceId); } @@ -528,7 +494,6 @@ public async Task TriggerRestoreAsync( var result = await rpResource.TriggerRestoreAsync(WaitUntil.Started, restoreContent, cancellationToken); - // The Azure-AsyncOperation header contains an operation ID, not a job ID. var jobId = await FindLatestJobIdAsync(armClient, subscription, resourceGroup, vaultName, "Restore", cancellationToken); jobId ??= ExtractOperationIdFromResponse(result.GetRawResponse()); // Fallback to operation ID @@ -679,7 +644,6 @@ public async Task> ListRecoveryPointsAsync( return points; } - // ── New methods ── public async Task UpdateVaultAsync( string vaultName, string resourceGroup, string subscription, @@ -743,14 +707,12 @@ public async Task UpdatePolicyAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // Fetch the existing policy — will throw RequestFailedException (404) if it doesn't exist var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier( subscription, resourceGroup, vaultName, policyName); var policyResource = armClient.GetBackupProtectionPolicyResource(policyId); var existingPolicy = await policyResource.GetAsync(cancellationToken); var policyData = existingPolicy.Value.Data; - // Modify only the requested fields on the existing policy if (policyData.Properties is FileShareProtectionPolicy fsPolicy) { UpdateFileSharePolicyRetention(fsPolicy, dailyRetentionDays, weeklyRetentionWeeks); @@ -765,13 +727,11 @@ public async Task UpdatePolicyAsync( UpdateWorkloadPolicyRetention(workloadPolicy, dailyRetentionDays); } - // Fetch vault location for PUT — the GET response data may lack location, causing NullRef in SDK constructor var vaultResourceId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResourceId); var vault = await vaultResource.GetAsync(cancellationToken); var vaultLocation = vault.Value.Data.Location; - // PUT the modified policy back using fresh data with vault location var freshPolicyData = new BackupProtectionPolicyData(vaultLocation) { Properties = policyData.Properties @@ -786,9 +746,6 @@ public async Task UpdatePolicyAsync( } catch (NullReferenceException) { - // SDK v1.3.0 bug: BackupProtectionPolicyResource constructor throws NullRef - // when deserializing certain policy types (e.g. VM policies with enhanced schedules). - // The PUT REST call itself succeeds — verify by re-fetching the updated policy. var verifyPolicy = await policyResource.GetAsync(cancellationToken); if (verifyPolicy.Value?.Data?.Properties == null) { @@ -913,19 +870,16 @@ public async Task CreatePolicyAsync( var retentionDays = int.TryParse(dailyRetentionDays, out var dd) ? dd : 30; - // Parse schedule time or default to 2:00 AM UTC var scheduleDateTime = DateTimeOffset.TryParse(scheduleTime, out var st) ? st : new DateTimeOffset(DateTime.UtcNow.Date.AddHours(2), TimeSpan.Zero); var scheduleRunTime = new DateTimeOffset(scheduleDateTime.Year, scheduleDateTime.Month, scheduleDateTime.Day, scheduleDateTime.Hour, scheduleDateTime.Minute, 0, TimeSpan.Zero); BackupGenericProtectionPolicy policyProperties; - // Resolve the profile to determine policy type var profile = RsvDatasourceRegistry.ResolveOrDefault(workloadType); if (profile.PolicyType == RsvPolicyType.VmWorkload) { - // SQL/HANA/ASE workload policy var fullSchedule = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; fullSchedule.ScheduleRunTimes.Add(scheduleRunTime); @@ -963,7 +917,6 @@ public async Task CreatePolicyAsync( } else if (profile.PolicyType == RsvPolicyType.AzureFileShare) { - // Azure File Share policy var schedulePolicy = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; schedulePolicy.ScheduleRunTimes.Add(scheduleRunTime); @@ -979,7 +932,6 @@ public async Task CreatePolicyAsync( } else { - // Standard VM policy var schedulePolicy = new SimpleSchedulePolicy { ScheduleRunFrequency = ScheduleRunType.Daily }; schedulePolicy.ScheduleRunTimes.Add(scheduleRunTime); @@ -1049,7 +1001,6 @@ public async Task StopProtectionAsync( return new OperationResult("Accepted", null, "Protection stopped and data deletion initiated."); } - // RetainData mode - update with ProtectionState = ProtectionStopped var vaultRes = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultRes); var vault = await vaultResource.GetAsync(cancellationToken); @@ -1058,7 +1009,6 @@ public async Task StopProtectionAsync( subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); var piResource = armClient.GetBackupProtectedItemResource(piId); - // Get existing item to determine type and construct the correct SDK type via profile matching var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); BackupGenericProtectedItem stopProps = existingItem.Value.Data.Properties switch { @@ -1099,7 +1049,6 @@ public async Task ResumeProtectionAsync( subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); var piResource = armClient.GetBackupProtectedItemResource(piId); - // Get existing item to determine type and construct the correct SDK type via profile matching var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); ResourceIdentifier? policyArmId = null; if (!string.IsNullOrEmpty(policyName)) @@ -1146,12 +1095,9 @@ public async Task ModifyProtectionAsync( subscription, resourceGroup, vaultName, FabricName, containerName, protectedItemName); var piResource = armClient.GetBackupProtectedItemResource(piId); - // Get existing protected item to determine type and extract SourceResourceId var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); var existingProperties = existingItem.Value.Data.Properties; - // Bug #49: Check item state before attempting modify — IRPending items cannot be modified. - // ProtectionState is on concrete types, not the base BackupGenericProtectedItem. var protectionState = GetProtectionState(existingProperties); if (string.Equals(protectionState, "IRPending", StringComparison.OrdinalIgnoreCase)) { @@ -1185,7 +1131,6 @@ public async Task ModifyProtectionAsync( var sourceResourceId = (existingProperties as IaasComputeVmProtectedItem)?.SourceResourceId ?? (existingProperties as BackupGenericProtectedItem)?.SourceResourceId; - // If SourceResourceId not available from cast, derive from container name if (sourceResourceId is null && !string.IsNullOrEmpty(containerName)) { var parts = containerName.Split(';'); @@ -1213,7 +1158,6 @@ public Task UndeleteProtectedItemAsync( string protectedItemName, string? containerName, string? tenant, RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken) { - // RSV undelete is handled via support request or portal; SDK doesn't expose a direct undelete for RSV return Task.FromResult(new OperationResult("NotSupported", null, "Undelete for RSV protected items requires Azure portal or support request. Use soft-delete recovery instead.")); } @@ -1317,14 +1261,10 @@ public async Task ConfigureCrossRegionRestoreAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // CRR must be enabled via the BackupResourceConfig sub-resource (backupstorageconfig), - // NOT via the vault-level PATCH which returns CloudInternalError. var configResourceId = BackupResourceConfigResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var configResource = armClient.GetBackupResourceConfigResource(configResourceId); var currentConfig = await configResource.GetAsync(cancellationToken); - // Update the data and use CreateOrUpdate via the collection - // (BackupResourceConfigResource.UpdateAsync has an SDK NullRef bug in its constructor) var data = currentConfig.Value.Data; data.Properties.EnableCrossRegionRestore = true; @@ -1348,12 +1288,10 @@ public async Task RunBackupHealthCheckAsync( var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken); - // Get vault info var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultId); var vault = await vaultResource.GetAsync(cancellationToken); - // List protected items and check health var items = await ListProtectedItemsAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); var rpoThreshold = rpoThresholdHours ?? 24; var now = DateTimeOffset.UtcNow; @@ -1563,7 +1501,6 @@ private static RecoveryPointInfo MapToRecoveryPointInfo(BackupRecoveryPointData var rgResource = armClient.GetResourceGroupResource(rgId); var jobCollection = rgResource.GetBackupJobs(vaultName); - // Look for the most recent job of the given operation type started within the last minute await foreach (var job in jobCollection.GetAllAsync(cancellationToken: cancellationToken)) { if (job.Data.Properties is BackupGenericJob genericJob) @@ -1594,20 +1531,16 @@ private static RestoreContent CreateWorkloadSqlRestoreContent( { if (!string.IsNullOrEmpty(targetDatabaseName)) { - // ALR: restore to a different database on the same or different SQL instance var resolvedInstanceName = targetInstanceName ?? "mssqlserver"; - // Derive VM ARM ID from container name (format: VMAppContainer;Compute;{RG};{VMName}) var containerParts = containerName.Split(';'); var vmResourceGroup = containerParts.Length > 2 ? containerParts[2] : resourceGroup; var vmName = containerParts.Length > 3 ? containerParts[3] : string.Empty; var vmId = new ResourceIdentifier( $"/subscriptions/{subscription}/resourceGroups/{vmResourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}"); - // ContainerId must be the full ARM resource ID with lowercase container name var fullContainerId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.RecoveryServices/vaults/{vaultName}/backupFabrics/{FabricName}/protectionContainers/{containerName.ToLowerInvariant()}"; - // DatabaseName format: {INSTANCE_UPPER}/SQLInstance;{instance_lower} (target SQL instance path) var databaseName = $"{resolvedInstanceName.ToUpperInvariant()}/SQLInstance;{resolvedInstanceName.ToLowerInvariant()}"; var content = new WorkloadSqlRestoreContent @@ -1625,7 +1558,6 @@ private static RestoreContent CreateWorkloadSqlRestoreContent( } }; - // Add alternate directory paths for SQL data/log file mapping if (dataDirectoryMappings != null) { foreach (var mapping in dataDirectoryMappings) @@ -1680,7 +1612,6 @@ public async Task> ListContainersAsync( var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); var rgResource = armClient.GetResourceGroupResource(rgId); - // The REST API requires a backupManagementType filter, so query the main types string[] backupManagementTypes = ["AzureWorkload", "AzureIaasVM", "AzureStorage", "MAB", "DPM"]; var containers = new List(); @@ -1696,7 +1627,6 @@ public async Task> ListContainersAsync( } catch (Azure.RequestFailedException ex) when (ex.Status == 400) { - // Some backup management types may not be supported for all vault configurations; skip them continue; } } @@ -1726,7 +1656,6 @@ public async Task RegisterContainerAsync( var containerResource = armClient.GetBackupProtectionContainerResource(containerId); - // Get vault location for the container data var vaultResId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResId); var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); @@ -1798,15 +1727,11 @@ public async Task> ListProtectableItemsAsync( string filter; if (!string.IsNullOrEmpty(workloadType)) { - // Normalize workload-type values to what the REST API filter expects (Bug #46). - // Users may pass common names like "SAPHana" but the API filter requires - // specific values like "SAPHanaDatabase" or "SAPHanaDBInstance". var normalizedType = NormalizeWorkloadTypeForFilter(workloadType); filter = $"backupManagementType eq 'AzureWorkload' and workloadType eq '{normalizedType}'"; } else { - // Azure API requires at least a backupManagementType filter (Bug #38) filter = "backupManagementType eq 'AzureWorkload'"; } @@ -1841,7 +1766,6 @@ public async Task EnableAutoProtectionAsync( var vmId = new ResourceIdentifier(vmResourceId); - // Intent name is a GUID as used by Azure Resource Manager var intentName = Guid.NewGuid().ToString(); var intentId = BackupProtectionIntentResource.CreateResourceIdentifier( @@ -1852,14 +1776,9 @@ public async Task EnableAutoProtectionAsync( var vaultResource = armClient.GetRecoveryServicesVaultResource(vaultResId); var vault = await vaultResource.GetAsync(cancellationToken: cancellationToken); - // Build container and protectable item references for the intent var containerName = $"VMAppContainer;Compute;{vmId.ResourceGroupName};{vmId.Name}"; var itemId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.RecoveryServices/vaults/{vaultName}/backupFabrics/Azure/protectionContainers/{containerName}/protectableItems/{instanceName}"; - // Bug #48: Use WorkloadSqlAutoProtectionIntent for all workload types (SQL and HANA). - // Despite the "Sql" in the name, this is the only SDK intent type that supports - // the WorkloadItemType property needed to distinguish SQL vs HANA auto-protection. - // The REST API type is AzureWorkloadSQLAutoProtectionIntent for both SQL and HANA. var workloadItemType = workloadType?.ToUpperInvariant() switch { "SQLDATABASE" or "SQLINSTANCE" or "SQL" => WorkloadItemType.SqlInstance, @@ -1929,7 +1848,6 @@ private static ProtectableItemInfo MapToProtectableItemInfo(WorkloadProtectableI if (data.Properties is WorkloadProtectableItem workloadItem) { - // ProtectableItemType is internal in the SDK, use type discrimination protectableItemType = workloadItem switch { VmWorkloadSqlDatabaseProtectableItem => "SQLDataBase", @@ -1971,7 +1889,6 @@ internal static class RsvNamingHelper { public static string DeriveContainerName(string datasourceId, string? datasourceType = null) { - // Use the RSV datasource registry for workload detection var profile = RsvDatasourceRegistry.Resolve(datasourceType); if (profile?.IsWorkloadType == true) { @@ -1981,14 +1898,11 @@ public static string DeriveContainerName(string datasourceId, string? datasource var vmResourceId = new ResourceIdentifier(datasourceId); - // Use profile-based detection first (handles nested ARM IDs like file shares - // where ResourceType.Type returns "storageAccounts/fileServices/shares" not "shares") if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) { return $"StorageContainer;Storage;{vmResourceId.ResourceGroupName};{ExtractStorageAccountName(vmResourceId)}"; } - // Fallback to resource type detection for untyped calls var resourceType = vmResourceId.ResourceType.Type; return resourceType.ToLowerInvariant() switch @@ -2001,7 +1915,6 @@ public static string DeriveContainerName(string datasourceId, string? datasource public static string DeriveProtectedItemName(string datasourceId, string? datasourceType = null) { - // For workload types, the protectable item name is used directly (passed as datasourceId) var profile = RsvDatasourceRegistry.Resolve(datasourceType); if (profile?.IsWorkloadType == true) { @@ -2010,14 +1923,11 @@ public static string DeriveProtectedItemName(string datasourceId, string? dataso var resourceId = new ResourceIdentifier(datasourceId); - // Use profile-based detection first (handles nested ARM IDs like file shares - // where ResourceType.Type returns "storageAccounts/fileServices/shares" not "shares") if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) { return $"AzureFileShare;{resourceId.Name}"; } - // Fallback to resource type detection for untyped calls var resourceType = resourceId.ResourceType.Type; return resourceType.ToLowerInvariant() switch @@ -2030,8 +1940,6 @@ public static string DeriveProtectedItemName(string datasourceId, string? dataso private static string ExtractStorageAccountName(ResourceIdentifier resourceId) { - // For file share ARM IDs like .../storageAccounts/{sa}/fileServices/default/shares/{share} - // Walk up the hierarchy to find the storage account name ResourceIdentifier? current = resourceId; while (current is not null) { @@ -2048,7 +1956,6 @@ private static string ExtractStorageAccountName(ResourceIdentifier resourceId) public static string GetStorageAccountId(ResourceIdentifier resourceId) { - // For file share ARM IDs, walk up to return the storage account ARM ID ResourceIdentifier? current = resourceId; while (current is not null) { diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs index 976d4422d9..6af1a05a86 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs @@ -80,7 +80,6 @@ public enum RsvRestoreContentType /// public sealed record RsvDatasourceProfile { - // ── Identity ── /// Friendly name used for user-facing resolution (e.g. "VM", "SQL"). public required string FriendlyName { get; init; } @@ -88,7 +87,6 @@ public sealed record RsvDatasourceProfile /// Alternative user-supplied names that resolve to this profile. public string[] Aliases { get; init; } = []; - // ── Classification ── /// /// Whether this is a "workload" type (SQL, HANA, ASE) that runs inside a VM. @@ -96,7 +94,6 @@ public sealed record RsvDatasourceProfile /// public bool IsWorkloadType { get; init; } - // ── SDK Type Mapping ── /// Which SDK ProtectedItem type to construct for protect/stop/resume/modify. public RsvProtectedItemType ProtectedItemType { get; init; } = RsvProtectedItemType.IaasVm; @@ -110,7 +107,6 @@ public sealed record RsvDatasourceProfile /// Which SDK RestoreContent type to construct for trigger-restore. public RsvRestoreContentType RestoreContentType { get; init; } = RsvRestoreContentType.IaasVm; - // ── Workload Type String ── /// /// The Azure API workload type string (e.g. "SQLDataBase", "SAPHanaDatabase"). @@ -119,7 +115,6 @@ public sealed record RsvDatasourceProfile /// public string? ApiWorkloadType { get; init; } - // ── Container Naming ── /// /// Prefix for the container name derivation. @@ -129,7 +124,6 @@ public sealed record RsvDatasourceProfile /// public required string ContainerNamePrefix { get; init; } - // ── Features ── /// Whether container registration is required before protection (workload types). public bool RequiresContainerRegistration { get; init; } diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs index a3b9d0c126..230e032253 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs @@ -16,7 +16,6 @@ namespace Azure.Mcp.Tools.AzureBackup.Services; /// public static class RsvDatasourceRegistry { - // ── Profile definitions ── public static readonly RsvDatasourceProfile IaasVm = new() { @@ -94,7 +93,6 @@ public static class RsvDatasourceRegistry SupportsPolicyUpdate = true, }; - // ── Registry ── /// All registered RSV datasource profiles. public static readonly RsvDatasourceProfile[] AllProfiles = @@ -179,7 +177,6 @@ public static RsvDatasourceProfile ResolveFromProtectedItemName(string protected return AzureFileShare; } - // VMAppContainer indicates a workload, default to SQL if we can't determine the exact type if (containerLower.StartsWith("vmappcontainer", StringComparison.OrdinalIgnoreCase)) { return SqlDatabase; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/Azure.Mcp.Tools.AzureBackup.LiveTests.csproj b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/Azure.Mcp.Tools.AzureBackup.LiveTests.csproj new file mode 100644 index 0000000000..0f06a032a0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/Azure.Mcp.Tools.AzureBackup.LiveTests.csproj @@ -0,0 +1,17 @@ + + + true + Exe + + + + + + + + + + + + + diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs new file mode 100644 index 0000000000..715f57a0ab --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Tests; +using Azure.Mcp.Tests.Client; +using Azure.Mcp.Tests.Client.Helpers; +using Azure.Mcp.Tests.Generated.Models; +using Xunit; + +namespace Azure.Mcp.Tools.AzureBackup.LiveTests; + +public class AzureBackupCommandTests(ITestOutputHelper output, TestProxyFixture fixture, LiveServerFixture liveServerFixture) + : RecordedCommandTestsBase(output, fixture, liveServerFixture) +{ + #region Vault Tests + + [Fact] + public async Task VaultGet_ListsVaults_Successfully() + { + var result = await CallToolAsync( + "azurebackup_vault_get", + new() + { + { "subscription", Settings.SubscriptionId } + }); + + Assert.NotNull(result); + } + + [Fact] + public async Task VaultGet_GetsSingleVault_Successfully() + { + var vaultName = $"{Settings.ResourceBaseName}-rsv"; + + var result = await CallToolAsync( + "azurebackup_vault_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName } + }); + + Assert.NotNull(result); + } + + #endregion + + #region Policy Tests + + [Fact] + public async Task PolicyGet_ListsPolicies_Successfully() + { + var vaultName = $"{Settings.ResourceBaseName}-rsv"; + + var result = await CallToolAsync( + "azurebackup_policy_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName } + }); + + Assert.NotNull(result); + } + + #endregion + + #region Governance Tests + + [Fact] + public async Task GovernanceSoftDelete_ChecksConfiguration_Successfully() + { + var vaultName = $"{Settings.ResourceBaseName}-rsv"; + + var result = await CallToolAsync( + "azurebackup_governance_soft-delete", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName } + }); + + Assert.NotNull(result); + } + + [Fact] + public async Task GovernanceImmutability_ChecksConfiguration_Successfully() + { + var vaultName = $"{Settings.ResourceBaseName}-rsv"; + + var result = await CallToolAsync( + "azurebackup_governance_immutability", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName } + }); + + Assert.NotNull(result); + } + + #endregion + + #region Job Tests + + [Fact] + public async Task JobGet_ListsJobs_Successfully() + { + var vaultName = $"{Settings.ResourceBaseName}-rsv"; + + var result = await CallToolAsync( + "azurebackup_job_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName } + }); + + Assert.NotNull(result); + } + + #endregion +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/assets.json b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/assets.json new file mode 100644 index 0000000000..197668f965 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/assets.json @@ -0,0 +1,6 @@ +{ + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "", + "TagPrefix": "Azure.Mcp.Tools.AzureBackup.LiveTests", + "Tag": "" +} diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources-post.ps1 new file mode 100644 index 0000000000..b8f569053d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources-post.ps1 @@ -0,0 +1,17 @@ +param( + [string] $TenantId, + [string] $TestApplicationId, + [string] $ResourceGroupName, + [string] $BaseName, + [hashtable] $DeploymentOutputs, + [hashtable] $AdditionalParameters +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/../../../eng/common/scripts/common.ps1" +. "$PSScriptRoot/../../../eng/scripts/helpers/TestResourcesHelpers.ps1" + +$testSettings = New-TestSettings @PSBoundParameters -OutputPath $PSScriptRoot + +Write-Host "Azure Backup test resources deployed successfully for vault: $BaseName-rsv" -ForegroundColor Yellow diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep new file mode 100644 index 0000000000..2512f86444 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep @@ -0,0 +1,42 @@ +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(24) +@description('The base resource name.') +param baseName string = resourceGroup().name + +@description('The location of the resource. By default, this is the same as the resource group.') +param location string = resourceGroup().location + +@description('The tenant ID to which the application and resources belong.') +param tenantId string = '72f988bf-86f1-41af-91ab-2d7cd011db47' + +@description('The client OID to grant access to test resources.') +param testApplicationOid string + +resource vault 'Microsoft.RecoveryServices/vaults@2024-04-01' = { + name: '${baseName}-rsv' + location: location + sku: { + name: 'RS0' + tier: 'Standard' + } + properties: { + publicNetworkAccess: 'Enabled' + } +} + +resource backupContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: subscription() + name: '5e467623-bb1f-42f4-a55d-6e525e11384b' +} + +resource appBackupContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(backupContributorRoleDefinition.id, testApplicationOid, vault.id) + scope: vault + properties: { + principalId: testApplicationOid + roleDefinitionId: backupContributorRoleDefinition.id + description: 'Backup Contributor for testApplicationOid' + } +} From 29f1dbdd2530f5c415dce0eb2ed13e85ae431fff Mon Sep 17 00:00:00 2001 From: shrja-ms <77041475+shrja-ms@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:51:51 +0530 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/Areas/Server/Resources/consolidated-tools.json | 4 ++-- servers/Azure.Mcp.Server/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json b/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json index d715450dbf..7413875fbc 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json @@ -3171,8 +3171,8 @@ "description": "Create and update Azure Recovery Services vaults for backup and disaster recovery.", "toolMetadata": { "destructive": { - "value": false, - "description": "This tool performs only additive updates without deleting or modifying existing resources." + "value": true, + "description": "This tool performs write operations by creating or updating Azure Recovery Services vault resources and can modify existing state." }, "idempotent": { "value": true, diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index 3669f337f9..acd64bd5a6 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -882,7 +882,7 @@ Check out the remote hosting [azd templates](https://github.com/microsoft/mcp/bl * "Get the details for website 'my-website'" * "Get the details for app service plan 'my-app-service-plan'" -### �️ Azure Backup +### 🛡️ Azure Backup * "Create a Recovery Services vault named 'myvault' in resource group 'myRG' in eastus" * "Get details of backup vault 'myvault' in resource group 'myRG'" @@ -894,7 +894,7 @@ Check out the remote hosting [azd templates](https://github.com/microsoft/mcp/bl * "Check soft delete and immutability settings on my vault" * "Enable cross-region restore on my vault" -### �🖥️ Azure CLI Generate +### 🖥️ Azure CLI Generate * Generate Azure CLI commands based on user intent From 9b7c6d71dcbc189398d841670cf2a15a211888e2 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 19 Mar 2026 20:59:19 +0530 Subject: [PATCH 4/4] Address PR review comments: split model files, fix Bicep template - Remove unused tenantId parameter from test-resources.bicep - Interpolate testApplicationOid in role assignment description - Split WorkflowResult.cs: move WorkflowStep to its own file - Split HealthCheckResult.cs: move HealthCheckItemDetail to its own file --- .../src/Models/HealthCheckItemDetail.cs | 11 +++++++++++ .../src/Models/HealthCheckResult.cs | 7 ------- .../src/Models/WorkflowResult.cs | 5 ----- .../src/Models/WorkflowStep.cs | 9 +++++++++ .../tests/test-resources.bicep | 5 +---- 5 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckItemDetail.cs create mode 100644 tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowStep.cs diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckItemDetail.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckItemDetail.cs new file mode 100644 index 0000000000..97998af14e --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckItemDetail.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record HealthCheckItemDetail( + string? Name, + string? ProtectionStatus, + string? HealthStatus, + DateTimeOffset? LastBackupTime, + bool RpoBreached); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs index 3750840ece..6e62d18afc 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs @@ -14,10 +14,3 @@ public sealed record HealthCheckResult( string? ImmutabilityState, string? EncryptionType, IReadOnlyList? Details); - -public sealed record HealthCheckItemDetail( - string? Name, - string? ProtectionStatus, - string? HealthStatus, - DateTimeOffset? LastBackupTime, - bool RpoBreached); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs index 72a9e4c07d..6a242c2c16 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs @@ -8,8 +8,3 @@ public sealed record WorkflowResult( string WorkflowName, IReadOnlyList Steps, string? Message); - -public sealed record WorkflowStep( - string StepName, - string Status, - string? Detail); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowStep.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowStep.cs new file mode 100644 index 0000000000..0758c78de8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowStep.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Models; + +public sealed record WorkflowStep( + string StepName, + string Status, + string? Detail); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep index 2512f86444..2c11d01eff 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep @@ -8,9 +8,6 @@ param baseName string = resourceGroup().name @description('The location of the resource. By default, this is the same as the resource group.') param location string = resourceGroup().location -@description('The tenant ID to which the application and resources belong.') -param tenantId string = '72f988bf-86f1-41af-91ab-2d7cd011db47' - @description('The client OID to grant access to test resources.') param testApplicationOid string @@ -37,6 +34,6 @@ resource appBackupContributorRoleAssignment 'Microsoft.Authorization/roleAssignm properties: { principalId: testApplicationOid roleDefinitionId: backupContributorRoleDefinition.id - description: 'Backup Contributor for testApplicationOid' + description: 'Backup Contributor for ${testApplicationOid}' } }