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
9 changes: 9 additions & 0 deletions descriptions/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ const RemoveIGroupInitiator = `Remove an initiator from an igroup on a cluster b
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 CreateSnapMirror = `Create a SnapMirror relationship on a cluster by cluster name.`
const UpdateSnapMirror = `Update a SnapMirror relationship on a cluster by cluster name. Supports updating the policy and transfer schedule of an existing relationship identified by its destination SVM and volume.`
const DeleteSnapMirror = `Delete a SnapMirror relationship on a cluster by cluster name. Identifies the relationship by destination SVM and volume names.`
const InitializeSnapMirror = `Initialize a SnapMirror relationship on a cluster by cluster name. Starts the baseline transfer from source to destination. Identifies the relationship by destination SVM and volume names.`
const UpdateSnapMirrorTransfer = `Trigger a SnapMirror update transfer on a cluster by cluster name. Transfers new data from source to destination to bring the relationship up to date. Identifies the relationship by destination SVM and volume names.`
const BreakSnapMirror = `Break a SnapMirror relationship on a cluster by cluster name. Sets the relationship state to broken_off, making the destination volume read-write. Identifies the relationship by destination SVM and volume names.`
const ResyncSnapMirror = `Resync a SnapMirror relationship on a cluster by cluster name. Re-establishes replication by setting the state back to snapmirrored. Identifies the relationship by destination SVM and volume names.`

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 All @@ -141,6 +149,7 @@ Pass cluster_name to automatically filter out fields and filters not available i
const CreateSVM = `Create an SVM on a cluster by cluster name.`
const UpdateSVM = `Update an SVM on a cluster by cluster name.`
const DeleteSVM = `Delete an SVM on a cluster by cluster name.`
const DeleteSVMPeer = `Delete an SVM peer on a cluster by cluster name and local SVM name. The peer relationship UUID is looked up internally using the svm.name filter.`

const OntapGet = `Execute a read-only GET against any ONTAP REST endpoint.

Expand Down
32 changes: 32 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,38 @@ Below are example questions that work well with the ONTAP MCP Server:

---

### Manage SnapMirror Relationships

**Create a SnapMirror relationship**

- On the umeng-aff300-05-06 cluster, create a snapmirror relationship from source svm srsvm and source volume srvol to destination svm dtsvm and destination volume dtvol with policy name XDPDefault

Expected Response: SnapMirror relationship created successfully

**Update a SnapMirror relationship**

- On the umeng-aff300-05-06 cluster, update a snapmirror relationship of destination svm dtsvm and destination volume dtvol with transfer schedule name to hourly

Expected Response: SnapMirror relationship updated successfully

**Update a SnapMirror relationship state**

- On the umeng-aff300-05-06 cluster, break a snapmirror relationship of destination svm dtsvm and destination volume dtvol

Expected Response: SnapMirror relationship broken successfully

- On the umeng-aff300-05-06 cluster, resync a snapmirror relationship of destination svm dtsvm and destination volume dtvol

Expected Response: SnapMirror relationship resynced successfully

**Delete a SnapMirror relationship**

- On the umeng-aff300-05-06 cluster, delete a snapmirror relationship of destination svm dtsvm and destination volume dtvol

Expected Response: SnapMirror relationship deleted successfully

---

## MCP Clients

Common MCP clients that work with ONTAP MCP Server:
Expand Down
223 changes: 223 additions & 0 deletions integration/test/snapmirror_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package main

import (
"context"
"crypto/tls"
"log/slog"
"net/http"
"testing"
"time"

"github.com/carlmjohnson/requests"
"github.com/netapp/ontap-mcp/config"
)

func TestSnapMirror(t *testing.T) {
SkipIfMissing(t, CheckTools)

tests := []struct {
name string
input string
expectedOntapErr string
verifyAPI ontapVerifier
}{
{
name: "Clean source SVM",
input: ClusterStr + "delete " + rn("srsvm") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("srsvm"), validationFunc: deleteObject},
},
{
name: "Clean destination SVM",
input: ClusterStr + "delete " + rn("dtsvm") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("dtsvm"), validationFunc: deleteObject},
},
{
name: "Create source SVM",
input: ClusterStr + "create " + rn("srsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("srsvm"), validationFunc: createObject},
},
{
name: "Create destination SVM",
input: ClusterStr + "create " + rn("dtsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("dtsvm"), validationFunc: createObject},
},
{
name: "Clean source volume",
input: ClusterStr + "delete volume " + rn("srvol") + " in " + rn("srsvm") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("srvol") + "&svm.name=" + rn("srsvm"), validationFunc: deleteObject},
},
{
name: "Clean destination volume",
input: ClusterStr + "delete volume " + rn("dtvol") + " in " + rn("dtsvm") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("dtvol") + "&svm.name=" + rn("dtsvm"), validationFunc: deleteObject},
},
{
name: "Create source volume",
input: ClusterStr + "create a 100MB volume named " + rn("srvol") + " on the " + rn("srsvm") + " svm and the harvest_vc_aggr aggregate",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("srvol") + "&svm.name=" + rn("srsvm"), validationFunc: createObject},
},
{
name: "Create destination volume",
input: ClusterStr + "create a 100MB volume named " + rn("dtvol") + " on the " + rn("dtsvm") + " svm and the harvest_vc_aggr aggregate with dp type",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("dtvol") + "&svm.name=" + rn("dtsvm"), validationFunc: createObject},
},
{
name: "Create SnapMirror relationship",
input: ClusterStr + "create a snapmirror relationship from source svm " + rn("srsvm") + " and source volume " + rn("srvol") + " to destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol") + " with policy name Asynchronous",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "Asynchronous", "uninitialized")},
},
{
name: "Update SnapMirror relationship",
input: ClusterStr + "update a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol") + " with transfer schedule name to hourly",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "Asynchronous", "uninitialized")},
},
{
name: "Update SnapMirror relationship 2",
input: ClusterStr + "update a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol") + " with policy name MirrorAndVault",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "MirrorAndVault", "uninitialized")},
},
{
name: "Initialize SnapMirror relationship",
input: ClusterStr + "initialize a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "MirrorAndVault", "snapmirrored")},
},
{
name: "Break SnapMirror relationship",
input: ClusterStr + "break a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "MirrorAndVault", "broken_off")},
},
{
name: "Resync SnapMirror relationship",
input: ClusterStr + "resync a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(true, "MirrorAndVault", "snapmirrored")},
},
{
name: "Delete SnapMirror relationship",
input: ClusterStr + "delete a snapmirror relationship of destination svm " + rn("dtsvm") + " and destination volume " + rn("dtvol"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/snapmirror/relationships?destination.path=" + rn("dtsvm") + ":" + rn("dtvol") + "&fields=state,policy.name", validationFunc: verifySnapMirror(false, "", "")},
},
{
name: "Clean source volume",
input: ClusterStr + "delete volume " + rn("srvol") + " in " + rn("srsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("srvol") + "&svm.name=" + rn("srsvm"), validationFunc: deleteObject},
},
{
name: "Clean destination volume",
input: ClusterStr + "delete volume " + rn("dtvol") + " in " + rn("dtsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("dtvol") + "&svm.name=" + rn("dtsvm"), validationFunc: deleteObject},
},
{
name: "Clean SVM peer",
input: ClusterStr + "delete svm peer of " + rn("srsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/peers?svm.name=" + rn("srsvm"), validationFunc: deleteObject},
},
{
name: "Clean source SVM",
input: ClusterStr + "delete " + rn("srsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("srsvm"), validationFunc: deleteObject},
},
{
name: "Clean destination SVM",
input: ClusterStr + "delete " + rn("dtsvm") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("dtsvm"), validationFunc: deleteObject},
},
}

cfg, err := config.ReadConfig(ConfigFile)
if err != nil {
t.Fatalf("Error parsing the config: %v", err)
}

poller := cfg.Pollers[Cluster]
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: poller.UseInsecureTLS, // #nosec G402
},
}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
slog.Debug("", slog.String("Input", tt.input))
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
if _, err := testAgent.ChatWithResponse(ctx, t, tt.input, tt.expectedOntapErr); err != nil {
t.Fatalf("Error processing input %q: %v", tt.input, err)
}
if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) {
t.Errorf("Error while accessing the object via prompt %q", tt.input)
}
})
}
}

func verifySnapMirror(exist bool, expectedPolicyName string, expectedState string) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
type NameData struct {
Name string `json:"name,omitempty"`
}
type SnapMirrorRelationship struct {
Policy NameData `json:"policy"`
State string `json:"state"`
}
type response struct {
NumRecords int `json:"num_records"`
Records []SnapMirrorRelationship `json:"records"`
}

var data response
err := requests.URL("https://"+poller.Addr+"/"+api).
BasicAuth(poller.Username, poller.Password).
Client(client).
ToJSON(&data).
Fetch(context.Background())
if err != nil {
t.Errorf("verifySnapMirror: request failed: %v", err)
return false
}

if exist {
if data.NumRecords != 1 {
t.Errorf("verifySnapMirror: expected 1 record, got %d", data.NumRecords)
return false
}

gotSnapMirror := data.Records[0]
if gotSnapMirror.Policy.Name != expectedPolicyName {
t.Errorf("verifySnapMirror: expected policy name %s, got %s", expectedPolicyName, gotSnapMirror.Policy.Name)
return false
}
if gotSnapMirror.State != expectedState {
t.Errorf("verifySnapMirror: expected state %s, got %s", expectedState, gotSnapMirror.State)
return false
}
return true
}

if !exist && data.NumRecords > 0 {
t.Errorf("verifySnapMirror: expected 0 record, got %d", data.NumRecords)
return false
}
return true
}
}
12 changes: 0 additions & 12 deletions integration/test/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,3 @@ func deleteObject(t *testing.T, api string, poller *config.Poller, client *http.
}
return true
}

func listObject(t *testing.T, api string, poller *config.Poller, client *http.Client) bool {
err := requests.URL("https://"+poller.Addr+"/"+api).
BasicAuth(poller.Username, poller.Password).
Client(client).
Fetch(context.Background())
if err != nil {
t.Errorf("listObject: request failed: %v", err)
return false
}
return true
}
Loading
Loading