Repository: threefoldtech/zos
Pull request #416 (2019-11-27 13:20:40)

Master upgrade bins


Patch:
diff --git a/.github/workflows/publish-bootstrap.yaml b/.github/workflows/publish-bootstrap.yaml
index 587f0fd4..b2fb6852 100644
--- a/.github/workflows/publish-bootstrap.yaml
+++ b/.github/workflows/publish-bootstrap.yaml
@@ -1,7 +1,7 @@
 name: Publish Bootstrap
 on:
   push:
-    paths:
+    path:
       - 'bootstrap/bootstrap/**'
       - '.github/workflows/publish-bootstrap.yaml'
 
diff --git a/cmds/identityd/main.go b/cmds/identityd/main.go
index b88212f7..9c84d019 100644
--- a/cmds/identityd/main.go
+++ b/cmds/identityd/main.go
@@ -56,9 +56,9 @@ func setup(zinit *zinit.Client) error {
 	return nil
 }
 
-// SafeUpgrade makes sure upgrade daemon is not interrupted
-// While
-func SafeUpgrade(upgrader *upgrade.Upgrader) error {
+// Safe makes sure function call not interrupted
+// with a signal while exection
+func Safe(fn func() error) error {
 	ch := make(chan os.Signal)
 	defer close(ch)
 	defer signal.Stop(ch)
@@ -66,7 +66,7 @@ func SafeUpgrade(upgrader *upgrade.Upgrader) error {
 	// try to upgraded to latest
 	// but mean while also make sure the daemon can not be killed by a signal
 	signal.Notify(ch)
-	return upgrader.Upgrade()
+	return fn()
 }
 
 // This daemon startup has the follow flow:
@@ -124,13 +124,9 @@ func main() {
 	// only used for RO mounts. Otherwise it will panic
 	flister := flist.New(root, nil)
 
-	upgrader := upgrade.Upgrader{
-		FLister:      flister,
-		Zinit:        zinit,
-		NoSelfUpdate: debug,
-	}
+	boot := upgrade.Boot{}
 
-	bootMethod := upgrade.DetectBootMethod()
+	bootMethod := boot.DetectBootMethod()
 
 	// 2. Register the node to BCDB
 	// at this point we are running latest version
@@ -146,7 +142,7 @@ func main() {
 
 	var current string
 	if bootMethod == upgrade.BootMethodFList {
-		v, err := upgrader.Version()
+		v, err := boot.Version()
 		if err != nil {
 			//NOTE: this is set to error intentionally (not fatal)
 			//this will cause version to be 0.0.0 and will force an
@@ -209,45 +205,55 @@ func main() {
 		return
 	}
 
+	//NOTE: code after this commit will only
+	//run if the system is booted from an flist
+
 	// 4. Start watcher for new version
 	log.Info().Msg("start upgrade daemon")
-	ticker := time.NewTicker(time.Second * time.Duration(interval))
-	defer ticker.Stop()
-
-	for {
-		err := SafeUpgrade(&upgrader)
-		if err == upgrade.ErrRestartNeeded {
-			log.Info().Msg("restarting upgraded")
-			return
-		} else if err != nil {
-			//TODO: crash or continue!
-			log.Error().Err(err).Msg("upgrade failed")
-		}
 
-		version, err := upgrader.Version()
-		if err != nil {
-			log.Fatal().Err(err).Msg("failed to read current version")
-		}
+	upgrader := upgrade.Upgrader{
+		FLister:      flister,
+		Zinit:        zinit,
+		NoSelfUpdate: debug,
+	}
+
+	watcher := upgrade.FListSemverWatcher{
+		FList:    boot.Name(),
+		Current:  boot.MustVersion(), //if we are here version must be valid
+		Duration: 600 * time.Second,
+	}
 
-		if version.String() != current {
-			log.Info().Str("version", version.String()).Msg("new version")
+	events, err := watcher.Watch(ctx)
+	if err != nil {
+		log.Fatal().Err(err).Str("flist", boot.Name()).Msg("failed to watch flist")
+	}
 
-			if err := backoff.Retry(func() error {
-				return register(version.String())
-			}, backoff.NewExponentialBackOff()); err != nil {
-				log.Error().Err(err).Msg("failed to register node")
-			} else {
-				log.Info().Str("version", version.String()).Msg("node registered successfully")
+	for event := range events {
+		switch event := event.(type) {
+		case *upgrade.FListEvent:
+			// new flist available
+			from, err := boot.Current()
+			if err != nil {
+				log.Fatal().Err(err).Msg("failed to load current boot information")
+				return
 			}
 
-			current = version.String()
-		}
+			err = Safe(func() error {
+				return upgrader.Upgrade(from, *event)
+			})
+
+			if err == upgrade.ErrRestartNeeded {
+				log.Info().Msg("restarting upgraded")
+				return
+			} else if err != nil {
+				//TODO: crash or continue!
+				log.Error().Err(err).Msg("upgrade failed")
+				continue
+			}
 
-		select {
-		case <-ticker.C:
-			log.Debug().Msg("checking for updates")
-		case <-ctx.Done():
-			return
+			if err := boot.Set(*event); err != nil {
+				log.Error().Err(err).Msg("failed to update boot information")
+			}
 		}
 	}
 }
diff --git a/pkg/container.go b/pkg/container.go
index fda0d697..ceca914b 100644
--- a/pkg/container.go
+++ b/pkg/container.go
@@ -42,10 +42,6 @@ type Container struct {
 	Entrypoint string
 	// Interactivity enable Core X as PID 1 on the container
 	Interactive bool
-	// CPU count limit
-	CPU uint
-	// Memory limit in bytes
-	Memory uint64
 }
 
 // ContainerModule defines rpc interface to containerd
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 43b543a6..4531826f 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -22,7 +22,6 @@ import (
 	"github.com/containerd/containerd/cio"
 	"github.com/containerd/containerd/namespaces"
 	"github.com/containerd/containerd/oci"
-	"github.com/containerd/containerd/runtime/restart"
 	"github.com/google/shlex"
 	"github.com/threefoldtech/zos/pkg"
 )
@@ -31,11 +30,6 @@ const (
 	containerdSock = "/run/containerd/containerd.sock"
 )
 
-const (
-	defaultMemory = 256 * 1024 * 1204 // 256MiB
-	defaultCPU    = 1
-)
-
 var (
 	ignoreMntTypes = map[string]struct{}{
 		"proc":   {},
@@ -87,13 +81,6 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID
 		return id, ErrEmptyRootFS
 	}
 
-	if data.Memory == 0 {
-		data.Memory = defaultMemory
-	}
-	if data.CPU == 0 {
-		data.CPU = defaultCPU
-	}
-
 	// we never allow any container to boot without a network namespace
 	if data.Network.Namespace == "" {
 		return "", fmt.Errorf("cannot create container without network namespace")
@@ -103,16 +90,20 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID
 		errors.Wrap(err, "error updating environment variable from startup file")
 	}
 
+	args, err := shlex.Split(data.Entrypoint)
+	if err != nil || len(args) == 0 {
+		return id, fmt.Errorf("invalid entrypoint definition '%s'", data.Entrypoint)
+	}
+
 	opts := []oci.SpecOpts{
 		oci.WithDefaultSpecForPlatform("linux/amd64"),
 		oci.WithRootFSPath(data.RootFS),
+		oci.WithProcessArgs(args...),
 		oci.WithEnv(data.Env),
 		oci.WithHostResolvconf,
 		removeRunMount(),
 		withNetworkNamespace(data.Network.Namespace),
 		withMounts(data.Mounts),
-		WithMemoryLimit(data.Memory),
-		WithCPUCount(data.CPU),
 	}
 
 	if data.WorkingDir != "" {
@@ -121,13 +112,6 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID
 
 	if data.Interactive {
 		opts = append(opts, withCoreX())
-	} else {
-		args, err := shlex.Split(data.Entrypoint)
-		if err != nil || len(args) == 0 {
-			return id, fmt.Errorf("invalid entrypoint definition '%s'", data.Entrypoint)
-		}
-
-		opts = append(opts, oci.WithProcessArgs(args...))
 	}
 
 	log.Info().
@@ -139,9 +123,6 @@ func (c *containerModule) Run(ns string, data pkg.Container) (id pkg.ContainerID
 		ctx,
 		data.Name,
 		containerd.WithNewSpec(opts...),
-		// this ensure that the container/task will be restarted automatically
-		// if it gets killed for whatever reason (mostly OOM killer)
-		restart.WithStatus(containerd.Running),
 	)
 	if err != nil {
 		return id, err
@@ -254,10 +235,6 @@ func (c *containerModule) Delete(ns string, id pkg.ContainerID) error {
 		return err
 	}
 
-	if err := container.Update(ctx, restart.WithNoRestarts); err != nil {
-		log.Warn().Err(err).Msg("failed to clear up restart task status, continuing anyways")
-	}
-
 	task, err := container.Task(ctx, nil)
 	if err == nil {
 		// err == nil, there is a task running inside the container
diff --git a/pkg/container/opts.go b/pkg/container/opts.go
index 560732a7..c05c4123 100644
--- a/pkg/container/opts.go
+++ b/pkg/container/opts.go
@@ -2,13 +2,10 @@ package container
 
 import (
 	"context"
-	"fmt"
-	"math"
 
 	"path"
 
 	"github.com/opencontainers/runtime-spec/specs-go"
-	"github.com/shirou/gopsutil/cpu"
 	"github.com/threefoldtech/zos/pkg"
 
 	"github.com/containerd/containerd/containers"
@@ -101,73 +98,3 @@ func withMounts(mounts []pkg.MountInfo) oci.SpecOpts {
 	}
 	return oci.Compose(oci.WithMounts(mnts))
 }
-
-// WithMemoryLimit sets the `Linux.LinuxResources.Memory.Limit` section to the
-// `limit` specified if the `Linux` section is not `nil`. Additionally sets the
-// `Windows.WindowsResources.Memory.Limit` section if the `Windows` section is
-// not `nil`.
-func WithMemoryLimit(limit uint64) oci.SpecOpts {
-	return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
-		if s.Linux != nil {
-			if s.Linux.Resources == nil {
-				s.Linux.Resources = &specs.LinuxResources{}
-			}
-			if s.Linux.Resources.Memory == nil {
-				s.Linux.Resources.Memory = &specs.LinuxMemory{}
-			}
-			l := int64(limit)
-			s.Linux.Resources.Memory.Limit = &l
-		}
-		if s.Windows != nil {
-			if s.Windows.Resources == nil {
-				s.Windows.Resources = &specs.WindowsResources{}
-			}
-			if s.Windows.Resources.Memory == nil {
-				s.Windows.Resources.Memory = &specs.WindowsMemoryResources{}
-			}
-			s.Windows.Resources.Memory.Limit = &limit
-		}
-		return nil
-	}
-}
-
-// WithCPUCount configure the CPU cgroup to limit the amount of CPU used by the container
-func WithCPUCount(cru uint) oci.SpecOpts {
-	return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
-		totalCPU, err := cpu.Counts(true)
-		if err != nil {
-			return err
-		}
-
-		if cru > uint(totalCPU) {
-			return fmt.Errorf("asked %d CRU while only %d are available", cru, totalCPU)
-		}
-
-		quota, period := cruToLimit(cru, totalCPU)
-
-		if s.Linux.Resources == nil {
-			s.Linux.Resources = &specs.LinuxResources{}
-		}
-		if s.Linux.Resources.CPU == nil {
-			s.Linux.Resources.CPU = &specs.LinuxCPU{
-				Quota:  &quota,
-				Period: &period,
-			}
-		}
-
-		return nil
-	}
-}
-
-func cruToLimit(cru uint, totalCPU int) (quota int64, period uint64) {
-	var (
-		required float64 = float64(cru)
-		total    float64 = float64(totalCPU)
-		p        float64
-	)
-	quota = int64(1000000) // 1 sec
-	p = required / total
-	p *= float64(quota)
-	p = math.Ceil(p)
-	return quota, uint64(p)
-}
diff --git a/pkg/flist/flist.go b/pkg/flist/flist.go
index 64530312..3b5fc5a4 100644
--- a/pkg/flist/flist.go
+++ b/pkg/flist/flist.go
@@ -2,6 +2,7 @@ package flist
 
 import (
 	"bufio"
+	"bytes"
 	"context"
 	"crypto/md5"
 	"crypto/rand"
@@ -11,6 +12,7 @@ import (
 	"net/http"
 	"os"
 	"os/exec"
+	"path"
 	"path/filepath"
 	"strings"
 	"syscall"
@@ -94,6 +96,18 @@ func newFlister(root string, storage pkg.VolumeAllocater, commander commander) p
 	}
 }
 
+type options []string
+
+func (o options) Find(k string) int {
+	for i, x := range o {
+		if strings.EqualFold(x, k) {
+			return i
+		}
+	}
+
+	return -1
+}
+
 // New creates a new flistModule
 func New(root string, storage pkg.VolumeAllocater) pkg.Flister {
 	return newFlister(root, storage, cmd(exec.Command))
@@ -169,6 +183,27 @@ func (f *flistModule) Mount(url, storage string, opts pkg.MountOptions) (string,
 	return mountpoint, nil
 }
 
+func (f *flistModule) getMountOptions(pidPath string) (options, error) {
+	pid, err := ioutil.ReadFile(pidPath)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to open pid file: %s", pidPath)
+	}
+
+	cmdline, err := ioutil.ReadFile(path.Join("/proc", string(pid), "cmdline"))
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read mount (%s) cmdline", pidPath)
+	}
+
+	parts := bytes.Split(cmdline, []byte{0})
+
+	result := make(options, 0, len(parts))
+	for _, part := range parts {
+		result = append(result, string(part))
+	}
+
+	return result, nil
+}
+
 // Umount implements the Flister.Umount interface
 func (f *flistModule) Umount(path string) error {
 	log.Info().Str("path", path).Msg("request unmount flist")
@@ -185,11 +220,21 @@ func (f *flistModule) Umount(path string) error {
 		return fmt.Errorf("trying to unmount a directory outside of the flist module boundaries")
 	}
 
+	_, name := filepath.Split(path)
+	pidPath := filepath.Join(f.pid, name) + ".pid"
+
+	opts, err := f.getMountOptions(pidPath)
+	if err != nil {
+		log.Error().Err(err).Msg("failed to read mount options")
+
+		//we don't return an error in case the file is gone somehow
+		//but the mount is still there. So better to fail on next call
+		//than leaking
+	}
+
 	if err := syscall.Unmount(path, syscall.MNT_DETACH); err != nil {
 		log.Error().Err(err).Str("path", path).Msg("fail to umount flist")
 	}
-	_, 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 stop properly")
 		return err
@@ -203,7 +248,12 @@ func (f *flistModule) Umount(path string) error {
 			return err
 		}
 	}
-	// clean up subvolume
+
+	// clean up subvolume should be done only for RW mounts.
+	if opts.Find("-ro") >= 0 {
+		return nil
+	}
+
 	if err := f.storage.ReleaseFilesystem(name); err != nil {
 		return errors.Wrap(err, "fail to clean up subvolume")
 	}
diff --git a/pkg/network/tnodb/store.go b/pkg/network/tnodb/store.go
index 054ef1f0..6b8f3901 100644
--- a/pkg/network/tnodb/store.go
+++ b/pkg/network/tnodb/store.go
@@ -128,12 +128,12 @@ func (s *httpTNoDB) GetNode(nodeID pkg.Identifier) (*types.Node, error) {
 		return nil, fmt.Errorf("wrong response status: %v", resp.Status)
 	}
 
-	node := directory.TfgridNode2{}
+	node := &types.Node{}
 	if err := json.NewDecoder(resp.Body).Decode(&node); err != nil {
 		return nil, err
 	}
 
-	return types.NewNodeFromSchema(node), nil
+	return node, nil
 }
 
 func (s *httpTNoDB) PublishInterfaces(local pkg.Identifier, ifaces []types.IfaceInfo) error {
diff --git a/pkg/network/types/types.go b/pkg/network/types/types.go
index 0fb77ebf..ca732a2c 100644
--- a/pkg/network/types/types.go
+++ b/pkg/network/types/types.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"net"
 
-	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
 	"github.com/threefoldtech/zos/pkg/schema"
 )
 
@@ -79,49 +78,6 @@ type Node struct {
 	WGPorts      []uint    `json:"wg_ports"`
 }
 
-// NewNodeFromSchema converts a TfgridNode2 into Node
-func NewNodeFromSchema(node directory.TfgridNode2) *Node {
-	n := &Node{
-		NodeID: node.NodeID,
-		FarmID: node.FarmID,
-
-		Ifaces: make([]*IfaceInfo, len(node.Ifaces)),
-
-		PublicConfig: nil,
-		ExitNode:     0,
-		WGPorts:      node.WGPorts,
-	}
-	if node.Ifaces != nil {
-		for i, iface := range node.Ifaces {
-			n.Ifaces[i] = &IfaceInfo{
-				Name:    iface.Name,
-				Addrs:   make([]IPNet, len(iface.Addrs)),
-				Gateway: iface.Gateway,
-			}
-
-			for y, addr := range iface.Addrs {
-				n.Ifaces[i].Addrs[y] = NewIPNetFromSchema(addr)
-			}
-		}
-	}
-	if node.PublicConfig != nil {
-		n.PublicConfig = &PubIface{
-			Master: node.PublicConfig.Master,
-			Type:   IfaceType(node.PublicConfig.Type.String()),
-			// Vlan: node.PublicConfig.
-			IPv4: NewIPNetFromSchema(node.PublicConfig.Ipv4),
-			IPv6: NewIPNetFromSchema(node.PublicConfig.Ipv6),
-
-			GW4: node.PublicConfig.Gw4,
-			GW6: node.PublicConfig.Gw6,
-
-			Version: int(node.PublicConfig.Version),
-		}
-	}
-
-	return n
-}
-
 // IPNet type
 type IPNet struct{ net.IPNet }
 
diff --git a/pkg/provision/container.go b/pkg/provision/container.go
index 0327b3da..aff02e1c 100644
--- a/pkg/provision/container.go
+++ b/pkg/provision/container.go
@@ -44,8 +44,6 @@ type Container struct {
 	Mounts []Mount `json:"mounts"`
 	// Network network info for container
 	Network Network `json:"network"`
-	// ContainerCapacity is the amount of resource to allocate to the container
-	Capacity ContainerCapacity `json:"capacity"`
 }
 
 // ContainerResult is the information return to the BCDB
@@ -56,14 +54,6 @@ type ContainerResult struct {
 	IPv4 string `json:"ipv4"`
 }
 
-// ContainerCapacity is the amount of resource to allocate to the container
-type ContainerCapacity struct {
-	// Number of CPU
-	CPU uint `json:"cpu"`
-	// Memory in MiB
-	Memory uint64 `json:"memory"`
-}
-
 func containerProvision(ctx context.Context, reservation *Reservation) (interface{}, error) {
 	return containerProvisionImpl(ctx, reservation)
 }
@@ -180,8 +170,6 @@ func containerProvisionImpl(ctx context.Context, reservation *Reservation) (Cont
 			Mounts:      mounts,
 			Entrypoint:  config.Entrypoint,
 			Interactive: config.Interactive,
-			CPU:         config.Capacity.CPU,
-			Memory:      config.Capacity.Memory * 1024 * 1024,
 		},
 	)
 
diff --git a/pkg/provision/zdb.go b/pkg/provision/zdb.go
index 4563822d..a87cfb30 100644
--- a/pkg/provision/zdb.go
+++ b/pkg/provision/zdb.go
@@ -384,10 +384,7 @@ func zdbDecommission(ctx context.Context, reservation *Reservation) error {
 	if len(c.Mounts) < 1 {
 		return fmt.Errorf("no mountpoint find in 0-db container, cannot reclaim storage")
 	}
-	volume, err := findDataVolume(c.Mounts)
-	if err != nil {
-		return err
-	}
+	volume := filepath.Base(c.Mounts[0].Source)
 
 	if err := storage.Claim(volume, config.Size); err != nil {
 		return errors.Wrapf(err, "failed to reclaim storage on volume %s", volume)
@@ -416,12 +413,3 @@ func isPublic(ip net.IP) bool {
 		ip.IsLinkLocalMulticast() ||
 		ip.IsInterfaceLocalMulticast())
 }
-
-func findDataVolume(mounts []pkg.MountInfo) (string, error) {
-	for _, mount := range mounts {
-		if mount.Target == "/data" {
-			return filepath.Base(mount.Source), nil
-		}
-	}
-	return "", fmt.Errorf("data volume not found")
-}
diff --git a/pkg/upgrade/boot.go b/pkg/upgrade/boot.go
new file mode 100644
index 00000000..8d9e541c
--- /dev/null
+++ b/pkg/upgrade/boot.go
@@ -0,0 +1,116 @@
+package upgrade
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/blang/semver"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+)
+
+const (
+	// those values must match the values
+	// in the bootstrap process. (bootstrap.sh)
+
+	nameFile = "/tmp/flist.name"
+	infoFile = "/tmp/flist.info"
+)
+
+// BootMethod defines the node boot method
+type BootMethod string
+
+const (
+	// BootMethodFList booted from an flist
+	BootMethodFList BootMethod = "flist"
+
+	// BootMethodOther booted with other methods
+	BootMethodOther BootMethod = "other"
+)
+
+//Boot struct
+type Boot struct{}
+
+// DetectBootMethod tries to detect the boot method
+// of the node
+func (b Boot) DetectBootMethod() BootMethod {
+	log.Info().Msg("detecting boot method")
+	_, err := os.Stat(nameFile)
+	if err != nil {
+		log.Warn().Err(err).Msg("no flist file found")
+		return BootMethodOther
+	}
+
+	// NOTE: we can add a check to see if the flist
+	// in the file is valid, but this means we need
+	// to do a call to the hub, hence the detection
+	// can be affected by the network state, or the
+	// hub state. So we return immediately
+	return BootMethodFList
+}
+
+// Name always return name of the boot flist. If name file
+// does not exist, an empty string is returned
+func (b *Boot) Name() string {
+	data, _ := ioutil.ReadFile(nameFile)
+	return strings.TrimSpace(string(data))
+}
+
+// Current returns current flist information
+func (b *Boot) Current() (FListEvent, error) {
+	name := b.Name()
+	if len(name) == 0 {
+		return FListEvent{}, fmt.Errorf("flist name is not known")
+	}
+
+	info, err := loadInfo(name, infoFile)
+	if err != nil {
+		return FListEvent{}, err
+	}
+
+	return FListEvent{info}, nil
+}
+
+// Set updates the stored flist info
+func (b *Boot) Set(c FListEvent) error {
+	return c.Commit(infoFile)
+}
+
+// Version always returns curent version of flist
+func (b *Boot) Version() (semver.Version, error) {
+	info, err := b.Current()
+	if err != nil {
+		return semver.Version{}, errors.Wrap(err, "failed to load flist info")
+	}
+
+	return info.Version()
+}
+
+//MustVersion must returns the current version or panic
+func (b *Boot) MustVersion() semver.Version {
+	ver, err := b.Version()
+	if err != nil {
+		panic(err)
+	}
+
+	return ver
+}
+
+// loadInfo get boot info set by bootstrap process
+func loadInfo(fqn string, path string) (info flistInfo, err error) {
+	info.Repository = filepath.Dir(fqn)
+	f, err := os.Open(path)
+	if err != nil {
+		return info, err
+	}
+
+	defer f.Close()
+	dec := json.NewDecoder(f)
+
+	err = dec.Decode(&info)
+	return info, err
+}
diff --git a/pkg/upgrade/hub.go b/pkg/upgrade/hub.go
index 2e1730d4..7d08901d 100644
--- a/pkg/upgrade/hub.go
+++ b/pkg/upgrade/hub.go
@@ -67,21 +67,6 @@ func (h *hubClient) Info(flist string) (info flistInfo, err error) {
 	return info, err
 }
 
-// loadInfo get boot info set by bootstrap process
-func loadInfo(fqn string, path string) (info flistInfo, err error) {
-	info.Repository = filepath.Dir(fqn)
-	f, err := os.Open(path)
-	if err != nil {
-		return info, err
-	}
-
-	defer f.Close()
-	dec := json.NewDecoder(f)
-
-	err = dec.Decode(&info)
-	return info, err
-}
-
 // flistInfo reflects node boot information (flist + version)
 type flistInfo struct {
 	Name       string `json:"name"`
diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go
index e7f0b6a9..ae3c66de 100644
--- a/pkg/upgrade/upgrade.go
+++ b/pkg/upgrade/upgrade.go
@@ -10,7 +10,6 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/blang/semver"
 	"github.com/pkg/errors"
 
 	"github.com/threefoldtech/zos/pkg/zinit"
@@ -24,12 +23,6 @@ type hookType string
 
 const (
 	provisionModuleName = "provisiond"
-
-	// those values must match the values
-	// in the bootstrap process. (bootstrap.sh)
-
-	nameFile = "/tmp/flist.name"
-	infoFile = "/tmp/flist.info"
 )
 
 var (
@@ -40,35 +33,6 @@ var (
 	protected = []string{"identityd", "redis"}
 )
 
-// BootMethod defines the node boot method
-type BootMethod string
-
-const (
-	// BootMethodFList booted from an flist
-	BootMethodFList BootMethod = "flist"
-
-	// BootMethodOther booted with other methods
-	BootMethodOther BootMethod = "other"
-)
-
-// DetectBootMethod tries to detect the boot method
-// of the node
-func DetectBootMethod() BootMethod {
-	log.Info().Msg("detecting boot method")
-	_, err := os.Stat(nameFile)
-	if err != nil {
-		log.Warn().Err(err).Msg("no flist file found")
-		return BootMethodOther
-	}
-
-	// NOTE: we can add a check to see if the flist
-	// in the file is valid, but this means we need
-	// to do a call to the hub, hence the detection
-	// can be affected by the network state, or the
-	// hub state. So we return immediately
-	return BootMethodFList
-}
-
 // Upgrader is the component that is responsible
 // to keep 0-OS up to date
 type Upgrader struct {
@@ -78,59 +42,14 @@ type Upgrader struct {
 	hub          hubClient
 }
 
-// Name always return name of the boot flist. If name file
-// does not exist, an empty string is returned
-func (u *Upgrader) Name() string {
-	data, _ := ioutil.ReadFile(nameFile)
-	return strings.TrimSpace(string(data))
-}
-
-// currentInfo returns current flist information
-func (u *Upgrader) currentInfo() (flistInfo, error) {
-	name := u.Name()
-	if len(name) == 0 {
-		return flistInfo{}, fmt.Errorf("flist name is not known")
-	}
-
-	return loadInfo(name, infoFile)
-}
-
-// Version always returns curent version of flist
-func (u *Upgrader) Version() (semver.Version, error) {
-	info, err := u.currentInfo()
-	if err != nil {
-		return semver.Version{}, errors.Wrap(err, "failed to load flist info")
-	}
-
-	return info.Version()
-}
-
 // Upgrade is the method that does a full upgrade flow
 // first check if a new version is available
 // if yes, applies the upgrade
 // on a successfully update, upgrade WILL NOT RETURN
 // instead the upgraded daemon will be completely stopped
-func (u *Upgrader) Upgrade() error {
-	info, err := u.hub.Info(u.Name())
-	if err != nil {
-		return errors.Wrap(err, "failed to get remote flist info")
-	}
+func (u *Upgrader) Upgrade(from, to FListEvent) error {
+	return u.applyUpgrade(from, to)
 
-	current, err := u.Version()
-	if err != nil {
-		log.Error().Err(err).Msg("failed to detect current version. Update to latest anyway")
-	}
-
-	latest, err := info.Version()
-	if err != nil {
-		return errors.Wrap(err, "failed to parse latest version")
-	}
-
-	if latest.GT(current) {
-		return u.applyUpgrade(latest, info)
-	}
-
-	return nil
 }
 
 func (u Upgrader) stopMultiple(timeout time.Duration, service ...string) error {
@@ -238,11 +157,8 @@ func (u *Upgrader) upgradeSelf(root string) error {
 	return ErrRestartNeeded
 }
 
-func (u *Upgrader) uninstall() error {
-	current, err := u.currentInfo()
-	if err != nil {
-		return errors.Wrap(err, "failed to get current flist info")
-	}
+func (u *Upgrader) uninstall(from FListEvent) error {
+	current := from
 
 	files, err := current.Files()
 	if err != nil {
@@ -305,10 +221,10 @@ func (u *Upgrader) uninstall() error {
 	return nil
 }
 
-func (u *Upgrader) applyUpgrade(version semver.Version, info flistInfo) error {
-	log.Info().Str("flist", u.Name()).Str("version", version.String()).Msg("start applying upgrade")
+func (u *Upgrader) applyUpgrade(from, to FListEvent) error {
+	log.Info().Str("flist", to.Fqdn()).Str("version", to.TryVersion().String()).Msg("start applying upgrade")
 
-	flistRoot, err := u.FLister.Mount(u.hub.MountURL(u.Name()), u.hub.StorageURL(), pkg.ReadOnlyMountOptions)
+	flistRoot, err := u.FLister.Mount(u.hub.MountURL(to.Fqdn()), u.hub.StorageURL(), pkg.ReadOnlyMountOptions)
 	if err != nil {
 		return err
 	}
@@ -323,7 +239,7 @@ func (u *Upgrader) applyUpgrade(version semver.Version, info flistInfo) error {
 		return err
 	}
 
-	if err := u.uninstall(); err != nil {
+	if err := u.uninstall(from); err != nil {
 		log.Error().Err(err).Msg("failed to unistall current flist. Upgraded anyway")
 	}
 
@@ -366,10 +282,6 @@ func (u *Upgrader) applyUpgrade(version semver.Version, info flistInfo) error {
 		}
 	}
 
-	if err := info.Commit(infoFile); err != nil {
-		return err
-	}
-
 	return nil
 }
 
diff --git a/pkg/upgrade/watcher.go b/pkg/upgrade/watcher.go
new file mode 100644
index 00000000..b8463aec
--- /dev/null
+++ b/pkg/upgrade/watcher.go
@@ -0,0 +1,136 @@
+package upgrade
+
+import "time"
+
+import "github.com/blang/semver"
+
+import "context"
+
+import "github.com/rs/zerolog/log"
+
+import "path"
+
+/**
+Watcher unifies the upgrade pipeline by making sure we can watch
+different types of required updates, while always make sure only
+one kind of update is applied at a time.
+
+This to prevent updates to step on each other toes.
+*/
+
+//EventType of the watcher
+type EventType string
+
+const (
+	//FList event type
+	FList EventType = "flist"
+	//Repo event type
+	Repo EventType = "repo"
+)
+
+//Event interface
+type Event interface {
+	EventType() EventType
+}
+
+//Watcher interface
+type Watcher interface {
+	Watch(ctx context.Context) (<-chan Event, error)
+}
+
+//FListEvent struct
+type FListEvent struct {
+	flistInfo
+}
+
+//EventType of the event
+func (f *FListEvent) EventType() EventType {
+	return FList
+}
+
+//Fqdn return full name of the flist (with repo)
+func (f *FListEvent) Fqdn() string {
+	return path.Join(f.Repository, f.Name)
+}
+
+//TryVersion will try to parse the version from flist
+//other wise return empty version 0.0.0
+func (f *FListEvent) TryVersion() semver.Version {
+	version, _ := f.Version()
+	return version
+}
+
+//FListSemverWatcher watches a single FList for changes in it's semver
+//the semver to change without the flist name itself changes, means
+//that this flist is mostly a symlink
+type FListSemverWatcher struct {
+	FList    string
+	Duration time.Duration
+	Current  semver.Version
+
+	client hubClient
+}
+
+// Watch an flist change in version
+func (w *FListSemverWatcher) Watch(ctx context.Context) (<-chan Event, error) {
+
+	if w.Duration == time.Duration(0) {
+		//default delay of 5min
+		w.Duration = 600 * time.Second
+	}
+
+	info, err := w.client.Info(w.FList)
+	if err != nil {
+		return nil, err
+	}
+
+	version, err := info.Version()
+	if err != nil {
+		return nil, err
+	}
+
+	ch := make(chan Event, 1)
+
+	if version.GT(w.Current) {
+		ch <- &FListEvent{
+			flistInfo: info,
+		}
+		w.Current = version
+	}
+
+	ticker := time.NewTicker(w.Duration)
+
+	go func() {
+		defer ticker.Stop()
+		defer close(ch)
+
+		for {
+			// wait for next tick
+			select {
+			case <-ticker.C:
+			case <-ctx.Done():
+				return
+			}
+
+			info, err := w.client.Info(w.FList)
+			if err != nil {
+				log.Error().Err(err).Str("flist", w.FList).Msg("failed to get flist info")
+				continue
+			}
+			version, err := info.Version()
+			if err != nil {
+				log.Error().Err(err).Str("flist", w.FList).Msg("failed to get flist version")
+				continue
+			}
+
+			if version.GT(w.Current) {
+				ch <- &FListEvent{
+					flistInfo: info,
+				}
+				w.Current = version
+			}
+		}
+	}()
+
+	return ch, nil
+}
diff --git a/tools/bcdb_mock/reservation_handlers.go b/tools/bcdb_mock/reservation_handlers.go
index a2d5af44..0a6f34f6 100644
--- a/tools/bcdb_mock/reservation_handlers.go
+++ b/tools/bcdb_mock/reservation_handlers.go
@@ -89,14 +89,7 @@ func (s *reservationsStore) get(w http.ResponseWriter, r *http.Request) {
 	w.Header().Add("content-type", "application/json")
 	w.WriteHeader(http.StatusOK)
 
-	x := struct {
-		*provision.Reservation
-		Result *provision.Result `json:"result"`
-	}{
-		Reservation: reservation.Reservation,
-		Result:      reservation.Result}
-
-	if err := json.NewEncoder(w).Encode(x); err != nil {
+	if err := json.NewEncoder(w).Encode(reservation.Reservation); err != nil {
 		log.Printf("error during json encoding of reservation: %v", err)
 	}
 }
diff --git a/tools/tfuser/cmds_container.go b/tools/tfuser/cmds_container.go
index f4feb4f8..81652351 100644
--- a/tools/tfuser/cmds_container.go
+++ b/tools/tfuser/cmds_container.go
@@ -23,11 +23,6 @@ func generateContainer(c *cli.Context) error {
 		return err
 	}
 
-	cap := provision.ContainerCapacity{
-		CPU:    c.Uint("cpu"),
-		Memory: c.Uint64("memory"),
-	}
-
 	container := provision.Container{
 		FList:        c.String("flist"),
 		FlistStorage: c.String("storage"),
@@ -41,7 +36,6 @@ func generateContainer(c *cli.Context) error {
 				net.ParseIP(c.String("ip")),
 			},
 		},
-		Capacity: cap,
 	}
 
 	if err := validateContainer(container); err != nil {
diff --git a/tools/tfuser/main.go b/tools/tfuser/main.go
index 889c9932..d1e85076 100644
--- a/tools/tfuser/main.go
+++ b/tools/tfuser/main.go
@@ -170,14 +170,6 @@ func main() {
 							Name:  "ip",
 							Usage: "ip address to assign to the container",
 						},
-						cli.UintFlag{
-							Name:  "cpu",
-							Usage: "limit the amount of CPU allocated to the container",
-						},
-						cli.Uint64Flag{
-							Name:  "memory",
-							Usage: "limit the amount of memory a container can allocate",
-						},
 					},
 					Action: generateContainer,
 				},


Test patch:
diff --git a/pkg/container/opts_test.go b/pkg/container/opts_test.go
deleted file mode 100644
index 1e272423..00000000
--- a/pkg/container/opts_test.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package container
-
-import "testing"
-
-func Test_cruToLimit(t *testing.T) {
-	type args struct {
-		cru      uint
-		totalCPU int
-	}
-	tests := []struct {
-		name       string
-		args       args
-		wantQuota  int64
-		wantPeriod uint64
-	}{
-		{
-			name: "1-1",
-			args: args{
-				cru:      1,
-				totalCPU: 1,
-			},
-			wantPeriod: 1000000,
-			wantQuota:  1000000,
-		},
-		{
-			name: "1-4",
-			args: args{
-				cru:      1,
-				totalCPU: 4,
-			},
-			wantPeriod: 250000,
-			wantQuota:  1000000,
-		},
-		{
-			name: "1-6",
-			args: args{
-				cru:      1,
-				totalCPU: 6,
-			},
-			wantPeriod: 166667,
-			wantQuota:  1000000,
-		},
-		{
-			name: "2-4",
-			args: args{
-				cru:      2,
-				totalCPU: 4,
-			},
-			wantPeriod: 500000,
-			wantQuota:  1000000,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			gotQuota, gotPeriod := cruToLimit(tt.args.cru, tt.args.totalCPU)
-			if gotQuota != tt.wantQuota {
-				t.Errorf("cruToLimit() gotQuota = %v, want %v", gotQuota, tt.wantQuota)
-			}
-			if gotPeriod != tt.wantPeriod {
-				t.Errorf("cruToLimit() gotPeriod = %v, want %v", gotPeriod, tt.wantPeriod)
-			}
-		})
-	}
-}
diff --git a/pkg/network/types/types_test.go b/pkg/network/types/types_test.go
index ce482c96..d3332802 100644
--- a/pkg/network/types/types_test.go
+++ b/pkg/network/types/types_test.go
@@ -6,8 +6,6 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/threefoldtech/zos/pkg/gedis/types/directory"
-	"github.com/threefoldtech/zos/pkg/schema"
 )
 
 func TestParseIPNet(t *testing.T) {
@@ -85,155 +83,3 @@ func TestDumpIPNet(t *testing.T) {
 		})
 	}
 }
-
-func TestNewNodeFromSchema(t *testing.T) {
-	type args struct {
-		node directory.TfgridNode2
-	}
-	tests := []struct {
-		name string
-		args args
-		want *Node
-	}{
-		{
-			name: "full",
-			args: args{
-				node: directory.TfgridNode2{
-					NodeID: "node_id",
-					FarmID: 1,
-					Ifaces: []directory.TfgridNodeIface1{
-						{
-							Name: "eth0",
-							Addrs: []schema.IPRange{
-								schema.MustParseIPRange("192.168.0.10/24"),
-							},
-							Gateway: []net.IP{
-								net.ParseIP("192.168.0.1"),
-							},
-						},
-					},
-					PublicConfig: &directory.TfgridNodePublicIface1{
-						Master: "eth1",
-						Type:   directory.TfgridNodePublicIface1TypeMacvlan,
-						Ipv4:   schema.MustParseIPRange("185.69.166.245/24"),
-						Gw4:    net.ParseIP("185.69.166.1"),
-						Ipv6:   schema.MustParseIPRange("2a02:1802:5e:0:1000:0:ff:1/64"),
-						Gw6:    net.ParseIP("2a02:1802:5e::1"),
-					},
-					WGPorts: []uint{1, 2, 3},
-				},
-			},
-			want: &Node{
-				NodeID: "node_id",
-				FarmID: 1,
-				Ifaces: []*IfaceInfo{
-					{
-						Name: "eth0",
-						Addrs: []IPNet{
-							{
-								net.IPNet{
-									IP:   net.ParseIP("192.168.0.10"),
-									Mask: net.CIDRMask(24, 32),
-								},
-							},
-						},
-						Gateway: []net.IP{
-							net.ParseIP("192.168.0.1"),
-						},
-					},
-				},
-				PublicConfig: &PubIface{
-					Master: "eth1",
-					Type:   MacVlanIface,
-					IPv4:   MustParseIPNet("185.69.166.245/24"),
-					GW4:    net.ParseIP("185.69.166.1"),
-					IPv6:   MustParseIPNet("2a02:1802:5e:0:1000:0:ff:1/64"),
-					GW6:    net.ParseIP("2a02:1802:5e::1"),
-				},
-				WGPorts: []uint{1, 2, 3},
-			},
-		},
-		{
-			name: "no-public",
-			args: args{
-				node: directory.TfgridNode2{
-					NodeID: "node_id",
-					FarmID: 1,
-					Ifaces: []directory.TfgridNodeIface1{
-						{
-							Name: "eth0",
-							Addrs: []schema.IPRange{
-								schema.MustParseIPRange("192.168.0.10/24"),
-							},
-							Gateway: []net.IP{
-								net.ParseIP("192.168.0.1"),
-							},
-						},
-					},
-					PublicConfig: nil,
-					WGPorts:      []uint{1, 2, 3},
-				},
-			},
-			want: &Node{
-				NodeID: "node_id",
-				FarmID: 1,
-				Ifaces: []*IfaceInfo{
-					{
-						Name: "eth0",
-						Addrs: []IPNet{
-							{
-								net.IPNet{
-									IP:   net.ParseIP("192.168.0.10"),
-									Mask: net.CIDRMask(24, 32),
-								},
-							},
-						},
-						Gateway: []net.IP{
-							net.ParseIP("192.168.0.1"),
-						},
-					},
-				},
-				PublicConfig: nil,
-				WGPorts:      []uint{1, 2, 3},
-			},
-		},
-		{
-			name: "empty-ifaces",
-			args: args{
-				node: directory.TfgridNode2{
-					NodeID: "node_id",
-					FarmID: 1,
-					Ifaces: []directory.TfgridNodeIface1{},
-					PublicConfig: &directory.TfgridNodePublicIface1{
-						Master: "eth1",
-						Type:   directory.TfgridNodePublicIface1TypeMacvlan,
-						Ipv4:   schema.MustParseIPRange("185.69.166.245/24"),
-						Gw4:    net.ParseIP("185.69.166.1"),
-						Ipv6:   schema.MustParseIPRange("2a02:1802:5e:0:1000:0:ff:1/64"),
-						Gw6:    net.ParseIP("2a02:1802:5e::1"),
-					},
-					WGPorts: []uint{1, 2, 3},
-				},
-			},
-			want: &Node{
-				NodeID: "node_id",
-				FarmID: 1,
-				Ifaces: []*IfaceInfo{},
-				PublicConfig: &PubIface{
-					Master: "eth1",
-					Type:   MacVlanIface,
-					IPv4:   MustParseIPNet("185.69.166.245/24"),
-					GW4:    net.ParseIP("185.69.166.1"),
-					IPv6:   MustParseIPNet("2a02:1802:5e:0:1000:0:ff:1/64"),
-					GW6:    net.ParseIP("2a02:1802:5e::1"),
-				},
-				WGPorts: []uint{1, 2, 3},
-			},
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			assert.Equal(t, tt.want, NewNodeFromSchema(tt.args.node))
-		})
-	}
-}
diff --git a/pkg/provision/zdb_test.go b/pkg/provision/zdb_test.go
index 4830ab40..20e9e745 100644
--- a/pkg/provision/zdb_test.go
+++ b/pkg/provision/zdb_test.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/threefoldtech/zbus"
 	"github.com/threefoldtech/zos/pkg"
@@ -210,64 +209,3 @@ func TestZDBProvisionNoMappingContainerDoesNotExists(t *testing.T) {
 	require.Error(err)
 	require.True(strings.Contains(err.Error(), "/var/run/zdb_container-id"))
 }
-
-func Test_findDataVolume(t *testing.T) {
-	type args struct {
-		mounts []pkg.MountInfo
-	}
-	tests := []struct {
-		name    string
-		args    args
-		want    string
-		wantErr bool
-	}{
-		{
-			name: "exists",
-			args: args{
-				mounts: []pkg.MountInfo{
-					{
-						Target: "/data",
-						Source: "/mnt/sda/subovl1",
-					},
-					{
-						Target: "/etc/resolv.conf",
-						Source: "/etc/resolv.conf",
-					},
-				},
-			},
-			want:    "subovl1",
-			wantErr: false,
-		},
-		{
-			name: "non-exists",
-			args: args{
-				mounts: []pkg.MountInfo{
-					{
-						Target: "/etc/resolv.conf",
-						Source: "/etc/resolv.conf",
-					},
-				},
-			},
-			want:    "",
-			wantErr: true,
-		},
-		{
-			name: "empty",
-			args: args{
-				mounts: []pkg.MountInfo{},
-			},
-			want:    "",
-			wantErr: true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := findDataVolume(tt.args.mounts)
-			if tt.wantErr {
-				require.Error(t, err)
-			} else {
-				assert.Equal(t, tt.want, got)
-			}
-		})
-	}
-}
diff --git a/pkg/upgrade/watcher_test.go b/pkg/upgrade/watcher_test.go
new file mode 100644
index 00000000..9668bd88
--- /dev/null
+++ b/pkg/upgrade/watcher_test.go
@@ -0,0 +1,29 @@
+package upgrade
+
+import "testing"
+
+import "context"
+
+import "github.com/stretchr/testify/require"
+
+func TestFListWatcher(t *testing.T) {
+	require := require.New(t)
+
+	watcher := FListSemverWatcher{
+		FList: "tf-zos/zos:development:latest.flist",
+	}
+
+	ctx := context.Background()
+
+	ch, err := watcher.Watch(ctx)
+	require.NoError(err)
+
+	event := <-ch
+	require.Equal(FList, event.EventType())
+
+	info, ok := event.(*FListEvent)
+	require.True(ok)
+
+	require.Equal("zos:development:latest.flist", info.Name)
+	require.Equal("symlink", info.Type)
+}