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