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