github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/loggo"
    15  	utilexec "github.com/juju/utils/exec"
    16  
    17  	"github.com/juju/juju/worker/uniter/runner/context"
    18  	"github.com/juju/juju/worker/uniter/runner/debug"
    19  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    20  	jujuos "github.com/juju/utils/os"
    21  )
    22  
    23  var logger = loggo.GetLogger("juju.worker.uniter.runner")
    24  
    25  // Runner is responsible for invoking commands in a context.
    26  type Runner interface {
    27  
    28  	// Context returns the context against which the runner executes.
    29  	Context() Context
    30  
    31  	// RunHook executes the hook with the supplied name.
    32  	RunHook(name string) error
    33  
    34  	// RunAction executes the action with the supplied name.
    35  	RunAction(name string) error
    36  
    37  	// RunCommands executes the supplied script.
    38  	RunCommands(commands string) (*utilexec.ExecResponse, error)
    39  }
    40  
    41  // Context exposes jujuc.Context, and additional methods needed by Runner.
    42  type Context interface {
    43  	jujuc.Context
    44  	Id() string
    45  	HookVars(paths context.Paths) ([]string, error)
    46  	ActionData() (*context.ActionData, error)
    47  	SetProcess(process *os.Process)
    48  	HasExecutionSetUnitStatus() bool
    49  	ResetExecutionSetUnitStatus()
    50  
    51  	Prepare() error
    52  	Flush(badge string, failure error) error
    53  }
    54  
    55  // NewRunner returns a Runner backed by the supplied context and paths.
    56  func NewRunner(context Context, paths context.Paths) Runner {
    57  	return &runner{context, paths}
    58  }
    59  
    60  // runner implements Runner.
    61  type runner struct {
    62  	context Context
    63  	paths   context.Paths
    64  }
    65  
    66  func (runner *runner) Context() Context {
    67  	return runner.context
    68  }
    69  
    70  // RunCommands exists to satisfy the Runner interface.
    71  func (runner *runner) RunCommands(commands string) (*utilexec.ExecResponse, error) {
    72  	srv, err := runner.startJujucServer()
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	defer srv.Close()
    77  
    78  	env, err := runner.context.HookVars(runner.paths)
    79  	if err != nil {
    80  		return nil, errors.Trace(err)
    81  	}
    82  	command := utilexec.RunParams{
    83  		Commands:    commands,
    84  		WorkingDir:  runner.paths.GetCharmDir(),
    85  		Environment: env,
    86  	}
    87  
    88  	err = command.Run()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	runner.context.SetProcess(command.Process())
    93  
    94  	// Block and wait for process to finish
    95  	result, err := command.Wait()
    96  	return result, runner.context.Flush("run commands", err)
    97  }
    98  
    99  // RunAction exists to satisfy the Runner interface.
   100  func (runner *runner) RunAction(actionName string) error {
   101  	if _, err := runner.context.ActionData(); err != nil {
   102  		return errors.Trace(err)
   103  	}
   104  	return runner.runCharmHookWithLocation(actionName, "actions")
   105  }
   106  
   107  // RunHook exists to satisfy the Runner interface.
   108  func (runner *runner) RunHook(hookName string) error {
   109  	return runner.runCharmHookWithLocation(hookName, "hooks")
   110  }
   111  
   112  func (runner *runner) runCharmHookWithLocation(hookName, charmLocation string) error {
   113  	srv, err := runner.startJujucServer()
   114  	if err != nil {
   115  		return err
   116  	}
   117  	defer srv.Close()
   118  
   119  	env, err := runner.context.HookVars(runner.paths)
   120  	if err != nil {
   121  		return errors.Trace(err)
   122  	}
   123  	if jujuos.HostOS() == jujuos.Windows {
   124  		// TODO(fwereade): somehow consolidate with utils/exec?
   125  		// We don't do this on the other code path, which uses exec.RunCommands,
   126  		// because that already has handling for windows environment requirements.
   127  		env = mergeWindowsEnvironment(env, os.Environ())
   128  	}
   129  
   130  	debugctx := debug.NewHooksContext(runner.context.UnitName())
   131  	if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) {
   132  		logger.Infof("executing %s via debug-hooks", hookName)
   133  		err = session.RunHook(hookName, runner.paths.GetCharmDir(), env)
   134  	} else {
   135  		err = runner.runCharmHook(hookName, env, charmLocation)
   136  	}
   137  	return runner.context.Flush(hookName, err)
   138  }
   139  
   140  func (runner *runner) runCharmHook(hookName string, env []string, charmLocation string) error {
   141  	charmDir := runner.paths.GetCharmDir()
   142  	hook, err := searchHook(charmDir, filepath.Join(charmLocation, hookName))
   143  	if err != nil {
   144  		if context.IsMissingHookError(err) {
   145  			// Missing hook is perfectly valid, but worth mentioning.
   146  			logger.Infof("skipped %q hook (not implemented)", hookName)
   147  		}
   148  		return err
   149  	}
   150  	hookCmd := hookCommand(hook)
   151  	ps := exec.Command(hookCmd[0], hookCmd[1:]...)
   152  	ps.Env = env
   153  	ps.Dir = charmDir
   154  	outReader, outWriter, err := os.Pipe()
   155  	if err != nil {
   156  		return errors.Errorf("cannot make logging pipe: %v", err)
   157  	}
   158  	ps.Stdout = outWriter
   159  	ps.Stderr = outWriter
   160  	hookLogger := &hookLogger{
   161  		r:      outReader,
   162  		done:   make(chan struct{}),
   163  		logger: runner.getLogger(hookName),
   164  	}
   165  	go hookLogger.run()
   166  	err = ps.Start()
   167  	outWriter.Close()
   168  	if err == nil {
   169  		// Record the *os.Process of the hook
   170  		runner.context.SetProcess(ps.Process)
   171  		// Block until execution finishes
   172  		err = ps.Wait()
   173  	}
   174  	hookLogger.stop()
   175  	return errors.Trace(err)
   176  }
   177  
   178  func (runner *runner) startJujucServer() (*jujuc.Server, error) {
   179  	// Prepare server.
   180  	getCmd := func(ctxId, cmdName string) (cmd.Command, error) {
   181  		if ctxId != runner.context.Id() {
   182  			return nil, errors.Errorf("expected context id %q, got %q", runner.context.Id(), ctxId)
   183  		}
   184  		return jujuc.NewCommand(runner.context, cmdName)
   185  	}
   186  	srv, err := jujuc.NewServer(getCmd, runner.paths.GetJujucSocket())
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	go srv.Run()
   191  	return srv, nil
   192  }
   193  
   194  func (runner *runner) getLogger(hookName string) loggo.Logger {
   195  	return loggo.GetLogger(fmt.Sprintf("unit.%s.%s", runner.context.UnitName(), hookName))
   196  }