github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/peergrouper/desired.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  	"sort"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/replicaset/v3"
    15  
    16  	"github.com/juju/juju/core/network"
    17  	"github.com/juju/juju/core/status"
    18  	"github.com/juju/juju/state"
    19  )
    20  
    21  // jujuNodeKey is the key for the tag where we save a member's node id.
    22  const jujuNodeKey = "juju-machine-id"
    23  
    24  // peerGroupInfo holds information used in attempting to determine a Mongo
    25  // peer group.
    26  type peerGroupInfo struct {
    27  	// Maps below are keyed on node ID.
    28  
    29  	// controllers holds the controllerTrackers for known controller nodes sourced from the peergrouper
    30  	// worker. Indexed by node.Id()
    31  	controllers map[string]*controllerTracker
    32  
    33  	// Replica-set members sourced from the Mongo session that are recognised by
    34  	// their association with known controller nodes.
    35  	recognised map[string]replicaset.Member
    36  
    37  	// Replica-set member statuses sourced from the Mongo session.
    38  	statuses map[string]replicaset.MemberStatus
    39  
    40  	toRemove    []replicaset.Member
    41  	extra       []replicaset.Member
    42  	maxMemberId int
    43  	mongoPort   int
    44  	haSpace     network.SpaceInfo
    45  }
    46  
    47  // desiredChanges tracks the specific changes we are asking to be made to the peer group.
    48  type desiredChanges struct {
    49  	// isChanged is set False if the existing peer group is already in a valid configuration.
    50  	isChanged bool
    51  
    52  	// stepDownPrimary is set if we want to remove the vote from the Mongo Primary. This is specially flagged,
    53  	// because you have to ask the primary to step down before you can remove its vote.
    54  	stepDownPrimary bool
    55  
    56  	// members is the map of Id to replicaset.Member for the desired list of controller nodes in the replicaset.
    57  	members map[string]*replicaset.Member
    58  }
    59  
    60  // peerGroupChanges tracks the process of computing the desiredChanges to the peer group.
    61  type peerGroupChanges struct {
    62  	// info is the input state we will be processing
    63  	info *peerGroupInfo
    64  
    65  	// this block all represents active processing state
    66  	toRemoveVote                []string
    67  	toAddVote                   []string
    68  	toKeepVoting                []string
    69  	toKeepNonVoting             []string
    70  	toKeepCreateNonVotingMember []string
    71  
    72  	// desired tracks the final changes to the peer group that we want to make
    73  	desired desiredChanges
    74  }
    75  
    76  func newPeerGroupInfo(
    77  	controllers map[string]*controllerTracker,
    78  	statuses []replicaset.MemberStatus,
    79  	members []replicaset.Member,
    80  	mongoPort int,
    81  	haSpace network.SpaceInfo,
    82  ) (*peerGroupInfo, error) {
    83  	if len(members) == 0 {
    84  		return nil, fmt.Errorf("current member set is empty")
    85  	}
    86  
    87  	info := peerGroupInfo{
    88  		controllers: controllers,
    89  		statuses:    make(map[string]replicaset.MemberStatus),
    90  		recognised:  make(map[string]replicaset.Member),
    91  		maxMemberId: -1,
    92  		mongoPort:   mongoPort,
    93  		haSpace:     haSpace,
    94  	}
    95  
    96  	// Iterate over the input members and associate them with a controller if
    97  	// possible; add any non-juju unassociated members to the "extra" slice.
    98  	// Unassociated members with the juju machine id tag are to be removed.
    99  	// Link the statuses with the controller node IDs where associated.
   100  	// Keep track of the highest member ID that we observe.
   101  	for _, m := range members {
   102  		if m.Id > info.maxMemberId {
   103  			info.maxMemberId = m.Id
   104  		}
   105  
   106  		controllerId, ok := m.Tags[jujuNodeKey]
   107  		if !ok {
   108  			info.extra = append(info.extra, m)
   109  			continue
   110  		}
   111  		found := false
   112  		if node, got := controllers[controllerId]; got {
   113  			info.recognised[controllerId] = m
   114  			found = node.host.Life() != state.Dead
   115  		}
   116  
   117  		// This invariably makes for N^2, but we anticipate small N.
   118  		for _, sts := range statuses {
   119  			if sts.Id == m.Id {
   120  				info.statuses[controllerId] = sts
   121  			}
   122  		}
   123  		if !found {
   124  			info.toRemove = append(info.toRemove, m)
   125  		}
   126  	}
   127  
   128  	return &info, nil
   129  }
   130  
   131  // isPrimary returns true if the given controller node id is the mongo primary.
   132  func (info *peerGroupInfo) isPrimary(workerControllerId string) (bool, error) {
   133  	primaryNodeId := -1
   134  	// Current status of replicaset contains node state.
   135  	// Here we determine node id of the primary node.
   136  	for _, m := range info.statuses {
   137  		if m.State == replicaset.PrimaryState {
   138  			primaryNodeId = m.Id
   139  			break
   140  		}
   141  	}
   142  	if primaryNodeId == -1 {
   143  		return false, errors.NotFoundf("HA primary machine")
   144  	}
   145  
   146  	for _, m := range info.recognised {
   147  		if m.Id == primaryNodeId {
   148  			if primaryControllerId, ok := m.Tags[jujuNodeKey]; ok {
   149  				return primaryControllerId == workerControllerId, nil
   150  			}
   151  		}
   152  	}
   153  	return false, errors.NotFoundf("HA primary machine")
   154  }
   155  
   156  // getLogMessage generates a nicely formatted log message from the known peer
   157  // group information.
   158  func (info *peerGroupInfo) getLogMessage() string {
   159  	lines := []string{
   160  		fmt.Sprintf("calculating desired peer group\ndesired voting members: (maxId: %d)", info.maxMemberId),
   161  	}
   162  
   163  	template := "\n   %#v: rs_id=%d, rs_addr=%s, rs_primary=%v"
   164  	ids := make([]string, 0, len(info.recognised))
   165  	for id := range info.recognised {
   166  		ids = append(ids, id)
   167  	}
   168  	sortAsInts(ids)
   169  	for _, id := range ids {
   170  		rm := info.recognised[id]
   171  		isPrimary := isPrimaryMember(info, id)
   172  		lines = append(lines, fmt.Sprintf(template, info.controllers[id], rm.Id, rm.Address, isPrimary))
   173  	}
   174  
   175  	if len(info.toRemove) > 0 {
   176  		lines = append(lines, "\nmembers to remove:")
   177  		template := "\n   rs_id=%d, rs_addr=%s, tags=%v, vote=%t"
   178  		for _, em := range info.toRemove {
   179  			vote := em.Votes != nil && *em.Votes > 0
   180  			lines = append(lines, fmt.Sprintf(template, em.Id, em.Address, em.Tags, vote))
   181  		}
   182  	}
   183  
   184  	if len(info.extra) > 0 {
   185  		lines = append(lines, "\nother non-juju  members:")
   186  		template := "\n   rs_id=%d, rs_addr=%s, tags=%v, vote=%t"
   187  		for _, em := range info.extra {
   188  			vote := em.Votes != nil && *em.Votes > 0
   189  			lines = append(lines, fmt.Sprintf(template, em.Id, em.Address, em.Tags, vote))
   190  		}
   191  	}
   192  
   193  	return strings.Join(lines, "")
   194  }
   195  
   196  // initNewReplicaSet creates a new node ID indexed map of known replica-set
   197  // members to use as the basis for a newly calculated replica-set.
   198  func (p *peerGroupChanges) initNewReplicaSet() map[string]*replicaset.Member {
   199  	rs := make(map[string]*replicaset.Member, len(p.info.recognised))
   200  	for id := range p.info.recognised {
   201  		// Local-scoped variable required here,
   202  		// or the same pointer to the loop variable is used each time.
   203  		m := p.info.recognised[id]
   204  		rs[id] = &m
   205  	}
   206  	return rs
   207  }
   208  
   209  // desiredPeerGroup returns a new Mongo peer-group calculated from the input
   210  // peerGroupInfo.
   211  // Returned are the new members indexed by node ID, and a map indicating
   212  // which controller nodes are set as voters in the new new peer-group.
   213  // If the new peer-group is does not differ from that indicated by the input
   214  // peerGroupInfo, a nil member map is returned along with the correct voters
   215  // map.
   216  // An error is returned if:
   217  //  1. There are members unrecognised by controller node association,
   218  //     and any of these are set as voters.
   219  //  2. There is no HA space configured and any nodes have multiple
   220  //     cloud-local addresses.
   221  func desiredPeerGroup(info *peerGroupInfo) (desiredChanges, error) {
   222  	logger.Debugf(info.getLogMessage())
   223  
   224  	peerChanges := peerGroupChanges{
   225  		info: info,
   226  		desired: desiredChanges{
   227  			isChanged:       false,
   228  			stepDownPrimary: false,
   229  			members:         map[string]*replicaset.Member{},
   230  		},
   231  	}
   232  	return peerChanges.computeDesiredPeerGroup()
   233  }
   234  
   235  func (p *peerGroupChanges) computeDesiredPeerGroup() (desiredChanges, error) {
   236  
   237  	// We may find extra peer group members if the controller nodes have been
   238  	// removed or their controller status removed.
   239  	// This should only happen if they had been set to non-voting before
   240  	// removal, in which case we want to remove them from the members list.
   241  	// If we find a member that is still configured to vote, it is an error.
   242  	// TODO: There are some other possibilities for what to do in that case.
   243  	// 1) Leave them untouched, but deal with others as usual (ignore).
   244  	// 2) Leave them untouched and deal with others, but make sure the extras
   245  	//    are not eligible to be primary.
   246  	// 3) Remove them.
   247  	// 4) Do nothing.
   248  	err := p.checkExtraMembers()
   249  	if err != nil {
   250  		return desiredChanges{}, errors.Trace(err)
   251  	}
   252  
   253  	p.desired.members = p.initNewReplicaSet()
   254  	p.possiblePeerGroupChanges()
   255  	p.reviewPeerGroupChanges()
   256  	p.createNonVotingMember()
   257  
   258  	// Set up initial record of controller node votes. Any changes after
   259  	// this will trigger a peer group election.
   260  	p.adjustVotes()
   261  
   262  	if err := p.updateAddresses(); err != nil {
   263  		return desiredChanges{}, errors.Trace(err)
   264  	}
   265  
   266  	return p.desired, nil
   267  }
   268  
   269  // checkExtraMembers checks to see if any of the input members, identified as
   270  // not being associated with controller nodes, is set as a voter in the peer group.
   271  // If any have, an error is returned.
   272  // The boolean indicates whether any extra members were present at all.
   273  func (p *peerGroupChanges) checkExtraMembers() error {
   274  	// Note: (jam 2018-04-18) With the new "juju remove-controller --force" it is much easier to get into this situation
   275  	// because an active controller that is in the replicaset would get removed while it still had voting rights.
   276  	// Given that Juju is in control of the replicaset we don't really just 'accept' that some other node has a vote.
   277  	// *maybe* we could allow non-voting members that would be used by 3rd parties to provide a warm database backup.
   278  	// But I think the right answer is probably to downgrade unknown members from voting.
   279  	// Note: (wallyworld) notwithstanding the above, each controller runs its own peer grouper worker. The
   280  	// mongo primary will remove nodes as needed from the replicaset. There will be a short time where
   281  	// Juju managed nodes will not yet be accounted for by the other secondary workers. These are accounted
   282  	// for in the 'toRemove' list.
   283  	for _, member := range p.info.extra {
   284  		if isVotingMember(&member) {
   285  			return fmt.Errorf("non juju voting member %v found in peer group", member)
   286  		}
   287  	}
   288  	if len(p.info.toRemove) > 0 || len(p.info.extra) > 0 {
   289  		p.desired.isChanged = true
   290  	}
   291  	return nil
   292  }
   293  
   294  // sortAsInts converts all the vals to an integer to sort them as numbers instead of strings
   295  // If any of the values are not valid integers, they will be sorted as strings, and added to the end
   296  // the slice will be sorted in place.
   297  // (generally this should only be used for strings we expect to represent ints, but we don't want to error if
   298  // something isn't an int.)
   299  func sortAsInts(vals []string) {
   300  	asInts := make([]int, 0, len(vals))
   301  	extra := []string{}
   302  	for _, val := range vals {
   303  		asInt, err := strconv.Atoi(val)
   304  		if err != nil {
   305  			extra = append(extra, val)
   306  		} else {
   307  			asInts = append(asInts, asInt)
   308  		}
   309  	}
   310  	sort.Ints(asInts)
   311  	sort.Strings(extra)
   312  	i := 0
   313  	for _, asInt := range asInts {
   314  		vals[i] = strconv.Itoa(asInt)
   315  		i++
   316  	}
   317  	for _, val := range extra {
   318  		vals[i] = val
   319  		i++
   320  	}
   321  }
   322  
   323  // possiblePeerGroupChanges returns a set of slices classifying all the
   324  // existing controller nodes according to how their vote might move.
   325  // toRemoveVote holds nodes whose vote should be removed;
   326  // toAddVote holds nodes which are ready to vote;
   327  // toKeep holds nodes with no desired change to their voting status
   328  // (this includes nodes that are not yet represented in the peer group).
   329  func (p *peerGroupChanges) possiblePeerGroupChanges() {
   330  	nodeIds := make([]string, 0, len(p.info.controllers))
   331  	for id := range p.info.controllers {
   332  		nodeIds = append(nodeIds, id)
   333  	}
   334  	sortAsInts(nodeIds)
   335  	logger.Debugf("assessing possible peer group changes:")
   336  	for _, id := range nodeIds {
   337  		m := p.info.controllers[id]
   338  		member := p.desired.members[id]
   339  		if m.host.Life() != state.Alive {
   340  			if _, ok := p.desired.members[id]; !ok {
   341  				// Dead machine already removed from replicaset.
   342  				continue
   343  			}
   344  			logger.Debugf("controller %v has died %q, wants vote: %v", id, m.host.Life(), m.WantsVote())
   345  			if isPrimaryMember(p.info, id) {
   346  				p.desired.stepDownPrimary = true
   347  			}
   348  			delete(p.desired.members, id)
   349  			p.desired.isChanged = true
   350  			continue
   351  		}
   352  		isVoting := member != nil && isVotingMember(member)
   353  		wantsVote := m.WantsVote()
   354  		switch {
   355  		case wantsVote && isVoting:
   356  			logger.Debugf("node %q is already voting", id)
   357  			p.toKeepVoting = append(p.toKeepVoting, id)
   358  		case wantsVote && !isVoting:
   359  			if status, ok := p.info.statuses[id]; ok && isReady(status) {
   360  				logger.Debugf("node %q is a potential voter", id)
   361  				p.toAddVote = append(p.toAddVote, id)
   362  			} else if member != nil {
   363  				logger.Debugf("node %q exists but is not ready (status: %v, healthy: %v)",
   364  					id, status.State, status.Healthy)
   365  				p.toKeepNonVoting = append(p.toKeepNonVoting, id)
   366  			} else {
   367  				logger.Debugf("node %q does not exist and is not ready (status: %v, healthy: %v)",
   368  					id, status.State, status.Healthy)
   369  				p.toKeepCreateNonVotingMember = append(p.toKeepCreateNonVotingMember, id)
   370  			}
   371  		case !wantsVote && isVoting:
   372  			p.toRemoveVote = append(p.toRemoveVote, id)
   373  			if isPrimaryMember(p.info, id) {
   374  				p.desired.stepDownPrimary = true
   375  				logger.Debugf("primary node %q is a potential non-voter", id)
   376  			} else {
   377  				logger.Debugf("node %q is a potential non-voter", id)
   378  			}
   379  		case !wantsVote && !isVoting:
   380  			logger.Debugf("node %q does not want the vote", id)
   381  			p.toKeepNonVoting = append(p.toKeepNonVoting, id)
   382  		}
   383  	}
   384  	logger.Debugf("assessed")
   385  }
   386  
   387  func isReady(status replicaset.MemberStatus) bool {
   388  	return status.Healthy && (status.State == replicaset.PrimaryState ||
   389  		status.State == replicaset.SecondaryState)
   390  }
   391  
   392  // reviewPeerGroupChanges adds some extra logic after creating
   393  // possiblePeerGroupChanges to safely add or remove controller nodes, keeping the
   394  // correct odd number of voters peer structure, and preventing the primary from
   395  // demotion.
   396  func (p *peerGroupChanges) reviewPeerGroupChanges() {
   397  	currVoters := 0
   398  	for _, m := range p.desired.members {
   399  		if isVotingMember(m) {
   400  			currVoters += 1
   401  		}
   402  	}
   403  	keptVoters := currVoters - len(p.toRemoveVote)
   404  	if keptVoters == 0 {
   405  		// to keep no voters means to step down the primary without a replacement, which is not possible.
   406  		// So restore the current primary. Once there is another member to work with after reconfiguring, we will then
   407  		// be able to ask the current primary to step down, and then we can finally remove it.
   408  		var tempToRemove []string
   409  		for _, id := range p.toRemoveVote {
   410  			isPrimary := isPrimaryMember(p.info, id)
   411  			if !isPrimary {
   412  				tempToRemove = append(tempToRemove, id)
   413  			} else {
   414  				logger.Debugf("asked to remove all voters, preserving primary voter %q", id)
   415  				p.desired.stepDownPrimary = false
   416  			}
   417  		}
   418  		p.toRemoveVote = tempToRemove
   419  	}
   420  	newCount := keptVoters + len(p.toAddVote)
   421  	if (newCount)%2 == 1 {
   422  		logger.Debugf("number of voters is odd")
   423  		// if this is true we will create an odd number of voters
   424  		return
   425  	}
   426  	if len(p.toAddVote) > 0 {
   427  		last := p.toAddVote[len(p.toAddVote)-1]
   428  		logger.Debugf("number of voters would be even, not adding %q to maintain odd", last)
   429  		p.toAddVote = p.toAddVote[:len(p.toAddVote)-1]
   430  		return
   431  	}
   432  	// we must remove an extra peer
   433  	// make sure we don't pick the primary to be removed.
   434  	for i, id := range p.toKeepVoting {
   435  		if !isPrimaryMember(p.info, id) {
   436  			p.toRemoveVote = append(p.toRemoveVote, id)
   437  			logger.Debugf("removing vote from %q to maintain odd number of voters", id)
   438  			if i == len(p.toKeepVoting)-1 {
   439  				p.toKeepVoting = p.toKeepVoting[:i]
   440  			} else {
   441  				p.toKeepVoting = append(p.toKeepVoting[:i], p.toKeepVoting[i+1:]...)
   442  			}
   443  			break
   444  		}
   445  	}
   446  }
   447  
   448  func isVotingMember(m *replicaset.Member) bool {
   449  	v := m.Votes
   450  	return v == nil || *v > 0
   451  }
   452  
   453  func isPrimaryMember(info *peerGroupInfo, id string) bool {
   454  	return info.statuses[id].State == replicaset.PrimaryState
   455  }
   456  
   457  func setMemberVoting(member *replicaset.Member, voting bool) {
   458  	if voting {
   459  		member.Votes = nil
   460  		member.Priority = nil
   461  	} else {
   462  		votes := 0
   463  		member.Votes = &votes
   464  		priority := 0.0
   465  		member.Priority = &priority
   466  	}
   467  }
   468  
   469  // adjustVotes removes and adds votes to the members via setVoting.
   470  func (p *peerGroupChanges) adjustVotes() {
   471  	setVoting := func(memberIds []string, voting bool) {
   472  		for _, id := range memberIds {
   473  			setMemberVoting(p.desired.members[id], voting)
   474  		}
   475  	}
   476  
   477  	if len(p.toAddVote) > 0 ||
   478  		len(p.toRemoveVote) > 0 ||
   479  		len(p.toKeepCreateNonVotingMember) > 0 {
   480  		p.desired.isChanged = true
   481  	}
   482  	setVoting(p.toAddVote, true)
   483  	setVoting(p.toRemoveVote, false)
   484  	setVoting(p.toKeepCreateNonVotingMember, false)
   485  }
   486  
   487  // createMembers from a list of member IDs, instantiate a new replica-set
   488  // member and add it to members map with the given ID.
   489  func (p *peerGroupChanges) createNonVotingMember() {
   490  	for _, id := range p.toKeepCreateNonVotingMember {
   491  		logger.Debugf("create member with id %q", id)
   492  		p.info.maxMemberId++
   493  		member := &replicaset.Member{
   494  			Tags: map[string]string{
   495  				jujuNodeKey: id,
   496  			},
   497  			Id: p.info.maxMemberId,
   498  		}
   499  		setMemberVoting(member, false)
   500  		p.desired.members[id] = member
   501  	}
   502  	for _, id := range p.toKeepNonVoting {
   503  		if p.desired.members[id] != nil {
   504  			continue
   505  		}
   506  		logger.Debugf("create member with id %q", id)
   507  		p.info.maxMemberId++
   508  		member := &replicaset.Member{
   509  			Tags: map[string]string{
   510  				jujuNodeKey: id,
   511  			},
   512  			Id: p.info.maxMemberId,
   513  		}
   514  		setMemberVoting(member, false)
   515  		p.desired.members[id] = member
   516  	}
   517  }
   518  
   519  // updateAddresses updates the member addresses in the new replica-set, using
   520  // the HA space if one is configured.
   521  func (p *peerGroupChanges) updateAddresses() error {
   522  	var err error
   523  	if p.info.haSpace.Name == "" {
   524  		err = p.updateAddressesFromInternal()
   525  	} else {
   526  		err = p.updateAddressesFromSpace()
   527  	}
   528  	return errors.Annotate(err, "updating member addresses")
   529  }
   530  
   531  const multiAddressMessage = "multiple usable addresses found" +
   532  	"\nrun \"juju controller-config juju-ha-space=<name>\" to set a space for Mongo peer communication"
   533  
   534  // updateAddressesFromInternal attempts to update each member with a
   535  // cloud-local address from the node.
   536  // If there is a single cloud local address available, it is used.
   537  // If there are multiple addresses, then a check is made to ensure that:
   538  //   - the member was previously in the replica-set and;
   539  //   - the previous address used for replication is still available.
   540  //
   541  // If the check is satisfied, then a warning is logged and no change is made.
   542  // Otherwise an error is returned to indicate that a HA space must be
   543  // configured in order to proceed. Such nodes have their status set to
   544  // indicate that they require intervention.
   545  func (p *peerGroupChanges) updateAddressesFromInternal() error {
   546  	var multipleAddresses []string
   547  
   548  	ids := p.sortedMemberIds()
   549  	singleController := len(ids) == 1
   550  
   551  	for _, id := range ids {
   552  		m := p.info.controllers[id]
   553  		hostPorts := m.GetPotentialMongoHostPorts(p.info.mongoPort)
   554  		addrs := hostPorts.AllMatchingScope(network.ScopeMatchCloudLocal)
   555  
   556  		// This should not happen because SelectInternalHostPorts will choose a
   557  		// public address when there are no cloud-local addresses.
   558  		// Zero addresses would mean the node is completely inaccessible.
   559  		// We ignore this outcome and leave the address alone.
   560  		if len(addrs) == 0 {
   561  			continue
   562  		}
   563  
   564  		// Unique address; we can use this for Mongo peer communication.
   565  		member := p.desired.members[id]
   566  		if len(addrs) == 1 {
   567  			addr := addrs[0]
   568  			logger.Debugf("node %q selected address %q by scope from %v", id, addr, hostPorts)
   569  
   570  			if member.Address != addr {
   571  				member.Address = addr
   572  				p.desired.isChanged = true
   573  			}
   574  			continue
   575  		}
   576  
   577  		// Multiple potential Mongo addresses.
   578  		// Checks are required in order to use it as a peer.
   579  		unchanged := false
   580  		if _, ok := p.info.recognised[id]; ok {
   581  			for _, addr := range addrs {
   582  				if member.Address == addr {
   583  					// If this is a single controller with multiple addresses,
   584  					// avoid warning logs for every peer-group check.
   585  					if !singleController {
   586  						logger.Warningf("%s\npreserving member with unchanged address %q", multiAddressMessage, addr)
   587  					}
   588  					unchanged = true
   589  					break
   590  				}
   591  			}
   592  		}
   593  
   594  		// If this member was not previously in the replica-set, or if its
   595  		// address has changed, we enforce the policy of requiring a
   596  		// configured HA space when there are multiple cloud-local addresses.
   597  		if !unchanged {
   598  			multipleAddresses = append(multipleAddresses, id)
   599  			if err := m.host.SetStatus(getStatusInfo(multiAddressMessage)); err != nil {
   600  				return errors.Trace(err)
   601  			}
   602  		}
   603  	}
   604  
   605  	if len(multipleAddresses) > 0 {
   606  		ids := strings.Join(multipleAddresses, ", ")
   607  		return fmt.Errorf("juju-ha-space is not set and these nodes have more than one usable address: %s"+
   608  			"\nrun \"juju controller-config juju-ha-space=<name>\" to set a space for Mongo peer communication", ids)
   609  	}
   610  	return nil
   611  }
   612  
   613  // updateAddressesFromSpace updates the member addresses based on the
   614  // configured HA space.
   615  // If no addresses are available for any of the nodes, then such nodes
   616  // have their status set and are included in the detail of the returned error.
   617  func (p *peerGroupChanges) updateAddressesFromSpace() error {
   618  	space := p.info.haSpace
   619  	var noAddresses []string
   620  
   621  	for _, id := range p.sortedMemberIds() {
   622  		m := p.info.controllers[id]
   623  		addr, err := m.SelectMongoAddressFromSpace(p.info.mongoPort, space)
   624  		if err != nil {
   625  			if errors.IsNotFound(err) {
   626  				noAddresses = append(noAddresses, id)
   627  				msg := fmt.Sprintf("no addresses in configured juju-ha-space %q", space.Name)
   628  				if err := m.host.SetStatus(getStatusInfo(msg)); err != nil {
   629  					return errors.Trace(err)
   630  				}
   631  				continue
   632  			}
   633  			return errors.Trace(err)
   634  		}
   635  		if addr != p.desired.members[id].Address {
   636  			p.desired.members[id].Address = addr
   637  			p.desired.isChanged = true
   638  		}
   639  	}
   640  
   641  	if len(noAddresses) > 0 {
   642  		ids := strings.Join(noAddresses, ", ")
   643  		return fmt.Errorf(
   644  			"no usable Mongo addresses found in configured juju-ha-space %q for nodes: %s", space.Name, ids)
   645  	}
   646  	return nil
   647  }
   648  
   649  // sortedMemberIds returns the list of p.desired.members in integer-sorted order
   650  func (p *peerGroupChanges) sortedMemberIds() []string {
   651  	memberIds := make([]string, 0, len(p.desired.members))
   652  	for id := range p.desired.members {
   653  		memberIds = append(memberIds, id)
   654  	}
   655  	sortAsInts(memberIds)
   656  	return memberIds
   657  }
   658  
   659  // getStatusInfo creates and returns a StatusInfo instance for use as a controller
   660  // status. The *controller* status is not ideal for conveying this information,
   661  // which is a really a characteristic of its role as a controller application.
   662  // For this reason we leave the status as "Started" and supplement with an
   663  // appropriate message.
   664  // This is subject to change if/when controller status is represented in its
   665  // own right.
   666  func getStatusInfo(msg string) status.StatusInfo {
   667  	now := time.Now()
   668  	return status.StatusInfo{
   669  		Status:  status.Started,
   670  		Message: msg,
   671  		Since:   &now,
   672  	}
   673  }