Merge lp:~axwalk/juju-core/lp1237736-bootstrap-context into lp:~go-bot/juju-core/trunk
- lp1237736-bootstrap-context
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2169 |
Proposed branch: | lp:~axwalk/juju-core/lp1237736-bootstrap-context |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
1764 lines (+392/-246) 33 files modified
cloudinit/sshinit/configure.go (+26/-8) cmd/cmd.go (+11/-0) cmd/juju/addmachine.go (+4/-1) cmd/juju/bootstrap.go (+20/-2) environs/bootstrap/bootstrap.go (+2/-2) environs/bootstrap/bootstrap_test.go (+14/-9) environs/interface.go (+26/-2) environs/jujutest/livetests.go (+2/-2) environs/jujutest/tests.go (+6/-2) environs/manual/bootstrap.go (+2/-1) environs/manual/bootstrap_test.go (+3/-0) environs/manual/provisioner.go (+20/-3) environs/open_test.go (+3/-1) environs/sshstorage/storage.go (+37/-21) environs/sshstorage/storage_test.go (+25/-14) environs/testing/bootstrap.go (+25/-1) juju/apiconn_test.go (+1/-1) juju/conn_test.go (+10/-6) juju/testing/conn.go (+2/-1) provider/azure/environ.go (+2/-2) provider/common/bootstrap.go (+40/-79) provider/common/bootstrap_test.go (+54/-56) provider/dummy/environs.go (+3/-2) provider/ec2/ec2.go (+2/-2) provider/ec2/local_test.go (+8/-4) provider/joyent/environ.go (+2/-2) provider/local/environ.go (+1/-1) provider/maas/environ.go (+2/-2) provider/maas/environ_whitebox_test.go (+10/-5) provider/null/environ.go (+11/-3) provider/null/environ_test.go (+6/-3) provider/openstack/local_test.go (+10/-6) provider/openstack/provider.go (+2/-2) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/lp1237736-bootstrap-context |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+197999@code.launchpad.net |
Commit message
Introduce BootstrapContext
Environ.Bootstrap now takes an additional
parameter, a *BootstrapContext. This is a
structure that permits access to the
caller's context. It currently comprises
stdio handles and a signal handler.
Fixes #1237736
Description of the change
Introduce BootstrapContext
Environ.Bootstrap now takes an additional
parameter, a *BootstrapContext. This is a
structure that permits access to the
caller's context. It currently comprises
stdio handles and a signal handler.
Fixes #1237736
Andrew Wilkins (axwalk) wrote : | # |
William Reade (fwereade) wrote : | # |
Looks generally clean, but I'd like rog's input on the interrupt
handling.
https:/
File cloudinit/
https:/
cloudinit/
to follow on from the discussion earlier, should this maybe actually be
a *instance.Config? Not necessarily actionable, just trying to have a
discussion.
https:/
cloudinit/
prompts,
s/solicit/respond to/?
https:/
File environs/
https:/
environs/
*environs.
constraints.Value) error {
Random thought. Might it be good to make this func a method on
BootstrapContext?
https:/
File environs/
https:/
environs/
I seem to recall rog wanted to do things differently here. Rog?
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File cloudinit/
https:/
cloudinit/
On 2013/12/11 08:36:46, fwereade wrote:
> to follow on from the discussion earlier, should this maybe actually
be a
> *instance.Config? Not necessarily actionable, just trying to have a
discussion.
I agree that environs/cloudinit should not be called what it is, but I'm
not sure what the right to call it is. I'm not super keen on putting it
in instance, as the configuration brings a lot of things together that
aren't relevant at the instance.Instance level after provisioning.
https:/
cloudinit/
prompts,
On 2013/12/11 08:36:46, fwereade wrote:
> s/solicit/respond to/?
Done.
https:/
File environs/
https:/
environs/
*environs.
constraints.Value) error {
On 2013/12/11 08:36:46, fwereade wrote:
> Random thought. Might it be good to make this func a method on
BootstrapContext?
I'd rather not, it changes the meaning of what the BootstrapContext is.
My intention is just to capture information (stdio handles) about the
context in which Environ.Bootstrap is being called, and provide a way of
manipulating that context (i.e. installing signal handlers).
Roger Peppe (rogpeppe) wrote : | # |
Looks reasonable in general, but some concerns outlined below.
https:/
File environs/
https:/
environs/
SetInterruptHan
I'm not keen on this approach, as it doesn't work well in a concurrent
situation. It's quite possible that we might have several concurrent
goroutines all wanting to know when they've been interrupted. This
scheme means each one may trample on another's handler.
The underlying os/signal mechanism works fine in that situation. Why not
use something more like that here?
e.g.
type BootstrapContext struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
InterruptNotify func(sig chan<- os.Signal)
StopInterruptN
}
func DefaultBootstra
return &BootstrapContext{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
InterruptNotify: func(c chan<- os.Signal) {
signal.Notify(c, os.Interrupt)
},
StopInterrupt
}
}
One thought - perhaps it might be nice to specify it as an interface
rather
than a concrete type; then cmd.Context could be made to satisfy it.
Something like this, perhaps?
type Stdio struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
type BootstrapContext interface {
Stdio() Stdio
InterruptN
StopInterr
}
https:/
File environs/
https:/
environs/
tests)
I know this is how it already is, but this doesn't make me feel happy.
Somehow it feels like the wrong level of abstraction, and feels like it
makes things hard to script.
This is just a handwave - I can't currently think of concrete
suggestions for anything better, sorry.
https:/
File environs/
https:/
environs/
directory for storage.
Why do we need this? Can't we use a directory inside StorageDir?
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
SetInterruptHan
On 2013/12/11 18:53:24, rog wrote:
> I'm not keen on this approach, as it doesn't work well in a concurrent
> situation. It's quite possible that we might have several concurrent
goroutines
> all wanting to know when they've been interrupted. This scheme means
each one
> may trample on another's handler.
> The underlying os/signal mechanism works fine in that situation. Why
not use
> something more like that here?
> e.g.
> type BootstrapContext struct {
> Stdin io.Reader
> Stdout io.Writer
> Stderr io.Writer
> InterruptNotify func(sig chan<- os.Signal)
> StopInterruptNotify func(chan<- os.Signal)
> }
> func DefaultBootstra
> return &BootstrapContext{
> Stdin: os.Stdin,
> Stdout: os.Stdout,
> Stderr: os.Stderr,
> InterruptNotify: func(c chan<- os.Signal) {
> signal.Notify(c, os.Interrupt)
> },
> StopInterruptNo
> }
> }
> One thought - perhaps it might be nice to specify it as an interface
rather
> than a concrete type; then cmd.Context could be made to satisfy it.
> Something like this, perhaps?
> type Stdio struct {
> Stdin io.Reader
> Stdout io.Writer
> Stderr io.Writer
> }
> type BootstrapContext interface {
> Stdio() Stdio
> InterruptNotify(sig chan<- os.Signal)
> StopInterruptNo
> }
I went with something very similar: Stdio(), Stdout(), and Stderr()
instead of Stdio(). Returning a struct makes for awkward package
dependencies. This does mean having a wrapper struct, as changing all
the fields of cmd.Context to methods is painful.
I've rewritten SetInterruptHan
provider/
InterruptNotify
https:/
File environs/
https:/
environs/
tests)
On 2013/12/11 18:53:24, rog wrote:
> I know this is how it already is, but this doesn't make me feel happy.
> Somehow it feels like the wrong level of abstraction, and feels like
it makes
> things hard to script.
> This is just a handwave - I can't currently think of concrete
suggestions for
> anything better, sorry.
I don't think it's necessarily that bad; perhaps my wording threw you.
It should read "must be a terminal (except in tests, or if the remote
user has passwordless sudo enabled)". If passwordless sudo is enabled,
then you can script it.
https:/
File environs/
https:/
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/19 08:44:33, axw wrote:
> Please take a look.
https:/
> File environs/
https:/
> environs/
> SetInterruptHan
> On 2013/12/11 18:53:24, rog wrote:
> > I'm not keen on this approach, as it doesn't work well in a
concurrent
> > situation. It's quite possible that we might have several concurrent
> goroutines
> > all wanting to know when they've been interrupted. This scheme means
each one
> > may trample on another's handler.
> >
> > The underlying os/signal mechanism works fine in that situation. Why
not use
> > something more like that here?
> >
> > e.g.
> > type BootstrapContext struct {
> > Stdin io.Reader
> > Stdout io.Writer
> > Stderr io.Writer
> > InterruptNotify func(sig chan<- os.Signal)
> > StopInterruptNotify func(chan<- os.Signal)
> > }
> >
> >
> > func DefaultBootstra
> > return &BootstrapContext{
> > Stdin: os.Stdin,
> > Stdout: os.Stdout,
> > Stderr: os.Stderr,
> > InterruptNotify: func(c chan<- os.Signal) {
> > signal.Notify(c, os.Interrupt)
> > },
> > StopInterruptNo
> > }
> > }
> >
> > One thought - perhaps it might be nice to specify it as an interface
rather
> > than a concrete type; then cmd.Context could be made to satisfy it.
> > Something like this, perhaps?
> >
> > type Stdio struct {
> > Stdin io.Reader
> > Stdout io.Writer
> > Stderr io.Writer
> > }
> >
> > type BootstrapContext interface {
> > Stdio() Stdio
> > InterruptNotify(sig chan<- os.Signal)
> > StopInterruptNo
> > }
> >
> I went with something very similar: Stdio(), Stdout(), and Stderr()
instead of
> Stdio(). Returning a struct makes for awkward package dependencies.
This does
> mean having a wrapper struct, as changing all the fields of
cmd.Context to
> methods is painful.
> I've rewritten SetInterruptHan
> provider/
> InterruptNotify
Actually, in the end I got rid of that, got rid of tomb, and just used
the channel directly. Much simpler now, I think.
https:/
> File environs/
https:/
> environs/
in tests)
> On 2013/12/11 18:53:24, rog wrote:
> > I know this is how it already is, but this doesn't make me feel
happy.
> > Somehow it feels like the wrong level of abstraction, and feels like
it makes
> > things hard to script.
> >
> > This is just a handwave - I can't currently think of concrete
suggestions for
> > anything better, sorry.
> I don't think it's necessarily that bad; perhaps my wording threw you.
It should
> read "must be a terminal (except in tests, or if the remote user has
> pa...
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/19 08:44:33, axw wrote:
> Please take a look.
https:/
> File environs/
https:/
> environs/
> SetInterruptHan
> On 2013/12/11 18:53:24, rog wrote:
> > I'm not keen on this approach, as it doesn't work well in a
concurrent
> > situation. It's quite possible that we might have several concurrent
> goroutines
> > all wanting to know when they've been interrupted. This scheme means
each one
> > may trample on another's handler.
> >
> > The underlying os/signal mechanism works fine in that situation. Why
not use
> > something more like that here?
> >
> > e.g.
> > type BootstrapContext struct {
> > Stdin io.Reader
> > Stdout io.Writer
> > Stderr io.Writer
> > InterruptNotify func(sig chan<- os.Signal)
> > StopInterruptNotify func(chan<- os.Signal)
> > }
> >
> >
> > func DefaultBootstra
> > return &BootstrapContext{
> > Stdin: os.Stdin,
> > Stdout: os.Stdout,
> > Stderr: os.Stderr,
> > InterruptNotify: func(c chan<- os.Signal) {
> > signal.Notify(c, os.Interrupt)
> > },
> > StopInterruptNo
> > }
> > }
> >
> > One thought - perhaps it might be nice to specify it as an interface
rather
> > than a concrete type; then cmd.Context could be made to satisfy it.
> > Something like this, perhaps?
> >
> > type Stdio struct {
> > Stdin io.Reader
> > Stdout io.Writer
> > Stderr io.Writer
> > }
> >
> > type BootstrapContext interface {
> > Stdio() Stdio
> > InterruptNotify(sig chan<- os.Signal)
> > StopInterruptNo
> > }
> >
> I went with something very similar: Stdio(), Stdout(), and Stderr()
instead of
> Stdio(). Returning a struct makes for awkward package dependencies.
This does
> mean having a wrapper struct, as changing all the fields of
cmd.Context to
> methods is painful.
> I've rewritten SetInterruptHan
> provider/
> InterruptNotify
Actually, in the end I got rid of that, got rid of tomb, and just used
the channel directly. Much simpler now, I think.
https:/
> File environs/
https:/
> environs/
in tests)
> On 2013/12/11 18:53:24, rog wrote:
> > I know this is how it already is, but this doesn't make me feel
happy.
> > Somehow it feels like the wrong level of abstraction, and feels like
it makes
> > things hard to script.
> >
> > This is just a handwave - I can't currently think of concrete
suggestions for
> > anything better, sorry.
> I don't think it's necessarily that bad; perhaps my wording threw you.
It should
> read "must be a terminal (except in tests, or if the remote user has
> pa...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
LGTM, thanks, with a few thoughts below.
Also, with respect to this:
> I don't think it's necessarily that bad; perhaps my wording threw you.
It should
> read "must be a terminal (except in tests, or if the remote user has
> passwordless sudo enabled)". If passwordless sudo is enabled, then you
can
> script it.
I think the feeling I'm wary of these changes is that it
feels like a gradual creeping of command-
interaction into some of the core logic. I would like it
if our core logic was independent of that (I wish we didn't
have to shell out to ssh at all FWIW).
For example, previously it would have been possible
to call Bootstrap inside a web service, but with the new dependency
on stdio, that's not really viable any more.
https:/
File cmd/juju/
https:/
cmd/juju/
nice
https:/
File environs/
https:/
environs/
obtaining
s/it // ?
https:/
File environs/
https:/
environs/
BootstrapContext {
Wouldn't it be better if this returned environs.
(the local type could be unexported)
https:/
File provider/
https:/
provider/
for _ = range ch {
fmt.
}
return
?
I wonder if we should allow the user to abort if they interrupt more
than 3 times...
https:/
provider/
subprocess at this
For the record, I don't believe this is actually true.
The interrupt gets sent to all processes in the foreground
process group of the terminal.
https:/
provider/
Much more direct - I like.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/19 11:29:57, rog wrote:
> LGTM, thanks, with a few thoughts below.
> Also, with respect to this:
> > I don't think it's necessarily that bad; perhaps my wording threw
you. It
> should
> > read "must be a terminal (except in tests, or if the remote user has
> > passwordless sudo enabled)". If passwordless sudo is enabled, then
you can
> > script it.
> I think the feeling I'm wary of these changes is that it
> feels like a gradual creeping of command-
> interaction into some of the core logic. I would like it
> if our core logic was independent of that (I wish we didn't
> have to shell out to ssh at all FWIW).
That's fair, although I do think bootstrap is somewhat removed from the
"core logic". Your point about web services is a good one.
> For example, previously it would have been possible
> to call Bootstrap inside a web service, but with the new dependency
> on stdio, that's not really viable any more.
As mentioned on IRC, I don't think that's true. We push authorized_keys
to the cloud, and passwordless sudo is enabled by default; there's no
need for any interactivity. The exception is manual provisioning *iff* a
prompt is required (it may not be), which has never been supported
outside of a non-interactive terminal session.
We can do it all now with $SSHPASS, but that's not really ideal due to
its process-global nature. We could change that to add some programmable
prompts, or use PTYs directly to do the same, but it's starting to get
quite complicated. Or we could do it with go.crypto/ssh, which is
something I hope to introduce in the not too distant future.
https:/
> File cmd/juju/
https:/
> cmd/juju/
> nice
https:/
> File environs/
https:/
> environs/
of
> obtaining
> s/it // ?
https:/
> File environs/
https:/
> environs/
> BootstrapContext {
> Wouldn't it be better if this returned environs.
> (the local type could be unexported)
https:/
> File provider/
https:/
> provider/
> for _ = range ch {
> fmt.Fprintln(
> }
> return
> ?
> I wonder if we should allow the user to abort if they interrupt more
than 3
> times...
https:/
> provider/
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
obtaining
On 2013/12/19 11:29:57, rog wrote:
> s/it // ?
I meant provided to Environ.Bootstrap. It makes sense either way to me;
I'm not fussed, I'll change it.
https:/
File environs/
https:/
environs/
BootstrapContext {
On 2013/12/19 11:29:57, rog wrote:
> Wouldn't it be better if this returned environs.
> (the local type could be unexported)
I had planned to do that, but some tests want to change the stdio
objects. Alternatively, NewBootstrapContext could just take a
*cmd.Context. I'll do that before landing.
https:/
File provider/
https:/
provider/
On 2013/12/19 11:29:57, rog wrote:
> for _ = range ch {
> fmt.Fprintln(
> }
> return
> ?
Wow, that was dumb. Thanks.
> I wonder if we should allow the user to abort if they interrupt more
than 3
> times...
Sounds reasonable, but I'd like to stick with what we've got for now and
tweak it based on feedback after it's been used a bit.
https:/
provider/
subprocess at this
On 2013/12/19 11:29:57, rog wrote:
> For the record, I don't believe this is actually true.
> The interrupt gets sent to all processes in the foreground
> process group of the terminal.
My statement wasn't meant imply that it was exclusive of all other
processes in the group. That's why the signal remains blocked; so that
when it gets delivered to the child and this one, this one ignores it
and simply waits on the child's death.
If I'm still wrong, I'd be happy for some schooling :)
Preview Diff
1 | === modified file 'cloudinit/sshinit/configure.go' |
2 | --- cloudinit/sshinit/configure.go 2013-12-16 07:20:01 +0000 |
3 | +++ cloudinit/sshinit/configure.go 2013-12-20 02:40:15 +0000 |
4 | @@ -6,7 +6,7 @@ |
5 | import ( |
6 | "encoding/base64" |
7 | "fmt" |
8 | - "os" |
9 | + "io" |
10 | "strings" |
11 | |
12 | "launchpad.net/loggo" |
13 | @@ -18,24 +18,42 @@ |
14 | |
15 | var logger = loggo.GetLogger("juju.cloudinit.sshinit") |
16 | |
17 | +type ConfigureParams struct { |
18 | + // Host is the host to configure, in the format [user@]hostname. |
19 | + Host string |
20 | + |
21 | + // Config is the cloudinit config to carry out. |
22 | + Config *cloudinit.Config |
23 | + |
24 | + // Stdin is required to respond to sudo prompts, |
25 | + // and must be a terminal (except in tests) |
26 | + Stdin io.Reader |
27 | + |
28 | + // Stdout is required to present sudo prompts to the user. |
29 | + Stdout io.Writer |
30 | + |
31 | + // Stderr is required to present bootstrap progress to the user. |
32 | + Stderr io.Writer |
33 | +} |
34 | + |
35 | // Configure connects to the specified host over SSH, |
36 | // and executes a script that carries out cloud-config. |
37 | -func Configure(host string, cfg *cloudinit.Config) error { |
38 | - logger.Infof("Provisioning machine agent on %s", host) |
39 | - script, err := generateScript(cfg) |
40 | +func Configure(params ConfigureParams) error { |
41 | + logger.Infof("Provisioning machine agent on %s", params.Host) |
42 | + script, err := generateScript(params.Config) |
43 | if err != nil { |
44 | return err |
45 | } |
46 | scriptBase64 := base64.StdEncoding.EncodeToString([]byte(script)) |
47 | script = fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, scriptBase64) |
48 | cmd := ssh.Command( |
49 | - host, |
50 | + params.Host, |
51 | []string{"sudo", fmt.Sprintf("bash -c '%s'", script)}, |
52 | ssh.AllocateTTY, |
53 | ) |
54 | - cmd.Stdout = os.Stdout |
55 | - cmd.Stderr = os.Stderr |
56 | - cmd.Stdin = os.Stdin |
57 | + cmd.Stdout = params.Stdout |
58 | + cmd.Stderr = params.Stderr |
59 | + cmd.Stdin = params.Stdin |
60 | return cmd.Run() |
61 | } |
62 | |
63 | |
64 | === modified file 'cmd/cmd.go' |
65 | --- cmd/cmd.go 2013-12-13 04:56:01 +0000 |
66 | +++ cmd/cmd.go 2013-12-20 02:40:15 +0000 |
67 | @@ -11,6 +11,7 @@ |
68 | "io/ioutil" |
69 | "net/http" |
70 | "os" |
71 | + "os/signal" |
72 | "path/filepath" |
73 | "strings" |
74 | |
75 | @@ -111,6 +112,16 @@ |
76 | return filepath.Join(ctx.Dir, path) |
77 | } |
78 | |
79 | +// InterruptNotify partially satisfies environs/bootstrap.BootstrapContext |
80 | +func (ctx *Context) InterruptNotify(c chan<- os.Signal) { |
81 | + signal.Notify(c, os.Interrupt) |
82 | +} |
83 | + |
84 | +// StopInterruptNotify partially satisfies environs/bootstrap.BootstrapContext |
85 | +func (ctx *Context) StopInterruptNotify(c chan<- os.Signal) { |
86 | + signal.Stop(c) |
87 | +} |
88 | + |
89 | // Info holds some of the usage documentation of a Command. |
90 | type Info struct { |
91 | // Name is the Command's name. |
92 | |
93 | === modified file 'cmd/juju/addmachine.go' |
94 | --- cmd/juju/addmachine.go 2013-12-19 14:19:21 +0000 |
95 | +++ cmd/juju/addmachine.go 2013-12-20 02:40:15 +0000 |
96 | @@ -141,11 +141,14 @@ |
97 | return m.String(), err |
98 | } |
99 | |
100 | -func (c *AddMachineCommand) Run(_ *cmd.Context) error { |
101 | +func (c *AddMachineCommand) Run(ctx *cmd.Context) error { |
102 | if c.SSHHost != "" { |
103 | args := manual.ProvisionMachineArgs{ |
104 | Host: c.SSHHost, |
105 | EnvName: c.EnvName, |
106 | + Stdin: ctx.Stdin, |
107 | + Stdout: ctx.Stdout, |
108 | + Stderr: ctx.Stderr, |
109 | } |
110 | _, err := manual.ProvisionMachine(args) |
111 | return err |
112 | |
113 | === modified file 'cmd/juju/bootstrap.go' |
114 | --- cmd/juju/bootstrap.go 2013-12-03 11:40:28 +0000 |
115 | +++ cmd/juju/bootstrap.go 2013-12-20 02:40:15 +0000 |
116 | @@ -5,6 +5,7 @@ |
117 | |
118 | import ( |
119 | "fmt" |
120 | + "io" |
121 | "strings" |
122 | |
123 | "launchpad.net/gnuflag" |
124 | @@ -77,6 +78,22 @@ |
125 | return cmd.CheckEmpty(args) |
126 | } |
127 | |
128 | +type bootstrapContext struct { |
129 | + *cmd.Context |
130 | +} |
131 | + |
132 | +func (c bootstrapContext) Stdin() io.Reader { |
133 | + return c.Context.Stdin |
134 | +} |
135 | + |
136 | +func (c bootstrapContext) Stdout() io.Writer { |
137 | + return c.Context.Stdout |
138 | +} |
139 | + |
140 | +func (c bootstrapContext) Stderr() io.Writer { |
141 | + return c.Context.Stderr |
142 | +} |
143 | + |
144 | // Run connects to the environment specified on the command line and bootstraps |
145 | // a juju in that environment if none already exists. If there is as yet no environments.yaml file, |
146 | // the user is informed how to create one. |
147 | @@ -89,10 +106,11 @@ |
148 | if err != nil { |
149 | return err |
150 | } |
151 | + bootstrapContext := bootstrapContext{ctx} |
152 | // If the environment has a special bootstrap Storage, use it wherever |
153 | // we'd otherwise use environ.Storage. |
154 | if bs, ok := environ.(environs.BootstrapStorager); ok { |
155 | - if err := bs.EnableBootstrapStorage(); err != nil { |
156 | + if err := bs.EnableBootstrapStorage(bootstrapContext); err != nil { |
157 | return fmt.Errorf("failed to enable bootstrap storage: %v", err) |
158 | } |
159 | } |
160 | @@ -116,7 +134,7 @@ |
161 | return err |
162 | } |
163 | } |
164 | - return bootstrap.Bootstrap(environ, c.Constraints) |
165 | + return bootstrap.Bootstrap(bootstrapContext, environ, c.Constraints) |
166 | } |
167 | |
168 | func (c *BootstrapCommand) uploadTools(environ environs.Environ) error { |
169 | |
170 | === modified file 'environs/bootstrap/bootstrap.go' |
171 | --- environs/bootstrap/bootstrap.go 2013-12-20 00:30:15 +0000 |
172 | +++ environs/bootstrap/bootstrap.go 2013-12-20 02:40:15 +0000 |
173 | @@ -20,7 +20,7 @@ |
174 | // Bootstrap bootstraps the given environment. The supplied constraints are |
175 | // used to provision the instance, and are also set within the bootstrapped |
176 | // environment. |
177 | -func Bootstrap(environ environs.Environ, cons constraints.Value) error { |
178 | +func Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, cons constraints.Value) error { |
179 | cfg := environ.Config() |
180 | if secret := cfg.AdminSecret(); secret == "" { |
181 | return fmt.Errorf("environment configuration has no admin-secret") |
182 | @@ -44,7 +44,7 @@ |
183 | return err |
184 | } |
185 | logger.Infof("bootstrapping environment %q", environ.Name()) |
186 | - return environ.Bootstrap(cons) |
187 | + return environ.Bootstrap(ctx, cons) |
188 | } |
189 | |
190 | // SetBootstrapTools returns the newest tools from the given tools list, |
191 | |
192 | === modified file 'environs/bootstrap/bootstrap_test.go' |
193 | --- environs/bootstrap/bootstrap_test.go 2013-11-18 05:41:36 +0000 |
194 | +++ environs/bootstrap/bootstrap_test.go 2013-12-20 02:40:15 +0000 |
195 | @@ -53,6 +53,11 @@ |
196 | s.LoggingSuite.TearDownTest(c) |
197 | } |
198 | |
199 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
200 | + ctx := coretesting.Context(c) |
201 | + return envtesting.NewBootstrapContext(ctx) |
202 | +} |
203 | + |
204 | func (s *bootstrapSuite) TestBootstrapNeedsSettings(c *gc.C) { |
205 | env := newEnviron("bar", noKeysDefined) |
206 | s.setDummyStorage(c, env) |
207 | @@ -64,20 +69,20 @@ |
208 | env.cfg = cfg |
209 | } |
210 | |
211 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
212 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
213 | c.Assert(err, gc.ErrorMatches, "environment configuration has no admin-secret") |
214 | |
215 | fixEnv("admin-secret", "whatever") |
216 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
217 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
218 | c.Assert(err, gc.ErrorMatches, "environment configuration has no ca-cert") |
219 | |
220 | fixEnv("ca-cert", coretesting.CACert) |
221 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
222 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
223 | c.Assert(err, gc.ErrorMatches, "environment configuration has no ca-private-key") |
224 | |
225 | fixEnv("ca-private-key", coretesting.CAKey) |
226 | uploadTools(c, env) |
227 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
228 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
229 | c.Assert(err, gc.IsNil) |
230 | } |
231 | |
232 | @@ -90,7 +95,7 @@ |
233 | func (s *bootstrapSuite) TestBootstrapEmptyConstraints(c *gc.C) { |
234 | env := newEnviron("foo", useDefaultKeys) |
235 | s.setDummyStorage(c, env) |
236 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
237 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
238 | c.Assert(err, gc.IsNil) |
239 | c.Assert(env.bootstrapCount, gc.Equals, 1) |
240 | c.Assert(env.constraints, gc.DeepEquals, constraints.Value{}) |
241 | @@ -100,7 +105,7 @@ |
242 | env := newEnviron("foo", useDefaultKeys) |
243 | s.setDummyStorage(c, env) |
244 | cons := constraints.MustParse("cpu-cores=2 mem=4G") |
245 | - err := bootstrap.Bootstrap(env, cons) |
246 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, cons) |
247 | c.Assert(err, gc.IsNil) |
248 | c.Assert(env.bootstrapCount, gc.Equals, 1) |
249 | c.Assert(env.constraints, gc.DeepEquals, cons) |
250 | @@ -163,7 +168,7 @@ |
251 | if test.Arch != "" { |
252 | cons = constraints.MustParse("arch=" + test.Arch) |
253 | } |
254 | - err = bootstrap.Bootstrap(env, cons) |
255 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, cons) |
256 | if test.Err != nil { |
257 | c.Check(err, gc.ErrorMatches, ".*"+test.Err.Error()) |
258 | continue |
259 | @@ -186,7 +191,7 @@ |
260 | env := newEnviron("foo", useDefaultKeys) |
261 | s.setDummyStorage(c, env) |
262 | envtesting.RemoveFakeTools(c, env.Storage()) |
263 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
264 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
265 | // bootstrap.Bootstrap leaves it to the provider to |
266 | // locate bootstrap tools. |
267 | c.Assert(err, gc.IsNil) |
268 | @@ -252,7 +257,7 @@ |
269 | return e.name |
270 | } |
271 | |
272 | -func (e *bootstrapEnviron) Bootstrap(cons constraints.Value) error { |
273 | +func (e *bootstrapEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
274 | e.bootstrapCount++ |
275 | e.constraints = cons |
276 | return nil |
277 | |
278 | === modified file 'environs/interface.go' |
279 | --- environs/interface.go 2013-11-18 05:41:36 +0000 |
280 | +++ environs/interface.go 2013-12-20 02:40:15 +0000 |
281 | @@ -4,6 +4,9 @@ |
282 | package environs |
283 | |
284 | import ( |
285 | + "io" |
286 | + "os" |
287 | + |
288 | "launchpad.net/juju-core/constraints" |
289 | "launchpad.net/juju-core/environs/config" |
290 | "launchpad.net/juju-core/environs/storage" |
291 | @@ -71,7 +74,7 @@ |
292 | // EnableBootstrapStorage enables bootstrap storage, returning an |
293 | // error if enablement failed. If nil is returned, then calling |
294 | // this again will have no effect and will return nil. |
295 | - EnableBootstrapStorage() error |
296 | + EnableBootstrapStorage(BootstrapContext) error |
297 | } |
298 | |
299 | // ConfigGetter implements access to an environments configuration. |
300 | @@ -136,7 +139,7 @@ |
301 | // Bootstrap is responsible for selecting the appropriate tools, |
302 | // and setting the agent-version configuration attribute prior to |
303 | // bootstrapping the environment. |
304 | - Bootstrap(cons constraints.Value) error |
305 | + Bootstrap(ctx BootstrapContext, cons constraints.Value) error |
306 | |
307 | // StateInfo returns information on the state initialized |
308 | // by Bootstrap. |
309 | @@ -192,3 +195,24 @@ |
310 | // Provider returns the EnvironProvider that created this Environ. |
311 | Provider() EnvironProvider |
312 | } |
313 | + |
314 | +// BootstrapContext is an interface that is passed to |
315 | +// Environ.Bootstrap, providing a means of obtaining |
316 | +// information about and manipulating the context in which |
317 | +// it is being invoked. |
318 | +type BootstrapContext interface { |
319 | + Stdin() io.Reader |
320 | + Stdout() io.Writer |
321 | + Stderr() io.Writer |
322 | + |
323 | + // InterruptNotify starts watching for interrupt signals |
324 | + // on behalf of the caller, sending them to the supplied |
325 | + // channel. |
326 | + InterruptNotify(sig chan<- os.Signal) |
327 | + |
328 | + // StopInterruptNotify undoes the effects of a previous |
329 | + // call to InterruptNotify with the same channel. After |
330 | + // StopInterruptNotify returns, no more signals will be |
331 | + // delivered to the channel. |
332 | + StopInterruptNotify(chan<- os.Signal) |
333 | +} |
334 | |
335 | === modified file 'environs/jujutest/livetests.go' |
336 | --- environs/jujutest/livetests.go 2013-12-19 19:54:23 +0000 |
337 | +++ environs/jujutest/livetests.go 2013-12-20 02:40:15 +0000 |
338 | @@ -140,7 +140,7 @@ |
339 | envtesting.UploadFakeTools(c, t.Env.Storage()) |
340 | err := common.EnsureNotBootstrapped(t.Env) |
341 | c.Assert(err, gc.IsNil) |
342 | - err = bootstrap.Bootstrap(t.Env, cons) |
343 | + err = bootstrap.Bootstrap(bootstrapContext(c), t.Env, cons) |
344 | c.Assert(err, gc.IsNil) |
345 | t.bootstrapped = true |
346 | } |
347 | @@ -924,7 +924,7 @@ |
348 | err = storageCopy(dummyStorage, currentName, envStorage, otherName) |
349 | c.Assert(err, gc.IsNil) |
350 | |
351 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
352 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
353 | c.Assert(err, gc.IsNil) |
354 | |
355 | conn, err := juju.NewConn(env) |
356 | |
357 | === modified file 'environs/jujutest/tests.go' |
358 | --- environs/jujutest/tests.go 2013-10-15 04:38:40 +0000 |
359 | +++ environs/jujutest/tests.go 2013-12-20 02:40:15 +0000 |
360 | @@ -78,6 +78,10 @@ |
361 | t.LoggingSuite.TearDownTest(c) |
362 | } |
363 | |
364 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
365 | + return envtesting.NewBootstrapContext(coretesting.Context(c)) |
366 | +} |
367 | + |
368 | func (t *Tests) TestStartStop(c *gc.C) { |
369 | e := t.Prepare(c) |
370 | envtesting.UploadFakeTools(c, e.Storage()) |
371 | @@ -134,7 +138,7 @@ |
372 | envtesting.UploadFakeTools(c, e.Storage()) |
373 | err := common.EnsureNotBootstrapped(e) |
374 | c.Assert(err, gc.IsNil) |
375 | - err = bootstrap.Bootstrap(e, constraints.Value{}) |
376 | + err = bootstrap.Bootstrap(bootstrapContext(c), e, constraints.Value{}) |
377 | c.Assert(err, gc.IsNil) |
378 | |
379 | info, apiInfo, err := e.StateInfo() |
380 | @@ -162,7 +166,7 @@ |
381 | |
382 | err = common.EnsureNotBootstrapped(e3) |
383 | c.Assert(err, gc.IsNil) |
384 | - err = bootstrap.Bootstrap(e3, constraints.Value{}) |
385 | + err = bootstrap.Bootstrap(bootstrapContext(c), e3, constraints.Value{}) |
386 | c.Assert(err, gc.IsNil) |
387 | |
388 | err = common.EnsureNotBootstrapped(e3) |
389 | |
390 | === modified file 'environs/manual/bootstrap.go' |
391 | --- environs/manual/bootstrap.go 2013-11-20 02:31:56 +0000 |
392 | +++ environs/manual/bootstrap.go 2013-12-20 02:40:15 +0000 |
393 | @@ -30,6 +30,7 @@ |
394 | DataDir string |
395 | Environ LocalStorageEnviron |
396 | PossibleTools tools.List |
397 | + Context environs.BootstrapContext |
398 | |
399 | // If series and hardware characteristics |
400 | // are known ahead of time, they can be |
401 | @@ -134,5 +135,5 @@ |
402 | for k, v := range agentEnv { |
403 | mcfg.AgentEnvironment[k] = v |
404 | } |
405 | - return provisionMachineAgent(args.Host, mcfg) |
406 | + return provisionMachineAgent(args.Host, mcfg, args.Context.Stdin(), args.Context.Stdout(), args.Context.Stderr()) |
407 | } |
408 | |
409 | === modified file 'environs/manual/bootstrap_test.go' |
410 | --- environs/manual/bootstrap_test.go 2013-11-18 04:53:44 +0000 |
411 | +++ environs/manual/bootstrap_test.go 2013-12-20 02:40:15 +0000 |
412 | @@ -12,9 +12,11 @@ |
413 | "launchpad.net/juju-core/environs/bootstrap" |
414 | "launchpad.net/juju-core/environs/filestorage" |
415 | "launchpad.net/juju-core/environs/storage" |
416 | + envtesting "launchpad.net/juju-core/environs/testing" |
417 | "launchpad.net/juju-core/environs/tools" |
418 | "launchpad.net/juju-core/instance" |
419 | "launchpad.net/juju-core/juju/testing" |
420 | + coretesting "launchpad.net/juju-core/testing" |
421 | ) |
422 | |
423 | type bootstrapSuite struct { |
424 | @@ -74,6 +76,7 @@ |
425 | DataDir: "/var/lib/juju", |
426 | Environ: s.env, |
427 | PossibleTools: toolsList, |
428 | + Context: envtesting.NewBootstrapContext(coretesting.Context(c)), |
429 | } |
430 | } |
431 | |
432 | |
433 | === modified file 'environs/manual/provisioner.go' |
434 | --- environs/manual/provisioner.go 2013-12-11 09:14:12 +0000 |
435 | +++ environs/manual/provisioner.go 2013-12-20 02:40:15 +0000 |
436 | @@ -6,6 +6,7 @@ |
437 | import ( |
438 | "errors" |
439 | "fmt" |
440 | + "io" |
441 | "net" |
442 | "strings" |
443 | |
444 | @@ -44,6 +45,16 @@ |
445 | // Tools to install on the machine. If nil, tools will be automatically |
446 | // chosen using environs/tools FindInstanceTools. |
447 | Tools *tools.Tools |
448 | + |
449 | + // Stdin is required to respond to sudo prompts, |
450 | + // and must be a terminal (except in tests) |
451 | + Stdin io.Reader |
452 | + |
453 | + // Stdout is required to present sudo prompts to the user. |
454 | + Stdout io.Writer |
455 | + |
456 | + // Stderr is required to present machine provisioning progress to the user. |
457 | + Stderr io.Writer |
458 | } |
459 | |
460 | // ErrProvisioned is returned by ProvisionMachine if the target |
461 | @@ -91,7 +102,7 @@ |
462 | } |
463 | |
464 | // Finally, provision the machine agent. |
465 | - err = provisionMachineAgent(args.Host, mcfg) |
466 | + err = provisionMachineAgent(args.Host, mcfg, args.Stdin, args.Stdout, args.Stderr) |
467 | if err != nil { |
468 | return machineId, err |
469 | } |
470 | @@ -205,7 +216,7 @@ |
471 | return mcfg, nil |
472 | } |
473 | |
474 | -func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig) error { |
475 | +func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig, stdin io.Reader, stdout, stderr io.Writer) error { |
476 | cloudcfg := coreCloudinit.New() |
477 | if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { |
478 | return err |
479 | @@ -213,5 +224,11 @@ |
480 | // Explicitly disabling apt_upgrade so as not to trample |
481 | // the target machine's existing configuration. |
482 | cloudcfg.SetAptUpgrade(false) |
483 | - return sshinit.Configure(host, cloudcfg) |
484 | + return sshinit.Configure(sshinit.ConfigureParams{ |
485 | + Host: host, |
486 | + Config: cloudcfg, |
487 | + Stdin: stdin, |
488 | + Stdout: stdout, |
489 | + Stderr: stderr, |
490 | + }) |
491 | } |
492 | |
493 | === modified file 'environs/open_test.go' |
494 | --- environs/open_test.go 2013-10-04 12:18:17 +0000 |
495 | +++ environs/open_test.go 2013-12-20 02:40:15 +0000 |
496 | @@ -40,7 +40,9 @@ |
497 | env, err := environs.Prepare(cfg, configstore.NewMem()) |
498 | c.Assert(err, gc.IsNil) |
499 | envtesting.UploadFakeTools(c, env.Storage()) |
500 | - c.Assert(bootstrap.Bootstrap(env, constraints.Value{}), gc.IsNil) |
501 | + ctx := envtesting.NewBootstrapContext(testing.Context(c)) |
502 | + err = bootstrap.Bootstrap(ctx, env, constraints.Value{}) |
503 | + c.Assert(err, gc.IsNil) |
504 | } |
505 | |
506 | func (*OpenSuite) TestNewUnknownEnviron(c *gc.C) { |
507 | |
508 | === modified file 'environs/sshstorage/storage.go' |
509 | --- environs/sshstorage/storage.go 2013-12-03 05:20:25 +0000 |
510 | +++ environs/sshstorage/storage.go 2013-12-20 02:40:15 +0000 |
511 | @@ -11,7 +11,6 @@ |
512 | "fmt" |
513 | "io" |
514 | "io/ioutil" |
515 | - "os" |
516 | "os/exec" |
517 | "path" |
518 | "sort" |
519 | @@ -59,32 +58,49 @@ |
520 | flockExclusive flockmode = "-x" |
521 | ) |
522 | |
523 | +type NewSSHStorageParams struct { |
524 | + // Host is the host to connect to, in the format [user@]hostname. |
525 | + Host string |
526 | + |
527 | + // StorageDir is the root of the remote storage directory. |
528 | + StorageDir string |
529 | + |
530 | + // TmpDir is the remote temporary directory for storage. |
531 | + // A temporary directory must be specified, and should be located on the |
532 | + // same filesystem as the storage directory to ensure atomic writes. |
533 | + // The temporary directory will be created when NewSSHStorage is invoked |
534 | + // if it doesn't already exist; it will never be removed. NewSSHStorage |
535 | + // will attempt to reassign ownership to the login user, and will return |
536 | + // an error if it cannot do so. |
537 | + TmpDir string |
538 | + |
539 | + // Stdin in required to respond to sudo passwords, |
540 | + // and must be a terminal (except in tests). |
541 | + Stdin io.Reader |
542 | + |
543 | + // Stdout is required to present sudo prompts to the user. |
544 | + Stdout io.Writer |
545 | +} |
546 | + |
547 | // NewSSHStorage creates a new SSHStorage, connected to the |
548 | // specified host, managing state under the specified remote path. |
549 | -// |
550 | -// A temporary directory must be specified, and should be located on the |
551 | -// same filesystem as the storage directory to ensure atomic writes. |
552 | -// The temporary directory will be created when NewSSHStorage is invoked |
553 | -// if it doesn't already exist; it will never be removed. NewSSHStorage |
554 | -// will attempt to reassign ownership to the login user, and will return |
555 | -// an error if it cannot do so. |
556 | -func NewSSHStorage(host, storagedir, tmpdir string) (*SSHStorage, error) { |
557 | - if storagedir == "" { |
558 | +func NewSSHStorage(params NewSSHStorageParams) (*SSHStorage, error) { |
559 | + if params.StorageDir == "" { |
560 | return nil, errors.New("storagedir must be specified and non-empty") |
561 | } |
562 | - if tmpdir == "" { |
563 | + if params.TmpDir == "" { |
564 | return nil, errors.New("tmpdir must be specified and non-empty") |
565 | } |
566 | |
567 | script := fmt.Sprintf( |
568 | "install -d -g $SUDO_GID -o $SUDO_UID %s %s", |
569 | - utils.ShQuote(storagedir), |
570 | - utils.ShQuote(tmpdir), |
571 | + utils.ShQuote(params.StorageDir), |
572 | + utils.ShQuote(params.TmpDir), |
573 | ) |
574 | |
575 | - cmd := sshCommand(host, true, fmt.Sprintf("sudo bash -c %s", utils.ShQuote(script))) |
576 | - cmd.Stdin = os.Stdin |
577 | - cmd.Stdout = os.Stdout // for sudo prompts/output |
578 | + cmd := sshCommand(params.Host, true, fmt.Sprintf("sudo bash -c %s", utils.ShQuote(script))) |
579 | + cmd.Stdin = params.Stdin |
580 | + cmd.Stdout = params.Stdout // for sudo prompts/output |
581 | var stderr bytes.Buffer |
582 | cmd.Stderr = &stderr |
583 | if err := cmd.Run(); err != nil { |
584 | @@ -95,7 +111,7 @@ |
585 | // We could use sftp, but then we'd be at the mercy of |
586 | // sftp's output messages for checking errors. Instead, |
587 | // we execute an interactive bash shell. |
588 | - cmd = sshCommand(host, false, "bash") |
589 | + cmd = sshCommand(params.Host, false, "bash") |
590 | stdin, err := cmd.StdinPipe() |
591 | if err != nil { |
592 | return nil, err |
593 | @@ -109,9 +125,9 @@ |
594 | // get at catastrophic failure messages. |
595 | cmd.Stderr = cmd.Stdout |
596 | stor := &SSHStorage{ |
597 | - host: host, |
598 | - remotepath: storagedir, |
599 | - tmpdir: tmpdir, |
600 | + host: params.Host, |
601 | + remotepath: params.StorageDir, |
602 | + tmpdir: params.TmpDir, |
603 | cmd: cmd, |
604 | stdin: stdin, |
605 | stdout: stdout, |
606 | @@ -120,7 +136,7 @@ |
607 | cmd.Start() |
608 | |
609 | // Verify we have write permissions. |
610 | - _, err = stor.runf(flockExclusive, "touch %s", utils.ShQuote(storagedir)) |
611 | + _, err = stor.runf(flockExclusive, "touch %s", utils.ShQuote(params.StorageDir)) |
612 | if err != nil { |
613 | stdin.Close() |
614 | stdout.Close() |
615 | |
616 | === modified file 'environs/sshstorage/storage_test.go' |
617 | --- environs/sshstorage/storage_test.go 2013-10-07 03:40:52 +0000 |
618 | +++ environs/sshstorage/storage_test.go 2013-12-20 02:40:15 +0000 |
619 | @@ -40,6 +40,17 @@ |
620 | return cmd |
621 | } |
622 | |
623 | +func newSSHStorage(host, storageDir, tmpDir string) (*SSHStorage, error) { |
624 | + params := NewSSHStorageParams{ |
625 | + Host: host, |
626 | + StorageDir: storageDir, |
627 | + TmpDir: tmpDir, |
628 | + Stdin: &bytes.Buffer{}, |
629 | + Stdout: ioutil.Discard, |
630 | + } |
631 | + return NewSSHStorage(params) |
632 | +} |
633 | + |
634 | // flockBin is the path to the original "flock" binary. |
635 | var flockBin string |
636 | |
637 | @@ -68,7 +79,7 @@ |
638 | |
639 | func (s *storageSuite) makeStorage(c *gc.C) (storage *SSHStorage, storageDir string) { |
640 | storageDir = c.MkDir() |
641 | - storage, err := NewSSHStorage("example.com", storageDir, storageDir+"-tmp") |
642 | + storage, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp") |
643 | c.Assert(err, gc.IsNil) |
644 | c.Assert(storage, gc.NotNil) |
645 | s.AddCleanup(func(*gc.C) { storage.Close() }) |
646 | @@ -89,12 +100,12 @@ |
647 | } |
648 | } |
649 | |
650 | -func (s *storageSuite) TestNewSSHStorage(c *gc.C) { |
651 | +func (s *storageSuite) TestnewSSHStorage(c *gc.C) { |
652 | storageDir := c.MkDir() |
653 | - // Run this block twice to ensure NewSSHStorage can reuse |
654 | + // Run this block twice to ensure newSSHStorage can reuse |
655 | // an existing storage location. |
656 | for i := 0; i < 2; i++ { |
657 | - stor, err := NewSSHStorage("example.com", storageDir, storageDir+"-tmp") |
658 | + stor, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp") |
659 | c.Assert(err, gc.IsNil) |
660 | c.Assert(stor, gc.NotNil) |
661 | c.Assert(stor.Close(), gc.IsNil) |
662 | @@ -106,7 +117,7 @@ |
663 | storageDir = c.MkDir() |
664 | err = os.Chmod(storageDir, 0555) |
665 | c.Assert(err, gc.IsNil) |
666 | - _, err = NewSSHStorage("example.com", filepath.Join(storageDir, "subdir"), storageDir+"-tmp") |
667 | + _, err = newSSHStorage("example.com", filepath.Join(storageDir, "subdir"), storageDir+"-tmp") |
668 | c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot change owner and permissions of(.|\n)*") |
669 | } |
670 | |
671 | @@ -177,13 +188,13 @@ |
672 | } |
673 | s.PatchValue(&sshCommand, badSshCommand) |
674 | |
675 | - stor, err := NewSSHStorage("example.com", c.MkDir(), c.MkDir()) |
676 | + stor, err := newSSHStorage("example.com", c.MkDir(), c.MkDir()) |
677 | c.Assert(err, gc.IsNil) |
678 | defer stor.Close() |
679 | err = stor.Put("whatever", bytes.NewBuffer(nil), 0) |
680 | c.Assert(err, gc.ErrorMatches, `failed to write input: write \|1: broken pipe \(output: "blah blah\\nmore"\)`) |
681 | |
682 | - _, err = NewSSHStorage("example.com", c.MkDir(), c.MkDir()) |
683 | + _, err = newSSHStorage("example.com", c.MkDir(), c.MkDir()) |
684 | c.Assert(err, gc.ErrorMatches, `failed to locate "JUJU-RC: " \(output: "Hey it's JUJU-RC: , but not at the beginning of the line\\nmore"\)`) |
685 | } |
686 | |
687 | @@ -292,7 +303,7 @@ |
688 | // |
689 | // flock exits with exit code 1 if it can't acquire the |
690 | // lock immediately in non-blocking mode (which the tests force). |
691 | - _, err := NewSSHStorage("example.com", storageDir, storageDir+"-tmp") |
692 | + _, err := newSSHStorage("example.com", storageDir, storageDir+"-tmp") |
693 | c.Assert(err, gc.ErrorMatches, "exit code 1") |
694 | } |
695 | |
696 | @@ -338,13 +349,13 @@ |
697 | |
698 | func (s *storageSuite) TestStorageDirBlank(c *gc.C) { |
699 | tmpdir := c.MkDir() |
700 | - _, err := NewSSHStorage("example.com", "", tmpdir) |
701 | + _, err := newSSHStorage("example.com", "", tmpdir) |
702 | c.Assert(err, gc.ErrorMatches, "storagedir must be specified and non-empty") |
703 | } |
704 | |
705 | func (s *storageSuite) TestTmpDirBlank(c *gc.C) { |
706 | storageDir := c.MkDir() |
707 | - _, err := NewSSHStorage("example.com", storageDir, "") |
708 | + _, err := newSSHStorage("example.com", storageDir, "") |
709 | c.Assert(err, gc.ErrorMatches, "tmpdir must be specified and non-empty") |
710 | } |
711 | |
712 | @@ -354,7 +365,7 @@ |
713 | storageDir := c.MkDir() |
714 | tmpdirs := []string{storageDir, filepath.Join(storageDir, "subdir")} |
715 | for _, tmpdir := range tmpdirs { |
716 | - stor, err := NewSSHStorage("example.com", storageDir, tmpdir) |
717 | + stor, err := newSSHStorage("example.com", storageDir, tmpdir) |
718 | defer stor.Close() |
719 | c.Assert(err, gc.IsNil) |
720 | err = stor.Put("test-write", bytes.NewReader(nil), 0) |
721 | @@ -363,13 +374,13 @@ |
722 | } |
723 | |
724 | func (s *storageSuite) TestTmpDirPermissions(c *gc.C) { |
725 | - // NewSSHStorage will fail if it can't create or change the |
726 | + // newSSHStorage will fail if it can't create or change the |
727 | // permissions of the temporary directory. |
728 | storageDir := c.MkDir() |
729 | tmpdir := c.MkDir() |
730 | os.Chmod(tmpdir, 0400) |
731 | defer os.Chmod(tmpdir, 0755) |
732 | - _, err := NewSSHStorage("example.com", storageDir, filepath.Join(tmpdir, "subdir2")) |
733 | + _, err := newSSHStorage("example.com", storageDir, filepath.Join(tmpdir, "subdir2")) |
734 | c.Assert(err, gc.ErrorMatches, ".*install: cannot create directory.*Permission denied.*") |
735 | } |
736 | |
737 | @@ -379,6 +390,6 @@ |
738 | tmpdir := filepath.Join(storageDirBase, `"`) |
739 | c.Assert(os.Mkdir(storageDir, 0755), gc.IsNil) |
740 | c.Assert(os.Mkdir(tmpdir, 0755), gc.IsNil) |
741 | - _, err := NewSSHStorage("example.com", storageDir, tmpdir) |
742 | + _, err := newSSHStorage("example.com", storageDir, tmpdir) |
743 | c.Assert(err, gc.IsNil) |
744 | } |
745 | |
746 | === modified file 'environs/testing/bootstrap.go' |
747 | --- environs/testing/bootstrap.go 2013-12-03 05:20:25 +0000 |
748 | +++ environs/testing/bootstrap.go 2013-12-20 02:40:15 +0000 |
749 | @@ -4,8 +4,12 @@ |
750 | package testing |
751 | |
752 | import ( |
753 | + "io" |
754 | + |
755 | "launchpad.net/loggo" |
756 | |
757 | + "launchpad.net/juju-core/cmd" |
758 | + "launchpad.net/juju-core/environs" |
759 | "launchpad.net/juju-core/environs/cloudinit" |
760 | "launchpad.net/juju-core/instance" |
761 | "launchpad.net/juju-core/provider/common" |
762 | @@ -18,9 +22,29 @@ |
763 | // do not attempt to SSH to non-existent machines. The result is a function |
764 | // that restores finishBootstrap. |
765 | func DisableFinishBootstrap() func() { |
766 | - f := func(*common.BootstrapContext, instance.Instance, *cloudinit.MachineConfig) error { |
767 | + f := func(environs.BootstrapContext, instance.Instance, *cloudinit.MachineConfig) error { |
768 | logger.Warningf("provider/common.FinishBootstrap is disabled") |
769 | return nil |
770 | } |
771 | return testbase.PatchValue(&common.FinishBootstrap, f) |
772 | } |
773 | + |
774 | +type bootstrapContext struct { |
775 | + *cmd.Context |
776 | +} |
777 | + |
778 | +func (c bootstrapContext) Stdin() io.Reader { |
779 | + return c.Context.Stdin |
780 | +} |
781 | + |
782 | +func (c bootstrapContext) Stdout() io.Writer { |
783 | + return c.Context.Stdout |
784 | +} |
785 | + |
786 | +func (c bootstrapContext) Stderr() io.Writer { |
787 | + return c.Context.Stderr |
788 | +} |
789 | + |
790 | +func NewBootstrapContext(ctx *cmd.Context) environs.BootstrapContext { |
791 | + return bootstrapContext{ctx} |
792 | +} |
793 | |
794 | === modified file 'juju/apiconn_test.go' |
795 | --- juju/apiconn_test.go 2013-12-04 10:18:57 +0000 |
796 | +++ juju/apiconn_test.go 2013-12-20 02:40:15 +0000 |
797 | @@ -49,7 +49,7 @@ |
798 | c.Assert(err, gc.IsNil) |
799 | |
800 | envtesting.UploadFakeTools(c, env.Storage()) |
801 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
802 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
803 | c.Assert(err, gc.IsNil) |
804 | |
805 | cfg = env.Config() |
806 | |
807 | === modified file 'juju/conn_test.go' |
808 | --- juju/conn_test.go 2013-12-19 19:54:23 +0000 |
809 | +++ juju/conn_test.go 2013-12-20 02:40:15 +0000 |
810 | @@ -56,13 +56,17 @@ |
811 | cs.LoggingSuite.TearDownTest(c) |
812 | } |
813 | |
814 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
815 | + return envtesting.NewBootstrapContext(coretesting.Context(c)) |
816 | +} |
817 | + |
818 | func (*NewConnSuite) TestNewConnWithoutAdminSecret(c *gc.C) { |
819 | cfg, err := config.New(config.NoDefaults, dummy.SampleConfig()) |
820 | c.Assert(err, gc.IsNil) |
821 | env, err := environs.Prepare(cfg, configstore.NewMem()) |
822 | c.Assert(err, gc.IsNil) |
823 | envtesting.UploadFakeTools(c, env.Storage()) |
824 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
825 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
826 | c.Assert(err, gc.IsNil) |
827 | |
828 | attrs := env.Config().AllAttrs() |
829 | @@ -81,7 +85,7 @@ |
830 | env, err := environs.PrepareFromName(envName, store) |
831 | c.Assert(err, gc.IsNil) |
832 | envtesting.UploadFakeTools(c, env.Storage()) |
833 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
834 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
835 | c.Assert(err, gc.IsNil) |
836 | } |
837 | |
838 | @@ -126,7 +130,7 @@ |
839 | env, err := environs.Prepare(cfg, configstore.NewMem()) |
840 | c.Assert(err, gc.IsNil) |
841 | envtesting.UploadFakeTools(c, env.Storage()) |
842 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
843 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
844 | c.Assert(err, gc.IsNil) |
845 | info, _, err := env.StateInfo() |
846 | c.Assert(err, gc.IsNil) |
847 | @@ -174,7 +178,7 @@ |
848 | env, err := environs.Prepare(cfg, configstore.NewMem()) |
849 | c.Assert(err, gc.IsNil) |
850 | envtesting.UploadFakeTools(c, env.Storage()) |
851 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
852 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
853 | c.Assert(err, gc.IsNil) |
854 | |
855 | // Make a new Conn, which will push the secrets. |
856 | @@ -210,7 +214,7 @@ |
857 | env, err := environs.Prepare(cfg, configstore.NewMem()) |
858 | c.Assert(err, gc.IsNil) |
859 | envtesting.UploadFakeTools(c, env.Storage()) |
860 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
861 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
862 | c.Assert(err, gc.IsNil) |
863 | |
864 | // Check that Bootstrap has correctly used a hash |
865 | @@ -264,7 +268,7 @@ |
866 | environ, err := environs.Prepare(cfg, configstore.NewMem()) |
867 | c.Assert(err, gc.IsNil) |
868 | envtesting.UploadFakeTools(c, environ.Storage()) |
869 | - err = bootstrap.Bootstrap(environ, constraints.Value{}) |
870 | + err = bootstrap.Bootstrap(bootstrapContext(c), environ, constraints.Value{}) |
871 | c.Assert(err, gc.IsNil) |
872 | s.conn, err = juju.NewConn(environ) |
873 | c.Assert(err, gc.IsNil) |
874 | |
875 | === modified file 'juju/testing/conn.go' |
876 | --- juju/testing/conn.go 2013-12-03 14:19:47 +0000 |
877 | +++ juju/testing/conn.go 2013-12-20 02:40:15 +0000 |
878 | @@ -198,7 +198,8 @@ |
879 | c.Assert(environ.Name(), gc.Equals, "dummyenv") |
880 | |
881 | envtesting.MustUploadFakeTools(environ.Storage()) |
882 | - c.Assert(bootstrap.Bootstrap(environ, constraints.Value{}), gc.IsNil) |
883 | + ctx := envtesting.NewBootstrapContext(testing.Context(c)) |
884 | + c.Assert(bootstrap.Bootstrap(ctx, environ, constraints.Value{}), gc.IsNil) |
885 | |
886 | s.BackingState = environ.(GetStater).GetStateInAPIServer() |
887 | |
888 | |
889 | === modified file 'provider/azure/environ.go' |
890 | --- provider/azure/environ.go 2013-11-18 05:41:36 +0000 |
891 | +++ provider/azure/environ.go 2013-12-20 02:40:15 +0000 |
892 | @@ -231,7 +231,7 @@ |
893 | } |
894 | |
895 | // Bootstrap is specified in the Environ interface. |
896 | -func (env *azureEnviron) Bootstrap(cons constraints.Value) (err error) { |
897 | +func (env *azureEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) (err error) { |
898 | // The creation of the affinity group and the virtual network is specific to the Azure provider. |
899 | err = env.createAffinityGroup() |
900 | if err != nil { |
901 | @@ -253,7 +253,7 @@ |
902 | env.deleteVirtualNetwork() |
903 | } |
904 | }() |
905 | - err = common.Bootstrap(env, cons) |
906 | + err = common.Bootstrap(ctx, env, cons) |
907 | return err |
908 | } |
909 | |
910 | |
911 | === modified file 'provider/common/bootstrap.go' |
912 | --- provider/common/bootstrap.go 2013-12-18 10:11:12 +0000 |
913 | +++ provider/common/bootstrap.go 2013-12-20 02:40:15 +0000 |
914 | @@ -7,14 +7,11 @@ |
915 | "fmt" |
916 | "io" |
917 | "os" |
918 | - "os/signal" |
919 | "path" |
920 | "strings" |
921 | - "sync" |
922 | "time" |
923 | |
924 | "launchpad.net/loggo" |
925 | - "launchpad.net/tomb" |
926 | |
927 | coreCloudinit "launchpad.net/juju-core/cloudinit" |
928 | "launchpad.net/juju-core/cloudinit/sshinit" |
929 | @@ -34,19 +31,13 @@ |
930 | // Bootstrap is a common implementation of the Bootstrap method defined on |
931 | // environs.Environ; we strongly recommend that this implementation be used |
932 | // when writing a new provider. |
933 | -func Bootstrap(env environs.Environ, cons constraints.Value) (err error) { |
934 | +func Bootstrap(ctx environs.BootstrapContext, env environs.Environ, cons constraints.Value) (err error) { |
935 | // TODO make safe in the case of racing Bootstraps |
936 | // If two Bootstraps are called concurrently, there's |
937 | // no way to make sure that only one succeeds. |
938 | |
939 | - // TODO(axw) 2013-11-22 #1237736 |
940 | - // Modify environs/Environ Bootstrap method signature |
941 | - // to take a new context structure, which contains |
942 | - // Std{in,out,err}, and interrupt signal handling. |
943 | - ctx := BootstrapContext{Stderr: os.Stderr} |
944 | - |
945 | var inst instance.Instance |
946 | - defer func() { handleBootstrapError(err, &ctx, inst, env) }() |
947 | + defer func() { handleBootstrapError(err, ctx, inst, env) }() |
948 | |
949 | // Create an empty bootstrap state file so we can get its URL. |
950 | // It will be updated with the instance id and hardware characteristics |
951 | @@ -62,12 +53,12 @@ |
952 | return err |
953 | } |
954 | |
955 | - fmt.Fprintln(ctx.Stderr, "Launching instance") |
956 | + fmt.Fprintln(ctx.Stderr(), "Launching instance") |
957 | inst, hw, err := env.StartInstance(cons, selectedTools, machineConfig) |
958 | if err != nil { |
959 | return fmt.Errorf("cannot start bootstrap instance: %v", err) |
960 | } |
961 | - fmt.Fprintf(ctx.Stderr, " - %s\n", inst.Id()) |
962 | + fmt.Fprintf(ctx.Stderr(), " - %s\n", inst.Id()) |
963 | |
964 | var characteristics []instance.HardwareCharacteristics |
965 | if hw != nil { |
966 | @@ -82,20 +73,28 @@ |
967 | if err != nil { |
968 | return fmt.Errorf("cannot save state: %v", err) |
969 | } |
970 | - return FinishBootstrap(&ctx, inst, machineConfig) |
971 | + return FinishBootstrap(ctx, inst, machineConfig) |
972 | } |
973 | |
974 | // handelBootstrapError cleans up after a failed bootstrap. |
975 | -func handleBootstrapError(err error, ctx *BootstrapContext, inst instance.Instance, env environs.Environ) { |
976 | +func handleBootstrapError(err error, ctx environs.BootstrapContext, inst instance.Instance, env environs.Environ) { |
977 | if err == nil { |
978 | return |
979 | } |
980 | + |
981 | logger.Errorf("bootstrap failed: %v", err) |
982 | - ctx.SetInterruptHandler(func() { |
983 | - fmt.Fprintln(ctx.Stderr, "Cleaning up failed bootstrap") |
984 | - }) |
985 | + ch := make(chan os.Signal, 1) |
986 | + ctx.InterruptNotify(ch) |
987 | + defer ctx.StopInterruptNotify(ch) |
988 | + defer close(ch) |
989 | + go func() { |
990 | + for _ = range ch { |
991 | + fmt.Fprintln(ctx.Stderr(), "Cleaning up failed bootstrap") |
992 | + } |
993 | + }() |
994 | + |
995 | if inst != nil { |
996 | - fmt.Fprintln(ctx.Stderr, "Stopping instance...") |
997 | + fmt.Fprintln(ctx.Stderr(), "Stopping instance...") |
998 | if stoperr := env.StopInstances([]instance.Instance{inst}); stoperr != nil { |
999 | logger.Errorf("cannot stop failed bootstrap instance %q: %v", inst.Id(), stoperr) |
1000 | } else { |
1001 | @@ -110,16 +109,16 @@ |
1002 | logger.Errorf("cannot delete bootstrap state file: %v", rmerr) |
1003 | } |
1004 | } |
1005 | - ctx.SetInterruptHandler(nil) |
1006 | } |
1007 | |
1008 | // FinishBootstrap completes the bootstrap process by connecting |
1009 | // to the instance via SSH and carrying out the cloud-config. |
1010 | // |
1011 | // Note: FinishBootstrap is exposed so it can be replaced for testing. |
1012 | -var FinishBootstrap = func(ctx *BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error { |
1013 | - var t tomb.Tomb |
1014 | - ctx.SetInterruptHandler(func() { t.Killf("interrupted") }) |
1015 | +var FinishBootstrap = func(ctx environs.BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error { |
1016 | + interrupted := make(chan os.Signal, 1) |
1017 | + ctx.InterruptNotify(interrupted) |
1018 | + defer ctx.StopInterruptNotify(interrupted) |
1019 | // Each attempt to connect to an address must verify the machine is the |
1020 | // bootstrap machine by checking its nonce file exists and contains the |
1021 | // nonce in the MachineConfig. This also blocks sshinit from proceeding |
1022 | @@ -141,21 +140,27 @@ |
1023 | // TODO: jam 2013-12-04 bug #1257649 |
1024 | // It would be nice if users had some controll over their bootstrap |
1025 | // timeout, since it is unlikely to be a perfect match for all clouds. |
1026 | - addr, err := waitSSH(ctx, checkNonceCommand, inst, &t, DefaultBootstrapSSHTimeout()) |
1027 | + addr, err := waitSSH(ctx, interrupted, checkNonceCommand, inst, DefaultBootstrapSSHTimeout()) |
1028 | if err != nil { |
1029 | return err |
1030 | } |
1031 | // Bootstrap is synchronous, and will spawn a subprocess |
1032 | // to complete the procedure. If the user hits Ctrl-C, |
1033 | // SIGINT is sent to the foreground process attached to |
1034 | - // the terminal, which will be the ssh subprocess at that |
1035 | - // point. |
1036 | - ctx.SetInterruptHandler(func() {}) |
1037 | + // the terminal, which will be the ssh subprocess at this |
1038 | + // point. For that reason, we do not call StopInterruptNotify |
1039 | + // until this function completes. |
1040 | cloudcfg := coreCloudinit.New() |
1041 | if err := cloudinit.ConfigureJuju(machineConfig, cloudcfg); err != nil { |
1042 | return err |
1043 | } |
1044 | - return sshinit.Configure("ubuntu@"+addr, cloudcfg) |
1045 | + return sshinit.Configure(sshinit.ConfigureParams{ |
1046 | + Host: "ubuntu@" + addr, |
1047 | + Config: cloudcfg, |
1048 | + Stdin: ctx.Stdin(), |
1049 | + Stdout: ctx.Stdout(), |
1050 | + Stderr: ctx.Stderr(), |
1051 | + }) |
1052 | } |
1053 | |
1054 | // SSHTimeoutOpts lists the amount of time we will wait for various parts of |
1055 | @@ -316,8 +321,7 @@ |
1056 | // the presence of a file on the machine that contains the |
1057 | // machine's nonce. The "checkHostScript" is a bash script |
1058 | // that performs this file check. |
1059 | -func waitSSH(ctx *BootstrapContext, checkHostScript string, inst addresser, t *tomb.Tomb, timeout SSHTimeoutOpts) (addr string, err error) { |
1060 | - defer t.Done() |
1061 | +func waitSSH(ctx environs.BootstrapContext, interrupted <-chan os.Signal, checkHostScript string, inst addresser, timeout SSHTimeoutOpts) (addr string, err error) { |
1062 | globalTimeout := time.After(timeout.Timeout) |
1063 | pollAddresses := time.NewTimer(0) |
1064 | |
1065 | @@ -326,24 +330,24 @@ |
1066 | // or the tomb is killed. |
1067 | checker := parallelHostChecker{ |
1068 | Try: parallel.NewTry(0, nil), |
1069 | - stderr: ctx.Stderr, |
1070 | + stderr: ctx.Stderr(), |
1071 | active: make(map[instance.Address]chan struct{}), |
1072 | checkDelay: timeout.ConnectDelay, |
1073 | checkHostScript: checkHostScript, |
1074 | } |
1075 | defer checker.Kill() |
1076 | |
1077 | - fmt.Fprintln(ctx.Stderr, "Waiting for address") |
1078 | + fmt.Fprintln(ctx.Stderr(), "Waiting for address") |
1079 | for { |
1080 | select { |
1081 | case <-pollAddresses.C: |
1082 | pollAddresses.Reset(timeout.AddressesDelay) |
1083 | if err := inst.Refresh(); err != nil { |
1084 | - return "", t.Killf("refreshing addresses: %v", err) |
1085 | + return "", fmt.Errorf("refreshing addresses: %v", err) |
1086 | } |
1087 | addresses, err := inst.Addresses() |
1088 | if err != nil { |
1089 | - return "", t.Killf("getting addresses: %v", err) |
1090 | + return "", fmt.Errorf("getting addresses: %v", err) |
1091 | } |
1092 | checker.UpdateAddresses(addresses) |
1093 | case <-globalTimeout: |
1094 | @@ -361,8 +365,8 @@ |
1095 | args = append(args, lastErr) |
1096 | } |
1097 | return "", fmt.Errorf(format, args...) |
1098 | - case <-t.Dying(): |
1099 | - return "", t.Err() |
1100 | + case <-interrupted: |
1101 | + return "", fmt.Errorf("interrupted") |
1102 | case <-checker.Dead(): |
1103 | result, err := checker.Result() |
1104 | if err != nil { |
1105 | @@ -373,49 +377,6 @@ |
1106 | } |
1107 | } |
1108 | |
1109 | -// TODO(axw) move this to environs; see |
1110 | -// comment near the top of common.Bootstrap. |
1111 | -type BootstrapContext struct { |
1112 | - once sync.Once |
1113 | - handlerchan chan func() |
1114 | - |
1115 | - Stderr io.Writer |
1116 | -} |
1117 | - |
1118 | -func (ctx *BootstrapContext) SetInterruptHandler(f func()) { |
1119 | - ctx.once.Do(ctx.initHandler) |
1120 | - ctx.handlerchan <- f |
1121 | -} |
1122 | - |
1123 | -func (ctx *BootstrapContext) initHandler() { |
1124 | - ctx.handlerchan = make(chan func()) |
1125 | - go ctx.handleInterrupt() |
1126 | -} |
1127 | - |
1128 | -func (ctx *BootstrapContext) handleInterrupt() { |
1129 | - signalchan := make(chan os.Signal, 1) |
1130 | - var s chan os.Signal |
1131 | - var handler func() |
1132 | - for { |
1133 | - select { |
1134 | - case handler = <-ctx.handlerchan: |
1135 | - if handler == nil { |
1136 | - if s != nil { |
1137 | - signal.Stop(signalchan) |
1138 | - s = nil |
1139 | - } |
1140 | - } else { |
1141 | - if s == nil { |
1142 | - s = signalchan |
1143 | - signal.Notify(signalchan, os.Interrupt) |
1144 | - } |
1145 | - } |
1146 | - case <-s: |
1147 | - handler() |
1148 | - } |
1149 | - } |
1150 | -} |
1151 | - |
1152 | // EnsureBootstrapTools finds tools, syncing with an external tools source as |
1153 | // necessary; it then selects the newest tools to bootstrap with, and sets |
1154 | // agent-version. |
1155 | |
1156 | === modified file 'provider/common/bootstrap_test.go' |
1157 | --- provider/common/bootstrap_test.go 2013-12-18 03:13:19 +0000 |
1158 | +++ provider/common/bootstrap_test.go 2013-12-20 02:40:15 +0000 |
1159 | @@ -6,11 +6,11 @@ |
1160 | import ( |
1161 | "bytes" |
1162 | "fmt" |
1163 | + "os" |
1164 | "time" |
1165 | |
1166 | gc "launchpad.net/gocheck" |
1167 | "launchpad.net/loggo" |
1168 | - "launchpad.net/tomb" |
1169 | |
1170 | "launchpad.net/juju-core/constraints" |
1171 | "launchpad.net/juju-core/environs" |
1172 | @@ -75,13 +75,23 @@ |
1173 | return func() *config.Config { return cfg } |
1174 | } |
1175 | |
1176 | +// bootstrapContext creates a BootstrapContext which |
1177 | +// writes stderr to the bytes.Buffer returned. |
1178 | +func bootstrapContext(c *gc.C) (ctx environs.BootstrapContext, stderr *bytes.Buffer) { |
1179 | + cmdContext := coretesting.Context(c) |
1180 | + stderr = &bytes.Buffer{} |
1181 | + cmdContext.Stderr = stderr |
1182 | + return envtesting.NewBootstrapContext(cmdContext), stderr |
1183 | +} |
1184 | + |
1185 | func (s *BootstrapSuite) TestCannotWriteStateFile(c *gc.C) { |
1186 | brokenStorage := &mockStorage{ |
1187 | Storage: newStorage(s, c), |
1188 | putErr: fmt.Errorf("noes!"), |
1189 | } |
1190 | env := &mockEnviron{storage: brokenStorage} |
1191 | - err := common.Bootstrap(env, constraints.Value{}) |
1192 | + ctx, _ := bootstrapContext(c) |
1193 | + err := common.Bootstrap(ctx, env, constraints.Value{}) |
1194 | c.Assert(err, gc.ErrorMatches, "cannot create initial state file: noes!") |
1195 | } |
1196 | |
1197 | @@ -107,7 +117,8 @@ |
1198 | config: configGetter(c), |
1199 | } |
1200 | |
1201 | - err = common.Bootstrap(env, checkCons) |
1202 | + ctx, _ := bootstrapContext(c) |
1203 | + err = common.Bootstrap(ctx, env, checkCons) |
1204 | c.Assert(err, gc.ErrorMatches, "cannot start bootstrap instance: meh, not started") |
1205 | } |
1206 | |
1207 | @@ -137,7 +148,8 @@ |
1208 | config: configGetter(c), |
1209 | } |
1210 | |
1211 | - err := common.Bootstrap(env, constraints.Value{}) |
1212 | + ctx, _ := bootstrapContext(c) |
1213 | + err := common.Bootstrap(ctx, env, constraints.Value{}) |
1214 | c.Assert(err, gc.ErrorMatches, "cannot save state: suddenly a wild blah") |
1215 | c.Assert(stopped, gc.HasLen, 1) |
1216 | c.Assert(stopped[0].Id(), gc.Equals, instance.Id("i-blah")) |
1217 | @@ -173,7 +185,8 @@ |
1218 | config: configGetter(c), |
1219 | } |
1220 | |
1221 | - err := common.Bootstrap(env, constraints.Value{}) |
1222 | + ctx, _ := bootstrapContext(c) |
1223 | + err := common.Bootstrap(ctx, env, constraints.Value{}) |
1224 | c.Assert(err, gc.ErrorMatches, "cannot save state: suddenly a wild blah") |
1225 | c.Assert(stopped, gc.HasLen, 1) |
1226 | c.Assert(stopped[0].Id(), gc.Equals, instance.Id("i-blah")) |
1227 | @@ -211,7 +224,8 @@ |
1228 | startInstance: startInstance, |
1229 | config: getConfig, |
1230 | } |
1231 | - err := common.Bootstrap(env, constraints.Value{}) |
1232 | + ctx, _ := bootstrapContext(c) |
1233 | + err := common.Bootstrap(ctx, env, constraints.Value{}) |
1234 | c.Assert(err, gc.IsNil) |
1235 | |
1236 | savedState, err := bootstrap.LoadStateFromURL(checkURL) |
1237 | @@ -244,27 +258,22 @@ |
1238 | } |
1239 | |
1240 | func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForAddresses(c *gc.C) { |
1241 | - ctx := &common.BootstrapContext{} |
1242 | - buf := &bytes.Buffer{} |
1243 | - ctx.Stderr = buf |
1244 | - var t tomb.Tomb |
1245 | - _, err := common.WaitSSH(ctx, "/bin/true", neverAddresses{}, &t, testSSHTimeout) |
1246 | + ctx, stderr := bootstrapContext(c) |
1247 | + _, err := common.WaitSSH(ctx, nil, "/bin/true", neverAddresses{}, testSSHTimeout) |
1248 | c.Check(err, gc.ErrorMatches, "waited for 10ms without getting any addresses") |
1249 | - c.Check(buf.String(), gc.Matches, "Waiting for address\n") |
1250 | + c.Check(stderr.String(), gc.Matches, "Waiting for address\n") |
1251 | } |
1252 | |
1253 | func (s *BootstrapSuite) TestWaitSSHKilledWaitingForAddresses(c *gc.C) { |
1254 | - ctx := &common.BootstrapContext{} |
1255 | - buf := &bytes.Buffer{} |
1256 | - ctx.Stderr = buf |
1257 | - var t tomb.Tomb |
1258 | + ctx, stderr := bootstrapContext(c) |
1259 | + interrupted := make(chan os.Signal, 1) |
1260 | go func() { |
1261 | <-time.After(2 * time.Millisecond) |
1262 | - t.Killf("stopping WaitSSH during Addresses") |
1263 | + interrupted <- os.Interrupt |
1264 | }() |
1265 | - _, err := common.WaitSSH(ctx, "/bin/true", neverAddresses{}, &t, testSSHTimeout) |
1266 | - c.Check(err, gc.ErrorMatches, "stopping WaitSSH during Addresses") |
1267 | - c.Check(buf.String(), gc.Matches, "Waiting for address\n") |
1268 | + _, err := common.WaitSSH(ctx, interrupted, "/bin/true", neverAddresses{}, testSSHTimeout) |
1269 | + c.Check(err, gc.ErrorMatches, "interrupted") |
1270 | + c.Check(stderr.String(), gc.Matches, "Waiting for address\n") |
1271 | } |
1272 | |
1273 | type brokenAddresses struct { |
1274 | @@ -276,13 +285,10 @@ |
1275 | } |
1276 | |
1277 | func (s *BootstrapSuite) TestWaitSSHStopsOnBadError(c *gc.C) { |
1278 | - ctx := &common.BootstrapContext{} |
1279 | - buf := &bytes.Buffer{} |
1280 | - ctx.Stderr = buf |
1281 | - var t tomb.Tomb |
1282 | - _, err := common.WaitSSH(ctx, "/bin/true", brokenAddresses{}, &t, testSSHTimeout) |
1283 | + ctx, stderr := bootstrapContext(c) |
1284 | + _, err := common.WaitSSH(ctx, nil, "/bin/true", brokenAddresses{}, testSSHTimeout) |
1285 | c.Check(err, gc.ErrorMatches, "getting addresses: Addresses will never work") |
1286 | - c.Check(buf.String(), gc.Equals, "Waiting for address\n") |
1287 | + c.Check(stderr.String(), gc.Equals, "Waiting for address\n") |
1288 | } |
1289 | |
1290 | type neverOpensPort struct { |
1291 | @@ -295,47 +301,42 @@ |
1292 | } |
1293 | |
1294 | func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDial(c *gc.C) { |
1295 | - ctx := &common.BootstrapContext{} |
1296 | - buf := &bytes.Buffer{} |
1297 | - ctx.Stderr = buf |
1298 | - var t tomb.Tomb |
1299 | + ctx, stderr := bootstrapContext(c) |
1300 | // 0.x.y.z addresses are always invalid |
1301 | - _, err := common.WaitSSH(ctx, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, &t, testSSHTimeout) |
1302 | + _, err := common.WaitSSH(ctx, nil, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout) |
1303 | c.Check(err, gc.ErrorMatches, |
1304 | `waited for 10ms without being able to connect: mock connection failure to 0.1.2.3`) |
1305 | - c.Check(buf.String(), gc.Matches, |
1306 | + c.Check(stderr.String(), gc.Matches, |
1307 | "Waiting for address\n"+ |
1308 | "(Attempting to connect to 0.1.2.3:22\n)+") |
1309 | } |
1310 | |
1311 | -type killOnDial struct { |
1312 | +type interruptOnDial struct { |
1313 | neverRefreshes |
1314 | - name string |
1315 | - tomb *tomb.Tomb |
1316 | - returned bool |
1317 | + name string |
1318 | + interrupted chan os.Signal |
1319 | + returned bool |
1320 | } |
1321 | |
1322 | -func (k *killOnDial) Addresses() ([]instance.Address, error) { |
1323 | +func (i *interruptOnDial) Addresses() ([]instance.Address, error) { |
1324 | // kill the tomb the second time Addresses is called |
1325 | - if !k.returned { |
1326 | - k.returned = true |
1327 | + if !i.returned { |
1328 | + i.returned = true |
1329 | } else { |
1330 | - k.tomb.Killf("stopping WaitSSH during Dial") |
1331 | + i.interrupted <- os.Interrupt |
1332 | } |
1333 | - return []instance.Address{instance.NewAddress(k.name)}, nil |
1334 | + return []instance.Address{instance.NewAddress(i.name)}, nil |
1335 | } |
1336 | |
1337 | func (s *BootstrapSuite) TestWaitSSHKilledWaitingForDial(c *gc.C) { |
1338 | - ctx := &common.BootstrapContext{} |
1339 | - buf := &bytes.Buffer{} |
1340 | - ctx.Stderr = buf |
1341 | - var t tomb.Tomb |
1342 | + ctx, stderr := bootstrapContext(c) |
1343 | timeout := testSSHTimeout |
1344 | timeout.Timeout = 1 * time.Minute |
1345 | - _, err := common.WaitSSH(ctx, "", &killOnDial{name: "0.1.2.3", tomb: &t}, &t, timeout) |
1346 | - c.Check(err, gc.ErrorMatches, "stopping WaitSSH during Dial") |
1347 | + interrupted := make(chan os.Signal, 1) |
1348 | + _, err := common.WaitSSH(ctx, interrupted, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout) |
1349 | + c.Check(err, gc.ErrorMatches, "interrupted") |
1350 | // Exact timing is imprecise but it should have tried a few times before being killed |
1351 | - c.Check(buf.String(), gc.Matches, |
1352 | + c.Check(stderr.String(), gc.Matches, |
1353 | "Waiting for address\n"+ |
1354 | "(Attempting to connect to 0.1.2.3:22\n)+") |
1355 | } |
1356 | @@ -360,25 +361,22 @@ |
1357 | } |
1358 | |
1359 | func (s *BootstrapSuite) TestWaitSSHRefreshAddresses(c *gc.C) { |
1360 | - ctx := &common.BootstrapContext{} |
1361 | - buf := &bytes.Buffer{} |
1362 | - ctx.Stderr = buf |
1363 | - var t tomb.Tomb |
1364 | - _, err := common.WaitSSH(ctx, "", &addressesChange{addrs: [][]string{ |
1365 | + ctx, stderr := bootstrapContext(c) |
1366 | + _, err := common.WaitSSH(ctx, nil, "", &addressesChange{addrs: [][]string{ |
1367 | nil, |
1368 | nil, |
1369 | []string{"0.1.2.3"}, |
1370 | []string{"0.1.2.3"}, |
1371 | nil, |
1372 | []string{"0.1.2.4"}, |
1373 | - }}, &t, testSSHTimeout) |
1374 | + }}, testSSHTimeout) |
1375 | // Not necessarily the last one in the list, due to scheduling. |
1376 | c.Check(err, gc.ErrorMatches, |
1377 | `waited for 10ms without being able to connect: mock connection failure to 0.1.2.[34]`) |
1378 | - c.Check(buf.String(), gc.Matches, |
1379 | + c.Check(stderr.String(), gc.Matches, |
1380 | "Waiting for address\n"+ |
1381 | "(.|\n)*(Attempting to connect to 0.1.2.3:22\n)+(.|\n)*") |
1382 | - c.Check(buf.String(), gc.Matches, |
1383 | + c.Check(stderr.String(), gc.Matches, |
1384 | "Waiting for address\n"+ |
1385 | "(.|\n)*(Attempting to connect to 0.1.2.4:22\n)+(.|\n)*") |
1386 | } |
1387 | |
1388 | === modified file 'provider/dummy/environs.go' |
1389 | --- provider/dummy/environs.go 2013-12-19 09:50:26 +0000 |
1390 | +++ provider/dummy/environs.go 2013-12-20 02:40:15 +0000 |
1391 | @@ -102,6 +102,7 @@ |
1392 | } |
1393 | |
1394 | type OpBootstrap struct { |
1395 | + Context environs.BootstrapContext |
1396 | Env string |
1397 | Constraints constraints.Value |
1398 | } |
1399 | @@ -522,7 +523,7 @@ |
1400 | storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil |
1401 | } |
1402 | |
1403 | -func (e *environ) Bootstrap(cons constraints.Value) error { |
1404 | +func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1405 | selectedTools, err := common.EnsureBootstrapTools(e, e.Config().DefaultSeries(), cons.Arch) |
1406 | if err != nil { |
1407 | return err |
1408 | @@ -592,7 +593,7 @@ |
1409 | estate.apiState = st |
1410 | } |
1411 | estate.bootstrapped = true |
1412 | - estate.ops <- OpBootstrap{Env: e.name, Constraints: cons} |
1413 | + estate.ops <- OpBootstrap{Context: ctx, Env: e.name, Constraints: cons} |
1414 | return nil |
1415 | } |
1416 | |
1417 | |
1418 | === modified file 'provider/ec2/ec2.go' |
1419 | --- provider/ec2/ec2.go 2013-12-18 14:16:37 +0000 |
1420 | +++ provider/ec2/ec2.go 2013-12-20 02:40:15 +0000 |
1421 | @@ -325,8 +325,8 @@ |
1422 | return stor |
1423 | } |
1424 | |
1425 | -func (e *environ) Bootstrap(cons constraints.Value) error { |
1426 | - return common.Bootstrap(e, cons) |
1427 | +func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1428 | + return common.Bootstrap(ctx, e, cons) |
1429 | } |
1430 | |
1431 | func (e *environ) StateInfo() (*state.Info, *api.Info, error) { |
1432 | |
1433 | === modified file 'provider/ec2/local_test.go' |
1434 | --- provider/ec2/local_test.go 2013-12-16 07:20:01 +0000 |
1435 | +++ provider/ec2/local_test.go 2013-12-20 02:40:15 +0000 |
1436 | @@ -191,6 +191,10 @@ |
1437 | t.srv.stopServer(c) |
1438 | } |
1439 | |
1440 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
1441 | + return envtesting.NewBootstrapContext(coretesting.Context(c)) |
1442 | +} |
1443 | + |
1444 | func (t *localServerSuite) TestPrecheck(c *gc.C) { |
1445 | env := t.Prepare(c) |
1446 | prechecker, ok := env.(environs.Prechecker) |
1447 | @@ -205,7 +209,7 @@ |
1448 | func (t *localServerSuite) TestBootstrapInstanceUserDataAndState(c *gc.C) { |
1449 | env := t.Prepare(c) |
1450 | envtesting.UploadFakeTools(c, env.Storage()) |
1451 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1452 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1453 | c.Assert(err, gc.IsNil) |
1454 | |
1455 | // check that the state holds the id of the bootstrap machine. |
1456 | @@ -277,7 +281,7 @@ |
1457 | func (t *localServerSuite) TestInstanceStatus(c *gc.C) { |
1458 | env := t.Prepare(c) |
1459 | envtesting.UploadFakeTools(c, env.Storage()) |
1460 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1461 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1462 | c.Assert(err, gc.IsNil) |
1463 | t.srv.ec2srv.SetInitialInstanceState(ec2test.Terminated) |
1464 | inst, _ := testing.AssertStartInstance(c, env, "1") |
1465 | @@ -288,7 +292,7 @@ |
1466 | func (t *localServerSuite) TestStartInstanceHardwareCharacteristics(c *gc.C) { |
1467 | env := t.Prepare(c) |
1468 | envtesting.UploadFakeTools(c, env.Storage()) |
1469 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1470 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1471 | c.Assert(err, gc.IsNil) |
1472 | _, hc := testing.AssertStartInstance(c, env, "1") |
1473 | c.Check(*hc.Arch, gc.Equals, "amd64") |
1474 | @@ -300,7 +304,7 @@ |
1475 | func (t *localServerSuite) TestAddresses(c *gc.C) { |
1476 | env := t.Prepare(c) |
1477 | envtesting.UploadFakeTools(c, env.Storage()) |
1478 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1479 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1480 | c.Assert(err, gc.IsNil) |
1481 | inst, _ := testing.AssertStartInstance(c, env, "1") |
1482 | c.Assert(err, gc.IsNil) |
1483 | |
1484 | === modified file 'provider/joyent/environ.go' |
1485 | --- provider/joyent/environ.go 2013-11-20 18:15:47 +0000 |
1486 | +++ provider/joyent/environ.go 2013-12-20 02:40:15 +0000 |
1487 | @@ -81,8 +81,8 @@ |
1488 | return environs.EmptyStorage |
1489 | } |
1490 | |
1491 | -func (env *environ) Bootstrap(cons constraints.Value) error { |
1492 | - return common.Bootstrap(env, cons) |
1493 | +func (env *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1494 | + return common.Bootstrap(ctx, env, cons) |
1495 | } |
1496 | |
1497 | func (env *environ) StateInfo() (*state.Info, *api.Info, error) { |
1498 | |
1499 | === modified file 'provider/local/environ.go' |
1500 | --- provider/local/environ.go 2013-12-05 22:32:47 +0000 |
1501 | +++ provider/local/environ.go 2013-12-20 02:40:15 +0000 |
1502 | @@ -104,7 +104,7 @@ |
1503 | } |
1504 | |
1505 | // Bootstrap is specified in the Environ interface. |
1506 | -func (env *localEnviron) Bootstrap(cons constraints.Value) error { |
1507 | +func (env *localEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1508 | if !env.config.runningAsRoot { |
1509 | return fmt.Errorf("bootstrapping a local environment must be done as root") |
1510 | } |
1511 | |
1512 | === modified file 'provider/maas/environ.go' |
1513 | --- provider/maas/environ.go 2013-12-16 09:53:26 +0000 |
1514 | +++ provider/maas/environ.go 2013-12-20 02:40:15 +0000 |
1515 | @@ -77,8 +77,8 @@ |
1516 | } |
1517 | |
1518 | // Bootstrap is specified in the Environ interface. |
1519 | -func (env *maasEnviron) Bootstrap(cons constraints.Value) error { |
1520 | - return common.Bootstrap(env, cons) |
1521 | +func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1522 | + return common.Bootstrap(ctx, env, cons) |
1523 | } |
1524 | |
1525 | // StateInfo is specified in the Environ interface. |
1526 | |
1527 | === modified file 'provider/maas/environ_whitebox_test.go' |
1528 | --- provider/maas/environ_whitebox_test.go 2013-12-16 09:53:26 +0000 |
1529 | +++ provider/maas/environ_whitebox_test.go 2013-12-20 02:40:15 +0000 |
1530 | @@ -25,6 +25,7 @@ |
1531 | "launchpad.net/juju-core/errors" |
1532 | "launchpad.net/juju-core/instance" |
1533 | "launchpad.net/juju-core/juju/testing" |
1534 | + coretesting "launchpad.net/juju-core/testing" |
1535 | jc "launchpad.net/juju-core/testing/checkers" |
1536 | "launchpad.net/juju-core/tools" |
1537 | "launchpad.net/juju-core/utils" |
1538 | @@ -148,12 +149,16 @@ |
1539 | return utils.Gunzip(data) |
1540 | } |
1541 | |
1542 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
1543 | + return envtesting.NewBootstrapContext(coretesting.Context(c)) |
1544 | +} |
1545 | + |
1546 | func (suite *environSuite) TestStartInstanceStartsInstance(c *gc.C) { |
1547 | suite.setupFakeTools(c) |
1548 | env := suite.makeEnviron() |
1549 | // Create node 0: it will be used as the bootstrap node. |
1550 | suite.testMAASObject.TestServer.NewNode(`{"system_id": "node0", "hostname": "host0"}`) |
1551 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1552 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1553 | c.Assert(err, gc.IsNil) |
1554 | // The bootstrap node has been acquired and started. |
1555 | operations := suite.testMAASObject.TestServer.NodeOperations() |
1556 | @@ -367,7 +372,7 @@ |
1557 | suite.setupFakeTools(c) |
1558 | env := suite.makeEnviron() |
1559 | suite.testMAASObject.TestServer.NewNode(`{"system_id": "thenode", "hostname": "host"}`) |
1560 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1561 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1562 | c.Assert(err, gc.IsNil) |
1563 | } |
1564 | |
1565 | @@ -383,14 +388,14 @@ |
1566 | c.Assert(err, gc.IsNil) |
1567 | err = env.SetConfig(cfg) |
1568 | c.Assert(err, gc.IsNil) |
1569 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
1570 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1571 | c.Check(err, gc.ErrorMatches, "cannot find bootstrap tools.*") |
1572 | } |
1573 | |
1574 | func (suite *environSuite) TestBootstrapFailsIfNoNodes(c *gc.C) { |
1575 | suite.setupFakeTools(c) |
1576 | env := suite.makeEnviron() |
1577 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1578 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1579 | // Since there are no nodes, the attempt to allocate one returns a |
1580 | // 409: Conflict. |
1581 | c.Check(err, gc.ErrorMatches, ".*409.*") |
1582 | @@ -402,7 +407,7 @@ |
1583 | suite.testMAASObject.TestServer.NewNode(`{"system_id": "bootstrapnode", "hostname": "host"}`) |
1584 | |
1585 | // bootstrap.Bootstrap calls Environ.Bootstrap. This works. |
1586 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1587 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1588 | c.Assert(err, gc.IsNil) |
1589 | } |
1590 | |
1591 | |
1592 | === modified file 'provider/null/environ.go' |
1593 | --- provider/null/environ.go 2013-11-19 06:43:27 +0000 |
1594 | +++ provider/null/environ.go 2013-12-20 02:40:15 +0000 |
1595 | @@ -85,7 +85,7 @@ |
1596 | return e.envConfig().Name() |
1597 | } |
1598 | |
1599 | -func (e *nullEnviron) Bootstrap(cons constraints.Value) error { |
1600 | +func (e *nullEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1601 | envConfig := e.envConfig() |
1602 | hc, series, err := manual.DetectSeriesAndHardwareCharacteristics(envConfig.sshHost()) |
1603 | if err != nil { |
1604 | @@ -96,6 +96,7 @@ |
1605 | return err |
1606 | } |
1607 | return manual.Bootstrap(manual.BootstrapArgs{ |
1608 | + Context: ctx, |
1609 | Host: e.envConfig().sshHost(), |
1610 | DataDir: dataDir, |
1611 | Environ: e, |
1612 | @@ -144,7 +145,7 @@ |
1613 | } |
1614 | |
1615 | // Implements environs.BootstrapStorager. |
1616 | -func (e *nullEnviron) EnableBootstrapStorage() error { |
1617 | +func (e *nullEnviron) EnableBootstrapStorage(ctx environs.BootstrapContext) error { |
1618 | e.bootstrapStorageMutex.Lock() |
1619 | defer e.bootstrapStorageMutex.Unlock() |
1620 | if e.bootstrapStorage != nil { |
1621 | @@ -153,7 +154,14 @@ |
1622 | cfg := e.envConfig() |
1623 | storageDir := e.StorageDir() |
1624 | storageTmpdir := path.Join(dataDir, storageTmpSubdir) |
1625 | - bootstrapStorage, err := sshstorage.NewSSHStorage(cfg.sshHost(), storageDir, storageTmpdir) |
1626 | + params := sshstorage.NewSSHStorageParams{ |
1627 | + Host: cfg.sshHost(), |
1628 | + StorageDir: storageDir, |
1629 | + TmpDir: storageTmpdir, |
1630 | + Stdin: ctx.Stdin(), |
1631 | + Stdout: ctx.Stdout(), |
1632 | + } |
1633 | + bootstrapStorage, err := sshstorage.NewSSHStorage(params) |
1634 | if err != nil { |
1635 | return err |
1636 | } |
1637 | |
1638 | === modified file 'provider/null/environ_test.go' |
1639 | --- provider/null/environ_test.go 2013-12-12 06:57:36 +0000 |
1640 | +++ provider/null/environ_test.go 2013-12-20 02:40:15 +0000 |
1641 | @@ -14,8 +14,10 @@ |
1642 | "launchpad.net/juju-core/environs" |
1643 | "launchpad.net/juju-core/environs/manual" |
1644 | "launchpad.net/juju-core/environs/sshstorage" |
1645 | + envtesting "launchpad.net/juju-core/environs/testing" |
1646 | "launchpad.net/juju-core/environs/tools" |
1647 | "launchpad.net/juju-core/instance" |
1648 | + coretesting "launchpad.net/juju-core/testing" |
1649 | jc "launchpad.net/juju-core/testing/checkers" |
1650 | "launchpad.net/juju-core/testing/testbase" |
1651 | ) |
1652 | @@ -116,17 +118,18 @@ |
1653 | s.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH")) |
1654 | |
1655 | s.PatchEnvironment("RC", "99") // simulate ssh failure |
1656 | - err = s.env.EnableBootstrapStorage() |
1657 | + ctx := envtesting.NewBootstrapContext(coretesting.Context(c)) |
1658 | + err = s.env.EnableBootstrapStorage(ctx) |
1659 | c.Assert(err, gc.ErrorMatches, "exit code 99") |
1660 | c.Assert(s.env.Storage(), gc.Not(gc.FitsTypeOf), new(sshstorage.SSHStorage)) |
1661 | |
1662 | s.PatchEnvironment("RC", "0") |
1663 | - err = s.env.EnableBootstrapStorage() |
1664 | + err = s.env.EnableBootstrapStorage(ctx) |
1665 | c.Assert(err, gc.IsNil) |
1666 | c.Assert(s.env.Storage(), gc.FitsTypeOf, new(sshstorage.SSHStorage)) |
1667 | |
1668 | // Check idempotency |
1669 | - err = s.env.EnableBootstrapStorage() |
1670 | + err = s.env.EnableBootstrapStorage(ctx) |
1671 | c.Assert(err, gc.IsNil) |
1672 | c.Assert(s.env.Storage(), gc.FitsTypeOf, new(sshstorage.SSHStorage)) |
1673 | } |
1674 | |
1675 | === modified file 'provider/openstack/local_test.go' |
1676 | --- provider/openstack/local_test.go 2013-12-10 04:25:06 +0000 |
1677 | +++ provider/openstack/local_test.go 2013-12-20 02:40:15 +0000 |
1678 | @@ -252,6 +252,10 @@ |
1679 | s.LoggingSuite.TearDownTest(c) |
1680 | } |
1681 | |
1682 | +func bootstrapContext(c *gc.C) environs.BootstrapContext { |
1683 | + return envtesting.NewBootstrapContext(coretesting.Context(c)) |
1684 | +} |
1685 | + |
1686 | func (s *localServerSuite) TestPrecheck(c *gc.C) { |
1687 | var cons constraints.Value |
1688 | env := s.Prepare(c) |
1689 | @@ -281,7 +285,7 @@ |
1690 | c.Assert(err, gc.IsNil) |
1691 | env, err := environs.New(cfg) |
1692 | c.Assert(err, gc.IsNil) |
1693 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
1694 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1695 | c.Assert(err, gc.ErrorMatches, "(.|\n)*cannot allocate a public IP as needed(.|\n)*") |
1696 | } |
1697 | |
1698 | @@ -310,7 +314,7 @@ |
1699 | c.Assert(err, gc.IsNil) |
1700 | env, err := environs.Prepare(cfg, s.ConfigStore) |
1701 | c.Assert(err, gc.IsNil) |
1702 | - err = bootstrap.Bootstrap(env, constraints.Value{}) |
1703 | + err = bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1704 | c.Assert(err, gc.IsNil) |
1705 | inst, _ := testing.AssertStartInstance(c, env, "100") |
1706 | err = env.StopInstances([]instance.Instance{inst}) |
1707 | @@ -319,7 +323,7 @@ |
1708 | |
1709 | func (s *localServerSuite) TestStartInstanceHardwareCharacteristics(c *gc.C) { |
1710 | env := s.Prepare(c) |
1711 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1712 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1713 | c.Assert(err, gc.IsNil) |
1714 | _, hc := testing.AssertStartInstanceWithConstraints(c, env, "100", constraints.MustParse("mem=1024")) |
1715 | c.Check(*hc.Arch, gc.Equals, "amd64") |
1716 | @@ -476,7 +480,7 @@ |
1717 | // It should be moved to environs.jujutests.Tests. |
1718 | func (s *localServerSuite) TestBootstrapInstanceUserDataAndState(c *gc.C) { |
1719 | env := s.Prepare(c) |
1720 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1721 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1722 | c.Assert(err, gc.IsNil) |
1723 | |
1724 | // check that the state holds the id of the bootstrap machine. |
1725 | @@ -757,7 +761,7 @@ |
1726 | openstack.UseTestImageData(metadataStorage, s.cred) |
1727 | defer openstack.RemoveTestImageData(metadataStorage) |
1728 | |
1729 | - err = bootstrap.Bootstrap(s.env, constraints.Value{}) |
1730 | + err = bootstrap.Bootstrap(bootstrapContext(c), s.env, constraints.Value{}) |
1731 | c.Assert(err, gc.IsNil) |
1732 | } |
1733 | |
1734 | @@ -900,7 +904,7 @@ |
1735 | |
1736 | func (s *localServerSuite) TestAllInstancesIgnoresOtherMachines(c *gc.C) { |
1737 | env := s.Prepare(c) |
1738 | - err := bootstrap.Bootstrap(env, constraints.Value{}) |
1739 | + err := bootstrap.Bootstrap(bootstrapContext(c), env, constraints.Value{}) |
1740 | c.Assert(err, gc.IsNil) |
1741 | |
1742 | // Check that we see 1 instance in the environment |
1743 | |
1744 | === modified file 'provider/openstack/provider.go' |
1745 | --- provider/openstack/provider.go 2013-12-18 10:11:12 +0000 |
1746 | +++ provider/openstack/provider.go 2013-12-20 02:40:15 +0000 |
1747 | @@ -489,7 +489,7 @@ |
1748 | return stor |
1749 | } |
1750 | |
1751 | -func (e *environ) Bootstrap(cons constraints.Value) error { |
1752 | +func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { |
1753 | // The client's authentication may have been reset when finding tools if the agent-version |
1754 | // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. |
1755 | // An authenticated client is needed for the URL() call below. |
1756 | @@ -497,7 +497,7 @@ |
1757 | if err != nil { |
1758 | return err |
1759 | } |
1760 | - return common.Bootstrap(e, cons) |
1761 | + return common.Bootstrap(ctx, e, cons) |
1762 | } |
1763 | |
1764 | func (e *environ) StateInfo() (*state.Info, *api.Info, error) { |
Reviewers: mp+197999_ code.launchpad. net,
Message:
Please take a look.
Description:
Introduce BootstrapContext
Environ.Bootstrap now takes an additional
parameter, a *BootstrapContext. This is a
structure that permits access to the
caller's context. It currently comprises
stdio handles and a signal handler.
Fixes #1237736
https:/ /code.launchpad .net/~axwalk/ juju-core/ lp1237736- bootstrap- context/ +merge/ 197999
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/38240043/
Affected files (+397, -174 lines): sshinit/ configure. go addmachine. go bootstrap. go bootstrap/ bootstrap. go bootstrap/ bootstrap_ test.go bootstrapcontex t.go bootstrapcontex t_test. go interface. go jujutest/ livetests. go jujutest/ tests.go manual/ bootstrap. go manual/ bootstrap_ test.go manual/ provisioner. go open_test. go sshstorage/ storage. go sshstorage/ storage_ test.go testing/ bootstrap. go test.go conn.go azure/environ. go common/ bootstrap. go common/ bootstrap_ test.go dummy/environs. go ec2/local_ test.go joyent/ environ. go local/environ. go maas/environ. go maas/environ_ test.go null/environ. go null/environ_ test.go openstack/ local_test. go openstack/ provider. go
A [revision details]
M cloudinit/
M cmd/juju/
M cmd/juju/
M environs/
M environs/
A environs/
A environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M juju/apiconn_
M juju/conn_test.go
M juju/testing/
M provider/
M provider/
M provider/
M provider/
M provider/ec2/ec2.go
M provider/
M provider/
M provider/
M provider/
M provider/
M provider/
M provider/
M provider/
M provider/