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 }