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
1 change: 1 addition & 0 deletions descriptions/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const DescribeOntapEndpoint = `Get filterable query params for an endpoint. Call
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 UpdateSVM = `Update 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.
Expand Down
28 changes: 28 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,34 @@ Expected Response: The fcp service has been successfully updated.

---

### SVM Provisioning

**Create a SVM**

- On the umeng-aff300-05-06 cluster, create marketing svm

Expected Response: SVM created successfully.

**Rename a SVM**

- On the umeng-aff300-05-06 cluster, rename svm marketing to marketingNew

Expected Response: SVM updated successfully.

**Update a SVM**

- On the umeng-aff300-05-06 cluster, update svm marketingNew state to stopped and comment as `stop_svm`

Expected Response: SVM updated successfully.

**Delete a SVM**

- On the umeng-aff300-05-06 cluster, delete marketingNew svm

Expected Response: SVM deleted successfully.

---

### Querying Specific Fields

**Get volume space and protection details**
Expand Down
121 changes: 121 additions & 0 deletions integration/test/svm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package main

import (
"context"
"crypto/tls"
"github.com/carlmjohnson/requests"
"github.com/netapp/ontap-mcp/config"
"log/slog"
"net/http"
"testing"
"time"
)

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

tests := []struct {
name string
input string
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: "Rename SVM",
input: ClusterStr + "rename svm " + rn("marketing") + " to " + rn("marketingNew"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew"), validationFunc: createObject},
},
{
name: "Update SVM",
input: ClusterStr + "update svm " + rn("marketingNew") + " state to stopped and comment as `stop_svm`",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew") + "&fields=state,comment", validationFunc: verifySVM("stopped", "stop_svm")},
},
{
name: "Clean SVM",
input: ClusterStr + "delete " + rn("marketingNew") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/svm/svms?name=" + rn("marketingNew"), 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 verifySVM(expectedState string, expectedComment 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 SVM struct {
Name string `json:"name"`
State string `json:"state"`
Comment string `json:"comment"`
}
type response struct {
NumRecords int `json:"num_records"`
Records []SVM `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("verifySVM: request failed: %v", err)
return false
}
if data.NumRecords != 1 {
t.Errorf("verifySVM: expected 1 record, got %d", data.NumRecords)
return false
}

gotSVM := data.Records[0]
if gotSVM.State != expectedState {
t.Errorf("verifySVM: got state = %s, want %s", gotSVM.State, expectedState)
return false
}
if gotSVM.Comment != expectedComment {
t.Errorf("verifySVM: got comment = %s, want %s", gotSVM.Comment, expectedComment)
return false
}
return true
}
}
7 changes: 6 additions & 1 deletion ontap/ontap.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,14 @@ type CIFSShare struct {
Path string `json:"path,omitzero" jsonschema:"cifs share path"`
}

type SVM struct {
type SVMCreate struct {
Name string `json:"name" jsonschema:"svm name"`
}
type SVM struct {
Name string `json:"name,omitzero" jsonschema:"svm name"`
State string `json:"state,omitzero" jsonschema:"svm state"` // enum: starting, running, stopping, stopped, deleting, initializing
Comment string `json:"comment,omitzero" jsonschema:"comment"`
}

type LUNSpace struct {
Size int64 `json:"size,omitempty" jsonschema:"size of the LUN"`
Expand Down
42 changes: 41 additions & 1 deletion rest/svm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"net/url"
)

func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error {
func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVMCreate) error {
var (
buf bytes.Buffer
statusCode int
Expand All @@ -27,6 +27,46 @@ func (c *Client) CreateSVM(ctx context.Context, svm ontap.SVM) error {
return c.handleJob(ctx, statusCode, buf)
}

func (c *Client) UpdateSVM(ctx context.Context, svm ontap.SVM, 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",
svmName, svmData.NumRecords)
}
Comment thread
Hardikl marked this conversation as resolved.

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

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

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

func (c *Client) DeleteSVM(ctx context.Context, svmName string) error {
var (
buf bytes.Buffer
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ func (a *App) createMCPServer() *mcp.Server {

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

// operation on CIFS share object
Expand Down
61 changes: 58 additions & 3 deletions server/svm.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/netapp/ontap-mcp/tool"
)

func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVM) (*mcp.CallToolResult, any, error) {
func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.SVMCreate) (*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
}
Expand Down Expand Up @@ -37,6 +37,34 @@ func (a *App) CreateSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters
}, nil, nil
}

func (a *App) UpdateSVM(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)

svmUpdate, err := newUpdateSVM(parameters)
if err != nil {
return nil, nil, err
}

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

err = client.UpdateSVM(ctx, svmUpdate, parameters.Name)
Comment thread
Hardikl marked this conversation as resolved.
if err != nil {
return errorResult(err), nil, err
}

return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "SVM updated 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
Expand Down Expand Up @@ -64,11 +92,38 @@ func (a *App) DeleteSVM(ctx context.Context, _ *mcp.CallToolRequest, parameters
}, nil, nil
}

func newCreateSVM(in tool.SVM) (ontap.SVM, error) {
out := ontap.SVM{}
func newCreateSVM(in tool.SVMCreate) (ontap.SVMCreate, error) {
out := ontap.SVMCreate{}
if in.Name == "" {
return out, errors.New("SVM name is required")
}
out.Name = in.Name
return out, nil
}

func newUpdateSVM(in tool.SVM) (ontap.SVM, error) {
out := ontap.SVM{}

if in.Name == "" {
return out, errors.New("SVM name is required")
}

hasUpdate := false
if in.NewName != "" {
out.Name = in.NewName
hasUpdate = true
}
if in.Comment != "" {
out.Comment = in.Comment
hasUpdate = true
}
if in.State != "" {
out.State = in.State
hasUpdate = true
}
if !hasUpdate {
return out, errors.New("at least one updatable field must be provided: new_name, comment, or state")
}

return out, nil
}
8 changes: 8 additions & 0 deletions tool/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,15 @@ type DescribeEndpointParams struct {
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 SVMCreate struct {
Cluster string `json:"cluster_name" jsonschema:"cluster name"`
Name string `json:"svm_name" jsonschema:"SVM name"`
}

type SVM struct {
Cluster string `json:"cluster_name" jsonschema:"cluster name"`
Name string `json:"svm_name" jsonschema:"SVM name"`
NewName string `json:"new_name,omitzero" jsonschema:"new name of SVM"`
State string `json:"state,omitzero" jsonschema:"state of SVM (e.g., starting, running, stopping, stopped, deleting, initializing)"`
Comment string `json:"comment,omitzero" jsonschema:"comment"`
}
Loading