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