github.com/mattyr/nomad@v0.3.3-0.20160919021406-3485a065154a/client/allocdir/alloc_dir.go (about) 1 package allocdir 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "log" 8 "math" 9 "os" 10 "path/filepath" 11 "sync" 12 "time" 13 14 "gopkg.in/tomb.v1" 15 16 "github.com/hashicorp/go-multierror" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/hpcloud/tail/watch" 19 ) 20 21 const ( 22 // The minimum frequency to use for disk monitoring. 23 minCheckDiskInterval = 3 * time.Minute 24 25 // The maximum frequency to use for disk monitoring. 26 maxCheckDiskInterval = 15 * time.Second 27 28 // The amount of time that maxCheckDiskInterval is always used after 29 // starting the allocation. This prevents unbounded disk usage that would 30 // otherwise be possible for a number of minutes if we started with the 31 // minCheckDiskInterval. 32 checkDiskMaxEnforcePeriod = 5 * time.Minute 33 ) 34 35 var ( 36 // The name of the directory that is shared across tasks in a task group. 37 SharedAllocName = "alloc" 38 39 // Name of the directory where logs of Tasks are written 40 LogDirName = "logs" 41 42 // The set of directories that exist inside eache shared alloc directory. 43 SharedAllocDirs = []string{LogDirName, "tmp", "data"} 44 45 // The name of the directory that exists inside each task directory 46 // regardless of driver. 47 TaskLocal = "local" 48 49 // TaskSecrets is the the name of the secret directory inside each task 50 // directory 51 TaskSecrets = "secrets" 52 53 // TaskDirs is the set of directories created in each tasks directory. 54 TaskDirs = []string{"tmp"} 55 ) 56 57 type AllocDir struct { 58 // AllocDir is the directory used for storing any state 59 // of this allocation. It will be purged on alloc destroy. 60 AllocDir string 61 62 // The shared directory is available to all tasks within the same task 63 // group. 64 SharedDir string 65 66 // TaskDirs is a mapping of task names to their non-shared directory. 67 TaskDirs map[string]string 68 69 // Size is the total consumed disk size of the shared directory in bytes 70 size int64 71 sizeLock sync.RWMutex 72 73 // The minimum frequency to use for disk monitoring. 74 MinCheckDiskInterval time.Duration 75 76 // The maximum frequency to use for disk monitoring. 77 MaxCheckDiskInterval time.Duration 78 79 // The amount of time that maxCheckDiskInterval is always used after 80 // starting the allocation. This prevents unbounded disk usage that would 81 // otherwise be possible for a number of minutes if we started with the 82 // minCheckDiskInterval. 83 CheckDiskMaxEnforcePeriod time.Duration 84 85 // running reflects the state of the disk watcher process. 86 running bool 87 88 // watchCh signals that the alloc directory is being torn down and that 89 // any monitoring on it should stop. 90 watchCh chan struct{} 91 92 // MaxSize represents the total amount of megabytes that the shared allocation 93 // directory is allowed to consume. 94 MaxSize int 95 } 96 97 // AllocFileInfo holds information about a file inside the AllocDir 98 type AllocFileInfo struct { 99 Name string 100 IsDir bool 101 Size int64 102 FileMode string 103 ModTime time.Time 104 } 105 106 // AllocDirFS exposes file operations on the alloc dir 107 type AllocDirFS interface { 108 List(path string) ([]*AllocFileInfo, error) 109 Stat(path string) (*AllocFileInfo, error) 110 ReadAt(path string, offset int64) (io.ReadCloser, error) 111 BlockUntilExists(path string, t *tomb.Tomb) chan error 112 ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error) 113 } 114 115 // NewAllocDir initializes the AllocDir struct with allocDir as base path for 116 // the allocation directory and maxSize as the maximum allowed size in megabytes. 117 func NewAllocDir(allocDir string, maxSize int) *AllocDir { 118 d := &AllocDir{ 119 AllocDir: allocDir, 120 MaxCheckDiskInterval: maxCheckDiskInterval, 121 MinCheckDiskInterval: minCheckDiskInterval, 122 CheckDiskMaxEnforcePeriod: checkDiskMaxEnforcePeriod, 123 TaskDirs: make(map[string]string), 124 MaxSize: maxSize, 125 } 126 d.SharedDir = filepath.Join(d.AllocDir, SharedAllocName) 127 return d 128 } 129 130 // Tears down previously build directory structure. 131 func (d *AllocDir) Destroy() error { 132 133 // Unmount all mounted shared alloc dirs. 134 var mErr multierror.Error 135 if err := d.UnmountAll(); err != nil { 136 mErr.Errors = append(mErr.Errors, err) 137 } 138 139 if err := os.RemoveAll(d.AllocDir); err != nil { 140 mErr.Errors = append(mErr.Errors, err) 141 } 142 143 return mErr.ErrorOrNil() 144 } 145 146 func (d *AllocDir) UnmountAll() error { 147 var mErr multierror.Error 148 for _, dir := range d.TaskDirs { 149 // Check if the directory has the shared alloc mounted. 150 taskAlloc := filepath.Join(dir, SharedAllocName) 151 if d.pathExists(taskAlloc) { 152 if err := d.unmountSharedDir(taskAlloc); err != nil { 153 mErr.Errors = append(mErr.Errors, 154 fmt.Errorf("failed to unmount shared alloc dir %q: %v", taskAlloc, err)) 155 } else if err := os.RemoveAll(taskAlloc); err != nil { 156 mErr.Errors = append(mErr.Errors, 157 fmt.Errorf("failed to delete shared alloc dir %q: %v", taskAlloc, err)) 158 } 159 } 160 161 taskSecret := filepath.Join(dir, TaskSecrets) 162 if d.pathExists(taskSecret) { 163 if err := d.removeSecretDir(taskSecret); err != nil { 164 mErr.Errors = append(mErr.Errors, 165 fmt.Errorf("failed to remove the secret dir %q: %v", taskSecret, err)) 166 } 167 } 168 169 // Unmount dev/ and proc/ have been mounted. 170 d.unmountSpecialDirs(dir) 171 } 172 173 return mErr.ErrorOrNil() 174 } 175 176 // Given a list of a task build the correct alloc structure. 177 func (d *AllocDir) Build(tasks []*structs.Task) error { 178 // Make the alloc directory, owned by the nomad process. 179 if err := os.MkdirAll(d.AllocDir, 0755); err != nil { 180 return fmt.Errorf("Failed to make the alloc directory %v: %v", d.AllocDir, err) 181 } 182 183 // Make the shared directory and make it available to all user/groups. 184 if err := os.MkdirAll(d.SharedDir, 0777); err != nil { 185 return err 186 } 187 188 // Make the shared directory have non-root permissions. 189 if err := d.dropDirPermissions(d.SharedDir); err != nil { 190 return err 191 } 192 193 for _, dir := range SharedAllocDirs { 194 p := filepath.Join(d.SharedDir, dir) 195 if err := os.MkdirAll(p, 0777); err != nil { 196 return err 197 } 198 if err := d.dropDirPermissions(p); err != nil { 199 return err 200 } 201 } 202 203 // Make the task directories. 204 for _, t := range tasks { 205 taskDir := filepath.Join(d.AllocDir, t.Name) 206 if err := os.MkdirAll(taskDir, 0777); err != nil { 207 return err 208 } 209 210 // Make the task directory have non-root permissions. 211 if err := d.dropDirPermissions(taskDir); err != nil { 212 return err 213 } 214 215 // Create a local directory that each task can use. 216 local := filepath.Join(taskDir, TaskLocal) 217 if err := os.MkdirAll(local, 0777); err != nil { 218 return err 219 } 220 221 if err := d.dropDirPermissions(local); err != nil { 222 return err 223 } 224 225 d.TaskDirs[t.Name] = taskDir 226 227 // Create the directories that should be in every task. 228 for _, dir := range TaskDirs { 229 local := filepath.Join(taskDir, dir) 230 if err := os.MkdirAll(local, 0777); err != nil { 231 return err 232 } 233 234 if err := d.dropDirPermissions(local); err != nil { 235 return err 236 } 237 } 238 239 // Create the secret directory 240 secret := filepath.Join(taskDir, TaskSecrets) 241 if err := d.createSecretDir(secret); err != nil { 242 return err 243 } 244 245 if err := d.dropDirPermissions(secret); err != nil { 246 return err 247 } 248 } 249 250 return nil 251 } 252 253 // Embed takes a mapping of absolute directory or file paths on the host to 254 // their intended, relative location within the task directory. Embed attempts 255 // hardlink and then defaults to copying. If the path exists on the host and 256 // can't be embedded an error is returned. 257 func (d *AllocDir) Embed(task string, entries map[string]string) error { 258 taskdir, ok := d.TaskDirs[task] 259 if !ok { 260 return fmt.Errorf("Task directory doesn't exist for task %v", task) 261 } 262 263 subdirs := make(map[string]string) 264 for source, dest := range entries { 265 // Check to see if directory exists on host. 266 s, err := os.Stat(source) 267 if os.IsNotExist(err) { 268 continue 269 } 270 271 // Embedding a single file 272 if !s.IsDir() { 273 destDir := filepath.Join(taskdir, filepath.Dir(dest)) 274 if err := os.MkdirAll(destDir, s.Mode().Perm()); err != nil { 275 return fmt.Errorf("Couldn't create destination directory %v: %v", destDir, err) 276 } 277 278 // Copy the file. 279 taskEntry := filepath.Join(destDir, filepath.Base(dest)) 280 if err := d.linkOrCopy(source, taskEntry, s.Mode().Perm()); err != nil { 281 return err 282 } 283 284 continue 285 } 286 287 // Create destination directory. 288 destDir := filepath.Join(taskdir, dest) 289 if err := os.MkdirAll(destDir, s.Mode().Perm()); err != nil { 290 return fmt.Errorf("Couldn't create destination directory %v: %v", destDir, err) 291 } 292 293 // Enumerate the files in source. 294 dirEntries, err := ioutil.ReadDir(source) 295 if err != nil { 296 return fmt.Errorf("Couldn't read directory %v: %v", source, err) 297 } 298 299 for _, entry := range dirEntries { 300 hostEntry := filepath.Join(source, entry.Name()) 301 taskEntry := filepath.Join(destDir, filepath.Base(hostEntry)) 302 if entry.IsDir() { 303 subdirs[hostEntry] = filepath.Join(dest, filepath.Base(hostEntry)) 304 continue 305 } 306 307 // Check if entry exists. This can happen if restarting a failed 308 // task. 309 if _, err := os.Lstat(taskEntry); err == nil { 310 continue 311 } 312 313 if !entry.Mode().IsRegular() { 314 // If it is a symlink we can create it, otherwise we skip it. 315 if entry.Mode()&os.ModeSymlink == 0 { 316 continue 317 } 318 319 link, err := os.Readlink(hostEntry) 320 if err != nil { 321 return fmt.Errorf("Couldn't resolve symlink for %v: %v", source, err) 322 } 323 324 if err := os.Symlink(link, taskEntry); err != nil { 325 // Symlinking twice 326 if err.(*os.LinkError).Err.Error() != "file exists" { 327 return fmt.Errorf("Couldn't create symlink: %v", err) 328 } 329 } 330 continue 331 } 332 333 if err := d.linkOrCopy(hostEntry, taskEntry, entry.Mode().Perm()); err != nil { 334 return err 335 } 336 } 337 } 338 339 // Recurse on self to copy subdirectories. 340 if len(subdirs) != 0 { 341 return d.Embed(task, subdirs) 342 } 343 344 return nil 345 } 346 347 // MountSharedDir mounts the shared directory into the specified task's 348 // directory. Mount is documented at an OS level in their respective 349 // implementation files. 350 func (d *AllocDir) MountSharedDir(task string) error { 351 taskDir, ok := d.TaskDirs[task] 352 if !ok { 353 return fmt.Errorf("No task directory exists for %v", task) 354 } 355 356 taskLoc := filepath.Join(taskDir, SharedAllocName) 357 if err := d.mountSharedDir(taskLoc); err != nil { 358 return fmt.Errorf("Failed to mount shared directory for task %v: %v", task, err) 359 } 360 361 return nil 362 } 363 364 // LogDir returns the log dir in the current allocation directory 365 func (d *AllocDir) LogDir() string { 366 return filepath.Join(d.AllocDir, SharedAllocName, LogDirName) 367 } 368 369 // List returns the list of files at a path relative to the alloc dir 370 func (d *AllocDir) List(path string) ([]*AllocFileInfo, error) { 371 p := filepath.Join(d.AllocDir, path) 372 finfos, err := ioutil.ReadDir(p) 373 if err != nil { 374 return []*AllocFileInfo{}, err 375 } 376 files := make([]*AllocFileInfo, len(finfos)) 377 for idx, info := range finfos { 378 files[idx] = &AllocFileInfo{ 379 Name: info.Name(), 380 IsDir: info.IsDir(), 381 Size: info.Size(), 382 FileMode: info.Mode().String(), 383 ModTime: info.ModTime(), 384 } 385 } 386 return files, err 387 } 388 389 // Stat returns information about the file at a path relative to the alloc dir 390 func (d *AllocDir) Stat(path string) (*AllocFileInfo, error) { 391 p := filepath.Join(d.AllocDir, path) 392 info, err := os.Stat(p) 393 if err != nil { 394 return nil, err 395 } 396 397 return &AllocFileInfo{ 398 Size: info.Size(), 399 Name: info.Name(), 400 IsDir: info.IsDir(), 401 FileMode: info.Mode().String(), 402 ModTime: info.ModTime(), 403 }, nil 404 } 405 406 // ReadAt returns a reader for a file at the path relative to the alloc dir 407 func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) { 408 p := filepath.Join(d.AllocDir, path) 409 f, err := os.Open(p) 410 if err != nil { 411 return nil, err 412 } 413 if _, err := f.Seek(offset, 0); err != nil { 414 return nil, fmt.Errorf("can't seek to offset %q: %v", offset, err) 415 } 416 return f, nil 417 } 418 419 // BlockUntilExists blocks until the passed file relative the allocation 420 // directory exists. The block can be cancelled with the passed tomb. 421 func (d *AllocDir) BlockUntilExists(path string, t *tomb.Tomb) chan error { 422 // Get the path relative to the alloc directory 423 p := filepath.Join(d.AllocDir, path) 424 watcher := getFileWatcher(p) 425 returnCh := make(chan error, 1) 426 go func() { 427 returnCh <- watcher.BlockUntilExists(t) 428 close(returnCh) 429 }() 430 return returnCh 431 } 432 433 // ChangeEvents watches for changes to the passed path relative to the 434 // allocation directory. The offset should be the last read offset. The tomb is 435 // used to clean up the watch. 436 func (d *AllocDir) ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error) { 437 // Get the path relative to the alloc directory 438 p := filepath.Join(d.AllocDir, path) 439 watcher := getFileWatcher(p) 440 return watcher.ChangeEvents(t, curOffset) 441 } 442 443 // getFileWatcher returns a FileWatcher for the given path. 444 func getFileWatcher(path string) watch.FileWatcher { 445 return watch.NewPollingFileWatcher(path) 446 } 447 448 func fileCopy(src, dst string, perm os.FileMode) error { 449 // Do a simple copy. 450 srcFile, err := os.Open(src) 451 if err != nil { 452 return fmt.Errorf("Couldn't open src file %v: %v", src, err) 453 } 454 455 dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, perm) 456 if err != nil { 457 return fmt.Errorf("Couldn't create destination file %v: %v", dst, err) 458 } 459 460 if _, err := io.Copy(dstFile, srcFile); err != nil { 461 return fmt.Errorf("Couldn't copy %v to %v: %v", src, dst, err) 462 } 463 464 return nil 465 } 466 467 // pathExists is a helper function to check if the path exists. 468 func (d *AllocDir) pathExists(path string) bool { 469 if _, err := os.Stat(path); err != nil { 470 if os.IsNotExist(err) { 471 return false 472 } 473 } 474 return true 475 } 476 477 // GetSize returns the size of the shared allocation directory. 478 func (d *AllocDir) GetSize() int64 { 479 d.sizeLock.Lock() 480 defer d.sizeLock.Unlock() 481 482 return d.size 483 } 484 485 // setSize sets the size of the shared allocation directory. 486 func (d *AllocDir) setSize(size int64) { 487 d.sizeLock.Lock() 488 defer d.sizeLock.Unlock() 489 490 d.size = size 491 } 492 493 // StartDiskWatcher periodically checks the disk space consumed by the shared 494 // allocation directory. 495 func (d *AllocDir) StartDiskWatcher() { 496 start := time.Now() 497 498 sync := time.NewTimer(d.MaxCheckDiskInterval) 499 defer sync.Stop() 500 501 d.running = true 502 d.watchCh = make(chan struct{}) 503 504 for { 505 select { 506 case <-d.watchCh: 507 return 508 case <-sync.C: 509 if err := d.syncDiskUsage(); err != nil { 510 log.Printf("[WARN] client: failed to sync disk usage: %v", err) 511 } 512 // Calculate the disk ratio. 513 diskRatio := float64(d.size) / float64(d.MaxSize*structs.BytesInMegabyte) 514 515 // Exponentially decrease the interval when the disk ratio increases. 516 nextInterval := time.Duration(int64(1.0/(0.1*math.Pow(diskRatio, 2))+5)) * time.Second 517 518 // Use the maximum interval for the first five minutes or if the 519 // disk ratio is sufficiently high. Also use the minimum check interval 520 // if the disk ratio becomes low enough. 521 if nextInterval < d.MaxCheckDiskInterval || time.Since(start) < d.CheckDiskMaxEnforcePeriod { 522 nextInterval = d.MaxCheckDiskInterval 523 } else if nextInterval > d.MinCheckDiskInterval { 524 nextInterval = d.MinCheckDiskInterval 525 } 526 sync.Reset(nextInterval) 527 } 528 } 529 } 530 531 // StopDiskWatcher closes the watch channel which causes the disk monitoring to stop. 532 func (d *AllocDir) StopDiskWatcher() { 533 if d.running { 534 d.running = false 535 close(d.watchCh) 536 } 537 } 538 539 // syncDiskUsage walks the allocation directory recursively and 540 // calculates the total consumed disk space. 541 func (d *AllocDir) syncDiskUsage() error { 542 var size int64 543 err := filepath.Walk(d.AllocDir, 544 func(path string, info os.FileInfo, err error) error { 545 // Ignore paths that do not have a valid FileInfo object 546 if err == nil { 547 size += info.Size() 548 } 549 return nil 550 }) 551 // Store the disk consumption 552 d.setSize(size) 553 return err 554 }