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