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