From ced327feb63bf0d8360b03b6d6ff01bec6335ec8 Mon Sep 17 00:00:00 2001 From: kinorirosnow Date: Tue, 2 Sep 2025 12:46:15 -0400 Subject: [PATCH] import job create command --- .github/CODEOWNERS | 10 +- .../TestResources/New-TestResources.ps1 | 21 +- .../ToolDescriptionEvaluator/prompts.json | 4 + servers/Azure.Mcp.Server/CHANGELOG.md | 9 +- .../Azure.Mcp.Server/docs/azmcp-commands.md | 12 +- .../Azure.Mcp.Server/docs/e2eTestPrompts.md | 2 + .../FileSystemImportJobCreateCommand.cs | 133 +++++++++ .../src/Commands/ManagedLustreJsonContext.cs | 2 + .../src/ManagedLustreSetup.cs | 12 +- .../src/Models/LustreFileSystem.cs | 12 + .../FileSystemImportJobCreateOptions.cs | 27 ++ .../Options/ManagedLustreOptionDefinitions.cs | 60 +++- .../src/Services/IManagedLustreService.cs | 11 + .../src/Services/ManagedLustreService.cs | 74 +++++ .../ManagedLustreCommandTests.cs | 23 ++ .../FileSystemImportJobCreateCommandTests.cs | 264 ++++++++++++++++++ .../tests/test-resources-post.ps1 | 88 ++++++ .../tests/test-resources.bicep | 120 +++++++- 18 files changed, 870 insertions(+), 14 deletions(-) create mode 100644 tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/FileSystem/ImportJob/FileSystemImportJobCreateCommand.cs create mode 100644 tools/Azure.Mcp.Tools.ManagedLustre/src/Options/FileSystem/ImportJob/FileSystemImportJobCreateOptions.cs create mode 100644 tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.UnitTests/FileSystem/ImportJob/FileSystemImportJobCreateCommandTests.cs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 33e25082f..e22d3927b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -135,10 +135,10 @@ # ServiceLabel: %tools-Monitor # ServiceOwners: @smritiy @srnagar @jongio -# PRLabel: %tools-ManagedLustre -/tools/Azure.Mcp.Tools.ManagedLustre/ @wolfgang-desalvador @microsoft/azure-mcp -# ServiceLabel: %tools-ManagedLustre -# ServiceOwners: @wolfgang-desalvador +# PRLabel: %tools-AzureManagedLustre +/tools/Azure.Mcp.Tools.AzureManagedLustre/ @wolfgang-desalvador @kinorirosnow @microsoft/azure-mcp +# ServiceLabel: %tools-AzureManagedLustre +# ServiceOwners: @wolfgang-desalvador @kinorirosnow # PRLabel: %tools-MySQL /tools/Azure.Mcp.Tools.MySql/ @ramnov @mattkohnms @microsoft/azure-mcp @@ -211,7 +211,7 @@ # PRLabel: %tools-EventGrid -/tools/Azure.Mcp.Tools.EventGrid/ @microsoft/azure-mcp +/tools/Azure.Mcp.Tools.EventGrid/ @microsoft/azure-mcp # ServiceLabel: %tools-EventGrid # ServiceOwners: @microsoft/azure-mcp diff --git a/eng/common/TestResources/New-TestResources.ps1 b/eng/common/TestResources/New-TestResources.ps1 index 1acc02fe4..12bfb36fa 100755 --- a/eng/common/TestResources/New-TestResources.ps1 +++ b/eng/common/TestResources/New-TestResources.ps1 @@ -179,7 +179,7 @@ try { } Write-Verbose "Overriding test resources search directory to '$root'" } - + $templateFiles = @() "$ResourceType-resources.json", "$ResourceType-resources.bicep" | ForEach-Object { @@ -203,7 +203,7 @@ try { # returns empty string if $ServiceDirectory is not set $serviceName = GetServiceLeafDirectoryName $ServiceDirectory - + # in ci, random names are used # in non-ci, without BaseName, ResourceGroupName or ServiceDirectory, all invocations will # generate the same resource group name and base name for a given user @@ -310,7 +310,7 @@ try { } } - # This needs to happen after we set the TenantId but before we use the ResourceGroupName + # This needs to happen after we set the TenantId but before we use the ResourceGroupName if ($wellKnownTMETenants.Contains($TenantId)) { # Add a prefix to the resource group name to avoid flagging the usages of local auth # See details at https://eng.ms/docs/products/onecert-certificates-key-vault-and-dsms/key-vault-dsms/certandsecretmngmt/credfreefaqs#how-can-i-disable-s360-reporting-when-testing-customer-facing-3p-features-that-depend-on-use-of-unsafe-local-auth @@ -606,6 +606,21 @@ try { $templateJson = Get-Content -LiteralPath $templateFile.jsonFilePath | ConvertFrom-Json $templateParameterNames = $templateJson.parameters.PSObject.Properties.Name + # Auto-resolve hpcCacheRpObjectId for AMLFS test resources if template expects it and it's not already supplied + if ($templateParameterNames -contains 'hpcCacheRpObjectId' -and -not $templateParameters.ContainsKey('hpcCacheRpObjectId')) { + try { + $sp = Get-AzADServicePrincipal -DisplayName 'HPC Cache Resource Provider' -ErrorAction Stop + if ($sp -and $sp.Id) { + $templateParameters['hpcCacheRpObjectId'] = $sp.Id + Write-Verbose "Resolved hpcCacheRpObjectId to '$($sp.Id)'" + } else { + Write-Warning "HPC Cache Resource Provider service principal not found; 'hpcCacheRpObjectId' will be missing and deployment may fail." + } + } catch { + Write-Warning "Failed to resolve HPC Cache Resource Provider service principal: $_" + } + } + $templateFileParameters = $templateParameters.Clone() foreach ($key in $templateParameters.Keys) { if ($templateParameterNames -notcontains $key) { diff --git a/eng/tools/ToolDescriptionEvaluator/prompts.json b/eng/tools/ToolDescriptionEvaluator/prompts.json index 67b8f8cda..a44db09e0 100644 --- a/eng/tools/ToolDescriptionEvaluator/prompts.json +++ b/eng/tools/ToolDescriptionEvaluator/prompts.json @@ -424,6 +424,10 @@ "azmcp_azuremanagedlustre_filesystem_subnetsize_validate": [ "Validate if can host of " ], + "azmcp_azuremanagedlustre_filesystem_importjob_create": [ + "Create an import job for the Azure Managed Lustre filesystem in resource group ", + "Start a filesystem import job for AMLFS with prefixes " + ], "azmcp_marketplace_product_get": [ "Get details about marketplace product " ], diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index ee862d170..c6fe2fa6d 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -205,7 +205,14 @@ The Azure MCP Server updates automatically by default whenever a new release com - `azmcp_foundry_agents_evaluate`: Evaluate a response from an agent by passing query and response inline - `azmcp_foundry_agents_query_and_evaluate`: Connect to an agent in an AI Foundry project, query it, and evaluate the response in one step - Enhanced AKS managed cluster information with comprehensive properties. [[#490](https://github.com/microsoft/mcp/pull/490)] -- Added support retrieving Key Vault Managed HSM account settings via the command `azmcp-keyvault-admin-settings-get`. [[#358](https://github.com/microsoft/mcp/pull/358)] +- Added support retrieving Key Vault Managed HSM account settings via the command `azmcp-keyvault-admin-settings-get`. [[358](https://github.com/microsoft/mcp/pull/358)] +- Added elicitation support. An elicitation request is sent if the tool annotation secret hint is true. [[#404](https://github.com/microsoft/mcp/pull/404)] +- Added `azmcp sql server create`, `azmcp sql server delete`, `azmcp sql server show` to support SQL server create, delete, and show commands. [[#312](https://github.com/microsoft/mcp/pull/312)] +- Added the following Azure Managed Lustre commands: [[#100](https://github.com/microsoft/mcp/issues/100)] + - `azmcp_azuremanagedlustre_filesystem_get_sku_info`: Get information about Azure Managed Lustre SKU. +- `azmcp_functionapp_get` can now list Function Apps on a resource group level. +- Added the following Azure Managed Lustre command (preview / placeholder implementation): + - `azmcp_azuremanagedlustre_filesystem_importjob_create`: Create a manual import job for an Azure Managed Lustre filesystem (hydrates namespace from linked HSM/Blob; current release returns placeholder status until REST API integration ships). ### Breaking Changes diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index a773ad0d3..655ab3f64 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -1243,7 +1243,7 @@ azmcp monitor metrics query --subscription \ # List Azure Managed Lustre Filesystems available in a subscription or resource group # ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired azmcp managedlustre filesystem list --subscription \ - --resource-group + --resource-group # Create an Azure Managed Lustre filesystem # ❌ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired @@ -1296,6 +1296,16 @@ azmcp managedlustre filesystem subnetsize validate --subscription # ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired azmcp managedlustre filesystem sku get --subscription \ --location + +# Create an Azure Managed Lustre filesystem import job (preview / placeholder) +azmcp azuremanagedlustre filesystem importjob create --subscription \ + --resource-group \ + --file-system \ + [--import-prefixes ... ] \ + [--conflict-resolution-mode ] \ + [--maximum-errors ] \ + [--admin-status ] \ + [--name ] ``` ### Azure Native ISV Operations diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index ff62f035f..035c08233 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -362,6 +362,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | azmcp_managedlustre_filesystem_subnetsize_ask | Tell me how many IP addresses I need for an Azure Managed Lustre filesystem of size using the SKU | | azmcp_managedlustre_filesystem_subnetsize_validate | Validate if the network can host Azure Managed Lustre filesystem of size using the SKU | | azmcp_managedlustre_filesystem_update | Update the maintenance window of the Azure Managed Lustre filesystem to at | +| azmcp_azuremanagedlustre_filesystem_importjob_create | Create an import job for the Azure Managed Lustre filesystem in resource group | +| azmcp_azuremanagedlustre_filesystem_importjob_create | Start a filesystem import job for AMLFS with prefixes | ## Azure Marketplace diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/FileSystem/ImportJob/FileSystemImportJobCreateCommand.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/FileSystem/ImportJob/FileSystemImportJobCreateCommand.cs new file mode 100644 index 000000000..75c84b81f --- /dev/null +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/FileSystem/ImportJob/FileSystemImportJobCreateCommand.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Core.Commands; +using Azure.Mcp.Core.Extensions; +using Azure.Mcp.Core.Models.Option; +using Azure.Mcp.Tools.ManagedLustre.Options; +using Azure.Mcp.Tools.ManagedLustre.Options.FileSystem; +using Azure.Mcp.Tools.ManagedLustre.Services; +using Microsoft.Extensions.Logging; + +namespace Azure.Mcp.Tools.ManagedLustre.Commands.FileSystem; + +public sealed class FileSystemImportJobCreateCommand(ILogger logger) + : BaseManagedLustreCommand(logger) +{ + private const string CommandTitle = "Create AMLFS Import Job"; + + public override string Name => "create"; + + public override string Description => + """ + Creates a manual import job for an Azure Managed Lustre (AMLFS) file system. The import job scans the linked HSM/Blob container and imports specified path prefixes (or all when omitted) honoring the chosen conflict resolution mode. Use to hydrate the AMLFS namespace or refresh content. + """; + + public override string Title => CommandTitle; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = true, + OpenWorld = true, + ReadOnly = false, + LocalRequired = false, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + base.RegisterOptions(command); + // Required common option + command.Options.Add(OptionDefinitions.Common.ResourceGroup.AsRequired()); + // Service-specific options + command.Options.Add(ManagedLustreOptionDefinitions.FileSystemOption); + command.Options.Add(ManagedLustreOptionDefinitions.ImportPrefixesOption); + command.Options.Add(ManagedLustreOptionDefinitions.ConflictResolutionModeOption); + command.Options.Add(ManagedLustreOptionDefinitions.MaximumErrorsOption); + command.Options.Add(ManagedLustreOptionDefinitions.JobNameOption); + + // Validation for conflict resolution mode (Skip|Fail) – consistent with validator style in SubnetSizeAskCommand + command.Validators.Add(cmdResult => + { + if (cmdResult.TryGetValue(ManagedLustreOptionDefinitions.ConflictResolutionModeOption, out var mode) + && !string.IsNullOrWhiteSpace(mode) + && !string.Equals(mode, "Skip", StringComparison.OrdinalIgnoreCase) + && !string.Equals(mode, "Fail", StringComparison.OrdinalIgnoreCase)) + { + cmdResult.AddError("Invalid conflict resolution mode. Allowed values: Skip, Fail."); + } + }); + } + + protected override FileSystemImportJobCreateOptions BindOptions(ParseResult parseResult) + { + var options = base.BindOptions(parseResult); + options.FileSystem = parseResult.GetValueOrDefault(ManagedLustreOptionDefinitions.FileSystemOption.Name); + options.ResourceGroup ??= parseResult.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup.Name); + var prefixes = parseResult.GetValueOrDefault(ManagedLustreOptionDefinitions.ImportPrefixesOption.Name); + if (prefixes == null || prefixes.Length == 0) + { + options.ImportPrefixes = new List { "/" }; + } + else + { + options.ImportPrefixes = prefixes.ToList(); + } + var conflictMode = parseResult.GetValueOrDefault(ManagedLustreOptionDefinitions.ConflictResolutionModeOption.Name); + conflictMode = string.IsNullOrWhiteSpace(conflictMode) + ? "Skip" + : char.ToUpperInvariant(conflictMode[0]) + conflictMode.Substring(1).ToLowerInvariant(); + options.ConflictResolutionMode = conflictMode; + options.MaximumErrors = parseResult.GetValueOrDefault(ManagedLustreOptionDefinitions.MaximumErrorsOption.Name) ?? -1; + options.AdminStatus = "Active"; // Hard-coded since service no longer accepts parameter + options.Name = parseResult.GetValueOrDefault(ManagedLustreOptionDefinitions.JobNameOption.Name); + return options; + } + + public override async Task ExecuteAsync(CommandContext context, ParseResult parseResult) + { + var options = BindOptions(parseResult); + try + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return context.Response; + } + + var svc = context.GetService(); + var result = await svc.CreateImportJobAsync( + options.Subscription!, + options.ResourceGroup!, + options.FileSystem!, + options.Name, + options.ImportPrefixes, + options.ConflictResolutionMode!, + options.MaximumErrors, + options.Tenant, + options.RetryPolicy); + + context.Response.Results = ResponseResult.Create( + new FileSystemImportJobCreateResult(result), + ManagedLustreJsonContext.Default.FileSystemImportJobCreateResult); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error creating AMLFS import job. FileSystem: {FileSystem} ResourceGroup: {ResourceGroup} Options: {@Options}", + options.FileSystem, options.ResourceGroup, options); + HandleException(context, ex); + } + + return context.Response; + } + + protected override HttpStatusCode GetStatusCode(Exception ex) => ex switch + { + Azure.RequestFailedException reqEx => (HttpStatusCode)reqEx.Status, + _ => base.GetStatusCode(ex) + }; + + internal record FileSystemImportJobCreateResult(Models.ImportJobInfo ImportJob); +} diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/ManagedLustreJsonContext.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/ManagedLustreJsonContext.cs index 7c289b84b..ac4b1228e 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/ManagedLustreJsonContext.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Commands/ManagedLustreJsonContext.cs @@ -16,5 +16,7 @@ namespace Azure.Mcp.Tools.ManagedLustre.Commands; [JsonSerializable(typeof(LustreFileSystem))] [JsonSerializable(typeof(ManagedLustreSkuInfo))] [JsonSerializable(typeof(ManagedLustreSkuCapability))] +[JsonSerializable(typeof(FileSystemImportJobCreateCommand.FileSystemImportJobCreateResult))] +[JsonSerializable(typeof(ImportJobInfo))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] internal partial class ManagedLustreJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/ManagedLustreSetup.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/ManagedLustreSetup.cs index 2a33dca86..6c13d71ae 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/ManagedLustreSetup.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/ManagedLustreSetup.cs @@ -23,12 +23,16 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } public CommandGroup RegisterCommands(IServiceProvider serviceProvider) { var managedLustre = new CommandGroup(Name, - "Azure Managed Lustre operations - Commands for creating, updating, listing and inspecting Azure Managed Lustre file systems (AMLFS) used for high-performance computing workloads. The tool focuses on managing all the aspects related to Azure Managed Lustre file system instances."); + """ + Azure Managed Lustre operations - Azure Managed Lustre file systems (AMLFS) interaction for high-performance computing workloads. + Use this tool to list and manage Azure Managed Lustre file systems, including creating import jobs to hydrate the file system namespace. + """); var fileSystem = new CommandGroup("filesystem", "Azure Managed Lustre file system operations - Commands for listing managed Lustre file systems."); managedLustre.AddSubGroup(fileSystem); @@ -57,6 +61,12 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider) var skuGet = serviceProvider.GetRequiredService(); sku.AddCommand(skuGet.Name, skuGet); + var importJob = new CommandGroup("importjob", "Azure Managed Lustre file system import job operations - Create manual import jobs to hydrate the file system namespace."); + fileSystem.AddSubGroup(importJob); + + var importJobCreate = serviceProvider.GetRequiredService(); + importJob.AddCommand(importJobCreate.Name, importJobCreate); + return managedLustre; } } diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Models/LustreFileSystem.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Models/LustreFileSystem.cs index 06d5045b2..bf31331ca 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/Models/LustreFileSystem.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Models/LustreFileSystem.cs @@ -26,3 +26,15 @@ public sealed record LustreFileSystem( [property: JsonPropertyName("squashUid")] long? SquashUid, [property: JsonPropertyName("squashGid")] long? SquashGid ); + +public sealed record ImportJobInfo( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("fileSystemName")] string FileSystemName, + [property: JsonPropertyName("resourceGroupName")] string ResourceGroupName, + [property: JsonPropertyName("subscriptionId")] string SubscriptionId, + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("conflictResolutionMode")] string ConflictResolutionMode, + [property: JsonPropertyName("maximumErrors")] int? MaximumErrors, + [property: JsonPropertyName("adminStatus")] string? AdminStatus, + [property: JsonPropertyName("importPrefixes")] IList? ImportPrefixes +); diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/FileSystem/ImportJob/FileSystemImportJobCreateOptions.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/FileSystem/ImportJob/FileSystemImportJobCreateOptions.cs new file mode 100644 index 000000000..8d0b8f4bf --- /dev/null +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/FileSystem/ImportJob/FileSystemImportJobCreateOptions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.Mcp.Tools.ManagedLustre.Options.FileSystem; + +public sealed class FileSystemImportJobCreateOptions : BaseManagedLustreOptions +{ + [JsonPropertyName(ManagedLustreOptionDefinitions.fileSystem)] + public string? FileSystem { get; set; } + + [JsonPropertyName(ManagedLustreOptionDefinitions.importPrefixes)] + public IList? ImportPrefixes { get; set; } + + [JsonPropertyName(ManagedLustreOptionDefinitions.conflictResolutionMode)] + public string? ConflictResolutionMode { get; set; } + + [JsonPropertyName(ManagedLustreOptionDefinitions.maximumErrors)] + public int? MaximumErrors { get; set; } + + [JsonPropertyName(ManagedLustreOptionDefinitions.adminStatus)] + public string? AdminStatus { get; set; } + + [JsonPropertyName(ManagedLustreOptionDefinitions.jobName)] + public string? Name { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/ManagedLustreOptionDefinitions.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/ManagedLustreOptionDefinitions.cs index ce1d9235c..2968d1bed 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/ManagedLustreOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Options/ManagedLustreOptionDefinitions.cs @@ -15,7 +15,14 @@ public static class ManagedLustreOptionDefinitions public const string zone = "zone"; public const string hsmContainer = "hsm-container"; public const string hsmLogContainer = "hsm-log-container"; - public const string importPrefix = "import-prefix"; + // Import job / file system constants (added during AzureManagedLustre -> ManagedLustre rename) + public const string fileSystem = "file-system"; + public const string importPrefixes = "import-prefixes"; + public const string conflictResolutionMode = "conflict-resolution-mode"; + public const string maximumErrors = "maximum-errors"; + public const string adminStatus = "admin-status"; + public const string jobName = "name"; // aligning with prior 'name' option intent for job naming + public const string importPrefix = "import-prefix"; // kept for backwards compatibility where singular prefix still used public const string maintenanceDay = "maintenance-day"; public const string maintenanceTime = "maintenance-time"; public const string rootSquashMode = "root-squash-mode"; @@ -67,6 +74,15 @@ public static class ManagedLustreOptionDefinitions Description = "The AMLFS resource name. Must be DNS-friendly (letters, numbers, hyphens). Example: --name amlfs-001" }; + // File system option (previously AzureManagedLustreOptionDefinitions.FileSystemOption) + public static readonly Option FileSystemOption = new( + $"--{fileSystem}" + ) + { + Required = true, + Description = "Target Managed Lustre file system name. Example: --file-system fs1" + }; + public static readonly Option SubnetIdOption = new( $"--{subnetId}" ) @@ -198,5 +214,47 @@ public static class ManagedLustreOptionDefinitions public static readonly Option OptionalMaintenanceDayOption = MaintenanceDayOption.AsOptional(); public static readonly Option OptionalMaintenanceTimeOption = MaintenanceTimeOption.AsOptional(); + + public static readonly Option ImportPrefixesOption = new( + $"--{importPrefixes}" + ) + { + Description = "List of path prefixes in the linked HSM/Blob container to import. Provide multiple prefixes separated by spaces. If omitted, the entire container may be considered depending on service defaults.", + Required = false, + AllowMultipleArgumentsPerToken = true + }; + + public static readonly Option ConflictResolutionModeOption = new( + $"--{conflictResolutionMode}" + ) + { + Description = "How to handle conflicts during import. Allowed values: Fail, Skip. Default: Skip.", + Required = false + }; + + public static readonly Option MaximumErrorsOption = new( + $"--{maximumErrors}" + ) + { + Description = "Maximum number of errors before the import job fails fast. Default: 0 (fail on first error).", + Required = false + }; + + public static readonly Option AdminStatusOption = new( + $"--{adminStatus}" + ) + { + Description = "Administrative status of the job. Usually 'Active'.", + Required = false + }; + + public static readonly Option JobNameOption = new( + $"--{jobName}" + ) + { + Description = "An optional name for the HSM job. If omitted a timestamp-based name will be generated.", + Required = false + }; + } diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/IManagedLustreService.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/IManagedLustreService.cs index b8ab78440..0318fdd18 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/IManagedLustreService.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/IManagedLustreService.cs @@ -77,5 +77,16 @@ Task UpdateFileSystemAsync( long? squashGid = null, string? tenant = null, RetryPolicyOptions? retryPolicy = null); + + Task CreateImportJobAsync( + string subscription, + string resourceGroup, + string name, + string? jobName = null, + IList? importPrefixes = null, + string conflictResolutionMode = "Skip", + int? maximumErrors = 0, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null); } diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/ManagedLustreService.cs b/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/ManagedLustreService.cs index 0f863eb29..8ad2ff7a1 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/ManagedLustreService.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/src/Services/ManagedLustreService.cs @@ -488,4 +488,78 @@ public async Task CheckAmlFSSubnetAsync( } } + + public async Task CreateImportJobAsync( + string subscription, + string resourceGroup, + string name, + string? jobName = null, + IList? importPrefixes = null, + string conflictResolutionMode = "Skip", + int? maximumErrors = 0, + string? tenant = null, + RetryPolicyOptions? retryPolicy = null) + { + ValidateRequiredParameters( + (nameof(subscription), subscription), + (nameof(resourceGroup), resourceGroup), + (nameof(name), name)); + + // NOTE: The StorageCache SDK (as of current version) does not expose an import job create API. + // Placeholder implementation constructs a job object. Wire up REST call when SDK/REST details available. + jobName ??= $"import-job-{DateTime.UtcNow:yyyyMMddHHmmss}"; + // Ensure default import prefix list is a single root path when none provided OR provided empty + if (importPrefixes == null || importPrefixes.Count == 0) + { + importPrefixes = new List { "/" }; + } + + try + { + // Resolve to ensure file system exists + var rg = await _resourceGroupService.GetResourceGroupResource(subscription, resourceGroup, tenant, retryPolicy) + ?? throw new Exception($"Resource group '{resourceGroup}' not found"); + + // NOTE: GetAmlFileSystemAsync returns a Response in this SDK version. + // The concrete Response here does NOT implement IDisposable (compiler error if used in a using statement), + // so no explicit disposal is required. If a future azure-core version makes it disposable, wrap in a using. + var fsResponse = await rg.GetAmlFileSystemAsync(name); + var fsResource = fsResponse.Value; + var jobs = fsResource.GetStorageCacheImportJobs(); + + // Import job configuration data + StorageCacheImportJobData data = new StorageCacheImportJobData(fsResource.Data.Location) + { + ConflictResolutionMode = conflictResolutionMode, + MaximumErrors = maximumErrors + }; + // ImportPrefixes is read-only, so add items to the collection instead of assigning + foreach (var prefix in importPrefixes) + { + data.ImportPrefixes.Add(prefix); + } + + var operation = await jobs.CreateOrUpdateAsync(WaitUntil.Completed, jobName, data); + var created = operation.Value; + } + catch (RequestFailedException rfe) + { + throw new Exception($"Failed to create import job '{jobName}' for AMLFS '{name}': {rfe.Message}", rfe); + } + catch (Exception ex) + { + throw new Exception($"Failed to create import job '{jobName}' for AMLFS '{name}': {ex.Message}", ex); + } + + return new ImportJobInfo( + jobName!, + name, + resourceGroup, + subscription, + "Submitted (placeholder)", + conflictResolutionMode, + maximumErrors, + "Active", + importPrefixes); + } } diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.LiveTests/ManagedLustreCommandTests.cs b/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.LiveTests/ManagedLustreCommandTests.cs index cbf457b4b..367eefdcb 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.LiveTests/ManagedLustreCommandTests.cs +++ b/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.LiveTests/ManagedLustreCommandTests.cs @@ -332,4 +332,27 @@ public async Task Should_update_root_squash_and_verify_with_list() Assert.True(found, $"Expected filesystem '{Settings.ResourceBaseName}' to be present after root squash update."); } + + [Fact] + public async Task Should_create_import_job() + { + var result = await CallToolAsync( + "azmcp_managedlustre_filesystem_importjob_create", + new() + { + { "subscription", Settings.SubscriptionId }, + { "resource-group", Settings.ResourceGroupName }, + { "file-system", Settings.ResourceBaseName }, + { "tenant", Settings.TenantId } + }); + + var importJob = result.AssertProperty("importJob"); + Assert.Equal(JsonValueKind.Object, importJob.ValueKind); + // The import job response currently returns the job name under 'name' + Assert.True(importJob.TryGetProperty("name", out var jobNameProp)); + Assert.False(string.IsNullOrWhiteSpace(jobNameProp.GetString())); + // The filesystem the job targets is under 'fileSystemName' + Assert.True(importJob.TryGetProperty("fileSystemName", out var fsNameProp)); + Assert.Equal(Settings.ResourceBaseName, fsNameProp.GetString()); + } } diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.UnitTests/FileSystem/ImportJob/FileSystemImportJobCreateCommandTests.cs b/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.UnitTests/FileSystem/ImportJob/FileSystemImportJobCreateCommandTests.cs new file mode 100644 index 000000000..10a20cf65 --- /dev/null +++ b/tools/Azure.Mcp.Tools.ManagedLustre/tests/Azure.Mcp.Tools.ManagedLustre.UnitTests/FileSystem/ImportJob/FileSystemImportJobCreateCommandTests.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Core.Models.Command; +using Azure.Mcp.Core.Options; +using Azure.Mcp.Tools.ManagedLustre.Commands.FileSystem; +using Azure.Mcp.Tools.ManagedLustre.Models; +using Azure.Mcp.Tools.ManagedLustre.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; + +namespace Azure.Mcp.Tools.ManagedLustre.UnitTests.FileSystem; + +public class FileSystemImportJobCreateCommandTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IManagedLustreService _amlfsService; + private readonly ILogger _logger; + private readonly FileSystemImportJobCreateCommand _command; + private readonly CommandContext _context; + private readonly Command _commandDefinition; + private readonly string _subscription = "sub123"; + private readonly string _resourceGroup = "rg1"; + private readonly string _fileSystem = "fs1"; + + public FileSystemImportJobCreateCommandTests() + { + _amlfsService = Substitute.For(); + _logger = Substitute.For>(); + + var services = new ServiceCollection().AddSingleton(_amlfsService); + _serviceProvider = services.BuildServiceProvider(); + + _command = new(_logger); + _context = new(_serviceProvider); + _commandDefinition = _command.GetCommand(); + } + + [Fact] + public void Constructor_InitializesCommandCorrectly() + { + var cmd = _command.GetCommand(); + Assert.Equal("create", cmd.Name); + Assert.False(string.IsNullOrWhiteSpace(cmd.Description)); + } + + [Fact] + public async Task ExecuteAsync_Succeeds_WithRequiredParameters() + { + // Arrange + _amlfsService.CreateImportJobAsync( + Arg.Is(_subscription), + Arg.Is(_resourceGroup), + Arg.Is(_fileSystem), + Arg.Is(x => x == null), + Arg.Any?>(), + Arg.Is("Skip"), + Arg.Is(-1), // defaulted by command now + Arg.Any(), + Arg.Any()) + .Returns(new ImportJobInfo( + "import-job-123", + _fileSystem, + _resourceGroup, + _subscription, + "Submitted (placeholder)", + "Skip", + -1, + "Active", + new List { "/" })); + + var args = _commandDefinition.Parse([ + "--subscription", _subscription, + "--resource-group", _resourceGroup, + "--file-system", _fileSystem + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert basic response status and that results object exists (contents validated indirectly via service return setup) + Assert.Equal(HttpStatusCode.OK, response.Status); + Assert.NotNull(response.Results); + + // Verify required args & defaults passed to service + await _amlfsService.Received(1).CreateImportJobAsync( + _subscription, + _resourceGroup, + _fileSystem, + null, + Arg.Any?>(), + "Skip", + -1, + Arg.Any(), + Arg.Any()); + + // Inspect captured call to verify prefixes list content + var call = _amlfsService.ReceivedCalls().First(c => c.GetMethodInfo().Name == nameof(IManagedLustreService.CreateImportJobAsync)); + var prefixesArg = (IList?)call.GetArguments()[4]; + Assert.NotNull(prefixesArg); + Assert.Single(prefixesArg!); // default now contains root path + Assert.Equal("/", prefixesArg![0]); + // Inspect captured call to verify conflict resolution mode + var conflictModeArg = (string)call.GetArguments()[5]!; + Assert.Equal("Skip", conflictModeArg); + } + + [Fact] + public async Task ExecuteAsync_PassesOptionalParameters() + { + // Arrange + var prefixes = new[] { "/a", "/b" }; + var name = "custom-job"; + _amlfsService.CreateImportJobAsync( + Arg.Is(_subscription), + Arg.Is(_resourceGroup), + Arg.Is(_fileSystem), + Arg.Is(name), + Arg.Is?>(p => p != null && p.Count == prefixes.Length && p[0] == prefixes[0] && p[1] == prefixes[1]), + Arg.Is("Skip"), + Arg.Is(5), + Arg.Any(), + Arg.Any()) + .Returns(new ImportJobInfo( + name, + _fileSystem, + _resourceGroup, + _subscription, + "Submitted (placeholder)", + "Skip", + 5, + "Active", + prefixes.ToList())); + + var args = _commandDefinition.Parse([ + "--subscription", _subscription, + "--resource-group", _resourceGroup, + "--file-system", _fileSystem, + "--import-prefixes", prefixes[0], prefixes[1], + "--conflict-resolution-mode", "Skip", + "--maximum-errors", "5", + "--name", name + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.Status); + await _amlfsService.Received(1).CreateImportJobAsync( + _subscription, + _resourceGroup, + _fileSystem, + name, + Arg.Is?>(p => p != null && p.Count == prefixes.Length && p[0] == prefixes[0] && p[1] == prefixes[1]), + "Skip", + 5, + Arg.Any(), + Arg.Any()); + } + + [Theory] + [InlineData("--resource-group rg1 --file-system fs1", false)] // missing subscription + [InlineData("--subscription sub123 --file-system fs1", false)] // missing resource-group + [InlineData("--subscription sub123 --resource-group rg1", false)] // missing file-system + public async Task ExecuteAsync_ValidationErrors_Return400(string argLine, bool shouldSucceed) + { + // Arrange + var args = _commandDefinition.Parse(argLine.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + var expectedStatus = shouldSucceed ? HttpStatusCode.OK : HttpStatusCode.BadRequest; + Assert.Equal(expectedStatus, response.Status); + if (!shouldSucceed) + { + Assert.Contains("required", response.Message, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + public async Task ExecuteAsync_ServiceThrows_RequestFailed_UsesStatusCode() + { + // Arrange + _amlfsService.CreateImportJobAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any?>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Azure.RequestFailedException(404, "not found")); + + var args = _commandDefinition.Parse([ + "--subscription", _subscription, + "--resource-group", _resourceGroup, + "--file-system", _fileSystem + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.Status); + Assert.Contains("not found", response.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ExecuteAsync_ServiceThrows_GenericException_Returns500() + { + // Arrange + _amlfsService.CreateImportJobAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any?>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .ThrowsAsync(new Exception("boom")); + + var args = _commandDefinition.Parse([ + "--subscription", _subscription, + "--resource-group", _resourceGroup, + "--file-system", _fileSystem + ]); + + // Act + var response = await _command.ExecuteAsync(_context, args); + + // Assert + Assert.True((int)response.Status >= 500); + Assert.Contains("boom", response.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ExecuteAsync_InvalidConflictResolutionMode_Returns400() + { + var args = _commandDefinition.Parse([ + "--subscription", _subscription, + "--resource-group", _resourceGroup, + "--file-system", _fileSystem, + "--conflict-resolution-mode", "OverwriteAlways" + ]); + + var response = await _command.ExecuteAsync(_context, args); + Assert.Equal(HttpStatusCode.BadRequest, response.Status); + Assert.Contains("Invalid conflict resolution mode", response.Message, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources-post.ps1 b/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources-post.ps1 index 7f4f3651b..251e8bde2 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources-post.ps1 +++ b/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources-post.ps1 @@ -88,4 +88,92 @@ $HPCCacheResourceProviderPrincipalId = $firstEvent.Caller foreach ($role in $rolesToAssign) { Write-Host "Assigning role '$role' to principal 'HPC Cache Resource Provider'on scope '$scope'..." -ForegroundColor Yellow New-AzRoleAssignment -Scope $scope -RoleDefinitionName $role -PrincipalId $HPCCacheResourceProviderPrincipalId | Out-Null +} + +# ---------- Below is required because HSM settings are not applied because can't list service principals ---------- +Write-Host "Recreating AMLFS instance to ensure required HSM settings are applied (always recreate strategy)..." -ForegroundColor Yellow + +# Capture desired parameters from existing instance if present so we preserve SKU/capacity/location/zone +$existingSku = $amlfsCluster?.SkuName +$existingCapacity = $amlfsCluster?.StorageCapacityTiB +$existingLocation = $amlfsCluster?.Location + +# Derive expected HSM container resource IDs from deployment outputs (preferred) with fallback +$expectedHsmContainerId = $DeploymentOutputs['HSM_CONTAINER_ID'] +$expectedHsmLogContainerId = $DeploymentOutputs['HSM_LOGS_CONTAINER_ID'] + +if (-not $expectedHsmContainerId -or -not $expectedHsmLogContainerId) { + Write-Warning "DeploymentOutputs missing HSM container IDs; falling back to constructing from storage account ID." + $storageAccountId = $sa.Id + $fallbackDataName = 'hsm-data' + $fallbackLogsName = 'hsm-logs' + if (-not $expectedHsmContainerId) { $expectedHsmContainerId = "$storageAccountId/blobServices/default/containers/$fallbackDataName" } + if (-not $expectedHsmLogContainerId) { $expectedHsmLogContainerId = "$storageAccountId/blobServices/default/containers/$fallbackLogsName" } +} + +# Preserve / resolve subnet before deletion (prefer deployment output) +$subnetId = $DeploymentOutputs['AMLFS_SUBNET_ID'] +if (-not $subnetId) { + # Fall back to discovering from existing NIC + $subnetId = $nic.IpConfigurations[0].Subnet.Id + Write-Warning "AMLFS_SUBNET_ID not found in DeploymentOutputs; using NIC-derived subnet id." +} + +if ($amlfsCluster) { + Write-Host "Deleting existing AMLFS '$amlfsName'..." -ForegroundColor Yellow + try { + Remove-AzStorageCacheAmlFileSystem -ResourceGroupName $ResourceGroupName -Name $amlfsName -ErrorAction Stop + } catch { + Write-Error "Failed to initiate deletion of AMLFS '$amlfsName': $($_.Exception.Message)"; throw + } + + # Poll for deletion completion (up to ~20 minutes) + $pollSeconds = 30 + for ($i=0; $i -lt 40; $i++) { + Start-Sleep -Seconds $pollSeconds + $exists = Get-AzStorageCacheAmlFileSystem -ResourceGroupName $ResourceGroupName -Name $amlfsName -ErrorAction SilentlyContinue + if (-not $exists) { Write-Host "Deletion confirmed." -ForegroundColor Gray; break } + Write-Host "Waiting for AMLFS deletion (attempt $($i+1))..." -ForegroundColor DarkGray + if ($i -eq 39) { Write-Warning "Timed out waiting for AMLFS deletion; will attempt creation anyway (may fail)." } + } +} +else { + Write-Host "No existing AMLFS instance found; proceeding to create a fresh one." -ForegroundColor Gray +} + +# Fallback defaults if original values were not present +if (-not $existingSku) { $existingSku = "AMLFS-Durable-Premium-500" } +if (-not $existingCapacity -or $existingCapacity -le 0) { $existingCapacity = 4 } # Align with template default (4 TiB) +if (-not $existingLocation) { $existingLocation = $sa.Location } +if (-not $existingLocation) { $existingLocation = 'westus' } + +# Override location from deployment outputs if provided +$locationFromOutputs = $DeploymentOutputs['LOCATION'] +if ($locationFromOutputs) { + $location = $locationFromOutputs +} else { + $location = $existingLocation # ensure variable used in creation call is defined + if (-not $locationFromOutputs) { Write-Host "LOCATION output not provided; using inferred location '$location'." -ForegroundColor Gray } +} + +Write-Host "Creating AMLFS '$amlfsName' (Sku=$existingSku, Capacity=${existingCapacity}TiB, Location=$existingLocation, Zone=1)" -ForegroundColor Yellow +Write-Host " HSM Data Container: $expectedHsmContainerId" -ForegroundColor Gray +Write-Host " HSM Logging Container: $expectedHsmLogContainerId" -ForegroundColor Gray + +try { + New-AzStorageCacheAmlFileSystem ` + -Name $amlfsName ` + -ResourceGroupName $ResourceGroupName ` + -Location $location ` + -SkuName $existingSku ` + -StorageCapacityTiB $existingCapacity ` + -FilesystemSubnet $subnetId ` + -SettingContainer $expectedHsmContainerId ` + -SettingLoggingContainer $expectedHsmLogContainerId ` + -MaintenanceWindowDayOfWeek Monday ` + -MaintenanceWindowTimeOfDayUtc "12:00" ` + -Confirm:$false | Out-Null + Write-Host "AMLFS '$amlfsName' recreated successfully with required HSM settings." -ForegroundColor Green +} catch { + Write-Error "Failed to (re)create AMLFS '$amlfsName': $($_.Exception.Message)"; throw } \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources.bicep b/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources.bicep index bcf3c3155..5933e5267 100644 --- a/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources.bicep +++ b/tools/Azure.Mcp.Tools.ManagedLustre/tests/test-resources.bicep @@ -17,8 +17,10 @@ param amlfsSubnetPrefix string = '10.20.1.0/24' @description('Subnet prefix for AMLFS small, for subnet validation live tests.') param amlfsSubnetSmallPrefix string = '10.20.2.0/28' -@description('The client OID to grant access to test resources.') -param testApplicationOid string = deployer().objectId +// Waiting on the a Read permission for the Managed Identity used for merge validation Pipelines. +// When that happens this param will be retrievable in the New-TestResources.ps1 script. +// @description('Object ID of the HPC Cache Resource Provider (service principal) that needs Storage roles on the storage account.') +// param hpcCacheRpObjectId string @description('AMLFS SKU name') @@ -54,6 +56,12 @@ resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = { natGateway: { id: natGateway.id } + // Allow this subnet to reach Storage via service endpoint (used by storage account network rules below) + serviceEndpoints: [ + { + service: 'Microsoft.Storage' + } + ] privateEndpointNetworkPolicies: 'Disabled' privateLinkServiceNetworkPolicies: 'Disabled' } @@ -100,6 +108,114 @@ resource natPublicIp 'Microsoft.Network/publicIPAddresses@2024-07-01' = { } } +// The below section can be used instead when the hpcCacheRpObjectId parameter is available. +// // Storage account used for HSM hydration and logging containers +// @minLength(3) +// @maxLength(24) +// @description('Storage account name for HSM hydration and logging containers') +// param storageAccountName string = toLower('${baseName}sa') + +// resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { +// name: storageAccountName +// location: location +// sku: { +// name: 'Standard_LRS' +// } +// kind: 'StorageV2' +// properties: { +// accessTier: 'Hot' +// // Restrict network access to the specified subnet (default Deny others) +// networkAcls: { +// bypass: 'AzureServices' +// virtualNetworkRules: [ +// { +// id: filesystemSubnetId +// action: 'Allow' +// } +// ] +// ipRules: [] +// defaultAction: 'Deny' +// } +// publicNetworkAccess: 'Enabled' +// } +// } + +// // Role assignments granting the HPC Cache RP required access to the storage account for HSM (imports/exports) +// // Storage Account Contributor +// resource storageAccountContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(storageAccount.id, '17d1049b-9a84-46fb-8f53-869881c3d3ab', hpcCacheRpObjectId) +// scope: storageAccount +// properties: { +// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') +// principalId: hpcCacheRpObjectId +// principalType: 'ServicePrincipal' +// } +// } + +// // Storage Blob Data Contributor +// resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +// name: guid(storageAccount.id, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe', hpcCacheRpObjectId) +// scope: storageAccount +// properties: { +// roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') +// principalId: hpcCacheRpObjectId +// principalType: 'ServicePrincipal' +// } +// } + +// resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2021-09-01' = { +// parent: storageAccount +// name: 'default' +// properties: {} +// } + +// resource dataContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-09-01' = { +// parent: blobService +// name: 'data' +// properties: { +// publicAccess: 'None' +// } +// } + +// resource loggingContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-09-01' = { +// parent: blobService +// name: 'logging' +// properties: { +// publicAccess: 'None' +// } +// } + +// var filesystemSubnetId = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'amlfs') +// var filesystemSmallSubnetId = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'amlfs-small') + +// resource amlfs 'Microsoft.StorageCache/amlFilesystems@2024-07-01' = { +// name: baseName +// location: location +// sku: { +// name: amlfsSku +// } +// properties: { +// storageCapacityTiB: amlfsCapacityTiB +// filesystemSubnet: filesystemSubnetId +// hsm: { +// settings: { +// // Resource IDs for the blob containers used by HSM +// // Use symbolic resource IDs so deployment engine creates dependency on containers +// container: dataContainer.id +// loggingContainer: loggingContainer.id +// // Only blobs prefixed with one of these paths will be imported during initial creation +// importPrefixesInitial: [ +// '/' +// ] +// } +// } +// maintenanceWindow: { +// dayOfWeek: 'Sunday' +// timeOfDayUTC: '02:00' +// } +// } +// } + var filesystemSubnetId = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'amlfs') var filesystemSmallSubnetId = resourceId('Microsoft.Network/virtualNetworks/subnets', vnet.name, 'amlfs-small')