From c9994c3e59027a07b0522d130de44f4d18deec81 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Fri, 11 Sep 2015 12:35:03 -0700 Subject: [PATCH 01/12] Added stub / spec for executor interface --- client/exec/exec.go | 47 +++++++++++++++++++++++++++++++++++++++ client/exec/exec_linux.go | 3 +++ 2 files changed, 50 insertions(+) create mode 100644 client/exec/exec.go create mode 100644 client/exec/exec_linux.go diff --git a/client/exec/exec.go b/client/exec/exec.go new file mode 100644 index 000000000..00476c1cb --- /dev/null +++ b/client/exec/exec.go @@ -0,0 +1,47 @@ +// Package exec is used to invoke child processes across various operating +// systems and child processes and provide the following features: +// +// - Least privilege +// - Resource constraints +// - Process isolation +// +// The semantics and implementation for these differ between operating systems, +// operating system versions, and types of child processes. For example, running +// Docker on Linux has different semantics than running Java on Windows. Also, +// versions of an OS may provide different capabilities for resource isolation, +// such as ulimits, cgroups, containers, jails, etc. Please refer to the +// relevant implementation for specific details. +package exec + +import "github.com/hashicorp/nomad/nomad/structs" + +type Command struct { + // This may be a username or Uid. The implementation will decide how to use it. + UserID string +} + +type Executor interface { + // Limit must be called before Start and restricts the amount of resources + // the process can use + Limit(structs.Resources) + + // Start the process. This may wrap the actual process in another command, + // depending on the capabilities in this environment. + Start() error + + // Shutdown should use a graceful stop mechanism so the application can + // perform checkpointing or cleanup, if such a mechanism is available. + // If such a mechanism is not available, Showdown() should call ForceStop(). + Shutdown() error + + // ForceStop will terminate the process without waiting for cleanup. Every + // implementations must provide this. + ForceStop() error +} + +// DefaultExecutor uses capability testing to give you the best available +// executor based on your platform and execution environment. If you need a +// specific executor, call it directly. +func DefaultExecutor() Executor { + // TODO Implement this +} diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go new file mode 100644 index 000000000..ce7bafc23 --- /dev/null +++ b/client/exec/exec_linux.go @@ -0,0 +1,3 @@ +package exec + +// TODO Implement this! From f4d489af6e2eb2687d5c7775fe3b5fc46a45e51b Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Fri, 11 Sep 2015 17:12:48 -0700 Subject: [PATCH 02/12] Update the interface, add UniversalExecutor, add stub for LinuxExecutor --- client/exec/exec.go | 136 +++++++++++++++++++++++++++++++++----- client/exec/exec_linux.go | 34 +++++++++- 2 files changed, 151 insertions(+), 19 deletions(-) diff --git a/client/exec/exec.go b/client/exec/exec.go index 00476c1cb..d72027685 100644 --- a/client/exec/exec.go +++ b/client/exec/exec.go @@ -1,47 +1,147 @@ -// Package exec is used to invoke child processes across various operating -// systems and child processes and provide the following features: +// Package exec is used to invoke child processes across various platforms to +// provide the following features: // // - Least privilege // - Resource constraints // - Process isolation // -// The semantics and implementation for these differ between operating systems, -// operating system versions, and types of child processes. For example, running -// Docker on Linux has different semantics than running Java on Windows. Also, -// versions of an OS may provide different capabilities for resource isolation, -// such as ulimits, cgroups, containers, jails, etc. Please refer to the -// relevant implementation for specific details. +// A "platform" may be defined as coarsely as "Windows" or as specifically as +// "linux 3.20 with systemd". This allows Nomad to use best-effort, best- +// available capabilities of each platform to provide resource constraints, +// process isolation, and security features, or otherwise take advantage of +// features that are unique to that platform. +// +// The semantics of any particular instance are left up to the implementation. +// However, these should be completely transparent to the calling context. In +// other words, the Java driver should be able to call exec for any platform and +// just work. package exec -import "github.com/hashicorp/nomad/nomad/structs" +import ( + "fmt" + "os" + "os/exec" + "path/filepath" -type Command struct { - // This may be a username or Uid. The implementation will decide how to use it. - UserID string -} + "github.com/hashicorp/nomad/nomad/structs" +) +// Executor is an interface that any platform- or capability-specific exec +// wrapper must implement. You should not need to implement a Java executor. +// Rather, you would implement a cgroups executor that the Java driver will use. type Executor interface { // Limit must be called before Start and restricts the amount of resources // the process can use Limit(structs.Resources) + // RunAs sets the user we should use to run this command. This may be set as + // a username, uid, or other identifier. The implementation will decide what + // to do with it, if anything. + RunAs(string) + // Start the process. This may wrap the actual process in another command, - // depending on the capabilities in this environment. + // depending on the capabilities in this environment. Errors that arise from + // Limits or Runas will bubble through Start() Start() error + // Open should be called to restore a previous pid. This might be needed if + // nomad is restarted. This sets os.Process internally. + Open(int) error + // Shutdown should use a graceful stop mechanism so the application can // perform checkpointing or cleanup, if such a mechanism is available. - // If such a mechanism is not available, Showdown() should call ForceStop(). + // If such a mechanism is not available, Shutdown() should call ForceStop(). Shutdown() error // ForceStop will terminate the process without waiting for cleanup. Every // implementations must provide this. ForceStop() error + + // Access the underlying Cmd struct. This should never be nil. Also, this is + // not intended to be access outside the exec package, so YMMV. + Command() *cmd } -// DefaultExecutor uses capability testing to give you the best available +// Cmd is an extension of exec.Cmd that incorporates functionality for +// re-attaching to processes, dropping priviledges, etc., based on platform- +// specific implementations. +type cmd struct { + exec.Cmd + + // Resources is used to limit CPU and RAM used by the process, by way of + // cgroups or a similar mechanism. + Resources structs.Resources + + // RunAs may be a username or Uid. The implementation will decide how to use it. + RunAs string +} + +// Command is a mirror of exec.Command that returns a platform-specific Executor +func Command(name string, arg ...string) Executor { + executor := AutoselectExecutor() + cmd := executor.Command() + cmd.Path = name + cmd.Args = append([]string{name}, arg...) + + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err != nil { + // cmd.lookPathErr = err + } else { + cmd.Path = lp + } + } + return executor +} + +func OpenPid(int) Executor { + executor := AutoselectExecutor() + return executor +} + +// AutoselectExecutor uses capability testing to give you the best available // executor based on your platform and execution environment. If you need a // specific executor, call it directly. -func DefaultExecutor() Executor { - // TODO Implement this +func AutoselectExecutor() Executor { + // TODO platform switching + return &UniversalExecutor{} +} + +// UniversalExecutor should work everywhere, and as a result does not include +// any resource restrictions or runas capabilities. +type UniversalExecutor struct { + cmd +} + +func (e *UniversalExecutor) Limit(resources structs.Resources) { + // No-op +} + +func (e *UniversalExecutor) RunAs(userid string) { + // No-op +} + +func (e *UniversalExecutor) Start() error { + // We don't want to call ourself. We want to call Start on our embedded Cmd + return e.cmd.Start() +} + +func (e *UniversalExecutor) Open(pid int) error { + process, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("Failed to reopen pid %d: %s", pid, err) + } + e.Process = process + return nil +} + +func (e *UniversalExecutor) Shutdown() error { + return e.ForceStop() +} + +func (e *UniversalExecutor) ForceStop() error { + return e.Process.Kill() +} + +func (e *UniversalExecutor) Command() *cmd { + return &e.cmd } diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index ce7bafc23..14591dad5 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -1,3 +1,35 @@ package exec -// TODO Implement this! +import "github.com/hashicorp/nomad/nomad/structs" + +type LinuxExecutor struct { + cmd +} + +func (e *LinuxExecutor) Limit(resources structs.Resources) { + +} + +func (e *LinuxExecutor) RunAs(userid string) { + +} + +func (e *LinuxExecutor) Start() error { + return nil +} + +func (e *LinuxExecutor) Open(pid int) error { + return nil +} + +func (e *LinuxExecutor) Shutdown() error { + return nil +} + +func (e *LinuxExecutor) ForceStop() error { + return nil +} + +func (e *LinuxExecutor) Command() *cmd { + return &e.cmd +} From d4c57c67a2ee2a95d06acd5a2eb0a8c5519ac210 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Mon, 14 Sep 2015 15:57:21 -0700 Subject: [PATCH 03/12] Implemented Linux driver with RunAs functionality -- added errors to RunAs and Limit --- client/exec/exec.go | 19 ++++--- client/exec/exec_linux.go | 105 +++++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/client/exec/exec.go b/client/exec/exec.go index d72027685..8f67768ce 100644 --- a/client/exec/exec.go +++ b/client/exec/exec.go @@ -31,13 +31,15 @@ import ( // Rather, you would implement a cgroups executor that the Java driver will use. type Executor interface { // Limit must be called before Start and restricts the amount of resources - // the process can use - Limit(structs.Resources) + // the process can use. Note that an error may be returned ONLY IF the + // executor implements resource limiting. Otherwise Limit is ignored. + Limit(structs.Resources) error // RunAs sets the user we should use to run this command. This may be set as // a username, uid, or other identifier. The implementation will decide what - // to do with it, if anything. - RunAs(string) + // to do with it, if anything. Note that an error may be returned ONLY IF + // the executor implements user lookups. Otherwise RunAs is ignored. + RunAs(string) error // Start the process. This may wrap the actual process in another command, // depending on the capabilities in this environment. Errors that arise from @@ -101,6 +103,9 @@ func OpenPid(int) Executor { // AutoselectExecutor uses capability testing to give you the best available // executor based on your platform and execution environment. If you need a // specific executor, call it directly. +// +// This is a simplistic strategy pattern. We can potentially improve this by +// using a decorator pattern instead. func AutoselectExecutor() Executor { // TODO platform switching return &UniversalExecutor{} @@ -112,12 +117,14 @@ type UniversalExecutor struct { cmd } -func (e *UniversalExecutor) Limit(resources structs.Resources) { +func (e *UniversalExecutor) Limit(resources structs.Resources) error { // No-op + return nil } -func (e *UniversalExecutor) RunAs(userid string) { +func (e *UniversalExecutor) RunAs(userid string) error { // No-op + return nil } func (e *UniversalExecutor) Start() error { diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index 14591dad5..668e2c1f5 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -1,33 +1,114 @@ package exec -import "github.com/hashicorp/nomad/nomad/structs" +import ( + "fmt" + "os" + "os/user" + "strconv" + "syscall" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/nomad/nomad/structs" +) + +// Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as +// a user you specify and limit resources using rlimit. type LinuxExecutor struct { cmd + user *user.User } -func (e *LinuxExecutor) Limit(resources structs.Resources) { - -} - -func (e *LinuxExecutor) RunAs(userid string) { - -} - -func (e *LinuxExecutor) Start() error { +// SetUID changes the Uid for this command (must be set before starting) +func SetUID(command *cmd, userid string) error { + uid, err := strconv.ParseUint(userid, 10, 32) + if err != nil { + return fmt.Errorf("Unable to convert userid to uint32: %s", err) + } + if command.SysProcAttr == nil { + command.SysProcAttr = &syscall.SysProcAttr{} + } + if command.SysProcAttr.Credential == nil { + command.SysProcAttr.Credential = &syscall.Credential{} + } + command.SysProcAttr.Credential.Uid = uint32(uid) return nil } +// SetGID changes the Gid for this command (must be set before starting) +func SetGID(command *cmd, groupid string) error { + gid, err := strconv.ParseUint(groupid, 10, 32) + if err != nil { + return fmt.Errorf("Unable to convert groupid to uint32: %s", err) + } + if command.SysProcAttr == nil { + command.SysProcAttr = &syscall.SysProcAttr{} + } + if command.SysProcAttr.Credential == nil { + command.SysProcAttr.Credential = &syscall.Credential{} + } + command.SysProcAttr.Credential.Uid = uint32(gid) + return nil +} + +func (e *LinuxExecutor) Limit(resources structs.Resources) error { + // TODO rlimit + return nil +} + +func (e *LinuxExecutor) RunAs(userid string) error { + errs := new(multierror.Error) + + // First, try to lookup the user by uid + u, err := user.LookupId(userid) + if err == nil { + e.user = u + return nil + } else { + errs = multierror.Append(errs, err) + } + + // Lookup failed, so try by username instead + u, err = user.Lookup(userid) + if err == nil { + e.user = u + return nil + } else { + errs = multierror.Append(errs, err) + } + + // If we got here we failed to looking based on id and username, so we'll + // return those errors. + return fmt.Errorf("Failed to identify user to run as: %s", errs) +} + +func (e *LinuxExecutor) Start() error { + // Set the user and group this process should run as + if e.user != nil { + SetUID(&e.cmd, e.user.Uid) + SetGID(&e.cmd, e.user.Gid) + } + + // We don't want to call ourself. We want to call Start on our embedded Cmd. + return e.cmd.Start() +} + func (e *LinuxExecutor) Open(pid int) error { + process, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("Failed to reopen pid %d: %s", pid, err) + } + // TODO signal the process with signal 0 to see if it's alive. Error if it + // is not. + e.Process = process return nil } func (e *LinuxExecutor) Shutdown() error { - return nil + return e.ForceStop() } func (e *LinuxExecutor) ForceStop() error { - return nil + return e.Process.Kill() } func (e *LinuxExecutor) Command() *cmd { From 80eef5362ef80a0368f9f9751412b936583651e6 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Mon, 14 Sep 2015 16:16:56 -0700 Subject: [PATCH 04/12] Send signal 0 to check whether the process is alive --- client/exec/exec_linux.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index 668e2c1f5..74d83b883 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -94,11 +94,19 @@ func (e *LinuxExecutor) Start() error { func (e *LinuxExecutor) Open(pid int) error { process, err := os.FindProcess(pid) + // FindProcess doesn't do any checking against the process table so it's + // unlikely we'll ever see this error. if err != nil { return fmt.Errorf("Failed to reopen pid %d: %s", pid, err) } - // TODO signal the process with signal 0 to see if it's alive. Error if it - // is not. + + // On linux FindProcess() will return a pid but doesn't actually check to + // see whether that process is running. We'll send signal 0 to see if the + // process is alive. + err := process.Signal(syscall.Signal(0)) + if err != nil { + return fmt.Errorf("Unable to signal pid %d: %s", err) + } e.Process = process return nil } From b38112af29237990d3b0fcf25cfa2331d6e7f524 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Mon, 14 Sep 2015 19:04:29 -0700 Subject: [PATCH 05/12] Run as nobody if no user is specified --- client/exec/exec_linux.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index 74d83b883..ea5d4cb48 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -82,6 +82,12 @@ func (e *LinuxExecutor) RunAs(userid string) error { } func (e *LinuxExecutor) Start() error { + if e.user == nil { + // If no user has been specified, try to run as "nobody" user so we + // don't leak root privilege to the spawned process. + e.RunAs("nobody") + } + // Set the user and group this process should run as if e.user != nil { SetUID(&e.cmd, e.user.Uid) From 2bcf7105e88bca3cf1ca2253c686b7d6b9db1699 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Mon, 14 Sep 2015 19:38:21 -0700 Subject: [PATCH 06/12] Use Linux on Linux --- client/exec/exec.go | 24 +++++++++++++++++++++++- client/exec/exec_linux.go | 25 +++++++++++++++---------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/client/exec/exec.go b/client/exec/exec.go index 8f67768ce..dcad3098c 100644 --- a/client/exec/exec.go +++ b/client/exec/exec.go @@ -30,6 +30,11 @@ import ( // wrapper must implement. You should not need to implement a Java executor. // Rather, you would implement a cgroups executor that the Java driver will use. type Executor interface { + // Available should return true or false based on whether the current platform + // can run this type of executor, based on capability testing. Returning + // true does not guarantee that this executor will be used. + Available() bool + // Limit must be called before Start and restricts the amount of resources // the process can use. Note that an error may be returned ONLY IF the // executor implements resource limiting. Otherwise Limit is ignored. @@ -107,7 +112,20 @@ func OpenPid(int) Executor { // This is a simplistic strategy pattern. We can potentially improve this by // using a decorator pattern instead. func AutoselectExecutor() Executor { - // TODO platform switching + // These will be IN ORDER and the first available will be used, so preferred + // ones should be at the top and fallbacks at the bottom. + // TODO refactor this to be more lightweight. + executors := []Executor{ + &LinuxExecutor{}, + } + + for _, executor := range executors { + if executor.Available() { + return executor + } + } + + // Always return something, even if we don't have advanced capabilities. return &UniversalExecutor{} } @@ -117,6 +135,10 @@ type UniversalExecutor struct { cmd } +func (e *UniversalExecutor) Available() bool { + return true +} + func (e *UniversalExecutor) Limit(resources structs.Resources) error { // No-op return nil diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index ea5d4cb48..1d6a79413 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/user" + "runtime" "strconv" "syscall" @@ -11,13 +12,6 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) -// Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as -// a user you specify and limit resources using rlimit. -type LinuxExecutor struct { - cmd - user *user.User -} - // SetUID changes the Uid for this command (must be set before starting) func SetUID(command *cmd, userid string) error { uid, err := strconv.ParseUint(userid, 10, 32) @@ -50,8 +44,19 @@ func SetGID(command *cmd, groupid string) error { return nil } +// Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as +// a user you specify and limit resources using rlimit. +type LinuxExecutor struct { + cmd + user *user.User +} + +func (e *LinuxExecutor) Available() bool { + return runtime.GOOS == "linux" +} + func (e *LinuxExecutor) Limit(resources structs.Resources) error { - // TODO rlimit + // TODO limit some things return nil } @@ -76,7 +81,7 @@ func (e *LinuxExecutor) RunAs(userid string) error { errs = multierror.Append(errs, err) } - // If we got here we failed to looking based on id and username, so we'll + // If we got here we failed to lookup based on id and username, so we'll // return those errors. return fmt.Errorf("Failed to identify user to run as: %s", errs) } @@ -109,7 +114,7 @@ func (e *LinuxExecutor) Open(pid int) error { // On linux FindProcess() will return a pid but doesn't actually check to // see whether that process is running. We'll send signal 0 to see if the // process is alive. - err := process.Signal(syscall.Signal(0)) + err = process.Signal(syscall.Signal(0)) if err != nil { return fmt.Errorf("Unable to signal pid %d: %s", err) } From b3ef6930b848828f50b3b7ad164b0f55add49ea7 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 13:11:56 -0700 Subject: [PATCH 07/12] Updated exec driver to use nomad/client/exec --- client/driver/exec.go | 34 ++++++++++++++++++---------------- client/exec/exec.go | 31 +++++++++++++++++++++++++++---- client/exec/exec_linux.go | 15 ++++++++++++++- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/client/driver/exec.go b/client/driver/exec.go index 5e4564923..7426f9fd4 100644 --- a/client/driver/exec.go +++ b/client/driver/exec.go @@ -2,15 +2,12 @@ package driver import ( "fmt" - "os" - "os/exec" "strconv" "strings" "time" - "golang.org/x/sys/unix" - "github.com/hashicorp/nomad/client/config" + nexec "github.com/hashicorp/nomad/client/exec" "github.com/hashicorp/nomad/nomad/structs" ) @@ -23,7 +20,7 @@ type ExecDriver struct { // execHandle is returned from Start/Open as a handle to the PID type execHandle struct { - proc *os.Process + cmd nexec.Executor waitCh chan error doneCh chan struct{} } @@ -54,15 +51,19 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, } // Setup the command - cmd := exec.Command(command, args...) - err := cmd.Start() + cmd := nexec.Command(command, args...) + err := cmd.Limit(task.Resources) + if err != nil { + return nil, fmt.Errorf("failed to constrain resources: %s", err) + } + err = cmd.Start() if err != nil { return nil, fmt.Errorf("failed to start command: %v", err) } // Return a driver handle h := &execHandle{ - proc: cmd.Process, + cmd: cmd, doneCh: make(chan struct{}), waitCh: make(chan error, 1), } @@ -79,14 +80,14 @@ func (d *ExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro } // Find the process - proc, err := os.FindProcess(pid) - if proc == nil || err != nil { + cmd, err := nexec.OpenPid(pid) + if cmd == nil || err != nil { return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) } // Return a driver handle h := &execHandle{ - proc: proc, + cmd: cmd, doneCh: make(chan struct{}), waitCh: make(chan error, 1), } @@ -96,7 +97,8 @@ func (d *ExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro func (h *execHandle) ID() string { // Return a handle to the PID - return fmt.Sprintf("PID:%d", h.proc.Pid) + pid, _ := h.cmd.Pid() + return fmt.Sprintf("PID:%d", pid) } func (h *execHandle) WaitCh() chan error { @@ -111,21 +113,21 @@ func (h *execHandle) Update(task *structs.Task) error { // Kill is used to terminate the task. We send an Interrupt // and then provide a 5 second grace period before doing a Kill. func (h *execHandle) Kill() error { - h.proc.Signal(unix.SIGTERM) + h.cmd.Shutdown() select { case <-h.doneCh: return nil case <-time.After(5 * time.Second): - return h.proc.Kill() + return h.cmd.ForceStop() } } func (h *execHandle) run() { - ps, err := h.proc.Wait() + err := h.cmd.Wait() close(h.doneCh) if err != nil { h.waitCh <- err - } else if !ps.Success() { + } else if !h.cmd.Command().ProcessState.Success() { h.waitCh <- fmt.Errorf("task exited with error") } close(h.waitCh) diff --git a/client/exec/exec.go b/client/exec/exec.go index dcad3098c..d68c1d510 100644 --- a/client/exec/exec.go +++ b/client/exec/exec.go @@ -38,7 +38,7 @@ type Executor interface { // Limit must be called before Start and restricts the amount of resources // the process can use. Note that an error may be returned ONLY IF the // executor implements resource limiting. Otherwise Limit is ignored. - Limit(structs.Resources) error + Limit(*structs.Resources) error // RunAs sets the user we should use to run this command. This may be set as // a username, uid, or other identifier. The implementation will decide what @@ -55,6 +55,12 @@ type Executor interface { // nomad is restarted. This sets os.Process internally. Open(int) error + // This is a convenience wrapper around Command().Wait() + Wait() error + + // This is a convenience wrapper around Command().Process.Pid + Pid() (int, error) + // Shutdown should use a graceful stop mechanism so the application can // perform checkpointing or cleanup, if such a mechanism is available. // If such a mechanism is not available, Shutdown() should call ForceStop(). @@ -100,9 +106,13 @@ func Command(name string, arg ...string) Executor { return executor } -func OpenPid(int) Executor { +func OpenPid(pid int) (Executor, error) { executor := AutoselectExecutor() - return executor + err := executor.Open(pid) + if err != nil { + return nil, err + } + return executor, nil } // AutoselectExecutor uses capability testing to give you the best available @@ -139,7 +149,7 @@ func (e *UniversalExecutor) Available() bool { return true } -func (e *UniversalExecutor) Limit(resources structs.Resources) error { +func (e *UniversalExecutor) Limit(resources *structs.Resources) error { // No-op return nil } @@ -163,6 +173,19 @@ func (e *UniversalExecutor) Open(pid int) error { return nil } +func (e *UniversalExecutor) Wait() error { + // We don't want to call ourself. We want to call Start on our embedded Cmd + return e.cmd.Wait() +} + +func (e *UniversalExecutor) Pid() (int, error) { + if e.cmd.Process != nil { + return e.cmd.Process.Pid, nil + } else { + return 0, fmt.Errorf("Process has finished or was never started") + } +} + func (e *UniversalExecutor) Shutdown() error { return e.ForceStop() } diff --git a/client/exec/exec_linux.go b/client/exec/exec_linux.go index 1d6a79413..3c641f2cf 100644 --- a/client/exec/exec_linux.go +++ b/client/exec/exec_linux.go @@ -55,7 +55,7 @@ func (e *LinuxExecutor) Available() bool { return runtime.GOOS == "linux" } -func (e *LinuxExecutor) Limit(resources structs.Resources) error { +func (e *LinuxExecutor) Limit(resources *structs.Resources) error { // TODO limit some things return nil } @@ -122,6 +122,19 @@ func (e *LinuxExecutor) Open(pid int) error { return nil } +func (e *LinuxExecutor) Wait() error { + // We don't want to call ourself. We want to call Start on our embedded Cmd + return e.cmd.Wait() +} + +func (e *LinuxExecutor) Pid() (int, error) { + if e.cmd.Process != nil { + return e.cmd.Process.Pid, nil + } else { + return 0, fmt.Errorf("Process has finished or was never started") + } +} + func (e *LinuxExecutor) Shutdown() error { return e.ForceStop() } From 5eb33546787848751a915c15cd4e65a66e33b24f Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 13:45:48 -0700 Subject: [PATCH 08/12] Update Java driver to use Executor --- client/driver/exec.go | 4 +--- client/driver/java.go | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/client/driver/exec.go b/client/driver/exec.go index 7426f9fd4..622fa8dd7 100644 --- a/client/driver/exec.go +++ b/client/driver/exec.go @@ -81,7 +81,7 @@ func (d *ExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro // Find the process cmd, err := nexec.OpenPid(pid) - if cmd == nil || err != nil { + if err != nil { return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) } @@ -110,8 +110,6 @@ func (h *execHandle) Update(task *structs.Task) error { return nil } -// Kill is used to terminate the task. We send an Interrupt -// and then provide a 5 second grace period before doing a Kill. func (h *execHandle) Kill() error { h.cmd.Shutdown() select { diff --git a/client/driver/java.go b/client/driver/java.go index b5e166205..53d4c2b23 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -27,7 +27,7 @@ type JavaDriver struct { // javaHandle is returned from Start/Open as a handle to the PID type javaHandle struct { - proc *os.Process + cmd nexec.Executor waitCh chan error doneCh chan struct{} } @@ -129,7 +129,11 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, // Setup the command // Assumes Java is in the $PATH, but could probably be detected - cmd := exec.Command("java", args...) + cmd := nexec.Command("java", args...) + err := cmd.Limit(task.Resources) + if err != nil { + return nil, fmt.Errorf("failed to constrain resources: %s", err) + } err = cmd.Start() if err != nil { return nil, fmt.Errorf("failed to start source: %v", err) @@ -137,7 +141,7 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, // Return a driver handle h := &javaHandle{ - proc: cmd.Process, + cmd: cmd, doneCh: make(chan struct{}), waitCh: make(chan error, 1), } @@ -156,13 +160,13 @@ func (d *JavaDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro // Find the process proc, err := os.FindProcess(pid) - if proc == nil || err != nil { + if err != nil { return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) } // Return a driver handle h := &javaHandle{ - proc: proc, + cmd: cmd, doneCh: make(chan struct{}), waitCh: make(chan error, 1), } @@ -173,7 +177,8 @@ func (d *JavaDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro func (h *javaHandle) ID() string { // Return a handle to the PID - return fmt.Sprintf("PID:%d", h.proc.Pid) + pid, _ := h.cmd.Pid() + return fmt.Sprintf("PID:%d", pid) } func (h *javaHandle) WaitCh() chan error { @@ -185,24 +190,22 @@ func (h *javaHandle) Update(task *structs.Task) error { return nil } -// Kill is used to terminate the task. We send an Interrupt -// and then provide a 5 second grace period before doing a Kill. func (h *javaHandle) Kill() error { - h.proc.Signal(unix.SIGTERM) + h.cmd.Shutdown() select { case <-h.doneCh: return nil case <-time.After(5 * time.Second): - return h.proc.Kill() + return h.cmd.ForceStop() } } func (h *javaHandle) run() { - ps, err := h.proc.Wait() + err := h.cmd.Wait() close(h.doneCh) if err != nil { h.waitCh <- err - } else if !ps.Success() { + } else if !h.cmd.Command().ProcessState.Success() { h.waitCh <- fmt.Errorf("task exited with error") } close(h.waitCh) From a5512c22e96be1b842ea177bea195cb7e73bd4dd Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 14:03:03 -0700 Subject: [PATCH 09/12] Rename exec package to executor so it works better with goimports; fixes for Java driver --- client/driver/exec.go | 8 ++++---- client/driver/java.go | 11 +++++------ client/{exec => executor}/exec.go | 10 +++++----- client/{exec/exec_linux.go => executor/linux.go} | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) rename client/{exec => executor}/exec.go (96%) rename client/{exec/exec_linux.go => executor/linux.go} (99%) diff --git a/client/driver/exec.go b/client/driver/exec.go index 622fa8dd7..a96f3121d 100644 --- a/client/driver/exec.go +++ b/client/driver/exec.go @@ -7,7 +7,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" - nexec "github.com/hashicorp/nomad/client/exec" + "github.com/hashicorp/nomad/client/executor" "github.com/hashicorp/nomad/nomad/structs" ) @@ -20,7 +20,7 @@ type ExecDriver struct { // execHandle is returned from Start/Open as a handle to the PID type execHandle struct { - cmd nexec.Executor + cmd executor.Executor waitCh chan error doneCh chan struct{} } @@ -51,7 +51,7 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, } // Setup the command - cmd := nexec.Command(command, args...) + cmd := executor.Command(command, args...) err := cmd.Limit(task.Resources) if err != nil { return nil, fmt.Errorf("failed to constrain resources: %s", err) @@ -80,7 +80,7 @@ func (d *ExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro } // Find the process - cmd, err := nexec.OpenPid(pid) + cmd, err := executor.OpenPid(pid) if err != nil { return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) } diff --git a/client/driver/java.go b/client/driver/java.go index 53d4c2b23..ac183e166 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -13,9 +13,8 @@ import ( "strings" "time" - "golang.org/x/sys/unix" - "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/executor" "github.com/hashicorp/nomad/nomad/structs" ) @@ -27,7 +26,7 @@ type JavaDriver struct { // javaHandle is returned from Start/Open as a handle to the PID type javaHandle struct { - cmd nexec.Executor + cmd executor.Executor waitCh chan error doneCh chan struct{} } @@ -129,8 +128,8 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, // Setup the command // Assumes Java is in the $PATH, but could probably be detected - cmd := nexec.Command("java", args...) - err := cmd.Limit(task.Resources) + cmd := executor.Command("java", args...) + err = cmd.Limit(task.Resources) if err != nil { return nil, fmt.Errorf("failed to constrain resources: %s", err) } @@ -159,7 +158,7 @@ func (d *JavaDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro } // Find the process - proc, err := os.FindProcess(pid) + cmd, err := executor.OpenPid(pid) if err != nil { return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) } diff --git a/client/exec/exec.go b/client/executor/exec.go similarity index 96% rename from client/exec/exec.go rename to client/executor/exec.go index d68c1d510..150f1e58c 100644 --- a/client/exec/exec.go +++ b/client/executor/exec.go @@ -15,7 +15,7 @@ // However, these should be completely transparent to the calling context. In // other words, the Java driver should be able to call exec for any platform and // just work. -package exec +package executor import ( "fmt" @@ -91,7 +91,7 @@ type cmd struct { // Command is a mirror of exec.Command that returns a platform-specific Executor func Command(name string, arg ...string) Executor { - executor := AutoselectExecutor() + executor := Default() cmd := executor.Command() cmd.Path = name cmd.Args = append([]string{name}, arg...) @@ -107,7 +107,7 @@ func Command(name string, arg ...string) Executor { } func OpenPid(pid int) (Executor, error) { - executor := AutoselectExecutor() + executor := Default() err := executor.Open(pid) if err != nil { return nil, err @@ -115,13 +115,13 @@ func OpenPid(pid int) (Executor, error) { return executor, nil } -// AutoselectExecutor uses capability testing to give you the best available +// Default uses capability testing to give you the best available // executor based on your platform and execution environment. If you need a // specific executor, call it directly. // // This is a simplistic strategy pattern. We can potentially improve this by // using a decorator pattern instead. -func AutoselectExecutor() Executor { +func Default() Executor { // These will be IN ORDER and the first available will be used, so preferred // ones should be at the top and fallbacks at the bottom. // TODO refactor this to be more lightweight. diff --git a/client/exec/exec_linux.go b/client/executor/linux.go similarity index 99% rename from client/exec/exec_linux.go rename to client/executor/linux.go index 3c641f2cf..5842dea14 100644 --- a/client/exec/exec_linux.go +++ b/client/executor/linux.go @@ -1,4 +1,4 @@ -package exec +package executor import ( "fmt" From c8516f3e9c4097d97c4cfd288462a4a7ae8324c4 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 16:40:40 -0700 Subject: [PATCH 10/12] Changed Default executor to use a factory --- client/executor/exec.go | 31 ++++++++++++++++----- client/executor/{linux.go => exec_linux.go} | 6 ++++ 2 files changed, 30 insertions(+), 7 deletions(-) rename client/executor/{linux.go => exec_linux.go} (98%) diff --git a/client/executor/exec.go b/client/executor/exec.go index 150f1e58c..ca86cd6f1 100644 --- a/client/executor/exec.go +++ b/client/executor/exec.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "sync" "github.com/hashicorp/nomad/nomad/structs" ) @@ -115,6 +116,23 @@ func OpenPid(pid int) (Executor, error) { return executor, nil } +// ExecutorFactory is an interface for a function that returns an Executor. This +// allows us to create Executors dynamically. +type ExecutorFactory func() Executor + +var executors []ExecutorFactory +var execFactoryMutex sync.Mutex + +// Register an ExecutorFactory so we can create it with Default() +func Register(executor ExecutorFactory) { + execFactoryMutex.Lock() + if executors == nil { + executors = []ExecutorFactory{} + } + executors = append(executors, executor) + execFactoryMutex.Unlock() +} + // Default uses capability testing to give you the best available // executor based on your platform and execution environment. If you need a // specific executor, call it directly. @@ -123,13 +141,12 @@ func OpenPid(pid int) (Executor, error) { // using a decorator pattern instead. func Default() Executor { // These will be IN ORDER and the first available will be used, so preferred - // ones should be at the top and fallbacks at the bottom. - // TODO refactor this to be more lightweight. - executors := []Executor{ - &LinuxExecutor{}, - } - - for _, executor := range executors { + // ones should be at the top and fallbacks at the bottom. Note that if these + // are added via init() calls then the order may be a be a bit mysterious + // even though it should be deterministic. + // TODO Make order more explicit + for _, factory := range executors { + executor := factory() if executor.Available() { return executor } diff --git a/client/executor/linux.go b/client/executor/exec_linux.go similarity index 98% rename from client/executor/linux.go rename to client/executor/exec_linux.go index 5842dea14..85ae5b65f 100644 --- a/client/executor/linux.go +++ b/client/executor/exec_linux.go @@ -44,6 +44,12 @@ func SetGID(command *cmd, groupid string) error { return nil } +func init() { + Register(func() Executor { + return &LinuxExecutor{} + }) +} + // Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as // a user you specify and limit resources using rlimit. type LinuxExecutor struct { From 06a0bd0bf71d325a5cdacb30f6a1e62a5156e030 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 18:54:55 -0700 Subject: [PATCH 11/12] Overhaul the executor code so we use build flags instead of dynamic loading --- client/executor/exec.go | 178 +++++++++--------------------- client/executor/exec_linux.go | 48 +------- client/executor/exec_universal.go | 69 ++++++++++++ 3 files changed, 127 insertions(+), 168 deletions(-) create mode 100644 client/executor/exec_universal.go diff --git a/client/executor/exec.go b/client/executor/exec.go index ca86cd6f1..b62ec399e 100644 --- a/client/executor/exec.go +++ b/client/executor/exec.go @@ -11,7 +11,7 @@ // process isolation, and security features, or otherwise take advantage of // features that are unique to that platform. // -// The semantics of any particular instance are left up to the implementation. +// The `semantics of any particular instance are left up to the implementation. // However, these should be completely transparent to the calling context. In // other words, the Java driver should be able to call exec for any platform and // just work. @@ -19,10 +19,10 @@ package executor import ( "fmt" - "os" "os/exec" "path/filepath" - "sync" + "strconv" + "syscall" "github.com/hashicorp/nomad/nomad/structs" ) @@ -31,11 +31,6 @@ import ( // wrapper must implement. You should not need to implement a Java executor. // Rather, you would implement a cgroups executor that the Java driver will use. type Executor interface { - // Available should return true or false based on whether the current platform - // can run this type of executor, based on capability testing. Returning - // true does not guarantee that this executor will be used. - Available() bool - // Limit must be called before Start and restricts the amount of resources // the process can use. Note that an error may be returned ONLY IF the // executor implements resource limiting. Otherwise Limit is ignored. @@ -49,7 +44,7 @@ type Executor interface { // Start the process. This may wrap the actual process in another command, // depending on the capabilities in this environment. Errors that arise from - // Limits or Runas will bubble through Start() + // Limits or Runas may bubble through Start() Start() error // Open should be called to restore a previous pid. This might be needed if @@ -71,11 +66,39 @@ type Executor interface { // implementations must provide this. ForceStop() error - // Access the underlying Cmd struct. This should never be nil. Also, this is - // not intended to be access outside the exec package, so YMMV. + // Command provides access the underlying Cmd struct in case the Executor + // interface doesn't expose the functionality you need. Command() *cmd } +// Command is a mirror of exec.Command that returns a platform-specific Executor +func Command(name string, arg ...string) Executor { + executor := NewExecutor() + cmd := executor.Command() + cmd.Path = name + cmd.Args = append([]string{name}, arg...) + + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err != nil { + // cmd.lookPathErr = err + } else { + cmd.Path = lp + } + } + return executor +} + +// OpenPid is similar to executor.Command but will initialize executor.Cmd with +// the Pid set to the one specified. +func OpenPid(pid int) (Executor, error) { + executor := NewExecutor() + err := executor.Open(pid) + if err != nil { + return nil, err + } + return executor, nil +} + // Cmd is an extension of exec.Cmd that incorporates functionality for // re-attaching to processes, dropping priviledges, etc., based on platform- // specific implementations. @@ -90,127 +113,34 @@ type cmd struct { RunAs string } -// Command is a mirror of exec.Command that returns a platform-specific Executor -func Command(name string, arg ...string) Executor { - executor := Default() - cmd := executor.Command() - cmd.Path = name - cmd.Args = append([]string{name}, arg...) - - if filepath.Base(name) == name { - if lp, err := exec.LookPath(name); err != nil { - // cmd.lookPathErr = err - } else { - cmd.Path = lp - } - } - return executor -} - -func OpenPid(pid int) (Executor, error) { - executor := Default() - err := executor.Open(pid) +// SetUID changes the Uid for this command (must be set before starting) +func (c *cmd) SetUID(userid string) error { + uid, err := strconv.ParseUint(userid, 10, 32) if err != nil { - return nil, err + return fmt.Errorf("Unable to convert userid to uint32: %s", err) } - return executor, nil -} - -// ExecutorFactory is an interface for a function that returns an Executor. This -// allows us to create Executors dynamically. -type ExecutorFactory func() Executor - -var executors []ExecutorFactory -var execFactoryMutex sync.Mutex - -// Register an ExecutorFactory so we can create it with Default() -func Register(executor ExecutorFactory) { - execFactoryMutex.Lock() - if executors == nil { - executors = []ExecutorFactory{} + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} } - executors = append(executors, executor) - execFactoryMutex.Unlock() -} - -// Default uses capability testing to give you the best available -// executor based on your platform and execution environment. If you need a -// specific executor, call it directly. -// -// This is a simplistic strategy pattern. We can potentially improve this by -// using a decorator pattern instead. -func Default() Executor { - // These will be IN ORDER and the first available will be used, so preferred - // ones should be at the top and fallbacks at the bottom. Note that if these - // are added via init() calls then the order may be a be a bit mysterious - // even though it should be deterministic. - // TODO Make order more explicit - for _, factory := range executors { - executor := factory() - if executor.Available() { - return executor - } + if c.SysProcAttr.Credential == nil { + c.SysProcAttr.Credential = &syscall.Credential{} } - - // Always return something, even if we don't have advanced capabilities. - return &UniversalExecutor{} -} - -// UniversalExecutor should work everywhere, and as a result does not include -// any resource restrictions or runas capabilities. -type UniversalExecutor struct { - cmd -} - -func (e *UniversalExecutor) Available() bool { - return true -} - -func (e *UniversalExecutor) Limit(resources *structs.Resources) error { - // No-op + c.SysProcAttr.Credential.Uid = uint32(uid) return nil } -func (e *UniversalExecutor) RunAs(userid string) error { - // No-op - return nil -} - -func (e *UniversalExecutor) Start() error { - // We don't want to call ourself. We want to call Start on our embedded Cmd - return e.cmd.Start() -} - -func (e *UniversalExecutor) Open(pid int) error { - process, err := os.FindProcess(pid) +// SetGID changes the Gid for this command (must be set before starting) +func (c *cmd) SetGID(groupid string) error { + gid, err := strconv.ParseUint(groupid, 10, 32) if err != nil { - return fmt.Errorf("Failed to reopen pid %d: %s", pid, err) + return fmt.Errorf("Unable to convert groupid to uint32: %s", err) } - e.Process = process + if c.SysProcAttr == nil { + c.SysProcAttr = &syscall.SysProcAttr{} + } + if c.SysProcAttr.Credential == nil { + c.SysProcAttr.Credential = &syscall.Credential{} + } + c.SysProcAttr.Credential.Uid = uint32(gid) return nil } - -func (e *UniversalExecutor) Wait() error { - // We don't want to call ourself. We want to call Start on our embedded Cmd - return e.cmd.Wait() -} - -func (e *UniversalExecutor) Pid() (int, error) { - if e.cmd.Process != nil { - return e.cmd.Process.Pid, nil - } else { - return 0, fmt.Errorf("Process has finished or was never started") - } -} - -func (e *UniversalExecutor) Shutdown() error { - return e.ForceStop() -} - -func (e *UniversalExecutor) ForceStop() error { - return e.Process.Kill() -} - -func (e *UniversalExecutor) Command() *cmd { - return &e.cmd -} diff --git a/client/executor/exec_linux.go b/client/executor/exec_linux.go index 85ae5b65f..567e7a84b 100644 --- a/client/executor/exec_linux.go +++ b/client/executor/exec_linux.go @@ -4,50 +4,14 @@ import ( "fmt" "os" "os/user" - "runtime" - "strconv" "syscall" "github.com/hashicorp/go-multierror" "github.com/hashicorp/nomad/nomad/structs" ) -// SetUID changes the Uid for this command (must be set before starting) -func SetUID(command *cmd, userid string) error { - uid, err := strconv.ParseUint(userid, 10, 32) - if err != nil { - return fmt.Errorf("Unable to convert userid to uint32: %s", err) - } - if command.SysProcAttr == nil { - command.SysProcAttr = &syscall.SysProcAttr{} - } - if command.SysProcAttr.Credential == nil { - command.SysProcAttr.Credential = &syscall.Credential{} - } - command.SysProcAttr.Credential.Uid = uint32(uid) - return nil -} - -// SetGID changes the Gid for this command (must be set before starting) -func SetGID(command *cmd, groupid string) error { - gid, err := strconv.ParseUint(groupid, 10, 32) - if err != nil { - return fmt.Errorf("Unable to convert groupid to uint32: %s", err) - } - if command.SysProcAttr == nil { - command.SysProcAttr = &syscall.SysProcAttr{} - } - if command.SysProcAttr.Credential == nil { - command.SysProcAttr.Credential = &syscall.Credential{} - } - command.SysProcAttr.Credential.Uid = uint32(gid) - return nil -} - -func init() { - Register(func() Executor { - return &LinuxExecutor{} - }) +func NewExecutor() Executor { + return &LinuxExecutor{} } // Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as @@ -57,10 +21,6 @@ type LinuxExecutor struct { user *user.User } -func (e *LinuxExecutor) Available() bool { - return runtime.GOOS == "linux" -} - func (e *LinuxExecutor) Limit(resources *structs.Resources) error { // TODO limit some things return nil @@ -101,8 +61,8 @@ func (e *LinuxExecutor) Start() error { // Set the user and group this process should run as if e.user != nil { - SetUID(&e.cmd, e.user.Uid) - SetGID(&e.cmd, e.user.Gid) + e.cmd.SetUID(e.user.Uid) + e.cmd.SetGID(e.user.Gid) } // We don't want to call ourself. We want to call Start on our embedded Cmd. diff --git a/client/executor/exec_universal.go b/client/executor/exec_universal.go new file mode 100644 index 000000000..109fc86fd --- /dev/null +++ b/client/executor/exec_universal.go @@ -0,0 +1,69 @@ +// +build !linux + +package executor + +import ( + "fmt" + "os" + + "github.com/hashicorp/nomad/nomad/structs" +) + +func NewExecutor() Executor { + return &UniversalExecutor{} +} + +// UniversalExecutor should work everywhere, and as a result does not include +// any resource restrictions or runas capabilities. +type UniversalExecutor struct { + cmd +} + +func (e *UniversalExecutor) Limit(resources *structs.Resources) error { + // No-op + return nil +} + +func (e *UniversalExecutor) RunAs(userid string) error { + // No-op + return nil +} + +func (e *UniversalExecutor) Start() error { + // We don't want to call ourself. We want to call Start on our embedded Cmd + return e.cmd.Start() +} + +func (e *UniversalExecutor) Open(pid int) error { + process, err := os.FindProcess(pid) + if err != nil { + return fmt.Errorf("Failed to reopen pid %d: %s", pid, err) + } + e.Process = process + return nil +} + +func (e *UniversalExecutor) Wait() error { + // We don't want to call ourself. We want to call Start on our embedded Cmd + return e.cmd.Wait() +} + +func (e *UniversalExecutor) Pid() (int, error) { + if e.cmd.Process != nil { + return e.cmd.Process.Pid, nil + } else { + return 0, fmt.Errorf("Process has finished or was never started") + } +} + +func (e *UniversalExecutor) Shutdown() error { + return e.ForceStop() +} + +func (e *UniversalExecutor) ForceStop() error { + return e.Process.Kill() +} + +func (e *UniversalExecutor) Command() *cmd { + return &e.cmd +} From cdd065cb07155e6f54166ad727d87c577ab78268 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 15 Sep 2015 20:17:23 -0700 Subject: [PATCH 12/12] Support running Linux exec as a non-root user --- client/executor/exec_linux.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/client/executor/exec_linux.go b/client/executor/exec_linux.go index 567e7a84b..be128b156 100644 --- a/client/executor/exec_linux.go +++ b/client/executor/exec_linux.go @@ -14,8 +14,7 @@ func NewExecutor() Executor { return &LinuxExecutor{} } -// Linux executor is designed to run on linux kernel 2.8+. It will fork/exec as -// a user you specify and limit resources using rlimit. +// Linux executor is designed to run on linux kernel 2.8+. type LinuxExecutor struct { cmd user *user.User @@ -53,13 +52,17 @@ func (e *LinuxExecutor) RunAs(userid string) error { } func (e *LinuxExecutor) Start() error { - if e.user == nil { - // If no user has been specified, try to run as "nobody" user so we - // don't leak root privilege to the spawned process. + // If no user has been specified, try to run as "nobody" user so we don't + // leak root privilege to the spawned process. Note that we will only do + // this if we can call SetUID. Otherwise we'll just run the other process + // as our current (non-root) user. This makes testing easier and also means + // we aren't forced to run nomad as root. + if e.user == nil && canSetUID() { e.RunAs("nobody") } - // Set the user and group this process should run as + // Set the user and group this process should run as. If RunAs was called + // but we are not root this will cause Start to fail. This is intentional. if e.user != nil { e.cmd.SetUID(e.user.Uid) e.cmd.SetGID(e.user.Gid) @@ -112,3 +115,23 @@ func (e *LinuxExecutor) ForceStop() error { func (e *LinuxExecutor) Command() *cmd { return &e.cmd } + +// canSetUID will tell us whether we're capable of using SetUID. If we are not +// rootish this command will fail. In that case we'll just run the forked +// process under our own user. +func canSetUID() bool { + checkroot := Command("true") + u, err := user.Current() + if err != nil { + return false + } + + // Make sure RunAs is explicitly set so we don't cause infinite recursion. + checkroot.RunAs(u.Uid) + + err = checkroot.Start() + if err != nil { + return false + } + return true +}