Skip to content

Commit 14e1d51

Browse files
authored
Merge pull request #153 from Azure/guwe/merge-vm-tools
Feat: support more vm/vmss operations
2 parents 514c0d8 + fecab94 commit 14e1d51

File tree

6 files changed

+482
-108
lines changed

6 files changed

+482
-108
lines changed
Lines changed: 177 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,203 @@
11
package compute
22

33
import (
4-
"github.com/Azure/aks-mcp/internal/utils"
4+
"fmt"
5+
"slices"
6+
7+
"github.com/Azure/aks-mcp/internal/config"
58
"github.com/mark3labs/mcp-go/mcp"
69
)
710

8-
// ComputeCommand defines a specific az vmss command to be registered as a tool
9-
type ComputeCommand struct {
10-
Name string
11-
Description string
12-
ArgsExample string // Example of command arguments
13-
}
11+
// ComputeOperationType defines the type of compute operation
12+
type ComputeOperationType string
13+
14+
// ResourceType defines the compute resource type
15+
type ResourceType string
16+
17+
const (
18+
// VM operations - safe operations only
19+
OpVMShow ComputeOperationType = "show"
20+
OpVMList ComputeOperationType = "list"
21+
OpVMStart ComputeOperationType = "start"
22+
OpVMStop ComputeOperationType = "stop"
23+
OpVMRestart ComputeOperationType = "restart"
24+
OpVMGetInstanceView ComputeOperationType = "get-instance-view"
25+
OpVMRunCommand ComputeOperationType = "run-command"
26+
27+
// VMSS operations - only safe operations for AKS-managed VMSS
28+
OpVMSSShow ComputeOperationType = "show"
29+
OpVMSSList ComputeOperationType = "list"
30+
OpVMSSRestart ComputeOperationType = "restart"
31+
OpVMSSReimage ComputeOperationType = "reimage"
32+
OpVMSSGetInstanceView ComputeOperationType = "get-instance-view"
33+
OpVMSSRunCommand ComputeOperationType = "run-command"
34+
35+
// Resource types
36+
ResourceTypeVM ResourceType = "vm"
37+
ResourceTypeVMSS ResourceType = "vmss"
38+
)
39+
40+
// generateToolDescription creates a tool description based on access level
41+
func generateToolDescription(accessLevel string) string {
42+
baseDesc := `Unified tool for managing Azure Virtual Machines (VMs) and Virtual Machine Scale Sets (VMSS) using Azure CLI.
43+
44+
IMPORTANT: VM/VMSS resources are managed by AKS. Write operations should be used carefully and only for debugging purposes.
45+
46+
Use resource_type="vm" for single virtual machines or resource_type="vmss" for virtual machine scale sets.
47+
48+
Available operation values:`
1449

15-
// RegisterAzComputeCommand registers a specific az vmss command as an MCP tool
16-
func RegisterAzComputeCommand(cmd ComputeCommand) mcp.Tool {
17-
// Convert spaces to underscores for valid tool name
18-
commandName := cmd.Name
19-
validToolName := utils.ReplaceSpacesWithUnderscores(commandName)
50+
// Add operations by access level
51+
desc := baseDesc + "\n"
2052

21-
description := "Run " + cmd.Name + " command: " + cmd.Description + "."
53+
// Basic operations for all access levels
54+
desc += "- show: Get details of a VM/VMSS\n"
55+
desc += "- list: List VMs/VMSS in subscription or resource group\n"
56+
desc += "- get-instance-view: Get runtime status\n"
2257

23-
// Add example if available, with proper punctuation
24-
if cmd.ArgsExample != "" {
25-
description += "\nExample: `" + cmd.ArgsExample + "`"
58+
// Management operations for readwrite/admin
59+
if accessLevel == "readwrite" || accessLevel == "admin" {
60+
desc += "- start: Start VM\n"
61+
desc += "- stop: Stop VM\n"
62+
desc += "- restart: Restart VM/VMSS instances\n"
63+
desc += "- run-command: Execute commands remotely on VM/VMSS instances\n"
64+
desc += "- reimage: Reimage VMSS instances (VM not supported for reimage)\n"
2665
}
2766

28-
return mcp.NewTool(validToolName,
67+
// Note: All destructive operations (create, delete, deallocate, update, resize, scale)
68+
// have been removed for AKS environment safety
69+
70+
// Examples
71+
desc += "\nEXAMPLES:\n"
72+
desc += `List VMSS: operation="list", resource_type="vmss", args="--resource-group myRG"` + "\n"
73+
desc += `Show VMSS: operation="show", resource_type="vmss", args="--name myVMSS --resource-group myRG"` + "\n"
74+
desc += `List VMs: operation="list", resource_type="vm", args="--resource-group myRG"` + "\n"
75+
76+
if accessLevel == "readwrite" || accessLevel == "admin" {
77+
desc += `Restart VMSS: operation="restart", resource_type="vmss", args="--name myVMSS --resource-group myRG"` + "\n"
78+
desc += `Reimage VMSS: operation="reimage", resource_type="vmss", args="--name myVMSS --resource-group myRG"` + "\n"
79+
desc += `Run command on VM: operation="run-command", resource_type="vm", args="--name myVM --resource-group myRG --command-id RunShellScript --scripts 'echo hello'"` + "\n"
80+
desc += `Run command on VMSS: operation="run-command", resource_type="vmss", args="--name myVMSS --resource-group myRG --command-id RunShellScript --scripts 'hostname' --instance-id 0"` + "\n"
81+
}
82+
83+
return desc
84+
}
85+
86+
// RegisterAzComputeOperations registers the unified compute operations tool
87+
func RegisterAzComputeOperations(cfg *config.ConfigData) mcp.Tool {
88+
description := generateToolDescription(cfg.AccessLevel)
89+
90+
return mcp.NewTool("az_compute_operations",
2991
mcp.WithDescription(description),
92+
mcp.WithString("operation",
93+
mcp.Required(),
94+
mcp.Description("Operation to perform. Common operations: list, show, start, stop, restart, deallocate, run-command, scale, etc."),
95+
),
96+
mcp.WithString("resource_type",
97+
mcp.Required(),
98+
mcp.Description("Resource type: 'vm' (single virtual machine) or 'vmss' (virtual machine scale set)"),
99+
),
30100
mcp.WithString("args",
31101
mcp.Required(),
32-
mcp.Description("Arguments for the `"+cmd.Name+"` command"),
102+
mcp.Description("Azure CLI arguments: '--resource-group myRG' (required for most operations), '--name myVM' (for specific resources), '--new-capacity 3' (for scaling)"),
33103
),
34104
)
35105
}
36106

37-
// GetReadOnlyVmssCommands returns all read-only az vmss commands
38-
func GetReadOnlyVmssCommands() []ComputeCommand {
39-
return []ComputeCommand{
40-
// No read-only commands for now
107+
// GetOperationAccessLevel returns the required access level for an operation
108+
func GetOperationAccessLevel(operation string) string {
109+
readOnlyOps := []string{
110+
string(OpVMShow), string(OpVMList), string(OpVMGetInstanceView),
111+
string(OpVMSSShow), string(OpVMSSList), string(OpVMSSGetInstanceView),
112+
}
113+
114+
readWriteOps := []string{
115+
// VM operations - safe operations only
116+
string(OpVMStart), string(OpVMStop), string(OpVMRestart), string(OpVMRunCommand),
117+
// VMSS operations - only safe operations for AKS-managed VMSS
118+
string(OpVMSSRestart), string(OpVMSSReimage), string(OpVMSSRunCommand),
119+
}
120+
121+
// No admin operations - all unsafe operations removed
122+
adminOps := []string{}
123+
124+
if slices.Contains(readOnlyOps, operation) {
125+
return "readonly"
126+
}
127+
128+
if slices.Contains(readWriteOps, operation) {
129+
return "readwrite"
130+
}
131+
132+
if slices.Contains(adminOps, operation) {
133+
return "admin"
41134
}
135+
136+
return "unknown"
42137
}
43138

44-
// GetReadWriteVmssCommands returns all read-write az vmss commands
45-
func GetReadWriteVmssCommands() []ComputeCommand {
46-
return []ComputeCommand{
47-
// Run command execution
48-
{Name: "az vmss run-command invoke", Description: "Execute a command on instances of a Virtual Machine Scale Set", ArgsExample: "--name myVMSS --resource-group myResourceGroup --command-id RunShellScript --scripts 'echo Hello World' --instance-id 0"},
139+
// ValidateOperationAccess checks if the operation is allowed for the given access level
140+
func ValidateOperationAccess(operation string, cfg *config.ConfigData) error {
141+
requiredLevel := GetOperationAccessLevel(operation)
142+
143+
switch requiredLevel {
144+
case "admin":
145+
if cfg.AccessLevel != "admin" {
146+
return fmt.Errorf("operation '%s' requires admin access level", operation)
147+
}
148+
case "readwrite":
149+
if cfg.AccessLevel != "readwrite" && cfg.AccessLevel != "admin" {
150+
return fmt.Errorf("operation '%s' requires readwrite or admin access level", operation)
151+
}
152+
case "readonly":
153+
// All access levels can perform readonly operations
154+
case "unknown":
155+
return fmt.Errorf("unknown operation: %s", operation)
49156
}
157+
158+
return nil
50159
}
51160

52-
// GetAdminVmssCommands returns all admin az vmss commands
53-
func GetAdminVmssCommands() []ComputeCommand {
54-
return []ComputeCommand{
55-
// No admin commands for now
161+
// MapOperationToCommand maps an operation and resource type to its corresponding az command
162+
func MapOperationToCommand(operation string, resourceType string) (string, error) {
163+
// Validate resource type
164+
if resourceType != string(ResourceTypeVM) && resourceType != string(ResourceTypeVMSS) {
165+
return "", fmt.Errorf("invalid resource type: %s (must be 'vm' or 'vmss')", resourceType)
166+
}
167+
168+
commandMap := map[string]map[string]string{
169+
string(ResourceTypeVM): {
170+
// Safe VM operations only
171+
string(OpVMShow): "az vm show",
172+
string(OpVMList): "az vm list",
173+
string(OpVMStart): "az vm start",
174+
string(OpVMStop): "az vm stop",
175+
string(OpVMRestart): "az vm restart",
176+
string(OpVMGetInstanceView): "az vm get-instance-view",
177+
string(OpVMRunCommand): "az vm run-command invoke",
178+
},
179+
string(ResourceTypeVMSS): {
180+
// Read-only operations
181+
string(OpVMSSShow): "az vmss show",
182+
string(OpVMSSList): "az vmss list",
183+
string(OpVMSSGetInstanceView): "az vmss get-instance-view",
184+
// Safe operations for AKS-managed VMSS
185+
string(OpVMSSRestart): "az vmss restart",
186+
string(OpVMSSReimage): "az vmss reimage",
187+
string(OpVMSSRunCommand): "az vmss run-command invoke",
188+
// Removed unsafe operations: create, delete, start, stop, deallocate, scale, update
189+
},
56190
}
191+
192+
resourceCommands, exists := commandMap[resourceType]
193+
if !exists {
194+
return "", fmt.Errorf("unsupported resource type: %s", resourceType)
195+
}
196+
197+
cmd, exists := resourceCommands[operation]
198+
if !exists {
199+
return "", fmt.Errorf("unsupported operation '%s' for resource type '%s'", operation, resourceType)
200+
}
201+
202+
return cmd, nil
57203
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package compute
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/Azure/aks-mcp/internal/command"
8+
"github.com/Azure/aks-mcp/internal/config"
9+
"github.com/Azure/aks-mcp/internal/security"
10+
)
11+
12+
// ComputeOperationsExecutor handles execution of compute operations
13+
type ComputeOperationsExecutor struct{}
14+
15+
// NewComputeOperationsExecutor creates a new ComputeOperationsExecutor
16+
func NewComputeOperationsExecutor() *ComputeOperationsExecutor {
17+
return &ComputeOperationsExecutor{}
18+
}
19+
20+
// Execute handles the compute operations
21+
func (e *ComputeOperationsExecutor) Execute(params map[string]interface{}, cfg *config.ConfigData) (string, error) {
22+
// Parse operation parameter
23+
operation, ok := params["operation"].(string)
24+
if !ok {
25+
return "", fmt.Errorf("missing or invalid 'operation' parameter. Common operations: list, show, start, stop, restart, run-command, reimage. Example: operation=\"list\"")
26+
}
27+
28+
// Parse resource_type parameter
29+
resourceType, ok := params["resource_type"].(string)
30+
if !ok {
31+
return "", fmt.Errorf("missing or invalid 'resource_type' parameter. Must be 'vm' (Virtual Machine) or 'vmss' (Virtual Machine Scale Set). Example: resource_type=\"vm\"")
32+
}
33+
34+
// Parse args parameter
35+
args, ok := params["args"].(string)
36+
if !ok {
37+
args = ""
38+
}
39+
40+
// Validate access for this operation
41+
if err := ValidateOperationAccess(operation, cfg); err != nil {
42+
// Enhance access error with suggestions
43+
requiredLevel := GetOperationAccessLevel(operation)
44+
return "", fmt.Errorf("%v. Your current access level is '%s', but this operation requires '%s' access. Contact your administrator to request higher access", err, cfg.AccessLevel, requiredLevel)
45+
}
46+
47+
// Map operation to Azure CLI command
48+
baseCommand, err := MapOperationToCommand(operation, resourceType)
49+
if err != nil {
50+
// Provide helpful suggestions for invalid operations
51+
validOps := getSuggestedOperations(resourceType, cfg.AccessLevel)
52+
return "", fmt.Errorf("%v. Valid operations for %s with %s access: %s", err, resourceType, cfg.AccessLevel, validOps)
53+
}
54+
55+
// Build full command
56+
fullCommand := baseCommand
57+
if args != "" {
58+
fullCommand += " " + args
59+
}
60+
61+
// Validate the command against security settings
62+
validator := security.NewValidator(cfg.SecurityConfig)
63+
err = validator.ValidateCommand(fullCommand, security.CommandTypeAz)
64+
if err != nil {
65+
return "", err
66+
}
67+
68+
// Extract binary name and arguments from command
69+
cmdParts := strings.Fields(fullCommand)
70+
if len(cmdParts) == 0 {
71+
return "", fmt.Errorf("empty command")
72+
}
73+
74+
// Use the first part as the binary name
75+
binaryName := cmdParts[0]
76+
77+
// The rest of the command becomes the arguments
78+
cmdArgs := ""
79+
if len(cmdParts) > 1 {
80+
cmdArgs = strings.Join(cmdParts[1:], " ")
81+
}
82+
83+
// If the command is not an az command, return an error
84+
if binaryName != "az" {
85+
return "", fmt.Errorf("command must start with 'az'")
86+
}
87+
88+
// Execute the command
89+
process := command.NewShellProcess(binaryName, cfg.Timeout)
90+
result, err := process.Run(cmdArgs)
91+
if err != nil {
92+
// Provide helpful error messages for common issues
93+
errorMsg := fmt.Sprintf("Azure CLI command failed: %v", err)
94+
95+
// Add contextual help based on the operation
96+
switch operation {
97+
case "list":
98+
errorMsg += "\nTip: For listing resources, try without --name parameter, or verify --resource-group exists"
99+
case "show":
100+
errorMsg += "\nTip: Verify the resource name and resource group are correct and the resource exists"
101+
case "start", "stop", "restart":
102+
errorMsg += "\nTip: Verify the resource exists and check if it's already in the desired state"
103+
case "reimage":
104+
errorMsg += "\nTip: Verify the VMSS name is correct and the instances are ready for reimaging"
105+
case "run-command":
106+
errorMsg += "\nTip: Ensure the resource is running and the command syntax is correct. Use --command-id RunShellScript for shell commands"
107+
}
108+
109+
return "", fmt.Errorf("%s\nExecuted command: %s", errorMsg, fullCommand)
110+
}
111+
112+
return strings.TrimSpace(result), nil
113+
}
114+
115+
// getSuggestedOperations returns a helpful list of valid operations for the given resource type and access level
116+
func getSuggestedOperations(resourceType, accessLevel string) string {
117+
var operations []string
118+
119+
// Read-only operations (available to all access levels)
120+
operations = append(operations, "list", "show", "get-instance-view")
121+
122+
// Read-write operations
123+
if accessLevel == "readwrite" || accessLevel == "admin" {
124+
switch resourceType {
125+
case "vm":
126+
// Only safe VM operations
127+
operations = append(operations, "start", "stop", "restart", "run-command")
128+
case "vmss":
129+
// Only safe operations for AKS-managed VMSS
130+
operations = append(operations, "restart", "reimage", "run-command")
131+
}
132+
}
133+
134+
// No admin operations - all unsafe operations removed for AKS safety
135+
136+
return strings.Join(operations, ", ")
137+
}

internal/components/compute/registry.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,3 @@ func RegisterAKSVMSSInfoTool() mcp.Tool {
2828
),
2929
)
3030
}
31-
32-
// VMSS Command Registration Functions
33-
34-
// RegisterAzVmssCommand registers a specific az vmss command as an MCP tool
35-
func RegisterAzVmssCommand(cmd ComputeCommand) mcp.Tool {
36-
return RegisterAzComputeCommand(cmd)
37-
}
38-
39-
// GetReadOnlyVmssAzCommands returns all read-only az vmss commands
40-
func GetReadOnlyVmssAzCommands() []ComputeCommand {
41-
return GetReadOnlyVmssCommands()
42-
}
43-
44-
// GetReadWriteVmssAzCommands returns all read-write az vmss commands
45-
func GetReadWriteVmssAzCommands() []ComputeCommand {
46-
return GetReadWriteVmssCommands()
47-
}
48-
49-
// GetAdminVmssAzCommands returns all admin az vmss commands
50-
func GetAdminVmssAzCommands() []ComputeCommand {
51-
return GetAdminVmssCommands()
52-
}

0 commit comments

Comments
 (0)