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
4 changes: 4 additions & 0 deletions descriptions/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ const DeleteNVMeNamespace = `Delete NVMe namespace on a cluster by cluster name.
const CreateNVMeSubsystemMap = `Create NVMe subsystem map on a cluster by cluster name.`
const DeleteNVMeSubsystemMap = `Delete NVMe subsystem map on a cluster by cluster name.`

const CreateLUN = `Create a LUN on a specified volume and SVM with a given size and OS type.`
const UpdateLUN = `Update a LUN: resize, rename, or toggle enabled/disabled state (online/offline).`
const DeleteLUN = `Delete a LUN from a specified volume and SVM.`

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 Down
34 changes: 34 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,40 @@ Expected Response: The Network IP interface deleted successfully.

---

### LUN Provisioning

**Create a LUN**

- On the umeng-aff300-05-06 cluster, create a 20MB lun named lundoc in volume doc on the marketing svm with os type linux

Expected Response: LUN has been created successfully.

**Resize a LUN**

- On the umeng-aff300-05-06 cluster, update lun lundoc size to 50mb in volume doc on the marketing svm
Comment thread
Hardikl marked this conversation as resolved.

Expected Response: LUN has been updated successfully.

**Rename a LUN**

- On the umeng-aff300-05-06 cluster, rename the lun lundoc in volume doc on the marketing svm to lundocnew

Expected Response: LUN has been updated successfully.

**State change of a LUN**

- On the umeng-aff300-05-06 cluster, disable the lun lundocnew in volume doc on the marketing svm

Expected Response: LUN has been updated successfully.

**Delete a LUN**

- On the umeng-aff300-05-06 cluster, delete lun lundocnew in volume doc in marketing svm

Expected Response: LUN has been deleted successfully.

---

### Querying Specific Fields

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

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

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

func TestLUN(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: "Clean LUN",
input: ClusterStr + "delete lun " + rn("lundoc") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Clean LUN",
input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
Comment thread
Hardikl marked this conversation as resolved.
{
name: "Create volume",
input: ClusterStr + "create a 100MB volume named " + rn("doc") + " on the " + rn("marketing") + " svm and the harvest_vc_aggr aggregate",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Create LUN",
input: ClusterStr + "create a 20MB lun named " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm with os type linux",
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundoc") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Update lun size",
input: ClusterStr + "update lun " + rn("lundoc") + " size to 50mb in volume " + rn("doc") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{},
},
{
name: "Rename lun",
input: ClusterStr + "rename the lun " + rn("lundoc") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm to " + rn("lundocnew"),
expectedOntapErr: "",
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: createObject},
},
{
name: "Update lun state",
input: ClusterStr + "disable the lun " + rn("lundocnew") + " in volume " + rn("doc") + " on the " + rn("marketing") + " svm",
expectedOntapErr: "",
verifyAPI: ontapVerifier{},
},
{
name: "Clean LUN",
input: ClusterStr + "delete lun " + rn("lundocnew") + " in volume " + rn("doc") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/luns?name=/vol/" + rn("doc") + "/" + rn("lundocnew") + "&svm.name=" + rn("marketing"), validationFunc: deleteObject},
},
{
name: "Clean volume",
input: ClusterStr + "delete volume " + rn("doc") + " in " + rn("marketing") + " svm",
expectedOntapErr: "because it does not exist",
verifyAPI: ontapVerifier{api: "api/storage/volumes?name=" + rn("doc") + "&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/svm/svms?name=" + rn("marketing"), 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)
}
})
}
}
12 changes: 12 additions & 0 deletions ontap/ontap.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ type SVM struct {
Name string `json:"name" jsonschema:"svm name"`
}

type LUNSpace struct {
Size int64 `json:"size,omitempty" jsonschema:"size of the LUN"`
}

type LUN struct {
SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"`
Name string `json:"name,omitempty" jsonschema:"LUN name"`
Space LUNSpace `json:"space,omitzero" jsonschema:"LUN space detail"`
OsType string `json:"os_type,omitempty" jsonschema:"os type of LUN"`
Enabled *bool `json:"enabled,omitempty" jsonschema:"LUN admin state"`
}

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

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

func (c *Client) CreateLUN(ctx context.Context, lun ontap.LUN) error {
var (
statusCode int
)
responseHeaders := http.Header{}

builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders).
BodyJSON(lun)

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

return c.checkStatus(statusCode)
}

func (c *Client) UpdateLUN(ctx context.Context, svmName, lunPath string, lun ontap.LUN) error {
var (
statusCode int
lunData ontap.GetData
)
responseHeaders := http.Header{}

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

builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders).
Params(params).
ToJSON(&lunData)

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

if lunData.NumRecords == 0 {
return fmt.Errorf("failed to get lun=%s on svm=%s because it does not exist", lunPath, svmName)
}
if lunData.NumRecords != 1 {
return fmt.Errorf("failed to get lun=%s on svm=%s because there are %d matching records", lunPath, svmName, lunData.NumRecords)
}

builder2 := c.baseRequestBuilder(`/api/storage/luns/`+lunData.Records[0].UUID, &statusCode, responseHeaders).
Patch().
BodyJSON(lun)

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

return c.checkStatus(statusCode)
}

func (c *Client) DeleteLUN(ctx context.Context, svmName, lunPath string, allowDeleteWhileMapped bool) error {
var (
statusCode int
lunData ontap.GetData
)
responseHeaders := http.Header{}

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

builder := c.baseRequestBuilder(`/api/storage/luns`, &statusCode, responseHeaders).
Params(params).
ToJSON(&lunData)

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

if lunData.NumRecords == 0 {
return fmt.Errorf("failed to get lun=%s on svm=%s because it does not exist", lunPath, svmName)
}
if lunData.NumRecords != 1 {
return fmt.Errorf("failed to get lun=%s on svm=%s because there are %d matching records", lunPath, svmName, lunData.NumRecords)
}

deleteParams := url.Values{}
deleteParams.Set("allow_delete_while_mapped", strconv.FormatBool(allowDeleteWhileMapped))
builder2 := c.baseRequestBuilder(`/api/storage/luns/`+lunData.Records[0].UUID, &statusCode, responseHeaders).
Params(deleteParams).
Delete()

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

return c.checkStatus(statusCode)
}
Loading
Loading