github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/worker/uniter/relationer_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter_test
     5  
     6  import (
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/juju/charm/hooks"
    12  	"github.com/juju/errors"
    13  	jc "github.com/juju/testing/checkers"
    14  	ft "github.com/juju/testing/filetesting"
    15  	"github.com/juju/utils"
    16  	gc "launchpad.net/gocheck"
    17  
    18  	jujutesting "github.com/juju/juju/juju/testing"
    19  	"github.com/juju/juju/network"
    20  	"github.com/juju/juju/state"
    21  	"github.com/juju/juju/state/api"
    22  	apiuniter "github.com/juju/juju/state/api/uniter"
    23  	coretesting "github.com/juju/juju/testing"
    24  	"github.com/juju/juju/worker/uniter"
    25  	"github.com/juju/juju/worker/uniter/hook"
    26  	"github.com/juju/juju/worker/uniter/relation"
    27  )
    28  
    29  type RelationerSuite struct {
    30  	jujutesting.JujuConnSuite
    31  	hooks   chan hook.Info
    32  	svc     *state.Service
    33  	rel     *state.Relation
    34  	dir     *relation.StateDir
    35  	dirPath string
    36  
    37  	st         *api.State
    38  	uniter     *apiuniter.State
    39  	apiRelUnit *apiuniter.RelationUnit
    40  }
    41  
    42  var _ = gc.Suite(&RelationerSuite{})
    43  
    44  func (s *RelationerSuite) SetUpTest(c *gc.C) {
    45  	s.JujuConnSuite.SetUpTest(c)
    46  	var err error
    47  	s.svc = s.AddTestingService(c, "u", s.AddTestingCharm(c, "riak"))
    48  	c.Assert(err, gc.IsNil)
    49  	rels, err := s.svc.Relations()
    50  	c.Assert(err, gc.IsNil)
    51  	c.Assert(rels, gc.HasLen, 1)
    52  	s.rel = rels[0]
    53  	_, unit := s.AddRelationUnit(c, "u/0")
    54  	s.dirPath = c.MkDir()
    55  	s.dir, err = relation.ReadStateDir(s.dirPath, s.rel.Id())
    56  	c.Assert(err, gc.IsNil)
    57  	s.hooks = make(chan hook.Info)
    58  
    59  	password, err := utils.RandomPassword()
    60  	c.Assert(err, gc.IsNil)
    61  	err = unit.SetPassword(password)
    62  	c.Assert(err, gc.IsNil)
    63  	s.st = s.OpenAPIAs(c, unit.Tag(), password)
    64  	s.uniter = s.st.Uniter()
    65  	c.Assert(s.uniter, gc.NotNil)
    66  
    67  	apiUnit, err := s.uniter.Unit(unit.Tag())
    68  	c.Assert(err, gc.IsNil)
    69  	apiRel, err := s.uniter.Relation(s.rel.Tag())
    70  	c.Assert(err, gc.IsNil)
    71  	s.apiRelUnit, err = apiRel.Unit(apiUnit)
    72  	c.Assert(err, gc.IsNil)
    73  }
    74  
    75  func (s *RelationerSuite) AddRelationUnit(c *gc.C, name string) (*state.RelationUnit, *state.Unit) {
    76  	u, err := s.svc.AddUnit()
    77  	c.Assert(err, gc.IsNil)
    78  	c.Assert(u.Name(), gc.Equals, name)
    79  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
    80  	c.Assert(err, gc.IsNil)
    81  	err = u.AssignToMachine(machine)
    82  	c.Assert(err, gc.IsNil)
    83  	privateAddr := network.NewAddress(
    84  		strings.Replace(name, "/", "-", 1)+".testing.invalid", network.ScopeCloudLocal)
    85  	err = machine.SetAddresses(privateAddr)
    86  	c.Assert(err, gc.IsNil)
    87  	ru, err := s.rel.Unit(u)
    88  	c.Assert(err, gc.IsNil)
    89  	return ru, u
    90  }
    91  
    92  func (s *RelationerSuite) TestStateDir(c *gc.C) {
    93  	// Create the relationer; check its state dir is not created.
    94  	r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks)
    95  	path := strconv.Itoa(s.rel.Id())
    96  	ft.Removed{path}.Check(c, s.dirPath)
    97  
    98  	// Join the relation; check the dir was created.
    99  	err := r.Join()
   100  	c.Assert(err, gc.IsNil)
   101  	ft.Dir{path, 0755}.Check(c, s.dirPath)
   102  
   103  	// Prepare to depart the relation; check the dir is still there.
   104  	hi := hook.Info{Kind: hooks.RelationBroken}
   105  	_, err = r.PrepareHook(hi)
   106  	c.Assert(err, gc.IsNil)
   107  	ft.Dir{path, 0755}.Check(c, s.dirPath)
   108  
   109  	// Actually depart it; check the dir is removed.
   110  	err = r.CommitHook(hi)
   111  	c.Assert(err, gc.IsNil)
   112  	ft.Removed{path}.Check(c, s.dirPath)
   113  }
   114  
   115  func (s *RelationerSuite) TestEnterLeaveScope(c *gc.C) {
   116  	ru1, _ := s.AddRelationUnit(c, "u/1")
   117  	r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks)
   118  
   119  	// u/1 does not consider u/0 to be alive.
   120  	w := ru1.Watch()
   121  	defer stop(c, w)
   122  	s.State.StartSync()
   123  	ch, ok := <-w.Changes()
   124  	c.Assert(ok, gc.Equals, true)
   125  	c.Assert(ch.Changed, gc.HasLen, 0)
   126  	c.Assert(ch.Departed, gc.HasLen, 0)
   127  
   128  	// u/0 enters scope; u/1 observes it.
   129  	err := r.Join()
   130  	c.Assert(err, gc.IsNil)
   131  	s.State.StartSync()
   132  	select {
   133  	case ch, ok := <-w.Changes():
   134  		c.Assert(ok, gc.Equals, true)
   135  		c.Assert(ch.Changed, gc.HasLen, 1)
   136  		_, found := ch.Changed["u/0"]
   137  		c.Assert(found, gc.Equals, true)
   138  		c.Assert(ch.Departed, gc.HasLen, 0)
   139  	case <-time.After(coretesting.LongWait):
   140  		c.Fatalf("timed out waiting for presence detection")
   141  	}
   142  
   143  	// re-Join is no-op.
   144  	err = r.Join()
   145  	c.Assert(err, gc.IsNil)
   146  	// TODO(jam): This would be a great to replace with statetesting.NotifyWatcherC
   147  	s.State.StartSync()
   148  	select {
   149  	case ch, ok := <-w.Changes():
   150  		c.Fatalf("got unexpected change: %#v, %#v", ch, ok)
   151  	case <-time.After(coretesting.ShortWait):
   152  	}
   153  
   154  	// u/0 leaves scope; u/1 observes it.
   155  	hi := hook.Info{Kind: hooks.RelationBroken}
   156  	_, err = r.PrepareHook(hi)
   157  	c.Assert(err, gc.IsNil)
   158  
   159  	err = r.CommitHook(hi)
   160  	c.Assert(err, gc.IsNil)
   161  	s.State.StartSync()
   162  	select {
   163  	case ch, ok := <-w.Changes():
   164  		c.Assert(ok, gc.Equals, true)
   165  		c.Assert(ch.Changed, gc.HasLen, 0)
   166  		c.Assert(ch.Departed, gc.DeepEquals, []string{"u/0"})
   167  	case <-time.After(worstCase):
   168  		c.Fatalf("timed out waiting for absence detection")
   169  	}
   170  }
   171  
   172  func (s *RelationerSuite) TestStartStopHooks(c *gc.C) {
   173  	ru1, _ := s.AddRelationUnit(c, "u/1")
   174  	ru2, _ := s.AddRelationUnit(c, "u/2")
   175  	r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks)
   176  	c.Assert(r.IsImplicit(), gc.Equals, false)
   177  	err := r.Join()
   178  	c.Assert(err, gc.IsNil)
   179  
   180  	// Check no hooks are being sent.
   181  	s.assertNoHook(c)
   182  
   183  	// Start hooks, and check that still no changes are sent.
   184  	r.StartHooks()
   185  	defer stopHooks(c, r)
   186  	s.assertNoHook(c)
   187  
   188  	// Check we can't start hooks again.
   189  	f := func() { r.StartHooks() }
   190  	c.Assert(f, gc.PanicMatches, "hooks already started!")
   191  
   192  	// Join u/1 to the relation, and check that we receive the expected hooks.
   193  	settings := map[string]interface{}{"unit": "settings"}
   194  	err = ru1.EnterScope(settings)
   195  	c.Assert(err, gc.IsNil)
   196  	s.assertHook(c, hook.Info{
   197  		Kind:       hooks.RelationJoined,
   198  		RemoteUnit: "u/1",
   199  	})
   200  	s.assertHook(c, hook.Info{
   201  		Kind:       hooks.RelationChanged,
   202  		RemoteUnit: "u/1",
   203  	})
   204  	s.assertNoHook(c)
   205  
   206  	// Stop hooks, make more changes, check no events.
   207  	err = r.StopHooks()
   208  	c.Assert(err, gc.IsNil)
   209  	err = ru1.LeaveScope()
   210  	c.Assert(err, gc.IsNil)
   211  	err = ru2.EnterScope(nil)
   212  	c.Assert(err, gc.IsNil)
   213  	node, err := ru2.Settings()
   214  	c.Assert(err, gc.IsNil)
   215  	node.Set("private-address", "roehampton")
   216  	_, err = node.Write()
   217  	c.Assert(err, gc.IsNil)
   218  	s.assertNoHook(c)
   219  
   220  	// Stop hooks again to verify safety.
   221  	err = r.StopHooks()
   222  	c.Assert(err, gc.IsNil)
   223  	s.assertNoHook(c)
   224  
   225  	// Start them again, and check we get the expected events sent.
   226  	r.StartHooks()
   227  	defer stopHooks(c, r)
   228  	s.assertHook(c, hook.Info{
   229  		Kind:       hooks.RelationDeparted,
   230  		RemoteUnit: "u/1",
   231  	})
   232  	s.assertHook(c, hook.Info{
   233  		Kind:          hooks.RelationJoined,
   234  		ChangeVersion: 1,
   235  		RemoteUnit:    "u/2",
   236  	})
   237  	s.assertHook(c, hook.Info{
   238  		Kind:          hooks.RelationChanged,
   239  		ChangeVersion: 1,
   240  		RemoteUnit:    "u/2",
   241  	})
   242  	s.assertNoHook(c)
   243  
   244  	// Stop them again, just to be sure.
   245  	err = r.StopHooks()
   246  	c.Assert(err, gc.IsNil)
   247  	s.assertNoHook(c)
   248  }
   249  
   250  func (s *RelationerSuite) TestPrepareCommitHooks(c *gc.C) {
   251  	r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks)
   252  	err := r.Join()
   253  	c.Assert(err, gc.IsNil)
   254  	ctx := r.Context()
   255  	c.Assert(ctx.UnitNames(), gc.HasLen, 0)
   256  
   257  	// Check preparing an invalid hook changes nothing.
   258  	changed := hook.Info{
   259  		Kind:          hooks.RelationChanged,
   260  		RemoteUnit:    "u/1",
   261  		ChangeVersion: 7,
   262  	}
   263  	_, err = r.PrepareHook(changed)
   264  	c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`)
   265  	c.Assert(ctx.UnitNames(), gc.HasLen, 0)
   266  	c.Assert(s.dir.State().Members, gc.HasLen, 0)
   267  
   268  	// Check preparing a valid hook updates the context, but not persistent
   269  	// relation state.
   270  	joined := hook.Info{
   271  		Kind:       hooks.RelationJoined,
   272  		RemoteUnit: "u/1",
   273  	}
   274  	name, err := r.PrepareHook(joined)
   275  	c.Assert(err, gc.IsNil)
   276  	c.Assert(s.dir.State().Members, gc.HasLen, 0)
   277  	c.Assert(name, gc.Equals, "ring-relation-joined")
   278  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"})
   279  
   280  	// Check that preparing the following hook fails as before...
   281  	_, err = r.PrepareHook(changed)
   282  	c.Assert(err, gc.ErrorMatches, `inappropriate "relation-changed" for "u/1": unit has not joined`)
   283  	c.Assert(s.dir.State().Members, gc.HasLen, 0)
   284  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"})
   285  
   286  	// ...but that committing the previous hook updates the persistent
   287  	// relation state...
   288  	err = r.CommitHook(joined)
   289  	c.Assert(err, gc.IsNil)
   290  	c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 0})
   291  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"})
   292  
   293  	// ...and allows us to prepare the next hook...
   294  	name, err = r.PrepareHook(changed)
   295  	c.Assert(err, gc.IsNil)
   296  	c.Assert(name, gc.Equals, "ring-relation-changed")
   297  	c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 0})
   298  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"})
   299  
   300  	// ...and commit it.
   301  	err = r.CommitHook(changed)
   302  	c.Assert(err, gc.IsNil)
   303  	c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 7})
   304  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1"})
   305  
   306  	// To verify implied behaviour above, prepare a new joined hook with
   307  	// missing membership information, and check relation context
   308  	// membership is updated appropriately...
   309  	joined.RemoteUnit = "u/2"
   310  	joined.ChangeVersion = 3
   311  	name, err = r.PrepareHook(joined)
   312  	c.Assert(err, gc.IsNil)
   313  	c.Assert(s.dir.State().Members, gc.HasLen, 1)
   314  	c.Assert(name, gc.Equals, "ring-relation-joined")
   315  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1", "u/2"})
   316  
   317  	// ...and so is relation state on commit.
   318  	err = r.CommitHook(joined)
   319  	c.Assert(err, gc.IsNil)
   320  	c.Assert(s.dir.State().Members, gc.DeepEquals, map[string]int64{"u/1": 7, "u/2": 3})
   321  	c.Assert(ctx.UnitNames(), gc.DeepEquals, []string{"u/1", "u/2"})
   322  }
   323  
   324  func (s *RelationerSuite) TestSetDying(c *gc.C) {
   325  	ru1, _ := s.AddRelationUnit(c, "u/1")
   326  	settings := map[string]interface{}{"unit": "settings"}
   327  	err := ru1.EnterScope(settings)
   328  	c.Assert(err, gc.IsNil)
   329  	r := uniter.NewRelationer(s.apiRelUnit, s.dir, s.hooks)
   330  	err = r.Join()
   331  	c.Assert(err, gc.IsNil)
   332  	r.StartHooks()
   333  	defer stopHooks(c, r)
   334  	s.assertHook(c, hook.Info{
   335  		Kind:       hooks.RelationJoined,
   336  		RemoteUnit: "u/1",
   337  	})
   338  
   339  	// While a changed hook is still pending, the relation (or possibly the unit,
   340  	// pending lifecycle work), changes Life to Dying, and the relationer is
   341  	// informed.
   342  	err = r.SetDying()
   343  	c.Assert(err, gc.IsNil)
   344  
   345  	// Check that we cannot rejoin the relation.
   346  	f := func() { r.Join() }
   347  	c.Assert(f, gc.PanicMatches, "dying relationer must not join!")
   348  
   349  	// ...but the hook stream continues, sending the required changed hook for
   350  	// u/1 before moving on to a departed, despite the fact that its pinger is
   351  	// still running, and closing with a broken.
   352  	s.assertHook(c, hook.Info{Kind: hooks.RelationChanged, RemoteUnit: "u/1"})
   353  	s.assertHook(c, hook.Info{Kind: hooks.RelationDeparted, RemoteUnit: "u/1"})
   354  	s.assertHook(c, hook.Info{Kind: hooks.RelationBroken})
   355  
   356  	// Check that the relation state has been broken.
   357  	err = s.dir.State().Validate(hook.Info{Kind: hooks.RelationBroken})
   358  	c.Assert(err, gc.ErrorMatches, ".*: relation is broken and cannot be changed further")
   359  }
   360  
   361  func (s *RelationerSuite) assertNoHook(c *gc.C) {
   362  	s.BackingState.StartSync()
   363  	select {
   364  	case hi, ok := <-s.hooks:
   365  		c.Fatalf("got unexpected hook info %#v (%t)", hi, ok)
   366  	case <-time.After(coretesting.ShortWait):
   367  	}
   368  }
   369  
   370  func (s *RelationerSuite) assertHook(c *gc.C, expect hook.Info) {
   371  	s.BackingState.StartSync()
   372  	// We must ensure the local state dir exists first.
   373  	c.Assert(s.dir.Ensure(), gc.IsNil)
   374  	select {
   375  	case hi, ok := <-s.hooks:
   376  		c.Assert(ok, gc.Equals, true)
   377  		expect.ChangeVersion = hi.ChangeVersion
   378  		c.Assert(hi, gc.DeepEquals, expect)
   379  		c.Assert(s.dir.Write(hi), gc.Equals, nil)
   380  	case <-time.After(coretesting.LongWait):
   381  		c.Fatalf("timed out waiting for %#v", expect)
   382  	}
   383  }
   384  
   385  type stopper interface {
   386  	Stop() error
   387  }
   388  
   389  func stop(c *gc.C, s stopper) {
   390  	c.Assert(s.Stop(), gc.IsNil)
   391  }
   392  
   393  func stopHooks(c *gc.C, r *uniter.Relationer) {
   394  	c.Assert(r.StopHooks(), gc.IsNil)
   395  }
   396  
   397  type RelationerImplicitSuite struct {
   398  	jujutesting.JujuConnSuite
   399  }
   400  
   401  var _ = gc.Suite(&RelationerImplicitSuite{})
   402  
   403  func (s *RelationerImplicitSuite) TestImplicitRelationer(c *gc.C) {
   404  	// Create a relationer for an implicit endpoint (mysql:juju-info).
   405  	mysql := s.AddTestingService(c, "mysql", s.AddTestingCharm(c, "mysql"))
   406  	u, err := mysql.AddUnit()
   407  	c.Assert(err, gc.IsNil)
   408  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
   409  	c.Assert(err, gc.IsNil)
   410  	err = u.AssignToMachine(machine)
   411  	c.Assert(err, gc.IsNil)
   412  	err = machine.SetAddresses(network.NewAddress("blah", network.ScopeCloudLocal))
   413  	c.Assert(err, gc.IsNil)
   414  	logging := s.AddTestingService(c, "logging", s.AddTestingCharm(c, "logging"))
   415  	eps, err := s.State.InferEndpoints([]string{"logging", "mysql"})
   416  	c.Assert(err, gc.IsNil)
   417  	rel, err := s.State.AddRelation(eps...)
   418  	c.Assert(err, gc.IsNil)
   419  	relsDir := c.MkDir()
   420  	dir, err := relation.ReadStateDir(relsDir, rel.Id())
   421  	c.Assert(err, gc.IsNil)
   422  	hooks := make(chan hook.Info)
   423  
   424  	password, err := utils.RandomPassword()
   425  	c.Assert(err, gc.IsNil)
   426  	err = u.SetPassword(password)
   427  	c.Assert(err, gc.IsNil)
   428  	st := s.OpenAPIAs(c, u.Tag(), password)
   429  	uniterState := st.Uniter()
   430  	c.Assert(uniterState, gc.NotNil)
   431  
   432  	apiUnit, err := uniterState.Unit(u.Tag())
   433  	c.Assert(err, gc.IsNil)
   434  	apiRel, err := uniterState.Relation(rel.Tag())
   435  	c.Assert(err, gc.IsNil)
   436  	apiRelUnit, err := apiRel.Unit(apiUnit)
   437  	c.Assert(err, gc.IsNil)
   438  
   439  	r := uniter.NewRelationer(apiRelUnit, dir, hooks)
   440  	c.Assert(r, jc.Satisfies, (*uniter.Relationer).IsImplicit)
   441  
   442  	// Join the relation.
   443  	err = r.Join()
   444  	c.Assert(err, gc.IsNil)
   445  	sub, err := logging.Unit("logging/0")
   446  	c.Assert(err, gc.IsNil)
   447  
   448  	// Join the other side; check no hooks are sent.
   449  	r.StartHooks()
   450  	defer func() { c.Assert(r.StopHooks(), gc.IsNil) }()
   451  	subru, err := rel.Unit(sub)
   452  	c.Assert(err, gc.IsNil)
   453  	err = subru.EnterScope(map[string]interface{}{"some": "data"})
   454  	c.Assert(err, gc.IsNil)
   455  	s.State.StartSync()
   456  	select {
   457  	case <-time.After(coretesting.ShortWait):
   458  	case <-hooks:
   459  		c.Fatalf("unexpected hook generated")
   460  	}
   461  
   462  	// Set it to Dying; check that the dir is removed immediately.
   463  	err = r.SetDying()
   464  	c.Assert(err, gc.IsNil)
   465  	path := strconv.Itoa(rel.Id())
   466  	ft.Removed{path}.Check(c, relsDir)
   467  
   468  	// Check that it left scope, by leaving scope on the other side and destroying
   469  	// the relation.
   470  	err = subru.LeaveScope()
   471  	c.Assert(err, gc.IsNil)
   472  	err = rel.Destroy()
   473  	c.Assert(err, gc.IsNil)
   474  	err = rel.Refresh()
   475  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   476  
   477  	// Verify that no other hooks were sent at any stage.
   478  	select {
   479  	case <-hooks:
   480  		c.Fatalf("unexpected hook generated")
   481  	default:
   482  	}
   483  }