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 }