Skip to content

Commit 613ae18

Browse files
Hardiklcgrinds
authored andcommitted
feat: adding lun-map tools
1 parent ec34903 commit 613ae18

9 files changed

Lines changed: 214 additions & 10 deletions

File tree

descriptions/descriptions.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ const DeleteIGroup = `Delete an igroup on a cluster by cluster name.`
9090
const AddIGroupInitiator = `Add an initiator to an igroup on a cluster by cluster name.`
9191
const RemoveIGroupInitiator = `Remove an initiator from an igroup on a cluster by cluster name.`
9292

93+
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.`
94+
const DeleteLunMap = `Delete a LUN map on a cluster by cluster name. Removes the mapping between a LUN and an igroup.`
95+
9396
const ListOntapEndpoints = `List ONTAP REST collection endpoints in the catalog.
9497
The catalog contains all endpoints — can be large. Prefer search_ontap_endpoints for targeted discovery.
9598
Use the optional 'match' parameter to filter by substring or regex pattern (e.g. "snapshot", "lun", ".*nfs.*export.*").

docs/examples.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,14 @@ Expected Response: A summary of aggregate free space, followed by a recommendati
315315

316316
Expected Response: igroup created successfully.
317317

318+
- On the umeng-aff300-05-06 cluster, create lun map of lun named lunpayroll and an igroup named igroupFin on the marketing svm
319+
320+
Expected Response: lun map created successfully.
321+
322+
- On the umeng-aff300-05-06 cluster, delete lun map of lun named lunpayroll and an igroup named igroupFin on the marketing svm
323+
324+
Expected Response: lun map deleted successfully.
325+
318326
**Rename an iGroup**
319327

320328
- On the umeng-aff300-05-06 cluster, rename igroup igroupFin to igroupFinNew and os type as windows on the marketing svm

integration/test/igroup_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/netapp/ontap-mcp/config"
1212
)
1313

14-
func TestIGroup(t *testing.T) {
14+
func TestIGroupLUNMap(t *testing.T) {
1515
SkipIfMissing(t, CheckTools)
1616

1717
tests := []struct {
@@ -38,24 +38,36 @@ func TestIGroup(t *testing.T) {
3838
expectedOntapErr: "",
3939
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=marketing", validationFunc: createObject},
4040
},
41-
{
42-
name: "Update igroup",
43-
input: ClusterStr + "rename igroup " + rn("igroupFin") + " to " + rn("igroupFinNew") + " and os type as windows on the marketing svm",
44-
expectedOntapErr: "",
45-
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=marketing", validationFunc: createObject},
46-
},
4741
{
4842
name: "Add initiator to igroup",
49-
input: ClusterStr + "add initiator iqn.2021-01.com.example:test to igroup " + rn("igroupFinNew") + " on the marketing svm",
43+
input: ClusterStr + "add initiator iqn.2021-01.com.example:test to igroup " + rn("igroupFin") + " on the marketing svm",
5044
expectedOntapErr: "",
5145
verifyAPI: ontapVerifier{},
5246
},
5347
{
5448
name: "Remove initiator from igroup",
55-
input: ClusterStr + "remove initiator iqn.2021-01.com.example:test from igroup " + rn("igroupFinNew") + " on the marketing svm",
49+
input: ClusterStr + "remove initiator iqn.2021-01.com.example:test from igroup " + rn("igroupFin") + " on the marketing svm",
5650
expectedOntapErr: "",
5751
verifyAPI: ontapVerifier{},
5852
},
53+
{
54+
name: "Rename igroup",
55+
input: ClusterStr + "rename igroup from " + rn("igroupFin") + " to " + rn("igroupFinNew") + " on the marketing svm",
56+
expectedOntapErr: "",
57+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=marketing", validationFunc: createObject},
58+
},
59+
{
60+
name: "Create lun map",
61+
input: ClusterStr + "create lun map of lun named " + "/vol/vol1/lunpayroll" + " and an igroup named " + rn("igroupFinNew") + " on the marketing svm",
62+
expectedOntapErr: "",
63+
verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/vol1/lunpayroll" + "&svm.name=marketing", validationFunc: createObject},
64+
},
65+
{
66+
name: "Clean lun map",
67+
input: ClusterStr + "delete lun map of lun named " + "/vol/vol1/lunpayroll" + " and an igroup named " + rn("igroupFinNew") + " on the marketing svm",
68+
expectedOntapErr: "because it does not exist",
69+
verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/vol1/lunpayroll" + "&svm.name=marketing", validationFunc: deleteObject},
70+
},
5971
{
6072
name: "Clean igroup",
6173
input: ClusterStr + "delete igroup " + rn("igroupFinNew") + " on the marketing svm",

integration/test/tools_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (a *Agent) ChatWithResponse(ctx context.Context, t *testing.T, userMessage
207207
if expectedOntapErrorStr != "" && strings.Contains(err.Error(), expectedOntapErrorStr) {
208208
slog.Debug("Expected tool error", slog.String("tool", toolName), slog.Any("error", err))
209209
} else {
210-
t.Errorf("Tool %q returned error LLM will retry: %v", toolName, err)
210+
t.Errorf("Tool %q args %v returned error LLM will retry: %v", toolName, args, err)
211211
}
212212
result = "Error: " + err.Error()
213213
}

ontap/ontap.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ type GetData struct {
5656
RwRule []string `json:"rw_rule,omitzero"`
5757
Clients []ClientData `json:"clients,omitzero"`
5858
Nas NAS `json:"nas,omitzero"`
59+
Lun NameAndUUID `json:"lun,omitzero"`
60+
IGroup NameAndUUID `json:"igroup,omitzero"`
5961
} `json:"records"`
6062
NumRecords int `json:"num_records"`
6163
}
@@ -245,6 +247,12 @@ type IGroup struct {
245247
Initiators []IGroupInitiator `json:"initiators,omitzero"`
246248
}
247249

250+
type LunMap struct {
251+
SVM NameAndUUID `json:"svm,omitzero"`
252+
Lun NameAndUUID `json:"lun,omitzero"`
253+
IGroup NameAndUUID `json:"igroup,omitzero"`
254+
}
255+
248256
const (
249257
ASAr2 = "asar2"
250258
CDOT = "cdot"

rest/lunmap.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package rest
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
9+
"github.com/netapp/ontap-mcp/ontap"
10+
)
11+
12+
func (c *Client) CreateLunMap(ctx context.Context, lunMap ontap.LunMap) error {
13+
var statusCode int
14+
responseHeaders := http.Header{}
15+
16+
builder := c.baseRequestBuilder(`/api/protocols/san/lun-maps`, &statusCode, responseHeaders).
17+
BodyJSON(lunMap)
18+
19+
if err := c.buildAndExecuteRequest(ctx, builder); err != nil {
20+
return err
21+
}
22+
23+
return c.checkStatus(statusCode)
24+
}
25+
26+
func (c *Client) DeleteLunMap(ctx context.Context, svmName, lunName, igroupName string) error {
27+
var (
28+
statusCode int
29+
lm ontap.GetData
30+
)
31+
responseHeaders := http.Header{}
32+
33+
params := url.Values{}
34+
params.Set("fields", "lun.uuid,igroup.uuid")
35+
params.Set("svm.name", svmName)
36+
params.Set("lun.name", lunName)
37+
params.Set("igroup.name", igroupName)
38+
39+
builder := c.baseRequestBuilder(`/api/protocols/san/lun-maps`, &statusCode, responseHeaders).
40+
Params(params).
41+
ToJSON(&lm)
42+
43+
if err := c.buildAndExecuteRequest(ctx, builder); err != nil {
44+
return err
45+
}
46+
47+
if lm.NumRecords == 0 {
48+
return fmt.Errorf("failed to find lun map for lun=%s igroup=%s on svm=%s because it does not exist", lunName, igroupName, svmName)
49+
}
50+
if lm.NumRecords != 1 {
51+
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)
52+
}
53+
54+
lunUUID := lm.Records[0].Lun.UUID
55+
igroupUUID := lm.Records[0].IGroup.UUID
56+
57+
builder = c.baseRequestBuilder(`/api/protocols/san/lun-maps/`+url.PathEscape(lunUUID)+`/`+url.PathEscape(igroupUUID), &statusCode, responseHeaders).
58+
Delete()
59+
60+
if err := c.buildAndExecuteRequest(ctx, builder); err != nil {
61+
return err
62+
}
63+
64+
return c.checkStatus(statusCode)
65+
}

server/lunmap.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"github.com/netapp/ontap-mcp/ontap"
10+
"github.com/netapp/ontap-mcp/tool"
11+
)
12+
13+
func (a *App) CreateLunMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LunMap) (*mcp.CallToolResult, any, error) {
14+
if !a.locks.TryLock(parameters.Cluster) {
15+
return errorResult(fmt.Errorf("another write operation is in progress on cluster %s, please try again", parameters.Cluster)), nil, nil
16+
}
17+
defer a.locks.Unlock(parameters.Cluster)
18+
19+
lunMapCreate, err := newCreateLunMap(parameters)
20+
if err != nil {
21+
return nil, nil, err
22+
}
23+
24+
client, err := a.getClient(parameters.Cluster)
25+
if err != nil {
26+
return errorResult(err), nil, err
27+
}
28+
29+
err = client.CreateLunMap(ctx, lunMapCreate)
30+
if err != nil {
31+
return errorResult(err), nil, err
32+
}
33+
34+
return &mcp.CallToolResult{
35+
Content: []mcp.Content{
36+
&mcp.TextContent{Text: "lun map created successfully"},
37+
},
38+
}, nil, nil
39+
}
40+
41+
func (a *App) DeleteLunMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.LunMap) (*mcp.CallToolResult, any, error) {
42+
if !a.locks.TryLock(parameters.Cluster) {
43+
return errorResult(fmt.Errorf("another write operation is in progress on cluster %s, please try again", parameters.Cluster)), nil, nil
44+
}
45+
defer a.locks.Unlock(parameters.Cluster)
46+
47+
if err := validateDeleteLunMap(parameters); err != nil {
48+
return nil, nil, err
49+
}
50+
51+
client, err := a.getClient(parameters.Cluster)
52+
if err != nil {
53+
return errorResult(err), nil, err
54+
}
55+
56+
err = client.DeleteLunMap(ctx, parameters.SVM, parameters.LunName, parameters.IGroupName)
57+
if err != nil {
58+
return errorResult(err), nil, err
59+
}
60+
61+
return &mcp.CallToolResult{
62+
Content: []mcp.Content{
63+
&mcp.TextContent{Text: "lun map deleted successfully"},
64+
},
65+
}, nil, nil
66+
}
67+
68+
func newCreateLunMap(in tool.LunMap) (ontap.LunMap, error) {
69+
out := ontap.LunMap{}
70+
if in.SVM == "" {
71+
return out, errors.New("SVM name is required")
72+
}
73+
if in.LunName == "" {
74+
return out, errors.New("LUN name is required")
75+
}
76+
if in.IGroupName == "" {
77+
return out, errors.New("igroup name is required")
78+
}
79+
80+
out.SVM = ontap.NameAndUUID{Name: in.SVM}
81+
out.Lun = ontap.NameAndUUID{Name: in.LunName}
82+
out.IGroup = ontap.NameAndUUID{Name: in.IGroupName}
83+
return out, nil
84+
}
85+
86+
func validateDeleteLunMap(in tool.LunMap) error {
87+
if in.SVM == "" {
88+
return errors.New("SVM name is required")
89+
}
90+
if in.LunName == "" {
91+
return errors.New("LUN name is required")
92+
}
93+
if in.IGroupName == "" {
94+
return errors.New("igroup name is required")
95+
}
96+
return nil
97+
}

server/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ func (a *App) createMCPServer() *mcp.Server {
150150
addTool(a, server, "add_igroup_initiator", descriptions.AddIGroupInitiator, createAnnotation, a.AddIGroupInitiator)
151151
addTool(a, server, "remove_igroup_initiator", descriptions.RemoveIGroupInitiator, deleteAnnotation, a.RemoveIGroupInitiator)
152152

153+
// operation on LUN map object
154+
addTool(a, server, "create_lun_map", descriptions.CreateLunMap, createAnnotation, a.CreateLunMap)
155+
addTool(a, server, "delete_lun_map", descriptions.DeleteLunMap, deleteAnnotation, a.DeleteLunMap)
156+
153157
if a.catalog != nil {
154158
addTool(a, server, "list_ontap_endpoints", descriptions.ListOntapEndpoints, readOnlyAnnotation, a.ListOntapEndpoints)
155159
addTool(a, server, "search_ontap_endpoints", descriptions.SearchOntapEndpoints, readOnlyAnnotation, a.SearchOntapEndpoints)

tool/tool.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ type IGroupInitiator struct {
158158
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."`
159159
}
160160

161+
type LunMap struct {
162+
Cluster string `json:"cluster_name" jsonschema:"cluster name"`
163+
SVM string `json:"svm_name" jsonschema:"SVM name"`
164+
LunName string `json:"lun_name" jsonschema:"LUN name (full path, e.g. /vol/vol1/lun1)"`
165+
IGroupName string `json:"igroup_name" jsonschema:"igroup name to map the LUN to"`
166+
}
167+
161168
type OntapGetParams struct {
162169
Cluster string `json:"cluster_name" jsonschema:"cluster name, from list_registered_clusters"`
163170
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"`

0 commit comments

Comments
 (0)