Merge lp:~axwalk/juju-core/lp1237736-bootstrap-context into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
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
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

https://codereview.appspot.com/38240043/

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

https://codereview.appspot.com/38240043/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

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):
   A [revision details]
   M cloudinit/sshinit/configure.go
   M cmd/juju/addmachine.go
   M cmd/juju/bootstrap.go
   M environs/bootstrap/bootstrap.go
   M environs/bootstrap/bootstrap_test.go
   A environs/bootstrapcontext.go
   A environs/bootstrapcontext_test.go
   M environs/interface.go
   M environs/jujutest/livetests.go
   M environs/jujutest/tests.go
   M environs/manual/bootstrap.go
   M environs/manual/bootstrap_test.go
   M environs/manual/provisioner.go
   M environs/open_test.go
   M environs/sshstorage/storage.go
   M environs/sshstorage/storage_test.go
   M environs/testing/bootstrap.go
   M juju/apiconn_test.go
   M juju/conn_test.go
   M juju/testing/conn.go
   M provider/azure/environ.go
   M provider/common/bootstrap.go
   M provider/common/bootstrap_test.go
   M provider/dummy/environs.go
   M provider/ec2/ec2.go
   M provider/ec2/local_test.go
   M provider/joyent/environ.go
   M provider/local/environ.go
   M provider/maas/environ.go
   M provider/maas/environ_test.go
   M provider/null/environ.go
   M provider/null/environ_test.go
   M provider/openstack/local_test.go
   M provider/openstack/provider.go

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

Looks generally clean, but I'd like rog's input on the interrupt
handling.

https://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go
File cloudinit/sshinit/configure.go (right):

https://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go#newcode26
cloudinit/sshinit/configure.go:26: Config *cloudinit.Config
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://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go#newcode28
cloudinit/sshinit/configure.go:28: // Stdin is required to solicit sudo
prompts,
s/solicit/respond to/?

https://codereview.appspot.com/38240043/diff/1/environs/bootstrap/bootstrap.go
File environs/bootstrap/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/1/environs/bootstrap/bootstrap.go#newcode22
environs/bootstrap/bootstrap.go:22: func Bootstrap(ctx
*environs.BootstrapContext, environ environs.Environ, cons
constraints.Value) error {
Random thought. Might it be good to make this func a method on
BootstrapContext?

https://codereview.appspot.com/38240043/diff/1/environs/bootstrapcontext_test.go
File environs/bootstrapcontext_test.go (right):

https://codereview.appspot.com/38240043/diff/1/environs/bootstrapcontext_test.go#newcode71
environs/bootstrapcontext_test.go:71: }
I seem to recall rog wanted to do things differently here. Rog?

https://codereview.appspot.com/38240043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go
File cloudinit/sshinit/configure.go (right):

https://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go#newcode26
cloudinit/sshinit/configure.go:26: Config *cloudinit.Config
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://codereview.appspot.com/38240043/diff/1/cloudinit/sshinit/configure.go#newcode28
cloudinit/sshinit/configure.go:28: // Stdin is required to solicit sudo
prompts,
On 2013/12/11 08:36:46, fwereade wrote:
> s/solicit/respond to/?

Done.

https://codereview.appspot.com/38240043/diff/1/environs/bootstrap/bootstrap.go
File environs/bootstrap/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/1/environs/bootstrap/bootstrap.go#newcode22
environs/bootstrap/bootstrap.go:22: func Bootstrap(ctx
*environs.BootstrapContext, environ environs.Environ, cons
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).

https://codereview.appspot.com/38240043/

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Looks reasonable in general, but some concerns outlined below.

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go
File environs/bootstrapcontext.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go#newcode49
environs/bootstrapcontext.go:49: func (ctx *BootstrapContext)
SetInterruptHandler(f func()) (old func()) {
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 DefaultBootstrapContext() *BootstrapContext {
 return &BootstrapContext{
  Stdin: os.Stdin,
  Stdout: os.Stdout,
  Stderr: os.Stderr,
  InterruptNotify: func(c chan<- os.Signal) {
   signal.Notify(c, os.Interrupt)
  },
  StopInterruptNotify: signal.Stop,
 }
}

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)
     StopInterruptNotify(chan<- os.Signal)
}

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go
File environs/manual/provisioner.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go#newcode50
environs/manual/provisioner.go:50: // and must be a terminal (except in
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://codereview.appspot.com/38240043/diff/20001/environs/sshstorage/storage.go
File environs/sshstorage/storage.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/sshstorage/storage.go#newcode68
environs/sshstorage/storage.go:68: // TmpDir is the remote temporary
directory for storage.
Why do we need this? Can't we use a directory inside StorageDir?

https://codereview.appspot.com/38240043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.8 KiB)

Please take a look.

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go
File environs/bootstrapcontext.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go#newcode49
environs/bootstrapcontext.go:49: func (ctx *BootstrapContext)
SetInterruptHandler(f func()) (old func()) {
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 DefaultBootstrapContext() *BootstrapContext {
> return &BootstrapContext{
> Stdin: os.Stdin,
> Stdout: os.Stdout,
> Stderr: os.Stderr,
> InterruptNotify: func(c chan<- os.Signal) {
> signal.Notify(c, os.Interrupt)
> },
> StopInterruptNotify: signal.Stop,
> }
> }

> 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)
> StopInterruptNotify(chan<- os.Signal)
> }

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 SetInterruptHandler, made it internal to
provider/common/bootstrap.go, and based it on the
InterruptNotify/StopInterruptNotify methods of BootstrapContext.

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go
File environs/manual/provisioner.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go#newcode50
environs/manual/provisioner.go:50: // and must be a terminal (except 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 passwordless sudo enabled)". If passwordless sudo is enabled,
then you can script it.

https://codereview.appspot.com/38240043/diff/20001/environs/sshstorage/storage.go
File environs/sshstorage/storage.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/sshstorage/storage.go#newcode6...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (4.1 KiB)

On 2013/12/19 08:44:33, axw wrote:
> Please take a look.

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go
> File environs/bootstrapcontext.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go#newcode49
> environs/bootstrapcontext.go:49: func (ctx *BootstrapContext)
> SetInterruptHandler(f func()) (old func()) {
> 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 DefaultBootstrapContext() *BootstrapContext {
> > return &BootstrapContext{
> > Stdin: os.Stdin,
> > Stdout: os.Stdout,
> > Stderr: os.Stderr,
> > InterruptNotify: func(c chan<- os.Signal) {
> > signal.Notify(c, os.Interrupt)
> > },
> > StopInterruptNotify: signal.Stop,
> > }
> > }
> >
> > 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)
> > StopInterruptNotify(chan<- os.Signal)
> > }
> >

> 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 SetInterruptHandler, made it internal to
> provider/common/bootstrap.go, and based it on the
> InterruptNotify/StopInterruptNotify methods of BootstrapContext.

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://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go
> File environs/manual/provisioner.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go#newcode50
> environs/manual/provisioner.go:50: // and must be a terminal (except
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...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (4.1 KiB)

On 2013/12/19 08:44:33, axw wrote:
> Please take a look.

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go
> File environs/bootstrapcontext.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/bootstrapcontext.go#newcode49
> environs/bootstrapcontext.go:49: func (ctx *BootstrapContext)
> SetInterruptHandler(f func()) (old func()) {
> 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 DefaultBootstrapContext() *BootstrapContext {
> > return &BootstrapContext{
> > Stdin: os.Stdin,
> > Stdout: os.Stdout,
> > Stderr: os.Stderr,
> > InterruptNotify: func(c chan<- os.Signal) {
> > signal.Notify(c, os.Interrupt)
> > },
> > StopInterruptNotify: signal.Stop,
> > }
> > }
> >
> > 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)
> > StopInterruptNotify(chan<- os.Signal)
> > }
> >

> 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 SetInterruptHandler, made it internal to
> provider/common/bootstrap.go, and based it on the
> InterruptNotify/StopInterruptNotify methods of BootstrapContext.

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://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go
> File environs/manual/provisioner.go (right):

https://codereview.appspot.com/38240043/diff/20001/environs/manual/provisioner.go#newcode50
> environs/manual/provisioner.go:50: // and must be a terminal (except
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...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
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-line-specific
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://codereview.appspot.com/38240043/diff/60001/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/cmd/juju/bootstrap.go#newcode81
cmd/juju/bootstrap.go:81: type bootstrapContext struct {
nice

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go
File environs/interface.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go#newcode200
environs/interface.go:200: // Environ.Bootstrap, providing it a means of
obtaining
s/it // ?

https://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go
File environs/testing/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go#newcode50
environs/testing/bootstrap.go:50: func NewBootstrapContext(c *gc.C)
BootstrapContext {
Wouldn't it be better if this returned environs.BootstrapContext ?
(the local type could be unexported)

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode91
provider/common/bootstrap.go:91: for {
for _ = range ch {
      fmt.Fprintln(ctx.Stderr(), "Cleaning up failed bootstrap")
}
return
?

I wonder if we should allow the user to abort if they interrupt more
than 3 times...

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode154
provider/common/bootstrap.go:154: // the terminal, which will be the ssh
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://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode372
provider/common/bootstrap.go:372: case <-interrupted:
Much more direct - I like.

https://codereview.appspot.com/38240043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.5 KiB)

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-line-specific
> 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://codereview.appspot.com/38240043/diff/60001/cmd/juju/bootstrap.go
> File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/cmd/juju/bootstrap.go#newcode81
> cmd/juju/bootstrap.go:81: type bootstrapContext struct {
> nice

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go
> File environs/interface.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go#newcode200
> environs/interface.go:200: // Environ.Bootstrap, providing it a means
of
> obtaining
> s/it // ?

https://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go
> File environs/testing/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go#newcode50
> environs/testing/bootstrap.go:50: func NewBootstrapContext(c *gc.C)
> BootstrapContext {
> Wouldn't it be better if this returned environs.BootstrapContext ?
> (the local type could be unexported)

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go
> File provider/common/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode91
> provider/common/bootstrap.go:91: for {
> for _ = range ch {
> fmt.Fprintln(ctx.Stderr(), "Cleaning up failed bootstrap")
> }
> return
> ?

> I wonder if we should allow the user to abort if they interrupt more
than 3
> times...

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode154
> provider/common/bootstrap.go:154: // the termina...

Read more...

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go
File environs/interface.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/interface.go#newcode200
environs/interface.go:200: // Environ.Bootstrap, providing it a means of
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://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go
File environs/testing/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/environs/testing/bootstrap.go#newcode50
environs/testing/bootstrap.go:50: func NewBootstrapContext(c *gc.C)
BootstrapContext {
On 2013/12/19 11:29:57, rog wrote:
> Wouldn't it be better if this returned environs.BootstrapContext ?
> (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://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go
File provider/common/bootstrap.go (right):

https://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode91
provider/common/bootstrap.go:91: for {
On 2013/12/19 11:29:57, rog wrote:
> for _ = range ch {
> fmt.Fprintln(ctx.Stderr(), "Cleaning up failed bootstrap")
> }
> 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://codereview.appspot.com/38240043/diff/60001/provider/common/bootstrap.go#newcode154
provider/common/bootstrap.go:154: // the terminal, which will be the ssh
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 :)

https://codereview.appspot.com/38240043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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) {

Subscribers

People subscribed via source and target branches

to status/vote changes: