diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7fb297a45d..e4eecd0ce2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,6 +94,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/Directory.Packages.props b/Directory.Packages.props index c03c789275..5f78705641 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,6 +37,9 @@ + + + diff --git a/Microsoft.Mcp.slnx b/Microsoft.Mcp.slnx index 2dca2e152d..fa228ba102 100644 --- a/Microsoft.Mcp.slnx +++ b/Microsoft.Mcp.slnx @@ -126,6 +126,14 @@ + + + + + + + + diff --git a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx index cce1b34d12..56ca00f4fd 100644 --- a/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx +++ b/servers/Azure.Mcp.Server/Azure.Mcp.Server.slnx @@ -87,6 +87,14 @@ + + + + + + + + diff --git a/servers/Azure.Mcp.Server/README.md b/servers/Azure.Mcp.Server/README.md index eefb9427b4..7aedf53bda 100644 --- a/servers/Azure.Mcp.Server/README.md +++ b/servers/Azure.Mcp.Server/README.md @@ -922,6 +922,18 @@ For full configuration options, see the [Sovereign Clouds documentation](https:/ * "List the deployments for web app 'my-webapp' in 'my-resource-group'" * "Get the deployment 'deployment-id' for web app 'my-webapp' in 'my-resource-group'" +### πŸ›‘οΈ 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 @@ -1119,6 +1131,7 @@ The Azure MCP Server provides tools for interacting with **43+ 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 3b11c359ae..5e45b67b23 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -580,6 +580,180 @@ 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 ] + #### Web Apps ```bash diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index 8e7458c779..315bc6c377 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -119,6 +119,41 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | appservice_webapp_settings_update-appsettings | Set application setting with to web app in | | appservice_webapp_settings_update-appsettings | Delete application setting from web app in | +## 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/servers/Azure.Mcp.Server/src/Program.cs b/servers/Azure.Mcp.Server/src/Program.cs index f75961ef4e..956581ae10 100644 --- a/servers/Azure.Mcp.Server/src/Program.cs +++ b/servers/Azure.Mcp.Server/src/Program.cs @@ -102,6 +102,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/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json index 40fc2f5705..61912260d8 100644 --- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json +++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json @@ -3562,6 +3562,213 @@ "mappedToolList": [ "deviceregistry_namespace_list" ] + }, + { + "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": true, + "description": "This tool performs write operations by creating or updating Azure Recovery Services vault resources and can modify existing state." + }, + "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": true, + "description": "This tool can create or modify Azure Backup policies, changing the configuration of backup schedules and retention." + }, + "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/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..bba204c080 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/AzureBackupSetup.cs @@ -0,0 +1,118 @@ +// 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(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + 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); + + 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); + + 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); + + 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); + + var protectableItem = new CommandGroup("protectableitem", "Protectable item operations – List discovered databases available for protection."); + azureBackup.AddSubGroup(protectableItem); + RegisterCommand(serviceProvider, protectableItem); + + var backup = new CommandGroup("backup", "Backup operations – Check backup status for a datasource."); + azureBackup.AddSubGroup(backup); + RegisterCommand(serviceProvider, backup); + + var job = new CommandGroup("job", "Backup job operations – Get job details or list all jobs in a vault."); + azureBackup.AddSubGroup(job); + RegisterCommand(serviceProvider, job); + + 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); + + 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); + + 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..c3953d943f --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/AzureBackupJsonContext.cs @@ -0,0 +1,58 @@ +// 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; + +[JsonSerializable(typeof(VaultGetCommand.VaultGetCommandResult))] +[JsonSerializable(typeof(VaultCreateCommand.VaultCreateCommandResult))] +[JsonSerializable(typeof(VaultUpdateCommand.VaultUpdateCommandResult))] +[JsonSerializable(typeof(PolicyGetCommand.PolicyGetCommandResult))] +[JsonSerializable(typeof(PolicyCreateCommand.PolicyCreateCommandResult))] +[JsonSerializable(typeof(ProtectedItemGetCommand.ProtectedItemGetCommandResult))] +[JsonSerializable(typeof(ProtectedItemProtectCommand.ProtectedItemProtectCommandResult))] +[JsonSerializable(typeof(ProtectableItemListCommand.ProtectableItemListCommandResult))] +[JsonSerializable(typeof(BackupStatusCommand.BackupStatusCommandResult))] +[JsonSerializable(typeof(JobGetCommand.JobGetCommandResult))] +[JsonSerializable(typeof(RecoveryPointGetCommand.RecoveryPointGetCommandResult))] +[JsonSerializable(typeof(GovernanceFindUnprotectedCommand.GovernanceFindUnprotectedCommandResult))] +[JsonSerializable(typeof(GovernanceImmutabilityCommand.GovernanceImmutabilityCommandResult))] +[JsonSerializable(typeof(GovernanceSoftDeleteCommand.GovernanceSoftDeleteCommandResult))] +[JsonSerializable(typeof(DrEnableCrrCommand.DrEnableCrrCommandResult))] +[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..dfee1fcb4d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Job/JobGetCommand.cs @@ -0,0 +1,118 @@ +// 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)) + { + 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 + { + 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..819f64ba80 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Policy/PolicyGetCommand.cs @@ -0,0 +1,118 @@ +// 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)) + { + 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 + { + 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..5a4d56bf50 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/ProtectedItem/ProtectedItemGetCommand.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.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)) + { + 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 + { + 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..e2180b6020 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/RecoveryPoint/RecoveryPointGetCommand.cs @@ -0,0 +1,123 @@ +// 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)) + { + 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 + { + 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..d24e735c3a --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultGetCommand.cs @@ -0,0 +1,124 @@ +// 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)) + { + 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 + { + 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/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 new file mode 100644 index 0000000000..6e62d18afc --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/HealthCheckResult.cs @@ -0,0 +1,16 @@ +// 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); 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..6a242c2c16 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/WorkflowResult.cs @@ -0,0 +1,10 @@ +// 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); 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/src/Options/AzureBackupOptionDefinitions.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs new file mode 100644 index 0000000000..1906ae2478 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Options/AzureBackupOptionDefinitions.cs @@ -0,0 +1,709 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.Mcp.Tools.AzureBackup.Options; + +public static class AzureBackupOptionDefinitions +{ + 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"; + + 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"; + + 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 + }; + + 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..c1d471900d --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -0,0 +1,789 @@ +// 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); + } + + 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); + } + + 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); + } + + + 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) + { + 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) + { + 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) + { + 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) + { + 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) + { + 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}'.")); + } + + + 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!; + } + + try + { + await rsvOps.GetVaultAsync(vaultName, resourceGroup, subscription, tenant, retryPolicy, cancellationToken); + return VaultTypeResolver.Rsv; + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + } + + 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) + { + } + + 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..0e9d9550f0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -0,0 +1,986 @@ +// 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) + { + 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); + + var resolvedDatasourceType = datasourceType ?? datasourceResourceId.ResourceType.ToString(); + var profile = ResolveProfile(resolvedDatasourceType); + + var instanceName = DppDatasourceRegistry.GenerateInstanceName(profile, datasourceResourceId); + + var policyInfo = new BackupInstancePolicyInfo(policyId); + + 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); + } + + 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", + }; + + 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); + + var instanceData = await instanceResource.GetAsync(cancellationToken); + var policyId = instanceData.Value.Data.Properties.PolicyInfo.PolicyId; + + 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); + + 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; + + if (!string.IsNullOrEmpty(targetResourceId) && + targetResourceId.Contains("Microsoft.Storage/storageAccounts", StringComparison.OrdinalIgnoreCase)) + { + var storageAccountId = targetResourceId; + var containerName = "pgflex-restore"; + + if (targetResourceId.Contains("/blobServices/", StringComparison.OrdinalIgnoreCase)) + { + var parts = targetResourceId.Split('/'); + containerName = parts[^1]; + var containerIndex = targetResourceId.IndexOf("/blobServices/", StringComparison.OrdinalIgnoreCase); + storageAccountId = targetResourceId[..containerIndex]; + } + + 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}"; + + 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 + { + 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, + }; + + 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; + } + + var datasourceTypeStr = datasourceInfo?.DataSourceType ?? string.Empty; + var storeProfile = ResolveProfile(datasourceTypeStr); + var sourceDataStoreType = storeProfile.UsesOperationalStore ? SourceDataStoreType.OperationalStore : SourceDataStoreType.VaultStore; + + 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) + { + 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; + } + + + 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); + + var policyId = DataProtectionBackupPolicyResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetDataProtectionBackupPolicyResource(policyId); + var existingPolicy = await policyResource.GetAsync(cancellationToken); + var policyData = existingPolicy.Value.Data; + + 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)); + } + } + } + } + + 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(); + + 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); + + var profile = DppDatasourceRegistry.Resolve(workloadType); + var dataStoreType = profile.UsesOperationalStore ? DataStoreType.OperationalStore : DataStoreType.VaultStore; + + var defaultRetention = retentionDays > 0 ? retentionDays : profile.DefaultRetentionDays; + + 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]; + + 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."); + } + + 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) + { + 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)) + { + 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..f245209e03 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceProfile.cs @@ -0,0 +1,128 @@ +// 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 +{ + + /// 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; } = []; + + + /// Whether this datasource uses OperationalStore (snapshot-based) or VaultStore. + public bool UsesOperationalStore { get; init; } + + + /// 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; + + + /// 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; + + + /// The default restore approach (RP-based, PIT, or restore-as-files). + public DppRestoreMode DefaultRestoreMode { get; init; } = DppRestoreMode.RecoveryPoint; + + + /// + /// 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; } + + + /// 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..eaf2328429 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppDatasourceRegistry.cs @@ -0,0 +1,251 @@ +// 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 +{ + + 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, + }; + + + /// 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; + } + } + } + + 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(); + + 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..3f918e21a0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IAzureBackupService.cs @@ -0,0 +1,88 @@ +// 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 +{ + 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); + + 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); + + 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); + + 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); + + 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); + + Task GetBackupStatusAsync(string datasourceId, string subscription, string location, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + Task CancelJobAsync(string vaultName, string resourceGroup, string subscription, string jobId, string? vaultType = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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..9aaeb8872f --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IDppBackupOperations.cs @@ -0,0 +1,38 @@ +// 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 +{ + 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); + + 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..fbb9c0c4f0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/IRsvBackupOperations.cs @@ -0,0 +1,46 @@ +// 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 +{ + 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); + + 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); + + 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..2c6505c649 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -0,0 +1,1972 @@ +// 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); + + 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); + + var profile = RsvDatasourceRegistry.ResolveOrDefault(datasourceType); + + if (profile.IsWorkloadType) + { + 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."); + } + + 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); + + 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."); + } + + if (profile.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + var fsContainer = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId, datasourceType); + var fsProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId, datasourceType); + + var containerId = BackupProtectionContainerResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, fsContainer); + var containerResource = armClient.GetBackupProtectionContainerResource(containerId); + try + { + await containerResource.InquireAsync(filter: null, cancellationToken); + await Task.Delay(5000, cancellationToken); + } + catch (RequestFailedException) + { + } + + var fsProtectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, FabricName, fsContainer, fsProtectedItemName); + + 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."); + } + + var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); + var rgResource = armClient.GetResourceGroupResource(rgId); + await rgResource.RefreshProtectionContainerAsync(vaultName, FabricName, filter: null, cancellationToken); + + await Task.Delay(30000, cancellationToken); + + 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); + + 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 (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); + + 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 (!string.IsNullOrEmpty(backupType) && !backupType.Equals("Default", StringComparison.OrdinalIgnoreCase)) + { + return new WorkloadBackupContent + { + BackupType = new BackupType(backupType), + RecoveryPointExpireOn = expiryTime, + EnableCompression = true + }; + } + + 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 + }; + } + + 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); + + 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; + + 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; + + if (existingProperties is VmWorkloadSqlDatabaseProtectedItem) + { + 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 + { + var sourceResourceId = (existingProperties as IaasComputeVmProtectedItem)?.SourceResourceId + ?? (existingProperties as BackupGenericProtectedItem)?.SourceResourceId; + + 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) + { + 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)) + { + 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); + + 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; + } + + + 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); + + var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, policyName); + var policyResource = armClient.GetBackupProtectionPolicyResource(policyId); + var existingPolicy = await policyResource.GetAsync(cancellationToken); + var policyData = existingPolicy.Value.Data; + + 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); + } + + 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 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) + { + 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; + + 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; + + var profile = RsvDatasourceRegistry.ResolveOrDefault(workloadType); + + if (profile.PolicyType == RsvPolicyType.VmWorkload) + { + 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) + { + 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 + { + 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."); + } + + 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); + + 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); + + 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); + + var existingItem = await piResource.GetAsync(cancellationToken: cancellationToken); + var existingProperties = existingItem.Value.Data.Properties; + + 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 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) + { + 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); + + var configResourceId = BackupResourceConfigResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var configResource = armClient.GetBackupResourceConfigResource(configResourceId); + var currentConfig = await configResource.GetAsync(cancellationToken); + + 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); + + var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName); + var vaultResource = armClient.GetRecoveryServicesVaultResource(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?.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); + + 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)) + { + var resolvedInstanceName = targetInstanceName ?? "mssqlserver"; + + 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}"); + + var fullContainerId = $"/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.RecoveryServices/vaults/{vaultName}/backupFabrics/{FabricName}/protectionContainers/{containerName.ToLowerInvariant()}"; + + 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 + } + }; + + 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); + + 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) + { + 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); + + 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)) + { + var normalizedType = NormalizeWorkloadTypeForFilter(workloadType); + filter = $"backupManagementType eq 'AzureWorkload' and workloadType eq '{normalizedType}'"; + } + else + { + 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); + + 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); + + 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}"; + + 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 = 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) + { + 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); + + if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + return $"StorageContainer;Storage;{vmResourceId.ResourceGroupName};{ExtractStorageAccountName(vmResourceId)}"; + } + + 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) + { + var profile = RsvDatasourceRegistry.Resolve(datasourceType); + if (profile?.IsWorkloadType == true) + { + return datasourceId; + } + + var resourceId = new ResourceIdentifier(datasourceId); + + if (profile?.ProtectedItemType == RsvProtectedItemType.AzureFileShare) + { + return $"AzureFileShare;{resourceId.Name}"; + } + + 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) + { + 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) + { + 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..6af1a05a86 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceProfile.cs @@ -0,0 +1,142 @@ +// 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 +{ + + /// 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; } = []; + + + /// + /// 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; } + + + /// 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; + + + /// + /// 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; } + + + /// + /// Prefix for the container name derivation. + /// VM: "IaasVMContainer;iaasvmcontainerv2", + /// SQL/HANA/ASE: "VMAppContainer;Compute", + /// FileShare: "StorageContainer". + /// + public required string ContainerNamePrefix { get; init; } + + + /// 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..230e032253 --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvDatasourceRegistry.cs @@ -0,0 +1,187 @@ +// 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 +{ + + 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, + }; + + + /// 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; + } + + 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.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/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); + } + } +} 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..2c11d01eff --- /dev/null +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep @@ -0,0 +1,39 @@ +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 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}' + } +}