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

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package migration
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/names/v5"
    11  	"github.com/juju/version/v2"
    12  
    13  	"github.com/juju/juju/apiserver/common"
    14  	coremigration "github.com/juju/juju/core/migration"
    15  	"github.com/juju/juju/core/status"
    16  	"github.com/juju/juju/state"
    17  	"github.com/juju/juju/tools"
    18  	"github.com/juju/juju/upgrades/upgradevalidation"
    19  )
    20  
    21  // SourcePrecheck checks the state of the source controller to make
    22  // sure that the preconditions for model migration are met. The
    23  // backend provided must be for the model to be migrated.
    24  func SourcePrecheck(
    25  	backend PrecheckBackend,
    26  	modelPresence ModelPresence, controllerPresence ModelPresence,
    27  	environscloudspecGetter environsCloudSpecGetter,
    28  ) error {
    29  	ctx := newPrecheckSource(backend, modelPresence, environscloudspecGetter)
    30  	if err := ctx.checkModel(); err != nil {
    31  		return errors.Trace(err)
    32  	}
    33  
    34  	if err := ctx.checkMachines(); err != nil {
    35  		return errors.Trace(err)
    36  	}
    37  
    38  	appUnits, err := ctx.checkApplications()
    39  	if err != nil {
    40  		return errors.Trace(err)
    41  	}
    42  
    43  	if err := ctx.checkRelations(appUnits); err != nil {
    44  		return errors.Trace(err)
    45  	}
    46  
    47  	if cleanupNeeded, err := backend.NeedsCleanup(); err != nil {
    48  		return errors.Annotate(err, "checking cleanups")
    49  	} else if cleanupNeeded {
    50  		return errors.New("cleanup needed")
    51  	}
    52  
    53  	// Check the source controller.
    54  	controllerBackend, err := backend.ControllerBackend()
    55  	if err != nil {
    56  		return errors.Trace(err)
    57  	}
    58  	controllerCtx := newPrecheckTarget(controllerBackend, controllerPresence, environscloudspecGetter)
    59  	if err := controllerCtx.checkController(); err != nil {
    60  		return errors.Annotate(err, "controller")
    61  	}
    62  	return nil
    63  }
    64  
    65  // TargetPrecheck checks the state of the target controller to make
    66  // sure that the preconditions for model migration are met. The
    67  // backend provided must be for the target controller.
    68  func TargetPrecheck(backend PrecheckBackend, pool Pool, modelInfo coremigration.ModelInfo, presence ModelPresence) error {
    69  	if err := modelInfo.Validate(); err != nil {
    70  		return errors.Trace(err)
    71  	}
    72  
    73  	// This check is necessary because there is a window between the
    74  	// REAP phase and then end of the DONE phase where a model's
    75  	// documents have been deleted but the migration isn't quite done
    76  	// yet. Migrating a model back into the controller during this
    77  	// window can upset the migrationmaster worker.
    78  	//
    79  	// See also https://lpad.tv/1611391
    80  	if migrating, err := backend.IsMigrationActive(modelInfo.UUID); err != nil {
    81  		return errors.Annotate(err, "checking for active migration")
    82  	} else if migrating {
    83  		return errors.New("model is being migrated out of target controller")
    84  	}
    85  
    86  	controllerVersion, err := backend.AgentVersion()
    87  	if err != nil {
    88  		return errors.Annotate(err, "retrieving model version")
    89  	}
    90  
    91  	if controllerVersion.Compare(modelInfo.AgentVersion) < 0 {
    92  		return errors.Errorf("model has higher version than target controller (%s > %s)",
    93  			modelInfo.AgentVersion, controllerVersion)
    94  	}
    95  
    96  	if !controllerVersionCompatible(modelInfo.ControllerAgentVersion, controllerVersion) {
    97  		return errors.Errorf("source controller has higher version than target controller (%s > %s)",
    98  			modelInfo.ControllerAgentVersion, controllerVersion)
    99  	}
   100  
   101  	// The MigrateToAllowed check is the same as validating if a model can be
   102  	// migrated to a controller running a newer Juju version.
   103  	allowed, minVer, err := upgradevalidation.MigrateToAllowed(modelInfo.AgentVersion, controllerVersion)
   104  	if err != nil {
   105  		return errors.Maskf(err, "unknown target controller version %v", controllerVersion)
   106  	}
   107  	if !allowed {
   108  		return errors.Errorf("model must be upgraded to at least version %s before being migrated to a controller with version %s", minVer, controllerVersion)
   109  	}
   110  
   111  	controllerCtx := newPrecheckTarget(backend, presence, nil)
   112  	if err := controllerCtx.checkController(); err != nil {
   113  		return errors.Trace(err)
   114  	}
   115  
   116  	// Check for conflicts with existing models
   117  	modelUUIDs, err := backend.AllModelUUIDs()
   118  	if err != nil {
   119  		return errors.Annotate(err, "retrieving models")
   120  	}
   121  	for _, modelUUID := range modelUUIDs {
   122  		model, release, err := pool.GetModel(modelUUID)
   123  		if err != nil {
   124  			return errors.Trace(err)
   125  		}
   126  		defer release()
   127  
   128  		// If the model is importing then it's probably left behind
   129  		// from a previous migration attempt. It will be removed
   130  		// before the next import.
   131  		if model.UUID() == modelInfo.UUID && model.MigrationMode() != state.MigrationModeImporting {
   132  			return errors.Errorf("model with same UUID already exists (%s)", modelInfo.UUID)
   133  		}
   134  		if model.Name() == modelInfo.Name && model.Owner() == modelInfo.Owner {
   135  			return errors.Errorf("model named %q already exists", model.Name())
   136  		}
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  type precheckTarget struct {
   143  	precheckContext
   144  }
   145  
   146  func newPrecheckTarget(backend PrecheckBackend, presence ModelPresence, environscloudspecGetter environsCloudSpecGetter) *precheckTarget {
   147  	return &precheckTarget{
   148  		precheckContext: precheckContext{
   149  			backend:                 backend,
   150  			presence:                presence,
   151  			environscloudspecGetter: environscloudspecGetter,
   152  		},
   153  	}
   154  }
   155  
   156  type precheckContext struct {
   157  	backend                 PrecheckBackend
   158  	presence                ModelPresence
   159  	environscloudspecGetter environsCloudSpecGetter
   160  }
   161  
   162  func (ctx *precheckContext) checkController() error {
   163  	model, err := ctx.backend.Model()
   164  	if err != nil {
   165  		return errors.Annotate(err, "retrieving model")
   166  	}
   167  	if model.Life() != state.Alive {
   168  		return errors.Errorf("model is %s", model.Life())
   169  	}
   170  
   171  	if upgrading, err := ctx.backend.IsUpgrading(); err != nil {
   172  		return errors.Annotate(err, "checking for upgrades")
   173  	} else if upgrading {
   174  		return errors.New("upgrade in progress")
   175  	}
   176  
   177  	return errors.Trace(ctx.checkMachines())
   178  }
   179  
   180  func (ctx *precheckContext) checkMachines() error {
   181  	modelVersion, err := ctx.backend.AgentVersion()
   182  	if err != nil {
   183  		return errors.Annotate(err, "retrieving model version")
   184  	}
   185  
   186  	machines, err := ctx.backend.AllMachines()
   187  	if err != nil {
   188  		return errors.Annotate(err, "retrieving machines")
   189  	}
   190  	modelPresenceContext := common.ModelPresenceContext{Presence: ctx.presence}
   191  	for _, machine := range machines {
   192  		if machine.Life() != state.Alive {
   193  			return errors.Errorf("machine %s is %s", machine.Id(), machine.Life())
   194  		}
   195  
   196  		if statusInfo, err := machine.InstanceStatus(); err != nil {
   197  			return errors.Annotatef(err, "retrieving machine %s instance status", machine.Id())
   198  		} else if statusInfo.Status != status.Running {
   199  			return newStatusError("machine %s not running", machine.Id(), statusInfo.Status)
   200  		}
   201  
   202  		if statusInfo, err := modelPresenceContext.MachineStatus(machine); err != nil {
   203  			return errors.Annotatef(err, "retrieving machine %s status", machine.Id())
   204  		} else if statusInfo.Status != status.Started {
   205  			return newStatusError("machine %s agent not functioning at this time",
   206  				machine.Id(), statusInfo.Status)
   207  		}
   208  
   209  		if rebootAction, err := machine.ShouldRebootOrShutdown(); err != nil {
   210  			return errors.Annotatef(err, "retrieving machine %s reboot status", machine.Id())
   211  		} else if rebootAction != state.ShouldDoNothing {
   212  			return errors.Errorf("machine %s is scheduled to %s", machine.Id(), rebootAction)
   213  		}
   214  
   215  		if err := checkAgentTools(modelVersion, machine, "machine "+machine.Id()); err != nil {
   216  			return errors.Trace(err)
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  func (ctx *precheckContext) checkApplications() (map[string][]PrecheckUnit, error) {
   223  	modelVersion, err := ctx.backend.AgentVersion()
   224  	if err != nil {
   225  		return nil, errors.Annotate(err, "retrieving model version")
   226  	}
   227  	apps, err := ctx.backend.AllApplications()
   228  	if err != nil {
   229  		return nil, errors.Annotate(err, "retrieving applications")
   230  	}
   231  
   232  	model, err := ctx.backend.Model()
   233  	if err != nil {
   234  		return nil, errors.Annotate(err, "retrieving model")
   235  	}
   236  	appUnits := make(map[string][]PrecheckUnit, len(apps))
   237  	for _, app := range apps {
   238  		if app.Life() != state.Alive {
   239  			return nil, errors.Errorf("application %s is %s", app.Name(), app.Life())
   240  		}
   241  		units, err := app.AllUnits()
   242  		if err != nil {
   243  			return nil, errors.Annotatef(err, "retrieving units for %s", app.Name())
   244  		}
   245  		err = ctx.checkUnits(app, units, modelVersion, model.Type())
   246  		if err != nil {
   247  			return nil, errors.Trace(err)
   248  		}
   249  		appUnits[app.Name()] = units
   250  	}
   251  	return appUnits, nil
   252  }
   253  
   254  func (ctx *precheckContext) checkUnits(app PrecheckApplication, units []PrecheckUnit, modelVersion version.Number, modelType state.ModelType) error {
   255  	if len(units) < app.MinUnits() {
   256  		return errors.Errorf("application %s is below its minimum units threshold", app.Name())
   257  	}
   258  
   259  	appCharmURL, _ := app.CharmURL()
   260  	if appCharmURL == nil {
   261  		return errors.Errorf("application charm url is nil")
   262  	}
   263  
   264  	for _, unit := range units {
   265  		if unit.Life() != state.Alive {
   266  			return errors.Errorf("unit %s is %s", unit.Name(), unit.Life())
   267  		}
   268  
   269  		if err := ctx.checkUnitAgentStatus(unit); err != nil {
   270  			return errors.Trace(err)
   271  		}
   272  
   273  		if modelType == state.ModelTypeIAAS {
   274  			if err := checkAgentTools(modelVersion, unit, "unit "+unit.Name()); err != nil {
   275  				return errors.Trace(err)
   276  			}
   277  		}
   278  
   279  		unitCharmURL := unit.CharmURL()
   280  		if unitCharmURL == nil || *appCharmURL != *unitCharmURL {
   281  			return errors.Errorf("unit %s is upgrading", unit.Name())
   282  		}
   283  	}
   284  	return nil
   285  }
   286  
   287  func (ctx *precheckContext) checkUnitAgentStatus(unit PrecheckUnit) error {
   288  	modelPresenceContext := common.ModelPresenceContext{Presence: ctx.presence}
   289  	statusData, _ := modelPresenceContext.UnitStatus(unit)
   290  	if statusData.Err != nil {
   291  		return errors.Annotatef(statusData.Err, "retrieving unit %s status", unit.Name())
   292  	}
   293  	agentStatus := statusData.Status.Status
   294  	switch agentStatus {
   295  	case status.Idle, status.Executing:
   296  		// These two are fine.
   297  	default:
   298  		return newStatusError("unit %s not idle or executing", unit.Name(), agentStatus)
   299  	}
   300  	return nil
   301  }
   302  
   303  func (ctx *precheckContext) checkRelations(appUnits map[string][]PrecheckUnit) error {
   304  	relations, err := ctx.backend.AllRelations()
   305  	if err != nil {
   306  		return errors.Annotate(err, "retrieving model relations")
   307  	}
   308  	for _, rel := range relations {
   309  		remoteAppName, crossModel, err := rel.RemoteApplication()
   310  		if err != nil && !errors.IsNotFound(err) {
   311  			return errors.Annotatef(err, "checking whether relation %s is cross-model", rel)
   312  		}
   313  
   314  		checkRelationUnit := func(ru PrecheckRelationUnit) error {
   315  			valid, err := ru.Valid()
   316  			if err != nil {
   317  				return errors.Trace(err)
   318  			}
   319  			if !valid {
   320  				return nil
   321  			}
   322  			inScope, err := ru.InScope()
   323  			if err != nil {
   324  				return errors.Trace(err)
   325  			}
   326  			if !inScope {
   327  				return errors.Errorf("unit %s hasn't joined relation %q yet", ru.UnitName(), rel)
   328  			}
   329  			return nil
   330  		}
   331  
   332  		for _, ep := range rel.Endpoints() {
   333  			// The endpoint app is either local or cross model.
   334  			// Handle each one as appropriate.
   335  			if crossModel && ep.ApplicationName == remoteAppName {
   336  				remoteUnits, err := rel.AllRemoteUnits(remoteAppName)
   337  				if err != nil {
   338  					return errors.Trace(err)
   339  				}
   340  				for _, ru := range remoteUnits {
   341  					if err := checkRelationUnit(ru); err != nil {
   342  						return errors.Trace(err)
   343  					}
   344  				}
   345  			} else {
   346  				for _, unit := range appUnits[ep.ApplicationName] {
   347  					ru, err := rel.Unit(unit)
   348  					if err != nil {
   349  						return errors.Trace(err)
   350  					}
   351  					if err := checkRelationUnit(ru); err != nil {
   352  						return errors.Trace(err)
   353  					}
   354  				}
   355  			}
   356  		}
   357  	}
   358  	return nil
   359  }
   360  
   361  type precheckSource struct {
   362  	precheckContext
   363  }
   364  
   365  func newPrecheckSource(backend PrecheckBackend, presence ModelPresence, environscloudspecGetter environsCloudSpecGetter) *precheckSource {
   366  	return &precheckSource{
   367  		precheckContext: precheckContext{
   368  			backend:                 backend,
   369  			presence:                presence,
   370  			environscloudspecGetter: environscloudspecGetter,
   371  		},
   372  	}
   373  }
   374  
   375  func (ctx *precheckSource) checkModel() error {
   376  	model, err := ctx.backend.Model()
   377  	if err != nil {
   378  		return errors.Annotate(err, "retrieving model")
   379  	}
   380  	if model.Life() != state.Alive {
   381  		return errors.Errorf("model is %s", model.Life())
   382  	}
   383  	if model.MigrationMode() == state.MigrationModeImporting {
   384  		return errors.New("model is being imported as part of another migration")
   385  	}
   386  	if credTag, found := model.CloudCredentialTag(); found {
   387  		creds, err := ctx.backend.CloudCredential(credTag)
   388  		if err != nil {
   389  			return errors.Trace(err)
   390  		}
   391  		if creds.Revoked {
   392  			return errors.New("model has revoked credentials")
   393  		}
   394  	}
   395  
   396  	cloudspec, err := ctx.environscloudspecGetter(names.NewModelTag(model.UUID()))
   397  	if err != nil {
   398  		return errors.Trace(err)
   399  	}
   400  	validators := upgradevalidation.ValidatorsForModelMigrationSource(cloudspec)
   401  	checker := upgradevalidation.NewModelUpgradeCheck(model.UUID(), nil, ctx.backend, model, validators...)
   402  	blockers, err := checker.Validate()
   403  	if err != nil {
   404  		return errors.Trace(err)
   405  	}
   406  	if blockers == nil {
   407  		return nil
   408  	}
   409  	return errors.NewNotSupported(nil, fmt.Sprintf("cannot migrate to controller due to issues:\n%s", blockers))
   410  }
   411  
   412  type agentToolsGetter interface {
   413  	AgentTools() (*tools.Tools, error)
   414  }
   415  
   416  func checkAgentTools(modelVersion version.Number, agent agentToolsGetter, agentLabel string) error {
   417  	tools, err := agent.AgentTools()
   418  	if err != nil {
   419  		return errors.Annotatef(err, "retrieving agent binaries for %s", agentLabel)
   420  	}
   421  	agentVersion := tools.Version.Number
   422  	if agentVersion != modelVersion {
   423  		return errors.Errorf("%s agent binaries don't match model (%s != %s)",
   424  			agentLabel, agentVersion, modelVersion)
   425  	}
   426  	return nil
   427  }
   428  
   429  func newStatusError(format, id string, s status.Status) error {
   430  	msg := fmt.Sprintf(format, id)
   431  	if s != status.Empty {
   432  		msg += fmt.Sprintf(" (%s)", s)
   433  	}
   434  	return errors.New(msg)
   435  }
   436  
   437  func controllerVersionCompatible(sourceVersion, targetVersion version.Number) bool {
   438  	// Compare source controller version to target controller version, only
   439  	// considering major and minor version numbers. Downgrades between
   440  	// patch, build releases for a given major.minor release are
   441  	// ok. Tag differences are ok too.
   442  	sourceVersion = versionToMajMin(sourceVersion)
   443  	targetVersion = versionToMajMin(targetVersion)
   444  	return sourceVersion.Compare(targetVersion) <= 0
   445  }
   446  
   447  func versionToMajMin(ver version.Number) version.Number {
   448  	ver.Patch = 0
   449  	ver.Build = 0
   450  	ver.Tag = ""
   451  	return ver
   452  }