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 }