From d1361062e0f3d9fd118c8392fa8c6c4ec0dac1d3 Mon Sep 17 00:00:00 2001 From: rahulguptajss Date: Tue, 14 Apr 2026 11:52:11 +0530 Subject: [PATCH 1/5] feat: gcnv ontap mode support --- cmd/poller/poller.go | 7 ++ cmd/tools/grafana/dashboard_test.go | 17 +-- cmd/tools/rest/client.go | 53 +++++++-- docs/configure-harvest-basic.md | 1 + docs/gcnv-ontap-mode.md | 56 +++++++++ .../dashboards/cmode-details/volumeBySVM.json | 3 +- .../cmode-details/volumeDeepDive.json | 3 +- grafana/dashboards/cmode/lun.json | 94 ++++++++++----- grafana/dashboards/cmode/security.json | 3 +- grafana/dashboards/cmode/svm.json | 3 +- grafana/dashboards/cmode/volume.json | 112 ++++++++++++++---- mkdocs.yml | 1 + pkg/conf/conf.go | 4 + 13 files changed, 281 insertions(+), 76 deletions(-) create mode 100644 docs/gcnv-ontap-mode.md diff --git a/cmd/poller/poller.go b/cmd/poller/poller.go index 818f2c41e..04011c35a 100644 --- a/cmd/poller/poller.go +++ b/cmd/poller/poller.go @@ -717,6 +717,13 @@ func (p *Poller) ping() (float32, bool) { // If the host includes a port, use that port, otherwise use portsToTry target := p.target + // For GCNV ontap mode, addr contains the full resource path (host/path/...). + // Extract just the hostname for TCP ping. + if p.params.GCNVOntapMode { + if i := strings.IndexByte(target, '/'); i != -1 { + target = target[:i] + } + } portsToTry := []int{443} // Extract host and port. This also handles IPv6 diff --git a/cmd/tools/grafana/dashboard_test.go b/cmd/tools/grafana/dashboard_test.go index 83a7174b4..a84f5d606 100644 --- a/cmd/tools/grafana/dashboard_test.go +++ b/cmd/tools/grafana/dashboard_test.go @@ -1889,14 +1889,15 @@ func TestTags(t *testing.T) { func checkTags(t *testing.T, path string, data []byte) { allowedTagsMap := map[string]bool{ - "asar2": true, - "cdot": true, - "cisco": true, - "eseries": true, - "fsx": true, - "harvest": true, - "ontap": true, - "storagegrid": true, + "asar2": true, + "cdot": true, + "cisco": true, + "eseries": true, + "fsx": true, + "gcnv-ontap-mode": true, + "harvest": true, + "ontap": true, + "storagegrid": true, } path = ShortPath(path) diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index 615257c2c..635b4d75a 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -29,16 +29,17 @@ const ( ) type Client struct { - client *http.Client - request *http.Request - buffer *bytes.Buffer - Logger *slog.Logger - baseURL string - remote conf.Remote - token string - logRest bool // used to log Rest request/response - auth *auth.Credentials - Metadata *collector.Metadata + client *http.Client + request *http.Request + buffer *bytes.Buffer + Logger *slog.Logger + baseURL string + remote conf.Remote + token string + logRest bool // used to log Rest request/response + isGCNVOntapMode bool + auth *auth.Credentials + Metadata *collector.Metadata } func New(poller *conf.Poller, timeout time.Duration, credentials *auth.Credentials) (*Client, error) { @@ -67,6 +68,7 @@ func New(poller *conf.Poller, timeout time.Duration, credentials *auth.Credentia url = "https://" + addr + "/" } client.baseURL = url + client.isGCNVOntapMode = poller.GCNVOntapMode transport, err = credentials.Transport(nil, poller) if err != nil { @@ -83,6 +85,21 @@ func New(poller *conf.Poller, timeout time.Duration, credentials *auth.Credentia return &client, nil } +func (c *Client) IsGCNVOntapMode() bool { + return c.isGCNVOntapMode +} + +// rewriteFieldsParam rewrites the "fields" query parameter to "ontap_fields" for GCNV pollers. +// Google's API framework reserves the "fields" keyword, so GCNV requires "ontap_fields" instead. +func (c *Client) rewriteFieldsParam(request string) string { + if !c.isGCNVOntapMode { + return request + } + request = strings.ReplaceAll(request, "?fields=", "?ontap_fields=") + request = strings.ReplaceAll(request, "&fields=", "&ontap_fields=") + return request +} + func (c *Client) SetTimeout(d time.Duration) { if c.client != nil { c.client.Timeout = d @@ -119,6 +136,7 @@ func (c *Client) GetPlainRest(request string, encodeURL bool, headers ...map[str } } + request = c.rewriteFieldsParam(request) u := c.baseURL + request c.request, err = requests.New("GET", u, nil) if err != nil { @@ -153,9 +171,24 @@ func (c *Client) GetPlainRest(request string, encodeURL bool, headers ...map[str c.Metadata.BytesRx += uint64(len(result)) c.Metadata.NumCalls++ + result = c.unwrapGCNVBody(result) + return result, err } +// unwrapGCNVBody extracts the "body" envelope from GCNV responses. +// GCNV wraps all REST responses inside {"body": {...}}, while regular ONTAP returns data at the top level. +func (c *Client) unwrapGCNVBody(data []byte) []byte { + if !c.isGCNVOntapMode || len(data) == 0 { + return data + } + body := gjson.GetBytes(data, "body") + if body.Exists() && body.Type == gjson.JSON { + return []byte(body.Raw) + } + return data +} + // GetRest makes a REST request to the cluster and returns a json response as a []byte func (c *Client) GetRest(request string, headers ...map[string]string) ([]byte, error) { return c.GetPlainRest(request, true, headers...) diff --git a/docs/configure-harvest-basic.md b/docs/configure-harvest-basic.md index 759a9340d..7a3f85f37 100644 --- a/docs/configure-harvest-basic.md +++ b/docs/configure-harvest-basic.md @@ -24,6 +24,7 @@ All pollers are defined in `harvest.yml`, the main configuration file of Harvest | `log_max_files` | | Number of rotated log files to keep | `5` | | `log` | optional, list of collector names | Matching collectors log their ZAPI request/response | | | `prefer_zapi` | optional, bool | Use the ZAPI API if the cluster supports it, otherwise allow Harvest to choose REST or ZAPI, whichever is appropriate to the ONTAP version. See [rest-strategy](https://github.com/NetApp/harvest/blob/main/docs/architecture/rest-strategy.md) for details. | | +| `gcnv_ontap_mode` | optional, bool | Set to `true` when the poller targets a [Google Cloud NetApp Volumes ONTAP mode](gcnv-ontap-mode.md) endpoint. | false | | `conf_path` | optional, `:` separated list of directories | The search path Harvest uses to load its [templates](configure-templates.md). Harvest walks each directory in order, stopping at the first one that contains the desired template. | conf | | `recorder` | optional, section | Section that determines if Harvest should record or replay HTTP requests. See [here](configure-harvest-basic.md#http-recorder) for details. | | | `pool` | optional, section | Section that determines if Harvest should limit the number of concurrent collectors. See [here](configure-harvest-basic.md#pool) for details. | | diff --git a/docs/gcnv-ontap-mode.md b/docs/gcnv-ontap-mode.md new file mode 100644 index 000000000..62b769c83 --- /dev/null +++ b/docs/gcnv-ontap-mode.md @@ -0,0 +1,56 @@ +Harvest supports monitoring [Google Cloud NetApp Volumes (GCNV) in ONTAP mode](https://docs.cloud.google.com/netapp/volumes/docs/ontap/overview). + +## Poller Configuration + +Add a poller to your `harvest.yml` with the full GCNV resource path as `addr` and set `gcnv_ontap_mode: true`. + +```yaml +Pollers: + my-gcnv-poller: + datacenter: gcp-us-central + addr: /v1beta1/projects//locations//storagePools//ontap + gcnv_ontap_mode: true + credentials_script: + path: /path/to/token-script.sh + schedule: 5m + timeout: 10s + collectors: + - Rest + - KeyPerf + - Ems + exporters: + - prometheus1 +``` + +`addr` must be the full GCNV resource path without a scheme — do not include `http://` or `https://`. + +GCNV uses Bearer token authentication. Use [`credentials_script`](configure-harvest-basic.md#credentials-script) to supply a token at runtime. + +For all poller parameters, see [Poller configuration](configure-harvest-basic.md#pollers). + +## Supported Harvest Metrics + +Capacity and configuration metrics are available via the `Rest` collector in GCNV ONTAP mode. +However, some metrics may not be available due to permission limitations imposed by the GCNV ONTAP mode environment. +Only limited performance metrics are supported because GCNV ONTAP mode does not support the `ZapiPerf` or `RestPerf` collectors. +Instead, use the [KeyPerf](configure-keyperf.md) collector to gather latency, IOPS, and throughput performance metrics for a limited set of objects. + +The following collectors are supported: + +- `Rest` — capacity, configuration, and inventory metrics +- [`KeyPerf`](configure-keyperf.md) — latency, IOPS, and throughput performance metrics +- `Ems` — events and alerts + +Performance metrics with the API name `KeyPerf` in the [ONTAP metrics documentation](ontap-metrics.md) are supported in GCNV ONTAP mode systems. +As a result, some panels in the dashboards may be missing information. + +## Supported Harvest Dashboards + +The dashboards that work with GCNV ONTAP mode are tagged with `gcnv-ontap-mode` and listed below: + +* ONTAP: LUN +* ONTAP: Security +* ONTAP: SVM +* ONTAP: Volume +* ONTAP: Volume by SVM +* ONTAP: Volume Deep Dive diff --git a/grafana/dashboards/cmode-details/volumeBySVM.json b/grafana/dashboards/cmode-details/volumeBySVM.json index 579a269c4..314699c4f 100644 --- a/grafana/dashboards/cmode-details/volumeBySVM.json +++ b/grafana/dashboards/cmode-details/volumeBySVM.json @@ -418,7 +418,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ diff --git a/grafana/dashboards/cmode-details/volumeDeepDive.json b/grafana/dashboards/cmode-details/volumeDeepDive.json index d44e0c434..576bd8b08 100644 --- a/grafana/dashboards/cmode-details/volumeDeepDive.json +++ b/grafana/dashboards/cmode-details/volumeDeepDive.json @@ -2626,7 +2626,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ diff --git a/grafana/dashboards/cmode/lun.json b/grafana/dashboards/cmode/lun.json index 674f6f131..696281898 100644 --- a/grafana/dashboards/cmode/lun.json +++ b/grafana/dashboards/cmode/lun.json @@ -1826,7 +1826,7 @@ "targets": [ { "exemplar": false, - "expr": "lun_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"}", + "expr": "label_join(\n lun_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\",\n \"lun\"\n)", "format": "table", "instant": true, "interval": "", @@ -1836,7 +1836,7 @@ }, { "exemplar": false, - "expr": "lun_new_status{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"}", + "expr": "label_join(\n lun_new_status{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\",\n \"lun\"\n)", "format": "table", "hide": false, "instant": true, @@ -1847,7 +1847,7 @@ }, { "exemplar": false, - "expr": "lun_size{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"}", + "expr": "label_join(\n lun_size{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\",\n \"lun\"\n)", "format": "table", "hide": false, "instant": true, @@ -1858,7 +1858,7 @@ }, { "exemplar": false, - "expr": "lun_size_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"}", + "expr": "label_join(\n lun_size_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\",\n \"lun\"\n)", "format": "table", "hide": false, "instant": true, @@ -1869,7 +1869,7 @@ }, { "exemplar": false, - "expr": "lun_block_size{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"}", + "expr": "label_join(\n lun_block_size{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",lun=~\"$LUN\",svm=~\"$SVM\",volume=~\"$Volume|\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\",\n \"lun\"\n)", "format": "table", "hide": false, "instant": true, @@ -1881,28 +1881,39 @@ ], "title": "LUNS in Cluster", "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "unique_id" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "(.*) 1$", + "renamePattern": "$1" + } + }, { "id": "filterFieldsByName", "options": { "include": { "names": [ - "node", - "serial_hex", + "datacenter", + "cluster", "svm", + "volume", "lun", + "node", + "serial_hex", "Value #A", "Value #C", "Value #D", - "Value #E", - "volume" + "Value #E" ] } } }, - { - "id": "merge", - "options": {} - }, { "id": "organize", "options": { @@ -1910,26 +1921,48 @@ "Time": true, "Value": true, "__name__": true, - "cluster": true, - "datacenter": true, + "cluster": false, + "cluster 2": true, + "cluster 3": true, + "cluster 4": true, + "cluster 5": true, + "datacenter": false, + "datacenter 2": true, + "datacenter 3": true, + "datacenter 4": true, + "datacenter 5": true, "instance": true, "job": true, - "state": true + "lun 2": true, + "lun 3": true, + "lun 4": true, + "lun 5": true, + "node 2": true, + "node 3": true, + "node 4": true, + "node 5": true, + "state": true, + "svm 2": true, + "svm 3": true, + "svm 4": true, + "svm 5": true, + "volume 2": true, + "volume 3": true, + "volume 4": true, + "volume 5": true }, "indexByName": { - "Time": 0, - "Value": 12, - "__name__": 1, - "aggr": 4, - "cluster": 5, - "datacenter": 6, - "instance": 7, - "job": 8, - "node": 2, - "state": 9, - "style": 11, - "svm": 3, - "volume": 10 + "Value #A": 7, + "Value #C": 8, + "Value #D": 9, + "Value #E": 10, + "cluster": 1, + "datacenter": 0, + "lun": 4, + "node": 5, + "serial_hex": 6, + "svm": 2, + "volume": 3 }, "renameByName": { "serial_hex": "Serial Hex" @@ -4151,7 +4184,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ diff --git a/grafana/dashboards/cmode/security.json b/grafana/dashboards/cmode/security.json index 12560e7b7..5b3ab1307 100644 --- a/grafana/dashboards/cmode/security.json +++ b/grafana/dashboards/cmode/security.json @@ -5354,7 +5354,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ diff --git a/grafana/dashboards/cmode/svm.json b/grafana/dashboards/cmode/svm.json index c3cd9cdc4..6052a7820 100644 --- a/grafana/dashboards/cmode/svm.json +++ b/grafana/dashboards/cmode/svm.json @@ -15618,7 +15618,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ diff --git a/grafana/dashboards/cmode/volume.json b/grafana/dashboards/cmode/volume.json index d5be1efbd..31b55bfa7 100644 --- a/grafana/dashboards/cmode/volume.json +++ b/grafana/dashboards/cmode/volume.json @@ -71,7 +71,7 @@ "gnetId": null, "graphTooltip": 1, "id": null, - "iteration": 1757501688710, + "iteration": 1775639972287, "links": [ { "asDropdown": true, @@ -927,7 +927,7 @@ "pluginVersion": "8.1.8", "targets": [ { - "expr": "volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "instant": true, "interval": "", @@ -936,7 +936,7 @@ "refId": "B" }, { - "expr": "volume_new_status{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_new_status{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -946,7 +946,7 @@ "refId": "A" }, { - "expr": "volume_size_total{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_size_total{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -956,7 +956,7 @@ "refId": "C" }, { - "expr": "volume_size_used_percent{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_size_used_percent{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -967,7 +967,7 @@ }, { "exemplar": false, - "expr": "volume_sis_dedup_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_sis_dedup_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -978,7 +978,7 @@ }, { "exemplar": false, - "expr": "volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -989,7 +989,7 @@ }, { "exemplar": false, - "expr": "volume_sis_dedup_saved\n+\n volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_sis_dedup_saved\n +\n volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -1000,7 +1000,7 @@ }, { "exemplar": false, - "expr": "volume_space_logical_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_space_logical_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -1011,7 +1011,7 @@ }, { "exemplar": false, - "expr": "volume_space_physical_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_space_physical_used{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -1022,7 +1022,7 @@ }, { "exemplar": false, - "expr": "volume_clone_split_estimate{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n* on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"}", + "expr": "label_join(\n volume_clone_split_estimate{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -1035,6 +1035,19 @@ "timeShift": null, "title": "Volumes in Cluster", "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "unique_id" + } + }, + { + "id": "renameByRegex", + "options": { + "regex": "(.*) 1$", + "renamePattern": "$1" + } + }, { "id": "filterFieldsByName", "options": { @@ -1064,10 +1077,6 @@ } } }, - { - "id": "merge", - "options": {} - }, { "id": "organize", "options": { @@ -1076,11 +1085,65 @@ "Value": true, "Value #C": false, "__name__": true, + "aggr 10": true, + "aggr 2": true, + "aggr 3": true, + "aggr 4": true, + "aggr 5": true, + "aggr 6": true, + "aggr 7": true, + "aggr 8": true, + "aggr 9": true, "cluster": false, + "cluster 10": true, + "cluster 2": true, + "cluster 3": true, + "cluster 4": true, + "cluster 5": true, + "cluster 6": true, + "cluster 7": true, + "cluster 8": true, + "cluster 9": true, "datacenter": false, + "datacenter 10": true, + "datacenter 2": true, + "datacenter 3": true, + "datacenter 4": true, + "datacenter 5": true, + "datacenter 6": true, + "datacenter 7": true, + "datacenter 8": true, + "datacenter 9": true, "instance": true, "job": true, - "state": true + "node 10": true, + "node 2": true, + "node 3": true, + "node 4": true, + "node 5": true, + "node 6": true, + "node 7": true, + "node 8": true, + "node 9": true, + "state": true, + "svm 10": true, + "svm 2": true, + "svm 3": true, + "svm 4": true, + "svm 5": true, + "svm 6": true, + "svm 7": true, + "svm 8": true, + "svm 9": true, + "volume 10": true, + "volume 2": true, + "volume 3": true, + "volume 4": true, + "volume 5": true, + "volume 6": true, + "volume 7": true, + "volume 8": true, + "volume 9": true }, "indexByName": { "Value #A": 6, @@ -8669,7 +8732,7 @@ "h": 1, "w": 24, "x": 0, - "y": 27 + "y": 28 }, "id": 92, "panels": [ @@ -9163,7 +9226,7 @@ "h": 1, "w": 24, "x": 0, - "y": 28 + "y": 29 }, "id": 99, "panels": [ @@ -10390,7 +10453,7 @@ "h": 1, "w": 24, "x": 0, - "y": 29 + "y": 30 }, "id": 98, "panels": [ @@ -10670,7 +10733,7 @@ "h": 1, "w": 24, "x": 0, - "y": 30 + "y": 31 }, "id": 105, "panels": [ @@ -10979,7 +11042,7 @@ "h": 1, "w": 24, "x": 0, - "y": 31 + "y": 32 }, "id": 135, "panels": [ @@ -11778,7 +11841,7 @@ "h": 1, "w": 24, "x": 0, - "y": 32 + "y": 33 }, "id": 136, "panels": [ @@ -12168,7 +12231,8 @@ "harvest", "ontap", "cdot", - "fsx" + "fsx", + "gcnv-ontap-mode" ], "templating": { "list": [ @@ -12462,5 +12526,5 @@ "timezone": "", "title": "ONTAP: Volume", "uid": "cdot-volume", - "version": 37 + "version": 38 } diff --git a/mkdocs.yml b/mkdocs.yml index 817ee11f6..3c5f0088f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,6 +22,7 @@ nav: - Prepare Monitored Systems: - 'ONTAP cDOT': 'prepare-cdot-clusters.md' - 'ASA r2': 'asar2.md' + - "GCNV ONTAP Mode": "gcnv-ontap-mode.md" - 'Amazon FSx for ONTAP': 'prepare-fsx-clusters.md' - 'ONTAP 7mode': 'prepare-7mode-clusters.md' - 'StorageGRID': 'prepare-storagegrid-clusters.md' diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index ea44375eb..2b8fe1ba9 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -638,6 +638,7 @@ type Poller struct { IsDisabled bool `yaml:"disabled,omitempty"` ExporterDefs []ExporterDef `yaml:"exporters,omitempty"` Exporters []string `yaml:"-"` + GCNVOntapMode bool `yaml:"gcnv_ontap_mode,omitempty"` IsKfs bool `yaml:"is_kfs,omitempty"` Labels *[]map[string]string `yaml:"labels,omitempty"` LogMaxBytes int64 `yaml:"log_max_bytes,omitempty"` @@ -669,6 +670,7 @@ func (p *Poller) Union(defaults *Poller) { isInsecureNil := true + pGCNVOntapMode := p.GCNVOntapMode pIsKfs := p.IsKfs pIsDisabled := p.IsDisabled @@ -690,6 +692,7 @@ func (p *Poller) Union(defaults *Poller) { p.UseInsecureTLS = &pUseInsecureTLS } + p.GCNVOntapMode = pGCNVOntapMode p.IsKfs = pIsKfs p.IsDisabled = pIsDisabled p.Password = pPassword @@ -724,6 +727,7 @@ func ZapiPoller(n *node.Node) *Poller { if addr := n.GetChildContentS("addr"); addr != "" { p.Addr = addr } + p.GCNVOntapMode = n.GetChildContentS("gcnv_ontap_mode") == "true" isKfs := n.GetChildContentS("is_kfs") p.IsKfs = isKfs == "true" From ba2ee851a8dba71884757d8e9c6824e19dfcfb64 Mon Sep 17 00:00:00 2001 From: rahulguptajss Date: Tue, 14 Apr 2026 15:59:26 +0530 Subject: [PATCH 2/5] feat: gcnv ontap mode support --- grafana/dashboards/cmode/volume.json | 4 ++-- harvest.cue | 1 + mkdocs.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/grafana/dashboards/cmode/volume.json b/grafana/dashboards/cmode/volume.json index 31b55bfa7..eec530f49 100644 --- a/grafana/dashboards/cmode/volume.json +++ b/grafana/dashboards/cmode/volume.json @@ -989,7 +989,7 @@ }, { "exemplar": false, - "expr": "label_join(\n volume_sis_dedup_saved\n +\n volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", + "expr": "label_join(\n volume_sis_dedup_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n +\n volume_sis_compress_saved{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",style!=\"flexgroup_constituent\",svm=~\"$SVM\",volume=~\"$Volume\"}\n * on (datacenter, cluster, svm, volume) group_left (node)\n volume_labels{cluster=~\"$Cluster\",datacenter=~\"$Datacenter\",junction_path=~\"$Junction\",svm=~\"$SVM\",tags=~\".*$Tag.*\",volume=~\"$Volume\"},\n \"unique_id\",\n \"-\",\n \"datacenter\",\n \"cluster\",\n \"svm\",\n \"volume\"\n)", "format": "table", "hide": false, "instant": true, @@ -12527,4 +12527,4 @@ "title": "ONTAP: Volume", "uid": "cdot-volume", "version": 38 -} +} \ No newline at end of file diff --git a/harvest.cue b/harvest.cue index a8d467007..00554aa37 100644 --- a/harvest.cue +++ b/harvest.cue @@ -92,6 +92,7 @@ Pollers: [Name=_]: #Poller datacenter?: string disabled?: bool exporters: [...#ExporterDefs] + gcnv_ontap_mode?: bool is_kfs?: bool labels?: [...label] log: [...string] diff --git a/mkdocs.yml b/mkdocs.yml index 3c5f0088f..c9a0ee5cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,7 +22,7 @@ nav: - Prepare Monitored Systems: - 'ONTAP cDOT': 'prepare-cdot-clusters.md' - 'ASA r2': 'asar2.md' - - "GCNV ONTAP Mode": "gcnv-ontap-mode.md" + - "GCNV ONTAP Mode": 'gcnv-ontap-mode.md' - 'Amazon FSx for ONTAP': 'prepare-fsx-clusters.md' - 'ONTAP 7mode': 'prepare-7mode-clusters.md' - 'StorageGRID': 'prepare-storagegrid-clusters.md' From f587c46ef0ba552d68c7f62177ab40731c8fd336 Mon Sep 17 00:00:00 2001 From: rahulguptajss Date: Tue, 14 Apr 2026 16:10:18 +0530 Subject: [PATCH 3/5] feat: gcnv ontap mode support --- grafana/dashboards/cmode/volume.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana/dashboards/cmode/volume.json b/grafana/dashboards/cmode/volume.json index eec530f49..c63f019e2 100644 --- a/grafana/dashboards/cmode/volume.json +++ b/grafana/dashboards/cmode/volume.json @@ -12527,4 +12527,4 @@ "title": "ONTAP: Volume", "uid": "cdot-volume", "version": 38 -} \ No newline at end of file +} From 71e17a4d7140fe0fe8f3798ca04226db268929b5 Mon Sep 17 00:00:00 2001 From: rahulguptajss Date: Tue, 14 Apr 2026 17:36:55 +0530 Subject: [PATCH 4/5] feat: gcnv ontap mode support --- cmd/tools/rest/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index 635b4d75a..d3d617ac2 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -183,7 +183,7 @@ func (c *Client) unwrapGCNVBody(data []byte) []byte { return data } body := gjson.GetBytes(data, "body") - if body.Exists() && body.Type == gjson.JSON { + if body.IsObject() { return []byte(body.Raw) } return data From 9cf8c929546539cba94aa24f97e818f538110504 Mon Sep 17 00:00:00 2001 From: rahulguptajss Date: Tue, 14 Apr 2026 19:31:41 +0530 Subject: [PATCH 5/5] feat: gcnv ontap mode support --- cmd/tools/rest/client.go | 4 ---- docs/gcnv-ontap-mode.md | 10 +++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index d3d617ac2..5dafcc7d1 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -85,10 +85,6 @@ func New(poller *conf.Poller, timeout time.Duration, credentials *auth.Credentia return &client, nil } -func (c *Client) IsGCNVOntapMode() bool { - return c.isGCNVOntapMode -} - // rewriteFieldsParam rewrites the "fields" query parameter to "ontap_fields" for GCNV pollers. // Google's API framework reserves the "fields" keyword, so GCNV requires "ontap_fields" instead. func (c *Client) rewriteFieldsParam(request string) string { diff --git a/docs/gcnv-ontap-mode.md b/docs/gcnv-ontap-mode.md index 62b769c83..7a105ce1d 100644 --- a/docs/gcnv-ontap-mode.md +++ b/docs/gcnv-ontap-mode.md @@ -24,7 +24,15 @@ Pollers: `addr` must be the full GCNV resource path without a scheme — do not include `http://` or `https://`. -GCNV uses Bearer token authentication. Use [`credentials_script`](configure-harvest-basic.md#credentials-script) to supply a token at runtime. +GCNV uses Bearer token authentication. Use [`credentials_script`](configure-harvest-basic.md#credentials-script) to supply a token at runtime. The script must print `authToken: ` to stdout. + +Below is an example script using a GCP service account key file: + +```bash +#!/bin/bash +gcloud auth activate-service-account --key-file=/path/to/service-account.json 2>/dev/null +echo "authToken: $(gcloud auth print-access-token)" +``` For all poller parameters, see [Poller configuration](configure-harvest-basic.md#pollers).