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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/mgo/v3/bson"
    13  	"github.com/juju/mgo/v3/txn"
    14  	"github.com/juju/names/v5"
    15  	jujutxn "github.com/juju/txn/v3"
    16  )
    17  
    18  // machineRemovalDoc indicates that this machine needs to be removed
    19  // and any necessary provider-level cleanup should now be done.
    20  type machineRemovalDoc struct {
    21  	DocID     string `bson:"_id"`
    22  	MachineID string `bson:"machine-id"`
    23  }
    24  
    25  func (m *Machine) markForRemovalOps() ([]txn.Op, error) {
    26  	if m.doc.Life != Dead {
    27  		return nil, errors.Errorf("machine is not dead")
    28  	}
    29  	ops := []txn.Op{{
    30  		C:      machinesC,
    31  		Id:     m.doc.DocID,
    32  		Assert: isDeadDoc,
    33  	}, {
    34  		C:      machineRemovalsC,
    35  		Id:     m.globalKey(),
    36  		Insert: &machineRemovalDoc{MachineID: m.Id()},
    37  	}}
    38  	return ops, nil
    39  }
    40  
    41  // MarkForRemoval requests that this machine be removed after any
    42  // needed provider-level cleanup is done.
    43  func (m *Machine) MarkForRemoval() (err error) {
    44  	defer errors.DeferredAnnotatef(&err, "cannot remove machine %s", m.doc.Id)
    45  	buildTxn := func(attempt int) ([]txn.Op, error) {
    46  		// Check if it has already been marked first.
    47  		// If so, we just do nothing and return success.
    48  		col, close := m.st.db().GetCollection(machineRemovalsC)
    49  		defer close()
    50  
    51  		remCount, err := col.FindId(m.globalKey()).Count()
    52  		if err != nil {
    53  			return nil, errors.Trace(err)
    54  		}
    55  		if remCount > 0 {
    56  			return nil, jujutxn.ErrNoOperations
    57  		}
    58  
    59  		if attempt != 0 {
    60  			if err := m.Refresh(); err != nil {
    61  				return nil, errors.Trace(err)
    62  			}
    63  		}
    64  		ops, err := m.markForRemovalOps()
    65  		if err != nil {
    66  			return nil, errors.Trace(err)
    67  		}
    68  		return ops, nil
    69  	}
    70  	return m.st.db().Run(buildTxn)
    71  }
    72  
    73  // AllMachineRemovals returns (the ids of) all of the machines that
    74  // need to be removed but need provider-level cleanup.
    75  func (st *State) AllMachineRemovals() ([]string, error) {
    76  	removals, close := st.db().GetCollection(machineRemovalsC)
    77  	defer close()
    78  
    79  	var docs []machineRemovalDoc
    80  	err := removals.Find(nil).All(&docs)
    81  	if err != nil {
    82  		return nil, errors.Trace(err)
    83  	}
    84  	results := make([]string, len(docs))
    85  	for i := range docs {
    86  		results[i] = docs[i].MachineID
    87  	}
    88  	return results, nil
    89  }
    90  
    91  func (st *State) allMachinesMatching(query bson.D) ([]*Machine, error) {
    92  	machines, close := st.db().GetCollection(machinesC)
    93  	defer close()
    94  
    95  	var docs []machineDoc
    96  	err := machines.Find(query).All(&docs)
    97  	if err != nil {
    98  		return nil, errors.Trace(err)
    99  	}
   100  	results := make([]*Machine, len(docs))
   101  	for i, doc := range docs {
   102  		results[i] = newMachine(st, &doc)
   103  	}
   104  	return results, nil
   105  }
   106  
   107  func plural(count int) string {
   108  	if count == 1 {
   109  		return ""
   110  	}
   111  	return "s"
   112  }
   113  
   114  func collectMissingMachineIds(expectedIds []string, machines []*Machine) []string {
   115  	expectedSet := set.NewStrings(expectedIds...)
   116  	actualSet := set.NewStrings()
   117  	for _, machine := range machines {
   118  		actualSet.Add(machine.Id())
   119  	}
   120  	return expectedSet.Difference(actualSet).SortedValues()
   121  }
   122  
   123  func checkValidMachineIds(machineIds []string) error {
   124  	var invalidIds []string
   125  	for _, id := range machineIds {
   126  		if !names.IsValidMachine(id) {
   127  			invalidIds = append(invalidIds, id)
   128  		}
   129  	}
   130  	if len(invalidIds) == 0 {
   131  		return nil
   132  	}
   133  	return errors.Errorf("Invalid machine id%s: %s",
   134  		plural(len(invalidIds)),
   135  		strings.Join(invalidIds, ", "),
   136  	)
   137  }
   138  
   139  func (st *State) completeMachineRemovalsOps(ids []string) ([]txn.Op, error) {
   140  	removals, err := st.AllMachineRemovals()
   141  	if err != nil {
   142  		return nil, errors.Trace(err)
   143  	}
   144  	removalSet := set.NewStrings(removals...)
   145  	query := bson.D{{"machineid", bson.D{{"$in", ids}}}}
   146  	machinesToRemove, err := st.allMachinesMatching(query)
   147  	if err != nil {
   148  		return nil, errors.Trace(err)
   149  	}
   150  
   151  	var ops []txn.Op
   152  	var missingRemovals []string
   153  	for _, machine := range machinesToRemove {
   154  		if !removalSet.Contains(machine.Id()) {
   155  			missingRemovals = append(missingRemovals, machine.Id())
   156  			continue
   157  		}
   158  
   159  		ops = append(ops, txn.Op{
   160  			C:      machineRemovalsC,
   161  			Id:     machine.globalKey(),
   162  			Assert: txn.DocExists,
   163  			Remove: true,
   164  		})
   165  		removeMachineOps, err := machine.removeOps()
   166  		if err != nil {
   167  			return nil, errors.Trace(err)
   168  		}
   169  		ops = append(ops, removeMachineOps...)
   170  	}
   171  	// We should complain about machines that still exist but haven't
   172  	// been marked for removal.
   173  	if len(missingRemovals) > 0 {
   174  		sort.Strings(missingRemovals)
   175  		return nil, errors.Errorf(
   176  			"cannot remove machine%s %s: not marked for removal",
   177  			plural(len(missingRemovals)),
   178  			strings.Join(missingRemovals, ", "),
   179  		)
   180  	}
   181  
   182  	// Log last to reduce the likelihood of repeating the message on
   183  	// retries.
   184  	if len(machinesToRemove) < len(ids) {
   185  		missingMachines := collectMissingMachineIds(ids, machinesToRemove)
   186  		logger.Debugf("skipping nonexistent machine%s: %s",
   187  			plural(len(missingMachines)),
   188  			strings.Join(missingMachines, ", "),
   189  		)
   190  	}
   191  
   192  	return ops, nil
   193  }
   194  
   195  // CompleteMachineRemovals finishes the removal of the specified
   196  // machines. The machines must have been marked for removal
   197  // previously. Valid-looking-but-unknown machine ids are ignored so
   198  // that this is idempotent.
   199  func (st *State) CompleteMachineRemovals(ids ...string) error {
   200  	if err := checkValidMachineIds(ids); err != nil {
   201  		return errors.Trace(err)
   202  	}
   203  
   204  	buildTxn := func(int) ([]txn.Op, error) {
   205  		// We don't need to reget state for subsequent attempts since
   206  		// completeMachineRemovalsOps gets the removals and the
   207  		// machines each time anyway.
   208  		ops, err := st.completeMachineRemovalsOps(ids)
   209  		if err != nil {
   210  			return nil, errors.Trace(err)
   211  		}
   212  		return ops, nil
   213  	}
   214  	return st.db().Run(buildTxn)
   215  }