diff --git a/examples/cirros-vm-persistent-rootfs.yaml b/examples/cirros-vm-persistent-rootfs.yaml
new file mode 100644
index 000000000..cfc80045d
--- /dev/null
+++ b/examples/cirros-vm-persistent-rootfs.yaml
@@ -0,0 +1,80 @@
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+ name: local-block-pvc
+spec:
+ accessModes:
+ - ReadWriteOnce
+ volumeMode: Block
+ storageClassName: local-storage
+ resources:
+ requests:
+ storage: 100Mi
+---
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: local-block-pv
+spec:
+ capacity:
+ storage: 100Mi
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ storageClassName: local-storage
+ volumeMode: Block
+ local:
+ # set up with:
+ # docker exec kube-node-1 /bin/bash -c 'dd if=/dev/zero of=/rawtest bs=1M count=1000 && losetup -f /rawtest --show'
+ path: /dev/loop0
+ claimRef:
+ name: local-block-pvc
+ namespace: default
+ nodeAffinity:
+ required:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - kube-node-1
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: cirros-vm-p
+ annotations:
+ kubernetes.io/target-runtime: virtlet.cloud
+ # CirrOS doesn't load nocloud data from SCSI CD-ROM for some reason
+ VirtletDiskDriver: virtio
+ VirtletSSHKeys: |
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCaJEcFDXEK2ZbX0ZLS1EIYFZRbDAcRfuVjpstSc0De8+sV1aiu+dePxdkuDRwqFtCyk6dEZkssjOkBXtri00MECLkir6FcH3kKOJtbJ6vy3uaJc9w1ERo+wyl6SkAh/+JTJkp7QRXj8oylW5E20LsbnA/dIwWzAF51PPwF7A7FtNg9DnwPqMkxFo1Th/buOMKbP5ZA1mmNNtmzbMpMfJATvVyiv3ccsSJKOiyQr6UG+j7sc/7jMVz5Xk34Vd0l8GwcB0334MchHckmqDB142h/NCWTr8oLakDNvkfC1YneAfAO41hDkUbxPtVBG5M/o7P4fxoqiHEX+ZLfRxDtHB53 me@localhost
+spec:
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: extraRuntime
+ operator: In
+ values:
+ - virtlet
+ # This is the number of seconds Virtlet gives the VM to shut down cleanly.
+ # The default value of 30 seconds is ok for containers but probably too
+ # low for VM, so overriding it here is strongly advised.
+ terminationGracePeriodSeconds: 120
+ containers:
+ - name: cirros-vm
+ image: virtlet.cloud/cirros
+ imagePullPolicy: IfNotPresent
+ # tty and stdin required for `kubectl attach -t` to work
+ tty: true
+ stdin: true
+ volumeDevices:
+ - devicePath: /
+ name: testpvc
+ volumes:
+ - name: testpvc
+ persistentVolumeClaim:
+ claimName: local-block-pvc
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 316e756ee..36add25c0 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -105,7 +105,7 @@ func configFieldSet(c *virtlet_v1.VirtletConfig) *fieldSet {
fs.addStringField("kubeletRootDir", "kubelet-root-dir", "", "Pod's root dir in kubelet", kubeletRootDirEnv, kubeletRootDir, &c.KubeletRootDir)
// this field duplicates glog's --v, so no option for it, which is signified
// by "+" here (it's only for doc)
- fs.addIntField("logLevel", "+v", "", "Log level to use", logLevelEnv, 1, 0, math.MaxInt32, &c.LogLevel)
+ fs.addIntField("logLevel", "+v", "", "Log level to use", logLevelEnv, 6, 0, math.MaxInt32, &c.LogLevel)
return &fs
}
diff --git a/pkg/image/fake/store.go b/pkg/image/fake/store.go
index 917b8473b..d5786e88a 100644
--- a/pkg/image/fake/store.go
+++ b/pkg/image/fake/store.go
@@ -119,12 +119,12 @@ func (s *FakeStore) GC() error {
}
// GetImagePathAndVirtualSize implements GC method of Store interface.
-func (s *FakeStore) GetImagePathAndVirtualSize(imageName string) (string, uint64, error) {
+func (s *FakeStore) GetImagePathDigestAndVirtualSize(imageName string) (string, digest.Digest, uint64, error) {
img, found := s.images[imageName]
if !found {
- return "", 0, fmt.Errorf("image not found: %q", imageName)
+ return "", "", 0, fmt.Errorf("image not found: %q", imageName)
}
- return img.Path, img.Size, nil
+ return img.Path, digest.Digest(img.Digest), img.Size, nil
}
// SetRefGetter implements SetRefGetter method of Store interface.
diff --git a/pkg/image/image.go b/pkg/image/image.go
index cc272d0bf..9e12bfd15 100644
--- a/pkg/image/image.go
+++ b/pkg/image/image.go
@@ -91,10 +91,10 @@ type Store interface {
// GC removes all unused or partially downloaded images.
GC() error
- // GetImagePathAndVirtualSize returns the path to image data
- // and virtual size for the specified image. It accepts
- // an image reference or a digest.
- GetImagePathAndVirtualSize(ref string) (string, uint64, error)
+ // GetImagePathDigestAndVirtualSize returns the path to image
+ // data, the digest and the virtual size for the specified
+ // image. It accepts an image reference or a digest.
+ GetImagePathDigestAndVirtualSize(ref string) (string, digest.Digest, uint64, error)
// SetRefGetter sets a function that will be used to determine
// the set of images that are currently in use.
@@ -480,29 +480,32 @@ func (s *FileStore) GC() error {
}
// GetImagePathAndVirtualSize implements GC method of Store interface.
-func (s *FileStore) GetImagePathAndVirtualSize(ref string) (string, uint64, error) {
+func (s *FileStore) GetImagePathDigestAndVirtualSize(ref string) (string, digest.Digest, uint64, error) {
s.Lock()
defer s.Unlock()
- glog.V(3).Infof("GetImagePathAndVirtualSize(): %q", ref)
+ glog.V(3).Infof("GetImagePathDigestAndVirtualSize(): %q", ref)
var pathViaDigest, pathViaName string
// parsing digest as ref gives bad results
- if d, err := digest.Parse(ref); err == nil {
+ d, err := digest.Parse(ref)
+ if err == nil {
if d.Algorithm() != digest.SHA256 {
- return "", 0, fmt.Errorf("bad image digest (need sha256): %q", d)
+ return "", "", 0, fmt.Errorf("bad image digest (need sha256): %q", d)
}
pathViaDigest = s.dataFileName(d.Hex())
} else {
parsed, err := reference.Parse(ref)
if err != nil {
- return "", 0, fmt.Errorf("bad image reference %q: %v", ref, err)
+ return "", "", 0, fmt.Errorf("bad image reference %q: %v", ref, err)
}
+ d = ""
if digested, ok := parsed.(reference.Digested); ok {
if digested.Digest().Algorithm() != digest.SHA256 {
- return "", 0, fmt.Errorf("bad image digest (need sha256): %q", digested.Digest())
+ return "", "", 0, fmt.Errorf("bad image digest (need sha256): %q", digested.Digest())
}
- pathViaDigest = s.dataFileName(digested.Digest().Hex())
+ d = digested.Digest()
+ pathViaDigest = s.dataFileName(d.Hex())
}
if named, ok := parsed.(reference.Named); ok && named.Name() != "" {
@@ -511,6 +514,7 @@ func (s *FileStore) GetImagePathAndVirtualSize(ref string) (string, uint64, erro
glog.Warningf("error reading link %q: %v", pathViaName, err)
} else {
pathViaName = filepath.Join(s.linkDir(), pathViaName)
+ d = digest.NewDigestFromHex(string(digest.SHA256), filepath.Base(pathViaName))
}
}
}
@@ -518,28 +522,28 @@ func (s *FileStore) GetImagePathAndVirtualSize(ref string) (string, uint64, erro
path := pathViaDigest
switch {
case pathViaDigest == "" && pathViaName == "":
- return "", 0, fmt.Errorf("bad image reference %q", ref)
+ return "", "", 0, fmt.Errorf("bad image reference %q", ref)
case pathViaDigest == "":
path = pathViaName
case pathViaName != "":
fi1, err := os.Stat(pathViaName)
if err != nil {
- return "", 0, err
+ return "", "", 0, err
}
fi2, err := os.Stat(pathViaDigest)
if err != nil {
- return "", 0, err
+ return "", "", 0, err
}
if !os.SameFile(fi1, fi2) {
- return "", 0, fmt.Errorf("digest / name path mismatch: %q vs %q", pathViaDigest, pathViaName)
+ return "", "", 0, fmt.Errorf("digest / name path mismatch: %q vs %q", pathViaDigest, pathViaName)
}
}
vsize, err := s.vsizeFunc(path)
if err != nil {
- return "", 0, fmt.Errorf("error getting image size for %q: %v", path, err)
+ return "", "", 0, fmt.Errorf("error getting image size for %q: %v", path, err)
}
- return path, vsize, nil
+ return path, d, vsize, nil
}
// SetRefGetter implements SetRefGetter method of Store interface.
diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go
index 4045a96a5..ce11e8f6e 100644
--- a/pkg/image/image_test.go
+++ b/pkg/image/image_test.go
@@ -187,9 +187,13 @@ func (tst *ifsTester) verifyListImages(filter string, expectedImages ...*Image)
}
func (tst *ifsTester) verifyImage(ref string, expectedContents string) {
- if path, vsize, err := tst.store.GetImagePathAndVirtualSize(ref); err != nil {
+ if path, digest, vsize, err := tst.store.GetImagePathDigestAndVirtualSize(ref); err != nil {
tst.t.Errorf("GetImagePathAndVirtualSize(): %v", err)
} else {
+ expectedDigest := "sha256:" + sha256str(expectedContents)
+ if string(digest) != expectedDigest {
+ tst.t.Errorf("bad digest: %s instead of %s", digest, expectedDigest)
+ }
tst.verifyFileContents(path, expectedContents)
expectedVirtualSize := uint64(len(expectedContents)) + 1000
if vsize != expectedVirtualSize {
diff --git a/pkg/libvirttools/TestContainerLifecycle.out.yaml b/pkg/libvirttools/TestContainerLifecycle.out.yaml
index 08d072b22..72ab84eb0 100755
--- a/pkg/libvirttools/TestContainerLifecycle.out.yaml
+++ b/pkg/libvirttools/TestContainerLifecycle.out.yaml
@@ -1,6 +1,6 @@
- name: 'domain conn: ListDomains'
value: []
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__ceph_flexvolume.out.yaml b/pkg/libvirttools/TestDomainDefinitions__ceph_flexvolume.out.yaml
index 6310c1bd8..56bde94eb 100755
--- a/pkg/libvirttools/TestDomainDefinitions__ceph_flexvolume.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__ceph_flexvolume.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__cloud-init.out.yaml b/pkg/libvirttools/TestDomainDefinitions__cloud-init.out.yaml
index 50316c543..800b8c2d2 100755
--- a/pkg/libvirttools/TestDomainDefinitions__cloud-init.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__cloud-init.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__cloud-init_with_user_data.out.yaml b/pkg/libvirttools/TestDomainDefinitions__cloud-init_with_user_data.out.yaml
index 0341a7835..c16c158e9 100755
--- a/pkg/libvirttools/TestDomainDefinitions__cloud-init_with_user_data.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__cloud-init_with_user_data.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__persistent_rootfs.out.yaml b/pkg/libvirttools/TestDomainDefinitions__persistent_rootfs.out.yaml
new file mode 100755
index 000000000..79b594c80
--- /dev/null
+++ b/pkg/libvirttools/TestDomainDefinitions__persistent_rootfs.out.yaml
@@ -0,0 +1,87 @@
+- name: GetImagePathDigestAndVirtualSize
+ value: fake/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /fakedev/69eec606-0493-5825-73a4-c5e0c0236155/volumeDevices/kubernetes.io~local-volume/root
+ stdout: "1000"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-69eec606-0493-5825-73a4-c5e0c0236155
+ stdin: |
+ 0 999 linear /fakedev/69eec606-0493-5825-73a4-c5e0c0236155/volumeDevices/kubernetes.io~local-volume/root 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/volume/path /dev/mapper/virtlet-dm-69eec606-0493-5825-73a4-c5e0c0236155
+- name: 'domain conn: DefineDomain'
+ value: |-
+
+ virtlet-231700d5-c9a6-container1
+ 231700d5-c9a6-5a49-738d-99a954c51550
+ 1024
+ 1
+
+ 0
+ 0
+ 0
+
+
+ hvm
+
+
+
+
+
+ destroy
+ restart
+ restart
+
+ /vmwrapper
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+- name: 'domain conn: virtlet-231700d5-c9a6-container1: Create'
+- name: 'domain conn: virtlet-231700d5-c9a6-container1: iso image'
+ value:
+ meta-data: '{"instance-id":"testName_0.default","local-hostname":"testName_0"}'
+ network-config: |
+ version: 1
+ user-data: |
+ #cloud-config
+- name: 'domain conn: virtlet-231700d5-c9a6-container1: Destroy'
+- name: 'domain conn: virtlet-231700d5-c9a6-container1: Undefine'
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-69eec606-0493-5825-73a4-c5e0c0236155
diff --git a/pkg/libvirttools/TestDomainDefinitions__plain_domain.out.yaml b/pkg/libvirttools/TestDomainDefinitions__plain_domain.out.yaml
index 19ccad185..2d5f14752 100755
--- a/pkg/libvirttools/TestDomainDefinitions__plain_domain.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__plain_domain.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__raw_block_volume.out.yaml b/pkg/libvirttools/TestDomainDefinitions__raw_block_volume.out.yaml
index 53f4cdb81..61a0abdf4 100755
--- a/pkg/libvirttools/TestDomainDefinitions__raw_block_volume.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__raw_block_volume.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__raw_devices.out.yaml b/pkg/libvirttools/TestDomainDefinitions__raw_devices.out.yaml
index 9af2223ac..0f0e2f7dd 100755
--- a/pkg/libvirttools/TestDomainDefinitions__raw_devices.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__raw_devices.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__vcpu_count.out.yaml b/pkg/libvirttools/TestDomainDefinitions__vcpu_count.out.yaml
index c833b4629..e8405390c 100755
--- a/pkg/libvirttools/TestDomainDefinitions__vcpu_count.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__vcpu_count.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__virtio_disk_driver.out.yaml b/pkg/libvirttools/TestDomainDefinitions__virtio_disk_driver.out.yaml
index a29c38fac..d124fb640 100755
--- a/pkg/libvirttools/TestDomainDefinitions__virtio_disk_driver.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__virtio_disk_driver.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainDefinitions__volumes.out.yaml b/pkg/libvirttools/TestDomainDefinitions__volumes.out.yaml
index 687e5221d..3cb2632a1 100755
--- a/pkg/libvirttools/TestDomainDefinitions__volumes.out.yaml
+++ b/pkg/libvirttools/TestDomainDefinitions__volumes.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestDomainForcedShutdown.out.yaml b/pkg/libvirttools/TestDomainForcedShutdown.out.yaml
index 819efd961..8fbcb25e1 100755
--- a/pkg/libvirttools/TestDomainForcedShutdown.out.yaml
+++ b/pkg/libvirttools/TestDomainForcedShutdown.out.yaml
@@ -1,4 +1,4 @@
-- name: GetImagePathAndVirtualSize
+- name: GetImagePathDigestAndVirtualSize
value: fake/image1
- name: 'storage: CreateStoragePool'
value: |-
diff --git a/pkg/libvirttools/TestPersistentRootVolume__first_image_too_big.out.yaml b/pkg/libvirttools/TestPersistentRootVolume__first_image_too_big.out.yaml
new file mode 100755
index 000000000..8ac4313fc
--- /dev/null
+++ b/pkg/libvirttools/TestPersistentRootVolume__first_image_too_big.out.yaml
@@ -0,0 +1,7 @@
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "8"
diff --git a/pkg/libvirttools/TestPersistentRootVolume__image_change.out.yaml b/pkg/libvirttools/TestPersistentRootVolume__image_change.out.yaml
new file mode 100755
index 000000000..682dd354f
--- /dev/null
+++ b/pkg/libvirttools/TestPersistentRootVolume__image_change.out.yaml
@@ -0,0 +1,52 @@
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "32"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 31 linear /dev/rootdev 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/path1 /dev/mapper/virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image2
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "32"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 31 linear /dev/rootdev 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/path2 /dev/mapper/virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
diff --git a/pkg/libvirttools/TestPersistentRootVolume__image_unchanged.out.yaml b/pkg/libvirttools/TestPersistentRootVolume__image_unchanged.out.yaml
new file mode 100755
index 000000000..054a43f45
--- /dev/null
+++ b/pkg/libvirttools/TestPersistentRootVolume__image_unchanged.out.yaml
@@ -0,0 +1,49 @@
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 16 linear /dev/rootdev 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/path1 /dev/mapper/virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 16 linear /dev/rootdev 1
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
diff --git a/pkg/libvirttools/TestPersistentRootVolume__second_image_too_big.out.yaml b/pkg/libvirttools/TestPersistentRootVolume__second_image_too_big.out.yaml
new file mode 100755
index 000000000..d572b14e4
--- /dev/null
+++ b/pkg/libvirttools/TestPersistentRootVolume__second_image_too_big.out.yaml
@@ -0,0 +1,33 @@
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 16 linear /dev/rootdev 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/path1 /dev/mapper/virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image2
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
diff --git a/pkg/libvirttools/TestPersistentRootVolume__symlinks.out.yaml b/pkg/libvirttools/TestPersistentRootVolume__symlinks.out.yaml
new file mode 100755
index 000000000..054a43f45
--- /dev/null
+++ b/pkg/libvirttools/TestPersistentRootVolume__symlinks.out.yaml
@@ -0,0 +1,49 @@
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 16 linear /dev/rootdev 1
+- name: CMD
+ value:
+ cmd: qemu-img convert -O raw /fake/path1 /dev/mapper/virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
+- name: setup
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: persistent/image1
+- name: CMD
+ value:
+ cmd: blockdev --getsz /dev/rootdev
+ stdout: "17"
+- name: CMD
+ value:
+ cmd: dmsetup create virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+ stdin: |
+ 0 16 linear /dev/rootdev 1
+- name: end setup -- root disk
+ value: |-
+
+
+
+
+- name: teardown
+- name: CMD
+ value:
+ cmd: dmsetup remove virtlet-dm-77f29a0e-46af-4188-a6af-9ff8b8a65224
+- name: end teardown
diff --git a/pkg/libvirttools/TestRootVolumeLifeCycle.out.yaml b/pkg/libvirttools/TestRootVolumeLifeCycle.out.yaml
index 37dbe2008..da4199082 100755
--- a/pkg/libvirttools/TestRootVolumeLifeCycle.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeLifeCycle.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/TestRootVolumeSize__default__zero_.out.yaml b/pkg/libvirttools/TestRootVolumeSize__default__zero_.out.yaml
index 1b7d0fbdc..b23fffcc9 100755
--- a/pkg/libvirttools/TestRootVolumeSize__default__zero_.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeSize__default__zero_.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/TestRootVolumeSize__greater_than_fakeImageVirtualSize.out.yaml b/pkg/libvirttools/TestRootVolumeSize__greater_than_fakeImageVirtualSize.out.yaml
index 62cb7529c..b08af1795 100755
--- a/pkg/libvirttools/TestRootVolumeSize__greater_than_fakeImageVirtualSize.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeSize__greater_than_fakeImageVirtualSize.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/TestRootVolumeSize__negative.out.yaml b/pkg/libvirttools/TestRootVolumeSize__negative.out.yaml
index 1b7d0fbdc..b23fffcc9 100755
--- a/pkg/libvirttools/TestRootVolumeSize__negative.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeSize__negative.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/TestRootVolumeSize__same_as_fakeImageVirtualSize.out.yaml b/pkg/libvirttools/TestRootVolumeSize__same_as_fakeImageVirtualSize.out.yaml
index 1b7d0fbdc..b23fffcc9 100755
--- a/pkg/libvirttools/TestRootVolumeSize__same_as_fakeImageVirtualSize.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeSize__same_as_fakeImageVirtualSize.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/TestRootVolumeSize__smaller_than_fakeImageVirtualSize.out.yaml b/pkg/libvirttools/TestRootVolumeSize__smaller_than_fakeImageVirtualSize.out.yaml
index 1b7d0fbdc..b23fffcc9 100755
--- a/pkg/libvirttools/TestRootVolumeSize__smaller_than_fakeImageVirtualSize.out.yaml
+++ b/pkg/libvirttools/TestRootVolumeSize__smaller_than_fakeImageVirtualSize.out.yaml
@@ -1,5 +1,5 @@
-- name: 'image: GetImagePathAndVirtualSize'
- value: rootfs image name
+- name: 'image: GetImagePathDigestAndVirtualSize'
+ value: fake/image1
- name: 'volumes: CreateStorageVol'
value: |-
diff --git a/pkg/libvirttools/block_volumesource.go b/pkg/libvirttools/block_volumesource.go
index 269fd84b0..be5ed4c75 100644
--- a/pkg/libvirttools/block_volumesource.go
+++ b/pkg/libvirttools/block_volumesource.go
@@ -59,6 +59,9 @@ func (v *blockVolume) Teardown() error {
func GetBlockVolumes(config *types.VMConfig, owner volumeOwner) ([]VMVolume, error) {
var vols []VMVolume
for _, dev := range config.VolumeDevices {
+ if dev.DevicePath == "/" {
+ continue
+ }
vols = append(vols, &blockVolume{
volumeBase: volumeBase{config, owner},
dev: dev,
diff --git a/pkg/libvirttools/cloudinit.go b/pkg/libvirttools/cloudinit.go
index b52763b6c..de833d1bf 100644
--- a/pkg/libvirttools/cloudinit.go
+++ b/pkg/libvirttools/cloudinit.go
@@ -506,6 +506,10 @@ func isRegularFile(path string) bool {
func (g *CloudInitGenerator) generateSymlinkScript(volumeMap diskPathMap) string {
var symlinkLines []string
for _, dev := range g.config.VolumeDevices {
+ if dev.DevicePath == "/" {
+ // special case for the persistent rootfs
+ continue
+ }
dpath, found := volumeMap[dev.UUID()]
if !found {
glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
@@ -523,6 +527,10 @@ func (g *CloudInitGenerator) generateSymlinkScript(volumeMap diskPathMap) string
func (g *CloudInitGenerator) fixMounts(volumeMap diskPathMap, mounts []interface{}) []interface{} {
devMap := make(map[string]string)
for _, dev := range g.config.VolumeDevices {
+ if dev.DevicePath == "/" {
+ // special case for the persistent rootfs
+ continue
+ }
dpath, found := volumeMap[dev.UUID()]
if !found {
glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath)
diff --git a/pkg/libvirttools/diag_test.go b/pkg/libvirttools/diag_test.go
index 399eff4cc..f478d9524 100644
--- a/pkg/libvirttools/diag_test.go
+++ b/pkg/libvirttools/diag_test.go
@@ -25,7 +25,7 @@ import (
)
func TestDump(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
diff --git a/pkg/libvirttools/gc_test.go b/pkg/libvirttools/gc_test.go
index deee99770..d2a93fc84 100644
--- a/pkg/libvirttools/gc_test.go
+++ b/pkg/libvirttools/gc_test.go
@@ -37,7 +37,7 @@ var (
)
func TestDomainCleanup(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
for _, uuid := range randomUUIDs {
@@ -74,7 +74,7 @@ func TestDomainCleanup(t *testing.T) {
}
func TestRootVolumesCleanup(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
pool, err := ct.virtTool.StoragePool()
@@ -116,7 +116,7 @@ func TestRootVolumesCleanup(t *testing.T) {
}
func TestQcow2VolumesCleanup(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
pool, err := ct.virtTool.StoragePool()
@@ -158,7 +158,7 @@ func TestQcow2VolumesCleanup(t *testing.T) {
}
func TestConfigISOsCleanup(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
directory, err := ioutil.TempDir("", "virtlet-tests-")
diff --git a/pkg/libvirttools/persistentroot_volumesource.go b/pkg/libvirttools/persistentroot_volumesource.go
new file mode 100644
index 000000000..214139a2b
--- /dev/null
+++ b/pkg/libvirttools/persistentroot_volumesource.go
@@ -0,0 +1,214 @@
+/*
+Copyright 2018 Mirantis
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package libvirttools
+
+import (
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ libvirtxml "github.com/libvirt/libvirt-go-xml"
+ digest "github.com/opencontainers/go-digest"
+
+ "github.com/Mirantis/virtlet/pkg/metadata/types"
+)
+
+const (
+ virtletRootfsMagic = 0x263dbe52ba576702
+ virtletRootfsMetadataVersion = 1
+ sectorSize = 512
+)
+
+type virtletRootfsHeader struct {
+ Magic uint64
+ MetadataVersion uint16
+ ImageHash [sha256.Size]byte
+}
+
+// persistentRootVolume represents a root volume that can survive the
+// deletion of its pod
+type persistentRootVolume struct {
+ volumeBase
+ dev types.VMVolumeDevice
+}
+
+var _ VMVolume = &persistentRootVolume{}
+
+func (v *persistentRootVolume) UUID() string {
+ return v.dev.UUID()
+}
+
+func (v *persistentRootVolume) dmName() string {
+ return "virtlet-dm-" + v.config.PodSandboxID
+}
+
+func (v *persistentRootVolume) dmPath() string {
+ return "/dev/mapper/" + v.dmName()
+}
+
+func (v *persistentRootVolume) ensureDevHeaderMatches(imageHash [sha256.Size]byte) (bool, error) {
+ f, err := os.OpenFile(v.dev.HostPath, os.O_RDWR|os.O_SYNC, 0)
+ if err != nil {
+ return false, fmt.Errorf("open %q: %v", v.dev.HostPath, err)
+ }
+ defer func() {
+ if f != nil {
+ f.Close()
+ }
+ }()
+
+ var hdr virtletRootfsHeader
+ if err := binary.Read(f, binary.BigEndian, &hdr); err != nil {
+ return false, fmt.Errorf("reading rootfs header: %v", err)
+ }
+
+ headerMatch := true
+ switch {
+ case hdr.Magic != virtletRootfsMagic || hdr.ImageHash != imageHash:
+ headerMatch = false
+ if _, err := f.Seek(0, os.SEEK_SET); err != nil {
+ return false, fmt.Errorf("seek: %v", err)
+ }
+ if err := binary.Write(f, binary.BigEndian, virtletRootfsHeader{
+ Magic: virtletRootfsMagic,
+ MetadataVersion: virtletRootfsMetadataVersion,
+ ImageHash: imageHash,
+ }); err != nil {
+ return false, fmt.Errorf("writing rootfs header: %v", err)
+ }
+ case hdr.MetadataVersion != virtletRootfsMetadataVersion:
+ // NOTE: we should handle earlier metadata versions
+ // after we introduce new ones. But we can't handle
+ // future metadata versions and any non-matching
+ // metadata versions are future ones currently, so we
+ // don't want to lose any data here.
+ return false, fmt.Errorf("unsupported virtlet root device metadata version %v", hdr.MetadataVersion)
+ }
+
+ if err := f.Close(); err != nil {
+ return false, fmt.Errorf("error closing rootfs device: %v", err)
+ }
+ f = nil
+ return headerMatch, nil
+}
+
+func (v *persistentRootVolume) blockDevSizeInSectors() (uint64, error) {
+ // NOTE: this is also doable via ioctl but this way it's
+ // shorter (no need for fake non-linux impl, extra interface,
+ // extra fake impl for it). Some links that may help if we
+ // decide to use the ioctl later on:
+ // https://github.com/karelzak/util-linux/blob/master/disk-utils/blockdev.c
+ // https://github.com/aicodix/smr/blob/24aa589f378827a69a07d220f114c169693dacec/smr.go#L29
+ out, err := v.owner.Commander().Command("blockdev", "--getsz", v.dev.HostPath).Run(nil)
+ if err != nil {
+ return 0, err
+ }
+ nSectors, err := strconv.ParseUint(strings.TrimSpace(string(out)), 10, 64)
+ if err != nil {
+ return 0, fmt.Errorf("bad size value returned by blockdev: %q: %v", out, err)
+ }
+ return nSectors, nil
+}
+
+func (v *persistentRootVolume) dmCmd(cmd []string, stdin string) error {
+ dmCmd := v.owner.Commander().Command(cmd[0], cmd[1:]...)
+ var stdinBytes []byte
+ if stdin != "" {
+ stdinBytes = []byte(stdin)
+ }
+ _, err := dmCmd.Run(stdinBytes)
+ return err
+}
+
+func (v *persistentRootVolume) dmSetup(imageSize uint64) error {
+ nSectors, err := v.blockDevSizeInSectors()
+ if err != nil {
+ return err
+ }
+ // sector 0 is reserved for the Virtlet metadata
+ minSectors := (imageSize+sectorSize-1)/sectorSize + 1
+ if nSectors < minSectors {
+ return fmt.Errorf("block device too small for the image: need at least %d bytes (%d sectors) but got %d bytes (%d sectors)",
+ minSectors*sectorSize,
+ minSectors,
+ nSectors*sectorSize,
+ nSectors)
+ }
+ hostPath, err := filepath.EvalSymlinks(v.dev.HostPath)
+ if err != nil {
+ return err
+ }
+ dmTable := fmt.Sprintf("0 %d linear %s 1\n", nSectors-1, hostPath)
+ dmCmd := v.owner.Commander().Command("dmsetup", "create", v.dmName())
+ _, err = dmCmd.Run([]byte(dmTable))
+ return err
+}
+
+func (v *persistentRootVolume) copyImageToDev(imagePath string) error {
+ _, err := v.owner.Commander().Command("qemu-img", "convert", "-O", "raw", imagePath, v.dmPath()).Run(nil)
+ return err
+}
+
+func (v *persistentRootVolume) Setup() (*libvirtxml.DomainDisk, *libvirtxml.DomainFilesystem, error) {
+ imagePath, imageDigest, imageSize, err := v.owner.ImageManager().GetImagePathDigestAndVirtualSize(v.config.Image)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if imageDigest.Algorithm() != digest.SHA256 {
+ return nil, nil, fmt.Errorf("unsupported digest algorithm %q", imageDigest.Algorithm())
+ }
+ imageHash, err := hex.DecodeString(imageDigest.Hex())
+ if err != nil {
+ return nil, nil, fmt.Errorf("bad digest hex: %q", imageDigest.Hex())
+ }
+ if len(imageHash) != sha256.Size {
+ return nil, nil, fmt.Errorf("bad digest size: %q", imageDigest.Hex())
+ }
+
+ var hash [sha256.Size]byte
+ copy(hash[:], imageHash)
+ headerMatches, err := v.ensureDevHeaderMatches(hash)
+
+ if err == nil {
+ err = v.dmSetup(imageSize)
+ }
+
+ if err == nil && !headerMatches {
+ err = v.copyImageToDev(imagePath)
+ }
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return &libvirtxml.DomainDisk{
+ Device: "disk",
+ Source: &libvirtxml.DomainDiskSource{Block: &libvirtxml.DomainDiskSourceBlock{Dev: v.dmPath()}}, //hostPath}},
+ Driver: &libvirtxml.DomainDiskDriver{Name: "qemu", Type: "raw"},
+ }, nil, nil
+}
+
+func (v *persistentRootVolume) Teardown() error {
+ _, err := v.owner.Commander().Command("dmsetup", "remove", v.dmName()).Run(nil)
+ return err
+}
diff --git a/pkg/libvirttools/persistentroot_volumesource_test.go b/pkg/libvirttools/persistentroot_volumesource_test.go
new file mode 100644
index 000000000..766975e90
--- /dev/null
+++ b/pkg/libvirttools/persistentroot_volumesource_test.go
@@ -0,0 +1,244 @@
+/*
+Copyright 2018 Mirantis
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package libvirttools
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/Mirantis/virtlet/tests/gm"
+ digest "github.com/opencontainers/go-digest"
+
+ "github.com/Mirantis/virtlet/pkg/metadata/types"
+ fakeutils "github.com/Mirantis/virtlet/pkg/utils/fake"
+ testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
+)
+
+func TestPersistentRootVolume(t *testing.T) {
+ fakeImages := []fakeImageSpec{
+ {
+ name: "persistent/image1",
+ path: "/fake/path1",
+ digest: digest.Digest("sha256:12b05d23a781e4aae1ab9a7de27721cbd1f1d666cfb4e21ab31338eb96eb1e3f"),
+ size: 8192,
+ },
+ {
+ name: "persistent/image2",
+ path: "/fake/path2",
+ digest: digest.Digest("sha256:d66ab8e0ea2931d41e27ba4f1d9c007a1d43ab883158a8a22f90872a8d9bb0e3"),
+ size: 10000,
+ },
+ }
+ for _, tc := range []struct {
+ name string
+ imageName string
+ secondImageName string
+ dmPath string
+ fileSize uint64
+ imageWrittenAgain bool
+ useSymlink bool
+ errors [2]string
+ }{
+ {
+ name: "image unchanged",
+ imageName: "persistent/image1",
+ secondImageName: "persistent/image1",
+ fileSize: 8704, // just added a sector
+ },
+ {
+ name: "image change",
+ imageName: "persistent/image1",
+ secondImageName: "persistent/image2",
+ fileSize: 16384,
+ imageWrittenAgain: true,
+ },
+ {
+ name: "first image too big",
+ imageName: "persistent/image1",
+ fileSize: 4096,
+ errors: [2]string{
+ "too small",
+ "",
+ },
+ },
+ {
+ name: "second image too big",
+ imageName: "persistent/image1",
+ secondImageName: "persistent/image2",
+ fileSize: 8704,
+ errors: [2]string{
+ "",
+ "too small",
+ },
+ },
+ {
+ name: "symlinks",
+ imageName: "persistent/image1",
+ secondImageName: "persistent/image1",
+ fileSize: 8704,
+ useSymlink: true,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := testutils.NewToplevelRecorder()
+ im := newFakeImageManager(rec.Child("image"), fakeImages...)
+ if tc.fileSize%512 != 0 {
+ t.Fatalf("block device size must be a multiple of 512")
+ }
+
+ tmpDir, err := ioutil.TempDir("", "fake-persistent-rootfs")
+ if err != nil {
+ t.Fatalf("TempDir(): %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+ fakeDevDir := filepath.Join(tmpDir, "__dev__")
+ if err := os.Mkdir(fakeDevDir, 0777); err != nil {
+ t.Fatalf("Mkdir(): %v", err)
+ }
+
+ devPath := filepath.Join(fakeDevDir, "rootdev")
+ devFile, err := os.Create(devPath)
+ if err != nil {
+ t.Fatalf("Create(): %v", err)
+ }
+ if _, err := devFile.Write(make([]byte, tc.fileSize)); err != nil {
+ devFile.Close()
+ t.Fatalf("Write(): %v", err)
+ }
+ if err := devFile.Close(); err != nil {
+ t.Fatalf("devFile.Close()")
+ }
+ devPathToUse := devPath
+ if tc.useSymlink {
+ devPathToUse := filepath.Join(fakeDevDir, "rootdevlink")
+ if err := os.Symlink(devPath, devPathToUse); err != nil {
+ t.Fatalf("Symlink(): %v", err)
+ }
+ }
+
+ for n, imageName := range []string{tc.imageName, tc.secondImageName} {
+ if imageName == "" {
+ continue
+ }
+ cmdSpecs := []fakeutils.CmdSpec{
+ {
+ Match: "blockdev --getsz",
+ Stdout: strconv.Itoa(int(tc.fileSize / 512)),
+ },
+ {
+ Match: "dmsetup create",
+ },
+ {
+ Match: "dmsetup remove",
+ },
+ }
+ if n == 0 || tc.imageWrittenAgain {
+ // qemu-img convert is used to write the image to the block device.
+ // It should only be called if the image changes.
+ cmdSpecs = append(cmdSpecs, fakeutils.CmdSpec{
+ Match: "qemu-img convert",
+ })
+ }
+ cmd := fakeutils.NewFakeCommander(rec, cmdSpecs)
+ cmd.ReplaceTempPath("__dev__", "/dev")
+ owner := newFakeVolumeOwner(nil, im, cmd)
+ rootVol := getPersistentRootVolume(t, imageName, devPathToUse, owner)
+ verifyRootVolumeSetup(t, rec, rootVol, tc.errors[n])
+ if tc.errors[n] == "" {
+ verifyRootVolumeTeardown(t, rec, rootVol)
+ }
+ }
+ gm.Verify(t, gm.NewYamlVerifier(rec.Content()))
+ })
+ }
+}
+
+func verifyRootVolumeSetup(t *testing.T, rec testutils.Recorder, rootVol *persistentRootVolume, expectedError string) {
+ rec.Rec("setup", nil)
+ vol, fs, err := rootVol.Setup()
+ if expectedError == "" {
+ if err != nil {
+ t.Fatalf("Setup returned an unexpected error: %v", err)
+ }
+ } else {
+ switch {
+ case err == nil:
+ t.Errorf("Setup didn't return the expected error")
+ case !strings.Contains(err.Error(), expectedError):
+ t.Errorf("Setup returned a wrong error message %q (must contain %q)", err, expectedError)
+ }
+ return
+ }
+
+ if fs != nil {
+ t.Errorf("Didn't expect a filesystem")
+ }
+
+ if vol.Source.Block == nil {
+ t.Errorf("Expected 'block' volume type")
+ }
+
+ if vol.Device != "disk" {
+ t.Errorf("Expected 'disk' as volume device, received: %s", vol.Device)
+ }
+
+ expectedDmPath := "/dev/mapper/virtlet-dm-" + testUUID
+ if vol.Source.Block.Dev != expectedDmPath {
+ t.Errorf("Expected '%s' as root block device path, received: %s", expectedDmPath, vol.Source.Block.Dev)
+ }
+
+ out, err := vol.Marshal()
+ if err != nil {
+ t.Fatalf("error marshalling the volume: %v", err)
+ }
+ rec.Rec("end setup -- root disk", out)
+}
+
+func verifyRootVolumeTeardown(t *testing.T, rec testutils.Recorder, rootVol *persistentRootVolume) {
+ rec.Rec("teardown", nil)
+ if err := rootVol.Teardown(); err != nil {
+ t.Fatalf("Teardown(): %v", err)
+ }
+ rec.Rec("end teardown", nil)
+}
+
+func getPersistentRootVolume(t *testing.T, imageName, devHostPath string, owner volumeOwner) *persistentRootVolume {
+ volumes, err := GetRootVolume(
+ &types.VMConfig{
+ PodSandboxID: testUUID,
+ Image: imageName,
+ VolumeDevices: []types.VMVolumeDevice{
+ {
+ DevicePath: "/",
+ HostPath: devHostPath,
+ },
+ },
+ }, owner)
+ if err != nil {
+ t.Fatalf("GetRootVolume returned an error: %v", err)
+ }
+
+ if len(volumes) != 1 {
+ t.Fatalf("GetRootVolumes returned non single number of volumes: %d", len(volumes))
+ }
+
+ return volumes[0].(*persistentRootVolume)
+}
diff --git a/pkg/libvirttools/qcow2_flexvolume_test.go b/pkg/libvirttools/qcow2_flexvolume_test.go
index 413411d8d..1aae41eed 100644
--- a/pkg/libvirttools/qcow2_flexvolume_test.go
+++ b/pkg/libvirttools/qcow2_flexvolume_test.go
@@ -24,6 +24,7 @@ import (
libvirtxml "github.com/libvirt/libvirt-go-xml"
"github.com/Mirantis/virtlet/pkg/metadata/types"
+ fakeutils "github.com/Mirantis/virtlet/pkg/utils/fake"
testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
"github.com/Mirantis/virtlet/pkg/virt/fake"
"github.com/Mirantis/virtlet/tests/gm"
@@ -73,7 +74,7 @@ func TestQCOW2VolumeLifeCycle(t *testing.T) {
Target: &libvirtxml.StoragePoolTarget{Path: volumesPoolPath},
})
- im := NewFakeImageManager(rec.Child("image"))
+ im := newFakeImageManager(rec.Child("image"))
optsFilePath, err := prepareOptsFileForQcow2Volume()
if err != nil {
@@ -85,7 +86,7 @@ func TestQCOW2VolumeLifeCycle(t *testing.T) {
TestVolumeName,
optsFilePath,
&types.VMConfig{DomainUUID: testUUID, Image: "rootfs image name"},
- newFakeVolumeOwner(spool, im),
+ newFakeVolumeOwner(spool, im, fakeutils.NewFakeCommander(nil, nil)),
)
if err != nil {
t.Fatalf("newQCOW2Volume returned an error: %v", err)
diff --git a/pkg/libvirttools/root_volumesource.go b/pkg/libvirttools/root_volumesource.go
index 99a55b370..de379df5b 100644
--- a/pkg/libvirttools/root_volumesource.go
+++ b/pkg/libvirttools/root_volumesource.go
@@ -34,11 +34,22 @@ var _ VMVolume = &rootVolume{}
// GetRootVolume returns volume source for root volume clone.
func GetRootVolume(config *types.VMConfig, owner volumeOwner) ([]VMVolume, error) {
- return []VMVolume{
- &rootVolume{
+ var vol VMVolume
+ for _, dev := range config.VolumeDevices {
+ if dev.DevicePath == "/" {
+ vol = &persistentRootVolume{
+ volumeBase: volumeBase{config, owner},
+ dev: dev,
+ }
+ break
+ }
+ }
+ if vol == nil {
+ vol = &rootVolume{
volumeBase{config, owner},
- },
- }, nil
+ }
+ }
+ return []VMVolume{vol}, nil
}
func (v *rootVolume) volumeName() string {
@@ -46,7 +57,7 @@ func (v *rootVolume) volumeName() string {
}
func (v *rootVolume) createVolume() (virt.StorageVolume, error) {
- imagePath, virtualSize, err := v.owner.ImageManager().GetImagePathAndVirtualSize(v.config.Image)
+ imagePath, _, virtualSize, err := v.owner.ImageManager().GetImagePathDigestAndVirtualSize(v.config.Image)
if err != nil {
return nil, err
}
diff --git a/pkg/libvirttools/root_volumesource_test.go b/pkg/libvirttools/root_volumesource_test.go
index 88f85d956..40207e8c0 100644
--- a/pkg/libvirttools/root_volumesource_test.go
+++ b/pkg/libvirttools/root_volumesource_test.go
@@ -17,12 +17,15 @@ limitations under the License.
package libvirttools
import (
+ "fmt"
"testing"
libvirtxml "github.com/libvirt/libvirt-go-xml"
+ digest "github.com/opencontainers/go-digest"
"github.com/Mirantis/virtlet/pkg/metadata/types"
"github.com/Mirantis/virtlet/pkg/utils"
+ fakeutils "github.com/Mirantis/virtlet/pkg/utils/fake"
testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
"github.com/Mirantis/virtlet/pkg/virt"
"github.com/Mirantis/virtlet/pkg/virt/fake"
@@ -32,23 +35,46 @@ import (
const (
testUUID = "77f29a0e-46af-4188-a6af-9ff8b8a65224"
fakeImageVirtualSize = 424242
+ fakeImageDigest = digest.Digest("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")
)
-type FakeImageManager struct {
- rec testutils.Recorder
+type fakeImageSpec struct {
+ name string
+ path string
+ digest digest.Digest
+ size uint64
}
-var _ ImageManager = &FakeImageManager{}
+type fakeImageManager struct {
+ rec testutils.Recorder
+ imageMap map[string]fakeImageSpec
+}
-func NewFakeImageManager(rec testutils.Recorder) *FakeImageManager {
- return &FakeImageManager{
- rec: rec,
+var _ ImageManager = &fakeImageManager{}
+
+func newFakeImageManager(rec testutils.Recorder, extraImages ...fakeImageSpec) *fakeImageManager {
+ m := make(map[string]fakeImageSpec)
+ for _, img := range append(extraImages, fakeImageSpec{
+ name: "fake/image1",
+ path: "/fake/volume/path",
+ digest: fakeImageDigest,
+ size: fakeImageVirtualSize,
+ }) {
+ m[img.name] = img
+ }
+ return &fakeImageManager{
+ rec: rec,
+ imageMap: m,
}
}
-func (im *FakeImageManager) GetImagePathAndVirtualSize(imageName string) (string, uint64, error) {
- im.rec.Rec("GetImagePathAndVirtualSize", imageName)
- return "/fake/volume/path", fakeImageVirtualSize, nil
+func (im *fakeImageManager) GetImagePathDigestAndVirtualSize(imageName string) (string, digest.Digest, uint64, error) {
+ im.rec.Rec("GetImagePathDigestAndVirtualSize", imageName)
+ spec, found := im.imageMap[imageName]
+ if !found {
+ return "", "", 0, fmt.Errorf("image %q not found", imageName)
+ }
+ return spec.path, spec.digest, spec.size, nil
}
func TestRootVolumeNaming(t *testing.T) {
@@ -72,12 +98,11 @@ func getRootVolumeForTest(t *testing.T, vmConfig *types.VMConfig) (*rootVolume,
Name: "volumes",
Target: &libvirtxml.StoragePoolTarget{Path: volumesPoolPath},
})
- im := NewFakeImageManager(rec.Child("image"))
+ im := newFakeImageManager(rec.Child("image"))
volumes, err := GetRootVolume(
vmConfig,
- newFakeVolumeOwner(spool, im),
- )
+ newFakeVolumeOwner(spool, im, fakeutils.NewFakeCommander(nil, nil)))
if err != nil {
t.Fatalf("GetRootVolume returned an error: %v", err)
}
@@ -124,7 +149,7 @@ func TestRootVolumeSize(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rootVol, rec, spool := getRootVolumeForTest(t, &types.VMConfig{
DomainUUID: testUUID,
- Image: "rootfs image name",
+ Image: "fake/image1",
ParsedAnnotations: &types.VirtletAnnotations{
RootVolumeSize: tc.specifiedRootVolumeSize,
},
@@ -157,14 +182,18 @@ func TestRootVolumeLifeCycle(t *testing.T) {
expectedRootVolumePath := "/fake/volumes/pool/virtlet_root_" + testUUID
rootVol, rec, _ := getRootVolumeForTest(t, &types.VMConfig{
DomainUUID: testUUID,
- Image: "rootfs image name",
+ Image: "fake/image1",
})
- vol, _, err := rootVol.Setup()
+ vol, fs, err := rootVol.Setup()
if err != nil {
t.Fatalf("Setup returned an error: %v", err)
}
+ if fs != nil {
+ t.Errorf("Didn't expect a filesystem")
+ }
+
if vol.Source.File == nil {
t.Errorf("Expected 'file' volume type")
}
@@ -192,15 +221,17 @@ func TestRootVolumeLifeCycle(t *testing.T) {
type fakeVolumeOwner struct {
storagePool *fake.FakeStoragePool
- imageManager *FakeImageManager
+ imageManager *fakeImageManager
+ commander *fakeutils.FakeCommander
}
var _ volumeOwner = fakeVolumeOwner{}
-func newFakeVolumeOwner(storagePool *fake.FakeStoragePool, imageManager *FakeImageManager) *fakeVolumeOwner {
+func newFakeVolumeOwner(storagePool *fake.FakeStoragePool, imageManager *fakeImageManager, commander *fakeutils.FakeCommander) *fakeVolumeOwner {
return &fakeVolumeOwner{
storagePool: storagePool,
imageManager: imageManager,
+ commander: commander,
}
}
@@ -225,3 +256,5 @@ func (vo fakeVolumeOwner) VolumePoolName() string { return "" }
func (vo fakeVolumeOwner) Mounter() utils.Mounter { return utils.NullMounter }
func (vo fakeVolumeOwner) SharedFilesystemPath() string { return "/var/lib/virtlet/fs" }
+
+func (vo fakeVolumeOwner) Commander() utils.Commander { return vo.commander }
diff --git a/pkg/libvirttools/virtualization.go b/pkg/libvirttools/virtualization.go
index d7865e479..aa70be2d2 100644
--- a/pkg/libvirttools/virtualization.go
+++ b/pkg/libvirttools/virtualization.go
@@ -224,6 +224,7 @@ type VirtualizationTool struct {
volumeSource VMVolumeSource
config VirtualizationConfig
mounter utils.Mounter
+ commander utils.Commander
}
var _ volumeOwner = &VirtualizationTool{}
@@ -233,7 +234,8 @@ var _ volumeOwner = &VirtualizationTool{}
func NewVirtualizationTool(domainConn virt.DomainConnection,
storageConn virt.StorageConnection, imageManager ImageManager,
metadataStore metadata.Store, volumeSource VMVolumeSource,
- config VirtualizationConfig, mounter utils.Mounter) *VirtualizationTool {
+ config VirtualizationConfig, mounter utils.Mounter,
+ commander utils.Commander) *VirtualizationTool {
return &VirtualizationTool{
domainConn: domainConn,
storageConn: storageConn,
@@ -243,6 +245,7 @@ func NewVirtualizationTool(domainConn virt.DomainConnection,
volumeSource: volumeSource,
config: config,
mounter: mounter,
+ commander: commander,
}
}
@@ -837,6 +840,9 @@ func (v *VirtualizationTool) Mounter() utils.Mounter { return v.mounter }
// SharedFilesystemPath implements volumeOwner SharedFilesystemPath method
func (v *VirtualizationTool) SharedFilesystemPath() string { return v.config.SharedFilesystemPath }
+// Commander implements volumeOwner Commander method
+func (v *VirtualizationTool) Commander() utils.Commander { return v.commander }
+
func filterContainer(containerInfo *types.ContainerInfo, filter types.ContainerFilter) bool {
if filter.Id != "" && containerInfo.Id != filter.Id {
return false
diff --git a/pkg/libvirttools/virtualization_test.go b/pkg/libvirttools/virtualization_test.go
index 213840b60..625c402f1 100644
--- a/pkg/libvirttools/virtualization_test.go
+++ b/pkg/libvirttools/virtualization_test.go
@@ -33,6 +33,7 @@ import (
fakemeta "github.com/Mirantis/virtlet/pkg/metadata/fake"
"github.com/Mirantis/virtlet/pkg/metadata/types"
"github.com/Mirantis/virtlet/pkg/utils"
+ fakeutils "github.com/Mirantis/virtlet/pkg/utils/fake"
testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
"github.com/Mirantis/virtlet/pkg/virt/fake"
"github.com/Mirantis/virtlet/tests/gm"
@@ -59,7 +60,7 @@ type containerTester struct {
metadataStore metadata.Store
}
-func newContainerTester(t *testing.T, rec *testutils.TopLevelRecorder) *containerTester {
+func newContainerTester(t *testing.T, rec *testutils.TopLevelRecorder, cmds []fakeutils.CmdSpec) *containerTester {
ct := &containerTester{
t: t,
clock: clockwork.NewFakeClockAt(time.Date(2017, 5, 30, 20, 19, 0, 0, time.UTC)),
@@ -83,7 +84,7 @@ func newContainerTester(t *testing.T, rec *testutils.TopLevelRecorder) *containe
t.Fatalf("Failed to create fake bolt client: %v", err)
}
- imageManager := NewFakeImageManager(ct.rec)
+ imageManager := newFakeImageManager(ct.rec)
ct.kubeletRootDir = filepath.Join(ct.tmpDir, "kubelet-root")
virtConfig := VirtualizationConfig{
VolumePoolName: "volumes",
@@ -91,7 +92,11 @@ func newContainerTester(t *testing.T, rec *testutils.TopLevelRecorder) *containe
KubeletRootDir: ct.kubeletRootDir,
StreamerSocketPath: "/var/lib/libvirt/streamer.sock",
}
- ct.virtTool = NewVirtualizationTool(ct.domainConn, ct.storageConn, imageManager, ct.metadataStore, GetDefaultVolumeSource(), virtConfig, utils.NullMounter)
+ fakeCommander := fakeutils.NewFakeCommander(rec, cmds)
+ fakeCommander.ReplaceTempPath("__pods__", "/fakedev")
+ ct.virtTool = NewVirtualizationTool(
+ ct.domainConn, ct.storageConn, imageManager, ct.metadataStore,
+ GetDefaultVolumeSource(), virtConfig, utils.NullMounter, fakeCommander)
ct.virtTool.SetClock(ct.clock)
return ct
@@ -181,7 +186,7 @@ func (ct *containerTester) verifyContainerRootfsExists(containerInfo *types.Cont
}
func TestContainerLifecycle(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
@@ -263,7 +268,7 @@ func TestContainerLifecycle(t *testing.T) {
}
func TestDomainForcedShutdown(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
@@ -298,7 +303,7 @@ func TestDomainForcedShutdown(t *testing.T) {
}
func TestDoubleStartError(t *testing.T) {
- ct := newContainerTester(t, testutils.NewToplevelRecorder())
+ ct := newContainerTester(t, testutils.NewToplevelRecorder(), nil)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
@@ -320,6 +325,7 @@ type volMount struct {
type volDevice struct {
name string
devicePath string
+ size int
}
func TestDomainDefinitions(t *testing.T) {
@@ -333,6 +339,7 @@ func TestDomainDefinitions(t *testing.T) {
flexVolumes map[string]map[string]interface{}
mounts []volMount
volDevs []volDevice
+ cmds []fakeutils.CmdSpec
}{
{
name: "plain domain",
@@ -418,11 +425,36 @@ func TestDomainDefinitions(t *testing.T) {
"VirtletDiskDriver": "virtio",
},
},
+ {
+ name: "persistent rootfs",
+ volDevs: []volDevice{
+ {
+ name: "root",
+ devicePath: "/",
+ size: 512000,
+ },
+ },
+ cmds: []fakeutils.CmdSpec{
+ {
+ Match: "blockdev --getsz",
+ Stdout: "1000",
+ },
+ {
+ Match: "qemu-img convert",
+ },
+ {
+ Match: "dmsetup create",
+ },
+ {
+ Match: "dmsetup remove",
+ },
+ },
+ },
} {
t.Run(tc.name, func(t *testing.T) {
rec := testutils.NewToplevelRecorder()
- ct := newContainerTester(t, rec)
+ ct := newContainerTester(t, rec, tc.cmds)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
@@ -458,10 +490,17 @@ func TestDomainDefinitions(t *testing.T) {
t.Fatal(err)
}
hostPath := filepath.Join(baseDir, d.name)
- if f, err := os.OpenFile(hostPath, os.O_RDONLY|os.O_CREATE, 0666); err != nil {
+ if f, err := os.Create(hostPath); err != nil {
t.Fatal(err)
} else {
- f.Close()
+ if d.size != 0 {
+ if _, err := f.Write(make([]byte, d.size)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := f.Close(); err != nil {
+ t.Fatal(err)
+ }
}
volDevs = append(volDevs, types.VMVolumeDevice{
DevicePath: d.devicePath,
@@ -489,7 +528,7 @@ func TestDomainResourceConstraints(t *testing.T) {
rec := testutils.NewToplevelRecorder()
rec.AddFilter("DefineDomain")
- ct := newContainerTester(t, rec)
+ ct := newContainerTester(t, rec, nil)
defer ct.teardown()
sandbox := fakemeta.GetSandboxes(1)[0]
sandbox.Annotations = map[string]string{
diff --git a/pkg/libvirttools/volumes.go b/pkg/libvirttools/volumes.go
index d4f496073..2b15f7b31 100644
--- a/pkg/libvirttools/volumes.go
+++ b/pkg/libvirttools/volumes.go
@@ -18,6 +18,7 @@ package libvirttools
import (
libvirtxml "github.com/libvirt/libvirt-go-xml"
+ digest "github.com/opencontainers/go-digest"
"github.com/Mirantis/virtlet/pkg/metadata/types"
"github.com/Mirantis/virtlet/pkg/utils"
@@ -26,7 +27,7 @@ import (
// ImageManager describes a images info provider.
type ImageManager interface {
- GetImagePathAndVirtualSize(ref string) (string, uint64, error)
+ GetImagePathDigestAndVirtualSize(ref string) (string, digest.Digest, uint64, error)
}
type volumeOwner interface {
@@ -38,6 +39,7 @@ type volumeOwner interface {
VolumePoolName() string
Mounter() utils.Mounter
SharedFilesystemPath() string
+ Commander() utils.Commander
}
// VMVolumeSource is a function that provides `VMVolume`s for VMs
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
index 57d8bc405..3dbfa5d2c 100644
--- a/pkg/manager/manager.go
+++ b/pkg/manager/manager.go
@@ -135,7 +135,7 @@ func (v *VirtletManager) Run() error {
}
volSrc := libvirttools.GetDefaultVolumeSource()
- v.virtTool = libvirttools.NewVirtualizationTool(conn, conn, v.imageStore, v.metadataStore, volSrc, virtConfig, utils.NewMounter())
+ v.virtTool = libvirttools.NewVirtualizationTool(conn, conn, v.imageStore, v.metadataStore, volSrc, virtConfig, utils.NewMounter(), utils.DefaultCommander)
runtimeService := NewVirtletRuntimeService(v.virtTool, v.metadataStore, v.fdManager, streamServer, v.imageStore, nil)
imageService := NewVirtletImageService(v.imageStore, translator, nil)
diff --git a/pkg/manager/runtime_test.go b/pkg/manager/runtime_test.go
index f02db3789..091ea76c1 100644
--- a/pkg/manager/runtime_test.go
+++ b/pkg/manager/runtime_test.go
@@ -45,6 +45,7 @@ import (
"github.com/Mirantis/virtlet/pkg/network"
"github.com/Mirantis/virtlet/pkg/tapmanager"
"github.com/Mirantis/virtlet/pkg/utils"
+ fakeutils "github.com/Mirantis/virtlet/pkg/utils/fake"
testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
fakevirt "github.com/Mirantis/virtlet/pkg/virt/fake"
"github.com/Mirantis/virtlet/tests/criapi"
@@ -236,7 +237,8 @@ func makeVirtletCRITester(t *testing.T) *virtletCRITester {
KubeletRootDir: kubeletRootDir,
StreamerSocketPath: streamerSocketPath,
}
- virtTool := libvirttools.NewVirtualizationTool(domainConn, storageConn, imageStore, metadataStore, libvirttools.GetDefaultVolumeSource(), virtConfig, utils.NullMounter)
+ commander := fakeutils.NewFakeCommander(rec, nil)
+ virtTool := libvirttools.NewVirtualizationTool(domainConn, storageConn, imageStore, metadataStore, libvirttools.GetDefaultVolumeSource(), virtConfig, utils.NullMounter, commander)
virtTool.SetClock(clock)
streamServer := newFakeStreamServer(rec.Child("streamServer"))
criHandler := &criHandler{
diff --git a/pkg/utils/command.go b/pkg/utils/command.go
new file mode 100644
index 000000000..d781a75da
--- /dev/null
+++ b/pkg/utils/command.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2018 Mirantis
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+ "bytes"
+ "fmt"
+ "os/exec"
+ "strings"
+)
+
+// Command represents an external command being prepared or run.
+type Command interface {
+ // Output runs the command and returns its standard output.
+ // If stdin is non-nil, it's passed to the command's standed
+ // input. Any returned error will usually be of type
+ // *ExitError with ExitError.Stderr containing the stderr
+ // output of the command.
+ Run(stdin []byte) ([]byte, error)
+}
+
+// Commander is used to run external commands.
+type Commander interface {
+ // Command returns an isntance of Command to execute the named
+ // program with the given arguments.
+ Command(name string, arg ...string) Command
+}
+
+type realCommand struct {
+ *exec.Cmd
+}
+
+func (c realCommand) Run(stdin []byte) ([]byte, error) {
+ if stdin != nil {
+ c.Stdin = bytes.NewBuffer(stdin)
+ }
+ out, err := c.Cmd.Output()
+ if err == nil {
+ return out, nil
+ }
+ fullCmd := strings.Join(append([]string{c.Path}, c.Args...), " ")
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ return out, fmt.Errorf("command %s: %v: %s", fullCmd, exitErr, exitErr.Stderr)
+ }
+ return out, fmt.Errorf("command %s: %v", fullCmd, err)
+}
+
+type defaultCommander struct{}
+
+func (c *defaultCommander) Command(name string, arg ...string) Command {
+ return realCommand{exec.Command(name, arg...)}
+}
+
+// DefaultCommander is an implementation of Commander that runs the
+// commands using exec.Command.
+var DefaultCommander Commander = &defaultCommander{}
diff --git a/pkg/utils/command_test.go b/pkg/utils/command_test.go
new file mode 100644
index 000000000..8cdab4252
--- /dev/null
+++ b/pkg/utils/command_test.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2018 Mirantis
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestCommand(t *testing.T) {
+ for _, tc := range []struct {
+ cmd []string
+ stdin []byte
+ expectedOutput string
+ expectedError string
+ }{
+ {
+ cmd: []string{"echo", "-n", "foobar"},
+ stdin: nil,
+ expectedOutput: "foobar",
+ },
+ {
+ cmd: []string{"/bin/bash", "-c", "echo -n >&2 'stderr here'; echo -n 'stdout here'; exit 1"},
+ stdin: nil,
+ expectedOutput: "stdout here",
+ expectedError: "stderr here",
+ },
+ {
+ cmd: []string{"sed", "s/foo/bar/g"},
+ stdin: []byte("here is foo; foo."),
+ expectedOutput: "here is bar; bar.",
+ },
+ } {
+ t.Run(strings.Join(tc.cmd, " "), func(t *testing.T) {
+ c := DefaultCommander.Command(tc.cmd[0], tc.cmd[1:]...)
+ switch out, err := c.Run(tc.stdin); {
+ case tc.expectedError == "" && err != nil:
+ t.Errorf("Command error: %v", err)
+ case tc.expectedError != "" && err == nil:
+ t.Errorf("Didn't get the expected error")
+ case tc.expectedError != "" && !strings.Contains(err.Error(), tc.expectedError):
+ t.Errorf("Bad error message %q (no substring %q)", err.Error(), tc.expectedError)
+ case tc.expectedError != "" && !strings.Contains(err.Error(), tc.cmd[0]):
+ t.Errorf("Bad error message %q (no substring %q)", err.Error(), tc.cmd[0])
+ case string(out) != tc.expectedOutput:
+ t.Errorf("Command output mismatch: %q instead of %q", out, tc.expectedOutput)
+ }
+ })
+ }
+}
diff --git a/pkg/utils/fake/command.go b/pkg/utils/fake/command.go
new file mode 100644
index 000000000..af34d5ac2
--- /dev/null
+++ b/pkg/utils/fake/command.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2018 Mirantis
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/Mirantis/virtlet/pkg/utils"
+ testutils "github.com/Mirantis/virtlet/pkg/utils/testing"
+)
+
+type CmdSpec struct {
+ Match string
+ Stdout string
+}
+
+type fakeCommand struct {
+ rec testutils.Recorder
+ cmd []string
+ commander *FakeCommander
+}
+
+var _ utils.Command = &fakeCommand{}
+
+func (c *fakeCommand) subst(text string) string {
+ if c.commander.replacePath == nil {
+ return text
+ }
+ return c.commander.replacePath.ReplaceAllString(text, c.commander.replacement)
+}
+
+func (c *fakeCommand) Run(stdin []byte) ([]byte, error) {
+ fullCmd := c.subst(strings.Join(c.cmd, " "))
+ r := map[string]string{
+ "cmd": fullCmd,
+ }
+ defer c.rec.Rec("CMD", r)
+ for _, spec := range c.commander.specs {
+ matched, err := regexp.MatchString(spec.Match, fullCmd)
+ if err != nil {
+ return nil, fmt.Errorf("failed to match regexp %q: %v", spec.Match, err)
+ }
+ if matched {
+ if c.rec != nil {
+ if stdin != nil {
+ r["stdin"] = c.subst(string(stdin))
+ }
+ if spec.Stdout != "" {
+ r["stdout"] = spec.Stdout
+ }
+ }
+ return []byte(spec.Stdout), nil
+ }
+ }
+ return nil, fmt.Errorf("unexpected command %q", fullCmd)
+}
+
+// FakeCommander records the commands instead of executing them. It
+// also provides stdout based on a table of (cmd_regexp, stdout)
+// pairs. The regexp is matched against the command and its arguments
+// joined with " ".
+type FakeCommander struct {
+ rec testutils.Recorder
+ specs []CmdSpec
+ replacePath *regexp.Regexp
+ replacement string
+}
+
+var _ utils.Commander = &FakeCommander{}
+
+// NewFakeCommander creates a new FakeCommander.
+// If rec is nil, all the commands will not be recorded
+func NewFakeCommander(rec testutils.Recorder, specs []CmdSpec) *FakeCommander {
+ return &FakeCommander{rec: rec, specs: specs}
+}
+
+// ReplaceTempPath makes the commander replace the path with specified
+// suffix with the specified string. The replacement is done on the
+// word boundary.
+func (c *FakeCommander) ReplaceTempPath(pathSuffix, replacement string) {
+ c.replacePath = regexp.MustCompile(`\S*` + regexp.QuoteMeta(pathSuffix))
+ c.replacement = replacement
+}
+
+// Command implements the Command method of FakeCommander interface.
+func (c *FakeCommander) Command(name string, arg ...string) utils.Command {
+ return &fakeCommand{
+ rec: c.rec,
+ cmd: append([]string{name}, arg...),
+ commander: c,
+ }
+}