github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/peergrouper/desired_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package peergrouper
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/juju/replicaset/v3"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	gc "gopkg.in/check.v1"
    17  
    18  	"github.com/juju/juju/core/network"
    19  	"github.com/juju/juju/state"
    20  )
    21  
    22  type desiredPeerGroupSuite struct {
    23  	testing.IsolationSuite
    24  }
    25  
    26  var _ = gc.Suite(&desiredPeerGroupSuite{})
    27  
    28  const (
    29  	mongoPort         = 1234
    30  	apiPort           = 5678
    31  	controllerAPIPort = 9876
    32  )
    33  
    34  type desiredPeerGroupTest struct {
    35  	about    string
    36  	machines []*controllerTracker
    37  	statuses []replicaset.MemberStatus
    38  	members  []replicaset.Member
    39  
    40  	expectChanged  bool
    41  	expectStepDown bool
    42  	expectMembers  []replicaset.Member
    43  	expectVoting   []bool
    44  	expectErr      string
    45  }
    46  
    47  // TestMember mirrors replicaset.Member but simplifies the structure
    48  // so that test assertions are easier to understand.
    49  //
    50  // See http://docs.mongodb.org/manual/reference/replica-configuration/
    51  // for more details
    52  type TestMember struct {
    53  	// Id is a unique id for a member in a set.
    54  	Id int
    55  
    56  	// Address holds the network address of the member,
    57  	// in the form hostname:port.
    58  	Address string
    59  
    60  	// Priority determines eligibility of a member to become primary.
    61  	// This value is optional; it defaults to 1.
    62  	Priority float64
    63  
    64  	// Tags store additional information about a replica member, often used for
    65  	// customizing read preferences and write concern.
    66  	Tags map[string]string
    67  
    68  	// Votes controls the number of votes a server has in a replica set election.
    69  	// This value is optional; it defaults to 1.
    70  	Votes int
    71  }
    72  
    73  func memberToTestMember(m replicaset.Member) TestMember {
    74  
    75  	priority := 1.0
    76  	if m.Priority != nil {
    77  		priority = *m.Priority
    78  	}
    79  	votes := 1
    80  	if m.Votes != nil {
    81  		votes = *m.Votes
    82  	}
    83  	return TestMember{
    84  		Id:       m.Id,
    85  		Address:  m.Address,
    86  		Priority: priority,
    87  		Tags:     m.Tags,
    88  		Votes:    votes,
    89  	}
    90  }
    91  
    92  func membersToTestMembers(m []replicaset.Member) []TestMember {
    93  	if m == nil {
    94  		return nil
    95  	}
    96  	result := make([]TestMember, len(m))
    97  	for i, member := range m {
    98  		result[i] = memberToTestMember(member)
    99  	}
   100  	return result
   101  }
   102  
   103  func desiredPeerGroupTests(ipVersion TestIPVersion) []desiredPeerGroupTest {
   104  	return []desiredPeerGroupTest{
   105  		{
   106  			about:         "one controller, one more proposed member",
   107  			machines:      mkMachines("10v 11v", ipVersion),
   108  			statuses:      mkStatuses("0p", ipVersion),
   109  			members:       mkMembers("0v", ipVersion),
   110  			expectMembers: mkMembers("0v 1", ipVersion),
   111  			expectVoting:  []bool{true, false},
   112  			expectChanged: true,
   113  		}, {
   114  			about:         "one controller, two more proposed members",
   115  			machines:      mkMachines("10v 11v 12v", ipVersion),
   116  			statuses:      mkStatuses("0p", ipVersion),
   117  			members:       mkMembers("0v", ipVersion),
   118  			expectMembers: mkMembers("0v 1 2", ipVersion),
   119  			expectVoting:  []bool{true, false, false},
   120  			expectChanged: true,
   121  		}, {
   122  			about:         "single controller, no change",
   123  			machines:      mkMachines("11v", ipVersion),
   124  			members:       mkMembers("1v", ipVersion),
   125  			statuses:      mkStatuses("1p", ipVersion),
   126  			expectVoting:  []bool{true},
   127  			expectMembers: mkMembers("1v", ipVersion),
   128  			expectChanged: false,
   129  		}, {
   130  			about:        "extra member with nil Vote",
   131  			machines:     mkMachines("11v", ipVersion),
   132  			members:      mkMembers("1vT 2v", ipVersion),
   133  			statuses:     mkStatuses("1p 2s", ipVersion),
   134  			expectVoting: []bool{true},
   135  			expectErr:    "non juju voting member.* found in peer group",
   136  		}, {
   137  			about:    "extra member with >1 votes",
   138  			machines: mkMachines("11v", ipVersion),
   139  			members: append(mkMembers("1vT", ipVersion), replicaset.Member{
   140  				Id:    2,
   141  				Votes: newInt(2),
   142  				Address: net.JoinHostPort(
   143  					fmt.Sprintf(ipVersion.formatHost, 12),
   144  					fmt.Sprint(mongoPort),
   145  				),
   146  			}),
   147  			statuses:     mkStatuses("1p 2s", ipVersion),
   148  			expectVoting: []bool{true},
   149  			expectErr:    "non juju voting member.* found in peer group",
   150  		}, {
   151  			about:         "one controller has become ready to vote (no change)",
   152  			machines:      mkMachines("11v 12v", ipVersion),
   153  			members:       mkMembers("1v 2", ipVersion),
   154  			statuses:      mkStatuses("1p 2s", ipVersion),
   155  			expectVoting:  []bool{true, false},
   156  			expectMembers: mkMembers("1v 2", ipVersion),
   157  			expectChanged: false,
   158  		}, {
   159  			about:         "two machines have become ready to vote (-> added)",
   160  			machines:      mkMachines("11v 12v 13v", ipVersion),
   161  			members:       mkMembers("1v 2 3", ipVersion),
   162  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   163  			expectVoting:  []bool{true, true, true},
   164  			expectMembers: mkMembers("1v 2v 3v", ipVersion),
   165  			expectChanged: true,
   166  		}, {
   167  			about:         "one controller has become ready to vote but one is not healthy",
   168  			machines:      mkMachines("11v 12v", ipVersion),
   169  			members:       mkMembers("1v 2", ipVersion),
   170  			statuses:      mkStatuses("1p 2sH", ipVersion),
   171  			expectVoting:  []bool{true, false},
   172  			expectMembers: mkMembers("1v 2", ipVersion),
   173  			expectChanged: false,
   174  		}, {
   175  			about:         "two machines have become ready to vote but one is not healthy (-> no change)",
   176  			machines:      mkMachines("11v 12v 13v", ipVersion),
   177  			members:       mkMembers("1v 2 3", ipVersion),
   178  			statuses:      mkStatuses("1p 2s 3sH", ipVersion),
   179  			expectVoting:  []bool{true, false, false},
   180  			expectMembers: mkMembers("1v 2 3", ipVersion),
   181  			expectChanged: false,
   182  		}, {
   183  			about:         "three machines have become ready to vote (-> 2 added)",
   184  			machines:      mkMachines("11v 12v 13v 14v", ipVersion),
   185  			members:       mkMembers("1v 2 3 4", ipVersion),
   186  			statuses:      mkStatuses("1p 2s 3s 4s", ipVersion),
   187  			expectVoting:  []bool{true, true, true, false},
   188  			expectMembers: mkMembers("1v 2v 3v 4", ipVersion),
   189  			expectChanged: true,
   190  		}, {
   191  			about:         "one controller ready to lose vote with no others -> no change",
   192  			machines:      mkMachines("11", ipVersion),
   193  			members:       mkMembers("1v", ipVersion),
   194  			statuses:      mkStatuses("1p", ipVersion),
   195  			expectVoting:  []bool{true},
   196  			expectMembers: mkMembers("1v", ipVersion),
   197  			expectChanged: false,
   198  		}, {
   199  			about:         "one controller ready to lose vote -> votes removed from secondaries",
   200  			machines:      mkMachines("11v 12v 13", ipVersion),
   201  			members:       mkMembers("1v 2v 3v", ipVersion),
   202  			statuses:      mkStatuses("1s 2p 3s", ipVersion),
   203  			expectVoting:  []bool{false, true, false},
   204  			expectMembers: mkMembers("1 2v 3", ipVersion),
   205  			expectChanged: true,
   206  		}, {
   207  			about:         "two machines ready to lose vote -> votes removed",
   208  			machines:      mkMachines("11 12v 13", ipVersion),
   209  			members:       mkMembers("1v 2v 3v", ipVersion),
   210  			statuses:      mkStatuses("1s 2p 3s", ipVersion),
   211  			expectVoting:  []bool{false, true, false},
   212  			expectMembers: mkMembers("1 2v 3", ipVersion),
   213  			expectChanged: true,
   214  		}, {
   215  			about:         "machines removed as controller -> removed from members",
   216  			machines:      mkMachines("11v", ipVersion),
   217  			members:       mkMembers("1v 2 3", ipVersion),
   218  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   219  			expectVoting:  []bool{true},
   220  			expectMembers: mkMembers("1v", ipVersion),
   221  			expectChanged: true,
   222  		}, {
   223  			about: "controller dead -> removed from members",
   224  			machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{
   225  				id: "13",
   226  				addresses: []network.SpaceAddress{{
   227  					MachineAddress: network.MachineAddress{
   228  						Value: ipVersion.extraHost,
   229  						Type:  ipVersion.addressType,
   230  						Scope: network.ScopeCloudLocal,
   231  					},
   232  				}},
   233  				host: mkController(state.Dead),
   234  			}),
   235  			members:       mkMembers("1v 2v 3v", ipVersion),
   236  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   237  			expectVoting:  []bool{true, false},
   238  			expectMembers: mkMembers("1v 2s", ipVersion),
   239  			expectChanged: true,
   240  		}, {
   241  			about:         "controller removed as controller -> removed from member",
   242  			machines:      mkMachines("11v 12", ipVersion),
   243  			members:       mkMembers("1v 2 3", ipVersion),
   244  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   245  			expectVoting:  []bool{true, false},
   246  			expectMembers: mkMembers("1v 2", ipVersion),
   247  			expectChanged: true,
   248  		}, {
   249  			about:         "a candidate can take the vote of a non-candidate when they're ready",
   250  			machines:      mkMachines("11v 12v 13 14v", ipVersion),
   251  			members:       mkMembers("1v 2v 3v 4", ipVersion),
   252  			statuses:      mkStatuses("1p 2s 3s 4s", ipVersion),
   253  			expectVoting:  []bool{true, true, false, true},
   254  			expectMembers: mkMembers("1v 2v 3 4v", ipVersion),
   255  			expectChanged: true,
   256  		}, {
   257  			about:         "several candidates can take non-candidates' votes",
   258  			machines:      mkMachines("11v 12v 13 14 15 16v 17v 18v", ipVersion),
   259  			members:       mkMembers("1v 2v 3v 4v 5v 6 7 8", ipVersion),
   260  			statuses:      mkStatuses("1p 2s 3s 4s 5s 6s 7s 8s", ipVersion),
   261  			expectVoting:  []bool{true, true, false, false, false, true, true, true},
   262  			expectMembers: mkMembers("1v 2v 3 4 5 6v 7v 8v", ipVersion),
   263  			expectChanged: true,
   264  		}, {
   265  			about: "a changed controller address should propagate to the members",
   266  			machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{
   267  				id:        "13",
   268  				wantsVote: true,
   269  				addresses: []network.SpaceAddress{{
   270  					MachineAddress: network.MachineAddress{
   271  						Value: ipVersion.extraHost,
   272  						Type:  ipVersion.addressType,
   273  						Scope: network.ScopeCloudLocal,
   274  					},
   275  				}},
   276  				host: mkController(state.Alive),
   277  			}),
   278  			statuses:     mkStatuses("1s 2p 3s", ipVersion),
   279  			members:      mkMembers("1v 2v 3v", ipVersion),
   280  			expectVoting: []bool{true, true, true},
   281  			expectMembers: append(mkMembers("1v 2v", ipVersion), replicaset.Member{
   282  				Id:      3,
   283  				Address: net.JoinHostPort(ipVersion.extraHost, fmt.Sprint(mongoPort)),
   284  				Tags:    memberTag("13"),
   285  			}),
   286  			expectChanged: true,
   287  		}, {
   288  			about: "a controller's address is ignored if it changes to empty",
   289  			machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{
   290  				id:        "13",
   291  				wantsVote: true,
   292  				host:      mkController(state.Alive),
   293  			}),
   294  			statuses:      mkStatuses("1s 2p 3s", ipVersion),
   295  			members:       mkMembers("1v 2v 3v", ipVersion),
   296  			expectVoting:  []bool{true, true, true},
   297  			expectMembers: mkMembers("1v 2v 3v", ipVersion),
   298  			expectChanged: false,
   299  		}, {
   300  			about:         "two voting members removes vote from secondary (first member)",
   301  			machines:      mkMachines("11v 12v", ipVersion),
   302  			members:       mkMembers("1v 2v", ipVersion),
   303  			statuses:      mkStatuses("1s 2p", ipVersion),
   304  			expectVoting:  []bool{false, true},
   305  			expectMembers: mkMembers("1 2v", ipVersion),
   306  			expectChanged: true,
   307  		}, {
   308  			about:         "two voting members removes vote from secondary (second member)",
   309  			machines:      mkMachines("11v 12v", ipVersion),
   310  			members:       mkMembers("1v 2v", ipVersion),
   311  			statuses:      mkStatuses("1p 2s", ipVersion),
   312  			expectVoting:  []bool{true, false},
   313  			expectMembers: mkMembers("1v 2", ipVersion),
   314  			expectChanged: true,
   315  		}, {
   316  			about:         "three voting members one ready to loose voting -> no consensus",
   317  			machines:      mkMachines("11v 12v 13", ipVersion),
   318  			members:       mkMembers("1v 2v 3v", ipVersion),
   319  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   320  			expectVoting:  []bool{true, false, false},
   321  			expectMembers: mkMembers("1v 2 3", ipVersion),
   322  			expectChanged: true,
   323  		}, {
   324  			about:         "three voting members remove one, to only one voting member left",
   325  			machines:      mkMachines("11v 12", ipVersion),
   326  			members:       mkMembers("1v 2v 3", ipVersion),
   327  			statuses:      mkStatuses("1p 2s 3s", ipVersion),
   328  			expectVoting:  []bool{true, false},
   329  			expectMembers: mkMembers("1v 2", ipVersion),
   330  			expectChanged: true,
   331  		}, {
   332  			about:         "three voting members remove all, keep primary",
   333  			machines:      mkMachines("11 12 13", ipVersion),
   334  			members:       mkMembers("1v 2v 3v", ipVersion),
   335  			statuses:      mkStatuses("1s 2s 3p", ipVersion),
   336  			expectVoting:  []bool{false, false, true},
   337  			expectMembers: mkMembers("1 2 3v", ipVersion),
   338  			expectChanged: true,
   339  		}, {
   340  			about:         "add controller, non-voting still add it to the replica set",
   341  			machines:      mkMachines("11v 12v 13v 14", ipVersion),
   342  			members:       mkMembers("1v 2v 3v", ipVersion),
   343  			statuses:      mkStatuses("1s 2s 3p", ipVersion),
   344  			expectVoting:  []bool{true, true, true, false},
   345  			expectMembers: mkMembers("1v 2v 3v 4", ipVersion),
   346  			expectChanged: true,
   347  		}, {
   348  			about:          "remove primary controller",
   349  			machines:       mkMachines("11 12v 13v", ipVersion),
   350  			members:        mkMembers("1v 2v 3v", ipVersion),
   351  			statuses:       mkStatuses("1p 2s 3s", ipVersion),
   352  			expectVoting:   []bool{false, false, true},
   353  			expectMembers:  mkMembers("1 2 3v", ipVersion),
   354  			expectStepDown: true,
   355  			expectChanged:  true,
   356  		},
   357  	}
   358  }
   359  
   360  func (s *desiredPeerGroupSuite) TestDesiredPeerGroupIPv4(c *gc.C) {
   361  	s.doTestDesiredPeerGroup(c, testIPv4)
   362  }
   363  
   364  func (s *desiredPeerGroupSuite) TestDesiredPeerGroupIPv6(c *gc.C) {
   365  	s.doTestDesiredPeerGroup(c, testIPv6)
   366  }
   367  
   368  func (s *desiredPeerGroupSuite) doTestDesiredPeerGroup(c *gc.C, ipVersion TestIPVersion) {
   369  	for ti, test := range desiredPeerGroupTests(ipVersion) {
   370  		c.Logf("\ntest %d: %s", ti, test.about)
   371  		trackerMap := make(map[string]*controllerTracker)
   372  		for _, m := range test.machines {
   373  			c.Assert(trackerMap[m.Id()], gc.IsNil)
   374  			trackerMap[m.Id()] = m
   375  		}
   376  
   377  		info, err := newPeerGroupInfo(trackerMap, test.statuses, test.members, mongoPort, network.SpaceInfo{})
   378  		c.Assert(err, jc.ErrorIsNil)
   379  
   380  		desired, err := desiredPeerGroup(info)
   381  		if test.expectErr != "" {
   382  			c.Assert(err, gc.ErrorMatches, test.expectErr)
   383  			c.Assert(desired.members, gc.IsNil)
   384  			c.Assert(desired.isChanged, jc.IsFalse)
   385  			continue
   386  		}
   387  		c.Assert(err, jc.ErrorIsNil)
   388  		c.Assert(info, gc.NotNil)
   389  
   390  		members := make([]replicaset.Member, 0, len(desired.members))
   391  		for _, m := range desired.members {
   392  			members = append(members, *m)
   393  		}
   394  
   395  		sort.Sort(membersById(members))
   396  		c.Assert(desired.isChanged, gc.Equals, test.expectChanged)
   397  		c.Assert(desired.stepDownPrimary, gc.Equals, test.expectStepDown)
   398  		c.Assert(membersToTestMembers(members), jc.DeepEquals, membersToTestMembers(test.expectMembers))
   399  		for i, m := range test.machines {
   400  			if m.host.Life() == state.Dead {
   401  				continue
   402  			}
   403  			var vote, votePresent bool
   404  			for _, member := range desired.members {
   405  				controllerId, ok := member.Tags[jujuNodeKey]
   406  				c.Assert(ok, jc.IsTrue)
   407  				if controllerId == m.Id() {
   408  					votePresent = true
   409  					vote = isVotingMember(member)
   410  					break
   411  				}
   412  			}
   413  			c.Check(votePresent, jc.IsTrue)
   414  			c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("controller %s", m.Id()))
   415  		}
   416  
   417  		// Assure ourselves that the total number of desired votes is odd in
   418  		// all circumstances.
   419  		c.Assert(countVotes(members)%2, gc.Equals, 1)
   420  
   421  		// Make sure that when the members are set as required, that there
   422  		// is no further change if desiredPeerGroup is called again.
   423  		info, err = newPeerGroupInfo(trackerMap, test.statuses, members, mongoPort, network.SpaceInfo{})
   424  		c.Assert(err, jc.ErrorIsNil)
   425  		c.Assert(info, gc.NotNil)
   426  
   427  		desired, err = desiredPeerGroup(info)
   428  		c.Assert(err, jc.ErrorIsNil)
   429  		c.Assert(desired.isChanged, jc.IsFalse)
   430  		c.Assert(desired.stepDownPrimary, jc.IsFalse)
   431  		countPrimaries := 0
   432  		c.Assert(err, gc.IsNil)
   433  		for i, m := range test.machines {
   434  			var vote, votePresent bool
   435  			for _, member := range desired.members {
   436  				controllerId, ok := member.Tags[jujuNodeKey]
   437  				c.Assert(ok, jc.IsTrue)
   438  				if controllerId == m.Id() {
   439  					votePresent = true
   440  					vote = isVotingMember(member)
   441  					break
   442  				}
   443  			}
   444  			if m.host.Life() == state.Dead {
   445  				c.Assert(votePresent, jc.IsFalse)
   446  				continue
   447  			}
   448  			c.Check(votePresent, jc.IsTrue)
   449  			c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("controller %s", m.Id()))
   450  			if isPrimaryMember(info, m.Id()) {
   451  				countPrimaries += 1
   452  			}
   453  		}
   454  		c.Assert(countPrimaries, gc.Equals, 1)
   455  		c.Assert(err, jc.ErrorIsNil)
   456  	}
   457  }
   458  
   459  func (s *desiredPeerGroupSuite) TestIsPrimary(c *gc.C) {
   460  	machines := mkMachines("11v 12v 13v", testIPv4)
   461  	trackerMap := make(map[string]*controllerTracker)
   462  	for _, m := range machines {
   463  		c.Assert(trackerMap[m.Id()], gc.IsNil)
   464  		trackerMap[m.Id()] = m
   465  	}
   466  	members := mkMembers("1v 2v 3v", testIPv4)
   467  	statuses := mkStatuses("1p 2s 3s", testIPv4)
   468  	info, err := newPeerGroupInfo(trackerMap, statuses, members, mongoPort, network.SpaceInfo{})
   469  	c.Assert(err, jc.ErrorIsNil)
   470  	isPrimary, err := info.isPrimary("11")
   471  	c.Assert(err, jc.ErrorIsNil)
   472  	c.Assert(isPrimary, jc.IsTrue)
   473  	isPrimary, err = info.isPrimary("12")
   474  	c.Assert(err, jc.ErrorIsNil)
   475  	c.Assert(isPrimary, jc.IsFalse)
   476  }
   477  
   478  func (s *desiredPeerGroupSuite) TestNewPeerGroupInfoErrWhenNoMembers(c *gc.C) {
   479  	_, err := newPeerGroupInfo(nil, nil, nil, 666, network.SpaceInfo{})
   480  	c.Check(err, gc.ErrorMatches, "current member set is empty")
   481  }
   482  
   483  func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsErrorWhenVoterFound(c *gc.C) {
   484  	v := 1
   485  	peerChanges := peerGroupChanges{
   486  		info: &peerGroupInfo{extra: []replicaset.Member{{Votes: &v}}},
   487  	}
   488  	err := peerChanges.checkExtraMembers()
   489  	c.Check(err, gc.ErrorMatches, "non juju voting member .+ found in peer group")
   490  }
   491  
   492  func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsTrueWhenCheckMade(c *gc.C) {
   493  	v := 0
   494  	peerChanges := peerGroupChanges{
   495  		info: &peerGroupInfo{extra: []replicaset.Member{{Votes: &v}}},
   496  	}
   497  	err := peerChanges.checkExtraMembers()
   498  	c.Check(peerChanges.desired.isChanged, jc.IsTrue)
   499  	c.Check(err, jc.ErrorIsNil)
   500  }
   501  
   502  func (s *desiredPeerGroupSuite) TestCheckToRemoveMembersReturnsTrueWhenCheckMade(c *gc.C) {
   503  	peerChanges := peerGroupChanges{
   504  		info: &peerGroupInfo{toRemove: []replicaset.Member{{}}},
   505  	}
   506  	err := peerChanges.checkExtraMembers()
   507  	c.Check(peerChanges.desired.isChanged, jc.IsTrue)
   508  	c.Check(err, jc.ErrorIsNil)
   509  }
   510  
   511  func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsFalseWhenEmpty(c *gc.C) {
   512  	peerChanges := peerGroupChanges{
   513  		info: &peerGroupInfo{},
   514  	}
   515  	err := peerChanges.checkExtraMembers()
   516  	c.Check(peerChanges.desired.isChanged, jc.IsFalse)
   517  	c.Check(err, jc.ErrorIsNil)
   518  }
   519  
   520  func countVotes(members []replicaset.Member) int {
   521  	tot := 0
   522  	for _, m := range members {
   523  		v := 1
   524  		if m.Votes != nil {
   525  			v = *m.Votes
   526  		}
   527  		tot += v
   528  	}
   529  	return tot
   530  }
   531  
   532  func newInt(i int) *int {
   533  	return &i
   534  }
   535  
   536  func newFloat64(f float64) *float64 {
   537  	return &f
   538  }
   539  
   540  func mkController(life state.Life) *fakeController {
   541  	ctrl := &fakeController{}
   542  	ctrl.val.Set(controllerDoc{life: life})
   543  	return ctrl
   544  }
   545  
   546  // mkMachines returns a slice of *machineTracker based on
   547  // the given description.
   548  // Each controller in the description is white-space separated
   549  // and holds the decimal controller id followed by an optional
   550  // "v" if the controller wants a vote.
   551  func mkMachines(description string, ipVersion TestIPVersion) []*controllerTracker {
   552  	descrs := parseDescr(description)
   553  	ms := make([]*controllerTracker, len(descrs))
   554  	for i, d := range descrs {
   555  		ms[i] = &controllerTracker{
   556  			id: fmt.Sprint(d.id),
   557  			addresses: []network.SpaceAddress{{
   558  				MachineAddress: network.MachineAddress{
   559  					Value: fmt.Sprintf(ipVersion.formatHost, d.id),
   560  					Type:  ipVersion.addressType,
   561  					Scope: network.ScopeCloudLocal,
   562  				},
   563  			}},
   564  			host:      mkController(state.Alive),
   565  			wantsVote: strings.Contains(d.flags, "v"),
   566  		}
   567  	}
   568  	return ms
   569  }
   570  
   571  func memberTag(id string) map[string]string {
   572  	return map[string]string{jujuNodeKey: id}
   573  }
   574  
   575  // mkMembers returns a slice of replicaset.Member based on the given
   576  // description.
   577  // Each member in the description is white-space separated and holds the decimal
   578  // replica-set id optionally followed by the characters:
   579  //   - 'v' if the member is voting.
   580  //   - 'T' if the member has no associated controller tags.
   581  //
   582  // Unless the T flag is specified, the controller tag
   583  // will be the replica-set id + 10.
   584  func mkMembers(description string, ipVersion TestIPVersion) []replicaset.Member {
   585  	descrs := parseDescr(description)
   586  	ms := make([]replicaset.Member, len(descrs))
   587  	for i, d := range descrs {
   588  		machineId := d.id + 10
   589  		m := replicaset.Member{
   590  			Id: d.id,
   591  			Address: net.JoinHostPort(
   592  				fmt.Sprintf(ipVersion.formatHost, machineId),
   593  				fmt.Sprint(mongoPort),
   594  			),
   595  			Tags: memberTag(fmt.Sprint(machineId)),
   596  		}
   597  		if !strings.Contains(d.flags, "v") {
   598  			m.Priority = newFloat64(0)
   599  			m.Votes = newInt(0)
   600  		}
   601  		if strings.Contains(d.flags, "T") {
   602  			m.Tags = nil
   603  		}
   604  		ms[i] = m
   605  	}
   606  	return ms
   607  }
   608  
   609  var stateFlags = map[rune]replicaset.MemberState{
   610  	'p': replicaset.PrimaryState,
   611  	's': replicaset.SecondaryState,
   612  }
   613  
   614  // mkStatuses returns a slice of replicaset.MemberStatus based on the given
   615  // description.
   616  // Each member in the description is white-space separated  and holds the
   617  // decimal replica-set id optionally followed by the characters:
   618  //   - 'H' if the instance is not healthy.
   619  //   - 'p' if the instance is in PrimaryState
   620  //   - 's' if the instance is in SecondaryState
   621  func mkStatuses(description string, ipVersion TestIPVersion) []replicaset.MemberStatus {
   622  	descrs := parseDescr(description)
   623  	ss := make([]replicaset.MemberStatus, len(descrs))
   624  	for i, d := range descrs {
   625  		machineId := d.id + 10
   626  		s := replicaset.MemberStatus{
   627  			Id: d.id,
   628  			Address: net.JoinHostPort(
   629  				fmt.Sprintf(ipVersion.formatHost, machineId),
   630  				fmt.Sprint(mongoPort),
   631  			),
   632  			Healthy: !strings.Contains(d.flags, "H"),
   633  			State:   replicaset.UnknownState,
   634  		}
   635  		for _, r := range d.flags {
   636  			if state, ok := stateFlags[r]; ok {
   637  				s.State = state
   638  			}
   639  		}
   640  		ss[i] = s
   641  	}
   642  	return ss
   643  }
   644  
   645  type descr struct {
   646  	id    int
   647  	flags string
   648  }
   649  
   650  func isNotDigit(r rune) bool {
   651  	return r < '0' || r > '9'
   652  }
   653  
   654  var parseDescrTests = []struct {
   655  	descr  string
   656  	expect []descr
   657  }{{
   658  	descr:  "",
   659  	expect: []descr{},
   660  }, {
   661  	descr:  "0",
   662  	expect: []descr{{id: 0}},
   663  }, {
   664  	descr:  "1foo",
   665  	expect: []descr{{id: 1, flags: "foo"}},
   666  }, {
   667  	descr: "10c  5 6443arble ",
   668  	expect: []descr{{
   669  		id:    10,
   670  		flags: "c",
   671  	}, {
   672  		id: 5,
   673  	}, {
   674  		id:    6443,
   675  		flags: "arble",
   676  	}},
   677  }}
   678  
   679  func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) {
   680  	for i, test := range parseDescrTests {
   681  		c.Logf("test %d. %q", i, test.descr)
   682  		c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect)
   683  	}
   684  }
   685  
   686  // parseDescr parses white-space separated fields of the form
   687  // <id><flags> into descr structures.
   688  func parseDescr(s string) []descr {
   689  	fields := strings.Fields(s)
   690  	descrs := make([]descr, len(fields))
   691  	for i, field := range fields {
   692  		d := &descrs[i]
   693  		i := strings.IndexFunc(field, isNotDigit)
   694  		if i == -1 {
   695  			i = len(field)
   696  		}
   697  		id, err := strconv.Atoi(field[0:i])
   698  		if err != nil {
   699  			panic(fmt.Errorf("bad field %q", field))
   700  		}
   701  		d.id = id
   702  		d.flags = field[i:]
   703  	}
   704  	return descrs
   705  }
   706  
   707  func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) {
   708  	c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{})
   709  	// Avoid mutating the obtained slice: because it's usually retrieved
   710  	// directly from the memberWatcher voyeur.Value,
   711  	// mutation can cause races.
   712  	obtainedMembers := deepCopy(obtained).([]replicaset.Member)
   713  	sort.Sort(membersById(obtainedMembers))
   714  	sort.Sort(membersById(expected))
   715  	c.Assert(membersToTestMembers(obtainedMembers), jc.DeepEquals, membersToTestMembers(expected))
   716  }
   717  
   718  type membersById []replicaset.Member
   719  
   720  func (l membersById) Len() int           { return len(l) }
   721  func (l membersById) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
   722  func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id }
   723  
   724  // AssertAPIHostPorts asserts of two sets of network.SpaceHostPort slices are the same.
   725  func AssertAPIHostPorts(c *gc.C, got, want []network.SpaceHostPorts) {
   726  	c.Assert(got, gc.HasLen, len(want))
   727  	sort.Sort(hostPortSliceByHostPort(got))
   728  	sort.Sort(hostPortSliceByHostPort(want))
   729  	c.Assert(got, gc.DeepEquals, want)
   730  }
   731  
   732  type hostPortSliceByHostPort []network.SpaceHostPorts
   733  
   734  func (h hostPortSliceByHostPort) Len() int      { return len(h) }
   735  func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
   736  func (h hostPortSliceByHostPort) Less(i, j int) bool {
   737  	a, b := h[i], h[j]
   738  	if len(a) != len(b) {
   739  		return len(a) < len(b)
   740  	}
   741  	for i := range a {
   742  		av, bv := a[i], b[i]
   743  		if av.Value != bv.Value {
   744  			return av.Value < bv.Value
   745  		}
   746  		if av.Port() != bv.Port() {
   747  			return av.Port() < bv.Port()
   748  		}
   749  	}
   750  	return false
   751  }
   752  
   753  type sortAsIntsSuite struct {
   754  	testing.IsolationSuite
   755  }
   756  
   757  var _ = gc.Suite(&sortAsIntsSuite{})
   758  
   759  func checkIntSorted(c *gc.C, vals, expected []string) {
   760  	// we sort in place, so leave 'vals' alone and copy to another slice
   761  	copied := append([]string(nil), vals...)
   762  	sortAsInts(copied)
   763  	c.Check(copied, gc.DeepEquals, expected)
   764  }
   765  
   766  func (*sortAsIntsSuite) TestAllInts(c *gc.C) {
   767  	checkIntSorted(c, []string{"1", "10", "2", "20"}, []string{"1", "2", "10", "20"})
   768  }
   769  
   770  func (*sortAsIntsSuite) TestStrings(c *gc.C) {
   771  	checkIntSorted(c, []string{"a", "c", "b", "X"}, []string{"X", "a", "b", "c"})
   772  }
   773  
   774  func (*sortAsIntsSuite) TestMixed(c *gc.C) {
   775  	checkIntSorted(c, []string{"1", "20", "10", "2", "2d", "c", "b", "X"},
   776  		[]string{"1", "2", "10", "20", "2d", "X", "b", "c"})
   777  }