From c4dc19d9b4294955fa49aeb320caff53dc0ce73d Mon Sep 17 00:00:00 2001 From: Hardikl <83282894+Hardikl@users.noreply.github.com> Date: Tue, 23 Jul 2024 22:16:32 +0530 Subject: [PATCH] feat: handled user/group quota in HistoricalLabels (#3060) * feat: handled user/group quota in HistoricalLabels for zapi/rest --- cmd/collectors/rest/plugins/qtree/qtree.go | 155 +++++------------- .../rest/plugins/qtree/qtree_test.go | 46 +++++- cmd/collectors/zapi/plugins/qtree/qtree.go | 150 ++++++----------- .../zapi/plugins/qtree/qtree_test.go | 45 ++++- 4 files changed, 164 insertions(+), 232 deletions(-) diff --git a/cmd/collectors/rest/plugins/qtree/qtree.go b/cmd/collectors/rest/plugins/qtree/qtree.go index 49eb1d31a..54831236f 100644 --- a/cmd/collectors/rest/plugins/qtree/qtree.go +++ b/cmd/collectors/rest/plugins/qtree/qtree.go @@ -14,7 +14,6 @@ import ( "github.com/netapp/harvest/v2/pkg/tree/node" "github.com/netapp/harvest/v2/pkg/util" "github.com/tidwall/gjson" - "strconv" "strings" "time" ) @@ -106,7 +105,6 @@ func (q *Qtree) Init() error { } instanceKeys.NewChildS("", "type") - instanceKeys.NewChildS("", "index") instanceKeys.NewChildS("", "unit") q.data.SetExportOptions(exportOptions) @@ -175,15 +173,9 @@ func (q *Qtree) Run(dataMap map[string]*matrix.Matrix) ([]*matrix.Matrix, *util. } quotaCount := 0 - cluster := data.GetGlobalLabels()["cluster"] - if q.historicalLabels { - // In 22.05, populate metrics with qtree prefix and old labels - err = q.handlingHistoricalMetrics(result, data, cluster, "aCount, &numMetrics) - } else { - // Populate metrics with quota prefix and current labels - err = q.handlingQuotaMetrics(result, "aCount, &numMetrics) - } + // Populate metrics with quota prefix + err = q.handlingQuotaMetrics(result, data, "aCount, &numMetrics) if err != nil { return nil, nil, err @@ -207,11 +199,11 @@ func (q *Qtree) Run(dataMap map[string]*matrix.Matrix) ([]*matrix.Matrix, *util. return []*matrix.Matrix{q.data}, q.client.Metadata, nil } -func (q *Qtree) handlingHistoricalMetrics(result []gjson.Result, data *matrix.Matrix, cluster string, quotaIndex *int, numMetrics *int) error { +func (q *Qtree) handlingQuotaMetrics(result []gjson.Result, data *matrix.Matrix, quotaCount *int, numMetrics *int) error { for _, quota := range result { var tree string - var quotaInstance *matrix.Instance - var err error + var qtreeInstance *matrix.Instance + var value float64 if !quota.IsObject() { q.Logger.Error().Str("type", quota.Type.String()).Msg("Quota is not an object, skipping") @@ -221,115 +213,39 @@ func (q *Qtree) handlingHistoricalMetrics(result []gjson.Result, data *matrix.Ma if quota.Get("qtree.name").Exists() { tree = quota.Get("qtree.name").String() } + quotaType := quota.Get("type").String() volume := quota.Get("volume.name").String() vserver := quota.Get("svm.name").String() - qIndex := quota.Get("index").String() - quotaType := quota.Get("type").String() - - // If quota-type is not a tree, then skip - if quotaType != "tree" { - continue - } - - // Ex. InstanceKey: vserver1vol1qtree15989279 - quotaInstanceKey := vserver + volume + tree + qIndex + uName := quota.Get("users.0.name").String() + uid := quota.Get("users.0.id").String() + group := quota.Get("group.name").String() + *quotaCount++ - if quotaInstance = q.data.GetInstance(quotaInstanceKey); quotaInstance == nil { - if quotaInstance, err = q.data.NewInstance(quotaInstanceKey); err != nil { - q.Logger.Error().Stack().Err(err).Str("quotaInstanceKey", quotaInstanceKey).Msg("Failed to create quota instance") - return err + // In 22.05, populate metrics with qtree labels + if q.historicalLabels { + // qtree instancekey would be qtree, svm and volume(sorted keys) + qtreeInstance = data.GetInstance(tree + vserver + volume) + if qtreeInstance == nil { + q.Logger.Warn(). + Str("tree", tree). + Str("volume", volume). + Str("vserver", vserver). + Msg("No instance matching tree.vserver.volume") + continue } - } - - // qtree instancekey would be qtree, svm and volume(sorted keys) - qtreeInstance := data.GetInstance(tree + vserver + volume) - if qtreeInstance == nil { - q.Logger.Warn(). - Str("tree", tree). - Str("volume", volume). - Str("vserver", vserver). - Msg("No instance matching tree.vserver.volume") - continue - } - - if !qtreeInstance.IsExportable() { - continue - } - - for _, label := range q.data.GetExportOptions().GetChildS("instance_keys").GetAllChildContentS() { - if value := qtreeInstance.GetLabel(label); value != "" { - quotaInstance.SetLabel(label, value) + if !qtreeInstance.IsExportable() { + continue } } - // set labels - quotaInstance.SetLabel("type", quotaType) - quotaInstance.SetLabel("qtree", tree) - quotaInstance.SetLabel("volume", volume) - quotaInstance.SetLabel("svm", vserver) - quotaInstance.SetLabel("index", cluster+"_"+strconv.Itoa(*quotaIndex)) - - // If the Qtree is the volume itself, then qtree label is empty, so copy the volume name to qtree. - if tree == "" { - quotaInstance.SetLabel("qtree", volume) - } - - *quotaIndex++ for attribute, m := range q.data.GetMetrics() { - value := 0.0 - - if attrValue := quota.Get(attribute); attrValue.Exists() { - // space limits are in bytes, converted to kilobytes - if attribute == "space.hard_limit" || attribute == "space.soft_limit" { - value = attrValue.Float() / 1024 - quotaInstance.SetLabel("unit", "Kbyte") - if attribute == "space.soft_limit" { - t := q.data.GetMetric("threshold") - if err = t.SetValueFloat64(quotaInstance, value); err != nil { - q.Logger.Error().Err(err).Str("attribute", attribute).Float64("value", value).Msg("Failed to parse value") - } else { - *numMetrics++ - } - } - } else { - value = attrValue.Float() - } - } - - // populate numeric data - if err = m.SetValueFloat64(quotaInstance, value); err != nil { - q.Logger.Error().Stack().Err(err).Str("attribute", attribute).Float64("value", value).Msg("Failed to parse value") + // In 22.05, populate metrics value with 0 + if q.historicalLabels { + value = 0.0 } else { - *numMetrics++ + // set -1 for unlimited + value = -1.0 } - } - } - return nil -} - -func (q *Qtree) handlingQuotaMetrics(result []gjson.Result, quotaCount *int, numMetrics *int) error { - for _, quota := range result { - var tree string - - if !quota.IsObject() { - q.Logger.Error().Str("type", quota.Type.String()).Msg("Quota is not an object, skipping") - return errs.New(errs.ErrNoInstance, "quota is not an object") - } - - if quota.Get("qtree.name").Exists() { - tree = quota.Get("qtree.name").String() - } - quotaType := quota.Get("type").String() - volume := quota.Get("volume.name").String() - vserver := quota.Get("svm.name").String() - uName := quota.Get("users.0.name").String() - uid := quota.Get("users.0.id").String() - group := quota.Get("group.name").String() - *quotaCount++ - - for attribute, m := range q.data.GetMetrics() { - // set -1 for unlimited - value := -1.0 quotaInstanceKey := vserver + "." + volume + "." + tree + "." + group + "." + uName + "." + attribute @@ -338,12 +254,26 @@ func (q *Qtree) handlingQuotaMetrics(result []gjson.Result, quotaCount *int, num q.Logger.Debug().Msgf("add (%s) instance: %v", attribute, err) return err } + // set labels quotaInstance.SetLabel("type", quotaType) quotaInstance.SetLabel("qtree", tree) quotaInstance.SetLabel("volume", volume) quotaInstance.SetLabel("svm", vserver) + // In 22.05, populate metrics with qtree labels + if q.historicalLabels { + for _, label := range q.data.GetExportOptions().GetChildS("instance_keys").GetAllChildContentS() { + if val := qtreeInstance.GetLabel(label); val != "" { + quotaInstance.SetLabel(label, val) + } + } + // If the Qtree is the volume itself, then qtree label is empty, so copy the volume name to qtree. + if tree == "" { + quotaInstance.SetLabel("qtree", volume) + } + } + if quotaType == "user" { if uName != "" { quotaInstance.SetLabel("user", uName) @@ -357,6 +287,7 @@ func (q *Qtree) handlingQuotaMetrics(result []gjson.Result, quotaCount *int, num quotaInstance.SetLabel("group", uid) } } + if attrValue := quota.Get(attribute); attrValue.Exists() { // space limits are in bytes, converted to kilobytes to match ZAPI if attribute == "space.hard_limit" || attribute == "space.soft_limit" || attribute == "space.used.total" { diff --git a/cmd/collectors/rest/plugins/qtree/qtree_test.go b/cmd/collectors/rest/plugins/qtree/qtree_test.go index fab9b4c09..4d157e8e2 100644 --- a/cmd/collectors/rest/plugins/qtree/qtree_test.go +++ b/cmd/collectors/rest/plugins/qtree/qtree_test.go @@ -4,6 +4,7 @@ import ( "github.com/netapp/harvest/v2/cmd/poller/plugin" "github.com/netapp/harvest/v2/pkg/logging" "github.com/netapp/harvest/v2/pkg/matrix" + "github.com/netapp/harvest/v2/pkg/tree/node" "github.com/tidwall/gjson" "os" "testing" @@ -19,8 +20,6 @@ func NewQtree() *Qtree { } func TestHandlingQuotaMetrics(t *testing.T) { - q := NewQtree() - jsonResponse, err := os.ReadFile("testdata/quota.json") if err != nil { t.Fatalf("Failed to read JSON response from file: %v", err) @@ -28,18 +27,49 @@ func TestHandlingQuotaMetrics(t *testing.T) { result := gjson.Get(string(jsonResponse), "records").Array() + // Case 1: with historicalLabels = false + q1 := NewQtree() + q1.historicalLabels = false + testLabels(t, q1, result, nil, "astra_300.trident_qtree_pool_trident_TIXRBILLKA.trident_pvc_19913841_a29f_4a54_8bc0_a3c1c4155826...space.hard_limit", 3, 6, 5) + + // Case 2: with historicalLabels = true + q2 := NewQtree() + data := matrix.New(q2.Parent+".Qtree", "qtree", "qtree") + qtreeInstance, _ := data.NewInstance("" + "abcde" + "abcd_root") + qtreeInstance.SetLabel("export_policy", "default") + qtreeInstance.SetLabel("oplocks", "enabled") + qtreeInstance.SetLabel("security_style", "unix") + qtreeInstance.SetLabel("status", "normal") + + exportOptions := node.NewS("export_options") + instanceKeys := exportOptions.NewChildS("instance_keys", "") + // apply all instance keys, instance labels from qtree.yaml to all quota metrics + keys := []string{"export_policy", "oplocks", "security_style", "status"} + for _, key := range keys { + instanceKeys.NewChildS("", key) + } + q2.data.SetExportOptions(exportOptions) + q2.historicalLabels = true + testLabels(t, q2, result, data, "abcde.abcd_root...root.space.used.total", 3, 4, 10) +} + +func testLabels(t *testing.T, q *Qtree, quotas []gjson.Result, data *matrix.Matrix, quotaInstanceKey string, expectedQuotaCount int, expectedQuotaMetricCount int, expectedQuotaLabels int) { quotaCount := 0 numMetrics := 0 - - err = q.handlingQuotaMetrics(result, "aCount, &numMetrics) + err := q.handlingQuotaMetrics(quotas, data, "aCount, &numMetrics) if err != nil { t.Errorf("handlingQuotaMetrics returned an error: %v", err) } - if quotaCount != 3 { - t.Errorf("quotaCount = %d; want 3", quotaCount) + if quotaCount != expectedQuotaCount { + t.Errorf("quotaCount = %d; want %d", quotaCount, expectedQuotaCount) } - if numMetrics != 6 { - t.Errorf("numMetrics = %d; want 6", numMetrics) + if numMetrics != expectedQuotaMetricCount { + t.Errorf("numMetrics = %d; want %d", numMetrics, expectedQuotaMetricCount) + } + + quotaInstance := q.data.GetInstance(quotaInstanceKey) + if len(quotaInstance.GetLabels()) != expectedQuotaLabels { + t.Errorf("labels = %d; want %d", len(quotaInstance.GetLabels()), expectedQuotaLabels) } } diff --git a/cmd/collectors/zapi/plugins/qtree/qtree.go b/cmd/collectors/zapi/plugins/qtree/qtree.go index 86620adce..5d64a45b3 100644 --- a/cmd/collectors/zapi/plugins/qtree/qtree.go +++ b/cmd/collectors/zapi/plugins/qtree/qtree.go @@ -89,8 +89,9 @@ func (q *Qtree) Init() error { } instanceKeys.NewChildS("", "type") - instanceKeys.NewChildS("", "index") instanceKeys.NewChildS("", "unit") + instanceKeys.NewChildS("", "user") + instanceKeys.NewChildS("", "group") q.data.SetExportOptions(exportOptions) q.historicalLabels = true @@ -177,8 +178,6 @@ func (q *Qtree) Run(dataMap map[string]*matrix.Matrix) ([]*matrix.Matrix, *util. } } - cluster := data.GetGlobalLabels()["cluster"] - for { response, tag, ad, pd, err = q.client.InvokeBatchWithTimers(request, tag) @@ -208,13 +207,8 @@ func (q *Qtree) Run(dataMap map[string]*matrix.Matrix) ([]*matrix.Matrix, *util. q.Logger.Debug().Int("quotas", len(quotas)).Msg("fetching quotas") - if q.historicalLabels { - // In 22.05, populate metrics with qtree prefix and old labels - err = q.handlingHistoricalMetrics(quotas, data, cluster, "aIndex, &numMetrics) - } else { - // Populate metrics with quota prefix and current labels - err = q.handlingQuotaMetrics(quotas, "aIndex, &numMetrics) - } + // Populate metrics with quota prefix + err = q.handlingQuotaMetrics(quotas, data, "aIndex, &numMetrics) if err != nil { return nil, nil, err @@ -242,99 +236,10 @@ func (q *Qtree) Run(dataMap map[string]*matrix.Matrix) ([]*matrix.Matrix, *util. return []*matrix.Matrix{q.data}, q.client.Metadata, nil } -func (q *Qtree) handlingHistoricalMetrics(quotas []*node.Node, data *matrix.Matrix, cluster string, quotaIndex *int, numMetrics *int) error { - for qIndex, quota := range quotas { - var vserver, quotaInstanceKey string - var qtreeInstance *matrix.Instance - - quotaType := quota.GetChildContentS("quota-type") - tree := quota.GetChildContentS("tree") - volume := quota.GetChildContentS("volume") - if q.client.IsClustered() { - vserver = quota.GetChildContentS("vserver") - } - - *quotaIndex++ - for attribute, m := range q.data.GetMetrics() { - - objectElem := quota.GetChildS(attribute) - if objectElem == nil { - continue - } - - if attrValue := quota.GetChildContentS(attribute); attrValue != "" { - if q.client.IsClustered() { - qtreeInstance = data.GetInstance(tree + "." + volume + "." + vserver) - } else { - qtreeInstance = data.GetInstance(volume + "." + tree) - } - if qtreeInstance == nil { - q.Logger.Warn(). - Str("tree", tree). - Str("volume", volume). - Str("vserver", vserver). - Msg("No instance matching tree.volume.vserver") - continue - } - if !qtreeInstance.IsExportable() { - continue - } - // Ex. InstanceKey: SVMA.vol1Abc.qtree1.5.disk-limit - if q.client.IsClustered() { - quotaInstanceKey = vserver + "." + volume + "." + tree + "." + strconv.Itoa(qIndex) + "." + attribute - } else { - quotaInstanceKey = volume + "." + tree + "." + strconv.Itoa(qIndex) + "." + attribute - } - quotaInstance, err := q.data.NewInstance(quotaInstanceKey) - - if err != nil { - q.Logger.Debug().Msgf("add (%s) instance: %v", attribute, err) - return err - } - - for _, label := range q.data.GetExportOptions().GetChildS("instance_keys").GetAllChildContentS() { - if value := qtreeInstance.GetLabel(label); value != "" { - quotaInstance.SetLabel(label, value) - } - } - - // set labels - quotaInstance.SetLabel("type", quotaType) - quotaInstance.SetLabel("qtree", tree) - quotaInstance.SetLabel("volume", volume) - quotaInstance.SetLabel("svm", vserver) - quotaInstance.SetLabel("index", cluster+"_"+strconv.Itoa(*quotaIndex)) - - // If the Qtree is the volume itself, then qtree label is empty, so copy the volume name to qtree. - if tree == "" { - quotaInstance.SetLabel("qtree", volume) - } - - // populate numeric data - if value := strings.Split(attrValue, " ")[0]; value != "" { - // Few quota metrics would have value '-' which means unlimited (ex: disk-limit) - if value == "-" { - value = "0" - } - if attribute == "soft-disk-limit" || attribute == "disk-limit" || attribute == "disk-used" { - quotaInstance.SetLabel("unit", "Kbyte") - } - if err := m.SetValueString(quotaInstance, value); err != nil { - q.Logger.Debug().Msgf("(%s) failed to parse value (%s): %v", attribute, value, err) - } else { - *numMetrics++ - } - } - - } - } - } - return nil -} - -func (q *Qtree) handlingQuotaMetrics(quotas []*node.Node, quotaCount *int, numMetrics *int) error { +func (q *Qtree) handlingQuotaMetrics(quotas []*node.Node, data *matrix.Matrix, quotaCount *int, numMetrics *int) error { for _, quota := range quotas { var vserver, quotaInstanceKey, uid, uName string + var qtreeInstance *matrix.Instance quotaType := quota.GetChildContentS("quota-type") tree := quota.GetChildContentS("tree") @@ -370,15 +275,33 @@ func (q *Qtree) handlingQuotaMetrics(quotas []*node.Node, quotaCount *int, numMe } *quotaCount++ - for attribute, m := range q.data.GetMetrics() { + // In 22.05, populate metrics with qtree labels + if q.historicalLabels { + if q.client.IsClustered() { + qtreeInstance = data.GetInstance(tree + "." + volume + "." + vserver) + } else { + qtreeInstance = data.GetInstance(volume + "." + tree) + } + if qtreeInstance == nil { + q.Logger.Warn(). + Str("tree", tree). + Str("volume", volume). + Str("vserver", vserver). + Msg("No instance matching tree.volume.vserver") + continue + } + if !qtreeInstance.IsExportable() { + continue + } + } + for attribute, m := range q.data.GetMetrics() { objectElem := quota.GetChildS(attribute) if objectElem == nil { continue } if attrValue := quota.GetChildContentS(attribute); attrValue != "" { - // Ex. InstanceKey: SVMA.vol1Abc.qtree1.5.disk-limit if q.client.IsClustered() { quotaInstanceKey = vserver + "." + volume + "." + tree + "." + uName + "." + attribute + "." + quotaType @@ -390,12 +313,26 @@ func (q *Qtree) handlingQuotaMetrics(quotas []*node.Node, quotaCount *int, numMe q.Logger.Debug().Msgf("add (%s) instance: %v", attribute, err) return err } + // set labels quotaInstance.SetLabel("type", quotaType) quotaInstance.SetLabel("qtree", tree) quotaInstance.SetLabel("volume", volume) quotaInstance.SetLabel("svm", vserver) + // In 22.05, populate metrics with qtree labels + if q.historicalLabels { + for _, label := range q.data.GetExportOptions().GetChildS("instance_keys").GetAllChildContentS() { + if value := qtreeInstance.GetLabel(label); value != "" { + quotaInstance.SetLabel(label, value) + } + } + // If the Qtree is the volume itself, then qtree label is empty, so copy the volume name to qtree. + if tree == "" { + quotaInstance.SetLabel("qtree", volume) + } + } + if quotaType == "user" { if uName != "" { quotaInstance.SetLabel("user", uName) @@ -414,7 +351,12 @@ func (q *Qtree) handlingQuotaMetrics(quotas []*node.Node, quotaCount *int, numMe if value := strings.Split(attrValue, " ")[0]; value != "" { // Few quota metrics would have value '-' which means unlimited (ex: disk-limit) if value == "-" { - value = "-1" + // In 22.05, populate metrics value with 0 + if q.historicalLabels { + value = "0" + } else { + value = "-1" + } } if attribute == "soft-disk-limit" || attribute == "disk-limit" || attribute == "disk-used" { diff --git a/cmd/collectors/zapi/plugins/qtree/qtree_test.go b/cmd/collectors/zapi/plugins/qtree/qtree_test.go index 566563f50..704211ba4 100644 --- a/cmd/collectors/zapi/plugins/qtree/qtree_test.go +++ b/cmd/collectors/zapi/plugins/qtree/qtree_test.go @@ -22,8 +22,6 @@ func NewQtree() *Qtree { } func TestHandlingQuotaMetrics(t *testing.T) { - q := NewQtree() - xmlResponse, err := os.ReadFile("testdata/quotas.xml") if err != nil { t.Fatalf("Failed to read XML response from file: %v", err) @@ -44,18 +42,49 @@ func TestHandlingQuotaMetrics(t *testing.T) { quotas = x.GetChildren() } + // Case 1: with historicalLabels = false + q1 := NewQtree() + q1.historicalLabels = false + testLabels(t, q1, quotas, nil, "astra_300.trident_qtree_pool_trident_TIXRBILLKA.trident_pvc_2a6d71d9_1c78_4e9a_84a2_59d316adfae9..disk-limit.tree", 3, 6, 5) + + // Case 2: with historicalLabels = true + q2 := NewQtree() + data := matrix.New(q2.Parent+".Qtree", "qtree", "qtree") + qtreeInstance, _ := data.NewInstance("" + "." + "abcd_root" + "." + "abcde") + qtreeInstance.SetLabel("export_policy", "default") + qtreeInstance.SetLabel("oplocks", "enabled") + qtreeInstance.SetLabel("security_style", "unix") + qtreeInstance.SetLabel("status", "normal") + + exportOptions := node.NewS("export_options") + instanceKeys := exportOptions.NewChildS("instance_keys", "") + // apply all instance keys, instance labels from qtree.yaml to all quota metrics + keys := []string{"export_policy", "oplocks", "security_style", "status"} + for _, key := range keys { + instanceKeys.NewChildS("", key) + } + q2.data.SetExportOptions(exportOptions) + q2.historicalLabels = true + testLabels(t, q2, quotas, data, "abcde.abcd_root..root.disk-used.user", 3, 4, 10) +} + +func testLabels(t *testing.T, q *Qtree, quotas []*node.Node, data *matrix.Matrix, quotaInstanceKey string, expectedQuotaCount int, expectedQuotaMetricCount int, expectedQuotaLabels int) { quotaCount := 0 numMetrics := 0 - - err = q.handlingQuotaMetrics(quotas, "aCount, &numMetrics) + err := q.handlingQuotaMetrics(quotas, data, "aCount, &numMetrics) if err != nil { t.Errorf("handlingQuotaMetrics returned an error: %v", err) } - if quotaCount != 3 { - t.Errorf("quotaCount = %d; want 3", quotaCount) + if quotaCount != expectedQuotaCount { + t.Errorf("quotaCount = %d; want %d", quotaCount, expectedQuotaCount) } - if numMetrics != 6 { - t.Errorf("numMetrics = %d; want 6", numMetrics) + if numMetrics != expectedQuotaMetricCount { + t.Errorf("numMetrics = %d; want %d", numMetrics, expectedQuotaMetricCount) + } + + quotaInstance := q.data.GetInstance(quotaInstanceKey) + if len(quotaInstance.GetLabels()) != expectedQuotaLabels { + t.Errorf("labels = %d; want %d", len(quotaInstance.GetLabels()), expectedQuotaLabels) } }