diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 562fbacd7..2688669e8 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -55,17 +55,6 @@ steps: - make test-in-docker timeout_in_minutes: 10 - - label: ":rotating_light: :hammer: snapshotter *root* tests" - agents: - queue: "${BUILDKITE_AGENT_META_DATA_QUEUE:-default}" - env: - DOCKER_IMAGE_TAG: "$BUILDKITE_BUILD_NUMBER" - EXTRAGOARGS: "-v -count=1 -race" - command: 'make -C snapshotter integ-test' - timeout_in_minutes: 10 - concurrency: 1 - concurrency_group: 'loop-device test' - - label: ":rotating_light: :running_shirt_with_sash: runtime isolated tests" agents: queue: "${BUILDKITE_AGENT_META_DATA_QUEUE:-default}" @@ -78,19 +67,6 @@ steps: - make -C runtime integ-test FICD_SNAPSHOTTER=devmapper FICD_DM_POOL=build_${BUILDKITE_BUILD_NUMBER}_runtime timeout_in_minutes: 10 - - label: ":rotating_light: :exclamation: example tests (naive)" - agents: - queue: "${BUILDKITE_AGENT_META_DATA_QUEUE:-default}" - env: - DOCKER_IMAGE_TAG: "$BUILDKITE_BUILD_NUMBER" - EXTRAGOARGS: "-v -count=1" - artifact_paths: - - "examples/logs/*" - command: 'make -C examples integ-test' - timeout_in_minutes: 10 - concurrency: 1 - concurrency_group: 'loop-device test' - - label: ":rotating_light: :exclamation: example tests (devmapper)" agents: queue: "${BUILDKITE_AGENT_META_DATA_QUEUE:-default}" diff --git a/Makefile b/Makefile index e50a7829d..fcf3000d5 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. -SUBDIRS:=agent runtime snapshotter internal examples firecracker-control/cmd/containerd eventbridge +SUBDIRS:=agent runtime internal examples firecracker-control/cmd/containerd eventbridge TEST_SUBDIRS:=$(addprefix test-,$(SUBDIRS)) INTEG_TEST_SUBDIRS:=$(addprefix integ-test-,$(SUBDIRS)) diff --git a/README.md b/README.md index e6d750051..ac1384bac 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,6 @@ container standards such as the OCI image format. There are several components in this repository that enable containerd to use Firecracker microVMs to run containers: -* A [snapshotter](snapshotter) that creates files used as block-devices for - pass-through into the microVM. This snapshotter is used for providing the - container image to the microVM. The snapshotter runs as an out-of-process - gRPC proxy plugin. We currently have two implementations of a snapshotter: a - [naive](snapshotter/cmd/naive) copy-ahead implementation and a - [devmapper-based](snapshotter/cmd/devmapper) copy-on-write implementation. * A [control plugin](firecracker-control) managing the lifecycle of the runtime and implementing our [control API](proto/firecracker.proto) to manage the lifecycle of microVMs. The control plugin is compiled in to the diff --git a/docs/getting-started.md b/docs/getting-started.md index 214b5c92b..0f5d12faf 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -78,8 +78,6 @@ GO111MODULE=on make all Once you have built the runtime, be sure to place the following binaries on your `$PATH`: * `runtime/containerd-shim-aws-firecracker` -* `snapshotter/cmd/devmapper/devmapper_snapshotter` -* `snapshotter/cmd/naive/naive_snapshotter` * `firecracker-control/cmd/containerd/firecracker-containerd` * `firecracker-control/cmd/containerd/firecracker-ctr` @@ -135,10 +133,11 @@ root = "/var/lib/firecracker-containerd/containerd" state = "/run/firecracker-containerd" [grpc] address = "/run/firecracker-containerd/containerd.sock" -[proxy_plugins] - [proxy_plugins.firecracker-devmapper] - type = "snapshot" - address = "/var/run/firecracker-containerd/devmapper-snapshotter.sock" +[plugins] + [plugins.devmapper] + pool_name = "fc-dev-thinpool" + base_image_size = "10GB" + root_path = "/var/lib/firecracker-containerd/snapshotter/devmapper" [debug] level = "debug" @@ -153,7 +152,7 @@ binaries are in sync with one another. While other builds of `ctr` may work with ### Prepare and configure snapshotter The devmapper snapshotter requires a thinpool to exist. -Below is a script to create a thinpool as well as an example config file. +Below is a script to create a thinpool device. `Note: The configuration with loopback devices is slow and not intended for use in production.` @@ -205,21 +204,6 @@ fi ``` -
-Snappshotter config file example. - -```json -{ - "base_image_size": "10GB", - "root_path": "/var/lib/firecracker-containerd/snapshotter/devmapper", - "pool_name": "fc-dev-thinpool" -} -``` - -
- -A reasonable location for this file is at `/etc/firecracker-dm-snapshotter/config.json`. - ### Configure containerd runtime plugin The runtime expects a JSON-formatted configuration file to be located either in @@ -277,17 +261,7 @@ configuration file has the following fields: ## Usage -Start the containerd snapshotter - -```bash -$ ./devmapper_snapshotter \ - -address /var/run/firecracker-containerd/devmapper-snapshotter.sock \ - -path /tmp/fc-snapshot \ - -config /etc/firecracker-dm-snapshotter/config.json -``` -`note: The path for -config needs to match the location used when configuring the devmapper snapshotter.` - -In another terminal, start containerd +Start containerd ```bash $ sudo PATH=$PATH /usr/local/bin/firecracker-containerd \ @@ -298,7 +272,7 @@ Pull an image ```bash $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock images \ - pull --snapshotter firecracker-devmapper\ + pull --snapshotter devmapper \ docker.io/library/busybox:latest ``` @@ -306,7 +280,9 @@ And start a container! ```bash $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ - run --snapshotter firecracker-devmapper --runtime aws.firecracker \ + run \ + --snapshotter devmapper \ + --runtime aws.firecracker \ --rm --tty --net-host \ docker.io/library/busybox:latest busybox-test ``` @@ -321,7 +297,7 @@ $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ namespaces label fc \ containerd.io/defaults/runtime=aws.firecracker \ - containerd.io/defaults/snapshotter=firecracker-devmapper + containerd.io/defaults/snapshotter=devmapper $ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \ -n fc \ diff --git a/go.sum b/go.sum index b2bb1f115..e353a3516 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/cgroups v0.0.0-20181105182409-82cb49fc1779 h1:j1IsLW6/hNZPIyBH1v0hhEeB1WXE2ffhHaqSuXhgknY= github.com/containerd/cgroups v0.0.0-20181105182409-82cb49fc1779/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= @@ -44,6 +45,7 @@ github.com/coreos/go-iptables v0.4.2/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK4aZY8jz+DAF0FHjI51BXjjSwCzS1Dk= github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= @@ -147,6 +149,7 @@ github.com/miekg/dns v1.1.16 h1:iMEQ/IVHxPTtx2Q07JP/k4CKRvSjiAZjZ0hnhgYEDmE= github.com/miekg/dns v1.1.16/go.mod h1:YNV562EiewvSmpCB6/W4c6yqjK7Z+M/aIS1JHsIVeg8= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mrunalp/fileutils v0.0.0-20171103030105-7d4729fb3618/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b h1:Ey6yH0acn50T/v6CB75bGP4EMJqnv9WvnjN7oZaj+xE= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a h1:KfNOeFvoAssuZLT7IntKZElKwi/5LRuxY71k+t6rfaM= @@ -159,6 +162,7 @@ github.com/opencontainers/runc v1.0.0-rc9 h1:/k06BMULKF5hidyoZymkoDCzdJzltZpz/UU github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v0.1.2-0.20181106065543-31e0d16c1cb7 h1:vg5OQBKq2D0TX7q7loKQBLZ54IUAbUvq1rlwDRdn1gY= github.com/opencontainers/runtime-spec v0.1.2-0.20181106065543-31e0d16c1cb7/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.3.0/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= diff --git a/snapshotter/.gitignore b/snapshotter/.gitignore deleted file mode 100644 index f06028280..000000000 --- a/snapshotter/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -naive_snapshotter -devmapper_snapshotter diff --git a/snapshotter/Makefile b/snapshotter/Makefile deleted file mode 100644 index abbf0ba92..000000000 --- a/snapshotter/Makefile +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You may -# not use this file except in compliance with the License. A copy of the -# License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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. - -# Set this to pass additional commandline flags to the go compiler, e.g. "make test EXTRAGOARGS=-v" -EXTRAGOARGS?= - - -SUBDIRS:=cmd/devmapper cmd/naive -SOURCES:=$(shell find . -name '*.go') -GOMOD := $(shell go env GOMOD) -GOSUM := $(GOMOD:.mod=.sum) -DOCKER_IMAGE_TAG?=latest -FIRECRACKER_CONTAINERD_TEST_IMAGE?=localhost/firecracker-containerd-test - -all: $(SUBDIRS) - -$(SUBDIRS): - $(MAKE) -C $@ - -install: $(SUBDIRS) - for d in $(SUBDIRS); do $(MAKE) -C $$d install; done - -test: - DISABLE_ROOT_TESTS=true go test ./... $(EXTRAGOARGS) - -integ-test: - docker run --rm -it \ - --privileged \ - --ipc=host \ - --volume /dev:/dev \ - --volume /sys:/sys \ - --volume /run/udev/control:/run/udev/control \ - --volume $(CURDIR)/..:/src \ - --workdir="/src/snapshotter" \ - $(FIRECRACKER_CONTAINERD_TEST_IMAGE):${DOCKER_IMAGE_TAG} \ - "go test ./... $(EXTRAGOARGS)" - -clean: - for d in $(SUBDIRS); do $(MAKE) -C $$d clean; done - -distclean: clean - -.PHONY: all $(SUBDIRS) clean distclean install test integ-test diff --git a/snapshotter/README.md b/snapshotter/README.md deleted file mode 100644 index 3d3d6fb63..000000000 --- a/snapshotter/README.md +++ /dev/null @@ -1,89 +0,0 @@ -## Requirements - -### Running -Due to its dependency on `dmsetup`, executing the snapshotter process in an environment where a udev -daemon is not accessible (such as a container) may result in unexpected behavior. In this case, try executing the -snapshotter with the `DM_DISABLE_UDEV=1` environment variable, which tells `dmsetup` to ignore udev and manage devices -itself. See [lvm(8)](http://man7.org/linux/man-pages/man8/lvm.8.html) and -[dmsetup(8)](http://man7.org/linux/man-pages/man8/dmsetup.8.html) for more information. - -## How to run snapshotters benchmark - -- `firecracker-containerd` project contains CloudFormation template to run an EC2 instance suitable for benchmarking. -It installs dependencies, prepares EBS volumes with same performance characteristics, and creates thin-pool device. -You can make stack with the following command (note: there is a charge for using AWS resources): - -```bash -aws cloudformation create-stack \ - --stack-name benchmark-instance \ - --template-body file://benchmark_aws.yml \ - --parameters \ - ParameterKey=Key,ParameterValue=SSH_KEY \ - ParameterKey=SecurityGroups,ParameterValue=sg-XXXXXXXX \ - ParameterKey=VolumesSize,ParameterValue=20 \ - ParameterKey=VolumesIOPS,ParameterValue=1000 -``` - -- You can find an IP address of newly created EC2 instance in AWS Console or via AWS CLI: - -```bash -$ aws ec2 describe-instances \ - --instance-ids $(aws cloudformation describe-stack-resources --stack-name benchmark-instance --query 'StackResources[*].PhysicalResourceId' --output text) \ - --query 'Reservations[*].Instances[*].PublicIpAddress' \ - --output text -``` - -- SSH to an instance and prepare `firecracker-containerd` project: - -```bash -ssh -i SSH_KEY ec2-user@IP -mkdir /mnt/disk1/data /mnt/disk2/data /mnt/disk3/data -cd -git clone https://github.com/firecracker-microvm/firecracker-containerd.git -cd firecracker-containerd -make -``` - -- Now you're ready to run the benchmark: - -```bash -sudo su - -cd snapshotter/ -go test -bench . \ - -dm.thinPoolDev=bench-docker--pool \ - -dm.rootPath=/mnt/disk1/data \ - -overlay.rootPath=/mnt/disk2/data \ - -naive.rootPath=/mnt/disk3/data -``` - -- The output will look like: - -```bash -goos: linux -goarch: amd64 -pkg: github.com/firecracker-microvm/firecracker-containerd/snapshotter - -BenchmarkNaive/run-4 1 177960054662 ns/op 0.94 MB/s -BenchmarkNaive/prepare 1 11284453889 ns/op -BenchmarkNaive/write 1 166035272412 ns/op -BenchmarkNaive/commit 1 640126721 ns/op - -BenchmarkOverlay/run-4 1 1019730210 ns/op 164.53 MB/s -BenchmarkOverlay/prepare 1 26799447 ns/op -BenchmarkOverlay/write 1 968200363 ns/op -BenchmarkOverlay/commit 1 24582560 ns/op - -BenchmarkDeviceMapper/run-4 1 3139232730 ns/op 53.44 MB/s -BenchmarkDeviceMapper/prepare 1 1758640440 ns/op -BenchmarkDeviceMapper/write 1 1356705388 ns/op -BenchmarkDeviceMapper/commit 1 23720367 ns/op - -PASS -ok github.com/firecracker-microvm/firecracker-containerd/snapshotter 185.204s -``` - -- Don't forget to tear down the stack so it does not continue to incur charges: - -```bash -aws cloudformation delete-stack --stack-name benchmark-instance -``` diff --git a/snapshotter/benchmark_aws.yml b/snapshotter/benchmark_aws.yml deleted file mode 100644 index 1f02138e6..000000000 --- a/snapshotter/benchmark_aws.yml +++ /dev/null @@ -1,139 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" - -Description: > - This templates spin ups an EC2 instance with EBS volumes suitable for containerd snapshotters benchmarking. - The template will create EBS volumes for benchmarking (/dev/sdb, /dev/sdc, and /dev/sdd) with same performance characteristics. - /dev/sde volume will be created and used for device mapper thin-pool device. - -Parameters: - Key: - Type: AWS::EC2::KeyPair::KeyName - Description: SSH key to use - - AMI: - Type: AWS::EC2::Image::Id - Description: AMI ID to use for the EC2 instance. Must be Amazon Linux 2. - Default: "ami-032509850cf9ee54e" - - SecurityGroups: - Type: List - Description: List of security groups to add to EC2 instance - - InstanceType: - Type: String - Default: m4.xlarge - Description: EC2 instance type to use - - VolumesIOPS: - Type: Number - Default: 1000 - MinValue: 100 - MaxValue: 20000 - Description: The number of I/O operations per second (IOPS) to reserve for EBS volumes. - - VolumesSize: - Type: Number - Default: 20 - MinValue: 4 - MaxValue: 16384 - Description: EBS volumes size, in gibibytes (GiB) - - VolumeType: - Type: String - Default: io1 - AllowedValues: - - io1 - - gp2 - - sc1 - - st1 - Description: > - Volume type to use for EBS volumes (io1 is recommended). - More information on volume types https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html - - ContainerStorageSetup: - Type: String - Default: https://github.com/projectatomic/container-storage-setup/archive/v0.6.0.tar.gz - Description: container-storage-setup tool version to install (more details at https://github.com/projectatomic/container-storage-setup) - -Resources: - Instance: - Type: AWS::EC2::Instance - Properties: - EbsOptimized: true - InstanceType: !Ref InstanceType - KeyName: !Ref Key - ImageId: !Ref AMI - SecurityGroupIds: !Ref SecurityGroups - BlockDeviceMappings: - - DeviceName: "/dev/xvda" # Root volume - Ebs: - VolumeSize: 64 - VolumeType: io1 - Iops: 1000 - DeleteOnTermination: true - - DeviceName: "/dev/sdb" - Ebs: - VolumeSize: !Ref VolumesSize - VolumeType: !Ref VolumeType - Iops: !Ref VolumesIOPS - DeleteOnTermination: true - - DeviceName: "/dev/sdc" - Ebs: - VolumeSize: !Ref VolumesSize - VolumeType: !Ref VolumeType - Iops: !Ref VolumesIOPS - DeleteOnTermination: true - - DeviceName: "/dev/sdd" - Ebs: - VolumeSize: !Ref VolumesSize - VolumeType: !Ref VolumeType - Iops: !Ref VolumesIOPS - DeleteOnTermination: true - - DeviceName: "/dev/sde" - Ebs: - VolumeSize: !Ref VolumesSize - VolumeType: !Ref VolumeType - Iops: !Ref VolumesIOPS - DeleteOnTermination: true - - UserData: - Fn::Base64: - !Sub | - #!/bin/bash - - set -ex - - yum install -y gcc git - amazon-linux-extras install -y golang1.11 - - # Install container-storage-setup - mkdir -p /tmp/container-storage-setup/unpacked/ - cd /tmp/container-storage-setup/ - curl -sL ${ContainerStorageSetup} -o archive.tar.gz - tar -xzf archive.tar.gz -C unpacked --strip 1 - cd unpacked/ - make install-core - rm -rf /tmp/container-storage-setup/ - - # Prepare EBS volumes - mkdir -p /mnt/{disk1,disk2,disk3} - - mkfs.ext4 /dev/sdb - mount /dev/sdb /mnt/disk1/ - - mkfs.ext4 /dev/sdc - mount /dev/sdc /mnt/disk2/ - - mkfs.ext4 /dev/sdd - mount /dev/sdd /mnt/disk3 - - chgrp -R wheel /mnt/disk1/ /mnt/disk2/ /mnt/disk3/ - chmod -R 2775 /mnt/disk1/ /mnt/disk2/ /mnt/disk3/ - - # Prepare thin-pool device - touch /etc/sysconfig/docker-storage-setup - echo DEVS=/dev/sde >> /etc/sysconfig/docker-storage-setup - echo VG=bench >> /etc/sysconfig/docker-storage-setup - container-storage-setup - - echo "Done" diff --git a/snapshotter/benchmark_test.go b/snapshotter/benchmark_test.go deleted file mode 100644 index cad8d37dc..000000000 --- a/snapshotter/benchmark_test.go +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 snapshotter - -import ( - "context" - "crypto/rand" - "flag" - "fmt" - "os" - "path/filepath" - "sync/atomic" - "testing" - "time" - - "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/snapshots" - "github.com/containerd/containerd/snapshots/overlay" - "github.com/containerd/continuity/fs/fstest" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/devmapper" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/naive" -) - -var ( - dmPoolDev string - dmRootPath string - overlayRootPath string - naiveRootPath string -) - -func init() { - flag.StringVar(&dmPoolDev, "dm.thinPoolDev", "", "Pool device to run benchmark on") - flag.StringVar(&dmRootPath, "dm.rootPath", "", "Root dir for devmapper snapshotter") - flag.StringVar(&overlayRootPath, "overlay.rootPath", "", "Root dir for overlay snapshotter") - flag.StringVar(&naiveRootPath, "naive.rootPath", "", "Root dir for naive snapshotter") - // Avoid mixing benchmark output and INFO messages - logrus.SetLevel(logrus.ErrorLevel) -} - -func BenchmarkNaive(b *testing.B) { - if naiveRootPath == "" { - b.Skip("naive snapshotter root dir must be provided") - } - - snapshotter, err := naive.NewSnapshotter(context.Background(), naiveRootPath) - require.NoErrorf(b, err, "failed to create naive snapshotter") - - defer func() { - err = snapshotter.Close() - assert.NoError(b, err) - - err = os.RemoveAll(naiveRootPath) - assert.NoError(b, err) - }() - - benchmarkSnapshotter(b, snapshotter) -} - -func BenchmarkOverlay(b *testing.B) { - if overlayRootPath == "" { - b.Skip("overlay root dir must be provided") - } - - snapshotter, err := overlay.NewSnapshotter(overlayRootPath) - require.NoErrorf(b, err, "failed to create overlay snapshotter") - - defer func() { - err = snapshotter.Close() - assert.NoError(b, err) - - err = os.RemoveAll(overlayRootPath) - assert.NoError(b, err) - }() - - benchmarkSnapshotter(b, snapshotter) -} - -func BenchmarkDeviceMapper(b *testing.B) { - if dmPoolDev == "" { - b.Skip("devmapper benchmark requires thin-pool device to be prepared in advance and provided") - } - - if dmRootPath == "" { - b.Skip("devmapper snapshotter root dir must be provided") - } - - config := &devmapper.Config{ - PoolName: dmPoolDev, - RootPath: dmRootPath, - BaseImageSize: "16Mb", - } - - ctx := context.Background() - - snapshotter, err := devmapper.NewSnapshotter(ctx, config) - require.NoError(b, err) - - defer func() { - err := snapshotter.ResetPool(ctx) - assert.NoError(b, err) - - err = snapshotter.Close() - assert.NoError(b, err) - - err = os.RemoveAll(dmRootPath) - assert.NoError(b, err) - }() - - benchmarkSnapshotter(b, snapshotter) -} - -// benchmarkSnapshotter tests snapshotter performance. -// It writes 16 layers with randomly created, modified, or removed files. -// Depending on layer index different sets of files are modified. -// In addition to total snapshotter execution time, benchmark outputs a few additional -// details - time taken to Prepare layer, mount, write data and unmount time, -// and Commit snapshot time. -func benchmarkSnapshotter(b *testing.B, snapshotter snapshots.Snapshotter) { - const ( - layerCount = 16 - fileSizeBytes = int64(1 * 1024 * 1024) // 1 MB - ) - - var ( - total = 0 - layers = make([]fstest.Applier, 0, layerCount) - layerIndex = int64(0) - ) - - for i := 1; i <= layerCount; i++ { - appliers := makeApplier(i, fileSizeBytes) - layers = append(layers, fstest.Apply(appliers...)) - total += len(appliers) - } - - var ( - benchN int - prepareDuration time.Duration - writeDuration time.Duration - commitDuration time.Duration - ) - - // Wrap test with Run so additional details output will be added right below the benchmark result - b.Run("run", func(b *testing.B) { - var ( - ctx = context.Background() - parent string - current string - ) - - // Reset durations since test might be ran multiple times - prepareDuration = 0 - writeDuration = 0 - commitDuration = 0 - benchN = b.N - - b.SetBytes(int64(total) * fileSizeBytes) - - var timer time.Time - for i := 0; i < b.N; i++ { - for l := 0; l < layerCount; l++ { - current = fmt.Sprintf("prepare-layer-%d", atomic.AddInt64(&layerIndex, 1)) - - timer = time.Now() - mounts, err := snapshotter.Prepare(ctx, current, parent) - require.NoError(b, err) - prepareDuration += time.Since(timer) - - timer = time.Now() - err = mount.WithTempMount(ctx, mounts, layers[l].Apply) - require.NoError(b, err) - writeDuration += time.Since(timer) - - parent = fmt.Sprintf("committed-%d", atomic.AddInt64(&layerIndex, 1)) - - timer = time.Now() - err = snapshotter.Commit(ctx, parent, current) - require.NoError(b, err) - commitDuration += time.Since(timer) - } - } - }) - - // Output extra measurements - total time taken to Prepare, mount and write data, and Commit - const outputFormat = "%-25s\t%s\n" - fmt.Fprintf(os.Stdout, - outputFormat, - b.Name()+"/prepare", - testing.BenchmarkResult{N: benchN, T: prepareDuration}) - - fmt.Fprintf(os.Stdout, - outputFormat, - b.Name()+"/write", - testing.BenchmarkResult{N: benchN, T: writeDuration}) - - fmt.Fprintf(os.Stdout, - outputFormat, - b.Name()+"/commit", - testing.BenchmarkResult{N: benchN, T: commitDuration}) - - fmt.Fprintln(os.Stdout) -} - -// applierFn represents helper func that implements fstest.Applier -type applierFn func(root string) error - -func (fn applierFn) Apply(root string) error { - return fn(root) -} - -// updateFile modifies a few bytes in the middle in order to demonstrate the difference in performance -// for block-based snapshotters (like devicemapper) against file-based snapshotters (like overlay, which need to -// perform a copy-up of the full file any time a single bit is modified). -func updateFile(name string) applierFn { - return func(root string) error { - path := filepath.Join(root, name) - file, err := os.OpenFile(path, os.O_WRONLY, 0600) - if err != nil { - return errors.Wrapf(err, "failed to open %q", path) - } - - info, err := file.Stat() - if err != nil { - return err - } - - var ( - offset = info.Size() / 2 - buf = make([]byte, 4) - ) - - if _, err := rand.Read(buf); err != nil { - return err - } - - if _, err := file.WriteAt(buf, offset); err != nil { - return errors.Wrapf(err, "failed to write %q at offset %d", path, offset) - } - - return file.Close() - } -} - -// makeApplier returns a slice of fstest.Applier where files are written randomly. -// Depending on layer index, the returned layers will overwrite some files with the -// same generated names with new contents or deletions. -func makeApplier(layerIndex int, fileSizeBytes int64) []fstest.Applier { - seed := time.Now().UnixNano() - - switch { - case layerIndex%3 == 0: - return []fstest.Applier{ - updateFile("/a"), - updateFile("/b"), - fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777), - updateFile("/d"), - fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777), - updateFile("/e"), - fstest.RemoveAll("/g"), - fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777), - updateFile("/i"), - fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777), - } - case layerIndex%2 == 0: - return []fstest.Applier{ - updateFile("/a"), - fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777), - fstest.RemoveAll("/c"), - fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777), - updateFile("/e"), - fstest.RemoveAll("/f"), - fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777), - updateFile("/h"), - fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777), - updateFile("/j"), - } - default: - return []fstest.Applier{ - fstest.CreateRandomFile("/a", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/b", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/c", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/d", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/e", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/f", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/g", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/h", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/i", seed, fileSizeBytes, 0777), - fstest.CreateRandomFile("/j", seed, fileSizeBytes, 0777), - } - } -} diff --git a/snapshotter/cmd/devmapper/.gitignore b/snapshotter/cmd/devmapper/.gitignore deleted file mode 100644 index 0637db1d5..000000000 --- a/snapshotter/cmd/devmapper/.gitignore +++ /dev/null @@ -1 +0,0 @@ -devmapper_snapshotter diff --git a/snapshotter/cmd/devmapper/Makefile b/snapshotter/cmd/devmapper/Makefile deleted file mode 100644 index 1dd7d7e94..000000000 --- a/snapshotter/cmd/devmapper/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You may -# not use this file except in compliance with the License. A copy of the -# License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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. - -GOMOD := $(shell go env GOMOD) -GOSUM := $(GOMOD:.mod=.sum) - -all: devmapper_snapshotter - -devmapper_snapshotter: *.go $(GOMOD) $(GOSUM) - go build -o devmapper_snapshotter - -install: devmapper_snapshotter - install -D -o root -g root -m755 -t $(INSTALLROOT)/bin devmapper_snapshotter - -clean: - - rm -f devmapper_snapshotter - -.PHONY: all clean install diff --git a/snapshotter/cmd/devmapper/README.md b/snapshotter/cmd/devmapper/README.md deleted file mode 100644 index 2d02df25b..000000000 --- a/snapshotter/cmd/devmapper/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# devmapper snapshotter for containerd and Firecracker - -This component is a -[snapshotter](https://github.com/containerd/containerd/blob/master/design/snapshots.md) -[plugin](https://github.com/containerd/containerd/blob/master/PLUGINS.md) for -containerd that stores snapshots in ext4-formatted filesystem images in a -devicemapper thin pool. The snapshots created by this snapshotter are usable -with the containerd-firecracker-runtime to run microVM-backed containers with -the Firecracker VMM. - -## Installation - -To make containerd aware of this plugin, you need to register it in -containerd's configuration file. This file is typically located at -`/etc/containerd/config.toml`. - -Here's a sample entry that can be made in the configuration file: - -```toml -[proxy_plugins] - [proxy_plugins.firecracker-dm-snapshotter] - type = "snapshot" - address = "/var/run/firecracker-dm-snapshotter.sock" -``` - -The name of the plugin in this example is "firecracker-dm-snapshotter". The -`address` entry points to a socket file exposed by the snapshotter, which is -determined when you run it. - -## Usage - -``` -./devmapper_snapshotter -address UNIX-DOMAIN-SOCKET -config CONFIG -debug -``` - -To run the snapshotter, you must specify the path to a Unix domain socket and a -path to a JSON configuration file. The config file must contain the following -fields: - -* `RootPath` - a directory where the metadata will be available -* `PoolName` - a name to use for the devicemapper thin pool -* `BaseImageSize` - defines how much space to allocate when creating the base - device - -Here's a sample entry that can be made in the configuration file `/etc/firecracker-dm-snapshotter/config.json`: -```json -{ - "base_image_size" : "20GB", - "root_path" : "/tmp/test", - "pool_name" : "test" -} -``` -Note: The `test` thin-pool needs to be prepared in advance. - -For example, to run the snapshotter with its domain socket at -`/var/run/firecracker-dm-snapshotter.sock` and its configuration file at -`/etc/firecracker-dm-snapshotter/config.json` you would run the snapshotter -plugin process as follows: - -``` -./devmapper_snapshotter -address /var/run/firecracker-dm-snapshotter.sock -config /etc/firecracker-dm-snapshotter/config.json -``` - -Now you can use snapshotter with containerd: - -``` -CONTAINERD_SNAPSHOTTER=firecracker-dm-snapshotter ctr images pull docker.io/library/alpine:latest -``` - -## Using container storage setup tool - -The device mapper snapshotter is compatible with `container_storage_setup` [utility](https://github.com/projectatomic/container-storage-setup) -(formerly known as `docker-storage-setup`). `container-storage-setup` is a script to -configure COW file systems like devicemapper and overlayfs. It is usually run via a systemd service. - -The snapshotter supports a subset of `dm.*` command line flags generated by `container_storage_setup`. - -Here is a quick tutorial how it can be used togather. - -- Run an EC2 instance with additional EBS volume `/dev/sdb` (this example uses CentOS 7) -- Install container storage setup tool - - ```bash - $ yum update -y - $ yum install -y container-storage-setup - ``` - -- Create thin-pool device using `container-storage-setup` tool - - ```bash - $ touch /etc/sysconfig/docker-storage-setup - $ echo DEVS=/dev/xvdb >> /etc/sysconfig/docker-storage-setup - $ echo VG=test >> /etc/sysconfig/docker-storage-setup - $ container-storage-setup - INFO: Volume group backing root filesystem could not be determined - INFO: Writing zeros to first 4MB of device /dev/xvdb - 4+0 records in - 4+0 records out - 4194304 bytes (4.2 MB) copied, 0.0162157 s, 259 MB/s - INFO: Device node /dev/xvdb1 exists. - Physical volume "/dev/xvdb1" successfully created. - Volume group "test" successfully created - Rounding up size to full physical extent 20.00 MiB - Thin pool volume with chunk size 512.00 KiB can address at most 126.50 TiB of data. - Logical volume "docker-pool" created. - Logical volume test/docker-pool changed. - ``` - - Run `man container-storage-setup` to get more information how to setup storage for container runtimes. - -- `container-storage-setup` creates a runtime storage file which looks as follows: - - ```bash - $ cat /etc/sysconfig/docker-storage - DOCKER_STORAGE_OPTIONS="--storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/test-docker--pool " - ``` - -- Now you can create a service script to run the device mapper snapshotter - - ``` - EnvironmentFile=/etc/sysconfig/docker-storage - ... - ExecStart=./devmapper_snapshotter -address /var/run/firecracker-dm-snapshotter.sock -debug $DOCKER_STORAGE_OPTIONS - ... - ``` - - The output should look like: - ``` - INFO[0000] loaded configuration file "/etc/containerd/devmapper-snapshotter.json" - INFO[0000] applying storage opt: dm.fs=xfs - WARN[0000] "xfs" not supported, defaulting to ext4 - INFO[0000] applying storage opt: dm.thinpooldev=/dev/mapper/test-docker--pool - INFO[0000] initializing pool device "test-docker--pool" - INFO[0000] using dmsetup: - Library version: 1.02.110 (2015-10-30) - Driver version: 4.37.0 - INFO[0000] running gRPC server unix_addr=/var/run/firecracker-dm-snapshotter.sock - ``` diff --git a/snapshotter/cmd/devmapper/flag.go b/snapshotter/cmd/devmapper/flag.go deleted file mode 100644 index 9448d1abb..000000000 --- a/snapshotter/cmd/devmapper/flag.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 main - -import ( - "fmt" - "os" - "strings" -) - -// parseKeyValueOpt splits a string to key=value -func parseKeyValueOpt(opt string) (string, string, error) { - pair := strings.SplitN(opt, "=", 2) - if len(pair) != 2 { - return "", "", fmt.Errorf("failed to split option: %q", opt) - } - - key := strings.TrimSpace(pair[0]) - if key == "" { - return "", "", fmt.Errorf("option %q has no key", opt) - } - - value := strings.TrimSpace(pair[1]) - - return key, value, nil -} - -// visitKeyValueOpts finds all --optName key=value in command line string and invokes callback for each pair -func visitKeyValueOpts(optName string, optFn func(key, value string) error) error { - // Nothing to visit - if len(os.Args) < 3 { - return nil - } - - // Skip exec path - args := os.Args[1:] - - for i := 0; i < len(args)-1; i++ { - if optName != args[i] { - continue - } - - // Next element must be key=value pair - key, value, err := parseKeyValueOpt(args[i+1]) - if err != nil { - return err - } - - if err := optFn(key, value); err != nil { - return err - } - } - - return nil -} diff --git a/snapshotter/cmd/devmapper/flag_test.go b/snapshotter/cmd/devmapper/flag_test.go deleted file mode 100644 index 4c5f57880..000000000 --- a/snapshotter/cmd/devmapper/flag_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 main - -import ( - "errors" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseKeyValueOpt(t *testing.T) { - key, value, err := parseKeyValueOpt("dm.baseSize=1") - assert.NoError(t, err) - assert.Equal(t, "dm.baseSize", key) - assert.Equal(t, "1", value) - - key, value, err = parseKeyValueOpt(" dm.device = /dev/mapper/test ") - assert.NoError(t, err) - assert.Equal(t, "dm.device", key) - assert.Equal(t, "/dev/mapper/test", value) - - key, value, err = parseKeyValueOpt(" dm.device = ") - assert.NoError(t, err) - assert.Equal(t, "dm.device", key) - assert.Equal(t, "", value) -} - -func TestParseInvalidKeyValueOpt(t *testing.T) { - _, _, err := parseKeyValueOpt("dm.baseSize") - assert.Error(t, err) - - _, _, err = parseKeyValueOpt("=value") - assert.Error(t, err) -} - -func TestVisitKeyValueOpts(t *testing.T) { - var ( - args = os.Args - called = false - ) - - os.Args = []string{"exec_path", "--opt", "a=b"} - err := visitKeyValueOpts("--opt", func(key, value string) error { - called = true - assert.Equal(t, "a", key) - assert.Equal(t, "b", value) - return nil - }) - - assert.NoError(t, err) - assert.True(t, called) - - defer func() { os.Args = args }() -} - -func TestVisitKeyValueOptCallbackErr(t *testing.T) { - var ( - args = os.Args - count = 0 - cbErr = errors.New("test") - ) - - os.Args = []string{"exec_path", "--opt", "a=b", "--opt", "c=d"} - err := visitKeyValueOpts("--opt", func(key, value string) error { - count++ - return cbErr - }) - - assert.Equal(t, cbErr, err) - assert.Equalf(t, 1, count, "callback should be called exactly once") - - defer func() { os.Args = args }() -} - -func TestVisitInvalidOpts(t *testing.T) { - args := os.Args - - tests := []struct { - name string - args []string - err error - }{ - {"Empty args", []string{}, nil}, - {"Nil", nil, nil}, - {"Empty string", []string{""}, nil}, - {"Empty strings", []string{"", "", "", "", "", ""}, nil}, - {"Just reboot flag", []string{"-reboot"}, nil}, - {"Single opt flag", []string{"--opt"}, nil}, - {"Opt flag at end", []string{"--type", "devmapper", "--opt"}, nil}, - {"Opt flag at start", []string{"--opt", "--type", "devmapper"}, errors.New("failed to split option: \"--type\"")}, - {"Opt with invalid kv pair", []string{"--opt", "1+1"}, errors.New("failed to split option: \"1+1\"")}, - {"Opt with empty kv", []string{"--opt", ""}, errors.New("failed to split option: \"\"")}, - } - - for _, test := range tests { - var ( - expectedErr = test.err - args = test.args - ) - - t.Run(test.name, func(t *testing.T) { - os.Args = append([]string{"exec_path_to_be_skipped"}, args...) - err := visitKeyValueOpts("--opt", func(_, _ string) error { - assert.Fail(t, "callback should not be called") - return nil - }) - - if expectedErr == nil { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, expectedErr.Error()) - } - }) - } - - defer func() { os.Args = args }() -} diff --git a/snapshotter/cmd/devmapper/main.go b/snapshotter/cmd/devmapper/main.go deleted file mode 100644 index 75b3b993d..000000000 --- a/snapshotter/cmd/devmapper/main.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 main - -import ( - "context" - "flag" - "os" - "strings" - - "github.com/containerd/containerd/log" - "github.com/containerd/containerd/snapshots" - "github.com/docker/go-units" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/devmapper" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/dmsetup" -) - -const ( - configPathEnvName = "DEVMAPPER_SNAPSHOTTER_CONFIG_PATH" - defaultConfigPath = "/etc/containerd/devmapper-snapshotter.json" -) - -func main() { - var ( - ctx = context.Background() - config = &devmapper.Config{} - configPath = "" - rootPath = "" - ) - - flag.StringVar(&configPath, "config", "", "Path to devmapper configuration file") - flag.StringVar(&rootPath, "path", "", "Path to snapshotter data") - - // These flags are needed for compatibility with container-storage-setup tool as there is no way to - // ignore unknown flags in `flag` package. Storage options are parsed using visitKeyValueOpts func. - flag.String("storage-driver", "devicemapper", "Storage driver to use. Always devicemapper.") - flag.Bool("storage-opt", false, "Storage configuration options (compatible with Docker dm.* flags)") - - flag.Parse() - - // Try load file from disk - if cfg, err := loadConfig(ctx, configPath); err == nil { - config = cfg - } else if err != os.ErrNotExist { - log.G(ctx).WithError(err).Fatal("failed to load config file") - } - - if rootPath != "" { - config.RootPath = rootPath - } - - // Append and/or overwrite file configuration with --storage-opt dm.XXX=YYY command line flags - if err := visitKeyValueOpts("--storage-opt", func(key, value string) error { - return applyStorageOpt(ctx, key, value, config) - }); err != nil { - log.G(ctx).WithError(err).Fatal("failed to apply storage options") - } - - if err := config.Validate(); err != nil { - log.G(ctx).WithError(err).Fatal("invalid configuration") - } - - snapshotter.Run(func(ctx context.Context) (snapshots.Snapshotter, error) { - return devmapper.NewSnapshotter(ctx, config) - }) -} - -// loadConfig loads configuration file from disk -func loadConfig(ctx context.Context, configPath string) (*devmapper.Config, error) { - if configPath == "" { - configPath = os.Getenv(configPathEnvName) - } - - if configPath == "" { - configPath = defaultConfigPath - } - - config, err := devmapper.LoadConfig(configPath) - if err != nil { - return nil, err - } - - log.G(ctx).Infof("loaded configuration file %q", configPath) - return config, nil -} - -// applyStorageOpt overwrites configuration with --storage-opt command line flags -func applyStorageOpt(ctx context.Context, key, value string, config *devmapper.Config) error { - log.G(ctx).Infof("applying storage opt: %s=%s", key, value) - - switch key { - case "dm.basesize": - size, err := units.RAMInBytes(value) - if err != nil { - return err - } - - config.BaseImageSize = value - config.BaseImageSizeBytes = uint64(size) - case "dm.thinpooldev": - config.PoolName = strings.TrimPrefix(value, dmsetup.DevMapperDir) - case "dm.fs": - // TODO: Support alternative file systems (https://github.com/firecracker-microvm/firecracker-containerd/issues/44) - if value != "ext4" { - log.G(ctx).Warnf("%q not supported, defaulting to ext4", value) - } - default: - log.G(ctx).Warnf("ignoring unsupported flag %q", key) - } - - return nil -} diff --git a/snapshotter/cmd/devmapper/main_test.go b/snapshotter/cmd/devmapper/main_test.go deleted file mode 100644 index 9fd57f465..000000000 --- a/snapshotter/cmd/devmapper/main_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 main - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/devmapper" -) - -func TestApplyStorageOpt(t *testing.T) { - var ( - config = &devmapper.Config{} - ctx = context.Background() - ) - - opts := map[string]string{ - "dm.basesize": "10gb", - "dm.metadatadev": "/meta_dev", - "dm.datadev": "/data_dev", - "dm.thinpooldev": "/pool_dev", - "dm.blocksize": "100mb", - } - - for key, value := range opts { - err := applyStorageOpt(ctx, key, value, config) - assert.NoErrorf(t, err, "failed to apply opt %q", key) - } - - assert.Equal(t, "10gb", config.BaseImageSize) - assert.EqualValues(t, 10*1024*1024*1024, config.BaseImageSizeBytes) - assert.Equal(t, "/pool_dev", config.PoolName) -} diff --git a/snapshotter/cmd/naive/.gitignore b/snapshotter/cmd/naive/.gitignore deleted file mode 100644 index c8e0cbdd3..000000000 --- a/snapshotter/cmd/naive/.gitignore +++ /dev/null @@ -1 +0,0 @@ -naive_snapshotter diff --git a/snapshotter/cmd/naive/Makefile b/snapshotter/cmd/naive/Makefile deleted file mode 100644 index 40bcd7acd..000000000 --- a/snapshotter/cmd/naive/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). You may -# not use this file except in compliance with the License. A copy of the -# License is located at -# -# http://aws.amazon.com/apache2.0/ -# -# or in the "license" file accompanying this file. This file 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. - -GOMOD := $(shell go env GOMOD) -GOSUM := $(GOMOD:.mod=.sum) - -all: naive_snapshotter - -naive_snapshotter: *.go $(GOMOD) $(GOSUM) - go build -o naive_snapshotter - -install: naive_snapshotter - install -D -o root -g root -m755 -t $(INSTALLROOT)/bin naive_snapshotter - -clean: - - rm -f naive_snapshotter - -.PHONY: all clean install diff --git a/snapshotter/cmd/naive/README.md b/snapshotter/cmd/naive/README.md deleted file mode 100644 index 9715db7fe..000000000 --- a/snapshotter/cmd/naive/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Naive device snapshotter for containerd and Firecracker - -This component is a -[snapshotter](https://github.com/containerd/containerd/blob/master/design/snapshots.md) -[plugin](https://github.com/containerd/containerd/blob/master/PLUGINS.md) for -containerd that stores snapshots in flat, ext4-formatted filesystem images. -The snapshots created by this snapshotter are usable with the -containerd-firecracker-runtime to run microVM-backed containers with the -Firecracker VMM. - -This snapshotter plugin is written for broad compatibility, and should run on -any Linux system capable of running Firecracker and containerd. However, it -sacrifices efficiency in order to achieve this compatibility. Each layer in a -container image is represented as a unique filesystem image. Each container is -given a complete private copy of it's root filesystem image upon creation. Thus, -container creation is expensive in terms IO and disk space. - -## Installation - -To make containerd aware of this plugin, you need to register it in -containerd's configuration file. This file is typically located at -`/etc/containerd/config.toml`. - -Here's a sample entry that can be made in the configuration file: - -```toml -[proxy_plugins] - [proxy_plugins.firecracker-snapshotter] - type = "snapshot" - address = "/var/run/firecracker-snapshotter.sock" -``` - -The name of the plugin in this example is "firecracker-snapshotter". The -`address` entry points to a socket file exposed by the snapshotter, which is -determined when you run it. - -## Usage - -``` -./naive_snapshotter -address UNIX-DOMAIN-SOCKET -path ROOT -debug -``` - -To run the snapshotter, you must specify both a Unix domain socket and a root -directory where the snapshots will be stored. For example, to run the -snapshotter with its domain socket at `/var/run/firecracker-snapshotter.sock` -and its storage at `/var/lib/firecracker-snapshotter`, you would run the -snapshotter plugin process as follows: - -``` -./naive_snapshotter -address /var/run/firecracker-snapshotter.sock -path /var/lib/firecracker-snapshotter -``` - -Now you can use snapshotter with containerd: - -``` -CONTAINERD_SNAPSHOTTER=firecracker-snapshotter ctr images pull docker.io/library/alpine:latest -``` diff --git a/snapshotter/cmd/naive/main.go b/snapshotter/cmd/naive/main.go deleted file mode 100644 index 7afc46ed1..000000000 --- a/snapshotter/cmd/naive/main.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 main - -import ( - "context" - "flag" - - "github.com/containerd/containerd/snapshots" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/naive" -) - -func main() { - var rootPath string - flag.StringVar(&rootPath, "path", "./images", "Path to snapshotter data (default: ./images)") - flag.Parse() - - snapshotter.Run(func(ctx context.Context) (snapshots.Snapshotter, error) { - return naive.NewSnapshotter(ctx, rootPath) - }) -} diff --git a/snapshotter/devmapper/config.go b/snapshotter/devmapper/config.go deleted file mode 100644 index 60b514507..000000000 --- a/snapshotter/devmapper/config.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - - "github.com/docker/go-units" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" -) - -// Config represents device mapper configuration loaded from file. -// Size units can be specified in human-readable string format (like "32KIB", "32GB", "32Tb") -type Config struct { - // Device snapshotter root directory for metadata - RootPath string `json:"root_path"` - - // Name for 'thin-pool' device to be used by snapshotter (without /dev/mapper/ prefix) - PoolName string `json:"pool_name"` - - // Defines how much space to allocate when creating base image for container - BaseImageSize string `json:"base_image_size"` - BaseImageSizeBytes uint64 `json:"-"` -} - -// LoadConfig reads devmapper configuration file JSON format from disk -func LoadConfig(path string) (*Config, error) { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return nil, os.ErrNotExist - } - - return nil, err - } - - data, err := ioutil.ReadFile(path) - if err != nil { - return nil, errors.Wrap(err, "failed to read file") - } - - config := Config{} - if err := json.Unmarshal(data, &config); err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal data at '%s'", path) - } - - if err := config.parse(); err != nil { - return nil, err - } - - if err := config.Validate(); err != nil { - return nil, err - } - - return &config, nil -} - -func (c *Config) parse() error { - baseImageSize, err := units.RAMInBytes(c.BaseImageSize) - if err != nil { - return errors.Wrapf(err, "failed to parse base image size: '%s'", c.BaseImageSize) - } - - c.BaseImageSizeBytes = uint64(baseImageSize) - return nil -} - -// Validate makes sure configuration fields are valid -func (c *Config) Validate() error { - var result *multierror.Error - - if c.PoolName == "" { - result = multierror.Append(result, fmt.Errorf("pool_name is required")) - } - - if c.RootPath == "" { - result = multierror.Append(result, fmt.Errorf("root_path is required")) - } - - if c.BaseImageSize == "" { - result = multierror.Append(result, fmt.Errorf("base_image_size is required")) - } - - return result.ErrorOrNil() -} diff --git a/snapshotter/devmapper/config_test.go b/snapshotter/devmapper/config_test.go deleted file mode 100644 index 314d71234..000000000 --- a/snapshotter/devmapper/config_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "encoding/json" - "io/ioutil" - "os" - "testing" - - "github.com/hashicorp/go-multierror" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadConfig(t *testing.T) { - expected := Config{ - RootPath: "/tmp", - PoolName: "test", - BaseImageSize: "128Mb", - } - - data, err := json.Marshal(&expected) - require.NoErrorf(t, err, "failed to serialize config") - - file, err := ioutil.TempFile("", "devmapper-config-") - require.NoError(t, err) - - defer func() { - err := file.Close() - assert.NoError(t, err) - - err = os.Remove(file.Name()) - assert.NoError(t, err) - }() - - _, err = file.Write(data) - require.NoError(t, err) - - loaded, err := LoadConfig(file.Name()) - require.NoError(t, err) - - assert.Equal(t, loaded.RootPath, expected.RootPath) - assert.Equal(t, loaded.PoolName, expected.PoolName) - assert.Equal(t, loaded.BaseImageSize, expected.BaseImageSize) - - assert.EqualValues(t, 128*1024*1024, loaded.BaseImageSizeBytes) -} - -func TestLoadConfigInvalidPath(t *testing.T) { - _, err := LoadConfig("") - require.Equal(t, os.ErrNotExist, err) - - _, err = LoadConfig("/dev/null") - require.Error(t, err) -} - -func TestParseInvalidData(t *testing.T) { - config := Config{ - BaseImageSize: "y", - } - - err := config.parse() - require.Error(t, err) - require.EqualError(t, err, "failed to parse base image size: 'y': invalid size: 'y'") -} - -func TestFieldValidation(t *testing.T) { - config := &Config{} - err := config.Validate() - require.Error(t, err) - - multErr := (err).(*multierror.Error) - require.Len(t, multErr.Errors, 3) - - assert.Error(t, multErr.Errors[0], "pool_name is empty") - assert.Error(t, multErr.Errors[1], "root_path is empty") - assert.Error(t, multErr.Errors[2], "base_image_size is empty") -} - -func TestExistingPoolFieldValidation(t *testing.T) { - config := &Config{ - PoolName: "test", - RootPath: "test", - BaseImageSize: "10mb", - } - - err := config.Validate() - assert.NoError(t, err) -} diff --git a/snapshotter/devmapper/device_info.go b/snapshotter/devmapper/device_info.go deleted file mode 100644 index 4994283d7..000000000 --- a/snapshotter/devmapper/device_info.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "fmt" -) - -const ( - maxDeviceID = 0xffffff // Device IDs are 24-bit numbers -) - -// DeviceState represents current devmapper device state reflected in meta store -type DeviceState int - -const ( - // Unknown means that device just allocated and no operations were performed - Unknown DeviceState = iota - // Creating means that device is going to be created - Creating - // Created means that devices successfully created - Created - // Activating means that device is going to be activated - Activating - // Activated means that device successfully activated - Activated - // Suspending means that device is going to be suspended - Suspending - // Suspended means that device successfully suspended - Suspended - // Resuming means that device is going to be resumed from suspended state - Resuming - // Resumed means that device successfully resumed - Resumed - // Deactivating means that device is going to be deactivated - Deactivating - // Deactivated means that device successfully deactivated - Deactivated - // Removing means that device is going to be removed - Removing - // Removed means that device successfully removed but not yet deleted from meta store - Removed -) - -func (s DeviceState) String() string { - switch s { - case Creating: - return "Creating" - case Created: - return "Created" - case Activating: - return "Activating" - case Activated: - return "Activated" - case Suspending: - return "Suspending" - case Suspended: - return "Suspended" - case Resuming: - return "Resuming" - case Resumed: - return "Resumed" - case Deactivating: - return "Deactivating" - case Deactivated: - return "Deactivated" - case Removing: - return "Removing" - case Removed: - return "Removed" - default: - return fmt.Sprintf("unknown %d", s) - } -} - -// DeviceInfo represents metadata for thin device within thin-pool -type DeviceInfo struct { - // DeviceID is a 24-bit number assigned to a device within thin-pool device - DeviceID uint32 `json:"device_id"` - // Size is a thin device size - Size uint64 `json:"size"` - // Name is a device name to be used in /dev/mapper/ - Name string `json:"name"` - // ParentName is a name of parent device (if snapshot) - ParentName string `json:"parent_name"` - // State represents current device state - State DeviceState `json:"state"` - // Error details if device state change failed - Error string `json:"error"` -} diff --git a/snapshotter/devmapper/metadata.go b/snapshotter/devmapper/metadata.go deleted file mode 100644 index 53897915d..000000000 --- a/snapshotter/devmapper/metadata.go +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - "github.com/pkg/errors" - bolt "go.etcd.io/bbolt" -) - -type ( - // DeviceInfoCallback is a callback used for device updates - DeviceInfoCallback func(deviceInfo *DeviceInfo) error -) - -type deviceIDState byte - -const ( - deviceFree deviceIDState = iota - deviceTaken -) - -// Bucket names -var ( - devicesBucketName = []byte("devices") // Contains thin devices metadata = - deviceIDBucketName = []byte("device_ids") // Tracks used device ids = -) - -var ( - // ErrNotFound represents an error returned when object not found in meta store - ErrNotFound = errors.New("not found") - // ErrAlreadyExists represents an error returned when object can't be duplicated in meta store - ErrAlreadyExists = errors.New("object already exists") -) - -// PoolMetadata keeps device info for the given thin-pool device, it also responsible for -// generating next available device ids and tracking devmapper transaction numbers -type PoolMetadata struct { - db *bolt.DB -} - -// NewPoolMetadata creates new or open existing pool metadata database -func NewPoolMetadata(dbfile string) (*PoolMetadata, error) { - db, err := bolt.Open(dbfile, 0600, nil) - if err != nil { - return nil, err - } - - metadata := &PoolMetadata{db: db} - if err := metadata.ensureDatabaseInitialized(); err != nil { - return nil, errors.Wrap(err, "failed to initialize database") - } - - return metadata, nil -} - -// ensureDatabaseInitialized creates buckets required for metadata store in order -// to avoid bucket existence checks across the code -func (m *PoolMetadata) ensureDatabaseInitialized() error { - return m.db.Update(func(tx *bolt.Tx) error { - if _, err := tx.CreateBucketIfNotExists(devicesBucketName); err != nil { - return err - } - - if _, err := tx.CreateBucketIfNotExists(deviceIDBucketName); err != nil { - return err - } - - return nil - }) -} - -// AddDevice saves device info to database. -func (m *PoolMetadata) AddDevice(ctx context.Context, info *DeviceInfo) error { - return m.db.Update(func(tx *bolt.Tx) error { - devicesBucket := tx.Bucket(devicesBucketName) - - // Make sure device name is unique - if err := getObject(devicesBucket, info.Name, nil); err == nil { - return ErrAlreadyExists - } - - // Find next available device ID - deviceID, err := getNextDeviceID(tx) - if err != nil { - return err - } - - info.DeviceID = deviceID - - return putObject(devicesBucket, info.Name, info, false) - }) -} - -// getNextDeviceID finds the next free device ID by taking a cursor -// through the deviceIDBucketName bucket and finding the next sequentially -// unassigned ID. Device ID state is marked by a byte deviceFree or -// deviceTaken. Low device IDs will be reused sooner. -func getNextDeviceID(tx *bolt.Tx) (uint32, error) { - bucket := tx.Bucket(deviceIDBucketName) - cursor := bucket.Cursor() - - // Check if any device id can be reused. - // Bolt stores its keys in byte-sorted order within a bucket. - // This makes sequential iteration extremely fast. - for key, taken := cursor.First(); key != nil; key, taken = cursor.Next() { - isFree := taken[0] == byte(deviceFree) - if !isFree { - continue - } - - parsedID, err := strconv.ParseUint(string(key), 10, 32) - if err != nil { - return 0, err - } - - id := uint32(parsedID) - if err := markDeviceID(tx, id, deviceTaken); err != nil { - return 0, err - } - - return id, nil - } - - // Try allocate new device ID - seq, err := bucket.NextSequence() - if err != nil { - return 0, err - } - - if seq >= maxDeviceID { - return 0, errors.Errorf("dm-meta: couldn't find free device key") - } - - id := uint32(seq) - if err := markDeviceID(tx, id, deviceTaken); err != nil { - return 0, err - } - - return id, nil -} - -// markDeviceID marks a device as deviceFree or deviceTaken -func markDeviceID(tx *bolt.Tx, deviceID uint32, state deviceIDState) error { - var ( - bucket = tx.Bucket(deviceIDBucketName) - key = strconv.FormatUint(uint64(deviceID), 10) - value = []byte{byte(state)} - ) - - if err := bucket.Put([]byte(key), value); err != nil { - return errors.Wrapf(err, "failed to free device id %q", key) - } - - return nil -} - -// UpdateDevice updates device info in metadata store. -// The callback should be used to indicate whether device info update was successful or not. -// An error returned from the callback will rollback the update transaction in the database. -// Name and Device ID are not allowed to change. -func (m *PoolMetadata) UpdateDevice(ctx context.Context, name string, fn DeviceInfoCallback) error { - return m.db.Update(func(tx *bolt.Tx) error { - var ( - device = &DeviceInfo{} - bucket = tx.Bucket(devicesBucketName) - ) - - if err := getObject(bucket, name, device); err != nil { - return err - } - - // Don't allow changing these values, keep things in sync with devmapper - name := device.Name - devID := device.DeviceID - - if err := fn(device); err != nil { - return err - } - - if name != device.Name { - return fmt.Errorf("failed to update device info, name didn't match: %q %q", name, device.Name) - } - - if devID != device.DeviceID { - return fmt.Errorf("failed to update device info, device id didn't match: %d %d", devID, device.DeviceID) - } - - return putObject(bucket, name, device, true) - }) -} - -// GetDevice retrieves device info by name from database -func (m *PoolMetadata) GetDevice(ctx context.Context, name string) (*DeviceInfo, error) { - var ( - dev DeviceInfo - err error - ) - - err = m.db.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket(devicesBucketName) - return getObject(bucket, name, &dev) - }) - - return &dev, err -} - -// RemoveDevice removes device info from store. -func (m *PoolMetadata) RemoveDevice(ctx context.Context, name string) error { - return m.db.Update(func(tx *bolt.Tx) error { - var ( - device = &DeviceInfo{} - bucket = tx.Bucket(devicesBucketName) - ) - - if err := getObject(bucket, name, device); err != nil { - return err - } - - if err := bucket.Delete([]byte(name)); err != nil { - return errors.Wrapf(err, "failed to delete device info for %q", name) - } - - if err := markDeviceID(tx, device.DeviceID, deviceFree); err != nil { - return err - } - - return nil - }) -} - -// GetDeviceNames retrieves the list of device names currently stored in database -func (m *PoolMetadata) GetDeviceNames(ctx context.Context) ([]string, error) { - var ( - names []string - err error - ) - - err = m.db.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket(devicesBucketName) - return bucket.ForEach(func(k, _ []byte) error { - names = append(names, string(k)) - return nil - }) - }) - - if err != nil { - return nil, err - } - - return names, nil -} - -// Close closes metadata store -func (m *PoolMetadata) Close() error { - if err := m.db.Close(); err != nil && err != bolt.ErrDatabaseNotOpen { - return err - } - - return nil -} - -func putObject(bucket *bolt.Bucket, key string, obj interface{}, overwrite bool) error { - keyBytes := []byte(key) - - if !overwrite && bucket.Get(keyBytes) != nil { - return errors.Errorf("object with key %q already exists", key) - } - - data, err := json.Marshal(obj) - if err != nil { - return errors.Wrapf(err, "failed to marshal object with key %q", key) - } - - if err := bucket.Put(keyBytes, data); err != nil { - return errors.Wrapf(err, "failed to insert object with key %q", key) - } - - return nil -} - -func getObject(bucket *bolt.Bucket, key string, obj interface{}) error { - data := bucket.Get([]byte(key)) - if data == nil { - return ErrNotFound - } - - if obj != nil { - if err := json.Unmarshal(data, obj); err != nil { - return errors.Wrapf(err, "failed to unmarshal object with key %q", key) - } - } - - return nil -} diff --git a/snapshotter/devmapper/metadata_test.go b/snapshotter/devmapper/metadata_test.go deleted file mode 100644 index d468183f0..000000000 --- a/snapshotter/devmapper/metadata_test.go +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - testCtx = context.Background() -) - -func TestPoolMetadata_AddDevice(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - expected := &DeviceInfo{ - Name: "test2", - ParentName: "test1", - Size: 1, - State: Activated, - } - - err := store.AddDevice(testCtx, expected) - assert.NoError(t, err) - - result, err := store.GetDevice(testCtx, "test2") - assert.NoError(t, err) - - assert.Equal(t, expected.Name, result.Name) - assert.Equal(t, expected.ParentName, result.ParentName) - assert.Equal(t, expected.Size, result.Size) - assert.Equal(t, expected.State, result.State) - assert.NotZero(t, result.DeviceID) - assert.Equal(t, expected.DeviceID, result.DeviceID) -} - -func TestPoolMetadata_AddDeviceRollback(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - err := store.AddDevice(testCtx, &DeviceInfo{Name: ""}) - assert.Error(t, err) - - _, err = store.GetDevice(testCtx, "") - assert.Equal(t, ErrNotFound, err) -} - -func TestPoolMetadata_AddDeviceDuplicate(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - err := store.AddDevice(testCtx, &DeviceInfo{Name: "test"}) - assert.NoError(t, err) - - err = store.AddDevice(testCtx, &DeviceInfo{Name: "test"}) - assert.Equal(t, ErrAlreadyExists, err) -} - -func TestPoolMetadata_ReuseDeviceID(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - info1 := &DeviceInfo{Name: "test1"} - err := store.AddDevice(testCtx, info1) - assert.NoError(t, err) - - info2 := &DeviceInfo{Name: "test2"} - err = store.AddDevice(testCtx, info2) - assert.NoError(t, err) - - assert.NotEqual(t, info1.DeviceID, info2.DeviceID) - assert.NotZero(t, info1.DeviceID) - - err = store.RemoveDevice(testCtx, info2.Name) - assert.NoError(t, err) - - info3 := &DeviceInfo{Name: "test3"} - err = store.AddDevice(testCtx, info3) - assert.NoError(t, err) - - assert.Equal(t, info2.DeviceID, info3.DeviceID) -} - -func TestPoolMetadata_RemoveDevice(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - err := store.AddDevice(testCtx, &DeviceInfo{Name: "test"}) - assert.NoError(t, err) - - err = store.RemoveDevice(testCtx, "test") - assert.NoError(t, err) - - _, err = store.GetDevice(testCtx, "test") - assert.Equal(t, ErrNotFound, err) -} - -func TestPoolMetadata_UpdateDevice(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - oldInfo := &DeviceInfo{ - Name: "test1", - ParentName: "test2", - Size: 3, - State: Activated, - } - - err := store.AddDevice(testCtx, oldInfo) - assert.NoError(t, err) - - err = store.UpdateDevice(testCtx, oldInfo.Name, func(info *DeviceInfo) error { - info.ParentName = "test5" - info.Size = 6 - info.State = Created - return nil - }) - - assert.NoError(t, err) - - newInfo, err := store.GetDevice(testCtx, "test1") - require.NoError(t, err) - - assert.Equal(t, "test1", newInfo.Name) - assert.Equal(t, "test5", newInfo.ParentName) - assert.EqualValues(t, 6, newInfo.Size) - assert.Equal(t, Created, newInfo.State) -} - -func TestPoolMetadata_GetDeviceNames(t *testing.T) { - tempDir, store := createStore(t) - defer cleanupStore(t, tempDir, store) - - err := store.AddDevice(testCtx, &DeviceInfo{Name: "test1"}) - assert.NoError(t, err) - - err = store.AddDevice(testCtx, &DeviceInfo{Name: "test2"}) - assert.NoError(t, err) - - names, err := store.GetDeviceNames(testCtx) - assert.NoError(t, err) - require.Len(t, names, 2) - - assert.Equal(t, "test1", names[0]) - assert.Equal(t, "test2", names[1]) -} - -func createStore(t *testing.T) (tempDir string, store *PoolMetadata) { - tempDir, err := ioutil.TempDir("", "pool-metadata-") - require.NoErrorf(t, err, "couldn't create temp directory for metadata tests") - - path := filepath.Join(tempDir, "test.db") - metadata, err := NewPoolMetadata(path) - require.NoError(t, err) - - return tempDir, metadata -} - -func cleanupStore(t *testing.T, tempDir string, store *PoolMetadata) { - err := store.Close() - assert.NoErrorf(t, err, "failed to close metadata store") - - err = os.RemoveAll(tempDir) - assert.NoErrorf(t, err, "failed to cleanup temp directory") -} diff --git a/snapshotter/devmapper/pool_device.go b/snapshotter/devmapper/pool_device.go deleted file mode 100644 index 8b0bee84d..000000000 --- a/snapshotter/devmapper/pool_device.go +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "path/filepath" - "strconv" - - "github.com/containerd/containerd/log" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/dmsetup" -) - -// PoolDevice ties together data and metadata volumes, represents thin-pool and manages volumes, snapshots and device ids. -type PoolDevice struct { - poolName string - metadata *PoolMetadata -} - -// NewPoolDevice creates new thin-pool from existing data and metadata volumes. -// If pool 'poolName' already exists, it'll be reloaded with new parameters. -func NewPoolDevice(ctx context.Context, config *Config) (*PoolDevice, error) { - log.G(ctx).Infof("initializing pool device %q", config.PoolName) - - version, err := dmsetup.Version() - if err != nil { - log.G(ctx).Errorf("dmsetup not available") - return nil, err - } - - log.G(ctx).Infof("using dmsetup:\n%s", version) - - dbpath := filepath.Join(config.RootPath, config.PoolName+".db") - poolMetaStore, err := NewPoolMetadata(dbpath) - if err != nil { - return nil, err - } - - // Make sure pool exists and available - poolPath := dmsetup.GetFullDevicePath(config.PoolName) - if _, err := dmsetup.Info(poolPath); err != nil { - return nil, errors.Wrapf(err, "failed to query pool %q", poolPath) - } - - return &PoolDevice{ - poolName: config.PoolName, - metadata: poolMetaStore, - }, nil -} - -// transition invokes 'updateStateFn' callback to perform devmapper operation and reflects device state changes/errors in meta store. -// 'tryingState' will be set before invoking callback. If callback succeeded 'successState' will be set, otherwise -// error details will be recorded in meta store. -func (p *PoolDevice) transition(ctx context.Context, deviceName string, tryingState DeviceState, successState DeviceState, updateStateFn func() error) error { - // Set device to trying state - uerr := p.metadata.UpdateDevice(ctx, deviceName, func(deviceInfo *DeviceInfo) error { - deviceInfo.State = tryingState - return nil - }) - - if uerr != nil { - return errors.Wrapf(uerr, "failed to set device %q state to %q", deviceName, tryingState) - } - - var result *multierror.Error - - // Invoke devmapper operation - err := updateStateFn() - - if err != nil { - result = multierror.Append(result, err) - } - - // If operation succeeded transition to success state, otherwise save error details - uerr = p.metadata.UpdateDevice(ctx, deviceName, func(deviceInfo *DeviceInfo) error { - if err == nil { - deviceInfo.State = successState - deviceInfo.Error = "" - } else { - deviceInfo.Error = err.Error() - } - return nil - }) - - if uerr != nil { - result = multierror.Append(result, uerr) - } - - return result.ErrorOrNil() -} - -// CreateThinDevice creates new devmapper thin-device with given name and size. -// Device ID for thin-device will be allocated from metadata store. -// If allocation successful, device will be activated with /dev/mapper/ -func (p *PoolDevice) CreateThinDevice(ctx context.Context, deviceName string, virtualSizeBytes uint64) (retErr error) { - info := &DeviceInfo{ - Name: deviceName, - Size: virtualSizeBytes, - State: Unknown, - } - - // Save initial device metadata and allocate new device ID from store - if err := p.metadata.AddDevice(ctx, info); err != nil { - return errors.Wrapf(err, "failed to save initial metadata for new thin device %q", deviceName) - } - - defer func() { - if retErr == nil { - return - } - - // Rollback metadata - retErr = multierror.Append(retErr, p.metadata.RemoveDevice(ctx, info.Name)) - }() - - // Create thin device - if err := p.createDevice(ctx, info); err != nil { - return err - } - - defer func() { - if retErr == nil { - return - } - - // Rollback creation - retErr = multierror.Append(retErr, p.deleteDevice(ctx, info)) - }() - - return p.activateDevice(ctx, info) -} - -// createDevice creates thin device -func (p *PoolDevice) createDevice(ctx context.Context, info *DeviceInfo) error { - if err := p.transition(ctx, info.Name, Creating, Created, func() error { - return dmsetup.CreateDevice(p.poolName, info.DeviceID) - }); err != nil { - return errors.Wrapf(err, "failed to create new thin device %q (dev: %d)", info.Name, info.DeviceID) - } - - return nil -} - -// activateDevice activates thin device -func (p *PoolDevice) activateDevice(ctx context.Context, info *DeviceInfo) error { - if err := p.transition(ctx, info.Name, Activating, Activated, func() error { - return dmsetup.ActivateDevice(p.poolName, info.Name, info.DeviceID, info.Size, "") - }); err != nil { - return errors.Wrapf(err, "failed to activate new thin device %q (dev: %d)", info.Name, info.DeviceID) - } - - return nil -} - -// CreateSnapshotDevice creates and activates new thin-device from parent thin-device (makes snapshot) -func (p *PoolDevice) CreateSnapshotDevice(ctx context.Context, deviceName string, snapshotName string, virtualSizeBytes uint64) (retErr error) { - baseInfo, err := p.metadata.GetDevice(ctx, deviceName) - if err != nil { - return errors.Wrapf(err, "failed to query device metadata for %q", deviceName) - } - - // Suspend thin device if it was activated previously to avoid corruptions - isActivated := p.IsActivated(baseInfo.Name) - if isActivated { - if err := p.suspendDevice(ctx, baseInfo); err != nil { - return err - } - - // Resume back base thin device on exit - defer func() { - retErr = multierror.Append(retErr, p.resumeDevice(ctx, baseInfo)).ErrorOrNil() - }() - } - - snapInfo := &DeviceInfo{ - Name: snapshotName, - Size: virtualSizeBytes, - ParentName: deviceName, - State: Unknown, - } - - // Save snapshot metadata and allocate new device ID - if err := p.metadata.AddDevice(ctx, snapInfo); err != nil { - return errors.Wrapf(err, "failed to save initial metadata for snapshot %q", snapshotName) - } - - defer func() { - if retErr == nil { - return - } - - // Rollback metadata - retErr = multierror.Append(retErr, p.metadata.RemoveDevice(ctx, snapInfo.Name)) - }() - - // Create thin device snapshot - if err := p.createSnapshot(ctx, baseInfo, snapInfo); err != nil { - return err - } - - defer func() { - if retErr == nil { - return - } - - // Rollback snapshot creation - retErr = multierror.Append(retErr, p.deleteDevice(ctx, snapInfo)) - }() - - // Activate snapshot device - return p.activateDevice(ctx, snapInfo) -} - -func (p *PoolDevice) suspendDevice(ctx context.Context, info *DeviceInfo) error { - if err := p.transition(ctx, info.Name, Suspending, Suspended, func() error { - return dmsetup.SuspendDevice(info.Name) - }); err != nil { - return errors.Wrapf(err, "failed to suspend device %q", info.Name) - } - - return nil -} - -func (p *PoolDevice) resumeDevice(ctx context.Context, info *DeviceInfo) error { - if err := p.transition(ctx, info.Name, Resuming, Resumed, func() error { - return dmsetup.ResumeDevice(info.Name) - }); err != nil { - return errors.Wrapf(err, "failed to resume device %q", info.Name) - } - - return nil -} - -func (p *PoolDevice) createSnapshot(ctx context.Context, baseInfo, snapInfo *DeviceInfo) error { - if err := p.transition(ctx, snapInfo.Name, Creating, Created, func() error { - return dmsetup.CreateSnapshot(p.poolName, snapInfo.DeviceID, baseInfo.DeviceID) - }); err != nil { - return errors.Wrapf(err, - "failed to create snapshot %q (dev: %d) from %q (dev: %d)", - snapInfo.Name, - snapInfo.DeviceID, - baseInfo.Name, - baseInfo.DeviceID) - } - - return nil -} - -// DeactivateDevice deactivates thin device -func (p *PoolDevice) DeactivateDevice(ctx context.Context, deviceName string, deferred bool) error { - if !p.IsActivated(deviceName) { - return nil - } - - opts := []dmsetup.RemoveDeviceOpt{dmsetup.RemoveWithForce, dmsetup.RemoveWithRetries} - if deferred { - opts = append(opts, dmsetup.RemoveDeferred) - } - - if err := p.transition(ctx, deviceName, Deactivating, Deactivated, func() error { - return dmsetup.RemoveDevice(deviceName, opts...) - }); err != nil { - return errors.Wrapf(err, "failed to deactivate device %q", deviceName) - } - - return nil -} - -// IsActivated returns true if thin-device is activated and not suspended -func (p *PoolDevice) IsActivated(deviceName string) bool { - infos, err := dmsetup.Info(deviceName) - if err != nil || len(infos) != 1 { - // Couldn't query device info, device not active - return false - } - - if devInfo := infos[0]; devInfo.Suspended { - return false - } - - return true -} - -// GetUsage reports total size in bytes consumed by a thin-device. -// It relies on the number of used blocks reported by 'dmsetup status'. -// The output looks like: -// device2: 0 204800 thin 17280 204799 -// Where 17280 is the number of used sectors -func (p *PoolDevice) GetUsage(deviceName string) (int64, error) { - status, err := dmsetup.Status(deviceName) - if err != nil { - return 0, errors.Wrapf(err, "can't get status for device %q", deviceName) - } - - if len(status.Params) == 0 { - return 0, errors.Errorf("failed to get the number of used blocks, unexpected output from dmsetup status") - } - - count, err := strconv.ParseInt(status.Params[0], 10, 64) - if err != nil { - return 0, errors.Wrapf(err, "failed to parse status params: %q", status.Params[0]) - } - - return count * dmsetup.SectorSize, nil -} - -// RemoveDevice completely wipes out thin device from thin-pool and frees it's device ID -func (p *PoolDevice) RemoveDevice(ctx context.Context, deviceName string) error { - info, err := p.metadata.GetDevice(ctx, deviceName) - if err != nil { - return errors.Wrapf(err, "can't query metadata for device %q", deviceName) - } - - if err := p.DeactivateDevice(ctx, deviceName, true); err != nil { - return err - } - - if err := p.deleteDevice(ctx, info); err != nil { - return err - } - - // Remove record from meta store and free device ID - if err := p.metadata.RemoveDevice(ctx, deviceName); err != nil { - return errors.Wrapf(err, "can't remove device %q metadata from store after removal", deviceName) - } - - return nil -} - -func (p *PoolDevice) deleteDevice(ctx context.Context, info *DeviceInfo) error { - if err := p.transition(ctx, info.Name, Removing, Removed, func() error { - // Send 'delete' message to thin-pool - return dmsetup.DeleteDevice(p.poolName, info.DeviceID) - }); err != nil { - return errors.Wrapf(err, "failed to delete device %q (dev id: %d)", info.Name, info.DeviceID) - } - - return nil -} - -// RemovePool deactivates all child thin-devices and removes thin-pool device -func (p *PoolDevice) RemovePool(ctx context.Context) error { - deviceNames, err := p.metadata.GetDeviceNames(ctx) - if err != nil { - return errors.Wrap(err, "can't query device names") - } - - var result *multierror.Error - - // Deactivate devices if any - for _, name := range deviceNames { - if err := p.DeactivateDevice(ctx, name, true); err != nil { - result = multierror.Append(result, errors.Wrapf(err, "failed to remove %q", name)) - } - } - - if err := dmsetup.RemoveDevice(p.poolName, dmsetup.RemoveWithForce, dmsetup.RemoveWithRetries, dmsetup.RemoveDeferred); err != nil { - result = multierror.Append(result, errors.Wrapf(err, "failed to remove pool %q", p.poolName)) - } - - return result.ErrorOrNil() -} - -// Close closes pool device (thin-pool will not be removed) -func (p *PoolDevice) Close() error { - return p.metadata.Close() -} diff --git a/snapshotter/devmapper/pool_device_test.go b/snapshotter/devmapper/pool_device_test.go deleted file mode 100644 index 9d7acee14..000000000 --- a/snapshotter/devmapper/pool_device_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "testing" - "time" - - "github.com/containerd/containerd/mount" - "github.com/docker/go-units" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/firecracker-microvm/firecracker-containerd/internal" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/dmsetup" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/losetup" -) - -const ( - thinDevice1 = "thin-1" - thinDevice2 = "thin-2" - snapDevice1 = "snap-1" - device1Size = 100000 - device2Size = 200000 - testsPrefix = "devmapper-snapshotter-tests-" -) - -// TestPoolDevice runs integration tests for pool device. -// The following scenario implemented: -// - Create pool device with name 'test-pool-device' -// - Create two thin volumes 'thin-1' and 'thin-2' -// - Write ext4 file system on 'thin-1' and make sure it'errs moutable -// - Write v1 test file on 'thin-1' volume -// - Take 'thin-1' snapshot 'snap-1' -// - Change v1 file to v2 on 'thin-1' -// - Mount 'snap-1' and make sure test file is v1 -// - Unmount volumes and remove all devices -func TestPoolDevice(t *testing.T) { - internal.RequiresRoot(t) - logrus.SetLevel(logrus.DebugLevel) - ctx := context.Background() - - tempDir, err := ioutil.TempDir("", "pool-device-test-") - require.NoErrorf(t, err, "couldn't get temp directory for testing") - - _, loopDataDevice := createLoopbackDevice(t, tempDir) - _, loopMetaDevice := createLoopbackDevice(t, tempDir) - - poolName := fmt.Sprintf("test-pool-device-%d", time.Now().Nanosecond()) - err = dmsetup.CreatePool(poolName, loopDataDevice, loopMetaDevice, 64*1024/dmsetup.SectorSize) - require.NoErrorf(t, err, "failed to create pool %q", poolName) - - defer func() { - // Detach loop devices and remove images - err := losetup.DetachLoopDevice(loopDataDevice, loopMetaDevice) - assert.NoError(t, err) - - err = os.RemoveAll(tempDir) - assert.NoErrorf(t, err, "couldn't cleanup temp directory") - }() - - config := &Config{ - PoolName: poolName, - RootPath: tempDir, - BaseImageSize: "16mb", - BaseImageSizeBytes: 16 * 1024 * 1024, - } - - pool, err := NewPoolDevice(ctx, config) - require.NoError(t, err, "can't create device pool") - require.NotNil(t, pool) - - defer func() { - err := pool.RemovePool(ctx) - require.NoError(t, err, "can't close device pool") - }() - - // Create thin devices - t.Run("CreateThinDevice", func(t *testing.T) { - testCreateThinDevice(t, pool) - }) - - // Make ext4 filesystem on 'thin-1' - t.Run("MakeFileSystem", func(t *testing.T) { - testMakeFileSystem(t, pool) - }) - - // Mount 'thin-1' - mount.WithTempMount(ctx, getMounts(thinDevice1), func(thin1MountPath string) error { - // Write v1 test file on 'thin-1' device - thin1TestFilePath := filepath.Join(thin1MountPath, "TEST") - err := ioutil.WriteFile(thin1TestFilePath, []byte("test file (v1)"), 0700) - require.NoErrorf(t, err, "failed to write test file v1 on '%s' volume", thinDevice1) - - // Take snapshot of 'thin-1' - t.Run("CreateSnapshotDevice", func(t *testing.T) { - testCreateSnapshot(t, pool) - }) - - // Update TEST file on 'thin-1' to v2 - err = ioutil.WriteFile(thin1TestFilePath, []byte("test file (v2)"), 0700) - assert.NoErrorf(t, err, "failed to write test file v2 on 'thin-1' volume after taking snapshot") - - return err - }) - - // Mount 'snap-1' and make sure TEST file is v1 - mount.WithTempMount(ctx, getMounts(snapDevice1), func(snap1MountPath string) error { - // Read test file from snapshot device and make sure it's v1 - fileData, err := ioutil.ReadFile(filepath.Join(snap1MountPath, "TEST")) - assert.NoErrorf(t, err, "couldn't read test file from '%s' device", snapDevice1) - assert.EqualValues(t, "test file (v1)", string(fileData), "test file content is invalid on snapshot") - - return err - }) - - t.Run("DeactivateDevice", func(t *testing.T) { - testDeactivateThinDevice(t, pool) - }) - - t.Run("RemoveDevice", func(t *testing.T) { - testRemoveThinDevice(t, pool) - }) -} - -func testCreateThinDevice(t *testing.T, pool *PoolDevice) { - ctx := context.Background() - - err := pool.CreateThinDevice(ctx, thinDevice1, device1Size) - require.NoError(t, err, "can't create first thin device") - - err = pool.CreateThinDevice(ctx, thinDevice1, device1Size) - require.Error(t, err, "device pool allows duplicated device names") - - err = pool.CreateThinDevice(ctx, thinDevice2, device2Size) - require.NoError(t, err, "can't create second thin device") - - deviceInfo1, err := pool.metadata.GetDevice(ctx, thinDevice1) - assert.NoError(t, err) - - deviceInfo2, err := pool.metadata.GetDevice(ctx, thinDevice2) - assert.NoError(t, err) - - assert.NotEqual(t, deviceInfo1.DeviceID, deviceInfo2.DeviceID, "assigned device ids should be different") - - usage, err := pool.GetUsage(thinDevice1) - assert.NoError(t, err) - assert.Zero(t, usage) -} - -func testMakeFileSystem(t *testing.T, pool *PoolDevice) { - devicePath := dmsetup.GetFullDevicePath(thinDevice1) - args := []string{ - devicePath, - "-E", - "nodiscard,lazy_itable_init=0,lazy_journal_init=0", - } - - output, err := exec.Command("mkfs.ext4", args...).CombinedOutput() - require.NoErrorf(t, err, "failed to make filesystem on '%s': %s", thinDevice1, string(output)) - - usage, err := pool.GetUsage(thinDevice1) - assert.NoError(t, err) - assert.NotZero(t, usage) -} - -func testCreateSnapshot(t *testing.T, pool *PoolDevice) { - err := pool.CreateSnapshotDevice(context.Background(), thinDevice1, snapDevice1, device1Size) - assert.NoErrorf(t, err, "failed to create snapshot from '%s' volume", thinDevice1) -} - -func testDeactivateThinDevice(t *testing.T, pool *PoolDevice) { - deviceList := []string{ - thinDevice2, - snapDevice1, - } - - for _, deviceName := range deviceList { - assert.True(t, pool.IsActivated(deviceName)) - - err := pool.DeactivateDevice(context.Background(), deviceName, false) - assert.NoErrorf(t, err, "failed to remove '%s'", deviceName) - - assert.False(t, pool.IsActivated(deviceName)) - } -} - -func testRemoveThinDevice(t *testing.T, pool *PoolDevice) { - err := pool.RemoveDevice(testCtx, thinDevice1) - assert.NoErrorf(t, err, "should delete thin device from pool") -} - -func getMounts(thinDeviceName string) []mount.Mount { - return []mount.Mount{ - { - Source: dmsetup.GetFullDevicePath(thinDeviceName), - Type: "ext4", - }, - } -} - -func createLoopbackDevice(t *testing.T, dir string) (string, string) { - file, err := ioutil.TempFile(dir, testsPrefix) - require.NoError(t, err) - - size, err := units.RAMInBytes("128Mb") - require.NoError(t, err) - - err = file.Truncate(size) - require.NoError(t, err) - - err = file.Close() - require.NoError(t, err) - - imagePath := file.Name() - - loopDevice, err := losetup.AttachLoopDevice(imagePath) - require.NoError(t, err) - - return imagePath, loopDevice -} diff --git a/snapshotter/devmapper/snapshotter.go b/snapshotter/devmapper/snapshotter.go deleted file mode 100644 index 88f68c836..000000000 --- a/snapshotter/devmapper/snapshotter.go +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - - "github.com/containerd/containerd/log" - "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/snapshots" - "github.com/containerd/containerd/snapshots/storage" - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/dmsetup" -) - -const ( - metadataFileName = "metadata.db" - fsTypeExt4 = "ext4" -) - -type closeFunc func() error - -// Snapshotter implements containerd's snapshotter (https://godoc.org/github.com/containerd/containerd/snapshots#Snapshotter) -// based on Linux device-mapper targets. -type Snapshotter struct { - store *storage.MetaStore - pool *PoolDevice - config *Config - cleanupFn []closeFunc - closeOnce sync.Once -} - -// NewSnapshotter creates new device mapper snapshotter. -// Internally it creates thin-pool device (or reloads if it's already exists) and -// initializes a database file for metadata. -func NewSnapshotter(ctx context.Context, config *Config) (*Snapshotter, error) { - // Make sure snapshotter configuration valid before running - if err := config.parse(); err != nil { - return nil, err - } - - if err := config.Validate(); err != nil { - return nil, err - } - - var cleanupFn []closeFunc - - if err := os.MkdirAll(config.RootPath, 0750); err != nil && !os.IsExist(err) { - return nil, errors.Wrapf(err, "failed to create root directory: %s", config.RootPath) - } - - store, err := storage.NewMetaStore(filepath.Join(config.RootPath, metadataFileName)) - if err != nil { - return nil, errors.Wrap(err, "failed to create metastore") - } - - cleanupFn = append(cleanupFn, store.Close) - - poolDevice, err := NewPoolDevice(ctx, config) - if err != nil { - return nil, err - } - - cleanupFn = append(cleanupFn, poolDevice.Close) - - return &Snapshotter{ - store: store, - config: config, - pool: poolDevice, - cleanupFn: cleanupFn, - }, nil -} - -// Stat returns the info for an active or committed snapshot from store -func (s *Snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { - log.G(ctx).WithField("key", key).Debug("stat") - - var ( - info snapshots.Info - err error - ) - - err = s.withTransaction(ctx, false, func(ctx context.Context) error { - _, info, _, err = storage.GetInfo(ctx, key) - return err - }) - - return info, err -} - -// Update updates an existing snapshot info's data -func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { - log.G(ctx).Debugf("update: %s", strings.Join(fieldpaths, ", ")) - - var err error - err = s.withTransaction(ctx, true, func(ctx context.Context) error { - info, err = storage.UpdateInfo(ctx, info, fieldpaths...) - return err - }) - - return info, err -} - -// Usage returns the resource usage of an active or committed snapshot excluding the usage of parent snapshots. -func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { - log.G(ctx).WithField("key", key).Debug("usage") - - var ( - id string - err error - info snapshots.Info - usage snapshots.Usage - ) - - err = s.withTransaction(ctx, false, func(ctx context.Context) error { - id, info, usage, err = storage.GetInfo(ctx, key) - if err != nil { - return err - } - - if info.Kind == snapshots.KindActive { - deviceName := s.getDeviceName(id) - usage.Size, err = s.pool.GetUsage(deviceName) - if err != nil { - return err - } - } - - if info.Parent != "" { - // GetInfo returns total number of bytes used by a snapshot (including parent). - // So subtract parent usage in order to get delta consumed by layer itself. - _, _, parentUsage, err := storage.GetInfo(ctx, info.Parent) - if err != nil { - return err - } - - usage.Size -= parentUsage.Size - } - - return err - }) - - return usage, err -} - -// Mounts return the list of mounts for the active or view snapshot -func (s *Snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { - log.G(ctx).WithField("key", key).Debug("mounts") - - var ( - snap storage.Snapshot - err error - ) - - err = s.withTransaction(ctx, false, func(ctx context.Context) error { - snap, err = storage.GetSnapshot(ctx, key) - return err - }) - - return s.buildMounts(snap), nil -} - -// Prepare creates thin device for an active snapshot identified by key -func (s *Snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare") - - var ( - mounts []mount.Mount - err error - ) - - err = s.withTransaction(ctx, true, func(ctx context.Context) error { - mounts, err = s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts...) - return err - }) - - return mounts, err -} - -// View creates readonly thin device for the given snapshot key -func (s *Snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare") - - var ( - mounts []mount.Mount - err error - ) - - err = s.withTransaction(ctx, true, func(ctx context.Context) error { - mounts, err = s.createSnapshot(ctx, snapshots.KindView, key, parent, opts...) - return err - }) - - return mounts, err -} - -// Commit marks an active snapshot as committed in meta store. -// Block device unmount operation captures snapshot changes by itself, so no -// additional actions needed within Commit operation. -func (s *Snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { - log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit") - - return s.withTransaction(ctx, true, func(ctx context.Context) error { - id, _, _, err := storage.GetInfo(ctx, key) - if err != nil { - return err - } - - deviceName := s.getDeviceName(id) - size, err := s.pool.GetUsage(deviceName) - if err != nil { - return err - } - - usage := snapshots.Usage{ - Size: size, - } - - _, err = storage.CommitActive(ctx, key, name, usage, opts...) - return err - }) -} - -// Remove removes thin device and snapshot metadata by key -func (s *Snapshotter) Remove(ctx context.Context, key string) error { - log.G(ctx).WithField("key", key).Debug("remove") - - return s.withTransaction(ctx, true, func(ctx context.Context) error { - return s.removeDevice(ctx, key) - }) -} - -func (s *Snapshotter) removeDevice(ctx context.Context, key string) error { - snapID, _, err := storage.Remove(ctx, key) - if err != nil { - return err - } - - deviceName := s.getDeviceName(snapID) - if err := s.pool.RemoveDevice(ctx, deviceName); err != nil { - log.G(ctx).WithError(err).Errorf("failed to remove device") - return err - } - - return nil -} - -// Walk iterates through all metadata Info for the stored snapshots and calls the provided function for each. -func (s *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { - log.G(ctx).Debug("walk") - return s.withTransaction(ctx, false, func(ctx context.Context) error { - return storage.WalkInfo(ctx, fn) - }) -} - -// ResetPool deactivates and deletes all thin devices in thin-pool. -// Used for cleaning pool after benchmarking. -func (s *Snapshotter) ResetPool(ctx context.Context) error { - names, err := s.pool.metadata.GetDeviceNames(ctx) - if err != nil { - return err - } - - var result *multierror.Error - for _, name := range names { - if err := s.pool.RemoveDevice(ctx, name); err != nil { - result = multierror.Append(result, err) - } - } - - return result.ErrorOrNil() -} - -// Close releases devmapper snapshotter resources. -// All subsequent Close calls will be ignored. -func (s *Snapshotter) Close() error { - log.L.Debug("close") - - var result *multierror.Error - s.closeOnce.Do(func() { - for _, fn := range s.cleanupFn { - if err := fn(); err != nil { - result = multierror.Append(result, err) - } - } - }) - - return result.ErrorOrNil() -} - -func (s *Snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - snap, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...) - if err != nil { - return nil, err - } - - if len(snap.ParentIDs) == 0 { - deviceName := s.getDeviceName(snap.ID) - log.G(ctx).Debugf("creating new thin device '%s'", deviceName) - - err := s.pool.CreateThinDevice(ctx, deviceName, s.config.BaseImageSizeBytes) - if err != nil { - log.G(ctx).WithError(err).Errorf("failed to create thin device for snapshot %s", snap.ID) - return nil, err - } - - if err := s.mkfs(ctx, deviceName); err != nil { - // Rollback thin device creation if mkfs failed - return nil, multierror.Append(err, - s.pool.RemoveDevice(ctx, deviceName)) - } - } else { - parentDeviceName := s.getDeviceName(snap.ParentIDs[0]) - snapDeviceName := s.getDeviceName(snap.ID) - log.G(ctx).Debugf("creating snapshot device '%s' from '%s'", snapDeviceName, parentDeviceName) - - err := s.pool.CreateSnapshotDevice(ctx, parentDeviceName, snapDeviceName, s.config.BaseImageSizeBytes) - if err != nil { - log.G(ctx).WithError(err).Errorf("failed to create snapshot device from parent %s", parentDeviceName) - return nil, err - } - } - - mounts := s.buildMounts(snap) - - // Remove default directories not expected by the container image - _ = mount.WithTempMount(ctx, mounts, func(root string) error { - return os.Remove(filepath.Join(root, "lost+found")) - }) - - return mounts, nil -} - -// mkfs creates ext4 filesystem on the given devmapper device -func (s *Snapshotter) mkfs(ctx context.Context, deviceName string) error { - args := []string{ - "-E", - // We don't want any zeroing in advance when running mkfs on thin devices (see "man mkfs.ext4") - "nodiscard,lazy_itable_init=0,lazy_journal_init=0", - dmsetup.GetFullDevicePath(deviceName), - } - - log.G(ctx).Debugf("mkfs.ext4 %s", strings.Join(args, " ")) - output, err := exec.Command("mkfs.ext4", args...).CombinedOutput() - if err != nil { - log.G(ctx).WithError(err).Errorf("failed to write fs:\n%s", string(output)) - return err - } - - log.G(ctx).Debugf("mkfs:\n%s", string(output)) - return nil -} - -func (s *Snapshotter) getDeviceName(snapID string) string { - // Add pool name as prefix to avoid collisions with devices from other pools - return fmt.Sprintf("%s-snap-%s", s.config.PoolName, snapID) -} - -func (s *Snapshotter) getDevicePath(snap storage.Snapshot) string { - name := s.getDeviceName(snap.ID) - return dmsetup.GetFullDevicePath(name) -} - -func (s *Snapshotter) buildMounts(snap storage.Snapshot) []mount.Mount { - var options []string - - if snap.Kind != snapshots.KindActive { - options = append(options, "ro") - } - - mounts := []mount.Mount{ - { - Source: s.getDevicePath(snap), - Type: fsTypeExt4, - Options: options, - }, - } - - return mounts -} - -// withTransaction wraps fn callback with containerd's meta store transaction. -// If callback returns an error or transaction is not writable, database transaction will be discarded. -func (s *Snapshotter) withTransaction(ctx context.Context, writable bool, fn func(ctx context.Context) error) error { - ctx, trans, err := s.store.TransactionContext(ctx, writable) - if err != nil { - return err - } - - var result *multierror.Error - - err = fn(ctx) - if err != nil { - result = multierror.Append(result, err) - } - - // Always rollback if transaction is not writable - if err != nil || !writable { - if terr := trans.Rollback(); terr != nil { - log.G(ctx).WithError(terr).Error("failed to rollback transaction") - result = multierror.Append(result, errors.Wrap(terr, "rollback failed")) - } - } else { - if terr := trans.Commit(); terr != nil { - log.G(ctx).WithError(terr).Error("failed to commit transaction") - result = multierror.Append(result, errors.Wrap(terr, "commit failed")) - } - } - - if err := result.ErrorOrNil(); err != nil { - log.G(ctx).WithError(err).Debug("snapshotter error") - - // Unwrap if just one error - if result.Len() == 1 { - return result.Errors[0] - } - - return err - } - - return nil -} diff --git a/snapshotter/devmapper/snapshotter_test.go b/snapshotter/devmapper/snapshotter_test.go deleted file mode 100644 index 22848327b..000000000 --- a/snapshotter/devmapper/snapshotter_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 devmapper - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "testing" - "time" - - "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/snapshots" - "github.com/containerd/containerd/snapshots/testsuite" - "github.com/containerd/continuity/fs/fstest" - "github.com/hashicorp/go-multierror" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/firecracker-microvm/firecracker-containerd/internal" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/dmsetup" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/losetup" -) - -func TestSnapshotterSuite(t *testing.T) { - internal.RequiresRoot(t) - logrus.SetLevel(logrus.DebugLevel) - - snapshotterFn := func(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) { - // Create loopback devices for each test case - _, loopDataDevice := createLoopbackDevice(t, root) - _, loopMetaDevice := createLoopbackDevice(t, root) - - poolName := fmt.Sprintf("containerd-snapshotter-suite-pool-%d", time.Now().Nanosecond()) - err := dmsetup.CreatePool(poolName, loopDataDevice, loopMetaDevice, 64*1024/dmsetup.SectorSize) - require.NoErrorf(t, err, "failed to create pool %q", poolName) - - config := &Config{ - RootPath: root, - PoolName: poolName, - BaseImageSize: "16Mb", - } - - snap, err := NewSnapshotter(context.Background(), config) - if err != nil { - return nil, nil, err - } - - // Remove device mapper pool and detach loop devices after test completes - removePool := func() error { - result := multierror.Append( - snap.pool.RemovePool(ctx), - losetup.DetachLoopDevice(loopDataDevice, loopMetaDevice)) - - return result.ErrorOrNil() - } - - // Pool cleanup should be called before closing metadata store (as we need to retrieve device names) - snap.cleanupFn = append([]closeFunc{removePool}, snap.cleanupFn...) - - return snap, snap.Close, nil - } - - testsuite.SnapshotterSuite(t, "devmapper", snapshotterFn) - - ctx := context.Background() - ctx = namespaces.WithNamespace(ctx, "testsuite") - - t.Run("DevMapperUsage", func(t *testing.T) { - tempDir, err := ioutil.TempDir("", "snapshot-suite-usage") - require.NoError(t, err) - defer os.RemoveAll(tempDir) - - snapshotter, closer, err := snapshotterFn(ctx, tempDir) - require.NoError(t, err) - defer closer() - - testUsage(t, snapshotter) - }) -} - -// testUsage tests devmapper's Usage implementation. This is an approximate test as it's hard to -// predict how many blocks will be consumed under different conditions and parameters. -func testUsage(t *testing.T, snapshotter snapshots.Snapshotter) { - ctx := context.Background() - - // Create empty base layer - _, err := snapshotter.Prepare(ctx, "prepare-1", "") - require.NoError(t, err) - - emptyLayerUsage, err := snapshotter.Usage(ctx, "prepare-1") - assert.NoError(t, err) - - // Should be > 0 as just written file system also consumes blocks - assert.NotZero(t, emptyLayerUsage.Size) - - err = snapshotter.Commit(ctx, "layer-1", "prepare-1") - require.NoError(t, err) - - // Create child layer with 1MB file - - var ( - sizeBytes int64 = 1048576 // 1MB - baseApplier = fstest.Apply(fstest.CreateRandomFile("/a", 12345679, sizeBytes, 0777)) - ) - - mounts, err := snapshotter.Prepare(ctx, "prepare-2", "layer-1") - require.NoError(t, err) - - err = mount.WithTempMount(ctx, mounts, baseApplier.Apply) - require.NoError(t, err) - - err = snapshotter.Commit(ctx, "layer-2", "prepare-2") - require.NoError(t, err) - - layer2Usage, err := snapshotter.Usage(ctx, "layer-2") - require.NoError(t, err) - - // Should be at least 1 MB + fs metadata - assert.InDelta(t, sizeBytes, layer2Usage.Size, 256*dmsetup.SectorSize) -} diff --git a/snapshotter/naive/naive.go b/snapshotter/naive/naive.go deleted file mode 100644 index ba859f48f..000000000 --- a/snapshotter/naive/naive.go +++ /dev/null @@ -1,391 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 naive - -import ( - "context" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/containerd/containerd/log" - "github.com/containerd/containerd/mount" - "github.com/containerd/containerd/snapshots" - "github.com/containerd/containerd/snapshots/storage" - "github.com/containerd/continuity/fs" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/losetup" -) - -const ( - metadataFileName = "metadata.db" - imageDirName = "images" - imageFSType = "ext4" - sparseImageSizeMB = 1024 - mib = 1048576 -) - -// Snapshotter implements naive snapshotter for containerd -type Snapshotter struct { - root string - store *storage.MetaStore -} - -// NewSnapshotter creates naive snapshotter for Firecracker. -// Each layer is represented by separate Linux image and corresponding containerd's snapshot ID. -// Snapshotter has the following file structure: -// {root}/images/{ID} - keeps filesystem images -// {root}/metadata.db - keeps metadata (info and relationships between layers) -func NewSnapshotter(ctx context.Context, root string) (snapshots.Snapshotter, error) { - log.G(ctx).WithField("root", root).Info("creating naive snapshotter") - - root, err := filepath.Abs(root) - if err != nil { - log.G(ctx).WithError(err).Error("failed to get absoluate path") - return nil, err - } - - for _, path := range []string{ - root, - filepath.Join(root, imageDirName), - } { - if err := os.Mkdir(path, 0750); err != nil && !os.IsExist(err) { - log.G(ctx).WithError(err).Errorf("mkdir failed for '%s'", path) - return nil, err - } - } - - ms, err := storage.NewMetaStore(filepath.Join(root, metadataFileName)) - if err != nil { - return nil, err - } - - return &Snapshotter{ - root: root, - store: ms, - }, nil -} - -// Stat returns the info for an active or committed snapshot by name or key. -func (s *Snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { - log.G(ctx).WithField("key", key).Debug("stat") - - ctx, trans, err := s.store.TransactionContext(ctx, false) - if err != nil { - return snapshots.Info{}, err - } - - defer trans.Rollback() - - _, info, _, err := storage.GetInfo(ctx, key) - if err != nil { - return snapshots.Info{}, err - } - - return info, nil -} - -// Update updates the info for a snapshot. -func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { - log.G(ctx).Debugf("update: %s", strings.Join(fieldpaths, ", ")) - - ctx, trans, err := s.store.TransactionContext(ctx, true) - if err != nil { - return snapshots.Info{}, err - } - - info, err = storage.UpdateInfo(ctx, info, fieldpaths...) - if err != nil { - return snapshots.Info{}, complete(ctx, trans, err) - } - - return info, complete(ctx, trans, nil) -} - -// Usage not yet implemented -func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { - log.G(ctx).WithField("key", key).Debug("usage") - - return snapshots.Usage{}, errors.New("not implemented") -} - -// Mounts returns loop device mount by snapshot key -func (s *Snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { - log.G(ctx).WithField("key", key).Debug("mounts") - - ctx, trans, err := s.store.TransactionContext(ctx, false) - if err != nil { - return nil, err - } - - defer trans.Rollback() - - snap, err := storage.GetSnapshot(ctx, key) - if err != nil { - return nil, err - } - - return s.buildMounts(snap) -} - -// Prepare creates block device (sparse image with attached loop device) for an active snapshot identified by key -func (s *Snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare") - return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts...) -} - -// View creates readonly block device for the given snapshot -func (s *Snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("view") - return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts...) -} - -// Commit detaches loop devices from sparse image if any and updates meta store info. -// Snapshot changes are captured when unmounting block device with 'sync' flag. -func (s *Snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { - log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit") - - ctx, trans, err := s.store.TransactionContext(ctx, true) - if err != nil { - return err - } - - snapID, _, _, err := storage.GetInfo(ctx, key) - if err != nil { - return complete(ctx, trans, err) - } - - imagePath := s.getImagePath(snapID) - if err := losetup.RemoveLoopDevicesAssociatedWithImage(imagePath); err != nil { - return complete(ctx, trans, err) - } - - if _, err := storage.CommitActive(ctx, key, name, snapshots.Usage{}, opts...); err != nil { - return complete(ctx, trans, err) - } - - return complete(ctx, trans, nil) -} - -// Remove unmounts an image and deletes it from images directory -func (s *Snapshotter) Remove(ctx context.Context, key string) error { - log.G(ctx).WithField("key", key).Debug("remove") - - ctx, trans, err := s.store.TransactionContext(ctx, true) - if err != nil { - return err - } - - id, _, err := storage.Remove(ctx, key) - if err != nil { - return complete(ctx, trans, err) - } - - imagePath := s.getImagePath(id) - - if err := losetup.RemoveLoopDevicesAssociatedWithImage(imagePath); err != nil { - log.G(ctx).WithError(err).Errorf("failed to detach loop devices from '%s'", imagePath) - return complete(ctx, trans, err) - } - - if err := os.Remove(imagePath); err != nil { - log.G(ctx).WithError(err).Errorf("failed to delete image '%s'", imagePath) - return complete(ctx, trans, err) - } - - return complete(ctx, trans, nil) -} - -// Walk walks all snapshots in the snapshotter. For each snapshot in the snapshotter, the function will be called. -func (s *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { - log.G(ctx).Debug("walk") - - ctx, trans, err := s.store.TransactionContext(ctx, false) - if err != nil { - return err - } - - defer trans.Rollback() - return storage.WalkInfo(ctx, fn) -} - -// Close releases snapshotter resources and detaches loop devices from all images -func (s *Snapshotter) Close() error { - log.L.Debug("close") - - if err := s.store.Close(); err != nil { - return err - } - - // Find all images and detach loop devices if any - imageDir := filepath.Join(s.root, imageDirName) - err := filepath.Walk(imageDir, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - - fullImagePath := filepath.Join(imageDir, info.Name()) - return losetup.RemoveLoopDevicesAssociatedWithImage(fullImagePath) - }) - - return err -} - -// createSnapshot creates an active snapshot for containerd and returns mount path. -// Behind the scene it creates an image, builds ext4 file system, and takes care of parent snapshot if needed. -// Command line for this will look like: -// dd if=/dev/zero of=drive-2.img bs=1k count=102400 -// mkfs -t ext4 image.img -// If snapshot has a parent, all files from parent will be copied to this snapshot first. -func (s *Snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - ctx, trans, err := s.store.TransactionContext(ctx, true) - if err != nil { - return nil, err - } - - snap, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...) - if err != nil { - return nil, complete(ctx, trans, err) - } - - hasParent := len(snap.ParentIDs) > 0 - if !hasParent { - // TODO: figure out what to do with file size - imagePath := s.getImagePath(snap.ID) - if err := s.createImage(ctx, imagePath, sparseImageSizeMB); err != nil { - return nil, complete(ctx, trans, err) - } - } else { - parentID := snap.ParentIDs[0] - log.G(ctx).Infof("copying data from parent snapshot %s", parentID) - - if err := fs.CopyFile(s.getImagePath(snap.ID), s.getImagePath(parentID)); err != nil { - log.G(ctx).WithError(err).Errorf("failed copy parent layer") - return nil, complete(ctx, trans, err) - } - } - - mounts, err := s.buildMounts(snap) - if err != nil { - return nil, complete(ctx, trans, err) - } - - if !hasParent { - // lost+found breaks diff comparisons of containerd snapshotter test suite - // Not needed for containerd snapshots - _ = mount.WithTempMount(ctx, mounts, func(root string) error { - return os.Remove(filepath.Join(root, "lost+found")) - }) - } - - return mounts, complete(ctx, trans, nil) -} - -func (s *Snapshotter) createImage(ctx context.Context, imagePath string, fileSizeMB int) error { - // Create a new empty file and resize - log.G(ctx).WithField("image", imagePath).Infof("creating new image of size %d MB", fileSizeMB) - file, err := os.Create(imagePath) - if err != nil { - return err - } - - if err := file.Truncate(int64(fileSizeMB) * mib); err != nil { - return err - } - - if err := file.Close(); err != nil { - return err - } - - // Build a Linux filesystem - log.G(ctx).WithField("image", file.Name()).Info("building file system") - if err := run("mkfs", "-t", imageFSType, "-F", file.Name()); err != nil { - return err - } - - return nil -} - -func (s *Snapshotter) getImagePath(id string) string { - return filepath.Join(s.root, imageDirName, id) -} - -// buildMounts returns a mount for the given snapshot. -// Block device represented as an attached loop device to a sparse image. -// buildMounts attaches new loop device to an image unless there is an existing association. -func (s *Snapshotter) buildMounts(snap storage.Snapshot) ([]mount.Mount, error) { - // Snapshot changes need to be flushed to disk immediately when unmounting. - options := []string{"sync", "dirsync"} - - if snap.Kind != snapshots.KindActive { - options = append(options, "ro") - } - - var ( - imagePath = s.getImagePath(snap.ID) - loopDevice string - ) - - // Try find existing loop device attached to the given image - loopDeviceList, err := losetup.FindAssociatedLoopDevices(imagePath) - if err != nil { - return nil, err - } - - if len(loopDeviceList) > 0 { - loopDevice = loopDeviceList[0] - } else { - // Find first unused loop device and attach to image - loopDevice, err = losetup.AttachLoopDevice(imagePath) - if err != nil { - return nil, err - } - } - - mounts := []mount.Mount{ - { - Source: loopDevice, - Type: imageFSType, - Options: options, - }, - } - - return mounts, nil -} - -func run(cmd string, args ...string) error { - command := exec.Command(cmd, args...) - if err := command.Run(); err != nil { - return errors.Wrapf(err, "exec failed: %s %s", cmd, strings.Join(args, " ")) - } - - return nil -} - -func complete(ctx context.Context, trans storage.Transactor, err error) error { - if err != nil { - if terr := trans.Rollback(); terr != nil { - log.G(ctx).WithError(terr).Error("failed to rollback transaction") - } - } else { - if terr := trans.Commit(); terr != nil { - log.G(ctx).WithError(terr).Error("failed to commit transaction") - } - } - - return err -} diff --git a/snapshotter/naive/naive_test.go b/snapshotter/naive/naive_test.go deleted file mode 100644 index 481d4af36..000000000 --- a/snapshotter/naive/naive_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 naive - -import ( - "context" - _ "crypto/sha256" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/containerd/containerd/snapshots" - "github.com/containerd/containerd/snapshots/testsuite" - - "github.com/firecracker-microvm/firecracker-containerd/internal" -) - -func TestCreateImage(t *testing.T) { - internal.RequiresRoot(t) - snap := Snapshotter{} - - tempDir, err := ioutil.TempDir("", "fc-snapshotter") - if err != nil { - t.Fatal(err) - } - - defer os.RemoveAll(tempDir) - - imgPath := filepath.Join(tempDir, "x.img") - - const ( - sizeMiB = 100 - sizeBytes = sizeMiB * mib - ) - - err = snap.createImage(context.Background(), imgPath, sizeMiB) - if err != nil { - t.Fatal(err) - } - - if stat, err := os.Stat(imgPath); os.IsNotExist(err) { - t.Fatal("error creating image file") - } else if stat.Size() != sizeBytes { - t.Errorf("wrong image size %d != %d", stat.Size(), sizeBytes) - } -} - -func createSnapshotter(ctx context.Context, root string) (snapshots.Snapshotter, func() error, error) { - snap, err := NewSnapshotter(ctx, root) - if err != nil { - return nil, nil, err - } - - return snap, snap.Close, nil -} - -func TestSnapshotterSuite(t *testing.T) { - internal.RequiresRoot(t) - testsuite.SnapshotterSuite(t, "Snapshotter", createSnapshotter) -} diff --git a/snapshotter/pkg/dmsetup/dmsetup.go b/snapshotter/pkg/dmsetup/dmsetup.go deleted file mode 100644 index b707a21e4..000000000 --- a/snapshotter/pkg/dmsetup/dmsetup.go +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 dmsetup - -import ( - "fmt" - "os/exec" - "strconv" - "strings" - - "github.com/pkg/errors" - "golang.org/x/sys/unix" -) - -const ( - // DevMapperDir represents devmapper devices location - DevMapperDir = "/dev/mapper/" - // SectorSize represents the number of bytes in one sector on devmapper devices - SectorSize = 512 -) - -// DeviceInfo represents device info returned by "dmsetup info". -// dmsetup(8) provides more information on each of these fields. -type DeviceInfo struct { - Name string - BlockDeviceName string - TableLive bool - TableInactive bool - Suspended bool - ReadOnly bool - Major uint32 - Minor uint32 - OpenCount uint32 // Open reference count - TargetCount uint32 // Number of targets in the live table - EventNumber uint32 // Last event sequence number (used by wait) -} - -var errTable map[string]unix.Errno - -func init() { - // Precompute map of = for optimal lookup - errTable = make(map[string]unix.Errno) - for errno := unix.EPERM; errno <= unix.EHWPOISON; errno++ { - errTable[errno.Error()] = errno - } -} - -// CreatePool creates a device with the given name, data and metadata file and block size (see "dmsetup create") -func CreatePool(poolName, dataFile, metaFile string, blockSizeSectors uint32) error { - thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors) - if err != nil { - return err - } - - _, err = dmsetup("create", poolName, "--table", thinPool) - return err -} - -// ReloadPool reloads existing thin-pool (see "dmsetup reload") -func ReloadPool(deviceName, dataFile, metaFile string, blockSizeSectors uint32) error { - thinPool, err := makeThinPoolMapping(dataFile, metaFile, blockSizeSectors) - if err != nil { - return err - } - - _, err = dmsetup("reload", deviceName, "--table", thinPool) - return err -} - -const ( - lowWaterMark = 32768 // Picked arbitrary, might need tuning - skipZeroing = "skip_block_zeroing" // Skipping zeroing to reduce latency for device creation -) - -// makeThinPoolMapping makes thin-pool table entry -func makeThinPoolMapping(dataFile, metaFile string, blockSizeSectors uint32) (string, error) { - dataDeviceSizeBytes, err := BlockDeviceSize(dataFile) - if err != nil { - return "", errors.Wrapf(err, "failed to get block device size: %s", dataFile) - } - - // Thin-pool mapping target has the following format: - // start - starting block in virtual device - // length - length of this segment - // metadata_dev - the metadata device - // data_dev - the data device - // data_block_size - the data block size in sectors - // low_water_mark - the low water mark, expressed in blocks of size data_block_size - // feature_args - the number of feature arguments - // args - lengthSectors := dataDeviceSizeBytes / SectorSize - target := fmt.Sprintf("0 %d thin-pool %s %s %d %d 1 %s", - lengthSectors, - metaFile, - dataFile, - blockSizeSectors, - lowWaterMark, - skipZeroing) - - return target, nil -} - -// CreateDevice sends "create_thin " message to the given thin-pool -func CreateDevice(poolName string, deviceID uint32) error { - _, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_thin %d", deviceID)) - return err -} - -// ActivateDevice activates the given thin-device using the 'thin' target -func ActivateDevice(poolName string, deviceName string, deviceID uint32, size uint64, external string) error { - mapping := makeThinMapping(poolName, deviceID, size, external) - _, err := dmsetup("create", deviceName, "--table", mapping) - return err -} - -// makeThinMapping makes thin target table entry -func makeThinMapping(poolName string, deviceID uint32, sizeBytes uint64, externalOriginDevice string) string { - lengthSectors := sizeBytes / SectorSize - - // Thin target has the following format: - // start - starting block in virtual device - // length - length of this segment - // pool_dev - the thin-pool device, can be /dev/mapper/pool_name or 253:0 - // dev_id - the internal device id of the device to be activated - // external_origin_dev - an optional block device outside the pool to be treated as a read-only snapshot origin. - target := fmt.Sprintf("0 %d thin %s %d %s", lengthSectors, GetFullDevicePath(poolName), deviceID, externalOriginDevice) - return strings.TrimSpace(target) -} - -// SuspendDevice suspends the given device (see "dmsetup suspend") -func SuspendDevice(deviceName string) error { - _, err := dmsetup("suspend", deviceName) - return err -} - -// ResumeDevice resumes the given device (see "dmsetup resume") -func ResumeDevice(deviceName string) error { - _, err := dmsetup("resume", deviceName) - return err -} - -// Table returns the current table for the device -func Table(deviceName string) (string, error) { - return dmsetup("table", deviceName) -} - -// CreateSnapshot sends "create_snap" message to the given thin-pool. -// Caller needs to suspend and resume device if it is active. -func CreateSnapshot(poolName string, deviceID uint32, baseDeviceID uint32) error { - _, err := dmsetup("message", poolName, "0", fmt.Sprintf("create_snap %d %d", deviceID, baseDeviceID)) - return err -} - -// DeleteDevice sends "delete " message to the given thin-pool -func DeleteDevice(poolName string, deviceID uint32) error { - _, err := dmsetup("message", poolName, "0", fmt.Sprintf("delete %d", deviceID)) - return err -} - -// RemoveDeviceOpt represents command line arguments for "dmsetup remove" command -type RemoveDeviceOpt string - -const ( - // RemoveWithForce flag replaces the table with one that fails all I/O if - // open device can't be removed - RemoveWithForce RemoveDeviceOpt = "--force" - // RemoveWithRetries option will cause the operation to be retried - // for a few seconds before failing - RemoveWithRetries RemoveDeviceOpt = "--retry" - // RemoveDeferred flag will enable deferred removal of open devices, - // the device will be removed when the last user closes it - RemoveDeferred RemoveDeviceOpt = "--deferred" -) - -// RemoveDevice removes a device (see "dmsetup remove") -func RemoveDevice(deviceName string, opts ...RemoveDeviceOpt) error { - args := []string{ - "remove", - } - - for _, opt := range opts { - args = append(args, string(opt)) - } - - args = append(args, GetFullDevicePath(deviceName)) - - _, err := dmsetup(args...) - return err -} - -// Info outputs device information (see "dmsetup info"). -// If device name is empty, all device infos will be returned. -func Info(deviceName string) ([]*DeviceInfo, error) { - output, err := dmsetup( - "info", - "--columns", - "--noheadings", - "-o", - "name,blkdevname,attr,major,minor,open,segments,events", - "--separator", - " ", - deviceName) - - if err != nil { - return nil, err - } - - var ( - lines = strings.Split(output, "\n") - devices = make([]*DeviceInfo, len(lines)) - ) - - for i, line := range lines { - var ( - attr = "" - info = &DeviceInfo{} - ) - - _, err := fmt.Sscan(line, - &info.Name, - &info.BlockDeviceName, - &attr, - &info.Major, - &info.Minor, - &info.OpenCount, - &info.TargetCount, - &info.EventNumber) - - if err != nil { - return nil, errors.Wrapf(err, "failed to parse line %q", line) - } - - // Parse attributes (see "man 8 dmsetup" for details) - info.Suspended = strings.Contains(attr, "s") - info.ReadOnly = strings.Contains(attr, "r") - info.TableLive = strings.Contains(attr, "L") - info.TableInactive = strings.Contains(attr, "I") - - devices[i] = info - } - - return devices, nil -} - -// Version returns "dmsetup version" output -func Version() (string, error) { - return dmsetup("version") -} - -// DeviceStatus represents devmapper device status information -type DeviceStatus struct { - Offset int64 - Length int64 - Target string - Params []string -} - -// Status provides status information for devmapper device -func Status(deviceName string) (*DeviceStatus, error) { - var ( - err error - status DeviceStatus - ) - - output, err := dmsetup("status", deviceName) - if err != nil { - return nil, err - } - - // Status output format: - // Offset (int64) - // Length (int64) - // Target type (string) - // Params (Array of strings) - const MinParseCount = 4 - parts := strings.Split(output, " ") - if len(parts) < MinParseCount { - return nil, errors.Errorf("failed to parse output: %q", output) - } - - status.Offset, err = strconv.ParseInt(parts[0], 10, 64) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse offset: %q", parts[0]) - } - - status.Length, err = strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse length: %q", parts[1]) - } - - status.Target = parts[2] - status.Params = parts[3:] - - return &status, nil -} - -// GetFullDevicePath returns full path for the given device name (like "/dev/mapper/name") -func GetFullDevicePath(deviceName string) string { - if strings.HasPrefix(deviceName, DevMapperDir) { - return deviceName - } - - return DevMapperDir + deviceName -} - -// BlockDeviceSize returns size of block device in bytes -func BlockDeviceSize(devicePath string) (uint64, error) { - data, err := exec.Command("blockdev", "--getsize64", "-q", devicePath).CombinedOutput() - output := string(data) - if err != nil { - return 0, errors.Wrapf(err, output) - } - - output = strings.TrimSuffix(output, "\n") - return strconv.ParseUint(output, 10, 64) -} - -func dmsetup(args ...string) (string, error) { - data, err := exec.Command("dmsetup", args...).CombinedOutput() - output := string(data) - if err != nil { - // Try find Linux error code otherwise return generic error with dmsetup output - if errno, ok := tryGetUnixError(output); ok { - return "", errno - } - - return "", errors.Wrapf(err, "dmsetup %s\nerror: %s\n", strings.Join(args, " "), output) - } - - output = strings.Trim(output, "\n") - output = strings.TrimSpace(output) - - return output, nil -} - -// tryGetUnixError tries to find Linux error code from dmsetup output -func tryGetUnixError(output string) (unix.Errno, bool) { - // It's useful to have Linux error codes like EBUSY, EPERM, ..., instead of just text. - // Unfortunately there is no better way than extracting/comparing error text. - text := parseDmsetupError(output) - if text == "" { - return 0, false - } - - err, ok := errTable[text] - return err, ok -} - -// dmsetup returns error messages in format: -// device-mapper: message ioctl on failed: File exists\n -// Command failed\n -// parseDmsetupError extracts text between "failed: " and "\n" -func parseDmsetupError(output string) string { - lines := strings.SplitN(output, "\n", 2) - if len(lines) < 2 { - return "" - } - - const failedSubstr = "failed: " - - line := lines[0] - idx := strings.LastIndex(line, failedSubstr) - if idx == -1 { - return "" - } - - str := line[idx:] - - // Strip "failed: " prefix - str = strings.TrimPrefix(str, failedSubstr) - - str = strings.ToLower(str) - return str -} diff --git a/snapshotter/pkg/dmsetup/dmsetup_test.go b/snapshotter/pkg/dmsetup/dmsetup_test.go deleted file mode 100644 index ec4d4d0dc..000000000 --- a/snapshotter/pkg/dmsetup/dmsetup_test.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 dmsetup - -import ( - "io/ioutil" - "os" - "strings" - "testing" - - "github.com/docker/go-units" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/sys/unix" - - "github.com/firecracker-microvm/firecracker-containerd/internal" - "github.com/firecracker-microvm/firecracker-containerd/snapshotter/pkg/losetup" -) - -const ( - testPoolName = "test-pool" - testDeviceName = "test-device" - deviceID = 1 - snapshotID = 2 -) - -func TestDMSetup(t *testing.T) { - internal.RequiresRoot(t) - tempDir, err := ioutil.TempDir("", "dmsetup-tests-") - require.NoErrorf(t, err, "failed to make temp dir for tests") - - defer func() { - err := os.RemoveAll(tempDir) - assert.NoError(t, err) - }() - - dataImage, loopDataDevice := createLoopbackDevice(t, tempDir) - metaImage, loopMetaDevice := createLoopbackDevice(t, tempDir) - - defer func() { - err = losetup.RemoveLoopDevicesAssociatedWithImage(dataImage) - assert.NoErrorf(t, err, "failed to detach loop devices for data image: %s", dataImage) - - err = losetup.RemoveLoopDevicesAssociatedWithImage(metaImage) - assert.NoErrorf(t, err, "failed to detach loop devices for meta image: %s", metaImage) - }() - - t.Run("CreatePool", func(t *testing.T) { - err := CreatePool(testPoolName, loopDataDevice, loopMetaDevice, 128) - require.NoErrorf(t, err, "failed to create thin-pool") - - table, err := Table(testPoolName) - t.Logf("table: %s", table) - assert.NoError(t, err) - assert.True(t, strings.HasPrefix(table, "0 32768 thin-pool")) - assert.True(t, strings.HasSuffix(table, "128 32768 1 skip_block_zeroing")) - }) - - t.Run("ReloadPool", func(t *testing.T) { - err := ReloadPool(testPoolName, loopDataDevice, loopMetaDevice, 256) - assert.NoErrorf(t, err, "failed to reload thin-pool") - }) - - t.Run("CreateDevice", testCreateDevice) - - t.Run("CreateSnapshot", testCreateSnapshot) - t.Run("DeleteSnapshot", testDeleteSnapshot) - - t.Run("ActivateDevice", testActivateDevice) - t.Run("DeviceStatus", testDeviceStatus) - t.Run("SuspendResumeDevice", testSuspendResumeDevice) - t.Run("RemoveDevice", testRemoveDevice) - - t.Run("RemovePool", func(t *testing.T) { - err = RemoveDevice(testPoolName, RemoveWithForce, RemoveWithRetries) - require.NoErrorf(t, err, "failed to remove thin-pool") - }) - - t.Run("Version", testVersion) -} - -func testCreateDevice(t *testing.T) { - err := CreateDevice(testPoolName, deviceID) - require.NoError(t, err, "failed to create test device") - - err = CreateDevice(testPoolName, deviceID) - assert.EqualValues(t, unix.EEXIST, err) - - infos, err := Info(testPoolName) - require.NoError(t, err) - require.Lenf(t, infos, 1, "got unexpected number of device infos") -} - -func testCreateSnapshot(t *testing.T) { - err := CreateSnapshot(testPoolName, snapshotID, deviceID) - require.NoError(t, err) -} - -func testDeleteSnapshot(t *testing.T) { - err := DeleteDevice(testPoolName, snapshotID) - require.NoErrorf(t, err, "failed to send delete message") - - err = DeleteDevice(testPoolName, snapshotID) - assert.EqualValues(t, unix.ENODATA, err) -} - -func testActivateDevice(t *testing.T) { - err := ActivateDevice(testPoolName, testDeviceName, 1, 1024, "") - require.NoErrorf(t, err, "failed to activate device") - - err = ActivateDevice(testPoolName, testDeviceName, 1, 1024, "") - assert.Equal(t, err, unix.EBUSY) - - if _, err := os.Stat("/dev/mapper/" + testDeviceName); err != nil && !os.IsExist(err) { - assert.Errorf(t, err, "failed to stat device") - } - - list, err := Info(testPoolName) - assert.NoError(t, err) - require.Len(t, list, 1) - - info := list[0] - assert.Equal(t, testPoolName, info.Name) - assert.True(t, info.TableLive) -} - -func testDeviceStatus(t *testing.T) { - status, err := Status(testDeviceName) - require.NoError(t, err) - - assert.EqualValues(t, 0, status.Offset) - assert.EqualValues(t, 2, status.Length) - assert.Equal(t, "thin", status.Target) - assert.EqualValues(t, status.Params, []string{"0", "-"}) -} - -func testSuspendResumeDevice(t *testing.T) { - err := SuspendDevice(testDeviceName) - assert.NoError(t, err) - - err = SuspendDevice(testDeviceName) - assert.NoError(t, err) - - list, err := Info(testDeviceName) - assert.NoError(t, err) - require.Len(t, list, 1) - - info := list[0] - assert.True(t, info.Suspended) - - err = ResumeDevice(testDeviceName) - assert.NoError(t, err) - - err = ResumeDevice(testDeviceName) - assert.NoError(t, err) -} - -func testRemoveDevice(t *testing.T) { - err := RemoveDevice(testPoolName) - assert.EqualValues(t, unix.EBUSY, err, "removing thin-pool with dependencies shouldn't be allowed") - - err = RemoveDevice(testDeviceName, RemoveWithRetries) - assert.NoErrorf(t, err, "failed to remove thin-device") -} - -func testVersion(t *testing.T) { - version, err := Version() - assert.NoError(t, err) - assert.NotEmpty(t, version) -} - -func createLoopbackDevice(t *testing.T, dir string) (string, string) { - file, err := ioutil.TempFile(dir, "dmsetup-tests-") - require.NoError(t, err) - - size, err := units.RAMInBytes("16Mb") - require.NoError(t, err) - - err = file.Truncate(size) - require.NoError(t, err) - - err = file.Close() - require.NoError(t, err) - - imagePath := file.Name() - - loopDevice, err := losetup.AttachLoopDevice(imagePath) - require.NoError(t, err) - - return imagePath, loopDevice -} diff --git a/snapshotter/pkg/losetup/losetup.go b/snapshotter/pkg/losetup/losetup.go deleted file mode 100644 index e77ac159e..000000000 --- a/snapshotter/pkg/losetup/losetup.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 losetup - -import ( - "os/exec" - "strings" - - "github.com/pkg/errors" -) - -// FindAssociatedLoopDevices returns a list of loop devices attached to a given image -func FindAssociatedLoopDevices(imagePath string) ([]string, error) { - output, err := losetup("--list", "--output", "NAME", "--associated", imagePath) - if err != nil { - return nil, errors.Wrapf(err, "failed to get loop devices: '%s'", output) - } - - if output == "" { - return []string{}, nil - } - - items := strings.Split(output, "\n") - if len(items) <= 1 { - return []string{}, nil - } - - // Skip header with column names - return items[1:], nil -} - -// AttachLoopDevice finds first available loop device and associates it with an image. -func AttachLoopDevice(imagePath string) (string, error) { - return losetup("--find", "--show", imagePath) -} - -// DetachLoopDevice detaches loop devices -func DetachLoopDevice(loopDevice ...string) error { - args := append([]string{"--detach"}, loopDevice...) - _, err := losetup(args...) - return err -} - -// RemoveLoopDevicesAssociatedWithImage detaches all loop devices attached to a given sparse image -func RemoveLoopDevicesAssociatedWithImage(imagePath string) error { - loopDevices, err := FindAssociatedLoopDevices(imagePath) - if err != nil { - return err - } - - for _, loopDevice := range loopDevices { - if err = DetachLoopDevice(loopDevice); err != nil { - return err - } - } - - return nil -} - -// losetup is a wrapper around losetup command line tool -func losetup(args ...string) (string, error) { - data, err := exec.Command("losetup", args...).CombinedOutput() - output := string(data) - if err != nil { - return "", errors.Wrapf(err, "losetup %s\nerror: %s\n", strings.Join(args, " "), output) - } - - return strings.TrimSuffix(output, "\n"), err -} diff --git a/snapshotter/pkg/losetup/losetup_test.go b/snapshotter/pkg/losetup/losetup_test.go deleted file mode 100644 index 1f31ff8a4..000000000 --- a/snapshotter/pkg/losetup/losetup_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 losetup - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/docker/go-units" - "github.com/firecracker-microvm/firecracker-containerd/internal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLosetup(t *testing.T) { - internal.RequiresRoot(t) - var ( - imagePath = createSparseImage(t) - loopDevice1 string - loopDevice2 string - ) - - defer func() { - err := os.Remove(imagePath) - assert.NoError(t, err) - }() - - t.Run("AttachLoopDevice", func(t *testing.T) { - dev1, err := AttachLoopDevice(imagePath) - require.NoError(t, err) - require.NotEmpty(t, dev1) - - dev2, err := AttachLoopDevice(imagePath) - assert.NoError(t, err) - assert.NotEqualf(t, dev2, dev1, "should attach different loop device") - - loopDevice1 = dev1 - loopDevice2 = dev2 - }) - - t.Run("AttachEmptyLoopDevice", func(t *testing.T) { - _, err := AttachLoopDevice("") - assert.Error(t, err, "shouldn't attach empty path") - }) - - t.Run("FindAssociatedLoopDevices", func(t *testing.T) { - devices, err := FindAssociatedLoopDevices(imagePath) - assert.NoError(t, err) - assert.Lenf(t, devices, 2, "unexpected number of attached devices") - assert.ElementsMatch(t, devices, []string{loopDevice1, loopDevice2}) - }) - - t.Run("FindAssociatedLoopDevicesForInvalidImage", func(t *testing.T) { - devices, err := FindAssociatedLoopDevices("") - assert.NoError(t, err) - assert.Empty(t, devices) - }) - - t.Run("DetachLoopDevice", func(t *testing.T) { - err := DetachLoopDevice(loopDevice2) - require.NoErrorf(t, err, "failed to detach %q", loopDevice2) - }) - - t.Run("DetachEmptyDevice", func(t *testing.T) { - err := DetachLoopDevice("") - assert.Error(t, err, "shouldn't detach empty path") - }) - - t.Run("RemoveLoopDevicesAssociatedWithImage", func(t *testing.T) { - err := RemoveLoopDevicesAssociatedWithImage(imagePath) - assert.NoError(t, err) - - devices, err := FindAssociatedLoopDevices(imagePath) - assert.NoError(t, err) - assert.Empty(t, devices) - }) - - t.Run("RemoveLoopDevicesAssociatedWithInvalidImage", func(t *testing.T) { - err := RemoveLoopDevicesAssociatedWithImage("") - assert.NoError(t, err) - }) -} - -func createSparseImage(t *testing.T) string { - file, err := ioutil.TempFile("", "losetup-tests-") - require.NoError(t, err) - - size, err := units.RAMInBytes("16Mb") - require.NoError(t, err) - - err = file.Truncate(size) - require.NoError(t, err) - - err = file.Close() - require.NoError(t, err) - - return file.Name() -} diff --git a/snapshotter/run.go b/snapshotter/run.go deleted file mode 100644 index 80bddf414..000000000 --- a/snapshotter/run.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 snapshotter - -import ( - "context" - "flag" - "net" - "os" - "os/signal" - "syscall" - - snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" - "github.com/containerd/containerd/contrib/snapshotservice" - "github.com/containerd/containerd/log" - "github.com/containerd/containerd/snapshots" - "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" -) - -var ( - unixAddr string - debug bool -) - -func init() { - flag.StringVar(&unixAddr, - "address", - "./firecracker-snapshotter.sock", - "RPC server unix address (default: ./firecracker-snapshotter.sock)") - - flag.BoolVar(&debug, - "debug", - false, - "Debug mode") -} - -// CreateFunc represents a callback to be used for creating concrete snapshotter implementation -type CreateFunc func(ctx context.Context) (snapshots.Snapshotter, error) - -// Run runs snapshotter ttrpc server for containerd (somewhat similar to shim.Run). -// snapInit should create concrete snapshotter implementation such as naive or devmapper. -// There are two command line parameters available out of the box: -// - address: specifies unix address to run ttrpc server on -// - debug: turns on debug logging -// Any extra flags might me specified if additional configuration needed, flags.Parse will -// be called prior snapshot create callback (see naive example). -func Run(snapInit CreateFunc) { - if !flag.Parsed() { - flag.Parse() - } - - if debug { - logrus.SetLevel(logrus.DebugLevel) - } - - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE, syscall.SIGHUP, syscall.SIGQUIT) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - group, ctx := errgroup.WithContext(ctx) - - rpc := grpc.NewServer() - - snap, err := snapInit(ctx) - if err != nil { - log.G(ctx).WithError(err).Fatal("failed to create snapshotter") - } - - // Convert the snapshotter interface to gRPC service and run server - log.G(ctx).WithField("unix_addr", unixAddr).Info("running gRPC server") - service := snapshotservice.FromSnapshotter(snap) - snapshotsapi.RegisterSnapshotsServer(rpc, service) - - listener, err := net.Listen("unix", unixAddr) - if err != nil { - log.G(ctx).WithError(err).Fatalf("failed to listen socket at %s", unixAddr) - } - - group.Go(func() error { - return rpc.Serve(listener) - }) - - group.Go(func() error { - defer func() { - log.G(ctx).Info("stopping server") - rpc.Stop() - - if err := snap.Close(); err != nil { - log.G(ctx).WithError(err).Error("failed to close snapshotter") - } - }() - - for { - select { - case <-stop: - cancel() - return nil - case <-ctx.Done(): - return ctx.Err() - } - } - }) - - if err := group.Wait(); err != nil { - log.G(ctx).WithError(err).Warn("snapshotter error") - } - - log.G(ctx).Info("done") -} diff --git a/tools/docker/entrypoint.sh b/tools/docker/entrypoint.sh index cb6409ec5..60b61184f 100755 --- a/tools/docker/entrypoint.sh +++ b/tools/docker/entrypoint.sh @@ -6,21 +6,6 @@ chmod a+rwx ${FICD_LOG_DIR} mkdir -p /etc/containerd/snapshotter case "$FICD_SNAPSHOTTER" in - naive) - cat > /etc/containerd/snapshotter/naive.toml <> ${FICD_SNAPSHOTTER_OUTFILE} & - ;; devmapper) cat > /etc/containerd/snapshotter/devmapper.toml <