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  }