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
7 changes: 7 additions & 0 deletions cmd/poller/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions cmd/tools/grafana/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 39 additions & 10 deletions cmd/tools/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -83,6 +85,17 @@ func New(poller *conf.Poller, timeout time.Duration, credentials *auth.Credentia
return &client, nil
}

// 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
}
Comment thread
rahulguptajss marked this conversation as resolved.
Comment thread
rahulguptajss marked this conversation as resolved.

func (c *Client) SetTimeout(d time.Duration) {
if c.client != nil {
c.client.Timeout = d
Expand Down Expand Up @@ -119,6 +132,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 {
Expand Down Expand Up @@ -153,9 +167,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
}

Comment thread
rahulguptajss marked this conversation as resolved.
// 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.IsObject() {
return []byte(body.Raw)
}
return data
}
Comment thread
rahulguptajss marked this conversation as resolved.
Comment thread
rahulguptajss marked this conversation as resolved.
Comment thread
rahulguptajss marked this conversation as resolved.

// 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...)
Expand Down
1 change: 1 addition & 0 deletions docs/configure-harvest-basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | |
Expand Down
64 changes: 64 additions & 0 deletions docs/gcnv-ontap-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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: <host>/v1beta1/projects/<project_id>/locations/<location>/storagePools/<pool_name>/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. The script must print `authToken: <token>` 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).

## 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
3 changes: 2 additions & 1 deletion grafana/dashboards/cmode-details/volumeBySVM.json
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@
"harvest",
"ontap",
"cdot",
"fsx"
"fsx",
"gcnv-ontap-mode"
],
"templating": {
"list": [
Expand Down
3 changes: 2 additions & 1 deletion grafana/dashboards/cmode-details/volumeDeepDive.json
Original file line number Diff line number Diff line change
Expand Up @@ -2626,7 +2626,8 @@
"harvest",
"ontap",
"cdot",
"fsx"
"fsx",
"gcnv-ontap-mode"
],
"templating": {
"list": [
Expand Down
94 changes: 64 additions & 30 deletions grafana/dashboards/cmode/lun.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -1881,55 +1881,88 @@
],
"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": {
"excludeByName": {
"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"
Expand Down Expand Up @@ -4151,7 +4184,8 @@
"harvest",
"ontap",
"cdot",
"fsx"
"fsx",
"gcnv-ontap-mode"
],
"templating": {
"list": [
Expand Down
3 changes: 2 additions & 1 deletion grafana/dashboards/cmode/security.json
Original file line number Diff line number Diff line change
Expand Up @@ -5354,7 +5354,8 @@
"harvest",
"ontap",
"cdot",
"fsx"
"fsx",
"gcnv-ontap-mode"
],
"templating": {
"list": [
Expand Down
Loading
Loading