Merge pull request #3022 from mxpv/devmapper
Integrate device mapper snapshotter
This commit is contained in:
commit
e7b6fea572
@ -104,6 +104,7 @@ make generate
|
|||||||
> * `no_btrfs`: A build tag disables building the btrfs snapshot driver.
|
> * `no_btrfs`: A build tag disables building the btrfs snapshot driver.
|
||||||
> * `no_cri`: A build tag disables building Kubernetes [CRI](http://blog.kubernetes.io/2016/12/container-runtime-interface-cri-in-kubernetes.html) support into containerd.
|
> * `no_cri`: A build tag disables building Kubernetes [CRI](http://blog.kubernetes.io/2016/12/container-runtime-interface-cri-in-kubernetes.html) support into containerd.
|
||||||
> See [here](https://github.com/containerd/cri-containerd#build-tags) for build tags of CRI plugin.
|
> See [here](https://github.com/containerd/cri-containerd#build-tags) for build tags of CRI plugin.
|
||||||
|
> * `no_devmapper`: A build tag disables building the device mapper snapshot driver.
|
||||||
>
|
>
|
||||||
> For example, adding `BUILDTAGS=no_btrfs` to your environment before calling the **binaries**
|
> For example, adding `BUILDTAGS=no_btrfs` to your environment before calling the **binaries**
|
||||||
> Makefile target will disable the btrfs driver within the containerd Go build.
|
> Makefile target will disable the btrfs driver within the containerd Go build.
|
||||||
|
21
cmd/containerd/builtins_devmapper_linux.go
Normal file
21
cmd/containerd/builtins_devmapper_linux.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// +build !no_devmapper
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import _ "github.com/containerd/containerd/snapshots/devmapper"
|
42
snapshots/devmapper/README.md
Normal file
42
snapshots/devmapper/README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## Devmapper snapshotter
|
||||||
|
|
||||||
|
Devmapper is a `containerd` snapshotter plugin that stores snapshots in ext4-formatted filesystem images
|
||||||
|
in a devicemapper thin pool.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
To make it work you need to prepare `thin-pool` in advance and update containerd's configuration file.
|
||||||
|
This file is typically located at `/etc/containerd/config.toml`.
|
||||||
|
|
||||||
|
Here's minimal sample entry that can be made in the configuration file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[plugins]
|
||||||
|
...
|
||||||
|
[plugins.devmapper]
|
||||||
|
pool_name = "containerd-pool"
|
||||||
|
base_image_size = "128MB"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The following configuration flags are supported:
|
||||||
|
* `root_path` - a directory where the metadata will be available (if empty
|
||||||
|
default location for `containerd` plugins will be used)
|
||||||
|
* `pool_name` - a name to use for the devicemapper thin pool. Pool name
|
||||||
|
should be the same as in `/dev/mapper/` directory
|
||||||
|
* `base_image_size` - defines how much space to allocate when creating the base device
|
||||||
|
|
||||||
|
Pool name and base image size are required snapshotter parameters.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
Give it a try with the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctr images pull --snapshotter devmapper docker.io/library/hello-world:latest
|
||||||
|
ctr run --snapshotter devmapper docker.io/library/hello-world:latest test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The devicemapper snapshotter requires `dmsetup` (>= 1.02.110) command line tool to be installed and
|
||||||
|
available on your computer. On Ubuntu, it can be installed with `apt-get install dmsetup` command.
|
98
snapshots/devmapper/config.go
Normal file
98
snapshots/devmapper/config.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"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 `toml:"root_path"`
|
||||||
|
|
||||||
|
// Name for 'thin-pool' device to be used by snapshotter (without /dev/mapper/ prefix)
|
||||||
|
PoolName string `toml:"pool_name"`
|
||||||
|
|
||||||
|
// Defines how much space to allocate when creating base image for container
|
||||||
|
BaseImageSize string `toml:"base_image_size"`
|
||||||
|
BaseImageSizeBytes uint64 `toml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig reads devmapper configuration file from disk in TOML format
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Config{}
|
||||||
|
if _, err := toml.DecodeFile(path, &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()
|
||||||
|
}
|
103
snapshots/devmapper/config_test.go
Normal file
103
snapshots/devmapper/config_test.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfig(t *testing.T) {
|
||||||
|
expected := Config{
|
||||||
|
RootPath: "/tmp",
|
||||||
|
PoolName: "test",
|
||||||
|
BaseImageSize: "128Mb",
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := ioutil.TempFile("", "devmapper-config-")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
encoder := toml.NewEncoder(file)
|
||||||
|
err = encoder.Encode(&expected)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := file.Close()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = os.Remove(file.Name())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
loaded, err := LoadConfig(file.Name())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, loaded.RootPath, expected.RootPath)
|
||||||
|
assert.Equal(t, loaded.PoolName, expected.PoolName)
|
||||||
|
assert.Equal(t, loaded.BaseImageSize, expected.BaseImageSize)
|
||||||
|
|
||||||
|
assert.Assert(t, loaded.BaseImageSizeBytes == 128*1024*1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigInvalidPath(t *testing.T) {
|
||||||
|
_, err := LoadConfig("")
|
||||||
|
assert.Equal(t, os.ErrNotExist, err)
|
||||||
|
|
||||||
|
_, err = LoadConfig("/dev/null")
|
||||||
|
assert.Assert(t, err != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInvalidData(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
BaseImageSize: "y",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.parse()
|
||||||
|
assert.Error(t, err, "failed to parse base image size: 'y': invalid size: 'y'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldValidation(t *testing.T) {
|
||||||
|
config := &Config{}
|
||||||
|
err := config.Validate()
|
||||||
|
assert.Assert(t, err != nil)
|
||||||
|
|
||||||
|
multErr := (err).(*multierror.Error)
|
||||||
|
assert.Assert(t, is.Len(multErr.Errors, 3))
|
||||||
|
|
||||||
|
assert.Assert(t, multErr.Errors[0] != nil, "pool_name is empty")
|
||||||
|
assert.Assert(t, multErr.Errors[1] != nil, "root_path is empty")
|
||||||
|
assert.Assert(t, multErr.Errors[2] != nil, "base_image_size is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExistingPoolFieldValidation(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
PoolName: "test",
|
||||||
|
RootPath: "test",
|
||||||
|
BaseImageSize: "10mb",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.Validate()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
106
snapshots/devmapper/device_info.go
Normal file
106
snapshots/devmapper/device_info.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package 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"`
|
||||||
|
}
|
390
snapshots/devmapper/dmsetup/dmsetup.go
Normal file
390
snapshots/devmapper/dmsetup/dmsetup.go
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package 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 <text>=<errno> 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 <deviceID>" 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 <deviceID>" 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.TrimSuffix(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 <name> 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
|
||||||
|
}
|
208
snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
208
snapshots/devmapper/dmsetup/dmsetup_test.go
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dmsetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/pkg/testutil"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testPoolName = "test-pool"
|
||||||
|
testDeviceName = "test-device"
|
||||||
|
deviceID = 1
|
||||||
|
snapshotID = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDMSetup(t *testing.T) {
|
||||||
|
testutil.RequiresRoot(t)
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "dmsetup-tests-")
|
||||||
|
assert.NilError(t, err, "failed to make temp dir for tests")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(tempDir)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
dataImage, loopDataDevice := createLoopbackDevice(t, tempDir)
|
||||||
|
metaImage, loopMetaDevice := createLoopbackDevice(t, tempDir)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = losetup.RemoveLoopDevicesAssociatedWithImage(dataImage)
|
||||||
|
assert.NilError(t, err, "failed to detach loop devices for data image: %s", dataImage)
|
||||||
|
|
||||||
|
err = losetup.RemoveLoopDevicesAssociatedWithImage(metaImage)
|
||||||
|
assert.NilError(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)
|
||||||
|
assert.NilError(t, err, "failed to create thin-pool")
|
||||||
|
|
||||||
|
table, err := Table(testPoolName)
|
||||||
|
t.Logf("table: %s", table)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, strings.HasPrefix(table, "0 32768 thin-pool"))
|
||||||
|
assert.Assert(t, strings.HasSuffix(table, "128 32768 1 skip_block_zeroing"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ReloadPool", func(t *testing.T) {
|
||||||
|
err := ReloadPool(testPoolName, loopDataDevice, loopMetaDevice, 256)
|
||||||
|
assert.NilError(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)
|
||||||
|
assert.NilError(t, err, "failed to remove thin-pool")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Version", testVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateDevice(t *testing.T) {
|
||||||
|
err := CreateDevice(testPoolName, deviceID)
|
||||||
|
assert.NilError(t, err, "failed to create test device")
|
||||||
|
|
||||||
|
err = CreateDevice(testPoolName, deviceID)
|
||||||
|
assert.Assert(t, err == unix.EEXIST)
|
||||||
|
|
||||||
|
infos, err := Info(testPoolName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(infos, 1), "got unexpected number of device infos")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateSnapshot(t *testing.T) {
|
||||||
|
err := CreateSnapshot(testPoolName, snapshotID, deviceID)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteSnapshot(t *testing.T) {
|
||||||
|
err := DeleteDevice(testPoolName, snapshotID)
|
||||||
|
assert.NilError(t, err, "failed to send delete message")
|
||||||
|
|
||||||
|
err = DeleteDevice(testPoolName, snapshotID)
|
||||||
|
assert.Assert(t, err == unix.ENODATA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testActivateDevice(t *testing.T) {
|
||||||
|
err := ActivateDevice(testPoolName, testDeviceName, 1, 1024, "")
|
||||||
|
assert.NilError(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.NilError(t, err, "failed to stat device")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := Info(testPoolName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(list, 1))
|
||||||
|
|
||||||
|
info := list[0]
|
||||||
|
assert.Equal(t, testPoolName, info.Name)
|
||||||
|
assert.Assert(t, info.TableLive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeviceStatus(t *testing.T) {
|
||||||
|
status, err := Status(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(0), status.Offset)
|
||||||
|
assert.Equal(t, int64(2), status.Length)
|
||||||
|
assert.Equal(t, "thin", status.Target)
|
||||||
|
assert.DeepEqual(t, status.Params, []string{"0", "-"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuspendResumeDevice(t *testing.T) {
|
||||||
|
err := SuspendDevice(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = SuspendDevice(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
list, err := Info(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(list, 1))
|
||||||
|
|
||||||
|
info := list[0]
|
||||||
|
assert.Assert(t, info.Suspended)
|
||||||
|
|
||||||
|
err = ResumeDevice(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = ResumeDevice(testDeviceName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoveDevice(t *testing.T) {
|
||||||
|
err := RemoveDevice(testPoolName)
|
||||||
|
assert.Assert(t, err == unix.EBUSY, "removing thin-pool with dependencies shouldn't be allowed")
|
||||||
|
|
||||||
|
err = RemoveDevice(testDeviceName, RemoveWithRetries)
|
||||||
|
assert.NilError(t, err, "failed to remove thin-device")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testVersion(t *testing.T) {
|
||||||
|
version, err := Version()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, version != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLoopbackDevice(t *testing.T, dir string) (string, string) {
|
||||||
|
file, err := ioutil.TempFile(dir, "dmsetup-tests-")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
size, err := units.RAMInBytes("16Mb")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Truncate(size)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
imagePath := file.Name()
|
||||||
|
|
||||||
|
loopDevice, err := losetup.AttachLoopDevice(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
return imagePath, loopDevice
|
||||||
|
}
|
85
snapshots/devmapper/losetup/losetup.go
Normal file
85
snapshots/devmapper/losetup/losetup.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package 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
|
||||||
|
}
|
117
snapshots/devmapper/losetup/losetup_test.go
Normal file
117
snapshots/devmapper/losetup/losetup_test.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package losetup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/pkg/testutil"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLosetup(t *testing.T) {
|
||||||
|
testutil.RequiresRoot(t)
|
||||||
|
|
||||||
|
var (
|
||||||
|
imagePath = createSparseImage(t)
|
||||||
|
loopDevice1 string
|
||||||
|
loopDevice2 string
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.Remove(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("AttachLoopDevice", func(t *testing.T) {
|
||||||
|
dev1, err := AttachLoopDevice(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, dev1 != "")
|
||||||
|
|
||||||
|
dev2, err := AttachLoopDevice(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, dev2 != dev1, "should attach different loop device")
|
||||||
|
|
||||||
|
loopDevice1 = dev1
|
||||||
|
loopDevice2 = dev2
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AttachEmptyLoopDevice", func(t *testing.T) {
|
||||||
|
_, err := AttachLoopDevice("")
|
||||||
|
assert.Assert(t, err != nil, "shouldn't attach empty path")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FindAssociatedLoopDevices", func(t *testing.T) {
|
||||||
|
devices, err := FindAssociatedLoopDevices(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(devices, 2), "unexpected number of attached devices")
|
||||||
|
assert.Assert(t, is.Contains(devices, loopDevice1))
|
||||||
|
assert.Assert(t, is.Contains(devices, loopDevice2))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FindAssociatedLoopDevicesForInvalidImage", func(t *testing.T) {
|
||||||
|
devices, err := FindAssociatedLoopDevices("")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(devices, 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DetachLoopDevice", func(t *testing.T) {
|
||||||
|
err := DetachLoopDevice(loopDevice2)
|
||||||
|
assert.NilError(t, err, "failed to detach %q", loopDevice2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DetachEmptyDevice", func(t *testing.T) {
|
||||||
|
err := DetachLoopDevice("")
|
||||||
|
assert.Assert(t, err != nil, "shouldn't detach empty path")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveLoopDevicesAssociatedWithImage", func(t *testing.T) {
|
||||||
|
err := RemoveLoopDevicesAssociatedWithImage(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
devices, err := FindAssociatedLoopDevices(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(devices, 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RemoveLoopDevicesAssociatedWithInvalidImage", func(t *testing.T) {
|
||||||
|
err := RemoveLoopDevicesAssociatedWithImage("")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSparseImage(t *testing.T) string {
|
||||||
|
file, err := ioutil.TempFile("", "losetup-tests-")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
size, err := units.RAMInBytes("16Mb")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Truncate(size)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
return file.Name()
|
||||||
|
}
|
315
snapshots/devmapper/metadata.go
Normal file
315
snapshots/devmapper/metadata.go
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package 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 <device_name>=<DeviceInfo>
|
||||||
|
deviceIDBucketName = []byte("device_ids") // Tracks used device ids <device_id_[0..maxDeviceID)>=<byte_[0/1]>
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
189
snapshots/devmapper/metadata_test.go
Normal file
189
snapshots/devmapper/metadata_test.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.NilError(t, err)
|
||||||
|
|
||||||
|
result, err := store.GetDevice(testCtx, "test2")
|
||||||
|
assert.NilError(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.Assert(t, result.DeviceID != 0)
|
||||||
|
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.Assert(t, err != nil)
|
||||||
|
|
||||||
|
_, 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.NilError(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.NilError(t, err)
|
||||||
|
|
||||||
|
info2 := &DeviceInfo{Name: "test2"}
|
||||||
|
err = store.AddDevice(testCtx, info2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Assert(t, info1.DeviceID != info2.DeviceID)
|
||||||
|
assert.Assert(t, info1.DeviceID != 0)
|
||||||
|
|
||||||
|
err = store.RemoveDevice(testCtx, info2.Name)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
info3 := &DeviceInfo{Name: "test3"}
|
||||||
|
err = store.AddDevice(testCtx, info3)
|
||||||
|
assert.NilError(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.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.RemoveDevice(testCtx, "test")
|
||||||
|
assert.NilError(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.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.UpdateDevice(testCtx, oldInfo.Name, func(info *DeviceInfo) error {
|
||||||
|
info.ParentName = "test5"
|
||||||
|
info.Size = 6
|
||||||
|
info.State = Created
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
newInfo, err := store.GetDevice(testCtx, "test1")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "test1", newInfo.Name)
|
||||||
|
assert.Equal(t, "test5", newInfo.ParentName)
|
||||||
|
assert.Assert(t, newInfo.Size == 6)
|
||||||
|
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.NilError(t, err)
|
||||||
|
|
||||||
|
err = store.AddDevice(testCtx, &DeviceInfo{Name: "test2"})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
names, err := store.GetDeviceNames(testCtx)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(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-")
|
||||||
|
assert.NilError(t, err, "couldn't create temp directory for metadata tests")
|
||||||
|
|
||||||
|
path := filepath.Join(tempDir, "test.db")
|
||||||
|
metadata, err := NewPoolMetadata(path)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
return tempDir, metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupStore(t *testing.T, tempDir string, store *PoolMetadata) {
|
||||||
|
err := store.Close()
|
||||||
|
assert.NilError(t, err, "failed to close metadata store")
|
||||||
|
|
||||||
|
err = os.RemoveAll(tempDir)
|
||||||
|
assert.NilError(t, err, "failed to cleanup temp directory")
|
||||||
|
}
|
386
snapshots/devmapper/pool_device.go
Normal file
386
snapshots/devmapper/pool_device.go
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/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/<deviceName>
|
||||||
|
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()
|
||||||
|
}
|
247
snapshots/devmapper/pool_device_test.go
Normal file
247
snapshots/devmapper/pool_device_test.go
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/mount"
|
||||||
|
"github.com/containerd/containerd/pkg/testutil"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
testutil.RequiresRoot(t)
|
||||||
|
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "pool-device-test-")
|
||||||
|
assert.NilError(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)
|
||||||
|
assert.NilError(t, err, "failed to create pool %q", poolName)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Detach loop devices and remove images
|
||||||
|
err := losetup.DetachLoopDevice(loopDataDevice, loopMetaDevice)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = os.RemoveAll(tempDir)
|
||||||
|
assert.NilError(t, err, "couldn't cleanup temp directory")
|
||||||
|
}()
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
PoolName: poolName,
|
||||||
|
RootPath: tempDir,
|
||||||
|
BaseImageSize: "16mb",
|
||||||
|
BaseImageSizeBytes: 16 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := NewPoolDevice(ctx, config)
|
||||||
|
assert.NilError(t, err, "can't create device pool")
|
||||||
|
assert.Assert(t, pool != nil)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := pool.RemovePool(ctx)
|
||||||
|
assert.NilError(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'
|
||||||
|
err = 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)
|
||||||
|
assert.NilError(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.NilError(t, err, "failed to write test file v2 on 'thin-1' volume after taking snapshot")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Mount 'snap-1' and make sure TEST file is v1
|
||||||
|
err = 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.NilError(t, err, "couldn't read test file from '%s' device", snapDevice1)
|
||||||
|
assert.Equal(t, "test file (v1)", string(fileData), "test file content is invalid on snapshot")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.NilError(t, 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)
|
||||||
|
assert.NilError(t, err, "can't create first thin device")
|
||||||
|
|
||||||
|
err = pool.CreateThinDevice(ctx, thinDevice1, device1Size)
|
||||||
|
assert.Assert(t, err != nil, "device pool allows duplicated device names")
|
||||||
|
|
||||||
|
err = pool.CreateThinDevice(ctx, thinDevice2, device2Size)
|
||||||
|
assert.NilError(t, err, "can't create second thin device")
|
||||||
|
|
||||||
|
deviceInfo1, err := pool.metadata.GetDevice(ctx, thinDevice1)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
deviceInfo2, err := pool.metadata.GetDevice(ctx, thinDevice2)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Assert(t, deviceInfo1.DeviceID != deviceInfo2.DeviceID, "assigned device ids should be different")
|
||||||
|
|
||||||
|
usage, err := pool.GetUsage(thinDevice1)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, usage, int64(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
assert.NilError(t, err, "failed to make filesystem on '%s': %s", thinDevice1, string(output))
|
||||||
|
|
||||||
|
usage, err := pool.GetUsage(thinDevice1)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, usage > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateSnapshot(t *testing.T, pool *PoolDevice) {
|
||||||
|
err := pool.CreateSnapshotDevice(context.Background(), thinDevice1, snapDevice1, device1Size)
|
||||||
|
assert.NilError(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.Assert(t, pool.IsActivated(deviceName))
|
||||||
|
|
||||||
|
err := pool.DeactivateDevice(context.Background(), deviceName, false)
|
||||||
|
assert.NilError(t, err, "failed to remove '%s'", deviceName)
|
||||||
|
|
||||||
|
assert.Assert(t, !pool.IsActivated(deviceName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoveThinDevice(t *testing.T, pool *PoolDevice) {
|
||||||
|
err := pool.RemoveDevice(testCtx, thinDevice1)
|
||||||
|
assert.NilError(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)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
size, err := units.RAMInBytes("128Mb")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Truncate(size)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = file.Close()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
imagePath := file.Name()
|
||||||
|
|
||||||
|
loopDevice, err := losetup.AttachLoopDevice(imagePath)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
return imagePath, loopDevice
|
||||||
|
}
|
477
snapshots/devmapper/snapshotter.go
Normal file
477
snapshots/devmapper/snapshotter.go
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package 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/plugin"
|
||||||
|
"github.com/containerd/containerd/snapshots"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
||||||
|
"github.com/containerd/containerd/snapshots/storage"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
plugin.Register(&plugin.Registration{
|
||||||
|
Type: plugin.SnapshotPlugin,
|
||||||
|
ID: "devmapper",
|
||||||
|
Config: &Config{},
|
||||||
|
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
|
||||||
|
ic.Meta.Platforms = append(ic.Meta.Platforms, ocispec.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Architecture: "amd64",
|
||||||
|
})
|
||||||
|
|
||||||
|
config, ok := ic.Config.(*Config)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid devmapper configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.PoolName == "" {
|
||||||
|
return nil, errors.New("devmapper not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RootPath == "" {
|
||||||
|
config.RootPath = ic.Root
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewSnapshotter(ic.Context, config)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 len(result.Errors) == 1 {
|
||||||
|
return result.Errors[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
142
snapshots/devmapper/snapshotter_test.go
Normal file
142
snapshots/devmapper/snapshotter_test.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package devmapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/continuity/fs/fstest"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/mount"
|
||||||
|
"github.com/containerd/containerd/namespaces"
|
||||||
|
"github.com/containerd/containerd/pkg/testutil"
|
||||||
|
"github.com/containerd/containerd/snapshots"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
|
||||||
|
"github.com/containerd/containerd/snapshots/devmapper/losetup"
|
||||||
|
"github.com/containerd/containerd/snapshots/testsuite"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSnapshotterSuite(t *testing.T) {
|
||||||
|
testutil.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)
|
||||||
|
assert.NilError(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")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
snapshotter, closer, err := snapshotterFn(ctx, tempDir)
|
||||||
|
assert.NilError(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", "")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
emptyLayerUsage, err := snapshotter.Usage(ctx, "prepare-1")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Should be > 0 as just written file system also consumes blocks
|
||||||
|
assert.Assert(t, emptyLayerUsage.Size > 0)
|
||||||
|
|
||||||
|
err = snapshotter.Commit(ctx, "layer-1", "prepare-1")
|
||||||
|
assert.NilError(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")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = mount.WithTempMount(ctx, mounts, baseApplier.Apply)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = snapshotter.Commit(ctx, "layer-2", "prepare-2")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
layer2Usage, err := snapshotter.Usage(ctx, "layer-2")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Should be at least 1 MB + fs metadata
|
||||||
|
assert.Assert(t, layer2Usage.Size > sizeBytes)
|
||||||
|
assert.Assert(t, layer2Usage.Size < sizeBytes+256*dmsetup.SectorSize)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user