open-nomad/drivers/shared/executor/executor.go

687 lines
19 KiB
Go
Raw Normal View History

2016-02-05 00:03:17 +00:00
package executor
2016-02-04 00:03:43 +00:00
import (
"context"
2016-02-04 00:03:43 +00:00
"fmt"
2018-12-07 01:54:14 +00:00
"io"
"io/ioutil"
2016-02-04 00:03:43 +00:00
"os"
"os/exec"
2016-03-19 19:18:10 +00:00
"path/filepath"
2016-02-04 00:03:43 +00:00
"runtime"
"strings"
2016-02-04 00:03:43 +00:00
"syscall"
"time"
"github.com/armon/circbuf"
"github.com/hashicorp/consul-template/signals"
hclog "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
2016-02-04 00:03:43 +00:00
"github.com/hashicorp/nomad/client/allocdir"
2018-12-07 01:54:14 +00:00
"github.com/hashicorp/nomad/client/lib/fifo"
2016-05-19 20:32:03 +00:00
"github.com/hashicorp/nomad/client/stats"
cstructs "github.com/hashicorp/nomad/client/structs"
"github.com/hashicorp/nomad/plugins/drivers"
"github.com/kr/pty"
2018-04-19 18:16:28 +00:00
shelpers "github.com/hashicorp/nomad/helper/stats"
2016-02-04 00:03:43 +00:00
)
const (
// ExecutorVersionLatest is the current and latest version of the executor
ExecutorVersionLatest = "2.0.0"
// ExecutorVersionPre0_9 is the version of executor use prior to the release
// of 0.9.x
ExecutorVersionPre0_9 = "1.1.0"
2021-02-08 16:58:44 +00:00
// IsolationModePrivate represents the private isolation mode for a namespace
IsolationModePrivate = "private"
2021-02-08 16:58:44 +00:00
// IsolationModeHost represents the host isolation mode for a namespace
IsolationModeHost = "host"
)
2016-06-10 02:45:41 +00:00
var (
// The statistics the basic executor exposes
ExecutorBasicMeasuredMemStats = []string{"RSS", "Swap"}
ExecutorBasicMeasuredCpuStats = []string{"System Mode", "User Mode", "Percent"}
2016-06-10 02:45:41 +00:00
)
2018-12-07 01:54:14 +00:00
// Executor is the interface which allows a driver to launch and supervise
// a process
type Executor interface {
// Launch a user process configured by the given ExecCommand
Launch(launchCmd *ExecCommand) (*ProcessState, error)
// Wait blocks until the process exits or an error occures
Wait(ctx context.Context) (*ProcessState, error)
// Shutdown will shutdown the executor by stopping the user process,
// cleaning up and resources created by the executor. The shutdown sequence
// will first send the given signal to the process. This defaults to "SIGINT"
// if not specified. The executor will then wait for the process to exit
// before cleaning up other resources. If the executor waits longer than the
// given grace period, the process is forcefully killed.
//
// To force kill the user process, gracePeriod can be set to 0.
Shutdown(signal string, gracePeriod time.Duration) error
// UpdateResources updates any resource isolation enforcement with new
// constraints if supported.
UpdateResources(*drivers.Resources) error
2018-12-07 01:54:14 +00:00
// Version returns the executor API version
Version() (*ExecutorVersion, error)
// Returns a channel of stats. Stats are collected and
// pushed to the channel on the given interval
Stats(context.Context, time.Duration) (<-chan *cstructs.TaskResourceUsage, error)
2018-12-07 01:54:14 +00:00
// Signal sends the given signal to the user process
Signal(os.Signal) error
// Exec executes the given command and args inside the executor context
// and returns the output and exit code.
Exec(deadline time.Time, cmd string, args []string) ([]byte, int, error)
ExecStreaming(ctx context.Context, cmd []string, tty bool,
stream drivers.ExecTaskStream) error
2018-12-07 01:54:14 +00:00
}
// ExecCommand holds the user command, args, and other isolation related
// settings.
type ExecCommand struct {
// Cmd is the command that the user wants to run.
Cmd string
// Args is the args of the command that the user wants to run.
Args []string
// Resources defined by the task
Resources *drivers.Resources
2018-12-07 01:54:14 +00:00
// StdoutPath is the path the process stdout should be written to
StdoutPath string
stdout io.WriteCloser
// StderrPath is the path the process stderr should be written to
StderrPath string
stderr io.WriteCloser
// Env is the list of KEY=val pairs of environment variables to be set
Env []string
// User is the user which the executor uses to run the command.
User string
// TaskDir is the directory path on the host where for the task
TaskDir string
// ResourceLimits determines whether resource limits are enforced by the
// executor.
ResourceLimits bool
// Cgroup marks whether we put the process in a cgroup. Setting this field
// doesn't enforce resource limits. To enforce limits, set ResourceLimits.
// Using the cgroup does allow more precise cleanup of processes.
BasicProcessCgroup bool
// NoPivotRoot disables using pivot_root for isolation, useful when the root
// partition is on a ramdisk which does not support pivot_root,
// see man 2 pivot_root
NoPivotRoot bool
// Mounts are the host paths to be be made available inside rootfs
Mounts []*drivers.MountConfig
// Devices are the the device nodes to be created in isolation environment
Devices []*drivers.DeviceConfig
// NetworkIsolation is the network isolation configuration.
NetworkIsolation *drivers.NetworkIsolationSpec
// ModePID is the PID isolation mode (private or host).
ModePID string
// ModeIPC is the IPC isolation mode (private or host).
ModeIPC string
2018-12-07 01:54:14 +00:00
}
// SetWriters sets the writer for the process stdout and stderr. This should
// not be used if writing to a file path such as a fifo file. SetStdoutWriter
// is mainly used for unit testing purposes.
func (c *ExecCommand) SetWriters(out io.WriteCloser, err io.WriteCloser) {
c.stdout = out
c.stderr = err
}
// GetWriters returns the unexported io.WriteCloser for the stdout and stderr
// handles. This is mainly used for unit testing purposes.
func (c *ExecCommand) GetWriters() (stdout io.WriteCloser, stderr io.WriteCloser) {
return c.stdout, c.stderr
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }
// Stdout returns a writer for the configured file descriptor
func (c *ExecCommand) Stdout() (io.WriteCloser, error) {
if c.stdout == nil {
if c.StdoutPath != "" {
2019-04-01 19:56:43 +00:00
f, err := fifo.OpenWriter(c.StdoutPath)
2018-12-07 01:54:14 +00:00
if err != nil {
return nil, fmt.Errorf("failed to create stdout: %v", err)
}
c.stdout = f
} else {
c.stdout = nopCloser{ioutil.Discard}
}
}
return c.stdout, nil
}
// Stderr returns a writer for the configured file descriptor
func (c *ExecCommand) Stderr() (io.WriteCloser, error) {
if c.stderr == nil {
if c.StderrPath != "" {
2019-04-01 19:56:43 +00:00
f, err := fifo.OpenWriter(c.StderrPath)
2018-12-07 01:54:14 +00:00
if err != nil {
return nil, fmt.Errorf("failed to create stderr: %v", err)
}
c.stderr = f
} else {
c.stderr = nopCloser{ioutil.Discard}
}
}
return c.stderr, nil
}
func (c *ExecCommand) Close() {
2019-04-01 16:53:37 +00:00
if c.stdout != nil {
c.stdout.Close()
2018-12-07 01:54:14 +00:00
}
2019-04-01 16:53:37 +00:00
if c.stderr != nil {
c.stderr.Close()
2018-12-07 01:54:14 +00:00
}
}
// ProcessState holds information about the state of a user process.
type ProcessState struct {
Pid int
ExitCode int
Signal int
Time time.Time
}
// ExecutorVersion is the version of the executor
type ExecutorVersion struct {
Version string
}
func (v *ExecutorVersion) GoString() string {
return v.Version
}
2016-02-05 00:18:10 +00:00
// UniversalExecutor is an implementation of the Executor which launches and
// supervises processes. In addition to process supervision it provides resource
// and file system isolation
2016-02-04 00:03:43 +00:00
type UniversalExecutor struct {
childCmd exec.Cmd
2018-12-07 01:54:14 +00:00
commandCfg *ExecCommand
2018-12-07 01:54:14 +00:00
exitState *ProcessState
processExited chan interface{}
// resConCtx is used to track and cleanup additional resources created by
// the executor. Currently this is only used for cgroups.
resConCtx resourceContainerContext
2016-02-04 00:03:43 +00:00
totalCpuStats *stats.CpuStats
userCpuStats *stats.CpuStats
systemCpuStats *stats.CpuStats
pidCollector *pidCollector
logger hclog.Logger
2016-02-04 00:03:43 +00:00
}
2016-02-05 00:18:10 +00:00
// NewExecutor returns an Executor
2018-12-07 01:54:14 +00:00
func NewExecutor(logger hclog.Logger) Executor {
logger = logger.Named("executor")
if err := shelpers.Init(); err != nil {
logger.Error("unable to initialize stats", "error", err)
}
return &UniversalExecutor{
logger: logger,
processExited: make(chan interface{}),
totalCpuStats: stats.NewCpuStats(),
userCpuStats: stats.NewCpuStats(),
systemCpuStats: stats.NewCpuStats(),
pidCollector: newPidCollector(logger),
2016-05-19 20:32:03 +00:00
}
2016-02-04 00:03:43 +00:00
}
2016-03-29 23:27:31 +00:00
// Version returns the api version of the executor
2018-12-07 01:54:14 +00:00
func (e *UniversalExecutor) Version() (*ExecutorVersion, error) {
return &ExecutorVersion{Version: ExecutorVersionLatest}, nil
2016-03-29 23:27:31 +00:00
}
// Launch launches the main process and returns its state. It also
// configures an applies isolation on certain platforms.
2018-12-07 01:54:14 +00:00
func (e *UniversalExecutor) Launch(command *ExecCommand) (*ProcessState, error) {
e.logger.Trace("preparing to launch command", "command", command.Cmd, "args", strings.Join(command.Args, " "))
e.commandCfg = command
2016-02-04 19:51:43 +00:00
// setting the user of the process
if command.User != "" {
e.logger.Debug("running command as user", "user", command.User)
if err := setCmdUser(&e.childCmd, command.User); err != nil {
2016-02-04 00:09:17 +00:00
return nil, err
}
2016-02-04 00:03:43 +00:00
}
// set the task dir as the working directory for the command
e.childCmd.Dir = e.commandCfg.TaskDir
// start command in separate process group
if err := e.setNewProcessGroup(); err != nil {
return nil, err
}
// Setup cgroups on linux
if e.commandCfg.ResourceLimits || e.commandCfg.BasicProcessCgroup {
if err := e.configureResourceContainer(os.Getpid()); err != nil {
return nil, err
}
2016-02-04 00:03:43 +00:00
}
stdout, err := e.commandCfg.Stdout()
if err != nil {
2016-03-19 19:18:10 +00:00
return nil, err
2016-02-04 00:03:43 +00:00
}
stderr, err := e.commandCfg.Stderr()
if err != nil {
return nil, err
2016-02-04 00:03:43 +00:00
}
e.childCmd.Stdout = stdout
e.childCmd.Stderr = stderr
2016-02-04 00:03:43 +00:00
2016-03-19 19:18:10 +00:00
// Look up the binary path and make it executable
2018-10-16 02:37:58 +00:00
absPath, err := lookupBin(command.TaskDir, command.Cmd)
2016-03-19 19:18:10 +00:00
if err != nil {
2016-03-16 02:22:40 +00:00
return nil, err
2016-02-04 19:51:43 +00:00
}
2018-10-16 02:37:58 +00:00
if err := makeExecutable(absPath); err != nil {
2016-03-19 19:18:10 +00:00
return nil, err
}
path := absPath
2016-03-19 19:18:10 +00:00
// Set the commands arguments
e.childCmd.Path = path
e.childCmd.Args = append([]string{e.childCmd.Path}, command.Args...)
e.childCmd.Env = e.commandCfg.Env
2016-03-19 19:18:10 +00:00
// Start the process
if err = withNetworkIsolation(e.childCmd.Start, command.NetworkIsolation); err != nil {
return nil, fmt.Errorf("failed to start command path=%q --- args=%q: %v", path, e.childCmd.Args, err)
}
2019-07-09 22:48:16 +00:00
go e.pidCollector.collectPids(e.processExited, e.getAllPids)
2016-02-04 00:26:10 +00:00
go e.wait()
2018-12-07 01:54:14 +00:00
return &ProcessState{Pid: e.childCmd.Process.Pid, ExitCode: -1, Time: time.Now()}, nil
2016-02-04 00:03:43 +00:00
}
// Exec a command inside a container for exec and java drivers.
func (e *UniversalExecutor) Exec(deadline time.Time, name string, args []string) ([]byte, int, error) {
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
return ExecScript(ctx, e.childCmd.Dir, e.commandCfg.Env, e.childCmd.SysProcAttr, e.commandCfg.NetworkIsolation, name, args)
2017-05-04 23:21:40 +00:00
}
2017-05-04 23:21:40 +00:00
// ExecScript executes cmd with args and returns the output, exit code, and
// error. Output is truncated to drivers/shared/structs.CheckBufSize
func ExecScript(ctx context.Context, dir string, env []string, attrs *syscall.SysProcAttr,
netSpec *drivers.NetworkIsolationSpec, name string, args []string) ([]byte, int, error) {
cmd := exec.CommandContext(ctx, name, args...)
// Copy runtime environment from the main command
2017-05-04 23:21:40 +00:00
cmd.SysProcAttr = attrs
cmd.Dir = dir
cmd.Env = env
// Capture output
buf, _ := circbuf.NewBuffer(int64(drivers.CheckBufSize))
cmd.Stdout = buf
cmd.Stderr = buf
if err := withNetworkIsolation(cmd.Run, netSpec); err != nil {
exitErr, ok := err.(*exec.ExitError)
if !ok {
// Non-exit error, return it and let the caller treat
// it as a critical failure
return nil, 0, err
}
// Some kind of error happened; default to critical
exitCode := 2
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
}
// Don't return the exitError as the caller only needs the
// output and code.
return buf.Bytes(), exitCode, nil
}
return buf.Bytes(), 0, nil
}
func (e *UniversalExecutor) ExecStreaming(ctx context.Context, command []string, tty bool,
stream drivers.ExecTaskStream) error {
if len(command) == 0 {
return fmt.Errorf("command is required")
}
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
cmd.Dir = "/"
cmd.Env = e.childCmd.Env
execHelper := &execHelper{
logger: e.logger,
newTerminal: func() (func() (*os.File, error), *os.File, error) {
pty, tty, err := pty.Open()
if err != nil {
return nil, nil, err
}
return func() (*os.File, error) { return pty, nil }, tty, err
},
setTTY: func(tty *os.File) error {
cmd.SysProcAttr = sessionCmdAttr(tty)
cmd.Stdin = tty
cmd.Stdout = tty
cmd.Stderr = tty
return nil
},
setIO: func(stdin io.Reader, stdout, stderr io.Writer) error {
cmd.Stdin = stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
return nil
},
processStart: func() error {
if u := e.commandCfg.User; u != "" {
if err := setCmdUser(cmd, u); err != nil {
return err
}
}
return withNetworkIsolation(cmd.Start, e.commandCfg.NetworkIsolation)
},
processWait: func() (*os.ProcessState, error) {
err := cmd.Wait()
return cmd.ProcessState, err
},
}
return execHelper.run(ctx, tty, stream)
}
2016-02-05 00:18:10 +00:00
// Wait waits until a process has exited and returns it's exitcode and errors
2018-12-07 01:54:14 +00:00
func (e *UniversalExecutor) Wait(ctx context.Context) (*ProcessState, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-e.processExited:
return e.exitState, nil
}
2016-02-04 00:26:10 +00:00
}
func (e *UniversalExecutor) UpdateResources(resources *drivers.Resources) error {
2016-03-24 02:20:08 +00:00
return nil
}
2016-02-04 00:26:10 +00:00
func (e *UniversalExecutor) wait() {
2016-02-04 18:21:33 +00:00
defer close(e.processExited)
defer e.commandCfg.Close()
pid := e.childCmd.Process.Pid
err := e.childCmd.Wait()
2016-02-04 00:03:43 +00:00
if err == nil {
2018-12-07 01:54:14 +00:00
e.exitState = &ProcessState{Pid: pid, ExitCode: 0, Time: time.Now()}
2016-02-04 00:26:10 +00:00
return
2016-02-04 00:03:43 +00:00
}
2016-10-10 18:46:27 +00:00
2016-02-04 00:03:43 +00:00
exitCode := 1
2016-04-01 20:28:20 +00:00
var signal int
2016-02-04 00:03:43 +00:00
if exitErr, ok := err.(*exec.ExitError); ok {
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
2016-04-01 20:28:20 +00:00
if status.Signaled() {
// bash(1) uses the lower 7 bits of a uint8
// to indicate normal program failure (see
// <sysexits.h>). If a process terminates due
// to a signal, encode the signal number to
// indicate which signal caused the process
// to terminate. Mirror this exit code
// encoding scheme.
const exitSignalBase = 128
2016-04-01 20:28:20 +00:00
signal = int(status.Signal())
exitCode = exitSignalBase + signal
2016-04-01 20:28:20 +00:00
}
2016-02-04 00:03:43 +00:00
}
2016-04-19 22:54:21 +00:00
} else {
e.logger.Warn("unexpected Cmd.Wait() error type", "error", err)
2016-02-04 00:03:43 +00:00
}
2016-04-19 22:54:21 +00:00
2018-12-07 01:54:14 +00:00
e.exitState = &ProcessState{Pid: pid, ExitCode: exitCode, Signal: signal, Time: time.Now()}
2016-02-04 00:03:43 +00:00
}
2016-02-09 18:00:42 +00:00
var (
// finishedErr is the error message received when trying to kill and already
// exited process.
finishedErr = "os: process already finished"
// noSuchProcessErr is the error message received when trying to kill a non
// existing process (e.g. when killing a process group).
noSuchProcessErr = "no such process"
2016-02-09 18:00:42 +00:00
)
// Exit cleans up the alloc directory, destroys resource container and kills the
// user process
func (e *UniversalExecutor) Shutdown(signal string, grace time.Duration) error {
e.logger.Debug("shutdown requested", "signal", signal, "grace_period_ms", grace.Round(time.Millisecond))
var merr multierror.Error
2016-10-26 00:27:13 +00:00
// If the executor did not launch a process, return.
if e.commandCfg == nil {
return nil
2016-10-26 00:27:13 +00:00
}
// If there is no process we can't shutdown
if e.childCmd.Process == nil {
e.logger.Warn("failed to shutdown", "error", "no process found")
return fmt.Errorf("executor failed to shutdown error: no process found")
2016-10-26 00:27:13 +00:00
}
proc, err := os.FindProcess(e.childCmd.Process.Pid)
if err != nil {
err = fmt.Errorf("executor failed to find process: %v", err)
e.logger.Warn("failed to shutdown", "error", err)
return err
}
// If grace is 0 then skip shutdown logic
if grace > 0 {
// Default signal to SIGINT if not set
if signal == "" {
signal = "SIGINT"
}
sig, ok := signals.SignalLookup[signal]
if !ok {
err = fmt.Errorf("error unknown signal given for shutdown: %s", signal)
e.logger.Warn("failed to shutdown", "error", err)
return err
}
2016-02-04 00:03:43 +00:00
if err := e.shutdownProcess(sig, proc); err != nil {
e.logger.Warn("failed to shutdown", "error", err)
return err
}
select {
case <-e.processExited:
case <-time.After(grace):
proc.Kill()
}
} else {
proc.Kill()
}
// Wait for process to exit
select {
case <-e.processExited:
case <-time.After(time.Second * 15):
e.logger.Warn("process did not exit after 15 seconds")
merr.Errors = append(merr.Errors, fmt.Errorf("process did not exit after 15 seconds"))
}
// Prefer killing the process via the resource container.
if !(e.commandCfg.ResourceLimits || e.commandCfg.BasicProcessCgroup) {
if err := e.cleanupChildProcesses(proc); err != nil && err.Error() != finishedErr {
merr.Errors = append(merr.Errors,
fmt.Errorf("can't kill process with pid %d: %v", e.childCmd.Process.Pid, err))
}
}
if e.commandCfg.ResourceLimits || e.commandCfg.BasicProcessCgroup {
if err := e.resConCtx.executorCleanup(); err != nil {
merr.Errors = append(merr.Errors, err)
}
}
if err := merr.ErrorOrNil(); err != nil {
e.logger.Warn("failed to shutdown", "error", err)
return err
}
return nil
}
2018-10-16 02:37:58 +00:00
// Signal sends the passed signal to the task
func (e *UniversalExecutor) Signal(s os.Signal) error {
if e.childCmd.Process == nil {
return fmt.Errorf("Task not yet run")
}
e.logger.Debug("sending signal to PID", "signal", s, "pid", e.childCmd.Process.Pid)
err := e.childCmd.Process.Signal(s)
if err != nil {
e.logger.Error("sending signal failed", "signal", s, "error", err)
return err
}
return nil
}
func (e *UniversalExecutor) Stats(ctx context.Context, interval time.Duration) (<-chan *cstructs.TaskResourceUsage, error) {
ch := make(chan *cstructs.TaskResourceUsage)
go e.handleStats(ch, ctx, interval)
return ch, nil
}
func (e *UniversalExecutor) handleStats(ch chan *cstructs.TaskResourceUsage, ctx context.Context, interval time.Duration) {
defer close(ch)
timer := time.NewTimer(0)
for {
select {
case <-ctx.Done():
return
case <-timer.C:
timer.Reset(interval)
}
pidStats, err := e.pidCollector.pidStats()
if err != nil {
e.logger.Warn("error collecting stats", "error", err)
return
}
select {
case <-ctx.Done():
return
case ch <- aggregatedResourceUsage(e.systemCpuStats, pidStats):
}
2018-10-16 02:37:58 +00:00
}
}
2016-03-19 19:18:10 +00:00
// lookupBin looks for path to the binary to run by looking for the binary in
2019-05-02 17:34:41 +00:00
// the following locations, in-order:
// task/local/, task/, on the host file system, in host $PATH
2016-03-19 19:18:10 +00:00
// The return path is absolute.
2018-10-16 02:37:58 +00:00
func lookupBin(taskDir string, bin string) (string, error) {
2016-03-19 19:18:10 +00:00
// Check in the local directory
2018-10-16 02:37:58 +00:00
local := filepath.Join(taskDir, allocdir.TaskLocal, bin)
2016-03-19 19:18:10 +00:00
if _, err := os.Stat(local); err == nil {
return local, nil
}
// Check at the root of the task's directory
2018-10-16 02:37:58 +00:00
root := filepath.Join(taskDir, bin)
2016-03-19 19:18:10 +00:00
if _, err := os.Stat(root); err == nil {
return root, nil
}
2019-04-03 00:00:54 +00:00
// when checking host paths, check with Stat first if path is absolute
// as exec.LookPath only considers files already marked as executable
// and only consider this for absolute paths to avoid depending on
// current directory of nomad which may cause unexpected behavior
if _, err := os.Stat(bin); err == nil && filepath.IsAbs(bin) {
return bin, nil
}
2016-03-19 19:18:10 +00:00
// Check the $PATH
if host, err := exec.LookPath(bin); err == nil {
return host, nil
}
return "", fmt.Errorf("binary %q could not be found", bin)
}
// makeExecutable makes the given file executable for root,group,others.
2018-10-16 02:37:58 +00:00
func makeExecutable(binPath string) error {
2016-03-19 19:18:10 +00:00
if runtime.GOOS == "windows" {
return nil
}
2016-03-16 02:22:40 +00:00
fi, err := os.Stat(binPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("binary %q does not exist", binPath)
}
return fmt.Errorf("specified binary is invalid: %v", err)
}
// If it is not executable, make it so.
perm := fi.Mode().Perm()
req := os.FileMode(0555)
if perm&req != req {
if err := os.Chmod(binPath, perm|req); err != nil {
return fmt.Errorf("error making %q executable: %s", binPath, err)
}
}
return nil
}