Skip to content

Commit 9f64548

Browse files
authored
feat: adding snapshot crud tool (#114)
1 parent a2e1e23 commit 9f64548

11 files changed

Lines changed: 761 additions & 281 deletions

File tree

descriptions/descriptions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const CreateVolume = `Create a volume on a cluster by cluster name.`
3636
const DeleteVolume = `Delete a volume on a cluster by cluster name.`
3737
const UpdateVolume = `Update volume name, size, state, nfs export policy of volume on a cluster by cluster name.`
3838

39+
const CreateSnapshot = `Create a snapshot of a volume on a cluster by cluster name.`
40+
const DeleteSnapshot = `Delete a snapshot of a volume on a cluster by cluster name.`
41+
const RestoreSnapshot = `Restore a volume to a snapshot on a cluster by cluster name.`
42+
3943
const CreateSnapshotPolicy = `Create a snapshot policy on a cluster by cluster name.`
4044
const UpdateSnapshotPolicy = `Update a snapshot policy on a cluster by cluster name.`
4145
const DeleteSnapshotPolicy = `Delete a snapshot policy on a cluster by cluster name.`

docs/examples.md

Lines changed: 187 additions & 259 deletions
Large diffs are not rendered by default.

docs/tools.md

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,122 @@
22

33
The following tools are provided by the ONTAP MCP server.
44

5+
ONTAP MCP provides a set of tools that can be used to interact with the ONTAP API. These tools are designed to help users discover and manage their ONTAP clusters more efficiently. The tools are categorized based on their functionality, such as API discovery, volume management, data protection, CIFS/SMB integration, NFS export policy management, performance management, SVM management, qtree management, network interface management, LUN and igroup management, iSCSI management, FCP management, NVMe management, and multi-cluster management.
6+
7+
All ONTAP MCP tools are annotated with hint metadata: `readOnlyHint`, `idempotentHint`, and `destructiveHint`. The `readOnlyHint` indicates that the tool does not modify any data and is safe to use for discovery and information retrieval. The `destructiveHint` indicates that the tool performs actions that can modify or delete data, and should be used with caution.
8+
9+
If you want to run the ONTAP MCP server in read-only mode, you can start the server with the `--read-only` flag. In this mode, only tools with the `readOnlyHint` will be available for use, ensuring that no modifications can be made to the ONTAP cluster. See the [configuration documentation](install.md#configuration) for more details on how to start the server in read-only mode.
10+
11+
## API Discovery
12+
13+
- `list_ontap_endpoints` (available when the API catalog is loaded)
14+
- `search_ontap_endpoints` (available when the API catalog is loaded)
15+
- `describe_ontap_endpoint` (available when the API catalog is loaded)
16+
- `ontap_get`
17+
518
## Volume Management
619

7-
- Volume lifecycle management: create, read, update, delete, resize
8-
- Volume autogrowth: enable, disable, status
9-
- Volume updates (multiple properties in a single operation)
10-
- QoS policy and snapshot policy assignment
11-
- NFS access control with export policies
12-
- Volume details: capacity, usage
20+
- `create_volume`
21+
- `update_volume`
22+
- `delete_volume`
1323

1424
## Data Protection
1525

16-
- Snapshot policies with flexible scheduling
17-
- Snapshot schedules
18-
- Policy application to SVMs
26+
- `create_snapshot`
27+
- `delete_snapshot`
28+
- `restore_snapshot`
29+
- `create_snapshot_policy`
30+
- `update_snapshot_policy`
31+
- `delete_snapshot_policy`
32+
- `create_schedule`
33+
- `add_schedule_in_snapshot_policy`
34+
- `update_schedule_in_snapshot_policy`
35+
- `remove_schedule_in_snapshot_policy`
1936

2037
## CIFS/SMB Integration
2138

22-
- CIFS list management: create, read, update, delete
23-
- Integration with volume provisioning
39+
- `create_cifs_share`
40+
- `update_cifs_share`
41+
- `delete_cifs_share`
2442

2543
## NFS Export Policy Management
2644

27-
- Export policy management: create, read, update, delete
28-
- Volume-to-policy association
45+
- `create_nfs_export_policies`
46+
- `update_nfs_export_policies`
47+
- `delete_nfs_export_policies`
48+
- `create_nfs_export_policies_rules`
49+
- `update_nfs_export_policies_rules`
50+
- `delete_nfs_export_policies_rules`
2951

3052
## Performance Management
3153

32-
- QoS policy management: create, read, update, delete
33-
- QoS policy assignment to SVMs
34-
- Fixed QoS policies with IOPS/bandwidth limits
35-
- Adaptive QoS policies with dynamic scaling
54+
- `list_qos_policies`
55+
- `create_qos_policy`
56+
- `update_qos_policy`
57+
- `delete_qos_policy`
58+
59+
## SVM Management
60+
61+
- `create_svm`
62+
- `update_svm`
63+
- `delete_svm`
64+
65+
## Qtree Management
66+
67+
- `create_qtree`
68+
- `update_qtree`
69+
- `delete_qtree`
70+
71+
## Network Interface Management
72+
73+
- `create_network_ip_interface`
74+
- `update_network_ip_interface`
75+
- `delete_network_ip_interface`
76+
77+
## LUN and igroup Management
78+
79+
- `create_lun`
80+
- `update_lun`
81+
- `delete_lun`
82+
- `create_igroup`
83+
- `update_igroup`
84+
- `delete_igroup`
85+
- `add_igroup_initiator`
86+
- `remove_igroup_initiator`
87+
- `create_lun_map`
88+
- `delete_lun_map`
89+
90+
## iSCSI Management
91+
92+
- `create_iscsi_service`
93+
- `update_iscsi_service`
94+
- `delete_iscsi_service`
95+
96+
## FCP Management
97+
98+
- `create_fcp_service`
99+
- `update_fcp_service`
100+
- `delete_fcp_service`
101+
- `create_fc_interface`
102+
- `update_fc_interface`
103+
- `delete_fc_interface`
104+
105+
## NVMe Management
106+
107+
- `create_nvme_service`
108+
- `update_nvme_service`
109+
- `delete_nvme_service`
110+
- `create_nvme_subsystem`
111+
- `update_nvme_subsystem`
112+
- `delete_nvme_subsystem`
113+
- `add_nvme_subsystem_host`
114+
- `remove_nvme_subsystem_host`
115+
- `create_nvme_namespace`
116+
- `update_nvme_namespace`
117+
- `delete_nvme_namespace`
118+
- `create_nvme_subsystem_map`
119+
- `delete_nvme_subsystem_map`
36120

37121
## Multi-Cluster Management
38122

39-
- Unified management of multiple ONTAP clusters
40-
- Centralized credential management
123+
- `list_registered_clusters`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/netapp/ontap-mcp/config"
1515
)
1616

17-
func TestSnapshot(t *testing.T) {
17+
func TestSnapshotPolicy(t *testing.T) {
1818
SkipIfMissing(t, CheckTools)
1919

2020
tests := []struct {

integration/test/snapshot_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"log/slog"
7+
"net/http"
8+
"slices"
9+
"testing"
10+
"time"
11+
12+
"github.com/netapp/ontap-mcp/ontap"
13+
14+
"github.com/carlmjohnson/requests"
15+
"github.com/netapp/ontap-mcp/config"
16+
)
17+
18+
func TestSnapshot(t *testing.T) {
19+
SkipIfMissing(t, CheckTools)
20+
21+
tests := []struct {
22+
name string
23+
input string
24+
expectedOntapErr string
25+
verifyAPI ontapVerifier
26+
}{
27+
{
28+
name: "Clean SVM",
29+
input: ClusterStr + "delete " + rn("marketing") + " svm",
30+
expectedOntapErr: "because it does not exist",
31+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
32+
},
33+
{
34+
name: "Create SVM",
35+
input: ClusterStr + "create " + rn("marketing") + " svm",
36+
expectedOntapErr: "",
37+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: createObject},
38+
},
39+
{
40+
name: "Clean volume",
41+
input: ClusterStr + "delete volume " + rn("docs") + " in " + rn("marketing") + " svm",
42+
expectedOntapErr: "because it does not exist",
43+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: deleteObject},
44+
},
45+
{
46+
name: "Create volume",
47+
input: ClusterStr + "create a 20MB volume named " + rn("docs") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
48+
expectedOntapErr: "",
49+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: createObject},
50+
},
51+
{
52+
name: "Clean snapshot",
53+
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
54+
expectedOntapErr: "does not exist",
55+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
56+
},
57+
{
58+
name: "Create snapshot",
59+
input: ClusterStr + "create a snapshot named " + rn("localsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
60+
expectedOntapErr: "",
61+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
62+
},
63+
{
64+
name: "Create 2nd snapshot",
65+
input: ClusterStr + "create a snapshot named " + rn("recentsnap") + " in the volume " + rn("docs") + " on the " + rn("marketing") + " svm",
66+
expectedOntapErr: "",
67+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("recentsnap"), rn("localsnap")}, true, 2)},
68+
},
69+
{
70+
name: "Restore volume from snapshot",
71+
input: ClusterStr + "Restore " + rn("docs") + " volume from a snapshot named " + rn("localsnap") + " in the " + rn("marketing") + " svm",
72+
expectedOntapErr: "",
73+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{rn("localsnap")}, true, 1)},
74+
},
75+
{
76+
name: "Clean snapshot",
77+
input: ClusterStr + "Delete " + rn("localsnap") + " snapshot in " + rn("docs") + " volume in " + rn("marketing") + " svm",
78+
expectedOntapErr: "",
79+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: verifySnapshot([]string{}, false, 0)},
80+
},
81+
{
82+
name: "Clean volume",
83+
input: ClusterStr + "delete volume " + rn("docs") + " in " + rn("marketing") + " svm",
84+
expectedOntapErr: "",
85+
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("docs") + "&svm=" + rn("marketing"), validationFunc: deleteObject},
86+
},
87+
{
88+
name: "Clean SVM",
89+
input: ClusterStr + "delete " + rn("marketing") + " svm",
90+
expectedOntapErr: "",
91+
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
92+
},
93+
}
94+
95+
cfg, err := config.ReadConfig(ConfigFile)
96+
if err != nil {
97+
t.Fatalf("Error parsing the config: %v", err)
98+
}
99+
100+
poller := cfg.Pollers[Cluster]
101+
transport := &http.Transport{
102+
TLSClientConfig: &tls.Config{
103+
InsecureSkipVerify: poller.UseInsecureTLS, // #nosec G402
104+
},
105+
}
106+
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
slog.Debug("", slog.String("Input", tt.input))
111+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
112+
defer cancel()
113+
if _, err = testAgent.ChatWithResponse(ctx, t, tt.input, tt.expectedOntapErr); err != nil {
114+
t.Fatalf("Error processing input %q: %v", tt.input, err)
115+
}
116+
if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) {
117+
t.Errorf("Error while accessing the object via prompt %s", tt.input)
118+
}
119+
})
120+
}
121+
}
122+
123+
func verifySnapshot(snapshotNames []string, exist bool, snapshotCount int) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
124+
return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
125+
var data ontap.GetData
126+
var gotSnapshotNames []string
127+
err := requests.URL("https://"+poller.Addr+"/"+api).
128+
BasicAuth(poller.Username, poller.Password).
129+
Client(client).
130+
ToJSON(&data).
131+
Fetch(context.Background())
132+
if err != nil {
133+
t.Errorf("verifySnapshot: request failed: %v", err)
134+
return false
135+
}
136+
if data.NumRecords != 1 {
137+
t.Errorf("verifySnapshot: expected %d records, got %d", snapshotCount, data.NumRecords)
138+
return false
139+
}
140+
volumeUUID := data.Records[0].UUID
141+
142+
err = requests.URL("https://"+poller.Addr+"/"+"api/storage/volumes/"+volumeUUID+"/snapshots").
143+
BasicAuth(poller.Username, poller.Password).
144+
Client(client).
145+
ToJSON(&data).
146+
Fetch(context.Background())
147+
if err != nil {
148+
t.Errorf("verifySnapshot: request failed: %v", err)
149+
return false
150+
}
151+
152+
if !exist {
153+
if data.NumRecords != 0 {
154+
t.Errorf("verifySnapshot: expected 0 record, got %d", data.NumRecords)
155+
return false
156+
}
157+
return true
158+
}
159+
if data.NumRecords != snapshotCount {
160+
t.Errorf("verifySnapshot: expected 1 record, got %d", data.NumRecords)
161+
return false
162+
}
163+
164+
for _, record := range data.Records {
165+
gotSnapshotNames = append(gotSnapshotNames, record.Name)
166+
}
167+
168+
slices.Sort(gotSnapshotNames)
169+
slices.Sort(snapshotNames)
170+
if !slices.Equal(gotSnapshotNames, snapshotNames) {
171+
t.Errorf("verifySnapshot: snapshot name = %q, want %q", gotSnapshotNames, snapshotNames)
172+
return false
173+
}
174+
175+
return true
176+
}
177+
}

ontap/ontap.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ type SnapshotPolicySchedule struct {
157157
SnapmirrorLabel string `json:"snapmirror_label,omitzero" jsonschema:"SnapMirror label for this schedule"`
158158
}
159159

160+
type Snapshot struct {
161+
Name string `json:"name" jsonschema:"snapshot name"`
162+
}
163+
164+
type RestoreTo struct {
165+
Snapshot Snapshot `json:"snapshot" jsonschema:"restore volume with a snapshot"`
166+
}
167+
168+
type SnapshotRestore struct {
169+
RestoreTo RestoreTo `json:"restore_to" jsonschema:"which snapshot to restore"`
170+
}
171+
160172
type Cron struct {
161173
Days []int `json:"days,omitzero"`
162174
Hours []int `json:"hours,omitzero"`

0 commit comments

Comments
 (0)