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 }