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