From 3d0b62d8bb8ae461fe12dcfb6df7a94ed5b2b049 Mon Sep 17 00:00:00 2001 From: hardikl Date: Fri, 10 Apr 2026 19:20:48 +0530 Subject: [PATCH 1/4] feat: adding lun crud tools with tests --- descriptions/descriptions.go | 4 + docs/examples.md | 34 +++++++ integration/test/lun_test.go | 105 ++++++++++++++++++++ ontap/ontap.go | 12 +++ rest/lun.go | 104 ++++++++++++++++++++ server/lun.go | 182 +++++++++++++++++++++++++++++++++++ server/server.go | 5 + tool/tool.go | 12 +++ 8 files changed, 458 insertions(+) create mode 100644 integration/test/lun_test.go create mode 100644 rest/lun.go create mode 100644 server/lun.go diff --git a/descriptions/descriptions.go b/descriptions/descriptions.go index 2028510..d6c5f48 100644 --- a/descriptions/descriptions.go +++ b/descriptions/descriptions.go @@ -84,6 +84,10 @@ const CreateNetworkIPInterface = `Create Network IP interface on a cluster by cl const UpdateNetworkIPInterface = `Update Network IP interface on a cluster by cluster name.` const DeleteNetworkIPInterface = `Delete Network IP interface on a cluster by cluster name.` +const CreateLUN = `Create a LUN on a specified volume and SVM with a given size and OS type.` +const UpdateLUN = `Update a LUN: resize, rename, or toggle enabled/disabled state (online/offline).` +const DeleteLUN = `Delete a LUN from a specified volume and SVM.` + const ListOntapEndpoints = `List ONTAP REST collection endpoints in the catalog. The catalog contains all endpoints — can be large. Prefer search_ontap_endpoints for targeted discovery. Use the optional 'match' parameter to filter by substring or regex pattern (e.g. "snapshot", "lun", ".*nfs.*export.*"). diff --git a/docs/examples.md b/docs/examples.md index 66be414..c13584e 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -291,6 +291,40 @@ Expected Response: The Network IP interface deleted successfully. --- +### LUN Provisioning + +**Create a LUN** + +- On the umeng-aff300-05-06 cluster, create a 20MB lun named lundoc in volume doc on the marketing svm + +Expected Response: LUN has been created successfully. + +**Resize a LUN** + +- On the umeng-aff300-05-06 cluster, update lun lundoc size to 50mb in volume doc on the marketing svm + +Expected Response: LUN size has been updated successfully. + +**Rename a LUN** + +- On the umeng-aff300-05-06 cluster, rename the lun lundoc in volume doc on the marketing svm to lundocnew + +Expected Response: LUN rename has been updated successfully. + +**State change of a LUN** + +- On the umeng-aff300-05-06 cluster, disable the lun lundocnew in volume doc on the marketing svm + +Expected Response: LUN state has been updated successfully. + +**Delete a LUN** + +- On the umeng-aff300-05-06 cluster, delete lun lundocnew in volume doc in marketing svm + +Expected Response: LUN has been deleted successfully. + +--- + ### Querying Specific Fields **Get volume space and protection details** diff --git a/integration/test/lun_test.go b/integration/test/lun_test.go new file mode 100644 index 0000000..adb3f32 --- /dev/null +++ b/integration/test/lun_test.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "crypto/tls" + "log/slog" + "net/http" + "testing" + "time" + + "github.com/netapp/ontap-mcp/config" +) + +func TestLUN(t *testing.T) { + SkipIfMissing(t, CheckTools) + + tests := []struct { + name string + input string + expectedOntapErr string + verifyAPI ontapVerifier + }{ + { + name: "Clean LUN", + input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in marketing svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=marketing", validationFunc: deleteObject}, + }, + { + name: "Clean LUN", + input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in marketing svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=marketing", validationFunc: deleteObject}, + }, + { + name: "Create volume", + input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the marketing svm and the harvest_vc_aggr aggregate", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=marketing", validationFunc: createObject}, + }, + { + name: "Create LUN", + input: ClusterStr + "create a 20MB lun named " + rn("lundoc") + " in volume " + rn("doc") + " on the marketing svm with os type linux", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=marketing", validationFunc: createObject}, + }, + { + name: "Update lun size", + input: ClusterStr + "update lun " + rn("lundoc") + "size to 50mb in volume " + rn("doc") + " on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Rename lun", + input: ClusterStr + "rename the lun " + rn("lundoc") + " in volume " + rn("doc") + " on the marketing svm to " + rn("lundocnew"), + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + rn("lundocnew") + "&svm.name=marketing", validationFunc: createObject}, + }, + { + name: "Update lun state", + input: ClusterStr + "disable the lun " + rn("lundocnew") + " in volume " + rn("doc") + " on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Clean LUN", + input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in marketing svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=marketing", validationFunc: deleteObject}, + }, + { + name: "Clean volume", + input: ClusterStr + "delete volume " + rn("doc") + " in marketing svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=marketing", 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) + } + }) + } +} diff --git a/ontap/ontap.go b/ontap/ontap.go index b5dd5c0..0b61e9a 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -198,6 +198,18 @@ type CIFSShare struct { Path string `json:"path,omitzero" jsonschema:"cifs share path"` } +type LUNSpace struct { + Size int64 `json:"size,omitempty" jsonschema:"size of the LUN"` +} + +type LUN struct { + SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"` + Name string `json:"name,omitempty" jsonschema:"LUN name"` + Space LUNSpace `json:"space,omitzero" jsonschema:"LUN space detail"` + OsType string `json:"os_type,omitempty" jsonschema:"os type of LUN"` + Enabled *bool `json:"enabled,omitempty" jsonschema:"LUN admin state"` +} + type Qtree struct { SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"` Volume NameAndUUID `json:"volume,omitzero" jsonschema:"volume name"` diff --git a/rest/lun.go b/rest/lun.go new file mode 100644 index 0000000..a584b5f --- /dev/null +++ b/rest/lun.go @@ -0,0 +1,104 @@ +package rest + +import ( + "context" + "fmt" + "github.com/netapp/ontap-mcp/ontap" + "net/http" + "net/url" + "strconv" +) + +func (c *Client) CreateLUN(ctx context.Context, lun ontap.LUN) error { + var ( + statusCode int + ) + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders). + BodyJSON(lun) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) UpdateLUN(ctx context.Context, svmName, lunPath string, lun ontap.LUN) error { + var ( + statusCode int + lunData ontap.GetData + ) + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("name", lunPath) + params.Set("svm.name", svmName) + params.Set("fields", "uuid") + + builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders). + Params(params). + ToJSON(&lunData) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + if lunData.NumRecords == 0 { + return fmt.Errorf("failed to get lun=%s on svm=%s because it does not exist", lunPath, svmName) + } + if lunData.NumRecords != 1 { + return fmt.Errorf("failed to get lun=%s on svm=%s because there are %d matching records", lunPath, svmName, lunData.NumRecords) + } + + builder2 := c.baseRequestBuilder(`/api/storage/luns/`+lunData.Records[0].UUID, &statusCode, responseHeaders). + Patch(). + BodyJSON(lun) + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteLUN(ctx context.Context, svmName, lunPath string, allowDeleteWhileMapped bool) error { + var ( + statusCode int + lunData ontap.GetData + ) + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("name", lunPath) + params.Set("svm.name", svmName) + params.Set("fields", "uuid") + + builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders). + Params(params). + ToJSON(&lunData) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + if lunData.NumRecords == 0 { + return fmt.Errorf("failed to get lun=%s on svm=%s because it does not exist", lunPath, svmName) + } + if lunData.NumRecords != 1 { + return fmt.Errorf("failed to get lun=%s on svm=%s because there are %d matching records", lunPath, svmName, lunData.NumRecords) + } + + deleteParams := url.Values{} + deleteParams.Set("allow_delete_while_mapped", strconv.FormatBool(allowDeleteWhileMapped)) + builder2 := c.baseRequestBuilder(`/api/storage/luns/`+lunData.Records[0].UUID, &statusCode, responseHeaders). + Params(deleteParams). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.checkStatus(statusCode) +} diff --git a/server/lun.go b/server/lun.go new file mode 100644 index 0000000..f24873a --- /dev/null +++ b/server/lun.go @@ -0,0 +1,182 @@ +package server + +import ( + "context" + "errors" + "fmt" + "strconv" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/netapp/ontap-mcp/ontap" + "github.com/netapp/ontap-mcp/tool" +) + +func lunPath(volume, name string) string { + return "/vol/" + volume + "/" + name +} + +func (a *App) CreateLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LUN) (*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) + + lunCreate, err := newCreateLUN(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + if err := client.CreateLUN(ctx, lunCreate); err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "LUN created successfully"}, + }, + }, nil, nil +} + +func (a *App) UpdateLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LUN) (*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) + + lunUpdate, err := newUpdateLUN(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + if err := client.UpdateLUN(ctx, parameters.SVM, lunPath(parameters.Volume, parameters.Name), lunUpdate); err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "LUN updated successfully"}, + }, + }, nil, nil +} + +func (a *App) DeleteLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LUN) (*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 err := newDeleteLUN(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + if err := client.DeleteLUN(ctx, parameters.SVM, lunPath(parameters.Volume, parameters.Name), parameters.AllowDeleteWhileMapped); err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "LUN deleted successfully"}, + }, + }, nil, nil +} + +// newCreateLUN validates the customer provided arguments and converts them into +// the corresponding ONTAP object ready to use via the REST API +func newCreateLUN(in tool.LUN) (ontap.LUN, error) { + out := ontap.LUN{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Volume == "" { + return out, errors.New("volume name is required") + } + if in.Name == "" { + return out, errors.New("LUN name is required") + } + if in.Size == "" { + return out, errors.New("LUN size is required") + } + if in.OsType == "" { + return out, errors.New("OS type is required") + } + + size, err := parseSize(in.Size) + if err != nil { + return out, fmt.Errorf("invalid size: %w", err) + } + + out.SVM = ontap.NameAndUUID{Name: in.SVM} + out.Name = lunPath(in.Volume, in.Name) + out.Space = ontap.LUNSpace{Size: size} + out.OsType = in.OsType + return out, nil +} + +// newUpdateLUN validates the customer provided arguments and converts them into +// the corresponding ONTAP object ready to use via the REST API +func newUpdateLUN(in tool.LUN) (ontap.LUN, error) { + out := ontap.LUN{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Volume == "" { + return out, errors.New("volume name is required") + } + if in.Name == "" { + return out, errors.New("LUN name is required") + } + if in.NewName == "" && in.Size == "" && in.Enabled == "" { + return out, errors.New("at least one of new_lun_name, size, or enabled must be provided") + } + + if in.NewName != "" { + out.Name = lunPath(in.Volume, in.NewName) + } + + if in.Size != "" { + size, err := parseSize(in.Size) + if err != nil { + return out, fmt.Errorf("invalid size: %w", err) + } + out.Space = ontap.LUNSpace{Size: size} + } + + if in.Enabled != "" { + enabled, err := strconv.ParseBool(in.Enabled) + if err != nil { + return out, fmt.Errorf("invalid enabled value %q: must be 'true' or 'false'", in.Enabled) + } + out.Enabled = &enabled + } + + return out, nil +} + +// newDeleteLUN validates the customer provided arguments for a LUN delete operation +func newDeleteLUN(in tool.LUN) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.Volume == "" { + return errors.New("volume name is required") + } + if in.Name == "" { + return errors.New("LUN name is required") + } + return nil +} diff --git a/server/server.go b/server/server.go index f16ea90..06af042 100644 --- a/server/server.go +++ b/server/server.go @@ -138,6 +138,11 @@ func (a *App) createMCPServer() *mcp.Server { addTool(a, server, "update_iscsi_service", descriptions.UpdateIscsiService, updateAnnotation, a.UpdateIscsiService) addTool(a, server, "delete_iscsi_service", descriptions.DeleteIscsiService, deleteAnnotation, a.DeleteIscsiService) + // operation on LUN object + addTool(a, server, "create_lun", descriptions.CreateLUN, createAnnotation, a.CreateLUN) + addTool(a, server, "update_lun", descriptions.UpdateLUN, updateAnnotation, a.UpdateLUN) + addTool(a, server, "delete_lun", descriptions.DeleteLUN, deleteAnnotation, a.DeleteLUN) + // operation on Network Interface object addTool(a, server, "create_network_ip_interface", descriptions.CreateNetworkIPInterface, createAnnotation, a.CreateNetworkIPInterface) addTool(a, server, "update_network_ip_interface", descriptions.UpdateNetworkIPInterface, updateAnnotation, a.UpdateNetworkIPInterface) diff --git a/tool/tool.go b/tool/tool.go index 6e37c5a..60a7727 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -109,6 +109,18 @@ type Qtree struct { NewName string `json:"new_name,omitzero" jsonschema:"new qtree name"` } +type LUN struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Volume string `json:"volume_name" jsonschema:"volume name where the LUN resides"` + Name string `json:"lun_name" jsonschema:"LUN name"` + NewName string `json:"new_lun_name,omitzero" jsonschema:"new LUN name for rename operation"` + Size string `json:"size,omitzero" jsonschema:"size of the LUN (e.g., '10GB', '1TB')"` + OsType string `json:"os_type,omitzero" jsonschema:"OS type (e.g., linux, windows, windows_2008, windows_gpt, aix, esxi, hyper_v, solaris, vmware, xen)"` + Enabled string `json:"enabled,omitzero" jsonschema:"LUN state: 'true' to enable (online) or 'false' to disable (offline) the LUN"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped LUN.. This parameter should be used with caution"` +} + type NVMeService struct { Cluster string `json:"cluster_name" jsonschema:"cluster name"` SVM string `json:"svm_name" jsonschema:"SVM name"` From 6c9f432144f6c769059ea2fbde33246da3b8e38d Mon Sep 17 00:00:00 2001 From: hardikl Date: Fri, 10 Apr 2026 20:33:33 +0530 Subject: [PATCH 2/4] feat: update test cases --- integration/test/lun_test.go | 50 ++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/integration/test/lun_test.go b/integration/test/lun_test.go index adb3f32..d4a77b3 100644 --- a/integration/test/lun_test.go +++ b/integration/test/lun_test.go @@ -20,59 +20,77 @@ func TestLUN(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 LUN", - input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in marketing svm", + input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject}, }, { name: "Clean LUN", - input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in marketing svm", + input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject}, }, { name: "Create volume", - input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the marketing svm and the harvest_vc_aggr aggregate", + input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate", expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=marketing", validationFunc: createObject}, + verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: createObject}, }, { name: "Create LUN", - input: ClusterStr + "create a 20MB lun named " + rn("lundoc") + " in volume " + rn("doc") + " on the marketing svm with os type linux", + input: ClusterStr + "create a 20MB lun named " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm with os type linux", expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=marketing", validationFunc: createObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: createObject}, }, { name: "Update lun size", - input: ClusterStr + "update lun " + rn("lundoc") + "size to 50mb in volume " + rn("doc") + " on the marketing svm", + input: ClusterStr + "update lun " + rn("lundoc") + "size to 50mb in volume " + rn("doc") + " on the " + rn("marketing") + " svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, { name: "Rename lun", - input: ClusterStr + "rename the lun " + rn("lundoc") + " in volume " + rn("doc") + " on the marketing svm to " + rn("lundocnew"), + input: ClusterStr + "rename the lun " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm to " + rn("lundocnew"), expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + rn("lundocnew") + "&svm.name=marketing", validationFunc: createObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: createObject}, }, { name: "Update lun state", - input: ClusterStr + "disable the lun " + rn("lundocnew") + " in volume " + rn("doc") + " on the marketing svm", + input: ClusterStr + "disable the lun " + rn("lundocnew") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, { name: "Clean LUN", - input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in marketing svm", + input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm", expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject}, }, { name: "Clean volume", - input: ClusterStr + "delete volume " + rn("doc") + " in marketing svm", + input: ClusterStr + "delete volume " + rn("doc") + " in " + rn("marketing") + " svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&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/storage/volumes?name=" + rn("doc") + "&svm.name=marketing", validationFunc: deleteObject}, + verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject}, }, } From 46d011bcaf5d9e9a51a7c1930ee8842633f3ac57 Mon Sep 17 00:00:00 2001 From: hardikl Date: Mon, 13 Apr 2026 13:25:29 +0530 Subject: [PATCH 3/4] feat: handled review comment --- docs/examples.md | 8 ++++---- integration/test/lun_test.go | 4 ++-- tool/tool.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index c13584e..aa7ad74 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -295,7 +295,7 @@ Expected Response: The Network IP interface deleted successfully. **Create a LUN** -- On the umeng-aff300-05-06 cluster, create a 20MB lun named lundoc in volume doc on the marketing svm +- On the umeng-aff300-05-06 cluster, create a 20MB lun named lundoc in volume doc on the marketing svm with os type linux Expected Response: LUN has been created successfully. @@ -303,19 +303,19 @@ Expected Response: LUN has been created successfully. - On the umeng-aff300-05-06 cluster, update lun lundoc size to 50mb in volume doc on the marketing svm -Expected Response: LUN size has been updated successfully. +Expected Response: LUN has been updated successfully. **Rename a LUN** - On the umeng-aff300-05-06 cluster, rename the lun lundoc in volume doc on the marketing svm to lundocnew -Expected Response: LUN rename has been updated successfully. +Expected Response: LUN has been updated successfully. **State change of a LUN** - On the umeng-aff300-05-06 cluster, disable the lun lundocnew in volume doc on the marketing svm -Expected Response: LUN state has been updated successfully. +Expected Response: LUN has been updated successfully. **Delete a LUN** diff --git a/integration/test/lun_test.go b/integration/test/lun_test.go index d4a77b3..b5f5a6d 100644 --- a/integration/test/lun_test.go +++ b/integration/test/lun_test.go @@ -58,7 +58,7 @@ func TestLUN(t *testing.T) { }, { name: "Update lun size", - input: ClusterStr + "update lun " + rn("lundoc") + "size to 50mb in volume " + rn("doc") + " on the " + rn("marketing") + " svm", + input: ClusterStr + "update lun " + rn("lundoc") + " size to 50mb in volume " + rn("doc") + " on the " + rn("marketing") + " svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, @@ -66,7 +66,7 @@ func TestLUN(t *testing.T) { name: "Rename lun", input: ClusterStr + "rename the lun " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm to " + rn("lundocnew"), expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: createObject}, + verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: createObject}, }, { name: "Update lun state", diff --git a/tool/tool.go b/tool/tool.go index 01ece15..3262e48 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -118,7 +118,7 @@ type LUN struct { Size string `json:"size,omitzero" jsonschema:"size of the LUN (e.g., '10GB', '1TB')"` OsType string `json:"os_type,omitzero" jsonschema:"OS type (e.g., linux, windows, windows_2008, windows_gpt, aix, esxi, hyper_v, solaris, vmware, xen)"` Enabled string `json:"enabled,omitzero" jsonschema:"LUN state: 'true' to enable (online) or 'false' to disable (offline) the LUN"` - AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped LUN.. This parameter should be used with caution"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped LUN. This parameter should be used with caution"` } type NVMeService struct { From d14878f9492118f3e3ea5bf96b16f68267647a1d Mon Sep 17 00:00:00 2001 From: hardikl Date: Mon, 13 Apr 2026 18:23:25 +0530 Subject: [PATCH 4/4] feat: handled review comments --- server/lun.go | 4 ++-- tool/tool.go | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/server/lun.go b/server/lun.go index f24873a..1a8797a 100644 --- a/server/lun.go +++ b/server/lun.go @@ -15,7 +15,7 @@ func lunPath(volume, name string) string { return "/vol/" + volume + "/" + name } -func (a *App) CreateLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LUN) (*mcp.CallToolResult, any, error) { +func (a *App) CreateLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LUNCreate) (*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 } @@ -97,7 +97,7 @@ func (a *App) DeleteLUN(ctx context.Context, _ *mcp.CallToolRequest, parameters // newCreateLUN validates the customer provided arguments and converts them into // the corresponding ONTAP object ready to use via the REST API -func newCreateLUN(in tool.LUN) (ontap.LUN, error) { +func newCreateLUN(in tool.LUNCreate) (ontap.LUN, error) { out := ontap.LUN{} if in.SVM == "" { return out, errors.New("SVM name is required") diff --git a/tool/tool.go b/tool/tool.go index f4e8263..55e5496 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -109,6 +109,15 @@ type Qtree struct { NewName string `json:"new_name,omitzero" jsonschema:"new qtree name"` } +type LUNCreate struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Volume string `json:"volume_name" jsonschema:"volume name where the LUN resides"` + Name string `json:"lun_name" jsonschema:"LUN name"` + Size string `json:"size" jsonschema:"size of the LUN (e.g., '10GB', '1TB')"` + OsType string `json:"os_type" jsonschema:"OS type (e.g., linux, windows, windows_2008, windows_gpt, aix, esxi, hyper_v, solaris, vmware, xen)"` +} + type LUN struct { Cluster string `json:"cluster_name" jsonschema:"cluster name"` SVM string `json:"svm_name" jsonschema:"SVM name"` @@ -116,7 +125,6 @@ type LUN struct { Name string `json:"lun_name" jsonschema:"LUN name"` NewName string `json:"new_lun_name,omitzero" jsonschema:"new LUN name for rename operation"` Size string `json:"size,omitzero" jsonschema:"size of the LUN (e.g., '10GB', '1TB')"` - OsType string `json:"os_type,omitzero" jsonschema:"OS type (e.g., linux, windows, windows_2008, windows_gpt, aix, esxi, hyper_v, solaris, vmware, xen)"` Enabled string `json:"enabled,omitzero" jsonschema:"LUN state: 'true' to enable (online) or 'false' to disable (offline) the LUN"` AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped LUN. This parameter should be used with caution"` }