github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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 "encoding/base64" 8 "fmt" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "time" 13 "unicode/utf8" 14 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/loggo" 18 "github.com/juju/utils/clock" 19 utilexec "github.com/juju/utils/exec" 20 21 "github.com/juju/juju/core/actions" 22 "github.com/juju/juju/worker/uniter/runner/context" 23 "github.com/juju/juju/worker/uniter/runner/debug" 24 "github.com/juju/juju/worker/uniter/runner/jujuc" 25 jujuos "github.com/juju/utils/os" 26 ) 27 28 var logger = loggo.GetLogger("juju.worker.uniter.runner") 29 30 // Runner is responsible for invoking commands in a context. 31 type Runner interface { 32 33 // Context returns the context against which the runner executes. 34 Context() Context 35 36 // RunHook executes the hook with the supplied name. 37 RunHook(name string) error 38 39 // RunAction executes the action with the supplied name. 40 RunAction(name string) error 41 42 // RunCommands executes the supplied script. 43 RunCommands(commands string) (*utilexec.ExecResponse, error) 44 } 45 46 // Context exposes jujuc.Context, and additional methods needed by Runner. 47 type Context interface { 48 jujuc.Context 49 Id() string 50 HookVars(paths context.Paths) ([]string, error) 51 ActionData() (*context.ActionData, error) 52 SetProcess(process context.HookProcess) 53 HasExecutionSetUnitStatus() bool 54 ResetExecutionSetUnitStatus() 55 56 Prepare() error 57 Flush(badge string, failure error) error 58 } 59 60 // NewRunner returns a Runner backed by the supplied context and paths. 61 func NewRunner(context Context, paths context.Paths) Runner { 62 return &runner{context, paths} 63 } 64 65 // runner implements Runner. 66 type runner struct { 67 context Context 68 paths context.Paths 69 } 70 71 func (runner *runner) Context() Context { 72 return runner.context 73 } 74 75 // RunCommands exists to satisfy the Runner interface. 76 func (runner *runner) RunCommands(commands string) (*utilexec.ExecResponse, error) { 77 result, err := runner.runCommandsWithTimeout(commands, 0, clock.WallClock) 78 return result, runner.context.Flush("run commands", err) 79 } 80 81 // runCommandsWithTimeout is a helper to abstract common code between run commands and 82 // juju-run as an action 83 func (runner *runner) runCommandsWithTimeout(commands string, timeout time.Duration, clock clock.Clock) (*utilexec.ExecResponse, error) { 84 srv, err := runner.startJujucServer() 85 if err != nil { 86 return nil, err 87 } 88 defer srv.Close() 89 90 env, err := runner.context.HookVars(runner.paths) 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 command := utilexec.RunParams{ 95 Commands: commands, 96 WorkingDir: runner.paths.GetCharmDir(), 97 Environment: env, 98 Clock: clock, 99 } 100 101 err = command.Run() 102 if err != nil { 103 return nil, err 104 } 105 runner.context.SetProcess(hookProcess{command.Process()}) 106 107 var cancel chan struct{} 108 if timeout != 0 { 109 cancel = make(chan struct{}) 110 go func() { 111 <-clock.After(timeout) 112 close(cancel) 113 }() 114 } 115 116 // Block and wait for process to finish 117 return command.WaitWithCancel(cancel) 118 } 119 120 // runJujuRunAction is the function that executes when a juju-run action is ran. 121 func (runner *runner) runJujuRunAction() (err error) { 122 params, err := runner.context.ActionParams() 123 if err != nil { 124 return errors.Trace(err) 125 } 126 command, ok := params["command"].(string) 127 if !ok { 128 return errors.New("no command parameter to juju-run action") 129 } 130 131 // The timeout is passed in in nanoseconds(which are represented in go as int64) 132 // But due to serialization it comes out as float64 133 timeout, ok := params["timeout"].(float64) 134 if !ok { 135 logger.Debugf("unable to read juju-run action timeout, will continue running action without one") 136 } 137 138 results, err := runner.runCommandsWithTimeout(command, time.Duration(timeout), clock.WallClock) 139 140 if err != nil { 141 return runner.context.Flush("juju-run", err) 142 } 143 144 if err := runner.updateActionResults(results); err != nil { 145 return runner.context.Flush("juju-run", err) 146 } 147 148 return runner.context.Flush("juju-run", nil) 149 } 150 151 func encodeBytes(input []byte) (value string, encoding string) { 152 if utf8.Valid(input) { 153 value = string(input) 154 encoding = "utf8" 155 } else { 156 value = base64.StdEncoding.EncodeToString(input) 157 encoding = "base64" 158 } 159 return value, encoding 160 } 161 162 func (runner *runner) updateActionResults(results *utilexec.ExecResponse) error { 163 if err := runner.context.UpdateActionResults([]string{"Code"}, fmt.Sprintf("%d", results.Code)); err != nil { 164 return errors.Trace(err) 165 } 166 167 stdout, encoding := encodeBytes(results.Stdout) 168 if err := runner.context.UpdateActionResults([]string{"Stdout"}, stdout); err != nil { 169 return errors.Trace(err) 170 } 171 if encoding != "utf8" { 172 if err := runner.context.UpdateActionResults([]string{"StdoutEncoding"}, encoding); err != nil { 173 return errors.Trace(err) 174 } 175 } 176 177 stderr, encoding := encodeBytes(results.Stderr) 178 if err := runner.context.UpdateActionResults([]string{"Stderr"}, stderr); err != nil { 179 return errors.Trace(err) 180 } 181 if encoding != "utf8" { 182 if err := runner.context.UpdateActionResults([]string{"StderrEncoding"}, encoding); err != nil { 183 return errors.Trace(err) 184 } 185 } 186 187 return nil 188 } 189 190 // RunAction exists to satisfy the Runner interface. 191 func (runner *runner) RunAction(actionName string) error { 192 if _, err := runner.context.ActionData(); err != nil { 193 return errors.Trace(err) 194 } 195 if actionName == actions.JujuRunActionName { 196 return runner.runJujuRunAction() 197 } 198 return runner.runCharmHookWithLocation(actionName, "actions") 199 } 200 201 // RunHook exists to satisfy the Runner interface. 202 func (runner *runner) RunHook(hookName string) error { 203 return runner.runCharmHookWithLocation(hookName, "hooks") 204 } 205 206 func (runner *runner) runCharmHookWithLocation(hookName, charmLocation string) error { 207 srv, err := runner.startJujucServer() 208 if err != nil { 209 return err 210 } 211 defer srv.Close() 212 213 env, err := runner.context.HookVars(runner.paths) 214 if err != nil { 215 return errors.Trace(err) 216 } 217 if jujuos.HostOS() == jujuos.Windows { 218 // TODO(fwereade): somehow consolidate with utils/exec? 219 // We don't do this on the other code path, which uses exec.RunCommands, 220 // because that already has handling for windows environment requirements. 221 env = mergeWindowsEnvironment(env, os.Environ()) 222 } 223 224 debugctx := debug.NewHooksContext(runner.context.UnitName()) 225 if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) { 226 logger.Infof("executing %s via debug-hooks", hookName) 227 err = session.RunHook(hookName, runner.paths.GetCharmDir(), env) 228 } else { 229 err = runner.runCharmHook(hookName, env, charmLocation) 230 } 231 return runner.context.Flush(hookName, err) 232 } 233 234 func (runner *runner) runCharmHook(hookName string, env []string, charmLocation string) error { 235 charmDir := runner.paths.GetCharmDir() 236 hook, err := searchHook(charmDir, filepath.Join(charmLocation, hookName)) 237 if err != nil { 238 return err 239 } 240 hookCmd := hookCommand(hook) 241 ps := exec.Command(hookCmd[0], hookCmd[1:]...) 242 ps.Env = env 243 ps.Dir = charmDir 244 outReader, outWriter, err := os.Pipe() 245 if err != nil { 246 return errors.Errorf("cannot make logging pipe: %v", err) 247 } 248 ps.Stdout = outWriter 249 ps.Stderr = outWriter 250 hookLogger := &hookLogger{ 251 r: outReader, 252 done: make(chan struct{}), 253 logger: runner.getLogger(hookName), 254 } 255 go hookLogger.run() 256 err = ps.Start() 257 outWriter.Close() 258 if err == nil { 259 // Record the *os.Process of the hook 260 runner.context.SetProcess(hookProcess{ps.Process}) 261 // Block until execution finishes 262 err = ps.Wait() 263 } 264 hookLogger.stop() 265 return errors.Trace(err) 266 } 267 268 func (runner *runner) startJujucServer() (*jujuc.Server, error) { 269 // Prepare server. 270 getCmd := func(ctxId, cmdName string) (cmd.Command, error) { 271 if ctxId != runner.context.Id() { 272 return nil, errors.Errorf("expected context id %q, got %q", runner.context.Id(), ctxId) 273 } 274 return jujuc.NewCommand(runner.context, cmdName) 275 } 276 srv, err := jujuc.NewServer(getCmd, runner.paths.GetJujucSocket()) 277 if err != nil { 278 return nil, err 279 } 280 go srv.Run() 281 return srv, nil 282 } 283 284 func (runner *runner) getLogger(hookName string) loggo.Logger { 285 return loggo.GetLogger(fmt.Sprintf("unit.%s.%s", runner.context.UnitName(), hookName)) 286 } 287 288 type hookProcess struct { 289 *os.Process 290 } 291 292 func (p hookProcess) Pid() int { 293 return p.Process.Pid 294 }