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 @@ -94,6 +94,9 @@ const SearchOntapEndpoints = `Search the catalog by keyword across endpoint path
const DescribeOntapEndpoint = `Get filterable query params for an endpoint. Call before ontap_get to learn valid filter names and which sub-objects need explicit fields (e.g. "space.*", "efficiency.*").
Pass cluster_name to automatically filter out fields and filters not available in that cluster's ONTAP version.`

const CreateSVM = `Create an SVM on a cluster by cluster name.`
const DeleteSVM = `Delete an SVM on a cluster by cluster name.`

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

RULES:
Expand Down
42 changes: 30 additions & 12 deletions integration/test/iscsi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,29 @@ func TestIscsiProtocol(t *testing.T) {
expectedOntapErr string
verifyAPI ontapVerifier
}{
{
name: "Clean SVM",
input: ClusterStr + "delete " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create SVM",
input: ClusterStr + "create " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean iSCSI service",
input: ClusterStr + "delete iscsi service in marketing svm",
input: ClusterStr + "delete iscsi service in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: deleteObject},
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Create iSCSI service",
input: ClusterStr + "create iscsi service target named alias " + rn("tgpath") + " on the marketing svm",
input: ClusterStr + "create iscsi service target named alias " + rn("tgpath") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: createObject},
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Clean cluster scope network interface",
Expand All @@ -40,7 +52,7 @@ func TestIscsiProtocol(t *testing.T) {
},
{
name: "Clean svm scope network interface",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject},
},
Expand All @@ -52,7 +64,7 @@ func TestIscsiProtocol(t *testing.T) {
},
{
name: "Create svm scope network interface with ip",
input: ClusterStr + "create network interface named " + rn("svg1") + " in marketing svm with ip address 10.63.41.7 and netmask 18 on node umeng-aff300-06",
input: ClusterStr + "create network interface named " + rn("svg1") + " in " + rn("marketing") + " svm with ip address 10.63.41.7 and netmask 18 on node umeng-aff300-06",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: createObject},
},
Expand All @@ -64,33 +76,39 @@ func TestIscsiProtocol(t *testing.T) {
},
{
name: "Clean svm scope network interface",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject},
},
{
name: "Create svm scope network interface with broadcast domain",
input: ClusterStr + "create network interface named " + rn("svg1") + " in marketing svm with ip address 10.63.41.7 and netmask 18 on broadcast domain as Default",
input: ClusterStr + "create network interface named " + rn("svg1") + " in " + rn("marketing") + " svm with ip address 10.63.41.7 and netmask 18 on broadcast domain as Default",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: createObject},
},
{
name: "Clean svm scope network interface with broadcast domain",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in marketing svm",
input: ClusterStr + "delete svm scope network interface named " + rn("svg1") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/network/ip/interfaces?name=" + rn("svg1") + "&scope=svm", validationFunc: deleteObject},
},
{
name: "Update iSCSI service",
input: ClusterStr + "disabled iscsi service on the marketing svm",
input: ClusterStr + "disabled iscsi service on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{},
},
{
name: "Clean iSCSI service",
input: ClusterStr + "delete iscsi service in marketing svm",
input: ClusterStr + "delete iscsi service in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Clean SVM",
input: ClusterStr + "delete " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/protocols/san/iscsi/services?svm.name=marketing", validationFunc: deleteObject},
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketing"), validationFunc: deleteObject},
},
}

Expand Down
4 changes: 4 additions & 0 deletions ontap/ontap.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ type CIFSShare struct {
Path string `json:"path,omitzero" jsonschema:"cifs share path"`
}

type SVM struct {
Name string `json:"name" jsonschema:"svm name"`
}

type Qtree struct {
SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"`
Volume NameAndUUID `json:"volume,omitzero" jsonschema:"volume name"`
Expand Down
67 changes: 67 additions & 0 deletions rest/svm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package rest

import (
"bytes"
"context"
"fmt"
"github.com/netapp/ontap-mcp/ontap"
"net/http"
"net/url"
)

func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error {
var (
buf bytes.Buffer
statusCode int
)
responseHeaders := http.Header{}

builder := c.baseRequestBuilder(`/api/svm/svms`, &statusCode, responseHeaders).
BodyJSON(svm).
ToBytesBuffer(&buf)

if err := c.buildAndExecuteRequest(ctx, builder); err != nil {
return err
}

return c.handleJob(ctx, statusCode, buf)
}

func (c *Client) DeleteSVM(ctx context.Context, svmName string) error {
var (
buf bytes.Buffer
statusCode int
svmData ontap.GetData
)
responseHeaders := http.Header{}

params := url.Values{}
params.Set("name", svmName)
params.Set("fields", "uuid")

builder := c.baseRequestBuilder(`/api/svm/svms`, &statusCode, responseHeaders).
Params(params).
ToJSON(&svmData)

if err := c.buildAndExecuteRequest(ctx, builder); err != nil {
return err
}

if svmData.NumRecords == 0 {
return fmt.Errorf("failed to get details of SVM %s because it does not exist", svmName)
}
if svmData.NumRecords != 1 {
return fmt.Errorf("failed to get detail of SVM %s because there are %d matching records",
Comment thread
Hardikl marked this conversation as resolved.
svmName, svmData.NumRecords)
}

builder2 := c.baseRequestBuilder(`/api/svm/svms/`+svmData.Records[0].UUID, &statusCode, responseHeaders).
Delete().
ToBytesBuffer(&buf)

if err := c.buildAndExecuteRequest(ctx, builder2); err != nil {
return err
}

return c.handleJob(ctx, statusCode, buf)
}
4 changes: 4 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func (a *App) createMCPServer() *mcp.Server {
addTool(a, server, "update_nfs_export_policies_rules", descriptions.UpdateNFSExportPolicyRules, updateAnnotation, a.UpdateNFSExportPoliciesRule)
addTool(a, server, "delete_nfs_export_policies_rules", descriptions.DeleteNFSExportPolicyRules, deleteAnnotation, a.DeleteNFSExportPoliciesRule)

// operation on SVM object
addTool(a, server, "create_svm", descriptions.CreateSVM, createAnnotation, a.CreateSVM)
addTool(a, server, "delete_svm", descriptions.DeleteSVM, deleteAnnotation, a.DeleteSVM)

// operation on CIFS share object
addTool(a, server, "create_cifs_share", descriptions.CreateCIFSShare, createAnnotation, a.CreateCIFSShare)
addTool(a, server, "update_cifs_share", descriptions.UpdateCIFSShare, updateAnnotation, a.UpdateCIFSShare)
Expand Down
74 changes: 74 additions & 0 deletions server/svm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*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)

svmCreate, err := newCreateSVM(parameters)
if err != nil {
return nil, nil, err
}

client, err := a.getClient(parameters.Cluster)
if err != nil {
return errorResult(err), nil, err
}

err = client.CreateSVM(ctx, svmCreate)
if err != nil {
return errorResult(err), nil, err
}

return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "SVM created successfully"},
},
}, nil, nil
}

func (a *App) DeleteSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*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 parameters.Name == "" {
return nil, nil, errors.New("SVM name is required")
}

client, err := a.getClient(parameters.Cluster)
if err != nil {
return errorResult(err), nil, err
}

err = client.DeleteSVM(ctx, parameters.Name)
if err != nil {
return errorResult(err), nil, err
}

return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "SVM deleted successfully"},
},
}, nil, nil
}

func newCreateSVM(in tool.SVM) (ontap.SVM, error) {
out := ontap.SVM{}
if in.Name == "" {
return out, errors.New("SVM name is required")
}
out.Name = in.Name
return out, nil
}
5 changes: 5 additions & 0 deletions tool/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,8 @@ type DescribeEndpointParams struct {
Path string `json:"path" jsonschema:"ONTAP REST API path, e.g. /storage/volumes"`
Cluster string `json:"cluster_name,omitzero" jsonschema:"cluster name — if provided, filters out fields and filters not available in that cluster's ONTAP version"`
}

type SVM struct {
Cluster string `json:"cluster_name" jsonschema:"cluster name"`
Name string `json:"svm_name" jsonschema:"SVM name"`
}
Loading