Repository: ClusterLabs/ha_cluster_exporter
Pull request #26 (2019-10-11 08:28:05)

Implement DRBD metrics for disk-state
# Description

This pr will implement this metric:

```

# HELP ha_cluster_drbd_resource_disk_state show per resource name, its role, the volume and disk_state (Diskless,Attaching, Failed, Negotiating, Inconsistent, Outdated, DUnknown, Consistent, UpToDate)
# TYPE ha_cluster_drbd_resource_disk_state gauge
ha_cluster_drbd_resource{disk_state="uptodate",resource_name="1-single-0",role="Secondary",volume="0"} 1
ha_cluster_drbd_resource{disk_state="uptodate",resource_name="1-single-1",role="Secondary",volume="0"} 1
ha_cluster_drbd_resource{disk_state="uptodate",resource_name="vg1",role="Secondary",volume="0"} 1
ha_cluster_drbd_resource{disk_state="uptodate",resource_name="vg2",role="Secondary",volume="0"} 1
ha_cluster_drbd_resource{disk_state="uptodate",resource_name="vg3",role="Secondary",volume="0"} 1
```

# Usage:

- Check if the state of DRBD disk are UptoDate, out of sync etc/
- monitor or alert drbd resource disks


## what is it is missing:

- [x] parse and populated types. 
- [x] Tests 
- [x] set the prometheus exporter metric according to metric




Patch:
diff --git a/.travis.yml b/.travis.yml
index a12c187..5743a42 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,7 @@ env:
   - GO111MODULE=on GOFLAGS=-mod=vendor
 git:
   depth: 1
-  go_import_path: github.com/Clusterlab/ha_cluster_exporter
+  go_import_path: github.com/dmacvicar/terraform-provider-libvirt
 
 script:
   - make 
diff --git a/README.md b/README.md
index 5ee56b9..cd871bd 100644
--- a/README.md
+++ b/README.md
@@ -3,37 +3,40 @@
 [![Build Status](https://travis-ci.org/ClusterLabs/ha_cluster_exporter.svg?branch=master)](https://travis-ci.org/ClusterLabs/ha_cluster_exporter)
 
 
-This prometheus exporter is used to serve metrics for pacemaker https://github.com/ClusterLabs/pacemaker.
+This prometheus exporter is used to serve metrics for pacemaker https://github.com/ClusterLabs/pacemaker
+
 It should run inside a node of the cluster or both.
 
 # Usage:
 
-You can find the RPM pkgs for the exporter here: https://build.opensuse.org/package/show/server:monitoring/prometheus-ha_cluster_exporter.
+You can find RPM pkgs for the exporter here: https://build.opensuse.org/package/show/server:monitoring/prometheus-ha_cluster_exporter
 
-Once installed run the exporter **inside a cluster node** with: 
+Once installed run **inside a cluster node** the exporter with: 
 
-`systemctl start prometheus-ha_cluster_exporter`. By default it will show on `http://YOUR_HOST_IP:9002/metrics`.
+`systemctl start prometheus-ha_cluster_exporter`, by default it will expose on `http://YOUR_HOST_IP:9002/metrics`.
 
 If you open a web-browser it will serve the metrics. 
-The exporter can't work outside a HA cluster node.
+
+The exporter can't work outside a HA cluster node
 
 **Hint:**
-For a terraform deployment you can also read: https://github.com/SUSE/ha-sap-terraform-deployments
+For a terraform deployment you can read also : https://github.com/SUSE/ha-sap-terraform-deployments
 
 # Features:
 
-- show cluster node and resource metrics via `crm_mon` (pacemaker data xml)
+- expose cluster node and resource metrics via `crm_mon` (pacemaker data xml)
 
-- show corosync metrics (ring errors, quorum metrics)
+- expose corosync metrics (ring errors, quorum metrics)
 
-- show SBD disk health metrics
+- expose SBD disk health metrics
 
 # Devel:
 
-Build the binary with `make` and run it inside a node of the ha cluster, it will show the metrics on port `9002` by default.
+Build the binary with `make` and run it inside a node of the ha cluster , it will expose the metrics on port `9002` by default.
+
 Use `ha_cluster_exporter -h` for options.
 
 #### Design:
 
-For the technical design of the exporter have look at [design](doc/design.md) (this is focused on cluster_metrics).
+For the technical design of the exporter have look at [design](doc/design.md) (this is focused on cluster_metrics)
 
diff --git a/drbd_metrics.go b/drbd_metrics.go
new file mode 100644
index 0000000..1a1e83b
--- /dev/null
+++ b/drbd_metrics.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"os/exec"
+)
+
+// drbdStatus is for parsing relevant data we want to convert to metrics
+type drbdStatus struct {
+	Name    string `json:"name"`
+	Role    string `json:"role"`
+	Devices []struct {
+		Volume    int    `json:"volume"`
+		DiskState string `json:"disk-state"`
+	} `json:"devices"`
+}
+
+// return drbd status in byte raw json
+func getDrbdInfo() ([]byte, error) {
+	// get ringStatus
+	log.Println("[INFO]: Reading drbd status with drbdsetup status ...")
+	drbdStatusRaw, err := exec.Command("/sbin/drbdsetup", "status", "--json").Output()
+	return drbdStatusRaw, err
+}
+
+func parseDrbdStatus(statusRaw []byte) ([]drbdStatus, error) {
+	var drbdDevs []drbdStatus
+	err := json.Unmarshal(statusRaw, &drbdDevs)
+	if err != nil {
+		return drbdDevs, err
+	}
+	return drbdDevs, nil
+}
diff --git a/ha_cluster_exporter.go b/ha_cluster_exporter.go
index cd6d162..a9cf7a2 100644
--- a/ha_cluster_exporter.go
+++ b/ha_cluster_exporter.go
@@ -5,7 +5,6 @@ import (
 	"flag"
 	"log"
 	"net/http"
-	"os"
 	"os/exec"
 	"strconv"
 	"strings"
@@ -15,61 +14,6 @@ import (
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
 
-// this types are for reading pacemaker configuration xml when running crm_mon command
-// and lookup the corrispective value
-type crmMon struct {
-	Version string  `xml:"version,attr"`
-	Summary summary `xml:"summary"`
-	Nodes   nodes   `xml:"nodes"`
-}
-
-type summary struct {
-	Nodes struct {
-		Number int `xml:"number,attr"`
-	} `xml:"nodes_configured"`
-	Resources resourcesConfigured `xml:"resources_configured"`
-}
-
-type resourcesConfigured struct {
-	Number   int `xml:"number,attr"`
-	Disabled int `xml:"disabled,attr"`
-	Blocked  int `xml:"blocked,attr"`
-}
-
-type nodes struct {
-	Node []node `xml:"node"`
-}
-
-type node struct {
-	Name             string     `xml:"name,attr"`
-	ID               string     `xml:"id,attr"`
-	Online           bool       `xml:"online,attr"`
-	Standby          bool       `xml:"standby,attr"`
-	StandbyOnFail    bool       `xml:"standby_onfail,attr"`
-	Maintenance      bool       `xml:"maintenance,attr"`
-	Pending          bool       `xml:"pending,attr"`
-	Unclean          bool       `xml:"unclean,attr"`
-	Shutdown         bool       `xml:"shutdown,attr"`
-	ExpectedUp       bool       `xml:"expected_up,attr"`
-	DC               bool       `xml:"is_dc,attr"`
-	ResourcesRunning int        `xml:"resources_running,attr"`
-	Type             string     `xml:"type,attr"`
-	Resources        []resource `xml:"resource"`
-}
-
-type resource struct {
-	ID             string `xml:"id,attr"`
-	Agent          string `xml:"resource_agent,attr"`
-	Role           string `xml:"role,attr"`
-	Active         bool   `xml:"active,attr"`
-	Orphaned       bool   `xml:"orphaned,attr"`
-	Blocked        bool   `xml:"blocked,attr"`
-	Managed        bool   `xml:"managed,attr"`
-	Failed         bool   `xml:"failed,attr"`
-	FailureIgnored bool   `xml:"failure_ignored,attr"`
-	NodesRunningOn int    `xml:"nodes_running_on,attr"`
-}
-
 var (
 	// corosync metrics
 	corosyncRingErrorsTotal = prometheus.NewGauge(prometheus.GaugeOpts{
@@ -121,6 +65,13 @@ var (
 			Name: "cluster_node_resources",
 			Help: "metric inherent per node resources",
 		}, []string{"node", "resource_name", "role", "managed", "status"})
+
+	// drbd metrics
+	drbdDiskState = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "ha_cluster_drbd_resource",
+			Help: "show per resource name, its role, the volume and disk_state (Diskless,Attaching, Failed, Negotiating, Inconsistent, Outdated, DUnknown, Consistent, UpToDate)",
+		}, []string{"resource_name", "role", "volume", "disk_state"})
 )
 
 func init() {
@@ -132,13 +83,12 @@ func init() {
 	prometheus.MustRegister(corosyncQuorum)
 	prometheus.MustRegister(corosyncQuorate)
 	prometheus.MustRegister(sbdDevStatus)
-
+	prometheus.MustRegister(drbdDiskState)
 }
 
 // this function is for some cluster metrics which have resource as labels.
 // since we cannot be sure a resource exists always, we need to destroy the metrics at each iteration
 // otherwise we will have wrong metrics ( thinking a resource exist when not)
-
 func resetClusterMetrics() error {
 	// We want to reset certains metrics to 0 each time for removing the state.
 	// since we have complex/nested metrics with multiples labels, unregistering/re-registering is the cleanest way.
@@ -168,6 +118,24 @@ func resetClusterMetrics() error {
 		log.Println("[ERROR]: failed to register clusterNode metric. Perhaps another exporter is already running?")
 		return err
 	}
+	return nil
+}
+
+func resetDrbdMetrics() error {
+	// for Drbd we need to reset remove metrics state because some disk could be removed during cluster lifecycle
+	// so we need to have a clean atomic snapshot
+	prometheus.Unregister(drbdDiskState)
+	// overwrite metric with an empty one
+	drbdDiskState = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "ha_cluster_drbd_resource",
+			Help: "show per resource name, its role, the volume and disk_state (Diskless,Attaching, Failed, Negotiating, Inconsistent, Outdated, DUnknown, Consistent, UpToDate)",
+		}, []string{"resource_name", "role", "volume", "disk_state"})
+	err := prometheus.Register(drbdDiskState)
+	if err != nil {
+		log.Println("[ERROR]: failed to register DRBD disk state metric. Perhaps another exporter is already running?")
+		return err
+	}
 
 	return nil
 }
@@ -182,27 +150,62 @@ func main() {
 
 	// for each different metrics, handle it in differents gorutines, and use same timeout.
 
-	// set SBD device metrics
+	// set DRBD metrics
 	go func() {
-		if _, err := os.Stat("/etc/sysconfig/sbd"); os.IsNotExist(err) {
-			return
-		}
+		for {
+			// retrieve drbdInfos calling its binary
+			drbdStatusJSONRaw, err := getDrbdInfo()
+			if err != nil {
+				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
+				continue
+			}
 
+			// populate structs and parse relevant info we will expose via metrics
+			drbdDev, err := parseDrbdStatus(drbdStatusJSONRaw)
+			if err != nil {
+				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
+				continue
+			}
+			// reset metrics before setting news to remove any state information
+			resetDrbdMetrics()
+
+			// create a metric like : ha_cluster_drbd_resource{resource_name="1-single-0", role="primary", volume="0",  disk_state="uptodate"} 1
+			// the metric is always set to 1 or is absent
+			for _, resource := range drbdDev {
+				for _, device := range resource.Devices {
+					drbdDiskState.WithLabelValues(resource.Name, resource.Role, strconv.Itoa(device.Volume), strings.ToLower(resource.Devices[device.Volume].DiskState)).Set(float64(1))
+				}
+			}
+			time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
+		}
+	}()
+	// set SBD device metrics
+	go func() {
 		for {
 			log.Println("[INFO]: Reading cluster SBD configuration..")
 			// read configuration of SBD
 			sbdConfiguration, err := readSdbFile()
 			if err != nil {
 				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 			// retrieve a list of sbd devices
-			sbdDevices := getSbdDevices(sbdConfiguration)
+			sbdDevices, err := getSbdDevices(sbdConfiguration)
+			// mostly, the sbd_device were not set in conf file for returning an error
+			if err != nil {
+				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
+				continue
+			}
 			// set and return a map of sbd devices with true healthy, false not
 			sbdStatus := setSbdDeviceHealth(sbdDevices)
 
 			if len(sbdStatus) == 0 {
 				log.Println("[WARN]: Could not retrieve any sbd device")
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 
@@ -227,6 +230,7 @@ func main() {
 			if err != nil {
 				log.Println("[ERROR]: could not execute command: usr/sbin/corosync-cfgtool -s")
 				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 			corosyncRingErrorsTotal.Set(float64(ringErrorsTotal))
@@ -241,6 +245,7 @@ func main() {
 
 			if len(voteQuorumInfo) == 0 {
 				log.Println("[ERROR]: Could not retrieve any quorum information, map is empty")
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 
@@ -272,6 +277,7 @@ func main() {
 			if err != nil {
 				log.Println("[ERROR]: fail to 	 reset metrics for cluster crm_mon component")
 				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 			// get cluster status xml
@@ -280,6 +286,7 @@ func main() {
 			if err != nil {
 				log.Println("[ERROR]: crm_mon command execution failed. Did you have crm_mon installed ?")
 				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 
@@ -289,6 +296,7 @@ func main() {
 			if err != nil {
 				log.Println("[ERROR]: could not read cluster XML configuration")
 				log.Println(err)
+				time.Sleep(time.Duration(int64(*timeoutSeconds)) * time.Second)
 				continue
 			}
 
diff --git a/pacemaker_metrics.go b/pacemaker_metrics.go
new file mode 100644
index 0000000..7239783
--- /dev/null
+++ b/pacemaker_metrics.go
@@ -0,0 +1,56 @@
+package main
+
+// this types are for reading pacemaker configuration xml when running crm_mon command
+// and lookup the corrispective value
+type crmMon struct {
+	Version string  `xml:"version,attr"`
+	Summary summary `xml:"summary"`
+	Nodes   nodes   `xml:"nodes"`
+}
+
+type summary struct {
+	Nodes struct {
+		Number int `xml:"number,attr"`
+	} `xml:"nodes_configured"`
+	Resources resourcesConfigured `xml:"resources_configured"`
+}
+
+type resourcesConfigured struct {
+	Number   int `xml:"number,attr"`
+	Disabled int `xml:"disabled,attr"`
+	Blocked  int `xml:"blocked,attr"`
+}
+
+type nodes struct {
+	Node []node `xml:"node"`
+}
+
+type node struct {
+	Name             string     `xml:"name,attr"`
+	ID               string     `xml:"id,attr"`
+	Online           bool       `xml:"online,attr"`
+	Standby          bool       `xml:"standby,attr"`
+	StandbyOnFail    bool       `xml:"standby_onfail,attr"`
+	Maintenance      bool       `xml:"maintenance,attr"`
+	Pending          bool       `xml:"pending,attr"`
+	Unclean          bool       `xml:"unclean,attr"`
+	Shutdown         bool       `xml:"shutdown,attr"`
+	ExpectedUp       bool       `xml:"expected_up,attr"`
+	DC               bool       `xml:"is_dc,attr"`
+	ResourcesRunning int        `xml:"resources_running,attr"`
+	Type             string     `xml:"type,attr"`
+	Resources        []resource `xml:"resource"`
+}
+
+type resource struct {
+	ID             string `xml:"id,attr"`
+	Agent          string `xml:"resource_agent,attr"`
+	Role           string `xml:"role,attr"`
+	Active         bool   `xml:"active,attr"`
+	Orphaned       bool   `xml:"orphaned,attr"`
+	Blocked        bool   `xml:"blocked,attr"`
+	Managed        bool   `xml:"managed,attr"`
+	Failed         bool   `xml:"failed,attr"`
+	FailureIgnored bool   `xml:"failure_ignored,attr"`
+	NodesRunningOn int    `xml:"nodes_running_on,attr"`
+}
diff --git a/sbd_metrics.go b/sbd_metrics.go
index fb40f8d..eac8f2b 100644
--- a/sbd_metrics.go
+++ b/sbd_metrics.go
@@ -25,16 +25,22 @@ func readSdbFile() ([]byte, error) {
 }
 
 // return a list of sbd devices that we get from config
-func getSbdDevices(sbdConfigRaw []byte) []string {
+func getSbdDevices(sbdConfigRaw []byte) ([]string, error) {
 	// in config it can be both SBD_DEVICE="/dev/foo" or SBD_DEVICE=/dev/foo;/dev/bro
 	wordOnly := regexp.MustCompile("SBD_DEVICE=\"?[a-zA-Z-/;]+\"?")
 	sbdDevicesConfig := wordOnly.FindString(string(sbdConfigRaw))
+
+	// check the case there is an sbd_config but the SBD_DEVICE is not set
+
+	if sbdDevicesConfig == "" {
+		return nil, fmt.Errorf("[ERROR] there are no SBD_DEVICE set in configuration file")
+	}
 	// remove the SBD_DEVICE
 	sbdArray := strings.Split(sbdDevicesConfig, "SBD_DEVICE=")[1]
 	// make a list of devices by ; seperators and remove double quotes if present
 	sbdDevices := strings.Split(strings.Trim(sbdArray, "\""), ";")
 
-	return sbdDevices
+	return sbdDevices, nil
 }
 
 // this function take a list of sbd devices and return
diff --git a/tools/deploy-to-cluster.sh b/tools/deploy-to-cluster.sh
index 07143e4..8851e45 100755
--- a/tools/deploy-to-cluster.sh
+++ b/tools/deploy-to-cluster.sh
@@ -4,7 +4,7 @@
 
 # this script is just for deploying the binary to the cluster. Nothing else.
 
-node="root@10.162.31.221"
+node="root@10.67.18.240"
 
 ssh $node "rm /root/ha_cluster_exporter"
 echo "copying binary"


Test patch:
diff --git a/drbd_metrics_test.go b/drbd_metrics_test.go
new file mode 100644
index 0000000..f182a7a
--- /dev/null
+++ b/drbd_metrics_test.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"testing"
+)
+
+// this test verify that we return something when call the function.
+// we can't really test it since we need the binary
+func TestDrbdStatusFuncMinimalError(t *testing.T) {
+	fmt.Println("=== Testing DRBD : testing function to get infos")
+	getDrbdInfo()
+}
+
+func TestDrbdParsing(t *testing.T) {
+	var drbdDataRaw = []byte(` [
+		{
+		  "name": "1-single-0",
+		  "node-id": 2,
+		  "role": "Secondary",
+		  "suspended": false,
+		  "write-ordering": "flush",
+		  "devices": [
+			{
+			  "volume": 0,
+			  "minor": 2,
+			  "disk-state": "UpToDate",
+			  "client": false,
+			  "quorum": true,
+			  "size": 409600,
+			  "read": 0,
+			  "written": 548525,
+			  "al-writes": 4,
+			  "bm-writes": 0,
+			  "upper-pending": 0,
+			  "lower-pending": 0
+			} ],
+		  "connections": [
+			{
+			  "peer-node-id": 1,
+			  "name": "SLE15-sp1-gm-drbd1145296-node1",
+			  "connection-state": "Connected", 
+			  "congested": false,
+			  "peer-role": "Primary",
+			  "ap-in-flight": 0,
+			  "rs-in-flight": 0,
+			  "peer_devices": [
+				{
+				  "volume": 0,
+				  "replication-state": "Established",
+				  "peer-disk-state": "UpToDate",
+				  "peer-client": false,
+				  "resync-suspended": "no",
+				  "received": 548525,
+				  "sent": 0,
+				  "out-of-sync": 0,
+				  "pending": 0,
+				  "unacked": 0,
+				  "has-sync-details": false,
+				  "has-online-verify-details": false,
+				  "percent-in-sync": 100.00
+				} ]
+			} ]
+		}
+		,
+		{
+		  "name": "1-single-1",
+		  "node-id": 2,
+		  "role": "Secondary",
+		  "suspended": false,
+		  "write-ordering": "flush",
+		  "devices": [
+			{
+			  "volume": 0,
+			  "minor": 3,
+			  "disk-state": "UpToDate",
+			  "client": false,
+			  "quorum": true,
+			  "size": 10200,
+			  "read": 0,
+			  "written": 546005,
+			  "al-writes": 1,
+			  "bm-writes": 0,
+			  "upper-pending": 0,
+			  "lower-pending": 0
+			} ],
+		  "connections": [
+			{
+			  "peer-node-id": 1,
+			  "name": "SLE15-sp1-gm-drbd1145296-node1",
+			  "connection-state": "Connected", 
+			  "congested": false,
+			  "peer-role": "Primary",
+			  "ap-in-flight": 0,
+			  "rs-in-flight": 0,
+			  "peer_devices": [
+				{
+				  "volume": 0,
+				  "replication-state": "Established",
+				  "peer-disk-state": "UpToDate",
+				  "peer-client": false,
+				  "resync-suspended": "no",
+				  "received": 546005,
+				  "sent": 0,
+				  "out-of-sync": 0,
+				  "pending": 0,
+				  "unacked": 0,
+				  "has-sync-details": false,
+				  "has-online-verify-details": false,
+				  "percent-in-sync": 100.00
+				} ]
+			} ]
+		}]`)
+
+	drbdDevs, err := parseDrbdStatus(drbdDataRaw)
+	if err != nil {
+		log.Fatalln("[ERROR]:", err)
+	}
+	// test attributes
+	if "1-single-0" != drbdDevs[0].Name {
+		t.Errorf("name doesn't correspond! fail got %s", drbdDevs[0].Name)
+	}
+
+	if "Secondary" != drbdDevs[0].Role {
+		t.Errorf("role doesn't correspond! fail got %s", drbdDevs[0].Role)
+	}
+
+	if "UpToDate" != drbdDevs[0].Devices[0].DiskState {
+		t.Errorf("disk-states doesn't correspond! fail got %s", drbdDevs[0].Devices[0].DiskState)
+
+	}
+
+	if 0 != drbdDevs[0].Devices[0].Volume {
+		t.Errorf("volumes should be 0")
+	}
+
+}
diff --git a/sbd_metrics_test.go b/sbd_metrics_test.go
index 60b217a..81900de 100644
--- a/sbd_metrics_test.go
+++ b/sbd_metrics_test.go
@@ -125,7 +125,7 @@ func TestGetSbdDevicesWithoutDoubleQuotes(t *testing.T) {
 	 SBD_DEVICE=/dev/vdc;/dev/brother;/dev/syster																					
 			`
 
-	sbdDevices := getSbdDevices([]byte(sbdConfig))
+	sbdDevices, _ := getSbdDevices([]byte(sbdConfig))
 	// we should have 3 devices
 	expected := "/dev/vdc"
 	if sbdDevices[0] != expected {
@@ -173,7 +173,7 @@ func TestGetSbdDevicesWithDoubleQuotes(t *testing.T) {
 	 #
 	 SBD_OPTS=`
 
-	sbdDevices := getSbdDevices([]byte(sbdConfig))
+	sbdDevices, _ := getSbdDevices([]byte(sbdConfig))
 	// we should have 3 devices
 	expected := "/dev/vdc"
 	if sbdDevices[0] != expected {
@@ -208,7 +208,7 @@ func TestOnlyOneDeviceSbd(t *testing.T) {
 	 ## Default: "flush,reboot"
 `
 
-	sbdDevices := getSbdDevices([]byte(sbdConfig))
+	sbdDevices, _ := getSbdDevices([]byte(sbdConfig))
 
 	// we should have 1 device
 	expected := "/dev/vdc"