2015-09-20 01:47:04 +00:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2015-10-28 23:23:33 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2015-09-20 01:47:04 +00:00
|
|
|
"os"
|
2015-10-28 23:23:33 +00:00
|
|
|
"os/exec"
|
|
|
|
"strconv"
|
2015-09-20 01:47:04 +00:00
|
|
|
"strings"
|
2015-10-28 23:23:33 +00:00
|
|
|
"syscall"
|
2015-09-20 01:47:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type SpawnDaemonCommand struct {
|
|
|
|
Meta
|
2015-10-28 23:23:33 +00:00
|
|
|
config *DaemonConfig
|
|
|
|
exitFile io.WriteCloser
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *SpawnDaemonCommand) Help() string {
|
|
|
|
helpText := `
|
|
|
|
Usage: nomad spawn-daemon [options] <daemon_config>
|
|
|
|
|
|
|
|
INTERNAL ONLY
|
|
|
|
|
|
|
|
Spawns a daemon process by double forking. The required daemon_config is a
|
|
|
|
json encoding of the DaemonConfig struct containing the isolation
|
|
|
|
configuration and command to run. SpawnStartStatus is json serialized to
|
|
|
|
stdout upon running the user command or if any error prevents its execution.
|
|
|
|
If there is no error, the process waits on the users command. Once the user
|
|
|
|
command exits, the exit code is written to a file specified in the
|
|
|
|
daemon_config and this process exits with the same exit status as the user
|
|
|
|
command.
|
|
|
|
`
|
|
|
|
|
|
|
|
return strings.TrimSpace(helpText)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *SpawnDaemonCommand) Synopsis() string {
|
|
|
|
return "Spawn a daemon command with configurable isolation."
|
2015-09-20 01:47:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Status of executing the user's command.
|
|
|
|
type SpawnStartStatus struct {
|
2015-10-28 23:23:33 +00:00
|
|
|
// The PID of the user's command.
|
|
|
|
UserPID int
|
|
|
|
|
2015-09-20 01:47:04 +00:00
|
|
|
// ErrorMsg will be empty if the user command was started successfully.
|
|
|
|
// Otherwise it will have an error message.
|
|
|
|
ErrorMsg string
|
|
|
|
}
|
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// Exit status of the user's command.
|
|
|
|
type SpawnExitStatus struct {
|
|
|
|
// The exit code of the user's command.
|
|
|
|
ExitCode int
|
|
|
|
}
|
2015-09-20 01:47:04 +00:00
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// Configuration for the command to start as a daemon.
|
|
|
|
type DaemonConfig struct {
|
|
|
|
exec.Cmd
|
2015-09-20 01:47:04 +00:00
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// The filepath to write the exit status to.
|
|
|
|
ExitStatusFile string
|
2015-09-20 01:47:04 +00:00
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// The paths, if not /dev/null, must be either in the tasks root directory
|
|
|
|
// or in the shared alloc directory.
|
|
|
|
StdoutFile string
|
|
|
|
StdinFile string
|
|
|
|
StderrFile string
|
2015-09-20 01:47:04 +00:00
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// An optional path specifying the directory to chroot the process in.
|
|
|
|
Chroot string
|
|
|
|
}
|
2015-09-20 01:47:04 +00:00
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// Whether to start the user command or abort.
|
|
|
|
type TaskStart bool
|
|
|
|
|
|
|
|
// parseConfig reads the DaemonConfig from the passed arguments. If not
|
|
|
|
// successful, an error is returned.
|
|
|
|
func (c *SpawnDaemonCommand) parseConfig(args []string) (*DaemonConfig, error) {
|
|
|
|
flags := c.Meta.FlagSet("spawn-daemon", FlagSetClient)
|
|
|
|
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse args: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that we got json input.
|
|
|
|
args = flags.Args()
|
|
|
|
if len(args) != 1 {
|
|
|
|
return nil, fmt.Errorf("incorrect number of args; got %v; want 1", len(args))
|
|
|
|
}
|
|
|
|
jsonInput, err := strconv.Unquote(args[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Failed to unquote json input: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// De-serialize the passed command.
|
|
|
|
var config DaemonConfig
|
|
|
|
dec := json.NewDecoder(strings.NewReader(jsonInput))
|
|
|
|
if err := dec.Decode(&config); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &config, nil
|
2015-09-20 01:47:04 +00:00
|
|
|
}
|
|
|
|
|
2015-10-28 23:23:33 +00:00
|
|
|
// configureLogs creates the log files and redirects the process
|
|
|
|
// stdin/stderr/stdout to them. If unsuccessful, an error is returned.
|
|
|
|
func (c *SpawnDaemonCommand) configureLogs() error {
|
2015-11-03 04:28:37 +00:00
|
|
|
if len(c.config.StdoutFile) != 0 {
|
|
|
|
stdo, err := os.OpenFile(c.config.StdoutFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error opening file to redirect stdout: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.config.Cmd.Stdout = stdo
|
2015-10-28 23:23:33 +00:00
|
|
|
}
|
|
|
|
|
2015-11-03 04:28:37 +00:00
|
|
|
if len(c.config.StderrFile) != 0 {
|
|
|
|
stde, err := os.OpenFile(c.config.StderrFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error opening file to redirect stderr: %v", err)
|
|
|
|
}
|
|
|
|
c.config.Cmd.Stderr = stde
|
2015-10-28 23:23:33 +00:00
|
|
|
}
|
|
|
|
|
2015-11-03 04:28:37 +00:00
|
|
|
if len(c.config.StdinFile) != 0 {
|
|
|
|
stdi, err := os.OpenFile(c.config.StdinFile, os.O_CREATE|os.O_RDONLY, 0666)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error opening file to redirect stdin: %v", err)
|
|
|
|
}
|
|
|
|
c.config.Cmd.Stdin = stdi
|
2015-10-28 23:23:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *SpawnDaemonCommand) Run(args []string) int {
|
|
|
|
var err error
|
|
|
|
c.config, err = c.parseConfig(args)
|
|
|
|
if err != nil {
|
|
|
|
return c.outputStartStatus(err, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open the file we will be using to write exit codes to. We do this early
|
|
|
|
// to ensure that we don't start the user process when we can't capture its
|
|
|
|
// exit status.
|
2015-11-03 04:28:37 +00:00
|
|
|
c.exitFile, err = os.OpenFile(c.config.ExitStatusFile, os.O_WRONLY, 0666)
|
2015-10-28 23:23:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return c.outputStartStatus(fmt.Errorf("Error opening file to store exit status: %v", err), 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Isolate the user process.
|
|
|
|
if err := c.isolateCmd(); err != nil {
|
|
|
|
return c.outputStartStatus(err, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Redirect logs.
|
|
|
|
if err := c.configureLogs(); err != nil {
|
|
|
|
return c.outputStartStatus(err, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Chroot jail the process and set its working directory.
|
|
|
|
c.configureChroot()
|
|
|
|
|
|
|
|
// Wait to get the start command.
|
|
|
|
var start TaskStart
|
|
|
|
dec := json.NewDecoder(os.Stdin)
|
|
|
|
if err := dec.Decode(&start); err != nil {
|
|
|
|
return c.outputStartStatus(err, 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Aborted by Nomad process.
|
|
|
|
if !start {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// Spawn the user process.
|
|
|
|
if err := c.config.Cmd.Start(); err != nil {
|
|
|
|
return c.outputStartStatus(fmt.Errorf("Error starting user command: %v", err), 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Indicate that the command was started successfully.
|
|
|
|
c.outputStartStatus(nil, 0)
|
|
|
|
|
|
|
|
// Wait and then output the exit status.
|
|
|
|
return c.writeExitStatus(c.config.Cmd.Wait())
|
2015-09-20 01:47:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// outputStartStatus is a helper function that outputs a SpawnStartStatus to
|
|
|
|
// Stdout with the passed error, which may be nil to indicate no error. It
|
|
|
|
// returns the passed status.
|
2015-09-21 22:24:08 +00:00
|
|
|
func (c *SpawnDaemonCommand) outputStartStatus(err error, status int) int {
|
2015-09-20 01:47:04 +00:00
|
|
|
startStatus := &SpawnStartStatus{}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
startStatus.ErrorMsg = err.Error()
|
|
|
|
}
|
|
|
|
|
2015-11-03 04:28:37 +00:00
|
|
|
if c.config != nil && c.config.Cmd.Process != nil {
|
2015-10-28 23:23:33 +00:00
|
|
|
startStatus.UserPID = c.config.Process.Pid
|
|
|
|
}
|
|
|
|
|
2015-09-20 01:47:04 +00:00
|
|
|
enc.Encode(startStatus)
|
|
|
|
return status
|
|
|
|
}
|
2015-10-28 23:23:33 +00:00
|
|
|
|
|
|
|
// writeExitStatus takes in the error result from calling wait and writes out
|
|
|
|
// the exit status to a file. It returns the same exit status as the user
|
|
|
|
// command.
|
|
|
|
func (c *SpawnDaemonCommand) writeExitStatus(exit error) int {
|
|
|
|
// Parse the exit code.
|
|
|
|
exitStatus := &SpawnExitStatus{}
|
|
|
|
if exit != nil {
|
|
|
|
// Default to exit code 1 if we can not get the actual exit code.
|
|
|
|
exitStatus.ExitCode = 1
|
|
|
|
|
|
|
|
if exiterr, ok := exit.(*exec.ExitError); ok {
|
|
|
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
|
|
exitStatus.ExitCode = status.ExitStatus()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.exitFile != nil {
|
|
|
|
enc := json.NewEncoder(c.exitFile)
|
|
|
|
enc.Encode(exitStatus)
|
|
|
|
c.exitFile.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
return exitStatus.ExitCode
|
|
|
|
}
|