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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added the resilience drill end tool."
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Features Added"
description: "Added the resilience drill start tool."
14 changes: 14 additions & 0 deletions servers/Azure.Mcp.Server/docs/azmcp-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3938,13 +3938,27 @@ azmcp resilience drill create --service-group <service-group> \
azmcp resilience drill get --service-group <service-group> \
[--name <name>]

# Start a new execution of a resilience drill in Failover or TestFailover mode
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp resilience drill start --service-group <service-group> \
--drill <drill> \
--mode <Failover|TestFailover>

# End the running execution of a resilience drill and attest its outcome
# ✅ Destructive | ❌ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp resilience drill end --service-group <service-group> \
--drill <drill> \
--attestation <Success|Failed> \
--attestation-notes <attestation-notes>

# Update mutable properties of a resilience drill
# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp resilience drill update --service-group <service-group> \
--drill <drill> \
[--subscription <subscription> --region <region>] \
[--rbac-setup-mode <AutomatedCustomRole|AutomatedBuiltinRoles|Manual>] \
[--recovery-plan <recovery-plan>]

# Delete a resilience drill from a service group
# ✅ Destructive | ✅ Idempotent | ❌ OpenWorld | ❌ ReadOnly | ❌ Secret | ❌ LocalRequired
azmcp resilience drill delete --service-group <service-group> \
Expand Down
4 changes: 4 additions & 0 deletions servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,8 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat
| resilience_drill_create | Create a resilience drill for service group <service_group> | clarification-required |
| resilience_drill_delete | Delete resilience drill <drill_name> from service group <service_group> | none |
| resilience_drill_delete | Permanently remove drill <drill_name> in service group <service_group> | none |
| resilience_drill_end | End resilience drill <drill_name> in service group <service_group> with a Success attestation and notes "Validation completed" | none |
| resilience_drill_end | Stop the running resilience drill <drill_name> in service group <service_group> and attest it as Failed with notes "Validation failed" | none |
| resilience_drill_get | List all resilience drills in service group <service_group> | none |
| resilience_drill_get | Get the details of resilience drill <drill_name> in service group <service_group> | none |
| resilience_drill_update | Update resilience drill <drill_name> in service group <service_group> to use manual RBAC setup | none |
Expand All @@ -950,6 +952,8 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat
| resilience_drill_resource_get | Get the complete details of drill resource <resource_name> for resilience drill <drill_name> in service group <service_group> | none |
| resilience_drill_resource_get | Get drill target <resource_name> for resilience drill <drill_name> in service group <service_group> | none |
| resilience_drill_resource_get | Retrieve the ARM properties of drill resource <resource_name> for resilience drill <drill_name> in service group <service_group> | none |
| resilience_drill_start | Start resilience drill <drill_name> in service group <service_group> in Failover mode | none |
| resilience_drill_start | Run resilience drill <drill_name> in service group <service_group> as a TestFailover | none |
| resilience_drill_run_get | List all runs of drill <drill_name> in service group <service_group> | none |
| resilience_drill_run_get | Get drill run <drill_run_name> for drill <drill_name> in service group <service_group> | none |
| resilience_drill_run_resource_get | List all resources of drill run <drill_run_name> for drill <drill_name> in service group <service_group> | none |
Expand Down
34 changes: 34 additions & 0 deletions servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,40 @@
"resilience_drill_delete"
]
},
{
"name": "manage_azure_resilience_management_drill_executions",
"description": "Start or end Azure Resilience Management drill executions for Azure service groups, including failover mode and completion attestation.",
"toolMetadata": {
"destructive": {
"value": true,
"description": "This tool may delete or modify existing resources in its environment."
},
"idempotent": {
"value": false,
"description": "Running this operation multiple times with the same arguments may have additional effects or produce different results."
},
"openWorld": {
"value": false,
"description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities."
},
"readOnly": {
"value": false,
"description": "This tool may modify its environment by creating, updating, or deleting data."
},
"secret": {
"value": false,
"description": "This tool does not handle sensitive or secret information."
},
"localRequired": {
"value": false,
"description": "This tool is available in both local and remote server modes."
}
},
"mappedToolList": [
"resilience_drill_start",
"resilience_drill_end"
]
},
{
"name": "get_azure_subscriptions_and_resource_groups",
"description": "Get information about Azure subscriptions, resource groups, and resources within resource groups that the user has access to.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Mcp.Core.Commands;

namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills;

internal static class DrillActionValidation
{
private static readonly HashSet<string> s_modes = new(StringComparer.OrdinalIgnoreCase)
{
"Failover",
"TestFailover"
};

private static readonly HashSet<string> s_attestations = new(StringComparer.OrdinalIgnoreCase)
{
"Success",
"Failed"
};

public static void ValidateResourceNames(string serviceGroup, string drill, ValidationResult validationResult)
{
if (serviceGroup.Length is < 1 or > 90 || !serviceGroup.All(IsValidServiceGroupNameCharacter))
{
validationResult.Errors.Add("The service group name must be 1 to 90 characters and contain only ASCII letters, numbers, hyphens, underscores, periods, or parentheses.");
}

if (drill.Length is < 1 or > 260 || !drill.All(IsValidDrillNameCharacter))
{
validationResult.Errors.Add("The drill name must be 1 to 260 characters and contain only ASCII letters, numbers, hyphens, underscores, or periods.");
}
}

public static void ValidateMode(string mode, ValidationResult validationResult)
{
if (!s_modes.Contains(mode))
{
validationResult.Errors.Add("The drill mode must be either Failover or TestFailover.");
}
}

public static void ValidateAttestation(string attestation, ValidationResult validationResult)
{
if (!s_attestations.Contains(attestation))
{
validationResult.Errors.Add("The drill attestation must be either Success or Failed.");
}
}

public static string NormalizeMode(string mode) =>
mode.Equals("Failover", StringComparison.OrdinalIgnoreCase) ? "Failover" : "TestFailover";

public static string NormalizeAttestation(string attestation) =>
attestation.Equals("Success", StringComparison.OrdinalIgnoreCase) ? "Success" : "Failed";

private static bool IsValidDrillNameCharacter(char character) =>
character is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9' or '-' or '_' or '.';

private static bool IsValidServiceGroupNameCharacter(char character) =>
IsValidDrillNameCharacter(character) || character is '(' or ')';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Tools.ResilienceManagement.Options.Drills;
using Azure.Mcp.Tools.ResilienceManagement.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills;

[CommandMetadata(
Id = "d0c5ad0f-e4e8-423a-92cb-faa0af84a599",
Name = "end",
Title = "End Resilience Drill",
Description = "Ends or stops a running resilience drill in a service group. Attests the drill as Success or Failed and records attestation notes.",
Destructive = true,
Idempotent = false,
OpenWorld = false,
ReadOnly = false,
Secret = false,
LocalRequired = false)]
public sealed class DrillEndCommand(ILogger<DrillEndCommand> logger, IResilienceManagementService resilienceManagementService)
: AuthenticatedCommand<DrillEndOptions, DrillEndCommand.DrillEndCommandResult>
{
private readonly ILogger<DrillEndCommand> _logger = logger;
private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService;

public override void ValidateOptions(DrillEndOptions options, ValidationResult validationResult)
{
base.ValidateOptions(options, validationResult);
DrillActionValidation.ValidateResourceNames(options.ServiceGroup, options.Drill, validationResult);
DrillActionValidation.ValidateAttestation(options.Attestation, validationResult);
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, DrillEndOptions options, CancellationToken cancellationToken)
{
try
{
string operationId = await _resilienceManagementService.EndDrillAsync(
options.ServiceGroup,
options.Drill,
DrillActionValidation.NormalizeAttestation(options.Attestation),
options.AttestationNotes,
options.Tenant,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(
new DrillEndCommandResult(operationId, options.Drill, "Accepted"),
ResilienceManagementJsonContext.Default.DrillEndCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error ending drill. ServiceGroup: {ServiceGroup}, Drill: {Drill}, Attestation: {Attestation}.",
options.ServiceGroup, options.Drill, options.Attestation);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict =>
"The drill cannot be ended in its current state. Verify a drill execution is active.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
"Authorization failed ending the drill. Verify you have permission to run drills in the service group.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"The drill was not found. Verify the drill and service group exist and you have access.",
RequestFailedException =>
"The drill end request failed. Verify the drill, service group, and attestation, then try again.",
_ => base.GetErrorMessage(ex)
};

public sealed record DrillEndCommandResult(string OperationId, string Drill, string Status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Tools.ResilienceManagement.Options.Drills;
using Azure.Mcp.Tools.ResilienceManagement.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Mcp.Core.Commands;
using Microsoft.Mcp.Core.Models.Command;

namespace Azure.Mcp.Tools.ResilienceManagement.Commands.Drills;

[CommandMetadata(
Id = "7de91784-03ba-47e9-8cac-73172828a382",
Name = "start",
Title = "Start Resilience Drill",
Description = "Starts, runs, or executes a resilience drill in Failover or TestFailover mode. Use this command to begin a new drill execution, not to get or list drill definitions. Returns the operation ID for the accepted asynchronous request.",
Destructive = true,
Idempotent = false,
OpenWorld = false,
ReadOnly = false,
Secret = false,
LocalRequired = false)]
public sealed class DrillStartCommand(ILogger<DrillStartCommand> logger, IResilienceManagementService resilienceManagementService)
: AuthenticatedCommand<DrillStartOptions, DrillStartCommand.DrillStartCommandResult>
{
private readonly ILogger<DrillStartCommand> _logger = logger;
private readonly IResilienceManagementService _resilienceManagementService = resilienceManagementService;

public override void ValidateOptions(DrillStartOptions options, ValidationResult validationResult)
{
base.ValidateOptions(options, validationResult);
DrillActionValidation.ValidateResourceNames(options.ServiceGroup, options.Drill, validationResult);
DrillActionValidation.ValidateMode(options.Mode, validationResult);
}

public override async Task<CommandResponse> ExecuteAsync(CommandContext context, DrillStartOptions options, CancellationToken cancellationToken)
{
try
{
string operationId = await _resilienceManagementService.StartDrillAsync(
options.ServiceGroup,
options.Drill,
DrillActionValidation.NormalizeMode(options.Mode),
options.Tenant,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(
new DrillStartCommandResult(operationId, options.Drill, "Accepted"),
ResilienceManagementJsonContext.Default.DrillStartCommandResult);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error starting drill. ServiceGroup: {ServiceGroup}, Drill: {Drill}, Mode: {Mode}.",
options.ServiceGroup, options.Drill, options.Mode);
HandleException(context, ex);
}

return context.Response;
}

protected override string GetErrorMessage(Exception ex) => ex switch
{
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Conflict =>
"The drill cannot be started in its current state. Verify no drill execution is already active.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.Forbidden =>
"Authorization failed starting the drill. Verify you have permission to run drills in the service group.",
RequestFailedException reqEx when reqEx.Status == (int)HttpStatusCode.NotFound =>
"The drill was not found. Verify the drill and service group exist and you have access.",
RequestFailedException =>
"The drill start request failed. Verify the drill, service group, and mode, then try again.",
_ => base.GetErrorMessage(ex)
};

public sealed record DrillStartCommandResult(string OperationId, string Drill, string Status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ namespace Azure.Mcp.Tools.ResilienceManagement.Commands;
[JsonSerializable(typeof(DrillResourceInfo))]
[JsonSerializable(typeof(DrillCreateCommand.DrillCreateCommandResult))]
[JsonSerializable(typeof(DrillGetCommand.DrillGetCommandResult))]
[JsonSerializable(typeof(DrillStartCommand.DrillStartCommandResult))]
[JsonSerializable(typeof(DrillEndCommand.DrillEndCommandResult))]
[JsonSerializable(typeof(DrillUpdateCommand.DrillUpdateCommandResult))]
[JsonSerializable(typeof(DrillDeleteCommand.DrillDeleteCommandResult))]
[JsonSerializable(typeof(DrillResourceGetCommand.DrillResourceGetCommandResult))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Options;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills;

public sealed class DrillEndOptions
{
[Option(Description = ResilienceManagementOptionDescriptions.ServiceGroup)]
public required string ServiceGroup { get; set; }

[Option(Description = "The name of the running resilience drill to end.")]
public required string Drill { get; set; }

[Option(Description = "The outcome attested when ending the drill. Allowed values: Success, Failed.")]
public required string Attestation { get; set; }

[Option(Description = "Notes explaining the attested drill outcome.")]
public required string AttestationNotes { get; set; }

[Option(Description = OptionDescriptions.Tenant)]
public string? Tenant { get; set; }

[OptionContainer(Prefix = "retry")]
public RetryPolicyOptions? RetryPolicy { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Options;
using Microsoft.Mcp.Core.Options;

namespace Azure.Mcp.Tools.ResilienceManagement.Options.Drills;

public sealed class DrillStartOptions
{
[Option(Description = ResilienceManagementOptionDescriptions.ServiceGroup)]
public required string ServiceGroup { get; set; }

[Option(Description = "The name of the resilience drill to start.")]
public required string Drill { get; set; }

[Option(Description = "The drill execution mode. Allowed values: Failover, TestFailover.")]
public required string Mode { get; set; }

[Option(Description = OptionDescriptions.Tenant)]
public string? Tenant { get; set; }

[OptionContainer(Prefix = "retry")]
public RetryPolicyOptions? RetryPolicy { get; set; }
}
Loading
Loading