github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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 "launchpad.net/gnuflag" 19 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/cmd/envcmd" 22 "github.com/juju/juju/cmd/juju/block" 23 "github.com/juju/juju/environs/config" 24 "github.com/juju/juju/environs/sync" 25 coretools "github.com/juju/juju/tools" 26 "github.com/juju/juju/version" 27 ) 28 29 // UpgradeJujuCommand upgrades the agents in a juju installation. 30 type UpgradeJujuCommand struct { 31 envcmd.EnvCommandBase 32 vers string 33 Version version.Number 34 UploadTools bool 35 DryRun bool 36 ResetPrevious bool 37 AssumeYes bool 38 Series []string 39 } 40 41 var upgradeJujuDoc = ` 42 The upgrade-juju command upgrades a running environment by setting a version 43 number for all juju agents to run. By default, it chooses the most recent 44 supported version compatible with the command-line tools version. 45 46 A development version is defined to be any version with an odd minor 47 version or a nonzero build component (for example version 2.1.1, 3.3.0 48 and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A 49 development version may be chosen in two cases: 50 51 - when the current agent version is a development one and there is 52 a more recent version available with the same major.minor numbers; 53 - when an explicit --version major.minor is given (e.g. --version 1.17, 54 or 1.17.2, but not just 1) 55 56 For development use, the --upload-tools flag specifies that the juju tools will 57 packaged (or compiled locally, if no jujud binaries exists, for which you will 58 need the golang packages installed) and uploaded before the version is set. 59 Currently the tools will be uploaded as if they had the version of the current 60 juju tool, unless specified otherwise by the --version flag. 61 62 When run without arguments. upgrade-juju will try to upgrade to the 63 following versions, in order of preference, depending on the current 64 value of the environment's agent-version setting: 65 66 - The highest patch.build version of the *next* stable major.minor version. 67 - The highest patch.build version of the *current* major.minor version. 68 69 Both of these depend on tools availability, which some situations (no 70 outgoing internet access) and provider types (such as maas) require that 71 you manage yourself; see the documentation for "sync-tools". 72 73 The upgrade-juju command will abort if an upgrade is already in 74 progress. It will also abort if a previous upgrade was partially 75 completed - this can happen if one of the state servers in a high 76 availability environment failed to upgrade. If a failed upgrade has 77 been resolved, the --reset-previous-upgrade flag can be used to reset 78 the environment's upgrade tracking state, allowing further upgrades.` 79 80 func (c *UpgradeJujuCommand) Info() *cmd.Info { 81 return &cmd.Info{ 82 Name: "upgrade-juju", 83 Purpose: "upgrade the tools in a juju environment", 84 Doc: upgradeJujuDoc, 85 } 86 } 87 88 func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { 89 f.StringVar(&c.vers, "version", "", "upgrade to specific version") 90 f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools") 91 f.BoolVar(&c.DryRun, "dry-run", false, "don't change anything, just report what would change") 92 f.BoolVar(&c.ResetPrevious, "reset-previous-upgrade", false, "clear the previous (incomplete) upgrade status (use with care)") 93 f.BoolVar(&c.AssumeYes, "y", false, "answer 'yes' to confirmation prompts") 94 f.BoolVar(&c.AssumeYes, "yes", false, "") 95 f.Var(newSeriesValue(nil, &c.Series), "series", "upload tools for supplied comma-separated series list (OBSOLETE)") 96 } 97 98 func (c *UpgradeJujuCommand) Init(args []string) error { 99 if c.vers != "" { 100 vers, err := version.Parse(c.vers) 101 if err != nil { 102 return err 103 } 104 if vers.Major != version.Current.Major { 105 return fmt.Errorf("cannot upgrade to version incompatible with CLI") 106 } 107 if c.UploadTools && vers.Build != 0 { 108 // TODO(fwereade): when we start taking versions from actual built 109 // code, we should disable --version when used with --upload-tools. 110 // For now, it's the only way to experiment with version upgrade 111 // behaviour live, so the only restriction is that Build cannot 112 // be used (because its value needs to be chosen internally so as 113 // not to collide with existing tools). 114 return fmt.Errorf("cannot specify build number when uploading tools") 115 } 116 c.Version = vers 117 } 118 if len(c.Series) > 0 && !c.UploadTools { 119 return fmt.Errorf("--series requires --upload-tools") 120 } 121 return cmd.CheckEmpty(args) 122 } 123 124 var errUpToDate = stderrors.New("no upgrades available") 125 126 func formatTools(tools coretools.List) string { 127 formatted := make([]string, len(tools)) 128 for i, tools := range tools { 129 formatted[i] = fmt.Sprintf(" %s", tools.Version.String()) 130 } 131 return strings.Join(formatted, "\n") 132 } 133 134 type upgradeJujuAPI interface { 135 EnvironmentGet() (map[string]interface{}, error) 136 FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error) 137 UploadTools(r io.Reader, vers version.Binary, additionalSeries ...string) (*coretools.Tools, error) 138 AbortCurrentUpgrade() error 139 SetEnvironAgentVersion(version version.Number) error 140 Close() error 141 } 142 143 var getUpgradeJujuAPI = func(c *UpgradeJujuCommand) (upgradeJujuAPI, error) { 144 return c.NewAPIClient() 145 } 146 147 // Run changes the version proposed for the juju envtools. 148 func (c *UpgradeJujuCommand) Run(ctx *cmd.Context) (err error) { 149 if len(c.Series) > 0 { 150 fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") 151 } 152 153 client, err := getUpgradeJujuAPI(c) 154 if err != nil { 155 return err 156 } 157 defer client.Close() 158 defer func() { 159 if err == errUpToDate { 160 ctx.Infof(err.Error()) 161 err = nil 162 } 163 }() 164 165 // Determine the version to upgrade to, uploading tools if necessary. 166 attrs, err := client.EnvironmentGet() 167 if err != nil { 168 return err 169 } 170 cfg, err := config.New(config.NoDefaults, attrs) 171 if err != nil { 172 return err 173 } 174 context, err := c.initVersions(client, cfg) 175 if err != nil { 176 return err 177 } 178 if c.UploadTools && !c.DryRun { 179 if err := context.uploadTools(); err != nil { 180 return block.ProcessBlockedError(err, block.BlockChange) 181 } 182 } 183 if err := context.validate(); err != nil { 184 return err 185 } 186 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 187 ctx.Infof("available tools:\n%s", formatTools(context.tools)) 188 ctx.Infof("best version:\n %s", context.chosen) 189 if c.DryRun { 190 ctx.Infof("upgrade to this version by running\n juju upgrade-juju --version=\"%s\"\n", context.chosen) 191 } else { 192 if c.ResetPrevious { 193 if ok, err := c.confirmResetPreviousUpgrade(ctx); !ok || err != nil { 194 const message = "previous upgrade not reset and no new upgrade triggered" 195 if err != nil { 196 return errors.Annotate(err, message) 197 } 198 return errors.New(message) 199 } 200 if err := client.AbortCurrentUpgrade(); err != nil { 201 return block.ProcessBlockedError(err, block.BlockChange) 202 } 203 } 204 if err := client.SetEnvironAgentVersion(context.chosen); err != nil { 205 if params.IsCodeUpgradeInProgress(err) { 206 return errors.Errorf("%s\n\n"+ 207 "Please wait for the upgrade to complete or if there was a problem with\n"+ 208 "the last upgrade that has been resolved, consider running the\n"+ 209 "upgrade-juju command with the --reset-previous-upgrade flag.", err, 210 ) 211 } else { 212 return block.ProcessBlockedError(err, block.BlockChange) 213 } 214 } 215 logger.Infof("started upgrade to %s", context.chosen) 216 } 217 return nil 218 } 219 220 const resetPreviousUpgradeMessage = ` 221 WARNING! using --reset-previous-upgrade when an upgrade is in progress 222 will cause the upgrade to fail. Only use this option to clear an 223 incomplete upgrade where the root cause has been resolved. 224 225 Continue [y/N]? ` 226 227 func (c *UpgradeJujuCommand) confirmResetPreviousUpgrade(ctx *cmd.Context) (bool, error) { 228 if c.AssumeYes { 229 return true, nil 230 } 231 fmt.Fprintf(ctx.Stdout, resetPreviousUpgradeMessage) 232 scanner := bufio.NewScanner(ctx.Stdin) 233 scanner.Scan() 234 err := scanner.Err() 235 if err != nil && err != io.EOF { 236 return false, err 237 } 238 answer := strings.ToLower(scanner.Text()) 239 return answer == "y" || answer == "yes", nil 240 } 241 242 // initVersions collects state relevant to an upgrade decision. The returned 243 // agent and client versions, and the list of currently available tools, will 244 // always be accurate; the chosen version, and the flag indicating development 245 // mode, may remain blank until uploadTools or validate is called. 246 func (c *UpgradeJujuCommand) initVersions(client upgradeJujuAPI, cfg *config.Config) (*upgradeContext, error) { 247 agent, ok := cfg.AgentVersion() 248 if !ok { 249 // Can't happen. In theory. 250 return nil, fmt.Errorf("incomplete environment configuration") 251 } 252 if c.Version == agent { 253 return nil, errUpToDate 254 } 255 clientVersion := version.Current.Number 256 findResult, err := client.FindTools(clientVersion.Major, -1, "", "") 257 if err != nil { 258 return nil, err 259 } 260 err = findResult.Error 261 if findResult.Error != nil { 262 if !params.IsCodeNotFound(err) { 263 return nil, err 264 } 265 if !c.UploadTools { 266 // No tools found and we shouldn't upload any, so if we are not asking for a 267 // major upgrade, pretend there is no more recent version available. 268 if c.Version == version.Zero && agent.Major == clientVersion.Major { 269 return nil, errUpToDate 270 } 271 return nil, err 272 } 273 } 274 return &upgradeContext{ 275 agent: agent, 276 client: clientVersion, 277 chosen: c.Version, 278 tools: findResult.List, 279 apiClient: client, 280 config: cfg, 281 }, nil 282 } 283 284 // upgradeContext holds the version information for making upgrade decisions. 285 type upgradeContext struct { 286 agent version.Number 287 client version.Number 288 chosen version.Number 289 tools coretools.List 290 config *config.Config 291 apiClient upgradeJujuAPI 292 } 293 294 // uploadTools compiles jujud from $GOPATH and uploads it into the supplied 295 // storage. If no version has been explicitly chosen, the version number 296 // reported by the built tools will be based on the client version number. 297 // In any case, the version number reported will have a build component higher 298 // than that of any otherwise-matching available envtools. 299 // uploadTools resets the chosen version and replaces the available tools 300 // with the ones just uploaded. 301 func (context *upgradeContext) uploadTools() (err error) { 302 // TODO(fwereade): this is kinda crack: we should not assume that 303 // version.Current matches whatever source happens to be built. The 304 // ideal would be: 305 // 1) compile jujud from $GOPATH into some build dir 306 // 2) get actual version with `jujud version` 307 // 3) check actual version for compatibility with CLI tools 308 // 4) generate unique build version with reference to available tools 309 // 5) force-version that unique version into the dir directly 310 // 6) archive and upload the build dir 311 // ...but there's no way we have time for that now. In the meantime, 312 // considering the use cases, this should work well enough; but it 313 // won't detect an incompatible major-version change, which is a shame. 314 if context.chosen == version.Zero { 315 context.chosen = context.client 316 } 317 context.chosen = uploadVersion(context.chosen, context.tools) 318 319 builtTools, err := sync.BuildToolsTarball(&context.chosen, "upgrade") 320 if err != nil { 321 return errors.Trace(err) 322 } 323 defer os.RemoveAll(builtTools.Dir) 324 325 var uploaded *coretools.Tools 326 toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) 327 logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024) 328 f, err := os.Open(toolsPath) 329 if err != nil { 330 return errors.Trace(err) 331 } 332 defer f.Close() 333 os, err := series.GetOSFromSeries(builtTools.Version.Series) 334 if err != nil { 335 return errors.Trace(err) 336 } 337 additionalSeries := series.OSSupportedSeries(os) 338 uploaded, err = context.apiClient.UploadTools(f, builtTools.Version, additionalSeries...) 339 if err != nil { 340 return errors.Trace(err) 341 } 342 context.tools = coretools.List{uploaded} 343 return nil 344 } 345 346 // validate chooses an upgrade version, if one has not already been chosen, 347 // and ensures the tools list contains no entries that do not have that version. 348 // If validate returns no error, the environment agent-version can be set to 349 // the value of the chosen field. 350 func (context *upgradeContext) validate() (err error) { 351 if context.chosen == version.Zero { 352 // No explicitly specified version, so find the version to which we 353 // need to upgrade. If the CLI and agent major versions match, we find 354 // next available stable release to upgrade to by incrementing the 355 // minor version, starting from the current agent version and doing 356 // major.minor+1.patch=0. If the CLI has a greater major version, 357 // we just use the CLI version as is. 358 nextVersion := context.agent 359 if nextVersion.Major == context.client.Major { 360 nextVersion.Minor += 1 361 nextVersion.Patch = 0 362 } else { 363 nextVersion = context.client 364 } 365 366 newestNextStable, found := context.tools.NewestCompatible(nextVersion) 367 if found { 368 logger.Debugf("found a more recent stable version %s", newestNextStable) 369 context.chosen = newestNextStable 370 } else { 371 newestCurrent, found := context.tools.NewestCompatible(context.agent) 372 if found { 373 logger.Debugf("found more recent current version %s", newestCurrent) 374 context.chosen = newestCurrent 375 } else { 376 if context.agent.Major != context.client.Major { 377 return fmt.Errorf("no compatible tools available") 378 } else { 379 return fmt.Errorf("no more recent supported versions available") 380 } 381 } 382 } 383 } else { 384 // If not completely specified already, pick a single tools version. 385 filter := coretools.Filter{Number: context.chosen} 386 if context.tools, err = context.tools.Match(filter); err != nil { 387 return err 388 } 389 context.chosen, context.tools = context.tools.Newest() 390 } 391 if context.chosen == context.agent { 392 return errUpToDate 393 } 394 395 // Disallow major.minor version downgrades. 396 if context.chosen.Major < context.agent.Major || 397 context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { 398 // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even 399 // *connecting* to environments with higher agent-versions; but ofc they 400 // have to connect in order to discover they shouldn't. However, once 401 // any of our tools detect an incompatible version, they should act to 402 // minimize damage: the CLI should abort politely, and the agents should 403 // run an Upgrader but no other tasks. 404 return fmt.Errorf("cannot change version from %s to %s", context.agent, context.chosen) 405 } 406 407 return nil 408 } 409 410 // uploadVersion returns a copy of the supplied version with a build number 411 // higher than any of the supplied tools that share its major, minor and patch. 412 func uploadVersion(vers version.Number, existing coretools.List) version.Number { 413 vers.Build++ 414 for _, t := range existing { 415 if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { 416 continue 417 } 418 if t.Version.Build >= vers.Build { 419 vers.Build = t.Version.Build + 1 420 } 421 } 422 return vers 423 }