github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/machine_upgradeseries.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/mgo/v3"
    14  	"github.com/juju/mgo/v3/bson"
    15  	"github.com/juju/mgo/v3/txn"
    16  	jujutxn "github.com/juju/txn/v3"
    17  
    18  	"github.com/juju/juju/core/model"
    19  	stateerrors "github.com/juju/juju/state/errors"
    20  )
    21  
    22  // upgradeSeriesLockDoc holds the attributes relevant to lock a machine during a
    23  // series update of a machine
    24  type upgradeSeriesLockDoc struct {
    25  	Id            string                             `bson:"machine-id"`
    26  	ToBase        string                             `bson:"to-base"`
    27  	FromBase      string                             `bson:"from-base"`
    28  	MachineStatus model.UpgradeSeriesStatus          `bson:"machine-status"`
    29  	Messages      []UpgradeSeriesMessage             `bson:"messages"`
    30  	TimeStamp     time.Time                          `bson:"timestamp"`
    31  	UnitStatuses  map[string]UpgradeSeriesUnitStatus `bson:"unit-statuses"`
    32  }
    33  
    34  type UpgradeSeriesUnitStatus struct {
    35  	Status    model.UpgradeSeriesStatus
    36  	Timestamp time.Time
    37  }
    38  
    39  // UpgradeSeriesMessage holds a message detailing why the upgrade series status
    40  // was updated. This format of this message should be a single sentence similar
    41  // to logging message. The string is accompanied by a timestamp and a boolean
    42  // value indicating whether or not the message has been observed by a client.
    43  type UpgradeSeriesMessage struct {
    44  	Message   string    `bson:"message"`
    45  	Timestamp time.Time `bson:"timestamp"`
    46  	Seen      bool      `bson:"seen"`
    47  }
    48  
    49  func newUpgradeSeriesMessage(name string, message string, timestamp time.Time) UpgradeSeriesMessage {
    50  	taggedMessage := fmt.Sprintf("%s %s", name, message)
    51  	return UpgradeSeriesMessage{
    52  		Message:   taggedMessage,
    53  		Timestamp: timestamp,
    54  		Seen:      false,
    55  	}
    56  }
    57  
    58  // CreateUpgradeSeriesLock create a prepare lock for series upgrade. If
    59  // this item exists in the database for a given machine it indicates that a
    60  // machine's operating system is being upgraded from one series to another;
    61  // for example, from xenial to bionic.
    62  func (m *Machine) CreateUpgradeSeriesLock(unitNames []string, toBase Base) error {
    63  	buildTxn := func(attempt int) ([]txn.Op, error) {
    64  		if attempt > 0 {
    65  			if err := m.Refresh(); err != nil {
    66  				return nil, errors.Trace(err)
    67  			}
    68  		}
    69  		locked, err := m.IsLockedForSeriesUpgrade()
    70  		if err != nil {
    71  			return nil, errors.Trace(err)
    72  		}
    73  		if locked {
    74  			return nil, errors.AlreadyExistsf("upgrade series lock for machine %q", m)
    75  		}
    76  		if err = m.isStillAlive(); err != nil {
    77  			return nil, errors.Trace(err)
    78  		}
    79  		// Exit early if the Machine base doesn't need to change.
    80  		fromBase := m.Base().String()
    81  		if fromBase == toBase.String() {
    82  			return nil, errors.Trace(errors.Errorf("machine %s already at base %s", m.Id(), toBase.String()))
    83  		}
    84  		// If the units have changed, the verification is no longer valid.
    85  		changed, err := m.unitsHaveChanged(unitNames)
    86  		if err != nil {
    87  			return nil, errors.Trace(err)
    88  		}
    89  		if changed {
    90  			return nil, errors.Errorf("Units have changed, please retry (%v)", unitNames)
    91  		}
    92  		data := m.prepareUpgradeBaseLock(unitNames, toBase)
    93  		return createUpgradeSeriesLockTxnOps(m.doc.Id, data), nil
    94  	}
    95  	err := m.st.db().Run(buildTxn)
    96  	if err != nil {
    97  		err = onAbort(err, stateerrors.ErrDead)
    98  		logger.Errorf("cannot prepare series upgrade for machine %q: %v", m, err)
    99  		return err
   100  	}
   101  	return nil
   102  }
   103  
   104  // IsParentLockedForSeriesUpgrade determines if a machine is a container who's
   105  // parent is locked for series upgrade.
   106  func (m *Machine) IsParentLockedForSeriesUpgrade() (bool, error) {
   107  	parentId, isContainer := m.ParentId()
   108  	if !isContainer {
   109  		return false, nil
   110  	}
   111  
   112  	parent, err := m.st.Machine(parentId)
   113  	if err != nil {
   114  		return false, errors.Trace(err)
   115  	}
   116  
   117  	locked, err := parent.IsLockedForSeriesUpgrade()
   118  	return locked, errors.Trace(err)
   119  }
   120  
   121  // IsLockedForSeriesUpgrade determines if a machine is locked for upgrade series.
   122  func (m *Machine) IsLockedForSeriesUpgrade() (bool, error) {
   123  	_, err := m.getUpgradeSeriesLock()
   124  	if err == nil {
   125  		return true, nil
   126  	}
   127  	if errors.IsNotFound(err) {
   128  		return false, nil
   129  	}
   130  	return false, errors.Trace(err)
   131  }
   132  
   133  func (m *Machine) unitsHaveChanged(unitNames []string) (bool, error) {
   134  	curUnits, err := m.Units()
   135  	if err != nil {
   136  		return true, err
   137  	}
   138  	if len(curUnits) == 0 && len(unitNames) == 0 {
   139  		return false, nil
   140  	}
   141  	if len(curUnits) != len(unitNames) {
   142  		return true, nil
   143  	}
   144  	curUnitSet := set.NewStrings()
   145  	for _, unit := range curUnits {
   146  		curUnitSet.Add(unit.Name())
   147  	}
   148  	unitNameSet := set.NewStrings(unitNames...)
   149  	return !unitNameSet.Difference(curUnitSet).IsEmpty(), nil
   150  }
   151  
   152  func (m *Machine) prepareUpgradeBaseLock(unitNames []string, toBase Base) *upgradeSeriesLockDoc {
   153  	// We want to put the unit statuses in to a prepared started state and only
   154  	// the machine status should be in a validate state. As we're only
   155  	// validating the machine and not each individual unit.
   156  	timestamp := bson.Now()
   157  	unitStatuses := make(map[string]UpgradeSeriesUnitStatus, len(unitNames))
   158  	for _, name := range unitNames {
   159  		unitStatuses[name] = UpgradeSeriesUnitStatus{
   160  			Status: model.UpgradeSeriesPrepareStarted, Timestamp: timestamp,
   161  		}
   162  	}
   163  
   164  	message := fmt.Sprintf("validation of upgrade base from %q to %q", m.Base().String(), toBase.String())
   165  	updateMessage := newUpgradeSeriesMessage(m.Tag().String(), message, timestamp)
   166  	return &upgradeSeriesLockDoc{
   167  		Id:            m.Id(),
   168  		ToBase:        toBase.String(),
   169  		FromBase:      m.Base().String(),
   170  		MachineStatus: model.UpgradeSeriesValidate,
   171  		UnitStatuses:  unitStatuses,
   172  		TimeStamp:     timestamp,
   173  		Messages: []UpgradeSeriesMessage{
   174  			updateMessage,
   175  		},
   176  	}
   177  }
   178  
   179  func createUpgradeSeriesLockTxnOps(machineDocId string, data *upgradeSeriesLockDoc) []txn.Op {
   180  	return []txn.Op{
   181  		{
   182  			C:      machinesC,
   183  			Id:     machineDocId,
   184  			Assert: isAliveDoc,
   185  		},
   186  		{
   187  			C:      machineUpgradeSeriesLocksC,
   188  			Id:     machineDocId,
   189  			Assert: txn.DocMissing,
   190  			Insert: data,
   191  		},
   192  	}
   193  }
   194  
   195  // UpgradeSeriesTarget returns the base
   196  // that the machine is being upgraded to.
   197  func (m *Machine) UpgradeSeriesTarget() (string, error) {
   198  	lock, err := m.getUpgradeSeriesLock()
   199  	if err != nil {
   200  		return "", errors.Trace(err)
   201  	}
   202  	return lock.ToBase, nil
   203  }
   204  
   205  // StartUpgradeSeriesUnitCompletion notifies units that an upgrade-machine
   206  // workflow is ready for its "completion" phase.
   207  func (m *Machine) StartUpgradeSeriesUnitCompletion(message string) error {
   208  	buildTxn := func(attempt int) ([]txn.Op, error) {
   209  		if attempt > 0 {
   210  			if err := m.Refresh(); err != nil {
   211  				return nil, errors.Trace(err)
   212  			}
   213  		}
   214  		if err := m.isStillAlive(); err != nil {
   215  			return nil, errors.Trace(err)
   216  		}
   217  		lock, err := m.getUpgradeSeriesLock()
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		if lock.MachineStatus != model.UpgradeSeriesCompleteStarted {
   222  			return nil, fmt.Errorf("machine %q can not complete its unit, the machine has not yet been marked as completed", m.Id())
   223  		}
   224  		timestamp := bson.Now()
   225  		lock.Messages = append(lock.Messages, newUpgradeSeriesMessage(m.Tag().String(), message, timestamp))
   226  		lock.TimeStamp = timestamp
   227  		changeCount := 0
   228  		for unitName, us := range lock.UnitStatuses {
   229  			if us.Status == model.UpgradeSeriesCompleteStarted {
   230  				continue
   231  			}
   232  			us.Status = model.UpgradeSeriesCompleteStarted
   233  			us.Timestamp = timestamp
   234  			lock.UnitStatuses[unitName] = us
   235  			changeCount++
   236  		}
   237  		if changeCount == 0 {
   238  			return nil, jujutxn.ErrNoOperations
   239  		}
   240  		return startUpgradeSeriesUnitCompletionTxnOps(m.doc.Id, lock), nil
   241  	}
   242  	err := m.st.db().Run(buildTxn)
   243  	if err != nil {
   244  		err = onAbort(err, stateerrors.ErrDead)
   245  		return err
   246  	}
   247  	return nil
   248  }
   249  
   250  func startUpgradeSeriesUnitCompletionTxnOps(machineDocID string, lock *upgradeSeriesLockDoc) []txn.Op {
   251  	statusField := "unit-statuses"
   252  	return []txn.Op{
   253  		{
   254  			C:      machinesC,
   255  			Id:     machineDocID,
   256  			Assert: isAliveDoc,
   257  		},
   258  		{
   259  			C:      machineUpgradeSeriesLocksC,
   260  			Id:     machineDocID,
   261  			Assert: bson.D{{"machine-status", model.UpgradeSeriesCompleteStarted}},
   262  			Update: bson.D{{"$set", bson.D{
   263  				{statusField, lock.UnitStatuses},
   264  				{"timestamp", lock.TimeStamp},
   265  				{"messages", lock.Messages}}}},
   266  		},
   267  	}
   268  }
   269  
   270  // CompleteUpgradeSeries notifies units and machines that an upgrade series is
   271  // ready for its "completion" phase.
   272  func (m *Machine) CompleteUpgradeSeries() error {
   273  	buildTxn := func(attempt int) ([]txn.Op, error) {
   274  		if attempt > 0 {
   275  			if err := m.Refresh(); err != nil {
   276  				return nil, errors.Trace(err)
   277  			}
   278  		}
   279  		if err := m.isStillAlive(); err != nil {
   280  			return nil, errors.Trace(err)
   281  		}
   282  		readyForCompletion, err := m.isReadyForCompletion()
   283  		if err != nil {
   284  			return nil, errors.Trace(err)
   285  		}
   286  		if !readyForCompletion {
   287  			return nil, fmt.Errorf("machine %q can not complete, it is either not prepared or already completed", m.Id())
   288  		}
   289  		timestamp := bson.Now()
   290  		message := newUpgradeSeriesMessage(m.Tag().String(), "complete phase started", timestamp)
   291  		return completeUpgradeSeriesTxnOps(m.doc.Id, timestamp, message), nil
   292  	}
   293  	err := m.st.db().Run(buildTxn)
   294  	if err != nil {
   295  		err = onAbort(err, stateerrors.ErrDead)
   296  		return err
   297  	}
   298  	return nil
   299  }
   300  
   301  func (m *Machine) isReadyForCompletion() (bool, error) {
   302  	lock, err := m.getUpgradeSeriesLock()
   303  	if err != nil {
   304  		return false, err
   305  	}
   306  	return lock.MachineStatus == model.UpgradeSeriesPrepareCompleted, nil
   307  }
   308  
   309  func completeUpgradeSeriesTxnOps(machineDocID string, timestamp time.Time, message UpgradeSeriesMessage) []txn.Op {
   310  	return []txn.Op{
   311  		{
   312  			C:      machinesC,
   313  			Id:     machineDocID,
   314  			Assert: isAliveDoc,
   315  		},
   316  		{
   317  			C:      machineUpgradeSeriesLocksC,
   318  			Id:     machineDocID,
   319  			Assert: bson.D{{"machine-status", model.UpgradeSeriesPrepareCompleted}},
   320  			Update: bson.D{
   321  				{"$set", bson.D{
   322  					{"machine-status", model.UpgradeSeriesCompleteStarted},
   323  					{"timestamp", timestamp},
   324  				}},
   325  				{"$push", bson.D{{"messages", message}}}},
   326  		},
   327  	}
   328  }
   329  
   330  func (m *Machine) RemoveUpgradeSeriesLock() error {
   331  	buildTxn := func(attempt int) ([]txn.Op, error) {
   332  		if attempt > 0 {
   333  			if err := m.Refresh(); err != nil {
   334  				return nil, errors.Trace(err)
   335  			}
   336  		}
   337  		locked, err := m.IsLockedForSeriesUpgrade()
   338  		if err != nil {
   339  			return nil, errors.Trace(err)
   340  		}
   341  		if !locked {
   342  			return nil, jujutxn.ErrNoOperations
   343  		}
   344  		return removeUpgradeSeriesLockTxnOps(m.doc.Id), nil
   345  	}
   346  	err := m.st.db().Run(buildTxn)
   347  	if err != nil {
   348  		err = onAbort(err, stateerrors.ErrDead)
   349  		return err
   350  	}
   351  	return nil
   352  }
   353  
   354  func removeUpgradeSeriesLockTxnOps(machineDocId string) []txn.Op {
   355  	return []txn.Op{
   356  		{
   357  			C:      machineUpgradeSeriesLocksC,
   358  			Id:     machineDocId,
   359  			Assert: txn.DocExists,
   360  			Remove: true,
   361  		},
   362  	}
   363  }
   364  
   365  func (m *Machine) UpgradeSeriesStatus() (model.UpgradeSeriesStatus, error) {
   366  	coll, closer := m.st.db().GetCollection(machineUpgradeSeriesLocksC)
   367  	defer closer()
   368  
   369  	var lock upgradeSeriesLockDoc
   370  	err := coll.FindId(m.Id()).One(&lock)
   371  	if err == mgo.ErrNotFound {
   372  		return "", errors.NotFoundf("upgrade series lock for machine %q", m.Id())
   373  	}
   374  	if err != nil {
   375  		return "", errors.Trace(err)
   376  	}
   377  
   378  	return lock.MachineStatus, errors.Trace(err)
   379  }
   380  
   381  // UpgradeSeriesUnitStatuses returns the unit statuses
   382  // from the upgrade-series lock for this machine.
   383  func (m *Machine) UpgradeSeriesUnitStatuses() (map[string]UpgradeSeriesUnitStatus, error) {
   384  	lock, err := m.getUpgradeSeriesLock()
   385  	if err != nil {
   386  		return nil, errors.Trace(err)
   387  	}
   388  	return lock.UnitStatuses, nil
   389  }
   390  
   391  // SetUpgradeSeriesUnitStatus sets the status of a series upgrade for a unit.
   392  func (m *Machine) SetUpgradeSeriesUnitStatus(unitName string, status model.UpgradeSeriesStatus, message string) error {
   393  	buildTxn := func(attempt int) ([]txn.Op, error) {
   394  		if attempt > 0 {
   395  			if err := m.Refresh(); err != nil {
   396  				return nil, errors.Trace(err)
   397  			}
   398  		}
   399  		if err := m.isStillAlive(); err != nil {
   400  			return nil, errors.Trace(err)
   401  		}
   402  		canUpdate, err := m.verifyUnitUpgradeSeriesStatus(unitName, status)
   403  		if err != nil {
   404  			return nil, errors.Trace(err)
   405  		}
   406  		if !canUpdate {
   407  			return nil, jujutxn.ErrNoOperations
   408  		}
   409  		timestamp := bson.Now()
   410  		updateMessage := newUpgradeSeriesMessage(unitName, message, timestamp)
   411  		return setUpgradeSeriesTxnOps(m.doc.Id, unitName, status, timestamp, updateMessage)
   412  	}
   413  	err := m.st.db().Run(buildTxn)
   414  	if err != nil {
   415  		err = onAbort(err, stateerrors.ErrDead)
   416  		return err
   417  	}
   418  	return nil
   419  }
   420  
   421  // verifyUnitUpgradeSeriesStatus returns a boolean indicating whether or not it
   422  // is safe to update the UpgradeSeriesStatus of a lock.
   423  func (m *Machine) verifyUnitUpgradeSeriesStatus(unitName string, status model.UpgradeSeriesStatus) (bool, error) {
   424  	lock, err := m.getUpgradeSeriesLock()
   425  	if err != nil {
   426  		return false, errors.Trace(err)
   427  	}
   428  	us, ok := lock.UnitStatuses[unitName]
   429  	if !ok {
   430  		return false, errors.NotFoundf(unitName)
   431  	}
   432  
   433  	fsm, err := model.NewUpgradeSeriesFSM(model.UpgradeSeriesGraph(), us.Status)
   434  	if err != nil {
   435  		return false, errors.Trace(err)
   436  	}
   437  	return fsm.TransitionTo(status), nil
   438  }
   439  
   440  // [TODO](externalreality): move some/all of these parameters into an argument structure.
   441  func setUpgradeSeriesTxnOps(
   442  	machineDocID,
   443  	unitName string,
   444  	status model.UpgradeSeriesStatus,
   445  	timestamp time.Time,
   446  	message UpgradeSeriesMessage,
   447  ) ([]txn.Op, error) {
   448  	statusField := "unit-statuses"
   449  	unitStatusField := fmt.Sprintf("%s.%s.status", statusField, unitName)
   450  	unitTimestampField := fmt.Sprintf("%s.%s.timestamp", statusField, unitName)
   451  	return []txn.Op{
   452  		{
   453  			C:      machinesC,
   454  			Id:     machineDocID,
   455  			Assert: isAliveDoc,
   456  		},
   457  		{
   458  			C:  machineUpgradeSeriesLocksC,
   459  			Id: machineDocID,
   460  			Assert: bson.D{{"$and", []bson.D{
   461  				{{statusField, bson.D{{"$exists", true}}}}, // if it doesn't exist something is wrong
   462  				{{unitStatusField, bson.D{{"$ne", status}}}}}}},
   463  			Update: bson.D{
   464  				{"$set", bson.D{
   465  					{unitStatusField, status},
   466  					{"timestamp", timestamp},
   467  					{unitTimestampField, timestamp}}},
   468  				{"$push", bson.D{{"messages", message}}},
   469  			},
   470  		},
   471  	}, nil
   472  }
   473  
   474  // SetUpgradeSeriesStatus sets the status of the machine in
   475  // the upgrade-series lock.
   476  func (m *Machine) SetUpgradeSeriesStatus(status model.UpgradeSeriesStatus, message string) error {
   477  	buildTxn := func(attempt int) ([]txn.Op, error) {
   478  		if attempt > 0 {
   479  			if err := m.Refresh(); err != nil {
   480  				return nil, errors.Trace(err)
   481  			}
   482  		}
   483  		if err := m.isStillAlive(); err != nil {
   484  			return nil, errors.Trace(err)
   485  		}
   486  		statusSet, err := m.isMachineUpgradeSeriesStatusSet(status)
   487  		if err != nil {
   488  			return nil, errors.Trace(err)
   489  		}
   490  		if statusSet {
   491  			return nil, jujutxn.ErrNoOperations
   492  		}
   493  		timestamp := bson.Now()
   494  		upgradeSeriesMessage := newUpgradeSeriesMessage(m.Tag().String(), message, timestamp)
   495  		return setMachineUpgradeSeriesTxnOps(m.doc.Id, status, upgradeSeriesMessage, timestamp), nil
   496  	}
   497  	err := m.st.db().Run(buildTxn)
   498  	if err != nil {
   499  		err = onAbort(err, stateerrors.ErrDead)
   500  		return err
   501  	}
   502  	return nil
   503  }
   504  
   505  // GetUpgradeSeriesMessages returns all 'unseen' upgrade series
   506  // notifications sorted by timestamp.
   507  func (m *Machine) GetUpgradeSeriesMessages() ([]string, bool, error) {
   508  	lock, err := m.getUpgradeSeriesLock()
   509  	if errors.IsNotFound(err) {
   510  		// If the lock is not found here then there are no more messages
   511  		return nil, true, nil
   512  	}
   513  	if err != nil {
   514  		return nil, false, errors.Trace(err)
   515  	}
   516  	// finished means that a subsequent call to this method, while the
   517  	// Machine Lock is of a similar Machine Status, would return no
   518  	// additional messages (notifications). Since the value of this variable
   519  	// is returned, callers may choose to close streams or stop watchers
   520  	// based on this information.
   521  	finished := lock.MachineStatus == model.UpgradeSeriesCompleted ||
   522  		lock.MachineStatus == model.UpgradeSeriesPrepareCompleted
   523  	// Filter seen messages
   524  	unseenMessages := make([]UpgradeSeriesMessage, 0)
   525  	for _, upgradeSeriesMessage := range lock.Messages {
   526  		if !upgradeSeriesMessage.Seen {
   527  			unseenMessages = append(unseenMessages, upgradeSeriesMessage)
   528  		}
   529  	}
   530  	if len(unseenMessages) == 0 {
   531  		return []string{}, finished, nil
   532  	}
   533  	sort.Slice(unseenMessages, func(i, j int) bool {
   534  		return unseenMessages[i].Timestamp.Before(unseenMessages[j].Timestamp)
   535  	})
   536  	messages := make([]string, 0)
   537  	for _, unseenMessage := range unseenMessages {
   538  		messages = append(messages, unseenMessage.Message)
   539  	}
   540  	err = m.SetUpgradeSeriesMessagesAsSeen(lock.Messages)
   541  	if err != nil {
   542  		return nil, false, errors.Trace(err)
   543  	}
   544  	return messages, finished, nil
   545  }
   546  
   547  // SetUpgradeSeriesMessagesAsSeen marks a given upgrade series messages as
   548  // having been seen by a client of the API.
   549  func (m *Machine) SetUpgradeSeriesMessagesAsSeen(messages []UpgradeSeriesMessage) error {
   550  	buildTxn := func(attempt int) ([]txn.Op, error) {
   551  		if attempt > 0 {
   552  			if err := m.Refresh(); err != nil {
   553  				return nil, errors.Trace(err)
   554  			}
   555  		}
   556  		if len(messages) == 0 {
   557  			return nil, jujutxn.ErrNoOperations
   558  		}
   559  		if err := m.isStillAlive(); err != nil {
   560  			return nil, errors.Trace(err)
   561  		}
   562  		return setUpgradeSeriesMessageTxnOps(m.doc.Id, messages, true), nil
   563  	}
   564  	err := m.st.db().Run(buildTxn)
   565  	if err != nil {
   566  		err = onAbort(err, stateerrors.ErrDead)
   567  		return err
   568  	}
   569  	return nil
   570  }
   571  
   572  func setUpgradeSeriesMessageTxnOps(machineDocID string, messages []UpgradeSeriesMessage, seen bool) []txn.Op {
   573  	ops := []txn.Op{
   574  		{
   575  			C:      machinesC,
   576  			Id:     machineDocID,
   577  			Assert: isAliveDoc,
   578  		},
   579  	}
   580  	fields := bson.D{}
   581  	for i := range messages {
   582  		field := fmt.Sprintf("messages.%d.seen", i)
   583  		fields = append(fields, bson.DocElem{Name: field, Value: seen})
   584  	}
   585  	ops = append(ops, txn.Op{
   586  		C:      machineUpgradeSeriesLocksC,
   587  		Id:     machineDocID,
   588  		Update: bson.D{{"$set", fields}},
   589  	})
   590  	return ops
   591  }
   592  
   593  func (m *Machine) isMachineUpgradeSeriesStatusSet(status model.UpgradeSeriesStatus) (bool, error) {
   594  	lock, err := m.getUpgradeSeriesLock()
   595  	if err != nil {
   596  		return false, err
   597  	}
   598  	return lock.MachineStatus == status, nil
   599  }
   600  
   601  func (m *Machine) getUpgradeSeriesLock() (*upgradeSeriesLockDoc, error) {
   602  	lock, err := m.st.getUpgradeSeriesLock(m.Id())
   603  	return lock, errors.Trace(err)
   604  }
   605  
   606  func (st *State) getUpgradeSeriesLock(machineID string) (*upgradeSeriesLockDoc, error) {
   607  	coll, closer := st.db().GetCollection(machineUpgradeSeriesLocksC)
   608  	defer closer()
   609  
   610  	var lock upgradeSeriesLockDoc
   611  	err := coll.FindId(machineID).One(&lock)
   612  	if err == mgo.ErrNotFound {
   613  		return nil, errors.NotFoundf("upgrade lock for machine %q", machineID)
   614  	}
   615  	if err != nil {
   616  		return nil, errors.Annotatef(err, "retrieving upgrade series lock for machine %v", machineID)
   617  	}
   618  	return &lock, nil
   619  }
   620  
   621  func setMachineUpgradeSeriesTxnOps(
   622  	machineDocID string, status model.UpgradeSeriesStatus, message UpgradeSeriesMessage, timestamp time.Time,
   623  ) []txn.Op {
   624  	field := "machine-status"
   625  
   626  	return []txn.Op{
   627  		{
   628  			C:      machinesC,
   629  			Id:     machineDocID,
   630  			Assert: isAliveDoc,
   631  		},
   632  		{
   633  			C:  machineUpgradeSeriesLocksC,
   634  			Id: machineDocID,
   635  			Update: bson.D{
   636  				{"$set", bson.D{{field, status}, {"timestamp", timestamp}}},
   637  				{"$push", bson.D{{"messages", message}}},
   638  			},
   639  		},
   640  	}
   641  }
   642  
   643  // upgradeSeriesMachineIds returns the IDs of all machines
   644  // currently locked for series-upgrade.
   645  func (st *State) upgradeSeriesMachineIds() ([]string, error) {
   646  	coll, closer := st.db().GetCollection(machineUpgradeSeriesLocksC)
   647  	defer closer()
   648  
   649  	var locks []struct {
   650  		Id string `bson:"machine-id"`
   651  	}
   652  	if err := coll.Find(nil).Select(bson.M{"machine-id": 1}).All(&locks); err != nil {
   653  		return nil, errors.Trace(err)
   654  	}
   655  
   656  	ids := make([]string, len(locks))
   657  	for i, l := range locks {
   658  		ids[i] = l.Id
   659  	}
   660  	return ids, nil
   661  }
   662  
   663  // HasUpgradeSeriesLocks returns true if there are any upgrade machine locks.
   664  func (st *State) HasUpgradeSeriesLocks() (bool, error) {
   665  	ids, err := st.upgradeSeriesMachineIds()
   666  	if err != nil {
   667  		return false, errors.Trace(err)
   668  	}
   669  	return len(ids) > 0, nil
   670  }