github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/commands/upgradejuju.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "bufio" 8 stderrors "errors" 9 "fmt" 10 "io" 11 "os" 12 "path" 13 "strings" 14 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/gnuflag" 18 "github.com/juju/utils/series" 19 "github.com/juju/version" 20 21 "github.com/juju/juju/api/controller" 22 "github.com/juju/juju/api/modelconfig" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/cmd/juju/block" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/environs/config" 27 "github.com/juju/juju/environs/sync" 28 coretools "github.com/juju/juju/tools" 29 jujuversion "github.com/juju/juju/version" 30 ) 31 32 var usageUpgradeJujuSummary = ` 33 Upgrades Juju on all machines in a model.`[1:] 34 35 var usageUpgradeJujuDetails = ` 36 Juju provides agent software to every machine it creates. This command 37 upgrades that software across an entire model, which is, by default, the 38 current model. 39 A model's agent version can be shown with `[1:] + "`juju get-model-config agent-\nversion`" + `. 40 A version is denoted by: major.minor.patch 41 The upgrade candidate will be auto-selected if '--agent-version' is not 42 specified: 43 - If the server major version matches the client major version, the 44 version selected is minor+1. If such a minor version is not available then 45 the next patch version is chosen. 46 - If the server major version does not match the client major version, 47 the version selected is that of the client version. 48 If the controller is without internet access, the client must first supply 49 the software to the controller's cache via the ` + "`juju sync-tools`" + ` command. 50 The command will abort if an upgrade is in progress. It will also abort if 51 a previous upgrade was not fully completed (e.g.: if one of the 52 controllers in a high availability model failed to upgrade). 53 If a failed upgrade has been resolved, '--reset-previous-upgrade' can be 54 used to allow the upgrade to proceed. 55 Backups are recommended prior to upgrading. 56 57 Examples: 58 juju upgrade-juju --dry-run 59 juju upgrade-juju --agent-version 2.0.1 60 61 See also: 62 sync-tools` 63 64 func newUpgradeJujuCommand(minUpgradeVers map[int]version.Number, options ...modelcmd.WrapOption) cmd.Command { 65 if minUpgradeVers == nil { 66 minUpgradeVers = minMajorUpgradeVersion 67 } 68 return modelcmd.Wrap(&upgradeJujuCommand{minMajorUpgradeVersion: minUpgradeVers}, options...) 69 } 70 71 // upgradeJujuCommand upgrades the agents in a juju installation. 72 type upgradeJujuCommand struct { 73 modelcmd.ModelCommandBase 74 vers string 75 Version version.Number 76 BuildAgent bool 77 DryRun bool 78 ResetPrevious bool 79 AssumeYes bool 80 81 // minMajorUpgradeVersion maps known major numbers to 82 // the minimum version that can be upgraded to that 83 // major version. For example, users must be running 84 // 1.25.4 or later in order to upgrade to 2.0. 85 minMajorUpgradeVersion map[int]version.Number 86 } 87 88 func (c *upgradeJujuCommand) Info() *cmd.Info { 89 return &cmd.Info{ 90 Name: "upgrade-juju", 91 Purpose: usageUpgradeJujuSummary, 92 Doc: usageUpgradeJujuDetails, 93 } 94 } 95 96 func (c *upgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { 97 c.ModelCommandBase.SetFlags(f) 98 f.StringVar(&c.vers, "agent-version", "", "Upgrade to specific version") 99 f.BoolVar(&c.BuildAgent, "build-agent", false, "Build a local version of the agent binary; for development use only") 100 f.BoolVar(&c.DryRun, "dry-run", false, "Don't change anything, just report what would be changed") 101 f.BoolVar(&c.ResetPrevious, "reset-previous-upgrade", false, "Clear the previous (incomplete) upgrade status (use with care)") 102 f.BoolVar(&c.AssumeYes, "y", false, "Answer 'yes' to confirmation prompts") 103 f.BoolVar(&c.AssumeYes, "yes", false, "") 104 } 105 106 func (c *upgradeJujuCommand) Init(args []string) error { 107 if c.vers != "" { 108 vers, err := version.Parse(c.vers) 109 if err != nil { 110 return err 111 } 112 if c.BuildAgent && vers.Build != 0 { 113 // TODO(fwereade): when we start taking versions from actual built 114 // code, we should disable --agent-version when used with --build-agent. 115 // For now, it's the only way to experiment with version upgrade 116 // behaviour live, so the only restriction is that Build cannot 117 // be used (because its value needs to be chosen internally so as 118 // not to collide with existing tools). 119 return errors.New("cannot specify build number when building an agent") 120 } 121 c.Version = vers 122 } 123 return cmd.CheckEmpty(args) 124 } 125 126 var ( 127 errUpToDate = stderrors.New("no upgrades available") 128 downgradeErrMsg = "cannot change version from %s to %s" 129 minMajorUpgradeVersion = map[int]version.Number{ 130 2: version.MustParse("1.25.4"), 131 } 132 ) 133 134 // canUpgradeRunningVersion determines if the version of the running 135 // environment can be upgraded using this version of the 136 // upgrade-juju command. Only versions with a minor version 137 // of 0 are expected to be able to upgrade environments running 138 // the previous major version. 139 // 140 // This check is needed because we do not guarantee API 141 // compatibility across major versions. For example, a 3.3.0 142 // version of the upgrade-juju command may not know how to upgrade 143 // an environment running juju 4.0.0. 144 // 145 // The exception is that a N.0.* client must be able to upgrade 146 // an environment one major version prior (N-1.*.*) so that 147 // it can be used to upgrade the environment to N.0.*. For 148 // example, the 2.0.1 upgrade-juju command must be able to upgrade 149 // environments running 1.* since it must be able to upgrade 150 // environments from 1.25.4 -> 2.0.*. 151 func canUpgradeRunningVersion(runningAgentVer version.Number) bool { 152 if runningAgentVer.Major == jujuversion.Current.Major { 153 return true 154 } 155 if jujuversion.Current.Minor == 0 && runningAgentVer.Major == (jujuversion.Current.Major-1) { 156 return true 157 } 158 return false 159 } 160 161 func formatTools(tools coretools.List) string { 162 formatted := make([]string, len(tools)) 163 for i, tools := range tools { 164 formatted[i] = fmt.Sprintf(" %s", tools.Version.String()) 165 } 166 return strings.Join(formatted, "\n") 167 } 168 169 type upgradeJujuAPI interface { 170 FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) 171 UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (coretools.List, error) 172 AbortCurrentUpgrade() error 173 SetModelAgentVersion(version version.Number) error 174 Close() error 175 } 176 177 type modelConfigAPI interface { 178 ModelGet() (map[string]interface{}, error) 179 Close() error 180 } 181 182 type controllerAPI interface { 183 ModelConfig() (map[string]interface{}, error) 184 Close() error 185 } 186 187 var getUpgradeJujuAPI = func(c *upgradeJujuCommand) (upgradeJujuAPI, error) { 188 return c.NewAPIClient() 189 } 190 191 var getModelConfigAPI = func(c *upgradeJujuCommand) (modelConfigAPI, error) { 192 api, err := c.NewAPIRoot() 193 if err != nil { 194 return nil, errors.Trace(err) 195 } 196 return modelconfig.NewClient(api), nil 197 } 198 199 var getControllerAPI = func(c *upgradeJujuCommand) (controllerAPI, error) { 200 api, err := c.NewControllerAPIRoot() 201 if err != nil { 202 return nil, errors.Trace(err) 203 } 204 return controller.NewClient(api), nil 205 } 206 207 // Run changes the version proposed for the juju envtools. 208 func (c *upgradeJujuCommand) Run(ctx *cmd.Context) (err error) { 209 210 client, err := getUpgradeJujuAPI(c) 211 if err != nil { 212 return err 213 } 214 defer client.Close() 215 modelConfigClient, err := getModelConfigAPI(c) 216 if err != nil { 217 return err 218 } 219 defer modelConfigClient.Close() 220 controllerClient, err := getControllerAPI(c) 221 if err != nil { 222 return err 223 } 224 defer controllerClient.Close() 225 defer func() { 226 if err == errUpToDate { 227 ctx.Infof(err.Error()) 228 err = nil 229 } 230 }() 231 232 // Determine the version to upgrade to, uploading tools if necessary. 233 attrs, err := modelConfigClient.ModelGet() 234 if err != nil { 235 return err 236 } 237 cfg, err := config.New(config.NoDefaults, attrs) 238 if err != nil { 239 return err 240 } 241 242 controllerModelConfig, err := controllerClient.ModelConfig() 243 if err != nil { 244 return err 245 } 246 isControllerModel := cfg.UUID() == controllerModelConfig[config.UUIDKey] 247 if c.BuildAgent && !isControllerModel { 248 // For UploadTools, model must be the "controller" model, 249 // that is, modelUUID == controllerUUID 250 return errors.Errorf("--build-agent can only be used with the controller model") 251 } 252 253 agentVersion, ok := cfg.AgentVersion() 254 if !ok { 255 // Can't happen. In theory. 256 return errors.New("incomplete model configuration") 257 } 258 259 if c.BuildAgent && c.Version == version.Zero { 260 // Currently, uploading tools assumes the version to be 261 // the same as jujuversion.Current if not specified with 262 // --agent-version. 263 c.Version = jujuversion.Current 264 } 265 warnCompat := false 266 switch { 267 case !canUpgradeRunningVersion(agentVersion): 268 // This version of upgrade-juju cannot upgrade the running 269 // environment version (can't guarantee API compatibility). 270 return errors.Errorf("cannot upgrade a %s model with a %s client", 271 agentVersion, jujuversion.Current) 272 case c.Version != version.Zero && c.Version.Major < agentVersion.Major: 273 // The specified version would downgrade the environment. 274 // Don't upgrade and return an error. 275 return errors.Errorf(downgradeErrMsg, agentVersion, c.Version) 276 case agentVersion.Major != jujuversion.Current.Major: 277 // Running environment is the previous major version (a higher major 278 // version wouldn't have passed the check in canUpgradeRunningVersion). 279 if c.Version == version.Zero || c.Version.Major == agentVersion.Major { 280 // Not requesting an upgrade across major release boundary. 281 // Warn of incompatible CLI and filter on the prior major version 282 // when searching for available tools. 283 // TODO(cherylj) Add in a suggestion to upgrade to 2.0 if 284 // no matching tools are found (bug 1532670) 285 warnCompat = true 286 break 287 } 288 // User requested an upgrade to the next major version. 289 // Fallthrough to the next case to verify that the upgrade 290 // conditions are met. 291 fallthrough 292 case c.Version.Major > agentVersion.Major: 293 // User is requesting an upgrade to a new major number 294 // Only upgrade to a different major number if: 295 // 1 - Explicitly requested with --agent-version or using --build-agent, and 296 // 2 - The environment is running a valid version to upgrade from, and 297 // 3 - The upgrade is to a minor version of 0. 298 minVer, ok := c.minMajorUpgradeVersion[c.Version.Major] 299 if !ok { 300 return errors.Errorf("unknown version %q", c.Version) 301 } 302 retErr := false 303 if c.Version.Minor != 0 { 304 ctx.Infof("upgrades to %s must first go through juju %d.0", 305 c.Version, c.Version.Major) 306 retErr = true 307 } 308 if comp := agentVersion.Compare(minVer); comp < 0 { 309 ctx.Infof("upgrades to a new major version must first go through %s", 310 minVer) 311 retErr = true 312 } 313 if retErr { 314 return errors.New("unable to upgrade to requested version") 315 } 316 } 317 318 context, err := c.initVersions(client, cfg, agentVersion, warnCompat) 319 if err != nil { 320 return err 321 } 322 // If we're running a custom build or the user has asked for a new agent 323 // to be built, upload a local jujud binary if possible. 324 uploadLocalBinary := isControllerModel && c.Version == version.Zero && tryImplicitUpload(agentVersion) 325 if !warnCompat && (uploadLocalBinary || c.BuildAgent) && !c.DryRun { 326 if err := context.uploadTools(c.BuildAgent); err != nil { 327 // If we've explicitly asked to build an agent binary, or the upload failed 328 // because changes were blocked, we'll return an error. 329 if err2 := block.ProcessBlockedError(err, block.BlockChange); c.BuildAgent || err2 == cmd.ErrSilent { 330 return err2 331 } 332 } 333 builtMsg := "" 334 if c.BuildAgent { 335 builtMsg = " (built from source)" 336 } 337 fmt.Fprintf(ctx.Stdout, "no prepackaged tools available, using local agent binary %v%s\n", context.chosen, builtMsg) 338 } 339 340 // If there was an error implicitly uploading a binary, we'll still look for any packaged binaries 341 // since there may still be a valid upgrade and the user didn't ask for any local binary. 342 if err := context.validate(); err != nil { 343 return err 344 } 345 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 346 ctx.Verbosef("available tools:\n%s", formatTools(context.tools)) 347 ctx.Verbosef("best version:\n %s", context.chosen) 348 if warnCompat { 349 fmt.Fprintf(ctx.Stderr, "version %s incompatible with this client (%s)\n", context.chosen, jujuversion.Current) 350 } 351 if c.DryRun { 352 fmt.Fprintf(ctx.Stderr, "upgrade to this version by running\n juju upgrade-juju --agent-version=\"%s\"\n", context.chosen) 353 } else { 354 if c.ResetPrevious { 355 if ok, err := c.confirmResetPreviousUpgrade(ctx); !ok || err != nil { 356 const message = "previous upgrade not reset and no new upgrade triggered" 357 if err != nil { 358 return errors.Annotate(err, message) 359 } 360 return errors.New(message) 361 } 362 if err := client.AbortCurrentUpgrade(); err != nil { 363 return block.ProcessBlockedError(err, block.BlockChange) 364 } 365 } 366 if err := client.SetModelAgentVersion(context.chosen); err != nil { 367 if params.IsCodeUpgradeInProgress(err) { 368 return errors.Errorf("%s\n\n"+ 369 "Please wait for the upgrade to complete or if there was a problem with\n"+ 370 "the last upgrade that has been resolved, consider running the\n"+ 371 "upgrade-juju command with the --reset-previous-upgrade flag.", err, 372 ) 373 } else { 374 return block.ProcessBlockedError(err, block.BlockChange) 375 } 376 } 377 fmt.Fprintf(ctx.Stdout, "started upgrade to %s\n", context.chosen) 378 } 379 return nil 380 } 381 382 func tryImplicitUpload(agentVersion version.Number) bool { 383 newerAgent := jujuversion.Current.Compare(agentVersion) > 0 384 return newerAgent || agentVersion.Build > 0 || jujuversion.Current.Build > 0 385 } 386 387 const resetPreviousUpgradeMessage = ` 388 WARNING! using --reset-previous-upgrade when an upgrade is in progress 389 will cause the upgrade to fail. Only use this option to clear an 390 incomplete upgrade where the root cause has been resolved. 391 392 Continue [y/N]? ` 393 394 func (c *upgradeJujuCommand) confirmResetPreviousUpgrade(ctx *cmd.Context) (bool, error) { 395 if c.AssumeYes { 396 return true, nil 397 } 398 fmt.Fprint(ctx.Stdout, resetPreviousUpgradeMessage) 399 scanner := bufio.NewScanner(ctx.Stdin) 400 scanner.Scan() 401 err := scanner.Err() 402 if err != nil && err != io.EOF { 403 return false, err 404 } 405 answer := strings.ToLower(scanner.Text()) 406 return answer == "y" || answer == "yes", nil 407 } 408 409 // initVersions collects state relevant to an upgrade decision. The returned 410 // agent and client versions, and the list of currently available tools, will 411 // always be accurate; the chosen version, and the flag indicating development 412 // mode, may remain blank until uploadTools or validate is called. 413 func (c *upgradeJujuCommand) initVersions(client upgradeJujuAPI, cfg *config.Config, agentVersion version.Number, filterOnPrior bool) (*upgradeContext, error) { 414 if c.Version == agentVersion { 415 return nil, errUpToDate 416 } 417 filterVersion := jujuversion.Current 418 if c.Version != version.Zero { 419 filterVersion = c.Version 420 } else if filterOnPrior { 421 // Trying to find the latest of the prior major version. 422 // TODO (cherylj) if no tools found, suggest upgrade to 423 // the current client version. 424 filterVersion.Major-- 425 } 426 logger.Debugf("searching for tools with major: %d", filterVersion.Major) 427 findResult, err := client.FindTools(filterVersion.Major, -1, "", "") 428 if err != nil { 429 return nil, err 430 } 431 err = findResult.Error 432 if findResult.Error != nil { 433 if !params.IsCodeNotFound(err) { 434 return nil, err 435 } 436 if !tryImplicitUpload(agentVersion) && !c.BuildAgent { 437 // No tools found and we shouldn't upload any, so if we are not asking for a 438 // major upgrade, pretend there is no more recent version available. 439 if c.Version == version.Zero && agentVersion.Major == filterVersion.Major { 440 return nil, errUpToDate 441 } 442 return nil, err 443 } 444 } 445 return &upgradeContext{ 446 agent: agentVersion, 447 client: jujuversion.Current, 448 chosen: c.Version, 449 tools: findResult.List, 450 apiClient: client, 451 config: cfg, 452 }, nil 453 } 454 455 // upgradeContext holds the version information for making upgrade decisions. 456 type upgradeContext struct { 457 agent version.Number 458 client version.Number 459 chosen version.Number 460 tools coretools.List 461 config *config.Config 462 apiClient upgradeJujuAPI 463 } 464 465 // uploadTools compiles jujud from $GOPATH and uploads it into the supplied 466 // storage. If no version has been explicitly chosen, the version number 467 // reported by the built tools will be based on the client version number. 468 // In any case, the version number reported will have a build component higher 469 // than that of any otherwise-matching available envtools. 470 // uploadTools resets the chosen version and replaces the available tools 471 // with the ones just uploaded. 472 func (context *upgradeContext) uploadTools(buildAgent bool) (err error) { 473 // TODO(fwereade): this is kinda crack: we should not assume that 474 // jujuversion.Current matches whatever source happens to be built. The 475 // ideal would be: 476 // 1) compile jujud from $GOPATH into some build dir 477 // 2) get actual version with `jujud version` 478 // 3) check actual version for compatibility with CLI tools 479 // 4) generate unique build version with reference to available tools 480 // 5) force-version that unique version into the dir directly 481 // 6) archive and upload the build dir 482 // ...but there's no way we have time for that now. In the meantime, 483 // considering the use cases, this should work well enough; but it 484 // won't detect an incompatible major-version change, which is a shame. 485 // 486 // TODO(cherylj) If the determination of version changes, we will 487 // need to also change the upgrade version checks in Run() that check 488 // if a major upgrade is allowed. 489 if context.chosen == version.Zero { 490 context.chosen = context.client 491 } 492 context.chosen = uploadVersion(context.chosen, context.tools) 493 494 builtTools, err := sync.BuildAgentTarball(buildAgent, &context.chosen, "upgrade") 495 if err != nil { 496 return errors.Trace(err) 497 } 498 defer os.RemoveAll(builtTools.Dir) 499 500 uploadToolsVersion := builtTools.Version 501 uploadToolsVersion.Number = context.chosen 502 toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) 503 logger.Infof("uploading agent binary %v (%dkB) to Juju controller", uploadToolsVersion, (builtTools.Size+512)/1024) 504 f, err := os.Open(toolsPath) 505 if err != nil { 506 return errors.Trace(err) 507 } 508 defer f.Close() 509 os, err := series.GetOSFromSeries(builtTools.Version.Series) 510 if err != nil { 511 return errors.Trace(err) 512 } 513 additionalSeries := series.OSSupportedSeries(os) 514 uploaded, err := context.apiClient.UploadTools(f, uploadToolsVersion, additionalSeries...) 515 if err != nil { 516 return errors.Trace(err) 517 } 518 context.tools = uploaded 519 return nil 520 } 521 522 // validate chooses an upgrade version, if one has not already been chosen, 523 // and ensures the tools list contains no entries that do not have that version. 524 // If validate returns no error, the environment agent-version can be set to 525 // the value of the chosen field. 526 func (context *upgradeContext) validate() (err error) { 527 if context.chosen == version.Zero { 528 // No explicitly specified version, so find the version to which we 529 // need to upgrade. We find next available stable release to upgrade 530 // to by incrementing the minor version, starting from the current 531 // agent version and doing major.minor+1.patch=0. 532 533 // Upgrading across a major release boundary requires that the version 534 // be specified with --agent-version. 535 nextVersion := context.agent 536 nextVersion.Minor += 1 537 nextVersion.Patch = 0 538 539 newestNextStable, found := context.tools.NewestCompatible(nextVersion) 540 if found { 541 logger.Debugf("found a more recent stable version %s", newestNextStable) 542 context.chosen = newestNextStable 543 } else { 544 newestCurrent, found := context.tools.NewestCompatible(context.agent) 545 if found { 546 logger.Debugf("found more recent current version %s", newestCurrent) 547 context.chosen = newestCurrent 548 } else { 549 if context.agent.Major != context.client.Major { 550 return errors.New("no compatible tools available") 551 } else { 552 return errors.New("no more recent supported versions available") 553 } 554 } 555 } 556 } else { 557 // If not completely specified already, pick a single tools version. 558 filter := coretools.Filter{Number: context.chosen} 559 if context.tools, err = context.tools.Match(filter); err != nil { 560 return err 561 } 562 context.chosen, context.tools = context.tools.Newest() 563 } 564 if context.chosen == context.agent { 565 return errUpToDate 566 } 567 568 // Disallow major.minor version downgrades. 569 if context.chosen.Major < context.agent.Major || 570 context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { 571 // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even 572 // *connecting* to environments with higher agent-versions; but ofc they 573 // have to connect in order to discover they shouldn't. However, once 574 // any of our tools detect an incompatible version, they should act to 575 // minimize damage: the CLI should abort politely, and the agents should 576 // run an Upgrader but no other tasks. 577 return errors.Errorf(downgradeErrMsg, context.agent, context.chosen) 578 } 579 580 return nil 581 } 582 583 // uploadVersion returns a copy of the supplied version with a build number 584 // higher than any of the supplied tools that share its major, minor and patch. 585 func uploadVersion(vers version.Number, existing coretools.List) version.Number { 586 vers.Build++ 587 for _, t := range existing { 588 if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { 589 continue 590 } 591 if t.Version.Build >= vers.Build { 592 vers.Build = t.Version.Build + 1 593 } 594 } 595 return vers 596 }