Repository: threefoldtech/zos
Pull request #308 (2019-10-03 13:56:00)

enable CI
Early draft to show the changes in tests

zaibon: Had a first quick overview, this looks realy cool.

One thing I forgot to mention. In the github action workload, to run the test we call `make coverage`:
https://github.com/threefoldtech/zos/blob/da3d88e815007cac78134a8b89758092708d6a3e/.github/workflows/test.yml#L26-L29
which in turn execute this script: https://github.com/threefoldtech/zos/blob/master/pkg/buildscripts/go-coverage.sh

So you have to also remove the module from the grep to enable the test on them. This is a bit confusing mess. maybe we could improve the makefile/workflow a bit
codecov[bot]: # [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/308?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@da3d88e`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `42.85%`.

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

```diff
@@            Coverage Diff            @@
##             master     #308   +/-   ##
=========================================
  Coverage          ?   43.82%           
=========================================
  Files             ?       32           
  Lines             ?     1896           
  Branches          ?        0           
=========================================
  Hits              ?      831           
  Misses            ?      963           
  Partials          ?      102
```


| [Impacted Files](https://codecov.io/gh/threefoldtech/zos/pull/308?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/storage.go](https://codecov.io/gh/threefoldtech/zos/pull/308/diff?src=pr&el=tree#diff-cGtnL3N0b3JhZ2UuZ28=) | `0% <ø> (ø)` | |
| [pkg/gedis/gedis.go](https://codecov.io/gh/threefoldtech/zos/pull/308/diff?src=pr&el=tree#diff-cGtnL2dlZGlzL2dlZGlzLmdv) | `14.03% <ø> (ø)` | |
| [pkg/schema/types.go](https://codecov.io/gh/threefoldtech/zos/pull/308/diff?src=pr&el=tree#diff-cGtnL3NjaGVtYS90eXBlcy5nbw==) | `59.52% <40%> (ø)` | |
| [pkg/flist/flist.go](https://codecov.io/gh/threefoldtech/zos/pull/308/diff?src=pr&el=tree#diff-cGtnL2ZsaXN0L2ZsaXN0Lmdv) | `56.35% <44.44%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/threefoldtech/zos/pull/308?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/308?src=pr&el=footer). Last update [da3d88e...c7c1953](https://codecov.io/gh/threefoldtech/zos/pull/308?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/go.mod b/go.mod
index b3a450cc..bfa40204 100644
--- a/go.mod
+++ b/go.mod
@@ -9,24 +9,16 @@ require (
 	github.com/blang/semver v3.5.1+incompatible
 	github.com/cenkalti/backoff v2.2.1+incompatible
 	github.com/cenkalti/backoff/v3 v3.0.0
-	github.com/containerd/cgroups v0.0.0-20190911145653-fc51bcbe4714 // indirect
 	github.com/containerd/containerd v1.2.3
-	github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 // indirect
-	github.com/containerd/cri v1.11.1 // indirect
-	github.com/containerd/fifo v0.0.0-20190816180239-bda0ff6ed73c // indirect
-	github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd // indirect
 	github.com/containernetworking/cni v0.7.1
 	github.com/containernetworking/plugins v0.8.2
 	github.com/dave/jennifer v1.3.0
 	github.com/deckarep/golang-set v1.7.1
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
-	github.com/docker/distribution v2.7.1+incompatible // indirect
-	github.com/docker/docker v1.13.1 // indirect
-	github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
 	github.com/garyburd/redigo v1.6.0
+	github.com/go-check/check v0.0.0-20190902080502-41f04d3bba15 // indirect
 	github.com/go-ole/go-ole v1.2.4 // indirect
 	github.com/go-redis/redis v6.15.5+incompatible
-	github.com/gogo/googleapis v1.3.0 // indirect
 	github.com/gomodule/redigo v2.0.0+incompatible
 	github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
 	github.com/google/uuid v1.1.1
@@ -34,26 +26,20 @@ require (
 	github.com/gorilla/mux v1.7.3
 	github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
 	github.com/jbenet/go-base58 v0.0.0-20150317085156-6237cf65f3a6
-	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
-	github.com/opencontainers/image-spec v1.0.1 // indirect
-	github.com/opencontainers/runc v0.1.1 // indirect
 	github.com/opencontainers/runtime-spec v1.0.1
 	github.com/pkg/errors v0.8.1
 	github.com/rs/zerolog v1.15.0
 	github.com/shirou/gopsutil v2.18.12+incompatible
 	github.com/stretchr/testify v1.3.0
-	github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect
 	github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8
 	github.com/threefoldtech/zbus v0.0.0-20190711124326-09379d5f12e0
+	github.com/threefoldtech/zosv2/modules v0.0.0-20191001184911-b6d8bc81a7f0
 	github.com/urfave/cli v1.22.1
 	github.com/vishvananda/netlink v1.0.0
 	github.com/whs/nacl-sealed-box v0.0.0-20180930164530-92b9ba845d8d
 	golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472
-	golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
 	golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa
 	golang.zx2c4.com/wireguard/wgctrl v0.0.0-20190607034155-226bf4e412cd
-	google.golang.org/appengine v1.6.1 // indirect
 	google.golang.org/grpc v1.24.0 // indirect
 	gopkg.in/yaml.v2 v2.2.2
-	gotest.tools v2.2.0+incompatible // indirect
 )
diff --git a/go.sum b/go.sum
index f6090177..bb9624e4 100644
--- a/go.sum
+++ b/go.sum
@@ -65,8 +65,11 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ
 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
 github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
 github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/dspinhirne/netaddr-go v0.0.0-20180510133009-a6cfb692cb10/go.mod h1:qYpr/lzZIoEWpzbsTHa3Tl9V+g2sN/MAjkIyEItb7/g=
 github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
 github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/go-check/check v0.0.0-20190902080502-41f04d3bba15 h1:xJdCV5uP69sUzCIIzmhAw6EKKdVk3Tu48oLzM86+XPI=
+github.com/go-check/check v0.0.0-20190902080502-41f04d3bba15/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
 github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk=
@@ -170,6 +173,9 @@ github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 h1:RB0v+/pc8oMzPsN
 github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg=
 github.com/threefoldtech/zbus v0.0.0-20190711124326-09379d5f12e0 h1:k+e+laNT3indP3ToSnw4yrD9d9nDTOjLKlc11QzspFo=
 github.com/threefoldtech/zbus v0.0.0-20190711124326-09379d5f12e0/go.mod h1:ZtiRpcqzEBJetVQDsEbw0p48h/AF3O1kf0tvd30I0BU=
+github.com/threefoldtech/zosv2 v0.0.0-20191002114057-b953302b36fc h1:J2lqgqkT3LmbbzMb4vMf1RHmSOIMed+0QY2HFbTmTRc=
+github.com/threefoldtech/zosv2/modules v0.0.0-20191001184911-b6d8bc81a7f0 h1:97hA6GlUHa+QBspggGLZvOtX9r9k+dgXSQzjjAiASGg=
+github.com/threefoldtech/zosv2/modules v0.0.0-20191001184911-b6d8bc81a7f0/go.mod h1:Te9x3NPyMBxjdALwqX5I8ksUUCte6Fv7FFISIKAYus4=
 github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
@@ -234,6 +240,7 @@ google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
 google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
 gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
diff --git a/pkg/Makefile b/pkg/Makefile
index dd578422..5e88a3cf 100644
--- a/pkg/Makefile
+++ b/pkg/Makefile
@@ -47,7 +47,7 @@ check: test
 test: verifiers build
 	# we already ran vet separately, so safe to turn it off here
 	@echo "Running unit tests with GOFLAGS=${GOFLAGS}"
-	for pkg in $(shell go list ./... | grep -Ev "stubs|flist|provision|network|storage|gedis" ); do \
+	for pkg in $(shell go list ./... | grep -Ev "stubs|provision|network" ); do \
 		go test -v -vet=off $$pkg; \
 	done
 
diff --git a/pkg/buildscripts/go-coverage.sh b/pkg/buildscripts/go-coverage.sh
index fb71c536..db5c4022 100644
--- a/pkg/buildscripts/go-coverage.sh
+++ b/pkg/buildscripts/go-coverage.sh
@@ -3,7 +3,7 @@
 set -e
 echo "" > coverage.txt
 
-for d in $(go list ./... | grep -Ev "stubs|flist|provision|network|storage|gedis" ); do
+for d in $(go list ./... | grep -Ev "stubs|provision|network" ); do
     echo "test $d"
     go test -vet=off -coverprofile=profile.out -race -covermode=atomic "$d"
     if [ -f profile.out ]; then
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 7b5e7609..26a87368 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -30,11 +30,11 @@ const (
 
 var (
 	ignoreMntTypes = map[string]struct{}{
-		"proc":   struct{}{},
-		"tmpfs":  struct{}{},
-		"devpts": struct{}{},
-		"mqueue": struct{}{},
-		"sysfs":  struct{}{},
+		"proc":   {},
+		"tmpfs":  {},
+		"devpts": {},
+		"mqueue": {},
+		"sysfs":  {},
 	}
 )
 
diff --git a/pkg/flist/flist.go b/pkg/flist/flist.go
index 694f859b..c7fcc7d7 100644
--- a/pkg/flist/flist.go
+++ b/pkg/flist/flist.go
@@ -28,6 +28,16 @@ const (
 
 const mib = 1024 * 1024
 
+type commander interface {
+	Command(name string, arg ...string) *exec.Cmd
+}
+
+type cmd func(name string, arg ...string) *exec.Cmd
+
+func (c cmd) Command(name string, args ...string) *exec.Cmd {
+	return c(name, args...)
+}
+
 type flistModule struct {
 	// root directory where all
 	// the working file of the module will be located
@@ -41,11 +51,11 @@ type flistModule struct {
 	pid        string
 	log        string
 
-	storage pkg.VolumeAllocater
+	storage   pkg.VolumeAllocater
+	commander commander
 }
 
-// New creates a new flistModule
-func New(root string, storage pkg.VolumeAllocater) pkg.Flister {
+func newFlister(root string, storage pkg.VolumeAllocater, commander commander) pkg.Flister {
 	if root == "" {
 		root = defaultRoot
 	}
@@ -69,10 +79,16 @@ func New(root string, storage pkg.VolumeAllocater) pkg.Flister {
 		pid:        filepath.Join(root, "pid"),
 		log:        filepath.Join(root, "log"),
 
-		storage: storage,
+		storage:   storage,
+		commander: commander,
 	}
 }
 
+// New creates a new flistModule
+func New(root string, storage pkg.VolumeAllocater) pkg.Flister {
+	return newFlister(root, storage, cmd(exec.Command))
+}
+
 // Mount implements the Flister.Mount interface
 func (f *flistModule) Mount(url, storage string) (string, error) {
 	sublog := log.With().Str("url", url).Str("storage", storage).Logger()
@@ -117,7 +133,7 @@ func (f *flistModule) Mount(url, storage string) (string, error) {
 		mountpoint,
 	}
 	sublog.Info().Strs("args", args).Msg("starting 0-fs daemon")
-	cmd := exec.Command("g8ufs", args...)
+	cmd := f.commander.Command("g8ufs", args...)
 
 	if out, err := cmd.CombinedOutput(); err != nil {
 		sublog.Err(err).Str("out", string(out)).Msg("fail to start 0-fs daemon")
@@ -161,7 +177,7 @@ func (f *flistModule) Umount(path string) error {
 	_, name := filepath.Split(path)
 	pidPath := filepath.Join(f.pid, name) + ".pid"
 	if err := waitPidFile(time.Second*2, pidPath, false); err != nil {
-		log.Error().Err(err).Str("path", path).Msg("0-fs daemon did not stopped properly")
+		log.Error().Err(err).Str("path", path).Msg("0-fs daemon did not stop properly")
 		return err
 	}
 
diff --git a/pkg/flist/mock/storage_mock.go b/pkg/flist/mock/storage_mock.go
deleted file mode 100644
index bfea663a..00000000
--- a/pkg/flist/mock/storage_mock.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package mock
-
-import (
-	"fmt"
-	"io/ioutil"
-	"os"
-
-	"github.com/threefoldtech/zos/pkg"
-)
-
-// StorageMock is a mock object of the storage modules
-type StorageMock struct {
-	subvolumes map[string]string
-}
-
-// CreateFilesystem implements the pkg.StorageModules interfaces
-func (s *StorageMock) CreateFilesystem(name string, size uint64, poolType pkg.DeviceType) (string, error) {
-	path, err := ioutil.TempDir("", "")
-	if err != nil {
-		return "", err
-	}
-
-	if s.subvolumes == nil {
-		s.subvolumes = make(map[string]string, 1)
-	}
-	s.subvolumes[name] = path
-
-	return name, nil
-}
-
-// ReleaseFilesystem implements the pkg.StorageModules interfaces
-func (s *StorageMock) ReleaseFilesystem(name string) error {
-	path, ok := s.subvolumes[name]
-	if !ok {
-		return nil
-	}
-	return os.RemoveAll(path)
-}
-
-// Path implements the pkg.StorageModules interfaces
-func (s *StorageMock) Path(name string) (path string, err error) {
-	path, ok := s.subvolumes[name]
-	if !ok {
-		return "", fmt.Errorf("subvolume %s not found", name)
-	}
-	return path, nil
-}
diff --git a/pkg/gedis/commands_provision.go b/pkg/gedis/commands_provision.go
index 16b9c44a..73c18ce1 100644
--- a/pkg/gedis/commands_provision.go
+++ b/pkg/gedis/commands_provision.go
@@ -150,14 +150,9 @@ func (g *Gedis) Feedback(id string, r *provision.Result) error {
 		Epoch:      schema.Date{r.Created},
 	}
 
-	b, err := json.Marshal(result)
-	if err != nil {
-		return err
-	}
-
 	_, err = g.Send("workload_manager", "set_workload_result", Args{
 		"reservation_id": rID,
-		"result":         b,
+		"result":         result,
 	})
 	return err
 }
diff --git a/pkg/gedis/gedis.go b/pkg/gedis/gedis.go
index 40c7075a..60c3ed23 100644
--- a/pkg/gedis/gedis.go
+++ b/pkg/gedis/gedis.go
@@ -38,12 +38,17 @@ var (
 // Args is a helper to create map easily
 type Args map[string]interface{}
 
+// Pool is interface for a redis pool
+type Pool interface {
+	Get() redis.Conn
+	Close() error
+}
+
 // Gedis struct represent a client to a gedis server
 type Gedis struct {
-	pool      *redis.Pool
+	pool      Pool
 	namespace string
 	password  string
-	conn      redis.Conn
 }
 
 // New creates a new Gedis client
@@ -63,12 +68,6 @@ func New(address, namespace, password string) (*Gedis, error) {
 // Close closes all connection to the gedis server and stops
 // the close the connection pool
 func (g *Gedis) Close() error {
-	if g.conn != nil {
-		if err := g.conn.Close(); err != nil {
-			return err
-		}
-		g.conn = nil
-	}
 	return g.pool.Close()
 }
 
diff --git a/pkg/gedis/types.go b/pkg/gedis/types.go
deleted file mode 100644
index 0c213a9d..00000000
--- a/pkg/gedis/types.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package gedis
-
-import (
-	"net"
-
-	schema "github.com/threefoldtech/zos/pkg/schema"
-)
-
-// 'structNameWithoutPrefix' are internal struct used
-// 'gedisStructNameBlabla' are gedis datatype wrappers
-
-type registerNodeBody struct {
-	NodeID  string `json:"node_id,omitempty"`
-	FarmID  string `json:"farm_id,omitempty"`
-	Version string `json:"os_version"`
-}
-
-type gedisRegisterNodeBody struct {
-	Node tfgridNode2 `json:"node"`
-}
-
-//
-// generated with schemac from model
-//
-type tfgridLocation1 struct {
-	City      string  `json:"city"`
-	Country   string  `json:"country"`
-	Continent string  `json:"continent"`
-	Latitude  float64 `json:"latitude"`
-	Longitude float64 `json:"longitude"`
-}
-
-type tfgridNodeResource1 struct {
-	Cru int64 `json:"cru"`
-	Mru int64 `json:"mru"`
-	Hru int64 `json:"hru"`
-	Sru int64 `json:"sru"`
-}
-
-type getNodeBody struct {
-	NodeID string `json:"node_id,omitempty"`
-}
-
-type gedisListNodeBodyPayload struct {
-	FarmID  string `json:"farmer_id"`
-	Country string `json:"country"`
-	City    string `json:"city"`
-	Cru     int    `json:",omitempty"`
-	Mru     int    `json:",omitempty"`
-	Sru     int    `json:",omitempty"`
-	Hru     int    `json:",omitempty"`
-}
-
-type gedisListNodeResponseBody struct {
-	Nodes []tfgridNode2 `json:"nodes"`
-}
-
-type tfgridNodePublicIface1TypeEnum uint8
-
-const (
-	tfgridNodePublicIface1Type tfgridNodePublicIface1TypeEnum = iota
-)
-
-type tfgridNodeIface1 struct {
-	Name    string           `json:"name"`
-	Addrs   []schema.IPRange `json:"addrs"`
-	Gateway []net.IP         `json:"gateway"`
-}
-
-type tfgridNodePublicIface1 struct {
-	Master  string                         `json:"master"`
-	Type    tfgridNodePublicIface1TypeEnum `json:"type"`
-	Ipv4    net.IP                         `json:"ipv4"`
-	Ipv6    net.IP                         `json:"ipv6"`
-	Gw4     net.IP                         `json:"gw4"`
-	Gw6     net.IP                         `json:"gw6"`
-	Version int64                          `json:"version"`
-}
-
-type tfgridFarm1 struct {
-	ThreebotID      string              `json:"threebot_id"`
-	IyoOrganization string              `json:"iyo_organization"`
-	Name            string              `json:"name"`
-	WalletAddresses []string            `json:"wallet_addresses"`
-	Location        tfgridLocation1     `json:"location"`
-	Vta             string              `json:"vta"`
-	ResourcePrice   tfgridNodeResource1 `json:"resource_price"`
-}
-
-type tfgridNode2 struct {
-	NodeID           string                 `json:"node_id,omitempty"`
-	FarmID           string                 `json:"farmer_id,omitempty"`
-	Version          string                 `json:"os_version"`
-	Uptime           int64                  `json:"uptime"`
-	Address          string                 `json:"address"`
-	Location         tfgridLocation1        `json:"location"`
-	TotalResource    tfgridNodeResource1    `json:"total_resource"`
-	UsedResource     tfgridNodeResource1    `json:"used_resource"`
-	ReservedResource tfgridNodeResource1    `json:"reserved_resource"`
-	Ifaces           []tfgridNodeIface1     `json:"ifaces"`
-	PublicConfig     tfgridNodePublicIface1 `json:"public_config"`
-	ExitNode         bool                   `json:"exit_node"`
-	Approved         bool                   `json:"approved"`
-}
-
-type gedisNodeUpdateCapacity struct {
-	NodeID   string              `json:"node_id"`
-	Resource tfgridNodeResource1 `json:"resource"`
-}
-
-//
-// Farms
-//
-
-type registerFarmBody struct {
-	Farm string `json:"farm_id,omitempty"`
-	Name string `json:"name,omitempty"`
-}
-
-type gedisRegisterFarmBody struct {
-	Farm gedisRegisterFarmBodyPayload `json:"farm"`
-}
-
-type gedisRegisterFarmBodyPayload struct {
-	ThreebotID string   `json:"threebot_id,omitempty"`
-	Name       string   `json:"name,omitempty"`
-	Email      string   `json:"email,omitempty"`
-	Wallet     []string `json:"wallet_addresses"`
-}
-
-type getFarmBody struct {
-	FarmID string `json:"farm_id,omitempty"`
-}
-
-type gedisGetFarmBody struct {
-	Farm tfgridFarm1 `json:"farm"`
-}
-
-type gedisUpdateFarmBody struct {
-	FarmID string      `json:"farm_id"`
-	Farm   tfgridFarm1 `json:"farm"`
-}
-
-type gedisListFarmBody struct {
-	Country string `json:"country"`
-	City    string `json:"city"`
-}
-
-type gedisListFarmResponseBody struct {
-	Farms []tfgridFarm1 `json:"farms"`
-}
diff --git a/pkg/schema/types.go b/pkg/schema/types.go
index d1564172..f14689b2 100644
--- a/pkg/schema/types.go
+++ b/pkg/schema/types.go
@@ -68,6 +68,12 @@ func (d *Date) UnmarshalJSON(bytes []byte) error {
 		return fmt.Errorf("unknown date format: %T(%s)", v, string(bytes))
 	}
 
+	if len(in) == 0 {
+		//null date
+		d.Time = time.Time{}
+		return nil
+	}
+
 	m := dateRe.FindStringSubmatch(in)
 	if m == nil {
 		return fmt.Errorf("invalid date string '%s'", in)
@@ -138,14 +144,40 @@ func (d Date) MarshalJSON() ([]byte, error) {
 // IPRange type
 type IPRange struct{ net.IPNet }
 
+// ParseIPRange parse iprange
+func ParseIPRange(txt string) (r IPRange, err error) {
+	if len(txt) == 0 {
+		//empty ip net value
+		return r, nil
+	}
+	//fmt.Println("parsing: ", string(text))
+	ip, net, err := net.ParseCIDR(txt)
+	if err != nil {
+		return r, err
+	}
+
+	net.IP = ip
+	r.IPNet = *net
+	return
+}
+
+// MustParseIPRange prases iprange, panics if invalid
+func MustParseIPRange(txt string) IPRange {
+	r, err := ParseIPRange(txt)
+	if err != nil {
+		panic(err)
+	}
+	return r
+}
+
 // UnmarshalText loads IPRange from string
 func (i *IPRange) UnmarshalText(text []byte) error {
-	_, net, err := net.ParseCIDR(string(text))
+	v, err := ParseIPRange(string(text))
 	if err != nil {
 		return err
 	}
 
-	i.IPNet = *net
+	i.IPNet = v.IPNet
 	return nil
 }
 
diff --git a/pkg/storage.go b/pkg/storage.go
index 9bffa39b..7ca4dc62 100644
--- a/pkg/storage.go
+++ b/pkg/storage.go
@@ -37,8 +37,8 @@ type DeviceType string
 
 // Known device types
 const (
-	SSDDevice = "SSD"
-	HDDDevice = "HDD"
+	SSDDevice DeviceType = "SSD"
+	HDDDevice DeviceType = "HDD"
 )
 
 // Validate make sure profile is correct
@@ -52,7 +52,7 @@ func (p RaidProfile) Validate() error {
 
 var (
 	raidProfiles = map[RaidProfile]struct{}{
-		Single: struct{}{}, Raid1: struct{}{}, Raid10: struct{}{},
+		Single: {}, Raid1: {}, Raid10: {},
 	}
 	// DefaultPolicy value
 	DefaultPolicy = StoragePolicy{
diff --git a/pkg/storage/filesystem/btrfs.go b/pkg/storage/filesystem/btrfs.go
index bebbe814..ee2030fe 100644
--- a/pkg/storage/filesystem/btrfs.go
+++ b/pkg/storage/filesystem/btrfs.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"path"
 	"path/filepath"
 	"strings"
 	"syscall"
@@ -39,18 +38,23 @@ var (
 // btrfs is the filesystem implementation for btrfs
 type btrfs struct {
 	devices DeviceManager
+	utils   BtrfsUtil
+}
+
+func newBtrfs(manager DeviceManager, exec executer) *btrfs {
+	return &btrfs{devices: manager, utils: newUtils(exec)}
 }
 
 // NewBtrfs creates a new filesystem that implements btrfs
 func NewBtrfs(manager DeviceManager) Filesystem {
-	return &btrfs{manager}
+	return newBtrfs(manager, executerFunc(run))
 }
 
 func (b *btrfs) btrfs(ctx context.Context, args ...string) ([]byte, error) {
-	return run(ctx, "btrfs", args...)
+	return b.utils.run(ctx, "btrfs", args...)
 }
 
-func (b *btrfs) Create(ctx context.Context, name string, devices DeviceCache, policy pkg.RaidProfile) (Pool, error) {
+func (b *btrfs) Create(ctx context.Context, name string, policy pkg.RaidProfile, devices ...*Device) (Pool, error) {
 	name = strings.TrimSpace(name)
 	if len(name) == 0 {
 		return nil, fmt.Errorf("invalid name")
@@ -81,7 +85,7 @@ func (b *btrfs) Create(ctx context.Context, name string, devices DeviceCache, po
 	}
 
 	args = append(args, paths...)
-	if _, err := run(ctx, "mkfs.btrfs", args...); err != nil {
+	if _, err := b.utils.run(ctx, "mkfs.btrfs", args...); err != nil {
 		return nil, err
 	}
 
@@ -91,7 +95,7 @@ func (b *btrfs) Create(ctx context.Context, name string, devices DeviceCache, po
 		dev.Filesystem = BtrfsFSType
 	}
 
-	return newBtrfsPool(name, devices), nil
+	return newBtrfsPool(name, devices, &b.utils), nil
 }
 
 func (b *btrfs) List(ctx context.Context, filter Filter) ([]Pool, error) {
@@ -99,7 +103,7 @@ func (b *btrfs) List(ctx context.Context, filter Filter) ([]Pool, error) {
 		filter = All
 	}
 	var pools []Pool
-	available, err := BtrfsList(ctx, "", false)
+	available, err := b.utils.List(ctx, "", false)
 	if err != nil {
 		return nil, err
 	}
@@ -115,7 +119,7 @@ func (b *btrfs) List(ctx context.Context, filter Filter) ([]Pool, error) {
 			return nil, err
 		}
 
-		pool := newBtrfsPool(fs.Label, devices)
+		pool := newBtrfsPool(fs.Label, devices, &b.utils)
 
 		if !filter(pool) {
 			continue
@@ -134,13 +138,15 @@ func (b *btrfs) List(ctx context.Context, filter Filter) ([]Pool, error) {
 
 type btrfsPool struct {
 	name    string
-	devices DeviceCache
+	devices []*Device
+	utils   *BtrfsUtil
 }
 
-func newBtrfsPool(name string, devices DeviceCache) *btrfsPool {
+func newBtrfsPool(name string, devices []*Device, utils *BtrfsUtil) *btrfsPool {
 	return &btrfsPool{
 		name:    name,
 		devices: devices,
+		utils:   utils,
 	}
 }
 
@@ -150,7 +156,7 @@ func newBtrfsPool(name string, devices DeviceCache) *btrfsPool {
 // under any location
 func (p *btrfsPool) Mounted() (string, bool) {
 	ctx := context.Background()
-	list, _ := BtrfsList(ctx, p.Name(), true)
+	list, _ := p.utils.List(ctx, p.Name(), true)
 	if len(list) != 1 {
 		return "", false
 	}
@@ -160,7 +166,7 @@ func (p *btrfsPool) Mounted() (string, bool) {
 
 func (p *btrfsPool) mounted(fs *Btrfs) (string, bool) {
 	for _, device := range fs.Devices {
-		if target, ok := getMountTarget(device.Path); ok {
+		if target, ok := GetMountTarget(device.Path); ok {
 			return target, true
 		}
 	}
@@ -176,15 +182,10 @@ func (p *btrfsPool) Path() string {
 	return filepath.Join("/mnt", p.name)
 }
 
-func (p *btrfsPool) enableQuota(mnt string) error {
-	_, err := run(context.Background(), "btrfs", "quota", "enable", mnt)
-	return err
-}
-
 // Mount mounts the pool in it's default mount location under /mnt/name
 func (p *btrfsPool) Mount() (string, error) {
 	ctx := context.Background()
-	list, _ := BtrfsList(ctx, p.name, false)
+	list, _ := p.utils.List(ctx, p.name, false)
 	if len(list) != 1 {
 		return "", fmt.Errorf("unknown pool '%s'", p.name)
 	}
@@ -204,7 +205,7 @@ func (p *btrfsPool) Mount() (string, error) {
 		return "", err
 	}
 
-	return mnt, p.enableQuota(mnt)
+	return mnt, p.utils.QGroupEnable(ctx, mnt)
 }
 
 func (p *btrfsPool) UnMount() error {
@@ -216,34 +217,35 @@ func (p *btrfsPool) UnMount() error {
 	return syscall.Unmount(mnt, syscall.MNT_DETACH)
 }
 
-func (p *btrfsPool) AddDevice(device *Device) error {
-	mnt, ok := p.Mounted()
-	if !ok {
-		return ErrDeviceNotMounted
-	}
+func (p *btrfsPool) addDevice(device *Device, root string) error {
 	ctx := context.Background()
 
-	if _, err := run(ctx, "btrfs", "device", "add", device.Path, mnt); err != nil {
+	if err := p.utils.DeviceAdd(ctx, device.Path, root); err != nil {
 		return err
 	}
 
-	p.devices = append(p.devices, device)
-
 	// update cached device
 	device.Label = p.name
 	device.Filesystem = BtrfsFSType
 
+	p.devices = append(p.devices, device)
+
 	return nil
 }
 
-func (p *btrfsPool) RemoveDevice(device *Device) error {
+func (p *btrfsPool) AddDevice(device *Device) error {
 	mnt, ok := p.Mounted()
 	if !ok {
 		return ErrDeviceNotMounted
 	}
+
+	return p.addDevice(device, mnt)
+}
+
+func (p *btrfsPool) removeDevice(device *Device, root string) error {
 	ctx := context.Background()
 
-	if _, err := run(ctx, "btrfs", "device", "remove", device.Path, mnt); err != nil {
+	if err := p.utils.DeviceRemove(ctx, device.Path, root); err != nil {
 		return err
 	}
 
@@ -261,6 +263,15 @@ func (p *btrfsPool) RemoveDevice(device *Device) error {
 	return nil
 }
 
+func (p *btrfsPool) RemoveDevice(device *Device) error {
+	mnt, ok := p.Mounted()
+	if !ok {
+		return ErrDeviceNotMounted
+	}
+
+	return p.removeDevice(device, mnt)
+}
+
 func (p *btrfsPool) Volumes() ([]Volume, error) {
 	mnt, ok := p.Mounted()
 	if !ok {
@@ -269,30 +280,41 @@ func (p *btrfsPool) Volumes() ([]Volume, error) {
 
 	var volumes []Volume
 
-	subs, err := BtrfsSubvolumeList(context.Background(), mnt)
+	subs, err := p.utils.SubvolumeList(context.Background(), mnt)
 	if err != nil {
 		return nil, err
 	}
 
 	for _, sub := range subs {
-		volumes = append(volumes, btrfsVolume(filepath.Join(mnt, sub.Path)))
+		volumes = append(volumes, newBtrfsVolume(
+			filepath.Join(mnt, sub.Path),
+			p.utils,
+		))
 	}
 
 	return volumes, nil
 }
 
+func (p *btrfsPool) addVolume(root string) (*btrfsVolume, error) {
+	if err := p.utils.SubvolumeAdd(context.Background(), root); err != nil {
+		return nil, err
+	}
+
+	return newBtrfsVolume(root, p.utils), nil
+}
+
 func (p *btrfsPool) AddVolume(name string) (Volume, error) {
 	mnt, ok := p.Mounted()
 	if !ok {
 		return nil, ErrDeviceNotMounted
 	}
 
-	mnt = filepath.Join(mnt, name)
-	if _, err := run(context.Background(), "btrfs", "subvolume", "create", mnt); err != nil {
-		return nil, err
-	}
+	root := filepath.Join(mnt, name)
+	return p.addVolume(root)
+}
 
-	return btrfsVolume(mnt), nil
+func (p *btrfsPool) removeVolume(root string) error {
+	return p.utils.SubvolumeRemove(context.Background(), root)
 }
 
 func (p *btrfsPool) RemoveVolume(name string) error {
@@ -301,10 +323,8 @@ func (p *btrfsPool) RemoveVolume(name string) error {
 		return ErrDeviceNotMounted
 	}
 
-	mnt = path.Join(mnt, name)
-	_, err := run(context.Background(), "btrfs", "subvolume", "delete", mnt)
-
-	return err
+	root := filepath.Join(mnt, name)
+	return p.removeVolume(root)
 }
 
 // Size return the pool size
@@ -314,12 +334,12 @@ func (p *btrfsPool) Usage() (usage Usage, err error) {
 		return usage, ErrDeviceNotMounted
 	}
 
-	du, err := BtrfsGetDiskUsage(context.Background(), mnt)
+	du, err := p.utils.GetDiskUsage(context.Background(), mnt)
 	if err != nil {
 		return Usage{}, err
 	}
 
-	fsi, err := BtrfsList(context.Background(), p.name, true)
+	fsi, err := p.utils.List(context.Background(), p.name, true)
 	if err != nil {
 		return Usage{}, err
 	}
@@ -373,52 +393,60 @@ func (p *btrfsPool) Reserved() (uint64, error) {
 	return total, nil
 }
 
-type btrfsVolume string
+type btrfsVolume struct {
+	path  string
+	utils *BtrfsUtil
+}
+
+func newBtrfsVolume(path string, utils *BtrfsUtil) *btrfsVolume {
+	return &btrfsVolume{
+		path:  path,
+		utils: utils,
+	}
+}
 
-func (v btrfsVolume) Path() string {
-	return string(v)
+func (v *btrfsVolume) Path() string {
+	return v.path
 }
 
-func (v btrfsVolume) Volumes() ([]Volume, error) {
+func (v *btrfsVolume) Volumes() ([]Volume, error) {
 	var volumes []Volume
 
-	subs, err := BtrfsSubvolumeList(context.Background(), string(v))
+	subs, err := v.utils.SubvolumeList(context.Background(), v.Path())
 	if err != nil {
 		return nil, err
 	}
 
 	for _, sub := range subs {
-		volumes = append(volumes, btrfsVolume(filepath.Join(string(v), sub.Path)))
+		volumes = append(volumes, newBtrfsVolume(filepath.Join(v.Path(), sub.Path), v.utils))
 	}
 
 	return volumes, nil
 }
 
-func (v btrfsVolume) AddVolume(name string) (Volume, error) {
-	mnt := path.Join(string(v), name)
-	if _, err := run(context.Background(), "btrfs", "subvolume", "create", mnt); err != nil {
+func (v *btrfsVolume) AddVolume(name string) (Volume, error) {
+	mnt := filepath.Join(v.Path(), name)
+	if err := v.utils.SubvolumeAdd(context.Background(), mnt); err != nil {
 		return nil, err
 	}
 
-	return btrfsVolume(mnt), nil
+	return newBtrfsVolume(mnt, v.utils), nil
 }
 
-func (v btrfsVolume) RemoveVolume(name string) error {
-	mnt := path.Join(string(v), name)
-	_, err := run(context.Background(), "btrfs", "subvolume", "remove", mnt)
-
-	return err
+func (v *btrfsVolume) RemoveVolume(name string) error {
+	mnt := filepath.Join(v.Path(), name)
+	return v.utils.SubvolumeRemove(context.Background(), mnt)
 }
 
 // Usage return the volume usage
-func (v btrfsVolume) Usage() (usage Usage, err error) {
+func (v *btrfsVolume) Usage() (usage Usage, err error) {
 	ctx := context.Background()
-	info, err := BtrfsSubvolumeInfo(ctx, string(v))
+	info, err := v.utils.SubvolumeInfo(ctx, v.Path())
 	if err != nil {
 		return usage, err
 	}
 
-	groups, err := BtrfsQGroupList(ctx, string(v))
+	groups, err := v.utils.QGroupList(ctx, v.Path())
 	if err != nil {
 		return usage, err
 	}
@@ -437,26 +465,18 @@ func (v btrfsVolume) Usage() (usage Usage, err error) {
 }
 
 // Limit size of volume, setting size to 0 means unlimited
-func (v btrfsVolume) Limit(size uint64) error {
+func (v *btrfsVolume) Limit(size uint64) error {
 	ctx := context.Background()
 
-	limit := "none"
-	if size > 0 {
-		limit = fmt.Sprint(size)
-	}
-	log.Debug().Str("limit", limit).Msgf("limit volume %s", v.Name())
-	_, err := run(ctx, "btrfs", "qgroup", "limit", limit, string(v))
-
-	return err
+	return v.utils.QGroupLimit(ctx, size, v.Path())
 }
 
 // Name of the filesystem
-func (v btrfsVolume) Name() string {
-	_, name := filepath.Split(string(v))
-	return name
+func (v *btrfsVolume) Name() string {
+	return filepath.Base(v.Path())
 }
 
 // FsType of the filesystem
-func (v btrfsVolume) FsType() string {
+func (v *btrfsVolume) FsType() string {
 	return "btrfs"
 }
diff --git a/pkg/storage/filesystem/btrfs_utils.go b/pkg/storage/filesystem/btrfs_utils.go
index 9ba01cb6..52187db4 100644
--- a/pkg/storage/filesystem/btrfs_utils.go
+++ b/pkg/storage/filesystem/btrfs_utils.go
@@ -66,10 +66,19 @@ type BtrfsDiskUsage struct {
 	GlobalReserve DiskUsage `json:"globalreserve"`
 }
 
-// BtrfsList lists all availabel btrfs pools
+// BtrfsUtil utils for btrfs
+type BtrfsUtil struct {
+	executer
+}
+
+func newUtils(exec executer) BtrfsUtil {
+	return BtrfsUtil{exec}
+}
+
+// List lists all availabel btrfs pools
 // if label is provided, only get fs of that label, if mounted = True, only return
 // mounted filesystems, otherwise any.
-func BtrfsList(ctx context.Context, label string, mounted bool) ([]Btrfs, error) {
+func (u *BtrfsUtil) List(ctx context.Context, label string, mounted bool) ([]Btrfs, error) {
 	args := []string{
 		"filesystem", "show", "--raw",
 	}
@@ -82,7 +91,7 @@ func BtrfsList(ctx context.Context, label string, mounted bool) ([]Btrfs, error)
 		args = append(args, label)
 	}
 
-	output, err := run(ctx, "btrfs", args...)
+	output, err := u.run(ctx, "btrfs", args...)
 	if err != nil {
 		return nil, err
 	}
@@ -90,9 +99,9 @@ func BtrfsList(ctx context.Context, label string, mounted bool) ([]Btrfs, error)
 	return parseList(string(output))
 }
 
-// BtrfsSubvolumeList list direct subvolumes of this location
-func BtrfsSubvolumeList(ctx context.Context, root string) ([]BtrfsVolume, error) {
-	output, err := run(ctx, "btrfs", "subvolume", "list", "-o", root)
+// SubvolumeList list direct subvolumes of this location
+func (u *BtrfsUtil) SubvolumeList(ctx context.Context, root string) ([]BtrfsVolume, error) {
+	output, err := u.run(ctx, "btrfs", "subvolume", "list", "-o", root)
 	if err != nil {
 		return nil, err
 	}
@@ -100,9 +109,9 @@ func BtrfsSubvolumeList(ctx context.Context, root string) ([]BtrfsVolume, error)
 	return parseSubvolList(string(output))
 }
 
-// BtrfsSubvolumeInfo get info of a subvolume giving its path
-func BtrfsSubvolumeInfo(ctx context.Context, path string) (volume BtrfsVolume, err error) {
-	output, err := run(context.Background(), "btrfs", "subvolume", "show", path)
+// SubvolumeInfo get info of a subvolume giving its path
+func (u *BtrfsUtil) SubvolumeInfo(ctx context.Context, path string) (volume BtrfsVolume, err error) {
+	output, err := u.run(context.Background(), "btrfs", "subvolume", "show", path)
 	if err != nil {
 		return
 	}
@@ -113,6 +122,68 @@ func BtrfsSubvolumeInfo(ctx context.Context, path string) (volume BtrfsVolume, e
 	return
 }
 
+// SubvolumeAdd adds a new subvolume at path
+func (u *BtrfsUtil) SubvolumeAdd(ctx context.Context, root string) error {
+	_, err := u.run(ctx, "btrfs", "subvolume", "create", root)
+	return err
+}
+
+// SubvolumeRemove removes a subvolume
+func (u *BtrfsUtil) SubvolumeRemove(ctx context.Context, root string) error {
+	_, err := u.run(ctx, "btrfs", "subvolume", "delete", root)
+	return err
+}
+
+// DeviceAdd adds a device to a btrfs pool
+func (u *BtrfsUtil) DeviceAdd(ctx context.Context, dev string, root string) error {
+	_, err := u.run(ctx, "btrfs", "device", "add", dev, root)
+	return err
+}
+
+// DeviceRemove removes a device from a btrfs pool
+func (u *BtrfsUtil) DeviceRemove(ctx context.Context, dev string, root string) error {
+	_, err := u.run(ctx, "btrfs", "device", "remove", dev, root)
+	return err
+}
+
+// QGroupEnable enable quota
+func (u *BtrfsUtil) QGroupEnable(ctx context.Context, root string) error {
+	_, err := u.run(ctx, "btrfs", "quota", "enable", root)
+	return err
+}
+
+// QGroupList list available qgroups
+func (u *BtrfsUtil) QGroupList(ctx context.Context, path string) (map[string]BtrfsQGroup, error) {
+	output, err := u.run(ctx, "btrfs", "qgroup", "show", "-re", "--raw", path)
+	if err != nil {
+		return nil, err
+	}
+
+	return parseQGroups(string(output)), nil
+}
+
+// QGroupLimit limit size on subvol
+func (u *BtrfsUtil) QGroupLimit(ctx context.Context, size uint64, path string) error {
+	limit := "none"
+	if size > 0 {
+		limit = fmt.Sprint(size)
+	}
+
+	_, err := u.run(ctx, "btrfs", "qgroup", "limit", limit, path)
+
+	return err
+}
+
+// GetDiskUsage get btrfs usage
+func (u *BtrfsUtil) GetDiskUsage(ctx context.Context, path string) (usage BtrfsDiskUsage, err error) {
+	output, err := u.run(ctx, "btrfs", "filesystem", "df", "--raw", path)
+	if err != nil {
+		return usage, err
+	}
+
+	return parseFilesystemDF(string(output))
+}
+
 func parseSubvolInfo(output string) (volume BtrfsVolume, err error) {
 	values := make(map[string]string)
 	for _, line := range strings.Split(output, "\n") {
@@ -246,16 +317,6 @@ func parseDevices(lines []string) ([]BtrfsDevice, error) {
 	return devs, nil
 }
 
-// BtrfsQGroupList list available qgroups
-func BtrfsQGroupList(ctx context.Context, path string) (map[string]BtrfsQGroup, error) {
-	output, err := run(ctx, "btrfs", "qgroup", "show", "-re", "--raw", path)
-	if err != nil {
-		return nil, err
-	}
-
-	return parseQGroups(string(output)), nil
-}
-
 func parseQGroups(output string) map[string]BtrfsQGroup {
 	qgroups := make(map[string]BtrfsQGroup)
 	for _, line := range reBtrfsQgroup.FindAllStringSubmatch(output, -1) {
@@ -279,16 +340,6 @@ func parseQGroups(output string) map[string]BtrfsQGroup {
 	return qgroups
 }
 
-// BtrfsGetDiskUsage get btrfs usage
-func BtrfsGetDiskUsage(ctx context.Context, path string) (usage BtrfsDiskUsage, err error) {
-	output, err := run(ctx, "btrfs", "filesystem", "df", "--raw", path)
-	if err != nil {
-		return usage, err
-	}
-
-	return parseFilesystemDF(string(output))
-}
-
 func parseFilesystemDF(output string) (usage BtrfsDiskUsage, err error) {
 	lines := reBtrfsFilesystemDf.FindAllStringSubmatch(output, -1)
 	for _, line := range lines {
diff --git a/pkg/storage/filesystem/device.go b/pkg/storage/filesystem/device.go
index a47ecc9a..8014c66f 100644
--- a/pkg/storage/filesystem/device.go
+++ b/pkg/storage/filesystem/device.go
@@ -17,11 +17,11 @@ type DeviceManager interface {
 	// Devices finds all devices on a system
 	Devices(ctx context.Context) (DeviceCache, error)
 	// ByLabel finds all devices with the specified label
-	ByLabel(ctx context.Context, label string) (DeviceCache, error)
+	ByLabel(ctx context.Context, label string) ([]*Device, error)
 }
 
 // DeviceCache represents a list of cached in memory devices
-type DeviceCache []*Device
+type DeviceCache []Device
 
 // FSType type of filesystem on device
 type FSType string
@@ -40,11 +40,16 @@ type Device struct {
 	Children   []Device       `json:"children"`
 	DiskType   pkg.DeviceType `json:"-"`
 	ReadTime   uint64         `json:"-"`
+	//HasPartions is different from children, because once the
+	//devices are flattend in the device, cache, the children list is
+	//zeroed (since all devices are flat), then has partions is set to
+	//make sure the device is not altered.
+	HasPartions bool `json:"-"`
 }
 
 // Used assumes that the device is used if it has custom label or fstype or children
 func (d *Device) Used() bool {
-	return len(d.Label) != 0 || len(d.Filesystem) != 0 || len(d.Children) > 0
+	return len(d.Label) != 0 || len(d.Filesystem) != 0 || len(d.Children) > 0 || d.HasPartions
 }
 
 // lsblkDeviceManager uses the lsblk utility to scann the disk for devices, and
@@ -53,13 +58,19 @@ func (d *Device) Used() bool {
 // Found devices are cached, and the cache is only repopulated after the `Scan`
 // method is called.
 type lsblkDeviceManager struct {
+	executer
 	devices DeviceCache
 }
 
 // DefaultDeviceManager returns a default device manager implementation
 func DefaultDeviceManager(ctx context.Context) (DeviceManager, error) {
+	return defaultDeviceManager(ctx, executerFunc(run))
+}
+
+func defaultDeviceManager(ctx context.Context, exec executer) (DeviceManager, error) {
 	m := &lsblkDeviceManager{
-		devices: DeviceCache{},
+		devices:  DeviceCache{},
+		executer: exec,
 	}
 
 	err := m.scan(ctx)
@@ -71,10 +82,11 @@ func (l *lsblkDeviceManager) Devices(ctx context.Context) (DeviceCache, error) {
 	return l.devices, nil
 }
 
-func (l *lsblkDeviceManager) ByLabel(ctx context.Context, label string) (DeviceCache, error) {
-	var filtered DeviceCache
+func (l *lsblkDeviceManager) ByLabel(ctx context.Context, label string) ([]*Device, error) {
+	var filtered []*Device
 
-	for _, device := range l.devices {
+	for idx := range l.devices {
+		device := &l.devices[idx]
 		if device.Label == label {
 			filtered = append(filtered, device)
 		}
@@ -86,7 +98,7 @@ func (l *lsblkDeviceManager) ByLabel(ctx context.Context, label string) (DeviceC
 func (l *lsblkDeviceManager) Device(ctx context.Context, path string) (device *Device, err error) {
 	for idx := range l.devices {
 		if l.devices[idx].Path == path {
-			return l.devices[idx], nil
+			return &l.devices[idx], nil
 		}
 	}
 
@@ -96,7 +108,7 @@ func (l *lsblkDeviceManager) Device(ctx context.Context, path string) (device *D
 
 // scan the system for disks using the `lsblk` command
 func (l *lsblkDeviceManager) scan(ctx context.Context) error {
-	bytes, err := run(ctx, "lsblk", "--json", "--output-all", "--bytes", "--exclude", "1,2,11", "--path")
+	bytes, err := l.run(ctx, "lsblk", "--json", "--output-all", "--bytes", "--exclude", "1,2,11", "--path")
 	if err != nil {
 		return err
 	}
@@ -109,70 +121,82 @@ func (l *lsblkDeviceManager) scan(ctx context.Context) error {
 		return err
 	}
 
-	typedDevs, err := setDeviceTypes(devices.BlockDevices)
-	if err != nil {
+	devs := DeviceCache(devices.BlockDevices)
+
+	if err := l.setDeviceTypes(devs); err != nil {
 		return err
 	}
 
-	var devs DeviceCache
+	l.devices = l.flattenDevices(devs)
+
+	return nil
+}
 
-	for idx := range typedDevs {
-		devs = append(devs, &typedDevs[idx])
+// seektime uses the seektime binary to try and determine the type of a disk
+// This function returns the type of the device, as reported by seektime,
+// and the elapsed time in microseconds (also reported by seektime)
+func (l *lsblkDeviceManager) seektime(ctx context.Context, path string) (string, uint64, error) {
+	bytes, err := l.run(ctx, "seektime", "-j", path)
+	if err != nil {
+		return "", 0, err
 	}
 
-	l.devices = flattenDevices(devs)
+	var seekTime struct {
+		Typ  string `json:"type"`
+		Time uint64 `json:"elapsed"`
+	}
 
-	return nil
+	err = json.Unmarshal(bytes, &seekTime)
+	return seekTime.Typ, seekTime.Time, err
 }
 
-func flattenDevices(devices DeviceCache) DeviceCache {
-	list := DeviceCache{}
-	for idx := range devices {
-		list = append(list, devices[idx])
-		if devices[idx].Children != nil {
-			childCache := DeviceCache{}
-			for jdx := range devices[idx].Children {
-				childCache = append(childCache, &devices[idx].Children[jdx])
-			}
-			list = append(list, flattenDevices(childCache)...)
+func (l *lsblkDeviceManager) flattenDevices(devices DeviceCache) DeviceCache {
+	var list DeviceCache
+	for _, device := range devices {
+		children := device.Children
+		device.Children = nil
+		if len(children) > 0 {
+			device.HasPartions = true
 		}
+		list = append(list, device)
+		list = append(list, l.flattenDevices(children)...)
 	}
+
 	return list
 }
 
-func setDeviceTypes(devices []Device) ([]Device, error) {
-	list := []Device{}
-	for _, d := range devices {
+func (l *lsblkDeviceManager) setDeviceTypes(devices []Device) error {
+	for idx := range devices {
+		d := &devices[idx]
 		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
 		defer cancel()
 
-		typ, rt, err := seektime(ctx, d.Path)
+		typ, rt, err := l.seektime(ctx, d.Path)
 		if err != nil {
 			// don't include errored devices in the result
 			log.Error().Msgf("Failed to get disk read time: %v", err)
-			return nil, err
+			return err
 		}
 
-		setDeviceType(&d, deviceTypeFromString(typ), rt)
-
-		list = append(list, d)
+		l.setDeviceType(d, l.deviceTypeFromString(typ), rt)
 	}
 
-	return list, nil
+	return nil
 }
 
 // setDeviceType recursively sets a device type and read time on a device and
 // all of its children
-func setDeviceType(device *Device, typ pkg.DeviceType, readTime uint64) {
+func (l *lsblkDeviceManager) setDeviceType(device *Device, typ pkg.DeviceType, readTime uint64) {
 	device.DiskType = typ
 	device.ReadTime = readTime
 
-	for _, dev := range device.Children {
-		setDeviceType(&dev, typ, readTime)
+	for idx := range device.Children {
+		dev := &device.Children[idx]
+		l.setDeviceType(dev, typ, readTime)
 	}
 }
 
-func deviceTypeFromString(typ string) pkg.DeviceType {
+func (l *lsblkDeviceManager) deviceTypeFromString(typ string) pkg.DeviceType {
 	switch typ {
 	case string(pkg.SSDDevice):
 		return pkg.SSDDevice
diff --git a/pkg/storage/filesystem/filesystem.go b/pkg/storage/filesystem/filesystem.go
index 5258d87d..bd2ae42e 100644
--- a/pkg/storage/filesystem/filesystem.go
+++ b/pkg/storage/filesystem/filesystem.go
@@ -69,7 +69,7 @@ type Filesystem interface {
 	// name: name of the filesystem
 	// devices: list of devices to use in the filesystem
 	// profile: Raid profile of the filesystem
-	Create(ctx context.Context, name string, devices DeviceCache, profile pkg.RaidProfile) (Pool, error)
+	Create(ctx context.Context, name string, profile pkg.RaidProfile, devices ...*Device) (Pool, error)
 	// List all existing filesystems on the node
 	List(ctx context.Context, filter Filter) ([]Pool, error)
 }
diff --git a/pkg/storage/filesystem/utils.go b/pkg/storage/filesystem/utils.go
index 2e7b3400..15a270f0 100644
--- a/pkg/storage/filesystem/utils.go
+++ b/pkg/storage/filesystem/utils.go
@@ -3,18 +3,35 @@ package filesystem
 import (
 	"bufio"
 	"context"
-	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 	"os/exec"
 	"strings"
 	"syscall"
 )
 
-// getMountTarget returns the mount target of a device or false if the
+func getMountTarget(f io.Reader, device string) (string, bool) {
+	scanner := bufio.NewScanner(f)
+
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if len(line) == 0 {
+			continue
+		}
+		fields := strings.Fields(line)
+		if fields[0] == device {
+			return fields[1], true
+		}
+	}
+
+	return "", false
+}
+
+// GetMountTarget returns the mount target of a device or false if the
 // device is not mounted.
 // panic, it panics if it can't read /proc/mounts
-func getMountTarget(device string) (string, bool) {
+func GetMountTarget(device string) (string, bool) {
 	file, err := os.Open("/proc/mounts")
 	if err != nil {
 		panic(fmt.Errorf("failed to read /proc/mounts: %s", err))
@@ -22,20 +39,11 @@ func getMountTarget(device string) (string, bool) {
 
 	defer file.Close()
 
-	scanner := bufio.NewScanner(file)
-
-	for scanner.Scan() {
-		fields := strings.Fields(scanner.Text())
-		if fields[0] == device {
-			return fields[1], true
-		}
-	}
-
-	return "", false
+	return getMountTarget(file, device)
 }
 
-// IsPathMounted checks if a path is mounted on a disk
-func IsPathMounted(path string) bool {
+// IsMountPoint checks if a path is a mount point
+func IsMountPoint(path string) bool {
 	file, err := os.Open("/proc/mounts")
 	if err != nil {
 		panic(fmt.Errorf("failed to read /proc/mounts: %s", err))
@@ -61,22 +69,14 @@ func BindMount(src Volume, target string) error {
 	return syscall.Mount(src.Path(), target, src.FsType(), syscall.MS_BIND, "")
 }
 
-// seektime uses the seektime binary to try and determine the type of a disk
-// This function returns the type of the device, as reported by seektime,
-// and the elapsed time in microseconds (also reported by seektime)
-func seektime(ctx context.Context, path string) (string, uint64, error) {
-	bytes, err := run(ctx, "seektime", "-j", path)
-	if err != nil {
-		return "", 0, err
-	}
+type executer interface {
+	run(ctx context.Context, name string, args ...string) ([]byte, error)
+}
 
-	var seekTime struct {
-		Typ  string `json:"type"`
-		Time uint64 `json:"elapsed"`
-	}
+type executerFunc func(ctx context.Context, name string, args ...string) ([]byte, error)
 
-	err = json.Unmarshal(bytes, &seekTime)
-	return seekTime.Typ, seekTime.Time, err
+func (e executerFunc) run(ctx context.Context, name string, args ...string) ([]byte, error) {
+	return e(ctx, name, args...)
 }
 
 func run(ctx context.Context, name string, args ...string) ([]byte, error) {
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
index c141c7a3..636537d6 100644
--- a/pkg/storage/storage.go
+++ b/pkg/storage/storage.go
@@ -53,6 +53,11 @@ func New() (pkg.StorageModule, error) {
 		Disks:    1,
 		MaxPools: 0,
 	})
+
+	if err == nil {
+		log.Info().Msgf("Finished initializing storage module")
+	}
+
 	return s, err
 }
 
@@ -86,7 +91,6 @@ What Initialize will do is the following:
 **/
 func (s *storageModule) initialize(policy pkg.StoragePolicy) error {
 	log.Info().Msgf("Initializing storage module")
-	defer log.Info().Msgf("Finished initializing storage module")
 
 	// Make sure we finish in 1 minute
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
@@ -172,14 +176,14 @@ func (s *storageModule) initialize(policy pkg.StoragePolicy) error {
 
 		for i := 0; i < possiblePools; i++ {
 			log.Debug().Msgf("Creating new volume %d", i)
-			poolDevices := filesystem.DeviceCache{}
+			poolDevices := []*filesystem.Device{}
 
 			for j := 0; j < int(policy.Disks); j++ {
 				log.Debug().Msgf("Grabbing device %d: %s for new volume", i*int(policy.Disks)+j, fdisks[idx][i*int(policy.Disks)+j].Path)
-				poolDevices = append(poolDevices, fdisks[idx][i*int(policy.Disks)+j])
+				poolDevices = append(poolDevices, &fdisks[idx][i*int(policy.Disks)+j])
 			}
 
-			pool, err := fs.Create(ctx, uuid.New().String(), poolDevices, policy.Raid)
+			pool, err := fs.Create(ctx, uuid.New().String(), policy.Raid, poolDevices...)
 			if err != nil {
 				return err
 			}
@@ -297,7 +301,7 @@ func (s *storageModule) ensureCache() error {
 		cacheFs = fs
 	}
 
-	if !filesystem.IsPathMounted(cacheTarget) {
+	if !filesystem.IsMountPoint(cacheTarget) {
 		log.Debug().Msgf("Mounting cache partition in %s", cacheTarget)
 		return filesystem.BindMount(cacheFs, cacheTarget)
 	}
diff --git a/tools/tfuser/cmds_volumes.go b/tools/tfuser/cmds_volumes.go
index afd1b203..0c0c5c8e 100644
--- a/tools/tfuser/cmds_volumes.go
+++ b/tools/tfuser/cmds_volumes.go
@@ -14,7 +14,7 @@ func generateVolume(c *cli.Context) error {
 	s := c.Uint64("size")
 	t := strings.ToUpper(c.String("type"))
 
-	if t != pkg.HDDDevice && t != pkg.SSDDevice {
+	if pkg.DeviceType(t) != pkg.HDDDevice && pkg.DeviceType(t) != pkg.SSDDevice {
 		return fmt.Errorf("volume type can only HHD or SSD")
 	}
 
diff --git a/tools/tfuser/cmds_zdb.go b/tools/tfuser/cmds_zdb.go
index 8dd05203..dd31be60 100644
--- a/tools/tfuser/cmds_zdb.go
+++ b/tools/tfuser/cmds_zdb.go
@@ -18,7 +18,7 @@ func generateZDB(c *cli.Context) error {
 		public   = c.Bool("Public")
 	)
 
-	if disktype != pkg.HDDDevice && disktype != pkg.SSDDevice {
+	if pkg.DeviceType(disktype) != pkg.HDDDevice && pkg.DeviceType(disktype) != pkg.SSDDevice {
 		return fmt.Errorf("volume type can only 'HHD' or 'SSD'")
 	}
 
diff --git a/tools/updatectl/hub.go b/tools/updatectl/hub.go
index b1e72e4c..38ccf87a 100644
--- a/tools/updatectl/hub.go
+++ b/tools/updatectl/hub.go
@@ -37,8 +37,8 @@ func NewHub(token string) (*Hub, error) {
 	}
 
 	jar.SetCookies(base, []*http.Cookie{
-		&http.Cookie{Name: "caddyoauth", Value: token},
-		&http.Cookie{Name: "active-user", Value: user},
+		{Name: "caddyoauth", Value: token},
+		{Name: "active-user", Value: user},
 	})
 
 	return &Hub{client: http.Client{Jar: jar}, base: base}, nil


Test patch:
diff --git a/pkg/capacity/dmi/dmi_test.go b/pkg/capacity/dmi/dmi_test.go
index 7288c309..c33101f4 100644
--- a/pkg/capacity/dmi/dmi_test.go
+++ b/pkg/capacity/dmi/dmi_test.go
@@ -382,13 +382,13 @@ var onBoardDevicesTests = []SubSection{
 	{
 		Title: "On Board Device 1 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Intel(R) HD Graphics Device",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Video",
 			},
 		},
@@ -396,91 +396,91 @@ var onBoardDevicesTests = []SubSection{
 	{
 		Title: "On Board Device 2 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Intel(R) I219-V Gigabit Network Device",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Ethernet",
 			},
 		},
 	}, {
 		Title: "On Board Device 3 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Realtek High Definition Audio Device",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Sound",
 			},
 		},
 	}, {
 		Title: "On Board Device 4 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "CIR Device",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Other",
 			},
 		},
 	}, {
 		Title: "On Board Device 5 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "SD",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Other",
 			},
 		},
 	}, {
 		Title: "On Board Device 6 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Intel Dual Band Wireless-AC 8265",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Other",
 			},
 		},
 	}, {
 		Title: "On Board Device 7 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Bluetooth",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Enabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Other",
 			},
 		},
 	}, {
 		Title: "On Board Device 8 Information",
 		Properties: map[string]PropertyData{
-			"Description": PropertyData{
+			"Description": {
 				Val: "Thunderbolt",
 			},
-			"Status": PropertyData{
+			"Status": {
 				Val: "Disabled",
 			},
-			"Type": PropertyData{
+			"Type": {
 				Val: "Other",
 			},
 		},
@@ -490,14 +490,14 @@ var onBoardDevicesTests = []SubSection{
 var oemSpecificTypeWithIndentedListItems = SubSection{
 	Title: "OEM-specific Type",
 	Properties: map[string]PropertyData{
-		"Header and Data": PropertyData{
+		"Header and Data": {
 			Val: "",
 			Items: []string{
 				"D8 17 76 00 06 00 01 02 05 20 31 2E 33 2E 31 39",
 				"00 00 00 00 00 00 00",
 			},
 		},
-		"Strings": PropertyData{
+		"Strings": {
 			Val: "",
 			Items: []string{
 				"Chassis Firmware",
@@ -511,53 +511,53 @@ var multpleSectionsWithSameTypeStr = []SubSection{
 	{
 		Title: "Memory Device",
 		Properties: map[string]PropertyData{
-			"Array Handle":             PropertyData{Val: "0x0035"},
-			"Error Information Handle": PropertyData{Val: "Not Provided"},
-			"Total Width":              PropertyData{Val: "64 bits"},
-			"Data Width":               PropertyData{Val: "64 bits"},
-			"Size":                     PropertyData{Val: "8192 MB"},
-			"Form Factor":              PropertyData{Val: "SODIMM"},
-			"Set":                      PropertyData{Val: "None"},
-			"Locator":                  PropertyData{Val: "ChannelA-DIMM0"},
-			"Bank Locator":             PropertyData{Val: "BANK 0"},
-			"Type":                     PropertyData{Val: "DDR4"},
-			"Type Detail":              PropertyData{Val: "Synchronous Unbuffered (Unregistered)"},
-			"Speed":                    PropertyData{Val: "2400 MT/s"},
-			"Manufacturer":             PropertyData{Val: "859B"},
-			"Serial Number":            PropertyData{Val: "E0AD159C"},
-			"Asset Tag":                PropertyData{Val: "9876543210"},
-			"Part Number":              PropertyData{Val: "CT8G4SFD824A.M16FB"},
-			"Rank":                     PropertyData{Val: "2"},
-			"Configured Clock Speed":   PropertyData{Val: "2133 MT/s"},
-			"Minimum Voltage":          PropertyData{Val: "1.2 V"},
-			"Maximum Voltage":          PropertyData{Val: "1.2 V"},
-			"Configured Voltage":       PropertyData{Val: "1.2 V"},
+			"Array Handle":             {Val: "0x0035"},
+			"Error Information Handle": {Val: "Not Provided"},
+			"Total Width":              {Val: "64 bits"},
+			"Data Width":               {Val: "64 bits"},
+			"Size":                     {Val: "8192 MB"},
+			"Form Factor":              {Val: "SODIMM"},
+			"Set":                      {Val: "None"},
+			"Locator":                  {Val: "ChannelA-DIMM0"},
+			"Bank Locator":             {Val: "BANK 0"},
+			"Type":                     {Val: "DDR4"},
+			"Type Detail":              {Val: "Synchronous Unbuffered (Unregistered)"},
+			"Speed":                    {Val: "2400 MT/s"},
+			"Manufacturer":             {Val: "859B"},
+			"Serial Number":            {Val: "E0AD159C"},
+			"Asset Tag":                {Val: "9876543210"},
+			"Part Number":              {Val: "CT8G4SFD824A.M16FB"},
+			"Rank":                     {Val: "2"},
+			"Configured Clock Speed":   {Val: "2133 MT/s"},
+			"Minimum Voltage":          {Val: "1.2 V"},
+			"Maximum Voltage":          {Val: "1.2 V"},
+			"Configured Voltage":       {Val: "1.2 V"},
 		},
 	},
 	{
 		Title: "Memory Device",
 		Properties: map[string]PropertyData{
-			"Array Handle":             PropertyData{Val: "0x0035"},
-			"Error Information Handle": PropertyData{Val: "Not Provided"},
-			"Total Width":              PropertyData{Val: "64 bits"},
-			"Data Width":               PropertyData{Val: "64 bits"},
-			"Size":                     PropertyData{Val: "8192 MB"},
-			"Form Factor":              PropertyData{Val: "SODIMM"},
-			"Set":                      PropertyData{Val: "None"},
-			"Locator":                  PropertyData{Val: "ChannelB-DIMM0"},
-			"Bank Locator":             PropertyData{Val: "BANK 2"},
-			"Type":                     PropertyData{Val: "DDR4"},
-			"Type Detail":              PropertyData{Val: "Synchronous Unbuffered (Unregistered)"},
-			"Speed":                    PropertyData{Val: "2400 MT/s"},
-			"Manufacturer":             PropertyData{Val: "859B"},
-			"Serial Number":            PropertyData{Val: "E0AD159D"},
-			"Asset Tag":                PropertyData{Val: "9876543210"},
-			"Part Number":              PropertyData{Val: "CT8G4SFD824A.M16FB"},
-			"Rank":                     PropertyData{Val: "2"},
-			"Configured Clock Speed":   PropertyData{Val: "2133 MT/s"},
-			"Minimum Voltage":          PropertyData{Val: "1.2 V"},
-			"Maximum Voltage":          PropertyData{Val: "1.2 V"},
-			"Configured Voltage":       PropertyData{Val: "1.2 V"},
+			"Array Handle":             {Val: "0x0035"},
+			"Error Information Handle": {Val: "Not Provided"},
+			"Total Width":              {Val: "64 bits"},
+			"Data Width":               {Val: "64 bits"},
+			"Size":                     {Val: "8192 MB"},
+			"Form Factor":              {Val: "SODIMM"},
+			"Set":                      {Val: "None"},
+			"Locator":                  {Val: "ChannelB-DIMM0"},
+			"Bank Locator":             {Val: "BANK 2"},
+			"Type":                     {Val: "DDR4"},
+			"Type Detail":              {Val: "Synchronous Unbuffered (Unregistered)"},
+			"Speed":                    {Val: "2400 MT/s"},
+			"Manufacturer":             {Val: "859B"},
+			"Serial Number":            {Val: "E0AD159D"},
+			"Asset Tag":                {Val: "9876543210"},
+			"Part Number":              {Val: "CT8G4SFD824A.M16FB"},
+			"Rank":                     {Val: "2"},
+			"Configured Clock Speed":   {Val: "2133 MT/s"},
+			"Minimum Voltage":          {Val: "1.2 V"},
+			"Maximum Voltage":          {Val: "1.2 V"},
+			"Configured Voltage":       {Val: "1.2 V"},
 		},
 	},
 }
diff --git a/pkg/flist/flist_test.go b/pkg/flist/flist_test.go
index 005afbe5..07cf1743 100644
--- a/pkg/flist/flist_test.go
+++ b/pkg/flist/flist_test.go
@@ -1,112 +1,202 @@
 package flist
 
 import (
+	"bytes"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
+	"strings"
 	"testing"
+	"text/template"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 	"github.com/threefoldtech/zos/pkg"
-	"github.com/threefoldtech/zos/pkg/flist/mock"
 )
 
-func testFlistModule(t *testing.T) (pkg.Flister, func()) {
-	root, err := ioutil.TempDir("", "flist_root")
-	require.NoError(t, err)
+var (
+	templ = template.Must(template.New("script").Parse(`
+echo $PPID > {{.pid}}
+echo "mount ready" > {{.log}}
+`))
+)
+
+// StorageMock is a mock object of the storage modules
+type StorageMock struct {
+	mock.Mock
+}
+
+// CreateFilesystem create filesystem mock
+func (s *StorageMock) CreateFilesystem(name string, size uint64, poolType pkg.DeviceType) (string, error) {
+	args := s.Called(name, size, poolType)
+	return args.String(0), args.Error(1)
+}
+
+// ReleaseFilesystem releases filesystem mock
+func (s *StorageMock) ReleaseFilesystem(name string) error {
+	args := s.Called(name)
+	return args.Error(0)
+}
+
+// Path implements the pkg.StorageModules interfaces
+func (s *StorageMock) Path(name string) (path string, err error) {
+	args := s.Called(name)
+	return args.String(0), args.Error(1)
+}
+
+type testCommander struct {
+	*testing.T
+	m map[string]string
+}
 
-	cleanup := func() {
-		os.RemoveAll(root)
+func (t *testCommander) args(args ...string) (map[string]string, []string) {
+	var lk string
+	v := make(map[string]string)
+	var r []string
+	for _, arg := range args {
+		if strings.HasPrefix(arg, "-") {
+			lk = strings.TrimPrefix(arg, "-")
+			v[lk] = ""
+			continue
+		}
+
+		//if no -, then it must be a value.
+		//so if lk is set, update, otherwise append to r
+		if len(lk) > 0 {
+			v[lk] = arg
+			lk = ""
+		} else {
+			r = append(r, arg)
+		}
 	}
 
-	return New(root, &mock.StorageMock{}), cleanup
+	t.m = v
+	return v, r
 }
-func TestMountUmount(t *testing.T) {
-	require := require.New(t)
-	assert := assert.New(t)
-	f, cleanup := testFlistModule(t)
-	defer cleanup()
 
-	path, err := f.Mount("https://hub.grid.tf/zaibon/ubuntu_bionic.flist", "")
-	require.NoError(err)
+func (t *testCommander) Command(name string, args ...string) *exec.Cmd {
+	if name != "g8ufs" {
+		t.Fatal("invalid command name, expected 'g8ufs'")
+	}
 
-	// check file are accessible
-	assert.NotEqual("", path)
-	infos, err := ioutil.ReadDir(path)
-	require.NoError(err)
-	assert.True(len(infos) > 0)
-
-	// check pid file exsists
-	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)
-
-	// try to unmount other part of the filesystem
-	err = f.Umount("/mnt/media/disk1")
-	assert.Error(err)
-
-	err = f.Umount(path)
-	assert.NoError(err)
-
-	// check pid file is gone after unmount
-	_, err = os.Stat(pidPath)
-	assert.Error(err)
-	assert.True(os.IsNotExist(err))
+	m, _ := t.args(args...)
+	var script bytes.Buffer
+	err := templ.Execute(&script, map[string]string{
+		"pid": m["pid"],
+		"log": m["log"],
+	})
+
+	require.NoError(t.T, err)
+
+	return exec.Command("sh", "-c", script.String())
+}
+
+func TestCommander(t *testing.T) {
+	cmder := testCommander{T: t}
+
+	m, r := cmder.args("-pid", "pid-file", "-log", "log-file", "remaining")
+
+	require.Equal(t, []string{"remaining"}, r)
+	require.Equal(t, map[string]string{
+		"pid": "pid-file",
+		"log": "log-file",
+	}, m)
+}
+
+/**
+* TestMount tests that the module actually call the underlying modules an binaries
+* with the correct arguments. It's mocked in a way that does not actually mount
+* an flist, but emulate the call of the g8ufs binary.
+* The method also validate that storage module is called as expected
+ */
+func TestMountUnmount(t *testing.T) {
+	cmder := &testCommander{T: t}
+	strg := &StorageMock{}
+
+	root, err := ioutil.TempDir("", "flist_root")
+	require.NoError(t, err)
+
+	defer os.RemoveAll(root)
+
+	flister := newFlister(root, strg, cmder)
+
+	strg.On("CreateFilesystem", mock.Anything, uint64(256*mib), pkg.SSDDevice).
+		Return("/my/backend", nil)
+
+	mnt, err := flister.Mount("https://hub.grid.tf/thabet/redis.flist", "")
+
+	require.NoError(t, err)
+
+	// Trick flister into thinking that 0-fs has exited
+	os.Remove(cmder.m["pid"])
+	strg.On("ReleaseFilesystem", filepath.Base(mnt)).Return(nil)
+
+	err = flister.Umount(mnt)
+	require.NoError(t, err)
 }
 
 func TestIsolation(t *testing.T) {
 	require := require.New(t)
-	assert := assert.New(t)
-	f, cleanup := testFlistModule(t)
-	defer cleanup()
 
-	path1, err := f.Mount("https://hub.grid.tf/zaibon/ubuntu_bionic.flist", "")
-	require.NoError(err)
+	cmder := &testCommander{T: t}
+	strg := &StorageMock{}
 
-	path2, err := f.Mount("https://hub.grid.tf/zaibon/ubuntu_bionic.flist", "")
+	root, err := ioutil.TempDir("", "flist_root")
 	require.NoError(err)
 
-	defer f.Umount(path1)
-	defer f.Umount(path2)
+	defer os.RemoveAll(root)
 
-	t.Run("new file isolated", func(t *testing.T) {
-		err = ioutil.WriteFile(filepath.Join(path1, "newfile"), []byte("hello world"), 0660)
-		require.NoError(err)
+	flister := newFlister(root, strg, cmder)
 
-		_, err = os.Open(filepath.Join(path2, "newfile"))
-		assert.True(os.IsNotExist(err))
-	})
+	strg.On("CreateFilesystem", mock.Anything, uint64(256*mib), pkg.SSDDevice).
+		Return("/my/backend", nil)
 
-	t.Run("common file isolation", func(t *testing.T) {
-		targetFile := "etc/resolv.conf"
-		content := []byte("nameserver 1.1.1.1")
-		err = ioutil.WriteFile(filepath.Join(path1, targetFile), content, 0660)
-		require.NoError(err)
+	path1, err := flister.Mount("https://hub.grid.tf/thabet/redis.flist", "")
+	require.NoError(err)
+	args1 := cmder.m
 
-		b, err := ioutil.ReadFile(filepath.Join(path2, targetFile))
-		require.NoError(err)
-		assert.NotEqual(content, b)
-	})
+	path2, err := flister.Mount("https://hub.grid.tf/thabet/redis.flist", "")
+	require.NoError(err)
+	args2 := cmder.m
+
+	require.NotEqual(path1, path2)
+	require.NotEqual(args1, args2)
+
+	// the 2 mounts since they are exactly the same flist
+	// should have same meta, and of course same cache
+	// but a different backend and pid
+	require.Equal(args1["cache"], args2["cache"])
+	require.Equal(args1["meta"], args2["meta"])
+	//TODO: the backend url is return by the storage mock, this is why this
+	// is failing.
+	// require.NotEqual(args1["backend"], args2["backend"])
+	require.NotEqual(args1["pid"], args2["pid"])
 }
 
 func TestDownloadFlist(t *testing.T) {
 	require := require.New(t)
 	assert := assert.New(t)
-	x, cleanup := testFlistModule(t)
-	defer cleanup()
+	cmder := &testCommander{T: t}
+	strg := &StorageMock{}
+
+	root, err := ioutil.TempDir("", "flist_root")
+	require.NoError(err)
+
+	//defer os.RemoveAll(root)
+
+	x := newFlister(root, strg, cmder)
 
 	f, ok := x.(*flistModule)
 	require.True(ok)
-	path1, err := f.downloadFlist("https://hub.grid.tf/zaibon/ubuntu_bionic.flist")
+	path1, err := f.downloadFlist("https://hub.grid.tf/thabet/redis.flist")
 	require.NoError(err)
 
 	info1, err := os.Stat(path1)
 	require.NoError(err)
 
-	path2, err := f.downloadFlist("https://hub.grid.tf/zaibon/ubuntu_bionic.flist")
+	path2, err := f.downloadFlist("https://hub.grid.tf/thabet/redis.flist")
 	require.NoError(err)
 
 	assert.Equal(path1, path2)
diff --git a/pkg/gedis/commands_identity_test.go b/pkg/gedis/commands_identity_test.go
new file mode 100644
index 00000000..f64c44b2
--- /dev/null
+++ b/pkg/gedis/commands_identity_test.go
@@ -0,0 +1,232 @@
+package gedis
+
+import (
+	"encoding/hex"
+	"fmt"
+	"testing"
+
+	"github.com/jbenet/go-base58"
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
+	"github.com/threefoldtech/zos/pkg/geoip"
+)
+
+func TestRegisterFarm(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	args := Args{
+		"farm": directory.TfgridFarm1{
+			ThreebotID:      "farm-id",
+			Name:            "my-node",
+			Email:           "azmy@test.com",
+			WalletAddresses: []string{"addr1", "addr2"},
+		},
+	}
+
+	conn.On("Do", "default.farms.register", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{"farm_id": 123}), nil)
+
+	id, err := gedis.RegisterFarm(
+		pkg.StrIdentifier("farm-id"),
+		"my-node",
+		"azmy@test.com",
+		[]string{"addr1", "addr2"},
+	)
+
+	require.NoError(err)
+	require.Equal(id, "123")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestRegisterNode(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	l, err := geoip.Fetch()
+	require.NoError(err)
+
+	args := Args{
+		"node": directory.TfgridNode2{
+			NodeID:       "node-1",
+			FarmID:       "farm-1",
+			OsVersion:    "v1.1.0",
+			PublicKeyHex: hex.EncodeToString(base58.Decode("node-1")),
+			Location: directory.TfgridLocation1{
+				Longitude: l.Longitute,
+				Latitude:  l.Latitude,
+				Continent: l.Continent,
+				Country:   l.Country,
+				City:      l.City,
+			},
+		},
+	}
+
+	conn.On("Do", "default.nodes.add", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{"node_id": "node-1"}), nil)
+
+	id, err := gedis.RegisterNode(
+		pkg.StrIdentifier("node-1"),
+		pkg.StrIdentifier("farm-1"),
+		"v1.1.0",
+	)
+
+	require.NoError(err)
+	require.Equal(id, "node-1")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestListNode(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	args := Args{
+		"farm_id": "farm-1",
+		"country": "eg",
+		"city":    "cairo",
+	}
+
+	conn.On("Do", "default.nodes.list", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{"nodes": []directory.TfgridNode2{
+			{NodeID: "node-1"},
+			{NodeID: "node-2"},
+		}}), nil)
+
+	nodes, err := gedis.ListNode(
+		pkg.StrIdentifier("farm-1"), "eg", "cairo",
+	)
+
+	require.NoError(err)
+	require.Len(nodes, 2)
+	require.Equal(nodes[0].NodeID, "node-1")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestGetNode(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	args := Args{
+		"node_id": "node-1",
+	}
+
+	conn.On("Do", "default.nodes.get", mustMarshal(t, args)).
+		Return(mustMarshal(t, directory.TfgridNode2{
+			NodeID: "node-1",
+		}), nil)
+
+	node, err := gedis.GetNode(
+		pkg.StrIdentifier("node-1"),
+	)
+
+	require.NoError(err)
+	require.Equal(node.NodeID, "node-1")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestGetFarm(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	args := Args{
+		"farm_id": 100,
+	}
+
+	conn.On("Do", "default.farms.get", mustMarshal(t, args)).
+		Return(mustMarshal(t, directory.TfgridFarm1{
+			ID:   100,
+			Name: "farm-1",
+		}), nil)
+
+	farm, err := gedis.GetFarm(
+		pkg.StrIdentifier("100"),
+	)
+
+	require.NoError(err)
+	require.Equal("100", farm.ID)
+	require.Equal("farm-1", farm.Name)
+	conn.AssertCalled(t, "Close")
+}
+
+func TestListFarm(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	args := Args{
+		"country": "eg",
+		"city":    "cairo",
+	}
+
+	conn.On("Do", "default.farms.list", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{"farms": []directory.TfgridFarm1{
+			{ID: 1, Name: "farm-1"},
+			{ID: 2, Name: "farm-2"},
+		}}), nil)
+
+	nodes, err := gedis.ListFarm("eg", "cairo")
+
+	require.NoError(err)
+	require.Len(nodes, 2)
+	require.Equal(nodes[0].ID, "1")
+	require.Equal(nodes[0].Name, "farm-1")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestUpdateGenericNodeCapacity(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	node := pkg.StrIdentifier("node-1")
+
+	args := Args{
+		"node_id": node.Identity(),
+		"resource": directory.TfgridNodeResourceAmount1{
+			Mru: 1,
+			Cru: 2,
+			Hru: 3,
+			Sru: 4,
+		},
+	}
+
+	const captype = "total"
+	action := fmt.Sprintf("default.nodes.update_%s_capacity", captype)
+	conn.On("Do", action, mustMarshal(t, args)).
+		Return(nil, nil)
+
+	err := gedis.updateGenericNodeCapacity(
+		"total",
+		node,
+		1, 2, 3, 4)
+
+	require.NoError(err)
+	conn.AssertCalled(t, "Close")
+}
diff --git a/pkg/gedis/commands_network_test.go b/pkg/gedis/commands_network_test.go
new file mode 100644
index 00000000..14cd7786
--- /dev/null
+++ b/pkg/gedis/commands_network_test.go
@@ -0,0 +1,87 @@
+package gedis
+
+import (
+	"net"
+	"testing"
+
+	"github.com/threefoldtech/zos/pkg/schema"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
+	"github.com/threefoldtech/zos/pkg/network/types"
+)
+
+func TestNetworkPublishInterfaces(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	id := pkg.StrIdentifier("node-1")
+	r := schema.MustParseIPRange("192.168.1.2/24")
+	args := Args{
+		"node_id": id,
+		"ifaces": []directory.TfgridNodeIface1{
+			{
+				Name: "eth0",
+				Addrs: []schema.IPRange{
+					r,
+				},
+				Gateway: []net.IP{
+					net.ParseIP("192.168.1.1"),
+				},
+			},
+		},
+	}
+
+	conn.On("Do", "default.nodes.publish_interfaces", mustMarshal(t, args)).
+		Return(nil, nil)
+
+	inf := types.IfaceInfo{
+		Name: "eth0",
+		Addrs: []*net.IPNet{
+			&r.IPNet,
+		},
+		Gateway: []net.IP{net.ParseIP("192.168.1.1")},
+	}
+	err := gedis.PublishInterfaces(id, []types.IfaceInfo{inf})
+
+	require.NoError(err)
+	conn.AssertCalled(t, "Close")
+}
+
+func TestNetworkSetPublicIface(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	id := pkg.StrIdentifier("node-1")
+	r := schema.MustParseIPRange("192.168.1.2/24")
+	args := Args{
+		"node_id": id,
+		"public": directory.TfgridNodePublicIface1{
+			Master: "eth0",
+			Ipv4:   r,
+			Gw4:    net.ParseIP("192.168.1.1"),
+		},
+	}
+
+	conn.On("Do", "default.nodes.set_public_iface", mustMarshal(t, args)).
+		Return(nil, nil)
+
+	err := gedis.SetPublicIface(id, &types.PubIface{
+		Master: "eth0",
+		Type:   types.MacVlanIface,
+		IPv4:   &r.IPNet,
+		GW4:    net.ParseIP("192.168.1.1"),
+	})
+
+	require.NoError(err)
+	conn.AssertCalled(t, "Close")
+}
diff --git a/pkg/gedis/commands_provision_test.go b/pkg/gedis/commands_provision_test.go
new file mode 100644
index 00000000..641f6ee9
--- /dev/null
+++ b/pkg/gedis/commands_provision_test.go
@@ -0,0 +1,193 @@
+package gedis
+
+import (
+	"encoding/json"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/mock"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zos/pkg"
+	types "github.com/threefoldtech/zos/pkg/gedis/types/provision"
+	"github.com/threefoldtech/zos/pkg/provision"
+	"github.com/threefoldtech/zos/pkg/schema"
+)
+
+func TestProvisionGet(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	id := "id"
+
+	args := Args{
+		"gwid": id,
+	}
+
+	conn.On("Do", "default.workload_manager.workload_get", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{
+			"workload_id": id,
+			"content":     "raw message",
+		}), nil)
+
+	res, err := gedis.Get(id)
+
+	require.NoError(err)
+	require.Equal(id, res.ID)
+	require.Equal(`"raw message"`, string(res.Data))
+	//require.Equal(node.NodeID, "node-1")
+	conn.AssertCalled(t, "Close")
+}
+
+func TestProvisionPoll(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	node := pkg.StrIdentifier("node-1")
+	time := time.Now()
+
+	args := Args{
+		"node_id": node.Identity(),
+		"epoch":   time.Unix(),
+	}
+
+	conn.On("Do", "default.workload_manager.workloads_list", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{
+			"workloads": []types.TfgridReservationWorkload1{
+				{WorkloadID: "1", Type: types.TfgridReservationWorkload1TypeVolume},
+				{WorkloadID: "2", Type: types.TfgridReservationWorkload1TypeContainer},
+			},
+		}), nil)
+
+	reservations, err := gedis.Poll(node, false, time) //setting false to true will force epoch to 0
+
+	require.NoError(err)
+	require.Len(reservations, 2)
+	require.Equal(reservations[1].Type, provision.ContainerReservation)
+	conn.AssertCalled(t, "Close")
+
+	args = Args{
+		"node_id": node.Identity(),
+		"epoch":   0,
+	}
+
+	conn.On("Do", "default.workload_manager.workloads_list", mustMarshal(t, args)).
+		Return(mustMarshal(t, Args{
+			"workloads": []types.TfgridReservationWorkload1{
+				{WorkloadID: "1", Type: types.TfgridReservationWorkload1TypeVolume},
+				{WorkloadID: "2", Type: types.TfgridReservationWorkload1TypeContainer},
+			},
+		}), nil)
+
+	reservations, err = gedis.Poll(node, true, time)
+
+	require.NoError(err)
+	require.Len(reservations, 2)
+	require.Equal(reservations[1].Type, provision.ContainerReservation)
+	conn.AssertCalled(t, "Close")
+
+}
+
+func TestProvisionFeedback(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	id := "101"
+	result := provision.Result{
+		Type:      provision.ContainerReservation,
+		ID:        id,
+		Created:   time.Now(),
+		State:     "ok",
+		Data:      json.RawMessage("returned data"),
+		Signature: []byte("signature"),
+	}
+
+	args := Args{
+		"reservation_id": 101,
+		"result": types.TfgridReservationResult1{
+			Category:   types.TfgridReservationResult1CategoryContainer,
+			WorkloadID: 101,
+			DataJSON:   string(result.Data),
+			Signature:  result.Signature,
+			State:      types.TfgridReservationResult1StateOk,
+			Epoch:      schema.Date{result.Created},
+		},
+	}
+
+	conn.On("Do", "default.workload_manager.set_workload_result", mustMarshal(t, args)).
+		Return(nil, nil)
+
+	err := gedis.Feedback(id, &result)
+
+	require.NoError(err)
+	conn.AssertCalled(t, "Close")
+}
+
+func TestProvisionReserve(t *testing.T) {
+	require := require.New(t)
+	pool, conn := getTestPool()
+	gedis := Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	id := pkg.StrIdentifier("101")
+	reservation := provision.Reservation{
+		Type:    provision.ContainerReservation,
+		ID:      "10",
+		Created: time.Now(),
+		Data: json.RawMessage(mustMarshal(t, provision.Container{
+			FList:      "http://hub.grid.tf/test/test.flist",
+			Entrypoint: "/bin/app",
+			Network: provision.Network{
+				NetwokID: "123",
+				IPs:      []net.IP{net.ParseIP("192.168.1.1")},
+			},
+		})),
+		Signature: []byte("signature"),
+	}
+
+	args := Args{
+		"reservation": types.TfgridReservation1{
+			DataReservation: types.TfgridReservationData1{
+				Containers: []types.TfgridReservationContainer1{
+					{
+						Flist:      "http://hub.grid.tf/test/test.flist",
+						Entrypoint: "/bin/app",
+						NetworkConnection: []types.TfgridReservationNetworkConnection1{
+							{NetworkID: "123", Ipaddress: net.ParseIP("192.168.1.1")},
+						},
+						Volumes: []types.TfgridReservationContainerMount1{},
+					},
+				},
+			},
+		},
+	}
+
+	conn.On("Do", "default.workload_manager.reservation_register", mock.MatchedBy(func(in []byte) bool {
+		EqualJSON(t, mustMarshal(t, args), in)
+		return true
+	})).
+		Return(mustMarshal(t, Args{
+			"id": 10,
+		}), nil)
+
+	result, err := gedis.Reserve(&reservation, id)
+
+	require.NoError(err)
+	require.Equal("10", result)
+	conn.AssertCalled(t, "Close")
+}
diff --git a/pkg/gedis/gedis_test.go b/pkg/gedis/gedis_test.go
new file mode 100644
index 00000000..8b785f7d
--- /dev/null
+++ b/pkg/gedis/gedis_test.go
@@ -0,0 +1,122 @@
+package gedis
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/garyburd/redigo/redis"
+	"github.com/stretchr/testify/mock"
+)
+
+type mockPool struct {
+	mock.Mock
+}
+
+func (p *mockPool) Get() redis.Conn {
+	args := p.Called()
+	return args.Get(0).(redis.Conn)
+}
+
+func (p *mockPool) Close() error {
+	return p.Called().Error(0)
+}
+
+type mockConn struct {
+	mock.Mock
+}
+
+func (c *mockConn) Close() error {
+	return c.Called().Error(0)
+}
+
+// Err returns a non-nil value when the connection is not usable.
+func (c *mockConn) Err() error {
+	return c.Called().Error(0)
+}
+
+// Do sends a command to the server and returns the received reply.
+func (c *mockConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	set := append([]interface{}{commandName}, args...)
+	a := c.Called(set...)
+	return a.Get(0), a.Error(1)
+}
+
+// Send writes the command to the client's output buffer.
+func (c *mockConn) Send(commandName string, args ...interface{}) error {
+	set := append([]interface{}{commandName}, args...)
+	a := c.Called(set...)
+	return a.Error(1)
+}
+
+// Flush flushes the output buffer to the Redis server.
+func (c *mockConn) Flush() error {
+	return c.Called().Error(0)
+}
+
+// Receive receives a single reply from the Redis server
+func (c *mockConn) Receive() (reply interface{}, err error) {
+	a := c.Called()
+	return a.Get(0), a.Error(1)
+}
+
+func getTestPool() (*mockPool, *mockConn) {
+	var pool mockPool
+	var conn mockConn
+	pool.On("Clone").Return(nil)
+	pool.On("Get").Return(&conn)
+
+	conn.On("Close").Return(nil)
+	return &pool, &conn
+}
+
+func mustMarshal(t *testing.T, v interface{}) []byte {
+	r, err := json.Marshal(v)
+	require.NoError(t, err)
+	return r
+}
+
+func EqualJSON(t *testing.T, expected, actual []byte) {
+	var exp interface{}
+	var act interface{}
+
+	if err := json.Unmarshal(expected, &exp); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := json.Unmarshal(actual, &act); err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, exp, act)
+}
+
+func TestGedisSend(t *testing.T) {
+	require := require.New(t)
+
+	pool, conn := getTestPool()
+
+	gedis := &Gedis{
+		pool:      pool,
+		namespace: "default",
+	}
+
+	conn.On("Do", "default.actor.method", mock.AnythingOfType("[]uint8")).
+		Return("mocked result", nil)
+
+	args := Args{
+		"key": "value",
+	}
+	result, err := gedis.Send("actor", "method", args)
+
+	require.NoError(err)
+	require.Equal("mocked result", result)
+
+	expected := mustMarshal(t, args)
+
+	conn.AssertCalled(t, "Do", "default.actor.method", expected)
+	conn.AssertCalled(t, "Close")
+}
diff --git a/pkg/schema/parse_test.go b/pkg/schema/parse_test.go
index 61bf4432..8065fd37 100644
--- a/pkg/schema/parse_test.go
+++ b/pkg/schema/parse_test.go
@@ -203,22 +203,22 @@ loaders= (LO) !jumpscale.digitalme.package.loader
 	if ok := assert.Equal(t, &Object{
 		URL: "jumpscale.digitalme.package",
 		Properties: []Property{
-			Property{Name: "name", Type: Type{
+			{Name: "name", Type: Type{
 				Default: `"UNKNOWN"`,
 				Kind:    StringKind,
 			}},
-			Property{Name: "enable", Type: Type{
+			{Name: "enable", Type: Type{
 				Default: `true`,
 				Kind:    BoolKind,
 			}},
-			Property{Name: "args", Type: Type{
+			{Name: "args", Type: Type{
 				Kind: ListKind,
 				Element: &Type{
 					Kind:      ObjectKind,
 					Reference: "jumpscale.digitalme.package.arg",
 				},
 			}},
-			Property{Name: "loaders", Type: Type{
+			{Name: "loaders", Type: Type{
 				Kind: ListKind,
 				Element: &Type{
 					Kind:      ObjectKind,
@@ -262,22 +262,22 @@ func TestNew(t *testing.T) {
 	if ok := assert.Equal(t, &Object{
 		URL: "jumpscale.digitalme.package",
 		Properties: []Property{
-			Property{Name: "name", Type: Type{
+			{Name: "name", Type: Type{
 				Default: `"UNKNOWN"`,
 				Kind:    StringKind,
 			}},
-			Property{Name: "enable", Type: Type{
+			{Name: "enable", Type: Type{
 				Default: `true`,
 				Kind:    BoolKind,
 			}},
-			Property{Name: "args", Type: Type{
+			{Name: "args", Type: Type{
 				Kind: ListKind,
 				Element: &Type{
 					Kind:      ObjectKind,
 					Reference: "jumpscale.digitalme.package.arg",
 				},
 			}},
-			Property{Name: "loaders", Type: Type{
+			{Name: "loaders", Type: Type{
 				Kind: ListKind,
 				Element: &Type{
 					Kind:      ObjectKind,
@@ -292,11 +292,11 @@ func TestNew(t *testing.T) {
 	if ok := assert.Equal(t, &Object{
 		URL: "jumpscale.digitalme.package.arg",
 		Properties: []Property{
-			Property{Name: "key", Type: Type{
+			{Name: "key", Type: Type{
 				Default: `""`,
 				Kind:    StringKind,
 			}},
-			Property{Name: "val", Type: Type{
+			{Name: "val", Type: Type{
 				Default: `""`,
 				Kind:    StringKind,
 			}},
@@ -308,13 +308,13 @@ func TestNew(t *testing.T) {
 	if ok := assert.Equal(t, &Object{
 		URL: "jumpscale.digitalme.package.loader",
 		Properties: []Property{
-			Property{Name: "giturl", Type: Type{
+			{Name: "giturl", Type: Type{
 				Kind: StringKind,
 			}},
-			Property{Name: "dest", Type: Type{
+			{Name: "dest", Type: Type{
 				Kind: StringKind,
 			}},
-			Property{Name: "enable", Type: Type{
+			{Name: "enable", Type: Type{
 				Default: "true",
 				Kind:    BoolKind,
 			}},
diff --git a/pkg/schema/types_test.go b/pkg/schema/types_test.go
index 1b78ea30..926caa90 100644
--- a/pkg/schema/types_test.go
+++ b/pkg/schema/types_test.go
@@ -38,14 +38,21 @@ func TestParseDate(t *testing.T) {
 
 func TestParseIPRange(t *testing.T) {
 	parser := func(t *testing.T, in string) IPRange {
-		//not in is surrounded by "" because it's json
-		if err := json.Unmarshal([]byte(in), &in); err != nil {
+		//note in is surrounded by "" because it's json
+		var str string
+		if err := json.Unmarshal([]byte(in), &str); err != nil {
 			t.Fatal(err)
 		}
-		_, ipNet, err := net.ParseCIDR(in)
+
+		if len(str) == 0 {
+			return IPRange{}
+		}
+
+		ip, ipNet, err := net.ParseCIDR(str)
 		if err != nil {
 			t.Fatal(err)
 		}
+		ipNet.IP = ip
 		return IPRange{*ipNet}
 	}
 
@@ -55,6 +62,7 @@ func TestParseIPRange(t *testing.T) {
 	}{
 		{`"192.168.1.0/24"`, parser},
 		{`"2001:db8::/32"`, parser},
+		{`""`, parser},
 	}
 
 	for _, c := range cases {
diff --git a/pkg/storage/filesystem/btrf_test.go b/pkg/storage/filesystem/btrfs_ci_test.go
similarity index 79%
rename from pkg/storage/filesystem/btrf_test.go
rename to pkg/storage/filesystem/btrfs_ci_test.go
index 8ea899da..60d95410 100644
--- a/pkg/storage/filesystem/btrf_test.go
+++ b/pkg/storage/filesystem/btrfs_ci_test.go
@@ -30,12 +30,16 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+var (
+	SkipCITests = false
+)
+
 type TestDevices map[string]string
 
 func (d TestDevices) Loops() DeviceCache {
 	var loops DeviceCache
 	for _, loop := range d {
-		loops = append(loops, &Device{Path: loop})
+		loops = append(loops, Device{Path: loop})
 	}
 
 	return loops
@@ -90,39 +94,16 @@ func SetupDevices(count int) (devices TestDevices, err error) {
 	return
 }
 
-type TestDeviceManager struct {
-	devices DeviceCache
-}
-
-func (m TestDeviceManager) Device(ctx context.Context, path string) (*Device, error) {
-	for _, loop := range m.devices {
-		if loop.Path == path {
-			return loop, nil
-		}
-	}
-
-	return nil, fmt.Errorf("device not found")
-}
-
-func (m TestDeviceManager) ByLabel(ctx context.Context, label string) (DeviceCache, error) {
-	var filterred DeviceCache
-	for _, device := range m.devices {
-		if device.Label == label {
-			filterred = append(filterred, device)
-		}
+func TestMain(m *testing.M) {
+	devices, err := SetupDevices(1)
+	if err != nil {
+		// we can't create devices. we need to skip
+		// CI test
+		SkipCITests = true
+	} else {
+		devices.Destroy()
 	}
-	return filterred, nil
-}
 
-func (m TestDeviceManager) Devices(ctx context.Context) (DeviceCache, error) {
-	return m.devices, nil
-}
-
-func (m TestDeviceManager) Scan(ctx context.Context) error {
-	return nil
-}
-
-func TestMain(m *testing.M) {
 	defer func() {
 		//make sure to try to detach all remaining loop devices from testing
 		run(context.Background(), "losetup", "-D")
@@ -216,30 +197,37 @@ func basePoolTest(t *testing.T, pool Pool) {
 	})
 }
 
-func TestBtrfsSingle(t *testing.T) {
+func TestBtrfsSingleCI(t *testing.T) {
+	if SkipCITests {
+		t.Skip("test requires ability to create loop devices")
+	}
+
 	devices, err := SetupDevices(1)
 	require.NoError(t, err, "failed to initialize devices")
 
 	defer devices.Destroy()
 	loops := devices.Loops()
 
-	fs := NewBtrfs(TestDeviceManager{loops})
-	pool, err := fs.Create(context.Background(), "test-single", loops, pkg.Single)
+	fs := NewBtrfs(&TestDeviceManager{loops})
+	pool, err := fs.Create(context.Background(), "test-single", pkg.Single, &loops[0])
 	require.NoError(t, err)
 
 	basePoolTest(t, pool)
 }
 
-func TestBtrfsRaid1(t *testing.T) {
+func TestBtrfsRaid1CI(t *testing.T) {
+	if SkipCITests {
+		t.Skip("test requires ability to create loop devices")
+	}
 	devices, err := SetupDevices(3)
 	require.NoError(t, err, "failed to initialize devices")
 
 	defer devices.Destroy()
 
 	loops := devices.Loops()
-	fs := NewBtrfs(TestDeviceManager{loops})
+	fs := NewBtrfs(&TestDeviceManager{loops})
 
-	pool, err := fs.Create(context.Background(), "test-raid1", loops[:2], pkg.Raid1) //use the first 2 disks
+	pool, err := fs.Create(context.Background(), "test-raid1", pkg.Raid1, &loops[0], &loops[1]) //use the first 2 disks
 
 	require.NoError(t, err)
 
@@ -255,40 +243,44 @@ func TestBtrfsRaid1(t *testing.T) {
 
 	t.Run("add device", func(t *testing.T) {
 		// add a device to array
-		err = pool.AddDevice(loops[2])
+		err = pool.AddDevice(&loops[2])
 		require.NoError(t, err)
 	})
 
 	t.Run("remove device", func(t *testing.T) {
 		// remove device from array
-		err = pool.RemoveDevice(loops[0])
+		err = pool.RemoveDevice(&loops[0])
 		require.NoError(t, err)
 	})
 
 	t.Run("remove second device", func(t *testing.T) {
 		// remove a 2nd device should fail because raid1 should
 		// have at least 2 devices
-		err = pool.RemoveDevice(loops[1])
+		err = pool.RemoveDevice(&loops[1])
 		require.Error(t, err)
 	})
 }
 
-func TestBtrfsList(t *testing.T) {
+func TestBtrfsListCI(t *testing.T) {
+	if SkipCITests {
+		t.Skip("test requires ability to create loop devices")
+	}
+
 	devices, err := SetupDevices(2)
 	require.NoError(t, err, "failed to initialize devices")
 
 	defer devices.Destroy()
 	loops := devices.Loops()
-	fs := NewBtrfs(TestDeviceManager{loops})
+	fs := NewBtrfs(&TestDeviceManager{loops})
 
 	names := make(map[string]struct{})
-	for i, loop := range loops {
-		name := fmt.Sprintf("test-list-%d", i)
+	for idx := range loops {
+		loop := &loops[idx]
+		name := fmt.Sprintf("test-list-%d", idx)
 		names[name] = struct{}{}
-		_, err := fs.Create(context.Background(), name, DeviceCache{loop}, pkg.Single)
+		_, err := fs.Create(context.Background(), name, pkg.Single, loop)
 		require.NoError(t, err)
 	}
-
 	pools, err := fs.List(context.Background(), func(p Pool) bool {
 		return strings.HasPrefix(p.Name(), "test-")
 	})
@@ -308,5 +300,4 @@ func TestBtrfsList(t *testing.T) {
 
 	ok := assert.Len(t, names, 0)
 	assert.True(t, ok, "not all pools were listed")
-
 }
diff --git a/pkg/storage/filesystem/btrfs_test.go b/pkg/storage/filesystem/btrfs_test.go
new file mode 100644
index 00000000..1e6d47f2
--- /dev/null
+++ b/pkg/storage/filesystem/btrfs_test.go
@@ -0,0 +1,91 @@
+package filesystem
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/threefoldtech/zos/pkg"
+
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+type TestDeviceManager struct {
+	devices DeviceCache
+}
+
+func (m *TestDeviceManager) Device(ctx context.Context, path string) (*Device, error) {
+	for idx := range m.devices {
+		loop := &m.devices[idx]
+		if loop.Path == path {
+			return loop, nil
+		}
+	}
+
+	return nil, fmt.Errorf("device not found")
+}
+
+func (m *TestDeviceManager) ByLabel(ctx context.Context, label string) ([]*Device, error) {
+	var filterred []*Device
+	for idx := range m.devices {
+		device := &m.devices[idx]
+		if device.Label == label {
+			filterred = append(filterred, device)
+		}
+	}
+	return filterred, nil
+}
+
+func (m *TestDeviceManager) Devices(ctx context.Context) (DeviceCache, error) {
+	return m.devices, nil
+}
+
+func TestBtrfsCreateSingle(t *testing.T) {
+	require := require.New(t)
+	mgr := &TestDeviceManager{
+		devices: DeviceCache{
+			Device{Path: "/tmp/dev1", DiskType: pkg.SSDDevice},
+		},
+	}
+
+	var exec TestExecuter
+
+	exec.On("run", mock.Anything, "mkfs.btrfs", "-L", "test-single", "-d", "single", "-m", "single", "/tmp/dev1").
+		Return([]byte{}, nil)
+
+	fs := newBtrfs(mgr, &exec)
+	_, err := fs.Create(context.Background(), "test-single", pkg.Single, &mgr.devices[0])
+	require.NoError(err)
+
+	require.Equal("test-single", mgr.devices[0].Label)
+	require.Equal(BtrfsFSType, mgr.devices[0].Filesystem)
+
+	//basePoolTest(t, &exec, pool)
+}
+
+func TestBtrfsCreateRaid1(t *testing.T) {
+	require := require.New(t)
+	mgr := &TestDeviceManager{
+		devices: DeviceCache{
+			Device{Path: "/tmp/dev1", DiskType: pkg.SSDDevice},
+			Device{Path: "/tmp/dev2", DiskType: pkg.SSDDevice},
+		},
+	}
+
+	var exec TestExecuter
+
+	exec.On("run", mock.Anything, "mkfs.btrfs", "-L", "test-raid1",
+		"-d", "raid1", "-m", "raid1",
+		"/tmp/dev1", "/tmp/dev2").Return([]byte{}, nil)
+
+	fs := newBtrfs(mgr, &exec)
+	_, err := fs.Create(context.Background(), "test-raid1", pkg.Raid1, &mgr.devices[0], &mgr.devices[1])
+	require.NoError(err)
+
+	require.Equal("test-raid1", mgr.devices[0].Label)
+	require.Equal(BtrfsFSType, mgr.devices[0].Filesystem)
+
+	require.Equal("test-raid1", mgr.devices[1].Label)
+	require.Equal(BtrfsFSType, mgr.devices[1].Filesystem)
+}
diff --git a/pkg/storage/filesystem/btrfs_utils_test.go b/pkg/storage/filesystem/btrfs_utils_test.go
index adcccf1f..b2e19dea 100644
--- a/pkg/storage/filesystem/btrfs_utils_test.go
+++ b/pkg/storage/filesystem/btrfs_utils_test.go
@@ -1,11 +1,13 @@
 package filesystem
 
 import (
+	"context"
 	"testing"
 
 	"github.com/threefoldtech/zos/pkg"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 )
 
@@ -237,3 +239,124 @@ qgroupid         rfer         excl     max_rfer     max_excl
 		MaxExcl: 0,
 	}, g)
 }
+
+func TestBtrfsList(t *testing.T) {
+	const tmp = `Label: 'pool-name'  uuid: 081717ad-77d5-488a-afd0-ab9108784f70
+Total devices 1 FS bytes used 206665822208
+devid    1 size 462713520128 used 211548110848 path /dev/sdb2
+`
+
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "filesystem", "show", "--raw", "-m", "pool-name").
+		Return([]byte(tmp), nil)
+
+	fs, err := utils.List(context.Background(), "pool-name", true)
+	require.NoError(err)
+	require.Len(fs, 1)
+}
+
+func TestBtrfsGetDiskUsage(t *testing.T) {
+	const tmp = `Data, single: total=8388608, used=65536
+System, single: total=4194304, used=16384
+Metadata, single: total=276824064, used=163840
+GlobalReserve, single: total=16777216, used=0
+	`
+
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "filesystem", "df", "--raw", "/mnt/pool").
+		Return([]byte(tmp), nil)
+
+	usage, err := utils.GetDiskUsage(context.Background(), "/mnt/pool")
+	require.NoError(err)
+
+	require.EqualValues(65536, usage.Data.Used)
+	require.EqualValues(163840, usage.Metadata.Used)
+}
+
+func TestBtrfsAddDevice(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	// pool must be mounted
+	exec.On("run", mock.Anything, "btrfs", "device", "add", "/tmp/dev1", "/tmp/root").
+		Return([]byte{}, nil)
+
+	err := utils.DeviceAdd(context.Background(), "/tmp/dev1", "/tmp/root")
+	require.NoError(err)
+}
+
+func TestBtrfsRemoveDevice(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	// pool must be mounted
+	exec.On("run", mock.Anything, "btrfs", "device", "remove", "/tmp/dev1", "/tmp/root").
+		Return([]byte{}, nil)
+
+	err := utils.DeviceRemove(context.Background(), "/tmp/dev1", "/tmp/root")
+	require.NoError(err)
+}
+
+func TestBtrfsAddVolume(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "subvolume", "create", "/tmp/root/subvol1").
+		Return([]byte{}, nil)
+
+	err := utils.SubvolumeAdd(context.Background(), "/tmp/root/subvol1")
+	require.NoError(err)
+}
+
+func TestBtrfsRemoveVolume(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "subvolume", "delete", "/tmp/root/subvol1").
+		Return([]byte{}, nil)
+
+	err := utils.SubvolumeRemove(context.Background(), "/tmp/root/subvol1")
+	require.NoError(err)
+}
+
+func TestBtrfsQGroupLimit(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "qgroup", "limit", "100", "/tmp/root/subvol1").
+		Return([]byte{}, nil)
+
+	err := utils.QGroupLimit(context.Background(), 100, "/tmp/root/subvol1")
+	require.NoError(err)
+}
+
+func TestBtrfsQGroupUnLimit(t *testing.T) {
+	require := require.New(t)
+
+	var exec TestExecuter
+	utils := newUtils(&exec)
+
+	exec.On("run", mock.Anything, "btrfs", "qgroup", "limit", "none", "/tmp/root/subvol1").
+		Return([]byte{}, nil)
+
+	err := utils.QGroupLimit(context.Background(), 0, "/tmp/root/subvol1")
+	require.NoError(err)
+}
diff --git a/pkg/storage/filesystem/device_test.go b/pkg/storage/filesystem/device_test.go
new file mode 100644
index 00000000..24ab2496
--- /dev/null
+++ b/pkg/storage/filesystem/device_test.go
@@ -0,0 +1,61 @@
+package filesystem
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDeviceManagerScan(t *testing.T) {
+	require := require.New(t)
+	var exec TestExecuter
+	ctx := context.Background()
+
+	var devices struct {
+		BlockDevices []Device `json:"blockdevices"`
+	}
+
+	devices.BlockDevices = []Device{
+		{Type: "block", Path: "/tmp/dev1", Label: "test"},
+		{Type: "block", Path: "/tmp/dev2", Children: []Device{
+			{Path: "/tmp/dev2-1"},
+			{Path: "/tmp/dev2-2"},
+		}},
+	}
+
+	bytes, err := json.Marshal(devices)
+	require.NoError(err)
+
+	// we expect this call to lsblk
+	exec.On("run", ctx, "lsblk", "--json", "--output-all", "--bytes", "--exclude", "1,2,11", "--path").
+		Return(bytes, nil)
+
+	// then the devices will be tested for types (per device)
+	exec.On("run", mock.Anything, "seektime", "-j", "/tmp/dev1").
+		Return([]byte(`{"type": "SSD", "elapsed": 100}`), nil)
+
+	exec.On("run", mock.Anything, "seektime", "-j", "/tmp/dev2").
+		Return([]byte(`{"type": "HDD", "elapsed": 5000}`), nil)
+
+	mgr, err := defaultDeviceManager(ctx, &exec)
+	require.NoError(err)
+
+	cached, err := mgr.Devices(ctx)
+	require.NoError(err)
+
+	require.Len(cached, 4)
+	// make sure all types are set.
+	for _, dev := range cached {
+		require.NotEmpty(dev.DiskType, "device: %s", dev.Path)
+	}
+
+	filtered, err := mgr.ByLabel(ctx, "test")
+	require.NoError(err)
+	require.Len(filtered, 1)
+
+	require.Equal("/tmp/dev1", cached[0].Path)
+
+}
diff --git a/pkg/storage/filesystem/utils_test.go b/pkg/storage/filesystem/utils_test.go
new file mode 100644
index 00000000..bc04566a
--- /dev/null
+++ b/pkg/storage/filesystem/utils_test.go
@@ -0,0 +1,43 @@
+package filesystem
+
+import (
+	"bytes"
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+type TestExecuter struct {
+	mock.Mock
+}
+
+func (t *TestExecuter) run(ctx context.Context, name string, args ...string) ([]byte, error) {
+	inputs := []interface{}{ctx, name}
+	for _, arg := range args {
+		inputs = append(inputs, arg)
+	}
+
+	result := t.Called(inputs...)
+	return result.Get(0).([]byte), result.Error(1)
+}
+
+func TestGetMountTarget(t *testing.T) {
+	buf := bytes.NewBufferString(`
+/dev/sda2 /home btrfs rw,relatime,ssd,space_cache,subvolid=258,subvol=/home 0 0
+/dev/sda1 /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
+/dev/sda2 /var/lib/docker/btrfs btrfs rw,relatime,ssd,space_cache,subvolid=257,subvol=/root/var/lib/docker/btrfs 0 0
+tmpfs /run/user/1000 tmpfs rw,nosuid,nodev,relatime,size=1626092k,mode=700,uid=1000,gid=1000 0 0
+fusectl /sys/fs/fuse/connections fusectl rw,nosuid,nodev,noexec,relatime 0 0
+gvfsd-fuse /run/user/1000/gvfs fuse.gvfsd-fuse rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+	`)
+
+	target, ok := getMountTarget(buf, "/dev/sda1")
+	require.True(t, ok)
+	require.Equal(t, "/boot", target)
+
+	target, ok = getMountTarget(buf, "/dev/sda3")
+	require.False(t, ok)
+	require.Equal(t, "", target)
+}
diff --git a/pkg/zinit/zinit_test.go b/pkg/zinit/zinit_test.go
index cea7869e..d7409bf0 100644
--- a/pkg/zinit/zinit_test.go
+++ b/pkg/zinit/zinit_test.go
@@ -28,19 +28,19 @@ sshd: Running`
 	require.NoError(t, err)
 
 	assert.Equal(t, map[string]ServiceState{
-		"ntp":            ServiceState{state: ServiceStateRunning},
-		"telnetd":        ServiceState{state: ServiceStateRunning},
-		"network-dhcp":   ServiceState{state: ServiceStateSuccess},
-		"haveged":        ServiceState{state: ServiceStateSuccess},
-		"debug-tty":      ServiceState{state: ServiceStateRunning},
-		"routing":        ServiceState{state: ServiceStateSuccess},
-		"udevd":          ServiceState{state: ServiceStateRunning},
-		"dhcp_zos":       ServiceState{state: ServiceStateRunning},
-		"udev-trigger":   ServiceState{state: ServiceStateSuccess},
-		"sshd-setup":     ServiceState{state: ServiceStateSuccess},
-		"local-modprobe": ServiceState{state: ServiceStateSuccess},
-		"networkd":       ServiceState{state: ServiceStateError, reason: "exited(pid(1592), 1)"},
-		"sshd":           ServiceState{state: ServiceStateRunning},
+		"ntp":            {state: ServiceStateRunning},
+		"telnetd":        {state: ServiceStateRunning},
+		"network-dhcp":   {state: ServiceStateSuccess},
+		"haveged":        {state: ServiceStateSuccess},
+		"debug-tty":      {state: ServiceStateRunning},
+		"routing":        {state: ServiceStateSuccess},
+		"udevd":          {state: ServiceStateRunning},
+		"dhcp_zos":       {state: ServiceStateRunning},
+		"udev-trigger":   {state: ServiceStateSuccess},
+		"sshd-setup":     {state: ServiceStateSuccess},
+		"local-modprobe": {state: ServiceStateSuccess},
+		"networkd":       {state: ServiceStateError, reason: "exited(pid(1592), 1)"},
+		"sshd":           {state: ServiceStateRunning},
 	}, services)
 }
 
diff --git a/tools/updatectl/hub_test.go b/tools/updatectl/hub_test.go
index b5aace42..73261ef0 100644
--- a/tools/updatectl/hub_test.go
+++ b/tools/updatectl/hub_test.go
@@ -22,8 +22,8 @@ func newTestHub(baseURL, user, token string) (*Hub, error) {
 	}
 
 	jar.SetCookies(base, []*http.Cookie{
-		&http.Cookie{Name: "caddyoauth", Value: token},
-		&http.Cookie{Name: "active-user", Value: user},
+		{Name: "caddyoauth", Value: token},
+		{Name: "active-user", Value: user},
 	})
 
 	return &Hub{client: http.Client{Jar: jar}, base: base}, nil