Repository: threefoldtech/zos
Pull request #1261 (2021-04-29 11:34:06)

Generic vms
Changes:

- Add generic VMs workload
- Add validation for ssh keys and plain secrets in k8s (if the plain secret contains white spaces or new lines, part of it is ignored or it might crash the VM)
- Pull out some of the common code of vms and k8s in vmutils

Notes:

Not tested on public ips yet.

Related issues:
https://github.com/threefoldtech/zos/issues/1189

muhamadazmy: Can we also implement this for version `0.5.0` things are much cleaner on this version. will be easier to implement. this version is available on branch `master-3`. i can explain the differences

Patch:
diff --git a/docs/vms/readme.md b/docs/vms/readme.md
new file mode 100644
index 00000000..9b76fac2
--- /dev/null
+++ b/docs/vms/readme.md
@@ -0,0 +1,33 @@
+# Generic Virtual Machines
+
+A generic VM consists of three files:
+- image.raw: raw disk image
+- kernel: The kernel executable file
+- initrd (optional)
+
+## Image file
+
+The raw disk image contains the filesystem in the btrfs format of the VM.
+
+Example of constructing a raw image:
+```
+wget http://cdimage.ubuntu.com/ubuntu-base/releases/20.04.1/release/ubuntu-base-20.04.1-base-amd64.tar.gz
+truncate -s 2G image.raw    # Create a blank raw disk
+mkfs -t btrfs image.raw     # Create a btrfs filesystem on it
+mkdir mnt
+mount -o loop image.raw mnt # mount it on the ./mnt directory
+cd mnt && tar zxvf ../ubuntu-base-20.04.1-base-amd64.tar.gz # Extract the ubuntu base image in the fs
+btrfs filesystem resize 1G mnt # Resize the filesystem to avoid non necessary large image.raw
+umount mnt                     # Unmount it
+truncate -s 1G image.raw       # Resize the disk
+```
+
+The image must have the `ip` utility installed to configure the network. It should also contain an ssh server enabled after boot to allow user access.
+
+## Initrd
+
+When provided. The initrd receives in its cmdline a list string of whitespace separated key-value entries. It can be used to create the authorized keys and configure the network interfaces. The ssh entries is in the format "ssh=key". The whitespaces in the ssh key is replaced with ",", so something like `$(echo "$SSH" | sed 's/,/ /g')` should be done before appending it to the authorized keys. The network parameters are processed using the [setupnetwork](https://raw.githubusercontent.com/threefoldtech/k3os/zos-patch/overlay/sbin/setupnetwork) script.
+
+## Example
+
+[Ubuntu focal](https://hub.grid.tf/omar0.3bot/omarelawady-zos-ubuntu-vm-latest.flist.md)
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 92b72b07..59486b66 100644
--- a/go.mod
+++ b/go.mod
@@ -34,7 +34,7 @@ require (
 	github.com/stretchr/testify v1.6.1
 	github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae
 	github.com/threefoldtech/0-fs v1.3.1-0.20201203163303-d963de9adea7
-	github.com/threefoldtech/tfexplorer v0.5.1-0.20210323101825-40cfe4c04c1d
+	github.com/threefoldtech/tfexplorer v0.5.1-0.20210506133851-4f801662813b
 	github.com/threefoldtech/zbus v0.1.4
 	github.com/tyler-smith/go-bip39 v1.0.2
 	github.com/urfave/cli v1.22.5
diff --git a/go.sum b/go.sum
index 22efced1..2cbccf04 100644
--- a/go.sum
+++ b/go.sum
@@ -631,6 +631,8 @@ github.com/threefoldtech/tfexplorer v0.4.10-0.20210222093825-2231bac31586 h1:f1Z
 github.com/threefoldtech/tfexplorer v0.4.10-0.20210222093825-2231bac31586/go.mod h1:Y/AUZYNp40rljkNlZiQbu54JL4DU3xTfDhGYoftxDyQ=
 github.com/threefoldtech/tfexplorer v0.5.1-0.20210323101825-40cfe4c04c1d h1:RCGd0uBLH7LQtUNOWvNvcNTuuBpQV+npxdhaDOLaiBk=
 github.com/threefoldtech/tfexplorer v0.5.1-0.20210323101825-40cfe4c04c1d/go.mod h1:Y/AUZYNp40rljkNlZiQbu54JL4DU3xTfDhGYoftxDyQ=
+github.com/threefoldtech/tfexplorer v0.5.1-0.20210506133851-4f801662813b h1:GHbEr/z5bHjJpjpYlpQyjeR0ORjaVX4bIFQvsS73kD8=
+github.com/threefoldtech/tfexplorer v0.5.1-0.20210506133851-4f801662813b/go.mod h1:IanOSCxRaiRi1RdUqhBEX0ZpKwQ03M9IG1nnNiDyF60=
 github.com/threefoldtech/zbus v0.1.3 h1:18DnIzximRbATle5ZdZz0i84n/bCYB8k/gkhr2dXayc=
 github.com/threefoldtech/zbus v0.1.3/go.mod h1:ZtiRpcqzEBJetVQDsEbw0p48h/AF3O1kf0tvd30I0BU=
 github.com/threefoldtech/zbus v0.1.4 h1:c8lF4H9HMDJGknhcEK2p+zJWWM09fByGe3uL9m/iKgM=
@@ -698,6 +700,8 @@ github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZ
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/zaibon/httpsig v0.0.0-20200401133919-ea9cb57b0946 h1:s12kADbuUVEJkYxqUojKvFR5JUpib9PCzARL1YR2ZDU=
 github.com/zaibon/httpsig v0.0.0-20200401133919-ea9cb57b0946/go.mod h1:T2e9wAAQHKLB08ONx+0xxxrlAfUnRbwLOpO0WodNbQ4=
+github.com/zaibon/httpsig v0.0.0-20210219100301-931cc471f406 h1:tuqYxrhWKt6WrZCZCLomXbtVc+hZlu4+PrUzEukF96s=
+github.com/zaibon/httpsig v0.0.0-20210219100301-931cc471f406/go.mod h1:T2e9wAAQHKLB08ONx+0xxxrlAfUnRbwLOpO0WodNbQ4=
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
 go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
diff --git a/pkg/environment/environment.go b/pkg/environment/environment.go
index 24e06d71..226da83f 100644
--- a/pkg/environment/environment.go
+++ b/pkg/environment/environment.go
@@ -57,7 +57,7 @@ const (
 var (
 	envDev = Environment{
 		RunningMode: RunningDev,
-		BcdbURL:     "https://explorer.devnet.grid.tf/explorer",
+		BcdbURL:     "http://4.tcp.ngrok.io:19441/explorer",
 		FlistURL:    "zdb://hub.grid.tf:9900",
 		BinRepo:     "tf-zos-bins.dev",
 	}
diff --git a/pkg/provision/engine.go b/pkg/provision/engine.go
index 83b51ab7..53c663ca 100644
--- a/pkg/provision/engine.go
+++ b/pkg/provision/engine.go
@@ -549,6 +549,7 @@ func (e *Engine) updateStats() error {
 		Uint16("zDBNamespace", wl.ZDBNamespace).
 		Uint16("container", wl.Container).
 		Uint16("k8sVM", wl.K8sVM).
+		Uint16("gVM", wl.GenericVM).
 		Uint16("proxy", wl.Proxy).
 		Uint16("reverseProxy", wl.ReverseProxy).
 		Uint16("subdomain", wl.Subdomain).
diff --git a/pkg/provision/primitives/converter.go b/pkg/provision/primitives/converter.go
index 362cdc6b..4cc58872 100644
--- a/pkg/provision/primitives/converter.go
+++ b/pkg/provision/primitives/converter.go
@@ -189,18 +189,20 @@ func K8SToProvisionType(w workloads.Workloader) (Kubernetes, string, error) {
 	}
 
 	k8s := Kubernetes{
-		Size: k.Size,
-		Custom: KubernetesCustomSize{
-			CRU: k.CustomSize.CRU,
-			MRU: k.CustomSize.MRU,
-			SRU: k.CustomSize.SRU,
+		VM: VM{
+			Size: k.Size,
+			Custom: VMCustomSize{
+				CRU: k.CustomSize.CRU,
+				MRU: k.CustomSize.MRU,
+				SRU: k.CustomSize.SRU,
+			},
+			IP:        k.Ipaddress,
+			SSHKeys:   k.SshKeys,
+			PublicIP:  k.PublicIP,
+			NetworkID: pkg.NetID(k.NetworkId),
 		},
-		NetworkID:             pkg.NetID(k.NetworkId),
-		IP:                    k.Ipaddress,
 		ClusterSecret:         k.ClusterSecret,
 		MasterIPs:             k.MasterIps,
-		SSHKeys:               k.SshKeys,
-		PublicIP:              k.PublicIP,
 		DatastoreEndpoint:     k.DatastoreEndpoint,
 		DisableDefaultIngress: k.DisableDefaultIngress,
 	}
@@ -208,6 +210,25 @@ func K8SToProvisionType(w workloads.Workloader) (Kubernetes, string, error) {
 	return k8s, k.NodeId, nil
 }
 
+// VMToProvisionType converts type to internal provision type
+func VMToProvisionType(w workloads.Workloader) (VM, string, error) {
+	k, ok := w.(*workloads.VirtualMachine)
+	if !ok {
+		return VM{}, "", fmt.Errorf("failed to convert kubernetes workload, wrong format")
+	}
+
+	vm := VM{
+		Size:      k.Size,
+		NetworkID: pkg.NetID(k.NetworkId),
+		IP:        k.Ipaddress,
+		Name:      k.Name,
+		SSHKeys:   k.SshKeys,
+		PublicIP:  k.PublicIP,
+	}
+
+	return vm, k.NodeId, nil
+}
+
 // PublicIPToProvisionType converts type to internal provision type
 func PublicIPToProvisionType(w workloads.Workloader) (PublicIP, string, error) {
 	p, ok := w.(*workloads.PublicIP)
@@ -321,6 +342,11 @@ func WorkloadToProvisionType(w workloads.Workloader) (*provision.Reservation, er
 		if err != nil {
 			return nil, err
 		}
+	case workloads.WorkloadTypeVirtualMachine:
+		data, reservation.NodeID, err = VMToProvisionType(w)
+		if err != nil {
+			return nil, err
+		}
 	default:
 		log.Error().Str("type", w.GetWorkloadType().String()).Msg("unsupported reservation type")
 		return reservation, nil
@@ -351,6 +377,8 @@ func ResultToSchemaType(r provision.Result) (*workloads.Result, error) {
 		rType = workloads.WorkloadTypeKubernetes
 	case PublicIPReservation:
 		rType = workloads.WorkloadTypePublicIP
+	case VirtualMachineReservation:
+		rType = workloads.WorkloadTypeVirtualMachine
 	default:
 		return nil, fmt.Errorf("unknown reservation type: %s", r.Type)
 	}
diff --git a/pkg/provision/primitives/counters.go b/pkg/provision/primitives/counters.go
index fcf1054c..85d0dda9 100644
--- a/pkg/provision/primitives/counters.go
+++ b/pkg/provision/primitives/counters.go
@@ -61,6 +61,7 @@ type Counters struct {
 	volumes    CounterUint64
 	networks   CounterUint64
 	zdbs       CounterUint64
+	k8s        CounterUint64
 	vms        CounterUint64
 
 	SRU CounterUint64 // SSD storage in bytes
@@ -76,7 +77,8 @@ func (c *Counters) CurrentWorkloads() directory.WorkloadAmount {
 		Volume:       uint16(c.volumes.Current()),
 		ZDBNamespace: uint16(c.zdbs.Current()),
 		Container:    uint16(c.containers.Current()),
-		K8sVM:        uint16(c.vms.Current()),
+		K8sVM:        uint16(c.k8s.Current()),
+		GenericVM:    uint16(c.vms.Current()),
 	}
 }
 
@@ -116,8 +118,11 @@ func (c *Counters) Increment(r *provision.Reservation) error {
 		c.zdbs.Increment(1)
 		u, err = processZdb(r)
 	case KubernetesReservation:
-		c.vms.Increment(1)
+		c.k8s.Increment(1)
 		u, err = processKubernetes(r)
+	case VirtualMachineReservation:
+		c.vms.Increment(1)
+		u, err = processVM(r)
 	case NetworkReservation, NetworkResourceReservation:
 		c.networks.Increment(1)
 		u = resourceUnits{}
@@ -157,8 +162,11 @@ func (c *Counters) Decrement(r *provision.Reservation) error {
 		c.zdbs.Decrement(1)
 		u, err = processZdb(r)
 	case KubernetesReservation:
-		c.vms.Decrement(1)
+		c.k8s.Decrement(1)
 		u, err = processKubernetes(r)
+	case VirtualMachineReservation:
+		c.vms.Decrement(1)
+		u, err = processVM(r)
 	case NetworkReservation, NetworkResourceReservation:
 		c.networks.Decrement(1)
 		u = resourceUnits{}
@@ -204,6 +212,12 @@ func (c *Counters) CheckMemoryRequirements(r *provision.Reservation, usableMemor
 		if err != nil {
 			return err
 		}
+
+	case VirtualMachineReservation:
+		requestedUnits, err = processVM(r)
+		if err != nil {
+			return err
+		}
 	}
 
 	if requestedUnits.MRU != 0 {
@@ -274,13 +288,24 @@ func processKubernetes(r *provision.Reservation) (u resourceUnits, err error) {
 	if err = json.Unmarshal(r.Data, &k8s); err != nil {
 		return u, err
 	}
+	return getResourceRequirement(k8s.VM), nil
+}
 
+func processVM(r *provision.Reservation) (u resourceUnits, err error) {
+	var vm VM
+	if err = json.Unmarshal(r.Data, &vm); err != nil {
+		return u, err
+	}
+	return getResourceRequirement(vm), nil
+}
+
+func getResourceRequirement(vm VM) (u resourceUnits) {
 	// size are defined at https://github.com/threefoldtech/zos/blob/master/pkg/provision/kubernetes.go#L311
-	switch k8s.Size {
+	switch vm.Size {
 	case -1:
-		u.CRU = uint64(k8s.Custom.CRU)
-		u.MRU = uint64(k8s.Custom.MRU) * gib
-		u.SRU = uint64(k8s.Custom.SRU) * gib
+		u.CRU = uint64(vm.Custom.CRU)
+		u.MRU = uint64(vm.Custom.MRU) * gib
+		u.SRU = uint64(vm.Custom.SRU) * gib
 	case 1:
 		u.CRU = 1
 		u.MRU = 2 * gib
@@ -361,5 +386,6 @@ func processKubernetes(r *provision.Reservation) (u resourceUnits, err error) {
 		extra = gib
 	}
 	u.MRU += extra
-	return u, nil
+
+	return u
 }
diff --git a/pkg/provision/primitives/kubernetes.go b/pkg/provision/primitives/kubernetes.go
index d1003f60..3bbace75 100644
--- a/pkg/provision/primitives/kubernetes.go
+++ b/pkg/provision/primitives/kubernetes.go
@@ -5,15 +5,12 @@ import (
 	"encoding/json"
 	"fmt"
 	"net"
+	"strings"
 	"time"
 
 	"github.com/pkg/errors"
 	"github.com/rs/zerolog/log"
-	"github.com/threefoldtech/tfexplorer/models/generated/workloads"
-	"github.com/threefoldtech/tfexplorer/schema"
 	"github.com/threefoldtech/zos/pkg"
-	"github.com/threefoldtech/zos/pkg/app"
-	"github.com/threefoldtech/zos/pkg/network/ifaceutil"
 	"github.com/threefoldtech/zos/pkg/provision"
 	"github.com/threefoldtech/zos/pkg/stubs"
 )
@@ -33,30 +30,13 @@ type KubernetesCustomSize struct {
 
 // Kubernetes reservation data
 type Kubernetes struct {
-	// Size of the vm, this defines the amount of vCpu, memory, and the disk size
-	// Docs: docs/kubernetes/sizes.md
-	Size int64 `json:"size"`
-
-	Custom KubernetesCustomSize `json:"custom_size"`
-	// NetworkID of the network namepsace in which to run the VM. The network
-	// must be provisioned previously.
-	NetworkID pkg.NetID `json:"network_id"`
-	// IP of the VM. The IP must be part of the subnet available in the network
-	// resource defined by the networkID on this node
-	IP net.IP `json:"ip"`
+	VM `json:",inline"`
 
 	// ClusterSecret is the hex encoded encrypted cluster secret.
 	ClusterSecret string `json:"cluster_secret"`
 	// MasterIPs define the URL's for the kubernetes master nodes. If this
 	// list is empty, this node is considered to be a master node.
 	MasterIPs []net.IP `json:"master_ips"`
-	// SSHKeys is a list of ssh keys to add to the VM. Keys can be either
-	// a full ssh key, or in the form of `github:${username}`. In case of
-	// the later, the VM will retrieve the github keys for this username
-	// when it boots.
-	SSHKeys []string `json:"ssh_keys"`
-	// PublicIP points to a reservation for a public ip
-	PublicIP schema.ID `json:"public_ip"`
 
 	PlainClusterSecret string `json:"-"`
 
@@ -98,6 +78,15 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 		return result, errors.Wrap(err, "failed to decode reservation schema")
 	}
 
+	config.PlainClusterSecret, err = decryptSecret(config.ClusterSecret, reservation.User, reservation.Version, p.zbus)
+
+	if err != nil {
+		return result, errors.Wrap(err, "failed to decrypt namespace password")
+	}
+
+	if err = config.Validate(); err != nil {
+		return result, err
+	}
 	netID := provision.NetworkID(reservation.User, string(config.NetworkID))
 
 	// check if the network tap already exists
@@ -109,7 +98,7 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 	}
 
 	if exists {
-		return result, errors.New("kubernetes vm with same network already exists")
+		return result, errors.New("another vm with same network already exists")
 	}
 
 	// check if public ipv4 is supported, should this be requested
@@ -120,12 +109,7 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 	result.ID = reservation.ID
 	result.IP = config.IP.String()
 
-	config.PlainClusterSecret, err = decryptSecret(config.ClusterSecret, reservation.User, reservation.Version, p.zbus)
-	if err != nil {
-		return result, errors.Wrap(err, "failed to decrypt namespace password")
-	}
-
-	cpu, memory, disk, err := vmSize(&config)
+	cpu, memory, disk, err := vmSize(config.VM)
 	if err != nil {
 		return result, errors.Wrap(err, "could not interpret vm size")
 	}
@@ -150,7 +134,7 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 		}
 		diskPath = info.Path
 	} else {
-		diskPath, err = storage.Allocate(diskName, int64(disk))
+		diskPath, err = storage.Allocate(diskName, int64(disk), "")
 		if err != nil {
 			return result, errors.Wrap(err, "failed to reserve filesystem for vm")
 		}
@@ -189,7 +173,7 @@ func (p *Provisioner) kubernetesProvisionImpl(ctx context.Context, reservation *
 	}
 
 	var netInfo pkg.VMNetworkInfo
-	netInfo, err = p.buildNetworkInfo(ctx, reservation.Version, reservation.User, iface, pubIface, config)
+	netInfo, err = p.buildNetworkInfo(ctx, reservation.Version, reservation.User, iface, pubIface, config.VM)
 	if err != nil {
 		return result, errors.Wrap(err, "could not generate network info")
 	}
@@ -229,7 +213,11 @@ func (p *Provisioner) kubernetesInstall(ctx context.Context, name string, cpu ui
 		cmdline = fmt.Sprintf("%s k3os.server_url=https://%s:6443", cmdline, ipstring)
 	}
 	for _, key := range cfg.SSHKeys {
-		cmdline = fmt.Sprintf("%s ssh_authorized_keys=\"%s\"", cmdline, key)
+		trimmed := strings.TrimSpace(key)
+		if strings.ContainsAny(trimmed, "\"\n") {
+			return errors.New("ssh keys shouldn't contain double quotes or intermediate new lines")
+		}
+		cmdline = fmt.Sprintf("%s ssh_authorized_keys=\"%s\"", cmdline, trimmed)
 	}
 	if cfg.DatastoreEndpoint != "" {
 		cmdline = fmt.Sprintf("%s k3os.k3s_args=\"--datastore-endpoint=%s\"", cmdline, cfg.DatastoreEndpoint)
@@ -243,7 +231,6 @@ func (p *Provisioner) kubernetesInstall(ctx context.Context, name string, cpu ui
 	disks[0] = pkg.VMDisk{Path: diskPath, ReadOnly: false, Root: false}
 	// install ISO
 	disks[1] = pkg.VMDisk{Path: imagePath + "/k3os-amd64.iso", ReadOnly: true, Root: false}
-
 	installVM := pkg.VM{
 		Name:        name,
 		CPU:         cpu,
@@ -314,234 +301,21 @@ func (p *Provisioner) kubernetesRun(ctx context.Context, name string, cpu uint8,
 	return vm.Run(kubevm)
 }
 
-func (p *Provisioner) kubernetesDecomission(ctx context.Context, reservation *provision.Reservation) error {
-	var (
-		storage = stubs.NewVDiskModuleStub(p.zbus)
-		network = stubs.NewNetworkerStub(p.zbus)
-		vm      = stubs.NewVMModuleStub(p.zbus)
-
-		cfg Kubernetes
-	)
-
-	if err := json.Unmarshal(reservation.Data, &cfg); err != nil {
-		return errors.Wrap(err, "failed to decode reservation schema")
-	}
-
-	if err := vm.Delete(reservation.ID); err != nil {
-		return errors.Wrapf(err, "failed to delete vm %s", reservation.ID)
-	}
-
-	netID := provision.NetworkID(reservation.User, string(cfg.NetworkID))
-	if err := network.RemoveTap(netID); err != nil {
-		return errors.Wrap(err, "could not clean up tap device")
-	}
-
-	if cfg.PublicIP != 0 {
-		if err := network.RemovePubTap(pubIPResID(cfg.PublicIP)); err != nil {
-			return errors.Wrap(err, "could not clean up public tap device")
-		}
-	}
-
-	if err := storage.Deallocate(fmt.Sprintf("%s-%s", reservation.ID, "vda")); err != nil {
-		return errors.Wrap(err, "could not remove vDisk")
-	}
-
-	return nil
-}
-
-func (p *Provisioner) buildNetworkInfo(ctx context.Context, rversion int, userID string, iface string, pubIface string, cfg Kubernetes) (pkg.VMNetworkInfo, error) {
-	network := stubs.NewNetworkerStub(p.zbus)
-
-	netID := provision.NetworkID(userID, string(cfg.NetworkID))
-	subnet, err := network.GetSubnet(netID)
-	if err != nil {
-		return pkg.VMNetworkInfo{}, errors.Wrapf(err, "could not get network resource subnet")
-	}
-
-	if !subnet.Contains(cfg.IP) {
-		return pkg.VMNetworkInfo{}, fmt.Errorf("IP %s is not part of local nr subnet %s", cfg.IP.String(), subnet.String())
-	}
-
-	privNet, err := network.GetNet(netID)
+// Validate validates the vm configuration, master ip, clutser secret
+func (k *Kubernetes) Validate() error {
+	err := k.VM.Validate()
 	if err != nil {
-		return pkg.VMNetworkInfo{}, errors.Wrapf(err, "could not get network range")
+		return err
 	}
 
-	addrCIDR := net.IPNet{
-		IP:   cfg.IP,
-		Mask: subnet.Mask,
+	if strings.ContainsAny(k.PlainClusterSecret, " \t\r\n\f") {
+		return errors.New("cluster secret shouldn't contain whitespace chars")
 	}
 
-	gw4, gw6, err := network.GetDefaultGwIP(netID)
-	if err != nil {
-		return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not get network resource default gateway")
-	}
-
-	privIP6, err := network.GetIPv6From4(netID, cfg.IP)
-	if err != nil {
-		return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not convert private ipv4 to ipv6")
-	}
-
-	networkInfo := pkg.VMNetworkInfo{
-		Ifaces: []pkg.VMIface{{
-			Tap:            iface,
-			MAC:            "", // rely on static IP configuration so we don't care here
-			IP4AddressCIDR: addrCIDR,
-			IP4GatewayIP:   net.IP(gw4),
-			IP4Net:         privNet,
-			IP6AddressCIDR: privIP6,
-			IP6GatewayIP:   gw6,
-			Public:         false,
-		}},
-		Nameservers: []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1"), net.ParseIP("2001:4860:4860::8888")},
-	}
-
-	// from this reservation version on we deploy new VM's with the custom boot script for IP
-	if rversion >= 2 {
-		networkInfo.NewStyle = true
-	}
-
-	if cfg.PublicIP != 0 {
-		// A public ip is set, load the reservation, extract the ip and make a config
-		// for it
-
-		pubIP, pubGw, err := p.getPubIPConfig(cfg.PublicIP)
-		if err != nil {
-			return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not get public ip config")
-		}
-
-		// the mac address uses the global workload id
-		// this needs to be the same as how we get it in the actual IP reservation
-		mac := ifaceutil.HardwareAddrFromInputBytes([]byte(fmt.Sprintf("%d-1", cfg.PublicIP)))
-
-		iface := pkg.VMIface{
-			Tap:            pubIface,
-			MAC:            mac.String(), // mac so we always get the same IPv6 from slaac
-			IP4AddressCIDR: pubIP,
-			IP4GatewayIP:   pubGw,
-			// for now we get ipv6 from slaac, so leave ipv6 stuffs this empty
-			//
-			Public: true,
-		}
-
-		networkInfo.Ifaces = append(networkInfo.Ifaces, iface)
-	}
-
-	return networkInfo, nil
-}
-
-// Get the public ip, and the gateway from the reservation ID
-func (p *Provisioner) getPubIPConfig(rid schema.ID) (net.IPNet, net.IP, error) {
-	// TODO: check if there is a better way to do this
-	explorerClient, err := app.ExplorerClient()
-	if err != nil {
-		return net.IPNet{}, nil, errors.Wrap(err, "could not create explorer client")
-	}
-
-	// explorerClient.Workloads.Get(...) is currently broken
-	workloadDefinition, err := explorerClient.Workloads.NodeWorkloadGet(fmt.Sprintf("%d-1", rid))
-	if err != nil {
-		return net.IPNet{}, nil, errors.Wrap(err, "could not load public ip reservation")
-	}
-	// load IP
-	ip, ok := workloadDefinition.(*workloads.PublicIP)
-	if !ok {
-		return net.IPNet{}, nil, errors.Wrap(err, "could not decode ip reservation")
-	}
-	identity := stubs.NewIdentityManagerStub(p.zbus)
-	self := identity.NodeID().Identity()
-	selfDescription, err := explorerClient.Directory.NodeGet(self, false)
-	if err != nil {
-		return net.IPNet{}, nil, errors.Wrap(err, "could not get our own node description")
-	}
-	farm, err := explorerClient.Directory.FarmGet(schema.ID(selfDescription.FarmId))
-	if err != nil {
-		return net.IPNet{}, nil, errors.Wrap(err, "could not get our own farm")
-	}
-
-	var pubGw schema.IP
-	for _, ips := range farm.IPAddresses {
-		if ips.ReservationID == rid {
-			pubGw = ips.Gateway
-			break
+	for _, ip := range k.MasterIPs {
+		if ip.To4() == nil && ip.To16() == nil {
+			return errors.New("invalid master IP")
 		}
 	}
-	if pubGw.IP == nil {
-		return net.IPNet{}, nil, errors.New("unable to identify public ip gateway")
-	}
-
-	return ip.IPaddress.IPNet, pubGw.IP, nil
-}
-
-// returns the vCpu's, memory, disksize for a vm size
-// memory and disk size is expressed in MiB
-func vmSize(vm *Kubernetes) (cpu uint8, memory uint64, storage uint64, err error) {
-	switch vm.Size {
-	case -1:
-		return uint8(vm.Custom.CRU),
-			uint64(vm.Custom.MRU * 1024),
-			uint64(vm.Custom.SRU * 1024),
-			nil
-	case 1:
-		// 1 vCpu, 2 GiB RAM, 50 GB disk
-		return 1, 2 * 1024, 50 * 1024, nil
-	case 2:
-		// 2 vCpu, 4 GiB RAM, 100 GB disk
-		return 2, 4 * 1024, 100 * 1024, nil
-	case 3:
-		// 2 vCpu, 8 GiB RAM, 25 GB disk
-		return 2, 8 * 1024, 25 * 1024, nil
-	case 4:
-		// 2 vCpu, 8 GiB RAM, 50 GB disk
-		return 2, 8 * 1024, 50 * 1024, nil
-	case 5:
-		// 2 vCpu, 8 GiB RAM, 200 GB disk
-		return 2, 8 * 1024, 200 * 1024, nil
-	case 6:
-		// 4 vCpu, 16 GiB RAM, 50 GB disk
-		return 4, 16 * 1024, 50 * 1024, nil
-	case 7:
-		// 4 vCpu, 16 GiB RAM, 100 GB disk
-		return 4, 16 * 1024, 100 * 1024, nil
-	case 8:
-		// 4 vCpu, 16 GiB RAM, 400 GB disk
-		return 4, 16 * 1024, 400 * 1024, nil
-	case 9:
-		// 8 vCpu, 32 GiB RAM, 100 GB disk
-		return 8, 32 * 1024, 100 * 1024, nil
-	case 10:
-		// 8 vCpu, 32 GiB RAM, 200 GB disk
-		return 8, 32 * 1024, 200 * 1024, nil
-	case 11:
-		// 8 vCpu, 32 GiB RAM, 800 GB disk
-		return 8, 32 * 1024, 800 * 1024, nil
-	case 12:
-		// 1 vCpu, 64 GiB RAM, 200 GB disk
-		return 1, 64 * 1024, 200 * 1024, nil
-	case 13:
-		// 1 mvCpu, 64 GiB RAM, 400 GB disk
-		return 1, 64 * 1024, 400 * 1024, nil
-	case 14:
-		//1 vCpu, 64 GiB RAM, 800 GB disk
-		return 1, 64 * 1024, 800 * 1024, nil
-	case 15:
-		//1 vCpu, 2 GiB RAM, 25 GB disk
-		return 1, 2 * 1024, 25 * 1024, nil
-	case 16:
-		//2 vCpu, 4 GiB RAM, 50 GB disk
-		return 2, 4 * 1024, 50 * 1024, nil
-	case 17:
-		//4 vCpu, 8 GiB RAM, 50 GB disk
-		return 4, 8 * 1024, 50 * 1024, nil
-	case 18:
-		//1 vCpu, 1 GiB RAM, 25 GB disk
-		return 1, 1 * 1024, 25 * 1024, nil
-	}
-
-	return 0, 0, 0, fmt.Errorf("unsupported vm size %d, only size 1 to 18 are supported", vm.Size)
-}
-
-func pubIPResID(reservationID schema.ID) string {
-	// TODO: should this change in the actual reservation?
-	return fmt.Sprintf("%d-1", reservationID)
+	return nil
 }
diff --git a/pkg/provision/primitives/order.go b/pkg/provision/primitives/order.go
index c74e530b..1afd02aa 100644
--- a/pkg/provision/primitives/order.go
+++ b/pkg/provision/primitives/order.go
@@ -19,6 +19,8 @@ const (
 	KubernetesReservation provision.ReservationType = "kubernetes"
 	// PublicIPReservation type
 	PublicIPReservation provision.ReservationType = "public_ip"
+	// VirtualMachineReservation type
+	VirtualMachineReservation provision.ReservationType = "virtual_machine"
 )
 
 // ProvisionOrder is used to sort the workload type
@@ -30,6 +32,7 @@ var ProvisionOrder = map[provision.ReservationType]int{
 	ZDBReservation:             3,
 	VolumeReservation:          4,
 	ContainerReservation:       5,
-	KubernetesReservation:      6,
-	PublicIPReservation:        7,
+	PublicIPReservation:        6,
+	KubernetesReservation:      7,
+	VirtualMachineReservation:  8,
 }
diff --git a/pkg/provision/primitives/provisioner.go b/pkg/provision/primitives/provisioner.go
index b2f4ea80..6514ba8e 100644
--- a/pkg/provision/primitives/provisioner.go
+++ b/pkg/provision/primitives/provisioner.go
@@ -32,6 +32,7 @@ func NewProvisioner(cache provision.ReservationCache, zbus zbus.Client) *Provisi
 		DebugReservation:           p.debugProvision,
 		KubernetesReservation:      p.kubernetesProvision,
 		PublicIPReservation:        p.publicIPProvision,
+		VirtualMachineReservation:  p.virtualMachineProvision,
 	}
 	p.Decommissioners = map[provision.ReservationType]provision.DecomissionerFunc{
 		ContainerReservation:       p.containerDecommission,
@@ -40,8 +41,9 @@ func NewProvisioner(cache provision.ReservationCache, zbus zbus.Client) *Provisi
 		NetworkResourceReservation: p.networkDecommission,
 		ZDBReservation:             p.zdbDecommission,
 		DebugReservation:           p.debugDecommission,
-		KubernetesReservation:      p.kubernetesDecomission,
+		KubernetesReservation:      p.vmDecomission,
 		PublicIPReservation:        p.publicIPDecomission,
+		VirtualMachineReservation:  p.vmDecomission,
 	}
 
 	return p
diff --git a/pkg/provision/primitives/vm.go b/pkg/provision/primitives/vm.go
new file mode 100644
index 00000000..0e92f7cc
--- /dev/null
+++ b/pkg/provision/primitives/vm.go
@@ -0,0 +1,256 @@
+package primitives
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/pkg/errors"
+	"github.com/threefoldtech/tfexplorer/schema"
+	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/provision"
+	"github.com/threefoldtech/zos/pkg/stubs"
+)
+
+// VMResult result returned by k3s reservation
+type VMResult struct {
+	ID string `json:"id"`
+	IP string `json:"ip"`
+}
+
+// VM reservation data
+type VM struct {
+	// Size of the vm, this defines the amount of vCpu, memory, and the disk size
+	// Docs: docs/kubernetes/sizes.md
+	Size int64 `json:"size"`
+
+	Custom VMCustomSize `json:"custom_size"`
+	// NetworkID of the network namepsace in which to run the VM. The network
+	// must be provisioned previously.
+	NetworkID pkg.NetID `json:"network_id"`
+	// IP of the VM. The IP must be part of the subnet available in the network
+	// resource defined by the networkID on this node
+	IP net.IP `json:"ip"`
+
+	SSHKeys []string `json:"ssh_keys"`
+	// PublicIP points to a reservation for a public ip
+	PublicIP schema.ID `json:"public_ip"`
+
+	// A name of a predefined list of VMs
+	Name string `json:"name"`
+}
+
+// VMInfo kernel initrd and the raw disk path of the vm
+type VMInfo struct {
+	Initrd    string
+	Kernel    string
+	ImagePath string
+}
+
+// VMREPO in which all the vm flists are stored
+const VMREPO = "https://hub.grid.tf/tf-official-vms/"
+
+// VMTAG the tag of the vm images
+const VMTAG = "latest"
+
+func (p *Provisioner) virtualMachineProvision(ctx context.Context, reservation *provision.Reservation) (interface{}, error) {
+	return p.virtualMachineProvisionImpl(ctx, reservation)
+}
+
+func (p *Provisioner) virtualMachineProvisionImpl(ctx context.Context, reservation *provision.Reservation) (result KubernetesResult, err error) {
+	var (
+		storage = stubs.NewVDiskModuleStub(p.zbus)
+		network = stubs.NewNetworkerStub(p.zbus)
+		flist   = stubs.NewFlisterStub(p.zbus)
+		vm      = stubs.NewVMModuleStub(p.zbus)
+
+		config VM
+	)
+	if err := json.Unmarshal(reservation.Data, &config); err != nil {
+		return result, errors.Wrap(err, "failed to decode reservation schema")
+	}
+
+	if err = config.Validate(); err != nil {
+		return result, err
+	}
+
+	netID := provision.NetworkID(reservation.User, string(config.NetworkID))
+
+	// check if the network tap already exists
+	// if it does, it's most likely that a vm with the same network id and node id already exists
+	// this will cause the reservation to fail
+	exists, err := network.TapExists(netID)
+	if err != nil {
+		return result, errors.Wrap(err, "could not check if tap device exists")
+	}
+
+	if exists {
+		return result, errors.New("another vm with same network already exists")
+	}
+
+	// check if public ipv4 is supported, should this be requested
+	if config.PublicIP != 0 && !network.PublicIPv4Support() {
+		return result, errors.New("public ipv4 is requested, but not supported on this node")
+	}
+
+	result.ID = reservation.ID
+	result.IP = config.IP.String()
+
+	cpu, memory, disk, err := vmSize(config)
+	if err != nil {
+		return result, errors.Wrap(err, "could not interpret vm size")
+	}
+
+	if _, err = vm.Inspect(reservation.ID); err == nil {
+		// vm is already running, nothing to do here
+		return result, nil
+	}
+
+	flistName := VMREPO + strings.ToLower(config.Name) + "-" + VMTAG + ".flist"
+	imagePath, err := ensureFList(flist, flistName)
+	if err != nil {
+		return result, errors.Wrap(err, "could not mount k3os flist")
+	}
+	imageInfo, err := constructImageInfo(imagePath)
+	if err != nil {
+		return result, err
+	}
+
+	var diskPath string
+	diskName := fmt.Sprintf("%s-%s", provision.FilesystemName(*reservation), "vda")
+	if storage.Exists(diskName) {
+		info, err := storage.Inspect(diskName)
+		if err != nil {
+			return result, errors.Wrap(err, "could not get path to existing disk")
+		}
+		diskPath = info.Path
+	} else {
+		diskPath, err = storage.Allocate(diskName, int64(disk), imageInfo.ImagePath)
+		if err != nil {
+			return result, errors.Wrap(err, "failed to reserve filesystem for vm")
+		}
+	}
+	// clean up the disk anyway, even if it has already been installed.
+	defer func() {
+		if err != nil {
+			_ = storage.Deallocate(diskName)
+		}
+	}()
+
+	var iface string
+	iface, err = network.SetupTap(netID)
+	if err != nil {
+		return result, errors.Wrap(err, "could not set up tap device")
+	}
+
+	defer func() {
+		if err != nil {
+			_ = network.RemoveTap(netID)
+		}
+	}()
+
+	var pubIface string
+	if config.PublicIP != 0 {
+		pubIface, err = network.SetupPubTap(pubIPResID(config.PublicIP))
+		if err != nil {
+			return result, errors.Wrap(err, "could not set up tap device for public network")
+		}
+
+		defer func() {
+			if err != nil {
+				_ = network.RemovePubTap(pubIPResID(config.PublicIP))
+			}
+		}()
+	}
+
+	var netInfo pkg.VMNetworkInfo
+	netInfo, err = p.buildNetworkInfo(ctx, reservation.Version, reservation.User, iface, pubIface, config)
+	if err != nil {
+		return result, errors.Wrap(err, "could not generate network info")
+	}
+	cmdline, err := constructCMDLine(config)
+	if err != nil {
+		return result, err
+	}
+	err = p.vmRun(ctx, reservation.ID, cpu, memory, diskPath, imageInfo, cmdline, netInfo)
+	if err != nil {
+		// attempt to delete the vm, should the process still be lingering
+		vm.Delete(reservation.ID)
+	}
+
+	return result, err
+}
+
+func (p *Provisioner) vmRun(ctx context.Context, name string, cpu uint8, memory uint64, diskPath string, imageInfo VMInfo, cmdline string, networkInfo pkg.VMNetworkInfo) error {
+	vm := stubs.NewVMModuleStub(p.zbus)
+
+	disks := make([]pkg.VMDisk, 1)
+	// installed disk
+	disks[0] = pkg.VMDisk{Path: diskPath, ReadOnly: false, Root: false}
+	vmObj := pkg.VM{
+		Name:        name,
+		CPU:         cpu,
+		Memory:      int64(memory),
+		Network:     networkInfo,
+		KernelImage: imageInfo.Kernel,
+		InitrdImage: imageInfo.Initrd,
+		KernelArgs:  cmdline,
+		Disks:       disks,
+	}
+
+	return vm.Run(vmObj)
+}
+
+func constructCMDLine(config VM) (string, error) {
+	cmdline := "root=/dev/vda rw console=ttyS0 reboot=k panic=1"
+	for _, key := range config.SSHKeys {
+		trimmed := strings.TrimSpace(key)
+		if strings.ContainsAny(trimmed, "\t\r\n\f") {
+			return "", errors.New("ssh keys can't contain intermediate whitespace chars other than white space")
+		}
+		cmdline = fmt.Sprintf("%s ssh=%s", cmdline, strings.Replace(trimmed, " ", ",", -1))
+	}
+	return cmdline, nil
+}
+
+func constructImageInfo(imagePath string) (VMInfo, error) {
+	initrd := ""
+	if _, err := os.Stat(imagePath + "/initrd"); err == nil {
+		initrd = imagePath + "/initrd"
+	}
+	kernel := imagePath + "/kernel"
+	if _, err := os.Stat(kernel); err != nil {
+		return VMInfo{}, errors.Wrap(err, "couldn't stat kernel")
+	}
+	image := imagePath + "/image.raw"
+	if _, err := os.Stat(image); err != nil {
+		return VMInfo{}, errors.Wrap(err, "couldn't stat image.raw")
+	}
+	return VMInfo{Initrd: initrd, Kernel: kernel, ImagePath: image}, nil
+}
+
+// Validate validates the vm config, name, ip, size, and ssh keys
+func (k *VM) Validate() error {
+	// avoid funny stuff like ../badaccount/another-flist
+	if matched, _ := regexp.MatchString("^[0-9a-zA-Z-.]*$", k.Name); !matched {
+		return errors.New("the name must consist of alphanumeric characters, dot, and dash ony")
+	}
+
+	if k.IP.To4() == nil && k.IP.To16() == nil {
+		return errors.New("invalid IP")
+	}
+	if k.Size != -1 && (k.Size < 1 || k.Size > 18) {
+		return errors.New("unsupported vm size %d, only size -1, and 1 to 18 are supported")
+	}
+	for _, key := range k.SSHKeys {
+		trimmed := strings.TrimSpace(key)
+		if strings.ContainsAny(trimmed, "\t\r\n\f\"") {
+			return errors.New("ssh keys can't contain intermediate whitespace chars other than white space")
+		}
+	}
+	return nil
+}
diff --git a/pkg/provision/primitives/vmutils.go b/pkg/provision/primitives/vmutils.go
new file mode 100644
index 00000000..d5444be0
--- /dev/null
+++ b/pkg/provision/primitives/vmutils.go
@@ -0,0 +1,257 @@
+package primitives
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net"
+
+	"github.com/pkg/errors"
+	"github.com/threefoldtech/tfexplorer/models/generated/workloads"
+	"github.com/threefoldtech/tfexplorer/schema"
+	"github.com/threefoldtech/zos/pkg"
+	"github.com/threefoldtech/zos/pkg/app"
+	"github.com/threefoldtech/zos/pkg/network/ifaceutil"
+	"github.com/threefoldtech/zos/pkg/provision"
+	"github.com/threefoldtech/zos/pkg/stubs"
+)
+
+// VMCustomSize type
+type VMCustomSize struct {
+	CRU int64   `json:"cru"`
+	MRU float64 `json:"mru"`
+	SRU float64 `json:"sru"`
+}
+
+func (p *Provisioner) vmDecomission(ctx context.Context, reservation *provision.Reservation) error {
+	var (
+		storage = stubs.NewVDiskModuleStub(p.zbus)
+		network = stubs.NewNetworkerStub(p.zbus)
+		vm      = stubs.NewVMModuleStub(p.zbus)
+
+		cfg VM
+	)
+
+	if err := json.Unmarshal(reservation.Data, &cfg); err != nil {
+		return errors.Wrap(err, "failed to decode reservation schema")
+	}
+
+	if err := vm.Delete(reservation.ID); err != nil {
+		return errors.Wrapf(err, "failed to delete vm %s", reservation.ID)
+	}
+
+	netID := provision.NetworkID(reservation.User, string(cfg.NetworkID))
+	if err := network.RemoveTap(netID); err != nil {
+		return errors.Wrap(err, "could not clean up tap device")
+	}
+
+	if cfg.PublicIP != 0 {
+		if err := network.RemovePubTap(pubIPResID(cfg.PublicIP)); err != nil {
+			return errors.Wrap(err, "could not clean up public tap device")
+		}
+	}
+
+	if err := storage.Deallocate(fmt.Sprintf("%s-%s", reservation.ID, "vda")); err != nil {
+		return errors.Wrap(err, "could not remove vDisk")
+	}
+
+	return nil
+}
+
+func (p *Provisioner) buildNetworkInfo(ctx context.Context, rversion int, userID string, iface string, pubIface string, cfg VM) (pkg.VMNetworkInfo, error) {
+	network := stubs.NewNetworkerStub(p.zbus)
+
+	netID := provision.NetworkID(userID, string(cfg.NetworkID))
+	subnet, err := network.GetSubnet(netID)
+	if err != nil {
+		return pkg.VMNetworkInfo{}, errors.Wrapf(err, "could not get network resource subnet")
+	}
+
+	if !subnet.Contains(cfg.IP) {
+		return pkg.VMNetworkInfo{}, fmt.Errorf("IP %s is not part of local nr subnet %s", cfg.IP.String(), subnet.String())
+	}
+
+	privNet, err := network.GetNet(netID)
+	if err != nil {
+		return pkg.VMNetworkInfo{}, errors.Wrapf(err, "could not get network range")
+	}
+
+	addrCIDR := net.IPNet{
+		IP:   cfg.IP,
+		Mask: subnet.Mask,
+	}
+
+	gw4, gw6, err := network.GetDefaultGwIP(netID)
+	if err != nil {
+		return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not get network resource default gateway")
+	}
+
+	privIP6, err := network.GetIPv6From4(netID, cfg.IP)
+	if err != nil {
+		return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not convert private ipv4 to ipv6")
+	}
+
+	networkInfo := pkg.VMNetworkInfo{
+		Ifaces: []pkg.VMIface{{
+			Tap:            iface,
+			MAC:            "", // rely on static IP configuration so we don't care here
+			IP4AddressCIDR: addrCIDR,
+			IP4GatewayIP:   net.IP(gw4),
+			IP4Net:         privNet,
+			IP6AddressCIDR: privIP6,
+			IP6GatewayIP:   gw6,
+			Public:         false,
+		}},
+		Nameservers: []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1"), net.ParseIP("2001:4860:4860::8888")},
+	}
+
+	// from this reservation version on we deploy new VM's with the custom boot script for IP
+	if rversion >= 2 {
+		networkInfo.NewStyle = true
+	}
+
+	if cfg.PublicIP != 0 {
+		// A public ip is set, load the reservation, extract the ip and make a config
+		// for it
+
+		pubIP, pubGw, err := p.getPubIPConfig(cfg.PublicIP)
+		if err != nil {
+			return pkg.VMNetworkInfo{}, errors.Wrap(err, "could not get public ip config")
+		}
+
+		// the mac address uses the global workload id
+		// this needs to be the same as how we get it in the actual IP reservation
+		mac := ifaceutil.HardwareAddrFromInputBytes([]byte(fmt.Sprintf("%d-1", pubIP)))
+
+		iface := pkg.VMIface{
+			Tap:            pubIface,
+			MAC:            mac.String(), // mac so we always get the same IPv6 from slaac
+			IP4AddressCIDR: pubIP,
+			IP4GatewayIP:   pubGw,
+			// for now we get ipv6 from slaac, so leave ipv6 stuffs this empty
+			//
+			Public: true,
+		}
+
+		networkInfo.Ifaces = append(networkInfo.Ifaces, iface)
+	}
+
+	return networkInfo, nil
+}
+
+// Get the public ip, and the gateway from the reservation ID
+func (p *Provisioner) getPubIPConfig(rid schema.ID) (net.IPNet, net.IP, error) {
+	// TODO: check if there is a better way to do this
+	explorerClient, err := app.ExplorerClient()
+	if err != nil {
+		return net.IPNet{}, nil, errors.Wrap(err, "could not create explorer client")
+	}
+
+	// explorerClient.Workloads.Get(...) is currently broken
+	workloadDefinition, err := explorerClient.Workloads.NodeWorkloadGet(fmt.Sprintf("%d-1", rid))
+	if err != nil {
+		return net.IPNet{}, nil, errors.Wrap(err, "could not load public ip reservation")
+	}
+	// load IP
+	ip, ok := workloadDefinition.(*workloads.PublicIP)
+	if !ok {
+		return net.IPNet{}, nil, errors.Wrap(err, "could not decode ip reservation")
+	}
+	identity := stubs.NewIdentityManagerStub(p.zbus)
+	self := identity.NodeID().Identity()
+	selfDescription, err := explorerClient.Directory.NodeGet(self, false)
+	if err != nil {
+		return net.IPNet{}, nil, errors.Wrap(err, "could not get our own node description")
+	}
+	farm, err := explorerClient.Directory.FarmGet(schema.ID(selfDescription.FarmId))
+	if err != nil {
+		return net.IPNet{}, nil, errors.Wrap(err, "could not get our own farm")
+	}
+
+	var pubGw schema.IP
+	for _, ips := range farm.IPAddresses {
+		if ips.ReservationID == rid {
+			pubGw = ips.Gateway
+			break
+		}
+	}
+	if pubGw.IP == nil {
+		return net.IPNet{}, nil, errors.New("unable to identify public ip gateway")
+	}
+
+	return ip.IPaddress.IPNet, pubGw.IP, nil
+}
+
+// returns the vCpu's, memory, disksize for a vm size
+// memory and disk size is expressed in MiB
+func vmSize(vm VM) (cpu uint8, memory uint64, storage uint64, err error) {
+	switch vm.Size {
+	case -1:
+		customSize := vm.Custom
+		return uint8(customSize.CRU),
+			uint64(customSize.MRU * 1024),
+			uint64(customSize.SRU * 1024),
+			nil
+	case 1:
+		// 1 vCpu, 2 GiB RAM, 50 GB disk
+		return 1, 2 * 1024, 50 * 1024, nil
+	case 2:
+		// 2 vCpu, 4 GiB RAM, 100 GB disk
+		return 2, 4 * 1024, 100 * 1024, nil
+	case 3:
+		// 2 vCpu, 8 GiB RAM, 25 GB disk
+		return 2, 8 * 1024, 25 * 1024, nil
+	case 4:
+		// 2 vCpu, 8 GiB RAM, 50 GB disk
+		return 2, 8 * 1024, 50 * 1024, nil
+	case 5:
+		// 2 vCpu, 8 GiB RAM, 200 GB disk
+		return 2, 8 * 1024, 200 * 1024, nil
+	case 6:
+		// 4 vCpu, 16 GiB RAM, 50 GB disk
+		return 4, 16 * 1024, 50 * 1024, nil
+	case 7:
+		// 4 vCpu, 16 GiB RAM, 100 GB disk
+		return 4, 16 * 1024, 100 * 1024, nil
+	case 8:
+		// 4 vCpu, 16 GiB RAM, 400 GB disk
+		return 4, 16 * 1024, 400 * 1024, nil
+	case 9:
+		// 8 vCpu, 32 GiB RAM, 100 GB disk
+		return 8, 32 * 1024, 100 * 1024, nil
+	case 10:
+		// 8 vCpu, 32 GiB RAM, 200 GB disk
+		return 8, 32 * 1024, 200 * 1024, nil
+	case 11:
+		// 8 vCpu, 32 GiB RAM, 800 GB disk
+		return 8, 32 * 1024, 800 * 1024, nil
+	case 12:
+		// 1 vCpu, 64 GiB RAM, 200 GB disk
+		return 1, 64 * 1024, 200 * 1024, nil
+	case 13:
+		// 1 mvCpu, 64 GiB RAM, 400 GB disk
+		return 1, 64 * 1024, 400 * 1024, nil
+	case 14:
+		//1 vCpu, 64 GiB RAM, 800 GB disk
+		return 1, 64 * 1024, 800 * 1024, nil
+	case 15:
+		//1 vCpu, 2 GiB RAM, 25 GB disk
+		return 1, 2 * 1024, 25 * 1024, nil
+	case 16:
+		//2 vCpu, 4 GiB RAM, 50 GB disk
+		return 2, 4 * 1024, 50 * 1024, nil
+	case 17:
+		//4 vCpu, 8 GiB RAM, 50 GB disk
+		return 4, 8 * 1024, 50 * 1024, nil
+	case 18:
+		//1 vCpu, 1 GiB RAM, 25 GB disk
+		return 1, 1 * 1024, 25 * 1024, nil
+	}
+
+	return 0, 0, 0, fmt.Errorf("unsupported vm size %d, only size -1, and 1 to 18 are supported", vm.Size)
+}
+
+func pubIPResID(reservationID schema.ID) string {
+	// TODO: should this change in the actual reservation?
+	return fmt.Sprintf("%d-1", reservationID)
+}
diff --git a/pkg/storage.go b/pkg/storage.go
index 4ffed5a3..0dd026b5 100644
--- a/pkg/storage.go
+++ b/pkg/storage.go
@@ -183,7 +183,7 @@ func (d *VDisk) Name() string {
 // VDiskModule interface
 type VDiskModule interface {
 	// AllocateDisk with given id and size, return path to virtual disk
-	Allocate(id string, size int64) (string, error)
+	Allocate(id string, size int64, sourceDisk string) (string, error)
 	// DeallocateVDisk removes a virtual disk
 	Deallocate(id string) error
 	// Exists checks if disk with that ID already allocated
diff --git a/pkg/storage/disk.go b/pkg/storage/disk.go
index 614685d8..3b98e4c7 100644
--- a/pkg/storage/disk.go
+++ b/pkg/storage/disk.go
@@ -2,8 +2,10 @@ package storage
 
 import (
 	"fmt"
+	"io"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"syscall"
 
@@ -50,7 +52,7 @@ func (d *vdiskModule) findDisk(id string) (string, error) {
 }
 
 // AllocateDisk with given size, return path to virtual disk (size in MB)
-func (d *vdiskModule) Allocate(id string, size int64) (string, error) {
+func (d *vdiskModule) Allocate(id string, size int64, sourceDisk string) (string, error) {
 	path, err := d.findDisk(id)
 	if err == nil {
 		return path, errors.Wrapf(os.ErrExist, "disk with id '%s' already exists", id)
@@ -79,12 +81,27 @@ func (d *vdiskModule) Allocate(id string, size int64) (string, error) {
 		return "", err
 	}
 
+	if sourceDisk != "" {
+		source, err := os.Open(sourceDisk)
+		if err != nil {
+			return "", err
+		}
+		defer source.Close()
+		io.Copy(file, source)
+	}
+
 	defer file.Close()
 	if err = chattr.SetAttr(file, chattr.FS_NOCOW_FL); err != nil {
 		return "", err
 	}
 
 	err = syscall.Fallocate(int(file.Fd()), 0, 0, size*mib)
+	if sourceDisk != "" {
+		err = d.expandfs(path)
+		if err != nil {
+			return "", err
+		}
+	}
 	return path, err
 }
 
@@ -101,6 +118,28 @@ func (d *vdiskModule) safePath(base, id string) (string, error) {
 	return path, nil
 }
 
+func (d *vdiskModule) expandfs(disk string) error {
+	dname, err := ioutil.TempDir("", "btrfs-resize")
+	if err != nil {
+		return errors.Wrap(err, "couldn't create a temp dir to mount the btrfs fs to resize it")
+	}
+	defer os.RemoveAll(dname)
+
+	cmd := exec.Command("mount", disk, dname)
+
+	if err := cmd.Run(); err != nil {
+		return errors.Wrap(err, "couldn't mount the btrfs fs to resize it")
+	}
+
+	defer syscall.Unmount(dname, 0)
+	cmd = exec.Command("btrfs", "filesystem", "resize", "max", dname)
+
+	if err := cmd.Run(); err != nil {
+		return errors.Wrap(err, "failed to resize file system to disk size")
+	}
+	return nil
+}
+
 // DeallocateVDisk removes a virtual disk
 func (d *vdiskModule) Deallocate(id string) error {
 	path, err := d.findDisk(id)
diff --git a/pkg/stubs/vdisk_stub.go b/pkg/stubs/vdisk_stub.go
index c0895db5..2e0c41d0 100644
--- a/pkg/stubs/vdisk_stub.go
+++ b/pkg/stubs/vdisk_stub.go
@@ -22,8 +22,8 @@ func NewVDiskModuleStub(client zbus.Client) *VDiskModuleStub {
 	}
 }
 
-func (s *VDiskModuleStub) Allocate(arg0 string, arg1 int64) (ret0 string, ret1 error) {
-	args := []interface{}{arg0, arg1}
+func (s *VDiskModuleStub) Allocate(arg0 string, arg1 int64, arg2 string) (ret0 string, ret1 error) {
+	args := []interface{}{arg0, arg1, arg2}
 	result, err := s.client.Request(s.module, s.object, "Allocate", args...)
 	if err != nil {
 		panic(err)
diff --git a/pkg/vm/ch.go b/pkg/vm/ch.go
index dc62aa2f..801a462f 100644
--- a/pkg/vm/ch.go
+++ b/pkg/vm/ch.go
@@ -21,9 +21,8 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 
 	// build command line
 	args := map[string][]string{
-		"--kernel":    {m.Boot.Kernel},
-		"--initramfs": {m.Boot.Initrd},
-		"--cmdline":   {m.Boot.Args},
+		"--kernel":  {m.Boot.Kernel},
+		"--cmdline": {m.Boot.Args},
 
 		"--cpus":   {m.Config.CPU.String()},
 		"--memory": {m.Config.Mem.String()},
@@ -31,7 +30,9 @@ func (m *Machine) Run(ctx context.Context, socket, logs string) error {
 		"--log-file":   {logs},
 		"--api-socket": {socket},
 	}
-
+	if m.Boot.Initrd != "" {
+		args["--initramfs"] = []string{m.Boot.Initrd}
+	}
 	// disks
 	if len(m.Disks) > 0 {
 		var disks []string


Test patch:
diff --git a/pkg/provision/primitives/resource_units_test.go b/pkg/provision/primitives/resource_units_test.go
index 2f753fe3..df21b36e 100644
--- a/pkg/provision/primitives/resource_units_test.go
+++ b/pkg/provision/primitives/resource_units_test.go
@@ -203,7 +203,9 @@ func Test_processKubernetes(t *testing.T) {
 				r: &provision.Reservation{
 					Type: KubernetesReservation,
 					Data: mustMarshalJSON(t, Kubernetes{
-						Size: 1,
+						VM: VM{
+							Size: 1,
+						},
 					}),
 				},
 			},
@@ -219,7 +221,9 @@ func Test_processKubernetes(t *testing.T) {
 				r: &provision.Reservation{
 					Type: KubernetesReservation,
 					Data: mustMarshalJSON(t, Kubernetes{
-						Size: 2,
+						VM: VM{
+							Size: 2,
+						},
 					}),
 				},
 			},