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 }