github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/worker/uniter/runner/jujuc/server.go (about) 1 // Copyright 2012, 2013, 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // The worker/uniter/runner/jujuc package implements the server side of the 5 // jujuc proxy tool, which forwards command invocations to the unit agent 6 // process so that they can be executed against specific state. 7 package jujuc 8 9 import ( 10 "bytes" 11 "fmt" 12 "io" 13 "net" 14 "net/rpc" 15 "path/filepath" 16 "sort" 17 "sync" 18 19 "github.com/juju/cmd" 20 "github.com/juju/errors" 21 "github.com/juju/loggo" 22 "github.com/juju/utils/exec" 23 24 "github.com/juju/juju/juju/sockets" 25 ) 26 27 // CmdSuffix is the filename suffix to use for executables. 28 const CmdSuffix = cmdSuffix 29 30 var logger = loggo.GetLogger("worker.uniter.jujuc") 31 32 // ErrNoStdin is returned by Jujuc.Main if the hook tool requests 33 // stdin, and none is supplied. 34 var ErrNoStdin = errors.New("hook tool requires stdin, none supplied") 35 36 type creator func(Context) (cmd.Command, error) 37 38 var registeredCommands = map[string]creator{} 39 40 func RegisterCommand(name string, f creator) { 41 registeredCommands[name+cmdSuffix] = f 42 } 43 44 // baseCommands maps Command names to creators. 45 var baseCommands = map[string]creator{ 46 "close-port" + cmdSuffix: NewClosePortCommand, 47 "config-get" + cmdSuffix: NewConfigGetCommand, 48 "juju-log" + cmdSuffix: NewJujuLogCommand, 49 "open-port" + cmdSuffix: NewOpenPortCommand, 50 "opened-ports" + cmdSuffix: NewOpenedPortsCommand, 51 "relation-get" + cmdSuffix: NewRelationGetCommand, 52 "action-get" + cmdSuffix: NewActionGetCommand, 53 "action-set" + cmdSuffix: NewActionSetCommand, 54 "action-fail" + cmdSuffix: NewActionFailCommand, 55 "relation-ids" + cmdSuffix: NewRelationIdsCommand, 56 "relation-list" + cmdSuffix: NewRelationListCommand, 57 "relation-set" + cmdSuffix: NewRelationSetCommand, 58 "unit-get" + cmdSuffix: NewUnitGetCommand, 59 "add-metric" + cmdSuffix: NewAddMetricCommand, 60 "juju-reboot" + cmdSuffix: NewJujuRebootCommand, 61 "status-get" + cmdSuffix: NewStatusGetCommand, 62 "status-set" + cmdSuffix: NewStatusSetCommand, 63 "network-get" + cmdSuffix: NewNetworkGetCommand, 64 } 65 66 var storageCommands = map[string]creator{ 67 "storage-add" + cmdSuffix: NewStorageAddCommand, 68 "storage-get" + cmdSuffix: NewStorageGetCommand, 69 "storage-list" + cmdSuffix: NewStorageListCommand, 70 } 71 72 var leaderCommands = map[string]creator{ 73 "is-leader" + cmdSuffix: NewIsLeaderCommand, 74 "leader-get" + cmdSuffix: NewLeaderGetCommand, 75 "leader-set" + cmdSuffix: NewLeaderSetCommand, 76 } 77 78 func allEnabledCommands() map[string]creator { 79 all := map[string]creator{} 80 add := func(m map[string]creator) { 81 for k, v := range m { 82 all[k] = v 83 } 84 } 85 add(baseCommands) 86 add(storageCommands) 87 add(leaderCommands) 88 add(registeredCommands) 89 return all 90 } 91 92 // CommandNames returns the names of all jujuc commands. 93 func CommandNames() (names []string) { 94 for name := range allEnabledCommands() { 95 names = append(names, name) 96 } 97 sort.Strings(names) 98 return 99 } 100 101 // NewCommand returns an instance of the named Command, initialized to execute 102 // against the supplied Context. 103 func NewCommand(ctx Context, name string) (cmd.Command, error) { 104 f := allEnabledCommands()[name] 105 if f == nil { 106 return nil, errors.Errorf("unknown command: %s", name) 107 } 108 command, err := f(ctx) 109 if err != nil { 110 return nil, errors.Trace(err) 111 } 112 return command, nil 113 } 114 115 // Request contains the information necessary to run a Command remotely. 116 type Request struct { 117 ContextId string 118 Dir string 119 CommandName string 120 Args []string 121 122 // StdinSet indicates whether or not the client supplied stdin. This is 123 // necessary as Stdin will be nil if the client supplied stdin but it 124 // is empty. 125 StdinSet bool 126 Stdin []byte 127 } 128 129 // CmdGetter looks up a Command implementation connected to a particular Context. 130 type CmdGetter func(contextId, cmdName string) (cmd.Command, error) 131 132 // Jujuc implements the jujuc command in the form required by net/rpc. 133 type Jujuc struct { 134 mu sync.Mutex 135 getCmd CmdGetter 136 } 137 138 // badReqErrorf returns an error indicating a bad Request. 139 func badReqErrorf(format string, v ...interface{}) error { 140 return fmt.Errorf("bad request: "+format, v...) 141 } 142 143 // Main runs the Command specified by req, and fills in resp. A single command 144 // is run at a time. 145 func (j *Jujuc) Main(req Request, resp *exec.ExecResponse) error { 146 if req.CommandName == "" { 147 return badReqErrorf("command not specified") 148 } 149 if !filepath.IsAbs(req.Dir) { 150 return badReqErrorf("Dir is not absolute") 151 } 152 c, err := j.getCmd(req.ContextId, req.CommandName) 153 if err != nil { 154 return badReqErrorf("%s", err) 155 } 156 var stdin io.Reader 157 if req.StdinSet { 158 stdin = bytes.NewReader(req.Stdin) 159 } else { 160 // noStdinReader will error with ErrNoStdin 161 // if its Read method is called. 162 stdin = noStdinReader{} 163 } 164 var stdout, stderr bytes.Buffer 165 ctx := &cmd.Context{ 166 Dir: req.Dir, 167 Stdin: stdin, 168 Stdout: &stdout, 169 Stderr: &stderr, 170 } 171 j.mu.Lock() 172 defer j.mu.Unlock() 173 logger.Infof("running hook tool %q %q", req.CommandName, req.Args) 174 logger.Debugf("hook context id %q; dir %q", req.ContextId, req.Dir) 175 wrapper := &cmdWrapper{c, nil} 176 resp.Code = cmd.Main(wrapper, ctx, req.Args) 177 if errors.Cause(wrapper.err) == ErrNoStdin { 178 return ErrNoStdin 179 } 180 resp.Stdout = stdout.Bytes() 181 resp.Stderr = stderr.Bytes() 182 return nil 183 } 184 185 // Server implements a server that serves command invocations via 186 // a unix domain socket. 187 type Server struct { 188 socketPath string 189 listener net.Listener 190 server *rpc.Server 191 closed chan bool 192 closing chan bool 193 wg sync.WaitGroup 194 } 195 196 // NewServer creates an RPC server bound to socketPath, which can execute 197 // remote command invocations against an appropriate Context. It will not 198 // actually do so until Run is called. 199 func NewServer(getCmd CmdGetter, socketPath string) (*Server, error) { 200 server := rpc.NewServer() 201 if err := server.Register(&Jujuc{getCmd: getCmd}); err != nil { 202 return nil, err 203 } 204 listener, err := sockets.Listen(socketPath) 205 if err != nil { 206 return nil, err 207 } 208 s := &Server{ 209 socketPath: socketPath, 210 listener: listener, 211 server: server, 212 closed: make(chan bool), 213 closing: make(chan bool), 214 } 215 return s, nil 216 } 217 218 // Run accepts new connections until it encounters an error, or until Close is 219 // called, and then blocks until all existing connections have been closed. 220 func (s *Server) Run() (err error) { 221 var conn net.Conn 222 for { 223 conn, err = s.listener.Accept() 224 if err != nil { 225 break 226 } 227 s.wg.Add(1) 228 go func(conn net.Conn) { 229 s.server.ServeConn(conn) 230 s.wg.Done() 231 }(conn) 232 } 233 select { 234 case <-s.closing: 235 // Someone has called Close(), so it is overwhelmingly likely that 236 // the error from Accept is a direct result of the Listener being 237 // closed, and can therefore be safely ignored. 238 err = nil 239 default: 240 } 241 s.wg.Wait() 242 close(s.closed) 243 return 244 } 245 246 // Close immediately stops accepting connections, and blocks until all existing 247 // connections have been closed. 248 func (s *Server) Close() { 249 close(s.closing) 250 s.listener.Close() 251 <-s.closed 252 } 253 254 type noStdinReader struct{} 255 256 // Read implements io.Reader, simply returning ErrNoStdin any time it's called. 257 func (noStdinReader) Read([]byte) (int, error) { 258 return 0, ErrNoStdin 259 } 260 261 // cmdWrapper wraps a cmd.Command's Run method so the error returned can be 262 // intercepted when the command is run via cmd.Main. 263 type cmdWrapper struct { 264 cmd.Command 265 err error 266 } 267 268 func (c *cmdWrapper) Run(ctx *cmd.Context) error { 269 c.err = c.Command.Run(ctx) 270 return c.err 271 }