github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/worker/uniter/runner/runner.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package runner
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"time"
    13  	"unicode/utf8"
    14  
    15  	"github.com/juju/cmd"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/utils/clock"
    19  	utilexec "github.com/juju/utils/exec"
    20  
    21  	"github.com/juju/juju/core/actions"
    22  	"github.com/juju/juju/worker/uniter/runner/context"
    23  	"github.com/juju/juju/worker/uniter/runner/debug"
    24  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    25  	jujuos "github.com/juju/utils/os"
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.worker.uniter.runner")
    29  
    30  // Runner is responsible for invoking commands in a context.
    31  type Runner interface {
    32  
    33  	// Context returns the context against which the runner executes.
    34  	Context() Context
    35  
    36  	// RunHook executes the hook with the supplied name.
    37  	RunHook(name string) error
    38  
    39  	// RunAction executes the action with the supplied name.
    40  	RunAction(name string) error
    41  
    42  	// RunCommands executes the supplied script.
    43  	RunCommands(commands string) (*utilexec.ExecResponse, error)
    44  }
    45  
    46  // Context exposes jujuc.Context, and additional methods needed by Runner.
    47  type Context interface {
    48  	jujuc.Context
    49  	Id() string
    50  	HookVars(paths context.Paths) ([]string, error)
    51  	ActionData() (*context.ActionData, error)
    52  	SetProcess(process context.HookProcess)
    53  	HasExecutionSetUnitStatus() bool
    54  	ResetExecutionSetUnitStatus()
    55  
    56  	Prepare() error
    57  	Flush(badge string, failure error) error
    58  }
    59  
    60  // NewRunner returns a Runner backed by the supplied context and paths.
    61  func NewRunner(context Context, paths context.Paths) Runner {
    62  	return &runner{context, paths}
    63  }
    64  
    65  // runner implements Runner.
    66  type runner struct {
    67  	context Context
    68  	paths   context.Paths
    69  }
    70  
    71  func (runner *runner) Context() Context {
    72  	return runner.context
    73  }
    74  
    75  // RunCommands exists to satisfy the Runner interface.
    76  func (runner *runner) RunCommands(commands string) (*utilexec.ExecResponse, error) {
    77  	result, err := runner.runCommandsWithTimeout(commands, 0, clock.WallClock)
    78  	return result, runner.context.Flush("run commands", err)
    79  }
    80  
    81  // runCommandsWithTimeout is a helper to abstract common code between run commands and
    82  // juju-run as an action
    83  func (runner *runner) runCommandsWithTimeout(commands string, timeout time.Duration, clock clock.Clock) (*utilexec.ExecResponse, error) {
    84  	srv, err := runner.startJujucServer()
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	defer srv.Close()
    89  
    90  	env, err := runner.context.HookVars(runner.paths)
    91  	if err != nil {
    92  		return nil, errors.Trace(err)
    93  	}
    94  	command := utilexec.RunParams{
    95  		Commands:    commands,
    96  		WorkingDir:  runner.paths.GetCharmDir(),
    97  		Environment: env,
    98  		Clock:       clock,
    99  	}
   100  
   101  	err = command.Run()
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	runner.context.SetProcess(hookProcess{command.Process()})
   106  
   107  	var cancel chan struct{}
   108  	if timeout != 0 {
   109  		cancel = make(chan struct{})
   110  		go func() {
   111  			<-clock.After(timeout)
   112  			close(cancel)
   113  		}()
   114  	}
   115  
   116  	// Block and wait for process to finish
   117  	return command.WaitWithCancel(cancel)
   118  }
   119  
   120  // runJujuRunAction is the function that executes when a juju-run action is ran.
   121  func (runner *runner) runJujuRunAction() (err error) {
   122  	params, err := runner.context.ActionParams()
   123  	if err != nil {
   124  		return errors.Trace(err)
   125  	}
   126  	command, ok := params["command"].(string)
   127  	if !ok {
   128  		return errors.New("no command parameter to juju-run action")
   129  	}
   130  
   131  	// The timeout is passed in in nanoseconds(which are represented in go as int64)
   132  	// But due to serialization it comes out as float64
   133  	timeout, ok := params["timeout"].(float64)
   134  	if !ok {
   135  		logger.Debugf("unable to read juju-run action timeout, will continue running action without one")
   136  	}
   137  
   138  	results, err := runner.runCommandsWithTimeout(command, time.Duration(timeout), clock.WallClock)
   139  
   140  	if err != nil {
   141  		return runner.context.Flush("juju-run", err)
   142  	}
   143  
   144  	if err := runner.updateActionResults(results); err != nil {
   145  		return runner.context.Flush("juju-run", err)
   146  	}
   147  
   148  	return runner.context.Flush("juju-run", nil)
   149  }
   150  
   151  func encodeBytes(input []byte) (value string, encoding string) {
   152  	if utf8.Valid(input) {
   153  		value = string(input)
   154  		encoding = "utf8"
   155  	} else {
   156  		value = base64.StdEncoding.EncodeToString(input)
   157  		encoding = "base64"
   158  	}
   159  	return value, encoding
   160  }
   161  
   162  func (runner *runner) updateActionResults(results *utilexec.ExecResponse) error {
   163  	if err := runner.context.UpdateActionResults([]string{"Code"}, fmt.Sprintf("%d", results.Code)); err != nil {
   164  		return errors.Trace(err)
   165  	}
   166  
   167  	stdout, encoding := encodeBytes(results.Stdout)
   168  	if err := runner.context.UpdateActionResults([]string{"Stdout"}, stdout); err != nil {
   169  		return errors.Trace(err)
   170  	}
   171  	if encoding != "utf8" {
   172  		if err := runner.context.UpdateActionResults([]string{"StdoutEncoding"}, encoding); err != nil {
   173  			return errors.Trace(err)
   174  		}
   175  	}
   176  
   177  	stderr, encoding := encodeBytes(results.Stderr)
   178  	if err := runner.context.UpdateActionResults([]string{"Stderr"}, stderr); err != nil {
   179  		return errors.Trace(err)
   180  	}
   181  	if encoding != "utf8" {
   182  		if err := runner.context.UpdateActionResults([]string{"StderrEncoding"}, encoding); err != nil {
   183  			return errors.Trace(err)
   184  		}
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  // RunAction exists to satisfy the Runner interface.
   191  func (runner *runner) RunAction(actionName string) error {
   192  	if _, err := runner.context.ActionData(); err != nil {
   193  		return errors.Trace(err)
   194  	}
   195  	if actionName == actions.JujuRunActionName {
   196  		return runner.runJujuRunAction()
   197  	}
   198  	return runner.runCharmHookWithLocation(actionName, "actions")
   199  }
   200  
   201  // RunHook exists to satisfy the Runner interface.
   202  func (runner *runner) RunHook(hookName string) error {
   203  	return runner.runCharmHookWithLocation(hookName, "hooks")
   204  }
   205  
   206  func (runner *runner) runCharmHookWithLocation(hookName, charmLocation string) error {
   207  	srv, err := runner.startJujucServer()
   208  	if err != nil {
   209  		return err
   210  	}
   211  	defer srv.Close()
   212  
   213  	env, err := runner.context.HookVars(runner.paths)
   214  	if err != nil {
   215  		return errors.Trace(err)
   216  	}
   217  	if jujuos.HostOS() == jujuos.Windows {
   218  		// TODO(fwereade): somehow consolidate with utils/exec?
   219  		// We don't do this on the other code path, which uses exec.RunCommands,
   220  		// because that already has handling for windows environment requirements.
   221  		env = mergeWindowsEnvironment(env, os.Environ())
   222  	}
   223  
   224  	debugctx := debug.NewHooksContext(runner.context.UnitName())
   225  	if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) {
   226  		logger.Infof("executing %s via debug-hooks", hookName)
   227  		err = session.RunHook(hookName, runner.paths.GetCharmDir(), env)
   228  	} else {
   229  		err = runner.runCharmHook(hookName, env, charmLocation)
   230  	}
   231  	return runner.context.Flush(hookName, err)
   232  }
   233  
   234  func (runner *runner) runCharmHook(hookName string, env []string, charmLocation string) error {
   235  	charmDir := runner.paths.GetCharmDir()
   236  	hook, err := searchHook(charmDir, filepath.Join(charmLocation, hookName))
   237  	if err != nil {
   238  		return err
   239  	}
   240  	hookCmd := hookCommand(hook)
   241  	ps := exec.Command(hookCmd[0], hookCmd[1:]...)
   242  	ps.Env = env
   243  	ps.Dir = charmDir
   244  	outReader, outWriter, err := os.Pipe()
   245  	if err != nil {
   246  		return errors.Errorf("cannot make logging pipe: %v", err)
   247  	}
   248  	ps.Stdout = outWriter
   249  	ps.Stderr = outWriter
   250  	hookLogger := &hookLogger{
   251  		r:      outReader,
   252  		done:   make(chan struct{}),
   253  		logger: runner.getLogger(hookName),
   254  	}
   255  	go hookLogger.run()
   256  	err = ps.Start()
   257  	outWriter.Close()
   258  	if err == nil {
   259  		// Record the *os.Process of the hook
   260  		runner.context.SetProcess(hookProcess{ps.Process})
   261  		// Block until execution finishes
   262  		err = ps.Wait()
   263  	}
   264  	hookLogger.stop()
   265  	return errors.Trace(err)
   266  }
   267  
   268  func (runner *runner) startJujucServer() (*jujuc.Server, error) {
   269  	// Prepare server.
   270  	getCmd := func(ctxId, cmdName string) (cmd.Command, error) {
   271  		if ctxId != runner.context.Id() {
   272  			return nil, errors.Errorf("expected context id %q, got %q", runner.context.Id(), ctxId)
   273  		}
   274  		return jujuc.NewCommand(runner.context, cmdName)
   275  	}
   276  	srv, err := jujuc.NewServer(getCmd, runner.paths.GetJujucSocket())
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	go srv.Run()
   281  	return srv, nil
   282  }
   283  
   284  func (runner *runner) getLogger(hookName string) loggo.Logger {
   285  	return loggo.GetLogger(fmt.Sprintf("unit.%s.%s", runner.context.UnitName(), hookName))
   286  }
   287  
   288  type hookProcess struct {
   289  	*os.Process
   290  }
   291  
   292  func (p hookProcess) Pid() int {
   293  	return p.Process.Pid
   294  }