Merge lp:~axwalk/juju-core/synchronous-bootstrap into lp:~go-bot/juju-core/trunk
- synchronous-bootstrap
- Merge into trunk
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 |
Related bugs: |
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/
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
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/
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
Andrew Wilkins (axwalk) wrote : | # |
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:/
File cloudinit/
https:/
cloudinit/
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:/
File cmd/juju/
https:/
cmd/juju/
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:/
File environs/
https:/
environs/
to environs/manual.
environs/manual or cloudinit/sshinit?
https:/
File environs/
https:/
environs/
thren bounce the
typo
https:/
File provider/
https:/
provider/
Could we break this logic out into a separate function outside of
Bootstrap for clarity. The func could be
handleBootstrap
https:/
provider/
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:/
provider/
func(instance.
Does this need to be exported? It's only called from the above.
Andrew Wilkins (axwalk) wrote : | # |
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:/
File cloudinit/
https:/
cloudinit/
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:/
File cmd/juju/
https:/
cmd/juju/
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:/
File environs/
https:/
environs/
to environs/manual.
On 2013/11/22 01:54:00, wallyworld wrote:
> environs/manual or cloudinit/sshinit?
cloudinit/sshinit; fixed, thanks
https:/
File environs/
https:/
environs/
thren bounce the
On 2013/11/22 01:54:00, wallyworld wrote:
> typo
Done.
https:/
File provider/
https:/
provider/
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
> handleBootstrap
Done.
https:/
provider/
func(instance.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
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:/
File cloudinit/
https:/
cloudinit/
"(.|\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:/
cloudinit/
"(.|\n)*apt-get -y upgrade(.|\n)*", false)
ditto
https:/
File provider/
https:/
provider/
a bug number would be great here
https:/
provider/
instance...")
pedant alert - I think we should use case consistently for out messages.
"Stopping...." vs "cleaning up..." etc
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:/
> File cloudinit/
https:/
> cloudinit/
> "(.|\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:/
> cloudinit/
> "(.|\n)*apt-get -y upgrade(.|\n)*", false)
> ditto
https:/
> File provider/
https:/
> provider/
> a bug number would be great here
https:/
> provider/
> instance...")
> pedant alert - I think we should use case consistently for out
messages.
> "Stopping...." vs "cleaning up..." etc
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File cloudinit/
https:/
cloudinit/
"(.|\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:/
cloudinit/
"(.|\n)*apt-get -y upgrade(.|\n)*", false)
On 2013/11/22 06:37:50, wallyworld wrote:
> ditto
Done.
https:/
File provider/
https:/
provider/
On 2013/11/22 06:37:50, wallyworld wrote:
> a bug number would be great here
Done.
https:/
provider/
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.
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-
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?
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-
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (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.
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:/
File cloudinit/
https:/
cloudinit/
environs.
Can we stick an explicit "test" into the name somewhere please?
https:/
File environs/
https:/
environs/
exist, juju don't carea
s/carea/care/
https:/
File provider/
https:/
provider/
signal handling.
I'd like this to be an imminent followup, please :).
https:/
provider/
selectedTools, machineConfig)
Wouldn't this still work with :=, even given inst? Maybe I'm still
sluggish after lunch...
https:/
File provider/
https:/
provider/
func(*common.
*cloudinit.
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:/
File provider/
https:/
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.
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://
iEYEARECAAYFAlK
sSAAn2dC2nbwNAT
=X9FF
-----END PGP SIGNATURE-----
Andrew Wilkins (axwalk) wrote : | # |
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/
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:/
> File cloudinit/
https:/
> cloudinit/
environs.
> &testProvider{})
> Can we stick an explicit "test" into the name somewhere please?
https:/
> File environs/
https:/
> environs/
doesn't exist,
> juju don't carea
> s/carea/care/
https:/
> File provider/
https:/
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File cloudinit/
https:/
cloudinit/
environs.
On 2013/12/02 14:54:09, fwereade wrote:
> Can we stick an explicit "test" into the name somewhere please?
Done.
https:/
File environs/
https:/
environs/
exist, juju don't carea
On 2013/12/02 14:54:09, fwereade wrote:
> s/carea/care/
Done.
https:/
File provider/
https:/
provider/
signal handling.
On 2013/12/02 14:54:09, fwereade wrote:
> I'd like this to be an imminent followup, please :).
No problems.
https:/
provider/
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:/
File provider/
https:/
provider/
func(*common.
*cloudinit.
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:/
File provider/
https:/
provider/
"launchpad.
On 2013/12/02 14:54:09, fwereade wrote:
> Oh, a thought. Might DisableFinishBo
environs/testing?
Hmm yeah that's fine, and stops the package proliferation. Moved.
https:/
File utils/ssh/ssh.go (left):
https:/
utils/ssh/
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/
tweak utils/ssh a bit, and add an ScpCommand function.
Preview Diff
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 | +} |
Reviewers: mp+196058_ code.launchpad. net,
Message:
Please take a look.
Description:
Implement synchronous bootstrap
environs/manual has been further refactored to sshinit) .
split the "manual cloud-init over SSH" out into
a separate package (cloudinit/
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): options. go sshinit/ configure. go sshinit/ configure_ test.go sshinit/ suite_test. go bootstrap. go bootstrap/ state.go bootstrap/ state_test. go cloudinit. go cloudinit/ cloudinit. go cloudinit_ test.go manual/ bootstrap. go manual/ detection. go manual/ provisioner. go common/ bootstrap. go common/ bootstrap_ test.go ec2/local_ test.go maas/maas_ test.go openstack/ local_test. go
A [revision details]
M cloudinit/
M cloudinit/
M cloudinit/
A cloudinit/
M cmd/juju/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M provider/
M provider/
M provider/
M provider/
M provider/
M utils/ssh/ssh.go