Repository: NetApp/harvest
Pull request #831 (2022-02-07 07:58:06)

feat: revamp grafana import cli
Commands supported

Import:
1.  bin/grafana import --addr my.grafana.server:3000
2. bin/grafana import --addr my.grafana.server:3000 --serverfolder [grafanafoldername]  --directory [path]

Export:
1. bin/grafana export --addr my.grafana.server:3000 --serverfolder [grafanafoldername] --directory [path]





Patch:
diff --git a/cmd/collectors/rest/plugins/qtree/qtree.go b/cmd/collectors/rest/plugins/qtree/qtree.go
index bf2a9acb..57394185 100644
--- a/cmd/collectors/rest/plugins/qtree/qtree.go
+++ b/cmd/collectors/rest/plugins/qtree/qtree.go
@@ -84,7 +84,7 @@ func (my *Qtree) Init() error {
 	}
 
 	for _, obj := range quotaMetric {
-		metricName, display, _ := util.ParseMetric(obj)
+		metricName, display, _, _ := util.ParseMetric(obj)
 
 		metric, err := my.data.NewMetricFloat64(metricName)
 		if err != nil {
diff --git a/cmd/collectors/rest/plugins/shelf/shelf.go b/cmd/collectors/rest/plugins/shelf/shelf.go
index 0c051e5d..d54fff03 100644
--- a/cmd/collectors/rest/plugins/shelf/shelf.go
+++ b/cmd/collectors/rest/plugins/shelf/shelf.go
@@ -119,7 +119,7 @@ func (my *Shelf) Init() error {
 
 		for _, c := range childObj {
 
-			metricName, display, kind := util.ParseMetric(c)
+			metricName, display, kind, _ := util.ParseMetric(c)
 
 			switch kind {
 			case "key":
diff --git a/cmd/collectors/rest/plugins/snapmirror/snapmirror.go b/cmd/collectors/rest/plugins/snapmirror/snapmirror.go
new file mode 100644
index 00000000..035adc6b
--- /dev/null
+++ b/cmd/collectors/rest/plugins/snapmirror/snapmirror.go
@@ -0,0 +1,142 @@
+/*
+ * Copyright NetApp Inc, 2022 All rights reserved
+ */
+package snapmirror
+
+import (
+	"encoding/json"
+	"github.com/tidwall/gjson"
+	"goharvest2/cmd/collectors/rest/plugins"
+	"goharvest2/cmd/poller/plugin"
+	"goharvest2/cmd/tools/rest"
+	"goharvest2/pkg/conf"
+	"goharvest2/pkg/errors"
+	"goharvest2/pkg/matrix"
+	"time"
+)
+
+const PluginInvocationRate = 10
+
+type SnapMirror struct {
+	*plugin.AbstractPlugin
+	client         *rest.Client
+	query          string
+	nodeUpdCounter int
+	svmVolToNode   map[string]string
+}
+
+func New(p *plugin.AbstractPlugin) plugin.Plugin {
+	return &SnapMirror{AbstractPlugin: p}
+}
+
+func (my *SnapMirror) Init() error {
+
+	var err error
+
+	if err = my.InitAbc(); err != nil {
+		return err
+	}
+
+	timeout := rest.DefaultTimeout * time.Second
+	if my.client, err = rest.New(conf.ZapiPoller(my.ParentParams), timeout); err != nil {
+		my.Logger.Error().Stack().Err(err).Msg("connecting")
+		return err
+	}
+
+	if err = my.client.Init(5); err != nil {
+		return err
+	}
+
+	my.query = "api/private/cli/volume"
+	my.svmVolToNode = make(map[string]string)
+
+	// Assigned the value to nodeUpdCounter so that plugin would be invoked first time to populate cache.
+	my.nodeUpdCounter = PluginInvocationRate
+
+	return nil
+}
+
+func (my *SnapMirror) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
+
+	if my.nodeUpdCounter >= PluginInvocationRate {
+		my.nodeUpdCounter = 0
+		if err := my.updateNodeCache(); err != nil {
+			return nil, err
+		}
+		my.Logger.Debug().Msg("updated node cache")
+	}
+
+	// update volume instance labels
+	my.updateSMLabels(data)
+	my.nodeUpdCounter++
+
+	return nil, nil
+}
+
+func (my *SnapMirror) updateNodeCache() error {
+	var (
+		records []interface{}
+		content []byte
+		err     error
+	)
+
+	// Clean svmVolToNode map
+	my.svmVolToNode = make(map[string]string)
+
+	href := rest.BuildHref("", "node", nil, "", "", "", "", my.query)
+
+	err = rest.FetchData(my.client, href, &records)
+	if err != nil {
+		my.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 {
+		my.Logger.Error().Err(err).Str("ApiPath", my.query).Msg("Unable to marshal rest pagination")
+	}
+
+	if !gjson.ValidBytes(content) {
+		my.Logger.Error().Err(err).Str("Api", my.query).Msg("Invalid json")
+		return errors.New(errors.API_RESPONSE, "Invalid json")
+	}
+
+	results := gjson.GetManyBytes(content, "num_records", "records")
+	numRecords := results[0]
+	if numRecords.Int() == 0 {
+		return errors.New(errors.ERR_NO_INSTANCE, "no "+my.query+" instances on cluster")
+	}
+
+	for _, volume := range results[1].Array() {
+		volumeName := volume.Get("volume").String()
+		vserverName := volume.Get("vserver").String()
+		nodeName := volume.Get("node").String()
+		key := vserverName + volumeName
+
+		if _, ok := my.svmVolToNode[key]; ok {
+			my.Logger.Warn().Str("key", key).Msg("Duplicate key found")
+		}
+		my.svmVolToNode[key] = nodeName
+	}
+	return nil
+}
+
+func (my *SnapMirror) updateSMLabels(data *matrix.Matrix) {
+	for _, instance := range data.GetInstances() {
+		volumeName := instance.GetLabel("source_volume")
+		vserverName := instance.GetLabel("source_vserver")
+
+		// Update source_node label in snapmirror
+		if node, ok := my.svmVolToNode[vserverName+volumeName]; ok {
+			instance.SetLabel("source_node", node)
+		}
+
+		// update the protectedBy and protectionSourceType fields and derivedRelationshipType in snapmirror_labels
+		plugins.UpdateProtectedFields(instance)
+	}
+}
diff --git a/cmd/collectors/rest/plugins/util.go b/cmd/collectors/rest/plugins/util.go
new file mode 100644
index 00000000..72a1e458
--- /dev/null
+++ b/cmd/collectors/rest/plugins/util.go
@@ -0,0 +1,62 @@
+package plugins
+
+import (
+	"goharvest2/pkg/matrix"
+	"strings"
+)
+
+func UpdateProtectedFields(instance *matrix.Instance) {
+
+	// check for group_type
+	// Supported group_type are: "none", "vserver", "infinitevol", "consistencygroup", "flexgroup"
+	if instance.GetLabel("group_type") != "" {
+
+		groupType := instance.GetLabel("group_type")
+		destinationVolume := instance.GetLabel("destination_volume")
+		sourceVolume := instance.GetLabel("source_volume")
+		destinationLocation := instance.GetLabel("destination_location")
+
+		isSvmDr := groupType == "vserver" && destinationVolume == "" && sourceVolume == ""
+		isCg := groupType == "CONSISTENCYGROUP" && strings.Contains(destinationLocation, ":/cg/")
+		isConstituentVolumeRelationshipWithinSvmDr := groupType == "vserver" && !strings.HasSuffix(destinationLocation, ":")
+		isConstituentVolumeRelationshipWithinCG := groupType == "CONSISTENCYGROUP" && !strings.Contains(destinationLocation, ":/cg/")
+
+		// Update protectedBy label
+		if isSvmDr || isConstituentVolumeRelationshipWithinSvmDr {
+			instance.SetLabel("protectedBy", "storage_vm")
+		} else if isCg || isConstituentVolumeRelationshipWithinCG {
+			instance.SetLabel("protectedBy", "cg")
+		} else {
+			instance.SetLabel("protectedBy", "volume")
+		}
+
+		// SVM-DR related information is populated, Update protectionSourceType label
+		if isSvmDr {
+			instance.SetLabel("protectionSourceType", "storage_vm")
+		} else if isCg {
+			instance.SetLabel("protectionSourceType", "cg")
+		} else if isConstituentVolumeRelationshipWithinSvmDr || isConstituentVolumeRelationshipWithinCG || groupType == "none" || groupType == "flexgroup" {
+			instance.SetLabel("protectionSourceType", "volume")
+		} else {
+			instance.SetLabel("protectionSourceType", "not_mapped")
+		}
+	}
+
+	// Update derived_relationship_type field based on the policyType
+	relationshipType := instance.GetLabel("relationship_type")
+	if policyType := instance.GetLabel("policy_type"); policyType != "" {
+		if policyType == "strict_sync_mirror" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror_strict")
+		} else if policyType == "sync_mirror" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror")
+		} else if policyType == "mirror_vault" {
+			instance.SetLabel("derived_relationship_type", "mirror_vault")
+		} else if policyType == "automated_failover" {
+			instance.SetLabel("derived_relationship_type", "sync_mirror")
+		} else {
+			instance.SetLabel("derived_relationship_type", relationshipType)
+		}
+	} else {
+		instance.SetLabel("derived_relationship_type", relationshipType)
+	}
+}
diff --git a/cmd/collectors/rest/plugins/volume/volume.go b/cmd/collectors/rest/plugins/volume/volume.go
new file mode 100644
index 00000000..3b314bb0
--- /dev/null
+++ b/cmd/collectors/rest/plugins/volume/volume.go
@@ -0,0 +1,312 @@
+/*
+ * Copyright NetApp Inc, 2022 All rights reserved
+ */
+
+package volume
+
+import (
+	"encoding/json"
+	"github.com/tidwall/gjson"
+	"goharvest2/cmd/collectors/rest/plugins"
+	"goharvest2/cmd/poller/plugin"
+	"goharvest2/cmd/tools/rest"
+	"goharvest2/pkg/conf"
+	"goharvest2/pkg/dict"
+	"goharvest2/pkg/errors"
+	"goharvest2/pkg/matrix"
+	"goharvest2/pkg/tree/node"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const DefaultPluginDuration = 1800 * time.Second
+const DefaultDataPollDuration = 180 * time.Second
+
+type Volume struct {
+	*plugin.AbstractPlugin
+	data                 *matrix.Matrix
+	instanceKeys         map[string]string
+	instanceLabels       map[string]*dict.Dict
+	pluginInvocationRate int
+	currentVal           int
+	client               *rest.Client
+	query                string
+	snapmirrorFields     []string
+	outgoingSM           map[string][]string
+	incomingSM           map[string]string
+	isHealthySM          map[string]bool
+}
+
+func New(p *plugin.AbstractPlugin) plugin.Plugin {
+	return &Volume{AbstractPlugin: p}
+}
+
+func (my *Volume) Init() error {
+
+	var err error
+	my.snapmirrorFields = []string{"relationship_id", "relationship_group_type", "destination_volume",
+		"source_volume", "destination_path", "type", "healthy", "source_vserver", "destination_vserver"}
+
+	if err = my.InitAbc(); err != nil {
+		return err
+	}
+
+	timeout := rest.DefaultTimeout * time.Second
+	if my.client, err = rest.New(conf.ZapiPoller(my.ParentParams), timeout); err != nil {
+		my.Logger.Error().Stack().Err(err).Msg("connecting")
+		return err
+	}
+
+	if err = my.client.Init(5); err != nil {
+		return err
+	}
+
+	my.query = "api/private/cli/snapmirror"
+	my.data = matrix.New(my.Parent+".Volume", "volume", "volume")
+
+	my.outgoingSM = make(map[string][]string)
+	my.incomingSM = make(map[string]string)
+	my.isHealthySM = make(map[string]bool)
+
+	// Assigned the value to currentVal so that plugin would be invoked first time to populate cache.
+	if my.currentVal, err = my.setPluginInterval(); err != nil {
+		my.Logger.Error().Err(err).Stack().Msg("Failed while setting the plugin interval")
+		return err
+	}
+
+	return nil
+}
+
+func (my *Volume) Run(data *matrix.Matrix) ([]*matrix.Matrix, error) {
+
+	// Purge and reset data
+	my.data.PurgeInstances()
+	my.data.Reset()
+
+	// Set all global labels from zapi.go if already not exist
+	my.data.SetGlobalLabels(data.GetGlobalLabels())
+
+	if my.currentVal >= my.pluginInvocationRate {
+		my.currentVal = 0
+
+		// invoke snapmirror zapi and populate info in source and destination snapmirror maps
+		if smSourceMap, smDestinationMap, err := my.GetSnapMirrors(); err != nil {
+			my.Logger.Error().Stack().Err(err).Msg("Failed to collect snapmirror data")
+		} else {
+			// update internal cache based on volume and SM maps
+			my.updateMaps(data, smSourceMap, smDestinationMap)
+		}
+	}
+
+	// update volume instance labels
+	my.updateVolumeLabels(data)
+
+	my.currentVal++
+	return nil, nil
+}
+
+func (my *Volume) setPluginInterval() (int, error) {
+
+	volumeDataInterval := my.getDataInterval(my.ParentParams, DefaultDataPollDuration)
+	pluginDataInterval := my.getDataInterval(my.Params, DefaultPluginDuration)
+	my.Logger.Debug().Float64("VolumeDataInterval", volumeDataInterval).Float64("PluginDataInterval", pluginDataInterval).Msg("Poll interval duration")
+	my.pluginInvocationRate = int(pluginDataInterval / volumeDataInterval)
+
+	return my.pluginInvocationRate, nil
+}
+
+func (my *Volume) getDataInterval(param *node.Node, defaultInterval time.Duration) float64 {
+	var dataIntervalStr = ""
+	schedule := param.GetChildS("schedule")
+	if schedule != nil {
+		dataInterval := schedule.GetChildS("data")
+		if dataInterval != nil {
+			dataIntervalStr = dataInterval.GetContentS()
+			if durationVal, err := time.ParseDuration(dataIntervalStr); err == nil {
+				return durationVal.Seconds()
+			}
+		}
+	}
+	return defaultInterval.Seconds()
+}
+
+func (my *Volume) GetSnapMirrors() (map[string][]*matrix.Instance, map[string]*matrix.Instance, error) {
+	var (
+		records []interface{}
+		content []byte
+		err     error
+	)
+
+	smSourceMap := make(map[string][]*matrix.Instance)
+	smDestinationMap := make(map[string]*matrix.Instance)
+
+	snapmirrorData := matrix.New(my.Parent+".SnapMirror", "sm", "sm")
+
+	href := rest.BuildHref("", strings.Join(my.snapmirrorFields, ","), nil, "", "", "", "", my.query)
+
+	err = rest.FetchData(my.client, href, &records)
+	if err != nil {
+		my.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return nil, nil, err
+	}
+
+	all := rest.Pagination{
+		Records:    records,
+		NumRecords: len(records),
+	}
+
+	content, err = json.Marshal(all)
+	if err != nil {
+		my.Logger.Error().Err(err).Str("ApiPath", my.query).Msg("Unable to marshal rest pagination")
+	}
+
+	if !gjson.ValidBytes(content) {
+		my.Logger.Error().Err(err).Str("Api", my.query).Msg("Invalid json")
+		return nil, nil, errors.New(errors.API_RESPONSE, "Invalid json")
+	}
+
+	results := gjson.GetManyBytes(content, "num_records", "records")
+	numRecords := results[0]
+	if numRecords.Int() == 0 {
+		return nil, nil, errors.New(errors.ERR_NO_INSTANCE, "no "+my.query+" instances on cluster")
+	}
+
+	for _, snapMirror := range results[1].Array() {
+
+		relationshipId := snapMirror.Get("relationship_id").String()
+		groupType := snapMirror.Get("relationship_group_type").String()
+		destinationVolume := snapMirror.Get("destination_volume").String()
+		sourceVolume := snapMirror.Get("source_volume").String()
+		destinationLocation := snapMirror.Get("destination_path").String()
+		relationshipType := snapMirror.Get("type").String()
+		isHealthy := snapMirror.Get("healthy").String()
+		sourceSvm := snapMirror.Get("source_vserver").String()
+		destinationSvm := snapMirror.Get("destination_vserver").String()
+
+		instanceKey := relationshipId
+		instance, err := snapmirrorData.NewInstance(instanceKey)
+
+		if err != nil {
+			my.Logger.Error().Err(err).Stack().Str("relationshipId", relationshipId).Msg("Failed to create snapmirror cache instance")
+			return nil, nil, err
+		}
+
+		instance.SetLabel("relationship_id", relationshipId)
+		instance.SetLabel("group_type", groupType)
+		instance.SetLabel("destination_volume", destinationVolume)
+		instance.SetLabel("source_volume", sourceVolume)
+		instance.SetLabel("destination_location", destinationLocation)
+		instance.SetLabel("relationship_type", relationshipType)
+		instance.SetLabel("is_healthy", isHealthy)
+		instance.SetLabel("source_svm", sourceSvm)
+		instance.SetLabel("destination_svm", destinationSvm)
+
+		// Update the protectedBy and protectionSourceType fields in snapmirror
+		plugins.UpdateProtectedFields(instance)
+
+		// Update source snapmirror and destination snapmirror info in maps
+		if relationshipType == "data_protection" || relationshipType == "extended_data_protection" || relationshipType == "vault" || relationshipType == "xdp" {
+			sourceKey := sourceVolume + "-" + sourceSvm
+			destinationKey := destinationVolume + "-" + destinationSvm
+			if instance.GetLabel("protectionSourceType") == "volume" {
+				smSourceMap[sourceKey] = append(smSourceMap[sourceKey], instance)
+			}
+			smDestinationMap[destinationKey] = instance
+		}
+	}
+
+	return smSourceMap, smDestinationMap, nil
+}
+
+func (my *Volume) updateMaps(data *matrix.Matrix, smSourceMap map[string][]*matrix.Instance, smDestinationMap map[string]*matrix.Instance) {
+	// Clean all the snapmirror maps
+	my.outgoingSM = make(map[string][]string)
+	my.incomingSM = make(map[string]string)
+	my.isHealthySM = make(map[string]bool)
+
+	for _, volume := range data.GetInstances() {
+		volumeName := volume.GetLabel("volume")
+		svmName := volume.GetLabel("svm")
+		volumeType := volume.GetLabel("type")
+		key := volumeName + "-" + svmName
+
+		protectedByMap := make(map[string]string)
+		var protectedByValue []string
+		healthStatus := true
+		for _, smRelationship := range smSourceMap[key] {
+			/* Example: If 3 relationships belongs to a volume, and out of 3, 2 are at snapmirror and one is svmdr,
+			   So, protectedByMap map has 2 records, and outgoingSM map value would be snapmirror, svmdr
+			*/
+			protectedByMap[smRelationship.GetLabel("protectedBy")] = ""
+
+			// Update isHealthySM map based on the source snapmirror info
+			if volumeType == "rw" {
+				/* Example: If 3 relationships belongs to a volume, and out of 3, 2 are healthy and one is not,
+				   So, isHealthySM map value would be unhealthy - false
+				*/
+				currentVal, _ := strconv.ParseBool(smRelationship.GetLabel("is_healthy"))
+				healthStatus = healthStatus && currentVal
+				my.isHealthySM[key] = healthStatus
+			}
+		}
+
+		// Update outgoingSM map based on the protectedByMap
+		protectedByValue = nil
+		for protectedByKey, _ := range protectedByMap {
+			protectedByValue = append(protectedByValue, protectedByKey)
+		}
+		if protectedByValue != nil {
+			my.outgoingSM[key] = protectedByValue
+		}
+
+		// Update incomingSM map based on the destination snapmirror info
+		if smDestinationMap[key] != nil {
+			my.incomingSM[key] = "destination"
+		}
+	}
+}
+
+func (my *Volume) updateVolumeLabels(data *matrix.Matrix) {
+	for _, volume := range data.GetInstances() {
+		volumeName := volume.GetLabel("volume")
+		svmName := volume.GetLabel("svm")
+		volumeType := volume.GetLabel("type")
+		key := volumeName + "-" + svmName
+
+		// Update protectionRole label in volume
+		if volumeType == "rw" && my.incomingSM[key] == "" && my.outgoingSM[key] == nil {
+			volume.SetLabel("protectionRole", "unprotected")
+		} else if volumeType == "rw" && my.outgoingSM[key] != nil {
+			volume.SetLabel("protectionRole", "protected")
+		} else if volumeType == "dp" || (volumeType == "rw" && my.incomingSM[key] != "") {
+			volume.SetLabel("protectionRole", "destination")
+		} else {
+			volume.SetLabel("protectionRole", "not_applicable")
+		}
+
+		// Update protectedBy label in volume
+		if outgoing, ok := my.outgoingSM[key]; ok {
+			outgoingJoinStr := strings.Join(outgoing, ",")
+			if outgoingJoinStr == "volume,storage_vm" || outgoingJoinStr == "storage_vm,volume" {
+				volume.SetLabel("protectedBy", "svmdr_and_snapmirror")
+			} else if outgoingJoinStr == "cg,volume" || outgoingJoinStr == "volume,cg" {
+				volume.SetLabel("protectedBy", "cg_and_snapmirror")
+			} else if outgoingJoinStr == "cg" {
+				volume.SetLabel("protectedBy", "consistency_group")
+			} else if outgoingJoinStr == "storage_vm" {
+				volume.SetLabel("protectedBy", "storage_vm_dr")
+			} else if outgoingJoinStr == "volume" {
+				volume.SetLabel("protectedBy", "snapmirror")
+			}
+		} else {
+			volume.SetLabel("protectedBy", "not_applicable")
+		}
+
+		// Update all_sm_healthy label in volume, when all relationships belongs to this volume are healthy then true, otherwise false
+		if healthy, ok := my.isHealthySM[key]; ok {
+			volume.SetLabel("all_sm_healthy", strconv.FormatBool(healthy))
+		}
+
+	}
+}
diff --git a/cmd/collectors/rest/rest.go b/cmd/collectors/rest/rest.go
index b15b5a95..cfe4bdf6 100644
--- a/cmd/collectors/rest/rest.go
+++ b/cmd/collectors/rest/rest.go
@@ -7,6 +7,8 @@ import (
 	"goharvest2/cmd/collectors/rest/plugins/disk"
 	"goharvest2/cmd/collectors/rest/plugins/qtree"
 	"goharvest2/cmd/collectors/rest/plugins/shelf"
+	"goharvest2/cmd/collectors/rest/plugins/snapmirror"
+	"goharvest2/cmd/collectors/rest/plugins/volume"
 	"goharvest2/cmd/poller/collector"
 	"goharvest2/cmd/poller/plugin"
 	"goharvest2/cmd/tools/rest"
@@ -14,7 +16,6 @@ import (
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/matrix"
 	"goharvest2/pkg/tree/node"
-	"goharvest2/pkg/util"
 	"os"
 	"strings"
 	"time"
@@ -45,8 +46,9 @@ type prop struct {
 }
 
 type metric struct {
-	label string
-	name  string
+	label      string
+	name       string
+	metricType string
 }
 
 func init() {
@@ -153,9 +155,6 @@ 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}
@@ -167,6 +166,7 @@ func (r *Rest) initEndPoints() error {
 			prop.counters = make(map[string]string)
 			prop.metrics = make([]metric, 0)
 			prop.apiType = "public"
+			prop.returnTimeOut = r.prop.returnTimeOut
 
 			for _, line1 := range line.GetChildren() {
 				if line1.GetNameS() == "query" {
@@ -174,37 +174,7 @@ func (r *Rest) initEndPoints() error {
 					prop.apiType = checkQueryType(prop.query)
 				}
 				if line1.GetNameS() == "counters" {
-					for _, c := range line1.GetAllChildContentS() {
-						name, display, kind = util.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
-						case "float":
-							m := metric{label: display, name: name}
-							prop.metrics = append(prop.metrics, m)
-						}
-					}
-					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()...)
-						}
-					}
+					r.ParseRestCounters(line1, &prop)
 				}
 			}
 			e.prop = &prop
@@ -267,16 +237,8 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	startTime = time.Now()
 
-	href := rest.BuildHref(r.prop.query, strings.Join(r.prop.fields, ","), nil, "", "", "", r.prop.returnTimeOut, r.prop.query)
-
-	r.Logger.Debug().Str("href", href).Msg("")
-	if href == "" {
-		return nil, 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")
+	if records, err = r.GetRestData(r.prop, r.client); err != nil {
+		r.Logger.Error().Stack().Err(err).Msg("Failed to fetch data")
 		return nil, err
 	}
 
@@ -291,11 +253,9 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 		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.prop.query)
 	}
-	parseD = time.Since(startTime)
 
 	results := gjson.GetManyBytes(content, "num_records", "records")
 	numRecords := results[0]
@@ -305,88 +265,15 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 
 	r.Logger.Debug().Str("object", r.Object).Str("number of records extracted", numRecords.String()).Msg("")
 
-	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 r.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 r.Params.GetChildContentS("only_cluster_instance") != "true" {
-			if instanceKey == "" {
-				return true
-			}
-		}
-
-		if instance = r.Matrix.GetInstance(instanceKey); instance == nil {
-			if instance, err = r.Matrix.NewInstance(instanceKey); err != nil {
-				r.Logger.Error().Err(err).Str("Instance key", instanceKey).Msg("")
-				return true
-			}
-		}
-
-		for label, display := range r.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())
-				}
-				count++
-			} 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 r.prop.metrics {
-			metr, ok := r.Matrix.GetMetrics()[metric.name]
-			if !ok {
-				if metr, err = r.Matrix.NewMetricFloat64(metric.name); err != nil {
-					r.Logger.Error().Err(err).
-						Str("name", metric.name).
-						Msg("NewMetricFloat64")
-				}
-			}
-			f := instanceData.Get(metric.name)
-			if f.Exists() {
-				metr.SetName(metric.label)
-				if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
-					r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
-						Msg("Unable to set float key on metric")
-				}
-				count++
-			}
-		}
-		return true
-	})
+	count = r.HandleResults(results[1], r.prop)
 
 	// process endpoints
-	err = r.processEndPoints(r.Matrix)
+	startTime = time.Now()
+	err = r.processEndPoints()
 	if err != nil {
 		r.Logger.Error().Err(err).Msg("Error while processing end points")
 	}
+	parseD = time.Since(startTime)
 
 	r.Logger.Info().
 		Uint64("dataPoints", count).
@@ -402,7 +289,7 @@ func (r *Rest) PollData() (*matrix.Matrix, error) {
 	return r.Matrix, nil
 }
 
-func (r *Rest) processEndPoints(data *matrix.Matrix) error {
+func (r *Rest) processEndPoints() error {
 	var (
 		err error
 	)
@@ -419,16 +306,8 @@ func (r *Rest) processEndPoints(data *matrix.Matrix) error {
 			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")
+		if records, err = r.GetRestData(endpoint.prop, r.client); err != nil {
+			r.Logger.Error().Stack().Err(err).Msg("Failed to fetch data")
 			return err
 		}
 
@@ -452,71 +331,7 @@ func (r *Rest) processEndPoints(data *matrix.Matrix) error {
 			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 {
-					metr, ok := data.GetMetrics()[metric.name]
-					if !ok {
-						if metr, err = data.NewMetricFloat64(metric.name); err != nil {
-							r.Logger.Error().Err(err).
-								Str("name", metric.name).
-								Msg("NewMetricFloat64")
-						}
-					}
-					f := instanceData.Get(metric.name)
-					if f.Exists() {
-						metr.SetName(metric.label)
-						if err = metr.SetValueFloat64(instance, f.Float()); err != nil {
-							r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
-								Msg("Unable to set float key on metric")
-						}
-					}
-				}
-			}
-			return true
-		})
-
+		r.HandleResults(results[1], endpoint.prop)
 	}
 
 	return nil
@@ -539,12 +354,137 @@ func (r *Rest) LoadPlugin(kind string, abc *plugin.AbstractPlugin) plugin.Plugin
 		return qtree.New(abc)
 	case "Shelf":
 		return shelf.New(abc)
+	case "Snapmirror":
+		return snapmirror.New(abc)
+	case "Volume":
+		return volume.New(abc)
 	default:
 		r.Logger.Warn().Str("kind", kind).Msg("no rest plugin found ")
 	}
 	return nil
 }
 
+func (r *Rest) HandleResults(result gjson.Result, prop *prop) uint64 {
+	var (
+		err   error
+		count uint64
+	)
+
+	result.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 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 r.Params.GetChildContentS("only_cluster_instance") != "true" {
+			if instanceKey == "" {
+				return true
+			}
+		}
+
+		if instance = r.Matrix.GetInstance(instanceKey); instance == nil {
+			if instance, err = r.Matrix.NewInstance(instanceKey); err != nil {
+				r.Logger.Error().Err(err).Str("Instance key", instanceKey).Msg("")
+				return true
+			}
+		}
+
+		for label, display := range 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())
+				}
+				count++
+			} 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 prop.metrics {
+			metr, ok := r.Matrix.GetMetrics()[metric.name]
+			if !ok {
+				if metr, err = r.Matrix.NewMetricFloat64(metric.name); err != nil {
+					r.Logger.Error().Err(err).
+						Str("name", metric.name).
+						Msg("NewMetricFloat64")
+				}
+			}
+			f := instanceData.Get(metric.name)
+			if f.Exists() {
+				metr.SetName(metric.label)
+
+				var floatValue float64
+				switch metric.metricType {
+				case "duration":
+					floatValue = HandleDuration(f.String())
+				case "timestamp":
+					floatValue = HandleTimestamp(f.String())
+				case "":
+					floatValue = f.Float()
+				default:
+					r.Logger.Warn().Str("type", metric.metricType).Str("metric", metric.name).Msg("unknown metric type")
+				}
+
+				if err = metr.SetValueFloat64(instance, floatValue); err != nil {
+					r.Logger.Error().Err(err).Str("key", metric.name).Str("metric", metric.label).
+						Msg("Unable to set float key on metric")
+				}
+				count++
+			}
+		}
+
+		return true
+	})
+	return count
+}
+
+func (r *Rest) GetRestData(prop *prop, client *rest.Client) ([]interface{}, error) {
+	var (
+		err     error
+		records []interface{}
+	)
+
+	href := rest.BuildHref(prop.query, strings.Join(prop.fields, ","), nil, "", "", "", prop.returnTimeOut, prop.query)
+
+	r.Logger.Debug().Str("href", href).Msg("")
+	if href == "" {
+		return nil, errors.New(errors.ERR_CONFIG, "empty url")
+	}
+
+	err = rest.FetchData(client, href, &records)
+	if err != nil {
+		r.Logger.Error().Stack().Err(err).Str("href", href).Msg("Failed to fetch data")
+		return nil, err
+	}
+
+	return records, nil
+}
+
 // Interface guards
 var (
 	_ collector.Collector = (*Rest)(nil)
diff --git a/cmd/collectors/rest/templating.go b/cmd/collectors/rest/templating.go
index 28fbf26a..7462f518 100644
--- a/cmd/collectors/rest/templating.go
+++ b/cmd/collectors/rest/templating.go
@@ -1,10 +1,14 @@
 package rest
 
 import (
+	"fmt"
 	"goharvest2/pkg/errors"
 	"goharvest2/pkg/tree/node"
 	"goharvest2/pkg/util"
+	"regexp"
+	"strconv"
 	"strings"
+	"time"
 )
 
 func (r *Rest) LoadTemplate() error {
@@ -26,8 +30,7 @@ func (r *Rest) LoadTemplate() error {
 func (r *Rest) initCache() error {
 
 	var (
-		counters            *node.Node
-		display, name, kind string
+		counters *node.Node
 	)
 
 	if x := r.Params.GetChildContentS("object"); x != "" {
@@ -58,57 +61,147 @@ func (r *Rest) initCache() error {
 	r.prop.instanceLabels = make(map[string]string)
 	r.prop.counters = make(map[string]string)
 
-	for _, c := range counters.GetAllChildContentS() {
+	// private end point do not support * as fields. We need to pass fields in endpoint
+	query := r.Params.GetChildS("query")
+	r.prop.apiType = "public"
+	if query != nil {
+		r.prop.apiType = checkQueryType(query.GetContentS())
+	}
+
+	r.ParseRestCounters(counters, r.prop)
+
+	r.Logger.Info().Strs("extracted Instance Keys", r.prop.instanceKeys).Msg("")
+	r.Logger.Info().Int("count metrics", len(r.prop.metrics)).Int("count labels", len(r.prop.instanceLabels)).Msg("initialized metric cache")
+
+	return nil
+}
+
+func HandleDuration(value string) float64 {
+	// Example: duration: PT8H35M42S
+	timeDurationRegex := `^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:.\d+)?)S)?$`
+
+	regexTimeDuration := regexp.MustCompile(timeDurationRegex)
+	if match := regexTimeDuration.MatchString(value); match {
+		// example: PT8H35M42S   ==>  30942
+		matches := regexTimeDuration.FindStringSubmatch(value)
+		if matches == nil {
+			return 0
+		}
+
+		seconds := 0.0
+
+		//years
+		//months
+
+		//days
+		if matches[3] != "" {
+			f, err := strconv.ParseFloat(matches[3], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 24 * 60 * 60
+		}
+
+		//hours
+		if matches[4] != "" {
+			f, err := strconv.ParseFloat(matches[4], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 60 * 60
+		}
+
+		//minutes
+		if matches[5] != "" {
+			f, err := strconv.ParseFloat(matches[5], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f * 60
+		}
+
+		//seconds & milliseconds
+		if matches[6] != "" {
+			f, err := strconv.ParseFloat(matches[6], 64)
+			if err != nil {
+				fmt.Printf("%v", err)
+				return 0
+			}
+			seconds += f
+		}
+		return seconds
+	}
+
+	return 0
+}
+
+func HandleTimestamp(value string) float64 {
+	var timestamp time.Time
+	var err error
+
+	// Example: timestamp: 2020-12-02T18:36:19-08:00
+	timestampRegex := `[+-]?\d{4}(-[01]\d(-[0-3]\d(T[0-2]\d:[0-5]\d:?([0-5]\d(\.\d+)?)?[+-][0-2]\d:[0-5]\d?)?)?)?`
+
+	regexTimeStamp := regexp.MustCompile(timestampRegex)
+	if match := regexTimeStamp.MatchString(value); match {
+		// example: 2020-12-02T18:36:19-08:00   ==>  1606962979
+		if timestamp, err = time.Parse(time.RFC3339, value); err != nil {
+			fmt.Printf("%v", err)
+			return 0
+		}
+		return float64(timestamp.Unix())
+	}
+	return 0
+}
+
+func (r *Rest) ParseRestCounters(counter *node.Node, prop *prop) {
+	var (
+		display, name, kind, metricType string
+	)
+
+	for _, c := range counter.GetAllChildContentS() {
 		if c != "" {
-			name, display, kind = util.ParseMetric(c)
+			name, display, kind, metricType = util.ParseMetric(c)
 			r.Logger.Debug().
 				Str("kind", kind).
 				Str("name", name).
 				Str("display", display).
 				Msg("Collected")
 
-			r.prop.counters[name] = display
+			prop.counters[name] = display
 			switch kind {
 			case "key":
-				r.prop.instanceLabels[name] = display
-				r.prop.instanceKeys = append(r.prop.instanceKeys, name)
+				prop.instanceLabels[name] = display
+				prop.instanceKeys = append(prop.instanceKeys, name)
 			case "label":
-				r.prop.instanceLabels[name] = display
+				prop.instanceLabels[name] = display
 			case "float":
-				m := metric{label: display, name: name}
-				r.prop.metrics = append(r.prop.metrics, m)
+				m := metric{label: display, name: name, metricType: metricType}
+				prop.metrics = append(prop.metrics, m)
 			}
 		}
 	}
 
-	// private end point do not support * as fields. We need to pass fields in endpoint
-	query := r.Params.GetChildS("query")
-	r.prop.apiType = "public"
-	if query != nil {
-		r.prop.apiType = checkQueryType(query.GetContentS())
-	}
-
-	if r.prop.apiType == "private" {
-		counterKey := make([]string, len(r.prop.counters))
+	if prop.apiType == "private" {
+		counterKey := make([]string, len(prop.counters))
 		i := 0
-		for k := range r.prop.counters {
+		for k := range prop.counters {
 			counterKey[i] = k
 			i++
 		}
-		r.prop.fields = counterKey
+		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()...)
+	if prop.apiType == "public" {
+		prop.fields = []string{"*"}
+		if counter != nil {
+			if x := counter.GetChildS("hidden_fields"); x != nil {
+				prop.fields = append(prop.fields, x.GetAllChildContentS()...)
 			}
 		}
 	}
 
-	r.Logger.Info().Strs("extracted Instance Keys", r.prop.instanceKeys).Msg("")
-	r.Logger.Info().Int("count metrics", len(r.prop.metrics)).Int("count labels", len(r.prop.instanceLabels)).Msg("initialized metric cache")
-
-	return nil
 }
diff --git a/cmd/collectors/zapi/plugins/quota/quota.go b/cmd/collectors/zapi/plugins/quota/quota.go
index bb97aca8..8967ea37 100644
--- a/cmd/collectors/zapi/plugins/quota/quota.go
+++ b/cmd/collectors/zapi/plugins/quota/quota.go
@@ -79,7 +79,7 @@ func (my *Quota) Init() error {
 	}
 
 	for _, obj := range objects.GetAllChildContentS() {
-		metricName, display, _ := util.ParseMetric(obj)
+		metricName, display, _, _ := util.ParseMetric(obj)
 
 		metric, err := my.data.NewMetricFloat64(metricName)
 		if err != nil {
diff --git a/cmd/collectors/zapi/plugins/shelf/shelf.go b/cmd/collectors/zapi/plugins/shelf/shelf.go
index 9776bcc6..21290838 100644
--- a/cmd/collectors/zapi/plugins/shelf/shelf.go
+++ b/cmd/collectors/zapi/plugins/shelf/shelf.go
@@ -90,7 +90,7 @@ func (my *Shelf) Init() error {
 
 			for _, c := range x.GetAllChildContentS() {
 
-				metricName, display, kind := util.ParseMetric(c)
+				metricName, display, kind, _ := util.ParseMetric(c)
 
 				switch kind {
 				case "key":
diff --git a/cmd/tools/doctor/restZapiDiff.go b/cmd/tools/doctor/restZapiDiff.go
index be7e78d6..ce7e7569 100644
--- a/cmd/tools/doctor/restZapiDiff.go
+++ b/cmd/tools/doctor/restZapiDiff.go
@@ -128,12 +128,10 @@ func metricValueDiff(metricName string) {
 		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.node")
 		keyIndexes = []int{2}
 	}
-	// ignore qtree for now as it is under development
-	//if strings.HasPrefix(metricName, "qtree_") && !strings.HasPrefix(metricName, "qtree_id") {
-	//	results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter","data.result.#.metric.qtree")
-	//keyIndexes = []int{2}
-	//}
-
+	if strings.HasPrefix(metricName, "qtree_") && !strings.HasPrefix(metricName, "qtree_id") {
+		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.qtree")
+		keyIndexes = []int{2}
+	}
 	if strings.HasPrefix(metricName, "environment_sensor_") {
 		results = gjson.GetMany(data, "data.result.#.value.1", "data.result.#.metric.datacenter", "data.result.#.metric.sensor", "data.result.#.metric.node")
 		keyIndexes = []int{2, 3}
@@ -224,7 +222,8 @@ func labelValueDiff(label string, labelNames []string) {
 	skipMatch := make([]string, 0)
 	skipMatch = append(skipMatch, "datacenter")
 
-	if strings.HasPrefix(label, "disk_") || strings.HasPrefix(label, "shelf_") {
+	// To ignore child labels(ex: shelf_psu_labels) from shelf object, following exact comparison
+	if strings.HasPrefix(label, "disk_") || strings.Compare(label, "shelf_labels") == 0 {
 		keyIndexes = append(keyIndexes, IndexOf(finalLabelNames, "serial_number"))
 		dataCenterIndex = IndexOf(finalLabelNames, "datacenter")
 		results = gjson.GetMany(data, prefixLabelsName...)
diff --git a/cmd/tools/grafana/README.md b/cmd/tools/grafana/README.md
index bdf2d693..a416b597 100644
--- a/cmd/tools/grafana/README.md
+++ b/cmd/tools/grafana/README.md
@@ -16,7 +16,7 @@ $ bin/grafana import --addr my.grafana.server:3000
 
 Similarly, to export:
 ```
-$ bin/grafana export --addr my.grafana.server:3000 --directory /path/to/export/directory
+$ bin/grafana export --addr my.grafana.server:3000 --directory /path/to/export/directory --serverfolder grafanaFolderName
 ```
 
 By default, the dashboards are connected to the `Prometheus` datasource defined in Grafana. If your datasource has a different name, use the `--datasource` flag during import/export.
diff --git a/cmd/tools/grafana/grafana.go b/cmd/tools/grafana/grafana.go
index bf2a7562..dcb319cf 100644
--- a/cmd/tools/grafana/grafana.go
+++ b/cmd/tools/grafana/grafana.go
@@ -34,57 +34,146 @@ const (
 )
 
 var (
-	grafanaMinVers          = "7.1.0" // lowest grafana version we require
-	homePath                string
-	harvestRelease          = version.VERSION
-	grafanaFolderTitle      = "Harvest " + harvestRelease + " - cDOT"
-	grafana7modeFolderTitle = "Harvest " + harvestRelease + " - 7-mode"
+	grafanaMinVers = "7.1.0" // lowest grafana version we require
+	homePath       string
+	harvestRelease = version.VERSION
 )
 
 type options struct {
-	command        string // one of: import, export, clean
-	addr           string // URL of Grafana server (e.g. "http://localhost:3000")
-	token          string // API token issued by Grafana server
-	dir            string // Local directory for import/export-ing cDot & 7mode dashboards (e.g. "opt/harvest/grafana/dashboards")
-	dircDOT        string // Local directory for import/export-ing cDOT dashboards (e.g. "opt/harvest/grafana/dashboards/cmode")
-	dir7mode       string // Local directory for import/export-ing 7mode dashboards (e.g. "opt/harvest/grafana/dashboards/7mode")
-	cmodeFolder    Folder // Server-side Grafana folder name for cDOT dashboards
-	mode7Folder    Folder // Server-side Grafana folder name for 7-mode dashboards
-	datasource     string
-	variable       bool
-	client         *http.Client
-	headers        http.Header
-	config         string
-	prefix         string
-	useHttps       bool
-	useInsecureTLS bool
-	labels         []string
+	command             string // one of: import, export, clean
+	addr                string // URL of Grafana server (e.g. "http://localhost:3000")
+	token               string // API token issued by Grafana server
+	dir                 string
+	serverfolder        Folder
+	datasource          string
+	variable            bool
+	client              *http.Client
+	headers             http.Header
+	config              string
+	prefix              string
+	useHttps            bool
+	useInsecureTLS      bool
+	labels              []string
+	dirGrafanaFolderMap map[string]*Folder
 }
 
 type Folder struct {
-	folderName string // Grafana folder where to upload from where to download dashboards
-	folderId   int64
-	folderUid  string
+	name string // Grafana folder where to upload from where to download dashboards
+	id   int64
+	uid  string
+}
+
+func adjustOptions() {
+	opts.config = conf.ConfigPath(opts.config)
+	homePath = conf.GetHarvestHomePath()
+	opts.dirGrafanaFolderMap = make(map[string]*Folder)
+
+	// When opt.addr starts with https don't change it
+	if !strings.HasPrefix(opts.addr, "https://") {
+		opts.addr = strings.TrimPrefix(opts.addr, "http://")
+		opts.addr = strings.TrimPrefix(opts.addr, "https://")
+		opts.addr = strings.TrimSuffix(opts.addr, "/")
+		if opts.useHttps {
+			opts.addr = "https://" + opts.addr
+		} else {
+			opts.addr = "http://" + opts.addr
+		}
+	}
+}
+
+func askForToken() {
+	// ask for API token if not provided as arg and validate
+	if err := checkToken(opts, false); err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
 }
 
 func doExport(_ *cobra.Command, _ []string) {
 	adjustOptions()
+	exitIfExist(opts.dir, "directory")
 	askForToken()
-	var doesFolderExist = doesGrafanaFolderExist()
-	var err error
+	initExportVars()
+	fmt.Printf("preparing to export dashboards from serverfolder %s directory %s\n", opts.serverfolder.name, opts.dir)
+	exportDashboards(opts)
+}
 
-	if !(doesFolderExist[&opts.cmodeFolder] && doesFolderExist[&opts.mode7Folder]) {
-		if !doesFolderExist[&opts.cmodeFolder] {
-			fmt.Printf("folder [%s] not found in Grafana\n", opts.cmodeFolder.folderName)
-		}
-		if !doesFolderExist[&opts.mode7Folder] {
-			fmt.Printf("folder [%s] not found in Grafana\n", opts.mode7Folder.folderName)
+func initExportVars() {
+	opts.dirGrafanaFolderMap[opts.dir] = &Folder{name: opts.serverfolder.name}
+}
+
+func exportDashboards(opts *options) {
+	for k, v := range opts.dirGrafanaFolderMap {
+		err := exportFiles(k, v)
+		if err != nil {
+			fmt.Printf("Error during export %v\n", err)
 		}
+	}
+}
+
+func exportFiles(dir string, folder *Folder) error {
+	var (
+		err   error
+		uids  map[string]string
+		count int
+	)
+
+	err = checkFolder(folder)
+	if err != nil {
+		fmt.Printf("folder %s error %v\n", folder.name, err)
 		os.Exit(1)
-	} else if err = exportDashboards(opts); err != nil {
-		fmt.Println(err)
+	}
+	if folder.id == 0 {
+		fmt.Printf("error folder %s doesn't exist in grafana, unable to continue\n", folder.name)
 		os.Exit(1)
 	}
+	fmt.Printf("querying for content of folder name [%s]\n", folder.name)
+
+	result, status, code, err := sendRequestArray(opts, "GET", "/api/search?folderIds="+strconv.FormatInt(folder.id, 10), nil)
+	if err != nil && code != 200 {
+		fmt.Printf("server response [%d: %s]: %v\n", code, status, err)
+		return err
+	}
+
+	uids = make(map[string]string)
+	rep := strings.NewReplacer("/", "_", "-", "_")
+	for _, elem := range result {
+		uid := elem["uid"].(string)
+		uri := elem["uri"].(string)
+		if uid != "" && uri != "" {
+			uids[uid] = rep.Replace(uri)
+		}
+	}
+
+	if err = os.MkdirAll(dir, 0755); err != nil {
+		fmt.Printf("error makedir [%s]: %v\n", dir, err)
+		return err
+	}
+	fmt.Printf("exporting dashboards to directory [%s]\n", dir)
+
+	for uid, uri := range uids {
+		result, status, code, err := sendRequest(opts, "GET", "/api/dashboards/uid/"+uid, nil)
+		if err != nil {
+			fmt.Printf("error requesting [%s]: [%d: %s] %v\n", uid, code, status, err)
+			return err
+		}
+		if dashboard, ok := result["dashboard"]; ok {
+			fp := path.Join(dir, uri+".json")
+			data, err := json.Marshal(dashboard)
+			if err != nil {
+				fmt.Printf("error marshall dashboard [%s]: %v\n\n", uid, err)
+				return err
+			}
+			if err = ioutil.WriteFile(fp, data, 0644); err != nil {
+				fmt.Printf("error write to [%s]: %v\n", fp, err)
+				return err
+			}
+			fmt.Printf("OK - exported [%s]\n", fp)
+			count++
+		}
+	}
+	fmt.Printf("exported %d dashboards to [%s]\n", count, dir)
+	return nil
 }
 
 func addLabel(content []byte, label string, labelMap map[string]string) []byte {
@@ -236,9 +325,11 @@ func doImport(_ *cobra.Command, _ []string) {
 	if err != nil {
 		return
 	}
+
 	adjustOptions()
+	validateImport()
 	askForToken()
-	checkAndCreateFolder()
+	initImportVars()
 
 	fmt.Printf("preparing to import dashboards...\n")
 	if err := importDashboards(opts); err != nil {
@@ -247,153 +338,85 @@ func doImport(_ *cobra.Command, _ []string) {
 	}
 }
 
-func checkAndCreateFolder() {
-	var folderExists = doesGrafanaFolderExist()
-	var err error
-
-	// first folder is Harvest 2.0 and second folder is 7mode
-	for folder, value := range folderExists {
-		if value {
-			fmt.Printf("folder [%s] exists in Grafana - OK\n", folder.folderName)
-		} else if err = createFolder(opts, folder); err != nil {
-			fmt.Println(err)
-			os.Exit(1)
-		} else {
-			fmt.Printf("created Grafana folder [%s] - OK\n", folder.folderName)
-		}
+func validateImport() {
+	// default case
+	if opts.dir == "" && opts.serverfolder.name == "" {
+		opts.dir = "grafana/dashboards"
+		opts.dir = path.Join(homePath, opts.dir)
 	}
 
-}
-
-func doesGrafanaFolderExist() map[*Folder]bool {
-	folderExists := make(map[*Folder]bool)
-	var err error
-	if folderExists, err = checkFolder(opts); err != nil {
-		fmt.Println(err)
+	exitIfMissing(opts.dir, "directory")
+	files, err := ioutil.ReadDir(opts.dir)
+	if err != nil {
+		fmt.Printf("Error %v while reading dir [%s] is the directory correct?\n", err, opts.dir)
 		os.Exit(1)
 	}
-	return folderExists
-}
-
-func askForToken() {
-	// ask for API token if not provided as arg and validate
-	if err := checkToken(opts, false); err != nil {
-		fmt.Println(err)
+	if len(files) == 0 {
+		fmt.Printf("No dashboards found in [%s] is the directory correct?\n", opts.dir)
 		os.Exit(1)
 	}
 }
 
-func adjustOptions() {
-	opts.config = conf.ConfigPath(opts.config)
-	homePath = conf.GetHarvestHomePath()
-	if opts.dircDOT != "" {
-		opts.dircDOT = path.Join(homePath, opts.dircDOT)
-		exitIfMissing(opts.dircDOT, "directory-cdot")
-	}
-	if opts.dir7mode != "" {
-		opts.dir7mode = path.Join(homePath, opts.dir7mode)
-		exitIfMissing(opts.dir7mode, "directory-7mode")
-	}
-	if opts.dircDOT == "" && opts.dir7mode == "" {
-		opts.dir = path.Join(homePath, opts.dir)
-		exitIfMissing(opts.dir, "directory")
-		opts.dircDOT = path.Join(opts.dir, "/cmode")
-		opts.dir7mode = path.Join(opts.dir, "/7mode")
+func initImportVars() {
+	m := make(map[string]*Folder)
+
+	// default behaviour
+	if opts.dir == "grafana/dashboards" && opts.serverfolder.name == "" {
+		m[path.Join(opts.dir, "/cmode")] = &Folder{name: "Harvest-" + harvestRelease + "-cDOT"}
+		m[path.Join(opts.dir, "/7mode")] = &Folder{name: "Harvest-" + harvestRelease + "-7mode"}
+	} else if opts.dir != "" && opts.serverfolder.name != "" {
+		m[opts.dir] = &Folder{name: opts.serverfolder.name}
 	}
 
-	// When opt.addr starts with https don't change it
-	if !strings.HasPrefix(opts.addr, "https://") {
-		opts.addr = strings.TrimPrefix(opts.addr, "http://")
-		opts.addr = strings.TrimPrefix(opts.addr, "https://")
-		opts.addr = strings.TrimSuffix(opts.addr, "/")
-		if opts.useHttps {
-			opts.addr = "https://" + opts.addr
-		} else {
-			opts.addr = "http://" + opts.addr
+	for k, v := range m {
+		err := checkAndCreateServerFolder(v)
+		if err != nil {
+			fmt.Print(err)
+			os.Exit(1)
 		}
+		opts.dirGrafanaFolderMap[k] = v
 	}
 }
 
-func exitIfMissing(fp string, s string) {
-	if _, err := os.Stat(fp); os.IsNotExist(err) {
-		fmt.Printf("error: %s file [%s] does not exist.\n", s, fp)
+func checkAndCreateServerFolder(folder *Folder) error {
+	err := checkFolder(folder)
+	if err != nil {
+		fmt.Printf("error %v for folder %s\n", err, folder.name)
 		os.Exit(1)
 	}
-}
 
-func exportDashboards(opts *options) error {
-	// Exporting C mode dashboards
-	_ = exportFiles(opts.cmodeFolder)
-	// Exporting 7mode dashboards
-	_ = exportFiles(opts.mode7Folder)
-	return nil
-}
-
-func exportFiles(folder Folder) error {
-	var (
-		err   error
-		uids  map[string]string
-		dir   string
-		count int
-	)
-
-	fmt.Printf("querying for content of folder id [%d]\n", folder.folderId)
-
-	result, status, code, err := sendRequestArray(opts, "GET", "/api/search?folderIds="+strconv.FormatInt(folder.folderId, 10), nil)
-	if err != nil && code != 200 {
-		fmt.Printf("server response [%d: %s]: %v\n", code, status, err)
+	if folder.uid != "" && folder.id != 0 {
+		fmt.Printf("folder [%s] exists in Grafana - OK\n", folder.name)
+	} else if err = createServerFolder(folder); err != nil {
 		return err
-	}
-
-	uids = make(map[string]string)
-	for _, elem := range result {
-		uid := elem["uid"].(string)
-		uri := elem["uri"].(string)
-		if uid != "" && uri != "" {
-			uids[uid] = strings.ReplaceAll(strings.ReplaceAll(uri, "/", "_"), "-", "_")
-		}
-	}
-
-	if opts.dir == "" {
-		dir = path.Join("./", strings.ReplaceAll(folder.folderName, " ", "_"))
 	} else {
-		dir = path.Join(opts.dir, strings.ReplaceAll(folder.folderName, " ", "_"))
+		fmt.Printf("created Grafana folder [%s] - OK\n", folder.name)
 	}
-	if err = os.MkdirAll(dir, 0755); err != nil {
-		fmt.Printf("error makedir [%s]: %v\n", dir, err)
-		return err
+	return nil
+}
+
+func exitIfMissing(fp string, s string) {
+	if _, err := os.Stat(fp); os.IsNotExist(err) {
+		fmt.Printf("error: %s [%s] does not exist.\n", s, fp)
+		os.Exit(1)
 	}
-	fmt.Printf("exporting dashboards to directory [%s]\n", dir)
+}
 
-	for uid, uri := range uids {
-		if result, status, code, err := sendRequest(opts, "GET", "/api/dashboards/uid/"+uid, nil); err != nil {
-			fmt.Printf("error requesting [%s]: [%d: %s] %v\n", uid, code, status, err)
-			return err
-		} else if dashboard, ok := result["dashboard"]; ok {
-			fp := path.Join(dir, uri+".json")
-			if data, err := json.Marshal(dashboard); err != nil {
-				fmt.Printf("error marshall dashboard [%s]: %v\n\n", uid, err)
-				return err
-			} else if err = ioutil.WriteFile(fp, data, 0644); err != nil {
-				fmt.Printf("error write to [%s]: %v\n", fp, err)
-				return err
-			} else {
-				fmt.Printf("OK - exported [%s]\n", fp)
-				count++
-			}
-		}
+func exitIfExist(fp string, s string) {
+	if _, err := os.Stat(fp); err == nil {
+		fmt.Printf("error: %s folder [%s] exists. Harvest creates a new folder and avoids overwriting existing one\n", s, fp)
+		os.Exit(1)
 	}
-	fmt.Printf("exported %d dashboards to [%s]\n", count, dir)
-	return nil
 }
 
 func importDashboards(opts *options) error {
-	importFiles(opts.dircDOT, opts.cmodeFolder)
-	importFiles(opts.dir7mode, opts.mode7Folder)
+	for k, v := range opts.dirGrafanaFolderMap {
+		importFiles(k, v)
+	}
 	return nil
 }
 
-func importFiles(dir string, folder Folder) {
+func importFiles(dir string, folder *Folder) {
 	var (
 		request, dashboard map[string]interface{}
 		files              []os.FileInfo
@@ -409,7 +432,8 @@ func importFiles(dir string, folder Folder) {
 		return
 	}
 
-	releaseVersion := strings.ReplaceAll(harvestRelease, ".", "-")
+	uidSuffix := strings.ReplaceAll(harvestRelease+"-"+folder.name, ".", "-")
+
 	for _, file := range files {
 		if !strings.HasSuffix(file.Name(), ".json") {
 			continue
@@ -424,7 +448,12 @@ func importFiles(dir string, folder Folder) {
 
 		// Updating the uid of dashboards based in the release
 		uid := gjson.GetBytes(data, "uid").String()
-		data, err = sjson.SetBytes(data, "uid", []byte(uid+"-"+releaseVersion))
+		uid = uid + "-" + uidSuffix
+		// uid length can be max 40 chars
+		if len(uid) > 40 {
+			uid = uid[:40]
+		}
+		data, err = sjson.SetBytes(data, "uid", []byte(uid))
 		if err != nil {
 			fmt.Printf("error while updating the uid %s into dashboard %s, err: %+v", uid, file.Name(), err)
 			continue
@@ -465,7 +494,7 @@ func importFiles(dir string, folder Folder) {
 
 		request = make(map[string]interface{})
 		request["overwrite"] = true
-		request["folderId"] = folder.folderId
+		request["folderId"] = folder.id
 		request["dashboard"] = dashboard
 
 		result, status, code, err := sendRequest(opts, "POST", "/api/dashboards/db", request)
@@ -479,11 +508,11 @@ func importFiles(dir string, folder Folder) {
 			fmt.Printf("error importing [%s] - server response (%d - %s) %v\n", file.Name(), code, status, result)
 			return
 		}
-		fmt.Printf("OK - imported %s / [%s]\n", folder.folderName, file.Name())
+		fmt.Printf("OK - imported %s / [%s]\n", folder.name, file.Name())
 		importedFiles++
 	}
 	if importedFiles > 0 {
-		fmt.Printf("Imported %d dashboards to [%s] from [%s]\n", importedFiles, folder.folderName, dir)
+		fmt.Printf("Imported %d dashboards to [%s] from [%s]\n", importedFiles, folder.name, dir)
 	} else {
 		fmt.Printf("No dashboards found in [%s] is the directory correct?\n", dir)
 	}
@@ -725,14 +754,9 @@ func checkVersion(inputVersion string) bool {
 	return satisfies
 }
 
-func createFolder(opts *options, folder *Folder) error {
+func checkFolder(folder *Folder) error {
 
-	var request map[string]interface{}
-	request = make(map[string]interface{})
-
-	request["title"] = folder.folderName
-
-	result, status, code, err := sendRequest(opts, "POST", "/api/folders", request)
+	result, status, code, err := sendRequestArray(opts, "GET", "/api/folders?limit=1000", nil)
 
 	if err != nil {
 		return err
@@ -742,51 +766,47 @@ func createFolder(opts *options, folder *Folder) error {
 		return errors.New("server response: " + status)
 	}
 
-	folder.folderId = int64(result["id"].(float64))
-	folder.folderUid = result["uid"].(string)
-
-	return nil
-}
-
-func checkFolder(opts *options) (map[*Folder]bool, error) {
-
-	result, status, code, err := sendRequestArray(opts, "GET", "/api/folders?limit=1000", nil)
-
-	if err != nil {
-		return map[*Folder]bool{&opts.cmodeFolder: false, &opts.mode7Folder: false}, err
-	}
-
-	if code != 200 {
-		return map[*Folder]bool{&opts.cmodeFolder: false, &opts.mode7Folder: false}, errors.New("server response: " + status)
-	}
-
 	if len(result) == 0 {
-		return map[*Folder]bool{&opts.cmodeFolder: false, &opts.mode7Folder: false}, nil
+		return nil
 	}
 
-	cmodeFolder := folderExist(result, &opts.cmodeFolder)
-	nonCmodeFolder := folderExist(result, &opts.mode7Folder)
-
-	return map[*Folder]bool{&opts.cmodeFolder: cmodeFolder, &opts.mode7Folder: nonCmodeFolder}, nil
-}
-
-func folderExist(result []map[string]interface{}, folder *Folder) bool {
 	for _, x := range result {
-
 		if name, ok := x["title"]; ok {
-			if name.(string) == folder.folderName {
+			if name.(string) == folder.name {
 				if id, idExist := x["id"]; idExist {
-					folder.folderId = int64(id.(float64))
+					folder.id = int64(id.(float64))
 					if uid, uidExist := x["uid"]; uidExist {
-						folder.folderUid = uid.(string)
-						return true
+						folder.uid = uid.(string)
 					}
 				}
 			}
 		}
+	}
+
+	return nil
+}
+
+func createServerFolder(folder *Folder) error {
+
+	var request map[string]interface{}
+	request = make(map[string]interface{})
 
+	request["title"] = folder.name
+
+	result, status, code, err := sendRequest(opts, "POST", "/api/folders", request)
+
+	if err != nil {
+		return err
 	}
-	return false
+
+	if code != 200 {
+		return errors.New("server response: " + status)
+	}
+
+	folder.id = int64(result["id"].(float64))
+	folder.uid = result["uid"].(string)
+
+	return nil
 }
 
 func sendRequest(opts *options, method, url string, query map[string]interface{}) (map[string]interface{}, string, int, error) {
@@ -876,8 +896,17 @@ var Cmd = &cobra.Command{
 }
 
 var importCmd = &cobra.Command{
-	Use:     "import",
-	Short:   "import Grafana dashboards",
+	Use:   "import",
+	Short: "import Grafana dashboards",
+	// Added so directory and serverfolder are required arguments except when both are empty. When both are empty use long accepted Harvest defaults
+	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+		dir := cmd.Flags().Lookup("directory")
+		folder := cmd.Flags().Lookup("serverfolder")
+		if dir.Value.String() == "" && folder.Value.String() == "" {
+			dir.Changed = true
+			folder.Changed = true
+		}
+	},
 	Run:     doImport,
 	Example: "grafana import --addr my.grafana.server:3000",
 }
@@ -886,7 +915,7 @@ var exportCmd = &cobra.Command{
 	Use:     "export",
 	Short:   "export Grafana dashboards",
 	Run:     doExport,
-	Example: "grafana export --addr my.grafana.server:3000 --directory exported_dash",
+	Example: "grafana export --addr my.grafana.server:3000 --directory exportdirectory --serverfolder grafanafoldername",
 }
 
 func init() {
@@ -895,17 +924,17 @@ func init() {
 	Cmd.PersistentFlags().StringVar(&opts.config, "config", "./harvest.yml", "harvest config file path")
 	Cmd.PersistentFlags().StringVarP(&opts.addr, "addr", "a", "http://127.0.0.1:3000", "Address of Grafana server (IP, FQDN or hostname)")
 	Cmd.PersistentFlags().StringVarP(&opts.token, "token", "t", "", "API token issued by Grafana server for authentication")
-	Cmd.PersistentFlags().StringVarP(&opts.dir, "directory", "d", "grafana/dashboards", "When importing, import cDOT and 7mode dashboards from this local directory.\nWhen exporting, local directory to write dashboards to")
-	Cmd.PersistentFlags().StringVar(&opts.dircDOT, "directory-cdot", "", "When importing, import cDOT dashboards from this local directory.\nWhen exporting, local directory to write dashboards to")
-	Cmd.PersistentFlags().StringVar(&opts.dir7mode, "directory-7mode", "", "When importing, import 7-mode dashboards from this local directory.\nWhen exporting, local directory to write dashboards to")
-	Cmd.PersistentFlags().StringVarP(&opts.cmodeFolder.folderName, "folder", "f", grafanaFolderTitle, "Grafana folder name for the cDOT dashboards")
-	Cmd.PersistentFlags().StringVarP(&opts.mode7Folder.folderName, "folder-7mode", "", grafana7modeFolderTitle, "Grafana folder name for the 7-mode dashboards")
 	Cmd.PersistentFlags().StringVarP(&opts.prefix, "prefix", "p", "", "Use global metric prefix in queries")
 	Cmd.PersistentFlags().StringVarP(&opts.datasource, "datasource", "s", grafanaDataSource, "Grafana datasource for the dashboards")
 	Cmd.PersistentFlags().BoolVarP(&opts.variable, "variable", "v", false, "Use datasource as variable, overrides: --datasource")
 	Cmd.PersistentFlags().BoolVarP(&opts.useHttps, "https", "S", false, "Use HTTPS")
 	Cmd.PersistentFlags().BoolVarP(&opts.useInsecureTLS, "insecure", "k", false, "Allow insecure server connections when using SSL")
+	Cmd.PersistentFlags().StringVarP(&opts.serverfolder.name, "serverfolder", "f", "", "Grafana folder name for dashboards")
+	Cmd.PersistentFlags().StringVarP(&opts.dir, "directory", "d", "", "When importing, import dashboards from this local directory.\nWhen exporting, local directory to write dashboards to")
 
 	importCmd.PersistentFlags().StringSliceVar(&opts.labels, "labels", nil,
 		"For each label, create a variable and add as chained query to other variables")
+
+	_ = Cmd.MarkPersistentFlagRequired("serverfolder")
+	_ = Cmd.MarkPersistentFlagRequired("directory")
 }
diff --git a/conf/rest/9.11.0/snapmirror.yaml b/conf/rest/9.11.0/snapmirror.yaml
deleted file mode 100644
index 087f6a2a..00000000
--- a/conf/rest/9.11.0/snapmirror.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-
-name:                         SnapMirror
-query:                        api/snapmirror/relationships
-object:                       snapmirror
-
-counters:
-  - ^^uuid => relationship_id
-  - ^destination.path  => destination_volume
-  - ^destination.svm.name => destination_vserver
-  - ^healthy
-  - ^transfer.bytes_transferred  => last_transfer_size
-  - ^transfer.state => relationship_status
-  - ^source.path => source_volume
-  - ^source.svm.name => source_vserver
-  #- ^source-node #missing mapping  ==> Needs plugin development
-  - ^unhealthy_reason.#.message => unhealthy_reason
-  - lag_time
-  - transfer.total_duration => last_transfer_duration
-  - transfer.end_time => last_transfer_end_timestamp
-  - transfer.bytes_transferred  => last_transfer_size
-
-
-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
-      - ^policy_type => policy_type
-      - ^destination_path => destination_location
-      - total_transfer_time_secs => total_transfer_time_secs
-      - total_transfer_bytes => total_transfer_bytes
-      - 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:
-    - relationship_id
-    - destination_volume
-    - destination_node
-    - destination_vserver
-    - source_volume
-    - source_vserver
-    - source_node
-  instance_labels:
-    - healthy
-    - unhealthy_reason
-    - last_transfer_type
-    - relationship_status
-    - relationship_type
-    - schedule
-    - destination_node_limit
-    - source_node_limit
-    - group_type
-    - policy_type
-    - destination_location
diff --git a/conf/rest/9.11.0/aggr.yaml b/conf/rest/9.12.0/aggr.yaml
similarity index 100%
rename from conf/rest/9.11.0/aggr.yaml
rename to conf/rest/9.12.0/aggr.yaml
diff --git a/conf/rest/9.11.0/disk.yaml b/conf/rest/9.12.0/disk.yaml
similarity index 100%
rename from conf/rest/9.11.0/disk.yaml
rename to conf/rest/9.12.0/disk.yaml
diff --git a/conf/rest/9.11.0/lun.yaml b/conf/rest/9.12.0/lun.yaml
similarity index 100%
rename from conf/rest/9.11.0/lun.yaml
rename to conf/rest/9.12.0/lun.yaml
diff --git a/conf/rest/9.11.0/node.yaml b/conf/rest/9.12.0/node.yaml
similarity index 82%
rename from conf/rest/9.11.0/node.yaml
rename to conf/rest/9.12.0/node.yaml
index 99c07753..4fcd4f9b 100644
--- a/conf/rest/9.11.0/node.yaml
+++ b/conf/rest/9.12.0/node.yaml
@@ -9,9 +9,6 @@ counters:
   - ^controller.failed_power_supply.message.message => failed_power_message
   - ^controller.over_temperature => over_temperature
   - ^state => healthy
-  #- ^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
   - ^location
   - ^model
   - ^serial_number => serial
@@ -28,6 +25,9 @@ endpoints:
     counters:
       - ^^node => node
       - ^vendor  => vendor
+      - ^max_aggr_size => max_aggr_size
+      - ^max_node_vvols => max_vol_num
+      - ^max_vol_size => max_vol_size
 
 plugins:
   - LabelAgent:
diff --git a/conf/rest/9.11.0/port.yaml b/conf/rest/9.12.0/port.yaml
similarity index 100%
rename from conf/rest/9.11.0/port.yaml
rename to conf/rest/9.12.0/port.yaml
diff --git a/conf/rest/9.11.0/qtree.yaml b/conf/rest/9.12.0/qtree.yaml
similarity index 100%
rename from conf/rest/9.11.0/qtree.yaml
rename to conf/rest/9.12.0/qtree.yaml
diff --git a/conf/rest/9.11.0/sensor.yaml b/conf/rest/9.12.0/sensor.yaml
similarity index 90%
rename from conf/rest/9.11.0/sensor.yaml
rename to conf/rest/9.12.0/sensor.yaml
index 9fc2a131..8bb03d85 100644
--- a/conf/rest/9.11.0/sensor.yaml
+++ b/conf/rest/9.12.0/sensor.yaml
@@ -12,7 +12,7 @@ counters:
   - ^discrete_value  => discrete_value
   - ^threshold_state  => threshold_state
   - ^value_units => unit
-  - value  => threshold_value
+  - threshold_value  => threshold_value
   - critical_high_threshold => critical_high
   - critical_low_threshold  => critical_low
 
diff --git a/conf/rest/9.11.0/shelf.yaml b/conf/rest/9.12.0/shelf.yaml
similarity index 100%
rename from conf/rest/9.11.0/shelf.yaml
rename to conf/rest/9.12.0/shelf.yaml
diff --git a/conf/rest/9.12.0/snapmirror.yaml b/conf/rest/9.12.0/snapmirror.yaml
new file mode 100644
index 00000000..ca3b2d73
--- /dev/null
+++ b/conf/rest/9.12.0/snapmirror.yaml
@@ -0,0 +1,64 @@
+# As private cli api has all the fields/metrics which we would need for our use-case, So using private api instead of public.
+
+name:                         SnapMirror
+query:                        api/private/cli/snapmirror
+object:                       snapmirror
+
+counters:
+  - ^^relationship_id => relationship_id
+  - ^destination_path => destination_location
+  - ^destination_volume_node => destination_node
+  - ^destination_vserver => destination_vserver
+  - ^destination_volume => destination_volume
+  - ^source_vserver => source_vserver
+  - ^source_volume => source_volume
+  - ^type => relationship_type
+  - ^state => relationship_status
+  - ^policy_type => policy_type
+  - ^relationship_group_type  => group_type
+  - ^schedule => schedule
+  - ^healthy => healthy
+  - ^last_transfer_type => last_transfer_type
+  - ^unhealthy_reason => unhealthy_reason
+  - lag_time(duration) => lag_time
+  - last_transfer_duration(duration) => last_transfer_duration
+  - last_transfer_end_timestamp(timestamp) => last_transfer_end_timestamp
+  - last_transfer_size => last_transfer_size
+  - total_transfer_time_secs => total_transfer_time_secs
+  - total_transfer_bytes => total_transfer_bytes
+  - newest_snapshot_timestamp(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
+
+
+plugins:
+  - Snapmirror
+
+export_options:
+  instance_keys:
+    - relationship_id
+    - destination_volume
+    - destination_node
+    - destination_vserver
+    - source_volume
+    - source_vserver
+    - source_node
+  instance_labels:
+    - healthy
+    - unhealthy_reason
+    - last_transfer_type
+    - relationship_status
+    - relationship_type
+    - schedule
+#    - destination_node_limit
+#    - source_node_limit
+    - group_type
+    - destination_location
+    - protectedBy
+    - protectionSourceType
+    - policy_type
+    - derived_relationship_type
diff --git a/conf/rest/9.11.0/snapshot.yaml b/conf/rest/9.12.0/snapshot.yaml
similarity index 100%
rename from conf/rest/9.11.0/snapshot.yaml
rename to conf/rest/9.12.0/snapshot.yaml
diff --git a/conf/rest/9.11.0/status.yaml b/conf/rest/9.12.0/status.yaml
similarity index 89%
rename from conf/rest/9.11.0/status.yaml
rename to conf/rest/9.12.0/status.yaml
index ce669ae5..dfb2f860 100644
--- a/conf/rest/9.11.0/status.yaml
+++ b/conf/rest/9.12.0/status.yaml
@@ -2,11 +2,11 @@
 name:       Status
 query:      api/cluster
 object:     cluster
-fields:
-  - health
 
 counters:
   - ^health => status
+  - hidden_fields:
+      - health
 
 collect_only_labels: true
 only_cluster_instance: true
diff --git a/conf/rest/9.11.0/subsystem.yaml b/conf/rest/9.12.0/subsystem.yaml
similarity index 100%
rename from conf/rest/9.11.0/subsystem.yaml
rename to conf/rest/9.12.0/subsystem.yaml
diff --git a/conf/rest/9.11.0/volume.yaml b/conf/rest/9.12.0/volume.yaml
similarity index 94%
rename from conf/rest/9.11.0/volume.yaml
rename to conf/rest/9.12.0/volume.yaml
index a6674f1a..318c6069 100644
--- a/conf/rest/9.11.0/volume.yaml
+++ b/conf/rest/9.12.0/volume.yaml
@@ -54,6 +54,9 @@ endpoints:
       - files_used => inode_files_used
 
 plugins:
+  - Volume:
+      schedule:
+        - data: 900s  # should be multiple of data poll duration
   - LabelAgent:
       value_to_num:
         - new_status state online online `0`
@@ -76,5 +79,9 @@ export_options:
     - is_sis_volume
     - snapshot_policy
     - type
+    - protectedByStatus
+    - protectedBy
+    - protectionRole
+    - all_sm_healthy
 
 
diff --git a/pkg/util/util.go b/pkg/util/util.go
index e3e016dc..0e2e4093 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -328,17 +328,20 @@ func Intersection(a []string, b []string) ([]string, []string) {
 	return matches, misses
 }
 
-// ParseMetric parses display name and type of field from the raw name of the metric as defined in (sub)template.
+// ParseMetric parses display name and type of field and metric type from the raw name of the metric as defined in (sub)template.
 // Users can rename a metric with "=>" (e.g. some_long_metric_name => short).
 // Trailing "^" characters are ignored/cleaned as they have special meaning in some collectors.
-func ParseMetric(rawName string) (string, string, string) {
+func ParseMetric(rawName string) (string, string, string, string) {
 	var (
 		name, display string
 		values        []string
 	)
+	metricType := ""
+	// Ex: last_transfer_duration(duration) => last_transfer_duration
 	if values = strings.SplitN(rawName, "=>", 2); len(values) == 2 {
 		name = strings.TrimSpace(values[0])
 		display = strings.TrimSpace(values[1])
+		name, metricType = ParseMetricType(name)
 	} else {
 		name = rawName
 		display = strings.ReplaceAll(rawName, ".", "_")
@@ -346,16 +349,24 @@ func ParseMetric(rawName string) (string, string, string) {
 	}
 
 	if strings.HasPrefix(name, "^^") {
-		return strings.TrimPrefix(name, "^^"), strings.TrimPrefix(display, "^^"), "key"
+		return strings.TrimPrefix(name, "^^"), strings.TrimPrefix(display, "^^"), "key", ""
 	}
 
 	if strings.HasPrefix(name, "^") {
-		return strings.TrimPrefix(name, "^"), strings.TrimPrefix(display, "^"), "label"
+		return strings.TrimPrefix(name, "^"), strings.TrimPrefix(display, "^"), "label", ""
 	}
 
-	if strings.HasPrefix(name, "?") {
-		return strings.TrimPrefix(name, "?"), strings.TrimPrefix(display, "?"), "bool"
-	}
+	return name, display, "float", metricType
+}
 
-	return name, display, "float"
+func ParseMetricType(metricName string) (string, string) {
+	metricTypeRegex := regexp.MustCompile(`(.*)\((.*?)\)`)
+	match := metricTypeRegex.FindAllStringSubmatch(metricName, -1)
+	if match != nil {
+		// For last_transfer_duration(duration), name would have 'last_transfer_duration' and metricType would have 'duration'.
+		name := match[0][1]
+		metricType := match[0][2]
+		return name, metricType
+	}
+	return metricName, ""
 }


Test patch:
diff --git a/cmd/collectors/rest/plugins/util_test.go b/cmd/collectors/rest/plugins/util_test.go
new file mode 100644
index 00000000..d5f17f3e
--- /dev/null
+++ b/cmd/collectors/rest/plugins/util_test.go
@@ -0,0 +1,225 @@
+package plugins
+
+import (
+	"goharvest2/pkg/matrix"
+	"testing"
+)
+
+func TestUpdateProtectedFields(t *testing.T) {
+	instance := matrix.NewInstance(0)
+
+	// Test cases for protectedBy and protectionSourceType
+	testWithoutGroupType(instance, t)
+	testSvmdr(instance, t)
+	testConstituentVolumeWithinSvmdr(instance, t)
+	testCg(instance, t)
+	testConstituentVolumeWithinCg(instance, t)
+	testNegativeCase1(instance, t)
+	testNegativeCase2(instance, t)
+	testGroupTypeNone(instance, t)
+	testGroupTypeFlexgroup(instance, t)
+
+	// Test cases for derived_relationship_type
+	testStrictSyncMirror(instance, t)
+	testSyncMirror(instance, t)
+	testMirrorVault(instance, t)
+	testAutomatedFailover(instance, t)
+	testOtherPolicyType(instance, t)
+	testWithNoPolicyType(instance, t)
+	testWithNoPolicyTypeNoRelationshipType(instance, t)
+}
+
+// Test cases for protectedBy and protectionSourceType
+func testWithoutGroupType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "" && instance.GetLabel("protectionSourceType") == "" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected empty  and protectionSourceType= %s, expected empty", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testSvmdr(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "")
+	instance.SetLabel("source_volume", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "storage_vm" && instance.GetLabel("protectionSourceType") == "storage_vm" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: storage_vm and protectionSourceType= %s, expected: storage_vm", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testConstituentVolumeWithinSvmdr(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "destvol")
+	instance.SetLabel("source_volume", "sourcevol")
+	instance.SetLabel("destination_location", "test1")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "storage_vm" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: storage_vm and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testCg(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "CONSISTENCYGROUP")
+	instance.SetLabel("destination_location", "test123:/cg/")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "cg" && instance.GetLabel("protectionSourceType") == "cg" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: cg and protectionSourceType= %s, expected: cg", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testConstituentVolumeWithinCg(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "CONSISTENCYGROUP")
+	instance.SetLabel("destination_location", "test123")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "cg" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: cg and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testNegativeCase1(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "infinitevol")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "not_mapped" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: not_mapped", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testNegativeCase2(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "vserver")
+	instance.SetLabel("destination_volume", "destvol")
+	instance.SetLabel("source_volume", "sourcevol")
+	instance.SetLabel("destination_location", "test123:")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "not_mapped" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: not_mapped", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testGroupTypeNone(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "none")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+func testGroupTypeFlexgroup(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("group_type", "flexgroup")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("protectedBy") == "volume" && instance.GetLabel("protectionSourceType") == "volume" {
+		// OK
+	} else {
+		t.Errorf("Labels protectedBy= %s, expected: volume and protectionSourceType= %s, expected: volume", instance.GetLabel("protectedBy"), instance.GetLabel("protectionSourceType"))
+	}
+}
+
+// Test cases for derived_relationship_type
+func testStrictSyncMirror(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "strict_sync_mirror")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror_strict" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror_strict", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testSyncMirror(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "sync_mirror")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testMirrorVault(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "mirror_vault")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "mirror_vault" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: mirror_vault", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testAutomatedFailover(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "automated_failover")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "sync_mirror" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: sync_mirror", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testOtherPolicyType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "vault")
+	instance.SetLabel("policy_type", "vault")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "vault" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: vault", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testWithNoPolicyType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "extended_data_protection")
+	instance.SetLabel("policy_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "extended_data_protection" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: extended_data_protection", instance.GetLabel("derived_relationship_type"))
+	}
+}
+
+func testWithNoPolicyTypeNoRelationshipType(instance *matrix.Instance, t *testing.T) {
+	instance.SetLabel("relationship_type", "")
+	instance.SetLabel("policy_type", "")
+	UpdateProtectedFields(instance)
+
+	if instance.GetLabel("derived_relationship_type") == "" {
+		// OK
+	} else {
+		t.Errorf("Labels derived_relationship_type= %s, expected: \"\"(empty)", instance.GetLabel("derived_relationship_type"))
+	}
+}
diff --git a/cmd/collectors/rest/templating_test.go b/cmd/collectors/rest/templating_test.go
new file mode 100644
index 00000000..7fd78df4
--- /dev/null
+++ b/cmd/collectors/rest/templating_test.go
@@ -0,0 +1,67 @@
+package rest
+
+import (
+	"testing"
+)
+
+func Test_HandleDuration(t *testing.T) {
+
+	type test struct {
+		timeFieldValue string
+		want           float64
+	}
+
+	var tests = []test{
+		{
+			timeFieldValue: "PT54S",
+			want:           54,
+		},
+		{
+			timeFieldValue: "PT48M",
+			want:           2880,
+		},
+		{
+			timeFieldValue: "P428DT22H45M19S",
+			want:           37061119,
+		},
+		{
+			timeFieldValue: "PT8H35M42S",
+			want:           30942,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.timeFieldValue, func(t *testing.T) {
+			if got := HandleDuration(tt.timeFieldValue); got != tt.want {
+				t.Errorf("actual value = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_HandleTimestamp(t *testing.T) {
+
+	type test struct {
+		timeFieldValue string
+		want           float64
+	}
+
+	var tests = []test{
+		{
+			timeFieldValue: "2020-12-02T18:36:19-08:00",
+			want:           1606962979,
+		},
+		{
+			timeFieldValue: "2022-01-31T04:05:02-05:00",
+			want:           1643619902,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.timeFieldValue, func(t *testing.T) {
+			if got := HandleTimestamp(tt.timeFieldValue); got != tt.want {
+				t.Errorf("actual value = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/integration/test/dashboard_test.go b/integration/test/dashboard_test.go
index 9df0a02c..587529a6 100644
--- a/integration/test/dashboard_test.go
+++ b/integration/test/dashboard_test.go
@@ -44,8 +44,8 @@ func (suite *DashboardImportTestSuite) SetupSuite() {
 	if !utils.IsUrlReachable(utils.GetPrometheusUrl()) {
 		panic(fmt.Errorf("Prometheus is not reachable."))
 	}
-	cDotFolder = "Harvest " + version.VERSION + " - cDOT"
-	sevenModeFolder = "Harvest " + version.VERSION + " - 7-mode"
+	cDotFolder = "Harvest-" + version.VERSION + "-cDOT"
+	sevenModeFolder = "Harvest-" + version.VERSION + "-7mode"
 	log.Info().Str("cMode", cDotFolder).Str("7mode", sevenModeFolder).Msg("Folder name details")
 	status, _ := new(grafana.GrafanaMgr).Import("") //send empty so that it will import all dashboards
 	if !status {
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index ba91914d..05ff60be 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -42,3 +42,46 @@ func TestIntersection(t *testing.T) {
 		})
 	}
 }
+
+func TestParseMetricType(t *testing.T) {
+
+	type test struct {
+		metricName         string
+		expectedName       string
+		expectedMetricType string
+	}
+
+	tests := []test{
+		{
+			metricName:         "last_transfer_duration(duration)",
+			expectedName:       "last_transfer_duration",
+			expectedMetricType: "duration",
+		},
+		{
+			metricName:         "newest_snapshot_timestamp(timestamp)",
+			expectedName:       "newest_snapshot_timestamp",
+			expectedMetricType: "timestamp",
+		},
+		{
+			metricName:         "resync_successful_count",
+			expectedName:       "resync_successful_count",
+			expectedMetricType: "",
+		},
+		{
+			metricName:         "total_transfer_bytes()",
+			expectedName:       "total_transfer_bytes",
+			expectedMetricType: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.metricName, func(t *testing.T) {
+			name, metricType := ParseMetricType(tt.metricName)
+			if name != tt.expectedName {
+				t.Errorf("metricname not matching, actual name = %v, want %v", name, tt.expectedName)
+			}
+			if metricType != tt.expectedMetricType {
+				t.Errorf("metrictype not matching, actual metric type = %v, want %v", metricType, tt.expectedMetricType)
+			}
+		})
+	}
+}