Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@
# ServiceOwners: @smritiy @srnagar @jongio

# PRLabel: %tools-AzureManagedLustre
/tools/Azure.Mcp.Tools.AzureManagedLustre/ @wolfgang-desalvador @microsoft/azure-mcp
/tools/Azure.Mcp.Tools.AzureManagedLustre/ @wolfgang-desalvador @kinorirosnow @microsoft/azure-mcp
# ServiceLabel: %tools-AzureManagedLustre
# ServiceOwners: @wolfgang-desalvador
# ServiceOwners: @wolfgang-desalvador @kinorirosnow

# PRLabel: %tools-MySQL
/tools/Azure.Mcp.Tools.MySql/ @ramnov @mattkohnms @microsoft/azure-mcp
Expand Down Expand Up @@ -205,6 +205,6 @@

# PRLabel: %tools-EventGrid
/tools/Azure.Mcp.Tools.EventGrid/ @microsoft/azure-mcp

# ServiceLabel: %tools-EventGrid
# ServiceOwners: @microsoft/azure-mcp
10 changes: 10 additions & 0 deletions docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,16 @@ azmcp azuremanagedlustre filesystem list --subscription <subscription> \
azmcp azuremanagedlustre filesystem required-subnet-size --subscription <subscription> \
--sku <azure-managed-lustre-sku> \
--size <filesystem-size-in-tib>

# Create an Azure Managed Lustre filesystem import job (preview / placeholder)
azmcp azuremanagedlustre filesystem import-job create --subscription <subscription> \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't include azure in the name

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a planned refactor which will be addressed after the current PRed actions are in. Here is the open conversion issue: #345 .

Is this a sufficient solution for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please take that as a PR this week, thanks.

--resource-group <resource-group> \
--file-system <filesystem-name> \
[--import-prefixes <prefix1> <prefix2> ... <prefixN>] \
[--conflict-resolution-mode <conflict-mode>] \
[--maximum-errors <maximum-errors>] \
[--admin-status <admin-status>] \
[--name <name>]
```


Expand Down
2 changes: 2 additions & 0 deletions docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ This file contains prompts used for end-to-end testing to ensure each tool is in
|:----------|:----------|
| azmcp_azuremanagedlustre_filesystem_list | List the Azure Managed Lustre filesystems in my subscription <subscription_name> |
| azmcp_azuremanagedlustre_filesystem_list | List the Azure Managed Lustre filesystems in my resource group <resource_group_name> |
| azmcp_azuremanagedlustre_filesystem_import-job_create | Create an import job for the Azure Managed Lustre filesystem <filesystem_name> in resource group <resource_group_name> |
| azmcp_azuremanagedlustre_filesystem_import-job_create | Start a filesystem import job for AMLFS <filesystem_name> with prefixes <prefixes> |
| azmcp_azuremanagedlustre_filesystem_required-subnet-size | Tell me how many IP addresses I need for <filesystem_size> of <amlfs_sku> |
| azmcp_azuremanagedlustre_filesystem_sku_get | List the Azure Managed Lustre SKUs available in <location> |

Expand Down
21 changes: 18 additions & 3 deletions eng/common/TestResources/New-TestResources.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ try {
}
Write-Verbose "Overriding test resources search directory to '$root'"
}

$templateFiles = @()

"$ResourceType-resources.json", "$ResourceType-resources.bicep" | ForEach-Object {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions eng/tools/ToolDescriptionEvaluator/prompts.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@
"azmcp_azuremanagedlustre_filesystem_sku_get": [
"List the Azure Managed Lustre SKUs available in <location>"
],
"azmcp_azuremanagedlustre_filesystem_import-job_create": [
"Create an import job for the Azure Managed Lustre filesystem <filesystem_name> in resource group <resource_group_name>",
"Start a filesystem import job for AMLFS <filesystem_name> with prefixes <prefixes>"
],
"azmcp_marketplace_product_get": [
"Get details about marketplace product <product_name>"
],
Expand Down
2 changes: 2 additions & 0 deletions servers/Azure.Mcp.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The Azure MCP Server updates automatically by default whenever a new release com
- 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_import-job_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).

### Features Removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ public void ConfigureServices(IServiceCollection services)
public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactory)
{
var azureManagedLustre = new CommandGroup(Name,
"Azure Managed Lustre operations - Commands for listing and inspecting Azure Managed Lustre file systems (AMLFS) used for high-performance computing workloads.");
"""
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.
""");
rootGroup.AddSubGroup(azureManagedLustre);

var fileSystem = new CommandGroup("filesystem", "Azure Managed Lustre file system operations - Commands for listing managed Lustre file systems.");
Expand All @@ -35,5 +38,10 @@ public void RegisterCommands(CommandGroup rootGroup, ILoggerFactory loggerFactor
fileSystem.AddSubGroup(sku);

sku.AddCommand("get", new SkuGetCommand(loggerFactory.CreateLogger<SkuGetCommand>()));

var importJob = new CommandGroup("import-job", "Azure Managed Lustre file system import job operations - Create manual import jobs to hydrate the file system namespace.");
fileSystem.AddSubGroup(importJob);

importJob.AddCommand("create", new FileSystemImportJobCreateCommand(loggerFactory.CreateLogger<FileSystemImportJobCreateCommand>()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ namespace Azure.Mcp.Tools.AzureManagedLustre.Commands;
[JsonSerializable(typeof(FileSystemSubnetSizeCommand.FileSystemSubnetSizeResult))]
[JsonSerializable(typeof(FileSystemListCommand.FileSystemListResult))]
[JsonSerializable(typeof(SkuGetCommand.SkuGetResult))]
[JsonSerializable(typeof(LustreFileSystem))]
[JsonSerializable(typeof(AzureManagedLustreSkuInfo))]
[JsonSerializable(typeof(AzureManagedLustreSkuCapability))]
[JsonSerializable(typeof(FileSystemImportJobCreateCommand.FileSystemImportJobCreateResult))]
[JsonSerializable(typeof(LustreFileSystem))]
[JsonSerializable(typeof(ImportJobInfo))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal partial class AzureManagedLustreJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Commands;
using Azure.Mcp.Core.Extensions;
using Azure.Mcp.Tools.AzureManagedLustre.Options;
using Azure.Mcp.Tools.AzureManagedLustre.Options.FileSystem;
using Azure.Mcp.Tools.AzureManagedLustre.Services;
using Microsoft.Extensions.Logging;

namespace Azure.Mcp.Tools.AzureManagedLustre.Commands.FileSystem;

public sealed class FileSystemImportJobCreateCommand(ILogger<FileSystemImportJobCreateCommand> logger)
: BaseAzureManagedLustreCommand<FileSystemImportJobCreateOptions>(logger)
{
private const string CommandTitle = "Create AMLFS Import Job";

private readonly Option<string> _fileSystemOption = AzureManagedLustreOptionDefinitions.FileSystemOption;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase and use new Option handling pattern. new-command.md can help

private readonly Option<string[]> _importPrefixesOption = AzureManagedLustreOptionDefinitions.ImportPrefixesOption;
private readonly Option<string> _conflictResolutionModeOption = AzureManagedLustreOptionDefinitions.ConflictResolutionModeOption;
private readonly Option<int?> _maximumErrorsOption = AzureManagedLustreOptionDefinitions.MaximumErrorsOption;
private readonly Option<string> _nameOption = AzureManagedLustreOptionDefinitions.NameOption;

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);
RequireResourceGroup();
command.Options.Add(_fileSystemOption);
command.Options.Add(_importPrefixesOption);
command.Options.Add(_conflictResolutionModeOption);
command.Options.Add(_maximumErrorsOption);
command.Options.Add(_nameOption);
}

protected override FileSystemImportJobCreateOptions BindOptions(ParseResult parseResult)
{
var options = base.BindOptions(parseResult);
options.FileSystem = parseResult.GetValueOrDefault(_fileSystemOption);
var prefixes = parseResult.GetValueOrDefault(_importPrefixesOption);
if (prefixes == null || prefixes.Length == 0)
{
options.ImportPrefixes = new List<string> { "/" };
}
else
{
options.ImportPrefixes = prefixes.ToList();
}
var conflictMode = parseResult.GetValueOrDefault(_conflictResolutionModeOption);
if (string.IsNullOrWhiteSpace(conflictMode))
{
conflictMode = "Skip"; // default
}
else
{
if (!string.Equals(conflictMode, "Skip", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(conflictMode, "Fail", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Azure MCP Server's Managed Lustre tooling does not support the specified conflict resolution mode. This is because the mode doesn't exist or the tool does not support potentially destructive import conflict resolution modes (overwrite variants).", nameof(_conflictResolutionModeOption));
}
// Normalize casing
conflictMode = char.ToUpperInvariant(conflictMode[0]) + conflictMode.Substring(1).ToLowerInvariant();
}
options.ConflictResolutionMode = conflictMode;
options.MaximumErrors = parseResult.GetValueOrDefault(_maximumErrorsOption) ?? -1;
options.AdminStatus = "Active"; // Hard-coded since service no longer accepts parameter
options.Name = parseResult.GetValueOrDefault(_nameOption);
return options;
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, ParseResult parseResult)
{
var options = BindOptions(parseResult);
try
{
if (!Validate(parseResult.CommandResult, context.Response).IsValid)
{
return context.Response;
}

var svc = context.GetService<IAzureManagedLustreService>();
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),
AzureManagedLustreJsonContext.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 int GetStatusCode(Exception ex) => ex switch
{
Azure.RequestFailedException reqEx => reqEx.Status,
_ => base.GetStatusCode(ex)
};

internal record FileSystemImportJobCreateResult(Models.ImportJobInfo ImportJob);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ public sealed record LustreFileSystem(
[property: JsonPropertyName("maintenanceDay")] string? MaintenanceDay,
[property: JsonPropertyName("maintenanceTime")] string? MaintenanceTime
);

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<string>? ImportPrefixes
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public static class AzureManagedLustreOptionDefinitions
public const string sku = "sku";
public const string size = "size";
public const string location = "location";
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 name = "name";
public static readonly Option<string> SkuOption = new(
$"--{sku}"
)
Expand Down Expand Up @@ -39,4 +45,53 @@ public static class AzureManagedLustreOptionDefinitions
Description = LocationOption.Description,
Required = false
};

public static readonly Option<string> FileSystemOption = new(
$"--{fileSystem}"
)
{
Description = "The name of the Azure Managed Lustre file system (AMLFS).",
Required = true
};

public static readonly Option<string[]> 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<string> ConflictResolutionModeOption = new(
$"--{conflictResolutionMode}"
)
{
Description = "How to handle conflicts during import. Allowed values: Fail, Skip. Default: Skip.",
Required = false
};

public static readonly Option<int?> 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<string> AdminStatusOption = new(
$"--{adminStatus}"
)
{
Description = "Administrative status of the job. Usually 'Active'.",
Required = false
};

public static readonly Option<string> NameOption = new(
$"--{name}"
)
{
Description = "An optional name for the import job. If omitted a timestamp-based name will be generated.",
Required = false
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.Mcp.Tools.AzureManagedLustre.Options.FileSystem;

public sealed class FileSystemImportJobCreateOptions : BaseAzureManagedLustreOptions
{
[JsonPropertyName(AzureManagedLustreOptionDefinitions.fileSystem)]
public string? FileSystem { get; set; }

[JsonPropertyName(AzureManagedLustreOptionDefinitions.importPrefixes)]
public IList<string>? ImportPrefixes { get; set; }

[JsonPropertyName(AzureManagedLustreOptionDefinitions.conflictResolutionMode)]
public string? ConflictResolutionMode { get; set; }

[JsonPropertyName(AzureManagedLustreOptionDefinitions.maximumErrors)]
public int? MaximumErrors { get; set; }

[JsonPropertyName(AzureManagedLustreOptionDefinitions.adminStatus)]
public string? AdminStatus { get; set; }

[JsonPropertyName(AzureManagedLustreOptionDefinitions.name)]
public string? Name { get; set; }
}
Loading
Loading