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 }