From ac6046e2ae9ed7f08042b3adfcc67711692689cd Mon Sep 17 00:00:00 2001 From: hardikl Date: Wed, 15 Apr 2026 17:21:37 +0530 Subject: [PATCH 1/3] feat: adding svm update tool to rename, state, comment --- descriptions/descriptions.go | 1 + docs/examples.md | 28 ++++++++ integration/test/svm_test.go | 122 +++++++++++++++++++++++++++++++++++ ontap/ontap.go | 4 +- rest/svm.go | 40 ++++++++++++ server/server.go | 1 + server/svm.go | 55 +++++++++++++++- tool/tool.go | 8 +++ 8 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 integration/test/svm_test.go diff --git a/descriptions/descriptions.go b/descriptions/descriptions.go index e375a01..1fbaaf2 100644 --- a/descriptions/descriptions.go +++ b/descriptions/descriptions.go @@ -113,6 +113,7 @@ const DescribeOntapEndpoint = `Get filterable query params for an endpoint. Call Pass cluster_name to automatically filter out fields and filters not available in that cluster's ONTAP version.` const CreateSVM = `Create an SVM on a cluster by cluster name.` +const UpdateSVM = `Update an SVM on a cluster by cluster name.` const DeleteSVM = `Delete an SVM on a cluster by cluster name.` const OntapGet = `Execute a read-only GET against any ONTAP REST endpoint. diff --git a/docs/examples.md b/docs/examples.md index ba85471..b6c199a 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -345,6 +345,34 @@ Expected Response: LUN has been deleted successfully. --- +### SVM Provisioning + +**Create a SVM** + +- On the umeng-aff300-05-06 cluster, create marketing svm + +Expected Response: SVM has been created successfully. + +**Rename a SVM** + +- On the umeng-aff300-05-06 cluster, rename svm marketing to marketingNew + +Expected Response: SVM has been updated successfully. + +**Update a SVM** + +- On the umeng-aff300-05-06 cluster, update svm marketingNew state to stopped and comment as `stop_svm` + +Expected Response: SVM has been updated successfully. + +**Delete a SVM** + +- On the umeng-aff300-05-06 cluster, delete marketingNew svm + +Expected Response: SVM has been deleted successfully. + +--- + ### Querying Specific Fields **Get volume space and protection details** diff --git a/integration/test/svm_test.go b/integration/test/svm_test.go new file mode 100644 index 0000000..e7b4622 --- /dev/null +++ b/integration/test/svm_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "context" + "crypto/tls" + "github.com/carlmjohnson/requests" + "log/slog" + "net/http" + "testing" + "time" + + "github.com/netapp/ontap-mcp/config" +) + +func TestSVM(t *testing.T) { + SkipIfMissing(t, CheckTools) + + tests := []struct { + name string + input string + expectedOntapErr string + verifyAPI ontapVerifier + }{ + { + name: "Clean SVM", + input: ClusterStr + "delete " + rn("marketing") + " svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject}, + }, + { + name: "Create SVM", + input: ClusterStr + "create " + rn("marketing") + " svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: createObject}, + }, + { + name: "Rename SVM", + input: ClusterStr + "rename svm " + rn("marketing") + " to " + rn("marketingNew"), + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew"), validationFunc: createObject}, + }, + { + name: "Update SVM", + input: ClusterStr + "update svm " + rn("marketingNew") + " state to stopped and comment as `stop_svm`", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew") + "&fields=state,comment", validationFunc: verifySVM("stopped", "stop_svm")}, + }, + { + name: "Clean SVM", + input: ClusterStr + "delete " + rn("marketingNew") + " svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew"), validationFunc: deleteObject}, + }, + } + + cfg, err := config.ReadConfig(ConfigFile) + if err != nil { + t.Fatalf("Error parsing the config: %v", err) + } + + poller := cfg.Pollers[Cluster] + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: poller.UseInsecureTLS, // #nosec G402 + }, + } + client := &http.Client{Transport: transport, Timeout: 10 * time.Second} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slog.Debug("", slog.String("Input", tt.input)) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + if _, err := testAgent.ChatWithResponse(ctx, t, tt.input, tt.expectedOntapErr); err != nil { + t.Fatalf("Error processing input %q: %v", tt.input, err) + } + if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) { + t.Errorf("Error while accessing the object via prompt %q", tt.input) + } + }) + } +} + +func verifySVM(expectedState string, expectedComment string) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool { + return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool { + type SVM struct { + Name string `json:"name"` + State string `json:"state"` + Comment string `json:"comment"` + } + type response struct { + NumRecords int `json:"num_records"` + Records []SVM `json:"records"` + } + + var data response + err := requests.URL("https://"+poller.Addr+"/"+api). + BasicAuth(poller.Username, poller.Password). + Client(client). + ToJSON(&data). + Fetch(context.Background()) + if err != nil { + t.Errorf("verifySVM: request failed: %v", err) + return false + } + if data.NumRecords != 1 { + t.Errorf("verifySVM: expected 1 record, got %d", data.NumRecords) + return false + } + + gotSVM := data.Records[0] + if gotSVM.State != expectedState { + t.Errorf("verifySVM: got state = %s, want %s", gotSVM.State, expectedState) + return false + } + if gotSVM.Comment != expectedComment { + t.Errorf("verifySVM: got comment = %s, want %s", gotSVM.Comment, expectedComment) + return false + } + return true + } +} diff --git a/ontap/ontap.go b/ontap/ontap.go index 85b4c62..ddeb09e 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -199,7 +199,9 @@ type CIFSShare struct { } type SVM struct { - Name string `json:"name" jsonschema:"svm name"` + Name string `json:"name" jsonschema:"svm name"` + State string `json:"state,omitzero" jsonschema:"svm state"` // enum: starting, running, stopping, stopped, deleting, initializing + Comment string `json:"comment,omitzero" jsonschema:"comment"` } type LUNSpace struct { diff --git a/rest/svm.go b/rest/svm.go index 0bfdbb3..c87e6b7 100644 --- a/rest/svm.go +++ b/rest/svm.go @@ -27,6 +27,46 @@ func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error { return c.handleJob(ctx, statusCode, buf) } +func (c *Client) UpdateSVM(ctx context.Context, svm ontap.SVM, svmName string) error { + var ( + buf bytes.Buffer + statusCode int + svmData ontap.GetData + ) + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("name", svmName) + params.Set("fields", "uuid") + + builder := c.baseRequestBuilder(`/api/svm/svms`, &statusCode, responseHeaders). + Params(params). + ToJSON(&svmData) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + if svmData.NumRecords == 0 { + return fmt.Errorf("failed to get details of SVM %s because it does not exist", svmName) + } + if svmData.NumRecords != 1 { + return fmt.Errorf("failed to get detail of SVM %s because there are %d matching records", + svmName, svmData.NumRecords) + } + + builder2 := c.baseRequestBuilder(`/api/svm/svms/`+svmData.Records[0].UUID, &statusCode, responseHeaders). + Patch(). + ToBytesBuffer(&buf). + BodyJSON(svm) + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.handleJob(ctx, statusCode, buf) +} + func (c *Client) DeleteSVM(ctx context.Context, svmName string) error { var ( buf bytes.Buffer diff --git a/server/server.go b/server/server.go index a44939b..9c0dbd1 100644 --- a/server/server.go +++ b/server/server.go @@ -120,6 +120,7 @@ func (a *App) createMCPServer() *mcp.Server { // operation on SVM object addTool(a, server, "create_svm", descriptions.CreateSVM, createAnnotation, a.CreateSVM) + addTool(a, server, "update_svm", descriptions.UpdateSVM, updateAnnotation, a.UpdateSVM) addTool(a, server, "delete_svm", descriptions.DeleteSVM, deleteAnnotation, a.DeleteSVM) // operation on CIFS share object diff --git a/server/svm.go b/server/svm.go index 1f7656d..47bbbf3 100644 --- a/server/svm.go +++ b/server/svm.go @@ -9,7 +9,7 @@ import ( "github.com/netapp/ontap-mcp/tool" ) -func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*mcp.CallToolResult, any, error) { +func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVMCreate) (*mcp.CallToolResult, any, error) { if !a.locks.TryLock(parameters.Cluster) { return errorResult(fmt.Errorf("another write operation is in progress on cluster %s, please try again", parameters.Cluster)), nil, nil } @@ -37,6 +37,34 @@ func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters }, nil, nil } +func (a *App) UpdateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*mcp.CallToolResult, any, error) { + if !a.locks.TryLock(parameters.Cluster) { + return errorResult(fmt.Errorf("another write operation is in progress on cluster %s, please try again", parameters.Cluster)), nil, nil + } + defer a.locks.Unlock(parameters.Cluster) + + svmUpdate, err := newUpdateSVM(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + err = client.UpdateSVM(ctx, svmUpdate, parameters.Name) + if err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "SVM updated successfully"}, + }, + }, nil, nil +} + func (a *App) DeleteSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*mcp.CallToolResult, any, error) { if !a.locks.TryLock(parameters.Cluster) { return errorResult(fmt.Errorf("another write operation is in progress on cluster %s, please try again", parameters.Cluster)), nil, nil @@ -64,7 +92,7 @@ func (a *App) DeleteSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters }, nil, nil } -func newCreateSVM(in tool.SVM) (ontap.SVM, error) { +func newCreateSVM(in tool.SVMCreate) (ontap.SVM, error) { out := ontap.SVM{} if in.Name == "" { return out, errors.New("SVM name is required") @@ -72,3 +100,26 @@ func newCreateSVM(in tool.SVM) (ontap.SVM, error) { out.Name = in.Name return out, nil } + +func newUpdateSVM(in tool.SVM) (ontap.SVM, error) { + out := ontap.SVM{} + + hasUpdate := false + if in.NewName != "" { + out.Name = in.NewName + hasUpdate = true + } + if in.Comment != "" { + out.Comment = in.Comment + hasUpdate = true + } + if in.State != "" { + out.State = in.State + hasUpdate = true + } + if !hasUpdate { + return out, errors.New("at least one updatable field must be provided: new_name, comment, or state") + } + + return out, nil +} diff --git a/tool/tool.go b/tool/tool.go index 55e5496..1046ab7 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -214,7 +214,15 @@ type DescribeEndpointParams struct { Cluster string `json:"cluster_name,omitzero" jsonschema:"cluster name — if provided, filters out fields and filters not available in that cluster's ONTAP version"` } +type SVMCreate struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + Name string `json:"svm_name" jsonschema:"SVM name"` +} + type SVM struct { Cluster string `json:"cluster_name" jsonschema:"cluster name"` Name string `json:"svm_name" jsonschema:"SVM name"` + NewName string `json:"new_name,omitzero" jsonschema:"new name of SVM"` + State string `json:"state,omitzero" jsonschema:"state of SVM (e.g., starting, running, stopping, stopped, deleting, initializing)"` + Comment string `json:"comment,omitzero" jsonschema:"comment"` } From 9102a34b4c7c7537f5379cbca163427419085b9e Mon Sep 17 00:00:00 2001 From: hardikl Date: Wed, 15 Apr 2026 17:46:51 +0530 Subject: [PATCH 2/3] feat: handled copilot comments --- docs/examples.md | 8 ++++---- integration/test/svm_test.go | 3 +-- ontap/ontap.go | 5 ++++- rest/svm.go | 2 +- server/svm.go | 8 ++++++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index b6c199a..9317476 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -351,25 +351,25 @@ Expected Response: LUN has been deleted successfully. - On the umeng-aff300-05-06 cluster, create marketing svm -Expected Response: SVM has been created successfully. +Expected Response: SVM created successfully. **Rename a SVM** - On the umeng-aff300-05-06 cluster, rename svm marketing to marketingNew -Expected Response: SVM has been updated successfully. +Expected Response: SVM updated successfully. **Update a SVM** - On the umeng-aff300-05-06 cluster, update svm marketingNew state to stopped and comment as `stop_svm` -Expected Response: SVM has been updated successfully. +Expected Response: SVM updated successfully. **Delete a SVM** - On the umeng-aff300-05-06 cluster, delete marketingNew svm -Expected Response: SVM has been deleted successfully. +Expected Response: SVM deleted successfully. --- diff --git a/integration/test/svm_test.go b/integration/test/svm_test.go index e7b4622..c72705f 100644 --- a/integration/test/svm_test.go +++ b/integration/test/svm_test.go @@ -4,12 +4,11 @@ import ( "context" "crypto/tls" "github.com/carlmjohnson/requests" + "github.com/netapp/ontap-mcp/config" "log/slog" "net/http" "testing" "time" - - "github.com/netapp/ontap-mcp/config" ) func TestSVM(t *testing.T) { diff --git a/ontap/ontap.go b/ontap/ontap.go index ddeb09e..9dd12a8 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -198,8 +198,11 @@ type CIFSShare struct { Path string `json:"path,omitzero" jsonschema:"cifs share path"` } +type SVMCreate struct { + Name string `json:"name" jsonschema:"svm name"` +} type SVM struct { - Name string `json:"name" jsonschema:"svm name"` + Name string `json:"name,omitzero" jsonschema:"svm name"` State string `json:"state,omitzero" jsonschema:"svm state"` // enum: starting, running, stopping, stopped, deleting, initializing Comment string `json:"comment,omitzero" jsonschema:"comment"` } diff --git a/rest/svm.go b/rest/svm.go index c87e6b7..bf87e5d 100644 --- a/rest/svm.go +++ b/rest/svm.go @@ -9,7 +9,7 @@ import ( "net/url" ) -func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error { +func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVMCreate) error { var ( buf bytes.Buffer statusCode int diff --git a/server/svm.go b/server/svm.go index 47bbbf3..d32ccc8 100644 --- a/server/svm.go +++ b/server/svm.go @@ -92,8 +92,8 @@ func (a *App) DeleteSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters }, nil, nil } -func newCreateSVM(in tool.SVMCreate) (ontap.SVM, error) { - out := ontap.SVM{} +func newCreateSVM(in tool.SVMCreate) (ontap.SVMCreate, error) { + out := ontap.SVMCreate{} if in.Name == "" { return out, errors.New("SVM name is required") } @@ -104,6 +104,10 @@ func newCreateSVM(in tool.SVMCreate) (ontap.SVM, error) { func newUpdateSVM(in tool.SVM) (ontap.SVM, error) { out := ontap.SVM{} + if in.Name == "" { + return out, errors.New("SVM name is required") + } + hasUpdate := false if in.NewName != "" { out.Name = in.NewName From 5b68e4718608c1378b27d207468cdd2d2ab92871 Mon Sep 17 00:00:00 2001 From: hardikl Date: Wed, 15 Apr 2026 20:40:46 +0530 Subject: [PATCH 3/3] feat: minor change --- integration/test/svm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/svm_test.go b/integration/test/svm_test.go index c72705f..cac15d1 100644 --- a/integration/test/svm_test.go +++ b/integration/test/svm_test.go @@ -47,7 +47,7 @@ func TestSVM(t *testing.T) { { name: "Clean SVM", input: ClusterStr + "delete " + rn("marketingNew") + " svm", - expectedOntapErr: "because it does not exist", + expectedOntapErr: "", verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew"), validationFunc: deleteObject}, }, }