github.com/ranjib/nomad@v0.1.1-0.20160225204057-97751b02f70b/client/driver/executor/executor.go (about)

     1  package executor
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  	"sync"
    12  	"syscall"
    13  	"time"
    14  
    15  	"github.com/hashicorp/go-multierror"
    16  	cgroupConfig "github.com/opencontainers/runc/libcontainer/configs"
    17  
    18  	"github.com/hashicorp/nomad/client/allocdir"
    19  	"github.com/hashicorp/nomad/client/driver/env"
    20  	"github.com/hashicorp/nomad/client/driver/logging"
    21  	cstructs "github.com/hashicorp/nomad/client/driver/structs"
    22  	"github.com/hashicorp/nomad/nomad/structs"
    23  )
    24  
    25  // ExecutorContext holds context to configure the command user
    26  // wants to run and isolate it
    27  type ExecutorContext struct {
    28  	// TaskEnv holds information about the environment of a Task
    29  	TaskEnv *env.TaskEnvironment
    30  
    31  	// AllocDir is the handle to do operations on the alloc dir of
    32  	// the task
    33  	AllocDir *allocdir.AllocDir
    34  
    35  	// TaskName is the name of the Task
    36  	TaskName string
    37  
    38  	// TaskResources are the resource constraints for the Task
    39  	TaskResources *structs.Resources
    40  
    41  	// FSIsolation is a flag for drivers to impose file system
    42  	// isolation on certain platforms
    43  	FSIsolation bool
    44  
    45  	// ResourceLimits is a flag for drivers to impose resource
    46  	// contraints on a Task on certain platforms
    47  	ResourceLimits bool
    48  
    49  	// UnprivilegedUser is a flag for drivers to make the process
    50  	// run as nobody
    51  	UnprivilegedUser bool
    52  
    53  	// LogConfig provides the configuration related to log rotation
    54  	LogConfig *structs.LogConfig
    55  }
    56  
    57  // ExecCommand holds the user command and args. It's a lightweight replacement
    58  // of exec.Cmd for serialization purposes.
    59  type ExecCommand struct {
    60  	Cmd  string
    61  	Args []string
    62  }
    63  
    64  // ProcessState holds information about the state of a user process.
    65  type ProcessState struct {
    66  	Pid             int
    67  	ExitCode        int
    68  	Signal          int
    69  	IsolationConfig *cstructs.IsolationConfig
    70  	Time            time.Time
    71  }
    72  
    73  // Executor is the interface which allows a driver to launch and supervise
    74  // a process
    75  type Executor interface {
    76  	LaunchCmd(command *ExecCommand, ctx *ExecutorContext) (*ProcessState, error)
    77  	Wait() (*ProcessState, error)
    78  	ShutDown() error
    79  	Exit() error
    80  	UpdateLogConfig(logConfig *structs.LogConfig) error
    81  }
    82  
    83  // UniversalExecutor is an implementation of the Executor which launches and
    84  // supervises processes. In addition to process supervision it provides resource
    85  // and file system isolation
    86  type UniversalExecutor struct {
    87  	cmd exec.Cmd
    88  	ctx *ExecutorContext
    89  
    90  	taskDir       string
    91  	groups        *cgroupConfig.Cgroup
    92  	exitState     *ProcessState
    93  	processExited chan interface{}
    94  	lre           *logging.FileRotator
    95  	lro           *logging.FileRotator
    96  
    97  	logger *log.Logger
    98  	lock   sync.Mutex
    99  }
   100  
   101  // NewExecutor returns an Executor
   102  func NewExecutor(logger *log.Logger) Executor {
   103  	return &UniversalExecutor{logger: logger, processExited: make(chan interface{})}
   104  }
   105  
   106  // LaunchCmd launches a process and returns it's state. It also configures an
   107  // applies isolation on certain platforms.
   108  func (e *UniversalExecutor) LaunchCmd(command *ExecCommand, ctx *ExecutorContext) (*ProcessState, error) {
   109  	e.logger.Printf("[DEBUG] executor: launching command %v %v", command.Cmd, strings.Join(command.Args, " "))
   110  
   111  	e.ctx = ctx
   112  
   113  	// configuring the task dir
   114  	if err := e.configureTaskDir(); err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// configuring the chroot, cgroup and enters the plugin process in the
   119  	// chroot
   120  	if err := e.configureIsolation(); err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	// setting the user of the process
   125  	if e.ctx.UnprivilegedUser {
   126  		if err := e.runAs("nobody"); err != nil {
   127  			return nil, err
   128  		}
   129  	}
   130  
   131  	logFileSize := int64(ctx.LogConfig.MaxFileSizeMB * 1024 * 1024)
   132  	lro, err := logging.NewFileRotator(ctx.AllocDir.LogDir(), fmt.Sprintf("%v.stdout", ctx.TaskName),
   133  		ctx.LogConfig.MaxFiles, logFileSize, e.logger)
   134  
   135  	if err != nil {
   136  		return nil, fmt.Errorf("error creating log rotator for stdout of task %v", err)
   137  	}
   138  	e.cmd.Stdout = lro
   139  	e.lro = lro
   140  
   141  	lre, err := logging.NewFileRotator(ctx.AllocDir.LogDir(), fmt.Sprintf("%v.stderr", ctx.TaskName),
   142  		ctx.LogConfig.MaxFiles, logFileSize, e.logger)
   143  	if err != nil {
   144  		return nil, fmt.Errorf("error creating log rotator for stderr of task %v", err)
   145  	}
   146  	e.cmd.Stderr = lre
   147  	e.lre = lre
   148  
   149  	// setting the env, path and args for the command
   150  	e.ctx.TaskEnv.Build()
   151  	e.cmd.Env = ctx.TaskEnv.EnvList()
   152  	e.cmd.Path = ctx.TaskEnv.ReplaceEnv(command.Cmd)
   153  	e.cmd.Args = append([]string{e.cmd.Path}, ctx.TaskEnv.ParseAndReplace(command.Args)...)
   154  	if filepath.Base(command.Cmd) == command.Cmd {
   155  		if lp, err := exec.LookPath(command.Cmd); err != nil {
   156  		} else {
   157  			e.cmd.Path = lp
   158  		}
   159  	}
   160  
   161  	// starting the process
   162  	if err := e.cmd.Start(); err != nil {
   163  		return nil, fmt.Errorf("error starting command: %v", err)
   164  	}
   165  	go e.wait()
   166  	ic := &cstructs.IsolationConfig{Cgroup: e.groups}
   167  	return &ProcessState{Pid: e.cmd.Process.Pid, ExitCode: -1, IsolationConfig: ic, Time: time.Now()}, nil
   168  }
   169  
   170  // Wait waits until a process has exited and returns it's exitcode and errors
   171  func (e *UniversalExecutor) Wait() (*ProcessState, error) {
   172  	<-e.processExited
   173  	return e.exitState, nil
   174  }
   175  
   176  // UpdateLogConfig updates the log configuration
   177  func (e *UniversalExecutor) UpdateLogConfig(logConfig *structs.LogConfig) error {
   178  	e.ctx.LogConfig = logConfig
   179  	if e.lro == nil {
   180  		return fmt.Errorf("log rotator for stdout doesn't exist")
   181  	}
   182  	e.lro.MaxFiles = logConfig.MaxFiles
   183  	e.lro.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
   184  
   185  	if e.lre == nil {
   186  		return fmt.Errorf("log rotator for stderr doesn't exist")
   187  	}
   188  	e.lre.MaxFiles = logConfig.MaxFiles
   189  	e.lre.FileSize = int64(logConfig.MaxFileSizeMB * 1024 * 1024)
   190  	return nil
   191  }
   192  
   193  func (e *UniversalExecutor) wait() {
   194  	defer close(e.processExited)
   195  	err := e.cmd.Wait()
   196  	e.lre.Close()
   197  	e.lro.Close()
   198  	if err == nil {
   199  		e.exitState = &ProcessState{Pid: 0, ExitCode: 0, Time: time.Now()}
   200  		return
   201  	}
   202  	exitCode := 1
   203  	if exitErr, ok := err.(*exec.ExitError); ok {
   204  		if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
   205  			exitCode = status.ExitStatus()
   206  		}
   207  	}
   208  	if e.ctx.FSIsolation {
   209  		e.removeChrootMounts()
   210  	}
   211  	if e.ctx.ResourceLimits {
   212  		e.lock.Lock()
   213  		DestroyCgroup(e.groups)
   214  		e.lock.Unlock()
   215  	}
   216  	e.exitState = &ProcessState{Pid: 0, ExitCode: exitCode, Time: time.Now()}
   217  }
   218  
   219  var (
   220  	// finishedErr is the error message received when trying to kill and already
   221  	// exited process.
   222  	finishedErr = "os: process already finished"
   223  )
   224  
   225  // Exit cleans up the alloc directory, destroys cgroups and kills the user
   226  // process
   227  func (e *UniversalExecutor) Exit() error {
   228  	var merr multierror.Error
   229  	if e.cmd.Process != nil {
   230  		proc, err := os.FindProcess(e.cmd.Process.Pid)
   231  		if err != nil {
   232  			e.logger.Printf("[ERR] executor: can't find process with pid: %v, err: %v",
   233  				e.cmd.Process.Pid, err)
   234  		} else if err := proc.Kill(); err != nil && err.Error() != finishedErr {
   235  			merr.Errors = append(merr.Errors,
   236  				fmt.Errorf("can't kill process with pid: %v, err: %v", e.cmd.Process.Pid, err))
   237  		}
   238  	}
   239  
   240  	if e.ctx.FSIsolation {
   241  		if err := e.removeChrootMounts(); err != nil {
   242  			merr.Errors = append(merr.Errors, err)
   243  		}
   244  	}
   245  	if e.ctx.ResourceLimits {
   246  		e.lock.Lock()
   247  		if err := DestroyCgroup(e.groups); err != nil {
   248  			merr.Errors = append(merr.Errors, err)
   249  		}
   250  		e.lock.Unlock()
   251  	}
   252  	return merr.ErrorOrNil()
   253  }
   254  
   255  // Shutdown sends an interrupt signal to the user process
   256  func (e *UniversalExecutor) ShutDown() error {
   257  	if e.cmd.Process == nil {
   258  		return fmt.Errorf("executor.shutdown error: no process found")
   259  	}
   260  	proc, err := os.FindProcess(e.cmd.Process.Pid)
   261  	if err != nil {
   262  		return fmt.Errorf("executor.shutdown error: %v", err)
   263  	}
   264  	if runtime.GOOS == "windows" {
   265  		return proc.Kill()
   266  	}
   267  	if err = proc.Signal(os.Interrupt); err != nil {
   268  		return fmt.Errorf("executor.shutdown error: %v", err)
   269  	}
   270  	return nil
   271  }
   272  
   273  // configureTaskDir sets the task dir in the executor
   274  func (e *UniversalExecutor) configureTaskDir() error {
   275  	taskDir, ok := e.ctx.AllocDir.TaskDirs[e.ctx.TaskName]
   276  	e.taskDir = taskDir
   277  	if !ok {
   278  		return fmt.Errorf("couldn't find task directory for task %v", e.ctx.TaskName)
   279  	}
   280  	e.cmd.Dir = taskDir
   281  	return nil
   282  }