Status: | Work in progress |
---|---|
Proposed branch: | lp:~mathieu-lonjaret/pyjuju/go-ssh |
Merge into: | lp:pyjuju/go |
Diff against target: |
100 lines (+96/-0) 1 file modified
state/connect.go (+96/-0) |
To merge this branch: | bzr merge lp:~mathieu-lonjaret/pyjuju/go-ssh |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+91301@code.launchpad.net |
Commit message
Description of the change
ssh integration with state info
- 50. By Gustavo Niemeyer
-
store: simplify storage of CharmEvent
This will also introduce bson marshaling of charm.URL and
port the store code to mgo tip (time.Time must be used
instead of bson.Timestamp).R=fwereade
CC=
https://codereview. appspot. com/5606053 - 51. By Roger Peppe
-
environs/ec2: add code to create juju-specific cloudinit data.
When we create another provider, this code will be factored out,
but for the time being we'll keep it local to environs/ec2.Also add a function to get ssh authorized keys.
R=fwereade, niemeyer
CC=
https://codereview. appspot. com/5610050 - 52. By mpl
-
ssh integration in state
Roger Peppe (rogpeppe) wrote : | # |
thanks for doing this.
needs a bit of work but not too much!
most importantly it needs some tests.
https:/
File state/connect.go (right):
https:/
state/connect.
this can go (see below)
https:/
state/connect.
this in
rather than having a sentence going down that right hand comment, i
think an actual comment would be better.
// We only implement SSH user/password authentication for now.
// We could put these fields in an SSH-specific struct type.
https:/
state/connect.
connection.
delete this comment
https:/
state/connect.
i'm not keen on "my" - i don't think it adds useful info.
i think this might be better:
// clientPassword implements the ssh ClientPassword interface
// for a single SSH user.
type clientPassword struct {
https:/
state/connect.
returns a
s/takes/initTunnel takes/
although i'm not sure initTunnel is a great name.
how about dialSSH ?
https:/
state/connect.
return client.Dial("tcp", addr)
https:/
state/connect.
i think the caller should be able to see the error when the dial of the
ssh address fails.
i think it's a mistake that the proxy is on a fixed port - that means
that we can't have two of these going on at the same time.
how about something like this? you'd call it every time you wanted to
make a new connection. it would be possible to see if there's an
existing proxy for a given address and reuse if so, but i don't think
it's worth it for the time being.
(warning: i haven't even compiled the code below)
// newProxy returns a local proxy for the given address.
// It dials info.SSHAddr, arranges an ssh port forward
// from there to addr, listens on the proxy address
// and copies data between it and the ssh connection.
func (info *Info) newProxy(addr string) (proxy string, err error) {
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
return "", err
}
proxyAddr := l.Addr().String()
zkServer, err := info.initTunnel
if err != nil {
return "", err
}
go func() {
defer l.Close()
defer zkServer.Close()
zkClient, err := l.Accept()
if err != nil {
err)
return
}
defer zkClient.Close()
done := make(chan bool)
go forwarder(zkClient, zkServer, don...
mpl (mathieu-lonjaret) wrote : | # |
Please take a look.
mpl (mathieu-lonjaret) wrote : | # |
Tests will come with the next commit.
https:/
File state/connect.go (right):
https:/
state/connect.
On 2012/02/06 09:36:39, rog wrote:
> this can go (see below)
Done.
https:/
state/connect.
this in
On 2012/02/06 09:36:39, rog wrote:
> rather than having a sentence going down that right hand comment, i
think an
> actual comment would be better.
> // We only implement SSH user/password authentication for now.
> // We could put these fields in an SSH-specific struct type.
Done.
https:/
state/connect.
connection.
On 2012/02/06 09:36:39, rog wrote:
> delete this comment
Done.
https:/
state/connect.
On 2012/02/06 09:36:39, rog wrote:
> i'm not keen on "my" - i don't think it adds useful info.
> i think this might be better:
> // clientPassword implements the ssh ClientPassword interface
> // for a single SSH user.
> type clientPassword struct {
Done.
https:/
state/connect.
returns a
On 2012/02/06 09:36:39, rog wrote:
> s/takes/initTunnel takes/
> although i'm not sure initTunnel is a great name.
> how about dialSSH ?
as agreed on irc, sshDial. Done.
https:/
state/connect.
On 2012/02/06 09:36:39, rog wrote:
> return client.Dial("tcp", addr)
Done.
https:/
state/connect.
On 2012/02/06 09:36:39, rog wrote:
> i think the caller should be able to see the error when the dial of
the ssh
> address fails.
> i think it's a mistake that the proxy is on a fixed port - that means
that we
> can't have two of these going on at the same time.
> how about something like this? you'd call it every time you wanted to
make a new
> connection. it would be possible to see if there's an existing proxy
for a given
> address and reuse if so, but i don't think it's worth it for the time
being.
> (warning: i haven't even compiled the code below)
> // newProxy returns a local proxy for the given address.
> // It dials info.SSHAddr, arranges an ssh port forward
> // from there to addr, listens on the proxy address
> // and copies data between it and the ssh connection.
> func (info *Info) newProxy(addr string) (proxy string, err error) {
> l, err := net.Listen("tcp", "localhost:0")
> if err != nil {
> return "", err
> }
> proxyAddr := l.Addr().String()
> zkServer, err := info.initTunnel
> if err != nil {
> return "", err
> }
> go func() {
> ...
Roger Peppe (rogpeppe) wrote : | # |
https:/
File state/connect.go (right):
https:/
state/connect.
On 2012/02/06 22:29:09, mpl wrote:
> On 2012/02/06 09:36:39, rog wrote:
> > i think the caller should be able to see the error when the dial of
the ssh
> > address fails.
> >
> > i think it's a mistake that the proxy is on a fixed port - that
means that we
> > can't have two of these going on at the same time.
> >
> > how about something like this? you'd call it every time you wanted
to make a
> new
> > connection. it would be possible to see if there's an existing proxy
for a
> given
> > address and reuse if so, but i don't think it's worth it for the
time being.
> >
> > (warning: i haven't even compiled the code below)
> >
> >
> > // newProxy returns a local proxy for the given address.
> > // It dials info.SSHAddr, arranges an ssh port forward
> > // from there to addr, listens on the proxy address
> > // and copies data between it and the ssh connection.
> > func (info *Info) newProxy(addr string) (proxy string, err error) {
> > l, err := net.Listen("tcp", "localhost:0")
> > if err != nil {
> > return "", err
> > }
> > proxyAddr := l.Addr().String()
> >
> > zkServer, err := info.initTunnel
> > if err != nil {
> > return "", err
> > }
> >
> > go func() {
> > defer l.Close()
> > defer zkServer.Close()
> >
> > zkClient, err := l.Accept()
> > if err != nil {
> > log.Printf("error accepting connection on %v: %v",
l.Addr(), err)
> > return
> > }
> > defer zkClient.Close()
> > done := make(chan bool)
> > go forwarder(zkClient, zkServer, done)
> > go forwarder(zkServer, zkClient, done)
> > <-done
> > <-done
> > }()
> > return proxyAddr, err
> > }
> >
> > func forwarder(w io.Writer, r io.Reader, done chan bool){
> > _, err := io.Copy(w, r)
> > if err != nil {
> > log.Printf("network I/O error: %v", err)
> > }
> > done <- true
> > }
> Ok. but it looks like the proxy is only used for one Dial, since
there's no
> looping around Accept? It's fine by me, but I'm just making sure
that's what we
> want.
hmm, thinking about this, perhaps we do want a loop, as the zk client
might dial its address multiple times.
that means we also need some way of telling the proxy to go away when
the State is closed. (perhaps closing the state should just close the
listener, which should cause Accept to fail)
> Also, I was using a chan to make sure that the proxy was as ready as
possible to
> be accepting before letting zookeeper.Dial happen. Why is it better to
drop it?
after the Listen has been done, the proxy is ready to accept.
https:/
state/connect.
On 2012/02/07 08:23:44, TheMue wrote:
> I'm still not happy with the idea of "opening an info to get a state".
I still
> prefer the more functional approach of "opening a state to...
mpl (mathieu-lonjaret) wrote : | # |
Please take a look.
Preview Diff
1 | === added file 'state/connect.go' |
2 | --- state/connect.go 1970-01-01 00:00:00 +0000 |
3 | +++ state/connect.go 2012-02-13 14:18:21 +0000 |
4 | @@ -0,0 +1,96 @@ |
5 | +package state |
6 | + |
7 | +import ( |
8 | + "errors" |
9 | + "log" |
10 | + "math/rand" |
11 | + "os" |
12 | + "os/exec" |
13 | + "strconv" |
14 | + "strings" |
15 | + |
16 | + "launchpad.net/gozk/zookeeper" |
17 | +) |
18 | + |
19 | +// Info represents information about a juju |
20 | +// state server or servers. |
21 | +type Info struct { |
22 | + UseSSH bool |
23 | + Addrs []string // List of IP address:port |
24 | + SSHUsername string // We only implement SSH user/password authentication for now. |
25 | + SSHPassword string // We could put these fields in an SSH-specific struct type. |
26 | + SSHAddr string |
27 | + SSHPortsRange [2]int |
28 | + // all fields should be public. |
29 | +} |
30 | + |
31 | +// TODO(mpl): add the key authentication |
32 | +func (info *Info) forwardPort(addr string) (string, error) { |
33 | + localPort := "" |
34 | + parts := strings.Split(addr, ":") |
35 | + if len(parts) != 2 { |
36 | + return localPort, errors.New("Wrong address format; was expecting \"host:port\"") |
37 | + } |
38 | + remoteHost := parts[0] |
39 | + remotePort := parts[1] |
40 | + |
41 | + // get a random local port in the defined range |
42 | + if info.SSHPortsRange[1] < info.SSHPortsRange[0] { |
43 | + return localPort, errors.New("Invalid ports range") |
44 | + } |
45 | + randPort := rand.Intn(info.SSHPortsRange[1]-info.SSHPortsRange[0]) + info.SSHPortsRange[0] |
46 | + localPort = strconv.Itoa(randPort) |
47 | + |
48 | + cmd, err := exec.LookPath("ssh") |
49 | + if err != nil { |
50 | + return localPort, err |
51 | + } |
52 | + args := []string{cmd, |
53 | + "-o", "PasswordAuthentication no", |
54 | + "-Llocalhost:" + localPort + ":localhost:" + remotePort, |
55 | + info.SSHUsername + "@" + remoteHost} |
56 | + fds := []*os.File{os.Stdin, os.Stdout, os.Stderr} |
57 | + |
58 | + _, err = os.StartProcess(args[0], args, &os.ProcAttr{Dir: "/", Files: fds}) |
59 | + if err != nil { |
60 | + return localPort, err |
61 | + } |
62 | + return localPort, nil |
63 | +} |
64 | + |
65 | +// Open returns a new State representing the |
66 | +// environment connected to using the given info. |
67 | +func Open(info *Info) (*State, error) { |
68 | + var ( |
69 | + zk *zookeeper.Conn |
70 | + session <-chan zookeeper.Event |
71 | + err error |
72 | + ) |
73 | + if info.UseSSH { |
74 | + for _, addr := range info.Addrs { |
75 | + localPort, err := info.forwardPort(addr) |
76 | + if err != nil { |
77 | + log.Print(err) |
78 | + continue |
79 | + } |
80 | + proxyAddr := "localhost:" + localPort |
81 | + zk, session, err = zookeeper.Dial(proxyAddr, 5e9) |
82 | + if err != nil { |
83 | + log.Print(err) |
84 | + continue |
85 | + } |
86 | + break |
87 | + } |
88 | + } else { |
89 | + zk, session, err = zookeeper.Dial(strings.Join(info.Addrs, ","), 5e9) |
90 | + if err != nil { |
91 | + return nil, err |
92 | + } |
93 | + } |
94 | + // Wait for connection. |
95 | + event := <-session |
96 | + if event.State != zookeeper.STATE_CONNECTED { |
97 | + return nil, errors.New("Could not connect to zookeeper") |
98 | + } |
99 | + return &State{zk}, nil |
100 | +} |
Reviewers: mp+91301_ code.launchpad. net,
Message:
Please take a look.
Description:
https:/ /code.launchpad .net/~mathieu- lonjaret/ juju/go- ssh/+merge/ 91301
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/5620051/
Affected files:
A state/connect.go