Skip to content

Commit 6e0ae68

Browse files
committed
devmapper: add snapshotter config
Signed-off-by: Maksym Pavlenko <[email protected]>
1 parent fcd9dc2 commit 6e0ae68

2 files changed

Lines changed: 280 additions & 0 deletions

File tree

snapshots/devmapper/config.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package devmapper
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/BurntSushi/toml"
24+
"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
25+
"github.com/docker/go-units"
26+
"github.com/hashicorp/go-multierror"
27+
"github.com/pkg/errors"
28+
)
29+
30+
const (
31+
// See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt for details
32+
dataBlockMinSize = 128
33+
dataBlockMaxSize = 2097152
34+
)
35+
36+
var (
37+
errInvalidBlockSize = errors.Errorf("block size should be between %d and %d", dataBlockMinSize, dataBlockMaxSize)
38+
errInvalidBlockAlignment = errors.Errorf("block size should be multiple of %d sectors", dataBlockMinSize)
39+
)
40+
41+
// Config represents device mapper configuration loaded from file.
42+
// Size units can be specified in human-readable string format (like "32KIB", "32GB", "32Tb")
43+
type Config struct {
44+
// Device snapshotter root directory for metadata
45+
RootPath string `toml:"root_path"`
46+
47+
// Name for 'thin-pool' device to be used by snapshotter (without /dev/mapper/ prefix)
48+
PoolName string `toml:"pool_name"`
49+
50+
// Path to data volume to be used by thin-pool
51+
DataDevice string `toml:"data_device"`
52+
53+
// Path to metadata volume to be used by thin-pool
54+
MetadataDevice string `toml:"meta_device"`
55+
56+
// The size of allocation chunks in data file.
57+
// Must be between 128 sectors (64KB) and 2097152 sectors (1GB) and a multiple of 128 sectors (64KB)
58+
// Block size can't be changed after pool created.
59+
// See https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt
60+
DataBlockSize string `toml:"data_block_size"`
61+
DataBlockSizeSectors uint32 `toml:"-"`
62+
63+
// Defines how much space to allocate when creating base image for container
64+
BaseImageSize string `toml:"base_image_size"`
65+
BaseImageSizeBytes uint64 `toml:"-"`
66+
}
67+
68+
// LoadConfig reads devmapper configuration file JSON format from disk
69+
func LoadConfig(path string) (*Config, error) {
70+
if _, err := os.Stat(path); err != nil {
71+
if os.IsNotExist(err) {
72+
return nil, os.ErrNotExist
73+
}
74+
75+
return nil, err
76+
}
77+
78+
config := Config{}
79+
if _, err := toml.DecodeFile(path, &config); err != nil {
80+
return nil, errors.Wrapf(err, "failed to unmarshal data at '%s'", path)
81+
}
82+
83+
if err := config.parse(); err != nil {
84+
return nil, err
85+
}
86+
87+
if err := config.Validate(); err != nil {
88+
return nil, err
89+
}
90+
91+
return &config, nil
92+
}
93+
94+
func (c *Config) parse() error {
95+
var result *multierror.Error
96+
97+
if c.DataBlockSize != "" {
98+
if blockSize, err := units.RAMInBytes(c.DataBlockSize); err != nil {
99+
result = multierror.Append(result, errors.Wrapf(err, "failed to parse data block size: %q", c.DataBlockSize))
100+
} else {
101+
c.DataBlockSizeSectors = uint32(blockSize / dmsetup.SectorSize)
102+
}
103+
}
104+
105+
if baseImageSize, err := units.RAMInBytes(c.BaseImageSize); err != nil {
106+
result = multierror.Append(result, errors.Wrapf(err, "failed to parse base image size: %q", c.BaseImageSize))
107+
} else {
108+
c.BaseImageSizeBytes = uint64(baseImageSize)
109+
}
110+
111+
return result.ErrorOrNil()
112+
}
113+
114+
// Validate makes sure configuration fields are valid
115+
func (c *Config) Validate() error {
116+
var result *multierror.Error
117+
118+
if c.PoolName == "" {
119+
result = multierror.Append(result, fmt.Errorf("pool_name is required"))
120+
}
121+
122+
if c.RootPath == "" {
123+
result = multierror.Append(result, fmt.Errorf("root_path is required"))
124+
}
125+
126+
if c.BaseImageSize == "" {
127+
result = multierror.Append(result, fmt.Errorf("base_image_size is required"))
128+
}
129+
130+
// The following fields are required only if we want to create or reload pool.
131+
// Otherwise existing pool with 'PoolName' (prepared in advance) can be used by snapshotter.
132+
if c.DataDevice != "" || c.MetadataDevice != "" || c.DataBlockSize != "" || c.DataBlockSizeSectors != 0 {
133+
strChecks := []struct {
134+
field string
135+
name string
136+
}{
137+
{c.DataDevice, "data_device"},
138+
{c.MetadataDevice, "meta_device"},
139+
{c.DataBlockSize, "data_block_size"},
140+
}
141+
142+
for _, check := range strChecks {
143+
if check.field == "" {
144+
result = multierror.Append(result, errors.Errorf("%s is empty", check.name))
145+
}
146+
}
147+
148+
if c.DataBlockSizeSectors < dataBlockMinSize || c.DataBlockSizeSectors > dataBlockMaxSize {
149+
result = multierror.Append(result, errInvalidBlockSize)
150+
}
151+
152+
if c.DataBlockSizeSectors%dataBlockMinSize != 0 {
153+
result = multierror.Append(result, errInvalidBlockAlignment)
154+
}
155+
}
156+
157+
return result.ErrorOrNil()
158+
}

snapshots/devmapper/config_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package devmapper
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"strings"
23+
"testing"
24+
25+
"github.com/BurntSushi/toml"
26+
"github.com/hashicorp/go-multierror"
27+
"gotest.tools/assert"
28+
is "gotest.tools/assert/cmp"
29+
)
30+
31+
func TestLoadConfig(t *testing.T) {
32+
expected := Config{
33+
RootPath: "/tmp",
34+
PoolName: "test",
35+
DataDevice: "/dev/loop0",
36+
MetadataDevice: "/dev/loop1",
37+
DataBlockSize: "1mb",
38+
BaseImageSize: "128Mb",
39+
}
40+
41+
file, err := ioutil.TempFile("", "devmapper-config-")
42+
assert.NilError(t, err)
43+
44+
encoder := toml.NewEncoder(file)
45+
err = encoder.Encode(&expected)
46+
assert.NilError(t, err)
47+
48+
defer func() {
49+
err := file.Close()
50+
assert.NilError(t, err)
51+
52+
err = os.Remove(file.Name())
53+
assert.NilError(t, err)
54+
}()
55+
56+
loaded, err := LoadConfig(file.Name())
57+
assert.NilError(t, err)
58+
59+
assert.Equal(t, loaded.RootPath, expected.RootPath)
60+
assert.Equal(t, loaded.PoolName, expected.PoolName)
61+
assert.Equal(t, loaded.DataDevice, expected.DataDevice)
62+
assert.Equal(t, loaded.MetadataDevice, expected.MetadataDevice)
63+
assert.Equal(t, loaded.DataBlockSize, expected.DataBlockSize)
64+
assert.Equal(t, loaded.BaseImageSize, expected.BaseImageSize)
65+
66+
assert.Assert(t, loaded.DataBlockSizeSectors == 1*1024*1024/512)
67+
assert.Assert(t, loaded.BaseImageSizeBytes == 128*1024*1024)
68+
}
69+
70+
func TestLoadConfigInvalidPath(t *testing.T) {
71+
_, err := LoadConfig("")
72+
assert.Equal(t, os.ErrNotExist, err)
73+
74+
_, err = LoadConfig("/dev/null")
75+
assert.Assert(t, err != nil)
76+
}
77+
78+
func TestParseInvalidData(t *testing.T) {
79+
config := Config{
80+
DataBlockSize: "x",
81+
BaseImageSize: "y",
82+
}
83+
84+
err := config.parse()
85+
assert.Assert(t, err != nil)
86+
87+
multErr := (err).(*multierror.Error)
88+
assert.Assert(t, is.Len(multErr.Errors, 2))
89+
90+
assert.Assert(t, strings.Contains(multErr.Errors[0].Error(), "failed to parse data block size: \"x\""))
91+
assert.Assert(t, strings.Contains(multErr.Errors[1].Error(), "failed to parse base image size: \"y\""))
92+
}
93+
94+
func TestFieldValidation(t *testing.T) {
95+
config := &Config{DataBlockSizeSectors: 1}
96+
err := config.Validate()
97+
assert.Assert(t, err != nil)
98+
99+
multErr := (err).(*multierror.Error)
100+
assert.Assert(t, is.Len(multErr.Errors, 8))
101+
102+
assert.Assert(t, multErr.Errors[0] != nil, "pool_name is empty")
103+
assert.Assert(t, multErr.Errors[1] != nil, "root_path is empty")
104+
assert.Assert(t, multErr.Errors[2] != nil, "base_image_size is empty")
105+
assert.Assert(t, multErr.Errors[3] != nil, "data_device is empty")
106+
assert.Assert(t, multErr.Errors[4] != nil, "meta_device is empty")
107+
assert.Assert(t, multErr.Errors[5] != nil, "data_block_size is empty")
108+
109+
assert.Equal(t, multErr.Errors[6], errInvalidBlockSize)
110+
assert.Equal(t, multErr.Errors[7], errInvalidBlockAlignment)
111+
}
112+
113+
func TestExistingPoolFieldValidation(t *testing.T) {
114+
config := &Config{
115+
PoolName: "test",
116+
RootPath: "test",
117+
BaseImageSize: "10mb",
118+
}
119+
120+
err := config.Validate()
121+
assert.NilError(t, err)
122+
}

0 commit comments

Comments
 (0)