Repository: threefoldtech/zos
Pull request #321 (2019-10-09 14:21:14)

provision testing

codecov[bot]: # [Codecov](https://codecov.io/gh/threefoldtech/zos/pull/321?src=pr&el=h1) Report
> Merging [#321](https://codecov.io/gh/threefoldtech/zos/pull/321?src=pr&el=desc) into [master](https://codecov.io/gh/threefoldtech/zos/commit/3c3777ffde399e63b2cd6adea60da9e1d5b58e35?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##           master     #321   +/-   ##
=======================================
  Coverage   48.56%   48.56%           
=======================================
  Files          37       37           
  Lines        2432     2432           
=======================================
  Hits         1181     1181           
  Misses       1114     1114           
  Partials      137      137
```



------

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


Patch:
diff --git a/cmds/networkd/main.go b/cmds/networkd/main.go
index 4b84e5f5..2fae1416 100644
--- a/cmds/networkd/main.go
+++ b/cmds/networkd/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"flag"
-	"net"
 	"os"
 	"time"
 
@@ -141,10 +140,10 @@ func getLocalInterfaces() ([]types.IfaceInfo, error) {
 
 		info := types.IfaceInfo{
 			Name:  link.Attrs().Name,
-			Addrs: make([]*net.IPNet, len(addrs)),
+			Addrs: make([]types.IPNet, len(addrs)),
 		}
 		for i, addr := range addrs {
-			info.Addrs[i] = addr.IPNet
+			info.Addrs[i] = types.NewIPNet(addr.IPNet)
 		}
 
 		if gw != nil {
diff --git a/cmds/provisiond/main.go b/cmds/provisiond/main.go
index 9e9e6c79..69749c0f 100644
--- a/cmds/provisiond/main.go
+++ b/cmds/provisiond/main.go
@@ -84,12 +84,12 @@ func main() {
 	ctx := context.Background()
 	ctx = provision.WithZBus(ctx, client)
 	ctx = provision.WithOwnerCache(ctx, ownerCache)
-	ctx = provision.WithZDBMapping(ctx, &provision.ZDBMapping{})
+	ctx = provision.WithZDBMapping(ctx, provision.NewZDBMapping())
 
 	// From here we start the real provision engine that will live
 	// for the rest of the life of the node
 	source := provision.CombinedSource(
-		provision.HTTPSource(remoteStore, nodeID),
+		provision.PollSource(remoteStore, nodeID),
 		provision.NewDecommissionSource(localStore),
 	)
 
diff --git a/pkg/Makefile b/pkg/Makefile
index 5e88a3cf..3ee97935 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|provision|network" ); do \
+	for pkg in $(shell go list ./... | grep -Ev "stubs|network" ); do \
 		go test -v -vet=off $$pkg; \
 	done
 
diff --git a/pkg/buildscripts/go-coverage.sh b/pkg/buildscripts/go-coverage.sh
index db5c4022..479f1bb6 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|provision|network" ); do
+for d in $(go list ./... | grep -Ev "stubs|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/gedis/commands_identity.go b/pkg/gedis/commands_identity.go
index 0a9bd2cb..5a67d0c0 100644
--- a/pkg/gedis/commands_identity.go
+++ b/pkg/gedis/commands_identity.go
@@ -4,7 +4,6 @@ import (
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
-	"net"
 	"strconv"
 
 	"github.com/jbenet/go-base58"
@@ -142,10 +141,10 @@ func infFromSchema(inf directory.TfgridNodeIface1) types.IfaceInfo {
 	return types.IfaceInfo{
 		Name:    inf.Name,
 		Gateway: inf.Gateway,
-		Addrs: func() []*net.IPNet {
-			var r []*net.IPNet
+		Addrs: func() []types.IPNet {
+			var r []types.IPNet
 			for _, addr := range inf.Addrs {
-				r = append(r, &addr.IPNet)
+				r = append(r, types.NewIPNetFromSchema(addr))
 			}
 			return r
 		}(),
@@ -174,8 +173,8 @@ func nodeFromSchema(node directory.TfgridNode2) types.Node {
 			pub := types.PubIface{
 				Master:  cfg.Master,
 				Type:    types.IfaceType(cfg.Type.String()),
-				IPv4:    &cfg.Ipv4.IPNet,
-				IPv6:    &cfg.Ipv6.IPNet,
+				IPv4:    types.NewIPNetFromSchema(cfg.Ipv4),
+				IPv6:    types.NewIPNetFromSchema(cfg.Ipv6),
 				GW4:     cfg.Gw4,
 				GW6:     cfg.Gw6,
 				Version: int(cfg.Version),
diff --git a/pkg/gedis/commands_network.go b/pkg/gedis/commands_network.go
index aa398d73..f8e0848c 100644
--- a/pkg/gedis/commands_network.go
+++ b/pkg/gedis/commands_network.go
@@ -17,7 +17,7 @@ func (g *Gedis) getLocalInterfaces(ifaces []types.IfaceInfo) []directory.TfgridN
 				Addrs: func() []schema.IPRange {
 					var o []schema.IPRange
 					for _, r := range iface.Addrs {
-						o = append(o, schema.IPRange{IPNet: *r})
+						o = append(o, r.ToSchema())
 					}
 					return o
 				}(),
@@ -63,13 +63,13 @@ func (g *Gedis) SetPublicIface(node pkg.Identifier, pub *types.PubIface) error {
 		Version: int64(pub.Version),
 	}
 
-	if pub.IPv4 != nil {
-		public.Ipv4 = schema.IPRange{IPNet: *pub.IPv4}
+	if !pub.IPv4.Nil() {
+		public.Ipv4 = pub.IPv4.ToSchema()
 		public.Gw4 = pub.GW4
 	}
 
-	if pub.IPv6 != nil {
-		public.Ipv6 = schema.IPRange{IPNet: *pub.IPv6}
+	if !pub.IPv6.Nil() {
+		public.Ipv6 = pub.IPv6.ToSchema()
 		public.Gw6 = pub.GW6
 	}
 
diff --git a/pkg/gedis/commands_provision.go b/pkg/gedis/commands_provision.go
index 73c18ce1..1cc66c04 100644
--- a/pkg/gedis/commands_provision.go
+++ b/pkg/gedis/commands_provision.go
@@ -202,7 +202,7 @@ func networkReservation(i interface{}) types.TfgridReservationNetwork1 {
 	n := i.(pkg.Network)
 	network := types.TfgridReservationNetwork1{
 		Name:             n.Name,
-		Iprange:          schema.IPRange{IPNet: *n.IPRange},
+		Iprange:          n.IPRange.ToSchema(),
 		WorkloadID:       1,
 		NetworkResources: make([]types.TfgridNetworkNetResource1, len(n.NetResources)),
 	}
@@ -210,7 +210,7 @@ func networkReservation(i interface{}) types.TfgridReservationNetwork1 {
 	for i, nr := range n.NetResources {
 		network.NetworkResources[i] = types.TfgridNetworkNetResource1{
 			NodeID:                       nr.NodeID,
-			IPRange:                      schema.IPRange{IPNet: *nr.Subnet},
+			IPRange:                      nr.Subnet.ToSchema(),
 			WireguardPrivateKeyEncrypted: nr.WGPrivateKey,
 			WireguardPublicKey:           nr.WGPublicKey,
 			Peers:                        make([]types.WireguardPeer1, len(nr.Peers)),
diff --git a/pkg/network.go b/pkg/network.go
index a2cb498f..e6ebb9d2 100644
--- a/pkg/network.go
+++ b/pkg/network.go
@@ -3,6 +3,7 @@ package pkg
 import (
 	"net"
 
+	"github.com/threefoldtech/zos/pkg/network/types"
 	"github.com/threefoldtech/zos/pkg/versioned"
 )
 
@@ -49,7 +50,7 @@ type Network struct {
 	//unique id inside the reservation is an autoincrement (USE AS NET_ID)
 	NetID NetID `json:"net_id"`
 	// IP range of the network, must be an IPv4 /16
-	IPRange *net.IPNet `json:"ip_range"`
+	IPRange types.IPNet `json:"ip_range"`
 
 	NetResources []NetResource `json:"net_resources"`
 }
@@ -58,7 +59,7 @@ type Network struct {
 type NetResource struct {
 	NodeID string `json:"node_id"`
 	// IPV4 subnet from network IPRange
-	Subnet *net.IPNet `json:"subnet"`
+	Subnet types.IPNet `json:"subnet"`
 
 	WGPrivateKey string `json:"wg_private_key"`
 	WGPublicKey  string `json:"wg_public_key"`
@@ -70,11 +71,11 @@ type NetResource struct {
 // Peer is the description of a peer of a NetResource
 type Peer struct {
 	// IPV4 subnet of the network resource of the peer
-	Subnet *net.IPNet `json:"subnet"`
+	Subnet types.IPNet `json:"subnet"`
 
-	WGPublicKey string      `json:"wg_public_key"`
-	AllowedIPs  []net.IPNet `json:"allowed_ips"`
-	Endpoint    string      `json:"endpoint"`
+	WGPublicKey string        `json:"wg_public_key"`
+	AllowedIPs  []types.IPNet `json:"allowed_ips"`
+	Endpoint    string        `json:"endpoint"`
 }
 
 // NetID is a type defining the ID of a network
diff --git a/pkg/network/networker.go b/pkg/network/networker.go
index 47b751dd..c64b318d 100644
--- a/pkg/network/networker.go
+++ b/pkg/network/networker.go
@@ -65,7 +65,7 @@ func validateNetwork(n *pkg.Network) error {
 		return fmt.Errorf("network name cannot be empty")
 	}
 
-	if n.IPRange == nil {
+	if n.IPRange.Nil() {
 		return fmt.Errorf("network IP range cannot be empty")
 	}
 
@@ -86,7 +86,7 @@ func validateNR(nr pkg.NetResource) error {
 	if nr.NodeID == "" {
 		return fmt.Errorf("network resource node ID cannot empty")
 	}
-	if nr.Subnet == nil {
+	if nr.Subnet.IP == nil {
 		return fmt.Errorf("network resource subnet cannot empty")
 	}
 
@@ -116,7 +116,7 @@ func validatePeer(p pkg.Peer) error {
 		return fmt.Errorf("peer wireguard public key cannot empty")
 	}
 
-	if p.Subnet == nil {
+	if p.Subnet.Nil() {
 		return fmt.Errorf("peer wireguard subnet cannot empty")
 	}
 
@@ -147,7 +147,7 @@ func (n *networker) Join(networkdID pkg.NetID, containerID string, addrs []strin
 		return join, err
 	}
 
-	netRes, err := nr.New(networkdID, localNR, network.IPRange)
+	netRes, err := nr.New(networkdID, localNR, &network.IPRange.IPNet)
 	if err != nil {
 		return join, errors.Wrap(err, "failed to load network resource")
 	}
@@ -174,7 +174,7 @@ func (n *networker) Leave(networkdID pkg.NetID, containerID string) error {
 		return err
 	}
 
-	netRes, err := nr.New(networkdID, localNR, network.IPRange)
+	netRes, err := nr.New(networkdID, localNR, &network.IPRange.IPNet)
 	if err != nil {
 		return errors.Wrap(err, "failed to load network resource")
 	}
@@ -312,7 +312,7 @@ func (n *networker) CreateNR(network pkg.Network) (string, error) {
 		}
 	}
 
-	netr, err := nr.New(network.NetID, netNR, network.IPRange)
+	netr, err := nr.New(network.NetID, netNR, &network.IPRange.IPNet)
 	if err != nil {
 		return "", err
 	}
@@ -410,7 +410,7 @@ func (n *networker) DeleteNR(network pkg.Network) error {
 		return err
 	}
 
-	nr, err := nr.New(network.NetID, netNR, network.IPRange)
+	nr, err := nr.New(network.NetID, netNR, &network.IPRange.IPNet)
 	if err != nil {
 		return errors.Wrap(err, "failed to load network resource")
 	}
diff --git a/pkg/network/nr/net_resource.go b/pkg/network/nr/net_resource.go
index fa2feea9..5adc0c14 100644
--- a/pkg/network/nr/net_resource.go
+++ b/pkg/network/nr/net_resource.go
@@ -169,7 +169,7 @@ func (nr *NetResource) ConfigureWG(privateKey string) error {
 		}
 
 		newAddrs := mapset.NewSet()
-		newAddrs.Add(wgIP(nr.resource.Subnet).String())
+		newAddrs.Add(wgIP(&nr.resource.Subnet.IPNet).String())
 
 		toRemove := curAddrs.Difference(newAddrs)
 		toAdd := newAddrs.Difference(curAddrs)
@@ -252,11 +252,11 @@ func (nr *NetResource) Delete() error {
 func (nr *NetResource) routes() ([]*netlink.Route, error) {
 	routes := make([]*netlink.Route, 0, len(nr.resource.Peers)+1)
 
-	wgIP := wgIP(nr.resource.Subnet)
+	wgIP := wgIP(&nr.resource.Subnet.IPNet)
 
 	for _, peer := range nr.resource.Peers {
 		routes = append(routes, &netlink.Route{
-			Dst: peer.Subnet,
+			Dst: &peer.Subnet.IPNet,
 			Gw:  wgIP.IP,
 		})
 	}
@@ -379,7 +379,7 @@ func (nr *NetResource) createVethPair() error {
 		ipnet.IP[len(ipnet.IP)-1] = 0x01
 		log.Info().Str("addr", ipnet.String()).Msg("set address on veth interface")
 
-		addr := &netlink.Addr{IPNet: ipnet, Label: ""}
+		addr := &netlink.Addr{IPNet: &ipnet.IPNet, Label: ""}
 		if err = netlink.AddrAdd(link, addr); err != nil && !os.IsExist(err) {
 			return err
 		}
diff --git a/pkg/network/public.go b/pkg/network/public.go
index 41ac7af2..ff7d1a1b 100644
--- a/pkg/network/public.go
+++ b/pkg/network/public.go
@@ -65,7 +65,7 @@ func CreatePublicNS(iface *types.PubIface) error {
 		Str("pub iface", fmt.Sprintf("%+v", pubIface)).
 		Msg("configure public interface inside public namespace")
 
-	if iface.IPv6 != nil && iface.GW6 != nil {
+	if !iface.IPv6.Nil() && iface.GW6 != nil {
 		routes = append(routes, &netlink.Route{
 			Dst: &net.IPNet{
 				IP:   net.ParseIP("::"),
@@ -74,9 +74,9 @@ func CreatePublicNS(iface *types.PubIface) error {
 			Gw:        iface.GW6,
 			LinkIndex: pubIface.Attrs().Index,
 		})
-		ips = append(ips, iface.IPv6)
+		ips = append(ips, &iface.IPv6.IPNet)
 	}
-	if iface.IPv4 != nil && iface.GW4 != nil {
+	if !iface.IPv4.Nil() && iface.GW4 != nil {
 		routes = append(routes, &netlink.Route{
 			Dst: &net.IPNet{
 				IP:   net.ParseIP("0.0.0.0"),
@@ -85,7 +85,7 @@ func CreatePublicNS(iface *types.PubIface) error {
 			Gw:        iface.GW4,
 			LinkIndex: pubIface.Attrs().Index,
 		})
-		ips = append(ips, iface.IPv4)
+		ips = append(ips, &iface.IPv4.IPNet)
 	}
 
 	if len(ips) <= 0 || len(routes) <= 0 {
diff --git a/pkg/network/types/types.go b/pkg/network/types/types.go
index bf21e50b..b7b0d0d2 100644
--- a/pkg/network/types/types.go
+++ b/pkg/network/types/types.go
@@ -1,9 +1,10 @@
 package types
 
 import (
-	"encoding/json"
 	"fmt"
 	"net"
+
+	"github.com/threefoldtech/zos/pkg/schema"
 )
 
 // IfaceType define the different public interface supported
@@ -20,76 +21,9 @@ const (
 // that the node will publish publicly
 // this is used to be able to configure public side of a node
 type IfaceInfo struct {
-	Name    string       `json:"name"`
-	Addrs   []*net.IPNet `json:"addrs"`
-	Gateway []net.IP     `json:"gateway"`
-}
-
-// MarshalJSON implements encoding/json.Unmarshaler
-func (i *IfaceInfo) MarshalJSON() ([]byte, error) {
-	tmp := struct {
-		Name    string   `json:"name"`
-		Addrs   []string `json:"addrs"`
-		Gateway []string `json:"gateway"`
-	}{
-		Name:    i.Name,
-		Addrs:   make([]string, 0, len(i.Addrs)),
-		Gateway: make([]string, 0, len(i.Gateway)),
-	}
-	for _, addr := range i.Addrs {
-		if addr == nil {
-			continue
-		}
-		tmp.Addrs = append(tmp.Addrs, addr.String())
-	}
-	for _, gw := range i.Gateway {
-		if gw == nil {
-			continue
-		}
-		tmp.Gateway = append(tmp.Gateway, gw.String())
-	}
-
-	return json.Marshal(tmp)
-}
-
-// UnmarshalJSON implements encoding/json.Unmarshaler
-func (i *IfaceInfo) UnmarshalJSON(b []byte) (err error) {
-	tmp := struct {
-		Name    string   `json:"name"`
-		Addrs   []string `json:"addrs"`
-		Gateway []string `json:"gateway"`
-	}{}
-
-	if err := json.Unmarshal(b, &tmp); err != nil {
-		return err
-	}
-
-	*i = IfaceInfo{
-		Name:    tmp.Name,
-		Addrs:   make([]*net.IPNet, 0, len(tmp.Addrs)),
-		Gateway: make([]net.IP, 0, len(tmp.Gateway)),
-	}
-	i.Name = tmp.Name
-	for _, addr := range tmp.Addrs {
-		if addr == "" {
-			continue
-		}
-		ip, ipnet, err := net.ParseCIDR(addr)
-		if err != nil {
-			return err
-		}
-		ipnet.IP = ip
-		i.Addrs = append(i.Addrs, ipnet)
-	}
-	for _, gw := range tmp.Gateway {
-		if gw == "" {
-			continue
-		}
-		ip := net.ParseIP(gw)
-		i.Gateway = append(i.Gateway, ip)
-	}
-
-	return nil
+	Name    string   `json:"name"`
+	Addrs   []IPNet  `json:"addrs"`
+	Gateway []net.IP `json:"gateway"`
 }
 
 // DefaultIP return the IP address of the interface that has a default gateway configured
@@ -123,8 +57,8 @@ type PubIface struct {
 	Vlan int16     `json:"vlan"`
 	// Macvlan net.HardwareAddr
 
-	IPv4 *net.IPNet `json:"ip_v4"`
-	IPv6 *net.IPNet `json:"ip_v6"`
+	IPv4 IPNet `json:"ip_v4"`
+	IPv6 IPNet `json:"ip_v6"`
 
 	GW4 net.IP `json:"gw4"`
 	GW6 net.IP `json:"gw6"`
@@ -132,90 +66,87 @@ type PubIface struct {
 	Version int `json:"version"`
 }
 
-// MarshalJSON implements encoding/json.Unmarshaler
-func (p *PubIface) MarshalJSON() ([]byte, error) {
-	tmp := struct {
-		Master  string `json:"master"`
-		Type    string `json:"iface_type"`
-		Vlan    int16  `json:"vlan"`
-		IPv4    string `json:"ip_v4"`
-		IPv6    string `json:"ip_v6"`
-		GW4     string `json:"gw4"`
-		GW6     string `json:"gw6"`
-		Version int    `json:"version"`
-	}{
-		Master:  p.Master,
-		Type:    string(p.Type),
-		Vlan:    p.Vlan,
-		Version: p.Version,
-	}
-	if p.IPv4 != nil {
-		tmp.IPv4 = p.IPv4.String()
-	}
-	if p.IPv6 != nil {
-		tmp.IPv6 = p.IPv6.String()
-	}
-	if p.GW4 != nil {
-		tmp.GW4 = p.GW4.String()
+// Node is the public information about a node
+type Node struct {
+	NodeID string `json:"node_id"`
+	FarmID string `json:"farm_id"`
+
+	Ifaces []*IfaceInfo `json:"ifaces"`
+
+	PublicConfig *PubIface `json:"public_config"`
+	ExitNode     int       `json:"exit_node"`
+	WGPorts      []uint    `json:"wg_ports"`
+}
+
+// IPNet type
+type IPNet struct{ net.IPNet }
+
+// NewIPNet creates a new IPNet from net.IPNet
+func NewIPNet(n *net.IPNet) IPNet {
+	return IPNet{IPNet: *n}
+}
+
+// NewIPNetFromSchema creates an IPNet from schema.IPRange
+func NewIPNetFromSchema(n schema.IPRange) IPNet {
+	return IPNet{n.IPNet}
+}
+
+// ParseIPNet parse iprange
+func ParseIPNet(txt string) (r IPNet, err error) {
+	if len(txt) == 0 {
+		//empty ip net value
+		return r, nil
 	}
-	if p.GW6 != nil {
-		tmp.GW6 = p.GW6.String()
+	//fmt.Println("parsing: ", string(text))
+	ip, net, err := net.ParseCIDR(txt)
+	if err != nil {
+		return r, err
 	}
 
-	return json.Marshal(tmp)
+	net.IP = ip
+	r.IPNet = *net
+	return
 }
 
-// UnmarshalJSON implements encoding/json.Unmarshaler
-func (p *PubIface) UnmarshalJSON(b []byte) (err error) {
-	tmp := struct {
-		Master  string `json:"master"`
-		Type    string `json:"iface_type"`
-		Vlan    int16  `json:"vlan"`
-		IPv4    string `json:"ip_v4"`
-		IPv6    string `json:"ip_v6"`
-		GW4     string `json:"gw4"`
-		GW6     string `json:"gw6"`
-		Version int    `json:"version"`
-	}{}
-
-	if err := json.Unmarshal(b, &tmp); err != nil {
-		return err
+// MustParseIPNet prases iprange, panics if invalid
+func MustParseIPNet(txt string) IPNet {
+	r, err := ParseIPNet(txt)
+	if err != nil {
+		panic(err)
 	}
+	return r
+}
 
-	*p = PubIface{}
-	p.Master = tmp.Master
-	p.Type = IfaceType(tmp.Type)
-	p.Vlan = tmp.Vlan
-	if tmp.IPv4 != "" {
-		ip, ipnet, err := net.ParseCIDR(tmp.IPv4)
-		if err != nil {
-			return err
-		}
-		ipnet.IP = ip
-		p.IPv4 = ipnet
-	}
-	if tmp.IPv6 != "" {
-		ip, ipnet, err := net.ParseCIDR(tmp.IPv6)
-		if err != nil {
-			return err
-		}
-		ipnet.IP = ip
-		p.IPv6 = ipnet
+// UnmarshalText loads IPRange from string
+func (i *IPNet) UnmarshalText(text []byte) error {
+	v, err := ParseIPNet(string(text))
+	if err != nil {
+		return err
 	}
-	p.GW4 = net.ParseIP(tmp.GW4)
-	p.GW6 = net.ParseIP(tmp.GW6)
-	p.Version = tmp.Version
+
+	i.IPNet = v.IPNet
 	return nil
 }
 
-// Node is the public information about a node
-type Node struct {
-	NodeID string `json:"node_id"`
-	FarmID string `json:"farm_id"`
+// MarshalJSON dumps iprange as a string
+func (i IPNet) MarshalJSON() ([]byte, error) {
+	if len(i.IPNet.IP) == 0 {
+		return []byte(`""`), nil
+	}
+	v := fmt.Sprint("\"", i.String(), "\"")
+	return []byte(v), nil
+}
 
-	Ifaces []*IfaceInfo `json:"ifaces"`
+func (i IPNet) String() string {
+	return i.IPNet.String()
+}
 
-	PublicConfig *PubIface `json:"public_config"`
-	ExitNode     int       `json:"exit_node"`
-	WGPorts      []uint    `json:"wg_ports"`
+// Nil returns true if IPNet is not set
+func (i *IPNet) Nil() bool {
+	return i.IP == nil && i.Mask == nil
+}
+
+// ToSchema creates a schema IPRange from IPNet
+func (i *IPNet) ToSchema() schema.IPRange {
+	return schema.IPRange{IPNet: i.IPNet}
 }
diff --git a/pkg/network_encoding.go b/pkg/network_encoding.go
deleted file mode 100644
index db7be928..00000000
--- a/pkg/network_encoding.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package pkg
-
-import (
-	"encoding/json"
-	"net"
-)
-
-// UnmarshalJSON implements encoding/json.Unmarshaler
-func (p *Peer) UnmarshalJSON(b []byte) error {
-	tmp := struct {
-		Subnet      string   `json:"subnet"`
-		WGPublicKey string   `json:"wg_public_key"`
-		AllowedIPs  []string `json:"allowed_ips"`
-		Endpoint    string   `json:"endpoint"`
-	}{}
-
-	if err := json.Unmarshal(b, &tmp); err != nil {
-		return err
-	}
-
-	var err error
-	*p = Peer{}
-
-	if tmp.Subnet != "" {
-		_, p.Subnet, err = net.ParseCIDR(tmp.Subnet)
-		if err != nil {
-			return err
-		}
-	}
-	p.WGPublicKey = tmp.WGPublicKey
-	p.WGPublicKey = tmp.WGPublicKey
-	p.Endpoint = tmp.Endpoint
-	p.AllowedIPs = make([]net.IPNet, len(tmp.AllowedIPs))
-	for i, ip := range tmp.AllowedIPs {
-		ip, ipnet, err := net.ParseCIDR(ip)
-		if err != nil {
-			return err
-		}
-		ipnet.IP = ip
-		p.AllowedIPs[i] = *ipnet
-	}
-
-	return nil
-}
-
-// MarshalJSON implements encoding/json.Marshaler
-func (p *Peer) MarshalJSON() ([]byte, error) {
-
-	allowedIPs := make([]string, len(p.AllowedIPs))
-	for i, ip := range p.AllowedIPs {
-		allowedIPs[i] = ip.String()
-	}
-	tmp := struct {
-		Subnet      string   `json:"subnet"`
-		WGPublicKey string   `json:"wg_public_key"`
-		AllowedIPs  []string `json:"allowed_ips"`
-		Endpoint    string   `json:"endpoint"`
-	}{
-		WGPublicKey: p.WGPublicKey,
-		AllowedIPs:  allowedIPs,
-		Endpoint:    p.Endpoint,
-	}
-	if p.Subnet != nil {
-		tmp.Subnet = p.Subnet.String()
-
-	}
-
-	return json.Marshal(tmp)
-}
-
-// UnmarshalJSON implements encoding/json.Unmarshaler
-func (r *NetResource) UnmarshalJSON(b []byte) error {
-	tmp := struct {
-		NodeID       string `json:"node_id"`
-		Subnet       string `json:"subnet"`
-		WGPrivateKey string `json:"wg_private_key"`
-		WGPublicKey  string `json:"wg_public_key"`
-		WGListenPort uint16 `json:"wg_listen_port"`
-		Peers        []Peer `json:"peers"`
-	}{}
-
-	if err := json.Unmarshal(b, &tmp); err != nil {
-		return err
-	}
-
-	var err error
-	*r = NetResource{}
-
-	r.NodeID = tmp.NodeID
-	r.Peers = tmp.Peers
-	if tmp.Subnet != "" {
-		_, r.Subnet, err = net.ParseCIDR(tmp.Subnet)
-		if err != nil {
-			return err
-		}
-	}
-	r.WGPrivateKey = tmp.WGPrivateKey
-	r.WGPublicKey = tmp.WGPublicKey
-	r.WGListenPort = tmp.WGListenPort
-
-	return nil
-}
-
-// MarshalJSON implements encoding/json.Unmarshaler
-func (r *NetResource) MarshalJSON() ([]byte, error) {
-	tmp := struct {
-		NodeID       string `json:"node_id"`
-		Subnet       string `json:"subnet"`
-		Peers        []Peer `json:"peers"`
-		WGPrivateKey string `json:"wg_private_key"`
-		WGPublicKey  string `json:"wg_public_key"`
-		WGListenPort uint16 `json:"wg_listen_port"`
-	}{
-		NodeID:       r.NodeID,
-		Peers:        r.Peers,
-		WGPrivateKey: r.WGPrivateKey,
-		WGPublicKey:  r.WGPublicKey,
-		WGListenPort: r.WGListenPort,
-	}
-	if r.Subnet != nil {
-		tmp.Subnet = r.Subnet.String()
-	}
-
-	return json.Marshal(tmp)
-}
-
-// UnmarshalJSON implements encoding/json.Unmarshaler
-func (d *Network) UnmarshalJSON(b []byte) (err error) {
-	tmp := struct {
-		Name         string        `json:"name,omitempty"`
-		NetID        string        `json:"net_id,omitempty"`
-		NetResources []NetResource `json:"net_resources,omitempty"`
-		IPRange      string        `json:"ip_range,omitempty"`
-	}{}
-
-	if err := json.Unmarshal(b, &tmp); err != nil {
-		return err
-	}
-
-	*d = Network{}
-	d.Name = tmp.Name
-	d.NetID = NetID(tmp.NetID)
-	d.NetResources = tmp.NetResources
-	if tmp.IPRange != "" {
-		_, d.IPRange, err = net.ParseCIDR(tmp.IPRange)
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// MarshalJSON implements encoding/json.Marshaler
-func (d *Network) MarshalJSON() ([]byte, error) {
-	tmp := struct {
-		Name         string        `json:"name,omitempty"`
-		NetID        string        `json:"net_id,omitempty"`
-		NetResources []NetResource `json:"net_resources,omitempty"`
-		IPRange      string        `json:"ip_range,omitempty"`
-	}{
-		Name:         d.Name,
-		NetID:        string(d.NetID),
-		NetResources: d.NetResources,
-	}
-	if d.IPRange != nil {
-		tmp.IPRange = d.IPRange.String()
-	}
-	return json.Marshal(tmp)
-}
diff --git a/pkg/provision/container.go b/pkg/provision/container.go
index f54e5af8..98bcf61e 100644
--- a/pkg/provision/container.go
+++ b/pkg/provision/container.go
@@ -54,8 +54,12 @@ type ContainerResult struct {
 	IPv4 string `json:"ipv4"`
 }
 
-// ContainerProvision is entry point to container reservation
 func containerProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+	return containerProvisionImpl(ctx, reservation)
+}
+
+// ContainerProvision is entry point to container reservation
+func containerProvisionImpl(ctx context.Context, reservation *Reservation) (ContainerResult, error) {
 	client := GetZBus(ctx)
 	cache := GetOwnerCache(ctx)
 
@@ -70,16 +74,20 @@ func containerProvision(ctx context.Context, reservation *Reservation) (interfac
 	_, err := containerClient.Inspect(tenantNS, pkg.ContainerID(containerID))
 	if err == nil {
 		log.Info().Str("id", containerID).Msg("container already deployed")
-		return containerID, nil
+		return ContainerResult{
+			ID: containerID,
+			//IMPORTANT
+			//TODO: set IPv4 and IPv6 here
+		}, nil
 	}
 
 	var config Container
 	if err := json.Unmarshal(reservation.Data, &config); err != nil {
-		return nil, err
+		return ContainerResult{}, err
 	}
 
 	if err := validateContainerConfig(config); err != nil {
-		return nil, errors.Wrap(err, "container provision schema not valid")
+		return ContainerResult{}, errors.Wrap(err, "container provision schema not valid")
 	}
 
 	netID := networkID(reservation.User, string(config.Network.NetwokID))
@@ -97,7 +105,7 @@ func containerProvision(ctx context.Context, reservation *Reservation) (interfac
 
 	join, err := networkMgr.Join(netID, containerID, ips)
 	if err != nil {
-		return nil, err
+		return ContainerResult{}, err
 	}
 
 	// TODO: Push IP back to bcdb
@@ -110,7 +118,7 @@ func containerProvision(ctx context.Context, reservation *Reservation) (interfac
 	log.Debug().Str("flist", config.FList).Msg("mounting flist")
 	mnt, err := flistClient.Mount(config.FList, config.FlistStorage)
 	if err != nil {
-		return nil, err
+		return ContainerResult{}, err
 	}
 
 	var env []string
@@ -123,23 +131,23 @@ func containerProvision(ctx context.Context, reservation *Reservation) (interfac
 
 		owner, err := cache.OwnerOf(mount.VolumeID)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to retrieve the owner of volume %s", mount.VolumeID)
+			return ContainerResult{}, errors.Wrapf(err, "failed to retrieve the owner of volume %s", mount.VolumeID)
 		}
 
 		if owner != reservation.User {
-			return nil, fmt.Errorf("cannot use volume %s, user %s is not the owner of it", mount.VolumeID, reservation.User)
+			return ContainerResult{}, fmt.Errorf("cannot use volume %s, user %s is not the owner of it", mount.VolumeID, reservation.User)
 		}
 
 		// we make sure that mountpoint in config doesn't have relative parts
 		mountpoint := path.Join("/", mount.Mountpoint)
 
 		if err := os.MkdirAll(path.Join(mnt, mountpoint), 0755); err != nil {
-			return nil, err
+			return ContainerResult{}, err
 		}
 
 		source, err := storageClient.Path(mount.VolumeID)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to get the mountpoint path of the volume %s", mount.VolumeID)
+			return ContainerResult{}, errors.Wrapf(err, "failed to get the mountpoint path of the volume %s", mount.VolumeID)
 		}
 
 		mounts = append(
@@ -172,7 +180,7 @@ func containerProvision(ctx context.Context, reservation *Reservation) (interfac
 			log.Error().Err(err).Str("path", mnt).Msgf("failed to unmount")
 		}
 
-		return nil, err
+		return ContainerResult{}, err
 	}
 
 	log.Info().Msgf("container created with id: '%s'", id)
diff --git a/pkg/provision/midleware.go b/pkg/provision/midleware.go
index ac9ae716..e60f853d 100644
--- a/pkg/provision/midleware.go
+++ b/pkg/provision/midleware.go
@@ -27,32 +27,48 @@ func GetZBus(ctx context.Context) zbus.Client {
 	return value.(zbus.Client)
 }
 
+// OwnerCache interface
+type OwnerCache interface {
+	OwnerOf(reservationID string) (string, error)
+}
+
 // WithOwnerCache adds the owner cache to context
-func WithOwnerCache(ctx context.Context, cache *OwnerCache) context.Context {
+func WithOwnerCache(ctx context.Context, cache OwnerCache) context.Context {
 	return context.WithValue(ctx, owerCacheKey{}, cache)
 }
 
 // GetOwnerCache gets the owner cache from context
-func GetOwnerCache(ctx context.Context) *OwnerCache {
+func GetOwnerCache(ctx context.Context) OwnerCache {
 	value := ctx.Value(owerCacheKey{})
 	if value == nil {
 		panic("no reservation cache associated with context")
 	}
 
-	return value.(*OwnerCache)
+	return value.(OwnerCache)
+}
+
+// ZDBMapping interface
+type ZDBMapping interface {
+
+	// Get returns the container ID where namespace lives
+	// if the namespace is not found an empty string and false is returned
+	Get(namespace string) (string, bool)
+
+	// Set saves the mapping between the namespace and a container ID
+	Set(namespace, container string)
 }
 
 // WithZDBMapping set ZDBMapping into the context
-func WithZDBMapping(ctx context.Context, mapping *ZDBMapping) context.Context {
+func WithZDBMapping(ctx context.Context, mapping ZDBMapping) context.Context {
 	return context.WithValue(ctx, zdbMappingKey{}, mapping)
 }
 
 // GetZDBMapping gets the zdb mapping from the context
-func GetZDBMapping(ctx context.Context) *ZDBMapping {
+func GetZDBMapping(ctx context.Context) ZDBMapping {
 	value := ctx.Value(zdbMappingKey{})
 	if value == nil {
 		panic("no reservation mapping associated with context")
 	}
 
-	return value.(*ZDBMapping)
+	return value.(ZDBMapping)
 }
diff --git a/pkg/provision/network.go b/pkg/provision/network.go
index e05d374d..f774bb36 100644
--- a/pkg/provision/network.go
+++ b/pkg/provision/network.go
@@ -16,10 +16,10 @@ import (
 )
 
 // networkProvision is entry point to provision a network
-func networkProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+func networkProvisionImpl(ctx context.Context, reservation *Reservation) error {
 	network := &pkg.Network{}
 	if err := json.Unmarshal(reservation.Data, network); err != nil {
-		return nil, errors.Wrap(err, "failed to unmarshal network from reservation")
+		return errors.Wrap(err, "failed to unmarshal network from reservation")
 	}
 
 	network.NetID = networkID(reservation.User, network.Name)
@@ -30,11 +30,14 @@ func networkProvision(ctx context.Context, reservation *Reservation) (interface{
 
 	_, err := mgr.CreateNR(*network)
 	if err != nil {
-		return nil, errors.Wrapf(err, "failed to create network resource for network %s", network.NetID)
+		return errors.Wrapf(err, "failed to create network resource for network %s", network.NetID)
 	}
 
-	// nothing to return to BCDB
-	return nil, nil
+	return nil
+}
+
+func networkProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+	return nil, networkProvisionImpl(ctx, reservation)
 }
 
 func networkDecommission(ctx context.Context, reservation *Reservation) error {
@@ -57,6 +60,9 @@ func networkID(userID, name string) pkg.NetID {
 	buf := bytes.Buffer{}
 	buf.WriteString(userID)
 	buf.WriteString(name)
-	b := base58.Encode(buf.Bytes())[:13]
+	b := base58.Encode(buf.Bytes())
+	if len(b) > 13 {
+		b = b[:13]
+	}
 	return pkg.NetID(string(b))
 }
diff --git a/pkg/provision/owner_cache.go b/pkg/provision/owner_cache.go
index b6b86d57..ea6bb5ce 100644
--- a/pkg/provision/owner_cache.go
+++ b/pkg/provision/owner_cache.go
@@ -12,15 +12,15 @@ type ReservationGetter interface {
 	Get(id string) (*Reservation, error)
 }
 
-// OwnerCache allows to get the user ID of owner of a reservation
-type OwnerCache struct {
+// ownerCache allows to get the user ID of owner of a reservation
+type ownerCache struct {
 	local  ReservationGetter
 	remote ReservationGetter
 }
 
 // NewCache returns a new initialized reservation cache
-func NewCache(local, remote ReservationGetter) *OwnerCache {
-	return &OwnerCache{
+func NewCache(local, remote ReservationGetter) OwnerCache {
+	return &ownerCache{
 		local:  local,
 		remote: remote,
 	}
@@ -28,7 +28,7 @@ func NewCache(local, remote ReservationGetter) *OwnerCache {
 
 // OwnerOf return the userID of the creator of the reservation
 // identified by reservationID
-func (c *OwnerCache) OwnerOf(reservationID string) (string, error) {
+func (c *ownerCache) OwnerOf(reservationID string) (string, error) {
 	var (
 		r   *Reservation
 		err error
diff --git a/pkg/provision/source.go b/pkg/provision/source.go
index 8f65489b..29b3a6f7 100644
--- a/pkg/provision/source.go
+++ b/pkg/provision/source.go
@@ -10,11 +10,19 @@ import (
 	"github.com/threefoldtech/zos/pkg"
 )
 
-type httpSource struct {
-	store  ReservationPoller
-	nodeID string
+type pollSource struct {
+	store    ReservationPoller
+	nodeID   string
+	maxSleep time.Duration
 }
 
+var (
+	// ErrPollEOS can be returned by a reservation poll to
+	// notify the caller that it has reached end of stream
+	// and next calls will not return any more data.
+	ErrPollEOS = fmt.Errorf("end of stream")
+)
+
 // ReservationPoller define the interface to implement
 // to poll the BCDB for new reservation
 type ReservationPoller interface {
@@ -24,18 +32,19 @@ type ReservationPoller interface {
 	Poll(nodeID pkg.Identifier, all bool, since time.Time) ([]*Reservation, error)
 }
 
-// HTTPSource does a long poll on address to get new and to be deleted
+// PollSource does a long poll on address to get new and to be deleted
 // reservations. the server should only return unique reservations
 // stall the connection as long as possible if no new reservations
 // are available.
-func HTTPSource(store ReservationPoller, nodeID pkg.Identifier) ReservationSource {
-	return &httpSource{
-		store:  store,
-		nodeID: nodeID.Identity(),
+func PollSource(store ReservationPoller, nodeID pkg.Identifier) ReservationSource {
+	return &pollSource{
+		store:    store,
+		nodeID:   nodeID.Identity(),
+		maxSleep: 10 * time.Second,
 	}
 }
 
-func (s *httpSource) Reservations(ctx context.Context) <-chan *Reservation {
+func (s *pollSource) Reservations(ctx context.Context) <-chan *Reservation {
 	log.Info().Msg("start reservation http source")
 	ch := make(chan *Reservation)
 
@@ -43,31 +52,22 @@ func (s *httpSource) Reservations(ctx context.Context) <-chan *Reservation {
 	// ever made to this know, to make sure we provision
 	// everything at boot
 	// after that, we only ask for the new reservations
-	lastRun := time.Time{}
+	all := true
 	go func() {
-		next := time.Now().Add(time.Second * 10)
+		next := time.Now()
 		defer close(ch)
 		for {
-			// make sure we wait at least 10 second between calls
-			if !time.Now().After(next) {
-				time.Sleep(time.Second)
-				continue
-			}
-
-			log.Info().Msg("check for new reservations")
-			next = time.Now().Add(time.Second * 10)
-
-			all := false
-			if (lastRun == time.Time{}) {
-				all = true
-			}
+			time.Sleep(next.Sub(time.Now()))
+			since := next
+			next = time.Now().Add(s.maxSleep)
 
-			res, err := s.store.Poll(pkg.StrIdentifier(s.nodeID), all, lastRun)
-			if err != nil {
+			res, err := s.store.Poll(pkg.StrIdentifier(s.nodeID), all, since)
+			if err != nil && err != ErrPollEOS {
 				log.Error().Err(err).Msg("failed to get reservation")
 				time.Sleep(time.Second * 20)
 			}
-			lastRun = time.Now()
+
+			all = false
 
 			select {
 			case <-ctx.Done():
@@ -77,6 +77,10 @@ func (s *httpSource) Reservations(ctx context.Context) <-chan *Reservation {
 					ch <- r
 				}
 			}
+
+			if err == ErrPollEOS {
+				return
+			}
 		}
 	}()
 
diff --git a/pkg/provision/volume.go b/pkg/provision/volume.go
index a69537c5..58fd4b54 100644
--- a/pkg/provision/volume.go
+++ b/pkg/provision/volume.go
@@ -22,7 +22,7 @@ const (
 
 const (
 	// gigabyte to byte conversion
-	gigabyte = 1024 * 1024 * 1024
+	gigabyte uint64 = 1024 * 1024 * 1024
 )
 
 // Volume defines a mount point
@@ -39,20 +39,21 @@ type VolumeResult struct {
 	ID string `json:"volume_id"`
 }
 
-// VolumeProvision is entry point to provision a volume
-func volumeProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+func volumeProvisionImpl(ctx context.Context, reservation *Reservation) (VolumeResult, error) {
 	client := GetZBus(ctx)
 	var config Volume
 	if err := json.Unmarshal(reservation.Data, &config); err != nil {
-		return nil, err
+		return VolumeResult{}, err
 	}
 
 	storageClient := stubs.NewStorageModuleStub(client)
 
-	path, err := storageClient.Path(reservation.ID)
+	_, err := storageClient.Path(reservation.ID)
 	if err == nil {
 		log.Info().Str("id", reservation.ID).Msg("volume already deployed")
-		return path, nil
+		return VolumeResult{
+			ID: reservation.ID,
+		}, nil
 	}
 
 	_, err = storageClient.CreateFilesystem(reservation.ID, config.Size*gigabyte, pkg.DeviceType(config.Type))
@@ -62,6 +63,11 @@ func volumeProvision(ctx context.Context, reservation *Reservation) (interface{}
 	}, err
 }
 
+// VolumeProvision is entry point to provision a volume
+func volumeProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+	return volumeProvisionImpl(ctx, reservation)
+}
+
 func volumeDecommission(ctx context.Context, reservation *Reservation) error {
 	client := GetZBus(ctx)
 	storageClient := stubs.NewStorageModuleStub(client)
diff --git a/pkg/provision/zdb.go b/pkg/provision/zdb.go
index dfca831f..4cc53f89 100644
--- a/pkg/provision/zdb.go
+++ b/pkg/provision/zdb.go
@@ -48,38 +48,41 @@ type ZDBResult struct {
 // ZDBMapping is a helper struct that allow to keep
 // a mapping between a 0-db namespace and the container ID
 // in which it lives
-type ZDBMapping struct {
+type zdbMapping struct {
 	m map[string]string
 	sync.RWMutex
 }
 
+// NewZDBMapping creates a new ZDBMapping object
+func NewZDBMapping() ZDBMapping {
+	return &zdbMapping{
+		m: make(map[string]string),
+	}
+}
+
 // Get returns the container ID where namespace lives
 // if the namespace is not found an empty string and false is returned
-func (z *ZDBMapping) Get(namespace string) (string, bool) {
+func (z *zdbMapping) Get(namespace string) (string, bool) {
 	z.RLock()
 	defer z.RUnlock()
 
-	if z.m == nil {
-		return "", false
-	}
-
 	id, ok := z.m[namespace]
 	return id, ok
 }
 
 // Set saves the mapping between the namespace and a container ID
-func (z *ZDBMapping) Set(namespace, container string) {
+func (z *zdbMapping) Set(namespace, container string) {
 	z.Lock()
 	defer z.Unlock()
 
-	if z.m == nil {
-		z.m = make(map[string]string, 1)
-	}
-
 	z.m[namespace] = container
 }
 
 func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
+	return zdbProvisionImpl(ctx, reservation)
+}
+
+func zdbProvisionImpl(ctx context.Context, reservation *Reservation) (ZDBResult, error) {
 
 	var (
 		client = GetZBus(ctx)
@@ -94,7 +97,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 		containerIP net.IP
 	)
 	if err := json.Unmarshal(reservation.Data, &config); err != nil {
-		return nil, errors.Wrap(err, "failed to decode reservation schema")
+		return ZDBResult{}, errors.Wrap(err, "failed to decode reservation schema")
 	}
 
 	// verify if the namespace isn't already deployed
@@ -103,12 +106,12 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 	if ok {
 		c, err := container.Inspect(zdbContainerNS, pkg.ContainerID(containerID))
 		if err != nil {
-			return nil, err
+			return ZDBResult{}, err
 		}
 
 		ips, err := network.Addrs(nwmod.ZDBIface, c.Network.Namespace)
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to get 0-db container IP")
+			return ZDBResult{}, errors.Wrap(err, "failed to get 0-db container IP")
 		}
 		for _, ip := range ips {
 			if isPublic(ip) {
@@ -116,8 +119,9 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 				break
 			}
 		}
+
 		if containerIP == nil {
-			return nil, errors.Wrap(err, "failed to get 0-db container IP")
+			return ZDBResult{}, errors.New("failed to get 0-db container IP")
 		}
 
 		return ZDBResult{
@@ -131,7 +135,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 	log.Debug().Msg("try to allocate storage")
 	containerID, vPath, err := storage.Allocate(config.DiskType, config.Size*gigabyte, config.Mode)
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to allocate storage")
+		return ZDBResult{}, errors.Wrap(err, "failed to allocate storage")
 	}
 
 	slog := log.With().
@@ -145,7 +149,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 	//FIXME: find a better way then parsing error content
 	// Here we loose the error value cause the error comes from zbus
 	if err != nil && !strings.Contains(err.Error(), "not found") {
-		return nil, errors.Wrapf(err, "failed to check if 0-db container already exists")
+		return ZDBResult{}, errors.Wrapf(err, "failed to check if 0-db container already exists")
 	}
 
 	//FIXME: find a better way then parsing error content
@@ -154,7 +158,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 
 		containerIP, err = createZdbContainer(ctx, containerID, config.Mode, vPath)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to create 0-db container")
+			return ZDBResult{}, errors.Wrapf(err, "failed to create 0-db container")
 		}
 
 		// save in which container this namespace will be running
@@ -167,12 +171,12 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 		slog.Debug().Msg("not known, check it from the container")
 		c, err := container.Inspect(zdbContainerNS, pkg.ContainerID(containerID))
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to have a 0-db container running")
+			return ZDBResult{}, errors.Wrap(err, "failed to have a 0-db container running")
 		}
 
 		ips, err := network.Addrs(nwmod.ZDBIface, c.Network.Namespace)
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to get 0-db container IP")
+			return ZDBResult{}, errors.Wrap(err, "failed to get 0-db container IP")
 		}
 		for _, ip := range ips {
 			if isPublic(ip) {
@@ -181,7 +185,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 			}
 		}
 		if containerIP == nil {
-			return nil, errors.Wrap(err, "failed to get 0-db container IP")
+			return ZDBResult{}, errors.Wrap(err, "failed to get 0-db container IP")
 		}
 	}
 	slog.Info().IPAddr("ip", containerIP).Msg("container IP found")
@@ -192,7 +196,7 @@ func zdbProvision(ctx context.Context, reservation *Reservation) (interface{}, e
 		Msg("create 0-db namespace")
 
 	if err := createZDBNamespace(containerID, nsID, config); err != nil {
-		return nil, err
+		return ZDBResult{}, err
 	}
 
 	return ZDBResult{
diff --git a/tools/tffarmer/network.go b/tools/tffarmer/network.go
index aadadeca..92a35846 100644
--- a/tools/tffarmer/network.go
+++ b/tools/tffarmer/network.go
@@ -66,8 +66,8 @@ func configPublic(c *cli.Context) error {
 
 	if err := db.SetPublicIface(pkg.StrIdentifier(node), &types.PubIface{
 		Master: iface,
-		IPv4:   nv4,
-		IPv6:   nv6,
+		IPv4:   types.NewIPNet(nv4),
+		IPv6:   types.NewIPNet(nv6),
 		GW4:    gw4,
 		GW6:    gw6,
 	}); err != nil {
diff --git a/tools/tfuser/cmds_network.go b/tools/tfuser/cmds_network.go
index 723520d2..7297f1d2 100644
--- a/tools/tfuser/cmds_network.go
+++ b/tools/tfuser/cmds_network.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/threefoldtech/zos/pkg"
 	"github.com/threefoldtech/zos/pkg/crypto"
+	"github.com/threefoldtech/zos/pkg/network/types"
 	"github.com/threefoldtech/zos/pkg/provision"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 
@@ -31,11 +32,11 @@ func cmdCreateNetwork(c *cli.Context) error {
 	if ipRange == "" {
 		return fmt.Errorf("ip range cannot be empty")
 	}
-	_, ipnet, err := net.ParseCIDR(ipRange)
+
+	ipnet, err := types.ParseIPNet(ipRange)
 	if err != nil {
 		errors.Wrap(err, "invalid ip range")
 	}
-
 	network := &pkg.Network{
 		Name:         name,
 		IPRange:      ipnet,
@@ -74,7 +75,7 @@ func cmdsAddNode(c *cli.Context) error {
 		return fmt.Errorf("subnet cannot be empty")
 	}
 	fmt.Println("subnet", subnet)
-	_, ipnet, err := net.ParseCIDR(subnet)
+	ipnet, err := types.ParseIPNet(subnet)
 	if err != nil {
 		return errors.Wrap(err, "invalid subnet")
 	}
@@ -220,9 +221,9 @@ func generatePeers(nodeID string, n pkg.Network) []pkg.Peer {
 			continue
 		}
 
-		allowedIPs := make([]net.IPNet, 2)
-		allowedIPs[0] = *nr.Subnet
-		allowedIPs[1] = *wgIP(nr.Subnet)
+		allowedIPs := make([]types.IPNet, 2)
+		allowedIPs[0] = nr.Subnet
+		allowedIPs[1] = types.NewIPNet(wgIP(&nr.Subnet.IPNet))
 
 		peers = append(peers, pkg.Peer{
 			WGPublicKey: nr.WGPublicKey,


Test patch:
diff --git a/pkg/gedis/commands_network_test.go b/pkg/gedis/commands_network_test.go
index 14cd7786..880404b4 100644
--- a/pkg/gedis/commands_network_test.go
+++ b/pkg/gedis/commands_network_test.go
@@ -21,14 +21,14 @@ func TestNetworkPublishInterfaces(t *testing.T) {
 	}
 
 	id := pkg.StrIdentifier("node-1")
-	r := schema.MustParseIPRange("192.168.1.2/24")
+	r := types.MustParseIPNet("192.168.1.2/24")
 	args := Args{
 		"node_id": id,
 		"ifaces": []directory.TfgridNodeIface1{
 			{
 				Name: "eth0",
 				Addrs: []schema.IPRange{
-					r,
+					r.ToSchema(),
 				},
 				Gateway: []net.IP{
 					net.ParseIP("192.168.1.1"),
@@ -41,10 +41,8 @@ func TestNetworkPublishInterfaces(t *testing.T) {
 		Return(nil, nil)
 
 	inf := types.IfaceInfo{
-		Name: "eth0",
-		Addrs: []*net.IPNet{
-			&r.IPNet,
-		},
+		Name:    "eth0",
+		Addrs:   []types.IPNet{r},
 		Gateway: []net.IP{net.ParseIP("192.168.1.1")},
 	}
 	err := gedis.PublishInterfaces(id, []types.IfaceInfo{inf})
@@ -62,12 +60,12 @@ func TestNetworkSetPublicIface(t *testing.T) {
 	}
 
 	id := pkg.StrIdentifier("node-1")
-	r := schema.MustParseIPRange("192.168.1.2/24")
+	r := types.MustParseIPNet("192.168.1.2/24")
 	args := Args{
 		"node_id": id,
 		"public": directory.TfgridNodePublicIface1{
 			Master: "eth0",
-			Ipv4:   r,
+			Ipv4:   r.ToSchema(),
 			Gw4:    net.ParseIP("192.168.1.1"),
 		},
 	}
@@ -78,7 +76,7 @@ func TestNetworkSetPublicIface(t *testing.T) {
 	err := gedis.SetPublicIface(id, &types.PubIface{
 		Master: "eth0",
 		Type:   types.MacVlanIface,
-		IPv4:   &r.IPNet,
+		IPv4:   r,
 		GW4:    net.ParseIP("192.168.1.1"),
 	})
 
diff --git a/pkg/network/public_test.go b/pkg/network/public_test.go
index 61b4220f..28df096d 100644
--- a/pkg/network/public_test.go
+++ b/pkg/network/public_test.go
@@ -13,10 +13,10 @@ func TestCreatePublicNS(t *testing.T) {
 	iface := &types.PubIface{
 		Master: "zos0",
 		Type:   types.MacVlanIface,
-		IPv6: &net.IPNet{
+		IPv6: types.IPNet{net.IPNet{
 			IP:   net.ParseIP("2a02:1802:5e:ff02::100"),
 			Mask: net.CIDRMask(64, 128),
-		},
+		}},
 		GW6: net.ParseIP("fe80::1"),
 	}
 
diff --git a/pkg/network/types/types_test.go b/pkg/network/types/types_test.go
new file mode 100644
index 00000000..d3332802
--- /dev/null
+++ b/pkg/network/types/types_test.go
@@ -0,0 +1,85 @@
+package types
+
+import (
+	"encoding/json"
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseIPNet(t *testing.T) {
+	parser := func(t *testing.T, in string) IPNet {
+		//note in is surrounded by "" because it's json
+		var str string
+		if err := json.Unmarshal([]byte(in), &str); err != nil {
+			t.Fatal(err)
+		}
+
+		if len(str) == 0 {
+			return IPNet{}
+		}
+
+		ip, ipNet, err := net.ParseCIDR(str)
+		if err != nil {
+			t.Fatal(err)
+		}
+		ipNet.IP = ip
+		return IPNet{*ipNet}
+	}
+
+	cases := []struct {
+		Input  string
+		Output func(*testing.T, string) IPNet
+	}{
+		{`"192.168.1.0/24"`, parser},
+		{`"2001:db8::/32"`, parser},
+		{`""`, parser},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Input, func(t *testing.T) {
+			var d IPNet
+			err := json.Unmarshal([]byte(c.Input), &d)
+			if ok := assert.NoError(t, err); !ok {
+				t.Fatal()
+			}
+
+			if ok := assert.Equal(t, c.Output(t, c.Input), d); !ok {
+				t.Error()
+			}
+		})
+	}
+}
+
+func TestDumpIPNet(t *testing.T) {
+	mustParse := func(in string) IPNet {
+		_, ipNet, err := net.ParseCIDR(in)
+		if err != nil {
+			panic(err)
+		}
+		return IPNet{*ipNet}
+	}
+
+	cases := []struct {
+		Input  IPNet
+		Output string
+	}{
+		{IPNet{}, `""`},
+		{mustParse("192.168.1.0/24"), `"192.168.1.0/24"`},
+		{mustParse("2001:db8::/32"), `"2001:db8::/32"`},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Output, func(t *testing.T) {
+			out, err := json.Marshal(c.Input)
+			if ok := assert.NoError(t, err); !ok {
+				t.Fatal()
+			}
+
+			if ok := assert.Equal(t, c.Output, string(out)); !ok {
+				t.Error()
+			}
+		})
+	}
+}
diff --git a/pkg/network_encoding_test.go b/pkg/network_encoding_test.go
index 50d67186..9ce6f794 100644
--- a/pkg/network_encoding_test.go
+++ b/pkg/network_encoding_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zos/pkg/network/types"
 )
 
 func TestNetworkUnmarshal(t *testing.T) {
@@ -45,30 +46,30 @@ func TestEncodeDecode(t *testing.T) {
 	network := &Network{
 		NetID: NetID("test"),
 		Name:  "supernet",
-		IPRange: &net.IPNet{
+		IPRange: types.NewIPNet(&net.IPNet{
 			IP:   net.ParseIP("10.0.0.0"),
 			Mask: net.CIDRMask(16, 32),
-		},
+		}),
 		NetResources: []NetResource{
 			{
 				NodeID: "node1",
-				Subnet: &net.IPNet{
+				Subnet: types.NewIPNet(&net.IPNet{
 					IP:   net.ParseIP("10.0.1.0"),
 					Mask: net.CIDRMask(24, 32),
-				},
+				}),
 				Peers: []Peer{
 					{
-						Subnet: &net.IPNet{
+						Subnet: types.NewIPNet(&net.IPNet{
 							IP:   net.ParseIP("10.0.2.0"),
 							Mask: net.CIDRMask(24, 32),
-						},
+						}),
 						Endpoint:    "172.20.0.90:6380",
 						WGPublicKey: "pubkey",
-						AllowedIPs: []net.IPNet{
-							{
+						AllowedIPs: []types.IPNet{
+							types.NewIPNet(&net.IPNet{
 								IP:   net.ParseIP("10.0.1.0"),
 								Mask: net.CIDRMask(24, 32),
-							},
+							}),
 						},
 					},
 				},
diff --git a/pkg/provision/container_test.go b/pkg/provision/container_test.go
new file mode 100644
index 00000000..077ee4ff
--- /dev/null
+++ b/pkg/provision/container_test.go
@@ -0,0 +1,237 @@
+package provision
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zbus"
+	"github.com/threefoldtech/zos/pkg"
+)
+
+func TestContainerProvisionExists(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestOwnerCache
+
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithOwnerCache(ctx, &cache)
+
+	// const module = "network"
+	// version := zbus.ObjectID{Name: "network", Version: "0.0.1"}
+
+	container := Container{
+		FList: "https://hub.grid.tf/thabet/redis.flist",
+		Network: Network{
+			NetwokID: pkg.NetID("net1"),
+			IPs: []net.IP{
+				net.ParseIP("192.168.1.1"),
+			},
+		},
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ContainerReservation,
+		Data: MustMarshal(t, container),
+	}
+
+	// first, the provision will inspect the container
+	client.On("Request",
+		"container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect",
+		fmt.Sprintf("ns%s", reservation.User),
+		pkg.ContainerID(reservation.ID)).
+		Return(pkg.Container{Name: reservation.ID}, nil)
+
+	result, err := containerProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+	require.Equal("reservation-id", result.ID)
+}
+
+func TestContainerProvisionNew(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestOwnerCache
+
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithOwnerCache(ctx, &cache)
+
+	// const module = "network"
+	// version := zbus.ObjectID{Name: "network", Version: "0.0.1"}
+
+	container := Container{
+		FList: "https://hub.grid.tf/thabet/redis.flist",
+		Network: Network{
+			NetwokID: pkg.NetID("net1"),
+			IPs: []net.IP{
+				net.ParseIP("192.168.1.1"),
+			},
+		},
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ContainerReservation,
+		Data: MustMarshal(t, container),
+	}
+
+	userNs := fmt.Sprintf("ns%s", reservation.User)
+	// first, the provision will inspect the container
+	// see if it exists
+	client.On("Request",
+		"container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect",
+		userNs,
+		pkg.ContainerID(reservation.ID)).
+		Return(pkg.Container{}, zbus.RemoteError{"does not exist"})
+
+	netID := networkID(reservation.User, string(container.Network.NetwokID))
+
+	// since it's a new container, a call to join the network is made
+	client.On("Request",
+		"network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"Join",
+		netID, reservation.ID, []string{"192.168.1.1"}).
+		Return(pkg.Member{
+			Namespace: "net-ns",
+			IPv4:      net.ParseIP("192.168.1.1"),
+		}, nil)
+
+	// then another call to mount the flist
+	client.On("Request",
+		"flist", zbus.ObjectID{Name: "flist", Version: "0.0.1"},
+		"Mount",
+		container.FList, container.FlistStorage).
+		Return("/tmp/root", nil)
+
+	// once root is mounted, a final call to contd to run the container
+	client.On("Request",
+		"container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Run",
+		userNs, pkg.Container{
+			Name:   reservation.ID,
+			RootFS: "/tmp/root", //returned by flist.Mount
+			Network: pkg.NetworkInfo{
+				Namespace: "net-ns",
+			},
+		}).
+		Return(pkg.ContainerID(reservation.ID), nil)
+
+	result, err := containerProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+	require.Equal("reservation-id", result.ID)
+	require.Equal("192.168.1.1", result.IPv4)
+}
+
+func TestContainerProvisionWithMounts(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestOwnerCache
+
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithOwnerCache(ctx, &cache)
+
+	// const module = "network"
+	// version := zbus.ObjectID{Name: "network", Version: "0.0.1"}
+
+	container := Container{
+		FList: "https://hub.grid.tf/thabet/redis.flist",
+		Network: Network{
+			NetwokID: pkg.NetID("net1"),
+			IPs: []net.IP{
+				net.ParseIP("192.168.1.1"),
+			},
+		},
+		Mounts: []Mount{
+			{
+				VolumeID:   "vol1",
+				Mountpoint: "/opt",
+			},
+		},
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ContainerReservation,
+		Data: MustMarshal(t, container),
+	}
+
+	//NOTE: since we have moutns, a call is made to get the owner
+	//of the volume to validate it's the same owner of the reservation
+	cache.On("OwnerOf", "vol1").Return(reservation.User, nil)
+
+	userNs := fmt.Sprintf("ns%s", reservation.User)
+	// first, the provision will inspect the container
+	// see if it exists
+	client.On("Request",
+		"container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect",
+		userNs,
+		pkg.ContainerID(reservation.ID)).
+		Return(pkg.Container{}, zbus.RemoteError{"does not exist"})
+
+	netID := networkID(reservation.User, string(container.Network.NetwokID))
+
+	// since it's a new container, a call to join the network is made
+	client.On("Request",
+		"network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"Join",
+		netID, reservation.ID, []string{"192.168.1.1"}).
+		Return(pkg.Member{
+			Namespace: "net-ns",
+			IPv4:      net.ParseIP("192.168.1.1"),
+		}, nil)
+
+	// then another call to mount the flist
+	client.On("Request",
+		"storage", zbus.ObjectID{Name: "storage", Version: "0.0.1"},
+		"Path",
+		"vol1").
+		Return("/some/path/to/vol1", nil)
+
+	//for each volume in the list, a call is made to get the mount path
+	//of that volume
+	client.On("Request",
+		"flist", zbus.ObjectID{Name: "flist", Version: "0.0.1"},
+		"Mount",
+		container.FList, container.FlistStorage).
+		Return("/tmp/root", nil)
+
+	// once root is mounted, a final call to contd to run the container
+	client.On("Request",
+		"container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Run",
+		userNs, pkg.Container{
+			Name:   reservation.ID,
+			RootFS: "/tmp/root", //returned by flist.Mount
+			Network: pkg.NetworkInfo{
+				Namespace: "net-ns",
+			},
+			Mounts: []pkg.MountInfo{
+				{
+					Source:  "/some/path/to/vol1",
+					Target:  "/opt",
+					Type:    "none",
+					Options: []string{"bind"},
+				},
+			},
+		}).
+		Return(pkg.ContainerID(reservation.ID), nil)
+
+	result, err := containerProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+	require.Equal("reservation-id", result.ID)
+	require.Equal("192.168.1.1", result.IPv4)
+}
diff --git a/pkg/provision/local_store_test.go b/pkg/provision/local_store_test.go
index 5431c40b..62543b52 100644
--- a/pkg/provision/local_store_test.go
+++ b/pkg/provision/local_store_test.go
@@ -35,7 +35,7 @@ func TestLocalStore(t *testing.T) {
 			args: args{
 				r: &Reservation{
 					ID:       "r-1",
-					Created:  time.Now().Add(-time.Minute).Round(time.Second),
+					Created:  time.Now().UTC().Add(-time.Minute).Round(time.Second),
 					Duration: time.Second * 10,
 				},
 			},
diff --git a/pkg/provision/network_test.go b/pkg/provision/network_test.go
new file mode 100644
index 00000000..418a5991
--- /dev/null
+++ b/pkg/provision/network_test.go
@@ -0,0 +1,87 @@
+package provision
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zbus"
+	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/network/types"
+)
+
+func TestNetworkProvision(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+
+	const module = "network"
+	version := zbus.ObjectID{Name: "network", Version: "0.0.1"}
+
+	network := pkg.Network{
+		Name: "net1",
+		// we set netid here ourselves although it's set by the provision
+		// method just to make sure that assertion pass.
+		NetID:   networkID("user", "net1"),
+		IPRange: types.MustParseIPNet("192.168.1.0/24"),
+		NetResources: []pkg.NetResource{
+			{
+				NodeID: "node-1",
+				Subnet: types.MustParseIPNet("192.168.1.0/24"),
+			},
+		},
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: NetworkReservation,
+		Data: MustMarshal(t, network),
+	}
+
+	client.On("Request", module, version, "CreateNR", network).
+		Return("ns", nil)
+
+	err := networkProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+}
+
+func TestNetworkDecommission(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+
+	const module = "network"
+	version := zbus.ObjectID{Name: "network", Version: "0.0.1"}
+
+	network := pkg.Network{
+		Name: "net1",
+		// we set netid here ourselves although it's set by the provision
+		// method just to make sure that assertion pass.
+		NetID:   networkID("user", "net1"),
+		IPRange: types.MustParseIPNet("192.168.1.0/24"),
+		NetResources: []pkg.NetResource{
+			{
+				NodeID: "node-1",
+				Subnet: types.MustParseIPNet("192.168.1.0/24"),
+			},
+		},
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: NetworkReservation,
+		Data: MustMarshal(t, network),
+	}
+
+	client.On("Request", module, version, "DeleteNR", network).
+		Return(nil)
+
+	err := networkDecommission(ctx, &reservation)
+	require.NoError(err)
+}
diff --git a/pkg/provision/provision_test.go b/pkg/provision/provision_test.go
new file mode 100644
index 00000000..63ff312b
--- /dev/null
+++ b/pkg/provision/provision_test.go
@@ -0,0 +1,82 @@
+package provision
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zbus"
+)
+
+func MustMarshal(t *testing.T, v interface{}) []byte {
+	bytes, err := json.Marshal(v)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return bytes
+}
+
+type TestClient struct {
+	mock.Mock
+}
+
+// Request makes a request and return the response data
+func (t *TestClient) Request(module string, object zbus.ObjectID, method string, args ...interface{}) (*zbus.Response, error) {
+	inputs := []interface{}{
+		module, object, method,
+	}
+	for _, arg := range args {
+		inputs = append(inputs, arg)
+	}
+
+	return zbus.NewResponse("", "", t.Called(inputs...)...)
+}
+
+// Stream listens to a stream of events from the server
+func (t *TestClient) Stream(ctx context.Context, module string, object zbus.ObjectID, event string) (<-chan zbus.Event, error) {
+	panic("not implemented")
+}
+
+func TestClientOperation(t *testing.T) {
+	require := require.New(t)
+	var client TestClient
+
+	client.On("Request", "module", zbus.ObjectID{}, "test", 123, []string{"hello", "world"}).
+		Return("result", nil)
+
+	response, err := client.Request("module", zbus.ObjectID{}, "test", 123, []string{"hello", "world"})
+	require.NoError(err)
+
+	var str string
+	var rerr *zbus.RemoteError
+	require.NoError(response.Unmarshal(0, &str))
+	require.NoError(response.Unmarshal(1, &rerr))
+
+	require.Equal("result", str)
+	require.Nil(rerr)
+}
+
+type TestOwnerCache struct {
+	mock.Mock
+}
+
+func (t *TestOwnerCache) OwnerOf(id string) (string, error) {
+	result := t.Called(id)
+	return result.String(0), result.Error(1)
+}
+
+type TestZDBCache struct {
+	mock.Mock
+}
+
+func (t *TestZDBCache) Get(namespace string) (string, bool) {
+	result := t.Called(namespace)
+	return result.String(0), result.Bool(1)
+}
+
+// Set saves the mapping between the namespace and a container ID
+func (t *TestZDBCache) Set(namespace, container string) {
+	t.Called(namespace, container)
+}
diff --git a/pkg/provision/source_test.go b/pkg/provision/source_test.go
index c9d5c4e8..d042abc9 100644
--- a/pkg/provision/source_test.go
+++ b/pkg/provision/source_test.go
@@ -5,36 +5,76 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
 	"github.com/threefoldtech/zos/pkg"
 )
 
-type testReservationStore struct {
-	fReserve func(r Reservation, nodeID pkg.Identifier) error
-	fPoll    func(nodeID pkg.Identifier, all bool, since time.Time) ([]*Reservation, error)
-	fGet     func(id string) (Reservation, error)
+type TestPollSource struct {
+	mock.Mock
 }
 
-func (s testReservationStore) Reserve(r Reservation, nodeID pkg.Identifier) error {
-	return s.fReserve(r, nodeID)
-}
-func (s testReservationStore) Poll(nodeID pkg.Identifier, all bool, since time.Time) ([]*Reservation, error) {
-	return s.fPoll(nodeID, all, since)
-}
-func (s testReservationStore) Get(id string) (Reservation, error) {
-	return s.fGet(id)
+func (s *TestPollSource) Poll(nodeID pkg.Identifier, all bool, since time.Time) ([]*Reservation, error) {
+	returns := s.Called(nodeID, all, since)
+	return returns.Get(0).([]*Reservation), returns.Error(1)
 }
 
 func TestHTTPReservationSource(t *testing.T) {
-	t.Skip()
-	store := testReservationStore{
-		fPoll: func(nodeID pkg.Identifier, all bool, since time.Time) ([]*Reservation, error) {
-			time.Sleep(time.Second * 10)
-			return []*Reservation{}, nil
-		},
+	require := require.New(t)
+	var store TestPollSource
+
+	nodeID := pkg.StrIdentifier("node-id")
+	source := PollSource(&store, nodeID)
+	chn := source.Reservations(context.Background())
+
+	store.On("Poll", nodeID, true, mock.Anything).
+		Return([]*Reservation{
+			&Reservation{ID: "res-1"},
+			&Reservation{ID: "res-2"},
+		}, ErrPollEOS)
+
+	reservations := []*Reservation{}
+	for res := range chn {
+		reservations = append(reservations, res)
+	}
+
+	require.Len(reservations, 2)
+	require.Equal("res-1", reservations[0].ID)
+	require.Equal("res-2", reservations[1].ID)
+}
+
+func TestHTTPReservationSourceMultiple(t *testing.T) {
+	require := require.New(t)
+	var store TestPollSource
+
+	nodeID := pkg.StrIdentifier("node-id")
+	source := PollSource(&store, nodeID)
+
+	// don't delay the test for long
+	source.(*pollSource).maxSleep = 500 * time.Microsecond
+
+	chn := source.Reservations(context.Background())
+
+	store.On("Poll", nodeID, true, mock.Anything).
+		Return([]*Reservation{
+			&Reservation{ID: "res-1"},
+			&Reservation{ID: "res-2"},
+		}, nil) // return nil error so it tries again
+
+	store.On("Poll", nodeID, false, mock.Anything).
+		Return([]*Reservation{
+			&Reservation{ID: "res-3"},
+			&Reservation{ID: "res-4"},
+		}, ErrPollEOS)
+
+	reservations := []*Reservation{}
+	for res := range chn {
+		reservations = append(reservations, res)
 	}
 
-	source := HTTPSource(store, pkg.StrIdentifier("nodeID"))
-	c := source.Reservations(context.TODO())
-	_ = <-c
-	// assert.Equal(t, 0, len(result))
+	require.Len(reservations, 4)
+	require.Equal("res-1", reservations[0].ID)
+	require.Equal("res-2", reservations[1].ID)
+	require.Equal("res-3", reservations[2].ID)
+	require.Equal("res-4", reservations[3].ID)
 }
diff --git a/pkg/provision/volume_test.go b/pkg/provision/volume_test.go
new file mode 100644
index 00000000..9129c5ed
--- /dev/null
+++ b/pkg/provision/volume_test.go
@@ -0,0 +1,91 @@
+package provision
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zbus"
+	"github.com/threefoldtech/zos/pkg"
+)
+
+func TestVolumeProvisionExists(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+
+	const module = "storage"
+	version := zbus.ObjectID{Name: "storage", Version: "0.0.1"}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: VolumeReservation,
+		Data: MustMarshal(t, Volume{}),
+	}
+
+	client.On("Request", module, version, "Path", reservation.ID).
+		Return("/some/path", nil)
+
+	result, err := volumeProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+	require.EqualValues(VolumeResult{"reservation-id"}, result)
+}
+
+func TestVolumeProvisionNew(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+
+	const module = "storage"
+	version := zbus.ObjectID{Name: "storage", Version: "0.0.1"}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: VolumeReservation,
+		Data: MustMarshal(t, Volume{
+			Size: 10,
+			Type: SSDDiskType,
+		}),
+	}
+
+	// force creation by returning an error
+	client.On("Request", module, version, "Path", reservation.ID).
+		Return("", zbus.RemoteError{"does not exist"})
+
+	client.On("Request", module, version, "CreateFilesystem", reservation.ID, 10*gigabyte, pkg.DeviceType(SSDDiskType)).
+		Return("/some/path", nil)
+
+	result, err := volumeProvisionImpl(ctx, &reservation)
+	require.NoError(err)
+	require.EqualValues(VolumeResult{"reservation-id"}, result)
+}
+
+func TestVolumeDecomission(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+
+	const module = "storage"
+	version := zbus.ObjectID{Name: "storage", Version: "0.0.1"}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: VolumeReservation,
+	}
+
+	// force decomission by returning a nil error
+	client.On("Request", module, version, "ReleaseFilesystem", reservation.ID).
+		Return(nil)
+
+	err := volumeDecommission(ctx, &reservation)
+	require.NoError(err)
+}
diff --git a/pkg/provision/zdb_test.go b/pkg/provision/zdb_test.go
new file mode 100644
index 00000000..88db326e
--- /dev/null
+++ b/pkg/provision/zdb_test.go
@@ -0,0 +1,212 @@
+package provision
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/threefoldtech/zbus"
+	"github.com/threefoldtech/zos/pkg"
+)
+
+func TestZDBProvisionMappingExists(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestZDBCache
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithZDBMapping(ctx, &cache)
+
+	zdb := ZDB{
+		Size:     280682,
+		Mode:     pkg.ZDBModeSeq,
+		Password: "pa$$w0rd",
+		DiskType: pkg.SSDDevice,
+		Public:   true,
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ZDBReservation,
+		Data: MustMarshal(t, zdb),
+	}
+
+	cache.On("Get", reservation.ID).Return("container-id", true)
+
+	client.On("Request", "container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect", "zdb", pkg.ContainerID("container-id")).
+		Return(pkg.Container{
+			Name: "container-id",
+			Network: pkg.NetworkInfo{
+				Namespace: "net-ns",
+			},
+		}, nil)
+
+	client.On("Request", "network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"Addrs",
+		"zdb0", "net-ns").Return([]net.IP{net.ParseIP("2001:cdba::3257:9652")}, nil)
+
+	result, err := zdbProvisionImpl(ctx, &reservation)
+
+	require.NoError(err)
+	require.Equal(reservation.ID, result.Namespace)
+	require.Equal("2001:cdba::3257:9652", result.IP)
+	require.EqualValues(zdbPort, result.Port)
+}
+
+func TestZDBProvisionNoMappingContainerExists(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestZDBCache
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithZDBMapping(ctx, &cache)
+
+	zdb := ZDB{
+		Size:     10,
+		Mode:     pkg.ZDBModeSeq,
+		Password: "pa$$w0rd",
+		DiskType: pkg.SSDDevice,
+		Public:   true,
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ZDBReservation,
+		Data: MustMarshal(t, zdb),
+	}
+
+	// return false on cache force creation
+	cache.On("Get", reservation.ID).Return("", false)
+
+	// it's followed by allocation request to see if there is already a
+	// zdb instance running that has enough space for the ns
+
+	client.On("Request", "storage", zbus.ObjectID{Name: "storage", Version: "0.0.1"},
+		"Allocate",
+		zdb.DiskType, zdb.Size*gigabyte, zdb.Mode,
+	).Return("container-id", "/path/to/volume", nil)
+
+	// container exists, so inspect still should return No error
+	client.On("Request", "container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect", "zdb", pkg.ContainerID("container-id")).
+		Return(pkg.Container{
+			Name: "container-id",
+			Network: pkg.NetworkInfo{
+				Namespace: "net-ns",
+			},
+		}, nil)
+
+	client.On("Request", "network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"Addrs",
+		"zdb0", "net-ns").Return([]net.IP{net.ParseIP("2001:cdba::3257:9652")}, nil)
+
+	_, err := zdbProvisionImpl(ctx, &reservation)
+	// unfortunately the provision will try (as last step) to dial
+	// the unix socket of the server, and then actually configure
+	// the namespace and set the limits, password, etc...
+	// for now we just handle the connection error and assume
+	// it succeeded.
+	// TODO: start a mock server on this address
+	require.Error(err)
+	require.True(strings.Contains(err.Error(), "failed to connect to 0-db at unix:///var/run/zdb_container-id/zdb.sock"))
+}
+
+func TestZDBProvisionNoMappingContainerDoesNotExists(t *testing.T) {
+	require := require.New(t)
+
+	var client TestClient
+	var cache TestZDBCache
+	ctx := context.Background()
+	ctx = WithZBus(ctx, &client)
+	ctx = WithZDBMapping(ctx, &cache)
+
+	zdb := ZDB{
+		Size:     10,
+		Mode:     pkg.ZDBModeSeq,
+		Password: "pa$$w0rd",
+		DiskType: pkg.SSDDevice,
+		Public:   true,
+	}
+
+	reservation := Reservation{
+		ID:   "reservation-id",
+		User: "user",
+		Type: ZDBReservation,
+		Data: MustMarshal(t, zdb),
+	}
+
+	// return false on cache force creation
+	cache.On("Get", reservation.ID).Return("", false)
+
+	// it's followed by allocation request to see if there is already a
+	// zdb instance running that has enough space for the ns
+
+	client.On("Request", "storage", zbus.ObjectID{Name: "storage", Version: "0.0.1"},
+		"Allocate",
+		zdb.DiskType, zdb.Size*gigabyte, zdb.Mode,
+	).Return("container-id", "/path/to/volume", nil)
+
+	// container does NOT exists, so inspect still should return an error
+	client.On("Request", "container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Inspect", "zdb", pkg.ContainerID("container-id")).
+		Return(pkg.Container{}, fmt.Errorf("not found"))
+
+	client.On("Request", "network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"Addrs",
+		"zdb0", "net-ns").Return([]net.IP{net.ParseIP("2001:cdba::3257:9652")}, nil)
+
+	client.On("Request", "flist", zbus.ObjectID{Name: "flist", Version: "0.0.1"},
+		"Mount",
+		"https://hub.grid.tf/tf-autobuilder/threefoldtech-0-db-development.flist",
+		"").Return("/path/to/volume", nil)
+
+	client.On("Request", "network", zbus.ObjectID{Name: "network", Version: "0.0.1"},
+		"ZDBPrepare").Return("net-ns", nil)
+
+	client.On("Request", "container", zbus.ObjectID{Name: "container", Version: "0.0.1"},
+		"Run",
+		"zdb",
+		pkg.Container{
+			Name:    "container-id",
+			RootFS:  "/path/to/volume",
+			Network: pkg.NetworkInfo{Namespace: "net-ns"},
+			Mounts: []pkg.MountInfo{
+				pkg.MountInfo{
+					Source:  "/path/to/volume",
+					Target:  "/data",
+					Type:    "none",
+					Options: []string{"bind"},
+				},
+				pkg.MountInfo{
+					Source:  "/var/run/zdb_container-id",
+					Target:  "/socket",
+					Type:    "none",
+					Options: []string{"bind"},
+				},
+			},
+			Entrypoint:  "/bin/zdb --data /data --index /data --mode seq  --listen :: --port 9900 --socket /socket/zdb.sock --dualnet",
+			Interactive: false,
+		},
+	).Return(pkg.ContainerID("container-id"), nil)
+
+	cache.On("Set", reservation.ID, "container-id")
+
+	_, err := zdbProvisionImpl(ctx, &reservation)
+
+	// unfortunately the provision will try (as last step) to dial
+	// the unix socket of the server, and then actually configure
+	// the namespace and set the limits, password, etc...
+	// for now we just handle the connection error and assume
+	// it succeeded.
+	// TODO: start a mock server on this address
+	require.Error(err)
+	require.True(strings.Contains(err.Error(), "/var/run/zdb_container-id"))
+}