github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/client/driver/executor/exec_linux.go (about)

     1  package executor
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"os/user"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"syscall"
    16  
    17  	"github.com/hashicorp/go-multierror"
    18  	"github.com/opencontainers/runc/libcontainer/cgroups"
    19  	cgroupFs "github.com/opencontainers/runc/libcontainer/cgroups/fs"
    20  	"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
    21  	cgroupConfig "github.com/opencontainers/runc/libcontainer/configs"
    22  
    23  	"github.com/hashicorp/nomad/client/allocdir"
    24  	"github.com/hashicorp/nomad/client/driver/spawn"
    25  	cstructs "github.com/hashicorp/nomad/client/driver/structs"
    26  	"github.com/hashicorp/nomad/nomad/structs"
    27  )
    28  
    29  var (
    30  	// A mapping of directories on the host OS to attempt to embed inside each
    31  	// task's chroot.
    32  	chrootEnv = map[string]string{
    33  		"/bin":       "/bin",
    34  		"/etc":       "/etc",
    35  		"/lib":       "/lib",
    36  		"/lib32":     "/lib32",
    37  		"/lib64":     "/lib64",
    38  		"/usr/bin":   "/usr/bin",
    39  		"/usr/lib":   "/usr/lib",
    40  		"/usr/share": "/usr/share",
    41  	}
    42  )
    43  
    44  func NewExecutor(ctx *ExecutorContext) Executor {
    45  	return NewLinuxExecutor(ctx)
    46  }
    47  
    48  func NewLinuxExecutor(ctx *ExecutorContext) Executor {
    49  	return &LinuxExecutor{ExecutorContext: ctx}
    50  }
    51  
    52  // Linux executor is designed to run on linux kernel 2.8+.
    53  type LinuxExecutor struct {
    54  	*ExecutorContext
    55  	cmd  exec.Cmd
    56  	user *user.User
    57  	l    sync.Mutex
    58  
    59  	// Isolation configurations.
    60  	groups   *cgroupConfig.Cgroup
    61  	taskName string
    62  	taskDir  string
    63  	allocDir string
    64  
    65  	// Spawn process.
    66  	spawn *spawn.Spawner
    67  }
    68  
    69  func (e *LinuxExecutor) Command() *exec.Cmd {
    70  	return &e.cmd
    71  }
    72  
    73  func (e *LinuxExecutor) Limit(resources *structs.Resources) error {
    74  	if resources == nil {
    75  		return errNoResources
    76  	}
    77  
    78  	return e.configureCgroups(resources)
    79  }
    80  
    81  // execLinuxID contains the necessary information to reattach to an executed
    82  // process and cleanup the created cgroups.
    83  type ExecLinuxID struct {
    84  	Groups  *cgroupConfig.Cgroup
    85  	Spawn   *spawn.Spawner
    86  	TaskDir string
    87  }
    88  
    89  func (e *LinuxExecutor) Open(id string) error {
    90  	// De-serialize the ID.
    91  	dec := json.NewDecoder(strings.NewReader(id))
    92  	var execID ExecLinuxID
    93  	if err := dec.Decode(&execID); err != nil {
    94  		return fmt.Errorf("Failed to parse id: %v", err)
    95  	}
    96  
    97  	// Setup the executor.
    98  	e.groups = execID.Groups
    99  	e.spawn = execID.Spawn
   100  	e.taskDir = execID.TaskDir
   101  	return e.spawn.Valid()
   102  }
   103  
   104  func (e *LinuxExecutor) ID() (string, error) {
   105  	if e.groups == nil || e.spawn == nil || e.taskDir == "" {
   106  		return "", fmt.Errorf("LinuxExecutor not properly initialized.")
   107  	}
   108  
   109  	// Build the ID.
   110  	id := ExecLinuxID{
   111  		Groups:  e.groups,
   112  		Spawn:   e.spawn,
   113  		TaskDir: e.taskDir,
   114  	}
   115  
   116  	var buffer bytes.Buffer
   117  	enc := json.NewEncoder(&buffer)
   118  	if err := enc.Encode(id); err != nil {
   119  		return "", fmt.Errorf("Failed to serialize id: %v", err)
   120  	}
   121  
   122  	return buffer.String(), nil
   123  }
   124  
   125  // runAs takes a user id as a string and looks up the user, and sets the command
   126  // to execute as that user.
   127  func (e *LinuxExecutor) runAs(userid string) error {
   128  	u, err := user.Lookup(userid)
   129  	if err != nil {
   130  		return fmt.Errorf("Failed to identify user %v: %v", userid, err)
   131  	}
   132  
   133  	// Convert the uid and gid
   134  	uid, err := strconv.ParseUint(u.Uid, 10, 32)
   135  	if err != nil {
   136  		return fmt.Errorf("Unable to convert userid to uint32: %s", err)
   137  	}
   138  	gid, err := strconv.ParseUint(u.Gid, 10, 32)
   139  	if err != nil {
   140  		return fmt.Errorf("Unable to convert groupid to uint32: %s", err)
   141  	}
   142  
   143  	// Set the command to run as that user and group.
   144  	if e.cmd.SysProcAttr == nil {
   145  		e.cmd.SysProcAttr = &syscall.SysProcAttr{}
   146  	}
   147  	if e.cmd.SysProcAttr.Credential == nil {
   148  		e.cmd.SysProcAttr.Credential = &syscall.Credential{}
   149  	}
   150  	e.cmd.SysProcAttr.Credential.Uid = uint32(uid)
   151  	e.cmd.SysProcAttr.Credential.Gid = uint32(gid)
   152  
   153  	return nil
   154  }
   155  
   156  func (e *LinuxExecutor) Start() error {
   157  	// Run as "nobody" user so we don't leak root privilege to the spawned
   158  	// process.
   159  	if err := e.runAs("nobody"); err != nil {
   160  		return err
   161  	}
   162  
   163  	// Parse the commands arguments and replace instances of Nomad environment
   164  	// variables.
   165  	e.cmd.Path = e.taskEnv.ReplaceEnv(e.cmd.Path)
   166  	e.cmd.Args = e.taskEnv.ParseAndReplace(e.cmd.Args)
   167  	e.cmd.Env = e.taskEnv.EnvList()
   168  
   169  	spawnState := filepath.Join(e.allocDir, fmt.Sprintf("%s_%s", e.taskName, "exit_status"))
   170  	e.spawn = spawn.NewSpawner(spawnState)
   171  	e.spawn.SetCommand(&e.cmd)
   172  	e.spawn.SetChroot(e.taskDir)
   173  	e.spawn.SetLogs(&spawn.Logs{
   174  		Stdout: filepath.Join(e.taskDir, allocdir.TaskLocal, fmt.Sprintf("%v.stdout", e.taskName)),
   175  		Stderr: filepath.Join(e.taskDir, allocdir.TaskLocal, fmt.Sprintf("%v.stderr", e.taskName)),
   176  		Stdin:  os.DevNull,
   177  	})
   178  
   179  	enterCgroup := func(pid int) error {
   180  		// Join the spawn-daemon to the cgroup.
   181  		manager := e.getCgroupManager(e.groups)
   182  
   183  		// Apply will place the spawn dameon into the created cgroups.
   184  		if err := manager.Apply(pid); err != nil {
   185  			return fmt.Errorf("Failed to join spawn-daemon to the cgroup (%+v): %v", e.groups, err)
   186  		}
   187  
   188  		return nil
   189  	}
   190  
   191  	return e.spawn.Spawn(enterCgroup)
   192  }
   193  
   194  // Wait waits til the user process exits and returns an error on non-zero exit
   195  // codes. Wait also cleans up the task directory and created cgroups.
   196  func (e *LinuxExecutor) Wait() *cstructs.WaitResult {
   197  	errs := new(multierror.Error)
   198  	res := e.spawn.Wait()
   199  	if res.Err != nil {
   200  		errs = multierror.Append(errs, res.Err)
   201  	}
   202  
   203  	if err := e.destroyCgroup(); err != nil {
   204  		errs = multierror.Append(errs, err)
   205  	}
   206  
   207  	if err := e.cleanTaskDir(); err != nil {
   208  		errs = multierror.Append(errs, err)
   209  	}
   210  
   211  	res.Err = errs.ErrorOrNil()
   212  	return res
   213  }
   214  
   215  // Shutdown sends the user process an interrupt signal indicating that it is
   216  // about to be forcefully shutdown in sometime
   217  func (e *LinuxExecutor) Shutdown() error {
   218  	proc, err := os.FindProcess(e.spawn.UserPid)
   219  	if err != nil {
   220  		return fmt.Errorf("Failed to find user processes %v: %v", e.spawn.UserPid, err)
   221  	}
   222  
   223  	return proc.Signal(os.Interrupt)
   224  }
   225  
   226  // ForceStop immediately exits the user process and cleans up both the task
   227  // directory and the cgroups.
   228  func (e *LinuxExecutor) ForceStop() error {
   229  	errs := new(multierror.Error)
   230  	if err := e.destroyCgroup(); err != nil {
   231  		errs = multierror.Append(errs, err)
   232  	}
   233  
   234  	if err := e.cleanTaskDir(); err != nil {
   235  		errs = multierror.Append(errs, err)
   236  	}
   237  
   238  	return errs.ErrorOrNil()
   239  }
   240  
   241  // Task Directory related functions.
   242  
   243  // ConfigureTaskDir creates the necessary directory structure for a proper
   244  // chroot. cleanTaskDir should be called after.
   245  func (e *LinuxExecutor) ConfigureTaskDir(taskName string, alloc *allocdir.AllocDir) error {
   246  	e.taskName = taskName
   247  	e.allocDir = alloc.AllocDir
   248  
   249  	taskDir, ok := alloc.TaskDirs[taskName]
   250  	if !ok {
   251  		fmt.Errorf("Couldn't find task directory for task %v", taskName)
   252  	}
   253  	e.taskDir = taskDir
   254  
   255  	if err := alloc.MountSharedDir(taskName); err != nil {
   256  		return err
   257  	}
   258  
   259  	if err := alloc.Embed(taskName, chrootEnv); err != nil {
   260  		return err
   261  	}
   262  
   263  	// Mount dev
   264  	dev := filepath.Join(taskDir, "dev")
   265  	if !e.pathExists(dev) {
   266  		if err := os.Mkdir(dev, 0777); err != nil {
   267  			return fmt.Errorf("Mkdir(%v) failed: %v", dev, err)
   268  		}
   269  
   270  		if err := syscall.Mount("none", dev, "devtmpfs", syscall.MS_RDONLY, ""); err != nil {
   271  			return fmt.Errorf("Couldn't mount /dev to %v: %v", dev, err)
   272  		}
   273  	}
   274  
   275  	// Mount proc
   276  	proc := filepath.Join(taskDir, "proc")
   277  	if !e.pathExists(proc) {
   278  		if err := os.Mkdir(proc, 0777); err != nil {
   279  			return fmt.Errorf("Mkdir(%v) failed: %v", proc, err)
   280  		}
   281  
   282  		if err := syscall.Mount("none", proc, "proc", syscall.MS_RDONLY, ""); err != nil {
   283  			return fmt.Errorf("Couldn't mount /proc to %v: %v", proc, err)
   284  		}
   285  	}
   286  
   287  	// Set the tasks AllocDir environment variable.
   288  	e.taskEnv.SetAllocDir(filepath.Join("/", allocdir.SharedAllocName)).SetTaskLocalDir(filepath.Join("/", allocdir.TaskLocal)).Build()
   289  	return nil
   290  }
   291  
   292  // pathExists is a helper function to check if the path exists.
   293  func (e *LinuxExecutor) pathExists(path string) bool {
   294  	if _, err := os.Stat(path); err != nil {
   295  		if os.IsNotExist(err) {
   296  			return false
   297  		}
   298  	}
   299  	return true
   300  }
   301  
   302  // cleanTaskDir is an idempotent operation to clean the task directory and
   303  // should be called when tearing down the task.
   304  func (e *LinuxExecutor) cleanTaskDir() error {
   305  	// Prevent a race between Wait/ForceStop
   306  	e.l.Lock()
   307  	defer e.l.Unlock()
   308  
   309  	// Unmount dev.
   310  	errs := new(multierror.Error)
   311  	dev := filepath.Join(e.taskDir, "dev")
   312  	if e.pathExists(dev) {
   313  		if err := syscall.Unmount(dev, 0); err != nil {
   314  			errs = multierror.Append(errs, fmt.Errorf("Failed to unmount dev (%v): %v", dev, err))
   315  		}
   316  
   317  		if err := os.RemoveAll(dev); err != nil {
   318  			errs = multierror.Append(errs, fmt.Errorf("Failed to delete dev directory (%v): %v", dev, err))
   319  		}
   320  	}
   321  
   322  	// Unmount proc.
   323  	proc := filepath.Join(e.taskDir, "proc")
   324  	if e.pathExists(proc) {
   325  		if err := syscall.Unmount(proc, 0); err != nil {
   326  			errs = multierror.Append(errs, fmt.Errorf("Failed to unmount proc (%v): %v", proc, err))
   327  		}
   328  
   329  		if err := os.RemoveAll(proc); err != nil {
   330  			errs = multierror.Append(errs, fmt.Errorf("Failed to delete proc directory (%v): %v", dev, err))
   331  		}
   332  	}
   333  
   334  	return errs.ErrorOrNil()
   335  }
   336  
   337  // Cgroup related functions.
   338  
   339  // configureCgroups converts a Nomad Resources specification into the equivalent
   340  // cgroup configuration. It returns an error if the resources are invalid.
   341  func (e *LinuxExecutor) configureCgroups(resources *structs.Resources) error {
   342  	e.groups = &cgroupConfig.Cgroup{}
   343  	e.groups.Resources = &cgroupConfig.Resources{}
   344  	e.groups.Name = structs.GenerateUUID()
   345  
   346  	// TODO: verify this is needed for things like network access
   347  	e.groups.Resources.AllowAllDevices = true
   348  
   349  	if resources.MemoryMB > 0 {
   350  		// Total amount of memory allowed to consume
   351  		e.groups.Resources.Memory = int64(resources.MemoryMB * 1024 * 1024)
   352  		// Disable swap to avoid issues on the machine
   353  		e.groups.Resources.MemorySwap = int64(-1)
   354  	}
   355  
   356  	if resources.CPU < 2 {
   357  		return fmt.Errorf("resources.CPU must be equal to or greater than 2: %v", resources.CPU)
   358  	}
   359  
   360  	// Set the relative CPU shares for this cgroup.
   361  	e.groups.Resources.CpuShares = int64(resources.CPU)
   362  
   363  	if resources.IOPS != 0 {
   364  		// Validate it is in an acceptable range.
   365  		if resources.IOPS < 10 || resources.IOPS > 1000 {
   366  			return fmt.Errorf("resources.IOPS must be between 10 and 1000: %d", resources.IOPS)
   367  		}
   368  
   369  		e.groups.Resources.BlkioWeight = uint16(resources.IOPS)
   370  	}
   371  
   372  	return nil
   373  }
   374  
   375  // destroyCgroup kills all processes in the cgroup and removes the cgroup
   376  // configuration from the host.
   377  func (e *LinuxExecutor) destroyCgroup() error {
   378  	if e.groups == nil {
   379  		return errors.New("Can't destroy: cgroup configuration empty")
   380  	}
   381  
   382  	// Prevent a race between Wait/ForceStop
   383  	e.l.Lock()
   384  	defer e.l.Unlock()
   385  
   386  	manager := e.getCgroupManager(e.groups)
   387  	pids, err := manager.GetPids()
   388  	if err != nil {
   389  		return fmt.Errorf("Failed to get pids in the cgroup %v: %v", e.groups.Name, err)
   390  	}
   391  
   392  	errs := new(multierror.Error)
   393  	for _, pid := range pids {
   394  		process, err := os.FindProcess(pid)
   395  		if err != nil {
   396  			multierror.Append(errs, fmt.Errorf("Failed to find Pid %v: %v", pid, err))
   397  			continue
   398  		}
   399  
   400  		if err := process.Kill(); err != nil && err.Error() != "os: process already finished" {
   401  			multierror.Append(errs, fmt.Errorf("Failed to kill Pid %v: %v", pid, err))
   402  			continue
   403  		}
   404  	}
   405  
   406  	// Remove the cgroup.
   407  	if err := manager.Destroy(); err != nil {
   408  		multierror.Append(errs, fmt.Errorf("Failed to delete the cgroup directories: %v", err))
   409  	}
   410  
   411  	if len(errs.Errors) != 0 {
   412  		return fmt.Errorf("Failed to destroy cgroup: %v", errs)
   413  	}
   414  
   415  	return nil
   416  }
   417  
   418  // getCgroupManager returns the correct libcontainer cgroup manager.
   419  func (e *LinuxExecutor) getCgroupManager(groups *cgroupConfig.Cgroup) cgroups.Manager {
   420  	var manager cgroups.Manager
   421  	manager = &cgroupFs.Manager{Cgroups: groups}
   422  	if systemd.UseSystemd() {
   423  		manager = &systemd.Manager{Cgroups: groups}
   424  	}
   425  	return manager
   426  }