devmapper: add snapshotter config

Signed-off-by: Maksym Pavlenko <makpav@amazon.com>
This commit is contained in:
Maksym Pavlenko 2019-02-14 13:31:39 -08:00
parent fcd9dc2749
commit 6e0ae68e17
No known key found for this signature in database
GPG Key ID: BDA48CBFE7A0FC14
2 changed files with 280 additions and 0 deletions

View File

@ -0,0 +1,158 @@
/*
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/containerd/containerd/snapshots/devmapper/dmsetup"
"github.com/docker/go-units"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
)
const (
// See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt for details
dataBlockMinSize = 128
dataBlockMaxSize = 2097152
)
var (
errInvalidBlockSize = errors.Errorf("block size should be between %d and %d", dataBlockMinSize, dataBlockMaxSize)
errInvalidBlockAlignment = errors.Errorf("block size should be multiple of %d sectors", dataBlockMinSize)
)
// 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"`
// Path to data volume to be used by thin-pool
DataDevice string `toml:"data_device"`
// Path to metadata volume to be used by thin-pool
MetadataDevice string `toml:"meta_device"`
// The size of allocation chunks in data file.
// Must be between 128 sectors (64KB) and 2097152 sectors (1GB) and a multiple of 128 sectors (64KB)
// Block size can't be changed after pool created.
// See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt
DataBlockSize string `toml:"data_block_size"`
DataBlockSizeSectors uint32 `toml:"-"`
// 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 JSON format from disk
func LoadConfig(path string) (*Config, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return nil, os.ErrNotExist
}
return nil, err
}
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 {
var result *multierror.Error
if c.DataBlockSize != "" {
if blockSize, err := units.RAMInBytes(c.DataBlockSize); err != nil {
result = multierror.Append(result, errors.Wrapf(err, "failed to parse data block size: %q", c.DataBlockSize))
} else {
c.DataBlockSizeSectors = uint32(blockSize / dmsetup.SectorSize)
}
}
if baseImageSize, err := units.RAMInBytes(c.BaseImageSize); err != nil {
result = multierror.Append(result, errors.Wrapf(err, "failed to parse base image size: %q", c.BaseImageSize))
} else {
c.BaseImageSizeBytes = uint64(baseImageSize)
}
return result.ErrorOrNil()
}
// 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"))
}
// The following fields are required only if we want to create or reload pool.
// Otherwise existing pool with 'PoolName' (prepared in advance) can be used by snapshotter.
if c.DataDevice != "" || c.MetadataDevice != "" || c.DataBlockSize != "" || c.DataBlockSizeSectors != 0 {
strChecks := []struct {
field string
name string
}{
{c.DataDevice, "data_device"},
{c.MetadataDevice, "meta_device"},
{c.DataBlockSize, "data_block_size"},
}
for _, check := range strChecks {
if check.field == "" {
result = multierror.Append(result, errors.Errorf("%s is empty", check.name))
}
}
if c.DataBlockSizeSectors < dataBlockMinSize || c.DataBlockSizeSectors > dataBlockMaxSize {
result = multierror.Append(result, errInvalidBlockSize)
}
if c.DataBlockSizeSectors%dataBlockMinSize != 0 {
result = multierror.Append(result, errInvalidBlockAlignment)
}
}
return result.ErrorOrNil()
}

View File

@ -0,0 +1,122 @@
/*
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"
"strings"
"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",
DataDevice: "/dev/loop0",
MetadataDevice: "/dev/loop1",
DataBlockSize: "1mb",
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.DataDevice, expected.DataDevice)
assert.Equal(t, loaded.MetadataDevice, expected.MetadataDevice)
assert.Equal(t, loaded.DataBlockSize, expected.DataBlockSize)
assert.Equal(t, loaded.BaseImageSize, expected.BaseImageSize)
assert.Assert(t, loaded.DataBlockSizeSectors == 1*1024*1024/512)
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{
DataBlockSize: "x",
BaseImageSize: "y",
}
err := config.parse()
assert.Assert(t, err != nil)
multErr := (err).(*multierror.Error)
assert.Assert(t, is.Len(multErr.Errors, 2))
assert.Assert(t, strings.Contains(multErr.Errors[0].Error(), "failed to parse data block size: \"x\""))
assert.Assert(t, strings.Contains(multErr.Errors[1].Error(), "failed to parse base image size: \"y\""))
}
func TestFieldValidation(t *testing.T) {
config := &Config{DataBlockSizeSectors: 1}
err := config.Validate()
assert.Assert(t, err != nil)
multErr := (err).(*multierror.Error)
assert.Assert(t, is.Len(multErr.Errors, 8))
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")
assert.Assert(t, multErr.Errors[3] != nil, "data_device is empty")
assert.Assert(t, multErr.Errors[4] != nil, "meta_device is empty")
assert.Assert(t, multErr.Errors[5] != nil, "data_block_size is empty")
assert.Equal(t, multErr.Errors[6], errInvalidBlockSize)
assert.Equal(t, multErr.Errors[7], errInvalidBlockAlignment)
}
func TestExistingPoolFieldValidation(t *testing.T) {
config := &Config{
PoolName: "test",
RootPath: "test",
BaseImageSize: "10mb",
}
err := config.Validate()
assert.NilError(t, err)
}