github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/worker/uniter/uniter_test.go (about) 1 // Copyright 2012-2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package uniter_test 5 6 import ( 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "syscall" 15 "time" 16 17 jc "github.com/juju/testing/checkers" 18 ft "github.com/juju/testing/filetesting" 19 gc "gopkg.in/check.v1" 20 corecharm "gopkg.in/juju/charm.v5" 21 22 "github.com/juju/juju/agent/tools" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/juju/testing" 25 "github.com/juju/juju/state" 26 "github.com/juju/juju/testcharms" 27 coretesting "github.com/juju/juju/testing" 28 "github.com/juju/juju/worker/uniter" 29 ) 30 31 type UniterSuite struct { 32 coretesting.GitSuite 33 testing.JujuConnSuite 34 dataDir string 35 oldLcAll string 36 unitDir string 37 38 metricsTicker *uniter.ManualTicker 39 updateStatusHookTicker *uniter.ManualTicker 40 } 41 42 var _ = gc.Suite(&UniterSuite{}) 43 44 // This guarantees that we get proper platform 45 // specific error directly from their source 46 // This works on both windows and unix 47 var errNotDir = syscall.ENOTDIR.Error() 48 49 func (s *UniterSuite) SetUpSuite(c *gc.C) { 50 s.GitSuite.SetUpSuite(c) 51 s.JujuConnSuite.SetUpSuite(c) 52 s.dataDir = c.MkDir() 53 toolsDir := tools.ToolsDir(s.dataDir, "unit-u-0") 54 err := os.MkdirAll(toolsDir, 0755) 55 c.Assert(err, jc.ErrorIsNil) 56 // TODO(fwereade) GAAAAAAAAAAAAAAAAAH this is LUDICROUS. 57 cmd := exec.Command("go", "build", "github.com/juju/juju/cmd/jujud") 58 cmd.Dir = toolsDir 59 out, err := cmd.CombinedOutput() 60 c.Logf(string(out)) 61 c.Assert(err, jc.ErrorIsNil) 62 s.oldLcAll = os.Getenv("LC_ALL") 63 os.Setenv("LC_ALL", "en_US") 64 s.unitDir = filepath.Join(s.dataDir, "agents", "unit-u-0") 65 } 66 67 func (s *UniterSuite) TearDownSuite(c *gc.C) { 68 os.Setenv("LC_ALL", s.oldLcAll) 69 s.JujuConnSuite.TearDownSuite(c) 70 s.GitSuite.TearDownSuite(c) 71 } 72 73 func (s *UniterSuite) SetUpTest(c *gc.C) { 74 s.metricsTicker = uniter.NewManualTicker() 75 s.updateStatusHookTicker = uniter.NewManualTicker() 76 s.GitSuite.SetUpTest(c) 77 s.JujuConnSuite.SetUpTest(c) 78 s.PatchValue(uniter.IdleWaitTime, 1*time.Millisecond) 79 } 80 81 func (s *UniterSuite) TearDownTest(c *gc.C) { 82 s.ResetContext(c) 83 s.JujuConnSuite.TearDownTest(c) 84 s.GitSuite.TearDownTest(c) 85 } 86 87 func (s *UniterSuite) Reset(c *gc.C) { 88 s.JujuConnSuite.Reset(c) 89 s.ResetContext(c) 90 } 91 92 func (s *UniterSuite) ResetContext(c *gc.C) { 93 err := os.RemoveAll(s.unitDir) 94 c.Assert(err, jc.ErrorIsNil) 95 } 96 97 func (s *UniterSuite) runUniterTests(c *gc.C, uniterTests []uniterTest) { 98 for i, t := range uniterTests { 99 c.Logf("\ntest %d: %s\n", i, t.summary) 100 func() { 101 defer s.Reset(c) 102 env, err := s.State.Environment() 103 c.Assert(err, jc.ErrorIsNil) 104 ctx := &context{ 105 s: s, 106 st: s.State, 107 uuid: env.UUID(), 108 path: s.unitDir, 109 dataDir: s.dataDir, 110 charms: make(map[string][]byte), 111 metricsTicker: s.metricsTicker, 112 updateStatusHookTicker: s.updateStatusHookTicker, 113 } 114 ctx.run(c, t.steps) 115 }() 116 } 117 } 118 119 func (s *UniterSuite) TestUniterStartup(c *gc.C) { 120 s.runUniterTests(c, []uniterTest{ 121 // Check conditions that can cause the uniter to fail to start. 122 ut( 123 "unable to create state dir", 124 writeFile{"state", 0644}, 125 createCharm{}, 126 createServiceAndUnit{}, 127 startUniter{}, 128 waitUniterDead{`failed to initialize uniter for "unit-u-0": .*` + errNotDir}, 129 ), ut( 130 "unknown unit", 131 // We still need to create a unit, because that's when we also 132 // connect to the API, but here we use a different service 133 // (and hence unit) name. 134 createCharm{}, 135 createServiceAndUnit{serviceName: "w"}, 136 startUniter{"unit-u-0"}, 137 waitUniterDead{`failed to initialize uniter for "unit-u-0": permission denied`}, 138 ), 139 }) 140 } 141 142 func (s *UniterSuite) TestUniterBootstrap(c *gc.C) { 143 //TODO(bogdanteleaga): Fix this on windows 144 if runtime.GOOS == "windows" { 145 c.Skip("bug 1403084: currently does not work on windows") 146 } 147 s.runUniterTests(c, []uniterTest{ 148 // Check error conditions during unit bootstrap phase. 149 ut( 150 "insane deployment", 151 createCharm{}, 152 serveCharm{}, 153 writeFile{"charm", 0644}, 154 createUniter{}, 155 waitUniterDead{`ModeInstalling cs:quantal/wordpress-0: executing operation "install cs:quantal/wordpress-0": open .*` + errNotDir}, 156 ), ut( 157 "charm cannot be downloaded", 158 createCharm{}, 159 // don't serve charm 160 createUniter{}, 161 waitUniterDead{`ModeInstalling cs:quantal/wordpress-0: preparing operation "install cs:quantal/wordpress-0": failed to download charm .* 404 Not Found`}, 162 ), 163 }) 164 } 165 166 func (s *UniterSuite) TestUniterInstallHook(c *gc.C) { 167 s.runUniterTests(c, []uniterTest{ 168 ut( 169 "install hook fail and resolve", 170 startupError{"install"}, 171 verifyWaiting{}, 172 173 resolveError{state.ResolvedNoHooks}, 174 waitUnitAgent{ 175 status: params.StatusIdle, 176 }, 177 waitUnitAgent{ 178 statusGetter: unitStatusGetter, 179 status: params.StatusUnknown, 180 }, 181 waitHooks{"leader-elected", "config-changed", "start"}, 182 ), ut( 183 "install hook fail and retry", 184 startupError{"install"}, 185 verifyWaiting{}, 186 187 resolveError{state.ResolvedRetryHooks}, 188 waitUnitAgent{ 189 statusGetter: unitStatusGetter, 190 status: params.StatusError, 191 info: `hook failed: "install"`, 192 data: map[string]interface{}{ 193 "hook": "install", 194 }, 195 }, 196 waitHooks{"fail-install"}, 197 fixHook{"install"}, 198 verifyWaiting{}, 199 200 resolveError{state.ResolvedRetryHooks}, 201 waitUnitAgent{ 202 status: params.StatusIdle, 203 }, 204 waitHooks{"install", "leader-elected", "config-changed", "start"}, 205 ), 206 }) 207 } 208 209 func (s *UniterSuite) TestUniterUpdateStatusHook(c *gc.C) { 210 s.runUniterTests(c, []uniterTest{ 211 ut( 212 "update status hook runs on timer", 213 createCharm{}, 214 serveCharm{}, 215 createUniter{}, 216 waitHooks(startupHooks(false)), 217 waitUnitAgent{status: params.StatusIdle}, 218 updateStatusHookTick{}, 219 waitHooks{"update-status"}, 220 ), 221 }) 222 } 223 224 func (s *UniterSuite) TestUniterStartHook(c *gc.C) { 225 s.runUniterTests(c, []uniterTest{ 226 ut( 227 "start hook fail and resolve", 228 startupError{"start"}, 229 verifyWaiting{}, 230 231 resolveError{state.ResolvedNoHooks}, 232 waitUnitAgent{ 233 status: params.StatusIdle, 234 }, 235 waitUnitAgent{ 236 statusGetter: unitStatusGetter, 237 status: params.StatusMaintenance, 238 info: "installing charm software", 239 }, 240 waitHooks{"config-changed"}, 241 verifyRunning{}, 242 ), ut( 243 "start hook fail and retry", 244 startupError{"start"}, 245 verifyWaiting{}, 246 247 resolveError{state.ResolvedRetryHooks}, 248 waitUnitAgent{ 249 statusGetter: unitStatusGetter, 250 status: params.StatusError, 251 info: `hook failed: "start"`, 252 data: map[string]interface{}{ 253 "hook": "start", 254 }, 255 }, 256 waitHooks{"fail-start"}, 257 verifyWaiting{}, 258 259 fixHook{"start"}, 260 resolveError{state.ResolvedRetryHooks}, 261 waitUnitAgent{ 262 status: params.StatusIdle, 263 }, 264 waitHooks{"start", "config-changed"}, 265 verifyRunning{}, 266 ), 267 }) 268 } 269 270 func (s *UniterSuite) TestUniterMultipleErrors(c *gc.C) { 271 s.runUniterTests(c, []uniterTest{ 272 ut( 273 "resolved is cleared before moving on to next hook", 274 createCharm{badHooks: []string{"install", "leader-elected", "config-changed", "start"}}, 275 serveCharm{}, 276 createUniter{}, 277 waitUnitAgent{ 278 statusGetter: unitStatusGetter, 279 status: params.StatusError, 280 info: `hook failed: "install"`, 281 data: map[string]interface{}{ 282 "hook": "install", 283 }, 284 }, 285 resolveError{state.ResolvedNoHooks}, 286 waitUnitAgent{ 287 statusGetter: unitStatusGetter, 288 status: params.StatusError, 289 info: `hook failed: "leader-elected"`, 290 data: map[string]interface{}{ 291 "hook": "leader-elected", 292 }, 293 }, 294 resolveError{state.ResolvedNoHooks}, 295 waitUnitAgent{ 296 statusGetter: unitStatusGetter, 297 status: params.StatusError, 298 info: `hook failed: "config-changed"`, 299 data: map[string]interface{}{ 300 "hook": "config-changed", 301 }, 302 }, 303 resolveError{state.ResolvedNoHooks}, 304 waitUnitAgent{ 305 statusGetter: unitStatusGetter, 306 status: params.StatusError, 307 info: `hook failed: "start"`, 308 data: map[string]interface{}{ 309 "hook": "start", 310 }, 311 }, 312 ), 313 }) 314 } 315 316 func (s *UniterSuite) TestUniterConfigChangedHook(c *gc.C) { 317 s.runUniterTests(c, []uniterTest{ 318 ut( 319 "config-changed hook fail and resolve", 320 startupError{"config-changed"}, 321 verifyWaiting{}, 322 323 // Note: we'll run another config-changed as soon as we hit the 324 // started state, so the broken hook would actually prevent us 325 // from advancing at all if we didn't fix it. 326 fixHook{"config-changed"}, 327 resolveError{state.ResolvedNoHooks}, 328 waitUnitAgent{ 329 status: params.StatusIdle, 330 }, 331 waitUnitAgent{ 332 statusGetter: unitStatusGetter, 333 status: params.StatusUnknown, 334 }, 335 waitHooks{"start", "config-changed"}, 336 // If we'd accidentally retried that hook, somehow, we would get 337 // an extra config-changed as we entered started; see that we don't. 338 waitHooks{}, 339 verifyRunning{}, 340 ), ut( 341 "config-changed hook fail and retry", 342 startupError{"config-changed"}, 343 verifyWaiting{}, 344 345 resolveError{state.ResolvedRetryHooks}, 346 waitUnitAgent{ 347 statusGetter: unitStatusGetter, 348 status: params.StatusError, 349 info: `hook failed: "config-changed"`, 350 data: map[string]interface{}{ 351 "hook": "config-changed", 352 }, 353 }, 354 waitHooks{"fail-config-changed"}, 355 verifyWaiting{}, 356 357 fixHook{"config-changed"}, 358 resolveError{state.ResolvedRetryHooks}, 359 waitUnitAgent{ 360 status: params.StatusIdle, 361 }, 362 waitHooks{"config-changed", "start"}, 363 verifyRunning{}, 364 ), ut( 365 "steady state config change with config-get verification", 366 createCharm{ 367 customize: func(c *gc.C, ctx *context, path string) { 368 appendHook(c, path, "config-changed", appendConfigChanged) 369 }, 370 }, 371 serveCharm{}, 372 createUniter{}, 373 waitUnitAgent{ 374 status: params.StatusIdle, 375 }, 376 waitHooks{"install", "leader-elected", "config-changed", "start"}, 377 assertYaml{"charm/config.out", map[string]interface{}{ 378 "blog-title": "My Title", 379 }}, 380 changeConfig{"blog-title": "Goodness Gracious Me"}, 381 waitHooks{"config-changed"}, 382 verifyRunning{}, 383 assertYaml{"charm/config.out", map[string]interface{}{ 384 "blog-title": "Goodness Gracious Me", 385 }}, 386 ), 387 }) 388 } 389 390 func (s *UniterSuite) TestUniterHookSynchronisation(c *gc.C) { 391 s.runUniterTests(c, []uniterTest{ 392 ut( 393 "verify config change hook not run while lock held", 394 quickStart{}, 395 acquireHookSyncLock{}, 396 changeConfig{"blog-title": "Goodness Gracious Me"}, 397 waitHooks{}, 398 releaseHookSyncLock, 399 waitHooks{"config-changed"}, 400 ), ut( 401 "verify held lock by this unit is broken", 402 acquireHookSyncLock{"u/0:fake"}, 403 quickStart{}, 404 verifyHookSyncLockUnlocked, 405 ), ut( 406 "verify held lock by another unit is not broken", 407 acquireHookSyncLock{"u/1:fake"}, 408 // Can't use quickstart as it has a built in waitHooks. 409 createCharm{}, 410 serveCharm{}, 411 ensureStateWorker{}, 412 createServiceAndUnit{}, 413 startUniter{}, 414 waitAddresses{}, 415 waitHooks{}, 416 verifyHookSyncLockLocked, 417 releaseHookSyncLock, 418 waitUnitAgent{status: params.StatusIdle}, 419 waitHooks{"install", "leader-elected", "config-changed", "start"}, 420 ), 421 }) 422 } 423 424 func (s *UniterSuite) TestUniterDyingReaction(c *gc.C) { 425 s.runUniterTests(c, []uniterTest{ 426 // Reaction to entity deaths. 427 ut( 428 "steady state service dying", 429 quickStart{}, 430 serviceDying, 431 waitHooks{"stop"}, 432 waitUniterDead{}, 433 ), ut( 434 "steady state unit dying", 435 quickStart{}, 436 unitDying, 437 waitHooks{"stop"}, 438 waitUniterDead{}, 439 ), ut( 440 "steady state unit dead", 441 quickStart{}, 442 unitDead, 443 waitUniterDead{}, 444 waitHooks{}, 445 ), ut( 446 "hook error service dying", 447 startupError{"start"}, 448 serviceDying, 449 verifyWaiting{}, 450 fixHook{"start"}, 451 resolveError{state.ResolvedRetryHooks}, 452 waitHooks{"start", "config-changed", "stop"}, 453 waitUniterDead{}, 454 ), ut( 455 "hook error unit dying", 456 startupError{"start"}, 457 unitDying, 458 verifyWaiting{}, 459 fixHook{"start"}, 460 resolveError{state.ResolvedRetryHooks}, 461 waitHooks{"start", "config-changed", "stop"}, 462 waitUniterDead{}, 463 ), ut( 464 "hook error unit dead", 465 startupError{"start"}, 466 unitDead, 467 waitUniterDead{}, 468 waitHooks{}, 469 ), 470 }) 471 } 472 473 func (s *UniterSuite) TestUniterSteadyStateUpgrade(c *gc.C) { 474 s.runUniterTests(c, []uniterTest{ 475 // Upgrade scenarios from steady state. 476 ut( 477 "steady state upgrade", 478 quickStart{}, 479 createCharm{revision: 1}, 480 upgradeCharm{revision: 1}, 481 waitUnitAgent{ 482 status: params.StatusIdle, 483 charm: 1, 484 }, 485 waitUnitAgent{ 486 statusGetter: unitStatusGetter, 487 status: params.StatusUnknown, 488 charm: 1, 489 }, 490 waitHooks{"upgrade-charm", "config-changed"}, 491 verifyCharm{revision: 1}, 492 verifyRunning{}, 493 ), ut( 494 "steady state forced upgrade (identical behaviour)", 495 quickStart{}, 496 createCharm{revision: 1}, 497 upgradeCharm{revision: 1, forced: true}, 498 waitUnitAgent{ 499 status: params.StatusIdle, 500 charm: 1, 501 }, 502 waitUnitAgent{ 503 statusGetter: unitStatusGetter, 504 status: params.StatusUnknown, 505 charm: 1, 506 }, 507 waitHooks{"upgrade-charm", "config-changed"}, 508 verifyCharm{revision: 1}, 509 verifyRunning{}, 510 ), ut( 511 "steady state upgrade hook fail and resolve", 512 quickStart{}, 513 createCharm{revision: 1, badHooks: []string{"upgrade-charm"}}, 514 upgradeCharm{revision: 1}, 515 waitUnitAgent{ 516 statusGetter: unitStatusGetter, 517 status: params.StatusError, 518 info: `hook failed: "upgrade-charm"`, 519 data: map[string]interface{}{ 520 "hook": "upgrade-charm", 521 }, 522 charm: 1, 523 }, 524 waitHooks{"fail-upgrade-charm"}, 525 verifyCharm{revision: 1}, 526 verifyWaiting{}, 527 528 resolveError{state.ResolvedNoHooks}, 529 waitUnitAgent{ 530 status: params.StatusIdle, 531 charm: 1, 532 }, 533 waitUnitAgent{ 534 statusGetter: unitStatusGetter, 535 status: params.StatusUnknown, 536 charm: 1, 537 }, 538 waitHooks{"config-changed"}, 539 verifyRunning{}, 540 ), ut( 541 "steady state upgrade hook fail and retry", 542 quickStart{}, 543 createCharm{revision: 1, badHooks: []string{"upgrade-charm"}}, 544 upgradeCharm{revision: 1}, 545 waitUnitAgent{ 546 statusGetter: unitStatusGetter, 547 status: params.StatusError, 548 info: `hook failed: "upgrade-charm"`, 549 data: map[string]interface{}{ 550 "hook": "upgrade-charm", 551 }, 552 charm: 1, 553 }, 554 waitHooks{"fail-upgrade-charm"}, 555 verifyCharm{revision: 1}, 556 verifyWaiting{}, 557 558 resolveError{state.ResolvedRetryHooks}, 559 waitUnitAgent{ 560 statusGetter: unitStatusGetter, 561 status: params.StatusError, 562 info: `hook failed: "upgrade-charm"`, 563 data: map[string]interface{}{ 564 "hook": "upgrade-charm", 565 }, 566 charm: 1, 567 }, 568 waitHooks{"fail-upgrade-charm"}, 569 verifyWaiting{}, 570 571 fixHook{"upgrade-charm"}, 572 resolveError{state.ResolvedRetryHooks}, 573 waitUnitAgent{ 574 status: params.StatusIdle, 575 charm: 1, 576 }, 577 waitHooks{"upgrade-charm", "config-changed"}, 578 verifyRunning{}, 579 ), ut( 580 // This test does an add-relation as quickly as possible 581 // after an upgrade-charm, in the hope that the scheduler will 582 // deliver the events in the wrong order. The observed 583 // behaviour should be the same in either case. 584 "ignore unknown relations until upgrade is done", 585 quickStart{}, 586 createCharm{ 587 revision: 2, 588 customize: func(c *gc.C, ctx *context, path string) { 589 renameRelation(c, path, "db", "db2") 590 hpath := filepath.Join(path, "hooks", "db2-relation-joined") 591 ctx.writeHook(c, hpath, true) 592 }, 593 }, 594 serveCharm{}, 595 upgradeCharm{revision: 2}, 596 addRelation{}, 597 addRelationUnit{}, 598 waitHooks{"upgrade-charm", "config-changed", "db2-relation-joined mysql/0 db2:0"}, 599 verifyCharm{revision: 2}, 600 ), 601 }) 602 } 603 604 func (s *UniterSuite) TestUniterUpgradeOverwrite(c *gc.C) { 605 //TODO(bogdanteleaga): Fix this on windows 606 if runtime.GOOS == "windows" { 607 c.Skip("bug 1403084: currently does not work on windows") 608 } 609 makeTest := func(description string, content, extraChecks ft.Entries) uniterTest { 610 return ut(description, 611 createCharm{ 612 // This is the base charm which all upgrade tests start out running. 613 customize: func(c *gc.C, ctx *context, path string) { 614 ft.Entries{ 615 ft.Dir{"dir", 0755}, 616 ft.File{"file", "blah", 0644}, 617 ft.Symlink{"symlink", "file"}, 618 }.Create(c, path) 619 // Note that it creates "dir/user-file" at runtime, which may be 620 // preserved or removed depending on the test. 621 script := "echo content > dir/user-file && chmod 755 dir/user-file" 622 appendHook(c, path, "start", script) 623 }, 624 }, 625 serveCharm{}, 626 createUniter{}, 627 waitUnitAgent{ 628 status: params.StatusIdle, 629 }, 630 waitUnitAgent{ 631 statusGetter: unitStatusGetter, 632 status: params.StatusUnknown, 633 }, 634 waitHooks{"install", "leader-elected", "config-changed", "start"}, 635 636 createCharm{ 637 revision: 1, 638 customize: func(c *gc.C, _ *context, path string) { 639 content.Create(c, path) 640 }, 641 }, 642 serveCharm{}, 643 upgradeCharm{revision: 1}, 644 waitUnitAgent{ 645 status: params.StatusIdle, 646 charm: 1, 647 }, 648 waitUnitAgent{ 649 statusGetter: unitStatusGetter, 650 status: params.StatusUnknown, 651 charm: 1, 652 }, 653 waitHooks{"upgrade-charm", "config-changed"}, 654 verifyCharm{revision: 1}, 655 custom{func(c *gc.C, ctx *context) { 656 path := filepath.Join(ctx.path, "charm") 657 content.Check(c, path) 658 extraChecks.Check(c, path) 659 }}, 660 verifyRunning{}, 661 ) 662 } 663 664 s.runUniterTests(c, []uniterTest{ 665 makeTest( 666 "files overwite files, dirs, symlinks", 667 ft.Entries{ 668 ft.File{"file", "new", 0755}, 669 ft.File{"dir", "new", 0755}, 670 ft.File{"symlink", "new", 0755}, 671 }, 672 ft.Entries{ 673 ft.Removed{"dir/user-file"}, 674 }, 675 ), makeTest( 676 "symlinks overwite files, dirs, symlinks", 677 ft.Entries{ 678 ft.Symlink{"file", "new"}, 679 ft.Symlink{"dir", "new"}, 680 ft.Symlink{"symlink", "new"}, 681 }, 682 ft.Entries{ 683 ft.Removed{"dir/user-file"}, 684 }, 685 ), makeTest( 686 "dirs overwite files, symlinks; merge dirs", 687 ft.Entries{ 688 ft.Dir{"file", 0755}, 689 ft.Dir{"dir", 0755}, 690 ft.File{"dir/charm-file", "charm-content", 0644}, 691 ft.Dir{"symlink", 0755}, 692 }, 693 ft.Entries{ 694 ft.File{"dir/user-file", "content\n", 0755}, 695 }, 696 ), 697 }) 698 } 699 700 func (s *UniterSuite) TestUniterErrorStateUpgrade(c *gc.C) { 701 s.runUniterTests(c, []uniterTest{ 702 // Upgrade scenarios from error state. 703 ut( 704 "error state unforced upgrade (ignored until started state)", 705 startupError{"start"}, 706 createCharm{revision: 1}, 707 upgradeCharm{revision: 1}, 708 waitUnitAgent{ 709 statusGetter: unitStatusGetter, 710 status: params.StatusError, 711 info: `hook failed: "start"`, 712 data: map[string]interface{}{ 713 "hook": "start", 714 }, 715 }, 716 waitHooks{}, 717 verifyCharm{}, 718 verifyWaiting{}, 719 720 resolveError{state.ResolvedNoHooks}, 721 waitUnitAgent{ 722 status: params.StatusIdle, 723 charm: 1, 724 }, 725 waitUnitAgent{ 726 statusGetter: unitStatusGetter, 727 status: params.StatusMaintenance, 728 info: "installing charm software", 729 charm: 1, 730 }, 731 waitHooks{"config-changed", "upgrade-charm", "config-changed"}, 732 verifyCharm{revision: 1}, 733 verifyRunning{}, 734 ), ut( 735 "error state forced upgrade", 736 startupError{"start"}, 737 createCharm{revision: 1}, 738 upgradeCharm{revision: 1, forced: true}, 739 // It's not possible to tell directly from state when the upgrade is 740 // complete, because the new unit charm URL is set at the upgrade 741 // process's point of no return (before actually deploying, but after 742 // the charm has been downloaded and verified). However, it's still 743 // useful to wait until that point... 744 waitUnitAgent{ 745 statusGetter: unitStatusGetter, 746 status: params.StatusError, 747 info: `hook failed: "start"`, 748 data: map[string]interface{}{ 749 "hook": "start", 750 }, 751 charm: 1, 752 }, 753 // ...because the uniter *will* complete a started deployment even if 754 // we stop it from outside. So, by stopping and starting, we can be 755 // sure that the operation has completed and can safely verify that 756 // the charm state on disk is as we expect. 757 verifyWaiting{}, 758 verifyCharm{revision: 1}, 759 760 resolveError{state.ResolvedNoHooks}, 761 waitUnitAgent{ 762 status: params.StatusIdle, 763 charm: 1, 764 }, 765 waitHooks{"config-changed"}, 766 verifyRunning{}, 767 ), 768 }) 769 } 770 771 func (s *UniterSuite) TestUniterDeployerConversion(c *gc.C) { 772 coretesting.SkipIfGitNotAvailable(c) 773 774 deployerConversionTests := []uniterTest{ 775 ut( 776 "install normally, check not using git", 777 quickStart{}, 778 verifyCharm{ 779 checkFiles: ft.Entries{ft.Removed{".git"}}, 780 }, 781 ), ut( 782 "install with git, restart in steady state", 783 prepareGitUniter{[]stepper{ 784 quickStart{}, 785 verifyGitCharm{}, 786 stopUniter{}, 787 }}, 788 startUniter{}, 789 waitHooks{"config-changed"}, 790 791 // At this point, the deployer has been converted, but the 792 // charm directory itself hasn't; the *next* deployment will 793 // actually hit the charm directory and strip out the git 794 // stuff. 795 createCharm{revision: 1}, 796 upgradeCharm{revision: 1}, 797 waitHooks{"upgrade-charm", "config-changed"}, 798 waitUnitAgent{ 799 status: params.StatusIdle, 800 charm: 1, 801 }, 802 waitUnitAgent{ 803 statusGetter: unitStatusGetter, 804 status: params.StatusUnknown, 805 charm: 1, 806 }, 807 verifyCharm{ 808 revision: 1, 809 checkFiles: ft.Entries{ft.Removed{".git"}}, 810 }, 811 verifyRunning{}, 812 ), ut( 813 "install with git, get conflicted, mark resolved", 814 prepareGitUniter{[]stepper{ 815 startGitUpgradeError{}, 816 stopUniter{}, 817 }}, 818 startUniter{}, 819 820 resolveError{state.ResolvedNoHooks}, 821 waitHooks{"upgrade-charm", "config-changed"}, 822 waitUnitAgent{ 823 status: params.StatusIdle, 824 charm: 1, 825 }, 826 waitUnitAgent{ 827 statusGetter: unitStatusGetter, 828 status: params.StatusUnknown, 829 charm: 1, 830 }, 831 verifyCharm{revision: 1}, 832 verifyRunning{}, 833 834 // Due to the uncertainties around marking upgrade conflicts resolved, 835 // the charm directory again remains unconverted, although the deployer 836 // should have been fixed. Again, we check this by running another 837 // upgrade and verifying the .git dir is then removed. 838 createCharm{revision: 2}, 839 upgradeCharm{revision: 2}, 840 waitHooks{"upgrade-charm", "config-changed"}, 841 waitUnitAgent{ 842 status: params.StatusIdle, 843 charm: 2, 844 }, 845 waitUnitAgent{ 846 statusGetter: unitStatusGetter, 847 status: params.StatusUnknown, 848 charm: 2, 849 }, 850 verifyCharm{ 851 revision: 2, 852 checkFiles: ft.Entries{ft.Removed{".git"}}, 853 }, 854 verifyRunning{}, 855 ), ut( 856 "install with git, get conflicted, force an upgrade", 857 prepareGitUniter{[]stepper{ 858 startGitUpgradeError{}, 859 stopUniter{}, 860 }}, 861 startUniter{}, 862 863 createCharm{ 864 revision: 2, 865 customize: func(c *gc.C, ctx *context, path string) { 866 ft.File{"data", "OVERWRITE!", 0644}.Create(c, path) 867 }, 868 }, 869 serveCharm{}, 870 upgradeCharm{revision: 2, forced: true}, 871 waitHooks{"upgrade-charm", "config-changed"}, 872 waitUnitAgent{ 873 status: params.StatusIdle, 874 charm: 2, 875 }, 876 877 // A forced upgrade allows us to swap out the git deployer *and* 878 // the .git dir inside the charm immediately; check we did so. 879 verifyCharm{ 880 revision: 2, 881 checkFiles: ft.Entries{ 882 ft.Removed{".git"}, 883 ft.File{"data", "OVERWRITE!", 0644}, 884 }, 885 }, 886 verifyRunning{}, 887 ), 888 } 889 s.runUniterTests(c, deployerConversionTests) 890 } 891 892 func (s *UniterSuite) TestUniterUpgradeConflicts(c *gc.C) { 893 coretesting.SkipIfPPC64EL(c, "lp:1448308") 894 //TODO(bogdanteleaga): Fix this on windows 895 if runtime.GOOS == "windows" { 896 c.Skip("bug 1403084: currently does not work on windows") 897 } 898 s.runUniterTests(c, []uniterTest{ 899 // Upgrade scenarios - handling conflicts. 900 ut( 901 "upgrade: resolving doesn't help until underlying problem is fixed", 902 startUpgradeError{}, 903 resolveError{state.ResolvedNoHooks}, 904 verifyWaitingUpgradeError{revision: 1}, 905 fixUpgradeError{}, 906 resolveError{state.ResolvedNoHooks}, 907 waitHooks{"upgrade-charm", "config-changed"}, 908 waitUnitAgent{ 909 status: params.StatusIdle, 910 charm: 1, 911 }, 912 waitUnitAgent{ 913 statusGetter: unitStatusGetter, 914 status: params.StatusUnknown, 915 charm: 1, 916 }, 917 verifyCharm{revision: 1}, 918 ), ut( 919 `upgrade: forced upgrade does work without explicit resolution if underlying problem was fixed`, 920 startUpgradeError{}, 921 resolveError{state.ResolvedNoHooks}, 922 verifyWaitingUpgradeError{revision: 1}, 923 fixUpgradeError{}, 924 createCharm{revision: 2}, 925 serveCharm{}, 926 upgradeCharm{revision: 2, forced: true}, 927 waitHooks{"upgrade-charm", "config-changed"}, 928 waitUnitAgent{ 929 status: params.StatusIdle, 930 charm: 2, 931 }, 932 waitUnitAgent{ 933 statusGetter: unitStatusGetter, 934 status: params.StatusUnknown, 935 charm: 2, 936 }, 937 verifyCharm{revision: 2}, 938 ), ut( 939 "upgrade conflict service dying", 940 startUpgradeError{}, 941 serviceDying, 942 verifyWaitingUpgradeError{revision: 1}, 943 fixUpgradeError{}, 944 resolveError{state.ResolvedNoHooks}, 945 waitHooks{"upgrade-charm", "config-changed", "stop"}, 946 waitUniterDead{}, 947 ), ut( 948 "upgrade conflict unit dying", 949 startUpgradeError{}, 950 unitDying, 951 verifyWaitingUpgradeError{revision: 1}, 952 fixUpgradeError{}, 953 resolveError{state.ResolvedNoHooks}, 954 waitHooks{"upgrade-charm", "config-changed", "stop"}, 955 waitUniterDead{}, 956 ), ut( 957 "upgrade conflict unit dead", 958 startUpgradeError{}, 959 unitDead, 960 waitUniterDead{}, 961 waitHooks{}, 962 fixUpgradeError{}, 963 ), 964 }) 965 } 966 967 func (s *UniterSuite) TestUniterUpgradeGitConflicts(c *gc.C) { 968 coretesting.SkipIfGitNotAvailable(c) 969 970 // These tests are copies of the old git-deployer-related tests, to test that 971 // the uniter with the manifest-deployer work patched out still works how it 972 // used to; thus demonstrating that the *other* tests that verify manifest 973 // deployer behaviour in the presence of an old git deployer are working against 974 // an accurate representation of the base state. 975 // The only actual behaviour change is that we no longer commit changes after 976 // each hook execution; this is reflected by checking that it's dirty in a couple 977 // of places where we once checked it was not. 978 979 s.runUniterTests(c, []uniterTest{ 980 // Upgrade scenarios - handling conflicts. 981 ugt( 982 "upgrade: conflicting files", 983 startGitUpgradeError{}, 984 985 // NOTE: this is just dumbly committing the conflicts, but AFAICT this 986 // is the only reasonable solution; if the user tells us it's resolved 987 // we have to take their word for it. 988 resolveError{state.ResolvedNoHooks}, 989 waitHooks{"upgrade-charm", "config-changed"}, 990 waitUnitAgent{ 991 status: params.StatusIdle, 992 charm: 1, 993 }, 994 waitUnitAgent{ 995 statusGetter: unitStatusGetter, 996 status: params.StatusUnknown, 997 charm: 1, 998 }, 999 verifyGitCharm{revision: 1}, 1000 ), ugt( 1001 `upgrade: conflicting directories`, 1002 createCharm{ 1003 customize: func(c *gc.C, ctx *context, path string) { 1004 err := os.Mkdir(filepath.Join(path, "data"), 0755) 1005 c.Assert(err, jc.ErrorIsNil) 1006 appendHook(c, path, "start", "echo DATA > data/newfile") 1007 }, 1008 }, 1009 serveCharm{}, 1010 createUniter{}, 1011 waitUnitAgent{ 1012 status: params.StatusIdle, 1013 }, 1014 waitUnitAgent{ 1015 statusGetter: unitStatusGetter, 1016 status: params.StatusUnknown, 1017 }, 1018 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1019 verifyGitCharm{dirty: true}, 1020 1021 createCharm{ 1022 revision: 1, 1023 customize: func(c *gc.C, ctx *context, path string) { 1024 data := filepath.Join(path, "data") 1025 err := ioutil.WriteFile(data, []byte("<nelson>ha ha</nelson>"), 0644) 1026 c.Assert(err, jc.ErrorIsNil) 1027 }, 1028 }, 1029 serveCharm{}, 1030 upgradeCharm{revision: 1}, 1031 waitUnitAgent{ 1032 statusGetter: unitStatusGetter, 1033 status: params.StatusError, 1034 info: "upgrade failed", 1035 charm: 1, 1036 }, 1037 verifyWaiting{}, 1038 verifyGitCharm{dirty: true}, 1039 1040 resolveError{state.ResolvedNoHooks}, 1041 waitHooks{"upgrade-charm", "config-changed"}, 1042 waitUnitAgent{ 1043 status: params.StatusIdle, 1044 charm: 1, 1045 }, 1046 verifyGitCharm{revision: 1}, 1047 ), ugt( 1048 "upgrade conflict resolved with forced upgrade", 1049 startGitUpgradeError{}, 1050 createCharm{ 1051 revision: 2, 1052 customize: func(c *gc.C, ctx *context, path string) { 1053 otherdata := filepath.Join(path, "otherdata") 1054 err := ioutil.WriteFile(otherdata, []byte("blah"), 0644) 1055 c.Assert(err, jc.ErrorIsNil) 1056 }, 1057 }, 1058 serveCharm{}, 1059 upgradeCharm{revision: 2, forced: true}, 1060 waitUnitAgent{ 1061 status: params.StatusIdle, 1062 charm: 2, 1063 }, waitUnitAgent{ 1064 statusGetter: unitStatusGetter, 1065 status: params.StatusUnknown, 1066 charm: 2, 1067 }, 1068 waitHooks{"upgrade-charm", "config-changed"}, 1069 verifyGitCharm{revision: 2}, 1070 custom{func(c *gc.C, ctx *context) { 1071 // otherdata should exist (in v2) 1072 otherdata, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "otherdata")) 1073 c.Assert(err, jc.ErrorIsNil) 1074 c.Assert(string(otherdata), gc.Equals, "blah") 1075 1076 // ignore should not (only in v1) 1077 _, err = os.Stat(filepath.Join(ctx.path, "charm", "ignore")) 1078 c.Assert(err, jc.Satisfies, os.IsNotExist) 1079 1080 // data should contain what was written in the start hook 1081 data, err := ioutil.ReadFile(filepath.Join(ctx.path, "charm", "data")) 1082 c.Assert(err, jc.ErrorIsNil) 1083 c.Assert(string(data), gc.Equals, "STARTDATA\n") 1084 }}, 1085 ), ugt( 1086 "upgrade conflict service dying", 1087 startGitUpgradeError{}, 1088 serviceDying, 1089 verifyWaiting{}, 1090 resolveError{state.ResolvedNoHooks}, 1091 waitHooks{"upgrade-charm", "config-changed", "stop"}, 1092 waitUniterDead{}, 1093 ), ugt( 1094 "upgrade conflict unit dying", 1095 startGitUpgradeError{}, 1096 unitDying, 1097 verifyWaiting{}, 1098 resolveError{state.ResolvedNoHooks}, 1099 waitHooks{"upgrade-charm", "config-changed", "stop"}, 1100 waitUniterDead{}, 1101 ), ugt( 1102 "upgrade conflict unit dead", 1103 startGitUpgradeError{}, 1104 unitDead, 1105 waitUniterDead{}, 1106 waitHooks{}, 1107 ), 1108 }) 1109 } 1110 1111 func (s *UniterSuite) TestUniterRelations(c *gc.C) { 1112 waitDyingHooks := custom{func(c *gc.C, ctx *context) { 1113 // There is no ordering relationship between relation hooks and 1114 // leader-settings-changed hooks; and while we're dying we may 1115 // never get to leader-settings-changed before it's time to run 1116 // the stop (as we might not react to a config change in time). 1117 // It's actually clearer to just list the possible orders: 1118 possibles := [][]string{{ 1119 "leader-settings-changed", 1120 "db-relation-departed mysql/0 db:0", 1121 "db-relation-broken db:0", 1122 "stop", 1123 }, { 1124 "db-relation-departed mysql/0 db:0", 1125 "leader-settings-changed", 1126 "db-relation-broken db:0", 1127 "stop", 1128 }, { 1129 "db-relation-departed mysql/0 db:0", 1130 "db-relation-broken db:0", 1131 "leader-settings-changed", 1132 "stop", 1133 }, { 1134 "db-relation-departed mysql/0 db:0", 1135 "db-relation-broken db:0", 1136 "stop", 1137 }} 1138 unchecked := ctx.hooksCompleted[len(ctx.hooks):] 1139 for _, possible := range possibles { 1140 if ok, _ := jc.DeepEqual(unchecked, possible); ok { 1141 return 1142 } 1143 } 1144 c.Fatalf("unexpected hooks: %v", unchecked) 1145 }} 1146 s.runUniterTests(c, []uniterTest{ 1147 // Relations. 1148 ut( 1149 "simple joined/changed/departed", 1150 quickStartRelation{}, 1151 addRelationUnit{}, 1152 waitHooks{ 1153 "db-relation-joined mysql/1 db:0", 1154 "db-relation-changed mysql/1 db:0", 1155 }, 1156 changeRelationUnit{"mysql/0"}, 1157 waitHooks{"db-relation-changed mysql/0 db:0"}, 1158 removeRelationUnit{"mysql/1"}, 1159 waitHooks{"db-relation-departed mysql/1 db:0"}, 1160 verifyRunning{}, 1161 ), ut( 1162 "relation becomes dying; unit is not last remaining member", 1163 quickStartRelation{}, 1164 relationDying, 1165 waitHooks{ 1166 "db-relation-departed mysql/0 db:0", 1167 "db-relation-broken db:0", 1168 }, 1169 verifyRunning{}, 1170 relationState{life: state.Dying}, 1171 removeRelationUnit{"mysql/0"}, 1172 verifyRunning{}, 1173 relationState{removed: true}, 1174 verifyRunning{}, 1175 ), ut( 1176 "relation becomes dying; unit is last remaining member", 1177 quickStartRelation{}, 1178 removeRelationUnit{"mysql/0"}, 1179 waitHooks{"db-relation-departed mysql/0 db:0"}, 1180 relationDying, 1181 waitHooks{"db-relation-broken db:0"}, 1182 verifyRunning{}, 1183 relationState{removed: true}, 1184 verifyRunning{}, 1185 ), ut( 1186 "service becomes dying while in a relation", 1187 quickStartRelation{}, 1188 serviceDying, 1189 waitUniterDead{}, 1190 waitDyingHooks, 1191 relationState{life: state.Dying}, 1192 removeRelationUnit{"mysql/0"}, 1193 relationState{removed: true}, 1194 ), ut( 1195 "unit becomes dying while in a relation", 1196 quickStartRelation{}, 1197 unitDying, 1198 waitUniterDead{}, 1199 waitDyingHooks, 1200 relationState{life: state.Alive}, 1201 removeRelationUnit{"mysql/0"}, 1202 relationState{life: state.Alive}, 1203 ), ut( 1204 "unit becomes dead while in a relation", 1205 quickStartRelation{}, 1206 unitDead, 1207 waitUniterDead{}, 1208 waitHooks{}, 1209 // TODO BUG(?): the unit doesn't leave the scope, leaving the relation 1210 // unkillable without direct intervention. I'm pretty sure it's not a 1211 // uniter bug -- it should be the responsibility of `juju remove-unit 1212 // --force` to cause the unit to leave any relation scopes it may be 1213 // in -- but it's worth noting here all the same. 1214 ), ut( 1215 "unknown local relation dir is removed", 1216 quickStartRelation{}, 1217 stopUniter{}, 1218 custom{func(c *gc.C, ctx *context) { 1219 ft.Dir{"state/relations/90210", 0755}.Create(c, ctx.path) 1220 }}, 1221 startUniter{}, 1222 waitHooks{"config-changed"}, 1223 custom{func(c *gc.C, ctx *context) { 1224 ft.Removed{"state/relations/90210"}.Check(c, ctx.path) 1225 }}, 1226 ), ut( 1227 "all relations are available to config-changed on bounce, even if state dir is missing", 1228 createCharm{ 1229 customize: func(c *gc.C, ctx *context, path string) { 1230 script := uniterRelationsCustomizeScript 1231 appendHook(c, path, "config-changed", script) 1232 }, 1233 }, 1234 serveCharm{}, 1235 createUniter{}, 1236 waitUnitAgent{ 1237 status: params.StatusIdle, 1238 }, 1239 waitUnitAgent{ 1240 statusGetter: unitStatusGetter, 1241 status: params.StatusUnknown, 1242 }, 1243 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1244 addRelation{waitJoin: true}, 1245 stopUniter{}, 1246 custom{func(c *gc.C, ctx *context) { 1247 // Check the state dir was created, and remove it. 1248 path := fmt.Sprintf("state/relations/%d", ctx.relation.Id()) 1249 ft.Dir{path, 0755}.Check(c, ctx.path) 1250 ft.Removed{path}.Create(c, ctx.path) 1251 1252 // Check that config-changed didn't record any relations, because 1253 // they shouldn't been available until after the start hook. 1254 ft.File{"charm/relations.out", "", 0644}.Check(c, ctx.path) 1255 }}, 1256 startUniter{}, 1257 waitHooks{"config-changed"}, 1258 custom{func(c *gc.C, ctx *context) { 1259 // Check the state dir was recreated. 1260 path := fmt.Sprintf("state/relations/%d", ctx.relation.Id()) 1261 ft.Dir{path, 0755}.Check(c, ctx.path) 1262 1263 // Check that config-changed did record the joined relations. 1264 data := fmt.Sprintf("db:%d\n", ctx.relation.Id()) 1265 ft.File{"charm/relations.out", data, 0644}.Check(c, ctx.path) 1266 }}, 1267 ), 1268 }) 1269 } 1270 1271 func (s *UniterSuite) TestUniterRelationErrors(c *gc.C) { 1272 s.runUniterTests(c, []uniterTest{ 1273 ut( 1274 "hook error during join of a relation", 1275 startupRelationError{"db-relation-joined"}, 1276 waitUnitAgent{ 1277 statusGetter: unitStatusGetter, 1278 status: params.StatusError, 1279 info: `hook failed: "db-relation-joined"`, 1280 data: map[string]interface{}{ 1281 "hook": "db-relation-joined", 1282 "relation-id": 0, 1283 "remote-unit": "mysql/0", 1284 }, 1285 }, 1286 ), ut( 1287 "hook error during change of a relation", 1288 startupRelationError{"db-relation-changed"}, 1289 waitUnitAgent{ 1290 statusGetter: unitStatusGetter, 1291 status: params.StatusError, 1292 info: `hook failed: "db-relation-changed"`, 1293 data: map[string]interface{}{ 1294 "hook": "db-relation-changed", 1295 "relation-id": 0, 1296 "remote-unit": "mysql/0", 1297 }, 1298 }, 1299 ), ut( 1300 "hook error after a unit departed", 1301 startupRelationError{"db-relation-departed"}, 1302 waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"}, 1303 removeRelationUnit{"mysql/0"}, 1304 waitUnitAgent{ 1305 statusGetter: unitStatusGetter, 1306 status: params.StatusError, 1307 info: `hook failed: "db-relation-departed"`, 1308 data: map[string]interface{}{ 1309 "hook": "db-relation-departed", 1310 "relation-id": 0, 1311 "remote-unit": "mysql/0", 1312 }, 1313 }, 1314 ), 1315 ut( 1316 "hook error after a relation died", 1317 startupRelationError{"db-relation-broken"}, 1318 waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"}, 1319 relationDying, 1320 waitUnitAgent{ 1321 statusGetter: unitStatusGetter, 1322 status: params.StatusError, 1323 info: `hook failed: "db-relation-broken"`, 1324 data: map[string]interface{}{ 1325 "hook": "db-relation-broken", 1326 "relation-id": 0, 1327 }, 1328 }, 1329 ), 1330 }) 1331 } 1332 1333 func (s *UniterSuite) TestUniterMeterStatusChanged(c *gc.C) { 1334 s.runUniterTests(c, []uniterTest{ 1335 ut( 1336 "meter status event triggered by unit meter status change", 1337 quickStart{}, 1338 changeMeterStatus{"AMBER", "Investigate charm."}, 1339 waitHooks{"meter-status-changed"}, 1340 ), 1341 }) 1342 } 1343 1344 func (s *UniterSuite) TestUniterCollectMetrics(c *gc.C) { 1345 s.runUniterTests(c, []uniterTest{ 1346 ut( 1347 "collect-metrics event triggered by manual timer", 1348 createCharm{ 1349 customize: func(c *gc.C, ctx *context, path string) { 1350 ctx.writeMetricsYaml(c, path) 1351 }, 1352 }, 1353 serveCharm{}, 1354 createUniter{}, 1355 waitUnitAgent{status: params.StatusIdle}, 1356 waitUnitAgent{ 1357 statusGetter: unitStatusGetter, 1358 status: params.StatusUnknown, 1359 }, 1360 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1361 verifyCharm{}, 1362 metricsTick{}, 1363 waitHooks{"collect-metrics"}, 1364 ), 1365 ut( 1366 "collect-metrics resumed after hook error", 1367 startupErrorWithCustomCharm{ 1368 badHook: "config-changed", 1369 customize: func(c *gc.C, ctx *context, path string) { 1370 ctx.writeMetricsYaml(c, path) 1371 }, 1372 }, 1373 metricsTick{}, 1374 fixHook{"config-changed"}, 1375 resolveError{state.ResolvedRetryHooks}, 1376 waitUnitAgent{ 1377 status: params.StatusIdle, 1378 }, 1379 waitUnitAgent{ 1380 statusGetter: unitStatusGetter, 1381 status: params.StatusUnknown, 1382 }, 1383 waitHooks{"config-changed", "start", "collect-metrics"}, 1384 verifyRunning{}, 1385 ), 1386 ut( 1387 "collect-metrics state maintained during uniter restart", 1388 startupErrorWithCustomCharm{ 1389 badHook: "config-changed", 1390 customize: func(c *gc.C, ctx *context, path string) { 1391 ctx.writeMetricsYaml(c, path) 1392 }, 1393 }, 1394 metricsTick{}, 1395 fixHook{"config-changed"}, 1396 stopUniter{}, 1397 startUniter{}, 1398 resolveError{state.ResolvedRetryHooks}, 1399 waitUnitAgent{ 1400 status: params.StatusIdle, 1401 }, 1402 waitUnitAgent{ 1403 statusGetter: unitStatusGetter, 1404 status: params.StatusUnknown, 1405 }, 1406 waitHooks{"config-changed", "start", "collect-metrics"}, 1407 verifyRunning{}, 1408 ), 1409 ut( 1410 "collect-metrics event not triggered for non-metered charm", 1411 quickStart{}, 1412 metricsTick{}, 1413 waitHooks{}, 1414 ), 1415 }) 1416 } 1417 1418 func (s *UniterSuite) TestActionEvents(c *gc.C) { 1419 s.runUniterTests(c, []uniterTest{ 1420 ut( 1421 "simple action event: defined in actions.yaml, no args", 1422 createCharm{ 1423 customize: func(c *gc.C, ctx *context, path string) { 1424 ctx.writeAction(c, path, "action-log") 1425 ctx.writeActionsYaml(c, path, "action-log") 1426 }, 1427 }, 1428 serveCharm{}, 1429 ensureStateWorker{}, 1430 createServiceAndUnit{}, 1431 startUniter{}, 1432 waitAddresses{}, 1433 waitUnitAgent{status: params.StatusIdle}, 1434 waitUnitAgent{ 1435 statusGetter: unitStatusGetter, 1436 status: params.StatusUnknown, 1437 }, 1438 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1439 verifyCharm{}, 1440 addAction{"action-log", nil}, 1441 waitActionResults{[]actionResult{{ 1442 name: "action-log", 1443 results: map[string]interface{}{}, 1444 status: params.ActionCompleted, 1445 }}}, 1446 waitUnitAgent{status: params.StatusIdle}, 1447 waitUnitAgent{ 1448 statusGetter: unitStatusGetter, 1449 status: params.StatusUnknown, 1450 }, 1451 ), ut( 1452 "action-fail causes the action to fail with a message", 1453 createCharm{ 1454 customize: func(c *gc.C, ctx *context, path string) { 1455 ctx.writeAction(c, path, "action-log-fail") 1456 ctx.writeActionsYaml(c, path, "action-log-fail") 1457 }, 1458 }, 1459 serveCharm{}, 1460 ensureStateWorker{}, 1461 createServiceAndUnit{}, 1462 startUniter{}, 1463 waitAddresses{}, 1464 waitUnitAgent{status: params.StatusIdle}, 1465 waitUnitAgent{ 1466 statusGetter: unitStatusGetter, 1467 status: params.StatusUnknown, 1468 }, 1469 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1470 verifyCharm{}, 1471 addAction{"action-log-fail", nil}, 1472 waitActionResults{[]actionResult{{ 1473 name: "action-log-fail", 1474 results: map[string]interface{}{ 1475 "foo": "still works", 1476 }, 1477 message: "I'm afraid I can't let you do that, Dave.", 1478 status: params.ActionFailed, 1479 }}}, 1480 waitUnitAgent{status: params.StatusIdle}, waitUnitAgent{ 1481 statusGetter: unitStatusGetter, 1482 status: params.StatusUnknown, 1483 }, 1484 ), ut( 1485 "action-fail with the wrong arguments fails but is not an error", 1486 createCharm{ 1487 customize: func(c *gc.C, ctx *context, path string) { 1488 ctx.writeAction(c, path, "action-log-fail-error") 1489 ctx.writeActionsYaml(c, path, "action-log-fail-error") 1490 }, 1491 }, 1492 serveCharm{}, 1493 ensureStateWorker{}, 1494 createServiceAndUnit{}, 1495 startUniter{}, 1496 waitAddresses{}, 1497 waitUnitAgent{status: params.StatusIdle}, 1498 waitUnitAgent{ 1499 statusGetter: unitStatusGetter, 1500 status: params.StatusUnknown, 1501 }, 1502 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1503 verifyCharm{}, 1504 addAction{"action-log-fail-error", nil}, 1505 waitActionResults{[]actionResult{{ 1506 name: "action-log-fail-error", 1507 results: map[string]interface{}{ 1508 "foo": "still works", 1509 }, 1510 message: "A real message", 1511 status: params.ActionFailed, 1512 }}}, 1513 waitUnitAgent{status: params.StatusIdle}, 1514 waitUnitAgent{ 1515 statusGetter: unitStatusGetter, 1516 status: params.StatusUnknown, 1517 }, 1518 ), ut( 1519 "actions with correct params passed are not an error", 1520 createCharm{ 1521 customize: func(c *gc.C, ctx *context, path string) { 1522 ctx.writeAction(c, path, "snapshot") 1523 ctx.writeActionsYaml(c, path, "snapshot") 1524 }, 1525 }, 1526 serveCharm{}, 1527 ensureStateWorker{}, 1528 createServiceAndUnit{}, 1529 startUniter{}, 1530 waitAddresses{}, 1531 waitUnitAgent{status: params.StatusIdle}, 1532 waitUnitAgent{ 1533 statusGetter: unitStatusGetter, 1534 status: params.StatusUnknown, 1535 }, 1536 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1537 verifyCharm{}, 1538 addAction{ 1539 name: "snapshot", 1540 params: map[string]interface{}{"outfile": "foo.bar"}, 1541 }, 1542 waitActionResults{[]actionResult{{ 1543 name: "snapshot", 1544 results: map[string]interface{}{ 1545 "outfile": map[string]interface{}{ 1546 "name": "snapshot-01.tar", 1547 "size": map[string]interface{}{ 1548 "magnitude": "10.3", 1549 "units": "GB", 1550 }, 1551 }, 1552 "completion": "yes", 1553 }, 1554 status: params.ActionCompleted, 1555 }}}, 1556 waitUnitAgent{status: params.StatusIdle}, 1557 waitUnitAgent{ 1558 statusGetter: unitStatusGetter, 1559 status: params.StatusUnknown, 1560 }, 1561 ), ut( 1562 "actions with incorrect params passed are not an error but fail", 1563 createCharm{ 1564 customize: func(c *gc.C, ctx *context, path string) { 1565 ctx.writeAction(c, path, "snapshot") 1566 ctx.writeActionsYaml(c, path, "snapshot") 1567 }, 1568 }, 1569 serveCharm{}, 1570 ensureStateWorker{}, 1571 createServiceAndUnit{}, 1572 startUniter{}, 1573 waitAddresses{}, 1574 waitUnitAgent{status: params.StatusIdle}, 1575 waitUnitAgent{ 1576 statusGetter: unitStatusGetter, 1577 status: params.StatusUnknown, 1578 }, 1579 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1580 verifyCharm{}, 1581 addAction{ 1582 name: "snapshot", 1583 params: map[string]interface{}{"outfile": 2}, 1584 }, 1585 waitActionResults{[]actionResult{{ 1586 name: "snapshot", 1587 results: map[string]interface{}{}, 1588 status: params.ActionFailed, 1589 message: `cannot run "snapshot" action: validation failed: (root).outfile : must be of type string, given 2`, 1590 }}}, 1591 waitUnitAgent{status: params.StatusIdle}, 1592 waitUnitAgent{ 1593 statusGetter: unitStatusGetter, 1594 status: params.StatusUnknown, 1595 }, 1596 ), ut( 1597 "actions not defined in actions.yaml fail without causing a uniter error", 1598 createCharm{ 1599 customize: func(c *gc.C, ctx *context, path string) { 1600 ctx.writeAction(c, path, "snapshot") 1601 }, 1602 }, 1603 serveCharm{}, 1604 ensureStateWorker{}, 1605 createServiceAndUnit{}, 1606 startUniter{}, 1607 waitAddresses{}, 1608 waitUnitAgent{status: params.StatusIdle}, 1609 waitUnitAgent{ 1610 statusGetter: unitStatusGetter, 1611 status: params.StatusUnknown, 1612 }, 1613 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1614 verifyCharm{}, 1615 addAction{"snapshot", map[string]interface{}{"outfile": "foo.bar"}}, 1616 waitActionResults{[]actionResult{{ 1617 name: "snapshot", 1618 results: map[string]interface{}{}, 1619 status: params.ActionFailed, 1620 message: `cannot run "snapshot" action: not defined`, 1621 }}}, 1622 waitUnitAgent{status: params.StatusIdle}, 1623 waitUnitAgent{ 1624 statusGetter: unitStatusGetter, 1625 status: params.StatusUnknown, 1626 }, 1627 ), ut( 1628 "pending actions get consumed", 1629 createCharm{ 1630 customize: func(c *gc.C, ctx *context, path string) { 1631 ctx.writeAction(c, path, "action-log") 1632 ctx.writeActionsYaml(c, path, "action-log") 1633 }, 1634 }, 1635 serveCharm{}, 1636 ensureStateWorker{}, 1637 createServiceAndUnit{}, 1638 addAction{"action-log", nil}, 1639 addAction{"action-log", nil}, 1640 addAction{"action-log", nil}, 1641 startUniter{}, 1642 waitAddresses{}, 1643 waitUnitAgent{status: params.StatusIdle}, 1644 waitUnitAgent{ 1645 statusGetter: unitStatusGetter, 1646 status: params.StatusUnknown, 1647 }, 1648 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1649 verifyCharm{}, 1650 waitActionResults{[]actionResult{{ 1651 name: "action-log", 1652 results: map[string]interface{}{}, 1653 status: params.ActionCompleted, 1654 }, { 1655 name: "action-log", 1656 results: map[string]interface{}{}, 1657 status: params.ActionCompleted, 1658 }, { 1659 name: "action-log", 1660 results: map[string]interface{}{}, 1661 status: params.ActionCompleted, 1662 }}}, 1663 waitUnitAgent{status: params.StatusIdle}, 1664 waitUnitAgent{ 1665 statusGetter: unitStatusGetter, 1666 status: params.StatusUnknown, 1667 }, 1668 ), ut( 1669 "actions not implemented fail but are not errors", 1670 createCharm{ 1671 customize: func(c *gc.C, ctx *context, path string) { 1672 ctx.writeActionsYaml(c, path, "action-log") 1673 }, 1674 }, 1675 serveCharm{}, 1676 ensureStateWorker{}, 1677 createServiceAndUnit{}, 1678 startUniter{}, 1679 waitAddresses{}, 1680 waitUnitAgent{status: params.StatusIdle}, 1681 waitUnitAgent{ 1682 statusGetter: unitStatusGetter, 1683 status: params.StatusUnknown, 1684 }, 1685 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1686 verifyCharm{}, 1687 addAction{"action-log", nil}, 1688 waitActionResults{[]actionResult{{ 1689 name: "action-log", 1690 results: map[string]interface{}{}, 1691 status: params.ActionFailed, 1692 message: `action not implemented on unit "u/0"`, 1693 }}}, 1694 waitUnitAgent{status: params.StatusIdle}, 1695 waitUnitAgent{ 1696 statusGetter: unitStatusGetter, 1697 status: params.StatusUnknown, 1698 }, 1699 ), ut( 1700 "actions may run from ModeHookError, but do not clear the error", 1701 startupErrorWithCustomCharm{ 1702 badHook: "start", 1703 customize: func(c *gc.C, ctx *context, path string) { 1704 ctx.writeAction(c, path, "action-log") 1705 ctx.writeActionsYaml(c, path, "action-log") 1706 }, 1707 }, 1708 addAction{"action-log", nil}, 1709 waitUnitAgent{ 1710 statusGetter: unitStatusGetter, 1711 status: params.StatusError, 1712 info: `hook failed: "start"`, 1713 data: map[string]interface{}{ 1714 "hook": "start", 1715 }, 1716 }, 1717 waitActionResults{[]actionResult{{ 1718 name: "action-log", 1719 results: map[string]interface{}{}, 1720 status: params.ActionCompleted, 1721 }}}, 1722 waitUnitAgent{ 1723 statusGetter: unitStatusGetter, 1724 status: params.StatusError, 1725 info: `hook failed: "start"`, 1726 data: map[string]interface{}{"hook": "start"}, 1727 }, 1728 verifyWaiting{}, 1729 resolveError{state.ResolvedNoHooks}, 1730 waitUnitAgent{status: params.StatusIdle}, 1731 waitUnitAgent{ 1732 statusGetter: unitStatusGetter, 1733 status: params.StatusMaintenance, 1734 info: "installing charm software", 1735 }, 1736 ), 1737 }) 1738 } 1739 1740 func (s *UniterSuite) TestUniterSubordinates(c *gc.C) { 1741 s.runUniterTests(c, []uniterTest{ 1742 // Subordinates. 1743 ut( 1744 "unit becomes dying while subordinates exist", 1745 quickStart{}, 1746 addSubordinateRelation{"juju-info"}, 1747 waitSubordinateExists{"logging/0"}, 1748 unitDying, 1749 waitSubordinateDying{}, 1750 waitHooks{"stop"}, 1751 verifyWaiting{}, 1752 removeSubordinate{}, 1753 waitUniterDead{}, 1754 ), ut( 1755 "new subordinate becomes necessary while old one is dying", 1756 quickStart{}, 1757 addSubordinateRelation{"juju-info"}, 1758 waitSubordinateExists{"logging/0"}, 1759 removeSubordinateRelation{"juju-info"}, 1760 // The subordinate Uniter would usually set Dying in this situation. 1761 subordinateDying, 1762 addSubordinateRelation{"logging-dir"}, 1763 verifyRunning{}, 1764 removeSubordinate{}, 1765 waitSubordinateExists{"logging/1"}, 1766 ), 1767 }) 1768 } 1769 1770 func (s *UniterSuite) TestSubordinateDying(c *gc.C) { 1771 // Create a test context for later use. 1772 ctx := &context{ 1773 s: s, 1774 st: s.State, 1775 path: filepath.Join(s.dataDir, "agents", "unit-u-0"), 1776 dataDir: s.dataDir, 1777 charms: make(map[string][]byte), 1778 metricsTicker: s.metricsTicker, 1779 updateStatusHookTicker: s.updateStatusHookTicker, 1780 } 1781 1782 addStateServerMachine(c, ctx.st) 1783 1784 // Create the subordinate service. 1785 dir := testcharms.Repo.ClonedDir(c.MkDir(), "logging") 1786 curl, err := corecharm.ParseURL("cs:quantal/logging") 1787 c.Assert(err, jc.ErrorIsNil) 1788 curl = curl.WithRevision(dir.Revision()) 1789 step(c, ctx, addCharm{dir, curl}) 1790 ctx.svc = s.AddTestingService(c, "u", ctx.sch) 1791 1792 // Create the principal service and add a relation. 1793 wps := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) 1794 wpu, err := wps.AddUnit() 1795 c.Assert(err, jc.ErrorIsNil) 1796 eps, err := s.State.InferEndpoints("wordpress", "u") 1797 c.Assert(err, jc.ErrorIsNil) 1798 rel, err := s.State.AddRelation(eps...) 1799 c.Assert(err, jc.ErrorIsNil) 1800 assertAssignUnit(c, s.State, wpu) 1801 1802 // Create the subordinate unit by entering scope as the principal. 1803 wpru, err := rel.Unit(wpu) 1804 c.Assert(err, jc.ErrorIsNil) 1805 err = wpru.EnterScope(nil) 1806 c.Assert(err, jc.ErrorIsNil) 1807 ctx.unit, err = s.State.Unit("u/0") 1808 c.Assert(err, jc.ErrorIsNil) 1809 ctx.apiLogin(c) 1810 1811 // Run the actual test. 1812 ctx.run(c, []stepper{ 1813 serveCharm{}, 1814 startUniter{}, 1815 waitAddresses{}, 1816 custom{func(c *gc.C, ctx *context) { 1817 c.Assert(rel.Destroy(), gc.IsNil) 1818 }}, 1819 waitUniterDead{}, 1820 }) 1821 } 1822 1823 func (s *UniterSuite) TestReboot(c *gc.C) { 1824 s.runUniterTests(c, []uniterTest{ 1825 ut( 1826 "test that juju-reboot disabled in actions", 1827 createCharm{ 1828 customize: func(c *gc.C, ctx *context, path string) { 1829 ctx.writeAction(c, path, "action-reboot") 1830 ctx.writeActionsYaml(c, path, "action-reboot") 1831 }, 1832 }, 1833 serveCharm{}, 1834 ensureStateWorker{}, 1835 createServiceAndUnit{}, 1836 addAction{"action-reboot", nil}, 1837 startUniter{}, 1838 waitAddresses{}, 1839 waitUnitAgent{ 1840 status: params.StatusIdle, 1841 }, 1842 waitUnitAgent{ 1843 statusGetter: unitStatusGetter, 1844 status: params.StatusUnknown, 1845 }, 1846 waitActionResults{[]actionResult{{ 1847 name: "action-reboot", 1848 results: map[string]interface{}{ 1849 "reboot-delayed": "good", 1850 "reboot-now": "good", 1851 }, 1852 status: params.ActionCompleted, 1853 }}}, 1854 ), ut( 1855 "test that juju-reboot finishes hook, and reboots", 1856 createCharm{ 1857 customize: func(c *gc.C, ctx *context, path string) { 1858 hpath := filepath.Join(path, "hooks", "install") 1859 ctx.writeExplicitHook(c, hpath, rebootHook) 1860 }, 1861 }, 1862 serveCharm{}, 1863 ensureStateWorker{}, 1864 createServiceAndUnit{}, 1865 startUniter{}, 1866 waitAddresses{}, 1867 waitUniterDead{"machine needs to reboot"}, 1868 waitHooks{"install"}, 1869 startUniter{}, 1870 waitUnitAgent{ 1871 status: params.StatusIdle, 1872 }, 1873 waitUnitAgent{ 1874 statusGetter: unitStatusGetter, 1875 status: params.StatusUnknown, 1876 }, 1877 waitHooks{"leader-elected", "config-changed", "start"}, 1878 ), ut( 1879 "test that juju-reboot --now kills hook and exits", 1880 createCharm{ 1881 customize: func(c *gc.C, ctx *context, path string) { 1882 hpath := filepath.Join(path, "hooks", "install") 1883 ctx.writeExplicitHook(c, hpath, rebootNowHook) 1884 }, 1885 }, 1886 serveCharm{}, 1887 ensureStateWorker{}, 1888 createServiceAndUnit{}, 1889 startUniter{}, 1890 waitAddresses{}, 1891 waitUniterDead{"machine needs to reboot"}, 1892 waitHooks{"install"}, 1893 startUniter{}, 1894 waitUnitAgent{ 1895 status: params.StatusIdle, 1896 }, 1897 waitUnitAgent{ 1898 statusGetter: unitStatusGetter, 1899 status: params.StatusUnknown, 1900 }, 1901 waitHooks{"install", "leader-elected", "config-changed", "start"}, 1902 ), ut( 1903 "test juju-reboot will not happen if hook errors out", 1904 createCharm{ 1905 customize: func(c *gc.C, ctx *context, path string) { 1906 hpath := filepath.Join(path, "hooks", "install") 1907 ctx.writeExplicitHook(c, hpath, badRebootHook) 1908 }, 1909 }, 1910 serveCharm{}, 1911 ensureStateWorker{}, 1912 createServiceAndUnit{}, 1913 startUniter{}, 1914 waitAddresses{}, 1915 waitUnitAgent{ 1916 statusGetter: unitStatusGetter, 1917 status: params.StatusError, 1918 info: fmt.Sprintf(`hook failed: "install"`), 1919 }, 1920 ), 1921 }) 1922 } 1923 1924 func (s *UniterSuite) TestRebootFromJujuRun(c *gc.C) { 1925 //TODO(bogdanteleaga): Fix this on windows 1926 if runtime.GOOS == "windows" { 1927 c.Skip("bug 1403084: currently does not work on windows") 1928 } 1929 s.runUniterTests(c, []uniterTest{ 1930 ut( 1931 "test juju-reboot", 1932 quickStart{}, 1933 runCommands{"juju-reboot"}, 1934 waitUniterDead{"machine needs to reboot"}, 1935 startUniter{}, 1936 waitHooks{"config-changed"}, 1937 ), ut( 1938 "test juju-reboot with bad hook", 1939 startupError{"install"}, 1940 runCommands{"juju-reboot"}, 1941 waitUniterDead{"machine needs to reboot"}, 1942 startUniter{}, 1943 waitHooks{}, 1944 ), ut( 1945 "test juju-reboot --now", 1946 quickStart{}, 1947 runCommands{"juju-reboot --now"}, 1948 waitUniterDead{"machine needs to reboot"}, 1949 startUniter{}, 1950 waitHooks{"config-changed"}, 1951 ), ut( 1952 "test juju-reboot --now with bad hook", 1953 startupError{"install"}, 1954 runCommands{"juju-reboot --now"}, 1955 waitUniterDead{"machine needs to reboot"}, 1956 startUniter{}, 1957 waitHooks{}, 1958 ), 1959 }) 1960 } 1961 1962 func (s *UniterSuite) TestLeadership(c *gc.C) { 1963 s.runUniterTests(c, []uniterTest{ 1964 ut( 1965 "hook tools when leader", 1966 quickStart{}, 1967 runCommands{"leader-set foo=bar baz=qux"}, 1968 verifyLeaderSettings{"foo": "bar", "baz": "qux"}, 1969 ), ut( 1970 "hook tools when not leader", 1971 quickStart{minion: true}, 1972 runCommands{leadershipScript}, 1973 ), ut( 1974 "leader-elected triggers when elected", 1975 quickStart{minion: true}, 1976 forceLeader{}, 1977 waitHooks{"leader-elected"}, 1978 ), ut( 1979 "leader-settings-changed triggers when leader settings change", 1980 quickStart{minion: true}, 1981 setLeaderSettings{"ping": "pong"}, 1982 waitHooks{"leader-settings-changed"}, 1983 ), ut( 1984 "leader-settings-changed triggers when bounced", 1985 quickStart{minion: true}, 1986 verifyRunning{minion: true}, 1987 ), ut( 1988 "leader-settings-changed triggers when deposed (while stopped)", 1989 quickStart{}, 1990 stopUniter{}, 1991 forceMinion{}, 1992 verifyRunning{minion: true}, 1993 ), 1994 }) 1995 } 1996 1997 func (s *UniterSuite) TestLeadershipUnexpectedDepose(c *gc.C) { 1998 s.PatchValue(uniter.LeadershipGuarantee, coretesting.ShortWait) 1999 s.runUniterTests(c, []uniterTest{ 2000 ut( 2001 // NOTE: this is a strange and ugly test, intended to detect what 2002 // *would* happen if the uniter suddenly failed to renew its lease; 2003 // it depends on an artificially shortened tracker refresh time to 2004 // run in a reasonable amount of time. 2005 "leader-settings-changed triggers when deposed (while running)", 2006 quickStart{}, 2007 forceMinion{}, 2008 waitHooks{"leader-settings-changed"}, 2009 ), 2010 }) 2011 } 2012 2013 func (s *UniterSuite) TestStorage(c *gc.C) { 2014 // appendStorageMetadata customises the wordpress charm's metadata, 2015 // adding a "wp-content" filesystem store. We do it here rather 2016 // than in the charm itself to avoid modifying all of the other 2017 // scenarios. 2018 appendStorageMetadata := func(c *gc.C, ctx *context, path string) { 2019 f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644) 2020 c.Assert(err, jc.ErrorIsNil) 2021 defer func() { 2022 err := f.Close() 2023 c.Assert(err, jc.ErrorIsNil) 2024 }() 2025 _, err = io.WriteString(f, "storage:\n wp-content:\n type: filesystem\n") 2026 c.Assert(err, jc.ErrorIsNil) 2027 } 2028 s.runUniterTests(c, []uniterTest{ 2029 ut( 2030 "test that storage-attached is called", 2031 createCharm{customize: appendStorageMetadata}, 2032 serveCharm{}, 2033 ensureStateWorker{}, 2034 createServiceAndUnit{}, 2035 provisionStorage{}, 2036 startUniter{}, 2037 waitAddresses{}, 2038 waitHooks{"wp-content-storage-attached"}, 2039 waitHooks(startupHooks(false)), 2040 ), ut( 2041 "test that storage-detaching is called before stop", 2042 createCharm{customize: appendStorageMetadata}, 2043 serveCharm{}, 2044 ensureStateWorker{}, 2045 createServiceAndUnit{}, 2046 provisionStorage{}, 2047 startUniter{}, 2048 waitAddresses{}, 2049 waitHooks{"wp-content-storage-attached"}, 2050 waitHooks(startupHooks(false)), 2051 unitDying, 2052 waitHooks{"leader-settings-changed"}, 2053 // "stop" hook is not called until storage is detached 2054 waitHooks{"wp-content-storage-detaching", "stop"}, 2055 verifyStorageDetached{}, 2056 waitUniterDead{}, 2057 ), ut( 2058 "test that storage-detaching is called only if previously attached", 2059 createCharm{customize: appendStorageMetadata}, 2060 serveCharm{}, 2061 ensureStateWorker{}, 2062 createServiceAndUnit{}, 2063 // provision and destroy the storage before the uniter starts, 2064 // to ensure it never sees the storage as attached 2065 provisionStorage{}, 2066 destroyStorageAttachment{}, 2067 startUniter{}, 2068 waitHooks(startupHooks(false)), 2069 unitDying, 2070 // storage-detaching is not called because it was never attached 2071 waitHooks{"stop"}, 2072 verifyStorageDetached{}, 2073 waitUniterDead{}, 2074 ), ut( 2075 "test that delay-provisioned storage does not block forever", 2076 createCharm{customize: appendStorageMetadata}, 2077 serveCharm{}, 2078 ensureStateWorker{}, 2079 createServiceAndUnit{}, 2080 startUniter{}, 2081 // no hooks should be run, as storage isn't provisioned 2082 waitHooks{}, 2083 provisionStorage{}, 2084 waitHooks{"wp-content-storage-attached"}, 2085 waitHooks(startupHooks(false)), 2086 ), ut( 2087 "test that unprovisioned storage does not block unit termination", 2088 createCharm{customize: appendStorageMetadata}, 2089 serveCharm{}, 2090 ensureStateWorker{}, 2091 createServiceAndUnit{}, 2092 startUniter{}, 2093 // no hooks should be run, as storage isn't provisioned 2094 waitHooks{}, 2095 unitDying, 2096 // TODO(axw) should we really be running startup hooks 2097 // when the unit is dying? 2098 waitHooks(startupHooks(true)), 2099 waitHooks{"stop"}, 2100 waitUniterDead{}, 2101 ), 2102 // TODO(axw) test that storage-attached is run for new 2103 // storage attachments before upgrade-charm is run. This 2104 // requires additions to state to add storage when a charm 2105 // is upgraded. 2106 }) 2107 }