Merge lp:~axwalk/juju-core/lp1261343-manual-ubuntu-user into lp:~go-bot/juju-core/trunk
- lp1261343-manual-ubuntu-user
- Merge into trunk
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 |
Related bugs: |
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
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
Andrew Wilkins (axwalk) wrote : | # |
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
LGTM, definitely a move in a nice direction. Just trivials and
longer-term discussion points.
https:/
File environs/
https:/
environs/
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:/
File environs/
https:/
environs/
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:/
File provider/
https:/
provider/
Inited?
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File environs/
https:/
environs/
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-
onerous.
The simplestreams/tools lookup code is all part of the Go binary, so how
would it be done from the server anyway?
https:/
File environs/
https:/
environs/
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:/
File provider/
https:/
provider/
On 2014/01/03 13:07:55, fwereade wrote:
> Inited?
Done.
Preview Diff
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 | } |
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/