Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1988 lines (1902 sloc) 54.3 KB
// Copyright 2012-2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package uniter_test
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/juju/errors"
"github.com/juju/mutex"
jujutesting "github.com/juju/testing"
jc "github.com/juju/testing/checkers"
ft "github.com/juju/testing/filetesting"
gc "gopkg.in/check.v1"
corecharm "gopkg.in/juju/charm.v6-unstable"
"github.com/juju/juju/agent/tools"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/component/all"
"github.com/juju/juju/juju/testing"
"github.com/juju/juju/state"
"github.com/juju/juju/status"
"github.com/juju/juju/testcharms"
coretesting "github.com/juju/juju/testing"
"github.com/juju/juju/worker/uniter/operation"
)
type UniterSuite struct {
coretesting.GitSuite
testing.JujuConnSuite
dataDir string
oldLcAll string
unitDir string
updateStatusHookTicker *manualTicker
}
var _ = gc.Suite(&UniterSuite{})
var leaseClock *jujutesting.Clock
// This guarantees that we get proper platform
// specific error directly from their source
// This works on both windows and unix
var errNotDir = syscall.ENOTDIR.Error()
func (s *UniterSuite) SetUpSuite(c *gc.C) {
s.GitSuite.SetUpSuite(c)
s.JujuConnSuite.SetUpSuite(c)
s.dataDir = c.MkDir()
toolsDir := tools.ToolsDir(s.dataDir, "unit-u-0")
err := os.MkdirAll(toolsDir, 0755)
c.Assert(err, jc.ErrorIsNil)
// TODO(fwereade) GAAAAAAAAAAAAAAAAAH this is LUDICROUS.
cmd := exec.Command(jujudBuildArgs[0], jujudBuildArgs[1:]...)
cmd.Dir = toolsDir
out, err := cmd.CombinedOutput()
c.Logf(string(out))
c.Assert(err, jc.ErrorIsNil)
s.oldLcAll = os.Getenv("LC_ALL")
os.Setenv("LC_ALL", "en_US")
s.unitDir = filepath.Join(s.dataDir, "agents", "unit-u-0")
all.RegisterForServer()
}
func (s *UniterSuite) TearDownSuite(c *gc.C) {
os.Setenv("LC_ALL", s.oldLcAll)
s.JujuConnSuite.TearDownSuite(c)
s.GitSuite.TearDownSuite(c)
}
func (s *UniterSuite) SetUpTest(c *gc.C) {
zone, err := time.LoadLocation("")
c.Assert(err, jc.ErrorIsNil)
now := time.Date(2030, 11, 11, 11, 11, 11, 11, zone)
leaseClock = jujutesting.NewClock(now)
s.updateStatusHookTicker = newManualTicker()
s.GitSuite.SetUpTest(c)
s.JujuConnSuite.SetUpTest(c)
err = s.State.SetClockForTesting(leaseClock)
c.Assert(err, jc.ErrorIsNil)
}
func (s *UniterSuite) TearDownTest(c *gc.C) {
s.ResetContext(c)
s.JujuConnSuite.TearDownTest(c)
s.GitSuite.TearDownTest(c)
}
func (s *UniterSuite) Reset(c *gc.C) {
s.JujuConnSuite.Reset(c)
s.ResetContext(c)
}
func (s *UniterSuite) ResetContext(c *gc.C) {
err := os.RemoveAll(s.unitDir)
c.Assert(err, jc.ErrorIsNil)
}
func (s *UniterSuite) runUniterTests(c *gc.C, uniterTests []uniterTest) {
for i, t := range uniterTests {
c.Logf("\ntest %d: %s\n", i, t.summary)
func() {
defer s.Reset(c)
env, err := s.State.Model()
c.Assert(err, jc.ErrorIsNil)
ctx := &context{
s: s,
st: s.State,
uuid: env.UUID(),
path: s.unitDir,
dataDir: s.dataDir,
charms: make(map[string][]byte),
updateStatusHookTicker: s.updateStatusHookTicker,
charmDirGuard: &mockCharmDirGuard{},
}
ctx.run(c, t.steps)
}()
}
}
func (s *UniterSuite) TestUniterStartup(c *gc.C) {
s.runUniterTests(c, []uniterTest{
// Check conditions that can cause the uniter to fail to start.
ut(
"unable to create state dir",
writeFile{"state", 0644},
createCharm{},
createServiceAndUnit{},
startUniter{},
waitUniterDead{err: `failed to initialize uniter for "unit-u-0": .*` + errNotDir},
), ut(
"unknown unit",
// We still need to create a unit, because that's when we also
// connect to the API, but here we use a different service
// (and hence unit) name.
createCharm{},
createServiceAndUnit{serviceName: "w"},
startUniter{unitTag: "unit-u-0"},
waitUniterDead{err: `failed to initialize uniter for "unit-u-0": permission denied`},
),
})
}
func (s *UniterSuite) TestPreviousDownloadsCleared(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"Ensure stale download files are cleared on uniter startup",
createCharm{},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
createDownloads{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
verifyDownloadsCleared{},
),
})
}
func (s *UniterSuite) TestUniterBootstrap(c *gc.C) {
//TODO(bogdanteleaga): Fix this on windows
if runtime.GOOS == "windows" {
c.Skip("bug 1403084: currently does not work on windows")
}
s.runUniterTests(c, []uniterTest{
// Check error conditions during unit bootstrap phase.
ut(
"insane deployment",
createCharm{},
serveCharm{},
writeFile{"charm", 0644},
createUniter{},
waitUniterDead{err: `executing operation "install cs:quantal/wordpress-0": open .*` + errNotDir},
), ut(
"charm cannot be downloaded",
createCharm{},
// don't serve charm
createUniter{},
waitUniterDead{err: `preparing operation "install cs:quantal/wordpress-0": failed to download charm .* not found`},
),
})
}
type noopExecutor struct {
operation.Executor
}
func (m *noopExecutor) Run(op operation.Operation) error {
return errors.New("some error occurred")
}
func (s *UniterSuite) TestUniterStartupStatus(c *gc.C) {
executorFunc := func(stateFilePath string, getInstallCharm func() (*corecharm.URL, error), acquireLock func() (mutex.Releaser, error)) (operation.Executor, error) {
e, err := operation.NewExecutor(stateFilePath, getInstallCharm, acquireLock)
c.Assert(err, jc.ErrorIsNil)
return &mockExecutor{e}, nil
}
s.runUniterTests(c, []uniterTest{
ut(
"unit status and message at startup",
createCharm{},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{
newExecutorFunc: executorFunc,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Waiting,
info: status.MessageInitializingAgent,
},
waitUnitAgent{
status: status.Failed,
info: "resolver loop error",
},
expectError{".*some error occurred.*"},
),
})
}
func (s *UniterSuite) TestUniterInstallHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"install hook fail and resolve",
startupError{"install"},
verifyWaiting{},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"leader-elected", "config-changed", "start"},
), ut(
"install hook fail and retry",
startupError{"install"},
verifyWaiting{},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "install"`,
data: map[string]interface{}{
"hook": "install",
},
},
waitHooks{"fail-install"},
fixHook{"install"},
verifyWaiting{},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
status: status.Idle,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
),
})
}
func (s *UniterSuite) TestUniterUpdateStatusHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"update status hook runs on timer",
createCharm{},
serveCharm{},
createUniter{},
waitHooks(startupHooks(false)),
waitUnitAgent{status: status.Idle},
updateStatusHookTick{},
waitHooks{"update-status"},
),
})
}
func (s *UniterSuite) TestNoUniterUpdateStatusHookInError(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"update status hook doesn't run if in error",
startupError{"start"},
waitHooks{},
updateStatusHookTick{},
waitHooks{},
// Resolve and hook should run.
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
},
waitHooks{},
updateStatusHookTick{},
waitHooks{"update-status"},
),
})
}
func (s *UniterSuite) TestUniterStartHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"start hook fail and resolve",
startupError{"start"},
verifyWaiting{},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Maintenance,
info: "installing charm software",
},
waitHooks{"config-changed"},
verifyRunning{},
), ut(
"start hook fail and retry",
startupError{"start"},
verifyWaiting{},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{
"hook": "start",
},
},
waitHooks{"fail-start"},
verifyWaiting{},
fixHook{"start"},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
status: status.Idle,
},
waitHooks{"start", "config-changed"},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterMultipleErrors(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"resolved is cleared before moving on to next hook",
createCharm{badHooks: []string{"install", "leader-elected", "config-changed", "start"}},
serveCharm{},
createUniter{},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "install"`,
data: map[string]interface{}{
"hook": "install",
},
},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "leader-elected"`,
data: map[string]interface{}{
"hook": "leader-elected",
},
},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "config-changed"`,
data: map[string]interface{}{
"hook": "config-changed",
},
},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{
"hook": "start",
},
},
),
})
}
func (s *UniterSuite) TestUniterConfigChangedHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"config-changed hook fail and resolve",
startupError{"config-changed"},
verifyWaiting{},
// Note: we'll run another config-changed as soon as we hit the
// started state, so the broken hook would actually prevent us
// from advancing at all if we didn't fix it.
fixHook{"config-changed"},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
// TODO(axw) confirm with fwereade that this is correct.
// Previously we would see "start", "config-changed".
// I don't think we should see another config-changed,
// since config did not change since we resolved the
// failed one above.
waitHooks{"start"},
// If we'd accidentally retried that hook, somehow, we would get
// an extra config-changed as we entered started; see that we don't.
waitHooks{},
verifyRunning{},
), ut(
"config-changed hook fail and retry",
startupError{"config-changed"},
verifyWaiting{},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "config-changed"`,
data: map[string]interface{}{
"hook": "config-changed",
},
},
waitHooks{"fail-config-changed"},
verifyWaiting{},
fixHook{"config-changed"},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
status: status.Idle,
},
waitHooks{"config-changed", "start"},
verifyRunning{},
), ut(
"steady state config change with config-get verification",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
appendHook(c, path, "config-changed", appendConfigChanged)
},
},
serveCharm{},
createUniter{},
waitUnitAgent{
status: status.Idle,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
assertYaml{"charm/config.out", map[string]interface{}{
"blog-title": "My Title",
}},
changeConfig{"blog-title": "Goodness Gracious Me"},
waitHooks{"config-changed"},
verifyRunning{},
assertYaml{"charm/config.out", map[string]interface{}{
"blog-title": "Goodness Gracious Me",
}},
),
})
}
func (s *UniterSuite) TestUniterHookSynchronisation(c *gc.C) {
var lock hookLock
s.runUniterTests(c, []uniterTest{
ut(
"verify config change hook not run while lock held",
quickStart{},
lock.acquire(),
changeConfig{"blog-title": "Goodness Gracious Me"},
waitHooks{},
lock.release(),
waitHooks{"config-changed"},
), ut(
"verify held lock by another unit is not broken",
lock.acquire(),
// Can't use quickstart as it has a built in waitHooks.
createCharm{},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitHooks{},
lock.release(),
waitUnitAgent{status: status.Idle},
waitHooks{"install", "leader-elected", "config-changed", "start"},
),
})
}
func (s *UniterSuite) TestUniterDyingReaction(c *gc.C) {
s.runUniterTests(c, []uniterTest{
// Reaction to entity deaths.
ut(
"steady state unit dying",
quickStart{},
unitDying,
waitHooks{"leader-settings-changed", "stop"},
waitUniterDead{},
), ut(
"steady state unit dead",
quickStart{},
unitDead,
waitUniterDead{},
waitHooks{},
), ut(
"hook error unit dying",
startupError{"start"},
unitDying,
verifyWaiting{},
fixHook{"start"},
resolveError{state.ResolvedRetryHooks},
waitHooks{"start", "leader-settings-changed", "stop"},
waitUniterDead{},
), ut(
"hook error unit dead",
startupError{"start"},
unitDead,
waitUniterDead{},
waitHooks{},
),
})
}
func (s *UniterSuite) TestUniterSteadyStateUpgrade(c *gc.C) {
s.runUniterTests(c, []uniterTest{
// Upgrade scenarios from steady state.
ut(
"steady state upgrade",
quickStart{},
createCharm{revision: 1},
upgradeCharm{revision: 1},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 1,
},
waitHooks{"upgrade-charm", "config-changed"},
verifyCharm{revision: 1},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterSteadyStateUpgradeForce(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"steady state forced upgrade (identical behaviour)",
quickStart{},
createCharm{revision: 1},
upgradeCharm{revision: 1, forced: true},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 1,
},
waitHooks{"upgrade-charm", "config-changed"},
verifyCharm{revision: 1},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterSteadyStateUpgradeResolve(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"steady state upgrade hook fail and resolve",
quickStart{},
createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
upgradeCharm{revision: 1},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "upgrade-charm"`,
data: map[string]interface{}{
"hook": "upgrade-charm",
},
charm: 1,
},
waitHooks{"fail-upgrade-charm"},
verifyCharm{revision: 1},
verifyWaiting{},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 1,
},
waitHooks{"config-changed"},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterSteadyStateUpgradeRetry(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"steady state upgrade hook fail and retry",
quickStart{},
createCharm{revision: 1, badHooks: []string{"upgrade-charm"}},
upgradeCharm{revision: 1},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "upgrade-charm"`,
data: map[string]interface{}{
"hook": "upgrade-charm",
},
charm: 1,
},
waitHooks{"fail-upgrade-charm"},
verifyCharm{revision: 1},
verifyWaiting{},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "upgrade-charm"`,
data: map[string]interface{}{
"hook": "upgrade-charm",
},
charm: 1,
},
waitHooks{"fail-upgrade-charm"},
verifyWaiting{},
fixHook{"upgrade-charm"},
resolveError{state.ResolvedRetryHooks},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitHooks{"upgrade-charm", "config-changed"},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterSteadyStateUpgradeRelations(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
// This test does an add-relation as quickly as possible
// after an upgrade-charm, in the hope that the scheduler will
// deliver the events in the wrong order. The observed
// behaviour should be the same in either case.
"ignore unknown relations until upgrade is done",
quickStart{},
createCharm{
revision: 2,
customize: func(c *gc.C, ctx *context, path string) {
renameRelation(c, path, "db", "db2")
hpath := filepath.Join(path, "hooks", "db2-relation-joined")
ctx.writeHook(c, hpath, true)
},
},
serveCharm{},
upgradeCharm{revision: 2},
addRelation{},
addRelationUnit{},
waitHooks{"upgrade-charm", "config-changed", "db2-relation-joined mysql/0 db2:0"},
verifyCharm{revision: 2},
),
})
}
func (s *UniterSuite) TestUpdateResourceCausesUpgrade(c *gc.C) {
// appendStorageMetadata customises the wordpress charm's metadata,
// adding a "wp-content" filesystem store. We do it here rather
// than in the charm itself to avoid modifying all of the other
// scenarios.
appendResource := func(c *gc.C, ctx *context, path string) {
f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644)
c.Assert(err, jc.ErrorIsNil)
defer func() {
err := f.Close()
c.Assert(err, jc.ErrorIsNil)
}()
_, err = io.WriteString(f, `
resources:
data:
Type: file
filename: filename.tgz
comment: One line that is useful when operators need to push it.`)
c.Assert(err, jc.ErrorIsNil)
}
s.runUniterTests(c, []uniterTest{
ut(
"update resource causes upgrade",
// These steps are just copied from quickstart with a customized
// createCharm.
createCharm{customize: appendResource},
serveCharm{},
createUniter{},
waitUnitAgent{status: status.Idle},
waitHooks(startupHooks(false)),
verifyCharm{},
pushResource{},
waitHooks{"upgrade-charm", "config-changed"},
),
})
}
func (s *UniterSuite) TestUniterUpgradeOverwrite(c *gc.C) {
//TODO(bogdanteleaga): Fix this on windows
if runtime.GOOS == "windows" {
c.Skip("bug 1403084: currently does not work on windows")
}
makeTest := func(description string, content, extraChecks ft.Entries) uniterTest {
return ut(description,
createCharm{
// This is the base charm which all upgrade tests start out running.
customize: func(c *gc.C, ctx *context, path string) {
ft.Entries{
ft.Dir{"dir", 0755},
ft.File{"file", "blah", 0644},
ft.Symlink{"symlink", "file"},
}.Create(c, path)
// Note that it creates "dir/user-file" at runtime, which may be
// preserved or removed depending on the test.
script := "echo content > dir/user-file && chmod 755 dir/user-file"
appendHook(c, path, "start", script)
},
},
serveCharm{},
createUniter{},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
createCharm{
revision: 1,
customize: func(c *gc.C, _ *context, path string) {
content.Create(c, path)
},
},
serveCharm{},
upgradeCharm{revision: 1},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 1,
},
waitHooks{"upgrade-charm", "config-changed"},
verifyCharm{revision: 1},
custom{func(c *gc.C, ctx *context) {
path := filepath.Join(ctx.path, "charm")
content.Check(c, path)
extraChecks.Check(c, path)
}},
verifyRunning{},
)
}
s.runUniterTests(c, []uniterTest{
makeTest(
"files overwite files, dirs, symlinks",
ft.Entries{
ft.File{"file", "new", 0755},
ft.File{"dir", "new", 0755},
ft.File{"symlink", "new", 0755},
},
ft.Entries{
ft.Removed{"dir/user-file"},
},
), makeTest(
"symlinks overwite files, dirs, symlinks",
ft.Entries{
ft.Symlink{"file", "new"},
ft.Symlink{"dir", "new"},
ft.Symlink{"symlink", "new"},
},
ft.Entries{
ft.Removed{"dir/user-file"},
},
), makeTest(
"dirs overwite files, symlinks; merge dirs",
ft.Entries{
ft.Dir{"file", 0755},
ft.Dir{"dir", 0755},
ft.File{"dir/charm-file", "charm-content", 0644},
ft.Dir{"symlink", 0755},
},
ft.Entries{
ft.File{"dir/user-file", "content\n", 0755},
},
),
})
}
func (s *UniterSuite) TestUniterErrorStateUnforcedUpgrade(c *gc.C) {
s.runUniterTests(c, []uniterTest{
// Upgrade scenarios from error state.
ut(
"error state unforced upgrade (ignored until started state)",
startupError{"start"},
createCharm{revision: 1},
upgradeCharm{revision: 1},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{
"hook": "start",
},
},
waitHooks{},
verifyCharm{},
verifyWaiting{},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Maintenance,
info: "installing charm software",
charm: 1,
},
waitHooks{"upgrade-charm", "config-changed"},
verifyCharm{revision: 1},
verifyRunning{},
)})
}
func (s *UniterSuite) TestUniterErrorStateForcedUpgrade(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"error state forced upgrade",
startupError{"start"},
createCharm{revision: 1},
upgradeCharm{revision: 1, forced: true},
// It's not possible to tell directly from state when the upgrade is
// complete, because the new unit charm URL is set at the upgrade
// process's point of no return (before actually deploying, but after
// the charm has been downloaded and verified). However, it's still
// useful to wait until that point...
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{
"hook": "start",
},
charm: 1,
},
// ...because the uniter *will* complete a started deployment even if
// we stop it from outside. So, by stopping and starting, we can be
// sure that the operation has completed and can safely verify that
// the charm state on disk is as we expect.
verifyWaiting{},
verifyCharm{revision: 1},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitHooks{"config-changed"},
verifyRunning{},
),
})
}
func (s *UniterSuite) TestUniterUpgradeConflicts(c *gc.C) {
coretesting.SkipIfPPC64EL(c, "lp:1448308")
//TODO(bogdanteleaga): Fix this on windows
if runtime.GOOS == "windows" {
c.Skip("bug 1403084: currently does not work on windows")
}
s.runUniterTests(c, []uniterTest{
// Upgrade scenarios - handling conflicts.
ut(
"upgrade: resolving doesn't help until underlying problem is fixed",
startUpgradeError{},
resolveError{state.ResolvedNoHooks},
verifyWaitingUpgradeError{revision: 1},
fixUpgradeError{},
resolveError{state.ResolvedNoHooks},
waitHooks{"upgrade-charm", "config-changed"},
waitUnitAgent{
status: status.Idle,
charm: 1,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 1,
},
verifyCharm{revision: 1},
), ut(
`upgrade: forced upgrade does work without explicit resolution if underlying problem was fixed`,
startUpgradeError{},
resolveError{state.ResolvedNoHooks},
verifyWaitingUpgradeError{revision: 1},
fixUpgradeError{},
createCharm{revision: 2},
serveCharm{},
upgradeCharm{revision: 2, forced: true},
waitHooks{"upgrade-charm", "config-changed"},
waitUnitAgent{
status: status.Idle,
charm: 2,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
charm: 2,
},
verifyCharm{revision: 2},
), ut(
"upgrade conflict unit dying",
startUpgradeError{},
unitDying,
verifyWaitingUpgradeError{revision: 1},
fixUpgradeError{},
resolveError{state.ResolvedNoHooks},
waitHooks{"upgrade-charm", "config-changed", "leader-settings-changed", "stop"},
waitUniterDead{},
), ut(
"upgrade conflict unit dead",
startUpgradeError{},
unitDead,
waitUniterDead{},
waitHooks{},
fixUpgradeError{},
),
})
}
func (s *UniterSuite) TestUniterRelations(c *gc.C) {
waitDyingHooks := custom{func(c *gc.C, ctx *context) {
// There is no ordering relationship between relation hooks and
// leader-settings-changed hooks; and while we're dying we may
// never get to leader-settings-changed before it's time to run
// the stop (as we might not react to a config change in time).
// It's actually clearer to just list the possible orders:
possibles := [][]string{{
"leader-settings-changed",
"db-relation-departed mysql/0 db:0",
"db-relation-broken db:0",
"stop",
}, {
"db-relation-departed mysql/0 db:0",
"leader-settings-changed",
"db-relation-broken db:0",
"stop",
}, {
"db-relation-departed mysql/0 db:0",
"db-relation-broken db:0",
"leader-settings-changed",
"stop",
}, {
"db-relation-departed mysql/0 db:0",
"db-relation-broken db:0",
"stop",
}}
unchecked := ctx.hooksCompleted[len(ctx.hooks):]
for _, possible := range possibles {
if ok, _ := jc.DeepEqual(unchecked, possible); ok {
return
}
}
c.Fatalf("unexpected hooks: %v", unchecked)
}}
s.runUniterTests(c, []uniterTest{
// Relations.
ut(
"simple joined/changed/departed",
quickStartRelation{},
addRelationUnit{},
waitHooks{
"db-relation-joined mysql/1 db:0",
"db-relation-changed mysql/1 db:0",
},
changeRelationUnit{"mysql/0"},
waitHooks{"db-relation-changed mysql/0 db:0"},
removeRelationUnit{"mysql/1"},
waitHooks{"db-relation-departed mysql/1 db:0"},
verifyRunning{},
), ut(
"relation becomes dying; unit is not last remaining member",
quickStartRelation{},
relationDying,
waitHooks{
"db-relation-departed mysql/0 db:0",
"db-relation-broken db:0",
},
verifyRunning{},
relationState{life: state.Dying},
removeRelationUnit{"mysql/0"},
verifyRunning{},
relationState{removed: true},
verifyRunning{},
), ut(
"relation becomes dying; unit is last remaining member",
quickStartRelation{},
removeRelationUnit{"mysql/0"},
waitHooks{"db-relation-departed mysql/0 db:0"},
relationDying,
waitHooks{"db-relation-broken db:0"},
verifyRunning{},
relationState{removed: true},
verifyRunning{},
), ut(
"unit becomes dying while in a relation",
quickStartRelation{},
unitDying,
waitUniterDead{},
waitDyingHooks,
relationState{life: state.Alive},
removeRelationUnit{"mysql/0"},
relationState{life: state.Alive},
), ut(
"unit becomes dead while in a relation",
quickStartRelation{},
unitDead,
waitUniterDead{},
waitHooks{},
// TODO BUG(?): the unit doesn't leave the scope, leaving the relation
// unkillable without direct intervention. I'm pretty sure it's not a
// uniter bug -- it should be the responsibility of `juju remove-unit
// --force` to cause the unit to leave any relation scopes it may be
// in -- but it's worth noting here all the same.
), ut(
"unknown local relation dir is removed",
quickStartRelation{},
stopUniter{},
custom{func(c *gc.C, ctx *context) {
ft.Dir{"state/relations/90210", 0755}.Create(c, ctx.path)
}},
startUniter{},
waitHooks{"config-changed"},
custom{func(c *gc.C, ctx *context) {
ft.Removed{"state/relations/90210"}.Check(c, ctx.path)
}},
), ut(
"all relations are available to config-changed on bounce, even if state dir is missing",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
script := uniterRelationsCustomizeScript
appendHook(c, path, "config-changed", script)
},
},
serveCharm{},
createUniter{},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
addRelation{waitJoin: true},
stopUniter{},
custom{func(c *gc.C, ctx *context) {
// Check the state dir was created, and remove it.
path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
ft.Dir{path, 0755}.Check(c, ctx.path)
ft.Removed{path}.Create(c, ctx.path)
// Check that config-changed didn't record any relations, because
// they shouldn't been available until after the start hook.
ft.File{"charm/relations.out", "", 0644}.Check(c, ctx.path)
}},
startUniter{},
waitHooks{"config-changed"},
custom{func(c *gc.C, ctx *context) {
// Check the state dir was recreated.
path := fmt.Sprintf("state/relations/%d", ctx.relation.Id())
ft.Dir{path, 0755}.Check(c, ctx.path)
// Check that config-changed did record the joined relations.
data := fmt.Sprintf("db:%d\n", ctx.relation.Id())
ft.File{"charm/relations.out", data, 0644}.Check(c, ctx.path)
}},
),
})
}
func (s *UniterSuite) TestUniterRelationErrors(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"hook error during join of a relation",
startupRelationError{"db-relation-joined"},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "db-relation-joined"`,
data: map[string]interface{}{
"hook": "db-relation-joined",
"relation-id": 0,
"remote-unit": "mysql/0",
},
},
), ut(
"hook error during change of a relation",
startupRelationError{"db-relation-changed"},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "db-relation-changed"`,
data: map[string]interface{}{
"hook": "db-relation-changed",
"relation-id": 0,
"remote-unit": "mysql/0",
},
},
), ut(
"hook error after a unit departed",
startupRelationError{"db-relation-departed"},
waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
removeRelationUnit{"mysql/0"},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "db-relation-departed"`,
data: map[string]interface{}{
"hook": "db-relation-departed",
"relation-id": 0,
"remote-unit": "mysql/0",
},
},
),
ut(
"hook error after a relation died",
startupRelationError{"db-relation-broken"},
waitHooks{"db-relation-joined mysql/0 db:0", "db-relation-changed mysql/0 db:0"},
relationDying,
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "db-relation-broken"`,
data: map[string]interface{}{
"hook": "db-relation-broken",
"relation-id": 0,
},
},
),
})
}
func (s *UniterSuite) TestActionEvents(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"simple action event: defined in actions.yaml, no args",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-log")
ctx.writeActionsYaml(c, path, "action-log")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{"action-log", nil},
waitActionResults{[]actionResult{{
name: "action-log",
results: map[string]interface{}{},
status: params.ActionCompleted,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"action-fail causes the action to fail with a message",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-log-fail")
ctx.writeActionsYaml(c, path, "action-log-fail")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{"action-log-fail", nil},
waitActionResults{[]actionResult{{
name: "action-log-fail",
results: map[string]interface{}{
"foo": "still works",
},
message: "I'm afraid I can't let you do that, Dave.",
status: params.ActionFailed,
}}},
waitUnitAgent{status: status.Idle}, waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"action-fail with the wrong arguments fails but is not an error",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-log-fail-error")
ctx.writeActionsYaml(c, path, "action-log-fail-error")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{"action-log-fail-error", nil},
waitActionResults{[]actionResult{{
name: "action-log-fail-error",
results: map[string]interface{}{
"foo": "still works",
},
message: "A real message",
status: params.ActionFailed,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"actions with correct params passed are not an error",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "snapshot")
ctx.writeActionsYaml(c, path, "snapshot")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{
name: "snapshot",
params: map[string]interface{}{"outfile": "foo.bar"},
},
waitActionResults{[]actionResult{{
name: "snapshot",
results: map[string]interface{}{
"outfile": map[string]interface{}{
"name": "snapshot-01.tar",
"size": map[string]interface{}{
"magnitude": "10.3",
"units": "GB",
},
},
"completion": "yes",
},
status: params.ActionCompleted,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"actions with incorrect params passed are not an error but fail",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "snapshot")
ctx.writeActionsYaml(c, path, "snapshot")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{
name: "snapshot",
params: map[string]interface{}{"outfile": 2},
},
waitActionResults{[]actionResult{{
name: "snapshot",
results: map[string]interface{}{},
status: params.ActionFailed,
message: `cannot run "snapshot" action: validation failed: (root).outfile : must be of type string, given 2`,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"actions not defined in actions.yaml fail without causing a uniter error",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "snapshot")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{"snapshot", map[string]interface{}{"outfile": "foo.bar"}},
waitActionResults{[]actionResult{{
name: "snapshot",
results: map[string]interface{}{},
status: params.ActionFailed,
message: `cannot run "snapshot" action: not defined`,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"pending actions get consumed",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-log")
ctx.writeActionsYaml(c, path, "action-log")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
addAction{"action-log", nil},
addAction{"action-log", nil},
addAction{"action-log", nil},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
waitActionResults{[]actionResult{{
name: "action-log",
results: map[string]interface{}{},
status: params.ActionCompleted,
}, {
name: "action-log",
results: map[string]interface{}{},
status: params.ActionCompleted,
}, {
name: "action-log",
results: map[string]interface{}{},
status: params.ActionCompleted,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"actions not implemented fail but are not errors",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeActionsYaml(c, path, "action-log")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
verifyCharm{},
addAction{"action-log", nil},
waitActionResults{[]actionResult{{
name: "action-log",
results: map[string]interface{}{},
status: params.ActionFailed,
message: `action not implemented on unit "u/0"`,
}}},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
), ut(
"actions may run from ModeHookError, but do not clear the error",
startupErrorWithCustomCharm{
badHook: "start",
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-log")
ctx.writeActionsYaml(c, path, "action-log")
},
},
addAction{"action-log", nil},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{"hook": "start"},
},
waitActionResults{[]actionResult{{
name: "action-log",
results: map[string]interface{}{},
status: params.ActionCompleted,
}}},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "start"`,
data: map[string]interface{}{"hook": "start"},
},
verifyWaiting{},
resolveError{state.ResolvedNoHooks},
waitUnitAgent{status: status.Idle},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Maintenance,
info: "installing charm software",
},
),
})
}
func (s *UniterSuite) TestUniterSubordinates(c *gc.C) {
s.runUniterTests(c, []uniterTest{
// Subordinates.
ut(
"unit becomes dying while subordinates exist",
quickStart{},
addSubordinateRelation{"juju-info"},
waitSubordinateExists{"logging/0"},
unitDying,
waitSubordinateDying{},
waitHooks{"leader-settings-changed", "stop"},
verifyWaiting{},
removeSubordinate{},
waitUniterDead{},
), ut(
"new subordinate becomes necessary while old one is dying",
quickStart{},
addSubordinateRelation{"juju-info"},
waitSubordinateExists{"logging/0"},
removeSubordinateRelation{"juju-info"},
// The subordinate Uniter would usually set Dying in this situation.
subordinateDying,
addSubordinateRelation{"logging-dir"},
verifyRunning{},
removeSubordinate{},
waitSubordinateExists{"logging/1"},
),
})
}
func (s *UniterSuite) TestSubordinateDying(c *gc.C) {
// Create a test context for later use.
ctx := &context{
s: s,
st: s.State,
path: filepath.Join(s.dataDir, "agents", "unit-u-0"),
dataDir: s.dataDir,
charms: make(map[string][]byte),
updateStatusHookTicker: s.updateStatusHookTicker,
charmDirGuard: &mockCharmDirGuard{},
}
addControllerMachine(c, ctx.st)
// Create the subordinate service.
dir := testcharms.Repo.ClonedDir(c.MkDir(), "logging")
curl, err := corecharm.ParseURL("cs:quantal/logging")
c.Assert(err, jc.ErrorIsNil)
curl = curl.WithRevision(dir.Revision())
step(c, ctx, addCharm{dir, curl})
ctx.svc = s.AddTestingService(c, "u", ctx.sch)
// Create the principal service and add a relation.
wps := s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
wpu, err := wps.AddUnit()
c.Assert(err, jc.ErrorIsNil)
eps, err := s.State.InferEndpoints("wordpress", "u")
c.Assert(err, jc.ErrorIsNil)
rel, err := s.State.AddRelation(eps...)
c.Assert(err, jc.ErrorIsNil)
assertAssignUnit(c, s.State, wpu)
// Create the subordinate unit by entering scope as the principal.
wpru, err := rel.Unit(wpu)
c.Assert(err, jc.ErrorIsNil)
err = wpru.EnterScope(nil)
c.Assert(err, jc.ErrorIsNil)
ctx.unit, err = s.State.Unit("u/0")
c.Assert(err, jc.ErrorIsNil)
ctx.apiLogin(c)
// Run the actual test.
ctx.run(c, []stepper{
serveCharm{},
startUniter{},
waitAddresses{},
custom{func(c *gc.C, ctx *context) {
c.Assert(rel.Destroy(), gc.IsNil)
}},
waitUniterDead{},
})
}
func (s *UniterSuite) TestRebootDisabledInActions(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"test that juju-reboot disabled in actions",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
ctx.writeAction(c, path, "action-reboot")
ctx.writeActionsYaml(c, path, "action-reboot")
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
addAction{"action-reboot", nil},
startUniter{},
waitAddresses{},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitActionResults{[]actionResult{{
name: "action-reboot",
results: map[string]interface{}{
"reboot-delayed": "good",
"reboot-now": "good",
},
status: params.ActionCompleted,
}}},
)})
}
func (s *UniterSuite) TestRebootFinishesHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"test that juju-reboot finishes hook, and reboots",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
hpath := filepath.Join(path, "hooks", "install")
ctx.writeExplicitHook(c, hpath, rebootHook)
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUniterDead{err: "machine needs to reboot"},
waitHooks{"install"},
startUniter{},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"leader-elected", "config-changed", "start"},
)})
}
func (s *UniterSuite) TestRebootNowKillsHook(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"test that juju-reboot --now kills hook and exits",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
hpath := filepath.Join(path, "hooks", "install")
ctx.writeExplicitHook(c, hpath, rebootNowHook)
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUniterDead{err: "machine needs to reboot"},
waitHooks{"install"},
startUniter{},
waitUnitAgent{
status: status.Idle,
},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Unknown,
},
waitHooks{"install", "leader-elected", "config-changed", "start"},
)})
}
func (s *UniterSuite) TestRebootDisabledOnHookError(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"test juju-reboot will not happen if hook errors out",
createCharm{
customize: func(c *gc.C, ctx *context, path string) {
hpath := filepath.Join(path, "hooks", "install")
ctx.writeExplicitHook(c, hpath, badRebootHook)
},
},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{},
startUniter{},
waitAddresses{},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: fmt.Sprintf(`hook failed: "install"`),
},
),
})
}
func (s *UniterSuite) TestJujuRunExecutionSerialized(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"hook failed status should stay around after juju run",
createCharm{badHooks: []string{"config-changed"}},
serveCharm{},
createUniter{},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "config-changed"`,
data: map[string]interface{}{
"hook": "config-changed",
},
},
runCommands{"exit 0"},
waitUnitAgent{
statusGetter: unitStatusGetter,
status: status.Error,
info: `hook failed: "config-changed"`,
data: map[string]interface{}{
"hook": "config-changed",
},
},
)})
}
func (s *UniterSuite) TestRebootFromJujuRun(c *gc.C) {
//TODO(bogdanteleaga): Fix this on windows
if runtime.GOOS == "windows" {
c.Skip("bug 1403084: currently does not work on windows")
}
s.runUniterTests(c, []uniterTest{
ut(
"test juju-reboot",
quickStart{},
runCommands{"juju-reboot"},
waitUniterDead{err: "machine needs to reboot"},
startUniter{},
waitHooks{"config-changed"},
), ut(
"test juju-reboot with bad hook",
startupError{"install"},
runCommands{"juju-reboot"},
waitUniterDead{err: "machine needs to reboot"},
startUniter{},
waitHooks{},
), ut(
"test juju-reboot --now",
quickStart{},
runCommands{"juju-reboot --now"},
waitUniterDead{err: "machine needs to reboot"},
startUniter{},
waitHooks{"config-changed"},
), ut(
"test juju-reboot --now with bad hook",
startupError{"install"},
runCommands{"juju-reboot --now"},
waitUniterDead{err: "machine needs to reboot"},
startUniter{},
waitHooks{},
),
})
}
func (s *UniterSuite) TestLeadership(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
"hook tools when leader",
quickStart{},
runCommands{"leader-set foo=bar baz=qux"},
verifyLeaderSettings{"foo": "bar", "baz": "qux"},
), ut(
"hook tools when not leader",
quickStart{minion: true},
runCommands{leadershipScript},
), ut(
"leader-elected triggers when elected",
quickStart{minion: true},
forceLeader{},
waitHooks{"leader-elected"},
), ut(
"leader-settings-changed triggers when leader settings change",
quickStart{minion: true},
setLeaderSettings{"ping": "pong"},
waitHooks{"leader-settings-changed"},
), ut(
"leader-settings-changed triggers when bounced",
quickStart{minion: true},
verifyRunning{minion: true},
), ut(
"leader-settings-changed triggers when deposed (while stopped)",
quickStart{},
stopUniter{},
forceMinion{},
verifyRunning{minion: true},
),
})
}
func (s *UniterSuite) TestLeadershipUnexpectedDepose(c *gc.C) {
s.runUniterTests(c, []uniterTest{
ut(
// NOTE: this is a strange and ugly test, intended to detect what
// *would* happen if the uniter suddenly failed to renew its lease;
// it depends on an artificially shortened tracker refresh time to
// run in a reasonable amount of time.
"leader-settings-changed triggers when deposed (while running)",
quickStart{},
forceMinion{},
waitHooks{"leader-settings-changed"},
),
})
}
func (s *UniterSuite) TestStorage(c *gc.C) {
// appendStorageMetadata customises the wordpress charm's metadata,
// adding a "wp-content" filesystem store. We do it here rather
// than in the charm itself to avoid modifying all of the other
// scenarios.
appendStorageMetadata := func(c *gc.C, ctx *context, path string) {
f, err := os.OpenFile(filepath.Join(path, "metadata.yaml"), os.O_RDWR|os.O_APPEND, 0644)
c.Assert(err, jc.ErrorIsNil)
defer func() {
err := f.Close()
c.Assert(err, jc.ErrorIsNil)
}()
_, err = io.WriteString(f, `
storage:
wp-content:
type: filesystem
multiple:
range: 0-
`[1:])
c.Assert(err, jc.ErrorIsNil)
}
storageConstraints := map[string]state.StorageConstraints{
"wp-content": {Count: 1},
}
s.runUniterTests(c, []uniterTest{
ut(
"test that storage-attached is called",
createCharm{customize: appendStorageMetadata},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{storage: storageConstraints},
provisionStorage{},
startUniter{},
waitAddresses{},
waitHooks{"wp-content-storage-attached"},
waitHooks(startupHooks(false)),
), ut(
"test that storage-detaching is called before stop",
createCharm{customize: appendStorageMetadata},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{storage: storageConstraints},
provisionStorage{},
startUniter{},
waitAddresses{},
waitHooks{"wp-content-storage-attached"},
waitHooks(startupHooks(false)),
unitDying,
waitHooks{"leader-settings-changed"},
// "stop" hook is not called until storage is detached
waitHooks{"wp-content-storage-detaching", "stop"},
verifyStorageDetached{},
waitUniterDead{},
), ut(
"test that storage-detaching is called only if previously attached",
createCharm{customize: appendStorageMetadata},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{storage: storageConstraints},
// provision and destroy the storage before the uniter starts,
// to ensure it never sees the storage as attached
provisionStorage{},
destroyStorageAttachment{},
startUniter{},
waitHooks(startupHooks(false)),
unitDying,
// storage-detaching is not called because it was never attached
waitHooks{"leader-settings-changed", "stop"},
verifyStorageDetached{},
waitUniterDead{},
), ut(
"test that delay-provisioned storage does not block forever",
createCharm{customize: appendStorageMetadata},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{storage: storageConstraints},
startUniter{},
// no hooks should be run, as storage isn't provisioned
waitHooks{},
provisionStorage{},
waitHooks{"wp-content-storage-attached"},
waitHooks(startupHooks(false)),
), ut(
"test that unprovisioned storage does not block unit termination",
createCharm{customize: appendStorageMetadata},
serveCharm{},
ensureStateWorker{},
createServiceAndUnit{storage: storageConstraints},
unitDying,
startUniter{},
// no hooks should be run, and unit agent should terminate
waitHooks{},
waitUniterDead{},
),
// TODO(axw) test that storage-attached is run for new
// storage attachments before upgrade-charm is run. This
// requires additions to state to add storage when a charm
// is upgraded.
})
}
var mockExecutorErr = errors.New("some error occurred")
type mockExecutor struct {
operation.Executor
}
func (m *mockExecutor) Run(op operation.Operation) error {
// want to allow charm unpacking to occur
if strings.HasPrefix(op.String(), "install") {
return m.Executor.Run(op)
}
// but hooks should error
return mockExecutorErr
}
func (s *UniterSuite) TestOperationErrorReported(c *gc.C) {
executorFunc := func(stateFilePath string, getInstallCharm func() (*corecharm.URL, error), acquireLock func() (mutex.Releaser, error)) (operation.Executor, error) {
e, err := operation.NewExecutor(stateFilePath, getInstallCharm, acquireLock)
c.Assert(err, jc.ErrorIsNil)
return &mockExecutor{e}, nil
}
s.runUniterTests(c, []uniterTest{
ut(
"error running operations are reported",
createCharm{},
serveCharm{},
createUniter{executorFunc: executorFunc},
waitUnitAgent{
status: status.Failed,
info: "resolver loop error",
},
expectError{".*some error occurred.*"},
),
})
}
func (s *UniterSuite) TestTranslateResolverError(c *gc.C) {
executorFunc := func(stateFilePath string, getInstallCharm func() (*corecharm.URL, error), acquireLock func() (mutex.Releaser, error)) (operation.Executor, error) {
e, err := operation.NewExecutor(stateFilePath, getInstallCharm, acquireLock)
c.Assert(err, jc.ErrorIsNil)
return &mockExecutor{e}, nil
}
translateResolverErr := func(in error) error {
c.Check(errors.Cause(in), gc.Equals, mockExecutorErr)
return errors.New("some other error")
}
s.runUniterTests(c, []uniterTest{
ut(
"resolver errors are translated",
createCharm{},
serveCharm{},
createUniter{
executorFunc: executorFunc,
translateResolverErr: translateResolverErr,
},
waitUnitAgent{
status: status.Failed,
info: "resolver loop error",
},
expectError{".*some other error.*"},
),
})
}