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  }