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 }