github.com/containerd/Containerd@v1.4.13/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/errdefs"
    31  	"github.com/containerd/containerd/log"
    32  	"github.com/containerd/containerd/mount"
    33  	"github.com/containerd/containerd/platforms"
    34  	"github.com/containerd/containerd/plugin"
    35  	"github.com/containerd/containerd/snapshots"
    36  	"github.com/containerd/containerd/snapshots/devmapper/dmsetup"
    37  	"github.com/containerd/containerd/snapshots/storage"
    38  	"github.com/hashicorp/go-multierror"
    39  	"github.com/pkg/errors"
    40  	"github.com/sirupsen/logrus"
    41  )
    42  
    43  func init() {
    44  	plugin.Register(&plugin.Registration{
    45  		Type:   plugin.SnapshotPlugin,
    46  		ID:     "devmapper",
    47  		Config: &Config{},
    48  		InitFn: func(ic *plugin.InitContext) (interface{}, error) {
    49  			ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
    50  
    51  			config, ok := ic.Config.(*Config)
    52  			if !ok {
    53  				return nil, errors.New("invalid devmapper configuration")
    54  			}
    55  
    56  			if config.PoolName == "" {
    57  				return nil, errors.New("devmapper not configured")
    58  			}
    59  
    60  			if config.RootPath == "" {
    61  				config.RootPath = ic.Root
    62  			}
    63  
    64  			return NewSnapshotter(ic.Context, config)
    65  		},
    66  	})
    67  }
    68  
    69  const (
    70  	metadataFileName = "metadata.db"
    71  	fsTypeExt4       = "ext4"
    72  )
    73  
    74  type closeFunc func() error
    75  
    76  // Snapshotter implements containerd's snapshotter (https://godoc.org/github.com/containerd/containerd/snapshots#Snapshotter)
    77  // based on Linux device-mapper targets.
    78  type Snapshotter struct {
    79  	store     *storage.MetaStore
    80  	pool      *PoolDevice
    81  	config    *Config
    82  	cleanupFn []closeFunc
    83  	closeOnce sync.Once
    84  }
    85  
    86  // NewSnapshotter creates new device mapper snapshotter.
    87  // Internally it creates thin-pool device (or reloads if it's already exists) and
    88  // initializes a database file for metadata.
    89  func NewSnapshotter(ctx context.Context, config *Config) (*Snapshotter, error) {
    90  	// Make sure snapshotter configuration valid before running
    91  	if err := config.parse(); err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	if err := config.Validate(); err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	var cleanupFn []closeFunc
   100  
   101  	if err := os.MkdirAll(config.RootPath, 0750); err != nil && !os.IsExist(err) {
   102  		return nil, errors.Wrapf(err, "failed to create root directory: %s", config.RootPath)
   103  	}
   104  
   105  	store, err := storage.NewMetaStore(filepath.Join(config.RootPath, metadataFileName))
   106  	if err != nil {
   107  		return nil, errors.Wrap(err, "failed to create metastore")
   108  	}
   109  
   110  	cleanupFn = append(cleanupFn, store.Close)
   111  
   112  	poolDevice, err := NewPoolDevice(ctx, config)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	cleanupFn = append(cleanupFn, poolDevice.Close)
   118  
   119  	return &Snapshotter{
   120  		store:     store,
   121  		config:    config,
   122  		pool:      poolDevice,
   123  		cleanupFn: cleanupFn,
   124  	}, nil
   125  }
   126  
   127  // Stat returns the info for an active or committed snapshot from store
   128  func (s *Snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) {
   129  	log.G(ctx).WithField("key", key).Debug("stat")
   130  
   131  	var (
   132  		info snapshots.Info
   133  		err  error
   134  	)
   135  
   136  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   137  		_, info, _, err = storage.GetInfo(ctx, key)
   138  		return err
   139  	})
   140  
   141  	return info, err
   142  }
   143  
   144  // Update updates an existing snapshot info's data
   145  func (s *Snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) {
   146  	log.G(ctx).Debugf("update: %s", strings.Join(fieldpaths, ", "))
   147  
   148  	var err error
   149  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   150  		info, err = storage.UpdateInfo(ctx, info, fieldpaths...)
   151  		return err
   152  	})
   153  
   154  	return info, err
   155  }
   156  
   157  // Usage returns the resource usage of an active or committed snapshot excluding the usage of parent snapshots.
   158  func (s *Snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
   159  	log.G(ctx).WithField("key", key).Debug("usage")
   160  
   161  	var (
   162  		id    string
   163  		err   error
   164  		info  snapshots.Info
   165  		usage snapshots.Usage
   166  	)
   167  
   168  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   169  		id, info, usage, err = storage.GetInfo(ctx, key)
   170  		if err != nil {
   171  			return err
   172  		}
   173  
   174  		if info.Kind == snapshots.KindActive {
   175  			deviceName := s.getDeviceName(id)
   176  			usage.Size, err = s.pool.GetUsage(deviceName)
   177  			if err != nil {
   178  				return err
   179  			}
   180  		}
   181  
   182  		if info.Parent != "" {
   183  			// GetInfo returns total number of bytes used by a snapshot (including parent).
   184  			// So subtract parent usage in order to get delta consumed by layer itself.
   185  			_, _, parentUsage, err := storage.GetInfo(ctx, info.Parent)
   186  			if err != nil {
   187  				return err
   188  			}
   189  
   190  			usage.Size -= parentUsage.Size
   191  		}
   192  
   193  		return err
   194  	})
   195  
   196  	return usage, err
   197  }
   198  
   199  // Mounts return the list of mounts for the active or view snapshot
   200  func (s *Snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) {
   201  	log.G(ctx).WithField("key", key).Debug("mounts")
   202  
   203  	var (
   204  		snap storage.Snapshot
   205  		err  error
   206  	)
   207  
   208  	err = s.withTransaction(ctx, false, func(ctx context.Context) error {
   209  		snap, err = storage.GetSnapshot(ctx, key)
   210  		return err
   211  	})
   212  
   213  	return s.buildMounts(snap), nil
   214  }
   215  
   216  // Prepare creates thin device for an active snapshot identified by key
   217  func (s *Snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   218  	log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("prepare")
   219  
   220  	var (
   221  		mounts []mount.Mount
   222  		err    error
   223  	)
   224  
   225  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   226  		mounts, err = s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts...)
   227  		return err
   228  	})
   229  
   230  	return mounts, err
   231  }
   232  
   233  // View creates readonly thin device for the given snapshot key
   234  func (s *Snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   235  	log.G(ctx).WithFields(logrus.Fields{"key": key, "parent": parent}).Debug("view")
   236  
   237  	var (
   238  		mounts []mount.Mount
   239  		err    error
   240  	)
   241  
   242  	err = s.withTransaction(ctx, true, func(ctx context.Context) error {
   243  		mounts, err = s.createSnapshot(ctx, snapshots.KindView, key, parent, opts...)
   244  		return err
   245  	})
   246  
   247  	return mounts, err
   248  }
   249  
   250  // Commit marks an active snapshot as committed in meta store.
   251  // Block device unmount operation captures snapshot changes by itself, so no
   252  // additional actions needed within Commit operation.
   253  func (s *Snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error {
   254  	log.G(ctx).WithFields(logrus.Fields{"name": name, "key": key}).Debug("commit")
   255  
   256  	return s.withTransaction(ctx, true, func(ctx context.Context) error {
   257  		id, _, _, err := storage.GetInfo(ctx, key)
   258  		if err != nil {
   259  			return err
   260  		}
   261  
   262  		deviceName := s.getDeviceName(id)
   263  		size, err := s.pool.GetUsage(deviceName)
   264  		if err != nil {
   265  			return err
   266  		}
   267  
   268  		usage := snapshots.Usage{
   269  			Size: size,
   270  		}
   271  
   272  		_, err = storage.CommitActive(ctx, key, name, usage, opts...)
   273  		if err != nil {
   274  			return err
   275  		}
   276  
   277  		// After committed, the snapshot device will not be directly
   278  		// used anymore. We'd better deativate it to make it *invisible*
   279  		// in userspace, so that tools like LVM2 and fdisk cannot touch it,
   280  		// and avoid useless IOs on it.
   281  		//
   282  		// Before deactivation, we need to flush the outstanding IO by suspend.
   283  		// Afterward, we resume it again to prevent a race window which may cause
   284  		// a process IO hang. See the issue below for details:
   285  		//   (https://github.com/containerd/containerd/issues/4234)
   286  		err = s.pool.SuspendDevice(ctx, deviceName)
   287  		if err != nil {
   288  			return err
   289  		}
   290  
   291  		err = s.pool.ResumeDevice(ctx, deviceName)
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		return s.pool.DeactivateDevice(ctx, deviceName, false, false)
   297  	})
   298  }
   299  
   300  // Remove removes thin device and snapshot metadata by key
   301  func (s *Snapshotter) Remove(ctx context.Context, key string) error {
   302  	log.G(ctx).WithField("key", key).Debug("remove")
   303  
   304  	return s.withTransaction(ctx, true, func(ctx context.Context) error {
   305  		return s.removeDevice(ctx, key)
   306  	})
   307  }
   308  
   309  func (s *Snapshotter) removeDevice(ctx context.Context, key string) error {
   310  	snapID, _, err := storage.Remove(ctx, key)
   311  	if err != nil {
   312  		return err
   313  	}
   314  
   315  	deviceName := s.getDeviceName(snapID)
   316  	if !s.config.AsyncRemove {
   317  		if err := s.pool.RemoveDevice(ctx, deviceName); err != nil {
   318  			log.G(ctx).WithError(err).Errorf("failed to remove device")
   319  			// Tell snapshot GC continue to collect other snapshots.
   320  			// Otherwise, one snapshot collection failure will stop
   321  			// the GC, and all snapshots won't be collected even though
   322  			// having no relationship with the failed one.
   323  			return errdefs.ErrFailedPrecondition
   324  		}
   325  	} else {
   326  		// The asynchronous cleanup will do the real device remove work.
   327  		log.G(ctx).WithField("device", deviceName).Debug("async remove")
   328  		if err := s.pool.MarkDeviceState(ctx, deviceName, Removed); err != nil {
   329  			log.G(ctx).WithError(err).Errorf("failed to mark device as removed")
   330  			return err
   331  		}
   332  	}
   333  
   334  	return nil
   335  }
   336  
   337  // Walk iterates through all metadata Info for the stored snapshots and calls the provided function for each.
   338  func (s *Snapshotter) Walk(ctx context.Context, fn snapshots.WalkFunc, fs ...string) error {
   339  	log.G(ctx).Debug("walk")
   340  	return s.withTransaction(ctx, false, func(ctx context.Context) error {
   341  		return storage.WalkInfo(ctx, fn, fs...)
   342  	})
   343  }
   344  
   345  // ResetPool deactivates and deletes all thin devices in thin-pool.
   346  // Used for cleaning pool after benchmarking.
   347  func (s *Snapshotter) ResetPool(ctx context.Context) error {
   348  	names, err := s.pool.metadata.GetDeviceNames(ctx)
   349  	if err != nil {
   350  		return err
   351  	}
   352  
   353  	var result *multierror.Error
   354  	for _, name := range names {
   355  		if err := s.pool.RemoveDevice(ctx, name); err != nil {
   356  			result = multierror.Append(result, err)
   357  		}
   358  	}
   359  
   360  	return result.ErrorOrNil()
   361  }
   362  
   363  // Close releases devmapper snapshotter resources.
   364  // All subsequent Close calls will be ignored.
   365  func (s *Snapshotter) Close() error {
   366  	log.L.Debug("close")
   367  
   368  	var result *multierror.Error
   369  	s.closeOnce.Do(func() {
   370  		for _, fn := range s.cleanupFn {
   371  			if err := fn(); err != nil {
   372  				result = multierror.Append(result, err)
   373  			}
   374  		}
   375  	})
   376  
   377  	return result.ErrorOrNil()
   378  }
   379  
   380  func (s *Snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) {
   381  	snap, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	if len(snap.ParentIDs) == 0 {
   387  		deviceName := s.getDeviceName(snap.ID)
   388  		log.G(ctx).Debugf("creating new thin device '%s'", deviceName)
   389  
   390  		err := s.pool.CreateThinDevice(ctx, deviceName, s.config.BaseImageSizeBytes)
   391  		if err != nil {
   392  			log.G(ctx).WithError(err).Errorf("failed to create thin device for snapshot %s", snap.ID)
   393  			return nil, err
   394  		}
   395  
   396  		if err := s.mkfs(ctx, deviceName); err != nil {
   397  			// Rollback thin device creation if mkfs failed
   398  			return nil, multierror.Append(err,
   399  				s.pool.RemoveDevice(ctx, deviceName))
   400  		}
   401  	} else {
   402  		parentDeviceName := s.getDeviceName(snap.ParentIDs[0])
   403  		snapDeviceName := s.getDeviceName(snap.ID)
   404  		log.G(ctx).Debugf("creating snapshot device '%s' from '%s'", snapDeviceName, parentDeviceName)
   405  
   406  		err := s.pool.CreateSnapshotDevice(ctx, parentDeviceName, snapDeviceName, s.config.BaseImageSizeBytes)
   407  		if err != nil {
   408  			log.G(ctx).WithError(err).Errorf("failed to create snapshot device from parent %s", parentDeviceName)
   409  			return nil, err
   410  		}
   411  	}
   412  
   413  	mounts := s.buildMounts(snap)
   414  
   415  	// Remove default directories not expected by the container image
   416  	_ = mount.WithTempMount(ctx, mounts, func(root string) error {
   417  		return os.Remove(filepath.Join(root, "lost+found"))
   418  	})
   419  
   420  	return mounts, nil
   421  }
   422  
   423  // mkfs creates ext4 filesystem on the given devmapper device
   424  func (s *Snapshotter) mkfs(ctx context.Context, deviceName string) error {
   425  	args := []string{
   426  		"-E",
   427  		// We don't want any zeroing in advance when running mkfs on thin devices (see "man mkfs.ext4")
   428  		"nodiscard,lazy_itable_init=0,lazy_journal_init=0",
   429  		dmsetup.GetFullDevicePath(deviceName),
   430  	}
   431  
   432  	log.G(ctx).Debugf("mkfs.ext4 %s", strings.Join(args, " "))
   433  	output, err := exec.Command("mkfs.ext4", args...).CombinedOutput()
   434  	if err != nil {
   435  		log.G(ctx).WithError(err).Errorf("failed to write fs:\n%s", string(output))
   436  		return err
   437  	}
   438  
   439  	log.G(ctx).Debugf("mkfs:\n%s", string(output))
   440  	return nil
   441  }
   442  
   443  func (s *Snapshotter) getDeviceName(snapID string) string {
   444  	// Add pool name as prefix to avoid collisions with devices from other pools
   445  	return fmt.Sprintf("%s-snap-%s", s.config.PoolName, snapID)
   446  }
   447  
   448  func (s *Snapshotter) getDevicePath(snap storage.Snapshot) string {
   449  	name := s.getDeviceName(snap.ID)
   450  	return dmsetup.GetFullDevicePath(name)
   451  }
   452  
   453  func (s *Snapshotter) buildMounts(snap storage.Snapshot) []mount.Mount {
   454  	var options []string
   455  
   456  	if snap.Kind != snapshots.KindActive {
   457  		options = append(options, "ro")
   458  	}
   459  
   460  	mounts := []mount.Mount{
   461  		{
   462  			Source:  s.getDevicePath(snap),
   463  			Type:    fsTypeExt4,
   464  			Options: options,
   465  		},
   466  	}
   467  
   468  	return mounts
   469  }
   470  
   471  // withTransaction wraps fn callback with containerd's meta store transaction.
   472  // If callback returns an error or transaction is not writable, database transaction will be discarded.
   473  func (s *Snapshotter) withTransaction(ctx context.Context, writable bool, fn func(ctx context.Context) error) error {
   474  	ctx, trans, err := s.store.TransactionContext(ctx, writable)
   475  	if err != nil {
   476  		return err
   477  	}
   478  
   479  	var result *multierror.Error
   480  
   481  	err = fn(ctx)
   482  	if err != nil {
   483  		result = multierror.Append(result, err)
   484  	}
   485  
   486  	// Always rollback if transaction is not writable
   487  	if err != nil || !writable {
   488  		if terr := trans.Rollback(); terr != nil {
   489  			log.G(ctx).WithError(terr).Error("failed to rollback transaction")
   490  			result = multierror.Append(result, errors.Wrap(terr, "rollback failed"))
   491  		}
   492  	} else {
   493  		if terr := trans.Commit(); terr != nil {
   494  			log.G(ctx).WithError(terr).Error("failed to commit transaction")
   495  			result = multierror.Append(result, errors.Wrap(terr, "commit failed"))
   496  		}
   497  	}
   498  
   499  	if err := result.ErrorOrNil(); err != nil {
   500  		log.G(ctx).WithError(err).Debug("snapshotter error")
   501  
   502  		// Unwrap if just one error
   503  		if len(result.Errors) == 1 {
   504  			return result.Errors[0]
   505  		}
   506  
   507  		return err
   508  	}
   509  
   510  	return nil
   511  }
   512  
   513  func (s *Snapshotter) Cleanup(ctx context.Context) error {
   514  	log.G(ctx).Debug("cleanup")
   515  
   516  	var removedDevices []*DeviceInfo
   517  
   518  	if !s.config.AsyncRemove {
   519  		return nil
   520  	}
   521  
   522  	if err := s.pool.WalkDevices(ctx, func(info *DeviceInfo) error {
   523  		if info.State == Removed {
   524  			removedDevices = append(removedDevices, info)
   525  		}
   526  		return nil
   527  	}); err != nil {
   528  		log.G(ctx).WithError(err).Errorf("failed to query devices from metastore")
   529  		return err
   530  	}
   531  
   532  	var result *multierror.Error
   533  	for _, dev := range removedDevices {
   534  		log.G(ctx).WithField("device", dev.Name).Debug("cleanup device")
   535  		if err := s.pool.RemoveDevice(ctx, dev.Name); err != nil {
   536  			log.G(ctx).WithField("device", dev.Name).Error("failed to cleanup device")
   537  			result = multierror.Append(result, err)
   538  		} else {
   539  			log.G(ctx).WithField("device", dev.Name).Debug("cleanuped device")
   540  		}
   541  	}
   542  
   543  	return result.ErrorOrNil()
   544  }