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