diff --git a/descriptions/descriptions.go b/descriptions/descriptions.go index a26cc49..2996119 100644 --- a/descriptions/descriptions.go +++ b/descriptions/descriptions.go @@ -84,6 +84,20 @@ const CreateNetworkIPInterface = `Create Network IP interface on a cluster by cl const UpdateNetworkIPInterface = `Update Network IP interface on a cluster by cluster name.` const DeleteNetworkIPInterface = `Delete Network IP interface on a cluster by cluster name.` +const CreateNVMeSubsystem = `Create NVMe subsystem on a cluster by cluster name.` +const UpdateNVMeSubsystem = `Update NVMe subsystem on a cluster by cluster name.` +const DeleteNVMeSubsystem = `Delete NVMe subsystem on a cluster by cluster name.` + +const AddNVMeSubsystemHost = `Add a host NQN to an NVMe subsystem on a cluster by cluster name.` +const RemoveNVMeSubsystemHost = `Remove a host NQN from an NVMe subsystem on a cluster by cluster name.` + +const CreateNVMeNamespace = `Create NVMe namespace on a cluster by cluster name.` +const UpdateNVMeNamespace = `Update NVMe namespace on a cluster by cluster name.` +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 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.*"). diff --git a/docs/examples.md b/docs/examples.md index 66be414..a45fa42 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -251,7 +251,7 @@ Expected Response: The qtree has been successfully renamed. --- -### Manage NVMe Service +### Manage NVMe - On the umeng-aff300-05-06 cluster, create nvme service on the marketing svm @@ -261,6 +261,26 @@ Expected Response: The nvme service has been successfully created. Expected Response: The nvme service has been successfully updated. +- On the umeng-aff300-05-06 cluster, create nvme subsystem sys1 with linux os on the marketing svm + +Expected Response: The nvme subsystem has been successfully created. + +- On the umeng-aff300-05-06 cluster, add host nqn as nqn.1992-01.example.com:host1 in sys1 nvme subsystem in marketing svm + +Expected Response: The nvme subsystem Host has been successfully added. + +- On the umeng-aff300-05-06 cluster, delete nvme subsystem sys1 with in marketing svm + +Expected Response: The nvme subsystem has been successfully deleted. + +- On the umeng-aff300-05-06 cluster, create nvme namespace /vol/docns/ns1 with linux os and 20mb size in nvmevs1 svm + +Expected Response: The nvme namespace has been successfully created. + +- On the umeng-aff300-05-06 cluster, create subsystem map of sys1 subsystem and /vol/docns/ns1 namespace in nvmevs1 svm + +Expected Response: The nvme subsystem map has been successfully created. + --- ### Manage iSCSI Service diff --git a/integration/test/nvme_service_test.go b/integration/test/nvme_service_test.go deleted file mode 100644 index 59f5747..0000000 --- a/integration/test/nvme_service_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "log/slog" - "net/http" - "testing" - "time" - - "github.com/netapp/ontap-mcp/config" -) - -const SarCluster = "sar" -const SarClusterStr = "On the " + SarCluster + " cluster, " - -func TestNVMeService(t *testing.T) { - SkipIfMissing(t, CheckTools) - - tests := []struct { - name string - input string - expectedOntapErr string - verifyAPI ontapVerifier - }{ - { - name: "Clean NVMe service", - input: SarClusterStr + "delete nvme service in marketing svm", - expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/protocols/nvme/services?svm.name=marketing", validationFunc: deleteObject}, - }, - { - name: "Create NVMe service", - input: SarClusterStr + "create nvme service on the marketing svm", - expectedOntapErr: "", - verifyAPI: ontapVerifier{api: "api/protocols/nvme/services?svm.name=marketing", validationFunc: createObject}, - }, - { - name: "Update NVMe service", - input: SarClusterStr + "update nvme service to disable on the marketing svm", - expectedOntapErr: "", - verifyAPI: ontapVerifier{}, - }, - { - name: "Clean NVMe service", - input: SarClusterStr + "delete nvme service in marketing svm", - expectedOntapErr: "because it does not exist", - verifyAPI: ontapVerifier{api: "api/protocols/nvme/services?svm.name=marketing", validationFunc: deleteObject}, - }, - } - - cfg, err := config.ReadConfig(ConfigFile) - if err != nil { - t.Fatalf("Error parsing the config: %v", err) - } - - poller := cfg.Pollers[SarCluster] - if poller == nil { - t.Skipf("Cluster %q not found in %s, skipping NVMe tests", SarCluster, ConfigFile) - } - 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 { - slog.Error("Error processing input", slog.Any("error", err)) - } - if tt.verifyAPI.api != "" && !tt.verifyAPI.validationFunc(t, tt.verifyAPI.api, poller, client) { - t.Errorf("Error while accessing the object via prompt %s", tt.input) - } - }) - } -} diff --git a/integration/test/nvme_test.go b/integration/test/nvme_test.go new file mode 100644 index 0000000..67c58c4 --- /dev/null +++ b/integration/test/nvme_test.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "crypto/tls" + "github.com/carlmjohnson/requests" + "github.com/netapp/ontap-mcp/ontap" + "log/slog" + "net/http" + "testing" + "time" + + "github.com/netapp/ontap-mcp/config" +) + +const NvmeCluster = "aff" +const NvmeClusterStr = "On the " + NvmeCluster + " cluster, " + +func TestNVMe(t *testing.T) { + SkipIfMissing(t, CheckTools) + + tests := []struct { + name string + input string + expectedOntapErr string + verifyAPI ontapVerifier + }{ + { + name: "Clean NVMe subsystem", + input: NvmeClusterStr + "delete nvme subsystem " + rn("sys2") + " with in marketing svm with allow_delete_while_mapped and allow_delete_with_hosts", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=marketing&name=" + rn("sys2"), validationFunc: deleteObject}, + }, + { + name: "Create NVMe subsystem", + input: NvmeClusterStr + "create nvme subsystem " + rn("sys2") + " with linux os and with host nqns as nqn.1992-01.example.com:host1, nqn.1992-01.example.com:host2 on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=marketing&name=" + rn("sys2"), validationFunc: createObject}, + }, + { + name: "Update NVMe subsystem", + input: NvmeClusterStr + "add comment as `comment about the` in " + rn("sys2") + " nvme subsystem on the marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Add host in NVMe subsystem", + input: NvmeClusterStr + "add host nqn as nqn.1992-01.example.com:host3 in " + rn("sys2") + " nvme subsystem in marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Remove host in NVMe subsystem", + input: NvmeClusterStr + "remove host nqn as nqn.1992-01.example.com:host3 in " + rn("sys2") + " nvme subsystem in marketing svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Clean NVMe subsystem", + input: NvmeClusterStr + "delete nvme subsystem " + rn("sys2") + " with in marketing svm with allow_delete_while_mapped and allow_delete_with_hosts", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=marketing&name=" + rn("sys2"), validationFunc: deleteObject}, + }, + { + name: "Clean NVMe subsystem", + input: NvmeClusterStr + "delete nvme subsystem " + rn("sys1") + " with in nvmevs1 svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=nvmevs1&name=" + rn("sys1"), validationFunc: deleteObject}, + }, + { + name: "Create NVMe subsystem", + input: NvmeClusterStr + "create nvme subsystem " + rn("sys1") + " with linux os on the nvmevs1 svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=nvmevs1&name=" + rn("sys1"), validationFunc: createObject}, + }, + { + name: "Clean NVMe namespace", + input: NvmeClusterStr + "delete nvme namespace '" + rn("/vol/docns/ns1") + "' with in nvmevs1 svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/namespaces?svm.name=nvmevs1&name=" + rn(`/vol/docns/ns1`), validationFunc: deleteObject}, + }, + { + name: "Create NVMe namespace", + input: NvmeClusterStr + "create nvme namespace '" + rn("/vol/docns/ns1") + "' with linux os and 20mb size in nvmevs1 svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/storage/namespaces?svm.name=nvmevs1&name=" + rn(`/vol/docns/ns1`), validationFunc: createObject}, + }, + { + name: "Update NVMe namespace", + input: NvmeClusterStr + "update nvme namespace '" + rn("/vol/docns/ns1") + "' with to 40mb size in nvmevs1 svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{}, + }, + { + name: "Create NVMe subsystem map", + input: NvmeClusterStr + "create subsystem map of " + rn("sys1") + " subsystem and '" + rn("/vol/docns/ns1") + "' namespace in nvmevs1 svm", + expectedOntapErr: "", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystem-maps?svm.name=nvmevs1", validationFunc: verifySubsystemMaps(rn("sys1"), rn(`/vol/docns/ns1`), true)}, + }, + { + name: "Clean NVMe subsystem map", + input: NvmeClusterStr + "delete subsystem map of " + rn("sys1") + " subsystem and namespace '" + rn("/vol/docns/ns1") + "' in nvmevs1 svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystem-maps?svm.name=nvmevs1", validationFunc: verifySubsystemMaps(rn("sys1"), rn(`/vol/docns/ns1`), false)}, + }, + { + name: "Clean NVMe namespace", + input: NvmeClusterStr + "delete nvme namespace '" + rn("/vol/docns/ns1") + "' with in nvmevs1 svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/storage/namespaces?svm.name=nvmevs1&name=" + rn(`/vol/docns/ns1`), validationFunc: deleteObject}, + }, + { + name: "Clean NVMe subsystem", + input: NvmeClusterStr + "delete nvme subsystem " + rn("sys1") + " with in nvmevs1 svm", + expectedOntapErr: "because it does not exist", + verifyAPI: ontapVerifier{api: "api/protocols/nvme/subsystems?svm.name=nvmevs1&name=" + rn("sys1"), validationFunc: deleteObject}, + }, + } + + cfg, err := config.ReadConfig(ConfigFile) + if err != nil { + t.Fatalf("Error parsing the config: %v", err) + } + + poller := cfg.Pollers[NvmeCluster] + if poller == nil { + t.Skipf("Cluster %q not found in %s, skipping NVMe tests", NvmeCluster, ConfigFile) + } + 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 %s", tt.input) + } + }) + } +} + +func verifySubsystemMaps(subsystemName, namespaceName string, exist bool) func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool { //nolint:unparam + return func(t *testing.T, api string, poller *config.Poller, client *http.Client) bool { + type subsystemMapRecord struct { + Namespace ontap.NameAndUUID `json:"namespace"` + Subsystem ontap.NameAndUUID `json:"subsystem"` + } + type response struct { + NumRecords int `json:"num_records"` + Records []subsystemMapRecord `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("verifySubsystemMaps: request failed: %v", err) + return false + } + + if exist { + for _, record := range data.Records { + gotSubsystem := record.Subsystem.Name + gotNamespace := record.Namespace.Name + if gotSubsystem == subsystemName && gotNamespace == namespaceName { + return true + } + } + t.Errorf("subsystem map does not exist") + } else { + sbsMapRecord := false + for _, record := range data.Records { + gotSubsystem := record.Subsystem.Name + gotNamespace := record.Namespace.Name + if gotSubsystem == subsystemName && gotNamespace == namespaceName { + sbsMapRecord = true + break + } + } + if !sbsMapRecord { + return true + } + t.Errorf("subsystem map exists") + } + return false + } +} diff --git a/ontap/ontap.go b/ontap/ontap.go index 41d7dc8..00fc01f 100644 --- a/ontap/ontap.go +++ b/ontap/ontap.go @@ -230,6 +230,43 @@ type NetworkIPInterface struct { ServicePolicy NameAndUUID `json:"service_policy,omitzero" jsonschema:"service policy"` } +type NVMeSubsystem struct { + SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"` + Name string `json:"name,omitzero" jsonschema:"name for NVMe subsystem"` + OSType string `json:"os_type,omitzero" jsonschema:"operating system of the NVMe subsystem's hosts"` + Hosts []NVMeHost `json:"hosts,omitzero" jsonschema:"NVMe hosts configured for access to the NVMe subsystem"` + Comment string `json:"comment,omitzero" jsonschema:"configurable comment for the NVMe subsystem"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows for the deletion of a mapped NVMe subsystem. This parameter should be used with caution."` + AllowDeleteWithHosts bool `json:"allow_delete_with_hosts,omitzero" jsonschema:"Allows for the deletion of an NVMe subsystem with NVMe hosts. This parameter should be used with caution."` +} + +type NVMeHost struct { + NQN string `json:"nqn,omitzero" jsonschema:"NVMe qualified name (NQN) used to identify the NVMe host"` +} + +type NVMeSubsystemHost struct { + NQN string `json:"nqn,omitzero" jsonschema:"NVMe qualified name (NQN) used to identify the NVMe host"` + Records []NVMeHost `json:"records,omitzero" jsonschema:"array of NVMe hosts specified to add multiple NVMe hosts to an NVMe subsystem"` +} + +type NVMeNamespace struct { + SVM NameAndUUID `json:"svm,omitzero" jsonschema:"svm name"` + Name string `json:"name,omitzero" jsonschema:"name for NVMe namespace"` + OSType string `json:"os_type,omitzero" jsonschema:"operating system type of the NVMe namespace"` + Space Space `json:"space,omitzero" jsonschema:"space of NVMe namespace"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped NVMe namespace. This parameter should be used with caution."` +} + +type Space struct { + Size string `json:"size,omitzero" jsonschema:"total provisioned size of the NVMe namespace (e.g., '100GB', '1TB')"` +} + +type NVMeSubsystemMap struct { + SVM NameAndUUID `json:"svm" jsonschema:"svm name"` + Subsystem NameAndUUID `json:"subsystem" jsonschema:"subsystem name"` + Namespace NameAndUUID `json:"namespace" jsonschema:"namespace name"` +} + const ( ASAr2 = "asar2" CDOT = "cdot" diff --git a/rest/nvme.go b/rest/nvme.go new file mode 100644 index 0000000..6631220 --- /dev/null +++ b/rest/nvme.go @@ -0,0 +1,463 @@ +package rest + +import ( + "context" + "fmt" + "github.com/netapp/ontap-mcp/ontap" + "net/http" + "net/url" + "strconv" +) + +func (c *Client) CreateNVMeService(ctx context.Context, nvmeService ontap.NVMeService) error { + var ( + statusCode int + ) + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). + BodyJSON(nvmeService) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) UpdateNVMeService(ctx context.Context, svmName string, nvmeService ontap.NVMeService) error { + var ( + statusCode int + nvmeSr ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSr) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSr.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme service in svm %s because it does not exist", svmName) + } + + builder = c.baseRequestBuilder(`/api/protocols/nvme/services/`+nvmeSr.Records[0].Svm.UUID, &statusCode, responseHeaders). + BodyJSON(nvmeService). + Patch() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteNVMeService(ctx context.Context, svmName string) error { + var ( + statusCode int + nvmeSr ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSr) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSr.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme service in svm %s because it does not exist", svmName) + } + + builder = c.baseRequestBuilder(`/api/protocols/nvme/services/`+nvmeSr.Records[0].Svm.UUID, &statusCode, responseHeaders). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) CreateNVMeSubsystem(ctx context.Context, nvmeSubsystem ontap.NVMeSubsystem) error { + var ( + statusCode int + ) + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + BodyJSON(nvmeSubsystem) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) UpdateNVMeSubsystem(ctx context.Context, svmName string, name string, nvmeSubsystem ontap.NVMeSubsystem) error { + var ( + statusCode int + nvmeSs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme subsystem of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeSs.NumRecords != 1 { + return fmt.Errorf("failed to update NVMe subsystem %s in svm=%s because there are %d matching records", + name, svmName, nvmeSs.NumRecords) + } + + builder = c.baseRequestBuilder(`/api/protocols/nvme/subsystems/`+nvmeSs.Records[0].UUID, &statusCode, responseHeaders). + BodyJSON(nvmeSubsystem). + Patch() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteNVMeSubsystem(ctx context.Context, svmName string, name string, allowDeleteWhileMapped bool, allowDeleteWithHosts bool) error { + var ( + statusCode int + nvmeSs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme subsystem of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeSs.NumRecords != 1 { + return fmt.Errorf("failed to delete NVMe subsystem %s in svm=%s because there are %d matching records", + name, svmName, nvmeSs.NumRecords) + } + + deleteParams := url.Values{} + deleteParams.Set("allow_delete_while_mapped", strconv.FormatBool(allowDeleteWhileMapped)) + deleteParams.Set("allow_delete_with_hosts", strconv.FormatBool(allowDeleteWithHosts)) + builder = c.baseRequestBuilder(`/api/protocols/nvme/subsystems/`+nvmeSs.Records[0].UUID, &statusCode, responseHeaders). + Params(deleteParams). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) AddNVMeSubsystemHost(ctx context.Context, svmName string, name string, nvmeSubsystemHost ontap.NVMeSubsystemHost) error { + var ( + statusCode int + nvmeSs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme subsystem of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeSs.NumRecords != 1 { + return fmt.Errorf("failed to get NVMe subsystem %s in svm=%s because there are %d matching records", + name, svmName, nvmeSs.NumRecords) + } + + builder2 := c.baseRequestBuilder(`/api/protocols/nvme/subsystems/`+nvmeSs.Records[0].UUID+`/hosts`, &statusCode, responseHeaders). + BodyJSON(nvmeSubsystemHost) + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) RemoveNVMeSubsystemHost(ctx context.Context, svmName string, name string, nqn string) error { + var ( + statusCode int + nvmeSs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeSs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme subsystem of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeSs.NumRecords != 1 { + return fmt.Errorf("failed to get NVMe subsystem %s in svm=%s because there are %d matching records", + name, svmName, nvmeSs.NumRecords) + } + + builder2 := c.baseRequestBuilder(`/api/protocols/nvme/subsystems/`+nvmeSs.Records[0].UUID+`/hosts/`+url.PathEscape(nqn), &statusCode, responseHeaders). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) CreateNVMeNamespace(ctx context.Context, nvmeNamespace ontap.NVMeNamespace) error { + var ( + statusCode int + ) + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/storage/namespaces`, &statusCode, responseHeaders). + BodyJSON(nvmeNamespace) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) UpdateNVMeNamespace(ctx context.Context, svmName string, name string, nvmeNamespace ontap.NVMeNamespace) error { + var ( + statusCode int + nvmeNs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/storage/namespaces`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeNs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeNs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme namespace of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeNs.NumRecords != 1 { + return fmt.Errorf("failed to update NVMe namespace %s in svm=%s because there are %d matching records", + name, svmName, nvmeNs.NumRecords) + } + + builder = c.baseRequestBuilder(`/api/storage/namespaces/`+nvmeNs.Records[0].UUID, &statusCode, responseHeaders). + BodyJSON(nvmeNamespace). + Patch() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteNVMeNamespace(ctx context.Context, svmName string, name string, allowDeleteWhileMapped bool) error { + var ( + statusCode int + nvmeNs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", name) + + builder := c.baseRequestBuilder(`/api/storage/namespaces`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeNs) + + err := c.buildAndExecuteRequest(ctx, builder) + + if err != nil { + return err + } + + if nvmeNs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme namespace of name %s in svm %s because it does not exist", name, svmName) + } + + if nvmeNs.NumRecords != 1 { + return fmt.Errorf("failed to delete NVMe namespace %s in svm=%s because there are %d matching records", + name, svmName, nvmeNs.NumRecords) + } + + deleteParams := url.Values{} + deleteParams.Set("allow_delete_while_mapped", strconv.FormatBool(allowDeleteWhileMapped)) + builder = c.baseRequestBuilder(`/api/storage/namespaces/`+nvmeNs.Records[0].UUID, &statusCode, responseHeaders). + Params(deleteParams). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) CreateNVMeSubsystemMap(ctx context.Context, nvmeSubsystemMap ontap.NVMeSubsystemMap) error { + var ( + statusCode int + ) + + responseHeaders := http.Header{} + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystem-maps`, &statusCode, responseHeaders). + BodyJSON(nvmeSubsystemMap) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + return c.checkStatus(statusCode) +} + +func (c *Client) DeleteNVMeSubsystemMap(ctx context.Context, svmName string, subsystemName string, namespaceName string) error { + var ( + statusCode int + nvmeSs ontap.GetData + nvmeNs ontap.GetData + ) + + responseHeaders := http.Header{} + + params := url.Values{} + params.Set("svm.name", svmName) + params.Set("name", subsystemName) + + builder := c.baseRequestBuilder(`/api/protocols/nvme/subsystems`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeSs) + + if err := c.buildAndExecuteRequest(ctx, builder); err != nil { + return err + } + + if nvmeSs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme subsystem of name %s in svm %s because it does not exist", subsystemName, svmName) + } + + if nvmeSs.NumRecords != 1 { + return fmt.Errorf("failed to get NVMe subsystem %s in svm=%s because there are %d matching records", + subsystemName, svmName, nvmeSs.NumRecords) + } + + responseHeaders = http.Header{} + params = url.Values{} + params.Set("svm.name", svmName) + params.Set("name", namespaceName) + + builder2 := c.baseRequestBuilder(`/api/storage/namespaces`, &statusCode, responseHeaders). + Params(params). + ToJSON(&nvmeNs) + + if err := c.buildAndExecuteRequest(ctx, builder2); err != nil { + return err + } + + if nvmeNs.NumRecords == 0 { + return fmt.Errorf("failed to get detail of nvme namespace of name %s in svm %s because it does not exist", namespaceName, svmName) + } + + if nvmeNs.NumRecords != 1 { + return fmt.Errorf("failed to get NVMe namespace %s in svm=%s because there are %d matching records", + namespaceName, svmName, nvmeNs.NumRecords) + } + + builder3 := c.baseRequestBuilder(`/api/protocols/nvme/subsystem-maps/`+nvmeSs.Records[0].UUID+`/`+nvmeNs.Records[0].UUID, &statusCode, responseHeaders). + Delete() + + if err := c.buildAndExecuteRequest(ctx, builder3); err != nil { + return err + } + + return c.checkStatus(statusCode) +} diff --git a/rest/nvmeService.go b/rest/nvmeService.go deleted file mode 100644 index 15f9e40..0000000 --- a/rest/nvmeService.go +++ /dev/null @@ -1,96 +0,0 @@ -package rest - -import ( - "context" - "fmt" - "github.com/netapp/ontap-mcp/ontap" - "net/http" - "net/url" -) - -func (c *Client) CreateNVMeService(ctx context.Context, nvmeService ontap.NVMeService) error { - var ( - statusCode int - ) - responseHeaders := http.Header{} - - builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). - BodyJSON(nvmeService) - - if err := c.buildAndExecuteRequest(ctx, builder); err != nil { - return err - } - - return c.checkStatus(statusCode) -} - -func (c *Client) UpdateNVMeService(ctx context.Context, svmName string, nvmeService ontap.NVMeService) error { - var ( - statusCode int - nvmeSr ontap.GetData - ) - - responseHeaders := http.Header{} - - params := url.Values{} - params.Set("svm.name", svmName) - - builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). - Params(params). - ToJSON(&nvmeSr) - - err := c.buildAndExecuteRequest(ctx, builder) - - if err != nil { - return err - } - - if nvmeSr.NumRecords == 0 { - return fmt.Errorf("failed to get detail of nvme service in svm %s because it does not exist", svmName) - } - - builder = c.baseRequestBuilder(`/api/protocols/nvme/services/`+nvmeSr.Records[0].Svm.UUID, &statusCode, responseHeaders). - BodyJSON(nvmeService). - Patch() - - if err := c.buildAndExecuteRequest(ctx, builder); err != nil { - return err - } - - return c.checkStatus(statusCode) -} - -func (c *Client) DeleteNVMeService(ctx context.Context, svmName string) error { - var ( - statusCode int - nvmeSr ontap.GetData - ) - - responseHeaders := http.Header{} - - params := url.Values{} - params.Set("svm.name", svmName) - - builder := c.baseRequestBuilder(`/api/protocols/nvme/services`, &statusCode, responseHeaders). - Params(params). - ToJSON(&nvmeSr) - - err := c.buildAndExecuteRequest(ctx, builder) - - if err != nil { - return err - } - - if nvmeSr.NumRecords == 0 { - return fmt.Errorf("failed to get detail of nvme service in svm %s because it does not exist", svmName) - } - - builder = c.baseRequestBuilder(`/api/protocols/nvme/services/`+nvmeSr.Records[0].Svm.UUID, &statusCode, responseHeaders). - Delete() - - if err := c.buildAndExecuteRequest(ctx, builder); err != nil { - return err - } - - return c.checkStatus(statusCode) -} diff --git a/server/nvme.go b/server/nvme.go new file mode 100644 index 0000000..c285014 --- /dev/null +++ b/server/nvme.go @@ -0,0 +1,603 @@ +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) CreateNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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) + + nvmeServiceCreate, err := newCreateNVMeService(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.CreateNVMeService(ctx, nvmeServiceCreate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Service created successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) UpdateNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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) + + nvmeServiceUpdate, err := newUpdateNVMeService(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.UpdateNVMeService(ctx, parameters.SVM, nvmeServiceUpdate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Service updated successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) DeleteNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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 err := newDeleteNVMeService(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.DeleteNVMeService(ctx, parameters.SVM) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Service deleted successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func newCreateNVMeService(in tool.NVMeService) (ontap.NVMeService, error) { + out := ontap.NVMeService{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + + out.SVM = ontap.NameAndUUID{Name: in.SVM} + out.Enabled = in.Enabled + return out, nil +} + +func newUpdateNVMeService(in tool.NVMeService) (ontap.NVMeService, error) { + out := ontap.NVMeService{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + out.Enabled = in.Enabled + return out, nil +} + +func newDeleteNVMeService(in tool.NVMeService) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + return nil +} + +func (a *App) CreateNVMeSubsystem(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystem) (*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) + + nvmeSubsystemCreate, err := newCreateNVMeSubsystem(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.CreateNVMeSubsystem(ctx, nvmeSubsystemCreate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem created successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) UpdateNVMeSubsystem(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystem) (*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) + + nvmeSubsystemUpdate, err := newUpdateNVMeSubsystem(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.UpdateNVMeSubsystem(ctx, parameters.SVM, parameters.Name, nvmeSubsystemUpdate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem updated successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) DeleteNVMeSubsystem(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystem) (*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 err := newDeleteNVMeSubsystem(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.DeleteNVMeSubsystem(ctx, parameters.SVM, parameters.Name, parameters.AllowDeleteWhileMapped, parameters.AllowDeleteWithHosts) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem deleted successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func newCreateNVMeSubsystem(in tool.NVMeSubsystem) (ontap.NVMeSubsystem, error) { + out := ontap.NVMeSubsystem{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Name == "" { + return out, errors.New("NVMe subsystem name is required") + } + if in.OSType == "" { + return out, errors.New("OS type is required") + } + + out.SVM = ontap.NameAndUUID{Name: in.SVM} + out.Name = in.Name + out.OSType = in.OSType + + for _, nqn := range in.HostNQNs { + out.Hosts = append(out.Hosts, ontap.NVMeHost{NQN: nqn}) + } + + if in.Comment != "" { + out.Comment = in.Comment + } + + return out, nil +} + +func newUpdateNVMeSubsystem(in tool.NVMeSubsystem) (ontap.NVMeSubsystem, error) { + out := ontap.NVMeSubsystem{} + + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Name == "" { + return out, errors.New("NVMe subsystem name is required") + } + + out.Comment = in.Comment + if out.Comment == "" { + return out, errors.New("no update fields provided; specify at least one of: comment") + } + return out, nil +} + +func newDeleteNVMeSubsystem(in tool.NVMeSubsystem) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.Name == "" { + return errors.New("NVMe subsystem name is required") + } + return nil +} + +func (a *App) AddNVMeSubsystemHost(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystemHost) (*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) + + nvmeSubsystemHostAdd, err := newAddNVMeSubsystemHost(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.AddNVMeSubsystemHost(ctx, parameters.SVM, parameters.Name, nvmeSubsystemHostAdd) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem Host added successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) RemoveNVMeSubsystemHost(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystemHost) (*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 err := newRemoveNVMeSubsystemHost(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.RemoveNVMeSubsystemHost(ctx, parameters.SVM, parameters.Name, parameters.NQN) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem Host removed successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func newAddNVMeSubsystemHost(in tool.NVMeSubsystemHost) (ontap.NVMeSubsystemHost, error) { + out := ontap.NVMeSubsystemHost{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Name == "" { + return out, errors.New("NVMe subsystem name is required") + } + + if in.NQN == "" && len(in.Records) == 0 { + return out, errors.New("either NVMe subsystem host NQN OR one or more host NQNs (records) must be provided") + } + + // Enforce mutual exclusivity: cannot specify both a single NQN and an array + if in.NQN != "" && len(in.Records) > 0 { + return out, errors.New("specify either a single NVMe subsystem host NQN or an array of NQNs, but not both") + } + + if in.NQN != "" { + out.NQN = in.NQN + return out, nil + } + + for _, nqn := range in.Records { + if nqn == "" { + return out, errors.New("all NQNs in the array must be non-empty") + } + out.Records = append(out.Records, ontap.NVMeHost{NQN: nqn}) + } + return out, nil +} + +func newRemoveNVMeSubsystemHost(in tool.NVMeSubsystemHost) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.Name == "" { + return errors.New("NVMe subsystem name is required") + } + if in.NQN == "" { + return errors.New("NQN is required") + } + return nil +} + +func (a *App) CreateNVMeNamespace(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeNamespace) (*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) + + nvmeNamespaceCreate, err := newCreateNVMeNamespace(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.CreateNVMeNamespace(ctx, nvmeNamespaceCreate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Namespace created successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) UpdateNVMeNamespace(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeNamespace) (*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) + + nvmeNamespaceUpdate, err := newUpdateNVMeNamespace(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.UpdateNVMeNamespace(ctx, parameters.SVM, parameters.Name, nvmeNamespaceUpdate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Namespace updated successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) DeleteNVMeNamespace(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeNamespace) (*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 err := newDeleteNVMeNamespace(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.DeleteNVMeNamespace(ctx, parameters.SVM, parameters.Name, parameters.AllowDeleteWhileMapped) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Namespace deleted successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func newCreateNVMeNamespace(in tool.NVMeNamespace) (ontap.NVMeNamespace, error) { + out := ontap.NVMeNamespace{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Name == "" { + return out, errors.New("NVMe namespace name is required") + } + if in.OSType == "" { + return out, errors.New("OS type is required") + } + if in.Size == "" { + return out, errors.New("size of namespace is required") + } + + out.SVM = ontap.NameAndUUID{Name: in.SVM} + out.Name = in.Name + out.OSType = in.OSType + out.Space.Size = in.Size + + return out, nil +} + +func newUpdateNVMeNamespace(in tool.NVMeNamespace) (ontap.NVMeNamespace, error) { + out := ontap.NVMeNamespace{} + + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Name == "" { + return out, errors.New("NVMe namespace name is required") + } + if in.Size == "" { + return out, errors.New("at least one supported update field must be provided; size is supported for update") + } + out.Space.Size = in.Size + + return out, nil +} + +func newDeleteNVMeNamespace(in tool.NVMeNamespace) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.Name == "" { + return errors.New("NVMe namespace name is required") + } + + return nil +} + +func (a *App) CreateNVMeSubsystemMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystemMap) (*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) + + nvmeSubsystemMapCreate, err := newCreateNVMeSubsystemMap(parameters) + if err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.CreateNVMeSubsystemMap(ctx, nvmeSubsystemMapCreate) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem Map created successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func (a *App) DeleteNVMeSubsystemMap(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeSubsystemMap) (*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 err := newDeleteNVMeSubsystemMap(parameters); err != nil { + return nil, nil, err + } + + client, err := a.getClient(parameters.Cluster) + if err != nil { + return errorResult(err), nil, err + } + err = client.DeleteNVMeSubsystemMap(ctx, parameters.SVM, parameters.Subsystem, parameters.Namespace) + + if err != nil { + return errorResult(err), nil, err + } + + responseText := "NVMe Subsystem Map deleted successfully" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: responseText}, + }, + }, nil, nil +} + +func newCreateNVMeSubsystemMap(in tool.NVMeSubsystemMap) (ontap.NVMeSubsystemMap, error) { + out := ontap.NVMeSubsystemMap{} + if in.SVM == "" { + return out, errors.New("SVM name is required") + } + if in.Subsystem == "" { + return out, errors.New("NVMe subsystem name is required") + } + if in.Namespace == "" { + return out, errors.New("NVMe namespace name is required") + } + + out.SVM.Name = in.SVM + out.Subsystem.Name = in.Subsystem + out.Namespace.Name = in.Namespace + return out, nil +} + +func newDeleteNVMeSubsystemMap(in tool.NVMeSubsystemMap) error { + if in.SVM == "" { + return errors.New("SVM name is required") + } + if in.Subsystem == "" { + return errors.New("NVMe subsystem name is required") + } + if in.Namespace == "" { + return errors.New("NVMe namespace name is required") + } + return nil +} diff --git a/server/nvmeService.go b/server/nvmeService.go deleted file mode 100644 index e23c3c9..0000000 --- a/server/nvmeService.go +++ /dev/null @@ -1,126 +0,0 @@ -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) CreateNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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) - - nvmeServiceCreate, err := newCreateNVMeService(parameters) - if err != nil { - return nil, nil, err - } - - client, err := a.getClient(parameters.Cluster) - if err != nil { - return errorResult(err), nil, err - } - err = client.CreateNVMeService(ctx, nvmeServiceCreate) - - if err != nil { - return errorResult(err), nil, err - } - - responseText := "NVMe Service created successfully" - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{Text: responseText}, - }, - }, nil, nil -} - -func (a *App) UpdateNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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) - - nvmeServiceUpdate, err := newUpdateNVMeService(parameters) - if err != nil { - return nil, nil, err - } - - client, err := a.getClient(parameters.Cluster) - if err != nil { - return errorResult(err), nil, err - } - err = client.UpdateNVMeService(ctx, parameters.SVM, nvmeServiceUpdate) - - if err != nil { - return errorResult(err), nil, err - } - - responseText := "NVMe Service updated successfully" - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{Text: responseText}, - }, - }, nil, nil -} - -func (a *App) DeleteNVMeService(ctx context.Context, _ *mcp.CallToolRequest, parameters tool.NVMeService) (*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 err := newDeleteNVMeService(parameters); err != nil { - return nil, nil, err - } - - client, err := a.getClient(parameters.Cluster) - if err != nil { - return errorResult(err), nil, err - } - err = client.DeleteNVMeService(ctx, parameters.SVM) - - if err != nil { - return errorResult(err), nil, err - } - - responseText := "NVMe Service deleted successfully" - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{Text: responseText}, - }, - }, nil, nil -} - -func newCreateNVMeService(in tool.NVMeService) (ontap.NVMeService, error) { - out := ontap.NVMeService{} - if in.SVM == "" { - return out, errors.New("SVM name is required") - } - - out.SVM = ontap.NameAndUUID{Name: in.SVM} - out.Enabled = in.Enabled - return out, nil -} - -func newUpdateNVMeService(in tool.NVMeService) (ontap.NVMeService, error) { - out := ontap.NVMeService{} - if in.SVM == "" { - return out, errors.New("SVM name is required") - } - out.Enabled = in.Enabled - return out, nil -} - -func newDeleteNVMeService(in tool.NVMeService) error { - if in.SVM == "" { - return errors.New("SVM name is required") - } - return nil -} diff --git a/server/server.go b/server/server.go index 1e5b4b0..0109839 100644 --- a/server/server.go +++ b/server/server.go @@ -147,6 +147,24 @@ func (a *App) createMCPServer() *mcp.Server { addTool(a, server, "update_network_ip_interface", descriptions.UpdateNetworkIPInterface, updateAnnotation, a.UpdateNetworkIPInterface) addTool(a, server, "delete_network_ip_interface", descriptions.DeleteNetworkIPInterface, deleteAnnotation, a.DeleteNetworkIPInterface) + // operation on NVMe subsystem object + addTool(a, server, "create_nvme_subsystem", descriptions.CreateNVMeSubsystem, createAnnotation, a.CreateNVMeSubsystem) + addTool(a, server, "update_nvme_subsystem", descriptions.UpdateNVMeSubsystem, updateAnnotation, a.UpdateNVMeSubsystem) + addTool(a, server, "delete_nvme_subsystem", descriptions.DeleteNVMeSubsystem, deleteAnnotation, a.DeleteNVMeSubsystem) + + // operation on NVMe subsystem host object + addTool(a, server, "add_nvme_subsystem_host", descriptions.AddNVMeSubsystemHost, createAnnotation, a.AddNVMeSubsystemHost) + addTool(a, server, "remove_nvme_subsystem_host", descriptions.RemoveNVMeSubsystemHost, deleteAnnotation, a.RemoveNVMeSubsystemHost) + + // operation on NVMe namespace object + addTool(a, server, "create_nvme_namespace", descriptions.CreateNVMeNamespace, createAnnotation, a.CreateNVMeNamespace) + addTool(a, server, "update_nvme_namespace", descriptions.UpdateNVMeNamespace, updateAnnotation, a.UpdateNVMeNamespace) + addTool(a, server, "delete_nvme_namespace", descriptions.DeleteNVMeNamespace, deleteAnnotation, a.DeleteNVMeNamespace) + + // operation on NVMe subsystem map object + addTool(a, server, "create_nvme_subsystem_map", descriptions.CreateNVMeSubsystemMap, createAnnotation, a.CreateNVMeSubsystemMap) + addTool(a, server, "delete_nvme_subsystem_map", descriptions.DeleteNVMeSubsystemMap, deleteAnnotation, a.DeleteNVMeSubsystemMap) + if a.catalog != nil { addTool(a, server, "list_ontap_endpoints", descriptions.ListOntapEndpoints, readOnlyAnnotation, a.ListOntapEndpoints) addTool(a, server, "search_ontap_endpoints", descriptions.SearchOntapEndpoints, readOnlyAnnotation, a.SearchOntapEndpoints) diff --git a/tool/tool.go b/tool/tool.go index a472fc5..9c85fc1 100644 --- a/tool/tool.go +++ b/tool/tool.go @@ -137,6 +137,41 @@ type NetworkIPInterface struct { ServicePolicy string `json:"service_policy,omitzero" jsonschema:"service policy"` } +type NVMeSubsystem struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Name string `json:"name" jsonschema:"name for NVMe subsystem"` + OSType string `json:"os_type,omitzero" jsonschema:"operating system of the NVMe subsystem's hosts (e.g., aix, linux, vmware, windows)"` + HostNQNs []string `json:"hosts_nqns,omitzero" jsonschema:"array of NVMe qualified name (NQN) used to identify the NVMe hosts"` + Comment string `json:"comment,omitzero" jsonschema:"configurable comment for the NVMe subsystem"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows for the deletion of a mapped NVMe subsystem. This parameter should be used with caution."` + AllowDeleteWithHosts bool `json:"allow_delete_with_hosts,omitzero" jsonschema:"Allows for the deletion of an NVMe subsystem with NVMe hosts. This parameter should be used with caution."` +} + +type NVMeSubsystemHost struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Name string `json:"name" jsonschema:"name for NVMe subsystem"` + NQN string `json:"nqn,omitzero" jsonschema:"NVMe qualified name (NQN) used to identify the NVMe host"` + Records []string `json:"records_nqns,omitzero" jsonschema:"array of NVMe hosts specified to add multiple NVMe hosts to an NVMe subsystem"` +} + +type NVMeNamespace struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Name string `json:"name" jsonschema:"name for NVMe namespace"` + OSType string `json:"os_type,omitzero" jsonschema:"operating system type of the NVMe namespace (e.g., aix, linux, vmware, windows)"` + Size string `json:"space.size,omitzero" jsonschema:"total provisioned size of the NVMe namespace (e.g., '100GB', '1TB')"` + AllowDeleteWhileMapped bool `json:"allow_delete_while_mapped,omitzero" jsonschema:"Allows deletion of a mapped NVMe namespace. This parameter should be used with caution."` +} + +type NVMeSubsystemMap struct { + Cluster string `json:"cluster_name" jsonschema:"cluster name"` + SVM string `json:"svm_name" jsonschema:"SVM name"` + Subsystem string `json:"subsystem_name" jsonschema:"name for NVMe subsystem"` + Namespace string `json:"namespace_name" jsonschema:"name for NVMe namespace"` +} + type OntapGetParams struct { Cluster string `json:"cluster_name" jsonschema:"cluster name, from list_registered_clusters"` Fields string `json:"fields,omitzero" jsonschema:"comma-separated dot-notation fields to return, e.g. \"name,svm.name,space.size\" — use space.* to expand all space sub-fields"`