containerd/snapshot/btrfs/btrfs.go
Stephen J Day aeffd4f92c
btrfs: test btrfs snapshots with driver suite
We now include btrfs in the snapshot driver test suite. This includes
the addition of parent links and name hashing into the btrfs driver.
We'll probably endup replacing this with a common metadata store, as
these relationships are generally identical between implementations.

A small bug was discovered in the delete implementation in the course
testing, so the btrfs package has been updated with a fix.

The overlay driver was modified accordingly with the btrfs driver to use
`Driver` as the exported type of each driver packge.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
2017-02-03 15:54:11 -08:00

259 lines
6.5 KiB
Go

package btrfs
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/docker/containerd"
"github.com/docker/containerd/log"
"github.com/pkg/errors"
"github.com/stevvooe/go-btrfs"
)
type Driver struct {
device string // maybe we can resolve it with path?
root string // root provides paths for internal storage.
}
func NewDriver(device, root string) (*Driver, error) {
return &Driver{device: device, root: root}, nil
}
func (b *Driver) Prepare(key, parent string) ([]containerd.Mount, error) {
return b.makeActive(key, parent, false)
}
func (b *Driver) View(key, parent string) ([]containerd.Mount, error) {
return b.makeActive(key, parent, true)
}
func (b *Driver) makeActive(key, parent string, readonly bool) ([]containerd.Mount, error) {
var (
active = filepath.Join(b.root, "active")
parents = filepath.Join(b.root, "parents")
snapshots = filepath.Join(b.root, "snapshots")
names = filepath.Join(b.root, "names")
dir = filepath.Join(active, hash(key))
namep = filepath.Join(names, hash(key))
parentlink = filepath.Join(parents, hash(key))
parentp = filepath.Join(snapshots, hash(parent))
)
for _, path := range []string{
active,
parents,
names,
} {
if err := os.MkdirAll(path, 0755); err != nil {
return nil, err
}
}
if parent == "" {
// create new subvolume
// btrfs subvolume create /dir
if err := btrfs.SubvolCreate(dir); err != nil {
return nil, err
}
} else {
// btrfs subvolume snapshot /parent /subvol
if err := btrfs.SubvolSnapshot(dir, parentp, readonly); err != nil {
return nil, err
}
if err := os.Symlink(parentp, parentlink); err != nil {
return nil, err
}
}
if err := ioutil.WriteFile(namep, []byte(key), 0644); err != nil {
return nil, err
}
return b.mounts(dir)
}
func (b *Driver) mounts(dir string) ([]containerd.Mount, error) {
var options []string
// get the subvolume id back out for the mount
info, err := btrfs.SubvolInfo(dir)
if err != nil {
return nil, err
}
options = append(options, fmt.Sprintf("subvolid=%d", info.ID))
if info.Readonly {
options = append(options, "ro")
}
return []containerd.Mount{
{
Type: "btrfs",
Source: b.device, // device?
// NOTE(stevvooe): While it would be nice to use to uuids for
// mounts, they don't work reliably if the uuids are missing.
Options: options,
},
}, nil
}
func (b *Driver) Commit(name, key string) error {
var (
active = filepath.Join(b.root, "active")
snapshots = filepath.Join(b.root, "snapshots")
names = filepath.Join(b.root, "names")
parents = filepath.Join(b.root, "parents")
dir = filepath.Join(active, hash(key))
target = filepath.Join(snapshots, hash(name))
keynamep = filepath.Join(names, hash(key))
namep = filepath.Join(names, hash(name))
keyparentlink = filepath.Join(parents, hash(key))
parentlink = filepath.Join(parents, hash(name))
)
info, err := btrfs.SubvolInfo(dir)
if err != nil {
return err
}
if info.Readonly {
return fmt.Errorf("may not commit view snapshot %q", dir)
}
// look up the parent information to make sure we have the right parent link.
parentp, err := os.Readlink(keyparentlink)
if err != nil {
if !os.IsNotExist(err) {
return err
}
// we have no parent!
}
if err := os.MkdirAll(snapshots, 0755); err != nil {
return err
}
fmt.Println("commit snapshot", target)
if err := btrfs.SubvolSnapshot(target, dir, true); err != nil {
fmt.Println("snapshot error")
return err
}
// remove the key name path as we no longer need it.
if err := os.Remove(keynamep); err != nil {
return err
}
if err := ioutil.WriteFile(namep, []byte(name), 0755); err != nil {
return err
}
if parentp != "" {
// move over the parent link into the commit name.
if err := os.Rename(keyparentlink, parentlink); err != nil {
return err
}
// TODO(stevvooe): For this to not break horribly, we should really
// start taking a full lock. We are going to move all this metadata
// into common storage, so let's not fret over it for now.
}
if err := btrfs.SubvolDelete(dir); err != nil {
return errors.Wrapf(err, "delete subvol failed on %v", dir)
}
return nil
}
// Mounts returns the mounts for the transaction identified by key. Can be
// called on an read-write or readonly transaction.
//
// This can be used to recover mounts after calling View or Prepare.
func (b *Driver) Mounts(key string) ([]containerd.Mount, error) {
dir := filepath.Join(b.root, "active", hash(key))
return b.mounts(dir)
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
func (b *Driver) Remove(key string) error {
panic("not implemented")
}
// Parent returns the parent of snapshot identified by name.
func (b *Driver) Parent(name string) (string, error) {
var (
parents = filepath.Join(b.root, "parents")
names = filepath.Join(b.root, "names")
parentlink = filepath.Join(parents, hash(name))
)
parentp, err := os.Readlink(parentlink)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
return "", nil // no parent!
}
// okay, grab the basename of the parent and look up its name!
parentnamep := filepath.Join(names, filepath.Base(parentp))
p, err := ioutil.ReadFile(parentnamep)
if err != nil {
return "", err
}
return string(p), nil
}
// Exists returns true if the snapshot with name exists.
func (b *Driver) Exists(name string) bool {
target := filepath.Join(b.root, "snapshots", hash(name))
if _, err := os.Stat(target); err != nil {
if !os.IsNotExist(err) {
// TODO(stevvooe): Very rare condition when this fails horribly,
// such as an access error. Ideally, Exists is simple, but we may
// consider returning an error.
log.L.WithError(err).Fatal("error encountered checking for snapshot existence")
}
return false
}
return true
}
// Delete the snapshot idenfitied by name.
//
// If name has children, the operation will fail.
func (b *Driver) Delete(name string) error {
panic("not implemented")
}
// TODO(stevvooe): The methods below are still in flux. We'll need to work
// out the roles of active and committed snapshots for this to become more
// clear.
// Walk the committed snapshots.
func (b *Driver) Walk(fn func(name string) error) error {
panic("not implemented")
}
// Active will call fn for each active transaction.
func (b *Driver) Active(fn func(key string) error) error {
panic("not implemented")
}
func hash(k string) string {
return fmt.Sprintf("%x", sha256.Sum224([]byte(k)))
}