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  }