Repository: NetApp/harvest
Pull request #381 (2021-08-04 15:46:17)

fix: improve 7-mode version parsing
Fixes: #376


Patch:
diff --git a/integration/Jenkinsfile b/integration/Jenkinsfile
new file mode 100644
index 00000000..b870ea64
--- /dev/null
+++ b/integration/Jenkinsfile
@@ -0,0 +1,147 @@
+pipeline {
+
+    agent none
+
+    options {
+        buildDiscarder logRotator(
+                    daysToKeepStr: '16',
+                    numToKeepStr: '10'
+            )
+    }
+    parameters {
+        string(name: 'BRANCH', defaultValue: 'install_automation_test', description: '[Optional] Branch name to clone. Default (main) ')
+        string(name: 'RPM', defaultValue: '', description: 'http url of RPM build')
+        string(name: 'NATIVE', defaultValue: '', description: 'http url of native tar file')
+    }
+
+    stages {
+        stage('Setup') {
+            agent {
+                label "master"
+            }
+            steps {
+                sh '''
+                echo "Setup"
+                '''
+            }
+        }
+        stage('Run Tests') {
+            parallel {
+                stage('RPM') {
+                    agent {
+                        label "rhel"
+                    }
+                    stages('Test on RPM Pkg') {
+                        stage('Cleanup Workspace') {
+                            steps {
+                                cleanWs()
+                                sh """
+                                echo "Cleaned Up Workspace For Project"
+                                """
+                            }
+                        }
+
+                        stage('Setup GO') {
+                            steps {
+                                sh '''
+                                wget --quiet -O go.tar.gz "https://dl.google.com/go/$(curl https://golang.org/VERSION?m=text).linux-amd64.tar.gz"
+                                ls -ltrh
+                                rm -rf /usr/local/go && tar -C /usr/local -xzf ./go.tar.gz
+                                '''
+                            }
+                        }
+
+                        stage('Code Checkout') {
+                            steps {
+                               sh '''
+                               git clone --single-branch --branch $BRANCH https://github.com/NetApp/harvest.git
+                                '''
+                            }
+                        }
+
+                        stage('Install RPM') {
+                            steps {
+                                sh """
+                                echo "Installing"
+                                export PATH=$PATH:/usr/local/go/bin
+                                export BUILD_PATH=$RPM
+                                cd $WORKSPACE/harvest/integration/test/
+                                go mod vendor
+                                go test -tags=install_rpm
+                                """
+                            }
+                        }
+                        stage('Running Test') {
+                            steps {
+                                sh """
+                                echo "Running Unit Tests"
+                                export PATH=$PATH:/usr/local/go/bin
+                                cd $WORKSPACE/harvest/integration/test/
+                                go test -tags=regression
+                                """
+                            }
+                        }
+                    }
+
+                }
+                stage('Native') {
+                    agent {
+                        label "rhel"
+                    }
+                    stages('Test on Native Pkg') {
+                        stage('Cleanup Workspace') {
+                            steps {
+                                cleanWs()
+                                sh """
+                                echo "Cleaned Up Workspace For Project"
+                                """
+                            }
+                        }
+
+                        stage('Setup GO') {
+                            steps {
+                                sh '''
+                                wget --quiet -O go.tar.gz "https://dl.google.com/go/$(curl https://golang.org/VERSION?m=text).linux-amd64.tar.gz"
+                                ls -ltrh
+                                rm -rf /usr/local/go && tar -C /usr/local -xzf ./go.tar.gz
+                                '''
+                            }
+                        }
+
+                        stage('Code Checkout') {
+                           steps {
+                               sh '''
+                               git clone --single-branch --branch $BRANCH https://github.com/NetApp/harvest.git
+                                '''
+                            }
+                        }
+
+                        stage('Install Native') {
+                            steps {
+                                sh """
+                                echo "Installing"
+                                export PATH=$PATH:/usr/local/go/bin
+                                export BUILD_PATH=$NATIVE
+                                cd $WORKSPACE/harvest/integration/test/
+                                go mod vendor
+                                go test -tags=install_native
+                                """
+                            }
+                        }
+                        stage('Running Test') {
+                            steps {
+                                sh """
+                                echo "Running Unit Tests"
+                                export PATH=$PATH:/usr/local/go/bin
+                                cd $WORKSPACE/harvest/integration/test/
+                                go test -tags=regression
+                                """
+                            }
+                        }
+                    }
+
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/jenkins/artifacts/jenkinsfile b/jenkins/artifacts/jenkinsfile
index 6a019625..801b44e9 100644
--- a/jenkins/artifacts/jenkinsfile
+++ b/jenkins/artifacts/jenkinsfile
@@ -1,5 +1,5 @@
 pipeline {
-    agent any
+    agent {label 'buildserver'}
 
     triggers {
         cron('@midnight')
diff --git a/pkg/api/ontapi/zapi/system.go b/pkg/api/ontapi/zapi/system.go
index 74c484bd..6aa3d916 100644
--- a/pkg/api/ontapi/zapi/system.go
+++ b/pkg/api/ontapi/zapi/system.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"goharvest2/pkg/tree/node"
+	"regexp"
 	"strconv"
 )
 
@@ -18,6 +19,48 @@ type system struct {
 	clustered bool
 }
 
+// See system_test.go for examples
+func (s *system) parse7mode(release string) error {
+	// NetApp Release 8.2P4 7-Mode: Tue Oct 1 11:24:04 PDT 2013
+	r := regexp.MustCompile(`NetApp Release (\d+)\.(\d+)\w(\d+)`)
+	matches := r.FindStringSubmatch(release)
+	if len(matches) == 4 {
+		setInt(&s.version[0], matches[1])
+		setInt(&s.version[1], matches[2])
+		setInt(&s.version[2], matches[3])
+		return nil
+	}
+
+	r = regexp.MustCompile(`NetApp Release (\d+)\.(\d+)\.(\d+)`)
+	matches = r.FindStringSubmatch(release)
+	if len(matches) == 4 {
+		setInt(&s.version[0], matches[1])
+		setInt(&s.version[1], matches[2])
+		setInt(&s.version[2], matches[3])
+		return nil
+	}
+
+	r = regexp.MustCompile(`NetApp Release (\d+)\.(\d+)\.(\d+)\.(\d+)`)
+	matches = r.FindStringSubmatch(release)
+	if len(matches) == 5 {
+		// 7.0.0.1 becomes 7.0.0
+		setInt(&s.version[0], matches[1])
+		setInt(&s.version[1], matches[2])
+		setInt(&s.version[2], matches[3])
+		return nil
+	}
+
+	return fmt.Errorf("no valid version tuple found for=[%s]", release)
+}
+
+func setInt(i *int, s string) {
+	value, err := strconv.Atoi(s)
+	if err != nil {
+		return
+	}
+	*i = value
+}
+
 // getSystem connects to ONTAP system and retrieves its identity and version
 // this works for Clustered and 7-mode systems
 func (c *Client) getSystem() error {
@@ -47,11 +90,13 @@ func (c *Client) getSystem() error {
 		}
 	}
 
-	// if version tuple is missing try to parse from the release strirng
+	// if version tuple is missing try to parse from the release string
 	// this is usually the case with 7mode systems
+	// e.g. NetApp Release 8.2P4 7-Mode: Tue Oct 1 11:24:04 PDT 2013
 	if s.version[0] == 0 {
-		if _, err = fmt.Sscanf(s.release, "NetApp Release %d.%d.%d", &s.version[0], &s.version[1], &s.version[2]); err != nil {
-			return errors.New("no valid version tuple found")
+		err := s.parse7mode(s.release)
+		if err != nil {
+			return err
 		}
 	}
 


Test patch:
diff --git a/integration/test/core/poller.go b/integration/test/core/poller.go
new file mode 100644
index 00000000..87ccd2a3
--- /dev/null
+++ b/integration/test/core/poller.go
@@ -0,0 +1,25 @@
+package core
+
+import "strings"
+
+type Poller struct {
+	DataCenter string
+	Poller     string
+	Pid        string
+	PromPort   string
+	Status     string
+}
+
+func (p *Poller) New(dataCenter string,
+	poller string, pid string, promPort string,
+	status string) {
+	p.Pid = strings.TrimSpace(pid)
+	p.PromPort = strings.TrimSpace(promPort)
+	p.Status = status
+	p.DataCenter = strings.TrimSpace(dataCenter)
+	p.Poller = strings.TrimSpace(poller)
+}
+
+func (p *Poller) MetricUrl() string {
+	return "http://localhost:" + strings.TrimSpace(p.PromPort) + "/metrics"
+}
diff --git a/integration/test/harvest.yml b/integration/test/harvest.yml
new file mode 100644
index 00000000..8c0d4256
--- /dev/null
+++ b/integration/test/harvest.yml
@@ -0,0 +1,36 @@
+Exporters:
+  prometheus1:
+    exporter: Prometheus
+    addr: 0.0.0.0
+    port: 12990
+  prometheus2:
+    exporter: Prometheus
+    addr: 0.0.0.0
+    port: 12991
+
+Defaults:
+  collectors:
+    - Zapi
+    - ZapiPerf
+  use_insecure_tls: true
+
+Pollers:
+  # this is a special/optional poller that provides stats about harvest itself
+  # to remove this poller, simply delete the section "unix"
+  umeng_aff300:
+    datacenter: umeng_aff300
+    addr: 10.193.48.11
+    auth_style: password
+    username: admin
+    password: netapp1!
+    exporters:
+      - prometheus1
+  F2240-127-26:
+    datacenter: F2240-127-26
+    addr: 10.193.6.61
+    auth_style: password
+    username: admin
+    password: netapp1!
+    exporters:
+      - prometheus2
+Tools:
diff --git a/integration/test/installer/docker.go b/integration/test/installer/docker.go
new file mode 100644
index 00000000..978b7d87
--- /dev/null
+++ b/integration/test/installer/docker.go
@@ -0,0 +1,16 @@
+package installer
+
+import "fmt"
+
+type Docker struct {
+	path string
+}
+
+func (d *Docker) Init(path string) {
+	d.path = path
+}
+
+func (d *Docker) Install() bool {
+	fmt.Printf("Docker: %s", d.path)
+	return true
+}
diff --git a/integration/test/installer/harvest.go b/integration/test/installer/harvest.go
new file mode 100644
index 00000000..19f43432
--- /dev/null
+++ b/integration/test/installer/harvest.go
@@ -0,0 +1,78 @@
+package installer
+
+import (
+	"fmt"
+	"goharvest2/integration/test/core"
+	"goharvest2/integration/test/utils"
+	"log"
+	"strings"
+	"time"
+)
+
+const HarvestHome = "/opt/harvest"
+const HarvestBin = "./bin/harvest"
+
+type Harvest struct {
+}
+
+func (h *Harvest) Start() {
+	status := utils.Exec(HarvestHome, HarvestBin, "start")
+	fmt.Println(status)
+	time.Sleep(30 * time.Second)
+	h.AllRunning()
+
+}
+
+func (h *Harvest) Stop() {
+	status := utils.Exec(HarvestHome, HarvestBin, "stop")
+	fmt.Println(status)
+}
+
+func (h *Harvest) AllRunning() bool {
+	pollerArray := h.GetPollerInfo()
+	for _, poller := range pollerArray {
+		if poller.Status != "running" {
+			return false
+		}
+	}
+	return true
+}
+
+func (h *Harvest) AllStopped() bool {
+	pollerArray := h.GetPollerInfo()
+	for _, poller := range pollerArray {
+		if poller.Status != "not running" {
+			return false
+		}
+	}
+	return true
+}
+
+func (h *Harvest) GetPollerInfo() []core.Poller {
+	log.Println("Getting all pollers details")
+	harvestStatus := utils.Exec(HarvestHome, HarvestBin, "status")
+	fmt.Println(harvestStatus)
+	rows := strings.Split(harvestStatus, "\n")
+	var pollerArray []core.Poller
+	for i := range rows {
+		columns := strings.Split(rows[i], `|`)
+		count := len(columns)
+		if count != 5 || i == 0 { //ignore header and junk entries
+			continue
+		}
+		dataCenter := columns[0]
+		poller := columns[1]
+		pid := columns[2]
+		promPort := columns[3]
+		pollerStatus := columns[4]
+		pollerObject := core.Poller{
+			DataCenter: dataCenter,
+			Poller:     poller,
+			Pid:        pid,
+			PromPort:   promPort,
+			Status:     strings.TrimSpace(pollerStatus),
+		}
+		pollerArray = append(pollerArray, pollerObject)
+	}
+	return pollerArray
+}
diff --git a/integration/test/installer/installer.go b/integration/test/installer/installer.go
new file mode 100644
index 00000000..e4def039
--- /dev/null
+++ b/integration/test/installer/installer.go
@@ -0,0 +1,5 @@
+package installer
+
+type Installer interface {
+	Install() bool
+}
diff --git a/integration/test/installer/installerFactory.go b/integration/test/installer/installerFactory.go
new file mode 100644
index 00000000..f8935e64
--- /dev/null
+++ b/integration/test/installer/installerFactory.go
@@ -0,0 +1,26 @@
+package installer
+
+import "fmt"
+
+const (
+	DOCKER = "docker"
+	RHEL   = "rpm"
+	NATIVE = "tar"
+)
+
+func GetInstaller(installType string, path string) (Installer, error) {
+	if installType == DOCKER {
+		d := new(Docker)
+		d.Init(path)
+		return d, nil
+	} else if installType == RHEL {
+		d := new(RPM)
+		d.Init(path)
+		return d, nil
+	} else if installType == NATIVE {
+		d := new(Native)
+		d.Init(path)
+		return d, nil
+	}
+	return nil, fmt.Errorf("Wrong installer type passed " + installType)
+}
diff --git a/integration/test/installer/native.go b/integration/test/installer/native.go
new file mode 100644
index 00000000..8044f940
--- /dev/null
+++ b/integration/test/installer/native.go
@@ -0,0 +1,40 @@
+package installer
+
+import (
+	"goharvest2/integration/test/utils"
+	"log"
+)
+
+type Native struct {
+	path string
+}
+
+func (r *Native) Init(path string) {
+	r.path = path
+}
+
+func (r *Native) Install() bool {
+	harvestFile := "harvest.yml"
+	harvestObj := new(Harvest)
+	tarFileName := "harvest.tar.gz"
+	utils.RemoveSafely(tarFileName)
+	err := utils.DownloadFile(tarFileName, r.path)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("Downloaded: " + r.path)
+	log.Println("Check and remove harvest ")
+	if utils.FileExists(HarvestHome) && !harvestObj.AllStopped() {
+		harvestObj.Stop()
+	}
+	unInstallOutput := utils.Run("rm", "-rf", HarvestHome)
+	log.Println(unInstallOutput)
+	log.Println("Installing " + tarFileName)
+	unTarOutput := utils.Run("tar", "-xf", tarFileName, "--one-top-level=harvest", "--strip-components", "1", "-C", "/opt")
+	log.Println(unTarOutput)
+	utils.RemoveSafely(HarvestHome + "/" + harvestFile)
+	utils.CopyFile(harvestFile, HarvestHome+"/"+harvestFile)
+	harvestObj.Start()
+	status := harvestObj.AllRunning()
+	return status
+}
diff --git a/integration/test/installer/rpm.go b/integration/test/installer/rpm.go
new file mode 100644
index 00000000..71b90b42
--- /dev/null
+++ b/integration/test/installer/rpm.go
@@ -0,0 +1,37 @@
+package installer
+
+import (
+	"goharvest2/integration/test/utils"
+	"log"
+)
+
+type RPM struct {
+	path string
+}
+
+func (r *RPM) Init(path string) {
+	r.path = path
+}
+
+func (r *RPM) Install() bool {
+	//harvestFile := "harvest.yml"
+	rpmFileName := "harvest.rpm"
+	utils.RemoveSafely(rpmFileName)
+	err := utils.DownloadFile(rpmFileName, r.path)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("Downloaded: " + r.path)
+	log.Println("Check and remove harvest ")
+	unInstallOutput := utils.Run("yum", "remove", "-y", "harvest")
+	log.Println(unInstallOutput)
+	log.Println("Installing " + rpmFileName)
+	installOutput := utils.Run("yum", "install", "-y", rpmFileName)
+	log.Println(installOutput)
+	//utils.RemoveSafely(harvestFile)
+	//utils.CopyFile(HARVEST_HOME+"/harvest.yml", harvestFile) use file directly from the repo
+	harvestObj := new(Harvest)
+	harvestObj.Start()
+	status := harvestObj.AllRunning()
+	return status
+}
diff --git a/integration/test/metric_test.go b/integration/test/metric_test.go
new file mode 100644
index 00000000..4c1f0ca2
--- /dev/null
+++ b/integration/test/metric_test.go
@@ -0,0 +1,54 @@
+//+build  regression
+
+package main
+
+import (
+	"goharvest2/integration/test/installer"
+	"goharvest2/integration/test/utils"
+	"log"
+	"strconv"
+	"strings"
+	"testing"
+)
+
+func TestPollerMetrics(t *testing.T) {
+	harvestObj := new(installer.Harvest)
+	status := harvestObj.AllRunning()
+	if status == false {
+		panic("One or more pollers are not running.")
+	}
+	pollers := harvestObj.GetPollerInfo()
+	for i := range pollers {
+		var validCounters = 0
+		sb, error := utils.GetResponse(pollers[i].MetricUrl())
+		if error != nil {
+			panic("Unable to get metric data")
+		}
+		rows := strings.Split(sb, "\n")
+		for i := range rows {
+			row := rows[i]
+			length := len(row)
+			if length == 0 {
+				continue
+			}
+			open := strings.Index(row, "{")
+			close := strings.Index(row, "}")
+			space := strings.Index(row, " ")
+			if open > 0 && close > 0 && space > 0 {
+				//objectName := row[0:open]
+				//metricContent := row[open:(close+1)]
+				metricValue, _ := strconv.Atoi(strings.TrimSpace(row[(close + 1):length]))
+				if metricValue > 0 {
+					validCounters++
+				}
+			} else {
+				log.Println("invalid string data found in the metric output " + row)
+			}
+		}
+		if validCounters == 0 {
+			panic("Empty values found for all counters for poller " + pollers[i].Poller)
+		}
+		log.Printf("Total number of counters verified %d for poller '%s' \n", validCounters, pollers[i].Poller)
+	}
+
+}
diff --git a/integration/test/native_installer_test.go b/integration/test/native_installer_test.go
new file mode 100644
index 00000000..a3b12857
--- /dev/null
+++ b/integration/test/native_installer_test.go
@@ -0,0 +1,35 @@
+//+build install_native
+
+package main
+
+import (
+	"goharvest2/integration/test/installer"
+	"log"
+	"os"
+	"testing"
+)
+
+func TestNativeInstall(t *testing.T) {
+	var path = os.Getenv("BUILD_PATH")
+	if len(path) == 0 {
+		panic("BUILD_PATH variable is not set.")
+	}
+	installObject, error := installer.GetInstaller(installer.NATIVE, path)
+	if error != nil {
+		log.Println("Unable to initialize installer object")
+		panic(error)
+	}
+	if installObject.Install() {
+		log.Println("Installation is successful..")
+	} else {
+		log.Println("Setup completed")
+		panic("installation is failed.")
+	}
+	harvestObj := new(installer.Harvest)
+	if harvestObj.AllRunning() {
+		log.Println("All pollers are running")
+	} else {
+		t.Errorf("One or more pollers are not running.")
+	}
+
+}
diff --git a/integration/test/rpm_installer_test.go b/integration/test/rpm_installer_test.go
new file mode 100644
index 00000000..019528ad
--- /dev/null
+++ b/integration/test/rpm_installer_test.go
@@ -0,0 +1,35 @@
+//+build install_rpm
+
+package main
+
+import (
+	"goharvest2/integration/test/installer"
+	"log"
+	"os"
+	"testing"
+)
+
+func TestRHELInstall(t *testing.T) {
+	var path = os.Getenv("BUILD_PATH")
+	if len(path) == 0 {
+		panic("BUILD_PATH variable is not set.")
+	}
+	installObject, error := installer.GetInstaller(installer.RHEL, path)
+	if error != nil {
+		log.Println("Unable to initialize installer object")
+		panic(error)
+	}
+	if installObject.Install() {
+		log.Println("Installation is successful..")
+	} else {
+		log.Println("Setup completed")
+		panic("installation is failed.")
+	}
+	harvestObj := new(installer.Harvest)
+	if harvestObj.AllRunning() {
+		log.Println("All pollers are running")
+	} else {
+		t.Errorf("One or more pollers are not running.")
+	}
+
+}
diff --git a/integration/test/utils/http_utils.go b/integration/test/utils/http_utils.go
new file mode 100644
index 00000000..532c83a2
--- /dev/null
+++ b/integration/test/utils/http_utils.go
@@ -0,0 +1,20 @@
+package utils
+
+import (
+	"io/ioutil"
+	"log"
+	"net/http"
+)
+
+func GetResponse(url string) (string, error) {
+	resp, err := http.Get(url)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		log.Fatalln(err)
+		return "", err
+	}
+	return string(body), nil
+}
diff --git a/integration/test/utils/utils.go b/integration/test/utils/utils.go
new file mode 100644
index 00000000..1e90c5d5
--- /dev/null
+++ b/integration/test/utils/utils.go
@@ -0,0 +1,127 @@
+package utils
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+)
+
+func Run(command string, arg ...string) string {
+	out, err := exec.Command(command, arg...).Output()
+	if err != nil {
+		panic(err)
+	}
+	output := string(out[:])
+	return output
+}
+
+func Exec(dir string, command string, arg ...string) string {
+	cmd := exec.Command(command, arg...)
+	cmd.Dir = dir
+	out, err := cmd.Output()
+	if err != nil {
+		panic(err)
+	}
+	output := string(out[:])
+	return output
+}
+
+// DownloadFile will download a url to a local file. It's efficient because it will
+// write as it downloads and not load the whole file into memory.
+func DownloadFile(filepath string, url string) error {
+
+	// Get the data
+	resp, err := http.Get(url)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	// Create the file
+	out, err := os.Create(filepath)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	// Write the body to file
+	_, err = io.Copy(out, resp.Body)
+	return err
+}
+
+func FileExists(path string) bool {
+	_, err := os.Stat(path)
+	return !os.IsNotExist(err)
+}
+
+func RemoveSafely(filename string) bool {
+	exist := FileExists(filename)
+	if exist {
+		err := os.Remove(filename)
+		if err != nil {
+			fmt.Println(err)
+			return false
+		}
+		fmt.Println("File " + filename + " has been deleted.")
+	}
+	return true
+}
+
+func CopyFile(src, dst string) (err error) {
+	sfi, err := os.Stat(src)
+	if err != nil {
+		return
+	}
+	if !sfi.Mode().IsRegular() {
+		// cannot copy non-regular files (e.g., directories,
+		// symlinks, devices, etc.)
+		return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
+	}
+	dfi, err := os.Stat(dst)
+	if err != nil {
+		if !os.IsNotExist(err) {
+			return
+		}
+	} else {
+		if !(dfi.Mode().IsRegular()) {
+			return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
+		}
+		if os.SameFile(sfi, dfi) {
+			return
+		}
+	}
+	if err = os.Link(src, dst); err == nil {
+		return
+	}
+	err = copyFileContents(src, dst)
+	return
+}
+
+// copyFileContents copies the contents of the file named src to the file named
+// by dst. The file will be created if it does not already exist. If the
+// destination file exists, all it's contents will be replaced by the contents
+// of the source file.
+func copyFileContents(src, dst string) (err error) {
+	in, err := os.Open(src)
+	if err != nil {
+		return
+	}
+	defer in.Close()
+	out, err := os.Create(dst)
+	if err != nil {
+		return
+	}
+	defer func() {
+		cerr := out.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+	if _, err = io.Copy(out, in); err != nil {
+		return
+	}
+	err = out.Sync()
+	return
+}
diff --git a/pkg/api/ontapi/zapi/system_test.go b/pkg/api/ontapi/zapi/system_test.go
new file mode 100644
index 00000000..50d4d80c
--- /dev/null
+++ b/pkg/api/ontapi/zapi/system_test.go
@@ -0,0 +1,42 @@
+package zapi
+
+import (
+	"testing"
+)
+
+func Test_7modeParsing(t *testing.T) {
+	type test struct {
+		name    string
+		release string
+		want    system
+		wantErr bool
+	}
+
+	tests := []test{
+		{name: "issue-376", release: "NetApp Release 8.2P4 7-Mode: Tue Oct 1 11:24:04 PDT 2013", want: system{version: [3]int{8, 2, 4}}},
+		{name: "7.0.1", release: "NetApp Release 7.0.0.1: Wed Mar 2 22:20:44 PST 2005", want: system{version: [3]int{7, 0, 0}}},
+		{name: "7.0.4", release: "NetApp Release 7.0.4: Sun Feb  5 00:52:53 PST 2006", want: system{version: [3]int{7, 0, 4}}},
+		{name: "6.4.1", release: "NetApp Release 6.4R1: Thu Mar 13 23:56:30 PST 2003", want: system{version: [3]int{6, 4, 1}}},
+		{name: "7.3.1", release: "NetApp Release 7.3.1: Thu Jan 8 01:31:42 PST 2009 ", want: system{version: [3]int{7, 3, 1}}},
+		{name: "fail", release: "cromulent house", wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			s := system{}
+			err := s.parse7mode(tt.release)
+			if err != nil {
+				if tt.wantErr {
+					return
+				}
+				t.Errorf("got error=%v, want %v", err, tt.want)
+
+			}
+			if s.version[0] == 0 {
+				t.Errorf("got zero version=%v, want %v", s.version, tt.want.version)
+			}
+			if s.version[0] != tt.want.version[0] || s.version[1] != tt.want.version[1] || s.version[2] != tt.want.version[2] {
+				t.Errorf("got version=%v, want %v", s.version, tt.want.version)
+			}
+		})
+	}
+}