github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/cmd/juju/upgradejuju.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	stderrors "errors"
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  
    12  	"github.com/juju/cmd"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/cmd/envcmd"
    16  	"github.com/juju/juju/environs"
    17  	"github.com/juju/juju/environs/bootstrap"
    18  	"github.com/juju/juju/environs/config"
    19  	"github.com/juju/juju/environs/sync"
    20  	envtools "github.com/juju/juju/environs/tools"
    21  	"github.com/juju/juju/juju"
    22  	"github.com/juju/juju/state/api"
    23  	"github.com/juju/juju/state/api/params"
    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  	Series      []string
    35  }
    36  
    37  var upgradeJujuDoc = `
    38  The upgrade-juju command upgrades a running environment by setting a version
    39  number for all juju agents to run. By default, it chooses the most recent
    40  supported version compatible with the command-line tools version.
    41  
    42  A development version is defined to be any version with an odd minor
    43  version or a nonzero build component (for example version 2.1.1, 3.3.0
    44  and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A
    45  development version may be chosen in two cases:
    46  
    47   - when the current agent version is a development one and there is
    48     a more recent version available with the same major.minor numbers;
    49   - when an explicit --version major.minor is given (e.g. --version 1.17,
    50     or 1.17.2, but not just 1)
    51  
    52  For development use, the --upload-tools flag specifies that the juju tools will
    53  packaged (or compiled locally, if no jujud binaries exists, for which you will
    54  need the golang packages installed) and uploaded before the version is set.
    55  Currently the tools will be uploaded as if they had the version of the current
    56  juju tool, unless specified otherwise by the --version flag.
    57  
    58  When run without arguments. upgrade-juju will try to upgrade to the
    59  following versions, in order of preference, depending on the current
    60  value of the environment's agent-version setting:
    61  
    62   - The highest patch.build version of the *next* stable major.minor version.
    63   - The highest patch.build version of the *current* major.minor version.
    64  
    65  Both of these depend on tools availability, which some situations (no
    66  outgoing internet access) and provider types (such as maas) require that
    67  you manage yourself; see the documentation for "sync-tools".
    68  `
    69  
    70  func (c *UpgradeJujuCommand) Info() *cmd.Info {
    71  	return &cmd.Info{
    72  		Name:    "upgrade-juju",
    73  		Purpose: "upgrade the tools in a juju environment",
    74  		Doc:     upgradeJujuDoc,
    75  	}
    76  }
    77  
    78  func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) {
    79  	f.StringVar(&c.vers, "version", "", "upgrade to specific version")
    80  	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools")
    81  	f.Var(newSeriesValue(nil, &c.Series), "series", "upload tools for supplied comma-separated series list")
    82  }
    83  
    84  func (c *UpgradeJujuCommand) Init(args []string) error {
    85  	if c.vers != "" {
    86  		vers, err := version.Parse(c.vers)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		if vers.Major != version.Current.Major {
    91  			return fmt.Errorf("cannot upgrade to version incompatible with CLI")
    92  		}
    93  		if c.UploadTools && vers.Build != 0 {
    94  			// TODO(fwereade): when we start taking versions from actual built
    95  			// code, we should disable --version when used with --upload-tools.
    96  			// For now, it's the only way to experiment with version upgrade
    97  			// behaviour live, so the only restriction is that Build cannot
    98  			// be used (because its value needs to be chosen internally so as
    99  			// not to collide with existing tools).
   100  			return fmt.Errorf("cannot specify build number when uploading tools")
   101  		}
   102  		c.Version = vers
   103  	}
   104  	if len(c.Series) > 0 && !c.UploadTools {
   105  		return fmt.Errorf("--series requires --upload-tools")
   106  	}
   107  	return cmd.CheckEmpty(args)
   108  }
   109  
   110  var errUpToDate = stderrors.New("no upgrades available")
   111  
   112  // Run changes the version proposed for the juju envtools.
   113  func (c *UpgradeJujuCommand) Run(_ *cmd.Context) (err error) {
   114  	client, err := juju.NewAPIClientFromName(c.EnvName)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	defer client.Close()
   119  	defer func() {
   120  		if err == errUpToDate {
   121  			logger.Infof(err.Error())
   122  			err = nil
   123  		}
   124  	}()
   125  
   126  	// Determine the version to upgrade to, uploading tools if necessary.
   127  	attrs, err := client.EnvironmentGet()
   128  	if err != nil {
   129  		return err
   130  	}
   131  	cfg, err := config.New(config.NoDefaults, attrs)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	context, err := c.initVersions(client, cfg)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	if c.UploadTools {
   140  		series := bootstrap.SeriesToUpload(cfg, c.Series)
   141  		if err := context.uploadTools(series); err != nil {
   142  			return err
   143  		}
   144  	}
   145  	if err := context.validate(); err != nil {
   146  		return err
   147  	}
   148  	logger.Infof("upgrade version chosen: %s", context.chosen)
   149  	// TODO(fwereade): this list may be incomplete, pending envtools.Upload change.
   150  	logger.Infof("available tools: %s", context.tools)
   151  
   152  	if err := client.SetEnvironAgentVersion(context.chosen); err != nil {
   153  		return err
   154  	}
   155  	logger.Infof("started upgrade to %s", context.chosen)
   156  	return nil
   157  }
   158  
   159  // initVersions collects state relevant to an upgrade decision. The returned
   160  // agent and client versions, and the list of currently available tools, will
   161  // always be accurate; the chosen version, and the flag indicating development
   162  // mode, may remain blank until uploadTools or validate is called.
   163  func (c *UpgradeJujuCommand) initVersions(client *api.Client, cfg *config.Config) (*upgradeContext, error) {
   164  	agent, ok := cfg.AgentVersion()
   165  	if !ok {
   166  		// Can't happen. In theory.
   167  		return nil, fmt.Errorf("incomplete environment configuration")
   168  	}
   169  	if c.Version == agent {
   170  		return nil, errUpToDate
   171  	}
   172  	clientVersion := version.Current.Number
   173  	findResult, err := client.FindTools(clientVersion.Major, -1, "", "")
   174  	var availableTools coretools.List
   175  	if params.IsCodeNotImplemented(err) {
   176  		availableTools, err = findTools1dot17(cfg)
   177  	} else {
   178  		availableTools = findResult.List
   179  	}
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	err = findResult.Error
   184  	if findResult.Error != nil {
   185  		if !params.IsCodeNotFound(err) {
   186  			return nil, err
   187  		}
   188  		if !c.UploadTools {
   189  			// No tools found and we shouldn't upload any, so if we are not asking for a
   190  			// major upgrade, pretend there is no more recent version available.
   191  			if c.Version == version.Zero && agent.Major == clientVersion.Major {
   192  				return nil, errUpToDate
   193  			}
   194  			return nil, err
   195  		}
   196  	}
   197  	return &upgradeContext{
   198  		agent:     agent,
   199  		client:    clientVersion,
   200  		chosen:    c.Version,
   201  		tools:     availableTools,
   202  		apiClient: client,
   203  		config:    cfg,
   204  	}, nil
   205  }
   206  
   207  // findTools1dot17 allows 1.17.x versions to be upgraded.
   208  func findTools1dot17(cfg *config.Config) (coretools.List, error) {
   209  	logger.Warningf("running find tools in 1.17 compatibility mode")
   210  	env, err := environs.New(cfg)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	clientVersion := version.Current.Number
   215  	return envtools.FindTools(env, clientVersion.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry)
   216  }
   217  
   218  // upgradeContext holds the version information for making upgrade decisions.
   219  type upgradeContext struct {
   220  	agent     version.Number
   221  	client    version.Number
   222  	chosen    version.Number
   223  	tools     coretools.List
   224  	config    *config.Config
   225  	apiClient *api.Client
   226  }
   227  
   228  // uploadTools compiles jujud from $GOPATH and uploads it into the supplied
   229  // storage. If no version has been explicitly chosen, the version number
   230  // reported by the built tools will be based on the client version number.
   231  // In any case, the version number reported will have a build component higher
   232  // than that of any otherwise-matching available envtools.
   233  // uploadTools resets the chosen version and replaces the available tools
   234  // with the ones just uploaded.
   235  func (context *upgradeContext) uploadTools(series []string) (err error) {
   236  	// TODO(fwereade): this is kinda crack: we should not assume that
   237  	// version.Current matches whatever source happens to be built. The
   238  	// ideal would be:
   239  	//  1) compile jujud from $GOPATH into some build dir
   240  	//  2) get actual version with `jujud version`
   241  	//  3) check actual version for compatibility with CLI tools
   242  	//  4) generate unique build version with reference to available tools
   243  	//  5) force-version that unique version into the dir directly
   244  	//  6) archive and upload the build dir
   245  	// ...but there's no way we have time for that now. In the meantime,
   246  	// considering the use cases, this should work well enough; but it
   247  	// won't detect an incompatible major-version change, which is a shame.
   248  	if context.chosen == version.Zero {
   249  		context.chosen = context.client
   250  	}
   251  	context.chosen = uploadVersion(context.chosen, context.tools)
   252  
   253  	builtTools, err := sync.BuildToolsTarball(&context.chosen)
   254  	if err != nil {
   255  		return err
   256  	}
   257  	defer os.RemoveAll(builtTools.Dir)
   258  
   259  	var uploaded *coretools.Tools
   260  	toolsPath := path.Join(builtTools.Dir, builtTools.StorageName)
   261  	logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024)
   262  	uploaded, err = context.apiClient.UploadTools(toolsPath, builtTools.Version, series...)
   263  	if params.IsCodeNotImplemented(err) {
   264  		uploaded, err = context.uploadTools1dot17(builtTools, series...)
   265  	}
   266  	if err != nil {
   267  		return err
   268  	}
   269  	context.tools = coretools.List{uploaded}
   270  	return nil
   271  }
   272  
   273  func (context *upgradeContext) uploadTools1dot17(builtTools *sync.BuiltTools,
   274  	series ...string) (*coretools.Tools, error) {
   275  
   276  	logger.Warningf("running upload tools in 1.17 compatibility mode")
   277  	env, err := environs.New(context.config)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	return sync.SyncBuiltTools(env.Storage(), builtTools, series...)
   282  }
   283  
   284  // validate chooses an upgrade version, if one has not already been chosen,
   285  // and ensures the tools list contains no entries that do not have that version.
   286  // If validate returns no error, the environment agent-version can be set to
   287  // the value of the chosen field.
   288  func (context *upgradeContext) validate() (err error) {
   289  	if context.chosen == version.Zero {
   290  		// No explicitly specified version, so find the version to which we
   291  		// need to upgrade. If the CLI and agent major versions match, we find
   292  		// next available stable release to upgrade to by incrementing the
   293  		// minor version, starting from the current agent version and doing
   294  		// major.minor+1 or +2 as needed. If the CLI has a greater major version,
   295  		// we just use the CLI version as is.
   296  		nextVersion := context.agent
   297  		if nextVersion.Major == context.client.Major {
   298  			if context.agent.IsDev() {
   299  				nextVersion.Minor += 1
   300  			} else {
   301  				nextVersion.Minor += 2
   302  			}
   303  		} else {
   304  			nextVersion = context.client
   305  		}
   306  
   307  		newestNextStable, found := context.tools.NewestCompatible(nextVersion)
   308  		if found {
   309  			logger.Debugf("found a more recent stable version %s", newestNextStable)
   310  			context.chosen = newestNextStable
   311  		} else {
   312  			newestCurrent, found := context.tools.NewestCompatible(context.agent)
   313  			if found {
   314  				logger.Debugf("found more recent current version %s", newestCurrent)
   315  				context.chosen = newestCurrent
   316  			} else {
   317  				if context.agent.Major != context.client.Major {
   318  					return fmt.Errorf("no compatible tools available")
   319  				} else {
   320  					return fmt.Errorf("no more recent supported versions available")
   321  				}
   322  			}
   323  		}
   324  	} else {
   325  		// If not completely specified already, pick a single tools version.
   326  		filter := coretools.Filter{Number: context.chosen, Released: !context.chosen.IsDev()}
   327  		if context.tools, err = context.tools.Match(filter); err != nil {
   328  			return err
   329  		}
   330  		context.chosen, context.tools = context.tools.Newest()
   331  	}
   332  	if context.chosen == context.agent {
   333  		return errUpToDate
   334  	}
   335  
   336  	// Disallow major.minor version downgrades.
   337  	if context.chosen.Major < context.agent.Major ||
   338  		context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor {
   339  		// TODO(fwereade): I'm a bit concerned about old agent/CLI tools even
   340  		// *connecting* to environments with higher agent-versions; but ofc they
   341  		// have to connect in order to discover they shouldn't. However, once
   342  		// any of our tools detect an incompatible version, they should act to
   343  		// minimize damage: the CLI should abort politely, and the agents should
   344  		// run an Upgrader but no other tasks.
   345  		return fmt.Errorf("cannot change version from %s to %s", context.agent, context.chosen)
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  // uploadVersion returns a copy of the supplied version with a build number
   352  // higher than any of the supplied tools that share its major, minor and patch.
   353  func uploadVersion(vers version.Number, existing coretools.List) version.Number {
   354  	vers.Build++
   355  	for _, t := range existing {
   356  		if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch {
   357  			continue
   358  		}
   359  		if t.Version.Build >= vers.Build {
   360  			vers.Build = t.Version.Build + 1
   361  		}
   362  	}
   363  	return vers
   364  }