github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/provider/common/bootstrap_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package common_test
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"time"
    10  
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/testing"
    13  	jc "github.com/juju/testing/checkers"
    14  	gc "launchpad.net/gocheck"
    15  
    16  	"github.com/juju/juju/constraints"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/cloudinit"
    19  	"github.com/juju/juju/environs/config"
    20  	"github.com/juju/juju/environs/storage"
    21  	envtesting "github.com/juju/juju/environs/testing"
    22  	"github.com/juju/juju/instance"
    23  	"github.com/juju/juju/network"
    24  	"github.com/juju/juju/provider/common"
    25  	coretesting "github.com/juju/juju/testing"
    26  	"github.com/juju/juju/tools"
    27  	"github.com/juju/juju/utils/ssh"
    28  )
    29  
    30  type BootstrapSuite struct {
    31  	coretesting.FakeJujuHomeSuite
    32  	envtesting.ToolsFixture
    33  }
    34  
    35  var _ = gc.Suite(&BootstrapSuite{})
    36  
    37  type cleaner interface {
    38  	AddCleanup(testing.CleanupFunc)
    39  }
    40  
    41  func (s *BootstrapSuite) SetUpTest(c *gc.C) {
    42  	s.FakeJujuHomeSuite.SetUpTest(c)
    43  	s.ToolsFixture.SetUpTest(c)
    44  	s.PatchValue(common.ConnectSSH, func(_ ssh.Client, host, checkHostScript string) error {
    45  		return fmt.Errorf("mock connection failure to %s", host)
    46  	})
    47  }
    48  
    49  func (s *BootstrapSuite) TearDownTest(c *gc.C) {
    50  	s.ToolsFixture.TearDownTest(c)
    51  	s.FakeJujuHomeSuite.TearDownTest(c)
    52  }
    53  
    54  func newStorage(suite cleaner, c *gc.C) storage.Storage {
    55  	closer, stor, _ := envtesting.CreateLocalTestStorage(c)
    56  	suite.AddCleanup(func(*gc.C) { closer.Close() })
    57  	envtesting.UploadFakeTools(c, stor)
    58  	return stor
    59  }
    60  
    61  func minimalConfig(c *gc.C) *config.Config {
    62  	attrs := map[string]interface{}{
    63  		"name":           "whatever",
    64  		"type":           "anything, really",
    65  		"ca-cert":        coretesting.CACert,
    66  		"ca-private-key": coretesting.CAKey,
    67  	}
    68  	cfg, err := config.New(config.UseDefaults, attrs)
    69  	c.Assert(err, gc.IsNil)
    70  	return cfg
    71  }
    72  
    73  func configGetter(c *gc.C) configFunc {
    74  	cfg := minimalConfig(c)
    75  	return func() *config.Config { return cfg }
    76  }
    77  
    78  func (s *BootstrapSuite) TestCannotStartInstance(c *gc.C) {
    79  	checkPlacement := "directive"
    80  	checkCons := constraints.MustParse("mem=8G")
    81  
    82  	startInstance := func(
    83  		placement string, cons constraints.Value, _ []string, possibleTools tools.List, mcfg *cloudinit.MachineConfig,
    84  	) (
    85  		instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
    86  	) {
    87  		c.Assert(placement, gc.DeepEquals, checkPlacement)
    88  		c.Assert(cons, gc.DeepEquals, checkCons)
    89  		c.Assert(mcfg, gc.DeepEquals, environs.NewBootstrapMachineConfig(mcfg.SystemPrivateSSHKey))
    90  		return nil, nil, nil, fmt.Errorf("meh, not started")
    91  	}
    92  
    93  	env := &mockEnviron{
    94  		storage:       newStorage(s, c),
    95  		startInstance: startInstance,
    96  		config:        configGetter(c),
    97  	}
    98  
    99  	ctx := coretesting.Context(c)
   100  	err := common.Bootstrap(ctx, env, environs.BootstrapParams{
   101  		Constraints: checkCons,
   102  		Placement:   checkPlacement,
   103  	})
   104  	c.Assert(err, gc.ErrorMatches, "cannot start bootstrap instance: meh, not started")
   105  }
   106  
   107  func (s *BootstrapSuite) TestCannotRecordStartedInstance(c *gc.C) {
   108  	innerStorage := newStorage(s, c)
   109  	stor := &mockStorage{Storage: innerStorage}
   110  
   111  	startInstance := func(
   112  		_ string, _ constraints.Value, _ []string, _ tools.List, _ *cloudinit.MachineConfig,
   113  	) (
   114  		instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
   115  	) {
   116  		stor.putErr = fmt.Errorf("suddenly a wild blah")
   117  		return &mockInstance{id: "i-blah"}, nil, nil, nil
   118  	}
   119  
   120  	var stopped []instance.Id
   121  	stopInstances := func(ids []instance.Id) error {
   122  		stopped = append(stopped, ids...)
   123  		return nil
   124  	}
   125  
   126  	env := &mockEnviron{
   127  		storage:       stor,
   128  		startInstance: startInstance,
   129  		stopInstances: stopInstances,
   130  		config:        configGetter(c),
   131  	}
   132  
   133  	ctx := coretesting.Context(c)
   134  	err := common.Bootstrap(ctx, env, environs.BootstrapParams{})
   135  	c.Assert(err, gc.ErrorMatches, "cannot save state: suddenly a wild blah")
   136  	c.Assert(stopped, gc.HasLen, 1)
   137  	c.Assert(stopped[0], gc.Equals, instance.Id("i-blah"))
   138  }
   139  
   140  func (s *BootstrapSuite) TestCannotRecordThenCannotStop(c *gc.C) {
   141  	innerStorage := newStorage(s, c)
   142  	stor := &mockStorage{Storage: innerStorage}
   143  
   144  	startInstance := func(
   145  		_ string, _ constraints.Value, _ []string, _ tools.List, _ *cloudinit.MachineConfig,
   146  	) (
   147  		instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
   148  	) {
   149  		stor.putErr = fmt.Errorf("suddenly a wild blah")
   150  		return &mockInstance{id: "i-blah"}, nil, nil, nil
   151  	}
   152  
   153  	var stopped []instance.Id
   154  	stopInstances := func(instances []instance.Id) error {
   155  		stopped = append(stopped, instances...)
   156  		return fmt.Errorf("bork bork borken")
   157  	}
   158  
   159  	tw := &loggo.TestWriter{}
   160  	c.Assert(loggo.RegisterWriter("bootstrap-tester", tw, loggo.DEBUG), gc.IsNil)
   161  	defer loggo.RemoveWriter("bootstrap-tester")
   162  
   163  	env := &mockEnviron{
   164  		storage:       stor,
   165  		startInstance: startInstance,
   166  		stopInstances: stopInstances,
   167  		config:        configGetter(c),
   168  	}
   169  
   170  	ctx := coretesting.Context(c)
   171  	err := common.Bootstrap(ctx, env, environs.BootstrapParams{})
   172  	c.Assert(err, gc.ErrorMatches, "cannot save state: suddenly a wild blah")
   173  	c.Assert(stopped, gc.HasLen, 1)
   174  	c.Assert(stopped[0], gc.Equals, instance.Id("i-blah"))
   175  	c.Assert(tw.Log, jc.LogMatches, []jc.SimpleMessage{{
   176  		loggo.ERROR, `cannot stop failed bootstrap instance "i-blah": bork bork borken`,
   177  	}})
   178  }
   179  
   180  func (s *BootstrapSuite) TestSuccess(c *gc.C) {
   181  	stor := newStorage(s, c)
   182  	checkInstanceId := "i-success"
   183  	checkHardware := instance.MustParseHardware("mem=2T")
   184  
   185  	startInstance := func(
   186  		_ string, _ constraints.Value, _ []string, _ tools.List, mcfg *cloudinit.MachineConfig,
   187  	) (
   188  		instance.Instance, *instance.HardwareCharacteristics, []network.Info, error,
   189  	) {
   190  		return &mockInstance{id: checkInstanceId}, &checkHardware, nil, nil
   191  	}
   192  	var mocksConfig = minimalConfig(c)
   193  	var getConfigCalled int
   194  	getConfig := func() *config.Config {
   195  		getConfigCalled++
   196  		return mocksConfig
   197  	}
   198  	setConfig := func(c *config.Config) error {
   199  		mocksConfig = c
   200  		return nil
   201  	}
   202  
   203  	restore := envtesting.DisableFinishBootstrap()
   204  	defer restore()
   205  
   206  	env := &mockEnviron{
   207  		storage:       stor,
   208  		startInstance: startInstance,
   209  		config:        getConfig,
   210  		setConfig:     setConfig,
   211  	}
   212  	originalAuthKeys := env.Config().AuthorizedKeys()
   213  	ctx := coretesting.Context(c)
   214  	err := common.Bootstrap(ctx, env, environs.BootstrapParams{})
   215  	c.Assert(err, gc.IsNil)
   216  
   217  	authKeys := env.Config().AuthorizedKeys()
   218  	c.Assert(authKeys, gc.Not(gc.Equals), originalAuthKeys)
   219  	c.Assert(authKeys, jc.HasSuffix, "juju-system-key\n")
   220  }
   221  
   222  type neverRefreshes struct {
   223  }
   224  
   225  func (neverRefreshes) Refresh() error {
   226  	return nil
   227  }
   228  
   229  type neverAddresses struct {
   230  	neverRefreshes
   231  }
   232  
   233  func (neverAddresses) Addresses() ([]network.Address, error) {
   234  	return nil, nil
   235  }
   236  
   237  var testSSHTimeout = config.SSHTimeoutOpts{
   238  	Timeout:        coretesting.ShortWait,
   239  	RetryDelay:     1 * time.Millisecond,
   240  	AddressesDelay: 1 * time.Millisecond,
   241  }
   242  
   243  func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForAddresses(c *gc.C) {
   244  	ctx := coretesting.Context(c)
   245  	_, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
   246  	c.Check(err, gc.ErrorMatches, `waited for `+testSSHTimeout.Timeout.String()+` without getting any addresses`)
   247  	c.Check(coretesting.Stderr(ctx), gc.Matches, "Waiting for address\n")
   248  }
   249  
   250  func (s *BootstrapSuite) TestWaitSSHKilledWaitingForAddresses(c *gc.C) {
   251  	ctx := coretesting.Context(c)
   252  	interrupted := make(chan os.Signal, 1)
   253  	interrupted <- os.Interrupt
   254  	_, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "/bin/true", neverAddresses{}, testSSHTimeout)
   255  	c.Check(err, gc.ErrorMatches, "interrupted")
   256  	c.Check(coretesting.Stderr(ctx), gc.Matches, "Waiting for address\n")
   257  }
   258  
   259  type brokenAddresses struct {
   260  	neverRefreshes
   261  }
   262  
   263  func (brokenAddresses) Addresses() ([]network.Address, error) {
   264  	return nil, fmt.Errorf("Addresses will never work")
   265  }
   266  
   267  func (s *BootstrapSuite) TestWaitSSHStopsOnBadError(c *gc.C) {
   268  	ctx := coretesting.Context(c)
   269  	_, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", brokenAddresses{}, testSSHTimeout)
   270  	c.Check(err, gc.ErrorMatches, "getting addresses: Addresses will never work")
   271  	c.Check(coretesting.Stderr(ctx), gc.Equals, "Waiting for address\n")
   272  }
   273  
   274  type neverOpensPort struct {
   275  	neverRefreshes
   276  	addr string
   277  }
   278  
   279  func (n *neverOpensPort) Addresses() ([]network.Address, error) {
   280  	return network.NewAddresses(n.addr), nil
   281  }
   282  
   283  func (s *BootstrapSuite) TestWaitSSHTimesOutWaitingForDial(c *gc.C) {
   284  	ctx := coretesting.Context(c)
   285  	// 0.x.y.z addresses are always invalid
   286  	_, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "/bin/true", &neverOpensPort{addr: "0.1.2.3"}, testSSHTimeout)
   287  	c.Check(err, gc.ErrorMatches,
   288  		`waited for `+testSSHTimeout.Timeout.String()+` without being able to connect: mock connection failure to 0.1.2.3`)
   289  	c.Check(coretesting.Stderr(ctx), gc.Matches,
   290  		"Waiting for address\n"+
   291  			"(Attempting to connect to 0.1.2.3:22\n)+")
   292  }
   293  
   294  type interruptOnDial struct {
   295  	neverRefreshes
   296  	name        string
   297  	interrupted chan os.Signal
   298  	returned    bool
   299  }
   300  
   301  func (i *interruptOnDial) Addresses() ([]network.Address, error) {
   302  	// kill the tomb the second time Addresses is called
   303  	if !i.returned {
   304  		i.returned = true
   305  	} else {
   306  		i.interrupted <- os.Interrupt
   307  	}
   308  	return []network.Address{network.NewAddress(i.name, network.ScopeUnknown)}, nil
   309  }
   310  
   311  func (s *BootstrapSuite) TestWaitSSHKilledWaitingForDial(c *gc.C) {
   312  	ctx := coretesting.Context(c)
   313  	timeout := testSSHTimeout
   314  	timeout.Timeout = 1 * time.Minute
   315  	interrupted := make(chan os.Signal, 1)
   316  	_, err := common.WaitSSH(ctx, interrupted, ssh.DefaultClient, "", &interruptOnDial{name: "0.1.2.3", interrupted: interrupted}, timeout)
   317  	c.Check(err, gc.ErrorMatches, "interrupted")
   318  	// Exact timing is imprecise but it should have tried a few times before being killed
   319  	c.Check(coretesting.Stderr(ctx), gc.Matches,
   320  		"Waiting for address\n"+
   321  			"(Attempting to connect to 0.1.2.3:22\n)+")
   322  }
   323  
   324  type addressesChange struct {
   325  	addrs [][]string
   326  }
   327  
   328  func (ac *addressesChange) Refresh() error {
   329  	if len(ac.addrs) > 1 {
   330  		ac.addrs = ac.addrs[1:]
   331  	}
   332  	return nil
   333  }
   334  
   335  func (ac *addressesChange) Addresses() ([]network.Address, error) {
   336  	var addrs []network.Address
   337  	for _, addr := range ac.addrs[0] {
   338  		addrs = append(addrs, network.NewAddress(addr, network.ScopeUnknown))
   339  	}
   340  	return addrs, nil
   341  }
   342  
   343  func (s *BootstrapSuite) TestWaitSSHRefreshAddresses(c *gc.C) {
   344  	ctx := coretesting.Context(c)
   345  	_, err := common.WaitSSH(ctx, nil, ssh.DefaultClient, "", &addressesChange{addrs: [][]string{
   346  		nil,
   347  		nil,
   348  		[]string{"0.1.2.3"},
   349  		[]string{"0.1.2.3"},
   350  		nil,
   351  		[]string{"0.1.2.4"},
   352  	}}, testSSHTimeout)
   353  	// Not necessarily the last one in the list, due to scheduling.
   354  	c.Check(err, gc.ErrorMatches,
   355  		`waited for `+testSSHTimeout.Timeout.String()+` without being able to connect: mock connection failure to 0.1.2.[34]`)
   356  	stderr := coretesting.Stderr(ctx)
   357  	c.Check(stderr, gc.Matches,
   358  		"Waiting for address\n"+
   359  			"(.|\n)*(Attempting to connect to 0.1.2.3:22\n)+(.|\n)*")
   360  	c.Check(stderr, gc.Matches,
   361  		"Waiting for address\n"+
   362  			"(.|\n)*(Attempting to connect to 0.1.2.4:22\n)+(.|\n)*")
   363  }