Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ public static class AzureBackupTelemetryTags

/// <summary>
/// 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).
/// </summary>
public static string? NormalizeVaultType(string? vaultType) =>
string.IsNullOrWhiteSpace(vaultType) ? null : vaultType.ToLowerInvariant();
public static string NormalizeVaultType(string? vaultType) =>
string.IsNullOrWhiteSpace(vaultType) ? "auto" : vaultType.ToLowerInvariant();

/// <summary>
/// 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.
/// </summary>
public static string? NormalizeWorkloadType(string? workloadType) =>
string.IsNullOrWhiteSpace(workloadType) ? null : workloadType.ToLowerInvariant();
public static string NormalizeWorkloadType(string? workloadType) =>
string.IsNullOrWhiteSpace(workloadType) ? "unspecified" : workloadType.ToLowerInvariant();

/// <summary>
/// Adds a normalized vault type tag to the activity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,14 +422,21 @@ public async Task<List<ProtectableItemInfo>> ListProtectableItemsAsync(
/// Backup Status API. Returns null for DPP-only resource types (disks, blobs, etc.)
/// that are not supported by the RSV BackupStatus API.
/// </summary>
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<List<UnprotectedResourceInfo>> FindUnprotectedResourcesAsync(
string subscription, string? resourceTypeFilter, string? resourceGroup,
Expand Down Expand Up @@ -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();
Comment thread
shrja-ms marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,32 @@ public async Task<List<BackupJobInfo>> ListJobsAsync(
var collection = vaultResource.GetDataProtectionBackupJobs();

var jobs = new List<BackupJobInfo>();
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.
Comment thread
shrja-ms marked this conversation as resolved.
break;
}
}
}
finally
{
jobs.Add(MapToJobInfo(job.Data));
await enumerator.DisposeAsync();
}

return jobs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VaultCreateResult> CreateVaultAsync(
string vaultName, string resourceGroup, string subscription, string location,
string? sku, string? storageType, string? tenant,
Expand All @@ -40,7 +30,6 @@ public async Task<VaultCreateResult> 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);
Expand Down Expand Up @@ -75,7 +64,6 @@ public async Task<BackupVaultInfo> 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);
Expand All @@ -90,7 +78,6 @@ public async Task<List<BackupVaultInfo>> ListVaultsAsync(
RetryPolicyOptions? retryPolicy, CancellationToken cancellationToken)
{
ValidateRequiredParameters((nameof(subscription), subscription));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var subId = SubscriptionResource.CreateResourceIdentifier(subscription);
Expand Down Expand Up @@ -118,7 +105,6 @@ public async Task<ProtectResult> ProtectItemAsync(
(nameof(subscription), subscription),
(nameof(datasourceId), datasourceId),
(nameof(policyName), policyName));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);

Expand Down Expand Up @@ -256,7 +242,6 @@ public async Task<ProtectedItemInfo> GetProtectedItemAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);

Expand Down Expand Up @@ -321,7 +306,6 @@ public async Task<List<ProtectedItemInfo>> 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);
Expand All @@ -346,7 +330,6 @@ public async Task<BackupPolicyInfo> GetPolicyAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(policyName), policyName));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var policyId = BackupProtectionPolicyResource.CreateResourceIdentifier(
Expand All @@ -365,7 +348,6 @@ public async Task<List<BackupPolicyInfo>> 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);
Expand All @@ -390,7 +372,6 @@ public async Task<BackupJobInfo> GetJobAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(jobId), jobId));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);
var jobResourceId = BackupJobResource.CreateResourceIdentifier(
Expand All @@ -409,7 +390,6 @@ public async Task<List<BackupJobInfo>> 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);
Expand All @@ -435,7 +415,6 @@ public async Task<RecoveryPointInfo> GetRecoveryPointAsync(
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName),
(nameof(recoveryPointId), recoveryPointId));
ValidateSubscriptionFormat(subscription);

if (string.IsNullOrEmpty(containerName))
{
Expand Down Expand Up @@ -465,7 +444,6 @@ public async Task<List<RecoveryPointInfo>> ListRecoveryPointsAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(protectedItemName), protectedItemName));
ValidateSubscriptionFormat(subscription);

if (string.IsNullOrEmpty(containerName))
{
Expand Down Expand Up @@ -528,7 +506,6 @@ public async Task<OperationResult> 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);
Expand Down Expand Up @@ -612,7 +589,6 @@ public async Task<OperationResult> 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);
Expand Down Expand Up @@ -899,7 +875,6 @@ public async Task<OperationResult> 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);
Expand Down Expand Up @@ -931,7 +906,6 @@ public async Task<OperationResult> 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);
Expand Down Expand Up @@ -980,7 +954,6 @@ public async Task<OperationResult> 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);
Expand Down Expand Up @@ -1339,7 +1312,6 @@ public async Task<OperationResult> UndeleteProtectedItemAsync(
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription),
(nameof(datasourceId), datasourceId));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);

Expand Down Expand Up @@ -1618,7 +1590,6 @@ public async Task<List<ProtectableItemInfo>> ListProtectableItemsAsync(
(nameof(vaultName), vaultName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));
ValidateSubscriptionFormat(subscription);

var armClient = await CreateArmClientAsync(tenant, retryPolicy, cancellationToken: cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ActivityContext> _) => 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<ActivityContext> _) => 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<ActivityContext> _) => 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);
}
}