1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
15
"github.com/juju/errors"
16
"github.com/juju/loggo"
17
"github.com/juju/utils"
18
"github.com/juju/utils/parallel"
19
"github.com/juju/utils/series"
20
"github.com/juju/utils/shell"
21
"github.com/juju/utils/ssh"
23
"github.com/juju/juju/agent"
24
"github.com/juju/juju/cloudconfig"
25
"github.com/juju/juju/cloudconfig/cloudinit"
26
"github.com/juju/juju/cloudconfig/instancecfg"
27
"github.com/juju/juju/cloudconfig/sshinit"
28
"github.com/juju/juju/environs"
29
"github.com/juju/juju/environs/config"
30
"github.com/juju/juju/environs/imagemetadata"
31
"github.com/juju/juju/environs/simplestreams"
32
"github.com/juju/juju/instance"
33
"github.com/juju/juju/network"
34
"github.com/juju/juju/status"
35
coretools "github.com/juju/juju/tools"
38
var logger = loggo.GetLogger("juju.provider.common")
40
// Bootstrap is a common implementation of the Bootstrap method defined on
41
// environs.Environ; we strongly recommend that this implementation be used
42
// when writing a new provider.
43
func Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args environs.BootstrapParams,
44
) (*environs.BootstrapResult, error) {
45
result, series, finalizer, err := BootstrapInstance(ctx, env, args)
47
return nil, errors.Trace(err)
50
bsResult := &environs.BootstrapResult{
51
Arch: *result.Hardware.Arch,
58
// BootstrapInstance creates a new instance with the series and architecture
59
// of its choice, constrained to those of the available tools, and
60
// returns the instance result, series, and a function that
61
// must be called to finalize the bootstrap process by transferring
62
// the tools and installing the initial Juju controller.
63
// This method is called by Bootstrap above, which implements environs.Bootstrap, but
64
// is also exported so that providers can manipulate the started instance.
65
func BootstrapInstance(ctx environs.BootstrapContext, env environs.Environ, args environs.BootstrapParams,
66
) (_ *environs.StartInstanceResult, selectedSeries string, _ environs.BootstrapFinalizer, err error) {
67
// TODO make safe in the case of racing Bootstraps
68
// If two Bootstraps are called concurrently, there's
69
// no way to make sure that only one succeeds.
71
// First thing, ensure we have tools otherwise there's no point.
72
if args.BootstrapSeries != "" {
73
selectedSeries = args.BootstrapSeries
75
selectedSeries = config.PreferredSeries(env.Config())
77
availableTools, err := args.AvailableTools.Match(coretools.Filter{
78
Series: selectedSeries,
81
return nil, "", nil, err
84
// Filter image metadata to the selected series.
85
var imageMetadata []*imagemetadata.ImageMetadata
86
seriesVersion, err := series.SeriesVersion(selectedSeries)
88
return nil, "", nil, errors.Trace(err)
90
for _, m := range args.ImageMetadata {
91
if m.Version != seriesVersion {
94
imageMetadata = append(imageMetadata, m)
97
// Get the bootstrap SSH client. Do this early, so we know
98
// not to bother with any of the below if we can't finish the job.
99
client := ssh.DefaultClient
101
// This should never happen: if we don't have OpenSSH, then
102
// go.crypto/ssh should be used with an auto-generated key.
103
return nil, "", nil, fmt.Errorf("no SSH client available")
106
publicKey, err := simplestreams.UserPublicSigningKey()
108
return nil, "", nil, err
110
envCfg := env.Config()
111
instanceConfig, err := instancecfg.NewBootstrapInstanceConfig(
112
args.ControllerConfig, args.BootstrapConstraints, args.ModelConstraints, selectedSeries, publicKey,
115
return nil, "", nil, err
117
instanceConfig.EnableOSRefreshUpdate = env.Config().EnableOSRefreshUpdate()
118
instanceConfig.EnableOSUpgrade = env.Config().EnableOSUpgrade()
120
instanceConfig.Tags = instancecfg.InstanceTags(envCfg.UUID(), args.ControllerConfig.ControllerUUID(), envCfg, instanceConfig.Jobs)
121
maybeSetBridge := func(icfg *instancecfg.InstanceConfig) {
122
// If we need to override the default bridge name, do it now. When
123
// args.ContainerBridgeName is empty, the default names for LXC
124
// (lxcbr0) and KVM (virbr0) will be used.
125
if args.ContainerBridgeName != "" {
126
logger.Debugf("using %q as network bridge for all container types", args.ContainerBridgeName)
127
if icfg.AgentEnvironment == nil {
128
icfg.AgentEnvironment = make(map[string]string)
130
icfg.AgentEnvironment[agent.LxcBridge] = args.ContainerBridgeName
133
maybeSetBridge(instanceConfig)
135
fmt.Fprintln(ctx.GetStderr(), "Launching instance")
136
instanceStatus := func(settableStatus status.Status, info string, data map[string]interface{}) error {
137
fmt.Fprintf(ctx.GetStderr(), "%s \r", info)
140
result, err := env.StartInstance(environs.StartInstanceParams{
141
ControllerUUID: args.ControllerConfig.ControllerUUID(),
142
Constraints: args.BootstrapConstraints,
143
Tools: availableTools,
144
InstanceConfig: instanceConfig,
145
Placement: args.Placement,
146
ImageMetadata: imageMetadata,
147
StatusCallback: instanceStatus,
150
return nil, "", nil, errors.Annotate(err, "cannot start bootstrap instance")
152
fmt.Fprintf(ctx.GetStderr(), " - %s\n", result.Instance.Id())
154
finalize := func(ctx environs.BootstrapContext, icfg *instancecfg.InstanceConfig, opts environs.BootstrapDialOpts) error {
155
icfg.Bootstrap.BootstrapMachineInstanceId = result.Instance.Id()
156
icfg.Bootstrap.BootstrapMachineHardwareCharacteristics = result.Hardware
157
envConfig := env.Config()
158
if result.Config != nil {
159
updated, err := envConfig.Apply(result.Config.UnknownAttrs())
161
return errors.Trace(err)
165
if err := instancecfg.FinishInstanceConfig(icfg, envConfig); err != nil {
169
return FinishBootstrap(ctx, client, env, result.Instance, icfg, opts)
171
return result, selectedSeries, finalize, nil
174
// FinishBootstrap completes the bootstrap process by connecting
175
// to the instance via SSH and carrying out the cloud-config.
177
// Note: FinishBootstrap is exposed so it can be replaced for testing.
178
var FinishBootstrap = func(
179
ctx environs.BootstrapContext,
181
env environs.Environ,
182
inst instance.Instance,
183
instanceConfig *instancecfg.InstanceConfig,
184
opts environs.BootstrapDialOpts,
186
interrupted := make(chan os.Signal, 1)
187
ctx.InterruptNotify(interrupted)
188
defer ctx.StopInterruptNotify(interrupted)
189
addr, err := WaitSSH(
193
GetCheckNonceCommand(instanceConfig),
194
&RefreshableInstance{inst, env},
200
return ConfigureMachine(ctx, client, addr, instanceConfig)
203
func GetCheckNonceCommand(instanceConfig *instancecfg.InstanceConfig) string {
204
// Each attempt to connect to an address must verify the machine is the
205
// bootstrap machine by checking its nonce file exists and contains the
206
// nonce in the InstanceConfig. This also blocks sshinit from proceeding
207
// until cloud-init has completed, which is necessary to ensure apt
208
// invocations don't trample each other.
209
nonceFile := utils.ShQuote(path.Join(instanceConfig.DataDir, cloudconfig.NonceFile))
210
checkNonceCommand := fmt.Sprintf(`
212
if [ ! -e "$noncefile" ]; then
213
echo "$noncefile does not exist" >&2
216
content=$(cat $noncefile)
217
if [ "$content" != %s ]; then
218
echo "$noncefile contents do not match machine nonce" >&2
221
`, nonceFile, utils.ShQuote(instanceConfig.MachineNonce))
222
return checkNonceCommand
225
func ConfigureMachine(ctx environs.BootstrapContext, client ssh.Client, host string, instanceConfig *instancecfg.InstanceConfig) error {
226
// Bootstrap is synchronous, and will spawn a subprocess
227
// to complete the procedure. If the user hits Ctrl-C,
228
// SIGINT is sent to the foreground process attached to
229
// the terminal, which will be the ssh subprocess at this
230
// point. For that reason, we do not call StopInterruptNotify
231
// until this function completes.
232
cloudcfg, err := cloudinit.New(instanceConfig.Series)
234
return errors.Trace(err)
237
// Set packaging update here
238
cloudcfg.SetSystemUpdate(instanceConfig.EnableOSRefreshUpdate)
239
cloudcfg.SetSystemUpgrade(instanceConfig.EnableOSUpgrade)
241
udata, err := cloudconfig.NewUserdataConfig(instanceConfig, cloudcfg)
245
if err := udata.ConfigureJuju(); err != nil {
248
configScript, err := cloudcfg.RenderScript()
252
script := shell.DumpFileOnErrorScript(instanceConfig.CloudInitOutputLog) + configScript
253
return sshinit.RunConfigureScript(script, sshinit.ConfigureParams{
254
Host: "ubuntu@" + host,
257
ProgressWriter: ctx.GetStderr(),
258
Series: instanceConfig.Series,
262
type Addresser interface {
263
// Refresh refreshes the addresses for the instance.
266
// Addresses returns the addresses for the instance.
267
// To ensure that the results are up to date, call
269
Addresses() ([]network.Address, error)
272
type RefreshableInstance struct {
277
// Refresh refreshes the addresses for the instance.
278
func (i *RefreshableInstance) Refresh() error {
279
instances, err := i.Env.Instances([]instance.Id{i.Id()})
281
return errors.Trace(err)
283
i.Instance = instances[0]
287
type hostChecker struct {
292
// checkDelay is the amount of time to wait between retries.
293
checkDelay time.Duration
295
// checkHostScript is executed on the host via SSH.
296
// hostChecker.loop will return once the script
297
// runs without error.
298
checkHostScript string
300
// closed is closed to indicate that the host checker should
301
// return, without waiting for the result of any ongoing
303
closed <-chan struct{}
306
// Close implements io.Closer, as required by parallel.Try.
307
func (*hostChecker) Close() error {
311
func (hc *hostChecker) loop(dying <-chan struct{}) (io.Closer, error) {
313
// The value of connectSSH is taken outside the goroutine that may outlive
314
// hostChecker.loop, or we evoke the wrath of the race detector.
315
connectSSH := connectSSH
316
done := make(chan error, 1)
319
address := hc.addr.Value
321
done <- connectSSH(hc.client, address, hc.checkHostScript)
326
case lastErr = <-done:
330
logger.Debugf("connection attempt for %s failed: %v", address, lastErr)
336
case <-time.After(hc.checkDelay):
341
type parallelHostChecker struct {
347
// active is a map of adresses to channels for addresses actively
348
// being tested. The goroutine testing the address will continue
349
// to attempt connecting to the address until it succeeds, the Try
350
// is killed, or the corresponding channel in this map is closed.
351
active map[network.Address]chan struct{}
353
// checkDelay is how long each hostChecker waits between attempts.
354
checkDelay time.Duration
356
// checkHostScript is the script to run on each host to check that
357
// it is the host we expect.
358
checkHostScript string
361
func (p *parallelHostChecker) UpdateAddresses(addrs []network.Address) {
362
for _, addr := range addrs {
363
if _, ok := p.active[addr]; ok {
366
fmt.Fprintf(p.stderr, "Attempting to connect to %s:22\n", addr.Value)
367
closed := make(chan struct{})
371
checkDelay: p.checkDelay,
372
checkHostScript: p.checkHostScript,
377
p.active[addr] = closed
382
// Close prevents additional functions from being added to
383
// the Try, and tells each active hostChecker to exit.
384
func (p *parallelHostChecker) Close() error {
385
// We signal each checker to stop and wait for them
386
// each to complete; this allows us to get the error,
387
// as opposed to when using try.Kill which does not
388
// wait for the functions to complete.
390
for _, ch := range p.active {
396
// connectSSH is called to connect to the specified host and
397
// execute the "checkHostScript" bash script on it.
398
var connectSSH = func(client ssh.Client, host, checkHostScript string) error {
399
cmd := client.Command("ubuntu@"+host, []string{"/bin/bash"}, nil)
400
cmd.Stdin = strings.NewReader(checkHostScript)
401
output, err := cmd.CombinedOutput()
402
if err != nil && len(output) > 0 {
403
err = fmt.Errorf("%s", strings.TrimSpace(string(output)))
408
// WaitSSH waits for the instance to be assigned a routable
409
// address, then waits until we can connect to it via SSH.
411
// waitSSH attempts on all addresses returned by the instance
412
// in parallel; the first succeeding one wins. We ensure that
413
// private addresses are for the correct machine by checking
414
// the presence of a file on the machine that contains the
415
// machine's nonce. The "checkHostScript" is a bash script
416
// that performs this file check.
417
func WaitSSH(stdErr io.Writer, interrupted <-chan os.Signal, client ssh.Client, checkHostScript string, inst Addresser, opts environs.BootstrapDialOpts) (addr string, err error) {
418
globalTimeout := time.After(opts.Timeout)
419
pollAddresses := time.NewTimer(0)
421
// checker checks each address in a loop, in parallel,
422
// until one succeeds, the global timeout is reached,
423
// or the tomb is killed.
424
checker := parallelHostChecker{
425
Try: parallel.NewTry(0, nil),
428
active: make(map[network.Address]chan struct{}),
429
checkDelay: opts.RetryDelay,
430
checkHostScript: checkHostScript,
432
defer checker.wg.Wait()
435
fmt.Fprintln(stdErr, "Waiting for address")
438
case <-pollAddresses.C:
439
pollAddresses.Reset(opts.AddressesDelay)
440
if err := inst.Refresh(); err != nil {
441
return "", fmt.Errorf("refreshing addresses: %v", err)
443
addresses, err := inst.Addresses()
445
return "", fmt.Errorf("getting addresses: %v", err)
447
checker.UpdateAddresses(addresses)
448
case <-globalTimeout:
450
lastErr := checker.Wait()
451
format := "waited for %v "
452
args := []interface{}{opts.Timeout}
453
if len(checker.active) == 0 {
454
format += "without getting any addresses"
456
format += "without being able to connect"
458
if lastErr != nil && lastErr != parallel.ErrStopped {
460
args = append(args, lastErr)
462
return "", fmt.Errorf(format, args...)
464
return "", fmt.Errorf("interrupted")
465
case <-checker.Dead():
466
result, err := checker.Result()
470
return result.(*hostChecker).addr.Value, nil