Skip to content

Commit fda3bab

Browse files
authored
feat: implement igroup crud tools (#98)
1 parent 9b00b1d commit fda3bab

12 files changed

Lines changed: 997 additions & 2 deletions

File tree

descriptions/descriptions.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ const CreateFCInterface = `Create FC interface on a cluster by cluster name.`
115115
const UpdateFCInterface = `Update FC interface on a cluster by cluster name.`
116116
const DeleteFCInterface = `Delete FC interface on a cluster by cluster name.`
117117

118+
const CreateIGroup = `Create an igroup (initiator group) on a cluster by cluster name.`
119+
const UpdateIGroup = `Update an igroup on a cluster by cluster name.`
120+
const DeleteIGroup = `Delete an igroup on a cluster by cluster name.`
121+
const AddIGroupInitiator = `Add an initiator to an igroup on a cluster by cluster name.`
122+
const RemoveIGroupInitiator = `Remove an initiator from an igroup on a cluster by cluster name.`
123+
124+
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.`
125+
const DeleteLunMap = `Delete a LUN map on a cluster by cluster name. Removes the mapping between a LUN and an igroup.`
126+
118127
const ListOntapEndpoints = `List ONTAP REST collection endpoints in the catalog.
119128
The catalog contains all endpoints — can be large. Prefer search_ontap_endpoints for targeted discovery.
120129
Use the optional 'match' parameter to filter by substring or regex pattern (e.g. "snapshot", "lun", ".*nfs.*export.*").

docs/examples.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,48 @@ Expected Response: A summary of aggregate free space, followed by a recommendati
409409

410410
---
411411

412+
### Manage iGroups (SAN)
413+
414+
**Create an iGroup**
415+
416+
- On the umeng-aff300-05-06 cluster, create an igroup named igroupFin with OS type linux and protocol iscsi on the marketing svm
417+
418+
Expected Response: igroup created successfully.
419+
420+
- On the umeng-aff300-05-06 cluster, create lun map of lun named /vol/docs/lunpayroll and an igroup named igroupFin on the marketing svm
421+
422+
Expected Response: lun map created successfully.
423+
424+
- On the umeng-aff300-05-06 cluster, delete lun map of lun named /vol/docs/lunpayroll and an igroup named igroupFin on the marketing svm
425+
426+
Expected Response: lun map deleted successfully.
427+
428+
**Rename an iGroup**
429+
430+
- On the umeng-aff300-05-06 cluster, rename igroup igroupFin to igroupFinNew and os type as windows on the marketing svm
431+
432+
Expected Response: igroup updated successfully.
433+
434+
**Add an Initiator to an iGroup**
435+
436+
- On the umeng-aff300-05-06 cluster, add initiator iqn.2021-01.com.example:test to igroup igroupFinNew on the marketing svm
437+
438+
Expected Response: initiator added to igroup successfully.
439+
440+
**Remove an Initiator from an iGroup**
441+
442+
- On the umeng-aff300-05-06 cluster, remove initiator iqn.2021-01.com.example:test from igroup igroupFinNew on the marketing svm
443+
444+
Expected Response: initiator removed from igroup successfully.
445+
446+
**Delete an iGroup**
447+
448+
- On the umeng-aff300-05-06 cluster, delete igroup igroupFinNew on the marketing svm
449+
450+
Expected Response: igroup deleted successfully.
451+
452+
---
453+
412454
## MCP Clients
413455

414456
Common MCP clients that work with ONTAP MCP Server:

integration/test/igroup_test.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"log/slog"
7+
"net/http"
8+
"testing"
9+
"time"
10+
11+
"github.com/carlmjohnson/requests"
12+
"github.com/netapp/ontap-mcp/config"
13+
)
14+
15+
func TestIGroupLUNMap(t *testing.T) {
16+
SkipIfMissing(t, CheckTools)
17+
18+
tests := []struct {
19+
name string
20+
input string
21+
expectedOntapErr string
22+
verifyAPI ontapVerifier
23+
}{
24+
{
25+
name: "Clean SVM",
26+
input: ClusterStr + "delete " + rn("marketing") + " svm",
27+
expectedOntapErr: "because it does not exist",
28+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
29+
},
30+
{
31+
name: "Create SVM",
32+
input: ClusterStr + "create " + rn("marketing") + " svm",
33+
expectedOntapErr: "",
34+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: createObject},
35+
},
36+
{
37+
name: "Create iSCSI service",
38+
input: ClusterStr + "create iscsi service on the " + rn("marketing") + " svm",
39+
expectedOntapErr: "",
40+
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: createObject},
41+
},
42+
{
43+
name: "Create svm scope network interface with ip 1",
44+
input: ClusterStr + "create network interface named " + rn("svg1") + " in " + rn("marketing") + " svm with ip address 10.63.41.117 and netmask 18 on node umeng-aff300-05 with service policy as blocks",
45+
expectedOntapErr: "",
46+
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: createObject},
47+
},
48+
{
49+
name: "Create svm scope network interface with ip 2",
50+
input: ClusterStr + "create network interface named " + rn("svg2") + " in " + rn("marketing") + " svm with ip address 10.63.41.118 and netmask 18 on node umeng-aff300-06 with service policy as blocks",
51+
expectedOntapErr: "",
52+
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg2") + "&scope=svm", validationFunc: createObject},
53+
},
54+
{
55+
name: "Clean igroup igroupFin",
56+
input: ClusterStr + "delete igroup " + rn("igroupFin") + " on the " + rn("marketing") + " svm",
57+
expectedOntapErr: "because it does not exist",
58+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
59+
},
60+
{
61+
name: "Clean igroup igroupFinNew",
62+
input: ClusterStr + "delete igroup " + rn("igroupFinNew") + " on the " + rn("marketing") + " svm",
63+
expectedOntapErr: "because it does not exist",
64+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
65+
},
66+
{
67+
name: "Clean volume doc",
68+
input: ClusterStr + "delete volume " + rn("doc") + " in " + rn("marketing") + " svm",
69+
expectedOntapErr: "because it does not exist",
70+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
71+
},
72+
{
73+
name: "Clean LUN lundoc",
74+
input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm",
75+
expectedOntapErr: "because it does not exist",
76+
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
77+
},
78+
{
79+
name: "Create igroup",
80+
input: ClusterStr + "create an igroup named " + rn("igroupFin") + " with OS type linux and protocol iscsi on the " + rn("marketing") + " svm",
81+
expectedOntapErr: "",
82+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
83+
},
84+
{
85+
name: "Add initiator to igroup",
86+
input: ClusterStr + "add initiator iqn.2021-01.com.example:test to igroup " + rn("igroupFin") + " on the " + rn("marketing") + " svm",
87+
expectedOntapErr: "",
88+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=" + rn("marketing") + "&fields=initiators", validationFunc: verifyInitiator(true, "iqn.2021-01.com.example:test")},
89+
},
90+
{
91+
name: "Remove initiator from igroup",
92+
input: ClusterStr + "remove initiator iqn.2021-01.com.example:test from igroup " + rn("igroupFin") + " on the " + rn("marketing") + " svm",
93+
expectedOntapErr: "",
94+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFin") + "&svm.name=" + rn("marketing") + "&fields=initiators", validationFunc: verifyInitiator(false, "iqn.2021-01.com.example:test")},
95+
},
96+
{
97+
name: "Rename igroup",
98+
input: ClusterStr + "rename igroup from " + rn("igroupFin") + " to " + rn("igroupFinNew") + " on the " + rn("marketing") + " svm",
99+
expectedOntapErr: "",
100+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
101+
},
102+
{
103+
name: "Create volume",
104+
input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
105+
expectedOntapErr: "",
106+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
107+
},
108+
{
109+
name: "Create LUN",
110+
input: ClusterStr + "create a 20MB lun named " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm with os type linux",
111+
expectedOntapErr: "",
112+
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
113+
},
114+
{
115+
name: "Create lun map",
116+
input: ClusterStr + "create lun map of lun named " + "/vol/" + rn("doc") + "/" + rn("lundoc") + " and an igroup named " + rn("igroupFinNew") + " on the " + rn("marketing") + " svm",
117+
expectedOntapErr: "",
118+
verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
119+
},
120+
{
121+
name: "Clean lun map",
122+
input: ClusterStr + "delete lun map of lun named " + "/vol/" + rn("doc") + "/" + rn("lundoc") + " and an igroup named " + rn("igroupFinNew") + " on the " + rn("marketing") + " svm",
123+
expectedOntapErr: "",
124+
verifyAPI: ontapVerifier{api: "api/protocols/san/lun-maps?igroup.name=" + rn("igroupFinNew") + "&lun.name=" + "/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
125+
},
126+
{
127+
name: "Clean volume",
128+
input: ClusterStr + "delete volume " + rn("doc") + " in " + rn("marketing") + " svm",
129+
expectedOntapErr: "",
130+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
131+
},
132+
{
133+
name: "Clean LUN",
134+
input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm",
135+
expectedOntapErr: "",
136+
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
137+
},
138+
{
139+
name: "Clean igroup",
140+
input: ClusterStr + "delete igroup " + rn("igroupFinNew") + " on the " + rn("marketing") + " svm",
141+
expectedOntapErr: "",
142+
verifyAPI: ontapVerifier{api: "api/protocols/san/igroups?name=" + rn("igroupFinNew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
143+
},
144+
{
145+
name: "Clean svm scope network interface with ip 1",
146+
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm",
147+
expectedOntapErr: "",
148+
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject},
149+
},
150+
{
151+
name: "Clean svm scope network interface with ip 2",
152+
input: ClusterStr + "delete svm scope network interface named " + rn("svg2") + " in " + rn("marketing") + " svm",
153+
expectedOntapErr: "",
154+
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg2") + "&scope=svm", validationFunc: deleteObject},
155+
},
156+
{
157+
name: "Update iSCSI service",
158+
input: ClusterStr + "disabled iscsi service on the " + rn("marketing") + " svm",
159+
expectedOntapErr: "",
160+
verifyAPI: ontapVerifier{},
161+
},
162+
{
163+
name: "Clean iSCSI service",
164+
input: ClusterStr + "delete iscsi service in " + rn("marketing") + " svm",
165+
expectedOntapErr: "",
166+
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: deleteObject},
167+
},
168+
{
169+
name: "Clean SVM",
170+
input: ClusterStr + "delete " + rn("marketing") + " svm",
171+
expectedOntapErr: "",
172+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
173+
},
174+
}
175+
176+
cfg, err := config.ReadConfig(ConfigFile)
177+
if err != nil {
178+
t.Fatalf("Error parsing the config: %v", err)
179+
}
180+
181+
poller := cfg.Pollers[Cluster]
182+
transport := &http.Transport{
183+
TLSClientConfig: &tls.Config{
184+
InsecureSkipVerify: poller.UseInsecureTLS, // #nosec G402
185+
},
186+
}
187+
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
188+
189+
for _, tt := range tests {
190+
t.Run(tt.name, func(t *testing.T) {
191+
slog.Debug("", slog.String("Input", tt.input))
192+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
193+
defer cancel()
194+
if _, err := testAgent.ChatWithResponse(ctx, t, tt.input, tt.expectedOntapErr); err != nil {
195+
t.Fatalf("Error processing input %q: %v", tt.input, err)
196+
}
197+
if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) {
198+
t.Errorf("Error while accessing the object via prompt %q", tt.input)
199+
}
200+
})
201+
}
202+
}
203+
204+
func verifyInitiator(exist bool, expectedInitiatorName string) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
205+
return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
206+
type InitiatorName struct {
207+
Name string `json:"name"`
208+
}
209+
type IGroup struct {
210+
Initiators []InitiatorName `json:"initiators"`
211+
}
212+
type response struct {
213+
NumRecords int `json:"num_records"`
214+
Records []IGroup `json:"records"`
215+
}
216+
217+
var data response
218+
var initiatorFound bool
219+
err := requests.URL("https://"+poller.Addr+"/"+api).
220+
BasicAuth(poller.Username, poller.Password).
221+
Client(client).
222+
ToJSON(&data).
223+
Fetch(context.Background())
224+
if err != nil {
225+
t.Errorf("verifyInitiator: request failed: %v", err)
226+
return false
227+
}
228+
if data.NumRecords != 1 {
229+
t.Errorf("verifyInitiator: expected 1 record, got %d", data.NumRecords)
230+
return false
231+
}
232+
gotIgroup := data.Records[0]
233+
for _, initiator := range gotIgroup.Initiators {
234+
if initiator.Name != expectedInitiatorName {
235+
continue
236+
}
237+
if !exist {
238+
t.Errorf("verifyInitiator: initiator should not exist")
239+
return false
240+
}
241+
initiatorFound = true
242+
}
243+
if !initiatorFound && exist {
244+
t.Errorf("verifyInitiator: initiator must exist")
245+
return false
246+
}
247+
return true
248+
}
249+
}

integration/test/lun_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ func TestLUN(t *testing.T) {
4444
expectedOntapErr: "because it does not exist",
4545
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
4646
},
47+
{
48+
name: "Clean volume",
49+
input: ClusterStr + "delete volume " + rn("doc") + " in " + rn("marketing") + " svm",
50+
expectedOntapErr: "because it does not exist",
51+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
52+
},
4753
{
4854
name: "Create volume",
4955
input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",

ontap/ontap.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type GetData struct {
5757
Clients []ClientData `json:"clients,omitzero"`
5858
Nas NAS `json:"nas,omitzero"`
5959
Schedule NameAndUUID `json:"schedule,omitzero"`
60+
Lun NameAndUUID `json:"lun,omitzero"`
61+
IGroup NameAndUUID `json:"igroup,omitzero"`
6062
} `json:"records"`
6163
NumRecords int `json:"num_records"`
6264
}
@@ -253,7 +255,7 @@ type NetworkIPInterface struct {
253255
IP IP `json:"ip,omitzero" jsonschema:"ip address"`
254256
Subnet NameAndUUID `json:"subnet,omitzero" jsonschema:"subnet name"`
255257
Location Location `json:"location,omitzero" jsonschema:"location name"`
256-
ServicePolicy NameAndUUID `json:"service_policy,omitzero" jsonschema:"service policy"`
258+
ServicePolicy NameAndUUID `json:"service_policy,omitzero" jsonschema:"service policy"` // default-data-files, default-data-blocks, default-data-iscsi, default-management, default-intercluster, default-route-announce
257259
}
258260

259261
type NVMeSubsystem struct {
@@ -315,6 +317,30 @@ type FCInterface struct {
315317
Location FCInterfaceLocation `json:"location,omitzero" jsonschema:"location of the FC interface"`
316318
}
317319

320+
type InitiatorName struct {
321+
Name string `json:"name,omitzero" jsonschema:"The FC WWPN, iSCSI IQN, or iSCSI EUI that identifies the host initiator."`
322+
}
323+
324+
type IGroupInitiator struct {
325+
Name string `json:"name,omitzero"`
326+
Comment string `json:"comment,omitzero"`
327+
Records []InitiatorName `json:"records,omitzero" jsonschema:"An array of initiators specified to add multiple initiators to an initiator group in a single API call. Not allowed when the name property is used."`
328+
}
329+
330+
type IGroup struct {
331+
SVM NameAndUUID `json:"svm,omitzero"`
332+
Name string `json:"name,omitzero"`
333+
OSType string `json:"os_type,omitzero"`
334+
Protocol string `json:"protocol,omitzero"`
335+
Comment string `json:"comment,omitzero"`
336+
}
337+
338+
type LunMap struct {
339+
SVM NameAndUUID `json:"svm,omitzero"`
340+
Lun NameAndUUID `json:"lun,omitzero"`
341+
IGroup NameAndUUID `json:"igroup,omitzero"`
342+
}
343+
318344
const (
319345
ASAr2 = "asar2"
320346
CDOT = "cdot"

0 commit comments

Comments
 (0)