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 }