diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/AzureBackupTelemetryTags.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/AzureBackupTelemetryTags.cs
index 1154b3ac98..ad39bd4ff2 100644
--- a/tools/Azure.Mcp.Tools.AzureBackup/src/Models/AzureBackupTelemetryTags.cs
+++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Models/AzureBackupTelemetryTags.cs
@@ -16,17 +16,17 @@ public static class AzureBackupTelemetryTags
///
/// Normalizes the vault type to canonical lowercase values (rsv/dpp).
- /// Returns null when the input is null or empty.
+ /// Returns "auto" when the input is null or empty (user didn't specify --vault-type).
///
- public static string? NormalizeVaultType(string? vaultType) =>
- string.IsNullOrWhiteSpace(vaultType) ? null : vaultType.ToLowerInvariant();
+ public static string NormalizeVaultType(string? vaultType) =>
+ string.IsNullOrWhiteSpace(vaultType) ? "auto" : vaultType.ToLowerInvariant();
///
/// Normalizes the workload type to canonical lowercase for consistent telemetry.
- /// Returns null when the input is null or empty.
+ /// Returns "unspecified" when the input is null or empty.
///
- public static string? NormalizeWorkloadType(string? workloadType) =>
- string.IsNullOrWhiteSpace(workloadType) ? null : workloadType.ToLowerInvariant();
+ public static string NormalizeWorkloadType(string? workloadType) =>
+ string.IsNullOrWhiteSpace(workloadType) ? "unspecified" : workloadType.ToLowerInvariant();
///
/// Adds a normalized vault type tag to the activity.
diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs
index 4ec23711e2..817ed7b18d 100644
--- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs
+++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs
@@ -422,14 +422,21 @@ public async Task> ListProtectableItemsAsync(
/// Backup Status API. Returns null for DPP-only resource types (disks, blobs, etc.)
/// that are not supported by the RSV BackupStatus API.
///
- private static BackupDataSourceType? MapArmResourceTypeToBackupDataSourceType(string armResourceType) =>
- armResourceType switch
+ private static BackupDataSourceType? MapArmResourceTypeToBackupDataSourceType(string? armResourceType)
+ {
+ if (string.IsNullOrEmpty(armResourceType))
+ {
+ return null;
+ }
+
+ return armResourceType switch
{
"microsoft.compute/virtualmachines" => BackupDataSourceType.Vm,
"microsoft.storage/storageaccounts" => BackupDataSourceType.AzureFileShare,
"microsoft.sql/servers/databases" => BackupDataSourceType.SqlDatabase,
_ => null // DPP-only types handled via DPP vault lookup
};
+ }
public async Task> FindUnprotectedResourcesAsync(
string subscription, string? resourceTypeFilter, string? resourceGroup,
@@ -777,6 +784,6 @@ private static string[] ValidateAndParseResourceTypeFilter(string resourceTypeFi
return types;
}
- [GeneratedRegex(@"^[A-Za-z0-9]+\.[A-Za-z0-9]+/[A-Za-z0-9]+$")]
+ [GeneratedRegex(@"^[A-Za-z0-9]+\.[A-Za-z0-9]+(/[A-Za-z0-9]+)+$")]
private static partial Regex ArmResourceTypeRegex();
}
diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs
index 5c6197daa4..9adb680dab 100644
--- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs
+++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/DppBackupOperations.cs
@@ -496,9 +496,32 @@ public async Task> ListJobsAsync(
var collection = vaultResource.GetDataProtectionBackupJobs();
var jobs = new List();
- await foreach (var job in collection.GetAllAsync(cancellationToken))
+ var enumerator = collection.GetAllAsync(cancellationToken).GetAsyncEnumerator(cancellationToken);
+ try
+ {
+ while (true)
+ {
+ try
+ {
+ if (!await enumerator.MoveNextAsync())
+ {
+ break;
+ }
+
+ jobs.Add(MapToJobInfo(enumerator.Current.Data));
+ }
+ catch (FormatException)
+ {
+ // The Azure SDK may throw FormatException when deserializing jobs with
+ // non-standard ISO 8601 duration fields (XmlConvert.ToTimeSpan limitation).
+ // Return the jobs collected so far rather than failing the entire list.
+ break;
+ }
+ }
+ }
+ finally
{
- jobs.Add(MapToJobInfo(job.Data));
+ await enumerator.DisposeAsync();
}
return jobs;
diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs
index d849f1e9c1..af72337c61 100644
--- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs
+++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/RsvBackupOperations.cs
@@ -20,16 +20,6 @@ public sealed class RsvBackupOperations(ITenantService tenantService) : BaseAzur
private const string VaultType = VaultTypeResolver.Rsv;
private const string FabricName = "Azure";
- private static void ValidateSubscriptionFormat(string subscription)
- {
- if (!Guid.TryParse(subscription, out _))
- {
- throw new ArgumentException(
- $"Invalid subscription ID '{subscription}'. Expected a GUID (e.g., 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'). " +
- "If you provided a subscription name, use the subscription ID instead.");
- }
- }
-
public async Task CreateVaultAsync(
string vaultName, string resourceGroup, string subscription, string location,
string? sku, string? storageType, string? tenant,
@@ -40,7 +30,6 @@ public async Task CreateVaultAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(location), location));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup);
@@ -75,7 +64,6 @@ public async Task GetVaultAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -90,7 +78,6 @@ public async Task> ListVaultsAsync(
RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken)
{
ValidateRequiredParameters((nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var subId = SubscriptionResource.CreateResourceIdentifier(subscription);
@@ -118,7 +105,6 @@ public async Task ProtectItemAsync(
(nameof(subscription), subscription),
(nameof(datasourceId), datasourceId),
(nameof(policyName), policyName));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
@@ -256,7 +242,6 @@ public async Task GetProtectedItemAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
@@ -321,7 +306,6 @@ public async Task> ListProtectedItemsAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup);
@@ -346,7 +330,6 @@ public async Task GetPolicyAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(policyName), policyName));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier(
@@ -365,7 +348,6 @@ public async Task> ListPoliciesAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup);
@@ -390,7 +372,6 @@ public async Task GetJobAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(jobId), jobId));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var jobResourceId = BackupJobResource.CreateResourceIdentifier(
@@ -409,7 +390,6 @@ public async Task> ListJobsAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var rgId = ResourceGroupResource.CreateResourceIdentifier(subscription, resourceGroup);
@@ -435,7 +415,6 @@ public async Task GetRecoveryPointAsync(
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName),
(nameof(recoveryPointId), recoveryPointId));
- ValidateSubscriptionFormat(subscription);
if (string.IsNullOrEmpty(containerName))
{
@@ -465,7 +444,6 @@ public async Task> ListRecoveryPointsAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName));
- ValidateSubscriptionFormat(subscription);
if (string.IsNullOrEmpty(containerName))
{
@@ -528,7 +506,6 @@ public async Task UpdateVaultAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -612,7 +589,6 @@ public async Task CreatePolicyAsync(
(nameof(subscription), subscription),
(nameof(policyName), policyName),
(nameof(workloadType), workloadType));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultResourceId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -899,7 +875,6 @@ public async Task ConfigureImmutabilityAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(immutabilityState), immutabilityState));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -931,7 +906,6 @@ public async Task ConfigureSoftDeleteAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(softDeleteState), softDeleteState));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -980,7 +954,6 @@ public async Task ConfigureCrossRegionRestoreAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var vaultId = RecoveryServicesVaultResource.CreateResourceIdentifier(subscription, resourceGroup, vaultName);
@@ -1339,7 +1312,6 @@ public async Task UndeleteProtectedItemAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(datasourceId), datasourceId));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
@@ -1618,7 +1590,6 @@ public async Task> ListProtectableItemsAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
- ValidateSubscriptionFormat(subscription);
var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
diff --git a/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Models/AzureBackupTelemetryTagsTests.cs b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Models/AzureBackupTelemetryTagsTests.cs
new file mode 100644
index 0000000000..b20eeed992
--- /dev/null
+++ b/tools/Azure.Mcp.Tools.AzureBackup/tests/Azure.Mcp.Tools.AzureBackup.UnitTests/Models/AzureBackupTelemetryTagsTests.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using Azure.Mcp.Tools.AzureBackup.Models;
+using Xunit;
+
+namespace Azure.Mcp.Tools.AzureBackup.UnitTests.Models;
+
+public class AzureBackupTelemetryTagsTests
+{
+ [Theory]
+ [InlineData(null, "auto")]
+ [InlineData("", "auto")]
+ [InlineData(" ", "auto")]
+ [InlineData("RSV", "rsv")]
+ [InlineData("DPP", "dpp")]
+ [InlineData("Rsv", "rsv")]
+ public void NormalizeVaultType_ReturnsExpectedValue(string? input, string expected)
+ {
+ Assert.Equal(expected, AzureBackupTelemetryTags.NormalizeVaultType(input));
+ }
+
+ [Theory]
+ [InlineData(null, "unspecified")]
+ [InlineData("", "unspecified")]
+ [InlineData(" ", "unspecified")]
+ [InlineData("VM", "vm")]
+ [InlineData("SqlDatabase", "sqldatabase")]
+ [InlineData("AzureFileShare", "azurefileshare")]
+ public void NormalizeWorkloadType_ReturnsExpectedValue(string? input, string expected)
+ {
+ Assert.Equal(expected, AzureBackupTelemetryTags.NormalizeWorkloadType(input));
+ }
+
+ [Fact]
+ public void AddVaultTags_NullActivity_DoesNotThrow()
+ {
+ AzureBackupTelemetryTags.AddVaultTags(null, "rsv");
+ }
+
+ [Fact]
+ public void AddVaultTags_NullVaultType_SetsAutoTag()
+ {
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ using var source = new ActivitySource("test");
+ using var activity = source.StartActivity("test-op");
+ Assert.NotNull(activity);
+
+ AzureBackupTelemetryTags.AddVaultTags(activity, null);
+
+ var tag = activity.GetTagItem(AzureBackupTelemetryTags.VaultType);
+ Assert.Equal("auto", tag);
+ }
+
+ [Fact]
+ public void AddVaultTags_WithVaultType_SetsNormalizedTag()
+ {
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ using var source = new ActivitySource("test");
+ using var activity = source.StartActivity("test-op");
+ Assert.NotNull(activity);
+
+ AzureBackupTelemetryTags.AddVaultTags(activity, "RSV");
+
+ var tag = activity.GetTagItem(AzureBackupTelemetryTags.VaultType);
+ Assert.Equal("rsv", tag);
+ }
+
+ [Fact]
+ public void AddVaultAndWorkloadTags_SetsAllTags()
+ {
+ using var listener = new ActivityListener
+ {
+ ShouldListenTo = _ => true,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
+ };
+ ActivitySource.AddActivityListener(listener);
+
+ using var source = new ActivitySource("test");
+ using var activity = source.StartActivity("test-op");
+ Assert.NotNull(activity);
+
+ AzureBackupTelemetryTags.AddVaultAndWorkloadTags(activity, null, null);
+
+ Assert.Equal("auto", activity.GetTagItem(AzureBackupTelemetryTags.VaultType));
+ Assert.Equal("unspecified", activity.GetTagItem(AzureBackupTelemetryTags.WorkloadType));
+ }
+
+ [Fact]
+ public void TagConstants_HaveCorrectPrefix()
+ {
+ Assert.Equal("azurebackup/VaultType", AzureBackupTelemetryTags.VaultType);
+ Assert.Equal("azurebackup/WorkloadType", AzureBackupTelemetryTags.WorkloadType);
+ Assert.Equal("azurebackup/DatasourceType", AzureBackupTelemetryTags.DatasourceType);
+ Assert.Equal("azurebackup/OperationScope", AzureBackupTelemetryTags.OperationScope);
+ }
+}