Merge lp:~axwalk/juju-core/synchronous-bootstrap 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: 2116
Proposed branch: lp:~axwalk/juju-core/synchronous-bootstrap
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1532 lines (+680/-120)
30 files modified
cloudinit/cloudinit_test.go (+31/-0)
cloudinit/options.go (+21/-0)
cloudinit/progress.go (+43/-0)
cloudinit/progress_test.go (+27/-0)
cloudinit/sshinit/configure.go (+45/-29)
cloudinit/sshinit/configure_test.go (+68/-18)
cloudinit/sshinit/suite_test.go (+14/-0)
cmd/juju/debughooks.go (+1/-1)
cmd/juju/debughooks_test.go (+3/-3)
cmd/juju/scp.go (+2/-4)
cmd/juju/scp_test.go (+1/-1)
cmd/juju/ssh.go (+8/-4)
cmd/juju/ssh_test.go (+7/-7)
environs/bootstrap/state.go (+5/-0)
environs/bootstrap/state_test.go (+21/-0)
environs/cloudinit.go (+10/-3)
environs/cloudinit/cloudinit.go (+56/-8)
environs/cloudinit/cloudinit_test.go (+8/-0)
environs/cloudinit_test.go (+38/-4)
environs/manual/bootstrap.go (+1/-1)
environs/manual/detection.go (+3/-2)
environs/manual/provisioner.go (+14/-1)
environs/sshstorage/storage.go (+4/-4)
environs/testing/bootstrap.go (+26/-0)
provider/common/bootstrap.go (+163/-8)
provider/common/bootstrap_test.go (+3/-0)
provider/ec2/local_test.go (+16/-10)
provider/maas/maas_test.go (+2/-0)
provider/openstack/local_test.go (+7/-0)
utils/ssh/ssh.go (+32/-12)
To merge this branch: bzr merge lp:~axwalk/juju-core/synchronous-bootstrap
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+196058@code.launchpad.net

Commit message

Implement synchronous bootstrap

environs/manual has been further refactored to
split the "manual cloud-init over SSH" out into
a separate package (cloudinit/sshinit).

provider/common now starts an instance with the
basic cloud-init only (SSH keys + logging), then
waits for a DNS name, waits to be able to connect
via SSH, and then defers to sshinit to execute the
remaining cloud-init steps.

If the user hits Ctrl-C, SIGINT will terminate
the SSH connection, and the bootstrap process will
attempt to clean up by stopping the instance and
removing the state file (if the instance could be
cleanly stopped). We also ignore SIGINT in the
juju process so a second Ctrl-C will not terminate
the cleanup procedure.

Fixes #1224230

https://codereview.appspot.com/30190043/

Description of the change

Implement synchronous bootstrap

environs/manual has been further refactored to
split the "manual cloud-init over SSH" out into
a separate package (cloudinit/sshinit).

provider/common now starts an instance with the
basic cloud-init only (SSH keys + logging), then
waits for a DNS name, waits to be able to connect
via SSH, and then defers to sshinit to execute the
remaining cloud-init steps.

If the user hits Ctrl-C, SIGINT will terminate
the SSH connection, and the bootstrap process will
attempt to clean up by stopping the instance and
removing the state file (if the instance could be
cleanly stopped). We also ignore SIGINT in the
juju process so a second Ctrl-C will not terminate
the cleanup procedure.

Fixes #1224230

https://codereview.appspot.com/30190043/

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

Reviewers: mp+196058_code.launchpad.net,

Message:
Please take a look.

Description:
Implement synchronous bootstrap

environs/manual has been further refactored to
split the "manual cloud-init over SSH" out into
a separate package (cloudinit/sshinit).

provider/common now starts an instance with the
basic cloud-init only (SSH keys + logging), then
waits for a DNS name, waits to be able to connect
via SSH, and then defers to sshinit to execute the
remaining cloud-init steps.

If the user hits Ctrl-C, SIGINT will terminate
the SSH connection, and the bootstrap process will
attempt to clean up by stopping the instance and
removing the state file (if the instance could be
cleanly stopped). We also ignore SIGINT in the
juju process so a second Ctrl-C will not terminate
the cleanup procedure.

Fixes #1224230

https://code.launchpad.net/~axwalk/juju-core/synchronous-bootstrap/+merge/196058

(do not edit description out of merge proposal)

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

Affected files (+334, -88 lines):
   A [revision details]
   M cloudinit/options.go
   M cloudinit/sshinit/configure.go
   M cloudinit/sshinit/configure_test.go
   A cloudinit/sshinit/suite_test.go
   M cmd/juju/bootstrap.go
   M environs/bootstrap/state.go
   M environs/bootstrap/state_test.go
   M environs/cloudinit.go
   M environs/cloudinit/cloudinit.go
   M environs/cloudinit_test.go
   M environs/manual/bootstrap.go
   M environs/manual/detection.go
   M environs/manual/provisioner.go
   M provider/common/bootstrap.go
   M provider/common/bootstrap_test.go
   M provider/ec2/local_test.go
   M provider/maas/maas_test.go
   M provider/openstack/local_test.go
   M utils/ssh/ssh.go

Revision history for this message
Ian Booth (wallyworld) wrote :

This looks pretty solid. I like how manual provisioning and cloud
provisioning now share more code.

Before landing, please test live on each of the providers.

I have a nagging concern about the finishBootstrap step. WaitDNS and the
ssh connect retry should work most if not all of the time, but there are
times when canonistack for example is reaaaally slow to start an
instance. It worries me that we could needlessly kill bootstrap if we
think we have waited long enough but the provider is just slow.
Thoughts?

https://codereview.appspot.com/30190043/diff/1/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (left):

https://codereview.appspot.com/30190043/diff/1/cloudinit/sshinit/configure_test.go#oldcode3
cloudinit/sshinit/configure_test.go:3:
Is there a test that "apt-get -y upgrade" is only used when asked for? I
can't see it if there is. I think this is a pretty important change so
warrants an explicit test.

https://codereview.appspot.com/30190043/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/30190043/diff/1/cmd/juju/bootstrap.go#newcode126
cmd/juju/bootstrap.go:126: signal.Notify(make(chan os.Signal),
os.Interrupt)
Just a thought. One other option is to install a handler which prints a
message informing the user that their Ctrl-C cannot terminate the cmd
because cleanup is being done.

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit.go
File environs/cloudinit.go (right):

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit.go#newcode135
environs/cloudinit.go:135: // and setup the SSH keys. The rest we leave
to environs/manual.
environs/manual or cloudinit/sshinit?

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit_test.go
File environs/cloudinit_test.go (right):

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit_test.go#newcode207
environs/cloudinit_test.go:207: // for MAAS. MAAS needs to configure and
thren bounce the
typo

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

https://codereview.appspot.com/30190043/diff/1/provider/common/bootstrap.go#newcode35
provider/common/bootstrap.go:35: defer func() {
Could we break this logic out into a separate function outside of
Bootstrap for clarity. The func could be

handleBootstrapError(err error, inst instance.Instance)

https://codereview.appspot.com/30190043/diff/1/provider/common/bootstrap.go#newcode95
provider/common/bootstrap.go:95: func TestingDisableFinishBootstrap()
func() {
I was going to suggest move this to an export_tests.go file but I see
it's called outside this package. Bollocks.

https://codereview.appspot.com/30190043/diff/1/provider/common/bootstrap.go#newcode104
provider/common/bootstrap.go:104: func TestingPatchFinishBootstrap(f
func(instance.Instance, *cloudinit.MachineConfig) error) func() {
Does this need to be exported? It's only called from the above.

https://codereview.appspot.com/30190043/

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

I've tested against ec2, will do openstack and azure later. I don't have
access to a MAAS installation, so that may take a little while.

I've not made any changes to the signal handling yet, but I will do so.
I need to think a bit more about how to handle it.

https://codereview.appspot.com/30190043/diff/1/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (left):

https://codereview.appspot.com/30190043/diff/1/cloudinit/sshinit/configure_test.go#oldcode3
cloudinit/sshinit/configure_test.go:3:
On 2013/11/22 01:54:00, wallyworld wrote:
> Is there a test that "apt-get -y upgrade" is only used when asked for?
I can't
> see it if there is. I think this is a pretty important change so
warrants an
> explicit test.

Added a test, also for apt-get update.

https://codereview.appspot.com/30190043/diff/1/cmd/juju/bootstrap.go
File cmd/juju/bootstrap.go (right):

https://codereview.appspot.com/30190043/diff/1/cmd/juju/bootstrap.go#newcode126
cmd/juju/bootstrap.go:126: signal.Notify(make(chan os.Signal),
os.Interrupt)
On 2013/11/22 01:54:00, wallyworld wrote:
> Just a thought. One other option is to install a handler which prints
a message
> informing the user that their Ctrl-C cannot terminate the cmd because
cleanup is
> being done.

Yeah, that's not a bad idea. The only catch is, what do we do before
finish bootstrap is entered? That's got me thinking about your concerns
some more: we need to be able to cancel the WaitDNS/ssh phase too, I
think, and that's not currently handled.

I think what I'll have to do is move the signal handling inside
finishBootstrap. This is in the same realm as direct-os.Stdin access, so
it should probably (eventually? now?) go into some kind of OS-context
structure that gets passed to the Environment (or maybe just to
Bootstrap).

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit.go
File environs/cloudinit.go (right):

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit.go#newcode135
environs/cloudinit.go:135: // and setup the SSH keys. The rest we leave
to environs/manual.
On 2013/11/22 01:54:00, wallyworld wrote:
> environs/manual or cloudinit/sshinit?

cloudinit/sshinit; fixed, thanks

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit_test.go
File environs/cloudinit_test.go (right):

https://codereview.appspot.com/30190043/diff/1/environs/cloudinit_test.go#newcode207
environs/cloudinit_test.go:207: // for MAAS. MAAS needs to configure and
thren bounce the
On 2013/11/22 01:54:00, wallyworld wrote:
> typo

Done.

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

https://codereview.appspot.com/30190043/diff/1/provider/common/bootstrap.go#newcode35
provider/common/bootstrap.go:35: defer func() {
On 2013/11/22 01:54:00, wallyworld wrote:
> Could we break this logic out into a separate function outside of
Bootstrap for
> clarity. The func could be

> handleBootstrapError(err error, inst instance.Instance)

Done.

https://codereview.appspot.com/30190043/diff/1/provider/common/bootstrap.go#newcode104
provider/common/bootstrap.go:104: func TestingPatchFinishBootstrap(f
func(instance.Inst...

Read more...

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

Thanks for dealing with the finishBootstrap issues - I think it's much
better now that it allows the process to complete in it's own time, but
still allows a Ctrl-C. The code looks ok from a reading it perspective,
and you said you've tested it live :-) One final suggestion - after each
"tick" in the for loops while waiting for dns and ssh, should we print a
"." or something so the user knows the system is alive. Every one second
would be too often so perhaps after each 10 1 second timeouts we could
do it?

We do need to test on maas before landing.

LGTM after a MAAS test and final move of those test helpers to a testing
package.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode136
cloudinit/sshinit/configure_test.go:136: assertScriptMatches(c, cfg,
"(.|\n)*apt-get -y update(.|\n)*", false)
To prevent typos causing the test to pass, I'd love to see the regexp
string which is cut and pasted be put into a const or var.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode149
cloudinit/sshinit/configure_test.go:149: assertScriptMatches(c, cfg,
"(.|\n)*apt-get -y upgrade(.|\n)*", false)
ditto

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

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode37
provider/common/bootstrap.go:37: // TODO(axw) 2013-11-22 #XXX
a bug number would be great here

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode90
provider/common/bootstrap.go:90: fmt.Fprintln(ctx.Stderr, "Stopping
instance...")
pedant alert - I think we should use case consistently for out messages.
"Stopping...." vs "cleaning up..." etc

https://codereview.appspot.com/30190043/

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

On 2013/11/22 06:37:49, wallyworld wrote:
> Thanks for dealing with the finishBootstrap issues - I think it's much
better
> now that it allows the process to complete in it's own time, but still
allows a
> Ctrl-C. The code looks ok from a reading it perspective, and you said
you've
> tested it live :-) One final suggestion - after each "tick" in the for
loops
> while waiting for dns and ssh, should we print a "." or something so
the user
> knows the system is alive. Every one second would be too often so
perhaps after
> each 10 1 second timeouts we could do it?

I've added the dots. I think 1 second is fine; the DNS wait is quick
anyway. The difference in rate should also give an indication of the
relative speed of the two phases.

> We do need to test on maas before landing.

> LGTM after a MAAS test and final move of those test helpers to a
testing
> package.

Thanks, I will do that. I might just run this by thumper (and fwereade?)
as well, as I know he was interested in it to begin with.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go
> File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode136
> cloudinit/sshinit/configure_test.go:136: assertScriptMatches(c, cfg,
> "(.|\n)*apt-get -y update(.|\n)*", false)
> To prevent typos causing the test to pass, I'd love to see the regexp
string
> which is cut and pasted be put into a const or var.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode149
> cloudinit/sshinit/configure_test.go:149: assertScriptMatches(c, cfg,
> "(.|\n)*apt-get -y upgrade(.|\n)*", false)
> ditto

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

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode37
> provider/common/bootstrap.go:37: // TODO(axw) 2013-11-22 #XXX
> a bug number would be great here

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode90
> provider/common/bootstrap.go:90: fmt.Fprintln(ctx.Stderr, "Stopping
> instance...")
> pedant alert - I think we should use case consistently for out
messages.
> "Stopping...." vs "cleaning up..." etc

https://codereview.appspot.com/30190043/

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

Please take a look.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode136
cloudinit/sshinit/configure_test.go:136: assertScriptMatches(c, cfg,
"(.|\n)*apt-get -y update(.|\n)*", false)
On 2013/11/22 06:37:50, wallyworld wrote:
> To prevent typos causing the test to pass, I'd love to see the regexp
string
> which is cut and pasted be put into a const or var.

Done.

https://codereview.appspot.com/30190043/diff/20001/cloudinit/sshinit/configure_test.go#newcode149
cloudinit/sshinit/configure_test.go:149: assertScriptMatches(c, cfg,
"(.|\n)*apt-get -y upgrade(.|\n)*", false)
On 2013/11/22 06:37:50, wallyworld wrote:
> ditto

Done.

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

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode37
provider/common/bootstrap.go:37: // TODO(axw) 2013-11-22 #XXX
On 2013/11/22 06:37:50, wallyworld wrote:
> a bug number would be great here

Done.

https://codereview.appspot.com/30190043/diff/20001/provider/common/bootstrap.go#newcode90
provider/common/bootstrap.go:90: fmt.Fprintln(ctx.Stderr, "Stopping
instance...")
On 2013/11/22 06:37:50, wallyworld wrote:
> pedant alert - I think we should use case consistently for out
messages.
> "Stopping...." vs "cleaning up..." etc

Absolutely - just an oversight, thanks for picking it up.

https://codereview.appspot.com/30190043/

Revision history for this message
John A Meinel (jameinel) wrote :

I tried this, and I *really* like the first steps where it is starting
to connect, letting you know what is going on, etc. However, once it can
SSH in, it just dumps all of cloud-init-output.log to stdout which seems
very overly verbose. I personally would like to know a *summary* of what
it is doing (installing packages) rather than 100s of lines about the
exact thing it is doing.

Thoughts?

https://codereview.appspot.com/30190043/

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

On 2013/11/24 08:11:01, jameinel wrote:
> I tried this, and I *really* like the first steps where it is starting
to
> connect, letting you know what is going on, etc. However, once it can
SSH in, it
> just dumps all of cloud-init-output.log to stdout which seems very
overly
> verbose. I personally would like to know a *summary* of what it is
doing
> (installing packages) rather than 100s of lines about the exact thing
it is
> doing.

> Thoughts?

I agree. It's maybe slightly tricky to implement, as it really is just
executing one big script over SSH. I'll have to do some investigation to
see what's practical.

https://codereview.appspot.com/30190043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
William Reade (fwereade) wrote :
Download full text (3.7 KiB)

This is *great*. Thanks very much. It would be really nice to have an
initial line saying "Launching instance", followed by the actual
instance id that got started -- and maybe to finish by running juju
status -- but those are just details.

So LGTM modulo trivials below, assuming jam is happy; and a few followup
questions:

1) Can we drop the secrets dance soon? (I guess we still need it for the
first connect to 1.16 via state :/... but maybe we don't need it for the
API)

2) How close are we to writing the state server address directly into
the .jenv, so we get fast API connections from the word go? I'd be
really keen to see that followup soon :).

3) Which SSH commands do we still use? I think it's just debug-log that
goes over the API now, so ssh, scp, and debug-hooks are all still SSHy
AIUI -- can we consolidate those bits now? Any I didn't think of?

4) How hard will it be to extract the manual-provisioning code such that
it's possible to run a "juju register" command on an instance that you
want to provision and already have direct access to? I think we touched
on this a while back, but I'm not sure we came to any conclusion.

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go#newcode36
cloudinit/sshinit/configure_test.go:36:
environs.RegisterProvider("sshinit", &testProvider{})
Can we stick an explicit "test" into the name somewhere please?

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

https://codereview.appspot.com/30190043/diff/60001/environs/bootstrap/state_test.go#newcode59
environs/bootstrap/state_test.go:59: c.Assert(err, gc.IsNil) // doesn't
exist, juju don't carea
s/carea/care/

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

https://codereview.appspot.com/30190043/diff/60001/provider/common/bootstrap.go#newcode40
provider/common/bootstrap.go:40: // Std{in,out,err}, and interrupt
signal handling.
I'd like this to be an imminent followup, please :).

https://codereview.appspot.com/30190043/diff/60001/provider/common/bootstrap.go#newcode61
provider/common/bootstrap.go:61: inst, hw, err = env.StartInstance(cons,
selectedTools, machineConfig)
Wouldn't this still work with :=, even given inst? Maybe I'm still
sluggish after lunch...

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

https://codereview.appspot.com/30190043/diff/60001/provider/common/testing/bootstrap.go#newcode17
provider/common/testing/bootstrap.go:17: f :=
func(*common.BootstrapContext, instance.Instance,
*cloudinit.MachineConfig) error {
Let's log something in here so that there's some way of diagnosing what
happened if Disable gets called in a surprising situation.

https://codereview.appspot.com/30190043/diff/60001/provider/maas/maas_test.go
File provider/maas/maas_test.go (right):

https://codereview.appspot.com/30190043/diff/60001/provider...

Read more...

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

Oh! And, can we run in-environment storage everywhere soon? This doesn't
handle the bootstrap-state case ofc, but it would be great to keep
charms and tools inside the environment.

https://codereview.appspot.com/30190043/

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

...

> 3) Which SSH commands do we still use? I think it's just debug-log
> that goes over the API now, so ssh, scp, and debug-hooks are all
> still SSHy AIUI -- can we consolidate those bits now? Any I didn't
> think of?

Right now debug-log still goes via ssh + tail, though Frank is working
on moving us to an actual API call.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.13 (Cygwin)
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iEYEARECAAYFAlKdYRYACgkQJdeBCYSNAAPUsgCfTgnsDcRvG+/CyNLYoptrrENk
sSAAn2dC2nbwNAT76g3n701gOdt1Yt9c
=X9FF
-----END PGP SIGNATURE-----

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

On 2013/12/02 14:54:09, fwereade wrote:
> This is *great*. Thanks very much. It would be really nice to have an
initial
> line saying "Launching instance", followed by the actual instance id
that got
> started -- and maybe to finish by running juju status -- but those are
just
> details.

I've added the "Launching instance" message. Leaving the status bit for
now; that can easily be added in afterwards, and is somewhat separate
from this CL.

> So LGTM modulo trivials below, assuming jam is happy; and a few
followup
> questions:

> 1) Can we drop the secrets dance soon? (I guess we still need it for
the first
> connect to 1.16 via state :/... but maybe we don't need it for the
API)

Yes, I intend to kill it soon.

> 2) How close are we to writing the state server address directly into
the .jenv,
> so we get fast API connections from the word go? I'd be really keen to
see that
> followup soon :).

I don't know, that wasn't part of my plan. Sounds easy enough.

> 3) Which SSH commands do we still use? I think it's just debug-log
that goes
> over the API now, so ssh, scp, and debug-hooks are all still SSHy AIUI
-- can we
> consolidate those bits now? Any I didn't think of?

debug-log still uses SSH; it only uses the API for getting the address.
IIANM, TheMue is looking at changing this.
ssh, scp, debug-* all use SSH, as do environs/manual,
environs/sshstorage, and cloudinit/sshinit. I've changed them all to use
utils/ssh.

> 4) How hard will it be to extract the manual-provisioning code such
that it's
> possible to run a "juju register" command on an instance that you want
to
> provision and already have direct access to? I think we touched on
this a while
> back, but I'm not sure we came to any conclusion.

The SSH-bits could easily be pretty easily swapped to use os/exec
Command, so it's all on localhost. The challenging bit will be how to
package configuration in a nice way.

> Oh! And, can we run in-environment storage everywhere soon? This
doesn't handle
> the bootstrap-state case ofc, but it would be great to keep charms and
tools
> inside the environment.

I can still see one major problem, which is that you couldn't then
sync-tools before bootstrapping. Other than that, I think it should be
doable.

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go
> File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go#newcode36
> cloudinit/sshinit/configure_test.go:36:
environs.RegisterProvider("sshinit",
> &testProvider{})
> Can we stick an explicit "test" into the name somewhere please?

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

https://codereview.appspot.com/30190043/diff/60001/environs/bootstrap/state_test.go#newcode59
> environs/bootstrap/state_test.go:59: c.Assert(err, gc.IsNil) //
doesn't exist,
> juju don't carea
> s/carea/care/

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

https://codereview.appspot.com/30190043/diff/60001/provider/common/bootstrap....

Read more...

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

Please take a look.

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go
File cloudinit/sshinit/configure_test.go (right):

https://codereview.appspot.com/30190043/diff/60001/cloudinit/sshinit/configure_test.go#newcode36
cloudinit/sshinit/configure_test.go:36:
environs.RegisterProvider("sshinit", &testProvider{})
On 2013/12/02 14:54:09, fwereade wrote:
> Can we stick an explicit "test" into the name somewhere please?

Done.

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

https://codereview.appspot.com/30190043/diff/60001/environs/bootstrap/state_test.go#newcode59
environs/bootstrap/state_test.go:59: c.Assert(err, gc.IsNil) // doesn't
exist, juju don't carea
On 2013/12/02 14:54:09, fwereade wrote:
> s/carea/care/

Done.

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

https://codereview.appspot.com/30190043/diff/60001/provider/common/bootstrap.go#newcode40
provider/common/bootstrap.go:40: // Std{in,out,err}, and interrupt
signal handling.
On 2013/12/02 14:54:09, fwereade wrote:
> I'd like this to be an imminent followup, please :).

No problems.

https://codereview.appspot.com/30190043/diff/60001/provider/common/bootstrap.go#newcode61
provider/common/bootstrap.go:61: inst, hw, err = env.StartInstance(cons,
selectedTools, machineConfig)
On 2013/12/02 14:54:09, fwereade wrote:
> Wouldn't this still work with :=, even given inst? Maybe I'm still
sluggish
> after lunch...

No, you're right, that'll work. Fixed.

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

https://codereview.appspot.com/30190043/diff/60001/provider/common/testing/bootstrap.go#newcode17
provider/common/testing/bootstrap.go:17: f :=
func(*common.BootstrapContext, instance.Instance,
*cloudinit.MachineConfig) error {
On 2013/12/02 14:54:09, fwereade wrote:
> Let's log something in here so that there's some way of diagnosing
what happened
> if Disable gets called in a surprising situation.

Done.

https://codereview.appspot.com/30190043/diff/60001/provider/maas/maas_test.go
File provider/maas/maas_test.go (right):

https://codereview.appspot.com/30190043/diff/60001/provider/maas/maas_test.go#newcode14
provider/maas/maas_test.go:14: commontesting
"launchpad.net/juju-core/provider/common/testing"
On 2013/12/02 14:54:09, fwereade wrote:
> Oh, a thought. Might DisableFinishBootstrap be happier in
environs/testing?

Hmm yeah that's fine, and stops the package proliferation. Moved.

https://codereview.appspot.com/30190043/diff/60001/utils/ssh/ssh.go
File utils/ssh/ssh.go (left):

https://codereview.appspot.com/30190043/diff/60001/utils/ssh/ssh.go#oldcode15
utils/ssh/ssh.go:15: // Move this to a common package for use in
cmd/juju, and others.
On 2013/12/02 14:54:09, fwereade wrote:
> Can the SSHy bits in cmd/juju use this now?

Done. Also updated environs/sshstorage, and environs/manual. Had to
tweak utils/ssh a bit, and add an ScpCommand function.

https://codereview.appspot.com/30190043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/cloudinit_test.go'
2--- cloudinit/cloudinit_test.go 2013-11-26 12:24:48 +0000
3+++ cloudinit/cloudinit_test.go 2013-12-03 06:15:09 +0000
4@@ -269,6 +269,37 @@
5 c.Assert(cfg.Packages(), gc.DeepEquals, []string{"a b c", "d!"})
6 }
7
8+func (S) TestSetOutput(c *gc.C) {
9+ type test struct {
10+ kind cloudinit.OutputKind
11+ stdout string
12+ stderr string
13+ }
14+ tests := []test{{
15+ cloudinit.OutAll, "a", "",
16+ }, {
17+ cloudinit.OutAll, "", "b",
18+ }, {
19+ cloudinit.OutInit, "a", "b",
20+ }, {
21+ cloudinit.OutAll, "a", "b",
22+ }, {
23+ cloudinit.OutAll, "", "",
24+ }}
25+
26+ cfg := cloudinit.New()
27+ stdout, stderr := cfg.Output(cloudinit.OutAll)
28+ c.Assert(stdout, gc.Equals, "")
29+ c.Assert(stderr, gc.Equals, "")
30+ for i, t := range tests {
31+ c.Logf("test %d: %+v", i, t)
32+ cfg.SetOutput(t.kind, t.stdout, t.stderr)
33+ stdout, stderr = cfg.Output(t.kind)
34+ c.Assert(stdout, gc.Equals, t.stdout)
35+ c.Assert(stderr, gc.Equals, t.stderr)
36+ }
37+}
38+
39 //#cloud-config
40 //packages:
41 //- juju
42
43=== modified file 'cloudinit/options.go'
44--- cloudinit/options.go 2013-11-26 12:24:48 +0000
45+++ cloudinit/options.go 2013-12-03 06:15:09 +0000
46@@ -31,6 +31,13 @@
47 cfg.set("apt_upgrade", yes, yes)
48 }
49
50+// AptUpgrade returns the value set by SetAptUpgrade, or
51+// false if no call to SetAptUpgrade has been made.
52+func (cfg *Config) AptUpgrade() bool {
53+ update, _ := cfg.attrs["apt_upgrade"].(bool)
54+ return update
55+}
56+
57 // SetUpdate sets whether cloud-init runs "apt-get update"
58 // on first boot.
59 func (cfg *Config) SetAptUpdate(yes bool) {
60@@ -233,6 +240,20 @@
61 cfg.attrs["output"] = out
62 }
63
64+// Output returns the output destination passed to SetOutput for
65+// the specified output kind.
66+func (cfg *Config) Output(kind OutputKind) (stdout, stderr string) {
67+ if out, ok := cfg.attrs["output"].(map[string]interface{}); ok {
68+ switch out := out[string(kind)].(type) {
69+ case string:
70+ stdout = out
71+ case []string:
72+ stdout, stderr = out[0], out[1]
73+ }
74+ }
75+ return stdout, stderr
76+}
77+
78 // AddSSHKey adds a pre-generated ssh key to the
79 // server keyring. Keys that are added like this will be
80 // written to /etc/ssh and new random keys will not
81
82=== added file 'cloudinit/progress.go'
83--- cloudinit/progress.go 1970-01-01 00:00:00 +0000
84+++ cloudinit/progress.go 2013-12-03 06:15:09 +0000
85@@ -0,0 +1,43 @@
86+// Copyright 2013 Canonical Ltd.
87+// Licensed under the AGPLv3, see LICENCE file for details.
88+
89+package cloudinit
90+
91+import (
92+ "fmt"
93+
94+ "launchpad.net/juju-core/utils"
95+)
96+
97+// progressFd is the file descriptor to which progress is logged.
98+// This is necessary so that we can redirect all non-progress
99+// related stderr away.
100+//
101+// Note, from the Bash manual:
102+// "Redirections using file descriptors greater than 9 should be
103+// used with care, as they may conflict with file descriptors the
104+// shell uses internally."
105+const progressFd = 9
106+
107+// InitProgressCmd will return a command to initialise progress
108+// reporting, sending messages to stderr. If LogProgressCmd is
109+// used in a script, InitProgressCmd MUST be executed beforehand.
110+//
111+// The returned command is idempotent; this is important, to
112+// allow a script to be embedded in another with stderr redirected,
113+// in which case InitProgressCmd must precede the redirection.
114+func InitProgressCmd() string {
115+ return fmt.Sprintf("test -e /proc/self/fd/%d || exec %d>&2", progressFd, progressFd)
116+}
117+
118+// LogProgressCmd will return a command to log the specified progress
119+// message to stderr; the resultant command should be added to the
120+// configuration as a runcmd or bootcmd as appropriate.
121+//
122+// If there are any uses of LogProgressCmd in a configuration, the
123+// configuration MUST precede the command with the result of
124+// InitProgressCmd.
125+func LogProgressCmd(format string, args ...interface{}) string {
126+ msg := utils.ShQuote(fmt.Sprintf(format, args...))
127+ return fmt.Sprintf("echo %s >&%d", msg, progressFd)
128+}
129
130=== added file 'cloudinit/progress_test.go'
131--- cloudinit/progress_test.go 1970-01-01 00:00:00 +0000
132+++ cloudinit/progress_test.go 2013-12-03 06:15:09 +0000
133@@ -0,0 +1,27 @@
134+// Copyright 2011, 2012, 2013 Canonical Ltd.
135+// Licensed under the AGPLv3, see LICENCE file for details.
136+
137+package cloudinit_test
138+
139+import (
140+ "regexp"
141+
142+ gc "launchpad.net/gocheck"
143+
144+ "launchpad.net/juju-core/cloudinit"
145+)
146+
147+type progressSuite struct{}
148+
149+var _ = gc.Suite(&progressSuite{})
150+
151+func (*progressSuite) TestProgressCmds(c *gc.C) {
152+ initCmd := cloudinit.InitProgressCmd()
153+ re := regexp.MustCompile(`test -e /proc/self/fd/([0-9]+) \|\| exec ([0-9]+)>&2`)
154+ submatch := re.FindStringSubmatch(initCmd)
155+ c.Assert(submatch, gc.HasLen, 3)
156+ c.Assert(submatch[0], gc.Equals, initCmd)
157+ c.Assert(submatch[1], gc.Equals, submatch[2])
158+ logCmd := cloudinit.LogProgressCmd("he'llo\"!")
159+ c.Assert(logCmd, gc.Equals, `echo 'he'"'"'llo"!' >&`+submatch[1])
160+}
161
162=== added directory 'cloudinit/sshinit'
163=== renamed file 'environs/manual/agent.go' => 'cloudinit/sshinit/configure.go'
164--- environs/manual/agent.go 2013-11-14 08:27:15 +0000
165+++ cloudinit/sshinit/configure.go 2013-12-03 06:15:09 +0000
166@@ -1,7 +1,7 @@
167 // Copyright 2013 Canonical Ltd.
168 // Licensed under the AGPLv3, see LICENCE file for details.
169
170-package manual
171+package sshinit
172
173 import (
174 "encoding/base64"
175@@ -9,25 +9,29 @@
176 "os"
177 "strings"
178
179- corecloudinit "launchpad.net/juju-core/cloudinit"
180- "launchpad.net/juju-core/environs/cloudinit"
181+ "launchpad.net/loggo"
182+
183+ "launchpad.net/juju-core/cloudinit"
184 "launchpad.net/juju-core/utils"
185+ "launchpad.net/juju-core/utils/ssh"
186 )
187
188-// ProvisionMachineAgent connects to a machine over SSH,
189-// and installs a machine agent.
190-func ProvisionMachineAgent(host string, mcfg *cloudinit.MachineConfig) error {
191+var logger = loggo.GetLogger("juju.cloudinit.sshinit")
192+
193+// Configure connects to the specified host over SSH,
194+// and executes a script that carries out cloud-config.
195+func Configure(host string, cfg *cloudinit.Config) error {
196 logger.Infof("Provisioning machine agent on %s", host)
197- script, err := provisionMachineAgentScript(mcfg)
198+ script, err := generateScript(cfg)
199 if err != nil {
200 return err
201 }
202 scriptBase64 := base64.StdEncoding.EncodeToString([]byte(script))
203 script = fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, scriptBase64)
204- cmd := sshCommand(
205+ cmd := ssh.Command(
206 host,
207- fmt.Sprintf("sudo bash -c '%s'", script),
208- allocateTTY,
209+ []string{"sudo", fmt.Sprintf("bash -c '%s'", script)},
210+ ssh.AllocateTTY,
211 )
212 cmd.Stdout = os.Stdout
213 cmd.Stderr = os.Stderr
214@@ -35,17 +39,9 @@
215 return cmd.Run()
216 }
217
218-// provisionMachineAgentScript generates the script necessary
219-// to install a machine agent with the provided MachineConfig.
220-func provisionMachineAgentScript(mcfg *cloudinit.MachineConfig) (string, error) {
221- // We generate a cloud-init config, which we'll then pull the runcmds
222- // and prerequisite packages out of. Rather than generating a cloud-config,
223- // we'll just generate a shell script.
224- cloudcfg := corecloudinit.New()
225- if err := cloudinit.Configure(mcfg, cloudcfg); err != nil {
226- return "", err
227- }
228-
229+// generateScript generates the script that applies
230+// the specified cloud-config.
231+func generateScript(cloudcfg *cloudinit.Config) (string, error) {
232 // TODO(axw): 2013-08-23 bug 1215777
233 // Carry out configuration for ssh-keys-per-user,
234 // machine-updates-authkeys, using cloud-init config.
235@@ -78,23 +74,38 @@
236 // We prepend "set -xe". This is already in runcmds,
237 // but added here to avoid relying on that to be
238 // invariant.
239- script := []string{"#!/bin/bash", "set -xe"}
240+ script := []string{"#!/bin/bash", "set -e"}
241+ // We must initialise progress reporting before entering
242+ // the subshell and redirecting stderr.
243+ script = append(script, cloudinit.InitProgressCmd())
244+ stdout, stderr := cloudcfg.Output(cloudinit.OutAll)
245+ script = append(script, "(")
246+ if stderr != "" {
247+ script = append(script, "(")
248+ }
249 script = append(script, bootcmds...)
250 script = append(script, pkgcmds...)
251 script = append(script, runcmds...)
252+ if stderr != "" {
253+ script = append(script, ") "+stdout)
254+ script = append(script, ") "+stderr)
255+ } else {
256+ script = append(script, ") "+stdout+" 2>&1")
257+ }
258 return strings.Join(script, "\n"), nil
259 }
260
261 // addPackageCommands returns a slice of commands that, when run,
262 // will add the required apt repositories and packages.
263-func addPackageCommands(cfg *corecloudinit.Config) ([]string, error) {
264+func addPackageCommands(cfg *cloudinit.Config) ([]string, error) {
265 var cmds []string
266 if len(cfg.AptSources()) > 0 {
267- // Ensure apt-add-repository is available.
268+ // Ensure add-apt-repository is available.
269+ cmds = append(cmds, cloudinit.LogProgressCmd("Installing add-apt-repository"))
270 cmds = append(cmds, "apt-get -y install python-software-properties")
271 }
272 for _, src := range cfg.AptSources() {
273- // PPA keys are obtained by apt-add-repository, from launchpad.
274+ // PPA keys are obtained by add-apt-repository, from launchpad.
275 if !strings.HasPrefix(src.Source, "ppa:") {
276 if src.Key != "" {
277 key := utils.ShQuote(src.Key)
278@@ -102,14 +113,19 @@
279 cmds = append(cmds, cmd)
280 }
281 }
282- cmds = append(cmds, "apt-add-repository -y "+utils.ShQuote(src.Source))
283+ cmds = append(cmds, cloudinit.LogProgressCmd("Adding apt repository: %s", src.Source))
284+ cmds = append(cmds, "add-apt-repository -y "+utils.ShQuote(src.Source))
285 }
286- if len(cfg.AptSources()) > 0 {
287+ if len(cfg.AptSources()) > 0 || cfg.AptUpdate() {
288+ cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get update"))
289 cmds = append(cmds, "apt-get -y update")
290 }
291- // Note: explicitly ignoring apt_upgrade, so as not to trample the target
292- // machine's existing configuration.
293+ if cfg.AptUpgrade() {
294+ cmds = append(cmds, cloudinit.LogProgressCmd("Running apt-get upgrade"))
295+ cmds = append(cmds, "apt-get -y upgrade")
296+ }
297 for _, pkg := range cfg.Packages() {
298+ cmds = append(cmds, cloudinit.LogProgressCmd("Installing package: %s", pkg))
299 cmd := fmt.Sprintf("apt-get -y install %s", utils.ShQuote(pkg))
300 cmds = append(cmds, cmd)
301 }
302
303=== renamed file 'environs/manual/agent_test.go' => 'cloudinit/sshinit/configure_test.go'
304--- environs/manual/agent_test.go 2013-11-14 08:27:15 +0000
305+++ cloudinit/sshinit/configure_test.go 2013-12-03 06:15:09 +0000
306@@ -1,35 +1,46 @@
307 // Copyright 2013 Canonical Ltd.
308 // Licensed under the AGPLv3, see LICENCE file for details.
309
310-package manual
311+package sshinit
312
313 import (
314 gc "launchpad.net/gocheck"
315
316+ "launchpad.net/juju-core/cloudinit"
317 "launchpad.net/juju-core/constraints"
318 "launchpad.net/juju-core/environs"
319- "launchpad.net/juju-core/environs/cloudinit"
320+ envcloudinit "launchpad.net/juju-core/environs/cloudinit"
321 "launchpad.net/juju-core/environs/config"
322 envtools "launchpad.net/juju-core/environs/tools"
323- _ "launchpad.net/juju-core/provider/dummy"
324 coretesting "launchpad.net/juju-core/testing"
325 "launchpad.net/juju-core/testing/testbase"
326 "launchpad.net/juju-core/tools"
327 "launchpad.net/juju-core/version"
328 )
329
330-type agentSuite struct {
331+type configureSuite struct {
332 testbase.LoggingSuite
333 }
334
335-var _ = gc.Suite(&agentSuite{})
336-
337-func dummyConfig(c *gc.C, stateServer bool, vers version.Binary) *config.Config {
338+var _ = gc.Suite(&configureSuite{})
339+
340+type testProvider struct {
341+ environs.EnvironProvider
342+}
343+
344+func (p *testProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
345+ return map[string]string{}, nil
346+}
347+
348+func init() {
349+ environs.RegisterProvider("sshinit_test", &testProvider{})
350+}
351+
352+func testConfig(c *gc.C, stateServer bool, vers version.Binary) *config.Config {
353 testConfig, err := config.New(config.UseDefaults, coretesting.FakeConfig())
354 c.Assert(err, gc.IsNil)
355 testConfig, err = testConfig.Apply(map[string]interface{}{
356- "type": "dummy",
357- "state-server": stateServer,
358+ "type": "sshinit_test",
359 "default-series": vers.Series,
360 "agent-version": vers.Number.String(),
361 })
362@@ -37,8 +48,8 @@
363 return testConfig
364 }
365
366-func (s *agentSuite) getMachineConfig(c *gc.C, stateServer bool, vers version.Binary) *cloudinit.MachineConfig {
367- var mcfg *cloudinit.MachineConfig
368+func (s *configureSuite) getCloudConfig(c *gc.C, stateServer bool, vers version.Binary) *cloudinit.Config {
369+ var mcfg *envcloudinit.MachineConfig
370 if stateServer {
371 mcfg = environs.NewBootstrapMachineConfig("http://whatever/dotcom")
372 } else {
373@@ -48,10 +59,13 @@
374 Version: vers,
375 URL: "file:///var/lib/juju/storage/" + envtools.StorageName(vers),
376 }
377- environConfig := dummyConfig(c, stateServer, vers)
378+ environConfig := testConfig(c, stateServer, vers)
379 err := environs.FinishMachineConfig(mcfg, environConfig, constraints.Value{})
380 c.Assert(err, gc.IsNil)
381- return mcfg
382+ cloudcfg := cloudinit.New()
383+ err = envcloudinit.Configure(mcfg, cloudcfg)
384+ c.Assert(err, gc.IsNil)
385+ return cloudcfg
386 }
387
388 var allSeries = [...]string{"precise", "quantal", "raring", "saucy"}
389@@ -63,10 +77,10 @@
390 return gc.Not(checker)
391 }
392
393-func (s *agentSuite) TestAptSources(c *gc.C) {
394+func (s *configureSuite) TestAptSources(c *gc.C) {
395 for _, series := range allSeries {
396 vers := version.MustParseBinary("1.16.0-" + series + "-amd64")
397- script, err := provisionMachineAgentScript(s.getMachineConfig(c, true, vers))
398+ script, err := generateScript(s.getCloudConfig(c, true, vers))
399 c.Assert(err, gc.IsNil)
400
401 // Only Precise requires the cloud-tools pocket.
402@@ -82,7 +96,7 @@
403 c.Assert(
404 script,
405 checkIff(gc.Matches, needsCloudTools),
406- "(.|\n)*apt-add-repository.*cloud-tools(.|\n)*",
407+ "(.|\n)*add-apt-repository.*cloud-tools(.|\n)*",
408 )
409
410 // Only Quantal requires the PPA (for mongo).
411@@ -90,10 +104,10 @@
412 c.Assert(
413 script,
414 checkIff(gc.Matches, needsJujuPPA),
415- "(.|\n)*apt-add-repository.*ppa:juju/stable(.|\n)*",
416+ "(.|\n)*add-apt-repository.*ppa:juju/stable(.|\n)*",
417 )
418
419- // Only install python-software-properties (apt-add-repository)
420+ // Only install python-software-properties (add-apt-repository)
421 // if we need to.
422 c.Assert(
423 script,
424@@ -102,3 +116,39 @@
425 )
426 }
427 }
428+
429+func assertScriptMatches(c *gc.C, cfg *cloudinit.Config, pattern string, match bool) {
430+ script, err := generateScript(cfg)
431+ c.Assert(err, gc.IsNil)
432+ checker := gc.Matches
433+ if !match {
434+ checker = gc.Not(checker)
435+ }
436+ c.Assert(script, checker, pattern)
437+}
438+
439+func (s *configureSuite) TestAptUpdate(c *gc.C) {
440+ // apt-get update is run if either AptUpdate is set,
441+ // or apt sources are defined.
442+ const aptGetUpdatePattern = "(.|\n)*apt-get -y update(.|\n)*"
443+ cfg := cloudinit.New()
444+ c.Assert(cfg.AptUpdate(), gc.Equals, false)
445+ c.Assert(cfg.AptSources(), gc.HasLen, 0)
446+ assertScriptMatches(c, cfg, aptGetUpdatePattern, false)
447+ cfg.SetAptUpdate(true)
448+ assertScriptMatches(c, cfg, aptGetUpdatePattern, true)
449+ cfg.SetAptUpdate(false)
450+ cfg.AddAptSource("source", "key")
451+ assertScriptMatches(c, cfg, aptGetUpdatePattern, true)
452+}
453+
454+func (s *configureSuite) TestAptUpgrade(c *gc.C) {
455+ // apt-get upgrade is only run if AptUpgrade is set.
456+ const aptGetUpgradePattern = "(.|\n)*apt-get -y upgrade(.|\n)*"
457+ cfg := cloudinit.New()
458+ cfg.SetAptUpdate(true)
459+ cfg.AddAptSource("source", "key")
460+ assertScriptMatches(c, cfg, aptGetUpgradePattern, false)
461+ cfg.SetAptUpgrade(true)
462+ assertScriptMatches(c, cfg, aptGetUpgradePattern, true)
463+}
464
465=== added file 'cloudinit/sshinit/suite_test.go'
466--- cloudinit/sshinit/suite_test.go 1970-01-01 00:00:00 +0000
467+++ cloudinit/sshinit/suite_test.go 2013-12-03 06:15:09 +0000
468@@ -0,0 +1,14 @@
469+// Copyright 2013 Canonical Ltd.
470+// Licensed under the AGPLv3, see LICENCE file for details.
471+
472+package sshinit
473+
474+import (
475+ stdtesting "testing"
476+
477+ gc "launchpad.net/gocheck"
478+)
479+
480+func Test(t *stdtesting.T) {
481+ gc.TestingT(t)
482+}
483
484=== modified file 'cmd/juju/debughooks.go'
485--- cmd/juju/debughooks.go 2013-11-28 11:57:57 +0000
486+++ cmd/juju/debughooks.go 2013-12-03 06:15:09 +0000
487@@ -146,7 +146,7 @@
488 debugctx := unitdebug.NewHooksContext(c.Target)
489 script := base64.StdEncoding.EncodeToString([]byte(unitdebug.ClientScript(debugctx, c.hooks)))
490 innercmd := fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, script)
491- args := []string{"--", fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)}
492+ args := []string{fmt.Sprintf("sudo /bin/bash -c '%s'", innercmd)}
493 c.Args = args
494 return c.SSHCommand.Run(ctx)
495 }
496
497=== modified file 'cmd/juju/debughooks_test.go'
498--- cmd/juju/debughooks_test.go 2013-11-08 04:45:20 +0000
499+++ cmd/juju/debughooks_test.go 2013-12-03 06:15:09 +0000
500@@ -19,7 +19,7 @@
501 SSHCommonSuite
502 }
503
504-const debugHooksArgs = `-l ubuntu -t ` + commonArgs
505+const debugHooksArgs = sshArgs
506
507 var debugHooksTests = []struct {
508 info string
509@@ -29,10 +29,10 @@
510 stderr string
511 }{{
512 args: []string{"mysql/0"},
513- result: regexp.QuoteMeta(debugHooksArgs + "dummyenv-0.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"),
514+ result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-0.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"),
515 }, {
516 args: []string{"mongodb/1"},
517- result: regexp.QuoteMeta(debugHooksArgs + "dummyenv-2.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"),
518+ result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-2.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"),
519 }, {
520 info: `"*" is a valid hook name: it means hook everything`,
521 args: []string{"mysql/0", "*"},
522
523=== modified file 'cmd/juju/scp.go'
524--- cmd/juju/scp.go 2013-11-20 13:29:15 +0000
525+++ cmd/juju/scp.go 2013-12-03 06:15:09 +0000
526@@ -5,10 +5,10 @@
527
528 import (
529 "errors"
530- "os/exec"
531 "strings"
532
533 "launchpad.net/juju-core/cmd"
534+ "launchpad.net/juju-core/utils/ssh"
535 )
536
537 // SCPCommand is responsible for launching a scp command to copy files to/from remote machine(s)
538@@ -80,9 +80,7 @@
539 }
540 }
541
542- args := []string{"-o", "StrictHostKeyChecking no", "-o", "PasswordAuthentication no"}
543- args = append(args, c.Args...)
544- cmd := exec.Command("scp", args...)
545+ cmd := ssh.ScpCommand(c.Args[0], c.Args[1], ssh.NoPasswordAuthentication)
546 cmd.Stdin = ctx.Stdin
547 cmd.Stdout = ctx.Stdout
548 cmd.Stderr = ctx.Stderr
549
550=== modified file 'cmd/juju/scp_test.go'
551--- cmd/juju/scp_test.go 2013-11-08 04:45:20 +0000
552+++ cmd/juju/scp_test.go 2013-12-03 06:15:09 +0000
553@@ -39,7 +39,7 @@
554 },
555 {
556 []string{"a", "b", "mysql/0"},
557- commonArgs + "a b mysql/0\n",
558+ commonArgs + "a b\n",
559 },
560 {
561 []string{"mongodb/1:foo", "mongodb/0:"},
562
563=== modified file 'cmd/juju/ssh.go'
564--- cmd/juju/ssh.go 2013-11-27 12:02:11 +0000
565+++ cmd/juju/ssh.go 2013-12-03 06:15:09 +0000
566@@ -6,7 +6,6 @@
567 import (
568 "errors"
569 "fmt"
570- "os/exec"
571 "time"
572
573 "launchpad.net/juju-core/cmd"
574@@ -16,6 +15,7 @@
575 "launchpad.net/juju-core/rpc"
576 "launchpad.net/juju-core/state/api"
577 "launchpad.net/juju-core/utils"
578+ "launchpad.net/juju-core/utils/ssh"
579 )
580
581 // SSHCommand is responsible for launching a ssh shell on a given unit or machine.
582@@ -82,9 +82,13 @@
583 if err != nil {
584 return err
585 }
586- args := []string{"-l", "ubuntu", "-t", "-o", "StrictHostKeyChecking no", "-o", "PasswordAuthentication no", host}
587- args = append(args, c.Args...)
588- cmd := exec.Command("ssh", args...)
589+ args := c.Args
590+ if len(args) > 0 && args[0] == "--" {
591+ // utils/ssh adds "--"; we will continue to accept
592+ // it from the CLI for backwards compatibility.
593+ args = args[1:]
594+ }
595+ cmd := ssh.Command("ubuntu@"+host, args, ssh.NoPasswordAuthentication, ssh.AllocateTTY)
596 cmd.Stdin = ctx.Stdin
597 cmd.Stdout = ctx.Stdout
598 cmd.Stderr = ctx.Stderr
599
600=== modified file 'cmd/juju/ssh_test.go'
601--- cmd/juju/ssh_test.go 2013-11-08 04:45:20 +0000
602+++ cmd/juju/ssh_test.go 2013-12-03 06:15:09 +0000
603@@ -60,7 +60,7 @@
604
605 const (
606 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
607- sshArgs = `-l ubuntu -t ` + commonArgs
608+ sshArgs = commonArgs + `-t `
609 )
610
611 var sshTests = []struct {
612@@ -69,30 +69,30 @@
613 }{
614 {
615 []string{"ssh", "0"},
616- sshArgs + "dummyenv-0.dns\n",
617+ sshArgs + "ubuntu@dummyenv-0.dns\n",
618 },
619 // juju ssh 0 'uname -a'
620 {
621 []string{"ssh", "0", "uname -a"},
622- sshArgs + "dummyenv-0.dns uname -a\n",
623+ sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
624 },
625 // juju ssh 0 -- uname -a
626 {
627 []string{"ssh", "0", "--", "uname", "-a"},
628- sshArgs + "dummyenv-0.dns -- uname -a\n",
629+ sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
630 },
631 // juju ssh 0 uname -a
632 {
633 []string{"ssh", "0", "uname", "-a"},
634- sshArgs + "dummyenv-0.dns uname -a\n",
635+ sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
636 },
637 {
638 []string{"ssh", "mysql/0"},
639- sshArgs + "dummyenv-0.dns\n",
640+ sshArgs + "ubuntu@dummyenv-0.dns\n",
641 },
642 {
643 []string{"ssh", "mongodb/1"},
644- sshArgs + "dummyenv-2.dns\n",
645+ sshArgs + "ubuntu@dummyenv-2.dns\n",
646 },
647 }
648
649
650=== modified file 'environs/bootstrap/state.go'
651--- environs/bootstrap/state.go 2013-11-18 04:53:44 +0000
652+++ environs/bootstrap/state.go 2013-12-03 06:15:09 +0000
653@@ -52,6 +52,11 @@
654 return storage.URL(StateFile)
655 }
656
657+// DeleteStateFile deletes the state file on the given storage.
658+func DeleteStateFile(storage storage.Storage) error {
659+ return storage.Remove(StateFile)
660+}
661+
662 // SaveState writes the given state to the given storage.
663 func SaveState(storage storage.StorageWriter, state *BootstrapState) error {
664 data, err := goyaml.Marshal(state)
665
666=== modified file 'environs/bootstrap/state_test.go'
667--- environs/bootstrap/state_test.go 2013-11-18 04:53:44 +0000
668+++ environs/bootstrap/state_test.go 2013-12-03 06:15:09 +0000
669@@ -6,6 +6,8 @@
670 import (
671 "bytes"
672 "io/ioutil"
673+ "os"
674+ "path/filepath"
675
676 gc "launchpad.net/gocheck"
677 "launchpad.net/goyaml"
678@@ -15,6 +17,7 @@
679 "launchpad.net/juju-core/environs/storage"
680 envtesting "launchpad.net/juju-core/environs/testing"
681 "launchpad.net/juju-core/instance"
682+ jc "launchpad.net/juju-core/testing/checkers"
683 "launchpad.net/juju-core/testing/testbase"
684 )
685
686@@ -48,6 +51,24 @@
687 c.Check(url, gc.Equals, expectedURL)
688 }
689
690+func (suite *StateSuite) TestDeleteStateFile(c *gc.C) {
691+ closer, stor, dataDir := envtesting.CreateLocalTestStorage(c)
692+ defer closer.Close()
693+
694+ err := bootstrap.DeleteStateFile(stor)
695+ c.Assert(err, gc.IsNil) // doesn't exist, juju don't care
696+
697+ _, err = bootstrap.CreateStateFile(stor)
698+ c.Assert(err, gc.IsNil)
699+ _, err = os.Stat(filepath.Join(dataDir, bootstrap.StateFile))
700+ c.Assert(err, gc.IsNil)
701+
702+ err = bootstrap.DeleteStateFile(stor)
703+ c.Assert(err, gc.IsNil)
704+ _, err = os.Stat(filepath.Join(dataDir, bootstrap.StateFile))
705+ c.Assert(err, jc.Satisfies, os.IsNotExist)
706+}
707+
708 func (suite *StateSuite) TestSaveStateWritesStateFile(c *gc.C) {
709 stor := suite.newStorage(c)
710 arch := "amd64"
711
712=== modified file 'environs/cloudinit.go'
713--- environs/cloudinit.go 2013-11-26 12:24:48 +0000
714+++ environs/cloudinit.go 2013-12-03 06:15:09 +0000
715@@ -133,9 +133,16 @@
716 for _, script := range additionalScripts {
717 cloudcfg.AddRunCmd(script)
718 }
719- err := cloudinit.Configure(cfg, cloudcfg)
720- if err != nil {
721- return nil, err
722+ // When bootstrapping, we only want to apt-get update/upgrade
723+ // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
724+ if cfg.StateServer {
725+ if err := cloudinit.ConfigureBasic(cfg, cloudcfg); err != nil {
726+ return nil, err
727+ }
728+ } else {
729+ if err := cloudinit.Configure(cfg, cloudcfg); err != nil {
730+ return nil, err
731+ }
732 }
733 data, err := cloudcfg.Render()
734 logger.Tracef("Generated cloud init:\n%s", string(data))
735
736=== modified file 'environs/cloudinit/cloudinit.go'
737--- environs/cloudinit/cloudinit.go 2013-11-26 12:24:48 +0000
738+++ environs/cloudinit/cloudinit.go 2013-12-03 06:15:09 +0000
739@@ -132,16 +132,60 @@
740 // Configure updates the provided cloudinit.Config with
741 // configuration to initialize a Juju machine agent.
742 func Configure(cfg *MachineConfig, c *cloudinit.Config) error {
743+ if err := ConfigureBasic(cfg, c); err != nil {
744+ return err
745+ }
746+ return ConfigureJuju(cfg, c)
747+}
748+
749+const cloudInitOutputLog = "/var/log/cloud-init-output.log"
750+
751+// ConfigureBasic updates the provided cloudinit.Config with
752+// basic configuration to initialise an OS image, such that it can
753+// be connected to via SSH, and log to a standard location.
754+//
755+// Any potentially failing operation should not be added to the
756+// configuration, but should instead be done in ConfigureJuju.
757+//
758+// Note: we don't do apt update/upgrade here so as not to have to wait on
759+// apt to finish when performing the second half of image initialisation.
760+// Doing it later brings the benefit of feedback in the face of errors,
761+// but adds to the running time of initialisation due to lack of activity
762+// between image bringup and start of agent installation.
763+func ConfigureBasic(cfg *MachineConfig, c *cloudinit.Config) error {
764+ c.AddSSHAuthorizedKeys(cfg.AuthorizedKeys)
765+ c.SetOutput(cloudinit.OutAll, "| tee -a "+cloudInitOutputLog, "")
766+ return nil
767+}
768+
769+// ConfigureJuju updates the provided cloudinit.Config with configuration
770+// to initialise a Juju machine agent.
771+func ConfigureJuju(cfg *MachineConfig, c *cloudinit.Config) error {
772 if err := verifyConfig(cfg); err != nil {
773 return err
774 }
775
776- // General options.
777+ // Initialise progress reporting. We need to do separately for runcmd
778+ // and (possibly, below) for bootcmd, as they may be run in different
779+ // shell sessions.
780+ initProgressCmd := cloudinit.InitProgressCmd()
781+ c.AddRunCmd(initProgressCmd)
782+
783+ // If we're doing synchronous bootstrap or manual provisioning, then
784+ // ConfigureBasic won't have been invoked; thus, the output log won't
785+ // have been set. We don't want to show the log to the user, so simply
786+ // append to the log file rather than teeing.
787+ if stdout, _ := c.Output(cloudinit.OutAll); stdout == "" {
788+ c.SetOutput(cloudinit.OutAll, ">> "+cloudInitOutputLog, "")
789+ c.AddBootCmd(initProgressCmd)
790+ c.AddBootCmd(cloudinit.LogProgressCmd("Logging to %s on remote host", cloudInitOutputLog))
791+ }
792+
793+ // Bring packages up-to-date.
794+ c.SetAptUpdate(true)
795 c.SetAptUpgrade(true)
796- c.SetAptUpdate(true)
797- c.SetOutput(cloudinit.OutAll, "| tee -a /var/log/cloud-init-output.log", "")
798- c.AddSSHAuthorizedKeys(cfg.AuthorizedKeys)
799
800+ // juju requires git for managing charm directories.
801 c.AddPackage("git")
802
803 c.AddScripts(
804@@ -149,17 +193,18 @@
805 fmt.Sprintf("mkdir -p %s", cfg.DataDir),
806 "mkdir -p /var/log/juju")
807
808- wgetCommand := "wget"
809- if cfg.DisableSSLHostnameVerification {
810- wgetCommand = "wget --no-check-certificate"
811- }
812 // Make a directory for the tools to live in, then fetch the
813 // tools and unarchive them into it.
814 var copyCmd string
815 if strings.HasPrefix(cfg.Tools.URL, fileSchemePrefix) {
816 copyCmd = fmt.Sprintf("cp %s $bin/tools.tar.gz", shquote(cfg.Tools.URL[len(fileSchemePrefix):]))
817 } else {
818+ wgetCommand := "wget"
819+ if cfg.DisableSSLHostnameVerification {
820+ wgetCommand = "wget --no-check-certificate"
821+ }
822 copyCmd = fmt.Sprintf("%s --no-verbose -O $bin/tools.tar.gz %s", wgetCommand, shquote(cfg.Tools.URL))
823+ c.AddRunCmd(cloudinit.LogProgressCmd("Fetching tools: %s", copyCmd))
824 }
825 toolsJson, err := json.Marshal(cfg.Tools)
826 if err != nil {
827@@ -231,6 +276,7 @@
828 if cons != "" {
829 cons = " --constraints " + shquote(cons)
830 }
831+ c.AddRunCmd(cloudinit.LogProgressCmd("Bootstrapping Juju machine agent"))
832 c.AddScripts(
833 fmt.Sprintf("echo %s > %s", shquote(cfg.StateInfoURL), BootstrapStateURLFile),
834 // The bootstrapping is always run with debug on.
835@@ -340,6 +386,7 @@
836 if err != nil {
837 return fmt.Errorf("cannot make cloud-init upstart script for the %s agent: %v", tag, err)
838 }
839+ c.AddRunCmd(cloudinit.LogProgressCmd("Starting Juju machine agent (%s)", name))
840 c.AddScripts(cmds...)
841 return nil
842 }
843@@ -361,6 +408,7 @@
844 if err != nil {
845 return fmt.Errorf("cannot make cloud-init upstart script for the state database: %v", err)
846 }
847+ c.AddRunCmd(cloudinit.LogProgressCmd("Starting MongoDB server (%s)", name))
848 c.AddScripts(cmds...)
849 return nil
850 }
851
852=== modified file 'environs/cloudinit/cloudinit_test.go'
853--- environs/cloudinit/cloudinit_test.go 2013-11-26 12:24:48 +0000
854+++ environs/cloudinit/cloudinit_test.go 2013-12-03 06:15:09 +0000
855@@ -88,9 +88,11 @@
856 setEnvConfig: true,
857 expectScripts: `
858 echo ENABLE_MONGODB="no" > /etc/default/mongodb
859+test -e /proc/self/fd/9 \|\| exec 9>&2
860 set -xe
861 mkdir -p /var/lib/juju
862 mkdir -p /var/log/juju
863+echo 'Fetching tools.*
864 bin='/var/lib/juju/tools/1\.2\.3-precise-amd64'
865 mkdir -p \$bin
866 wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-precise-amd64\.tgz'
867@@ -114,6 +116,7 @@
868 dd bs=1M count=1 if=/dev/zero of=/var/lib/juju/db/journal/prealloc\.0
869 dd bs=1M count=1 if=/dev/zero of=/var/lib/juju/db/journal/prealloc\.1
870 dd bs=1M count=1 if=/dev/zero of=/var/lib/juju/db/journal/prealloc\.2
871+echo 'Starting MongoDB server \(juju-db\)'.*
872 cat >> /etc/init/juju-db\.conf << 'EOF'\\ndescription "juju state database"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 65000 65000\\nlimit nproc 20000 20000\\n\\nexec /usr/bin/mongod --auth --dbpath=/var/lib/juju/db --sslOnNormalPorts --sslPEMKeyFile '/var/lib/juju/server\.pem' --sslPEMKeyPassword ignored --bind_ip 0\.0\.0\.0 --port 37017 --noprealloc --syslog --smallfiles\\nEOF\\n
873 start juju-db
874 mkdir -p '/var/lib/juju/agents/bootstrap'
875@@ -121,10 +124,12 @@
876 printf '%s\\n' '.*' > '/var/lib/juju/agents/bootstrap/format'
877 install -m 600 /dev/null '/var/lib/juju/agents/bootstrap/agent\.conf'
878 printf '%s\\n' '.*' > '/var/lib/juju/agents/bootstrap/agent\.conf'
879+echo 'Bootstrapping Juju machine agent'.*
880 echo 'some-url' > /tmp/provider-state-url
881 /var/lib/juju/tools/1\.2\.3-precise-amd64/jujud bootstrap-state --data-dir '/var/lib/juju' --env-config '[^']*' --constraints 'mem=2048M' --debug
882 rm -rf '/var/lib/juju/agents/bootstrap'
883 ln -s 1\.2\.3-precise-amd64 '/var/lib/juju/tools/machine-0'
884+echo 'Starting Juju machine agent \(jujud-machine-0\)'.*
885 cat >> /etc/init/jujud-machine-0\.conf << 'EOF'\\ndescription "juju machine-0 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-0/jujud machine --data-dir '/var/lib/juju' --machine-id 0 --debug >> /var/log/juju/machine-0\.log 2>&1\\nEOF\\n
886 start jujud-machine-0
887 `,
888@@ -193,9 +198,11 @@
889 SyslogPort: 514,
890 },
891 expectScripts: `
892+test -e /proc/self/fd/9 \|\| exec 9>&2
893 set -xe
894 mkdir -p /var/lib/juju
895 mkdir -p /var/log/juju
896+echo 'Fetching tools.*
897 bin='/var/lib/juju/tools/1\.2\.3-linux-amd64'
898 mkdir -p \$bin
899 wget --no-verbose -O \$bin/tools\.tar\.gz 'http://foo\.com/tools/releases/juju1\.2\.3-linux-amd64\.tgz'
900@@ -213,6 +220,7 @@
901 install -m 600 /dev/null '/var/lib/juju/agents/machine-99/agent\.conf'
902 printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/agent\.conf'
903 ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-99'
904+echo 'Starting Juju machine agent \(jujud-machine-99\)'.*
905 cat >> /etc/init/jujud-machine-99\.conf << 'EOF'\\ndescription "juju machine-99 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-99/jujud machine --data-dir '/var/lib/juju' --machine-id 99 --debug >> /var/log/juju/machine-99\.log 2>&1\\nEOF\\n
906 start jujud-machine-99
907 `,
908
909=== modified file 'environs/cloudinit_test.go'
910--- environs/cloudinit_test.go 2013-11-20 04:28:46 +0000
911+++ environs/cloudinit_test.go 2013-12-03 06:15:09 +0000
912@@ -139,7 +139,15 @@
913 c.Assert(err, gc.NotNil)
914 }
915
916-func (*CloudInitSuite) TestUserData(c *gc.C) {
917+func (s *CloudInitSuite) TestUserData(c *gc.C) {
918+ s.testUserData(c, false)
919+}
920+
921+func (s *CloudInitSuite) TestStateServerUserData(c *gc.C) {
922+ s.testUserData(c, true)
923+}
924+
925+func (*CloudInitSuite) testUserData(c *gc.C, stateServer bool) {
926 testJujuHome := c.MkDir()
927 defer config.SetJujuHome(config.SetJujuHome(testJujuHome))
928 tools := &tools.Tools{
929@@ -156,20 +164,25 @@
930 StateServerCert: []byte(testing.ServerCert),
931 StateServerKey: []byte(testing.ServerKey),
932 StateInfo: &state.Info{
933+ Addrs: []string{"127.0.0.1:1234"},
934 Password: "pw1",
935 CACert: []byte("CA CERT\n" + testing.CACert),
936+ Tag: "machine-10",
937 },
938 APIInfo: &api.Info{
939+ Addrs: []string{"127.0.0.1:1234"},
940 Password: "pw2",
941 CACert: []byte("CA CERT\n" + testing.CACert),
942+ Tag: "machine-10",
943 },
944 DataDir: environs.DataDir,
945 Config: envConfig,
946 StatePort: envConfig.StatePort(),
947 APIPort: envConfig.APIPort(),
948 SyslogPort: envConfig.SyslogPort(),
949- StateServer: true,
950+ StateServer: stateServer,
951 AgentEnvironment: map[string]string{agent.ProviderType: "dummy"},
952+ AuthorizedKeys: "wheredidileavemykeys",
953 }
954 script1 := "script1"
955 script2 := "script2"
956@@ -184,11 +197,32 @@
957 err = goyaml.Unmarshal(unzipped, &config)
958 c.Assert(err, gc.IsNil)
959
960- // Just check that the cloudinit config looks good.
961- c.Check(config["apt_upgrade"], gc.Equals, true)
962 // The scripts given to userData where added as the first
963 // commands to be run.
964 runCmd := config["runcmd"].([]interface{})
965 c.Check(runCmd[0], gc.Equals, script1)
966 c.Check(runCmd[1], gc.Equals, script2)
967+
968+ if stateServer {
969+ // The cloudinit config should have nothing but the basics:
970+ // SSH authorized keys, the additional runcmds, and log output.
971+ //
972+ // Note: the additional runcmds *do* belong here, at least
973+ // for MAAS. MAAS needs to configure and then bounce the
974+ // network interfaces, which would sever the SSH connection
975+ // in the synchronous bootstrap phase.
976+ c.Check(config, gc.DeepEquals, map[interface{}]interface{}{
977+ "output": map[interface{}]interface{}{
978+ "all": "| tee -a /var/log/cloud-init-output.log",
979+ },
980+ "runcmd": []interface{}{"script1", "script2"},
981+ "ssh_authorized_keys": []interface{}{"wheredidileavemykeys"},
982+ })
983+ } else {
984+ // Just check that the cloudinit config looks good,
985+ // and that there are more runcmds than the additional
986+ // ones we passed into ComposeUserData.
987+ c.Check(config["apt_upgrade"], gc.Equals, true)
988+ c.Check(len(runCmd) > 2, jc.IsTrue)
989+ }
990 }
991
992=== modified file 'environs/manual/bootstrap.go'
993--- environs/manual/bootstrap.go 2013-11-18 05:41:36 +0000
994+++ environs/manual/bootstrap.go 2013-12-03 06:15:09 +0000
995@@ -134,5 +134,5 @@
996 for k, v := range agentEnv {
997 mcfg.AgentEnvironment[k] = v
998 }
999- return ProvisionMachineAgent(args.Host, mcfg)
1000+ return provisionMachineAgent(args.Host, mcfg)
1001 }
1002
1003=== modified file 'environs/manual/detection.go'
1004--- environs/manual/detection.go 2013-10-09 06:54:15 +0000
1005+++ environs/manual/detection.go 2013-12-03 06:15:09 +0000
1006@@ -12,6 +12,7 @@
1007
1008 "launchpad.net/juju-core/instance"
1009 "launchpad.net/juju-core/utils"
1010+ "launchpad.net/juju-core/utils/ssh"
1011 )
1012
1013 // checkProvisionedScript is the script to run on the remote machine
1014@@ -25,7 +26,7 @@
1015 // exist on the host machine.
1016 func checkProvisioned(sshHost string) (bool, error) {
1017 logger.Infof("Checking if %s is already provisioned", sshHost)
1018- cmd := sshCommand(sshHost, fmt.Sprintf("bash -c %s", utils.ShQuote(checkProvisionedScript)))
1019+ cmd := ssh.Command(sshHost, []string{"bash", "-c", utils.ShQuote(checkProvisionedScript)})
1020 var stdout, stderr bytes.Buffer
1021 cmd.Stdout = &stdout
1022 cmd.Stderr = &stderr
1023@@ -52,7 +53,7 @@
1024 // The sshHost argument must be a hostname of the form [user@]host.
1025 func DetectSeriesAndHardwareCharacteristics(sshHost string) (hc instance.HardwareCharacteristics, series string, err error) {
1026 logger.Infof("Detecting series and characteristics on %s", sshHost)
1027- cmd := sshCommand(sshHost, "bash")
1028+ cmd := ssh.Command(sshHost, []string{"bash"})
1029 cmd.Stdin = bytes.NewBufferString(detectionScript)
1030 out, err := cmd.CombinedOutput()
1031 if err != nil {
1032
1033=== modified file 'environs/manual/provisioner.go'
1034--- environs/manual/provisioner.go 2013-11-26 14:06:20 +0000
1035+++ environs/manual/provisioner.go 2013-12-03 06:15:09 +0000
1036@@ -11,6 +11,8 @@
1037
1038 "launchpad.net/loggo"
1039
1040+ coreCloudinit "launchpad.net/juju-core/cloudinit"
1041+ "launchpad.net/juju-core/cloudinit/sshinit"
1042 "launchpad.net/juju-core/constraints"
1043 "launchpad.net/juju-core/environs"
1044 "launchpad.net/juju-core/environs/cloudinit"
1045@@ -89,7 +91,7 @@
1046 }
1047
1048 // Finally, provision the machine agent.
1049- err = ProvisionMachineAgent(args.Host, mcfg)
1050+ err = provisionMachineAgent(args.Host, mcfg)
1051 if err != nil {
1052 return machineId, err
1053 }
1054@@ -202,3 +204,14 @@
1055 }
1056 return mcfg, nil
1057 }
1058+
1059+func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig) error {
1060+ cloudcfg := coreCloudinit.New()
1061+ if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil {
1062+ return err
1063+ }
1064+ // Explicitly disabling apt_upgrade so as not to trample
1065+ // the target machine's existing configuration.
1066+ cloudcfg.SetAptUpgrade(false)
1067+ return sshinit.Configure(host, cloudcfg)
1068+}
1069
1070=== modified file 'environs/sshstorage/storage.go'
1071--- environs/sshstorage/storage.go 2013-10-10 01:31:43 +0000
1072+++ environs/sshstorage/storage.go 2013-12-03 06:15:09 +0000
1073@@ -20,6 +20,7 @@
1074
1075 coreerrors "launchpad.net/juju-core/errors"
1076 "launchpad.net/juju-core/utils"
1077+ "launchpad.net/juju-core/utils/ssh"
1078 )
1079
1080 // base64LineLength is the default line length for wrapping
1081@@ -44,12 +45,11 @@
1082 }
1083
1084 var sshCommand = func(host string, tty bool, command string) *exec.Cmd {
1085- sshArgs := []string{host}
1086+ var options []ssh.Option
1087 if tty {
1088- sshArgs = append(sshArgs, "-t")
1089+ options = append(options, ssh.AllocateTTY)
1090 }
1091- sshArgs = append(sshArgs, "--", command)
1092- return exec.Command("ssh", sshArgs...)
1093+ return ssh.Command(host, []string{command}, options...)
1094 }
1095
1096 type flockmode string
1097
1098=== added file 'environs/testing/bootstrap.go'
1099--- environs/testing/bootstrap.go 1970-01-01 00:00:00 +0000
1100+++ environs/testing/bootstrap.go 2013-12-03 06:15:09 +0000
1101@@ -0,0 +1,26 @@
1102+// Copyright 2013 Canonical Ltd.
1103+// Licensed under the AGPLv3, see LICENCE file for details.
1104+
1105+package testing
1106+
1107+import (
1108+ "launchpad.net/loggo"
1109+
1110+ "launchpad.net/juju-core/environs/cloudinit"
1111+ "launchpad.net/juju-core/instance"
1112+ "launchpad.net/juju-core/provider/common"
1113+ "launchpad.net/juju-core/testing/testbase"
1114+)
1115+
1116+var logger = loggo.GetLogger("juju.environs.testing")
1117+
1118+// DisableFinishBootstrap disables common.FinishBootstrap so that tests
1119+// do not attempt to SSH to non-existent machines. The result is a function
1120+// that restores finishBootstrap.
1121+func DisableFinishBootstrap() func() {
1122+ f := func(*common.BootstrapContext, instance.Instance, *cloudinit.MachineConfig) error {
1123+ logger.Warningf("provider/common.FinishBootstrap is disabled")
1124+ return nil
1125+ }
1126+ return testbase.PatchValue(&common.FinishBootstrap, f)
1127+}
1128
1129=== modified file 'provider/common/bootstrap.go'
1130--- provider/common/bootstrap.go 2013-11-18 04:53:44 +0000
1131+++ provider/common/bootstrap.go 2013-12-03 06:15:09 +0000
1132@@ -5,12 +5,21 @@
1133
1134 import (
1135 "fmt"
1136+ "net"
1137+ "os"
1138+ "os/signal"
1139+ "sync"
1140+ "time"
1141
1142 "launchpad.net/loggo"
1143+ "launchpad.net/tomb"
1144
1145+ coreCloudinit "launchpad.net/juju-core/cloudinit"
1146+ "launchpad.net/juju-core/cloudinit/sshinit"
1147 "launchpad.net/juju-core/constraints"
1148 "launchpad.net/juju-core/environs"
1149 "launchpad.net/juju-core/environs/bootstrap"
1150+ "launchpad.net/juju-core/environs/cloudinit"
1151 "launchpad.net/juju-core/instance"
1152 coretools "launchpad.net/juju-core/tools"
1153 )
1154@@ -20,11 +29,20 @@
1155 // Bootstrap is a common implementation of the Bootstrap method defined on
1156 // environs.Environ; we strongly recommend that this implementation be used
1157 // when writing a new provider.
1158-func Bootstrap(env environs.Environ, cons constraints.Value) error {
1159+func Bootstrap(env environs.Environ, cons constraints.Value) (err error) {
1160 // TODO make safe in the case of racing Bootstraps
1161 // If two Bootstraps are called concurrently, there's
1162 // no way to make sure that only one succeeds.
1163
1164+ // TODO(axw) 2013-11-22 #1237736
1165+ // Modify environs/Environ Bootstrap method signature
1166+ // to take a new context structure, which contains
1167+ // Std{in,out,err}, and interrupt signal handling.
1168+ ctx := BootstrapContext{Stderr: os.Stderr}
1169+
1170+ var inst instance.Instance
1171+ defer func() { handleBootstrapError(err, &ctx, inst, env) }()
1172+
1173 // Create an empty bootstrap state file so we can get its URL.
1174 // It will be updated with the instance id and hardware characteristics
1175 // after the bootstrap instance is started.
1176@@ -39,10 +57,13 @@
1177 return err
1178 }
1179
1180+ fmt.Fprintln(ctx.Stderr, "Launching instance")
1181 inst, hw, err := env.StartInstance(cons, selectedTools, machineConfig)
1182 if err != nil {
1183 return fmt.Errorf("cannot start bootstrap instance: %v", err)
1184 }
1185+ fmt.Fprintf(ctx.Stderr, " - %s\n", inst.Id())
1186+
1187 var characteristics []instance.HardwareCharacteristics
1188 if hw != nil {
1189 characteristics = []instance.HardwareCharacteristics{*hw}
1190@@ -54,14 +75,148 @@
1191 Characteristics: characteristics,
1192 })
1193 if err != nil {
1194- stoperr := env.StopInstances([]instance.Instance{inst})
1195- if stoperr != nil {
1196- // Failure upon failure. Log it, but return the original error.
1197+ return fmt.Errorf("cannot save state: %v", err)
1198+ }
1199+ return FinishBootstrap(&ctx, inst, machineConfig)
1200+}
1201+
1202+// handelBootstrapError cleans up after a failed bootstrap.
1203+func handleBootstrapError(err error, ctx *BootstrapContext, inst instance.Instance, env environs.Environ) {
1204+ if err == nil {
1205+ return
1206+ }
1207+ ctx.SetInterruptHandler(func() {
1208+ fmt.Fprintln(ctx.Stderr, "Cleaning up failed bootstrap")
1209+ })
1210+ if inst != nil {
1211+ fmt.Fprintln(ctx.Stderr, "Stopping instance...")
1212+ if stoperr := env.StopInstances([]instance.Instance{inst}); stoperr != nil {
1213 logger.Errorf("cannot stop failed bootstrap instance %q: %v", inst.Id(), stoperr)
1214- }
1215- return fmt.Errorf("cannot save state: %v", err)
1216- }
1217- return nil
1218+ } else {
1219+ // set to nil so we know we can safely delete the state file
1220+ inst = nil
1221+ }
1222+ }
1223+ // We only delete the bootstrap state file if either we didn't
1224+ // start an instance, or we managed to cleanly stop it.
1225+ if inst == nil {
1226+ if rmerr := bootstrap.DeleteStateFile(env.Storage()); rmerr != nil {
1227+ logger.Errorf("cannot delete bootstrap state file: %v", rmerr)
1228+ }
1229+ }
1230+ ctx.SetInterruptHandler(nil)
1231+}
1232+
1233+// FinishBootstrap completes the bootstrap process by connecting
1234+// to the instance via SSH and carrying out the cloud-config.
1235+//
1236+// Note: FinishBootstrap is exposed so it can be replaced for testing.
1237+var FinishBootstrap = func(ctx *BootstrapContext, inst instance.Instance, machineConfig *cloudinit.MachineConfig) error {
1238+ var t tomb.Tomb
1239+ ctx.SetInterruptHandler(func() { t.Killf("interrupted") })
1240+ dnsName, err := waitSSH(ctx, inst, &t)
1241+ if err != nil {
1242+ return err
1243+ }
1244+ // Bootstrap is synchronous, and will spawn a subprocess
1245+ // to complete the procedure. If the user hits Ctrl-C,
1246+ // SIGINT is sent to the foreground process attached to
1247+ // the terminal, which will be the ssh subprocess at that
1248+ // point.
1249+ ctx.SetInterruptHandler(func() {})
1250+ cloudcfg := coreCloudinit.New()
1251+ if err := cloudinit.ConfigureJuju(machineConfig, cloudcfg); err != nil {
1252+ return err
1253+ }
1254+ return sshinit.Configure("ubuntu@"+dnsName, cloudcfg)
1255+}
1256+
1257+// waitSSH waits for the instance to be assigned a DNS
1258+// entry, then waits until we can connect to it via SSH.
1259+func waitSSH(ctx *BootstrapContext, inst instance.Instance, t *tomb.Tomb) (dnsName string, err error) {
1260+ defer t.Done()
1261+
1262+ // Wait for a DNS name.
1263+ fmt.Fprint(ctx.Stderr, "Waiting for DNS name")
1264+ for {
1265+ fmt.Fprintf(ctx.Stderr, ".")
1266+ dnsName, err = inst.DNSName()
1267+ if err == nil {
1268+ break
1269+ } else if err != instance.ErrNoDNSName {
1270+ fmt.Fprintln(ctx.Stderr)
1271+ return "", t.Killf("getting DNS name: %v", err)
1272+ }
1273+ select {
1274+ case <-time.After(1 * time.Second):
1275+ case <-t.Dying():
1276+ fmt.Fprintln(ctx.Stderr)
1277+ return "", t.Err()
1278+ }
1279+ }
1280+ fmt.Fprintf(ctx.Stderr, "\n - %v\n", dnsName)
1281+
1282+ // Wait until we can open a connection to port 22.
1283+ fmt.Fprintf(ctx.Stderr, "Attempting to connect to %s:22", dnsName)
1284+ for {
1285+ fmt.Fprintf(ctx.Stderr, ".")
1286+ conn, err := net.DialTimeout("tcp", dnsName+":22", 5*time.Second)
1287+ if err == nil {
1288+ conn.Close()
1289+ fmt.Fprintln(ctx.Stderr)
1290+ return dnsName, nil
1291+ } else {
1292+ logger.Debugf("connection failed: %v", err)
1293+ }
1294+ select {
1295+ case <-time.After(5 * time.Second):
1296+ case <-t.Dying():
1297+ return "", t.Err()
1298+ }
1299+ }
1300+}
1301+
1302+// TODO(axw) move this to environs; see
1303+// comment near the top of common.Bootstrap.
1304+type BootstrapContext struct {
1305+ once sync.Once
1306+ handlerchan chan func()
1307+
1308+ Stderr *os.File
1309+}
1310+
1311+func (ctx *BootstrapContext) SetInterruptHandler(f func()) {
1312+ ctx.once.Do(ctx.initHandler)
1313+ ctx.handlerchan <- f
1314+}
1315+
1316+func (ctx *BootstrapContext) initHandler() {
1317+ ctx.handlerchan = make(chan func())
1318+ go ctx.handleInterrupt()
1319+}
1320+
1321+func (ctx *BootstrapContext) handleInterrupt() {
1322+ signalchan := make(chan os.Signal, 1)
1323+ var s chan os.Signal
1324+ var handler func()
1325+ for {
1326+ select {
1327+ case handler = <-ctx.handlerchan:
1328+ if handler == nil {
1329+ if s != nil {
1330+ signal.Stop(signalchan)
1331+ s = nil
1332+ }
1333+ } else {
1334+ if s == nil {
1335+ s = signalchan
1336+ signal.Notify(signalchan, os.Interrupt)
1337+ }
1338+ }
1339+ case <-s:
1340+ handler()
1341+ }
1342+ }
1343 }
1344
1345 // EnsureBootstrapTools finds tools, syncing with an external tools source as
1346
1347=== modified file 'provider/common/bootstrap_test.go'
1348--- provider/common/bootstrap_test.go 2013-11-18 04:53:44 +0000
1349+++ provider/common/bootstrap_test.go 2013-12-03 06:15:09 +0000
1350@@ -197,6 +197,9 @@
1351 return minimalConfig(c)
1352 }
1353
1354+ restore := envtesting.DisableFinishBootstrap()
1355+ defer restore()
1356+
1357 env := &mockEnviron{
1358 storage: stor,
1359 startInstance: startInstance,
1360
1361=== modified file 'provider/ec2/local_test.go'
1362--- provider/ec2/local_test.go 2013-11-18 05:41:36 +0000
1363+++ provider/ec2/local_test.go 2013-12-03 06:15:09 +0000
1364@@ -222,26 +222,28 @@
1365
1366 // check that the user data is configured to start zookeeper
1367 // and the machine and provisioning agents.
1368+ // check that the user data is configured to only configure
1369+ // authorized SSH keys and set the log output; everything
1370+ // else happens after the machine is brought up.
1371 inst := t.srv.ec2srv.Instance(string(insts[0].Id()))
1372 c.Assert(inst, gc.NotNil)
1373 bootstrapDNS, err := insts[0].DNSName()
1374 c.Assert(err, gc.IsNil)
1375 c.Assert(bootstrapDNS, gc.Not(gc.Equals), "")
1376-
1377 userData, err := utils.Gunzip(inst.UserData)
1378 c.Assert(err, gc.IsNil)
1379 c.Logf("first instance: UserData: %q", userData)
1380 var userDataMap map[interface{}]interface{}
1381 err = goyaml.Unmarshal(userData, &userDataMap)
1382 c.Assert(err, gc.IsNil)
1383- CheckPackage(c, userDataMap, "git", true)
1384- CheckScripts(c, userDataMap, "jujud bootstrap-state", true)
1385- // TODO check for provisioning agent
1386- // TODO check for machine agent
1387+ c.Assert(userDataMap, gc.DeepEquals, map[interface{}]interface{}{
1388+ "output": map[interface{}]interface{}{
1389+ "all": "| tee -a /var/log/cloud-init-output.log",
1390+ },
1391+ "ssh_authorized_keys": []interface{}{"my-keys"},
1392+ })
1393
1394- // check that a new instance will be started without
1395- // zookeeper, with a machine agent, and without a
1396- // provisioning agent.
1397+ // check that a new instance will be started with a machine agent
1398 inst1, hc := testing.AssertStartInstance(c, env, "1")
1399 c.Check(*hc.Arch, gc.Equals, "amd64")
1400 c.Check(*hc.Mem, gc.Equals, uint64(1740))
1401@@ -255,9 +257,11 @@
1402 userDataMap = nil
1403 err = goyaml.Unmarshal(userData, &userDataMap)
1404 c.Assert(err, gc.IsNil)
1405- CheckPackage(c, userDataMap, "zookeeperd", false)
1406+ CheckPackage(c, userDataMap, "git", true)
1407+ CheckPackage(c, userDataMap, "mongodb-server", false)
1408+ CheckScripts(c, userDataMap, "jujud bootstrap-state", false)
1409+ CheckScripts(c, userDataMap, "/var/lib/juju/agents/machine-1/agent.conf", true)
1410 // TODO check for provisioning agent
1411- // TODO check for machine agent
1412
1413 err = env.Destroy()
1414 c.Assert(err, gc.IsNil)
1415@@ -408,7 +412,9 @@
1416 ec2.UseTestInstanceTypeData(ec2.TestInstanceTypeCosts)
1417 ec2.UseTestRegionData(ec2.TestRegions)
1418 restoreTimeouts := envtesting.PatchAttemptStrategies(ec2.ShortAttempt, ec2.StorageAttempt)
1419+ restoreFinishBootstrap := envtesting.DisableFinishBootstrap()
1420 return func() {
1421+ restoreFinishBootstrap()
1422 restoreTimeouts()
1423 ec2.UseTestImageData(nil)
1424 ec2.UseTestInstanceTypeData(nil)
1425
1426=== modified file 'provider/maas/maas_test.go'
1427--- provider/maas/maas_test.go 2013-11-26 12:24:48 +0000
1428+++ provider/maas/maas_test.go 2013-12-03 06:15:09 +0000
1429@@ -33,6 +33,8 @@
1430 s.LoggingSuite.SetUpSuite(c)
1431 TestMAASObject := gomaasapi.NewTestMAAS("1.0")
1432 s.testMAASObject = TestMAASObject
1433+ restoreFinishBootstrap := envtesting.DisableFinishBootstrap()
1434+ s.AddSuiteCleanup(func(*gc.C) { restoreFinishBootstrap() })
1435 }
1436
1437 func (s *providerSuite) SetUpTest(c *gc.C) {
1438
1439=== modified file 'provider/openstack/local_test.go'
1440--- provider/openstack/local_test.go 2013-11-18 05:41:36 +0000
1441+++ provider/openstack/local_test.go 2013-12-03 06:15:09 +0000
1442@@ -167,6 +167,8 @@
1443 s.srv.start(c, s.cred)
1444 s.LiveTests.SetUpSuite(c)
1445 openstack.UseTestImageData(openstack.ImageMetadataStorage(s.Env), s.cred)
1446+ restoreFinishBootstrap := envtesting.DisableFinishBootstrap()
1447+ s.AddSuiteCleanup(func(*gc.C) { restoreFinishBootstrap() })
1448 }
1449
1450 func (s *localLiveSuite) TearDownSuite(c *gc.C) {
1451@@ -203,6 +205,8 @@
1452 func (s *localServerSuite) SetUpSuite(c *gc.C) {
1453 s.LoggingSuite.SetUpSuite(c)
1454 s.Tests.SetUpSuite(c)
1455+ restoreFinishBootstrap := envtesting.DisableFinishBootstrap()
1456+ s.AddSuiteCleanup(func(*gc.C) { restoreFinishBootstrap() })
1457 c.Logf("Running local tests")
1458 }
1459
1460@@ -740,6 +744,9 @@
1461 }
1462
1463 func (s *localHTTPSServerSuite) TestCanBootstrap(c *gc.C) {
1464+ restoreFinishBootstrap := envtesting.DisableFinishBootstrap()
1465+ defer restoreFinishBootstrap()
1466+
1467 // For testing, we create a storage instance to which is uploaded tools and image metadata.
1468 metadataStorage := openstack.MetadataStorage(s.env)
1469 url, err := metadataStorage.URL("")
1470
1471=== added directory 'utils/ssh'
1472=== renamed file 'environs/manual/ssh.go' => 'utils/ssh/ssh.go'
1473--- environs/manual/ssh.go 2013-09-12 02:04:05 +0000
1474+++ utils/ssh/ssh.go 2013-12-03 06:15:09 +0000
1475@@ -1,25 +1,45 @@
1476 // Copyright 2013 Canonical Ltd.
1477 // Licensed under the AGPLv3, see LICENCE file for details.
1478
1479-package manual
1480+package ssh
1481
1482 import (
1483 "os/exec"
1484 )
1485
1486-type sshOption []string
1487-
1488-var allocateTTY sshOption = []string{"-t"}
1489-
1490-// TODO(axw) 2013-09-12 bug #1224230
1491-// Move this to a common package for use in cmd/juju, and others.
1492-var commonSSHOptions = []string{"-o", "StrictHostKeyChecking no"}
1493-
1494-func sshCommand(host string, command string, options ...sshOption) *exec.Cmd {
1495- args := append([]string{}, commonSSHOptions...)
1496+type Option []string
1497+
1498+var (
1499+ commonOptions Option = []string{"-o", "StrictHostKeyChecking no"}
1500+
1501+ // AllocateTTY forces pseudo-TTY allocation, which is required,
1502+ // for example, for sudo password prompts on the target host.
1503+ AllocateTTY Option = []string{"-t"}
1504+
1505+ // NoPasswordAuthentication disallows password-based authentication.
1506+ NoPasswordAuthentication Option = []string{"-o", "PasswordAuthentication no"}
1507+)
1508+
1509+// Command initialises an os/exec.Cmd to execute the native ssh program.
1510+func Command(host string, command []string, options ...Option) *exec.Cmd {
1511+ args := append([]string{}, commonOptions...)
1512 for _, option := range options {
1513 args = append(args, option...)
1514 }
1515- args = append(args, host, "--", command)
1516+ args = append(args, host)
1517+ if len(command) > 0 {
1518+ args = append(args, "--")
1519+ args = append(args, command...)
1520+ }
1521 return exec.Command("ssh", args...)
1522 }
1523+
1524+// ScpCommand initialises an os/exec.Cmd to execute the native scp program.
1525+func ScpCommand(source, destination string, options ...Option) *exec.Cmd {
1526+ args := append([]string{}, commonOptions...)
1527+ for _, option := range options {
1528+ args = append(args, option...)
1529+ }
1530+ args = append(args, source, destination)
1531+ return exec.Command("scp", args...)
1532+}

Subscribers

People subscribed via source and target branches

to status/vote changes: