Repository: NetApp/harvest
Pull request #513 (2021-09-22 15:54:24)

feat: autosupport shouldn't save bogus cluster sn


Patch:
diff --git a/cmd/collectors/zapi/collector/zapi.go b/cmd/collectors/zapi/collector/zapi.go
index 9b50345b..9152aa52 100644
--- a/cmd/collectors/zapi/collector/zapi.go
+++ b/cmd/collectors/zapi/collector/zapi.go
@@ -13,6 +13,7 @@ import (
 	"goharvest2/cmd/collectors/zapiperf/plugins/nic"
 	"goharvest2/cmd/collectors/zapiperf/plugins/volume"
 	"goharvest2/cmd/poller/plugin"
+	"sort"
 	"strconv"
 	"strings"
 	"time"
@@ -406,9 +407,9 @@ func (me *Zapi) PollData() (*matrix.Matrix, error) {
 	}
 
 	// update metadata
-	me.Metadata.LazySetValueInt64("api_time", "data", apiT.Microseconds())
-	me.Metadata.LazySetValueInt64("parse_time", "data", parseT.Microseconds())
-	me.Metadata.LazySetValueUint64("count", "data", count)
+	_ = me.Metadata.LazySetValueInt64("api_time", "data", apiT.Microseconds())
+	_ = me.Metadata.LazySetValueInt64("parse_time", "data", parseT.Microseconds())
+	_ = me.Metadata.LazySetValueUint64("count", "data", count)
 	me.AddCollectCount(count)
 
 	return me.Matrix, nil
@@ -460,7 +461,9 @@ func (me *Zapi) CollectAutoSupport(p *collector.Payload) {
 	if me.Name == "Zapi" && (me.Object == "Volume" || me.Object == "Node") {
 		p.Target.Version = me.GetHostVersion()
 		p.Target.Model = me.GetHostModel()
-		p.Target.Serial = me.GetHostUUID()
+		if p.Target.Serial == "" {
+			p.Target.Serial = me.GetHostUUID()
+		}
 		p.Target.ClusterUuid = me.Client.ClusterUuid()
 
 		md := me.GetMetadata()
@@ -474,35 +477,45 @@ func (me *Zapi) CollectAutoSupport(p *collector.Payload) {
 		}
 
 		if me.Object == "Node" {
-			nodes, err := me.getNodeUuids()
+			nodeIds, err := me.getNodeUuids()
 			if err != nil {
 				// log but don't return so the other info below is collected
 				me.Logger.Error().
 					Err(err).
 					Msg("Unable to get nodes.")
-				nodes = make([]string, 0)
+				nodeIds = make([]collector.Id, 0)
 			}
-			info.Uuids = &nodes
+			info.Ids = nodeIds
 			p.Nodes = &info
+			if me.Client.IsClustered() {
+				// Since the serial number is bogus in c-mode
+				// use the first node's serial number instead (the nodes were ordered in getNodeUuids())
+				if len(nodeIds) > 0 {
+					p.Target.Serial = nodeIds[0].SerialNumber
+				}
+			}
 		} else if me.Object == "Volume" {
 			p.Volumes = &info
 		}
 	}
 }
 
-func (me *Zapi) getNodeUuids() ([]string, error) {
+func (me *Zapi) getNodeUuids() ([]collector.Id, error) {
 	var (
 		response *node.Node
 		nodes    []*node.Node
 		err      error
-		uuids    []string
+		infos    []collector.Id
 	)
 
-	// 7-mode does not have this information
+	// Since 7-mode is like single node, return the ids for it
 	if !me.Client.IsClustered() {
-		return make([]string, 0), err
+		return []collector.Id{{
+			SerialNumber: me.Client.Serial(),
+			SystemId:     me.Client.ClusterUuid(),
+		}}, nil
 	}
-	request := "cluster-node-get-iter"
+	request := "system-node-get-iter"
 
 	if response, err = me.Client.InvokeRequestString(request); err != nil {
 		return nil, fmt.Errorf("failure invoking zapi: %s %w", request, err)
@@ -513,10 +526,16 @@ func (me *Zapi) getNodeUuids() ([]string, error) {
 	}
 
 	for _, n := range nodes {
-		uuid := n.GetChildContentS("node-uuid")
-		uuids = append(uuids, uuid)
-	}
-	return uuids, nil
+		sn := n.GetChildContentS("node-serial-number")
+		systemId := n.GetChildContentS("node-system-id")
+		infos = append(infos, collector.Id{SerialNumber: sn, SystemId: systemId})
+	}
+	// When Harvest monitors a c-mode system, the first node is picked.
+	// Sort so there's a higher chance the same node is picked each time this method is called
+	sort.SliceStable(infos, func(i, j int) bool {
+		return infos[i].SerialNumber < infos[j].SerialNumber
+	})
+	return infos, nil
 }
 
 // Interface guards
diff --git a/cmd/exporters/influxdb/influxdb.go b/cmd/exporters/influxdb/influxdb.go
index d66b19ed..26f2c915 100644
--- a/cmd/exporters/influxdb/influxdb.go
+++ b/cmd/exporters/influxdb/influxdb.go
@@ -1,6 +1,7 @@
 /*
  * Copyright NetApp Inc, 2021 All rights reserved
  */
+
 package influxdb
 
 import (
@@ -12,6 +13,7 @@ import (
 	"goharvest2/pkg/matrix"
 	"io/ioutil"
 	"net/http"
+	url2 "net/url"
 	"strconv"
 	"time"
 )
@@ -112,7 +114,8 @@ func (e *InfluxDB) Init() error {
 
 		urlToUSe := "http://" + *addr + ":" + strconv.Itoa(*port)
 		url = &urlToUSe
-		e.url = fmt.Sprintf("%s/api/v%s/write?org=%s&bucket=%s&precision=%s", *url, *version, *org, *bucket, *precision)
+		e.url = fmt.Sprintf("%s/api/v%s/write?org=%s&bucket=%s&precision=%s",
+			*url, *version, url2.PathEscape(*org), url2.PathEscape(*bucket), *precision)
 	} else {
 		e.url = *url
 	}
diff --git a/cmd/poller/collector/asup.go b/cmd/poller/collector/asup.go
index b2fe16ce..619c6d97 100644
--- a/cmd/poller/collector/asup.go
+++ b/cmd/poller/collector/asup.go
@@ -37,6 +37,11 @@ type TargetInfo struct {
 	ClusterUuid string
 }
 
+type Id struct {
+	SerialNumber string `json:"serial-number"`
+	SystemId     string `json:"system-id"`
+}
+
 type InstanceInfo struct {
 	Count      int64
 	DataPoints int64
@@ -44,7 +49,7 @@ type InstanceInfo struct {
 	ApiTime    int64
 	ParseTime  int64
 	PluginTime int64
-	Uuids      *[]string `json:"Uuids,omitempty"`
+	Ids        []Id `json:"Ids,omitempty"`
 }
 
 type platformInfo struct {
diff --git a/cmd/poller/collector/helpers.go b/cmd/poller/collector/helpers.go
index 74a51553..b8b7f4b2 100644
--- a/cmd/poller/collector/helpers.go
+++ b/cmd/poller/collector/helpers.go
@@ -55,64 +55,86 @@ func (c *AbstractCollector) ImportSubTemplate(model, filename string, ver [3]int
 	var (
 		selectedVersion, pathPrefix, subTemplateFp string
 		availableVersions                          []string
+		err                                        error
+		finalTemplate                              *node.Node
+		tempTemplate                               *node.Node
 	)
 
-	pathPrefix = path.Join(c.Options.HomePath, "conf/", strings.ToLower(c.Name), model)
-	c.Logger.Debug().Msgf("Looking for best-fitting template in [%s]", pathPrefix)
-	verWithDots := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(ver)), "."), "[]")
-
-	// check for available versions, those are the subdirectories that include filename
-	if files, err := ioutil.ReadDir(pathPrefix); err == nil {
-		for _, file := range files {
-			if match, _ := regexp.MatchString(`\d+\.\d+\.\d+`, file.Name()); match == true && file.IsDir() {
-				if templates, err := ioutil.ReadDir(path.Join(pathPrefix, file.Name())); err == nil {
-					for _, t := range templates {
-						if t.Name() == filename {
-							c.Logger.Trace().Msgf("available version dir: [%s]", file.Name())
-							availableVersions = append(availableVersions, file.Name())
-							break
+	//split filename by comma
+	// in case of custom.yaml having same key, file names will be concatenated by comma
+	filenames := strings.Split(filename, ",")
+
+	for _, f := range filenames {
+		pathPrefix = path.Join(c.Options.HomePath, "conf/", strings.ToLower(c.Name), model)
+		c.Logger.Debug().Msgf("Looking for best-fitting template in [%s]", pathPrefix)
+		verWithDots := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(ver)), "."), "[]")
+
+		// check for available versions, those are the subdirectories that include filename
+		if files, err := ioutil.ReadDir(pathPrefix); err == nil {
+			for _, file := range files {
+				if match, _ := regexp.MatchString(`\d+\.\d+\.\d+`, file.Name()); match == true && file.IsDir() {
+					if templates, err := ioutil.ReadDir(path.Join(pathPrefix, file.Name())); err == nil {
+						for _, t := range templates {
+							if t.Name() == f {
+								c.Logger.Trace().Msgf("available version dir: [%s]", file.Name())
+								availableVersions = append(availableVersions, file.Name())
+								break
+							}
 						}
 					}
 				}
 			}
+		} else {
+			return nil, err
 		}
-	} else {
-		return nil, err
-	}
-	c.Logger.Trace().Msgf("checking for %d available versions: %v", len(availableVersions), availableVersions)
+		c.Logger.Trace().Msgf("checking for %d available versions: %v", len(availableVersions), availableVersions)
+
+		if len(availableVersions) > 0 {
+			versions := make([]*version.Version, len(availableVersions))
+			for i, raw := range availableVersions {
+				v, err := version.NewVersion(raw)
+				if err != nil {
+					c.Logger.Trace().Msgf("error parsing version: %s err: %s", raw, err)
+					continue
+				}
+				versions[i] = v
+			}
 
-	if len(availableVersions) > 0 {
-		versions := make([]*version.Version, len(availableVersions))
-		for i, raw := range availableVersions {
-			v, err := version.NewVersion(raw)
+			sort.Sort(version.Collection(versions))
+
+			verS, err := version.NewVersion(verWithDots)
 			if err != nil {
-				c.Logger.Trace().Msgf("error parsing version: %s err: %s", raw, err)
-				continue
+				c.Logger.Trace().Msgf("error parsing ONTAP version: %s err: %s", verWithDots, err)
+				return nil, errors.New("No best-fitting subtemplate version found")
+			}
+			// get closest index
+			idx := getClosestIndex(versions, verS)
+			if idx >= 0 && idx < len(versions) {
+				selectedVersion = versions[idx].String()
 			}
-			versions[i] = v
 		}
 
-		sort.Sort(version.Collection(versions))
-
-		verS, err := version.NewVersion(verWithDots)
-		if err != nil {
-			c.Logger.Trace().Msgf("error parsing ONTAP version: %s err: %s", verWithDots, err)
-			return nil, errors.New("No best-fitting subtemplate version found")
+		if selectedVersion == "" {
+			return nil, errors.New("No best-fit template found")
 		}
-		// get closest index
-		idx := getClosestIndex(versions, verS)
-		if idx >= 0 && idx < len(versions) {
-			selectedVersion = versions[idx].String()
-		}
-	}
 
-	if selectedVersion == "" {
-		return nil, errors.New("No best-fit template found")
+		subTemplateFp = path.Join(pathPrefix, selectedVersion, f)
+		c.Logger.Info().Msgf("best-fit template [%s] for [%s]", subTemplateFp, verWithDots)
+		if finalTemplate == nil {
+			finalTemplate, err = tree.Import("yaml", subTemplateFp)
+			if err == nil {
+				finalTemplate.PreprocessTemplate()
+			}
+		} else {
+			tempTemplate, err = tree.Import("yaml", subTemplateFp)
+			if err == nil {
+				tempTemplate.PreprocessTemplate()
+				// merge templates
+				finalTemplate.Merge(tempTemplate, nil)
+			}
+		}
 	}
-
-	subTemplateFp = path.Join(pathPrefix, selectedVersion, filename)
-	c.Logger.Info().Msgf("best-fit template [%s] for [%s]", subTemplateFp, verWithDots)
-	return tree.Import("yaml", subTemplateFp)
+	return finalTemplate, err
 }
 
 //getClosestIndex returns the closest left match to the sorted list of input versions
diff --git a/cmd/poller/plugin/label_agent/parse_rules.go b/cmd/poller/plugin/label_agent/parse_rules.go
index b2b42e45..b67ce678 100644
--- a/cmd/poller/plugin/label_agent/parse_rules.go
+++ b/cmd/poller/plugin/label_agent/parse_rules.go
@@ -28,33 +28,38 @@ func (me *LabelAgent) parseRules() int {
 
 	for _, c := range me.Params.GetChildren() {
 		name := c.GetNameS()
-		rule := strings.TrimSpace(c.GetContentS())
-
-		switch name {
-		case "split":
-			me.parseSplitSimpleRule(rule)
-		case "split_regex":
-			me.parseSplitRegexRule(rule)
-		case "split_pairs":
-			me.parseSplitPairsRule(rule)
-		case "join":
-			me.parseJoinSimpleRule(rule)
-		case "replace":
-			me.parseReplaceSimpleRule(rule)
-		case "replace_regex":
-			me.parseReplaceRegexRule(rule)
-		case "exclude_equals":
-			me.parseExcludeEqualsRule(rule)
-		case "exclude_contains":
-			me.parseExcludeContainsRule(rule)
-		case "exclude_regex":
-			me.parseExcludeRegexRule(rule)
-		case "value_mapping":
-			me.parseValueMappingRule(rule)
-		case "value_to_num":
-			me.parseValueToNumRule(rule)
-		default:
-			me.Logger.Warn().Msgf("unknown rule (%s)", name)
+
+		rules := c.GetChildren()
+		// loop over all rules
+		for _, rc := range rules {
+			rule := strings.TrimSpace(rc.GetContentS())
+
+			switch name {
+			case "split":
+				me.parseSplitSimpleRule(rule)
+			case "split_regex":
+				me.parseSplitRegexRule(rule)
+			case "split_pairs":
+				me.parseSplitPairsRule(rule)
+			case "join":
+				me.parseJoinSimpleRule(rule)
+			case "replace":
+				me.parseReplaceSimpleRule(rule)
+			case "replace_regex":
+				me.parseReplaceRegexRule(rule)
+			case "exclude_equals":
+				me.parseExcludeEqualsRule(rule)
+			case "exclude_contains":
+				me.parseExcludeContainsRule(rule)
+			case "exclude_regex":
+				me.parseExcludeRegexRule(rule)
+			case "value_mapping":
+				me.parseValueMappingRule(rule)
+			case "value_to_num":
+				me.parseValueToNumRule(rule)
+			default:
+				me.Logger.Warn().Msgf("unknown rule (%s)", name)
+			}
 		}
 	}
 
diff --git a/cmd/poller/poller.go b/cmd/poller/poller.go
index 4057d2e1..e71ec294 100644
--- a/cmd/poller/poller.go
+++ b/cmd/poller/poller.go
@@ -556,7 +556,8 @@ func (p *Poller) loadCollector(c conf.Collector, object string) error {
 				logger.Debug().
 					Str("template", t).
 					Msg("Merged template.")
-				template.Merge(subTemplate)
+				// do not overwrite child of objects. They will be concatenated
+				template.Merge(subTemplate, []string{"objects"})
 			}
 		}
 	}
diff --git a/cmd/tools/doctor/doctor.go b/cmd/tools/doctor/doctor.go
index 718d8232..d39c3955 100644
--- a/cmd/tools/doctor/doctor.go
+++ b/cmd/tools/doctor/doctor.go
@@ -5,6 +5,8 @@ import (
 	"github.com/spf13/cobra"
 	"goharvest2/pkg/color"
 	"goharvest2/pkg/conf"
+	"goharvest2/pkg/tree"
+	harvestyaml "goharvest2/pkg/tree/yaml"
 	"gopkg.in/yaml.v3"
 	"io/ioutil"
 	"os"
@@ -14,6 +16,8 @@ import (
 type options struct {
 	ShouldPrintConfig bool
 	Color             string
+	BaseTemplate      string
+	MergeTemplate     string
 }
 
 var opts = &options{
@@ -33,6 +37,39 @@ var Cmd = &cobra.Command{
 	Run:   doDoctorCmd,
 }
 
+var mergeCmd = &cobra.Command{
+	Use:    "merge",
+	Hidden: true,
+	Short:  "merge templates",
+	Run:    doMergeCmd,
+}
+
+func doMergeCmd(cmd *cobra.Command, _ []string) {
+	doMerge(opts.BaseTemplate, opts.MergeTemplate)
+}
+
+func doMerge(path1 string, path2 string) {
+	template, err := tree.Import("yaml", path1)
+	if err != nil {
+		fmt.Printf("error reading template file [%s]. err=%+v\n", path1, err)
+		return
+	}
+	subTemplate, err := tree.Import("yaml", path2)
+	if err != nil {
+		fmt.Printf("error reading template file [%s] err=%+v\n", path2, err)
+		return
+	}
+	template.PreprocessTemplate()
+	subTemplate.PreprocessTemplate()
+	template.Merge(subTemplate, nil)
+	data, err := harvestyaml.Dump(template)
+	if err != nil {
+		fmt.Printf("error reading parsing template file [%s]  err=%+v\n", data, err)
+		return
+	}
+	fmt.Println(string(data))
+}
+
 func doDoctorCmd(cmd *cobra.Command, _ []string) {
 	var config = cmd.Root().PersistentFlags().Lookup("config")
 	doDoctor(config.Value.String())
@@ -232,6 +269,14 @@ func collectNodes(root *yaml.Node, nodes *[]*yaml.Node) {
 }
 
 func init() {
+	Cmd.AddCommand(mergeCmd)
+	mFlags := mergeCmd.PersistentFlags()
+
+	mFlags.StringVarP(&opts.BaseTemplate, "template", "", "", "Base template path ")
+	mFlags.StringVarP(&opts.MergeTemplate, "with", "", "", "Extended file path. ")
+
+	_ = mergeCmd.MarkPersistentFlagRequired("template")
+	_ = mergeCmd.MarkPersistentFlagRequired("with")
 	Cmd.Flags().BoolVarP(
 		&opts.ShouldPrintConfig,
 		"print",
diff --git a/conf/zapi/7mode/8.6.0/aggr.yaml b/conf/zapi/7mode/8.6.0/aggr.yaml
index c8a494a2..20f7d95d 100644
--- a/conf/zapi/7mode/8.6.0/aggr.yaml
+++ b/conf/zapi/7mode/8.6.0/aggr.yaml
@@ -51,9 +51,11 @@ counters:
 
 plugins:
   LabelAgent:
-    value_mapping: status state online `1`
+    value_mapping:
+      - status state online `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online online `0`
+    value_to_num:
+      - new_status state online online `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/7mode/8.6.0/disk.yaml b/conf/zapi/7mode/8.6.0/disk.yaml
index 89429866..72d124a2 100644
--- a/conf/zapi/7mode/8.6.0/disk.yaml
+++ b/conf/zapi/7mode/8.6.0/disk.yaml
@@ -22,9 +22,11 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status offline false `0`
+    value_mapping:
+      - status offline false `0`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status offline - - `0` #ok_value is empty value, '-' would be converted to blank while processing.
+    value_to_num:
+      - new_status offline - - `0` #ok_value is empty value, '-' would be converted to blank while processing.
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/7mode/8.6.0/node.yaml b/conf/zapi/7mode/8.6.0/node.yaml
index ba1d409a..6edd9759 100644
--- a/conf/zapi/7mode/8.6.0/node.yaml
+++ b/conf/zapi/7mode/8.6.0/node.yaml
@@ -21,9 +21,11 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status healthy true `1`
+    value_mapping:
+      - status healthy true `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status healthy true up `0`
+    value_to_num:
+      - new_status healthy true up `0`
 
 export_options:
   require_instance_keys: false
diff --git a/conf/zapi/7mode/8.6.0/shelf.yaml b/conf/zapi/7mode/8.6.0/shelf.yaml
index b402ff79..05484cc0 100644
--- a/conf/zapi/7mode/8.6.0/shelf.yaml
+++ b/conf/zapi/7mode/8.6.0/shelf.yaml
@@ -78,9 +78,11 @@ plugins:
 #        - ^power-supply-is-error
 #        - ^power-supply-is-not-installed
   LabelAgent:
-    value_mapping: status state normal `1`
+    value_mapping:
+      - status state normal `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state normal ok `0`
+    value_to_num:
+      - new_status state normal ok `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/7mode/8.6.0/status.yaml b/conf/zapi/7mode/8.6.0/status.yaml
index 770b855a..55805f5a 100644
--- a/conf/zapi/7mode/8.6.0/status.yaml
+++ b/conf/zapi/7mode/8.6.0/status.yaml
@@ -13,9 +13,11 @@ no_max_records: true
 
 plugins:
   - LabelAgent:
-    value_mapping: status status ok `1`
+    value_mapping:
+      - status status ok `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status status ok ok `0`
+    value_to_num:
+      - new_status status ok ok `0`
 
 export_options:
   include_all_labels: true
diff --git a/conf/zapi/7mode/8.6.0/status_7.yaml b/conf/zapi/7mode/8.6.0/status_7.yaml
index f7349ef2..93eb6f24 100644
--- a/conf/zapi/7mode/8.6.0/status_7.yaml
+++ b/conf/zapi/7mode/8.6.0/status_7.yaml
@@ -15,7 +15,8 @@ collect_only_labels: true
 plugins:
   - LabelAgent:
     # metric label zapi_value rest_value `default_value`
-    value_to_num: 7mode_status health ok todo `0`
+    value_to_num:
+      - 7mode_status health ok todo `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/7mode/8.6.0/subsystem.yaml b/conf/zapi/7mode/8.6.0/subsystem.yaml
index 665cb34a..3c361d98 100644
--- a/conf/zapi/7mode/8.6.0/subsystem.yaml
+++ b/conf/zapi/7mode/8.6.0/subsystem.yaml
@@ -13,9 +13,11 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status health ok `1`
+    value_mapping:
+      - status health ok `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status health ok todo `0`  # REST gap will be filled up later.
+    value_to_num:
+      - new_status health ok todo `0`  # REST gap will be filled up later.
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/7mode/8.6.0/volume.yaml b/conf/zapi/7mode/8.6.0/volume.yaml
index 6046a2ac..64868b0d 100644
--- a/conf/zapi/7mode/8.6.0/volume.yaml
+++ b/conf/zapi/7mode/8.6.0/volume.yaml
@@ -29,9 +29,11 @@ counters:
 
 plugins:
   LabelAgent:
-    value_mapping: status state online `1`
+    value_mapping:
+      - status state online `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online online `0`
+    value_to_num:
+      - new_status state online online `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/aggr.yaml b/conf/zapi/cdot/9.8.0/aggr.yaml
index 3caace6c..e30725af 100644
--- a/conf/zapi/cdot/9.8.0/aggr.yaml
+++ b/conf/zapi/cdot/9.8.0/aggr.yaml
@@ -60,9 +60,11 @@ counters:
 
 plugins:
   LabelAgent:
-    value_mapping: status state online `1`
+    value_mapping:
+      - status state online `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online online `0`
+    value_to_num:
+      - new_status state online online `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/disk.yaml b/conf/zapi/cdot/9.8.0/disk.yaml
index 30f2f28e..fe413a6d 100644
--- a/conf/zapi/cdot/9.8.0/disk.yaml
+++ b/conf/zapi/cdot/9.8.0/disk.yaml
@@ -37,9 +37,11 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status outage , `0` # ok_value is empty value
+    value_mapping:
+      - status outage , `0` # ok_value is empty value
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status outage - - `0` #ok_value is empty value, '-' would be converted to blank while processing.
+    value_to_num:
+      - new_status outage - - `0` #ok_value is empty value, '-' would be converted to blank while processing.
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/lun.yaml b/conf/zapi/cdot/9.8.0/lun.yaml
index d9631ce0..732962d3 100644
--- a/conf/zapi/cdot/9.8.0/lun.yaml
+++ b/conf/zapi/cdot/9.8.0/lun.yaml
@@ -17,12 +17,15 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status state online `1`
+    value_mapping:
+      - status state online `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online online `0`
+    value_to_num:
+      - new_status state online online `0`
     # path is something like "/vol/vol_georg_fcp401/lun401"
     # we only want lun name, which is 4th element
-    split: path `/` ,,,lun
+    split:
+      - path `/` ,,,lun
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/node.yaml b/conf/zapi/cdot/9.8.0/node.yaml
index 1b13f9b0..77d99506 100644
--- a/conf/zapi/cdot/9.8.0/node.yaml
+++ b/conf/zapi/cdot/9.8.0/node.yaml
@@ -41,10 +41,13 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status healthy true `1`
+    value_mapping:
+      - status healthy true `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status healthy true up `0`
-    join: warnings `; ` failed_fan_message,failed_power_message,over_temperature
+    value_to_num:
+      - new_status healthy true up `0`
+    join:
+      - warnings `; ` failed_fan_message,failed_power_message,over_temperature
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/shelf.yaml b/conf/zapi/cdot/9.8.0/shelf.yaml
index 8e95d32b..7e56f5b2 100644
--- a/conf/zapi/cdot/9.8.0/shelf.yaml
+++ b/conf/zapi/cdot/9.8.0/shelf.yaml
@@ -63,9 +63,11 @@ plugins:
           - voltage-sensor-reading => reading
 
   LabelAgent:
-    value_mapping: status op_status normal `1`
+    value_mapping:
+      - status op_status normal `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online ok `0`
+    value_to_num:
+      - new_status state online ok `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/status.yaml b/conf/zapi/cdot/9.8.0/status.yaml
index 82a95334..5b9eb232 100644
--- a/conf/zapi/cdot/9.8.0/status.yaml
+++ b/conf/zapi/cdot/9.8.0/status.yaml
@@ -12,9 +12,11 @@ no_max_records: true
 
 plugins:
   - LabelAgent:
-    value_mapping: status status ok `1`
+    value_mapping:
+      - status status ok `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status status ok ok `0`
+    value_to_num:
+      - new_status status ok ok `0`
 
 export_options:
   include_all_labels: true
diff --git a/conf/zapi/cdot/9.8.0/subsystem.yaml b/conf/zapi/cdot/9.8.0/subsystem.yaml
index e94a3bef..9f0f65b1 100644
--- a/conf/zapi/cdot/9.8.0/subsystem.yaml
+++ b/conf/zapi/cdot/9.8.0/subsystem.yaml
@@ -12,9 +12,11 @@ counters:
 
 plugins:
   - LabelAgent:
-    value_mapping: status health ok `1`
+    value_mapping:
+      - status health ok `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status health ok todo `0`  # REST gap will be filled up later.
+    value_to_num:
+      - new_status health ok todo `0`  # REST gap will be filled up later.
 
 export_options:
   instance_keys:
diff --git a/conf/zapi/cdot/9.8.0/volume.yaml b/conf/zapi/cdot/9.8.0/volume.yaml
index eb8e5692..840b352c 100644
--- a/conf/zapi/cdot/9.8.0/volume.yaml
+++ b/conf/zapi/cdot/9.8.0/volume.yaml
@@ -56,11 +56,15 @@ counters:
 
 plugins:
   LabelAgent:
-    value_mapping: status state online `1`
+    value_mapping:
+      - status state online `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state online online `0`
-    exclude_equals: style `flexgroup_constituent`
-    replace: style style `flexgroup_constituent` `flexgroup`
+    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
 
diff --git a/conf/zapiperf/7mode/8.2.5/disk.yaml b/conf/zapiperf/7mode/8.2.5/disk.yaml
index 636379a7..bc0f0918 100644
--- a/conf/zapiperf/7mode/8.2.5/disk.yaml
+++ b/conf/zapiperf/7mode/8.2.5/disk.yaml
@@ -28,7 +28,8 @@ counters:
 
 plugins:
   LabelAgent:
-    split: raid_group `/` ,aggr,plex,raid
+    split:
+      - raid_group `/` ,aggr,plex,raid
   Aggregator:
     - aggr
 
diff --git a/conf/zapiperf/7mode/8.2.5/lun.yaml b/conf/zapiperf/7mode/8.2.5/lun.yaml
index 3b926a8c..7eb30a8f 100644
--- a/conf/zapiperf/7mode/8.2.5/lun.yaml
+++ b/conf/zapiperf/7mode/8.2.5/lun.yaml
@@ -20,7 +20,8 @@ counters:
 
 plugins:
   - LabelAgent:
-    split: lun `/` ,,volume,lun
+    split:
+      - lun `/` ,,volume,lun
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/7mode/8.2.5/nic_common.yaml b/conf/zapiperf/7mode/8.2.5/nic_common.yaml
index 9bc43a7a..0207637f 100644
--- a/conf/zapiperf/7mode/8.2.5/nic_common.yaml
+++ b/conf/zapiperf/7mode/8.2.5/nic_common.yaml
@@ -30,9 +30,11 @@ override:
 plugins:
   - Nic
   - LabelAgent:
-    value_mapping: status state up `1`
+    value_mapping:
+      - status state up `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state up up `0`
+    value_to_num:
+      - new_status state up up `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/7mode/8.2.5/path.yaml b/conf/zapiperf/7mode/8.2.5/path.yaml
index 0983ca87..8491f519 100644
--- a/conf/zapiperf/7mode/8.2.5/path.yaml
+++ b/conf/zapiperf/7mode/8.2.5/path.yaml
@@ -20,7 +20,8 @@ counters:
 
 plugins:
   LabelAgent:
-    split: path `_` hostadapter,target_wwpn
+    split:
+      - path `_` hostadapter,target_wwpn
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/cdot/9.8.0/disk.yaml b/conf/zapiperf/cdot/9.8.0/disk.yaml
index 1f5c5188..38021b8f 100644
--- a/conf/zapiperf/cdot/9.8.0/disk.yaml
+++ b/conf/zapiperf/cdot/9.8.0/disk.yaml
@@ -30,7 +30,8 @@ counters:
 
 plugins:
   LabelAgent:
-    split: raid_group `/` ,aggr,plex,raid
+    split:
+      - raid_group `/` ,aggr,plex,raid
   Aggregator:
   # plugin will create summary/average for each object
   # any names after the object names will be threated as
diff --git a/conf/zapiperf/cdot/9.8.0/lun.yaml b/conf/zapiperf/cdot/9.8.0/lun.yaml
index 23c38726..e181141a 100644
--- a/conf/zapiperf/cdot/9.8.0/lun.yaml
+++ b/conf/zapiperf/cdot/9.8.0/lun.yaml
@@ -38,7 +38,8 @@ override:
 
 plugins:
   LabelAgent:
-    split: lun `/` ,,volume,lun
+    split:
+      - lun `/` ,,volume,lun
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/cdot/9.8.0/nic_common.yaml b/conf/zapiperf/cdot/9.8.0/nic_common.yaml
index 43162eb6..d11ba6ec 100644
--- a/conf/zapiperf/cdot/9.8.0/nic_common.yaml
+++ b/conf/zapiperf/cdot/9.8.0/nic_common.yaml
@@ -30,9 +30,11 @@ override:
 plugins:
   - Nic
   - LabelAgent:
-    value_mapping: status state up `1`
+    value_mapping:
+      - status state up `1`
     # metric label zapi_value rest_value `default_value`
-    value_to_num: new_status state up up `0`
+    value_to_num:
+      - new_status state up up `0`
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/cdot/9.8.0/path.yaml b/conf/zapiperf/cdot/9.8.0/path.yaml
index a624b152..ec4f98d4 100644
--- a/conf/zapiperf/cdot/9.8.0/path.yaml
+++ b/conf/zapiperf/cdot/9.8.0/path.yaml
@@ -20,7 +20,8 @@ counters:
 
 plugins:
   LabelAgent:
-    split: path `_` hostadapter,target_wwpn
+    split:
+      - path `_` hostadapter,target_wwpn
 
 export_options:
   instance_keys:
diff --git a/conf/zapiperf/cdot/9.8.0/qtree.yaml b/conf/zapiperf/cdot/9.8.0/qtree.yaml
index 88370dab..acb96e9e 100644
--- a/conf/zapiperf/cdot/9.8.0/qtree.yaml
+++ b/conf/zapiperf/cdot/9.8.0/qtree.yaml
@@ -14,7 +14,8 @@ counters:
   - total_ops
 plugins:
   - LabelAgent:
-    split: qtreefull `/` ,qtree
+    split:
+      - qtreefull `/` ,qtree
 export_options:
   instance_keys:
     - qtree
diff --git a/jenkins/artifacts/jenkinsfile b/jenkins/artifacts/jenkinsfile
index 6adad4f4..d4bb10b4 100644
--- a/jenkins/artifacts/jenkinsfile
+++ b/jenkins/artifacts/jenkinsfile
@@ -36,6 +36,7 @@ pipeline {
         targetParentLocation = "/opt/home/nightly/"
         harvestPath = "harvest"
         imageName = "rahulguptajss/harvest"
+        COMMIT_ID = sh(returnStdout: true, script: 'git rev-parse HEAD')
     }
 
     stages {
@@ -58,6 +59,7 @@ pipeline {
                 wget -q -O go.tar.gz "https://dl.google.com/go/$(curl https://golang.org/VERSION?m=text).linux-amd64.tar.gz"
                 rm -rf /usr/local/go && tar -C /usr/local -xzf go.tar.gz
                 '''
+                updateStatus(env.COMMIT_ID,"pending",env.BUILD_URL, "Harvest CI is running", env.GIT_REPO_STATUS_TOKEN)
             }
         }
 
@@ -152,10 +154,10 @@ pipeline {
           }
       }
 
-    stage('Run Tests') {
+      stage('Run Tests') {
         when {
             expression {
-                return env.RUN_TEST == 'true';
+                return params.RUN_TEST == 'true';
             }
         }
         steps {
@@ -166,7 +168,18 @@ pipeline {
                 build job: 'harvest2_0/smoke', parameters: [string(name: 'BRANCH', value: "${BRANCH}"), string(name: 'NATIVE', value: "${nativeBuild}"),  string(name: 'RPM', value: "${rpmBuild}"), string(name: 'DOCKER', value: "${dockerBuild}")]
             }
         }
-     }
+      }
+    }
+    post {
+        failure {
+            updateStatus(env.COMMIT_ID,"failure",env.BUILD_URL , "Harvest CI has completed", env.GIT_REPO_STATUS_TOKEN)
+        }
+        success {
+            updateStatus(env.COMMIT_ID,"success",env.BUILD_URL, "Harvest CI has completed", env.GIT_REPO_STATUS_TOKEN)
+        }
+        aborted {
+            updateStatus(env.COMMIT_ID,"error",env.BUILD_URL, "Harvest CI has been aborted", env.GIT_REPO_STATUS_TOKEN)
+        }
     }
 }
 
@@ -175,4 +188,24 @@ def getBranchName(gitBranchName, paramBranchName) {
         return gitBranchName
     }
     return paramBranchName
+}
+
+def updateStatus(def commitId, def statusMsg, def buildUrl, def description, def gitToken) {
+    def post = (HttpURLConnection) new URL("https://api.github.com/repos/NetApp/harvest/statuses/${commitId}").openConnection();
+    def message = '{ "state" :  "'+statusMsg+'", "target_url": "'+buildUrl+'", "description": "'+description+'", "context" : "Integration test result"  }'
+    post.requestMethod = 'POST'
+    post.setDoInput(true);
+    post.setDoOutput(true);
+    post.setRequestProperty("Accept", "application/vnd.github.v3+json")
+    post.setRequestProperty("Authorization", "token ${gitToken}")
+    post.getOutputStream().write(message.getBytes("UTF-8"));
+    println(new String(post.getOutputStream().toByteArray(), "UTF-8"));
+    def postRC = post.getResponseCode();
+    println(postRC);
+    if(postRC.equals(201)) {
+        println(post.getInputStream().getText());
+    }else {
+        throw new RuntimeException("Failed to update GitHub Check "+post.getInputStream().getText())
+    }
+
 }
\ No newline at end of file
diff --git a/pkg/api/ontapi/zapi/system.go b/pkg/api/ontapi/zapi/system.go
index eff335b2..9f81064a 100644
--- a/pkg/api/ontapi/zapi/system.go
+++ b/pkg/api/ontapi/zapi/system.go
@@ -123,7 +123,6 @@ func (c *Client) getSystem() error {
 		if attrs := response.GetChildS("attributes"); attrs != nil {
 			if info := attrs.GetChildS("cluster-identity-info"); info != nil {
 				s.name = info.GetChildContentS("cluster-name")
-				s.serial = info.GetChildContentS("cluster-serial-number")
 				s.clusterUuid = info.GetChildContentS("cluster-uuid")
 			}
 		}
diff --git a/pkg/tree/node/node.go b/pkg/tree/node/node.go
index 97dce9e9..39786fff 100644
--- a/pkg/tree/node/node.go
+++ b/pkg/tree/node/node.go
@@ -202,6 +202,16 @@ func (n *Node) GetChildContentS(name string) string {
 	return ""
 }
 
+// GetChildByContent Compare child content
+func (n *Node) GetChildByContent(content string) *Node {
+	for _, child := range n.Children {
+		if child.GetContentS() == content {
+			return child
+		}
+	}
+	return nil
+}
+
 func (n *Node) SetChildContentS(name, content string) {
 	if child := n.GetChildS(name); child != nil {
 		child.SetContentS(content)
@@ -259,19 +269,64 @@ func (n *Node) Union(source *Node) {
 	}
 }
 
-func (me *Node) Merge(source *Node) {
+//fetchRoot return if a parent name ancestor exists
+func (n *Node) searchAncestor(ancestor string) *Node {
+	if n == nil {
+		return nil
+	}
+	p := n.GetParent()
+	if p == nil {
+		return nil
+	}
+	if p != nil && p.GetNameS() == ancestor {
+		return n
+	}
+	return p.searchAncestor(ancestor)
+}
+
+func (me *Node) PreprocessTemplate() {
+	for _, child := range me.Children {
+		mine := me.GetChild(child.GetName())
+		if mine != nil && len(child.GetName()) > 0 {
+			if mine.searchAncestor("LabelAgent") != nil {
+				if len(mine.GetContentS()) > 0 {
+					mine.NewChildS("", child.GetContentS())
+					mine.SetContentS("")
+				}
+			}
+			mine.PreprocessTemplate()
+		}
+	}
+}
 
+//Merge method will merge the subtemplate into the receiver, modifying the receiver in-place.
+//skipOverwrite is a readonly list of keys that will not be overwritten in the receiver.
+func (me *Node) Merge(subtemplate *Node, skipOverwrite []string) {
+	if subtemplate == nil {
+		return
+	}
 	if len(me.Content) == 0 {
-		me.Content = source.Content
+		me.Content = subtemplate.Content
 	}
-
-	for _, child := range source.Children {
-		if mine := me.GetChild(child.GetName()); mine == nil {
+	for _, child := range subtemplate.Children {
+		mine := me.GetChild(child.GetName())
+		if len(child.GetName()) == 0 {
+			if mine != nil && mine.GetParent() != nil && mine.GetParent().GetChildByContent(child.GetContentS()) == nil {
+				mine.GetParent().AddChild(child)
+			} else {
+				if me.GetChildByContent(child.GetContentS()) == nil {
+					me.AddChild(child)
+				}
+			}
+		} else if mine == nil {
 			me.AddChild(child)
 		} else {
-			// set content
-			mine.SetContentS(child.GetContentS())
-			mine.Merge(child)
+			if mine.GetParent() != nil && util.Contains(skipOverwrite, mine.GetParent().GetNameS()) {
+				mine.SetContentS(mine.GetContentS() + "," + child.GetContentS())
+			} else {
+				mine.SetContentS(child.GetContentS())
+			}
+			mine.Merge(child, skipOverwrite)
 		}
 	}
 }
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 209c77f9..087eaf2a 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -179,3 +179,12 @@ func ContainsWholeWord(source string, search string) bool {
 	}
 	return false
 }
+
+func Contains(s []string, e string) bool {
+	for _, a := range s {
+		if a == e {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/cmd/exporters/influxdb/influxdb_test.go b/cmd/exporters/influxdb/influxdb_test.go
index eef2091e..2fb739a3 100644
--- a/cmd/exporters/influxdb/influxdb_test.go
+++ b/cmd/exporters/influxdb/influxdb_test.go
@@ -111,6 +111,18 @@ func TestExportDebug(t *testing.T) {
 	}
 }
 
+// test that whitespace is escaped in the  parameters
+// are handled properly to construct server URL
+func TestWhiteSpaceInParameter(t *testing.T) {
+	expectedURL := "http://localhost:8086/api/v2/write?org=harvest%202&bucket=harvest%20%2009&precision=s"
+	exporterName := "influx-test-space"
+	influx := setupInfluxDB(exporterName, t)
+
+	if influx.url != expectedURL {
+		t.Fatalf("FAIL - expected [%s]\n                             got [%s]", expectedURL, influx.url)
+	}
+}
+
 /* Uncomment to test against a running InfluxDB instance
    ! Edit the params values below
    ! Uncomment import "goharvest2/share/tree/node"
diff --git a/cmd/poller/plugin/label_agent/label_agent_test.go b/cmd/poller/plugin/label_agent/label_agent_test.go
index 0618a613..a5a74afe 100644
--- a/cmd/poller/plugin/label_agent/label_agent_test.go
+++ b/cmd/poller/plugin/label_agent/label_agent_test.go
@@ -22,35 +22,35 @@ func TestInitPlugin(t *testing.T) {
 	// define plugin rules
 	params := node.NewS("LabelAgent")
 	// split value of "X" into 4 and take last 2 as new labels
-	params.NewChildS("split", "X `/` ,,C,D")
+	params.NewChildS("split", "").AddChild(params.NewChildS("", "X `/` ,,C,D"))
 	// split value of "X" and take first 2 as new labels
-	params.NewChildS("split_regex", "X `.*(A\\d+)_(B\\d+)` A,B")
+	params.NewChildS("split_regex", "").AddChild(params.NewChildS("", "X `.*(A\\d+)_(B\\d+)` A,B"))
 	// split value of "X" into key-value pairs
-	params.NewChildS("split_pairs", "X ` ` `:`")
+	params.NewChildS("split_pairs", "").AddChild(params.NewChildS("", "X ` ` `:`"))
 	// join values of "A" and "B" and set as label "X"
-	params.NewChildS("join", "X `_` A,B")
+	params.NewChildS("join", "").AddChild(params.NewChildS("", "X `_` A,B"))
 	// replace "aaa_" with "bbb_" and set as label "B"
-	params.NewChildS("replace", "A B `aaa_` `bbb_`")
+	params.NewChildS("replace", "").AddChild(params.NewChildS("", "A B `aaa_` `bbb_`"))
 	// remove occurences of "aaa_" from value of "A"
-	params.NewChildS("replace", "A A `aaa_` ``")
+	params.NewChildS("replace", "").AddChild(params.NewChildS("", "A A `aaa_` ``"))
 	// reverse the order of matching elements, replace underscore with dash and "aaa" with "bbb"
-	params.NewChildS("replace_regex", "A B `^(aaa)_(\\d+)_(\\w+)$` `$3-$2-bbb`")
+	params.NewChildS("replace_regex", "").AddChild(params.NewChildS("", "A B `^(aaa)_(\\d+)_(\\w+)$` `$3-$2-bbb`"))
 	// exclude instance if label "A" has value "aaa bbb ccc"
-	params.NewChildS("exclude_equals", "A `aaa bbb ccc`")
+	params.NewChildS("exclude_equals", "").AddChild(params.NewChildS("", "A `aaa bbb ccc`"))
 	// exclude instance if label "A" contains "_aaa_"
-	params.NewChildS("exclude_contains", "A `_aaa_`")
+	params.NewChildS("exclude_contains", "").AddChild(params.NewChildS("", "A `_aaa_`"))
 	// exclude instance of label value has prefix "aaa_" followed by at least one digit
-	params.NewChildS("exclude_regex", "A `^aaa_\\d+$`")
+	params.NewChildS("exclude_regex", "").AddChild(params.NewChildS("", "A `^aaa_\\d+$`"))
 	// create metric "status", if label "state" is one of the 3, map metric value to respective index
-	params.NewChildS("value_mapping", "status state up,sleeping,down")
+	params.NewChildS("value_mapping", "").AddChild(params.NewChildS("", "status state up,sleeping,down"))
 	// similar to above, but if none of the values is matching, use default value "4"
-	params.NewChildS("value_mapping", "stage stage init `1`")
+	params.NewChildS("value_mapping", "").AddChild(params.NewChildS("", "stage stage init `1`"))
 	// create metric "new_status", if label "state" is one of the up/ok[zapi/rest], map metric value to respective index
-	params.NewChildS("value_to_num", "new_status state up ok")
+	params.NewChildS("value_to_num", "").AddChild(params.NewChildS("", "new_status state up ok"))
 	// create metric "new_stage", but if none of the values is matching, use default value "4"
-	params.NewChildS("value_to_num", "new_stage stage init start `4`")
+	params.NewChildS("value_to_num", "").AddChild(params.NewChildS("", "new_stage stage init start `4`"))
 	// create metric "new_outage", if empty value is expected and non empty means wrong, use default value "0"
-	params.NewChildS("value_to_num", "new_outage outage - - `0`")
+	params.NewChildS("value_to_num", "").AddChild(params.NewChildS("", "new_outage outage - - `0`"))
 
 	abc := plugin.New("Test", nil, params, nil)
 	p = &LabelAgent{AbstractPlugin: abc}
diff --git a/cmd/tools/doctor/testdata/testConfig.yml b/cmd/tools/doctor/testdata/testConfig.yml
index a8062c26..5beb35b6 100644
--- a/cmd/tools/doctor/testdata/testConfig.yml
+++ b/cmd/tools/doctor/testdata/testConfig.yml
@@ -51,6 +51,13 @@ Exporters:
     version: 4
     port: 8088
     token: abcdefghijklmnopqrstuvwxyz
+  influx-test-space:
+    exporter: InfluxDB
+    addr: localhost
+    port: 8086
+    org: harvest 2
+    bucket: harvest  09
+    token: REDACTED
 Defaults:
   collectors:
     - Zapi
diff --git a/integration/test/dashboard_test.go b/integration/test/dashboard_test.go
index 8cc2c522..c0297a97 100644
--- a/integration/test/dashboard_test.go
+++ b/integration/test/dashboard_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/stretchr/testify/suite"
 	"log"
 	"testing"
+	"time"
 )
 
 type DashboardImportTestSuite struct {
@@ -31,6 +32,7 @@ type Dashboard struct {
 	Title       string `json:"title"`
 	FolderTitle string `json:"folderTitle"`
 	FolderUrl   string `json:"folderUrl"`
+	FolderId    int64  `json:"folderId"`
 }
 
 func (suite *DashboardImportTestSuite) SetupSuite() {
@@ -45,6 +47,7 @@ func (suite *DashboardImportTestSuite) SetupSuite() {
 	if !status {
 		assert.Fail(suite.T(), "Grafana import operation is failed")
 	}
+	time.Sleep(30 * time.Second)
 }
 
 func (suite *DashboardImportTestSuite) TestImport() {
@@ -63,46 +66,81 @@ func (suite *DashboardImportTestSuite) TestImport() {
 	assert.Fail(suite.T(), "Unable to find harvest folder")
 }
 
-func (suite *DashboardImportTestSuite) TestDashboardCount() {
-	log.Println("Find harvest folder id")
-	data, err := utils.GetResponseBody(utils.GetGrafanaHttpUrl() + "/api/folders?limit=10")
+func (suite *DashboardImportTestSuite) TestCModeDashboardCount() {
+	folderId := GetFolderId("Harvest 2.0", suite.T())
+	expectedName := []string{"Harvest Metadata", "NetApp Detail: Aggregate", "NetApp Detail: Cluster",
+		"NetApp Detail: Disk", "NetApp Detail: LUN", "NetApp Detail: Network", "NetApp Detail: Network  - Details",
+		"NetApp Detail: Network with NVMe/FC", "NetApp Detail: Node", "NetApp Detail: Node - Details",
+		"NetApp Detail: Shelf", "NetApp Detail: SnapMirror", "NetApp Detail: SVM", "NetApp Detail: SVM - Details",
+		"NetApp Detail: Volume", "NetApp Detail: Volume - Details"}
+
+	VerifyDashboards(folderId, expectedName, suite.T())
+}
+
+func (suite *DashboardImportTestSuite) TestSevenModeDashboardCount() {
+
+	folderId := GetFolderId("7 mode", suite.T())
+	expectedName := []string{"Harvest Metadata 7 mode", "NetApp Detail: Aggregate 7 mode", "NetApp Detail: Cluster 7 mode",
+		"NetApp Detail: Disk 7 mode", "NetApp Detail: LUN 7 mode", "NetApp Detail: Network 7 mode", "NetApp Detail: Network with NVMe/FC 7 mode",
+		"NetApp Detail: Node 7 mode", "NetApp Detail: Shelf 7 mode", "NetApp Detail: Volume 7 mode"}
+	VerifyDashboards(folderId, expectedName, suite.T())
+}
+
+func (suite *DashboardImportTestSuite) TestImportForInvalidJson() {
+	jsonDir := "grafana/dashboard_jsons"
+	if docker.IsDockerBasedPoller() {
+		containerId := docker.GetOnePollerContainers()
+		docker.CopyFile(containerId, "dashboard_jsons", "/opt/harvest/"+jsonDir)
+	} else {
+		utils.Run("cp", "-R", "dashboard_jsons", installer.HarvestHome+"/grafana")
+	}
+	status, _ := new(grafana.GrafanaMgr).Import(jsonDir)
+	if status {
+		assert.Fail(suite.T(), "Grafana import should fail but it is succeeded")
+	}
+}
+
+// In order for 'go test' to run this suite, we need to create
+// a normal test function and pass our suite to suite.Run
+func TestDashboardImportSuite(t *testing.T) {
+	suite.Run(t, new(DashboardImportTestSuite))
+}
+
+func GetFolderId(folderName string, t *testing.T) int64 {
+	log.Println("Find " + folderName + " folder id")
+	data, err := utils.GetResponseBody(utils.GetGrafanaHttpUrl() + "/api/folders?limit=100")
 	utils.PanicIfNotNil(err)
 	var dataFolder []Folder
 	var folderId int64
 	err = json.Unmarshal(data, &dataFolder)
 	utils.PanicIfNotNil(err)
 	for _, values := range dataFolder {
-		if values.Title == "Harvest 2.0" {
+		if values.Title == folderName {
 			folderId = values.Id
 			break
 		}
 	}
 	if !(folderId > 0) {
-		assert.Fail(suite.T(), "Folder id is empty or zero.")
+		assert.Fail(t, "Folder id is empty or zero.")
 	}
-	expectedName := []string{"Harvest Metadata", "NetApp Detail: Aggregate", "NetApp Detail: Cluster",
-		"NetApp Detail: Disk", "NetApp Detail: LUN", "NetApp Detail: Network", "NetApp Detail: Network  - Details",
-		"NetApp Detail: Network with NVMe/FC", "NetApp Detail: Node", "NetApp Detail: Node - Details",
-		"NetApp Detail: Shelf", "NetApp Detail: SnapMirror", "NetApp Detail: SVM", "NetApp Detail: SVM - Details",
-		"NetApp Detail: Volume", "NetApp Detail: Volume - Details"}
+	return folderId
+}
 
+func VerifyDashboards(folderId int64, expectedName []string, t *testing.T) {
 	log.Println(fmt.Sprintf("Find list of dashboard for folder %d", folderId))
-	url := utils.GetGrafanaHttpUrl() + fmt.Sprintf("/api/search?folderIds=%d", folderId)
+	url := utils.GetGrafanaHttpUrl() + "/api/search?type=dash-db"
 	log.Println(url)
-	data, err = utils.GetResponseBody(url)
+	data, err := utils.GetResponseBody(url)
 	utils.PanicIfNotNil(err)
 	var dataDashboard []Dashboard
 	err = json.Unmarshal(data, &dataDashboard)
 	utils.PanicIfNotNil(err)
-	totalDashboardCount := len(expectedName)
-	assert.True(suite.T(), totalDashboardCount == len(dataDashboard), fmt.Sprintf("Expected dashboard %d but found %d dashboards",
-		totalDashboardCount, len(dataDashboard)))
 	var actualNames []string
 	var notFoundList []string
+	log.Println(folderId)
 	for _, values := range dataDashboard {
 		actualNames = append(actualNames, values.Title)
 	}
-
 	for _, title := range expectedName {
 		if !(utils.Contains(actualNames, title)) {
 			notFoundList = append(notFoundList, title)
@@ -110,26 +148,6 @@ func (suite *DashboardImportTestSuite) TestDashboardCount() {
 	}
 	if len(notFoundList) > 0 {
 		log.Println("The following dashboards were not imported successfully.")
-		assert.Fail(suite.T(), fmt.Sprintf("One or more dashboards %s were missing/ not imported", notFoundList))
-	}
-}
-
-func (suite *DashboardImportTestSuite) TestImportForInvalidJson() {
-	jsonDir := "grafana/dashboard_jsons"
-	if docker.IsDockerBasedPoller() {
-		containerId := docker.GetOnePollerContainers()
-		docker.CopyFile(containerId, "dashboard_jsons", "/opt/harvest/"+jsonDir)
-	} else {
-		utils.Run("cp", "-R", "dashboard_jsons", installer.HarvestHome+"/grafana")
+		assert.Fail(t, fmt.Sprintf("One or more dashboards %s were missing/ not imported", notFoundList))
 	}
-	status, _ := new(grafana.GrafanaMgr).Import(jsonDir)
-	if status {
-		assert.Fail(suite.T(), "Grafana import should fail but it is succeeded")
-	}
-}
-
-// In order for 'go test' to run this suite, we need to create
-// a normal test function and pass our suite to suite.Run
-func TestDashboardImportSuite(t *testing.T) {
-	suite.Run(t, new(DashboardImportTestSuite))
 }
diff --git a/pkg/test/mergeTemplates/21.08.0_lun_merge_21.08.0_extended.yaml b/pkg/test/mergeTemplates/21.08.0_lun_merge_21.08.0_extended.yaml
new file mode 100644
index 00000000..66dfd35b
--- /dev/null
+++ b/pkg/test/mergeTemplates/21.08.0_lun_merge_21.08.0_extended.yaml
@@ -0,0 +1,38 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+    new_mapping:
+      - xyz
+  - nic
+  Aggregator:
+    - node<>node_cpu1
+    - nodetest
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file
diff --git a/pkg/test/mergeTemplates/21.08.0_lun_merge_extended.yaml b/pkg/test/mergeTemplates/21.08.0_lun_merge_extended.yaml
new file mode 100644
index 00000000..214d7d74
--- /dev/null
+++ b/pkg/test/mergeTemplates/21.08.0_lun_merge_extended.yaml
@@ -0,0 +1,41 @@
+name: customLun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+    new_mapping:
+      - xyz
+  Aggregator:
+    - node<>node_cpu1
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+    - node1
+  instance_labels:
+    - state
+override:
+  KB_copied: delta
+  key2: test
+export_data: false
\ No newline at end of file
diff --git a/pkg/test/mergeTemplates/lun_merge.yaml b/pkg/test/mergeTemplates/lun_merge.yaml
new file mode 100644
index 00000000..8873aea9
--- /dev/null
+++ b/pkg/test/mergeTemplates/lun_merge.yaml
@@ -0,0 +1,42 @@
+name: customLun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+    new_mapping:
+      - xyz
+  Aggregator:
+    - aggr
+    - node<>node_cpu1
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+    - node1
+  instance_labels:
+    - state
+override:
+  KB_copied: delta
+  key2: test
+export_data: false
\ No newline at end of file
diff --git a/pkg/test/mergeTemplates/lun_merge_21.08.0_extended.yaml b/pkg/test/mergeTemplates/lun_merge_21.08.0_extended.yaml
new file mode 100644
index 00000000..f565bf9f
--- /dev/null
+++ b/pkg/test/mergeTemplates/lun_merge_21.08.0_extended.yaml
@@ -0,0 +1,39 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+    new_mapping:
+      - xyz
+  Aggregator:
+    - aggr
+    - node<>node_cpu1
+    - nodetest
+  - nic
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file
diff --git a/pkg/test/node_test.go b/pkg/test/node_test.go
index 4efdb6c2..eaddf31e 100644
--- a/pkg/test/node_test.go
+++ b/pkg/test/node_test.go
@@ -2,13 +2,17 @@ package test
 
 import (
 	"goharvest2/pkg/tree"
+	"goharvest2/pkg/tree/yaml"
+	"io/ioutil"
+	"strings"
 	"testing"
 )
 
+// Merge default.yaml and custom.yaml
 func TestNode_Merge(t *testing.T) {
-	defaultTemplate, _ := tree.Import("yaml", "testdata/default.yaml")
-	customTemplate, _ := tree.Import("yaml", "testdata/custom.yaml")
-	defaultTemplate.Merge(customTemplate)
+	defaultTemplate, _ := tree.Import("yaml", "testdata/default_collector.yaml")
+	customTemplate, _ := tree.Import("yaml", "testdata/extend_collector.yaml")
+	defaultTemplate.Merge(customTemplate, []string{"objects"})
 
 	// count number of objects post merge
 	want := 10
@@ -23,7 +27,7 @@ func TestNode_Merge(t *testing.T) {
 	}
 
 	// Compare overwritten values for object
-	want1 := "node2.yaml"
+	want1 := "node.yaml,node2.yaml"
 	got1 := defaultTemplate.GetChildS("objects").GetChildS("Node").GetContentS()
 
 	if got1 != want1 {
@@ -45,3 +49,297 @@ func TestNode_Merge(t *testing.T) {
 	}
 
 }
+
+// merge collector templates for 21.08.6+ versions
+// change is LabelAgent child will have list of rules instead of key-value pair
+func TestNode_MergeCollector(t *testing.T) {
+	defaultTemplate, _ := tree.Import("yaml", "testdata/lun.yaml")
+	customTemplate, _ := tree.Import("yaml", "testdata/extend_lun.yaml")
+	defaultTemplate.PreprocessTemplate()
+	customTemplate.PreprocessTemplate()
+	defaultTemplate.Merge(customTemplate, nil)
+
+	gotString1, _ := yaml.Dump(defaultTemplate)
+	gotString := strings.TrimSpace(string(gotString1))
+	expected, _ := ioutil.ReadFile("mergeTemplates/lun_merge.yaml")
+	expectedString := strings.TrimSpace(string(expected))
+
+	if gotString != expectedString {
+		t.Errorf("got %v, want %v", gotString, expectedString)
+	}
+
+	// object name overwrite
+	want := "customLun"
+	got := ""
+	if name := defaultTemplate.GetChildS("name"); name != nil {
+		got = name.GetContentS()
+		if got != want {
+			t.Errorf("got %v, want %v", got, want)
+		}
+	}
+
+	// Add new counter
+	want1 := 9
+	got1 := 0
+	counters := defaultTemplate.GetChildS("counters").GetChildS("lun-info")
+
+	if counters != nil {
+		for range counters.GetChildren() {
+			got1 += 1
+		}
+	}
+
+	if got1 != want1 {
+		t.Errorf("got %v, want %v", got1, want1)
+	}
+
+	// plugins labelagent add new child to existing plugin
+	want2 := 2
+	got2 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("value_mapping")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got2 += 1
+		}
+	}
+
+	if got2 != want2 {
+		t.Errorf("got %v, want %v", got2, want2)
+	}
+
+	// plugins labelagent add same child to existing plugin
+	want3 := 1
+	got3 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("value_to_num")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got3 += 1
+		}
+	}
+
+	if got3 != want3 {
+		t.Errorf("got %v, want %v", got3, want3)
+	}
+
+	// plugins labelagent add same child to existing plugin
+	want4 := 1
+	got4 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("new_mapping")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got4 += 1
+		}
+	}
+
+	if got4 != want4 {
+		t.Errorf("got %v, want %v", got4, want4)
+	}
+
+	// plugins labelagent existing child no change
+	want5 := 1
+	got5 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("split")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got5 += 1
+		}
+	}
+
+	if got5 != want5 {
+		t.Errorf("got %v, want %v", got5, want5)
+	}
+
+	// plugins aggregator add new child
+	want8 := 2
+	got8 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("Aggregator")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got8 += 1
+		}
+	}
+
+	if got8 != want8 {
+		t.Errorf("got %v, want %v", got8, want8)
+	}
+
+	//export_options add new instance_key
+	want6 := 6
+	got6 := 0
+	counters = defaultTemplate.GetChildS("export_options").GetChildS("instance_keys")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got6 += 1
+		}
+	}
+
+	if got6 != want6 {
+		t.Errorf("got %v, want %v", want6, got6)
+	}
+
+	//export_options add same instance_labels
+	want7 := 1
+	got7 := 0
+	counters = defaultTemplate.GetChildS("export_options").GetChildS("instance_labels")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got7 += 1
+		}
+	}
+
+	if got7 != want7 {
+		t.Errorf("got %v, want %v", want7, got7)
+	}
+
+	//override block
+	want9 := 2
+	got9 := 0
+	counters = defaultTemplate.GetChildS("override")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got9 += 1
+		}
+	}
+
+	if want9 != got9 {
+		t.Errorf("got %v, want %v", want9, got9)
+	}
+
+	//export block
+
+	export := defaultTemplate.GetChildS("export")
+	if export != nil {
+		t.Errorf("missing export block")
+	}
+
+	if want9 != got9 {
+		t.Errorf("got %v, want %v", want9, got9)
+	}
+}
+
+// Merge collector templates where custom templates are from 21.08.6 and before
+// LabelAgent child did have key-value pair of rules instead of a list
+func TestNode_MergeCollectorOld(t *testing.T) {
+	defaultTemplate, _ := tree.Import("yaml", "testdata/lun.yaml")
+	customTemplate, _ := tree.Import("yaml", "testdata/21.08.0_extend_lun.yaml")
+	defaultTemplate.PreprocessTemplate()
+	customTemplate.PreprocessTemplate()
+	defaultTemplate.Merge(customTemplate, nil)
+
+	// plugins labelagent add new child to existing plugin
+	want2 := 2
+	got2 := 0
+	counters := defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("value_mapping")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got2 += 1
+		}
+	}
+
+	if got2 != want2 {
+		t.Errorf("got %v, want %v", got2, want2)
+	}
+
+	// plugins labelagent add same child to existing plugin
+	want3 := 1
+	got3 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("value_to_num")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got3 += 1
+		}
+	}
+
+	if got3 != want3 {
+		t.Errorf("got %v, want %v", got3, want3)
+	}
+
+	// plugins labelagent add same child to existing plugin
+	want4 := 1
+	got4 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("LabelAgent").GetChildS("new_mapping")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got4 += 1
+		}
+	}
+
+	if got4 != want4 {
+		t.Errorf("got %v, want %v", got4, want4)
+	}
+
+	// plugins aggregator add new child
+	want5 := 3
+	got5 := 0
+	counters = defaultTemplate.GetChildS("plugins").GetChildS("Aggregator")
+	if counters != nil {
+		for range counters.GetChildren() {
+			got5 += 1
+		}
+	}
+
+	if got5 != want5 {
+		t.Errorf("got %v, want %v", got5, want5)
+	}
+}
+
+func TestNode_PreProcessCollector(t *testing.T) {
+
+	tests := []struct {
+		name        string
+		sourceFile  string
+		compareFile string
+	}{
+		{name: "preprocess template from 21.08.0", sourceFile: "testdata/21.08.0_extend_lun.yaml", compareFile: "preProcessResultData/p_21.08.0_extend_lun.yaml"},
+		{name: "preprocess template after 21.08.0", sourceFile: "testdata/21.08.0_lun.yaml", compareFile: "preProcessResultData/p_21.08.0_lun.yaml"},
+		{name: "process collector template", sourceFile: "testdata/default_collector.yaml", compareFile: "preProcessResultData/p_default_collector.yaml"},
+		{name: "process extended collector template", sourceFile: "testdata/extend_collector.yaml", compareFile: "preProcessResultData/p_extend_collector.yaml"},
+		{name: "process extended object template", sourceFile: "testdata/extend_lun.yaml", compareFile: "preProcessResultData/p_extend_lun.yaml"},
+		{name: "process object template", sourceFile: "testdata/lun.yaml", compareFile: "preProcessResultData/p_lun.yaml"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			template, _ := tree.Import("yaml", tt.sourceFile)
+			template.PreprocessTemplate()
+			got, _ := yaml.Dump(template)
+			expected, _ := ioutil.ReadFile(tt.compareFile)
+			gotString := strings.TrimSpace(string(got))
+			expectedString := strings.TrimSpace(string(expected))
+			if gotString != expectedString {
+				t.Errorf("got %v, want %v", gotString, expectedString)
+			}
+		})
+	}
+}
+
+func TestNode_PreProcessMergeCollector(t *testing.T) {
+
+	tests := []struct {
+		name           string
+		baseTemplate   string
+		extendTemplate string
+		mergeTemplate  string
+	}{
+		{name: "Case1: Both base and extended template follow new convention for labelagent which is list", baseTemplate: "testdata/lun.yaml", extendTemplate: "testdata/extend_lun.yaml", mergeTemplate: "mergeTemplates/lun_merge.yaml"},
+		{name: "Case2: base template follow new convention for labelagent and extended template follow 21.08.0", baseTemplate: "testdata/lun.yaml", extendTemplate: "testdata/21.08.0_extend_lun.yaml", mergeTemplate: "mergeTemplates/lun_merge_21.08.0_extended.yaml"},
+		{name: "Case3: base template follow old convention for labelagent and extended template follow 21.08.0", baseTemplate: "testdata/21.08.0_lun.yaml", extendTemplate: "testdata/21.08.0_extend_lun.yaml", mergeTemplate: "mergeTemplates/21.08.0_lun_merge_21.08.0_extended.yaml"},
+		{name: "Case4: base template follow old convention for labelagent and extended template follow new", baseTemplate: "testdata/21.08.0_lun.yaml", extendTemplate: "testdata/extend_lun.yaml", mergeTemplate: "mergeTemplates/21.08.0_lun_merge_extended.yaml"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			baseTemplate, _ := tree.Import("yaml", tt.baseTemplate)
+			extendTemplate, _ := tree.Import("yaml", tt.extendTemplate)
+			baseTemplate.PreprocessTemplate()
+			extendTemplate.PreprocessTemplate()
+			baseTemplate.Merge(extendTemplate, nil)
+			gotString1, _ := yaml.Dump(baseTemplate)
+			gotString := strings.TrimSpace(string(gotString1))
+			expected, _ := ioutil.ReadFile(tt.mergeTemplate)
+			expectedString := strings.TrimSpace(string(expected))
+
+			if gotString != expectedString {
+				t.Errorf("got %v, want %v", gotString, expectedString)
+			}
+		})
+	}
+}
diff --git a/pkg/test/preProcessResultData/p_21.08.0_extend_lun.yaml b/pkg/test/preProcessResultData/p_21.08.0_extend_lun.yaml
new file mode 100644
index 00000000..ce033b32
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_21.08.0_extend_lun.yaml
@@ -0,0 +1,12 @@
+plugins:
+  - nic
+  LabelAgent:
+    value_mapping:
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    new_mapping:
+      - xyz
+  Aggregator:
+    - node<>node_cpu1
+    - nodetest
diff --git a/pkg/test/preProcessResultData/p_21.08.0_lun.yaml b/pkg/test/preProcessResultData/p_21.08.0_lun.yaml
new file mode 100644
index 00000000..c7fd66ce
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_21.08.0_lun.yaml
@@ -0,0 +1,31 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file
diff --git a/pkg/test/preProcessResultData/p_default_collector.yaml b/pkg/test/preProcessResultData/p_default_collector.yaml
new file mode 100644
index 00000000..51752773
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_default_collector.yaml
@@ -0,0 +1,14 @@
+collector: Zapi
+schedule:
+  instance: 600s
+  data: 180s
+objects:
+  Node: node.yaml
+  Aggregate: aggr.yaml
+  Volume: volume.yaml
+  SnapMirror: snapmirror.yaml
+  Disk: disk.yaml
+  Shelf: shelf.yaml
+  Status: status.yaml
+  Subsystem: subsystem.yaml
+  Lun: lun.yaml
\ No newline at end of file
diff --git a/pkg/test/preProcessResultData/p_extend_collector.yaml b/pkg/test/preProcessResultData/p_extend_collector.yaml
new file mode 100644
index 00000000..18f74e89
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_extend_collector.yaml
@@ -0,0 +1,6 @@
+schedule:
+  instance: 650s
+  data: 185s
+objects:
+  Node: node2.yaml
+  Sensor: sensor.yaml
\ No newline at end of file
diff --git a/pkg/test/preProcessResultData/p_extend_lun.yaml b/pkg/test/preProcessResultData/p_extend_lun.yaml
new file mode 100644
index 00000000..0888ef55
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_extend_lun.yaml
@@ -0,0 +1,23 @@
+name: customLun
+counters:
+  lun-info:
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    new_mapping:
+      - xyz
+  Aggregator:
+    - node<>node_cpu1
+override:
+  KB_copied: delta
+  key2: test
+export_data: false
+export_options:
+  instance_keys:
+    - node1
+  instance_labels:
+    - state
diff --git a/pkg/test/preProcessResultData/p_lun.yaml b/pkg/test/preProcessResultData/p_lun.yaml
new file mode 100644
index 00000000..3f9a6298
--- /dev/null
+++ b/pkg/test/preProcessResultData/p_lun.yaml
@@ -0,0 +1,33 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+  Aggregator:
+    - aggr
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file
diff --git a/pkg/test/testdata/21.08.0_extend_lun.yaml b/pkg/test/testdata/21.08.0_extend_lun.yaml
new file mode 100644
index 00000000..7071fa94
--- /dev/null
+++ b/pkg/test/testdata/21.08.0_extend_lun.yaml
@@ -0,0 +1,9 @@
+plugins:
+  - nic
+  LabelAgent:
+    value_mapping: custom_status state online `1`
+    value_to_num: new_status state online online `0`
+    new_mapping: xyz
+  Aggregator:
+    - node<>node_cpu1
+    - nodetest
diff --git a/pkg/test/testdata/21.08.0_lun.yaml b/pkg/test/testdata/21.08.0_lun.yaml
new file mode 100644
index 00000000..ed995983
--- /dev/null
+++ b/pkg/test/testdata/21.08.0_lun.yaml
@@ -0,0 +1,28 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  - LabelAgent:
+    value_mapping: status state online `1`
+    value_to_num: new_status state online online `0`
+    split: path `/` ,,,lun
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file
diff --git a/pkg/test/testdata/default.yaml b/pkg/test/testdata/default_collector.yaml
similarity index 93%
rename from pkg/test/testdata/default.yaml
rename to pkg/test/testdata/default_collector.yaml
index c7b55949..e9592137 100644
--- a/pkg/test/testdata/default.yaml
+++ b/pkg/test/testdata/default_collector.yaml
@@ -1,11 +1,7 @@
-
 collector:          Zapi
-
-# Order here matters!
 schedule:
   - instance: 600s
   - data: 180s
-
 objects:
   Node:             node.yaml
   Aggregate:        aggr.yaml
diff --git a/pkg/test/testdata/custom.yaml b/pkg/test/testdata/extend_collector.yaml
similarity index 100%
rename from pkg/test/testdata/custom.yaml
rename to pkg/test/testdata/extend_collector.yaml
diff --git a/pkg/test/testdata/extend_lun.yaml b/pkg/test/testdata/extend_lun.yaml
new file mode 100644
index 00000000..af532876
--- /dev/null
+++ b/pkg/test/testdata/extend_lun.yaml
@@ -0,0 +1,23 @@
+name: customLun
+counters:
+  lun-info:
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - custom_status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    new_mapping:
+      - xyz
+  Aggregator:
+    - node<>node_cpu1
+override:
+  - KB_copied: delta
+  - key2: test
+export_data: false
+export_options:
+  instance_keys:
+    - node1
+  instance_labels:
+    - state
diff --git a/pkg/test/testdata/lun.yaml b/pkg/test/testdata/lun.yaml
new file mode 100644
index 00000000..3f9a6298
--- /dev/null
+++ b/pkg/test/testdata/lun.yaml
@@ -0,0 +1,33 @@
+name: Lun
+query: lun-get-iter
+object: lun
+counters:
+  lun-info:
+    - ^node
+    - ^path
+    - ^qtree
+    - size
+    - size-used
+    - ^state
+    - ^^uuid
+    - ^volume
+    - ^vserver => svm
+plugins:
+  LabelAgent:
+    value_mapping:
+      - status state online `1`
+    value_to_num:
+      - new_status state online online `0`
+    split:
+      - path `/` ,,,lun
+  Aggregator:
+    - aggr
+export_options:
+  instance_keys:
+    - node
+    - qtree
+    - lun
+    - volume
+    - svm
+  instance_labels:
+    - state
\ No newline at end of file