launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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/log"
    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  			log.Noticef(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  	env, err := environs.New(cfg)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	v, err := c.initVersions(cfg, env)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	if c.UploadTools {
   146  		series := getUploadSeries(cfg, c.Series)
   147  		if err := v.uploadTools(env.Storage(), series); err != nil {
   148  			return err
   149  		}
   150  	}
   151  	if err := v.validate(); err != nil {
   152  		return err
   153  	}
   154  	log.Infof("upgrade version chosen: %s", v.chosen)
   155  	// TODO(fwereade): this list may be incomplete, pending envtools.Upload change.
   156  	log.Infof("available tools: %s", v.tools)
   157  
   158  	if err := client.SetEnvironAgentVersion(v.chosen); err != nil {
   159  		return err
   160  	}
   161  	log.Noticef("started upgrade to %s", v.chosen)
   162  	return nil
   163  }
   164  
   165  // run1dot16 implements the command without access to the API. This is
   166  // needed for compatibility, so 1.16 can be upgraded to newer
   167  // releases. It should be removed in 1.18.
   168  func (c *UpgradeJujuCommand) run1dot16() error {
   169  	log.Warningf("running in 1.16 compatibility mode")
   170  	conn, err := juju.NewConnFromName(c.EnvName)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	defer conn.Close()
   175  	defer func() {
   176  		if err == errUpToDate {
   177  			log.Noticef(err.Error())
   178  			err = nil
   179  		}
   180  	}()
   181  
   182  	// Determine the version to upgrade to, uploading tools if necessary.
   183  	env := conn.Environ
   184  	cfg, err := conn.State.EnvironConfig()
   185  	if err != nil {
   186  		return err
   187  	}
   188  	v, err := c.initVersions(cfg, env)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	if c.UploadTools {
   193  		series := getUploadSeries(cfg, c.Series)
   194  		if err := v.uploadTools(env.Storage(), series); err != nil {
   195  			return err
   196  		}
   197  	}
   198  	if err := v.validate(); err != nil {
   199  		return err
   200  	}
   201  	log.Infof("upgrade version chosen: %s", v.chosen)
   202  	// TODO(fwereade): this list may be incomplete, pending envtools.Upload change.
   203  	log.Infof("available tools: %s", v.tools)
   204  
   205  	if err := conn.State.SetEnvironAgentVersion(v.chosen); err != nil {
   206  		return err
   207  	}
   208  	log.Noticef("started upgrade to %s", v.chosen)
   209  	return nil
   210  }
   211  
   212  // initVersions collects state relevant to an upgrade decision. The returned
   213  // agent and client versions, and the list of currently available tools, will
   214  // always be accurate; the chosen version, and the flag indicating development
   215  // mode, may remain blank until uploadTools or validate is called.
   216  func (c *UpgradeJujuCommand) initVersions(cfg *config.Config, env environs.Environ) (*upgradeVersions, error) {
   217  	agent, ok := cfg.AgentVersion()
   218  	if !ok {
   219  		// Can't happen. In theory.
   220  		return nil, fmt.Errorf("incomplete environment configuration")
   221  	}
   222  	if c.Version == agent {
   223  		return nil, errUpToDate
   224  	}
   225  	client := version.Current.Number
   226  	// TODO use an API call rather than requiring the environment,
   227  	// so that we can restrict access to the provider secrets
   228  	// while still allowing users to upgrade.
   229  	available, err := envtools.FindTools(env, client.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry)
   230  	if err != nil {
   231  		if !errors.IsNotFoundError(err) {
   232  			return nil, err
   233  		}
   234  		if !c.UploadTools {
   235  			// No tools found and we shouldn't upload any, so pretend
   236  			// there is no more recent version available.
   237  			if c.Version == version.Zero {
   238  				return nil, errUpToDate
   239  			}
   240  			return nil, err
   241  		}
   242  	}
   243  	return &upgradeVersions{
   244  		agent:  agent,
   245  		client: client,
   246  		chosen: c.Version,
   247  		tools:  available,
   248  	}, nil
   249  }
   250  
   251  // upgradeVersions holds the version information for making upgrade decisions.
   252  type upgradeVersions struct {
   253  	agent  version.Number
   254  	client version.Number
   255  	chosen version.Number
   256  	tools  coretools.List
   257  }
   258  
   259  // uploadTools compiles jujud from $GOPATH and uploads it into the supplied
   260  // storage. If no version has been explicitly chosen, the version number
   261  // reported by the built tools will be based on the client version number.
   262  // In any case, the version number reported will have a build component higher
   263  // than that of any otherwise-matching available envtools.
   264  // uploadTools resets the chosen version and replaces the available tools
   265  // with the ones just uploaded.
   266  func (v *upgradeVersions) uploadTools(storage storage.Storage, series []string) error {
   267  	// TODO(fwereade): this is kinda crack: we should not assume that
   268  	// version.Current matches whatever source happens to be built. The
   269  	// ideal would be:
   270  	//  1) compile jujud from $GOPATH into some build dir
   271  	//  2) get actual version with `jujud version`
   272  	//  3) check actual version for compatibility with CLI tools
   273  	//  4) generate unique build version with reference to available tools
   274  	//  5) force-version that unique version into the dir directly
   275  	//  6) archive and upload the build dir
   276  	// ...but there's no way we have time for that now. In the meantime,
   277  	// considering the use cases, this should work well enough; but it
   278  	// won't detect an incompatible major-version change, which is a shame.
   279  	if v.chosen == version.Zero {
   280  		v.chosen = v.client
   281  	}
   282  	v.chosen = uploadVersion(v.chosen, v.tools)
   283  
   284  	// TODO(fwereade): envtools.Upload should return envtools.List, and should
   285  	// include all the extra series we build, so we can set *that* onto
   286  	// v.available and maybe one day be able to check that a given upgrade
   287  	// won't leave out-of-date machines lying around, starved of envtools.
   288  	uploaded, err := sync.Upload(storage, &v.chosen, series...)
   289  	if err != nil {
   290  		return err
   291  	}
   292  	v.tools = coretools.List{uploaded}
   293  	return nil
   294  }
   295  
   296  // validate chooses an upgrade version, if one has not already been chosen,
   297  // and ensures the tools list contains no entries that do not have that version.
   298  // If validate returns no error, the environment agent-version can be set to
   299  // the value of the chosen field.
   300  func (v *upgradeVersions) validate() (err error) {
   301  	if v.chosen == version.Zero {
   302  		// No explicitly specified version, so find the next available
   303  		// stable release to upgrade to, starting from the current agent
   304  		// version and doing major.minor+1 or +2 as needed.
   305  		nextStable := v.agent
   306  		if v.agent.IsDev() {
   307  			nextStable.Minor += 1
   308  		} else {
   309  			nextStable.Minor += 2
   310  		}
   311  
   312  		newestNextStable, found := v.tools.NewestCompatible(nextStable)
   313  		if found {
   314  			log.Debugf("found a more recent stable version %s", newestNextStable)
   315  			v.chosen = newestNextStable
   316  		} else {
   317  			newestCurrent, found := v.tools.NewestCompatible(v.agent)
   318  			if found {
   319  				log.Debugf("found more recent current version %s", newestCurrent)
   320  				v.chosen = newestCurrent
   321  			} else {
   322  				return fmt.Errorf("no more recent supported versions available")
   323  			}
   324  		}
   325  	} else {
   326  		// If not completely specified already, pick a single tools version.
   327  		filter := coretools.Filter{Number: v.chosen, Released: !v.chosen.IsDev()}
   328  		if v.tools, err = v.tools.Match(filter); err != nil {
   329  			return err
   330  		}
   331  		v.chosen, v.tools = v.tools.Newest()
   332  	}
   333  	if v.chosen == v.agent {
   334  		return errUpToDate
   335  	}
   336  
   337  	// Major version upgrade
   338  	if v.chosen.Major < v.agent.Major {
   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 major version from %d to %d", v.agent.Major, v.chosen.Major)
   346  	} else if v.chosen.Major > v.agent.Major {
   347  		return fmt.Errorf("major version upgrades are not supported yet")
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  // uploadVersion returns a copy of the supplied version with a build number
   354  // higher than any of the supplied tools that share its major, minor and patch.
   355  func uploadVersion(vers version.Number, existing coretools.List) version.Number {
   356  	vers.Build++
   357  	for _, t := range existing {
   358  		if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch {
   359  			continue
   360  		}
   361  		if t.Version.Build >= vers.Build {
   362  			vers.Build = t.Version.Build + 1
   363  		}
   364  	}
   365  	return vers
   366  }