github.com/demonoid81/containerd@v1.3.4/snapshots/devmapper/snapshotter.go (about)

     1  // +build linux
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package devmapper
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strings"
    28  	"sync"
    29  
    30  	"github.com/containerd/containerd/log"
    31  	"github.com/containerd/containerd/mount"
    32  	"github.com/containerd/containerd/plugin"
    33  	"github.com/containerd/containerd/snapshots"
    34  	"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
    35  	"github.com/containerd/containerd/snapshots/storage"
    36  	"github.com/hashicorp/go-multierror"
    37  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    38  	"github.com/pkg/errors"
    39  	"github.com/sirupsen/logrus"
    40  )
    41  
    42  func init() {
    43  	plugin.Register(&plugin.Registration{
    44  		Type:   plugin.SnapshotPlugin,
    45  		ID:     "devmapper",
    46  		Config: &Config{},
    47  		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
    48  			ic.Meta.Platforms = append(ic.Meta.Platforms, ocispec.Platform{
    49  				OS:           "linux",
    50  				Architecture: "amd64",
    51  			})
    52  
    53  			config, ok := ic.Config.(*Config)
    54  			if !ok {
    55  				return nil, errors.New("invalid devmapper configuration")
    56  			}
    57  
    58  			if config.PoolName == "" {
    59  				return nil, errors.New("devmapper not configured")
    60  			}
    61  
    62  			if config.RootPath == "" {
    63  				config.RootPath = ic.Root
    64  			}
    65  
    66  			return NewSnapshotter(ic.Context, config)
    67  		},
    68  	})
    69  }
    70  
    71  const (
    72  	metadataFileName = "metadata.db"
    73  	fsTypeExt4       = "ext4"
    74  )
    75  
    76  type closeFunc func() error
    77  
    78  // Snapshotter implements containerd's snapshotter (https://godoc.org/github.com/containerd/containerd/snapshots#Snapshotter)
    79  // based on Linux device-mapper targets.
    80  type Snapshotter struct {
    81  	store     *storage.MetaStore
    82  	pool      *PoolDevice
    83  	config    *Config
    84  	cleanupFn []closeFunc
    85  	closeOnce sync.Once
    86  }
    87  
    88  // NewSnapshotter creates new device mapper snapshotter.
    89  // Internally it creates thin-pool device (or reloads if it's already exists) and
    90  // initializes a database file for metadata.
    91  func NewSnapshotter(ctx context.Context, config *Config) (*Snapshotter, error) {
    92  	// Make sure snapshotter configuration valid before running
    93  	if err := config.parse(); err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	if err := config.Validate(); err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	var cleanupFn []closeFunc
   102  
   103  	if err := os.MkdirAll(config.RootPath, 0750); err != nil && !os.IsExist(err) {
   104  		return nil, errors.Wrapf(err, "failed to create root directory: %s", config.RootPath)
   105  	}
   106  
   107  	store, err := storage.NewMetaStore(filepath.Join(config.RootPath, metadataFileName))
   108  	if err != nil {
   109  		return nil, errors.Wrap(err, "failed to create metastore")
   110  	}
   111  
   112  	cleanupFn = append(cleanupFn, store.Close)
   113  
   114  	poolDevice, err := NewPoolDevice(ctx, config)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	cleanupFn = append(cleanupFn, poolDevice.Close)
   120  
   121  	return &Snapshotter{
   122  		store:     store,
   123  		config:    config,
   124  		pool:      poolDevice,
   125  		cleanupFn: cleanupFn,
   126  	}, nil
   127  }
   128  
   129  // Stat returns the info for an active or committed snapshot from store
   130  func (s *Snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
   131  	log.G(ctx).WithField("key", key).Debug("stat")
   132  
   133  	var (
   134  		info snapshots.Info
   135  		err  error
   136  	)
   137  
   138  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   139  		_, info, _, err = storage.GetInfo(ctx, key)
   140  		return err
   141  	})
   142  
   143  	return info, err
   144  }
   145  
   146  // Update updates an existing snapshot info's data
   147  func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
   148  	log.G(ctx).Debugf("update: %s", strings.Join(fieldpaths, ", "))
   149  
   150  	var err error
   151  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   152  		info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
   153  		return err
   154  	})
   155  
   156  	return info, err
   157  }
   158  
   159  // Usage returns the resource usage of an active or committed snapshot excluding the usage of parent snapshots.
   160  func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
   161  	log.G(ctx).WithField("key", key).Debug("usage")
   162  
   163  	var (
   164  		id    string
   165  		err   error
   166  		info  snapshots.Info
   167  		usage snapshots.Usage
   168  	)
   169  
   170  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   171  		id, info, usage, err = storage.GetInfo(ctx, key)
   172  		if err != nil {
   173  			return err
   174  		}
   175  
   176  		if info.Kind == snapshots.KindActive {
   177  			deviceName := s.getDeviceName(id)
   178  			usage.Size, err = s.pool.GetUsage(deviceName)
   179  			if err != nil {
   180  				return err
   181  			}
   182  		}
   183  
   184  		if info.Parent != "" {
   185  			// GetInfo returns total number of bytes used by a snapshot (including parent).
   186  			// So subtract parent usage in order to get delta consumed by layer itself.
   187  			_, _, parentUsage, err := storage.GetInfo(ctx, info.Parent)
   188  			if err != nil {
   189  				return err
   190  			}
   191  
   192  			usage.Size -= parentUsage.Size
   193  		}
   194  
   195  		return err
   196  	})
   197  
   198  	return usage, err
   199  }
   200  
   201  // Mounts return the list of mounts for the active or view snapshot
   202  func (s *Snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
   203  	log.G(ctx).WithField("key", key).Debug("mounts")
   204  
   205  	var (
   206  		snap storage.Snapshot
   207  		err  error
   208  	)
   209  
   210  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   211  		snap, err = storage.GetSnapshot(ctx, key)
   212  		return err
   213  	})
   214  
   215  	return s.buildMounts(snap), nil
   216  }
   217  
   218  // Prepare creates thin device for an active snapshot identified by key
   219  func (s *Snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   220  	log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare")
   221  
   222  	var (
   223  		mounts []mount.Mount
   224  		err    error
   225  	)
   226  
   227  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   228  		mounts, err = s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts...)
   229  		return err
   230  	})
   231  
   232  	return mounts, err
   233  }
   234  
   235  // View creates readonly thin device for the given snapshot key
   236  func (s *Snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   237  	log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare")
   238  
   239  	var (
   240  		mounts []mount.Mount
   241  		err    error
   242  	)
   243  
   244  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   245  		mounts, err = s.createSnapshot(ctx, snapshots.KindView, key, parent, opts...)
   246  		return err
   247  	})
   248  
   249  	return mounts, err
   250  }
   251  
   252  // Commit marks an active snapshot as committed in meta store.
   253  // Block device unmount operation captures snapshot changes by itself, so no
   254  // additional actions needed within Commit operation.
   255  func (s *Snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
   256  	log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit")
   257  
   258  	return s.withTransaction(ctx, true, func(ctx context.Context) error {
   259  		id, _, _, err := storage.GetInfo(ctx, key)
   260  		if err != nil {
   261  			return err
   262  		}
   263  
   264  		deviceName := s.getDeviceName(id)
   265  		size, err := s.pool.GetUsage(deviceName)
   266  		if err != nil {
   267  			return err
   268  		}
   269  
   270  		usage := snapshots.Usage{
   271  			Size: size,
   272  		}
   273  
   274  		_, err = storage.CommitActive(ctx, key, name, usage, opts...)
   275  		if err != nil {
   276  			return err
   277  		}
   278  
   279  		// The thin snapshot is not used for IO after committed, so
   280  		// suspend to flush the IO and deactivate the device.
   281  		err = s.pool.SuspendDevice(ctx, deviceName)
   282  		if err != nil {
   283  			return err
   284  		}
   285  
   286  		return s.pool.DeactivateDevice(ctx, deviceName, true, false)
   287  	})
   288  }
   289  
   290  // Remove removes thin device and snapshot metadata by key
   291  func (s *Snapshotter) Remove(ctx context.Context, key string) error {
   292  	log.G(ctx).WithField("key", key).Debug("remove")
   293  
   294  	return s.withTransaction(ctx, true, func(ctx context.Context) error {
   295  		return s.removeDevice(ctx, key)
   296  	})
   297  }
   298  
   299  func (s *Snapshotter) removeDevice(ctx context.Context, key string) error {
   300  	snapID, _, err := storage.Remove(ctx, key)
   301  	if err != nil {
   302  		return err
   303  	}
   304  
   305  	deviceName := s.getDeviceName(snapID)
   306  	if err := s.pool.RemoveDevice(ctx, deviceName); err != nil {
   307  		log.G(ctx).WithError(err).Errorf("failed to remove device")
   308  		return err
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  // Walk iterates through all metadata Info for the stored snapshots and calls the provided function for each.
   315  func (s *Snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error {
   316  	log.G(ctx).Debug("walk")
   317  	return s.withTransaction(ctx, false, func(ctx context.Context) error {
   318  		return storage.WalkInfo(ctx, fn)
   319  	})
   320  }
   321  
   322  // ResetPool deactivates and deletes all thin devices in thin-pool.
   323  // Used for cleaning pool after benchmarking.
   324  func (s *Snapshotter) ResetPool(ctx context.Context) error {
   325  	names, err := s.pool.metadata.GetDeviceNames(ctx)
   326  	if err != nil {
   327  		return err
   328  	}
   329  
   330  	var result *multierror.Error
   331  	for _, name := range names {
   332  		if err := s.pool.RemoveDevice(ctx, name); err != nil {
   333  			result = multierror.Append(result, err)
   334  		}
   335  	}
   336  
   337  	return result.ErrorOrNil()
   338  }
   339  
   340  // Close releases devmapper snapshotter resources.
   341  // All subsequent Close calls will be ignored.
   342  func (s *Snapshotter) Close() error {
   343  	log.L.Debug("close")
   344  
   345  	var result *multierror.Error
   346  	s.closeOnce.Do(func() {
   347  		for _, fn := range s.cleanupFn {
   348  			if err := fn(); err != nil {
   349  				result = multierror.Append(result, err)
   350  			}
   351  		}
   352  	})
   353  
   354  	return result.ErrorOrNil()
   355  }
   356  
   357  func (s *Snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   358  	snap, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  
   363  	if len(snap.ParentIDs) == 0 {
   364  		deviceName := s.getDeviceName(snap.ID)
   365  		log.G(ctx).Debugf("creating new thin device '%s'", deviceName)
   366  
   367  		err := s.pool.CreateThinDevice(ctx, deviceName, s.config.BaseImageSizeBytes)
   368  		if err != nil {
   369  			log.G(ctx).WithError(err).Errorf("failed to create thin device for snapshot %s", snap.ID)
   370  			return nil, err
   371  		}
   372  
   373  		if err := s.mkfs(ctx, deviceName); err != nil {
   374  			// Rollback thin device creation if mkfs failed
   375  			return nil, multierror.Append(err,
   376  				s.pool.RemoveDevice(ctx, deviceName))
   377  		}
   378  	} else {
   379  		parentDeviceName := s.getDeviceName(snap.ParentIDs[0])
   380  		snapDeviceName := s.getDeviceName(snap.ID)
   381  		log.G(ctx).Debugf("creating snapshot device '%s' from '%s'", snapDeviceName, parentDeviceName)
   382  
   383  		err := s.pool.CreateSnapshotDevice(ctx, parentDeviceName, snapDeviceName, s.config.BaseImageSizeBytes)
   384  		if err != nil {
   385  			log.G(ctx).WithError(err).Errorf("failed to create snapshot device from parent %s", parentDeviceName)
   386  			return nil, err
   387  		}
   388  	}
   389  
   390  	mounts := s.buildMounts(snap)
   391  
   392  	// Remove default directories not expected by the container image
   393  	_ = mount.WithTempMount(ctx, mounts, func(root string) error {
   394  		return os.Remove(filepath.Join(root, "lost+found"))
   395  	})
   396  
   397  	return mounts, nil
   398  }
   399  
   400  // mkfs creates ext4 filesystem on the given devmapper device
   401  func (s *Snapshotter) mkfs(ctx context.Context, deviceName string) error {
   402  	args := []string{
   403  		"-E",
   404  		// We don't want any zeroing in advance when running mkfs on thin devices (see "man mkfs.ext4")
   405  		"nodiscard,lazy_itable_init=0,lazy_journal_init=0",
   406  		dmsetup.GetFullDevicePath(deviceName),
   407  	}
   408  
   409  	log.G(ctx).Debugf("mkfs.ext4 %s", strings.Join(args, " "))
   410  	output, err := exec.Command("mkfs.ext4", args...).CombinedOutput()
   411  	if err != nil {
   412  		log.G(ctx).WithError(err).Errorf("failed to write fs:\n%s", string(output))
   413  		return err
   414  	}
   415  
   416  	log.G(ctx).Debugf("mkfs:\n%s", string(output))
   417  	return nil
   418  }
   419  
   420  func (s *Snapshotter) getDeviceName(snapID string) string {
   421  	// Add pool name as prefix to avoid collisions with devices from other pools
   422  	return fmt.Sprintf("%s-snap-%s", s.config.PoolName, snapID)
   423  }
   424  
   425  func (s *Snapshotter) getDevicePath(snap storage.Snapshot) string {
   426  	name := s.getDeviceName(snap.ID)
   427  	return dmsetup.GetFullDevicePath(name)
   428  }
   429  
   430  func (s *Snapshotter) buildMounts(snap storage.Snapshot) []mount.Mount {
   431  	var options []string
   432  
   433  	if snap.Kind != snapshots.KindActive {
   434  		options = append(options, "ro")
   435  	}
   436  
   437  	mounts := []mount.Mount{
   438  		{
   439  			Source:  s.getDevicePath(snap),
   440  			Type:    fsTypeExt4,
   441  			Options: options,
   442  		},
   443  	}
   444  
   445  	return mounts
   446  }
   447  
   448  // withTransaction wraps fn callback with containerd's meta store transaction.
   449  // If callback returns an error or transaction is not writable, database transaction will be discarded.
   450  func (s *Snapshotter) withTransaction(ctx context.Context, writable bool, fn func(ctx context.Context) error) error {
   451  	ctx, trans, err := s.store.TransactionContext(ctx, writable)
   452  	if err != nil {
   453  		return err
   454  	}
   455  
   456  	var result *multierror.Error
   457  
   458  	err = fn(ctx)
   459  	if err != nil {
   460  		result = multierror.Append(result, err)
   461  	}
   462  
   463  	// Always rollback if transaction is not writable
   464  	if err != nil || !writable {
   465  		if terr := trans.Rollback(); terr != nil {
   466  			log.G(ctx).WithError(terr).Error("failed to rollback transaction")
   467  			result = multierror.Append(result, errors.Wrap(terr, "rollback failed"))
   468  		}
   469  	} else {
   470  		if terr := trans.Commit(); terr != nil {
   471  			log.G(ctx).WithError(terr).Error("failed to commit transaction")
   472  			result = multierror.Append(result, errors.Wrap(terr, "commit failed"))
   473  		}
   474  	}
   475  
   476  	if err := result.ErrorOrNil(); err != nil {
   477  		log.G(ctx).WithError(err).Debug("snapshotter error")
   478  
   479  		// Unwrap if just one error
   480  		if len(result.Errors) == 1 {
   481  			return result.Errors[0]
   482  		}
   483  
   484  		return err
   485  	}
   486  
   487  	return nil
   488  }