~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/provider/common/bootstrap.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2013 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package common
 
5
 
 
6
import (
 
7
        "fmt"
 
8
        "io"
 
9
        "os"
 
10
        "path"
 
11
        "strings"
 
12
        "sync"
 
13
        "time"
 
14
 
 
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"
 
22
 
 
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"
 
36
)
 
37
 
 
38
var logger = loggo.GetLogger("juju.provider.common")
 
39
 
 
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)
 
46
        if err != nil {
 
47
                return nil, errors.Trace(err)
 
48
        }
 
49
 
 
50
        bsResult := &environs.BootstrapResult{
 
51
                Arch:     *result.Hardware.Arch,
 
52
                Series:   series,
 
53
                Finalize: finalizer,
 
54
        }
 
55
        return bsResult, nil
 
56
}
 
57
 
 
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.
 
70
 
 
71
        // First thing, ensure we have tools otherwise there's no point.
 
72
        if args.BootstrapSeries != "" {
 
73
                selectedSeries = args.BootstrapSeries
 
74
        } else {
 
75
                selectedSeries = config.PreferredSeries(env.Config())
 
76
        }
 
77
        availableTools, err := args.AvailableTools.Match(coretools.Filter{
 
78
                Series: selectedSeries,
 
79
        })
 
80
        if err != nil {
 
81
                return nil, "", nil, err
 
82
        }
 
83
 
 
84
        // Filter image metadata to the selected series.
 
85
        var imageMetadata []*imagemetadata.ImageMetadata
 
86
        seriesVersion, err := series.SeriesVersion(selectedSeries)
 
87
        if err != nil {
 
88
                return nil, "", nil, errors.Trace(err)
 
89
        }
 
90
        for _, m := range args.ImageMetadata {
 
91
                if m.Version != seriesVersion {
 
92
                        continue
 
93
                }
 
94
                imageMetadata = append(imageMetadata, m)
 
95
        }
 
96
 
 
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
 
100
        if client == nil {
 
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")
 
104
        }
 
105
 
 
106
        publicKey, err := simplestreams.UserPublicSigningKey()
 
107
        if err != nil {
 
108
                return nil, "", nil, err
 
109
        }
 
110
        envCfg := env.Config()
 
111
        instanceConfig, err := instancecfg.NewBootstrapInstanceConfig(
 
112
                args.ControllerConfig, args.BootstrapConstraints, args.ModelConstraints, selectedSeries, publicKey,
 
113
        )
 
114
        if err != nil {
 
115
                return nil, "", nil, err
 
116
        }
 
117
        instanceConfig.EnableOSRefreshUpdate = env.Config().EnableOSRefreshUpdate()
 
118
        instanceConfig.EnableOSUpgrade = env.Config().EnableOSUpgrade()
 
119
 
 
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)
 
129
                        }
 
130
                        icfg.AgentEnvironment[agent.LxcBridge] = args.ContainerBridgeName
 
131
                }
 
132
        }
 
133
        maybeSetBridge(instanceConfig)
 
134
 
 
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)
 
138
                return nil
 
139
        }
 
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,
 
148
        })
 
149
        if err != nil {
 
150
                return nil, "", nil, errors.Annotate(err, "cannot start bootstrap instance")
 
151
        }
 
152
        fmt.Fprintf(ctx.GetStderr(), " - %s\n", result.Instance.Id())
 
153
 
 
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())
 
160
                        if err != nil {
 
161
                                return errors.Trace(err)
 
162
                        }
 
163
                        envConfig = updated
 
164
                }
 
165
                if err := instancecfg.FinishInstanceConfig(icfg, envConfig); err != nil {
 
166
                        return err
 
167
                }
 
168
                maybeSetBridge(icfg)
 
169
                return FinishBootstrap(ctx, client, env, result.Instance, icfg, opts)
 
170
        }
 
171
        return result, selectedSeries, finalize, nil
 
172
}
 
173
 
 
174
// FinishBootstrap completes the bootstrap process by connecting
 
175
// to the instance via SSH and carrying out the cloud-config.
 
176
//
 
177
// Note: FinishBootstrap is exposed so it can be replaced for testing.
 
178
var FinishBootstrap = func(
 
179
        ctx environs.BootstrapContext,
 
180
        client ssh.Client,
 
181
        env environs.Environ,
 
182
        inst instance.Instance,
 
183
        instanceConfig *instancecfg.InstanceConfig,
 
184
        opts environs.BootstrapDialOpts,
 
185
) error {
 
186
        interrupted := make(chan os.Signal, 1)
 
187
        ctx.InterruptNotify(interrupted)
 
188
        defer ctx.StopInterruptNotify(interrupted)
 
189
        addr, err := WaitSSH(
 
190
                ctx.GetStderr(),
 
191
                interrupted,
 
192
                client,
 
193
                GetCheckNonceCommand(instanceConfig),
 
194
                &RefreshableInstance{inst, env},
 
195
                opts,
 
196
        )
 
197
        if err != nil {
 
198
                return err
 
199
        }
 
200
        return ConfigureMachine(ctx, client, addr, instanceConfig)
 
201
}
 
202
 
 
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(`
 
211
        noncefile=%s
 
212
        if [ ! -e "$noncefile" ]; then
 
213
                echo "$noncefile does not exist" >&2
 
214
                exit 1
 
215
        fi
 
216
        content=$(cat $noncefile)
 
217
        if [ "$content" != %s ]; then
 
218
                echo "$noncefile contents do not match machine nonce" >&2
 
219
                exit 1
 
220
        fi
 
221
        `, nonceFile, utils.ShQuote(instanceConfig.MachineNonce))
 
222
        return checkNonceCommand
 
223
}
 
224
 
 
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)
 
233
        if err != nil {
 
234
                return errors.Trace(err)
 
235
        }
 
236
 
 
237
        // Set packaging update here
 
238
        cloudcfg.SetSystemUpdate(instanceConfig.EnableOSRefreshUpdate)
 
239
        cloudcfg.SetSystemUpgrade(instanceConfig.EnableOSUpgrade)
 
240
 
 
241
        udata, err := cloudconfig.NewUserdataConfig(instanceConfig, cloudcfg)
 
242
        if err != nil {
 
243
                return err
 
244
        }
 
245
        if err := udata.ConfigureJuju(); err != nil {
 
246
                return err
 
247
        }
 
248
        configScript, err := cloudcfg.RenderScript()
 
249
        if err != nil {
 
250
                return err
 
251
        }
 
252
        script := shell.DumpFileOnErrorScript(instanceConfig.CloudInitOutputLog) + configScript
 
253
        return sshinit.RunConfigureScript(script, sshinit.ConfigureParams{
 
254
                Host:           "ubuntu@" + host,
 
255
                Client:         client,
 
256
                Config:         cloudcfg,
 
257
                ProgressWriter: ctx.GetStderr(),
 
258
                Series:         instanceConfig.Series,
 
259
        })
 
260
}
 
261
 
 
262
type Addresser interface {
 
263
        // Refresh refreshes the addresses for the instance.
 
264
        Refresh() error
 
265
 
 
266
        // Addresses returns the addresses for the instance.
 
267
        // To ensure that the results are up to date, call
 
268
        // Refresh first.
 
269
        Addresses() ([]network.Address, error)
 
270
}
 
271
 
 
272
type RefreshableInstance struct {
 
273
        instance.Instance
 
274
        Env environs.Environ
 
275
}
 
276
 
 
277
// Refresh refreshes the addresses for the instance.
 
278
func (i *RefreshableInstance) Refresh() error {
 
279
        instances, err := i.Env.Instances([]instance.Id{i.Id()})
 
280
        if err != nil {
 
281
                return errors.Trace(err)
 
282
        }
 
283
        i.Instance = instances[0]
 
284
        return nil
 
285
}
 
286
 
 
287
type hostChecker struct {
 
288
        addr   network.Address
 
289
        client ssh.Client
 
290
        wg     *sync.WaitGroup
 
291
 
 
292
        // checkDelay is the amount of time to wait between retries.
 
293
        checkDelay time.Duration
 
294
 
 
295
        // checkHostScript is executed on the host via SSH.
 
296
        // hostChecker.loop will return once the script
 
297
        // runs without error.
 
298
        checkHostScript string
 
299
 
 
300
        // closed is closed to indicate that the host checker should
 
301
        // return, without waiting for the result of any ongoing
 
302
        // attempts.
 
303
        closed <-chan struct{}
 
304
}
 
305
 
 
306
// Close implements io.Closer, as required by parallel.Try.
 
307
func (*hostChecker) Close() error {
 
308
        return nil
 
309
}
 
310
 
 
311
func (hc *hostChecker) loop(dying <-chan struct{}) (io.Closer, error) {
 
312
        defer hc.wg.Done()
 
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)
 
317
        var lastErr error
 
318
        for {
 
319
                address := hc.addr.Value
 
320
                go func() {
 
321
                        done <- connectSSH(hc.client, address, hc.checkHostScript)
 
322
                }()
 
323
                select {
 
324
                case <-dying:
 
325
                        return hc, lastErr
 
326
                case lastErr = <-done:
 
327
                        if lastErr == nil {
 
328
                                return hc, nil
 
329
                        }
 
330
                        logger.Debugf("connection attempt for %s failed: %v", address, lastErr)
 
331
                }
 
332
                select {
 
333
                case <-hc.closed:
 
334
                        return hc, lastErr
 
335
                case <-dying:
 
336
                case <-time.After(hc.checkDelay):
 
337
                }
 
338
        }
 
339
}
 
340
 
 
341
type parallelHostChecker struct {
 
342
        *parallel.Try
 
343
        client ssh.Client
 
344
        stderr io.Writer
 
345
        wg     sync.WaitGroup
 
346
 
 
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{}
 
352
 
 
353
        // checkDelay is how long each hostChecker waits between attempts.
 
354
        checkDelay time.Duration
 
355
 
 
356
        // checkHostScript is the script to run on each host to check that
 
357
        // it is the host we expect.
 
358
        checkHostScript string
 
359
}
 
360
 
 
361
func (p *parallelHostChecker) UpdateAddresses(addrs []network.Address) {
 
362
        for _, addr := range addrs {
 
363
                if _, ok := p.active[addr]; ok {
 
364
                        continue
 
365
                }
 
366
                fmt.Fprintf(p.stderr, "Attempting to connect to %s:22\n", addr.Value)
 
367
                closed := make(chan struct{})
 
368
                hc := &hostChecker{
 
369
                        addr:            addr,
 
370
                        client:          p.client,
 
371
                        checkDelay:      p.checkDelay,
 
372
                        checkHostScript: p.checkHostScript,
 
373
                        closed:          closed,
 
374
                        wg:              &p.wg,
 
375
                }
 
376
                p.wg.Add(1)
 
377
                p.active[addr] = closed
 
378
                p.Start(hc.loop)
 
379
        }
 
380
}
 
381
 
 
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.
 
389
        p.Try.Close()
 
390
        for _, ch := range p.active {
 
391
                close(ch)
 
392
        }
 
393
        return nil
 
394
}
 
395
 
 
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)))
 
404
        }
 
405
        return err
 
406
}
 
407
 
 
408
// WaitSSH waits for the instance to be assigned a routable
 
409
// address, then waits until we can connect to it via SSH.
 
410
//
 
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)
 
420
 
 
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),
 
426
                client:          client,
 
427
                stderr:          stdErr,
 
428
                active:          make(map[network.Address]chan struct{}),
 
429
                checkDelay:      opts.RetryDelay,
 
430
                checkHostScript: checkHostScript,
 
431
        }
 
432
        defer checker.wg.Wait()
 
433
        defer checker.Kill()
 
434
 
 
435
        fmt.Fprintln(stdErr, "Waiting for address")
 
436
        for {
 
437
                select {
 
438
                case <-pollAddresses.C:
 
439
                        pollAddresses.Reset(opts.AddressesDelay)
 
440
                        if err := inst.Refresh(); err != nil {
 
441
                                return "", fmt.Errorf("refreshing addresses: %v", err)
 
442
                        }
 
443
                        addresses, err := inst.Addresses()
 
444
                        if err != nil {
 
445
                                return "", fmt.Errorf("getting addresses: %v", err)
 
446
                        }
 
447
                        checker.UpdateAddresses(addresses)
 
448
                case <-globalTimeout:
 
449
                        checker.Close()
 
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"
 
455
                        } else {
 
456
                                format += "without being able to connect"
 
457
                        }
 
458
                        if lastErr != nil && lastErr != parallel.ErrStopped {
 
459
                                format += ": %v"
 
460
                                args = append(args, lastErr)
 
461
                        }
 
462
                        return "", fmt.Errorf(format, args...)
 
463
                case <-interrupted:
 
464
                        return "", fmt.Errorf("interrupted")
 
465
                case <-checker.Dead():
 
466
                        result, err := checker.Result()
 
467
                        if err != nil {
 
468
                                return "", err
 
469
                        }
 
470
                        return result.(*hostChecker).addr.Value, nil
 
471
                }
 
472
        }
 
473
}