From 98d1667e4554f57d4503a043303ca10beb6b4747 Mon Sep 17 00:00:00 2001 From: hardikl Date: Fri, 10 Apr 2026 16:09:32 +0530 Subject: [PATCH] feat: add svm tools and consumed in iscsi tests --- descriptions/descriptions.go | 3 ++ integration/test/iscsi_test.go | 42 +++++++++++++------ ontap/ontap.go | 4 ++ rest/svm.go | 67 ++++++++++++++++++++++++++++++ server/server.go | 4 ++ server/svm.go | 74 ++++++++++++++++++++++++++++++++++ tool/tool.go | 5 +++ 7 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 rest/svm.go create mode 100644 server/svm.go diff --git a/descriptions/descriptions.go b/descriptions/descriptions.go index 2028510..a26cc49 100644 --- a/descriptions/descriptions.go +++ b/descriptions/descriptions.go @@ -94,6 +94,9 @@ const SearchOntapEndpoints = `Search the catalog by keyword across endpoint path const DescribeOntapEndpoint = `Get filterable query params for an endpoint. Call before ontap_get to learn valid filter names and which sub-objects need explicit fields (e.g. "space.*", "efficiency.*"). 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 DeleteSVM = `Delete an SVM on a cluster by cluster name.` + const OntapGet = `Execute a read-only GET against any ONTAP REST endpoint. RULES: diff --git a/integration/test/iscsi_test.go b/integration/test/iscsi_test.go index d7bdd99..7f1f71e 100644 --- a/integration/test/iscsi_test.go +++ b/integration/test/iscsi_test.go @@ -20,17 +20,29 @@ func TestIscsiProtocol(t *testing.T) { 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: "Clean iSCSI service", - input: ClusterStr + "delete iscsi service in marketing svm", + input: ClusterStr + "delete iscsi service in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: deleteObject}, }, { name: "Create iSCSI service", - input: ClusterStr + "create iscsi service target named alias " + rn("tgpath") + " on the marketing svm", + input: ClusterStr + "create iscsi service target named alias " + rn("tgpath") + " on the " + rn("marketing") + " svm", expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: createObject}, + verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: createObject}, }, { name: "Clean cluster scope network interface", @@ -40,7 +52,7 @@ func TestIscsiProtocol(t *testing.T) { }, { name: "Clean svm scope network interface", - input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm", + input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject}, }, @@ -52,7 +64,7 @@ func TestIscsiProtocol(t *testing.T) { }, { name: "Create svm scope network interface with ip", - input: ClusterStr + "create network interface named " + rn("svg1") + " in marketing svm with ip address 10.63.41.7 and netmask 18 on node umeng-aff300-06", + input: ClusterStr + "create network interface named " + rn("svg1") + " in " + rn("marketing") + " svm with ip address 10.63.41.7 and netmask 18 on node umeng-aff300-06", expectedOntapErr: "", verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: createObject}, }, @@ -64,33 +76,39 @@ func TestIscsiProtocol(t *testing.T) { }, { name: "Clean svm scope network interface", - input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm", + input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject}, }, { name: "Create svm scope network interface with broadcast domain", - input: ClusterStr + "create network interface named " + rn("svg1") + " in marketing svm with ip address 10.63.41.7 and netmask 18 on broadcast domain as Default", + input: ClusterStr + "create network interface named " + rn("svg1") + " in " + rn("marketing") + " svm with ip address 10.63.41.7 and netmask 18 on broadcast domain as Default", expectedOntapErr: "", verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: createObject}, }, { name: "Clean svm scope network interface with broadcast domain", - input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm", + input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject}, }, { name: "Update iSCSI service", - input: ClusterStr + "disabled iscsi service on the marketing svm", + input: ClusterStr + "disabled iscsi service on the " + rn("marketing") + " svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, { name: "Clean iSCSI service", - input: ClusterStr + "delete iscsi service in marketing svm", + input: ClusterStr + "delete iscsi service in " + rn("marketing") + " svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: deleteObject}, + }, + { + name: "Clean SVM", + input: ClusterStr + "delete " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject}, }, } diff --git a/ontap/ontap.go b/ontap/ontap.go index b5dd5c0..41d7dc8 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -198,6 +198,10 @@ type CIFSShare struct { Path string `json:"path,omitzero" jsonschema:"cifs share path"` } +type SVM struct { + Name string `json:"name" jsonschema:"svm name"` +} + type Qtree struct { SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"` Volume NameAndUUID `json:"volume,omitzero" jsonschema:"volume name"` diff --git a/rest/svm.go b/rest/svm.go new file mode 100644 index 0000000..0bfdbb3 --- /dev/null +++ b/rest/svm.go @@ -0,0 +1,67 @@ +package rest + +import ( + "bytes" + "context" + "fmt" + "github.com/netapp/ontap-mcp/ontap" + "net/http" + "net/url" +) + +func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error { + var ( + buf bytes.Buffer + statusCode int + ) + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/svm/svms`, &statusCode, responseHeaders). + BodyJSON(svm). + ToBytesBuffer(&buf) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.handleJob(ctx, statusCode, buf) +} + +func (c *Client) DeleteSVM(ctx context.Context, 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). + Delete(). + ToBytesBuffer(&buf) + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.handleJob(ctx, statusCode, buf) +} diff --git a/server/server.go b/server/server.go index f16ea90..1e5b4b0 100644 --- a/server/server.go +++ b/server/server.go @@ -118,6 +118,10 @@ func (a *App) createMCPServer() *mcp.Server { addTool(a, server, "update_nfs_export_policies_rules", descriptions.UpdateNFSExportPolicyRules, updateAnnotation, a.UpdateNFSExportPoliciesRule) addTool(a, server, "delete_nfs_export_policies_rules", descriptions.DeleteNFSExportPolicyRules, deleteAnnotation, a.DeleteNFSExportPoliciesRule) + // operation on SVM object + addTool(a, server, "create_svm", descriptions.CreateSVM, createAnnotation, a.CreateSVM) + addTool(a, server, "delete_svm", descriptions.DeleteSVM, deleteAnnotation, a.DeleteSVM) + // operation on CIFS share object addTool(a, server, "create_cifs_share", descriptions.CreateCIFSShare, createAnnotation, a.CreateCIFSShare) addTool(a, server, "update_cifs_share", descriptions.UpdateCIFSShare, updateAnnotation, a.UpdateCIFSShare) diff --git a/server/svm.go b/server/svm.go new file mode 100644 index 0000000..1f7656d --- /dev/null +++ b/server/svm.go @@ -0,0 +1,74 @@ +package server + +import ( + "context" + "errors" + "fmt" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/netapp/ontap-mcp/ontap" + "github.com/netapp/ontap-mcp/tool" +) + +func (a *App) CreateSVM(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) + + svmCreate, err := newCreateSVM(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + err = client.CreateSVM(ctx, svmCreate) + if err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "SVM created 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 + } + defer a.locks.Unlock(parameters.Cluster) + + if parameters.Name == "" { + return nil, nil, errors.New("SVM name is required") + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + err = client.DeleteSVM(ctx, parameters.Name) + if err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "SVM deleted successfully"}, + }, + }, nil, nil +} + +func newCreateSVM(in tool.SVM) (ontap.SVM, error) { + out := ontap.SVM{} + if in.Name == "" { + return out, errors.New("SVM name is required") + } + out.Name = in.Name + return out, nil +} diff --git a/tool/tool.go b/tool/tool.go index 6e37c5a..a472fc5 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -158,3 +158,8 @@ type DescribeEndpointParams struct { Path string `json:"path" jsonschema:"ONTAP REST API path, e.g. /storage/volumes"` 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 SVM struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + Name string `json:"svm_name" jsonschema:"SVM name"` +}