From 0b0691fd4f912906437d57acb3ed626e574cc2b7 Mon Sep 17 00:00:00 2001 From: hardikl Date: Thu, 9 Apr 2026 17:34:03 +0530 Subject: [PATCH] feat: adding lun-map tools --- descriptions/descriptions.go | 3 + docs/examples.md | 8 +++ integration/test/igroup_test.go | 30 +++++++--- integration/test/tools_test.go | 2 +- ontap/ontap.go | 8 +++ rest/lunmap.go | 65 ++++++++++++++++++++++ server/lunmap.go | 97 +++++++++++++++++++++++++++++++++ server/server.go | 4 ++ tool/tool.go | 7 +++ 9 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 rest/lunmap.go create mode 100644 server/lunmap.go diff --git a/descriptions/descriptions.go b/descriptions/descriptions.go index 74b02cb..91b9a61 100644 --- a/descriptions/descriptions.go +++ b/descriptions/descriptions.go @@ -82,6 +82,9 @@ const DeleteIGroup = `Delete an igroup on a cluster by cluster name.` const AddIGroupInitiator = `Add an initiator to an igroup on a cluster by cluster name.` const RemoveIGroupInitiator = `Remove an initiator from an igroup on a cluster by cluster name.` +const CreateLunMap = `Create a LUN map on a cluster by cluster name. Maps a LUN to an igroup, making the LUN accessible to the initiators in the igroup.` +const DeleteLunMap = `Delete a LUN map on a cluster by cluster name. Removes the mapping between a LUN and an igroup.` + 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 dddd39a..cd1ad03 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -287,6 +287,14 @@ Expected Response: A summary of aggregate free space, followed by a recommendati Expected Response: igroup created successfully. +- On the umeng-aff300-05-06 cluster, create lun map of lun named lunpayroll and an igroup named igroupFin on the marketing svm + +Expected Response: lun map created successfully. + +- On the umeng-aff300-05-06 cluster, delete lun map of lun named lunpayroll and an igroup named igroupFin on the marketing svm + +Expected Response: lun map deleted successfully. + **Rename an iGroup** - On the umeng-aff300-05-06 cluster, rename igroup igroupFin to igroupFinNew and os type as windows on the marketing svm diff --git a/integration/test/igroup_test.go b/integration/test/igroup_test.go index f04e69a..bc9589e 100644 --- a/integration/test/igroup_test.go +++ b/integration/test/igroup_test.go @@ -11,7 +11,7 @@ import ( "github.com/netapp/ontap-mcp/config" ) -func TestIGroup(t *testing.T) { +func TestIGroupLUNMap(t *testing.T) { SkipIfMissing(t, CheckTools) tests := []struct { @@ -38,24 +38,36 @@ func TestIGroup(t *testing.T) { expectedOntapErr: "", verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=marketing", validationFunc: createObject}, }, - { - name: "Update igroup", - input: ClusterStr + "rename igroup " + rn("igroupFin") + " to " + rn("igroupFinNew") + " and os type as windows on the marketing svm", - expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=marketing", validationFunc: createObject}, - }, { name: "Add initiator to igroup", - input: ClusterStr + "add initiator iqn.2021-01.com.example:test to igroup " + rn("igroupFinNew") + " on the marketing svm", + input: ClusterStr + "add initiator iqn.2021-01.com.example:test to igroup " + rn("igroupFin") + " on the marketing svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, { name: "Remove initiator from igroup", - input: ClusterStr + "remove initiator iqn.2021-01.com.example:test from igroup " + rn("igroupFinNew") + " on the marketing svm", + input: ClusterStr + "remove initiator iqn.2021-01.com.example:test from igroup " + rn("igroupFin") + " on the marketing svm", expectedOntapErr: "", verifyAPI: ontapVerifier{}, }, + { + name: "Rename igroup", + input: ClusterStr + "rename igroup from " + rn("igroupFin") + " to " + rn("igroupFinNew") + " on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=marketing", validationFunc: createObject}, + }, + { + name: "Create lun map", + input: ClusterStr + "create lun map of lun named " + "/vol/vol1/lunpayroll" + " and an igroup named " + rn("igroupFinNew") + " on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/vol1/lunpayroll" + "&svm.name=marketing", validationFunc: createObject}, + }, + { + name: "Clean lun map", + input: ClusterStr + "delete lun map of lun named " + "/vol/vol1/lunpayroll" + " and an igroup named " + rn("igroupFinNew") + " on the marketing svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/vol1/lunpayroll" + "&svm.name=marketing", validationFunc: deleteObject}, + }, { name: "Clean igroup", input: ClusterStr + "delete igroup " + rn("igroupFinNew") + " on the marketing svm", diff --git a/integration/test/tools_test.go b/integration/test/tools_test.go index 817f5b8..cbc51d5 100644 --- a/integration/test/tools_test.go +++ b/integration/test/tools_test.go @@ -207,7 +207,7 @@ func (a *Agent) ChatWithResponse(ctx context.Context, t *testing.T, userMessage if expectedOntapErrorStr != "" && strings.Contains(err.Error(), expectedOntapErrorStr) { slog.Debug("Expected tool error", slog.String("tool", toolName), slog.Any("error", err)) } else { - t.Errorf("Tool %q returned error LLM will retry: %v", toolName, err) + t.Errorf("Tool %q args %v returned error LLM will retry: %v", toolName, args, err) } result = "Error: " + err.Error() } diff --git a/ontap/ontap.go b/ontap/ontap.go index 9495b99..178fdd9 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -56,6 +56,8 @@ type GetData struct { RwRule []string `json:"rw_rule,omitzero"` Clients []ClientData `json:"clients,omitzero"` Nas NAS `json:"nas,omitzero"` + Lun NameAndUUID `json:"lun,omitzero"` + IGroup NameAndUUID `json:"igroup,omitzero"` } `json:"records"` NumRecords int `json:"num_records"` } @@ -213,6 +215,12 @@ type IGroup struct { Initiators []IGroupInitiator `json:"initiators,omitzero"` } +type LunMap struct { + SVM NameAndUUID `json:"svm,omitzero"` + Lun NameAndUUID `json:"lun,omitzero"` + IGroup NameAndUUID `json:"igroup,omitzero"` +} + const ( ASAr2 = "asar2" CDOT = "cdot" diff --git a/rest/lunmap.go b/rest/lunmap.go new file mode 100644 index 0000000..74192c4 --- /dev/null +++ b/rest/lunmap.go @@ -0,0 +1,65 @@ +package rest + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/netapp/ontap-mcp/ontap" +) + +func (c *Client) CreateLunMap(ctx context.Context, lunMap ontap.LunMap) error { + var statusCode int + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/protocols/san/lun-maps`, &statusCode, responseHeaders). + BodyJSON(lunMap) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteLunMap(ctx context.Context, svmName, lunName, igroupName string) error { + var ( + statusCode int + lm ontap.GetData + ) + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("fields", "lun.uuid,igroup.uuid") + params.Set("svm.name", svmName) + params.Set("lun.name", lunName) + params.Set("igroup.name", igroupName) + + builder := c.baseRequestBuilder(`/api/protocols/san/lun-maps`, &statusCode, responseHeaders). + Params(params). + ToJSON(&lm) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + if lm.NumRecords == 0 { + return fmt.Errorf("failed to find lun map for lun=%s igroup=%s on svm=%s because it does not exist", lunName, igroupName, svmName) + } + if lm.NumRecords != 1 { + return fmt.Errorf("failed to find lun map for lun=%s igroup=%s on svm=%s because there are %d matching records", lunName, igroupName, svmName, lm.NumRecords) + } + + lunUUID := lm.Records[0].Lun.UUID + igroupUUID := lm.Records[0].IGroup.UUID + + builder = c.baseRequestBuilder(`/api/protocols/san/lun-maps/`+url.PathEscape(lunUUID)+`/`+url.PathEscape(igroupUUID), &statusCode, responseHeaders). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} diff --git a/server/lunmap.go b/server/lunmap.go new file mode 100644 index 0000000..31a6dd6 --- /dev/null +++ b/server/lunmap.go @@ -0,0 +1,97 @@ +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) CreateLunMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LunMap) (*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) + + lunMapCreate, err := newCreateLunMap(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + err = client.CreateLunMap(ctx, lunMapCreate) + if err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "lun map created successfully"}, + }, + }, nil, nil +} + +func (a *App) DeleteLunMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LunMap) (*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 := validateDeleteLunMap(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + + err = client.DeleteLunMap(ctx, parameters.SVM, parameters.LunName, parameters.IGroupName) + if err != nil { + return errorResult(err), nil, err + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "lun map deleted successfully"}, + }, + }, nil, nil +} + +func newCreateLunMap(in tool.LunMap) (ontap.LunMap, error) { + out := ontap.LunMap{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.LunName == "" { + return out, errors.New("LUN name is required") + } + if in.IGroupName == "" { + return out, errors.New("igroup name is required") + } + + out.SVM = ontap.NameAndUUID{Name: in.SVM} + out.Lun = ontap.NameAndUUID{Name: in.LunName} + out.IGroup = ontap.NameAndUUID{Name: in.IGroupName} + return out, nil +} + +func validateDeleteLunMap(in tool.LunMap) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.LunName == "" { + return errors.New("LUN name is required") + } + if in.IGroupName == "" { + return errors.New("igroup name is required") + } + return nil +} diff --git a/server/server.go b/server/server.go index cce9c3b..f77b1e4 100644 --- a/server/server.go +++ b/server/server.go @@ -140,6 +140,10 @@ func (a *App) createMCPServer() *mcp.Server { addTool(a, server, "add_igroup_initiator", descriptions.AddIGroupInitiator, createAnnotation, a.AddIGroupInitiator) addTool(a, server, "remove_igroup_initiator", descriptions.RemoveIGroupInitiator, deleteAnnotation, a.RemoveIGroupInitiator) + // operation on LUN map object + addTool(a, server, "create_lun_map", descriptions.CreateLunMap, createAnnotation, a.CreateLunMap) + addTool(a, server, "delete_lun_map", descriptions.DeleteLunMap, deleteAnnotation, a.DeleteLunMap) + if a.catalog != nil { addTool(a, server, "list_ontap_endpoints", descriptions.ListOntapEndpoints, readOnlyAnnotation, a.ListOntapEndpoints) addTool(a, server, "search_ontap_endpoints", descriptions.SearchOntapEndpoints, readOnlyAnnotation, a.SearchOntapEndpoints) diff --git a/tool/tool.go b/tool/tool.go index 261f5cc..0d35818 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -136,6 +136,13 @@ type IGroupInitiator struct { AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows the deletion of an initiator from of a mapped initiator group. This parameter should be used with caution."` } +type LunMap struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + LunName string `json:"lun_name" jsonschema:"LUN name (full path, e.g. /vol/vol1/lun1)"` + IGroupName string `json:"igroup_name" jsonschema:"igroup name to map the LUN to"` +} + type OntapGetParams struct { Cluster string `json:"cluster_name" jsonschema:"cluster name, from list_registered_clusters"` Fields string `json:"fields,omitzero" jsonschema:"comma-separated dot-notation fields to return, e.g. \"name,svm.name,space.size\" — use space.* to expand all space sub-fields"`