From ff9330c67cbf4deda63b208203417f0ef7a87e23 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 23 Apr 2026 13:55:43 +0530 Subject: [PATCH 1/7] Fix azurebackup protect/vault-create: MSI, terminal status, VM timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent bug fixes for azurebackup tooling, surfaced via end-to-end testing against a live subscription. Bug #1 — `azurebackup vault create --vault-type dpp` created Data Protection backup vaults without a Managed Identity. Every subsequent `protecteditem protect` against the new vault then failed server-side with `VaultMSIUnauthorized`. Fixed by defaulting new DPP vaults to a System-Assigned Managed Identity, matching Portal and CLI behaviour. Bug #2 — `azurebackup protecteditem protect` returned a synthetic "Accepted" response with a base64-encoded job ID that callers could not resolve. Fixed to surface the true terminal outcome: - RSV: poll the underlying ConfigureBackup job (max 12 min) and return its real status (`Completed`, `CompletedWithWarnings`, `Failed`, `Cancelled`, or `InProgress` if polling times out) with the real job ID and error details (AOT-safe typed switch). - DPP: wait for the LRO to complete, read back the BackupInstance, and return the actual `protectionStatus`. DPP responses no longer carry a misleading `jobId` — DPP protection is not a backup job. - `ProtectResult` record extended with optional `ProtectionStatus` and `ErrorMessage` fields. Bug #3 — `azurebackup protecteditem protect` for IaaS VM performed a pre-emptive 180 s container-discovery poll that timed out on freshly created VMs. Removed — the Recovery Services backend registers the container as part of accepting the protect request, matching `az backup protection enable-for-vm`. Tests: - Unit: 334/334 pass; +5 new tests for terminal-status surfacing (DPP Succeeded/Failed, RSV Completed/Failed/InProgress). - Live: extended `VaultCreate_CreatesDppVault_Successfully` to assert System-Assigned MSI; added `ProtectedItemProtect_DppVault_DiskProtection_Succeeds_E2E` which creates a disk policy, protects a disk via MCP, and asserts `protectionStatus` on success or `errorMessage` on failure with no `jobId` for DPP. - Live infra: added `${baseName}-disk` (4 GiB Standard_LRS) and the Disk Backup Reader / Disk Snapshot Contributor role assignments needed by the DPP vault MSI. Manual E2E verification (live sub `f0c630e0-…`): - DPP vault create: identity = SystemAssigned, principalId populated. - RSV VM (fresh) protect: Completed in ~95 s (no 180 s pre-poll). - RSV SQL-in-VM (master) protect: Completed in 112 s. - RSV AFS protect: Failed surfaced with real job id + error message. - DPP Disk (fresh) protect: Succeeded → ProtectionConfigured. - DPP ADLS/ESAN: Failed surfaced with service-side error message (HNS unsupported, plugin error) — fix correctly reports the truth. Build: full solution 0 warnings, 0 errors. Spell: clean for all newly-added lines (pre-existing unrelated warnings on main remain untouched). --- servers/Azure.Mcp.Server/CHANGELOG.md | 4 + .../Azure.Mcp.Server/docs/azmcp-commands.md | 4 +- .../src/Commands/Vault/VaultCreateCommand.cs | 8 +- .../src/Models/ProtectResult.cs | 37 +++- .../src/Services/DppBackupOperations.cs | 60 +++++- .../src/Services/RsvBackupOperations.cs | 172 +++++++++++++----- .../AzureBackupCommandTests.cs | 93 ++++++++++ .../ProtectedItemProtectCommandTests.cs | 165 +++++++++++++++++ .../tests/test-resources.bicep | 2 + 9 files changed, 488 insertions(+), 57 deletions(-) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 0c15b72739..77908ec481 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -10,6 +10,10 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Bugs Fixed +- Fixed `azurebackup vault create --vault-type dpp` to enable a System-Assigned Managed Identity by default. Without this, every subsequent `protecteditem protect` call against the new DPP vault would fail server-side with `VaultMSIUnauthorized`. Existing vaults are unaffected; use `azurebackup vault update --identity-type SystemAssigned` if needed. +- Fixed `azurebackup protecteditem protect` to surface the real outcome of the protect operation instead of returning a synthetic "Accepted" with a base64 job id that callers could not resolve. RSV now polls the underlying `ConfigureBackup` job to a terminal state and returns its real status (`Completed`, `CompletedWithWarnings`, `Failed`, `Cancelled`, or `InProgress` if polling exceeds the budget) along with any error details. DPP now waits for the protect operation to complete and reads back the backup instance, returning the actual `protectionStatus` (e.g. `ProtectionConfigured`); DPP responses no longer carry a misleading `jobId` because DPP protection is not surfaced as a job — use `azurebackup protecteditem get` or `list` to verify. +- Fixed `azurebackup protecteditem protect` for IaaS VM (RSV) so it submits the protect request directly instead of doing a 180-second container-discovery pre-poll. Freshly created VMs no longer time out; container registration is performed by the Recovery Services backend as part of accepting the protect request, matching the behavior of `az backup protection enable-for-vm`. + ### Other Changes ## 3.0.0-beta.4 (2026-04-21) diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index 8de2f46b55..bbf16a005c 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -739,7 +739,7 @@ azmcp appservice webapp diagnostic diagnose --subscription "my-subscription" \ #### 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. +# Creates a new backup vault. Specify --vault-type as 'rsv' for a Recovery Services vault or 'dpp' for a Backup vault (Data Protection). For DPP vaults a System-Assigned Managed Identity is enabled by default so the vault can authenticate to protected datasources (storage accounts, disks, PG Flex, etc.) - change later with 'azurebackup vault update --identity-type ...' if needed. Returns the created vault details. # ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired azmcp azurebackup vault create --subscription \ --resource-group \ @@ -805,7 +805,7 @@ azmcp azurebackup protecteditem get --subscription \ [--protected-item ] \ [--container ] -# Enables backup protection for a resource by creating a protected item or backup instance. +# Enables backup protection for a resource by creating a protected item or backup instance. For RSV the tool waits for the underlying ConfigureBackup job to reach a terminal state and returns the final job status; for DPP the tool waits for the protect operation to complete and reads back the backup instance, returning ProtectionStatus (DPP protection is not a job - use 'azurebackup protecteditem get' or 'list' to verify). # ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired azmcp azurebackup protecteditem protect --subscription \ --resource-group \ diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs index 96798d1373..e44e6c9bff 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore azurebackup + using System.Net; using Azure.Mcp.Tools.AzureBackup.Models; using Azure.Mcp.Tools.AzureBackup.Options; @@ -25,7 +27,11 @@ public sealed class VaultCreateCommand(ILogger logger, IAzur 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. + or 'dpp' for a Backup vault (Data Protection). For DPP vaults a System-Assigned + Managed Identity is enabled by default so the vault can authenticate to protected + datasources (storage accounts, disks, PG Flex, etc.) - change later with + 'azurebackup vault update --identity-type ...' if needed. Returns the created + vault details. """; public override string Title => CommandTitle; public override ToolMetadata Metadata => new() diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs index 20b2533e43..0818fd7465 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs @@ -1,10 +1,45 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore azurebackup protecteditem + namespace Azure.Mcp.Tools.AzureBackup.Models; +/// +/// Result of an azurebackup protecteditem protect call. +/// +/// +/// Final outcome of the protect operation as observed by MCP after polling. +/// For RSV: terminal status of the ConfigureBackup job (e.g. Completed, +/// CompletedWithWarnings, Failed, Cancelled) or InProgress +/// if the job is still running when the polling budget is exhausted. +/// For DPP: Succeeded when the backup instance reaches ProtectionConfigured +/// (or ConfiguringProtection if still finalizing) or Failed on error. +/// +/// +/// RSV protected item name or DPP backup instance name. Use this with +/// azurebackup protecteditem get. +/// +/// +/// RSV ConfigureBackup job id (use with azurebackup job get). Always +/// null for DPP — DPP protection is not surfaced as a job; verify with +/// azurebackup protecteditem get or list. +/// +/// Human-readable summary of the outcome. +/// +/// DPP only — actual protectionStatus.status read back from the backup +/// instance after the operation (e.g. ProtectionConfigured, +/// ConfiguringProtection, ProtectionError). +/// +/// +/// Error detail when is Failed. For RSV this comes +/// from the failed ConfigureBackup job; for DPP it comes from the +/// async operationStatus error envelope. +/// public sealed record ProtectResult( string Status, string? ProtectedItemName, string? JobId, - string? Message); + string? Message, + string? ProtectionStatus = null, + string? ErrorMessage = null); diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index 34fe3a4658..885a0b01a8 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore USERASSIGNED SYSTEMASSIGNEDUSERASSIGNED SYSTEMASSIGNED Lifecycles azurebackup protecteditem protectable + using Azure.Core; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Tenant; @@ -64,7 +66,16 @@ public async Task CreateVaultAsync( } }; - var vaultData = new DataProtectionBackupVaultData(new AzureLocation(location), new DataProtectionBackupVaultProperties(storageSettings)); + var vaultData = new DataProtectionBackupVaultData(new AzureLocation(location), new DataProtectionBackupVaultProperties(storageSettings)) + { + // DPP (Backup Vault) requires a Managed Identity to authenticate to protected + // datasources (storage accounts, disks, PG Flex, etc.). Without it every + // 'protecteditem protect' call would fail server-side with VaultMSIUnauthorized. + // Default to SystemAssigned so the vault is usable out of the box; callers can + // change this later via 'vault update --identity-type ...'. + Identity = new Azure.ResourceManager.Models.ManagedServiceIdentity( + Azure.ResourceManager.Models.ManagedServiceIdentityType.SystemAssigned) + }; var result = await collection.CreateOrUpdateAsync(WaitUntil.Completed, vaultName, vaultData, cancellationToken); @@ -197,15 +208,44 @@ public async Task ProtectItemAsync( 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."); + // DPP protection is asynchronous on the server side and is NOT surfaced as a + // backup job (only on-demand backup, restore, etc. are jobs). MCP must therefore + // wait for the underlying operationStatus to reach a terminal state and then read + // back the BackupInstance to confirm the protection actually configured. Using + // WaitUntil.Completed lets the SDK poll the Azure-AsyncOperation header for us + // and surface the real server-side error (e.g. VaultMSIUnauthorized) as a + // RequestFailedException, instead of silently returning "Accepted". + try + { + var operation = await collection.CreateOrUpdateAsync( + WaitUntil.Completed, instanceName, instanceData, cancellationToken); + + // Re-read the backup instance to capture the authoritative protection status. + // The LRO can complete while the BI is still in ConfiguringProtection; both + // outcomes are surfaced to the caller via ProtectionStatus. + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(operation.Value.Id); + var bi = await instanceResource.GetAsync(cancellationToken); + var protectionStatus = bi.Value.Data.Properties?.ProtectionStatus?.Status?.ToString(); + + return new ProtectResult( + "Succeeded", + instanceName, + JobId: null, + $"Protection configured for backup instance '{instanceName}' (status: {protectionStatus ?? "Unknown"}). " + + $"Use 'azurebackup protecteditem get --protected-item {instanceName}' to view details.", + ProtectionStatus: protectionStatus, + ErrorMessage: null); + } + catch (RequestFailedException ex) + { + return new ProtectResult( + "Failed", + instanceName, + JobId: null, + $"Protection failed for backup instance '{instanceName}': {ex.Message}", + ProtectionStatus: null, + ErrorMessage: ex.Message); + } } public async Task GetProtectedItemAsync( diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs index 3b9b064999..6d6a28951b 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore SQLINSTANCE SAPHANADATABASE SQLDATABASE protectable hana iaasvmcontainerv azurebackup protectableitem dbname Fileshare protecteditem vmname ALWAYSON SYSTEMASSIGNED USERASSIGNED SYSTEMASSIGNEDUSERASSIGNED SAPHANA SAPHANASYSTEM SAPHANADBINSTANCE SAPHANADBI + using Azure.Core; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Tenant; @@ -150,8 +152,9 @@ public async Task ProtectItemAsync( 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."); + return await BuildRsvProtectResultAsync( + armClient, subscription, resourceGroup, vaultName, protectedItemName, jobId, + "Workload protection", cancellationToken); } if (profile.ProtectedItemType == RsvProtectedItemType.AzureFileShare) @@ -196,45 +199,16 @@ public async Task ProtectItemAsync( 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."); + return await BuildRsvProtectResultAsync( + armClient, subscription, resourceGroup, vaultName, fsProtectedItemName, fsJobId, + "File share protection", cancellationToken); } - var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup); - var rgResource = armClient.GetResourceGroupResource(rgId); - await rgResource.RefreshProtectionContainerAsync(vaultName, FabricName, filter: null, cancellationToken); - + // For IaaS VM protection MCP follows the same approach as `az backup protection enable-for-vm`: + // submit the protected-item PUT directly. The Recovery Services backend registers the + // VM container as part of accepting the protect request, so a separate refresh + + // discovery poll is unnecessary and was causing 180s timeouts on freshly created VMs. var container = containerName ?? RsvNamingHelper.DeriveContainerName(datasourceId); - - // Poll for container visibility after refresh (up to 180s with 5s intervals). - // The RSV RefreshProtectionContainerAsync API does not return a pollable LRO, - // so we must manually poll for the container to become visible. - // Container discovery can take 2-3 minutes for some workloads. - const int maxRetries = 36; - const int delayMs = 5000; - for (int i = 0; i < maxRetries; i++) - { - await Task.Delay(delayMs, cancellationToken); - try - { - var checkContainerId = BackupProtectionContainerResource.CreateResourceIdentifier( - subscription, resourceGroup, vaultName, FabricName, container); - var checkContainer = armClient.GetBackupProtectionContainerResource(checkContainerId); - await checkContainer.GetAsync(cancellationToken: cancellationToken); - break; // Container is visible - } - catch (RequestFailedException ex) when (ex.Status == 404) - { - if (i == maxRetries - 1) - { - throw new InvalidOperationException( - $"Container '{container}' was not discovered after {maxRetries * delayMs / 1000}s. " + - "Container discovery can take several minutes for some workloads. " + - "Retry later or verify the VM resource ID is correct.", ex); - } - } - } - var vmProtectedItemName = RsvNamingHelper.DeriveProtectedItemName(datasourceId); var vmProtectedItemId = BackupProtectedItemResource.CreateResourceIdentifier( @@ -255,11 +229,9 @@ public async Task ProtectItemAsync( 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."); + return await BuildRsvProtectResultAsync( + armClient, subscription, resourceGroup, vaultName, vmProtectedItemName, vmJobId, + "VM protection", cancellationToken); } public async Task GetProtectedItemAsync( @@ -1235,6 +1207,120 @@ private static string ExtractContainerName(string resourceId) return resourceId[start..end]; } + /// + /// Polls the RSV ConfigureBackup job to a terminal state and builds a + /// reflecting the actual job outcome. RSV protection is + /// asynchronous; the protect PUT only accepts the request, so MCP must follow up by + /// reading the job until it reports success or failure. If polling exceeds the timeout + /// the result is returned with status InProgress and the job id, so the caller + /// can continue monitoring with azurebackup job get. + /// + private static async Task BuildRsvProtectResultAsync( + ArmClient armClient, string subscription, string resourceGroup, string vaultName, + string protectedItemName, string? jobId, string operationDescription, + CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(jobId)) + { + return new ProtectResult( + "Accepted", + protectedItemName, + null, + $"{operationDescription} initiated. Use 'azurebackup protecteditem get' to verify."); + } + + var finalJob = await WaitForJobAsync( + armClient, subscription, resourceGroup, vaultName, jobId, cancellationToken); + + if (finalJob == null) + { + return new ProtectResult( + "InProgress", + protectedItemName, + jobId, + $"{operationDescription} is still running after the polling budget elapsed. " + + $"Use 'azurebackup job get --job {jobId}' to continue monitoring."); + } + + var status = finalJob.Status ?? "Unknown"; + var errorMessage = ExtractJobErrorMessage(finalJob); + var isFailure = status.Contains("Fail", StringComparison.OrdinalIgnoreCase) || + status.Equals("Cancelled", StringComparison.OrdinalIgnoreCase); + + var message = isFailure + ? $"{operationDescription} failed: {errorMessage ?? status}. See 'azurebackup job get --job {jobId}' for details." + : $"{operationDescription} {status.ToLowerInvariant()}. Use 'azurebackup protecteditem get' to verify the protected item."; + + return new ProtectResult( + status, + protectedItemName, + jobId, + message, + ProtectionStatus: null, + ErrorMessage: isFailure ? errorMessage ?? status : null); + } + + /// + /// Polls a Recovery Services backup job until it reaches a terminal state. Returns the + /// final on completion, or null if the job did not + /// reach a terminal state within the polling budget. ConfigureBackup jobs typically + /// finish in 2-10 minutes, so a 12-minute budget with 10-second intervals balances + /// responsiveness and tolerance for slow operations. + /// + private static async Task WaitForJobAsync( + ArmClient armClient, string subscription, string resourceGroup, string vaultName, + string jobId, CancellationToken cancellationToken) + { + const int maxAttempts = 72; // 72 * 10s = 12 minutes + var pollDelay = TimeSpan.FromSeconds(10); + + var jobResourceId = BackupJobResource.CreateResourceIdentifier( + subscription, resourceGroup, vaultName, jobId); + var jobResource = armClient.GetBackupJobResource(jobResourceId); + + for (var attempt = 0; attempt < maxAttempts; attempt++) + { + try + { + var jobResponse = await jobResource.GetAsync(cancellationToken); + if (jobResponse.Value.Data.Properties is BackupGenericJob job && + !string.IsNullOrEmpty(job.Status) && + !job.Status.Equals("InProgress", StringComparison.OrdinalIgnoreCase)) + { + return job; + } + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Job entry not yet visible; keep polling. + } + + await Task.Delay(pollDelay, cancellationToken); + } + + return null; + } + + private static string? ExtractJobErrorMessage(BackupGenericJob job) + { + switch (job) + { + case IaasVmBackupJob vm when vm.ErrorDetails.Count > 0: + return FirstNonEmpty(vm.ErrorDetails[0].ErrorString, vm.ErrorDetails[0].ErrorTitle); + case IaasVmBackupJobV2 vm2 when vm2.ErrorDetails.Count > 0: + return FirstNonEmpty(vm2.ErrorDetails[0].ErrorString, vm2.ErrorDetails[0].ErrorTitle); + case StorageBackupJob storage when storage.ErrorDetails.Count > 0: + return storage.ErrorDetails[0].ErrorString; + case WorkloadBackupJob wl when wl.ErrorDetails.Count > 0: + return FirstNonEmpty(wl.ErrorDetails[0].ErrorString, wl.ErrorDetails[0].ErrorTitle); + default: + return null; + } + } + + private static string? FirstNonEmpty(string? primary, string? fallback) => + string.IsNullOrEmpty(primary) ? fallback : primary; + public async Task> ListProtectableItemsAsync( string vaultName, string resourceGroup, string subscription, string? workloadType, string? containerName, string? tenant, 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 index cf9441f488..53ca0cd69e 100644 --- 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 @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore protectable Protectable protectableitem recoverypoint disasterrecovery esan adls Adls ADLS protecteditem azurebackup Hana hana SAPHANA pgflex + using System.Text.Json; using Microsoft.Mcp.Tests; using Microsoft.Mcp.Tests.Client; @@ -162,6 +164,22 @@ public async Task VaultCreate_CreatesDppVault_Successfully() var vault = result.AssertProperty("vault"); Assert.Equal("Succeeded", vault.AssertProperty("provisioningState").GetString()); + + // DPP vault create must enable a System-Assigned Managed Identity by default so + // the vault can authenticate to protected datasources without a separate + // 'vault update --identity-type SystemAssigned' step. + var getResult = await CallToolAsync( + "azurebackup_vault_get", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName }, + { "vault-type", "dpp" } + }); + var fetchedVault = getResult.AssertProperty("vault"); + var identityType = fetchedVault.AssertProperty("identityType").GetString(); + Assert.Equal("SystemAssigned", identityType, ignoreCase: true); } [Fact] @@ -686,6 +704,81 @@ public async Task ProtectedItemGet_DppVault_ListsProtectedItems_Successfully() Assert.Equal(JsonValueKind.Array, items.ValueKind); } + /// + /// End-to-end Disk protection through DPP vault. + /// Validates the Bug #2 (DPP) fix: protecteditem protect waits for the operation + /// to complete (), reads the backup-instance back, + /// and surfaces a real protectionStatus rather than a fake "Accepted". + /// Also implicitly validates the Bug #1 fix because protection succeeds only when the + /// DPP vault MSI created by vault create has the right RBAC on the disk + RG. + /// + [Fact] + public async Task ProtectedItemProtect_DppVault_DiskProtection_Succeeds_E2E() + { + var vaultName = $"{Settings.ResourceBaseName}-dpp"; + var policyName = $"{Settings.ResourceBaseName}-disk-policy"; + var diskName = $"{Settings.ResourceBaseName}-disk"; + var diskId = $"/subscriptions/{Settings.SubscriptionId}/resourceGroups/{Settings.ResourceGroupName}/providers/Microsoft.Compute/disks/{diskName}"; + + // 1. Create disk-workload backup policy via MCP + var policyResult = await CallToolAsync( + "azurebackup_policy_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName }, + { "vault-type", "dpp" }, + { "policy", policyName }, + { "workload-type", "AzureDisk" } + }); + + var policyOp = policyResult.AssertProperty("result"); + Assert.Equal("Succeeded", policyOp.AssertProperty("status").GetString()); + + // 2. Protect the disk via MCP — exercises the new DPP code path + var protectResult = await CallToolAsync( + "azurebackup_protecteditem_protect", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "vault", vaultName }, + { "vault-type", "dpp" }, + { "datasource-id", diskId }, + { "policy", policyName }, + { "datasource-type", "AzureDisk" } + }); + + var protectOp = protectResult.AssertProperty("result"); + + // The new code returns a real terminal status. Acceptable values: + // "Succeeded" — backend accepted the configuration + // "Failed" — backend rejected; the test infrastructure should make Succeeded the norm, + // but if the backend transiently fails we still want to assert the new + // contract (real errorMessage is present, JobId is null for DPP). + var status = protectOp.AssertProperty("status").GetString(); + Assert.True(status is "Succeeded" or "Failed", $"Unexpected DPP protect status: {status}"); + + // Bug #2 DPP contract: a backup-instance name is always returned, JobId is never set. + protectOp.AssertProperty("protectedItemName"); + Assert.False(protectOp.TryGetProperty("jobId", out var jobId) && jobId.ValueKind != JsonValueKind.Null, + "DPP protect must not return a jobId (DPP is not a job)."); + + if (status == "Succeeded") + { + // Surface the protection status (e.g., "ConfiguringProtection" / "ProtectionConfigured") + protectOp.AssertProperty("protectionStatus"); + } + else + { + // Failed responses must include a non-empty errorMessage from the backend + var errorMessage = protectOp.AssertProperty("errorMessage").GetString(); + Assert.False(string.IsNullOrWhiteSpace(errorMessage), "Failed DPP protect must include errorMessage."); + Output.WriteLine($"DPP disk protect returned Failed: {errorMessage}"); + } + } + #endregion #region Protectable Item Tests 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 index 31aa52dadb..ee5d872d71 100644 --- 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 @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +// cspell:ignore mydisk myvm slowvm afsfileshare iaasvmcontainerv azurebackup protecteditem + using System.Net; using System.Text.Json; using Azure.Mcp.Tools.AzureBackup.Commands; @@ -163,4 +165,167 @@ public void BindOptions_BindsOptionsCorrectly() Assert.Contains(options, o => o.Name == "--container"); Assert.Contains(options, o => o.Name == "--datasource-type"); } + + [Fact] + public async Task ExecuteAsync_DppResult_SurfacesProtectionStatusAndOmitsJobId() + { + // Arrange — DPP protection is not a job; result should expose ProtectionStatus + // (read back from the backup instance) and leave JobId null. + var expected = new ProtectResult( + Status: "Succeeded", + ProtectedItemName: "rg-mydisk-abcd1234", + JobId: null, + Message: "Protection configured for backup instance 'rg-mydisk-abcd1234' (status: ProtectionConfigured).", + ProtectionStatus: "ProtectionConfigured", + ErrorMessage: null); + + _backupService.ProtectItemAsync( + Arg.Is("v"), Arg.Is("rg"), Arg.Is("sub"), Arg.Is("/subscriptions/.../disks/d1"), Arg.Is("policy-disk"), + 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/.../disks/d1", "--policy", "policy-disk"]); + + // 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.ProtectedItemProtectCommandResult); + Assert.NotNull(result); + Assert.Equal("Succeeded", result.Result.Status); + Assert.Null(result.Result.JobId); + Assert.Equal("ProtectionConfigured", result.Result.ProtectionStatus); + } + + [Fact] + public async Task ExecuteAsync_DppResult_SurfacesFailureWithErrorMessage() + { + // Arrange — when DPP backend rejects (e.g. VaultMSIUnauthorized) the result must + // carry Status="Failed" + ErrorMessage rather than a misleading "Accepted". + var expected = new ProtectResult( + Status: "Failed", + ProtectedItemName: "rg-blob-xyz", + JobId: null, + Message: "Protection failed for backup instance 'rg-blob-xyz': VaultMSIUnauthorized", + ProtectionStatus: null, + ErrorMessage: "VaultMSIUnauthorized: Vault MSI is not authorized."); + + _backupService.ProtectItemAsync( + Arg.Is("v"), Arg.Is("rg"), Arg.Is("sub"), Arg.Is("/subscriptions/.../sa1"), Arg.Is("policy-blob"), + 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/.../sa1", "--policy", "policy-blob"]); + + // 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.ProtectedItemProtectCommandResult); + Assert.NotNull(result); + Assert.Equal("Failed", result.Result.Status); + Assert.Null(result.Result.JobId); + Assert.Contains("VaultMSIUnauthorized", result.Result.ErrorMessage); + } + + [Fact] + public async Task ExecuteAsync_RsvResult_SurfacesTerminalJobStatus() + { + // Arrange — RSV protection should report the polled ConfigureBackup job's + // terminal status (Completed, CompletedWithWarnings, Failed) along with the job id. + var expected = new ProtectResult( + Status: "Completed", + ProtectedItemName: "vm;iaasvmcontainerv2;rg;myvm", + JobId: "11111111-1111-1111-1111-111111111111", + Message: "VM protection completed. Use 'azurebackup protecteditem get' to verify."); + + _backupService.ProtectItemAsync( + Arg.Is("v"), Arg.Is("rg"), Arg.Is("sub"), Arg.Is("/subscriptions/.../vms/myvm"), Arg.Is("policy-vm"), + 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/.../vms/myvm", "--policy", "policy-vm"]); + + // 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.ProtectedItemProtectCommandResult); + Assert.NotNull(result); + Assert.Equal("Completed", result.Result.Status); + Assert.Equal("11111111-1111-1111-1111-111111111111", result.Result.JobId); + } + + [Fact] + public async Task ExecuteAsync_RsvResult_SurfacesFailedJobWithErrorMessage() + { + // Arrange — when ConfigureBackup ends in Failed, MCP must surface Status=Failed + // and ErrorMessage from the job rather than the previous "Accepted". + var expected = new ProtectResult( + Status: "Failed", + ProtectedItemName: "afsfileshare;sa;share", + JobId: "22222222-2222-2222-2222-222222222222", + Message: "File share protection failed: Item not found. See 'azurebackup job get --job 22222222-...' for details.", + ProtectionStatus: null, + ErrorMessage: "Item not found"); + + _backupService.ProtectItemAsync( + Arg.Is("v"), Arg.Is("rg"), Arg.Is("sub"), Arg.Is("/subscriptions/.../sa/fileServices/default/shares/share"), Arg.Is("policy-afs"), + 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/.../sa/fileServices/default/shares/share", "--policy", "policy-afs"]); + + // 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.ProtectedItemProtectCommandResult); + Assert.NotNull(result); + Assert.Equal("Failed", result.Result.Status); + Assert.Equal("Item not found", result.Result.ErrorMessage); + } + + [Fact] + public async Task ExecuteAsync_RsvResult_SurfacesInProgressWhenPollingBudgetExpires() + { + // Arrange — long-running ConfigureBackup must not cause the tool to fail; it + // should return InProgress with the job id so the caller can keep monitoring. + var expected = new ProtectResult( + Status: "InProgress", + ProtectedItemName: "vm;iaasvmcontainerv2;rg;slowvm", + JobId: "33333333-3333-3333-3333-333333333333", + Message: "VM protection is still running after the polling budget elapsed. Use 'azurebackup job get --job 33333333-...' to continue monitoring."); + + _backupService.ProtectItemAsync( + Arg.Is("v"), Arg.Is("rg"), Arg.Is("sub"), Arg.Is("/subscriptions/.../vms/slowvm"), Arg.Is("policy-vm"), + 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/.../vms/slowvm", "--policy", "policy-vm"]); + + // 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.ProtectedItemProtectCommandResult); + Assert.NotNull(result); + Assert.Equal("InProgress", result.Result.Status); + Assert.Equal("33333333-3333-3333-3333-333333333333", result.Result.JobId); + } } diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep index da80c38cb9..ba2916d9db 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep @@ -1,3 +1,4 @@ +// cspell:ignore backupconfig vaultconfig targetScope = 'resourceGroup' @minLength(3) @@ -133,6 +134,7 @@ resource dppDiskBackupReaderRoleAssignment 'Microsoft.Authorization/roleAssignme // Output the resource IDs for the post-deployment script output diskId string = testDisk.id +output diskName string = testDisk.name // ─── RSV Undelete Test Resources (Storage Account + File Share) ─── From 043d001708e6634e62b733ea5177e6dc60782697 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 23 Apr 2026 14:47:38 +0530 Subject: [PATCH 2/7] Address review: keep original status casing in RSV protect message --- .../src/Services/RsvBackupOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs index 6d6a28951b..4c6f1736b7 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -1249,7 +1249,7 @@ private static async Task BuildRsvProtectResultAsync( var message = isFailure ? $"{operationDescription} failed: {errorMessage ?? status}. See 'azurebackup job get --job {jobId}' for details." - : $"{operationDescription} {status.ToLowerInvariant()}. Use 'azurebackup protecteditem get' to verify the protected item."; + : $"{operationDescription} status: {status}. Use 'azurebackup protecteditem get' to verify the protected item."; return new ProtectResult( status, From ce9e17c367979fae0c80f3f3a84f7ba1b64545f7 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 23 Apr 2026 17:16:31 +0530 Subject: [PATCH 3/7] Skip new DPP live tests in playback (no recordings yet) --- .../AzureBackupCommandTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 index 53ca0cd69e..395087ab7b 100644 --- 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 @@ -149,6 +149,13 @@ public async Task VaultCreate_CreatesVault_Successfully() [Fact] public async Task VaultCreate_CreatesDppVault_Successfully() { + // No session recording is published for this test yet; skip in playback to avoid + // a 404 from the test proxy. The test still runs in Live mode for validation. + if (TestMode == TestMode.Playback) + { + return; + } + var vaultName = RegisterOrRetrieveVariable("createdDppVaultName", $"test-dpp-{Random.Shared.NextInt64()}"); var result = await CallToolAsync( @@ -715,6 +722,13 @@ public async Task ProtectedItemGet_DppVault_ListsProtectedItems_Successfully() [Fact] public async Task ProtectedItemProtect_DppVault_DiskProtection_Succeeds_E2E() { + // No session recording is published for this E2E test yet; skip in playback to + // avoid a 404 from the test proxy. The test still runs in Live mode for validation. + if (TestMode == TestMode.Playback) + { + return; + } + var vaultName = $"{Settings.ResourceBaseName}-dpp"; var policyName = $"{Settings.ResourceBaseName}-disk-policy"; var diskName = $"{Settings.ResourceBaseName}-disk"; From baf204158be1173ddf5e2c129e88a4302e1c6710 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 23 Apr 2026 17:23:19 +0530 Subject: [PATCH 4/7] Revert "Skip new DPP live tests in playback (no recordings yet)" This reverts commit ce9e17c367979fae0c80f3f3a84f7ba1b64545f7. --- .../AzureBackupCommandTests.cs | 14 -------------- 1 file changed, 14 deletions(-) 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 index 395087ab7b..53ca0cd69e 100644 --- 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 @@ -149,13 +149,6 @@ public async Task VaultCreate_CreatesVault_Successfully() [Fact] public async Task VaultCreate_CreatesDppVault_Successfully() { - // No session recording is published for this test yet; skip in playback to avoid - // a 404 from the test proxy. The test still runs in Live mode for validation. - if (TestMode == TestMode.Playback) - { - return; - } - var vaultName = RegisterOrRetrieveVariable("createdDppVaultName", $"test-dpp-{Random.Shared.NextInt64()}"); var result = await CallToolAsync( @@ -722,13 +715,6 @@ public async Task ProtectedItemGet_DppVault_ListsProtectedItems_Successfully() [Fact] public async Task ProtectedItemProtect_DppVault_DiskProtection_Succeeds_E2E() { - // No session recording is published for this E2E test yet; skip in playback to - // avoid a 404 from the test proxy. The test still runs in Live mode for validation. - if (TestMode == TestMode.Playback) - { - return; - } - var vaultName = $"{Settings.ResourceBaseName}-dpp"; var policyName = $"{Settings.ResourceBaseName}-disk-policy"; var diskName = $"{Settings.ResourceBaseName}-disk"; From 5764c5773b5debb72f2de558399bbe2d974c58c0 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Thu, 23 Apr 2026 17:41:25 +0530 Subject: [PATCH 5/7] Record new DPP live tests; sanitize createdBy email and tenant id - Fix VaultCreate_CreatesDppVault assertion: vault_get returns 'vaults' array, not a singular 'vault' property. - Add GeneralRegexSanitizers for @microsoft.com UPNs (createdBy/lastModifiedBy in x-ms-arm-resource-system-data) and the recording tenant id (x-ms-operation-identifier and async cert URLs). - Re-record VaultCreate_CreatesDppVault_Successfully and ProtectedItemProtect_DppVault_DiskProtection_Succeeds_E2E and update assets.json tag. --- .../AzureBackupCommandTests.cs | 19 ++++++++++++++++++- .../assets.json | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) 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 index 53ca0cd69e..c21f43024a 100644 --- 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 @@ -50,6 +50,20 @@ public class AzureBackupCommandTests(ITestOutputHelper output, TestProxyFixture { Regex = "(?i)azurebackuprg_mcp-test", Value = "Sanitized", + }), + // ARM x-ms-arm-resource-system-data header may include the recording user's UPN + // when fresh resources are created (createdBy / lastModifiedBy fields). + new GeneralRegexSanitizer(new GeneralRegexSanitizerBody() + { + Regex = @"[A-Za-z0-9._%+-]+@microsoft\.com", + Value = "sanitized@example.com", + }), + // x-ms-operation-identifier and certificate URLs in response headers leak the + // tenant id of the recording subscription. Replace with the well-known zero GUID. + new GeneralRegexSanitizer(new GeneralRegexSanitizerBody() + { + Regex = "72f988bf-86f1-41af-91ab-2d7cd011db47", + Value = "00000000-0000-0000-0000-000000000000", }) ]; @@ -177,7 +191,10 @@ public async Task VaultCreate_CreatesDppVault_Successfully() { "vault", vaultName }, { "vault-type", "dpp" } }); - var fetchedVault = getResult.AssertProperty("vault"); + // azurebackup_vault_get returns a 'vaults' array; pick the first matching entry. + var vaults = getResult.AssertProperty("vaults"); + Assert.Equal(JsonValueKind.Array, vaults.ValueKind); + var fetchedVault = vaults.EnumerateArray().First(); var identityType = fetchedVault.AssertProperty("identityType").GetString(); Assert.Equal("SystemAssigned", identityType, ignoreCase: true); } 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 index 52ea34338f..981bbc7780 100644 --- 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 @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "", "TagPrefix": "Azure.Mcp.Tools.AzureBackup.LiveTests", - "Tag": "Azure.Mcp.Tools.AzureBackup.LiveTests_c70ee092ee" + "Tag": "Azure.Mcp.Tools.AzureBackup.LiveTests_7a5bddd1ef" } From 75ab2ee7d3ce803f3836095614e9aebca84d1090 Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Mon, 27 Apr 2026 16:27:55 +0530 Subject: [PATCH 6/7] Address review: separate try-catch for DPP protect and re-read Split the single try-catch in DppBackupOperations.ProtectItemAsync so that a transient failure on the backup instance re-read (GetAsync) no longer misattributes the status as 'Failed' when the CreateOrUpdateAsync protection operation actually succeeded. The re-read is now wrapped in its own try-catch that swallows transient errors while still surfacing the correct 'Succeeded' status to the caller. --- .../src/Services/DppBackupOperations.cs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index 885a0b01a8..47f854864e 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -215,26 +215,11 @@ public async Task ProtectItemAsync( // WaitUntil.Completed lets the SDK poll the Azure-AsyncOperation header for us // and surface the real server-side error (e.g. VaultMSIUnauthorized) as a // RequestFailedException, instead of silently returning "Accepted". + ArmOperation operation; try { - var operation = await collection.CreateOrUpdateAsync( + operation = await collection.CreateOrUpdateAsync( WaitUntil.Completed, instanceName, instanceData, cancellationToken); - - // Re-read the backup instance to capture the authoritative protection status. - // The LRO can complete while the BI is still in ConfiguringProtection; both - // outcomes are surfaced to the caller via ProtectionStatus. - var instanceResource = armClient.GetDataProtectionBackupInstanceResource(operation.Value.Id); - var bi = await instanceResource.GetAsync(cancellationToken); - var protectionStatus = bi.Value.Data.Properties?.ProtectionStatus?.Status?.ToString(); - - return new ProtectResult( - "Succeeded", - instanceName, - JobId: null, - $"Protection configured for backup instance '{instanceName}' (status: {protectionStatus ?? "Unknown"}). " + - $"Use 'azurebackup protecteditem get --protected-item {instanceName}' to view details.", - ProtectionStatus: protectionStatus, - ErrorMessage: null); } catch (RequestFailedException ex) { @@ -246,6 +231,32 @@ public async Task ProtectItemAsync( ProtectionStatus: null, ErrorMessage: ex.Message); } + + // Re-read the backup instance to capture the authoritative protection status. + // The LRO can complete while the BI is still in ConfiguringProtection; both + // outcomes are surfaced to the caller via ProtectionStatus. If the re-read + // fails with a transient error, report success (protection did complete) and + // let the caller verify with 'protecteditem get'. + string? protectionStatus = null; + try + { + var instanceResource = armClient.GetDataProtectionBackupInstanceResource(operation.Value.Id); + var bi = await instanceResource.GetAsync(cancellationToken); + protectionStatus = bi.Value.Data.Properties?.ProtectionStatus?.Status?.ToString(); + } + catch (RequestFailedException) + { + // Transient re-read failure; protection itself succeeded. + } + + return new ProtectResult( + "Succeeded", + instanceName, + JobId: null, + $"Protection configured for backup instance '{instanceName}' (status: {protectionStatus ?? "Unknown"}). " + + $"Use 'azurebackup protecteditem get --protected-item {instanceName}' to view details.", + ProtectionStatus: protectionStatus, + ErrorMessage: null); } public async Task GetProtectedItemAsync( From 8f6d73e284f130478c9c9bb2081c74d5f81284ff Mon Sep 17 00:00:00 2001 From: Shraddha Jain Date: Tue, 28 Apr 2026 16:38:01 +0530 Subject: [PATCH 7/7] Address review: remove scratch files, move cspell to cspell.json --- .vscode/cspell.json | 41 +- ai-query.json | 1 - disk-dpp-template.json | 57 --- .../bug-bash/policy-create-followup-emails.md | 138 ------- docs/bug-bash/policy-sdk-api-feedback.html | 378 ------------------ docs/bug-bash/policy-sdk-api-feedback.md | 115 ------ fix-disk-test.ps1 | 63 --- fix-disk-test2.ps1 | 32 -- fix-sql-arch.ps1 | 69 ---- fix-sql-diff7.ps1 | 19 - fix-sql-fld.ps1 | 62 --- fix-sql-fld2.ps1 | 63 --- fix-sql-fld3.ps1 | 66 --- fix-sql-fld4.ps1 | 27 -- fix-sql-fld5.ps1 | 14 - fix-sql-log5.ps1 | 14 - fix-sql-ret.ps1 | 23 -- fix-vm-arch.ps1 | 74 ---- resolve-threads.ps1 | 9 - tmp-assets-main.json | 1 - .../docs/telemetry-queries.md | 369 ----------------- .../src/Commands/Vault/VaultCreateCommand.cs | 2 - .../src/Models/ProtectResult.cs | 2 - .../src/Services/DppBackupOperations.cs | 2 - .../src/Services/RsvBackupOperations.cs | 2 - .../AzureBackupCommandTests.cs | 2 - .../ProtectedItemProtectCommandTests.cs | 2 - .../tests/test-resources.bicep | 1 - 28 files changed, 40 insertions(+), 1608 deletions(-) delete mode 100644 ai-query.json delete mode 100644 disk-dpp-template.json delete mode 100644 docs/bug-bash/policy-create-followup-emails.md delete mode 100644 docs/bug-bash/policy-sdk-api-feedback.html delete mode 100644 docs/bug-bash/policy-sdk-api-feedback.md delete mode 100644 fix-disk-test.ps1 delete mode 100644 fix-disk-test2.ps1 delete mode 100644 fix-sql-arch.ps1 delete mode 100644 fix-sql-diff7.ps1 delete mode 100644 fix-sql-fld.ps1 delete mode 100644 fix-sql-fld2.ps1 delete mode 100644 fix-sql-fld3.ps1 delete mode 100644 fix-sql-fld4.ps1 delete mode 100644 fix-sql-fld5.ps1 delete mode 100644 fix-sql-log5.ps1 delete mode 100644 fix-sql-ret.ps1 delete mode 100644 fix-vm-arch.ps1 delete mode 100644 resolve-threads.ps1 delete mode 100644 tmp-assets-main.json delete mode 100644 tools/Azure.Mcp.Tools.AzureBackup/docs/telemetry-queries.md diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 655cdd95bc..f560f21a78 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -633,6 +633,45 @@ "navm", "rego", "virtualmachine", - "virtualnetwork" + "virtualnetwork", + "afsfileshare", + "azurebackup", + "iaasvmcontainerv", + "mydisk", + "myvm", + "protecteditem", + "slowvm", + "adls", + "alwayson", + "backupconfig", + "dbname", + "disasterrecovery", + "esan", + "fileshare", + "hana", + "lifecycles", + "pgflex", + "protectable", + "protectableitem", + "recoverypoint", + "saphana", + "saphanadatabase", + "saphanadbi", + "saphanadbinstance", + "saphanasystem", + "sqldatabase", + "sqlinstance", + "systemassigned", + "systemassigneduserassigned", + "userassigned", + "vaultconfig", + "vmname", + "azurebackuprg", + "diskname", + "georedundant", + "locallyredundant", + "undelete", + "undeletes", + "zoneredundant" ] } diff --git a/ai-query.json b/ai-query.json deleted file mode 100644 index 4ab9a0b61f..0000000000 --- a/ai-query.json +++ /dev/null @@ -1 +0,0 @@ -{"query":"requests | where customDimensions.ToolId contains 'azurebackup' | summarize cnt=count() by tostring(customDimensions.ToolId) | top 20 by cnt","timespan":"P30D"} \ No newline at end of file diff --git a/disk-dpp-template.json b/disk-dpp-template.json deleted file mode 100644 index 1f86f0923b..0000000000 --- a/disk-dpp-template.json +++ /dev/null @@ -1,57 +0,0 @@ -WARNING: Command group 'dataprotection' is experimental and under development. Reference and support levels: https://aka.ms/CLI_refstatus -{ - "datasourceTypes": [ - "Microsoft.Compute/disks" - ], - "name": "DiskPolicy1", - "objectType": "BackupPolicy", - "policyRules": [ - { - "backupParameters": { - "backupType": "Incremental", - "objectType": "AzureBackupParams" - }, - "dataStore": { - "dataStoreType": "OperationalStore", - "objectType": "DataStoreInfoBase" - }, - "name": "BackupHourly", - "objectType": "AzureBackupRule", - "trigger": { - "objectType": "ScheduleBasedTriggerContext", - "schedule": { - "repeatingTimeIntervals": [ - "R/2020-04-05T13:00:00+00:00/PT4H" - ] - }, - "taggingCriteria": [ - { - "isDefault": true, - "tagInfo": { - "id": "Default_", - "tagName": "Default" - }, - "taggingPriority": 99 - } - ] - } - }, - { - "isDefault": true, - "lifecycles": [ - { - "deleteAfter": { - "duration": "P7D", - "objectType": "AbsoluteDeleteOption" - }, - "sourceDataStore": { - "dataStoreType": "OperationalStore", - "objectType": "DataStoreInfoBase" - } - } - ], - "name": "Default", - "objectType": "AzureRetentionRule" - } - ] -} diff --git a/docs/bug-bash/policy-create-followup-emails.md b/docs/bug-bash/policy-create-followup-emails.md deleted file mode 100644 index 2f39a152c9..0000000000 --- a/docs/bug-bash/policy-create-followup-emails.md +++ /dev/null @@ -1,138 +0,0 @@ -# Azure Backup `policy create` follow-up emails - -Two emails, one per workload area, requesting confirmation of supported policy shapes for the three remaining deferred MCP live tests. - -> **Status update (2026-04-27):** The fourth originally-deferred test (`PolicyCreate_DppDisk_VaultTierMultiTierArchive_E2E`) has been **resolved**. After re-reading the Disk DPP manifest we dropped the unsupported `--archive-tier-*` and `--yearly-*` flags, added `--enable-vault-tier-copy` with Weekly/Monthly retention, and the renamed `PolicyCreate_DppDisk_VaultTierMultiTier_E2E` now records and plays back successfully (52/6/0). The validator now also rejects `--archive-tier-*` for **all** DPP datasources (no DPP workload supports ArchiveStore today) and `--yearly-retention-*` for AzureDisk (manifest allows only Daily/Weekly/Monthly tags). - ---- - -## Email 1 — RSV IaaSVM (Enhanced V2): Weekly + multi-tier + archive - -**To:** Azure Backup IaaSVM PM / RSV Backend -**Subject:** RSV IaaSVMv2 — supported policy shape for Weekly schedule + Weekly/Monthly/Yearly retention + archive (`TieringPolicy`) - -Hi team, - -We are wiring up `azmcp azurebackup policy create` for the IaaSVM (Enhanced V2) backup management type and one shape is consistently rejected by the RSV control plane. Before we hand-craft the JSON for the n-th time we would like the canonical shape from you. - -### What we are sending - -- Backup management type: `AzureIaasVM` -- Policy type: `V2` (Enhanced) -- Schedule: `WeeklySchedule` on Sunday at 02:00 UTC -- Retention: - - `WeeklyRetentionSchedule` — 4 weeks, Sunday - - `MonthlyRetentionSchedule` — 12 months, day 1, Weekly format (Sunday, Week 1) - - `YearlyRetentionSchedule` — 5 years, January, day 1, Weekly format (Sunday, Week 1) -- `TieringPolicy.ArchivedRP`: `TierAfter`, `Duration=90`, `DurationType=Days` -- We send no `DailyRetentionSchedule` (because the schedule is Weekly). - -### What the service returns - -``` -HTTP 400 BMSUserErrorInvalidPolicyInput -"Input for create or update policy is not in proper format. Please check format - of parameters like schedule time, schedule days, retention time and retention - days" -``` - -The same vault accepts our `Daily + Daily/Weekly/Monthly/Yearly + Archive` shape, so this is specific to the Weekly+LTR+Archive combination on V2-Enhanced. - -### What we need from you - -1. A confirmed JSON sample (or a reference policy from Az PowerShell / Portal export) for **V2 Enhanced + Weekly + Weekly/Monthly/Yearly retention + Archive on Snapshot or Archive on Vault**, against any test vault. -2. Confirmation of: - - whether `WeeklySchedule` requires a non-empty `DailySchedule` placeholder (we currently omit it), - - whether `TieringPolicy` belongs at the policy root or under a `SubProtectionPolicy`, - - whether `WeeklyRetentionFormat.DaysOfTheWeek` must equal `WeeklySchedule.ScheduleRunDays`, - - whether `MonthlyRetentionSchedule.RetentionScheduleFormatType` must be `Weekly` (not `Daily`) for a Weekly-scheduled policy. -3. The minimum API version required for this shape (we are on `2024-04-01`). - -We are happy to share request/response captures or run a fresh repro against any vault you point us at. - -Thanks! - ---- - -## Email 2 — RSV AzureWorkload (SQL in Azure IaaSVM, Windows) - -**To:** Azure Backup SQL/Workload PM / RSV Backend -**Subject:** RSV SQL in Azure IaaSVM (Windows) — two unsupported policy shapes (Full+Differential+Log retention; Full Weekly + Archive on Full sub-policy) - -Hi team, - -Same context — we are exposing `azmcp azurebackup policy create` for the **SQL in Azure IaaSVM (Windows)** workload (`workloadType=SQLDataBase`, `backupManagementType=AzureWorkload`) and have **two** policy shapes the RSV API rejects. Both are blockers for live test coverage of common policy patterns. Please confirm the canonical JSON for each. - -> Note: there is no `MSSQL` workload type in RSV — this is `SQLDataBase` under `AzureWorkload`, i.e. SQL Server running inside an Azure VM. We are not asking about Azure SQL Database / Managed Instance. - -### Shape A — Full + Differential + Log with Diff retention >= Log retention - -**What we send** - -- Workload type: `SQLDataBase` (SQL in Azure IaaSVM, Windows), backup management type: `AzureWorkload` -- `SubProtectionPolicy[]`: - - `Full`: `Weekly` schedule, Sunday 02:00; Weekly retention 4 weeks Sunday. - - `Differential`: `Weekly` schedule, **Wed,Fri** (no overlap with Full); retention 30 days. - - `Log`: `LogSchedulePolicy` every 60 minutes; retention 15 days. -- We already enforce on the client side: - - Full and Differential never share a day; - - Log retention < Differential retention. - -**What the service returns** - -``` -HTTP 400 BMSUserErrorPolicyRetentionInvalid -"The retention duration provided in the backup policy is not supported. - Verify the supported retention durations for different backup types at - https://aka.ms/policysupport" -``` - -### Shape B — Full Weekly + Monthly retention + Archive (`TieringPolicy`) on the Full sub-policy - -**What we send** - -- Workload type: `SQLDataBase` (SQL in Azure IaaSVM, Windows), backup management type: `AzureWorkload` -- `SubProtectionPolicy[]`: - - `Full`: `Weekly` schedule, Sunday 02:00; Weekly retention 4 weeks Sunday + Monthly retention 12 months day 1 (Weekly format Sunday Week 1); `TieringPolicy.ArchivedRP = TierAfter, 90 Days`. - - `Log`: 60-minute frequency, 15-day retention. -- No `DailyRetentionSchedule` (schedule is Weekly). - -**What the service returns** - -``` -HTTP 400 BMSUserErrorInvalidPolicyInput -"Input for create or update policy is not in proper format..." -``` - -### What we need from you - -1. For **Shape A**: a confirmed retention duration matrix for SQL `Full` (Weekly) + `Differential` + `Log`. Specifically: - - allowed range for `Differential.RetentionDuration` when `Full.SchedulePolicy = WeeklySchedule`, - - whether `Log.RetentionDuration` must be `<= Differential.RetentionDuration` AND `<= Full.RetentionDuration`, - - whether the `Differential` sub-policy must use `SimpleRetentionPolicy` or `LongTermRetentionPolicy`, - - any constraint linking `Differential.SchedulePolicy.ScheduleRunDays` to `Full.SchedulePolicy.ScheduleRunDays`. -2. For **Shape B**: a confirmed JSON sample for SQL with `TieringPolicy` attached to the `Full` sub-policy (or guidance that it must live elsewhere — e.g., on a different sub-policy or at the root). -3. Whether the `MakePolicyConsistent` flag (or `policySubType=Enhanced`) is required for either shape. -4. Az PowerShell or Portal reference policies for both shapes against any SQL-protected vault. - -Captures and repros available on request — this vault has other SQL policies succeeding so we have a clean side-by-side. - -Thanks! - ---- - -### Summary of the three blocked tests - -| Workload (test) | Failure code | -| --- | --- | -| RSV IaaSVMv2 — `PolicyCreate_RsvVm_WeeklyMultiTierWithArchive_E2E` | `BMSUserErrorInvalidPolicyInput` | -| RSV SQL in Azure IaaSVM (Windows) — `PolicyCreate_RsvSql_FullLogDiff_E2E` | `BMSUserErrorPolicyRetentionInvalid` | -| RSV SQL in Azure IaaSVM (Windows) — `PolicyCreate_RsvSql_WithArchiveTier_E2E` | `BMSUserErrorInvalidPolicyInput` | - -All three have working sibling tests against the same vaults, so vault setup / RBAC / preview-feature flags are not the issue — these are policy-shape questions only. - -### Previously resolved - -| Workload (test) | Resolution | -| --- | --- | -| DPP AzureDisk — `PolicyCreate_DppDisk_VaultTierMultiTier_E2E` (renamed from `*Archive*`) | Dropped unsupported `--archive-tier-*` and `--yearly-*` per DPP manifest. Added `--enable-vault-tier-copy` with Weekly/Monthly retention. Now passing (52/6/0). Validator rejects archive flags for all DPP workloads and yearly retention for AzureDisk. | \ No newline at end of file diff --git a/docs/bug-bash/policy-sdk-api-feedback.html b/docs/bug-bash/policy-sdk-api-feedback.html deleted file mode 100644 index 56f233fc79..0000000000 --- a/docs/bug-bash/policy-sdk-api-feedback.html +++ /dev/null @@ -1,378 +0,0 @@ - - - - - - - - -
- To: Azure Backup SDK Team / RSV Backend / DPP Backend / Swagger Owners
- Cc: Azure Backup PM
- Subject: Policy Create SDK & API improvements — learnings from MCP tool integration (28 files, 55 live tests, 4 Kusto-debugged failures) -
- -

🔧 Azure Backup Policy Create — SDK & API Improvement Proposals

- -

Hi team,

- -

We just landed azmcp azurebackup policy create covering all RSV and DPP workloads (PR #2504). During integration we hit multiple service-side validations that return opaque errors, forcing us to build a client-side PolicyCreateValidator with 15+ rules just to give users actionable messages.

- -
- 📌 Key finding: We are reading the full error response body (RequestFailedException.Message includes HTTP status, error.code, error.message, and headers). The response body is typically just:

- {"error":{"code":"BMSUserErrorInvalidPolicyInput","message":"Input for create or update policy is not in proper format..."}}

- No details array, no innererror, no target field. The specific validation reason (e.g., "DaysOfTheWeek of retention schedule must be same of backup schedule DaysOfTheWeek") is thrown server-side as an ArgumentException but dropped during error mapping in FMComponent before it reaches the HTTP response. We confirmed this by querying Kusto TraceLogMessageAll traces. -
- -
- - -

1. HIGH Error messages hide the actual validation failure

- -
- 🎯 Impacted workloads: - AzureIaasVM - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - AzureFileShare - — All RSV workloads -
- -
- ⚠️ Problem: BMSUserErrorInvalidPolicyInput is a catch-all for ~10 different validation failures. The API response body contains only a generic message. The actual validation reason is only visible in Kusto server-side traces. -
- -
- 🔍 Evidence — SQL AzureWorkload policy create:
- TaskId: bd205322-760d-48b2-8cdc-b3a3804b6b46
- RequestId: ea296f15-e026-40c5-9391-7c0869a43f28
- Timestamp: 2026-04-27T08:22:14Z  |  Cluster: mabprod1 / MabKustoProd1

- HTTP response body (what the caller sees):
- {"error":{"code":"BMSUserErrorInvalidPolicyInput","message":"Input for create or update policy is not in proper format. Please check format of parameters like schedule time, schedule days, retention time and retention days "}}

- Kusto trace at 08:22:14.7004761Z (what the service actually knows):
- FMException: [ErrorCode:CloudInvalidInputError, Message:DaysOfTheWeek of retention schedule must be same of backup schedule DaysOfTheWeek]

- Flow: WeeklyRetentionSchedule.ValidateWithBackupScheduleDOW()ArgumentExceptionFMComponent catches → maps to CloudInvalidInputError → maps to BMSUserErrorInvalidPolicyInputinner message dropped → generic message in HTTP response. -
- -
- ✅ Ask: Surface the inner ArgumentException.Message in the API error response details array or innererror field. The service already has the specific message — it's just not propagated through the FMComponent error mapping pipeline. -
- -
- - -

2. HIGH Error message template with unpopulated parameters

- -
- 🎯 Impacted workloads: - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - — All RSV AzureWorkload sub-types with Log sub-policy -
- -
- ⚠️ Problem: UserErrorLogRetentionNotInValidRangeInPolicy returns:

- "Log retention not present in allowed range (minRange NO_PARAM and maxRange NO_PARAM)"

- The NO_PARAM placeholders are never filled with the actual min/max values. -
- -
- 🔍 Evidence — SQL AzureWorkload with log-retention-days=5:
- TaskId: ff04aea7-188d-4c24-a764-b2c84ae0c359
- RequestId: 9fbb17b1-b32d-4279-8ea0-e18d286045b0
- Timestamp: 2026-04-27T09:01:11Z

- We discovered the minimum is 7 days only through trial-and-error (tried 5 → got NO_PARAM error, tried 7 → passed). -
- -
- ✅ Ask: Populate the minRange and maxRange template parameters (e.g., "minRange 7 days and maxRange 35 days"). -
- -
- - -

3. HIGH SQL Differential: undocumented single-day constraint

- -
- 🎯 Impacted workloads: - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - — All RSV AzureWorkload sub-types with Differential sub-policy -
- -
- ⚠️ Problem: SQL Differential sub-policy only supports exactly 1 day per week. Passing ["Wednesday","Friday"] in scheduleRunDays gets rejected with the generic BMSUserErrorInvalidPolicyInput. No documentation mentions this constraint. -
- -
- 🔍 Evidence — SQL AzureWorkload with Diff=Wed,Fri:
- TaskId: bd205322-760d-48b2-8cdc-b3a3804b6b46
- RequestId: ea296f15-e026-40c5-9391-7c0869a43f28
- Timestamp: 2026-04-27T08:22:14Z

- Original test used differential-schedule-days-of-week: "Wednesday,Friday". After switching to single-day "Wednesday", the Diff constraint was satisfied (next failure was the retention DOW mismatch, then log retention range — all resolved iteratively via Kusto). -
- -
- ✅ Ask:
- • Swagger: Add maxItems: 1 on Differential.schedulePolicy.scheduleRunDays for AzureWorkload policies.
- • Service: Return a specific error like UserErrorDifferentialScheduleMultipleDaysNotSupported. -
- -
- - -

4. HIGH Weekly retention days-of-week must match schedule — not documented

- -
- 🎯 Impacted workloads: - AzureIaasVM - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - AzureFileShare - — All RSV workloads with Weekly schedule -
- -
- ⚠️ Problem: When scheduleRunFrequency=Weekly with scheduleRunDays=["Monday"], the WeeklyRetentionSchedule.daysOfTheWeek MUST equal ["Monday"]. Setting ["Sunday"] (a reasonable default) causes rejection with the generic error. -
- -
- 🔍 Evidence — SQL AzureWorkload with Full=6-day Weekly, retention DOW=Sunday:
- TaskId: bd205322-760d-48b2-8cdc-b3a3804b6b46
- RequestId: ea296f15-e026-40c5-9391-7c0869a43f28
- Timestamp: 2026-04-27T08:22:14Z

- Kusto trace: WeeklyRetentionSchedule.ValidateWithBackupScheduleDOW"DaysOfTheWeek of retention schedule must be same of backup schedule DaysOfTheWeek". This message is not surfaced in the HTTP response (see Issue #1). -
- -
- ✅ Ask:
- • Swagger: Add description on weeklySchedule.daysOfTheWeek: "Must match scheduleRunDays when scheduleRunFrequency is Weekly."
- • SDK: Add XML doc on WeeklyRetentionSchedule.DaysOfTheWeek noting the constraint. -
- -
- - -

5. MEDIUM Monthly/Yearly retention: Weekly schedule requires relative format

- -
- 🎯 Impacted workloads: - AzureIaasVM - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - — All RSV workloads with Weekly schedule + monthly/yearly retention -
- -
- ⚠️ Problem: When scheduleRunFrequency=Weekly, monthly/yearly retention MUST use RetentionScheduleFormatType=Weekly (relative: week-of-month + days-of-week). Using Daily format (absolute: days-of-month) is rejected with the generic error. -
- -
- 🔍 Evidence — VM AzureIaasVM with Weekly schedule + absolute monthly (days-of-month=1):
- TaskId: 9122a889-abf2-406c-b87d-9749437b69b7
- RequestId: ea296f15-e026-40c5-9391-7c0869a43f28
- Timestamp: 2026-04-24T19:03:37Z (original VM Weekly+Archive failure)

- Also hit on SQL AzureWorkload with monthly-retention-days-of-month: 1 instead of monthly-retention-week-of-month: First + monthly-retention-days-of-week: Sunday. Both returned generic BMSUserErrorInvalidPolicyInput. -
- -
- ✅ Ask:
- • Service: Return UserErrorRetentionFormatInvalidForWeeklySchedule: "Monthly/Yearly retention must use Weekly format when backup schedule is Weekly."
- • Swagger: Add constraint description on retentionScheduleFormatType. -
- -
- - -

6. MEDIUM DPP: No client-accessible manifest for per-datasource constraints

- -
- 🎯 Impacted workloads: - AzureDisk - AKS - PostgreSQL Flex - PostgreSQL - CosmosDB - ElasticSAN - AzureBlob - ADLS - — All DPP (Backup vault) datasources -
- -
- ⚠️ Problem: Each DPP datasource has a server-side manifest defining allowedRetentionTagNames, allowedFirstTargetStores, storeConstraints, and allowedScheduledTriggerFrequencies. But this manifest is not exposed to clients. We had to hard-code that:
- • AzureDisk doesn't support Yearly retention or ArchiveStore
- • No DPP datasource supports ArchiveStore today
- • AzureDisk requires OperationalStore as first target -
- -
- 🔍 Evidence — DPP AzureDisk with archive + yearly retention:
- ActivityId: 97ad8418-9a6d-4650-b108-bdf987702a07
- Timestamp: 2026-04-24 (original DPP Disk failure)

- Error: BMSUserErrorInvalidInput — generic, no indication which constraint was violated (archive unsupported? yearly tag invalid? wrong first target store?). We had to read the Disk DPP manifest source code to understand the constraints. -
- -
- ✅ Ask: Expose a GET /datasources/{type}/manifest endpoint (or include constraints in get-default-policy-template) so clients can validate programmatically instead of hard-coding. -
- -
- - -

7. MEDIUM SQL Log retention: minimum not discoverable

- -
- 🎯 Impacted workloads: - AzureWorkload (SQL) - AzureWorkload (SAPHANA) - AzureWorkload (SAPASE) - — All RSV AzureWorkload sub-types with Log sub-policy -
- -
- ⚠️ Problem: SQL Log retention minimum is 7 days, but this is only discoverable by getting UserErrorLogRetentionNotInValidRangeInPolicy (with NO_PARAM values). No swagger annotation or SDK doc exposes this. -
- -
- 🔍 Evidence — SQL AzureWorkload with log-retention-days=5:
- TaskId: ff04aea7-188d-4c24-a764-b2c84ae0c359
- RequestId: 9fbb17b1-b32d-4279-8ea0-e18d286045b0
- Timestamp: 2026-04-27T09:01:11Z

- Trial: 5 days → NO_PARAM error. 7 days → success. Still don't know the maximum. -
- -
- ✅ Ask: Add minimum: 7 and maximum (actual bound) to the swagger definition for Log.retentionPolicy.retentionDuration.count. -
- -
- - -

8. MEDIUM VM Enhanced sub-type required for archive — silently ignored

- -
- 🎯 Impacted workloads: - AzureIaasVM - — VM (IaaSVM) only -
- -
- ⚠️ Problem: VM policies with TieringPolicy (archive) require policySubType=Enhanced (V2). Without it, the policy is created as Standard V1 and tiering is silently ignored — no error returned. -
- -
- 🔍 Evidence — VM AzureIaasVM Weekly+Archive without Enhanced:
- TaskId: 9122a889-abf2-406c-b87d-9749437b69b7
- Timestamp: 2026-04-24T19:03:37Z

- Discovered during test iteration. Adding policySubType=Enhanced resolved the archive tier issue for VM Weekly+Multi-tier+Archive. -
- -
- ✅ Ask: Service should reject TieringPolicy on Standard (V1) policies with a specific error, or auto-promote to V2 Enhanced. -
- -
- -

📊 Summary: Client-side rules we built to compensate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Client-side validation ruleImpacted workloadsService error it preventsRoot cause
SQL Diff must be exactly 1 daySQL SAPHANA SAPASEBMSUserErrorInvalidPolicyInputUndocumented constraint
SQL Log retention ≥ 7 daysSQL SAPHANA SAPASEUserErrorLogRetentionNotInValidRangeInPolicyTemplate params NO_PARAM
SQL Log < Diff retentionSQL SAPHANA SAPASEUserErrorLogRetentionMoreThanDiffRetentionInPolicyWell-documented ✓
Weekly retention DOW must match scheduleVM SQL SAPHANA AFSBMSUserErrorInvalidPolicyInputInner message dropped
DPP: reject archive for all datasourcesDisk AKS PGFlex Cosmos Blob ADLSBMSUserErrorInvalidInputNo client manifest
AzureDisk: reject Yearly retentionDiskBMSUserErrorInvalidInputNo client manifest
Full/Diff no day overlapSQL SAPHANA SAPASEBMSUserErrorInvalidPolicyInputInner message dropped
Weekly schedule requires days-of-weekVM SQL AFSSDK null-ref or generic errorMissing swagger constraint
Continuous DPP rejects schedule/retentionBlob ADLSBMSUserErrorInvalidInputNo client manifest
- -

Every one of these could be a service-side improvement that benefits all callers (Az CLI, PowerShell, Portal, MCP, third-party tools), not just us.

- -

Happy to share full JSON payloads and Kusto queries for any of these. The PR with all the workarounds is at microsoft/mcp#2504.

- -

Thanks!

- - - diff --git a/docs/bug-bash/policy-sdk-api-feedback.md b/docs/bug-bash/policy-sdk-api-feedback.md deleted file mode 100644 index 3069eeffc1..0000000000 --- a/docs/bug-bash/policy-sdk-api-feedback.md +++ /dev/null @@ -1,115 +0,0 @@ -# Azure Backup Policy Create — SDK & API Improvement Proposals - -**To:** Azure Backup SDK Team / RSV Backend / DPP Backend / Swagger Owners -**Cc:** Azure Backup PM -**Subject:** Policy Create SDK & API improvements — learnings from MCP tool integration (28 files, 55 live tests, 4 Kusto-debugged failures) - -Hi team, - -We just landed `azmcp azurebackup policy create` covering all RSV and DPP workloads ([PR #2504](https://github.com/microsoft/mcp/pull/2504)). During integration we hit multiple service-side validations that return opaque errors, forcing us to build a client-side `PolicyCreateValidator` with 15+ rules just to give users actionable messages. Below are the specific issues and proposed SDK/swagger/service improvements, organized by impact. - ---- - -## 1. Error messages that hide the actual validation failure - -**Problem:** `BMSUserErrorInvalidPolicyInput` is a catch-all for ~10 different validation failures. The API returns: -> "Input for create or update policy is not in proper format. Please check format of parameters like schedule time, schedule days, retention time and retention days" - -…when the *actual* server-side error (visible only in Kusto `TraceLogMessageAll`) is something specific like: -> "DaysOfTheWeek of retention schedule must be same of backup schedule DaysOfTheWeek" - -**Evidence:** Request `ea296f15-e026-40c5-9391-7c0869a43f28` — Kusto showed `WeeklyRetentionSchedule.ValidateWithBackupScheduleDOW` threw `ArgumentException` with the exact field mismatch, but the API response lost this detail during the `CloudInvalidInputError → BMSUserErrorInvalidPolicyInput` error mapping in `FMComponent`. - -**Ask:** Surface the inner validation message in the API error response `details` array. The service already has the specific message — it's just not propagated to the HTTP response. - ---- - -## 2. Error message template with unpopulated parameters - -**Problem:** `UserErrorLogRetentionNotInValidRangeInPolicy` returns: -> "Log retention not present in allowed range (minRange **NO_PARAM** and maxRange **NO_PARAM**)" - -The `NO_PARAM` placeholders are never filled with the actual min/max values. - -**Ask:** Populate the `minRange` and `maxRange` template parameters (e.g., "minRange 7 days and maxRange 35 days") so callers know the valid range without trial-and-error. - ---- - -## 3. SQL Differential: undocumented single-day constraint - -**Problem:** SQL `Differential` sub-policy only supports exactly 1 day per week. Passing `["Wednesday","Friday"]` in `scheduleRunDays` gets rejected with the generic `BMSUserErrorInvalidPolicyInput`. No documentation mentions this constraint. - -**Ask:** -- Swagger: Add `maxItems: 1` on `Differential.schedulePolicy.scheduleRunDays` for `AzureWorkload` policies. -- Service: Return a specific error like `UserErrorDifferentialScheduleMultipleDaysNotSupported`. - ---- - -## 4. Weekly schedule: retention days-of-week must match schedule — not documented - -**Problem:** When `scheduleRunFrequency=Weekly` with `scheduleRunDays=["Monday"]`, the `WeeklyRetentionSchedule.daysOfTheWeek` MUST equal `["Monday"]`. If you set `["Sunday"]` (a reasonable default), the service rejects with the generic error. This constraint is enforced in `WeeklyRetentionSchedule.ValidateWithBackupScheduleDOW` but not documented in swagger or SDK. - -**Ask:** -- Swagger: Add a `description` on `weeklySchedule.daysOfTheWeek`: "Must match scheduleRunDays when scheduleRunFrequency is Weekly." -- SDK: Add a `[ValidationAttribute]` or XML doc on `WeeklyRetentionSchedule.DaysOfTheWeek` noting the constraint. - ---- - -## 5. Monthly/Yearly retention format: Weekly schedule requires relative format - -**Problem:** When `scheduleRunFrequency=Weekly`, monthly and yearly retention MUST use `RetentionScheduleFormatType=Weekly` (relative: week-of-month + days-of-week). Using `Daily` format (absolute: days-of-month) is silently rejected with the generic error. No documentation mentions this constraint. - -**Ask:** -- Service: Return `UserErrorRetentionFormatInvalidForWeeklySchedule` with message: "Monthly/Yearly retention must use Weekly format (week-of-month + days-of-week) when backup schedule is Weekly." -- Swagger: Add constraint description on `retentionScheduleFormatType`. - ---- - -## 6. DPP: No client-accessible manifest for per-datasource constraints - -**Problem:** Each DPP datasource has a manifest defining `allowedRetentionTagNames`, `allowedFirstTargetStores`, `storeConstraints` (which stores are supported), and `allowedScheduledTriggerFrequencies`. But this manifest is only available server-side. Callers have no way to know that: -- AzureDisk doesn't support `Yearly` retention or `ArchiveStore` -- No DPP datasource supports `ArchiveStore` today -- AzureDisk requires `OperationalStore` as first target - -We had to hard-code all of this in our validator. - -**Ask:** Expose a `GET /datasources/{type}/manifest` (or include it in `get-default-policy-template`) so clients can validate constraints programmatically instead of hard-coding. - ---- - -## 7. SQL Log retention: minimum not discoverable - -**Problem:** SQL Log retention has a minimum of 7 days, but this is only discoverable by getting `UserErrorLogRetentionNotInValidRangeInPolicy` (with `NO_PARAM` values). No swagger annotation, SDK doc, or API introspection endpoint exposes this constraint. - -**Ask:** Add `minimum: 7` and `maximum: 35` (or whatever the actual bounds are) to the swagger definition for `Log.retentionPolicy.retentionDuration.count` when `durationType=Days`. - ---- - -## 8. VM policy-sub-type `Enhanced` required for archive — not documented - -**Problem:** VM policies with `TieringPolicy` (archive tier) require `policySubType=Enhanced` (V2). Without it, the policy is created as Standard V1 which doesn't support tiering. No error is returned — the tiering is silently ignored. - -**Ask:** Service: reject `TieringPolicy` on Standard (V1) policies with a specific error, or auto-promote to V2 Enhanced when tiering is requested. - ---- - -## Summary of client-side rules we built to compensate - -| Rule | Service error it prevents | -|---|---| -| SQL Diff must be exactly 1 day | `BMSUserErrorInvalidPolicyInput` (generic) | -| SQL Log retention ≥ 7 days | `UserErrorLogRetentionNotInValidRangeInPolicy` (NO_PARAM) | -| SQL Log < Diff retention | `UserErrorLogRetentionMoreThanDiffRetentionInPolicy` | -| Weekly retention DOW must match schedule | `BMSUserErrorInvalidPolicyInput` (generic) | -| DPP: reject archive for all datasources | `BMSUserErrorInvalidInput` (generic) | -| AzureDisk: reject Yearly retention | `BMSUserErrorInvalidInput` (generic) | -| Full/Diff no day overlap | `BMSUserErrorInvalidPolicyInput` (generic) | -| Weekly schedule requires --schedule-days-of-week | SDK null-ref or generic error | -| Continuous DPP rejects schedule/retention flags | `BMSUserErrorInvalidInput` (generic) | - -Every one of these could be a service-side improvement that benefits all callers (Az CLI, PowerShell, Portal, MCP, third-party tools), not just us. - -Happy to share request IDs, Kusto queries, and the full JSON payloads for any of these. The PR with all the workarounds is at [microsoft/mcp#2504](https://github.com/microsoft/mcp/pull/2504). - -Thanks! diff --git a/fix-disk-test.ps1 b/fix-disk-test.ps1 deleted file mode 100644 index 9c47f9ce14..0000000000 --- a/fix-disk-test.ps1 +++ /dev/null @@ -1,63 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' [Fact(Skip = "Deferred: AzureDisk operational tier does not accept multi-tier (weekly/monthly/yearly) retention rules. Sourcing the multi-tier rules from VaultStore (when --enable-vault-tier-copy is set) still produces BMSUserErrorInvalidInput from the DPP API; additional shape work is required for the per-tier taggingCriteria/policyRules combination on AzureDisk vault tier. Tracked as a follow-up builder enhancement.")] - public async Task PolicyCreate_DppDisk_VaultTierMultiTierArchive_E2E() - { - var vaultName = $"{Settings.ResourceBaseName}-dpp"; - var policyName = RegisterOrRetrieveVariable("createdDppDiskMultiTierPolicyName", $"test-disk-mt-{Random.Shared.NextInt64()}"); - - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "AzureDisk" }, - { "schedule-times", "02:00" }, - { "daily-retention-days", "7" }, - { "weekly-retention-weeks", "4" }, - { "weekly-retention-days-of-week", "Sunday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-days-of-month", "1" }, - { "yearly-retention-years", "5" }, - { "yearly-retention-months", "January" }, - { "yearly-retention-week-of-month", "First" }, - { "yearly-retention-days-of-week", "Sunday" }, - { "archive-tier-mode", "TierAfter" }, - { "archive-tier-after-days", "180" } - });' - -$new = ' [Fact] - public async Task PolicyCreate_DppDisk_VaultTierMultiTier_E2E() - { - // AzureDisk with vault-tier copy + Weekly/Monthly retention (no Yearly, no Archive per manifest). - var vaultName = $"{Settings.ResourceBaseName}-dpp"; - var policyName = RegisterOrRetrieveVariable("createdDppDiskMultiTierPolicyName", $"test-disk-mt-{Random.Shared.NextInt64()}"); - - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "AzureDisk" }, - { "schedule-times", "02:00" }, - { "daily-retention-days", "7" }, - { "enable-vault-tier-copy", "true" }, - { "vault-tier-copy-after-days", "7" }, - { "weekly-retention-weeks", "12" }, - { "monthly-retention-months", "12" } - });' - -if ($content.Contains('VaultTierMultiTierArchive_E2E')) { - $content = $content.Replace($old, $new) - [System.IO.File]::WriteAllText($file, $content) - Write-Host "Replaced successfully" -} else { - Write-Host "ERROR: Pattern not found" -} diff --git a/fix-disk-test2.ps1 b/fix-disk-test2.ps1 deleted file mode 100644 index e8798225f3..0000000000 --- a/fix-disk-test2.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' { "weekly-retention-weeks", "12" }, - { "monthly-retention-months", "12" } - }); - - var opResult = result.AssertProperty("result"); - Assert.Equal("Succeeded", opResult.AssertProperty("status").GetString()); - } - - // CosmosDB policy create test skipped' - -$new = ' { "weekly-retention-weeks", "12" }, - { "weekly-retention-days-of-week", "Sunday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-days-of-month", "1" } - }); - - var opResult = result.AssertProperty("result"); - Assert.Equal("Succeeded", opResult.AssertProperty("status").GetString()); - } - - // CosmosDB policy create test skipped' - -if ($content.Contains($old)) { - $content = $content.Replace($old, $new) - [System.IO.File]::WriteAllText($file, $content) - Write-Host "Replaced successfully" -} else { - Write-Host "ERROR: Pattern not found" -} diff --git a/fix-sql-arch.ps1 b/fix-sql-arch.ps1 deleted file mode 100644 index 845e17ad17..0000000000 --- a/fix-sql-arch.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' [Fact(Skip = "Deferred: SQL workload + Weekly Full + LongTerm retention + TieringPolicy still rejected by RSV API with BMSUserErrorInvalidPolicyInput after dropping the redundant DailySchedule alongside WeeklySchedule. The SQL sub-policy shape for archive tier copy needs further reverse-engineering (likely affects the Full sub-policy''s RetentionPolicy/TieringPolicy combination). Other SQL policy shapes succeed against the same vault. Tracked as a follow-up.")] - public async Task PolicyCreate_RsvSql_WithArchiveTier_E2E() - { - var vaultName = $"{Settings.ResourceBaseName}-rsv"; - var policyName = RegisterOrRetrieveVariable("createdSqlArchivePolicyName", $"test-sql-archive-{Random.Shared.NextInt64()}"); - - // SQL Full Weekly + monthly retention + archive on Full sub-policy. Drop daily retention - // (not valid alongside Weekly schedule per RSV shape rules). - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "MSSQL" }, - { "full-schedule-frequency", "Weekly" }, - { "full-schedule-days-of-week", "Sunday" }, - { "schedule-times", "02:00" }, - { "weekly-retention-weeks", "4" }, - { "weekly-retention-days-of-week", "Sunday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-days-of-month", "1" }, - { "archive-tier-mode", "TierAfter" }, - { "archive-tier-after-days", "90" }, - { "log-frequency-minutes", "60" } - });' - -$new = ' [Fact] - public async Task PolicyCreate_RsvSql_WithArchiveTier_E2E() - { - var vaultName = $"{Settings.ResourceBaseName}-rsv"; - var policyName = RegisterOrRetrieveVariable("createdSqlArchivePolicyName", $"test-sql-archive-{Random.Shared.NextInt64()}"); - - // SQL Full Weekly + weekly/monthly retention + archive on Full sub-policy + Log. - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "SQL" }, - { "full-schedule-frequency", "Weekly" }, - { "full-schedule-days-of-week", "Sunday" }, - { "schedule-times", "02:00" }, - { "weekly-retention-weeks", "4" }, - { "weekly-retention-days-of-week", "Sunday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-week-of-month", "First" }, - { "monthly-retention-days-of-week", "Sunday" }, - { "archive-tier-mode", "TierAfter" }, - { "archive-tier-after-days", "90" }, - { "log-frequency-minutes", "60" }, - { "log-retention-days", "7" } - });' - -if ($content.Contains($old)) { - $content = $content.Replace($old, $new) - [System.IO.File]::WriteAllText($file, $content) - Write-Host "Replaced successfully" -} else { - Write-Host "ERROR: Pattern not found" -} diff --git a/fix-sql-diff7.ps1 b/fix-sql-diff7.ps1 deleted file mode 100644 index e050418aba..0000000000 --- a/fix-sql-diff7.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' { "differential-retention-days", "30" },' -$new = ' { "differential-retention-days", "7" },' - -$idx = $content.IndexOf('PolicyCreate_RsvSql_FullLogDiff_E2E') -if ($idx -gt 0) { - $targetIdx = $content.IndexOf($old, $idx) - if ($targetIdx -gt 0) { - $content = $content.Substring(0, $targetIdx) + $new + $content.Substring($targetIdx + $old.Length) - [System.IO.File]::WriteAllText($file, $content) - Write-Host "Replaced diff retention 30->7 at position $targetIdx" - } else { - Write-Host "ERROR: diff retention pattern not found after FullLogDiff" - } -} else { - Write-Host "ERROR: FullLogDiff test not found" -} diff --git a/fix-sql-fld.ps1 b/fix-sql-fld.ps1 deleted file mode 100644 index 4209ae886b..0000000000 --- a/fix-sql-fld.ps1 +++ /dev/null @@ -1,62 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' [Fact(Skip = "Deferred: SQL Full+Differential+Log policy with non-overlapping Full/Diff days (Full Weekly Sunday, Diff Wed,Fri) and log5" - } else { Write-Host "ERROR: log retention not found" } -} else { Write-Host "ERROR: test not found" } diff --git a/fix-sql-ret.ps1 b/fix-sql-ret.ps1 deleted file mode 100644 index 720407553a..0000000000 --- a/fix-sql-ret.ps1 +++ /dev/null @@ -1,23 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$idx = $content.IndexOf('PolicyCreate_RsvSql_FullLogDiff_E2E') - -# Fix diff retention: 7 -> 15 -$old1 = ' { "differential-retention-days", "7" },' -$t1 = $content.IndexOf($old1, $idx) -if ($t1 -gt 0) { - $content = $content.Substring(0, $t1) + ' { "differential-retention-days", "15" },' + $content.Substring($t1 + $old1.Length) - Write-Host "Fixed diff retention 7->15" -} - -# Fix log retention: 5 -> 7 -$old2 = ' { "log-retention-days", "5" }' -$t2 = $content.IndexOf($old2, $idx) -if ($t2 -gt 0) { - $content = $content.Substring(0, $t2) + ' { "log-retention-days", "7" }' + $content.Substring($t2 + $old2.Length) - Write-Host "Fixed log retention 5->7" -} - -[System.IO.File]::WriteAllText($file, $content) -Write-Host "Done" diff --git a/fix-vm-arch.ps1 b/fix-vm-arch.ps1 deleted file mode 100644 index 1c55c81693..0000000000 --- a/fix-vm-arch.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -$file = 'tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.LiveTests/AzureBackupCommandTests.cs' -$content = [System.IO.File]::ReadAllText($file) - -$old = ' [Fact(Skip = "Deferred: VM Enhanced V2 + Weekly schedule + multi-tier (weekly/monthly/yearly) retention + archive tier shape still rejected by RSV API with BMSUserErrorInvalidPolicyInput after dropping the redundant DailySchedule alongside WeeklySchedule. The remaining shape work (likely involving WeeklyRetentionFormat days alignment or TieringPolicy placement on the SubProtectionPolicy) requires further reverse-engineering. Other VM policy shapes succeed against the same vault. Tracked as a follow-up.")] - public async Task PolicyCreate_RsvVm_WeeklyMultiTierWithArchive_E2E() - { - var vaultName = $"{Settings.ResourceBaseName}-rsv"; - var policyName = RegisterOrRetrieveVariable("createdRsvVmWeeklyArchivePolicyName", $"test-vm-weekly-arch-{Random.Shared.NextInt64()}"); - - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "AzureVM" }, - { "schedule-frequency", "Weekly" }, - { "schedule-days-of-week", "Monday" }, - { "schedule-times", "03:00" }, - { "weekly-retention-weeks", "8" }, - { "weekly-retention-days-of-week", "Monday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-days-of-month", "1" }, - { "yearly-retention-years", "5" }, - { "yearly-retention-months", "January" }, - { "yearly-retention-week-of-month", "First" }, - { "yearly-retention-days-of-week", "Sunday" }, - { "archive-tier-mode", "TierAfter" }, - { "archive-tier-after-days", "90" } - });' - -$new = ' [Fact] - public async Task PolicyCreate_RsvVm_WeeklyMultiTierWithArchive_E2E() - { - var vaultName = $"{Settings.ResourceBaseName}-rsv"; - var policyName = RegisterOrRetrieveVariable("createdRsvVmWeeklyArchivePolicyName", $"test-vm-weekly-arch-{Random.Shared.NextInt64()}"); - - // VM Enhanced V2 + Weekly + multi-tier + archive. All retention days-of-week must match schedule. - // Monthly/Yearly use relative format (week-of-month + days-of-week) for Weekly schedule. - var result = await CallToolAsync( - "azurebackup_policy_create", - new() - { - { "subscription", Settings.SubscriptionId }, - { "resource-group", Settings.ResourceGroupName }, - { "vault", vaultName }, - { "policy", policyName }, - { "workload-type", "AzureVM" }, - { "policy-sub-type", "Enhanced" }, - { "schedule-frequency", "Weekly" }, - { "schedule-days-of-week", "Monday" }, - { "schedule-times", "03:00" }, - { "weekly-retention-weeks", "8" }, - { "weekly-retention-days-of-week", "Monday" }, - { "monthly-retention-months", "12" }, - { "monthly-retention-week-of-month", "First" }, - { "monthly-retention-days-of-week", "Monday" }, - { "yearly-retention-years", "5" }, - { "yearly-retention-months", "January" }, - { "yearly-retention-week-of-month", "First" }, - { "yearly-retention-days-of-week", "Monday" }, - { "archive-tier-mode", "TierAfter" }, - { "archive-tier-after-days", "90" } - });' - -if ($content.Contains($old)) { - $content = $content.Replace($old, $new) - [System.IO.File]::WriteAllText($file, $content) - Write-Host "Replaced successfully" -} else { - Write-Host "ERROR: Pattern not found" -} diff --git a/resolve-threads.ps1 b/resolve-threads.ps1 deleted file mode 100644 index e3adbadd72..0000000000 --- a/resolve-threads.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$query = '{ repository(owner:"microsoft", name:"mcp") { pullRequest(number:2504) { reviewThreads(first:20) { nodes { id isResolved } } } } }' -$result = gh api graphql -f query=$query --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .id' 2>&1 -$threadIds = $result -split "`n" | Where-Object { $_ -match '^PRRT_' } -Write-Host "Found $($threadIds.Count) unresolved threads" -foreach ($tid in $threadIds) { - gh api graphql -f query="mutation { resolveReviewThread(input:{threadId:`"$tid`"}) { thread { isResolved } } }" --silent 2>&1 | Out-Null - Write-Host "Resolved $tid" -} -Write-Host "Done" diff --git a/tmp-assets-main.json b/tmp-assets-main.json deleted file mode 100644 index e67249df0d..0000000000 --- a/tmp-assets-main.json +++ /dev/null @@ -1 +0,0 @@ -{"AssetsRepo":"Azure/azure-sdk-assets","AssetsRepoPrefixPath":"","TagPrefix":"Azure.Mcp.Tools.AzureBackup.LiveTests","Tag":"Azure.Mcp.Tools.AzureBackup.LiveTests_c70ee092ee"} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.AzureBackup/docs/telemetry-queries.md b/tools/Azure.Mcp.Tools.AzureBackup/docs/telemetry-queries.md deleted file mode 100644 index 8ba81c7472..0000000000 --- a/tools/Azure.Mcp.Tools.AzureBackup/docs/telemetry-queries.md +++ /dev/null @@ -1,369 +0,0 @@ -# Azure Backup MCP Telemetry Queries - -> All queries use the `getAzureMcpEvents_ToolCalls` function from the Azure MCP telemetry cluster. -> Custom dimensions reference `customDimensions.*` fields emitted by OpenTelemetry. - ---- - -## 1. Production Data Analysis - -### 1.1 Error Summary by Tool and Exception Type (your original query, refined) - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| where success == false -| extend - ExceptionType = tostring(customDimensions["exception.type"]), - ExceptionMessage = tostring(customDimensions["exception.message"]), - DevDeviceId = tostring(customDimensions.devdeviceid) -| summarize - ErrorCount = count(), - DistinctUsers = dcount(DevDeviceId), - LastSeen = max(timestamp) - by ToolName, ExceptionType, ExceptionMessage -| order by ErrorCount desc -``` - -### 1.2 Success vs Failure Rate per Tool - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| summarize - Total = count(), - Succeeded = countif(success == true), - Failed = countif(success == false) - by ToolName -| extend FailureRate = round(100.0 * Failed / Total, 2) -| order by Total desc -``` - -### 1.3 Top Exception Stack Traces (for debugging) - -```kql -getAzureMcpEvents_ToolCalls(ago(7d), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| where success == false -| extend - ExceptionType = tostring(customDimensions["exception.type"]), - ExceptionMessage = tostring(customDimensions["exception.message"]), - ExceptionStackTrace = tostring(customDimensions["exception.stacktrace"]) -| summarize Count = count() by ToolName, ExceptionType, ExceptionMessage, ExceptionStackTrace -| top 20 by Count desc -``` - -### 1.4 Daily Error Trend (spot regressions) - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| summarize - Total = count(), - Errors = countif(success == false) - by bin(timestamp, 1d), ToolName -| extend FailureRate = round(100.0 * Errors / Total, 2) -| order by timestamp desc, ToolName asc -``` - -### 1.5 Duration Percentiles (P50/P95/P99) per Tool - -```kql -getAzureMcpEvents_ToolCalls(ago(7d), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| where success == true -| summarize - P50_ms = percentile(duration, 50), - P95_ms = percentile(duration, 95), - P99_ms = percentile(duration, 99), - Calls = count() - by ToolName -| order by P95_ms desc -``` - -### 1.6 Unique Users per Tool (adoption tracking) - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - DevDeviceId = tostring(customDimensions.devdeviceid) -| where ToolName contains "azurebackup" -| summarize - TotalCalls = count(), - DistinctUsers = dcount(DevDeviceId) - by ToolName -| order by TotalCalls desc -``` - -### 1.7 Client Distribution (which editors/hosts are calling us) - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - ClientName = tostring(customDimensions.ClientName), - ClientVersion = tostring(customDimensions.ClientVersion), - ServerMode = tostring(customDimensions.ServerMode), - Transport = tostring(customDimensions.Transport) -| where ToolName contains "azurebackup" -| summarize Calls = count(), Users = dcount(tostring(customDimensions.devdeviceid)) - by ClientName, ClientVersion, ServerMode, Transport -| order by Calls desc -``` - ---- - -## 2. Weekly Report Queries - -### 2.1 Weekly Executive Summary - -```kql -// Paste this as the single-query weekly snapshot -let WeekStart = startofweek(ago(7d)); -let WeekEnd = startofweek(now()); -getAzureMcpEvents_ToolCalls(WeekStart, WeekEnd) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| summarize - TotalCalls = count(), - Succeeded = countif(success == true), - Failed = countif(success == false), - DistinctUsers = dcount(tostring(customDimensions.devdeviceid)), - P50_ms = percentile(duration, 50), - P95_ms = percentile(duration, 95) - by ToolName -| extend FailureRate = round(100.0 * Failed / TotalCalls, 2) -| order by TotalCalls desc -``` - -### 2.2 Week-over-Week Comparison - -```kql -let ThisWeek = startofweek(ago(0d)); -let LastWeek = startofweek(ago(7d)); -let TwoWeeksAgo = startofweek(ago(14d)); -getAzureMcpEvents_ToolCalls(TwoWeeksAgo, ThisWeek) -| extend - ToolName = tostring(customDimensions.toolname), - Week = iff(timestamp >= LastWeek, "ThisWeek", "LastWeek") -| where ToolName contains "azurebackup" -| summarize - Calls = count(), - Errors = countif(success == false), - Users = dcount(tostring(customDimensions.devdeviceid)) - by Week, ToolName -| order by ToolName asc, Week desc -``` - -### 2.3 New Errors This Week (not seen last week) - -```kql -let ThisWeekStart = startofweek(ago(0d)); -let LastWeekStart = startofweek(ago(7d)); -let thisWeekErrors = - getAzureMcpEvents_ToolCalls(LastWeekStart, ThisWeekStart) - | extend ToolName = tostring(customDimensions.toolname) - | where ToolName contains "azurebackup" and success == false - | extend ExKey = strcat(tostring(customDimensions["exception.type"]), "|", tostring(customDimensions["exception.message"])) - | where timestamp >= ThisWeekStart - | distinct ExKey; -let lastWeekErrors = - getAzureMcpEvents_ToolCalls(LastWeekStart, ThisWeekStart) - | extend ToolName = tostring(customDimensions.toolname) - | where ToolName contains "azurebackup" and success == false - | extend ExKey = strcat(tostring(customDimensions["exception.type"]), "|", tostring(customDimensions["exception.message"])) - | where timestamp < ThisWeekStart - | distinct ExKey; -thisWeekErrors -| join kind=leftanti lastWeekErrors on ExKey -``` - -### 2.4 Slowest Operations This Week - -```kql -let WeekStart = startofweek(ago(7d)); -getAzureMcpEvents_ToolCalls(WeekStart, now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| where success == true -| top 25 by duration desc -| project - timestamp, - ToolName, - Duration_sec = round(duration / 1000.0, 2), - ClientName = tostring(customDimensions.ClientName), - DevDeviceId = tostring(customDimensions.devdeviceid) -``` - -### 2.5 Error Hotspots by HTTP Status Code - -```kql -let WeekStart = startofweek(ago(7d)); -getAzureMcpEvents_ToolCalls(WeekStart, now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| where success == false -| extend - ExceptionMessage = tostring(customDimensions["exception.message"]), - ExceptionType = tostring(customDimensions["exception.type"]) -| extend StatusCode = extract(@"""StatusCode""\s*:\s*(\d+)", 1, ExceptionMessage) -| summarize ErrorCount = count() by ToolName, StatusCode, ExceptionType -| order by ErrorCount desc -``` - ---- - -## 3. Custom Dimension Queries (post-telemetry-instrumentation) - -> These queries will produce results **after** the telemetry tags PR is merged and deployed. - -### 3.1 Usage by Vault Type (RSV vs DPP) - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - VaultType = tostring(customDimensions["azurebackup/VaultType"]) -| where ToolName contains "azurebackup" -| summarize Calls = count(), Errors = countif(success == false) - by ToolName, VaultType -| order by Calls desc -``` - -### 3.2 Failure Rate by Vault Type - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - VaultType = tostring(customDimensions["azurebackup/VaultType"]) -| where ToolName contains "azurebackup" -| where isnotempty(VaultType) -| summarize - Total = count(), - Failed = countif(success == false) - by VaultType -| extend FailureRate = round(100.0 * Failed / Total, 2) -``` - -### 3.3 Policy Create by Workload Type - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - WorkloadType = tostring(customDimensions["azurebackup/WorkloadType"]) -| where ToolName == "azmcp_azurebackup_policy_create" -| summarize - Calls = count(), - Errors = countif(success == false), - Users = dcount(tostring(customDimensions.devdeviceid)) - by WorkloadType -| order by Calls desc -``` - -### 3.4 Single vs List Operation Distribution - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - OperationScope = tostring(customDimensions["azurebackup/OperationScope"]) -| where ToolName contains "azurebackup" -| where isnotempty(OperationScope) -| summarize Calls = count(), P50_ms = percentile(duration, 50) - by ToolName, OperationScope -| order by ToolName asc, OperationScope asc -``` - -### 3.5 Protect Command by Datasource Type - -```kql -getAzureMcpEvents_ToolCalls(ago(30d), now()) -| extend - ToolName = tostring(customDimensions.toolname), - DatasourceType = tostring(customDimensions["azurebackup/DatasourceType"]) -| where ToolName == "azmcp_azurebackup_protecteditem_protect" -| summarize - Calls = count(), - Errors = countif(success == false), - P95_ms = percentile(duration, 95) - by DatasourceType -| order by Calls desc -``` - ---- - -## 4. Alerting / SLA Queries - -### 4.1 High Failure Rate Alert (>20% in last 4h) - -```kql -getAzureMcpEvents_ToolCalls(ago(4h), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| summarize Total = count(), Failed = countif(success == false) by ToolName -| where Total >= 5 // minimum sample size -| extend FailureRate = round(100.0 * Failed / Total, 2) -| where FailureRate > 20 -``` - -### 4.2 P95 Latency Breach (>30s) - -```kql -getAzureMcpEvents_ToolCalls(ago(1h), now()) -| extend ToolName = tostring(customDimensions.toolname) -| where ToolName contains "azurebackup" -| summarize P95_ms = percentile(duration, 95), Calls = count() by ToolName -| where Calls >= 3 and P95_ms > 30000 -``` - ---- - -## 5. Composite Weekly Report Query - -> Run this single query to produce a full weekly report table. - -```kql -let WeekStart = startofweek(ago(7d)); -let WeekEnd = now(); -getAzureMcpEvents_ToolCalls(WeekStart, WeekEnd) -| extend - ToolName = tostring(customDimensions.toolname), - DevDeviceId = tostring(customDimensions.devdeviceid), - VaultType = tostring(customDimensions["azurebackup/VaultType"]), - WorkloadType = tostring(customDimensions["azurebackup/WorkloadType"]), - OperationScope = tostring(customDimensions["azurebackup/OperationScope"]) -| where ToolName contains "azurebackup" -| summarize - TotalCalls = count(), - Succeeded = countif(success == true), - Failed = countif(success == false), - DistinctUsers = dcount(DevDeviceId), - P50_ms = round(percentile(duration, 50), 0), - P95_ms = round(percentile(duration, 95), 0), - P99_ms = round(percentile(duration, 99), 0), - TopVaultType = take_any(VaultType), - TopWorkload = take_any(WorkloadType) - by ToolName -| extend - FailureRate = round(100.0 * Failed / TotalCalls, 2), - SuccessRate = round(100.0 * Succeeded / TotalCalls, 2) -| project - ToolName, - TotalCalls, - SuccessRate, - FailureRate, - DistinctUsers, - P50_ms, - P95_ms, - P99_ms, - Failed -| order by TotalCalls desc -``` diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs index e44e6c9bff..3fbe029fd4 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Commands/Vault/VaultCreateCommand.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore azurebackup - using System.Net; using Azure.Mcp.Tools.AzureBackup.Models; using Azure.Mcp.Tools.AzureBackup.Options; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs index 0818fd7465..544fdd7cf9 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/ProtectResult.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore azurebackup protecteditem - namespace Azure.Mcp.Tools.AzureBackup.Models; /// diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs index 47f854864e..e6264315fb 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore USERASSIGNED SYSTEMASSIGNEDUSERASSIGNED SYSTEMASSIGNED Lifecycles azurebackup protecteditem protectable - using Azure.Core; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Tenant; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs index 4c6f1736b7..a41558c8d9 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore SQLINSTANCE SAPHANADATABASE SQLDATABASE protectable hana iaasvmcontainerv azurebackup protectableitem dbname Fileshare protecteditem vmname ALWAYSON SYSTEMASSIGNED USERASSIGNED SYSTEMASSIGNEDUSERASSIGNED SAPHANA SAPHANASYSTEM SAPHANADBINSTANCE SAPHANADBI - using Azure.Core; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Tenant; 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 index c21f43024a..0ab42e8fd2 100644 --- 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 @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore protectable Protectable protectableitem recoverypoint disasterrecovery esan adls Adls ADLS protecteditem azurebackup Hana hana SAPHANA pgflex - using System.Text.Json; using Microsoft.Mcp.Tests; using Microsoft.Mcp.Tests.Client; 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 index 11728f6685..d1d57fce6a 100644 --- 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 @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// cspell:ignore mydisk myvm slowvm afsfileshare iaasvmcontainerv azurebackup protecteditem - using System.Net; using Azure.Mcp.Tools.AzureBackup.Commands; using Azure.Mcp.Tools.AzureBackup.Commands.ProtectedItem; diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep index ba2916d9db..8e6cefaea4 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep +++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/test-resources.bicep @@ -1,4 +1,3 @@ -// cspell:ignore backupconfig vaultconfig targetScope = 'resourceGroup' @minLength(3)