Merge lp:~fwereade/pyjuju/go-cmd-server-package into lp:pyjuju/go

Proposed by William Reade
Status: Work in progress
Proposed branch: lp:~fwereade/pyjuju/go-cmd-server-package
Merge into: lp:pyjuju/go
Prerequisite: lp:~fwereade/pyjuju/go-tweak-supercommand
Diff against target: 306 lines (+296/-0)
2 files modified
cmd/server/server.go (+144/-0)
cmd/server/server_test.go (+152/-0)
To merge this branch: bzr merge lp:~fwereade/pyjuju/go-cmd-server-package
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+96140@code.launchpad.net

Description of the change

Add cmd/server package to enable hosted Command execution via RPC

prereq: lp:~fwereade/juju/go-tweak-supercommand

This is all new code, and shouldn't impact anything else, so it should be
adequately explained by the inline comments (and if it isn't, those should
be fixed). Nonetheless, the general idea is as follows:

1) Hook commands need to run the "meat" of their code on the server.

2) The python implementation includes an awful lot of plumbing to support a
   mechanism whereby each individual command marshals relevant params to a
   server and gets back results which are then formatted for stdout; in
   juju.hooks, the cli, commands, and protocol modules all contain a lot of
   code purely to support this approach.

3) By implementing a single server method that takes a command line, a
   working directory, and a context ID; and returns an exit code and the
   command's output: at a stroke, we eliminate the vast majority of the
   marshalling busywork, and become able to implement each hook tool as a
   single Command, which can have direct access to the hook context in which
   it will execute.

https://codereview.appspot.com/5754052/

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Reviewers: mp+96140_code.launchpad.net,

Message:
Please take a look.

Description:
prereq: lp:~fwereade/juju/go-tweak-supercommand

This is all new code, and shouldn't impact anything else, so it should
be
adequately explained by the inline comments (and if it isn't, those
should
be fixed).

https://code.launchpad.net/~fwereade/juju/go-cmd-server-package/+merge/96140

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/5754052/

Affected files:
   M cmd/command.go
   A cmd/context.go
   A cmd/context_test.go
   M cmd/juju/bootstrap.go
   M cmd/juju/main_test.go
   M cmd/jujud/initzk.go
   M cmd/jujud/machine.go
   M cmd/jujud/main_test.go
   M cmd/jujud/provisioning.go
   M cmd/jujud/unit.go
   A cmd/server/server.go
   A cmd/server/server_test.go
   M cmd/supercommand.go
   M cmd/supercommand_test.go

Revision history for this message
William Reade (fwereade) wrote :
84. By William Reade

client id became context id

Revision history for this message
William Reade (fwereade) wrote :
85. By William Reade

fix param names

86. By William Reade

merge parent

87. By William Reade

merge parent

88. By William Reade

merge parent

Unmerged revisions

88. By William Reade

merge parent

87. By William Reade

merge parent

86. By William Reade

merge parent

85. By William Reade

fix param names

84. By William Reade

client id became context id

83. By William Reade

add cmd/server package

82. By William Reade

anticipatory SuperCommand tweaks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'cmd/server'
2=== added file 'cmd/server/server.go'
3--- cmd/server/server.go 1970-01-01 00:00:00 +0000
4+++ cmd/server/server.go 2012-03-17 01:16:19 +0000
5@@ -0,0 +1,144 @@
6+// The cmd/server package allows a process to expose an RPC interface that
7+// allows client processes to delegate execution of cmd.Commands to a server
8+// process (with exposed commands and their particular state amenable to
9+// selection by client id). This will allow the unit agent to provide tailor-
10+// made hook command sets for any hook execution context it creates.
11+package server
12+
13+import (
14+ "bytes"
15+ "fmt"
16+ "launchpad.net/juju/go/cmd"
17+ "launchpad.net/tomb"
18+ "net"
19+ "net/rpc"
20+ "os"
21+ "path/filepath"
22+)
23+
24+// Request contains the information necessary to run a Command whose view of the
25+// world is filtered according to its ContextId.
26+type Request struct {
27+ ContextId string
28+ Dir string
29+ Args []string
30+}
31+
32+// Response contains the return code and output generated by a Request.
33+type Response struct {
34+ Code int
35+ Stdout string
36+ Stderr string
37+}
38+
39+// CmdFactory returns the Commands that should be exposed in the context
40+// identified by contextId.
41+type CmdFactory func(contextId string) ([]cmd.Command, error)
42+
43+// Server wraps net.rpc.Server so as to allow Commands to be executed in one
44+// process on behalf of another.
45+type Server struct {
46+ sockPath string
47+ server *rpc.Server
48+ *tomb.Tomb
49+}
50+
51+// NewServer starts an RPC server which runs on sockPath and exposes Commands
52+// chosen (at run time) by factory (according to the Request's ContextId).
53+func NewServer(factory CmdFactory, sockPath string) (*Server, error) {
54+ s := &Server{sockPath, rpc.NewServer(), tomb.New()}
55+ if err := s.server.Register(&RPCcmd{factory}); err != nil {
56+ return nil, err
57+ }
58+ listener, err := net.Listen("unix", sockPath)
59+ if err != nil {
60+ return nil, err
61+ }
62+ go s.serve(listener)
63+ return s, nil
64+}
65+
66+// serve starts the RPC server (serving one request at a time).
67+func (s *Server) serve(listener net.Listener) {
68+ defer s.Done()
69+ go func() {
70+ <-s.Dying
71+ listener.Close()
72+ <-s.Dead
73+ os.Remove(s.sockPath)
74+ }()
75+ for {
76+ conn, err := listener.Accept()
77+ if err != nil {
78+ if s.Err() != tomb.Stop {
79+ s.Fatal(err)
80+ }
81+ return
82+ }
83+ s.server.ServeConn(conn)
84+ }
85+}
86+
87+// Close stops the RPC server.
88+func (s *Server) Close() error {
89+ s.Fatal(tomb.Stop)
90+ return s.Wait()
91+}
92+
93+// RPCcmd wraps cmd for RPC.
94+type RPCcmd struct {
95+ factory CmdFactory
96+}
97+
98+var jujucDoc = `
99+The jujuc command forwards invocations over RPC for execution by another
100+process. It expects to be called via a symlink named for the desired remote
101+command, and requires that JUJU_AGENT_SOCKET and JUJU_CONTEXT_ID be set in
102+its environment.
103+`
104+
105+// command returns a cmd.SuperCommand which is responsible for selecting one
106+// of the Commands produced by calling rpcc.factory.
107+func (rpcc *RPCcmd) command(contextId string) (cmd.Command, error) {
108+ cmds, err := rpcc.factory(contextId)
109+ if err != nil {
110+ return nil, err
111+ }
112+ sc := cmd.NewSuperCommand("(-> jujuc)", jujucDoc)
113+ sc.SetsLog = false
114+ sc.Purpose = "invoke a command implemented by another process"
115+ for _, c := range cmds {
116+ sc.Register(c)
117+ }
118+ return sc, nil
119+}
120+
121+// run invokes c with args and fills in resp.
122+func run(c cmd.Command, dir string, args []string, resp *Response) error {
123+ stdout := bytes.NewBuffer([]byte{})
124+ stderr := bytes.NewBuffer([]byte{})
125+ ctx := &cmd.Context{dir, stdout, stderr}
126+ resp.Code = cmd.Main(c, ctx, args)
127+ resp.Stdout = stdout.String()
128+ resp.Stderr = stderr.String()
129+ return nil
130+}
131+
132+func badReq(format string, v ...interface{}) error {
133+ return fmt.Errorf("bad request: "+format, v...)
134+}
135+
136+// Main implements the equivalent of cmd.Main for RPC.
137+func (rpcc *RPCcmd) Main(req Request, resp *Response) error {
138+ if req.Args == nil || len(req.Args) < 1 {
139+ return badReq("Args too short")
140+ }
141+ if !filepath.IsAbs(req.Dir) {
142+ return badReq("Dir is not absolute")
143+ }
144+ c, err := rpcc.command(req.ContextId)
145+ if err != nil {
146+ return badReq("%s", err)
147+ }
148+ return run(c, req.Dir, req.Args, resp)
149+}
150
151=== added file 'cmd/server/server_test.go'
152--- cmd/server/server_test.go 1970-01-01 00:00:00 +0000
153+++ cmd/server/server_test.go 2012-03-17 01:16:19 +0000
154@@ -0,0 +1,152 @@
155+package server_test
156+
157+import (
158+ "errors"
159+ "io/ioutil"
160+ "launchpad.net/gnuflag"
161+ . "launchpad.net/gocheck"
162+ "launchpad.net/juju/go/cmd"
163+ "launchpad.net/juju/go/cmd/server"
164+ "net/rpc"
165+ "os"
166+ "path/filepath"
167+ "strings"
168+ "testing"
169+)
170+
171+func Test(t *testing.T) { TestingT(t) }
172+
173+type RpcCommand struct {
174+ Value string
175+}
176+
177+func (c *RpcCommand) Info() *cmd.Info {
178+ return &cmd.Info{"magic", "[options]", "do magic", "blah doc", true}
179+}
180+
181+func (c *RpcCommand) InitFlagSet(f *gnuflag.FlagSet) {
182+ f.StringVar(&c.Value, "value", "", "doc")
183+}
184+
185+func (c *RpcCommand) ParsePositional(args []string) error {
186+ return cmd.CheckEmpty(args)
187+}
188+
189+func (c *RpcCommand) Run(ctx *cmd.Context) error {
190+ if c.Value != "zyxxy" {
191+ return errors.New("insufficiently magic")
192+ }
193+ ctx.Stdout.Write([]byte("eye of newt\n"))
194+ ctx.Stderr.Write([]byte("toe of frog\n"))
195+ return ioutil.WriteFile(ctx.AbsPath("local"), []byte{}, 0644)
196+}
197+
198+func factory(contextId string) ([]cmd.Command, error) {
199+ if contextId != "merlin" {
200+ return nil, errors.New("unknown client")
201+ }
202+ return []cmd.Command{&RpcCommand{}}, nil
203+}
204+
205+type ServerSuite struct {
206+ server *server.Server
207+ sockPath string
208+}
209+
210+var _ = Suite(&ServerSuite{})
211+
212+func (s *ServerSuite) SetUpTest(c *C) {
213+ s.sockPath = filepath.Join(c.MkDir(), "test.sock")
214+ srv, err := server.NewServer(factory, s.sockPath)
215+ c.Assert(err, IsNil)
216+ c.Assert(srv, NotNil)
217+ s.server = srv
218+}
219+
220+func (s *ServerSuite) TearDownTest(c *C) {
221+ c.Assert(s.server.Close(), IsNil)
222+ _, err := os.Open(s.sockPath)
223+ c.Assert(os.IsNotExist(err), Equals, true)
224+}
225+
226+func (s *ServerSuite) Call(c *C, req server.Request) (resp server.Response, err error) {
227+ client, err := rpc.Dial("unix", s.sockPath)
228+ c.Assert(err, IsNil)
229+ defer client.Close()
230+ err = client.Call("RPCcmd.Main", req, &resp)
231+ return resp, err
232+}
233+
234+func (s *ServerSuite) TestHappyPath(c *C) {
235+ dir := c.MkDir()
236+ resp, err := s.Call(c, server.Request{
237+ "merlin", dir, []string{"magic", "--value", "zyxxy"}})
238+ c.Assert(err, IsNil)
239+ c.Assert(resp.Code, Equals, 0)
240+ c.Assert(resp.Stdout, Equals, "eye of newt\n")
241+ c.Assert(resp.Stderr, Equals, "toe of frog\n")
242+ _, err = os.Stat(filepath.Join(dir, "local"))
243+ c.Assert(err, IsNil)
244+}
245+
246+func (s *ServerSuite) TestBadArgs(c *C) {
247+ dir := c.MkDir()
248+ for _, req := range []server.Request{
249+ {"merlin", dir, nil},
250+ {"merlin", dir, []string{}},
251+ {"mordred", dir, nil},
252+ {"mordred", dir, []string{}},
253+ } {
254+ _, err := s.Call(c, req)
255+ c.Assert(err, ErrorMatches, "bad request: Args too short")
256+ }
257+}
258+
259+func (s *ServerSuite) TestBadDir(c *C) {
260+ for _, req := range []server.Request{
261+ {"merlin", "", []string{"cmd"}},
262+ {"merlin", "foo/bar", []string{"cmd"}},
263+ } {
264+ _, err := s.Call(c, req)
265+ c.Assert(err, ErrorMatches, "bad request: Dir is not absolute")
266+ }
267+}
268+
269+func (s *ServerSuite) TestBadContextId(c *C) {
270+ _, err := s.Call(c, server.Request{"mordred", c.MkDir(), []string{"magic"}})
271+ c.Assert(err, ErrorMatches, "bad request: unknown client")
272+}
273+
274+func (s *ServerSuite) AssertBadCommand(c *C, args []string, code int) server.Response {
275+ resp, err := s.Call(c, server.Request{"merlin", c.MkDir(), args})
276+ c.Assert(err, IsNil)
277+ c.Assert(resp.Code, Equals, code)
278+ return resp
279+}
280+
281+func lines(s string) []string {
282+ return strings.Split(s, "\n")
283+}
284+
285+func (s *ServerSuite) TestUnknownCommand(c *C) {
286+ resp := s.AssertBadCommand(c, []string{"witchcraft"}, 2)
287+ c.Assert(resp.Stdout, Equals, "")
288+ usageStart := []string{
289+ "unrecognised command: (-> jujuc) witchcraft",
290+ "usage: (-> jujuc) <command> [options] ...",
291+ "purpose: invoke a command implemented by another process",
292+ }
293+ c.Assert(lines(resp.Stderr)[:3], DeepEquals, usageStart)
294+}
295+
296+func (s *ServerSuite) TestParseError(c *C) {
297+ resp := s.AssertBadCommand(c, []string{"magic", "--cheese"}, 2)
298+ c.Assert(resp.Stdout, Equals, "")
299+ c.Assert(lines(resp.Stderr)[0], Equals, "flag provided but not defined: --cheese")
300+}
301+
302+func (s *ServerSuite) TestBrokenCommand(c *C) {
303+ resp := s.AssertBadCommand(c, []string{"magic"}, 1)
304+ c.Assert(resp.Stdout, Equals, "")
305+ c.Assert(lines(resp.Stderr)[0], Equals, "insufficiently magic")
306+}

Subscribers

People subscribed via source and target branches