Repository: NetApp/harvest
Pull request #769 (2022-01-04 10:09:37)

feat: private CLI Rest infrastructure
This PR introduces a new plugin "RestGaps". Example in volume.yaml.
Any rest gaps can be filled with relevant private cli call based on the instance key.


Patch:
diff --git a/cmd/collectors/rest/rest.go b/cmd/collectors/rest/rest.go
index 5f2f5245..2adc90f5 100644
--- a/cmd/collectors/rest/rest.go
+++ b/cmd/collectors/rest/rest.go
@@ -19,14 +19,24 @@ import (
 
 type Rest struct {
 	*collector.AbstractCollector
-	client         *rest.Client
-	apiPath        string
+	client    *rest.Client
+	prop      *prop
+	endpoints []*endPoint
+}
+
+type endPoint struct {
+	prop *prop
+	name string
+}
+
+type prop struct {
+	query          string
 	instanceKeys   []string
 	instanceLabels map[string]string
+	metrics        []string
 	counters       map[string]string
-	fields         []string
-	misses         []string
 	returnTimeOut  string
+	fields         []string
 	apiType        string // public, private
 }
 
@@ -47,6 +57,8 @@ func (r *Rest) Init(a *collector.AbstractCollector) error {
 
 	r.AbstractCollector = a
 
+	r.prop = &prop{}
+
 	if r.client, err = r.getClient(a, a.Params); err != nil {
 		return err
 	}
@@ -59,6 +71,10 @@ func (r *Rest) Init(a *collector.AbstractCollector) error {
 		return err
 	}
 
+	if err = r.initEndPoints(); err != nil {
+		return err
+	}
+
 	if err = collector.Init(r); err != nil {
 		return err
 	}
@@ -90,16 +106,30 @@ func (r *Rest) InitVars() error {
 	r.Params.Union(template)
 	// private end point do not support * as fields. We need to pass fields in endpoint
 	query := r.Params.GetChildS("query")
-	if query != nil && strings.Contains(query.GetContentS(), "private") {
-		r.apiType = "private"
-	} else {
-		r.apiType = "public"
+	r.prop.apiType = "public"
+	if query != nil {
+		r.prop.apiType = checkQueryType(query.GetContentS())
 	}
 
-	r.fields = []string{"*"}
-	if x := r.Params.GetChildS("fields"); x != nil {
-		r.fields = append(r.fields, x.GetAllChildContentS()...)
+	if r.prop.apiType == "private" {
+		counterKey := make([]string, len(r.prop.counters))
+		i := 0
+		for k := range r.prop.counters {
+			counterKey[i] = k
+			i++
+		}
+		r.prop.fields = counterKey
 	}
+
+	if r.prop.apiType == "public" {
+		r.prop.fields = []string{"*"}
+		if c := r.Params.GetChildS("counters"); c != nil {
+			if x := c.GetChildS("hidden_fields"); x != nil {
+				r.prop.fields = append(r.prop.fields, x.GetAllChildContentS()...)
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -137,6 +167,72 @@ func (r *Rest) getClient(a *collector.AbstractCollector, config *node.Node) (*re
 	return client, err
 }
 
+func (r *Rest) initEndPoints() error {
+
+	endpoints := r.Params.GetChildS("endpoints")
+	if endpoints != nil {
+		for _, line := range endpoints.GetChildren() {
+			var (
+				display, name, kind string
+			)
+
+			n := line.GetNameS()
+			e := endPoint{name: n}
+
+			prop := prop{}
+
+			prop.instanceKeys = make([]string, 0)
+			prop.instanceLabels = make(map[string]string)
+			prop.counters = make(map[string]string)
+			prop.metrics = make([]string, 0)
+			prop.apiType = "public"
+
+			for _, line1 := range line.GetChildren() {
+				if line1.GetNameS() == "query" {
+					prop.query = line1.GetContentS()
+					prop.apiType = checkQueryType(prop.query)
+				}
+				if line1.GetNameS() == "counters" {
+					for _, c := range line1.GetAllChildContentS() {
+						name, display, kind = ParseMetric(c)
+
+						prop.counters[name] = display
+						switch kind {
+						case "key":
+							prop.instanceLabels[name] = display
+							prop.instanceKeys = append(prop.instanceKeys, name)
+						case "label":
+							prop.instanceLabels[name] = display
+						default:
+							prop.instanceLabels[name] = display
+							prop.metrics = append(prop.metrics, name)
+						}
+					}
+					if prop.apiType == "private" {
+						counterKey := make([]string, len(prop.counters))
+						i := 0
+						for k := range prop.counters {
+							counterKey[i] = k
+							i++
+						}
+						prop.fields = counterKey
+					}
+
+					if prop.apiType == "public" {
+						prop.fields = []string{"*"}
+						if x := line1.GetChildS("hidden_fields"); x != nil {
+							prop.fields = append(prop.fields, x.GetAllChildContentS()...)
+						}
+					}
+				}
+			}
+			e.prop = &prop
+			r.endpoints = append(r.endpoints, &e)
+		}
+	}
+	return nil
+}
+
 func (r *Rest) getTemplateFn() string {
 	var fn string
 	objects := r.Params.GetChildS("objects")
@@ -190,37 +286,17 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	startTime = time.Now()
 
-	if r.apiType == "public" {
-		href := rest.BuildHref(r.apiPath, strings.Join(r.fields, ","), nil, "", "", "", r.returnTimeOut, r.apiPath)
-		r.Logger.Debug().Str("href", href).Msg("")
-		err = rest.FetchData(r.client, href, &records)
-		if err != nil {
-			r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
-			return nil, err
-		}
-	}
+	href := rest.BuildHref(r.prop.query, strings.Join(r.prop.fields, ","), nil, "", "", "", r.prop.returnTimeOut, r.prop.query)
 
-	if r.apiType == "private" {
-		counterKey := make([]string, len(r.counters))
-		i := 0
-		for k := range r.counters {
-			counterKey[i] = k
-			i++
-		}
-		href := rest.BuildHref(r.apiPath, strings.Join(counterKey, ","), nil, "", "", "", r.returnTimeOut, r.apiPath)
-		r.Logger.Debug().Str("href", href).Msg("")
-		err = rest.FetchData(r.client, href, &records)
-		if err != nil {
-			r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
-			return nil, err
-		}
+	r.Logger.Debug().Str("href", href).Msg("")
+	if href == "" {
+		return nil, errors.New(errors.ERR_CONFIG, "empty url")
 	}
 
-	if len(r.misses) > 0 {
-		r.Logger.Warn().
-			Str("mis-configured counters", strings.Join(r.misses, ",")).
-			Str("ApiPath", r.apiPath).
-			Msg("")
+	err = rest.FetchData(r.client, href, &records)
+	if err != nil {
+		r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return nil, err
 	}
 
 	all := rest.Pagination{
@@ -231,12 +307,12 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	content, err = json.Marshal(all)
 	if err != nil {
-		r.Logger.Error().Err(err).Str("ApiPath", r.apiPath).Msg("Unable to marshal rest pagination")
+		r.Logger.Error().Err(err).Str("ApiPath", r.prop.query).Msg("Unable to marshal rest pagination")
 	}
 
 	startTime = time.Now()
 	if !gjson.ValidBytes(content) {
-		return nil, fmt.Errorf("json is not valid for: %s", r.apiPath)
+		return nil, fmt.Errorf("json is not valid for: %s", r.prop.query)
 	}
 	parseD = time.Since(startTime)
 
@@ -260,7 +336,7 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 		}
 
 		// extract instance key(s)
-		for _, k := range r.instanceKeys {
+		for _, k := range r.prop.instanceKeys {
 			value := instanceData.Get(k)
 			if value.Exists() {
 				instanceKey += value.String()
@@ -283,7 +359,7 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 			}
 		}
 
-		for label, display := range r.instanceLabels {
+		for label, display := range r.prop.instanceLabels {
 			value := instanceData.Get(label)
 			if value.Exists() {
 				if value.IsArray() {
@@ -304,29 +380,24 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 		}
 
 		for key, metric := range r.Matrix.GetMetrics() {
-
-			if metric.GetProperty() == "etl.bool" {
-				b := instanceData.Get(key)
-				if b.Exists() {
-					if err = metric.SetValueBool(instance, b.Bool()); err != nil {
-						r.Logger.Error().Err(err).Str("key", key).Str("metric", metric.GetName()).Msg("Unable to set bool key on metric")
-					}
-					count++
-				}
-			} else if metric.GetProperty() == "etl.float" {
-				f := instanceData.Get(key)
-				if f.Exists() {
-					if err = metric.SetValueFloat64(instance, f.Float()); err != nil {
-						r.Logger.Error().Err(err).Str("key", key).Str("metric", metric.GetName()).
-							Msg("Unable to set float key on metric")
-					}
-					count++
+			f := instanceData.Get(key)
+			if f.Exists() {
+				if err = metric.SetValueFloat64(instance, f.Float()); err != nil {
+					r.Logger.Error().Err(err).Str("key", key).Str("metric", metric.GetName()).
+						Msg("Unable to set float key on metric")
 				}
+				count++
 			}
 		}
 		return true
 	})
 
+	// process endpoints
+	err = r.processEndPoints(r.Matrix)
+	if err != nil {
+		r.Logger.Error().Err(err).Msg("Error while processing end points")
+	}
+
 	r.Logger.Info().
 		Uint64("dataPoints", count).
 		Str("apiTime", apiD.String()).
@@ -341,12 +412,146 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 	return r.Matrix, nil
 }
 
-func (me *Rest) LoadPlugin(kind string, abc *plugin.AbstractPlugin) plugin.Plugin {
+func (r *Rest) processEndPoints(data *matrix.Matrix) error {
+	var (
+		err error
+	)
+
+	for _, endpoint := range r.endpoints {
+		var (
+			records []interface{}
+			content []byte
+		)
+		counterKey := make([]string, len(endpoint.prop.counters))
+		i := 0
+		for k := range endpoint.prop.counters {
+			counterKey[i] = k
+			i++
+		}
+
+		href := rest.BuildHref(endpoint.prop.query, strings.Join(endpoint.prop.fields, ","), nil, "", "", "", r.prop.returnTimeOut, endpoint.prop.query)
+
+		r.Logger.Debug().Str("href", href).Msg("")
+
+		if href == "" {
+			return errors.New(errors.ERR_CONFIG, "empty url")
+		}
+		err = rest.FetchData(r.client, href, &records)
+		if err != nil {
+			r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+			return err
+		}
+
+		all := rest.Pagination{
+			Records:    records,
+			NumRecords: len(records),
+		}
+
+		content, err = json.Marshal(all)
+		if err != nil {
+			r.Logger.Error().Err(err).Str("ApiPath", endpoint.prop.query).Msg("Unable to marshal rest pagination")
+		}
+
+		if !gjson.ValidBytes(content) {
+			return fmt.Errorf("json is not valid for: %s", endpoint.prop.query)
+		}
+
+		results := gjson.GetManyBytes(content, "num_records", "records")
+		numRecords := results[0]
+		if numRecords.Int() == 0 {
+			return errors.New(errors.ERR_NO_INSTANCE, "no "+endpoint.prop.query+" instances on cluster")
+		}
+
+		results[1].ForEach(func(key, instanceData gjson.Result) bool {
+			var (
+				instanceKey string
+				instance    *matrix.Instance
+			)
+
+			if !instanceData.IsObject() {
+				r.Logger.Warn().Str("type", instanceData.Type.String()).Msg("Instance data is not object, skipping")
+				return true
+			}
+
+			// extract instance key(s)
+			for _, k := range endpoint.prop.instanceKeys {
+				value := instanceData.Get(k)
+				if value.Exists() {
+					instanceKey += value.String()
+				} else {
+					r.Logger.Warn().Str("key", k).Msg("skip instance, missing key")
+					break
+				}
+			}
+
+			if instance = data.GetInstance(instanceKey); instance != nil {
+
+				for label, display := range endpoint.prop.instanceLabels {
+					value := instanceData.Get(label)
+					if value.Exists() {
+						if value.IsArray() {
+							var labelArray []string
+							for _, r := range value.Array() {
+								labelString := r.String()
+								labelArray = append(labelArray, labelString)
+							}
+							instance.SetLabel(display, strings.Join(labelArray, ","))
+						} else {
+							instance.SetLabel(display, value.String())
+						}
+					} else {
+						// spams a lot currently due to missing label mappings. Moved to debug for now till rest gaps are filled
+						r.Logger.Debug().Str("Instance key", instanceKey).Str("label", label).Msg("Missing label value")
+					}
+				}
+
+				for _, metric := range endpoint.prop.metrics {
+					f := instanceData.Get(metric)
+					if f.Exists() {
+						if metr, ok := data.GetMetrics()[metric]; !ok {
+							if metr, _ = data.NewMetricFloat64(metric); err != nil {
+								r.Logger.Error().Err(err).
+									Str("name", metric).
+									Msg("NewMetricFloat64")
+							}
+							metr.SetName(endpoint.prop.instanceLabels[metric])
+							if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
+								r.Logger.Error().Err(err).Str("key", metric).Str("metric", endpoint.prop.instanceLabels[metric]).
+									Msg("Unable to set float key on metric")
+							}
+						} else {
+							metr.SetName(endpoint.prop.instanceLabels[metric])
+							if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
+								r.Logger.Error().Err(err).Str("key", metric).Str("metric", endpoint.prop.instanceLabels[metric]).
+									Msg("Unable to set float key on metric")
+							}
+						}
+					}
+				}
+			}
+			return true
+		})
+
+	}
+
+	return nil
+}
+
+// returns private if api endpoint has private keyword in it else public
+func checkQueryType(query string) string {
+	if strings.Contains(query, "private") {
+		return "private"
+	} else {
+		return "public"
+	}
+}
+
+func (r *Rest) LoadPlugin(kind string, abc *plugin.AbstractPlugin) plugin.Plugin {
 	switch kind {
 	case "Disk":
 		return disk.New(abc)
 	default:
-		me.Logger.Warn().Str("kind", kind).Msg("no rest plugin found ")
+		r.Logger.Warn().Str("kind", kind).Msg("no rest plugin found ")
 	}
 	return nil
 }
diff --git a/cmd/collectors/rest/templating.go b/cmd/collectors/rest/templating.go
index b5698e28..04754124 100644
--- a/cmd/collectors/rest/templating.go
+++ b/cmd/collectors/rest/templating.go
@@ -27,7 +27,7 @@ func (r *Rest) initCache() error {
 		r.Matrix.SetExportOptions(e)
 	}
 
-	if r.apiPath = r.Params.GetChildContentS("query"); r.apiPath == "" {
+	if r.prop.query = r.Params.GetChildContentS("query"); r.prop.query == "" {
 		return errors.New(errors.MISSING_PARAM, "query")
 	}
 
@@ -38,60 +38,52 @@ func (r *Rest) initCache() error {
 
 	// default value for ONTAP is 15 sec
 	if returnTimeout := r.Params.GetChildContentS("return_timeout"); returnTimeout != "" {
-		r.returnTimeOut = returnTimeout
+		r.prop.returnTimeOut = returnTimeout
 	}
 
-	r.instanceKeys = make([]string, 0)
-	r.instanceLabels = make(map[string]string)
-	r.counters = make(map[string]string)
+	r.prop.instanceKeys = make([]string, 0)
+	r.prop.instanceLabels = make(map[string]string)
+	r.prop.counters = make(map[string]string)
 
 	for _, c := range counters.GetAllChildContentS() {
-		name, display, kind = parseMetric(c)
-		r.Logger.Debug().
-			Str("kind", kind).
-			Str("name", name).
-			Str("display", display).
-			Msg("Collected")
-
-		r.counters[name] = display
-		switch kind {
-		case "key":
-			r.instanceLabels[name] = display
-			r.instanceKeys = append(r.instanceKeys, name)
-		case "label":
-			r.instanceLabels[name] = display
-		case "bool":
-			if metr, err = r.Matrix.NewMetricUint8(name); err != nil {
-				r.Logger.Error().Err(err).
-					Str("name", name).
-					Msg("NewMetricUint8")
-				return err
+		if c != "" {
+			name, display, kind = ParseMetric(c)
+			r.Logger.Debug().
+				Str("kind", kind).
+				Str("name", name).
+				Str("display", display).
+				Msg("Collected")
+
+			r.prop.counters[name] = display
+			switch kind {
+			case "key":
+				r.prop.instanceLabels[name] = display
+				r.prop.instanceKeys = append(r.prop.instanceKeys, name)
+			case "label":
+				r.prop.instanceLabels[name] = display
+			case "float":
+				if metr, err = r.Matrix.NewMetricFloat64(name); err != nil {
+					r.Logger.Error().Err(err).
+						Str("name", name).
+						Msg("NewMetricFloat64")
+					return err
+				}
+				metr.SetName(display)
+				metr.SetProperty("etl.float")
 			}
-			metr.SetName(display)
-			metr.SetProperty("etl.bool") // to distinct from internally generated metrics, e.g. from plugins
-		case "float":
-			if metr, err = r.Matrix.NewMetricFloat64(name); err != nil {
-				r.Logger.Error().Err(err).
-					Str("name", name).
-					Msg("NewMetricFloat64")
-				return err
-			}
-			metr.SetName(display)
-			metr.SetProperty("etl.float")
 		}
 	}
 
-	r.Logger.Info().Strs("extracted Instance Keys", r.instanceKeys).Msg("")
-	r.Logger.Info().Int("count metrics", len(r.Matrix.GetMetrics())).Int("count labels", len(r.instanceLabels)).Msg("initialized metric cache")
+	r.Logger.Info().Strs("extracted Instance Keys", r.prop.instanceKeys).Msg("")
+	r.Logger.Info().Int("count metrics", len(r.Matrix.GetMetrics())).Int("count labels", len(r.prop.instanceLabels)).Msg("initialized metric cache")
 
 	if len(r.Matrix.GetMetrics()) == 0 && r.Params.GetChildContentS("collect_only_labels") != "true" {
 		return errors.New(errors.ERR_NO_METRIC, "failed to parse numeric metrics")
 	}
 	return nil
-
 }
 
-func parseMetric(rawName string) (string, string, string) {
+func ParseMetric(rawName string) (string, string, string) {
 	var (
 		name, display string
 		values        []string
diff --git a/cmd/collectors/zapi/plugins/quota/quota.go b/cmd/collectors/zapi/plugins/quota/quota.go
index 89b9af96..32a00e97 100644
--- a/cmd/collectors/zapi/plugins/quota/quota.go
+++ b/cmd/collectors/zapi/plugins/quota/quota.go
@@ -49,7 +49,11 @@ func (my *Quota) Init() error {
 		return err
 	}
 
-	my.query = "quota-report-iter"
+	if my.client.IsClustered() {
+		my.query = "quota-report-iter"
+	} else {
+		my.query = "quota-report"
+	}
 	my.Logger.Debug().Msg("plugin connected!")
 
 	my.data = matrix.New(my.Parent+".Qtree", "qtree", "qtree")
@@ -95,7 +99,7 @@ func (my *Quota) Init() error {
 		if b := my.Params.GetChildContentS("batch_size"); b != "" {
 			if _, err := strconv.Atoi(b); err == nil {
 				my.batchSize = b
-				my.Logger.Info().Msgf("using batch-size [%s]", my.batchSize)
+				my.Logger.Info().Str("BatchSize", my.batchSize).Msg("using batch-size")
 			}
 		} else {
 			my.batchSize = BatchSize
@@ -107,16 +111,13 @@ func (my *Quota) Init() error {
 }
 
 func (my *Quota) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
-
 	var (
 		request, result *node.Node
 		quotas          []*node.Node
-		tag             string
+		output          []*matrix.Matrix
 		err             error
 	)
 
-	var output []*matrix.Matrix
-
 	// Purge and reset data
 	my.data.PurgeInstances()
 	my.data.Reset()
@@ -125,11 +126,12 @@ func (my *Quota) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 	my.data.SetGlobalLabels(data.GetGlobalLabels())
 
 	request = node.NewXmlS(my.query)
+
 	if my.client.IsClustered() && my.batchSize != "" {
 		request.NewChildS("max-records", my.batchSize)
 	}
 
-	tag = "initial"
+	tag := "initial"
 
 	for {
 		result, tag, err = my.client.InvokeBatchRequest(request, tag)
@@ -142,21 +144,29 @@ func (my *Quota) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
 			break
 		}
 
-		if x := result.GetChildS("attributes-list"); x != nil {
-			quotas = x.GetChildren()
+		if my.client.IsClustered() {
+			if x := result.GetChildS("attributes-list"); x != nil {
+				quotas = x.GetChildren()
+			}
+		} else {
+			quotas = result.SearchChildren([]string{"quota"})
 		}
 
 		if len(quotas) == 0 {
 			return nil, errors.New(errors.ERR_NO_INSTANCE, "no quota instances found")
 		}
 
-		my.Logger.Debug().Msgf("fetching %d quota counters", len(quotas))
+		my.Logger.Debug().Int("quotas", len(quotas)).Msg("fetching quotas")
 
 		for quotaIndex, quota := range quotas {
-
+			var vserver string
 			tree := quota.GetChildContentS("tree")
 			volume := quota.GetChildContentS("volume")
-			vserver := quota.GetChildContentS("vserver")
+			if my.client.IsClustered() {
+				vserver = quota.GetChildContentS("vserver")
+			} else {
+				vserver = quota.GetChildContentS("vfiler")
+			}
 
 			// If quota-type is not a Qtree, then skip
 			if quota.GetChildContentS("quota-type") != "tree" {
diff --git a/conf/rest/9.13.0/aggr.yaml b/conf/rest/9.13.0/aggr.yaml
index ec329299..94398aab 100644
--- a/conf/rest/9.13.0/aggr.yaml
+++ b/conf/rest/9.13.0/aggr.yaml
@@ -3,10 +3,6 @@ name:               Aggregate
 query:              api/storage/aggregates
 object:             aggr
 
-fields:
-  - space.block_storage.inactive_user_data
-  - space.block_storage.inactive_user_data_percent
-
 counters:
   - ^name  => aggr
   - ^^uuid  => uuid
@@ -53,11 +49,14 @@ counters:
   - snapshot.size_total => snapshot_size_total  #mapping seems wrong in doc
   - snapshot.size_used => snapshot_size_used  #mapping seems wrong in doc
   - snapshot.percent_snapshot_space => snapshot_reserve_percent #mapping seems wrong in doc
+  - hidden_fields:
+      - space.block_storage.inactive_user_data
+      - space.block_storage.inactive_user_data_percent
 
 plugins:
-  LabelAgent:
-    value_to_num:
-      - new_status state online online `0`
+  - LabelAgent:
+      value_to_num:
+        - new_status state online online `0`
 
 export_options:
   instance_keys:
diff --git a/conf/rest/9.13.0/disk.yaml b/conf/rest/9.13.0/disk.yaml
index 84fb8ae9..2a049f64 100644
--- a/conf/rest/9.13.0/disk.yaml
+++ b/conf/rest/9.13.0/disk.yaml
@@ -17,8 +17,6 @@ counters:
   - stats.average_latency     => stats_average_latency
   - stats.throughput          => stats_io_kbps
   - stats.power_on_hours      => uptime
-  #- sectors-read  #missing mapping
-  #- sectors-written #missing mapping
   - outage.reason             => outage
   - usable_size
   - ^state
@@ -26,8 +24,14 @@ counters:
   - ^node.name                => node
   - ^home_node.name           => owner_node
 
+endpoints:
+  - query: api/private/cli/disk
+    counters:
+      - ^^uid => uid
+      - sectors_read => stats_sectors_read
+      - sectors_written => stats_sectors_written
+
 plugins:
-  - Disk
   - LabelAgent:
       value_to_num:
         - new_status outage - - `0` #ok_value is empty value, '-' would be converted to blank while processing.
diff --git a/conf/rest/9.13.0/node.yaml b/conf/rest/9.13.0/node.yaml
index 6e5c986c..3d0ac358 100644
--- a/conf/rest/9.13.0/node.yaml
+++ b/conf/rest/9.13.0/node.yaml
@@ -3,14 +3,6 @@ name:                       Node
 query:                      api/cluster/nodes
 object:                     node
 
-fields:
-  - metric.processor_utilization
-  - is_capacity_optimized
-  - is_all_flash_optimized
-  - vendor_serial_number   #seems hidden but no response in rest
-  - is_capacity_optimized
-  - is_all_flash_select_optimized
-
 counters:
   - metric.processor_utilization => cpu_busytime   #hidden field
   - controller.failed_fan.count => failed_fan
@@ -18,13 +10,7 @@ counters:
   - controller.failed_power_supply.count => failed_power
   - ^controller.failed_power_supply.message.message => failed_power_message
   - ^controller.over_temperature => over_temperature
-  - ^is_all_flash_optimized => is_all_flash_optimized
-  - ^is_all_flash_select_optimized => is_all_flash_select_optimized
-  - ^is_capacity_optimized => is_capacity_optimized
-  #- ^system-node-get-iter.is-cloud-optimized => is_cloud_optimized #missing mapping
-  #- system-node-get-iter.is-node-cluster-eligible => is_node_cluster_eligible #missing mapping
   - ^state => healthy
-  - ^is_performance_optimized => is_perf_optimized  #not found in infinity rest call
   #- ^system-node-get-iter.maximum-aggregate-size => max_aggr_size  #missing mapping
   #- ^system-node-get-iter.maximum-number-of-volumes => max_vol_num #missing mapping
   #- ^system-node-get-iter.maximum-volume-size => max_vol_size #missing mapping
@@ -32,10 +18,16 @@ counters:
   - ^location
   - ^model
   - ^serial_number => serial
-  - uptime
-  - ^vendor_serial_number => vendor
+  - ^uptime
   - ^version.full => version
+  - hidden_fields:
+      - metric.processor_utilization
 
+endpoints:
+  - query: api/private/cli/node
+    counters:
+      - ^^node => node
+      - ^vendor  => vendor
 
 plugins:
   - LabelAgent:
diff --git a/conf/rest/9.13.0/snapmirror.yaml b/conf/rest/9.13.0/snapmirror.yaml
index 3d18a904..74e6cb78 100644
--- a/conf/rest/9.13.0/snapmirror.yaml
+++ b/conf/rest/9.13.0/snapmirror.yaml
@@ -4,10 +4,7 @@ query:                        api/snapmirror/relationships
 object:                       snapmirror
 
 counters:
-  #- snapmirror-get-iter.break-failed-count   #missing mapping
-  #- snapmirror-get-iter.break-successful-count #missing-mapping
   - ^destination.path  => destination_volume
-  #- ^destination_volume_node       => destination_node #missing-mapping
   - ^destination.svm.name => destination_vserver
   - ^healthy
   - lag_time
@@ -15,26 +12,33 @@ counters:
   - transfer.end_time => last_transfer_end_timestamp
   - transfer.bytes_transferred  => last_transfer_size
   - ^transfer.bytes_transferred  => last_transfer_size
-  #- newest-snapshot-timestamp #missing mapping
   - ^^uuid => relationship_id
   - ^transfer.state => relationship_status
-  #- ^relationship-type  #missing mapping
-  #- ^relationship-group-type      => group_type #missing mapping
-  #- resync-failed-count #missing mapping
-  #- resync-successful-count #missing mapping
-  #- ^schedule  #maps to snapmirror/policies?fields=transfer_schedule.name . will need plugin change
   - ^source.path => source_volume
   - ^source.svm.name => source_vserver
-  #- ^source-node #missing mapping
-  #- total-transfer-time-secs #missing mapping
+  #- ^source-node #missing mapping  ==> Needs plugin development
   - ^unhealthy_reason.#.message => unhealthy_reason
-  #- update-failed-count #missing mapping
-  #- update-successful-count #missing mapping
 
 
-#Need snapmirror plugin development to support missing field in export
-#plugins:
-#  - Snapmirror
+endpoints:
+  - query: api/private/cli/snapmirror
+    counters:
+      - ^^relationship_id => relationship_id
+      - ^destination_volume_node => destination_node
+      - ^type => relationship_type
+      - ^last_transfer_type => last_transfer_type
+      - ^relationship_group_type  => group_type
+      - ^schedule => schedule
+      - total_transfer_time_secs => total_transfer_time_secs
+      - newest_snapshot_timestamp => newest_snapshot_timestamp
+      - resync_successful_count => resync_successful_count
+      - update_successful_count => update_successful_count
+      - resync_failed_count => resync_failed_count
+      - update_failed_count => update_failed_count
+      - break_successful_count => break_successful_count
+      - break_failed_count => break_failed_count
+      - total_transfer_time_secs => total_transfer_time_secs
+      - newest_snapshot_timestamp => newest_snapshot_timestamp
 
 export_options:
   instance_keys:
diff --git a/conf/rest/9.13.0/snapshot.yaml b/conf/rest/9.13.0/snapshot.yaml
new file mode 100644
index 00000000..9e2e73b6
--- /dev/null
+++ b/conf/rest/9.13.0/snapshot.yaml
@@ -0,0 +1,21 @@
+name:                     Snapshot
+query:                    api/private/cli/snapshot/policy
+object:                   snapshot
+
+schedule:
+  - instance: 1200s
+  - data: 360s
+
+counters:
+  - ^^vserver => svm
+  - ^^policy  =>  snapshot_policy
+  - ^comment  =>  comment
+  - total_schedules => total_schedules
+
+export_options:
+  instance_keys:
+    - svm
+    - snapshot_policy
+    - comment
+
+
diff --git a/conf/rest/9.13.0/volume.yaml b/conf/rest/9.13.0/volume.yaml
index 36e3aaed..1a2b2762 100644
--- a/conf/rest/9.13.0/volume.yaml
+++ b/conf/rest/9.13.0/volume.yaml
@@ -2,29 +2,14 @@ name:                     Volume
 query:                    api/storage/volumes
 object:                   volume
 
-fields:
-  - space
-  - autosize
-  - files
-
 counters:
   - autosize.maximum => autosize_maximum_size
   - autosize.grow_threshold => autosize_grow_threshold_percent
   - ^^uuid => instance_uuid
   - ^name                       => volume
-  - ^node                       => node  #missing mapping
   - ^svm.name        => svm
   - ^aggregates.#.name  => aggr #zapi maps to field containing-aggregate-name while rest mapping is an array?
   - ^style             => style
-  - files.used => files_used
-  - files.maximum => files_maximum
-#  - compression-space-saved               => sis_compress_saved #missing mapping
-#  - deduplication-space-saved             => sis_dedup_saved #missing mapping
-#  - total-space-saved                     => sis_total_saved #missing mapping
-#  - percentage-compression-space-saved    => sis_compress_saved_percent #missing mapping
-#  - percentage-deduplication-space-saved  => sis_dedup_saved_percent #missing mapping
-#  - percentage-total-space-saved          => sis_total_saved_percent #missing mapping
-#  - ^is-sis-volume                        => is_sis_volume #missing mapping
   - space.expected_available => space_expected_available
   - space.filesystem_size                       => filesystem_size
   - space.logical_space.available  => space_logical_available  #not found in infinity cluster
@@ -47,18 +32,36 @@ counters:
   - space.snapshot.space_used_percent    => snapshot_reserve_used_percent
   - ^snapshot_policy.name  => snapshot_policy
   - ^state   => state
+  - hidden_fields:
+    - space
+    - autosize
 
+endpoints:
+  - query: api/private/cli/volume
+    counters:
+      - ^^instance_uuid => instance_uuid
+      - ^node  => node
+      - ^is_sis_volume => is_sis_volume
+      - snapshot_count => snapshot_count
+      - compression_space_saved_percent => sis_compress_saved_percent
+      - dedupe_space_saved_percent => sis_dedup_saved_percent
+      - compression_space_saved => sis_compress_saved
+      - dedupe_space_saved => sis_dedup_saved
+      - sis_space_saved => sis_total_saved
+      - sis_space_saved_percent => sis_total_saved_percent
+      - files => inode_files_total
+      - files_used => inode_files_used
 
 plugins:
-  LabelAgent:
-    value_to_num:
-      - new_status state online online `0`
-    exclude_equals:
-      - style `flexgroup_constituent`
-    replace:
-      - style style `flexgroup_constituent` `flexgroup`
-  Aggregator:
-    - volume<style=flexgroup>volume node,svm,aggr,style
+  - LabelAgent:
+      value_to_num:
+        - new_status state online online `0`
+      exclude_equals:
+        - style `flexgroup_constituent`
+      replace:
+        - style style `flexgroup_constituent` `flexgroup`
+  - Aggregator:
+      - volume<style=flexgroup>volume node,svm,aggr,style
 
 export_options:
   instance_keys:
@@ -69,7 +72,7 @@ export_options:
     - style
   instance_labels:
     - state
-    #- is-sis-volume
+    - is_sis_volume
     - snapshot_policy
 
 
diff --git a/conf/rest/default.yaml b/conf/rest/default.yaml
index fc13c5ce..d34d17ea 100644
--- a/conf/rest/default.yaml
+++ b/conf/rest/default.yaml
@@ -14,4 +14,5 @@ objects:
   SnapMirror:       snapmirror.yaml
   Volume:           volume.yaml
   Subsystem:        subsystem.yaml
+  Snapshot:         snapshot.yaml
 
diff --git a/conf/zapi/7mode/8.6.0/qtree.yaml b/conf/zapi/7mode/8.6.0/qtree.yaml
new file mode 100644
index 00000000..422c5f63
--- /dev/null
+++ b/conf/zapi/7mode/8.6.0/qtree.yaml
@@ -0,0 +1,41 @@
+
+name:                       Qtree
+query:                      qtree-list
+object:                     qtree
+
+global_labels:
+  - system_type: 7mode
+
+counters:
+  qtree-info:
+    - ^export-policy
+    - id
+    - ^oplocks
+    - ^security-style
+    - ^status
+    - ^^volume
+    - ^^owning-vfiler => svm
+    - ^^qtree         => qtree
+
+plugins:
+  Qtree:
+    objects:
+      - disk-limit
+      - disk-used
+      - file-limit
+      - files-used
+      - soft-disk-limit
+      - soft-file-limit
+      - threshold
+
+export_options:
+  instance_keys:
+    - qtree
+    - volume
+    - svm
+
+  instance_labels:
+    - export_policy
+    - oplocks
+    - security_style
+    - status
diff --git a/integration/Jenkinsfile b/integration/Jenkinsfile
index 2b817622..a81cf116 100644
--- a/integration/Jenkinsfile
+++ b/integration/Jenkinsfile
@@ -14,7 +14,11 @@ pipeline {
         string(name: 'RPM',     defaultValue: '', description: 'http url of rpm file')
         string(name: 'NATIVE',  defaultValue: '', description: 'http url of native tar file')
     }
-
+    environment {
+        BUILD_ID="dontKillMe"
+        JENKINS_NODE_COOKIE="dontKillMe"    
+    }
+    
     stages {
         stage('Setup') {
             agent {
@@ -201,4 +205,4 @@ def void printNodeInfo() {
         socket.connect(InetAddress.getByName("8.8.8.8"), 10002);
         nodeIp = socket.getLocalAddress().getHostAddress();
         println("The Node IP Address is: ${nodeIp}")
-}
\ No newline at end of file
+}
diff --git a/integration/go.mod b/integration/go.mod
index 6884fa3f..2cf772ea 100644
--- a/integration/go.mod
+++ b/integration/go.mod
@@ -17,7 +17,7 @@ require (
 require (
 	github.com/Microsoft/go-winio v0.4.17 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
-	github.com/containerd/containerd v1.5.8 // indirect
+	github.com/containerd/containerd v1.5.9 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/docker/distribution v2.7.1+incompatible // indirect
 	github.com/docker/go-connections v0.4.0 // indirect
@@ -43,7 +43,6 @@ require (
 	github.com/tklauser/numcpus v0.2.3 // indirect
 	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
 	golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 // indirect
-	golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
 	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
 	google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
 	google.golang.org/grpc v1.39.1 // indirect
diff --git a/integration/go.sum b/integration/go.sum
index 994d108e..3a7d5561 100644
--- a/integration/go.sum
+++ b/integration/go.sum
@@ -161,8 +161,8 @@ github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7
 github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
 github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
 github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
-github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw=
-github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
+github.com/containerd/containerd v1.5.9 h1:rs6Xg1gtIxaeyG+Smsb/0xaSDu1VgFhOCKBXxMxbsF4=
+github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@@ -948,7 +948,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s=
 golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
 golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/pkg/tree/tree.go b/pkg/tree/tree.go
index caf881b0..e5b92796 100644
--- a/pkg/tree/tree.go
+++ b/pkg/tree/tree.go
@@ -23,16 +23,16 @@ func ImportYaml(filepath string) (*node.Node, error) {
 		return nil, err
 	}
 	r := node.New([]byte("Root"))
-	consume(r, "", root.Content[0])
+	consume(r, "", root.Content[0], false)
 	return r, nil
 }
 
-func consume(r *node.Node, key string, y *y3.Node) {
+func consume(r *node.Node, key string, y *y3.Node, makeNewChild bool) {
 	if y.Kind == y3.ScalarNode {
 		r.NewChildS(key, y.Value)
 	} else if y.Kind == y3.MappingNode {
 		var s = r
-		if key != "" {
+		if key != "" || makeNewChild {
 			s = r.NewChildS(key, "")
 		}
 		for i := 0; i < len(y.Content); i += 2 {
@@ -42,12 +42,16 @@ func consume(r *node.Node, key string, y *y3.Node) {
 				s = r.NewChildS(k, "")
 				continue
 			}
-			consume(s, k, y.Content[i+1])
+			consume(s, k, y.Content[i+1], false)
 		}
 	} else { // sequence
 		s := r.NewChildS(key, "")
 		for _, child := range y.Content {
-			consume(s, "", child)
+			makeNewChild := false
+			if child.Tag == "!!map" {
+				makeNewChild = key == "endpoints"
+			}
+			consume(s, "", child, makeNewChild)
 		}
 	}
 }


Test patch:
diff --git a/pkg/tree/testdata/testTemplate.yaml b/pkg/tree/testdata/testTemplate.yaml
new file mode 100644
index 00000000..7bcbf8c9
--- /dev/null
+++ b/pkg/tree/testdata/testTemplate.yaml
@@ -0,0 +1,45 @@
+name:                     Volume
+query:                    api/storage/volumes
+object:                   volume
+
+counters:
+  - autosize.maximum => autosize_maximum_size
+  - autosize.grow_threshold => autosize_grow_threshold_percent
+  - ^^uuid => instance_uuid
+  - ^name                       => volume
+  - ^svm.name        => svm
+  - hidden_fields:
+      - space
+      - autosize
+
+endpoints:
+  - query: api/private/cli/volume
+    counters:
+      - ^^instance_uuid => instance_uuid
+      - ^node  => node
+  - query: api/private/cli/svm
+    counters:
+      - ^^instance_uuid => instance_uuid
+      - ^node  => node
+
+plugins:
+  - LabelAgent:
+      value_to_num:
+        - new_status state online online `0`
+      exclude_equals:
+        - style `flexgroup_constituent`
+      replace:
+        - style style `flexgroup_constituent` `flexgroup`
+  - Aggregator:
+      - volume<style=flexgroup>volume node,svm,aggr,style
+      - volume<style=flexgroup>volume node,svm,aggr,style
+
+export_options:
+  instance_keys:
+    - volume
+    - node
+  instance_labels:
+    - state
+
+
+
diff --git a/pkg/tree/testdata/testTemplate21.08.yaml b/pkg/tree/testdata/testTemplate21.08.yaml
new file mode 100644
index 00000000..238b7e70
--- /dev/null
+++ b/pkg/tree/testdata/testTemplate21.08.yaml
@@ -0,0 +1,14 @@
+name:                     Volume
+query:                    api/storage/volumes
+object:                   volume
+
+counters:
+  volume-attributes:
+    - volume-autosize-attributes:
+        - maximum-size
+        - grow-threshold-percent
+
+plugins:
+  - LabelAgent:
+    value_to_num: new_status state online online `0`
+    split: path `/` ,,,lun
\ No newline at end of file
diff --git a/pkg/tree/tree_test.go b/pkg/tree/tree_test.go
new file mode 100644
index 00000000..85b30156
--- /dev/null
+++ b/pkg/tree/tree_test.go
@@ -0,0 +1,247 @@
+package tree
+
+import (
+	"testing"
+)
+
+func TestImportYaml(t *testing.T) {
+	template, _ := ImportYaml("testdata/testTemplate.yaml")
+
+	// check key value pairs like
+	// name: Volume
+	want := 0
+	got := 0
+	if name := template.GetChildS("name"); name != nil {
+		for range name.GetChildren() {
+			got += 1
+		}
+		if name.GetContentS() == "" {
+			t.Errorf("empty content")
+		}
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	// check counters
+	want = 6
+	got = 0
+	if name := template.GetChildS("counters"); name != nil {
+		for range name.GetChildren() {
+			got += 1
+		}
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		hiddenFields := name.GetChildS("hidden_fields")
+		hiddenFieldsWant := 2
+		hiddenFieldsGot := 0
+		for range hiddenFields.GetChildren() {
+			hiddenFieldsGot += 1
+		}
+		if hiddenFieldsGot != hiddenFieldsWant {
+			t.Errorf("got %v, want %v", hiddenFieldsGot, hiddenFieldsWant)
+		}
+	}
+
+	// check endpoints
+	want = 2
+	got = 0
+	if endpoints := template.GetChildS("endpoints"); endpoints != nil {
+		for range endpoints.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	// check plugins
+	want = 2
+	got = 0
+	if plugins := template.GetChildS("plugins"); plugins != nil {
+		for range plugins.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		aggregator := plugins.GetChildS("Aggregator")
+		aggregatorWant := 2
+		aggregatorGot := 0
+		for range aggregator.GetChildren() {
+			aggregatorGot += 1
+		}
+		if aggregatorGot != aggregatorWant {
+			t.Errorf("got %v, want %v", aggregatorWant, aggregatorGot)
+		}
+	}
+
+	// export_options
+	want = 2
+	got = 0
+	if exportOptions := template.GetChildS("export_options"); exportOptions != nil {
+		for range exportOptions.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		instanceKeys := exportOptions.GetChildS("instance_keys")
+		instanceKeysWant := 2
+		instanceKeysGot := 0
+		for range instanceKeys.GetChildren() {
+			instanceKeysGot += 1
+		}
+		if instanceKeysGot != instanceKeysWant {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+}
+
+func TestHarvestConfigImportYaml(t *testing.T) {
+	template, _ := ImportYaml("../../cmd/tools/doctor/testdata/testConfig.yml")
+
+	want := 2
+	got := 0
+	if name := template.GetChildS("Tools"); name != nil {
+		for range name.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	want = 12
+	got = 0
+	if exporters := template.GetChildS("Exporters"); exporters != nil {
+		for range exporters.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		prometheus := exporters.GetChildS("prometheusrange")
+		prometheusWant := 2
+		prometheusGot := 0
+		for range prometheus.GetChildren() {
+			prometheusGot += 1
+		}
+		if prometheusGot != prometheusWant {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	want = 5
+	got = 0
+	if defaults := template.GetChildS("Defaults"); defaults != nil {
+		for range defaults.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		collectors := defaults.GetChildS("collectors")
+		collectorsWant := 2
+		collectorsGot := 0
+		for range collectors.GetChildren() {
+			collectorsGot += 1
+		}
+		if collectorsGot != collectorsWant {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	want = 12
+	got = 0
+	if pollers := template.GetChildS("Pollers"); pollers != nil {
+		for range pollers.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		unix := pollers.GetChildS("unix")
+		unixWant := 5
+		unixGot := 0
+		for range unix.GetChildren() {
+			unixGot += 1
+		}
+		if unixGot != unixWant {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+}
+
+func TestImport2108Yaml(t *testing.T) {
+	template, _ := ImportYaml("testdata/testTemplate21.08.yaml")
+	// check plugins 21.08 (old backward compatibility)
+	want := 1
+	got := 0
+	if plugins := template.GetChildS("plugins"); plugins != nil {
+		for range plugins.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		labelAgent := plugins.GetChildS("LabelAgent")
+		labelAgentWant := 2
+		labelAgentGot := 0
+		for range labelAgent.GetChildren() {
+			labelAgentGot += 1
+		}
+		if labelAgentGot != labelAgentWant {
+			t.Errorf("got %v, want %v", labelAgentWant, labelAgentGot)
+		}
+	}
+
+	// counters
+	want = 1
+	got = 0
+	if counters := template.GetChildS("counters"); counters != nil {
+		for range counters.GetChildren() {
+			got += 1
+		}
+
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+
+		volumeAttributes := counters.GetChildS("volume-attributes")
+		volumeAttributesWant := 1
+		volumeAttributesGot := 0
+		for range volumeAttributes.GetChildren() {
+			volumeAttributesGot += 1
+		}
+		if volumeAttributesGot != volumeAttributesWant {
+			t.Errorf("got %v, want %v", volumeAttributesWant, volumeAttributesGot)
+		}
+
+		volumeAutoSizeAttributes := volumeAttributes.GetChildS("volume-autosize-attributes")
+		volumeAutoSizeAttributesWant := 2
+		volumeAutoSizeAttributesGot := 0
+		for range volumeAutoSizeAttributes.GetChildren() {
+			volumeAutoSizeAttributesGot += 1
+		}
+		if volumeAutoSizeAttributesGot != volumeAutoSizeAttributesWant {
+			t.Errorf("got %v, want %v", volumeAutoSizeAttributesGot, volumeAttributesGot)
+		}
+	}
+}