Repository: threefoldtech/zos
Pull request #359 (2019-10-31 13:40:06)

capacityd: send the hardware proof to BCDB at boot
when capacityd starts it does a scan fo all the hardware of the node.
this scan is sent to BCDB and this information is used to mint tokens.

part of #357

codecov[bot]: # [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=h1) Report
> Merging [#359](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=desc) into [master](https://codecov.io/gh/threefoldtech/zos/commit/1e9dadeb1f0dbec83147719e0dc823015cb9d48c?src=pr&el=desc) will **increase** coverage by `0.1%`.
> The diff coverage is `54.41%`.

[![Impacted file tree graph](https://codecov.io/gh/threefoldtech/zos/pull/359/graphs/tree.svg?width=650&token=f7Vmq1OAEm&height=150&src=pr)](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=tree)

```diff
@@           Coverage Diff            @@
##           master    #359     +/-   ##
========================================
+ Coverage   40.69%   40.8%   +0.1%     
========================================
  Files          60      61      +1     
  Lines        3720    3769     +49     
========================================
+ Hits         1514    1538     +24     
- Misses       2007    2030     +23     
- Partials      199     201      +2
```


| [Impacted Files](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/gedis/commands\_identity.go](https://codecov.io/gh/threefoldtech/zos/pull/359/diff?src=pr&el=tree#diff-cGtnL2dlZGlzL2NvbW1hbmRzX2lkZW50aXR5Lmdv) | `64.12% <ø> (-0.07%)` | :arrow_down: |
| [pkg/gedis/commands\_capacity.go](https://codecov.io/gh/threefoldtech/zos/pull/359/diff?src=pr&el=tree#diff-cGtnL2dlZGlzL2NvbW1hbmRzX2NhcGFjaXR5Lmdv) | `34.37% <42.3%> (+34.37%)` | :arrow_up: |
| [pkg/capacity/smartctl/smartctl.go](https://codecov.io/gh/threefoldtech/zos/pull/359/diff?src=pr&el=tree#diff-cGtnL2NhcGFjaXR5L3NtYXJ0Y3RsL3NtYXJ0Y3RsLmdv) | `61.9% <61.9%> (ø)` | |
| [pkg/provision/source.go](https://codecov.io/gh/threefoldtech/zos/pull/359/diff?src=pr&el=tree#diff-cGtnL3Byb3Zpc2lvbi9zb3VyY2UuZ28=) | `32.46% <0%> (-1.71%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=footer). Last update [1e9dade...94f9efa](https://codecov.io/gh/threefoldtech/zos/pull/359?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmds/capacityd/main.go b/cmds/capacityd/main.go
index 0a880741..2e46076c 100644
--- a/cmds/capacityd/main.go
+++ b/cmds/capacityd/main.go
@@ -70,8 +70,13 @@ func main() {
 		log.Fatal().Err(err).Msgf("failed to read DMI information from hardware")
 	}
 
+	disks, err := r.Disks()
+	if err != nil {
+		log.Fatal().Err(err).Msgf("failed to read smartctl information from disks")
+	}
+
 	log.Info().Msg("sends capacity detail to BCDB")
-	if err := store.Register(identity.NodeID(), *resources, *dmi); err != nil {
+	if err := store.Register(identity.NodeID(), *resources, *dmi, disks); err != nil {
 		log.Fatal().Err(err).Msgf("failed to write resources capacity on BCDB")
 	}
 
diff --git a/pkg/capacity/capacity.go b/pkg/capacity/capacity.go
index e662d3d9..9b76c9e1 100644
--- a/pkg/capacity/capacity.go
+++ b/pkg/capacity/capacity.go
@@ -4,6 +4,7 @@ import (
 	"github.com/shirou/gopsutil/host"
 	"github.com/threefoldtech/zos/pkg"
 	"github.com/threefoldtech/zos/pkg/capacity/dmi"
+	"github.com/threefoldtech/zos/pkg/capacity/smartctl"
 )
 
 // Capacity hold the amount of resource unit of a node
@@ -61,3 +62,40 @@ func (r *ResourceOracle) Uptime() (uint64, error) {
 	}
 	return info.Uptime, nil
 }
+
+// Disks contains the hardware information about the disk of a node
+type Disks struct {
+	Tool        string          `json:"tool"`
+	Environment string          `json:"environment"`
+	Aggregator  string          `json:"aggregator"`
+	Devices     []smartctl.Info `json:"devices"`
+}
+
+// Disks list and parse the hardware information using smartctl
+func (r *ResourceOracle) Disks() (d Disks, err error) {
+	devices, err := smartctl.ListDevices()
+	if err != nil {
+		return
+	}
+
+	var info smartctl.Info
+	d.Devices = make([]smartctl.Info, len(devices))
+
+	for i, device := range devices {
+		info, err = smartctl.DeviceInfo(device)
+		if err != nil {
+			return
+		}
+		d.Devices[i] = info
+		if d.Environment == "" {
+			d.Environment = info.Environment
+		}
+		if d.Tool == "" {
+			d.Tool = info.Tool
+		}
+	}
+
+	d.Aggregator = "0-OS smartctl aggregator"
+
+	return
+}
diff --git a/pkg/capacity/smartctl/smartctl.go b/pkg/capacity/smartctl/smartctl.go
new file mode 100644
index 00000000..f6010c10
--- /dev/null
+++ b/pkg/capacity/smartctl/smartctl.go
@@ -0,0 +1,98 @@
+package smartctl
+
+import (
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strings"
+)
+
+var reScan = regexp.MustCompile(`(?m)^([^\s]+)\s+-d\s+([^\s]+)\s+#`)
+var reHeader = regexp.MustCompile(`(?m)([^\[]+)\[([^\[]+)\]`)
+var reInfo = regexp.MustCompile(`(?m)([^:]+):\s+(.+)`)
+
+// Device represents a device as returned by "smartctl --scan"
+type Device struct {
+	Type string
+	Path string
+}
+
+// ListDevices return a list of device present on the local system
+func ListDevices() ([]Device, error) {
+	cmd := exec.Command("smartctl", "--scan")
+	output, err := cmd.Output()
+	if err != nil {
+		return nil, err
+	}
+
+	return parseScan(output)
+}
+
+// Info contains information about a device as returned by "smartctl -i {path} -d {type}"
+type Info struct {
+	Tool        string
+	Environment string
+	Information map[string]string
+}
+
+// DeviceInfo info return the information from a specific device
+func DeviceInfo(d Device) (Info, error) {
+	cmd := exec.Command("smartctl", "-i", d.Path, "-d", d.Type)
+	output, err := cmd.Output()
+	if err != nil {
+		return Info{}, err
+	}
+
+	return parseInfo(output)
+}
+
+func parseScan(b []byte) ([]Device, error) {
+	trimed := strings.TrimSpace(string(b))
+	lines := strings.Split(trimed, "\n")
+	devices := make([]Device, len(lines))
+
+	for i, line := range lines {
+		match := reScan.FindStringSubmatch(line)
+		if len(match) != 3 {
+			return nil, fmt.Errorf("failed to parse ourput of 'smartctl --scan'")
+		}
+		devices[i] = Device{
+			Type: match[2],
+			Path: match[1],
+		}
+	}
+
+	return devices, nil
+}
+
+func parseInfo(b []byte) (Info, error) {
+	info := Info{}
+	trimed := strings.TrimSpace(string(b))
+	lines := strings.Split(trimed, "\n")
+	if len(lines) == 0 {
+		return info, fmt.Errorf("fail to parse smarctl device information")
+	}
+
+	header := reHeader.FindStringSubmatch(lines[0])
+	info.Tool = strings.TrimSpace(header[1])
+	info.Environment = strings.TrimSpace(header[2])
+	info.Information = map[string]string{}
+
+	for _, line := range lines[1:] {
+		match := reInfo.FindStringSubmatch(line)
+		if len(match) < 3 {
+			continue
+		}
+
+		k := strings.TrimSpace(match[1])
+		v := strings.TrimSpace(match[2])
+
+		if k == "Local Time is" {
+			// not needed and we don't want to have time based value
+			continue
+		}
+		info.Information[k] = v
+	}
+
+	return info, nil
+}
diff --git a/pkg/capacity/store.go b/pkg/capacity/store.go
index 7a26f8f7..652327a2 100644
--- a/pkg/capacity/store.go
+++ b/pkg/capacity/store.go
@@ -14,7 +14,7 @@ import (
 
 // Store is an interface to the bcdb store to report capacity
 type Store interface {
-	Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI) error
+	Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI, disks Disks) error
 	Ping(nodeID pkg.Identifier, uptime uint64) error
 }
 
@@ -29,8 +29,12 @@ func NewBCDBStore(gedis *gedis.Gedis) *BCDBStore {
 }
 
 // Register sends the capacity information to BCDB
-func (s *BCDBStore) Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI) error {
-	return s.g.UpdateTotalNodeCapacity(nodeID, c.MRU, c.CRU, c.HRU, c.SRU)
+func (s *BCDBStore) Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI, disks Disks) error {
+	if err := s.g.UpdateTotalNodeCapacity(nodeID, c.MRU, c.CRU, c.HRU, c.SRU); err != nil {
+		return err
+	}
+
+	return s.g.SendHardwareProof(nodeID, d, disks)
 }
 
 // Ping sends an heart-beat to BCDB
@@ -49,13 +53,15 @@ func NewHTTPStore(url string) *HTTPStore {
 }
 
 // Register sends the capacity information to BCDB
-func (s *HTTPStore) Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI) error {
+func (s *HTTPStore) Register(nodeID pkg.Identifier, c Capacity, d dmi.DMI, disks Disks) error {
 	x := struct {
-		Capacity Capacity `json:"capacity,omitempty"`
-		DMI      dmi.DMI  `json:"dmi,omitempty"`
+		Capacity Capacity `json:"capacity"`
+		DMI      dmi.DMI  `json:"dmi"`
+		Disks    Disks    `json:"disks"`
 	}{
 		Capacity: c,
 		DMI:      d,
+		Disks:    disks,
 	}
 	buf := bytes.Buffer{}
 	err := json.NewEncoder(&buf).Encode(x)
diff --git a/pkg/gedis/commands_capacity.go b/pkg/gedis/commands_capacity.go
index 329c985f..1257d4ae 100644
--- a/pkg/gedis/commands_capacity.go
+++ b/pkg/gedis/commands_capacity.go
@@ -2,6 +2,7 @@ package gedis
 
 import (
 	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
 )
 
 //UptimeUpdate send the uptime of the node to BCDB
@@ -14,3 +15,45 @@ func (g *Gedis) UptimeUpdate(nodeID pkg.Identifier, uptime uint64) error {
 
 	return err
 }
+
+func (g *Gedis) updateGenericNodeCapacity(captype string, node pkg.Identifier, mru, cru, hru, sru uint64) error {
+	_, err := g.Send("nodes", "update_"+captype+"_capacity", Args{
+		"node_id": node.Identity(),
+		"resource": directory.TfgridNodeResourceAmount1{
+			Cru: int64(cru),
+			Mru: int64(mru),
+			Hru: int64(hru),
+			Sru: int64(sru),
+		},
+	})
+
+	return err
+}
+
+//UpdateTotalNodeCapacity implements pkg.IdentityManager interface
+func (g *Gedis) UpdateTotalNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
+	return g.updateGenericNodeCapacity("total", node, mru, cru, hru, sru)
+}
+
+//UpdateReservedNodeCapacity implements pkg.IdentityManager interface
+func (g *Gedis) UpdateReservedNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
+	return g.updateGenericNodeCapacity("reserved", node, mru, cru, hru, sru)
+}
+
+//UpdateUsedNodeCapacity implements pkg.IdentityManager interface
+func (g *Gedis) UpdateUsedNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
+	return g.updateGenericNodeCapacity("used", node, mru, cru, hru, sru)
+}
+
+// SendHardwareProof sends a dump of hardware and disks to BCDB
+func (g *Gedis) SendHardwareProof(node pkg.Identifier, dmi interface{}, disks interface{}) error {
+	_, err := g.Send("nodes", "add_proof", Args{
+		"node_id": node.Identity(),
+		"proof": map[string]interface{}{
+			"hardware": dmi,
+			"disks":    disks,
+		},
+	})
+
+	return err
+}
diff --git a/pkg/gedis/commands_identity.go b/pkg/gedis/commands_identity.go
index 231a9ac4..73707f66 100644
--- a/pkg/gedis/commands_identity.go
+++ b/pkg/gedis/commands_identity.go
@@ -184,35 +184,6 @@ func farmFromSchema(farm directory.TfgridFarm1) network.Farm {
 	}
 }
 
-func (g *Gedis) updateGenericNodeCapacity(captype string, node pkg.Identifier, mru, cru, hru, sru uint64) error {
-	_, err := g.Send("nodes", "update_"+captype+"_capacity", Args{
-		"node_id": node.Identity(),
-		"resource": directory.TfgridNodeResourceAmount1{
-			Cru: int64(cru),
-			Mru: int64(mru),
-			Hru: int64(hru),
-			Sru: int64(sru),
-		},
-	})
-
-	return err
-}
-
-//UpdateTotalNodeCapacity implements pkg.IdentityManager interface
-func (g *Gedis) UpdateTotalNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
-	return g.updateGenericNodeCapacity("total", node, mru, cru, hru, sru)
-}
-
-//UpdateReservedNodeCapacity implements pkg.IdentityManager interface
-func (g *Gedis) UpdateReservedNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
-	return g.updateGenericNodeCapacity("reserved", node, mru, cru, hru, sru)
-}
-
-//UpdateUsedNodeCapacity implements pkg.IdentityManager interface
-func (g *Gedis) UpdateUsedNodeCapacity(node pkg.Identifier, mru, cru, hru, sru uint64) error {
-	return g.updateGenericNodeCapacity("used", node, mru, cru, hru, sru)
-}
-
 //GetFarm implements pkg.IdentityManager interface
 func (g *Gedis) GetFarm(farm pkg.Identifier) (*network.Farm, error) {
 	id, err := strconv.ParseInt(farm.Identity(), 10, 64)
diff --git a/pkg/gedis/types/directory/tfgrid_node_2.go b/pkg/gedis/types/directory/tfgrid_node_2.go
index 880a3464..3079664a 100644
--- a/pkg/gedis/types/directory/tfgrid_node_2.go
+++ b/pkg/gedis/types/directory/tfgrid_node_2.go
@@ -62,6 +62,11 @@ type TfgridNodeProof1 struct {
 	Disks        map[string]interface{} `json:"disks"`
 }
 
+// Equal test of 2 proofs have the same hashes
+func (p TfgridNodeProof1) Equal(proof TfgridNodeProof1) bool {
+	return p.DiskHash == proof.DiskHash && p.HardwareHash == proof.HardwareHash
+}
+
 // TfgridNodePublicIface1TypeEnum jsx schema
 type TfgridNodePublicIface1TypeEnum uint8
 
diff --git a/tools/bcdb_mock/node_handlers.go b/tools/bcdb_mock/node_handlers.go
index a753f5b7..debaed02 100644
--- a/tools/bcdb_mock/node_handlers.go
+++ b/tools/bcdb_mock/node_handlers.go
@@ -6,6 +6,8 @@ import (
 	"log"
 	"net/http"
 
+	"github.com/threefoldtech/zos/pkg/capacity"
+
 	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
 	"github.com/threefoldtech/zos/pkg/network/types"
 
@@ -97,7 +99,8 @@ func (s *nodeStore) cockpitListNodes(w http.ResponseWriter, r *http.Request) {
 func (s *nodeStore) registerCapacity(w http.ResponseWriter, r *http.Request) {
 	x := struct {
 		Capacity directory.TfgridNodeResourceAmount1 `json:"capacity,omitempty"`
-		DMI      *dmi.DMI                            `json:"dmi,omitempty"`
+		DMI      dmi.DMI                             `json:"dmi,omitempty"`
+		Disks    capacity.Disks                      `json:"disks,omitempty"`
 	}{}
 
 	if err := json.NewDecoder(r.Body).Decode(&x); err != nil {
@@ -111,6 +114,11 @@ func (s *nodeStore) registerCapacity(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if err := s.StoreProof(nodeID, x.DMI, x.Disks); err != nil {
+		httpError(w, err, http.StatusNotFound)
+		return
+	}
+
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
 }
diff --git a/tools/bcdb_mock/nodes_store.go b/tools/bcdb_mock/nodes_store.go
index 41a9a687..616036cc 100644
--- a/tools/bcdb_mock/nodes_store.go
+++ b/tools/bcdb_mock/nodes_store.go
@@ -1,15 +1,19 @@
 package main
 
 import (
+	"crypto/md5"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"os"
+	"sort"
 	"sync"
 	"time"
 
 	"github.com/gorilla/mux"
 	"github.com/pkg/errors"
+	"github.com/threefoldtech/zos/pkg/capacity"
+	"github.com/threefoldtech/zos/pkg/capacity/dmi"
 	"github.com/threefoldtech/zos/pkg/schema"
 
 	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
@@ -136,6 +140,48 @@ func (s *nodeStore) updateUptime(nodeID string, uptime int64) error {
 	return nil
 }
 
+func (s *nodeStore) StoreProof(nodeID string, dmi dmi.DMI, disks capacity.Disks) error {
+	node, err := s.Get(nodeID)
+	if err != nil {
+		return err
+	}
+
+	proof := directory.TfgridNodeProof1{
+		Created: schema.Date{Time: time.Now()},
+	}
+
+	proof.Hardware = map[string]interface{}{
+		"sections": dmi.Sections,
+		"tooling":  dmi.Tooling,
+	}
+	proof.HardwareHash, err = hashProof(proof.Hardware)
+	if err != nil {
+		return err
+	}
+
+	proof.Disks = map[string]interface{}{
+		"aggregator":  disks.Aggregator,
+		"environment": disks.Environment,
+		"devices":     disks.Devices,
+		"tool":        disks.Tool,
+	}
+	proof.DiskHash, err = hashProof(proof.Disks)
+	if err != nil {
+		return err
+	}
+
+	// don't save the proof if we already have one with the same
+	// hash/content
+	for _, p := range node.Proofs {
+		if proof.Equal(p) {
+			return nil
+		}
+	}
+
+	node.Proofs = append(node.Proofs, proof)
+	return nil
+}
+
 func (s *nodeStore) SetInterfaces(nodeID string, ifaces []directory.TfgridNodeIface1) error {
 	node, err := s.Get(nodeID)
 	if err != nil {
@@ -184,3 +230,30 @@ func (s *nodeStore) Requires(key string, handler http.HandlerFunc) http.HandlerF
 		handler(w, r)
 	}
 }
+
+// hashProof return the hex encoded md5 hash of the json encoded version of p
+func hashProof(p map[string]interface{}) (string, error) {
+
+	// we are trying to have always produce same hash for same content of p
+	// so we convert the map into a list so we can sort
+	// the key and workaround the fact that maps are not sorted
+
+	type kv struct {
+		k string
+		v interface{}
+	}
+
+	kvs := make([]kv, len(p))
+	for k, v := range p {
+		kvs = append(kvs, kv{k: k, v: v})
+	}
+	sort.Slice(kvs, func(i, j int) bool { return kvs[i].k < kvs[j].k })
+
+	b, err := json.Marshal(kvs)
+	if err != nil {
+		return "", err
+	}
+	h := md5.New()
+	bh := h.Sum(b)
+	return fmt.Sprintf("%x", bh), nil
+}


Test patch:
diff --git a/cmds/flistd/rpc_test.go b/cmds/flistd/rpc_test.go
deleted file mode 100644
index 523a26d3..00000000
--- a/cmds/flistd/rpc_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package main
-
-import (
-	"context"
-	"flag"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/rs/zerolog/log"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"github.com/threefoldtech/zbus"
-	"github.com/threefoldtech/zos/pkg"
-	"github.com/threefoldtech/zos/pkg/flist"
-	"github.com/threefoldtech/zos/pkg/flist/mock"
-	"github.com/threefoldtech/zos/pkg/stubs"
-)
-
-var (
-	rpcTest = flag.Bool("rpc", false, "run RPC tests")
-)
-
-func TestMain(m *testing.M) {
-	flag.Parse()
-
-	os.Exit(m.Run())
-}
-
-func testFlistModule(t *testing.T) (pkg.Flister, func()) {
-	root, err := ioutil.TempDir("", "flist_root")
-	require.NoError(t, err)
-
-	cleanup := func() {
-		os.RemoveAll(root)
-	}
-	return flist.New(root, &mock.StorageMock{}), cleanup
-}
-
-func testPrepareRPC(t *testing.T) (pkg.Flister, func()) {
-	const redisAddr = "tcp://localhost:6379"
-
-	f, cleanup := testFlistModule(t)
-
-	server, err := zbus.NewRedisServer("flist", redisAddr, 1)
-	if err != nil {
-		log.Fatal().Msgf("fail to connect to message broker server: %v\n", err)
-	}
-
-	server.Register(zbus.ObjectID{Name: "flist", Version: "0.0.1"}, f)
-	go func() {
-		err := server.Run(context.Background())
-		require.NoError(t, err)
-	}()
-
-	client, err := zbus.NewRedisClient(redisAddr)
-	require.NoError(t, err)
-
-	flist := stubs.NewFlisterStub(client)
-	return flist, cleanup
-}
-
-func TestRPCMountUmount(t *testing.T) {
-	// only run these test when rpc flag is passed
-	if !*rpcTest {
-		t.SkipNow()
-	}
-
-	require := require.New(t)
-	assert := assert.New(t)
-
-	flist, cleanup := testPrepareRPC(t)
-	defer cleanup()
-
-	path, err := flist.Mount("https://hub.grid.tf/zaibon/ubuntu_bionic.flist", "")
-	require.NoError(err)
-
-	// check file are accessible
-	assert.NotEqual("", path)
-	infos, err := ioutil.ReadDir(path)
-	require.NoError(err)
-	assert.True(len(infos) > 0)
-
-	// check pid file exists
-	dir, name := filepath.Split(path)
-	dir, _ = filepath.Split(dir[:len(dir)-1])
-	pidPath := filepath.Join(dir, "pid", name) + ".pid"
-	_, err = os.Stat(pidPath)
-	assert.NoError(err)
-
-	err = flist.Umount(path)
-	assert.NoError(err)
-
-	// check pid file is gone after unmount
-	_, err = os.Stat(pidPath)
-	assert.Error(err)
-	assert.True(os.IsNotExist(err))
-}
diff --git a/pkg/capacity/smartctl/smartctl_test.go b/pkg/capacity/smartctl/smartctl_test.go
new file mode 100644
index 00000000..89c07636
--- /dev/null
+++ b/pkg/capacity/smartctl/smartctl_test.go
@@ -0,0 +1,51 @@
+package smartctl
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseScanOutput(t *testing.T) {
+	b := []byte(`
+/dev/sda -d scsi # /dev/sda, SCSI device
+/dev/sdb -d scsi # /dev/sdb, SCSI device`)
+	devices, err := parseScan(b)
+	require.NoError(t, err)
+
+	assert.Equal(t, 2, len(devices))
+	assert.Equal(t, "/dev/sda", devices[0].Path)
+	assert.Equal(t, "scsi", devices[0].Type)
+	assert.Equal(t, "/dev/sdb", devices[1].Path)
+	assert.Equal(t, "scsi", devices[1].Type)
+}
+
+func TestParseInfo(t *testing.T) {
+	b := []byte(`
+	smartctl 7.0 2018-12-30 r4883 [x86_64-linux-4.14.82-Zero-OS] (local build)
+Copyright (C) 2002-18, Bruce Allen, Christian Franke, www.smartmontools.org
+
+User Capacity:        480,103,981,056 bytes [480 GB]
+Logical block size:   512 bytes
+LU is fully provisioned
+Rotation Rate:        Solid State Device
+Serial number:        E20150714150192
+Device type:          disk
+Local Time is:        Thu Oct 31 11:24:29 2019 UTC
+SMART support is:     Unavailable - device lacks SMART capability.`)
+
+	info, err := parseInfo(b)
+	require.NoError(t, err)
+	assert.Equal(t, "smartctl 7.0 2018-12-30 r4883", info.Tool)
+	assert.Equal(t, "x86_64-linux-4.14.82-Zero-OS", info.Environment)
+	assert.Equal(t, info.Information["User Capacity"], "480,103,981,056 bytes [480 GB]")
+	assert.Equal(t, info.Information["Logical block size"], "512 bytes")
+	assert.Equal(t, info.Information["Rotation Rate"], "Solid State Device")
+	assert.Equal(t, info.Information["Serial number"], "E20150714150192")
+	assert.Equal(t, info.Information["Device type"], "disk")
+	assert.Equal(t, info.Information["SMART support is"], "Unavailable - device lacks SMART capability.")
+	_, exists := info.Information["local Time is"]
+	assert.False(t, exists, "Local time should not be included in information")
+}
diff --git a/tools/bcdb_mock/hash_test.go b/tools/bcdb_mock/hash_test.go
new file mode 100644
index 00000000..f944f5db
--- /dev/null
+++ b/tools/bcdb_mock/hash_test.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"crypto/rand"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestHashProof(t *testing.T) {
+	// ensure hasProof always generate the same hash
+	// for the same content whatever the sorting in the map
+
+	m := map[string]interface{}{}
+
+	b := make([]byte, 4096)
+	for i := 0; i < 500; i++ {
+		_, err := rand.Read(b)
+		require.NoError(t, err)
+
+		k := fmt.Sprintf("%x", b[2048:])
+		v := fmt.Sprintf("%x", b[:2048])
+		m[k] = v
+	}
+
+	h, err := hashProof(m)
+	require.NoError(t, err)
+	for i := 0; i < 1000; i++ {
+		h2, err := hashProof(m)
+		require.NoError(t, err)
+		assert.Equal(t, h, h2)
+	}
+}