Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions descriptions/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.*").
Expand Down
8 changes: 8 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 21 additions & 9 deletions integration/test/igroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion integration/test/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
8 changes: 8 additions & 0 deletions ontap/ontap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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"
Expand Down
65 changes: 65 additions & 0 deletions rest/lunmap.go
Original file line number Diff line number Diff line change
@@ -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)
}
97 changes: 97 additions & 0 deletions server/lunmap.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 4 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions tool/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading