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  }