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