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  }