Merge lp:~axwalk/juju-core/lp1261343-manual-ubuntu-user into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 2183
Proposed branch: lp:~axwalk/juju-core/lp1261343-manual-ubuntu-user
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1037 lines (+298/-211)
17 files modified
cloudinit/sshinit/configure.go (+2/-11)
environs/config/authkeys.go (+2/-2)
environs/config/config.go (+1/-1)
environs/manual/bootstrap.go (+21/-26)
environs/manual/bootstrap_test.go (+18/-49)
environs/manual/fakessh.go (+21/-2)
environs/manual/init.go (+72/-10)
environs/manual/init_test.go (+25/-6)
environs/manual/provisioner.go (+23/-17)
environs/manual/provisioner_test.go (+25/-6)
environs/sshstorage/storage.go (+4/-17)
environs/sshstorage/storage_test.go (+4/-6)
provider/common/bootstrap.go (+3/-3)
provider/null/config.go (+0/-8)
provider/null/config_test.go (+0/-2)
provider/null/environ.go (+40/-11)
provider/null/environ_test.go (+37/-34)
To merge this branch: bzr merge lp:~axwalk/juju-core/lp1261343-manual-ubuntu-user
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+200365@code.launchpad.net

Commit message

environs/manual: add ubuntu user

Manual bootstrap and provisioning now add the
ubuntu user (if it doesn't exist), update its
authorized_keys, and enables passwordless sudo
for it. This is somewhat invasive, so we will
have to call it out clearly in the documentation.

All ssh-related things other than the user
initialisation can now do away with the special
logic for handling sudo interactions. We don't
pass stdin/stdout unless we need them; we don't
allocate a PTY unless we need it; we don't allow
password authentication unless we need it.

Fixes #1261343

https://codereview.appspot.com/47390043/

Description of the change

environs/manual: add ubuntu user

Manual bootstrap and provisioning now add the
ubuntu user (if it doesn't exist), update its
authorized_keys, and enables passwordless sudo
for it. This is somewhat invasive, so we will
have to call it out clearly in the documentation.

All ssh-related things other than the user
initialisation can now do away with the special
logic for handling sudo interactions. We don't
pass stdin/stdout unless we need them; we don't
allocate a PTY unless we need it; we don't allow
password authentication unless we need it.

Fixes #1261343

https://codereview.appspot.com/47390043/

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

Reviewers: mp+200365_code.launchpad.net,

Message:
Please take a look.

Description:
environs/manual: add ubuntu user

Manual bootstrap and provisioning now add the
ubuntu user (if it doesn't exist), update its
authorized_keys, and enables passwordless sudo
for it. This is somewhat invasive, so we will
have to call it out clearly in the documentation.

All ssh-related things other than the user
initialisation can now do away with the special
logic for handling sudo interactions. We don't
pass stdin/stdout unless we need them; we don't
allocate a PTY unless we need it; we don't allow
password authentication unless we need it.

https://code.launchpad.net/~axwalk/juju-core/lp1261343-manual-ubuntu-user/+merge/200365

(do not edit description out of merge proposal)

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

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

LGTM, definitely a move in a nice direction. Just trivials and
longer-term discussion points.

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

https://codereview.appspot.com/47390043/diff/20001/environs/manual/bootstrap.go#newcode32
environs/manual/bootstrap.go:32: PossibleTools tools.List
Not germane to this CL, but worth a discussion: we need to fix this
tools nonsense. Now we have manual bootstrap, is there any compelling
reason to worry about tools availability until we're connected to that
machine? Looking them all up is kinda tedious and expensive, and we keep
on doing it again and again, and -- worse -- we always do it from the
client (which may not see the same networks as does the bootstrapped
machine, and might be completely useless).

We *will* ofc still need *some* way to push tools up from a local
directory, but that shouldn't be too hard really, right?

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

https://codereview.appspot.com/47390043/diff/20001/environs/manual/provisioner_test.go#newcode95
environs/manual/provisioner_test.go:95: defer installFakeSSH(c, "", nil,
0)() // InitUbuntuUser
This stuff is both a bit repetitive and a bit opaque. Worth wrapping it
up into something a bit more literate maybe?

https://codereview.appspot.com/47390043/diff/20001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/47390043/diff/20001/provider/null/environ.go#newcode53
provider/null/environ.go:53: ubuntuUserInit bool
Inited?

https://codereview.appspot.com/47390043/

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

Please take a look.

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

https://codereview.appspot.com/47390043/diff/20001/environs/manual/bootstrap.go#newcode32
environs/manual/bootstrap.go:32: PossibleTools tools.List
On 2014/01/03 13:07:55, fwereade wrote:
> Not germane to this CL, but worth a discussion: we need to fix this
tools
> nonsense. Now we have manual bootstrap, is there any compelling reason
to worry
> about tools availability until we're connected to that machine?
Looking them all
> up is kinda tedious and expensive, and we keep on doing it again and
again, and
> -- worse -- we always do it from the client (which may not see the
same networks
> as does the bootstrapped machine, and might be completely useless).

> We *will* ofc still need *some* way to push tools up from a local
directory, but
> that shouldn't be too hard really, right?

The tools lookup for the null provider happens immediately before we
check if the machine is provisioned, and then connect. I don't think
there'd be much to gain in terms of moving it in time. As for moving
from client to the server...

There is possibly one compelling reason for doing it from the client:
supporting environments with limited egress. As it is now, manual
bootstrap effectively SCPs the tools to the bootstrap node, SSHs in and
then runs the installation script. We could require those environments
to have an internally-accessible tools repository, but this seems quite
onerous.

The simplestreams/tools lookup code is all part of the Go binary, so how
would it be done from the server anyway?

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

https://codereview.appspot.com/47390043/diff/20001/environs/manual/provisioner_test.go#newcode95
environs/manual/provisioner_test.go:95: defer installFakeSSH(c, "", nil,
0)() // InitUbuntuUser
On 2014/01/03 13:07:55, fwereade wrote:
> This stuff is both a bit repetitive and a bit opaque. Worth wrapping
it up into
> something a bit more literate maybe?

I cleaned up these couple, and one in bootstrap_test.go. There's some
others in init_test.go that could probably be cleaned up, but I've not
come up with a good idea how to do that just yet.

https://codereview.appspot.com/47390043/diff/20001/provider/null/environ.go
File provider/null/environ.go (right):

https://codereview.appspot.com/47390043/diff/20001/provider/null/environ.go#newcode53
provider/null/environ.go:53: ubuntuUserInit bool
On 2014/01/03 13:07:55, fwereade wrote:
> Inited?

Done.

https://codereview.appspot.com/47390043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cloudinit/sshinit/configure.go'
2--- cloudinit/sshinit/configure.go 2013-12-19 09:15:40 +0000
3+++ cloudinit/sshinit/configure.go 2014-01-06 07:18:52 +0000
4@@ -25,13 +25,6 @@
5 // Config is the cloudinit config to carry out.
6 Config *cloudinit.Config
7
8- // Stdin is required to respond to sudo prompts,
9- // and must be a terminal (except in tests)
10- Stdin io.Reader
11-
12- // Stdout is required to present sudo prompts to the user.
13- Stdout io.Writer
14-
15 // Stderr is required to present bootstrap progress to the user.
16 Stderr io.Writer
17 }
18@@ -48,12 +41,10 @@
19 script = fmt.Sprintf(`F=$(mktemp); echo %s | base64 -d > $F; . $F`, scriptBase64)
20 cmd := ssh.Command(
21 params.Host,
22- []string{"sudo", fmt.Sprintf("bash -c '%s'", script)},
23- ssh.AllocateTTY,
24+ []string{"sudo", "-n", fmt.Sprintf("bash -c '%s'", script)},
25+ ssh.NoPasswordAuthentication,
26 )
27- cmd.Stdout = params.Stdout
28 cmd.Stderr = params.Stderr
29- cmd.Stdin = params.Stdin
30 return cmd.Run()
31 }
32
33
34=== modified file 'environs/config/authkeys.go'
35--- environs/config/authkeys.go 2013-08-28 20:37:53 +0000
36+++ environs/config/authkeys.go 2014-01-06 07:18:52 +0000
37@@ -26,14 +26,14 @@
38 return f
39 }
40
41-// authorizedKeys implements the standard juju behaviour for finding
42+// ReadAuthorizedKeys implements the standard juju behaviour for finding
43 // authorized_keys. It returns a set of keys in in authorized_keys format
44 // (see sshd(8) for a description). If path is non-empty, it names the
45 // file to use; otherwise the user's .ssh directory will be searched.
46 // Home directory expansion will be performed on the path if it starts with
47 // a ~; if the expanded path is relative, it will be interpreted relative
48 // to $HOME/.ssh.
49-func readAuthorizedKeys(path string) (string, error) {
50+func ReadAuthorizedKeys(path string) (string, error) {
51 var files []string
52 if path == "" {
53 files = []string{"id_dsa.pub", "id_rsa.pub", "identity.pub"}
54
55=== modified file 'environs/config/config.go'
56--- environs/config/config.go 2013-12-19 00:38:03 +0000
57+++ environs/config/config.go 2014-01-06 07:18:52 +0000
58@@ -147,7 +147,7 @@
59 keys := c.asString("authorized-keys")
60 if path != "" || keys == "" {
61 var err error
62- c.m["authorized-keys"], err = readAuthorizedKeys(path)
63+ c.m["authorized-keys"], err = ReadAuthorizedKeys(path)
64 if err != nil {
65 return err
66 }
67
68=== modified file 'environs/manual/bootstrap.go'
69--- environs/manual/bootstrap.go 2013-12-20 03:12:26 +0000
70+++ environs/manual/bootstrap.go 2014-01-06 07:18:52 +0000
71@@ -27,16 +27,11 @@
72 }
73
74 type BootstrapArgs struct {
75- Host string
76- DataDir string
77- Environ LocalStorageEnviron
78- PossibleTools tools.List
79- Context environs.BootstrapContext
80-
81- // If series and hardware characteristics
82- // are known ahead of time, they can be
83- // set here and Bootstrap will not attempt
84- // to detect them again.
85+ Host string
86+ DataDir string
87+ Environ LocalStorageEnviron
88+ PossibleTools tools.List
89+ Context environs.BootstrapContext
90 Series string
91 HardwareCharacteristics *instance.HardwareCharacteristics
92 }
93@@ -48,6 +43,9 @@
94 // NewManualBootstrapEnviron wraps a LocalStorageEnviron with another which
95 // overrides the Bootstrap method; when Bootstrap is invoked, the specified
96 // host will be manually bootstrapped.
97+//
98+// InitUbuntuUser is expected to have been executed successfully against
99+// the host being bootstrapped.
100 func Bootstrap(args BootstrapArgs) (err error) {
101 if args.Host == "" {
102 return errors.New("host argument is empty")
103@@ -58,6 +56,15 @@
104 if args.DataDir == "" {
105 return errors.New("data-dir argument is empty")
106 }
107+ if args.Series == "" {
108+ return errors.New("series argument is empty")
109+ }
110+ if args.HardwareCharacteristics == nil {
111+ return errors.New("hardware characteristics argument is empty")
112+ }
113+ if len(args.PossibleTools) == 0 {
114+ return errors.New("possible tools is empty")
115+ }
116
117 provisioned, err := checkProvisioned(args.Host)
118 if err != nil {
119@@ -67,23 +74,11 @@
120 return ErrProvisioned
121 }
122
123- var series string
124- var hc instance.HardwareCharacteristics
125- if args.Series != "" && args.HardwareCharacteristics != nil {
126- series = args.Series
127- hc = *args.HardwareCharacteristics
128- } else {
129- hc, series, err = DetectSeriesAndHardwareCharacteristics(args.Host)
130- if err != nil {
131- return fmt.Errorf("error detecting hardware characteristics: %v", err)
132- }
133- }
134-
135 // Filter tools based on detected series/arch.
136 logger.Infof("Filtering possible tools: %v", args.PossibleTools)
137 possibleTools, err := args.PossibleTools.Match(tools.Filter{
138- Arch: *hc.Arch,
139- Series: series,
140+ Arch: *args.HardwareCharacteristics.Arch,
141+ Series: args.Series,
142 })
143 if err != nil {
144 return err
145@@ -96,7 +91,7 @@
146 bootstrapStorage,
147 &bootstrap.BootstrapState{
148 StateInstances: []instance.Id{BootstrapInstanceId},
149- Characteristics: []instance.HardwareCharacteristics{hc},
150+ Characteristics: []instance.HardwareCharacteristics{*args.HardwareCharacteristics},
151 },
152 )
153 if err != nil {
154@@ -141,5 +136,5 @@
155 for k, v := range agentEnv {
156 mcfg.AgentEnvironment[k] = v
157 }
158- return provisionMachineAgent(args.Host, mcfg, args.Context.Stdin(), args.Context.Stdout(), args.Context.Stderr())
159+ return provisionMachineAgent(args.Host, mcfg, args.Context.Stderr())
160 }
161
162=== modified file 'environs/manual/bootstrap_test.go'
163--- environs/manual/bootstrap_test.go 2013-12-20 02:38:56 +0000
164+++ environs/manual/bootstrap_test.go 2014-01-06 07:18:52 +0000
165@@ -71,12 +71,17 @@
166 c.Assert(err, gc.IsNil)
167 toolsList, err := tools.FindBootstrapTools(s.Conn.Environ, tools.BootstrapToolsParams{})
168 c.Assert(err, gc.IsNil)
169+ arch := "amd64"
170 return BootstrapArgs{
171 Host: hostname,
172 DataDir: "/var/lib/juju",
173 Environ: s.env,
174 PossibleTools: toolsList,
175- Context: envtesting.NewBootstrapContext(coretesting.Context(c)),
176+ Series: "precise",
177+ HardwareCharacteristics: &instance.HardwareCharacteristics{
178+ Arch: &arch,
179+ },
180+ Context: envtesting.NewBootstrapContext(coretesting.Context(c)),
181 }
182 }
183
184@@ -84,7 +89,7 @@
185 args := s.getArgs(c)
186 args.Host = "ubuntu@" + args.Host
187
188- defer fakeSSH{Series: s.Conn.Environ.Config().DefaultSeries()}.install(c).Restore()
189+ defer fakeSSH{SkipDetection: true}.install(c).Restore()
190 err := Bootstrap(args)
191 c.Assert(err, gc.IsNil)
192
193@@ -99,60 +104,24 @@
194 // Do it all again; this should work, despite the fact that
195 // there's a bootstrap state file. Existence for that is
196 // checked in general bootstrap code (environs/bootstrap).
197- defer fakeSSH{Series: s.Conn.Environ.Config().DefaultSeries()}.install(c).Restore()
198+ defer fakeSSH{SkipDetection: true}.install(c).Restore()
199 err = Bootstrap(args)
200 c.Assert(err, gc.IsNil)
201
202 // We *do* check that the machine has no juju* upstart jobs, though.
203- defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 0).Restore()
204+ defer fakeSSH{
205+ Provisioned: true,
206+ SkipDetection: true,
207+ SkipProvisionAgent: true,
208+ }.install(c).Restore()
209 err = Bootstrap(args)
210 c.Assert(err, gc.Equals, ErrProvisioned)
211 }
212
213-func (s *bootstrapSuite) TestBootstrapSkipDetection(c *gc.C) {
214- args := s.getArgs(c)
215- hc, err := instance.ParseHardware("arch=amd64")
216- c.Assert(err, gc.IsNil)
217-
218- type test struct {
219- series string
220- hc *instance.HardwareCharacteristics
221- }
222- tests := []test{{
223- series: "",
224- hc: nil,
225- }, {
226- series: "precise",
227- hc: nil,
228- }, {
229- series: "",
230- hc: &hc,
231- }, {
232- series: "precise",
233- hc: &hc,
234- }}
235-
236- for i, test := range tests {
237- c.Logf("test %d: %+v", i, test)
238- args.Series = test.series
239- args.HardwareCharacteristics = test.hc
240- var ssh fakeSSH
241- if args.Series != "" && args.HardwareCharacteristics != nil {
242- // If neither series nor hardware-characteristics
243- // is missing, detection is skipped.
244- ssh.SkipDetection = true
245- }
246- defer ssh.install(c).Restore()
247- err = Bootstrap(args)
248- c.Assert(err, gc.IsNil)
249- }
250-}
251-
252 func (s *bootstrapSuite) TestBootstrapScriptFailure(c *gc.C) {
253 args := s.getArgs(c)
254 args.Host = "ubuntu@" + args.Host
255- series := s.Conn.Environ.Config().DefaultSeries()
256- defer fakeSSH{Series: series, ProvisionAgentExitCode: 1}.install(c).Restore()
257+ defer fakeSSH{SkipDetection: true, ProvisionAgentExitCode: 1}.install(c).Restore()
258 err := Bootstrap(args)
259 c.Assert(err, gc.NotNil)
260
261@@ -184,12 +153,12 @@
262 // Empty tools list.
263 args := s.getArgs(c)
264 args.PossibleTools = nil
265- series := s.Conn.Environ.Config().DefaultSeries()
266- defer fakeSSH{Series: series, SkipProvisionAgent: true}.install(c).Restore()
267- c.Assert(Bootstrap(args), gc.ErrorMatches, "no matching tools available")
268+ defer fakeSSH{SkipDetection: true, SkipProvisionAgent: true}.install(c).Restore()
269+ c.Assert(Bootstrap(args), gc.ErrorMatches, "possible tools is empty")
270
271 // Non-empty list, but none that match the series/arch.
272- defer fakeSSH{Series: "edgy", SkipProvisionAgent: true}.install(c).Restore()
273 args = s.getArgs(c)
274+ args.Series = "edgy"
275+ defer fakeSSH{SkipDetection: true, SkipProvisionAgent: true}.install(c).Restore()
276 c.Assert(Bootstrap(args), gc.ErrorMatches, "no matching tools available")
277 }
278
279=== modified file 'environs/manual/fakessh.go'
280--- environs/manual/fakessh.go 2013-11-18 05:41:36 +0000
281+++ environs/manual/fakessh.go 2014-01-06 07:18:52 +0000
282@@ -91,16 +91,28 @@
283 Series string
284 Arch string
285
286+ // Provisioned should be set to true if the fakeSSH script
287+ // should respond to checkProvisioned with a non-empty result.
288+ Provisioned bool
289+
290+ // exit code for the checkProvisioned script.
291+ CheckProvisionedExitCode int
292+
293 // exit code for the machine agent provisioning script.
294 ProvisionAgentExitCode int
295
296+ // InitUbuntuUser should be set to true if the fakeSSH script
297+ // should respond to an attempt to initialise the ubuntu user.
298+ InitUbuntuUser bool
299+
300 // there are conditions other than error in the above
301 // that might cause provisioning to not go ahead, such
302 // as tools being missing.
303 SkipProvisionAgent bool
304
305 // detection will be skipped if the series/hardware were
306- // detected ahead of time.
307+ // detected ahead of time. This should always be set to
308+ // true when testing Bootstrap.
309 SkipDetection bool
310 }
311
312@@ -118,6 +130,13 @@
313 if !r.SkipDetection {
314 restore.Add(installDetectionFakeSSH(c, r.Series, r.Arch))
315 }
316- add("", nil, 0) // checkProvisioned
317+ var checkProvisionedOutput interface{}
318+ if r.Provisioned {
319+ checkProvisionedOutput = "/etc/init/jujud-machine-0.conf"
320+ }
321+ add("", checkProvisionedOutput, r.CheckProvisionedExitCode)
322+ if r.InitUbuntuUser {
323+ add("", nil, 0)
324+ }
325 return restore
326 }
327
328=== renamed file 'environs/manual/detection.go' => 'environs/manual/init.go'
329--- environs/manual/detection.go 2013-12-19 06:39:26 +0000
330+++ environs/manual/init.go 2014-01-06 07:18:52 +0000
331@@ -6,6 +6,7 @@
332 import (
333 "bytes"
334 "fmt"
335+ "io"
336 "regexp"
337 "strconv"
338 "strings"
339@@ -24,9 +25,9 @@
340
341 // checkProvisioned checks if any juju upstart jobs already
342 // exist on the host machine.
343-func checkProvisioned(sshHost string) (bool, error) {
344- logger.Infof("Checking if %s is already provisioned", sshHost)
345- cmd := ssh.Command(sshHost, []string{"bash", "-c", utils.ShQuote(checkProvisionedScript)})
346+func checkProvisioned(host string) (bool, error) {
347+ logger.Infof("Checking if %s is already provisioned", host)
348+ cmd := ssh.Command("ubuntu@"+host, []string{"bash", "-c", utils.ShQuote(checkProvisionedScript)}, ssh.NoPasswordAuthentication)
349 var stdout, stderr bytes.Buffer
350 cmd.Stdout = &stdout
351 cmd.Stderr = &stderr
352@@ -39,9 +40,9 @@
353 output := strings.TrimSpace(stdout.String())
354 provisioned := len(output) > 0
355 if provisioned {
356- logger.Infof("%s is already provisioned [%q]", sshHost, output)
357+ logger.Infof("%s is already provisioned [%q]", host, output)
358 } else {
359- logger.Infof("%s is not provisioned", sshHost)
360+ logger.Infof("%s is not provisioned", host)
361 }
362 return provisioned, nil
363 }
364@@ -49,11 +50,9 @@
365 // DetectSeriesAndHardwareCharacteristics detects the OS
366 // series and hardware characteristics of the remote machine
367 // by connecting to the machine and executing a bash script.
368-//
369-// The sshHost argument must be a hostname of the form [user@]host.
370-func DetectSeriesAndHardwareCharacteristics(sshHost string) (hc instance.HardwareCharacteristics, series string, err error) {
371- logger.Infof("Detecting series and characteristics on %s", sshHost)
372- cmd := ssh.Command(sshHost, []string{"bash"})
373+func DetectSeriesAndHardwareCharacteristics(host string) (hc instance.HardwareCharacteristics, series string, err error) {
374+ logger.Infof("Detecting series and characteristics on %s", host)
375+ cmd := ssh.Command("ubuntu@"+host, []string{"bash"}, ssh.NoPasswordAuthentication)
376 var stdout, stderr bytes.Buffer
377 cmd.Stdout = &stdout
378 cmd.Stderr = &stderr
379@@ -136,3 +135,66 @@
380 uname -m
381 grep MemTotal /proc/meminfo
382 cat /proc/cpuinfo`
383+
384+// InitUbuntuUser adds the ubuntu user if it doesn't
385+// already exist, updates its ~/.ssh/authorized_keys,
386+// and enables passwordless sudo for it.
387+//
388+// InitUbuntuUser will initially attempt to login as
389+// the ubuntu user, and verify that passwordless sudo
390+// is enabled; only if this is false will there be an
391+// attempt with the specified login.
392+//
393+// authorizedKeys may be empty, in which case the file
394+// will be created and left empty.
395+//
396+// stdin and stdout will be used for remote sudo prompts,
397+// if the ubuntu user must be created/updated.
398+func InitUbuntuUser(host, login, authorizedKeys string, stdin io.Reader, stdout io.Writer) error {
399+ logger.Infof("initialising %q, user %q", host, login)
400+
401+ // To avoid unnecessary prompting for the specified login,
402+ // initUbuntuUser will first attempt to ssh to the machine
403+ // as "ubuntu" with password authentication disabled, and
404+ // ensure that it can use sudo without a password.
405+ //
406+ // Note that we explicitly do not allocate a PTY, so we
407+ // get a failure if sudo prompts.
408+ cmd := ssh.Command("ubuntu@"+host, []string{"sudo", "-n", "true"}, ssh.NoPasswordAuthentication)
409+ if cmd.Run() == nil {
410+ return nil
411+ }
412+
413+ // Failed to login as ubuntu (or passwordless sudo is not enabled).
414+ // Use specified login, and execute the initUbuntuScript below.
415+ if login != "" {
416+ host = login + "@" + host
417+ }
418+ script := fmt.Sprintf(initUbuntuScript, utils.ShQuote(authorizedKeys))
419+ cmd = ssh.Command(host, []string{"sudo", "bash -c " + utils.ShQuote(script)}, ssh.AllocateTTY)
420+ var stderr bytes.Buffer
421+ cmd.Stdin = stdin
422+ cmd.Stdout = stdout // for sudo prompt
423+ cmd.Stderr = &stderr
424+ if err := cmd.Run(); err != nil {
425+ if stderr.Len() != 0 {
426+ err = fmt.Errorf("%v (%v)", err, strings.TrimSpace(stderr.String()))
427+ }
428+ return err
429+ }
430+ return nil
431+}
432+
433+const initUbuntuScript = `
434+set -e
435+(id ubuntu &> /dev/null) || useradd -m ubuntu
436+umask 0077
437+temp=$(mktemp)
438+echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp
439+install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu
440+rm $temp
441+su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys'
442+export authorized_keys=%s
443+if [ ! -z "$authorized_keys" ]; then
444+ su ubuntu -c 'printf "%%s\n" "$authorized_keys" >> ~/.ssh/authorized_keys'
445+fi`
446
447=== renamed file 'environs/manual/detection_test.go' => 'environs/manual/init_test.go'
448--- environs/manual/detection_test.go 2013-12-19 06:39:26 +0000
449+++ environs/manual/init_test.go 2014-01-06 07:18:52 +0000
450@@ -12,13 +12,13 @@
451 "launchpad.net/juju-core/testing/testbase"
452 )
453
454-type detectionSuite struct {
455+type initialisationSuite struct {
456 testbase.LoggingSuite
457 }
458
459-var _ = gc.Suite(&detectionSuite{})
460+var _ = gc.Suite(&initialisationSuite{})
461
462-func (s *detectionSuite) TestDetectSeries(c *gc.C) {
463+func (s *initialisationSuite) TestDetectSeries(c *gc.C) {
464 response := strings.Join([]string{
465 "edgy",
466 "armv4",
467@@ -31,7 +31,7 @@
468 c.Assert(series, gc.Equals, "edgy")
469 }
470
471-func (s *detectionSuite) TestDetectionError(c *gc.C) {
472+func (s *initialisationSuite) TestDetectionError(c *gc.C) {
473 scriptResponse := strings.Join([]string{
474 "edgy",
475 "armv4",
476@@ -50,7 +50,7 @@
477 c.Assert(hc.String(), gc.Equals, "arch=arm cpu-cores=1 mem=4M")
478 }
479
480-func (s *detectionSuite) TestDetectHardwareCharacteristics(c *gc.C) {
481+func (s *initialisationSuite) TestDetectHardwareCharacteristics(c *gc.C) {
482 tests := []struct {
483 summary string
484 scriptResponse []string
485@@ -112,7 +112,7 @@
486 }
487 }
488
489-func (s *detectionSuite) TestCheckProvisioned(c *gc.C) {
490+func (s *initialisationSuite) TestCheckProvisioned(c *gc.C) {
491 defer installFakeSSH(c, "", "", 0)()
492 provisioned, err := checkProvisioned("example.com")
493 c.Assert(err, gc.IsNil)
494@@ -135,3 +135,22 @@
495 _, err = checkProvisioned("example.com")
496 c.Assert(err, gc.ErrorMatches, "exit status 255 \\(non-empty-stderr\\)")
497 }
498+
499+func (s *initialisationSuite) TestInitUbuntuUserNonExisting(c *gc.C) {
500+ defer installFakeSSH(c, "", "", 0)() // successful creation of ubuntu user
501+ defer installFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
502+ err := InitUbuntuUser("testhost", "testuser", "", nil, nil)
503+ c.Assert(err, gc.IsNil)
504+}
505+
506+func (s *initialisationSuite) TestInitUbuntuUserExisting(c *gc.C) {
507+ defer installFakeSSH(c, "", nil, 0)()
508+ InitUbuntuUser("testhost", "testuser", "", nil, nil)
509+}
510+
511+func (s *initialisationSuite) TestInitUbuntuUserError(c *gc.C) {
512+ defer installFakeSSH(c, "", []string{"", "failed to create ubuntu user"}, 123)()
513+ defer installFakeSSH(c, "", "", 1)() // simulate failure of ubuntu@ login
514+ err := InitUbuntuUser("testhost", "testuser", "", nil, nil)
515+ c.Assert(err, gc.ErrorMatches, "exit status 123 \\(failed to create ubuntu user\\)")
516+}
517
518=== modified file 'environs/manual/provisioner.go'
519--- environs/manual/provisioner.go 2013-12-20 09:25:57 +0000
520+++ environs/manual/provisioner.go 2014-01-06 07:18:52 +0000
521@@ -81,16 +81,26 @@
522 client.Close()
523 }()
524
525+ // Create the "ubuntu" user and initialise passwordless sudo. We populate
526+ // the ubuntu user's authorized_keys file with the public keys in the current
527+ // user's ~/.ssh directory. The authenticationworker will later update the
528+ // ubuntu user's authorized_keys.
529+ user, host := splitUserHost(args.Host)
530+ authorizedKeys, err := config.ReadAuthorizedKeys("")
531+ if err := InitUbuntuUser(host, user, authorizedKeys, args.Stdin, args.Stdout); err != nil {
532+ return "", err
533+ }
534+
535 // Generate a unique nonce for the machine.
536 uuid, err := utils.NewUUID()
537 if err != nil {
538 return "", err
539 }
540- instanceId := instance.Id(manualInstancePrefix + hostWithoutUser(args.Host))
541+ instanceId := instance.Id(manualInstancePrefix + host)
542 nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String())
543
544 // Inform Juju that the machine exists.
545- machineId, series, arch, err := recordMachineInState(client, args.Host, nonce, instanceId)
546+ machineId, series, arch, err := recordMachineInState(client, host, nonce, instanceId)
547 if err != nil {
548 return "", err
549 }
550@@ -102,7 +112,7 @@
551 }
552
553 // Finally, provision the machine agent.
554- err = provisionMachineAgent(args.Host, mcfg, args.Stdin, args.Stdout, args.Stderr)
555+ err = provisionMachineAgent(host, mcfg, args.Stderr)
556 if err != nil {
557 return machineId, err
558 }
559@@ -111,20 +121,18 @@
560 return machineId, nil
561 }
562
563-func hostWithoutUser(host string) string {
564- hostWithoutUser := host
565- if at := strings.Index(hostWithoutUser, "@"); at != -1 {
566- hostWithoutUser = hostWithoutUser[at+1:]
567+func splitUserHost(host string) (string, string) {
568+ if at := strings.Index(host, "@"); at != -1 {
569+ return host[:at], host[at+1:]
570 }
571- return hostWithoutUser
572+ return "", host
573 }
574
575 func recordMachineInState(
576 client *api.Client, host, nonce string, instanceId instance.Id) (machineId, series, arch string, err error) {
577
578 // First, gather the parameters needed to inject the existing host into state.
579- sshHostWithoutUser := hostWithoutUser(host)
580- if ip := net.ParseIP(sshHostWithoutUser); ip != nil {
581+ if ip := net.ParseIP(host); ip != nil {
582 // Do a reverse-lookup on the IP. The IP may not have
583 // a DNS entry, so just log a warning if this fails.
584 names, err := net.LookupAddr(ip.String())
585@@ -132,14 +140,14 @@
586 logger.Infof("failed to resolve %v: %v", ip, err)
587 } else {
588 logger.Infof("resolved %v to %v", ip, names)
589- sshHostWithoutUser = names[0]
590+ host = names[0]
591 }
592 }
593- addrs, err := instance.HostAddresses(sshHostWithoutUser)
594+ addrs, err := instance.HostAddresses(host)
595 if err != nil {
596 return "", "", "", err
597 }
598- logger.Infof("addresses for %v: %v", sshHostWithoutUser, addrs)
599+ logger.Infof("addresses for %v: %v", host, addrs)
600
601 provisioned, err := checkProvisioned(host)
602 if err != nil {
603@@ -216,7 +224,7 @@
604 return mcfg, nil
605 }
606
607-func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig, stdin io.Reader, stdout, stderr io.Writer) error {
608+func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig, stderr io.Writer) error {
609 cloudcfg := coreCloudinit.New()
610 if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil {
611 return err
612@@ -225,10 +233,8 @@
613 // the target machine's existing configuration.
614 cloudcfg.SetAptUpgrade(false)
615 return sshinit.Configure(sshinit.ConfigureParams{
616- Host: host,
617+ Host: "ubuntu@" + host,
618 Config: cloudcfg,
619- Stdin: stdin,
620- Stdout: stdout,
621 Stderr: stderr,
622 })
623 }
624
625=== modified file 'environs/manual/provisioner_test.go'
626--- environs/manual/provisioner_test.go 2013-11-28 05:33:34 +0000
627+++ environs/manual/provisioner_test.go 2014-01-06 07:18:52 +0000
628@@ -43,7 +43,10 @@
629
630 envtesting.RemoveTools(c, s.Conn.Environ.Storage())
631 defer fakeSSH{
632- Series: series, Arch: arch, SkipProvisionAgent: true,
633+ Series: series,
634+ Arch: arch,
635+ InitUbuntuUser: true,
636+ SkipProvisionAgent: true,
637 }.install(c).Restore()
638 // Attempt to provision a machine with no tools available, expect it to fail.
639 machineId, err := ProvisionMachine(args)
640@@ -59,8 +62,9 @@
641 for i, errorCode := range []int{255, 0} {
642 c.Logf("test %d: code %d", i, errorCode)
643 defer fakeSSH{
644- Series: series,
645- Arch: arch,
646+ Series: series,
647+ Arch: arch,
648+ InitUbuntuUser: true,
649 ProvisionAgentExitCode: errorCode,
650 }.install(c).Restore()
651 machineId, err = ProvisionMachine(args)
652@@ -83,10 +87,21 @@
653
654 // Attempting to provision a machine twice should fail. We effect
655 // this by checking for existing juju upstart configurations.
656- defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 0)()
657+ defer fakeSSH{
658+ Provisioned: true,
659+ InitUbuntuUser: true,
660+ SkipDetection: true,
661+ SkipProvisionAgent: true,
662+ }.install(c).Restore()
663 _, err = ProvisionMachine(args)
664 c.Assert(err, gc.Equals, ErrProvisioned)
665- defer installFakeSSH(c, "", "/etc/init/jujud-machine-0.conf", 255)()
666+ defer fakeSSH{
667+ Provisioned: true,
668+ CheckProvisionedExitCode: 255,
669+ InitUbuntuUser: true,
670+ SkipDetection: true,
671+ SkipProvisionAgent: true,
672+ }.install(c).Restore()
673 _, err = ProvisionMachine(args)
674 c.Assert(err, gc.ErrorMatches, "error checking if provisioned: exit status 255")
675 }
676@@ -94,7 +109,11 @@
677 func (s *provisionerSuite) TestCreateMachineConfig(c *gc.C) {
678 const series = "precise"
679 const arch = "amd64"
680- defer fakeSSH{Series: series, Arch: arch}.install(c).Restore()
681+ defer fakeSSH{
682+ Series: series,
683+ Arch: arch,
684+ InitUbuntuUser: true,
685+ }.install(c).Restore()
686 machineId, err := ProvisionMachine(s.getArgs(c))
687 c.Assert(err, gc.IsNil)
688
689
690=== modified file 'environs/sshstorage/storage.go'
691--- environs/sshstorage/storage.go 2013-12-11 09:34:29 +0000
692+++ environs/sshstorage/storage.go 2014-01-06 07:18:52 +0000
693@@ -43,12 +43,8 @@
694 scanner *bufio.Scanner
695 }
696
697-var sshCommand = func(host string, tty bool, command string) *exec.Cmd {
698- var options []ssh.Option
699- if tty {
700- options = append(options, ssh.AllocateTTY)
701- }
702- return ssh.Command(host, []string{command}, options...)
703+var sshCommand = func(host string, command string) *exec.Cmd {
704+ return ssh.Command(host, []string{command}, ssh.NoPasswordAuthentication)
705 }
706
707 type flockmode string
708@@ -73,13 +69,6 @@
709 // will attempt to reassign ownership to the login user, and will return
710 // an error if it cannot do so.
711 TmpDir string
712-
713- // Stdin in required to respond to sudo passwords,
714- // and must be a terminal (except in tests).
715- Stdin io.Reader
716-
717- // Stdout is required to present sudo prompts to the user.
718- Stdout io.Writer
719 }
720
721 // NewSSHStorage creates a new SSHStorage, connected to the
722@@ -98,9 +87,7 @@
723 utils.ShQuote(params.TmpDir),
724 )
725
726- cmd := sshCommand(params.Host, true, fmt.Sprintf("sudo bash -c %s", utils.ShQuote(script)))
727- cmd.Stdin = params.Stdin
728- cmd.Stdout = params.Stdout // for sudo prompts/output
729+ cmd := sshCommand(params.Host, fmt.Sprintf("sudo -n bash -c %s", utils.ShQuote(script)))
730 var stderr bytes.Buffer
731 cmd.Stderr = &stderr
732 if err := cmd.Run(); err != nil {
733@@ -111,7 +98,7 @@
734 // We could use sftp, but then we'd be at the mercy of
735 // sftp's output messages for checking errors. Instead,
736 // we execute an interactive bash shell.
737- cmd = sshCommand(params.Host, false, "bash")
738+ cmd = sshCommand(params.Host, "bash")
739 stdin, err := cmd.StdinPipe()
740 if err != nil {
741 return nil, err
742
743=== modified file 'environs/sshstorage/storage_test.go'
744--- environs/sshstorage/storage_test.go 2013-12-06 07:02:50 +0000
745+++ environs/sshstorage/storage_test.go 2014-01-06 07:18:52 +0000
746@@ -30,7 +30,7 @@
747
748 var _ = gc.Suite(&storageSuite{})
749
750-func sshCommandTesting(host string, tty bool, command string) *exec.Cmd {
751+func sshCommandTesting(host string, command string) *exec.Cmd {
752 cmd := exec.Command("bash", "-c", command)
753 uid := fmt.Sprint(os.Getuid())
754 gid := fmt.Sprint(os.Getgid())
755@@ -45,8 +45,6 @@
756 Host: host,
757 StorageDir: storageDir,
758 TmpDir: tmpDir,
759- Stdin: &bytes.Buffer{},
760- Stdout: ioutil.Discard,
761 }
762 return NewSSHStorage(params)
763 }
764@@ -65,8 +63,8 @@
765 restoreEnv := testbase.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))
766 s.AddSuiteCleanup(func(*gc.C) { restoreEnv() })
767
768- // Create a "sudo" command which just executes its args.
769- err = os.Symlink("/usr/bin/env", filepath.Join(bin, "sudo"))
770+ // Create a "sudo" command which shifts away the "-n" and executes the remaining args.
771+ err = ioutil.WriteFile(filepath.Join(bin, "sudo"), []byte("#!/bin/sh\nshift; exec \"$@\""), 0755)
772 c.Assert(err, gc.IsNil)
773 restoreSshCommand := testbase.PatchValue(&sshCommand, sshCommandTesting)
774 s.AddSuiteCleanup(func(*gc.C) { restoreSshCommand() })
775@@ -169,7 +167,7 @@
776 // 3: second "install"
777 // 4: touch
778 var invocations int
779- badSshCommand := func(host string, tty bool, command string) *exec.Cmd {
780+ badSshCommand := func(host string, command string) *exec.Cmd {
781 invocations++
782 switch invocations {
783 case 1, 3:
784
785=== modified file 'provider/common/bootstrap.go'
786--- provider/common/bootstrap.go 2013-12-20 03:12:26 +0000
787+++ provider/common/bootstrap.go 2014-01-06 07:18:52 +0000
788@@ -185,8 +185,6 @@
789 return sshinit.Configure(sshinit.ConfigureParams{
790 Host: "ubuntu@" + addr,
791 Config: cloudcfg,
792- Stdin: ctx.Stdin(),
793- Stdout: ctx.Stdout(),
794 Stderr: ctx.Stderr(),
795 })
796 }
797@@ -332,7 +330,9 @@
798 // connectSSH is called to connect to the specified host and
799 // execute the "checkHostScript" bash script on it.
800 var connectSSH = func(host, checkHostScript string) error {
801- cmd := ssh.Command("ubuntu@"+host, []string{"/bin/bash", "-c", utils.ShQuote(checkHostScript)})
802+ cmd := ssh.Command("ubuntu@"+host, []string{
803+ "/bin/bash", "-c", utils.ShQuote(checkHostScript),
804+ }, ssh.NoPasswordAuthentication)
805 output, err := cmd.CombinedOutput()
806 if err != nil && len(output) > 0 {
807 err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
808
809=== modified file 'provider/null/config.go'
810--- provider/null/config.go 2013-10-15 05:11:29 +0000
811+++ provider/null/config.go 2014-01-06 07:18:52 +0000
812@@ -44,14 +44,6 @@
813 return c.attrs["bootstrap-user"].(string)
814 }
815
816-func (c *environConfig) sshHost() string {
817- host := c.bootstrapHost()
818- if user := c.bootstrapUser(); user != "" {
819- host = user + "@" + host
820- }
821- return host
822-}
823-
824 func (c *environConfig) storageListenIPAddress() string {
825 return c.attrs["storage-listen-ip"].(string)
826 }
827
828=== modified file 'provider/null/config_test.go'
829--- provider/null/config_test.go 2013-10-09 06:54:15 +0000
830+++ provider/null/config_test.go 2014-01-06 07:18:52 +0000
831@@ -103,13 +103,11 @@
832 testConfig := getEnvironConfig(c, values)
833 c.Assert(testConfig.bootstrapHost(), gc.Equals, "hostname")
834 c.Assert(testConfig.bootstrapUser(), gc.Equals, "")
835- c.Assert(testConfig.sshHost(), gc.Equals, "hostname")
836 values["bootstrap-host"] = "127.0.0.1"
837 values["bootstrap-user"] = "ubuntu"
838 testConfig = getEnvironConfig(c, values)
839 c.Assert(testConfig.bootstrapHost(), gc.Equals, "127.0.0.1")
840 c.Assert(testConfig.bootstrapUser(), gc.Equals, "ubuntu")
841- c.Assert(testConfig.sshHost(), gc.Equals, "ubuntu@127.0.0.1")
842 }
843
844 func (s *configSuite) TestStorageParams(c *gc.C) {
845
846=== modified file 'provider/null/environ.go'
847--- provider/null/environ.go 2013-12-19 09:15:40 +0000
848+++ provider/null/environ.go 2014-01-06 07:18:52 +0000
849@@ -48,8 +48,10 @@
850 type nullEnviron struct {
851 cfg *environConfig
852 cfgmutex sync.Mutex
853- bootstrapStorage *sshstorage.SSHStorage
854+ bootstrapStorage storage.Storage
855 bootstrapStorageMutex sync.Mutex
856+ ubuntuUserInited bool
857+ ubuntuUserInitMutex sync.Mutex
858 }
859
860 var _ environs.BootstrapStorager = (*nullEnviron)(nil)
861@@ -85,9 +87,32 @@
862 return e.envConfig().Name()
863 }
864
865+var initUbuntuUser = manual.InitUbuntuUser
866+
867+func (e *nullEnviron) ensureBootstrapUbuntuUser(ctx environs.BootstrapContext) error {
868+ e.ubuntuUserInitMutex.Lock()
869+ defer e.ubuntuUserInitMutex.Unlock()
870+ if e.ubuntuUserInited {
871+ return nil
872+ }
873+ cfg := e.envConfig()
874+ err := initUbuntuUser(cfg.bootstrapHost(), cfg.bootstrapUser(), cfg.AuthorizedKeys(), ctx.Stdin(), ctx.Stdout())
875+ if err != nil {
876+ logger.Errorf("initializing ubuntu user: %v", err)
877+ return err
878+ }
879+ logger.Infof("initialized ubuntu user")
880+ e.ubuntuUserInited = true
881+ return nil
882+}
883+
884 func (e *nullEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
885+ if err := e.ensureBootstrapUbuntuUser(ctx); err != nil {
886+ return err
887+ }
888 envConfig := e.envConfig()
889- hc, series, err := manual.DetectSeriesAndHardwareCharacteristics(envConfig.sshHost())
890+ host := envConfig.bootstrapHost()
891+ hc, series, err := manual.DetectSeriesAndHardwareCharacteristics(host)
892 if err != nil {
893 return err
894 }
895@@ -97,7 +122,7 @@
896 }
897 return manual.Bootstrap(manual.BootstrapArgs{
898 Context: ctx,
899- Host: e.envConfig().sshHost(),
900+ Host: host,
901 DataDir: dataDir,
902 Environ: e,
903 PossibleTools: selectedTools,
904@@ -144,6 +169,14 @@
905 return instances, err
906 }
907
908+var newSSHStorage = func(sshHost, storageDir, storageTmpdir string) (storage.Storage, error) {
909+ return sshstorage.NewSSHStorage(sshstorage.NewSSHStorageParams{
910+ Host: sshHost,
911+ StorageDir: storageDir,
912+ TmpDir: storageTmpdir,
913+ })
914+}
915+
916 // Implements environs.BootstrapStorager.
917 func (e *nullEnviron) EnableBootstrapStorage(ctx environs.BootstrapContext) error {
918 e.bootstrapStorageMutex.Lock()
919@@ -151,17 +184,13 @@
920 if e.bootstrapStorage != nil {
921 return nil
922 }
923+ if err := e.ensureBootstrapUbuntuUser(ctx); err != nil {
924+ return err
925+ }
926 cfg := e.envConfig()
927 storageDir := e.StorageDir()
928 storageTmpdir := path.Join(dataDir, storageTmpSubdir)
929- params := sshstorage.NewSSHStorageParams{
930- Host: cfg.sshHost(),
931- StorageDir: storageDir,
932- TmpDir: storageTmpdir,
933- Stdin: ctx.Stdin(),
934- Stdout: ctx.Stdout(),
935- }
936- bootstrapStorage, err := sshstorage.NewSSHStorage(params)
937+ bootstrapStorage, err := newSSHStorage("ubuntu@"+cfg.bootstrapHost(), storageDir, storageTmpdir)
938 if err != nil {
939 return err
940 }
941
942=== modified file 'provider/null/environ_test.go'
943--- provider/null/environ_test.go 2013-12-20 02:38:56 +0000
944+++ provider/null/environ_test.go 2014-01-06 07:18:52 +0000
945@@ -1,19 +1,18 @@
946-// Copyright 2013 Canonical Ltd.
947+// Copyright 2012 Canonical Ltd.
948 // Licensed under the AGPLv3, see LICENCE file for details.
949
950 package null
951
952 import (
953- "io/ioutil"
954- "os"
955- "path/filepath"
956+ "errors"
957+ "io"
958 "strings"
959
960 gc "launchpad.net/gocheck"
961
962 "launchpad.net/juju-core/environs"
963 "launchpad.net/juju-core/environs/manual"
964- "launchpad.net/juju-core/environs/sshstorage"
965+ "launchpad.net/juju-core/environs/storage"
966 envtesting "launchpad.net/juju-core/environs/testing"
967 "launchpad.net/juju-core/environs/tools"
968 "launchpad.net/juju-core/instance"
969@@ -101,35 +100,39 @@
970 c.Assert(strings.Contains(url, "/tools"), jc.IsTrue)
971 }
972
973+type dummyStorage struct {
974+ storage.Storage
975+}
976+
977 func (s *environSuite) TestEnvironBootstrapStorager(c *gc.C) {
978- var sshScript = `
979-#!/bin/bash --norc
980-if echo "$*" | grep -q -v sudo; then
981- # We're executing bash inside ssh. Wait
982- # for input to be written before exiting.
983- head -n 1 > /dev/null
984- echo JUJU-RC: $RC
985-fi
986-`[1:]
987- bin := c.MkDir()
988- ssh := filepath.Join(bin, "ssh")
989- err := ioutil.WriteFile(ssh, []byte(sshScript), 0755)
990- c.Assert(err, gc.IsNil)
991- s.PatchEnvironment("PATH", bin+":"+os.Getenv("PATH"))
992-
993- s.PatchEnvironment("RC", "99") // simulate ssh failure
994+ var newSSHStorageResult = struct {
995+ stor storage.Storage
996+ err error
997+ }{dummyStorage{}, errors.New("failed to get SSH storage")}
998+ s.PatchValue(&newSSHStorage, func(sshHost, storageDir, storageTmpdir string) (storage.Storage, error) {
999+ return newSSHStorageResult.stor, newSSHStorageResult.err
1000+ })
1001+
1002+ var initUbuntuResult error
1003+ s.PatchValue(&initUbuntuUser, func(host, user, authorizedKeys string, stdin io.Reader, stdout io.Writer) error {
1004+ return initUbuntuResult
1005+ })
1006+
1007 ctx := envtesting.NewBootstrapContext(coretesting.Context(c))
1008- err = s.env.EnableBootstrapStorage(ctx)
1009- c.Assert(err, gc.ErrorMatches, "exit code 99")
1010- c.Assert(s.env.Storage(), gc.Not(gc.FitsTypeOf), new(sshstorage.SSHStorage))
1011-
1012- s.PatchEnvironment("RC", "0")
1013- err = s.env.EnableBootstrapStorage(ctx)
1014- c.Assert(err, gc.IsNil)
1015- c.Assert(s.env.Storage(), gc.FitsTypeOf, new(sshstorage.SSHStorage))
1016-
1017- // Check idempotency
1018- err = s.env.EnableBootstrapStorage(ctx)
1019- c.Assert(err, gc.IsNil)
1020- c.Assert(s.env.Storage(), gc.FitsTypeOf, new(sshstorage.SSHStorage))
1021+ initUbuntuResult = errors.New("failed to initialise ubuntu user")
1022+ c.Assert(s.env.EnableBootstrapStorage(ctx), gc.Equals, initUbuntuResult)
1023+ initUbuntuResult = nil
1024+ c.Assert(s.env.EnableBootstrapStorage(ctx), gc.Equals, newSSHStorageResult.err)
1025+ // after the user is initialised once successfully,
1026+ // another attempt will not be made.
1027+ initUbuntuResult = errors.New("failed to initialise ubuntu user")
1028+ c.Assert(s.env.EnableBootstrapStorage(ctx), gc.Equals, newSSHStorageResult.err)
1029+
1030+ // after the bootstrap storage is initialised once successfully,
1031+ // another attempt will not be made.
1032+ backup := newSSHStorageResult.err
1033+ newSSHStorageResult.err = nil
1034+ c.Assert(s.env.EnableBootstrapStorage(ctx), gc.IsNil)
1035+ newSSHStorageResult.err = backup
1036+ c.Assert(s.env.EnableBootstrapStorage(ctx), gc.IsNil)
1037 }

Subscribers

People subscribed via source and target branches

to status/vote changes: