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