github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/commands/upgradejuju_test.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  	"archive/tar"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"io"
    11  	"io/ioutil"
    12  	"strings"
    13  
    14  	jc "github.com/juju/testing/checkers"
    15  	"github.com/juju/utils/arch"
    16  	"github.com/juju/utils/series"
    17  	"github.com/juju/version"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/apiserver/common"
    21  	"github.com/juju/juju/apiserver/params"
    22  	apiservertesting "github.com/juju/juju/apiserver/testing"
    23  	cmdcommon "github.com/juju/juju/cmd/juju/common"
    24  	"github.com/juju/juju/cmd/modelcmd"
    25  	"github.com/juju/juju/environs/filestorage"
    26  	"github.com/juju/juju/environs/sync"
    27  	envtesting "github.com/juju/juju/environs/testing"
    28  	"github.com/juju/juju/environs/tools"
    29  	toolstesting "github.com/juju/juju/environs/tools/testing"
    30  	jujutesting "github.com/juju/juju/juju/testing"
    31  	"github.com/juju/juju/network"
    32  	_ "github.com/juju/juju/provider/dummy"
    33  	"github.com/juju/juju/state"
    34  	coretesting "github.com/juju/juju/testing"
    35  	coretools "github.com/juju/juju/tools"
    36  	jujuversion "github.com/juju/juju/version"
    37  )
    38  
    39  type UpgradeJujuSuite struct {
    40  	jujutesting.JujuConnSuite
    41  
    42  	resources  *common.Resources
    43  	authoriser apiservertesting.FakeAuthorizer
    44  
    45  	toolsDir string
    46  	cmdcommon.CmdBlockHelper
    47  }
    48  
    49  func (s *UpgradeJujuSuite) SetUpTest(c *gc.C) {
    50  	s.JujuConnSuite.SetUpTest(c)
    51  	s.resources = common.NewResources()
    52  	s.authoriser = apiservertesting.FakeAuthorizer{
    53  		Tag: s.AdminUserTag(c),
    54  	}
    55  
    56  	s.CmdBlockHelper = cmdcommon.NewCmdBlockHelper(s.APIState)
    57  	c.Assert(s.CmdBlockHelper, gc.NotNil)
    58  	s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() })
    59  }
    60  
    61  var _ = gc.Suite(&UpgradeJujuSuite{})
    62  
    63  var upgradeJujuTests = []struct {
    64  	about          string
    65  	tools          []string
    66  	currentVersion string
    67  	agentVersion   string
    68  
    69  	args           []string
    70  	expectInitErr  string
    71  	expectErr      string
    72  	expectVersion  string
    73  	expectUploaded []string
    74  	upgradeMap     map[int]version.Number
    75  }{{
    76  	about:          "unwanted extra argument",
    77  	currentVersion: "1.0.0-quantal-amd64",
    78  	args:           []string{"foo"},
    79  	expectInitErr:  "unrecognized args:.*",
    80  }, {
    81  	about:          "removed arg --dev specified",
    82  	currentVersion: "1.0.0-quantal-amd64",
    83  	args:           []string{"--dev"},
    84  	expectInitErr:  "flag provided but not defined: --dev",
    85  }, {
    86  	about:          "invalid --version value",
    87  	currentVersion: "1.0.0-quantal-amd64",
    88  	args:           []string{"--version", "invalid-version"},
    89  	expectInitErr:  "invalid version .*",
    90  }, {
    91  	about:          "just major version, no minor specified",
    92  	currentVersion: "4.2.0-quantal-amd64",
    93  	args:           []string{"--version", "4"},
    94  	expectInitErr:  `invalid version "4"`,
    95  }, {
    96  	about:          "major version upgrade to incompatible version",
    97  	currentVersion: "2.0.0-quantal-amd64",
    98  	agentVersion:   "2.0.0",
    99  	args:           []string{"--version", "5.2.0"},
   100  	expectErr:      `unknown version "5.2.0"`,
   101  }, {
   102  	about:          "major version downgrade to incompatible version",
   103  	currentVersion: "4.2.0-quantal-amd64",
   104  	agentVersion:   "4.2.0",
   105  	args:           []string{"--version", "3.2.0"},
   106  	expectErr:      "cannot change version from 4.2.0 to 3.2.0",
   107  }, {
   108  	about:          "--upload-tools with inappropriate version 1",
   109  	currentVersion: "4.2.0-quantal-amd64",
   110  	agentVersion:   "4.2.0",
   111  	args:           []string{"--upload-tools", "--version", "3.1.0"},
   112  	expectErr:      "cannot change version from 4.2.0 to 3.1.0",
   113  }, {
   114  	about:          "--upload-tools with inappropriate version 2",
   115  	currentVersion: "3.2.7-quantal-amd64",
   116  	args:           []string{"--upload-tools", "--version", "3.2.8.4"},
   117  	expectInitErr:  "cannot specify build number when uploading tools",
   118  }, {
   119  	about:          "latest supported stable release",
   120  	tools:          []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64"},
   121  	currentVersion: "2.0.0-quantal-amd64",
   122  	agentVersion:   "2.0.0",
   123  	expectVersion:  "2.1.3",
   124  }, {
   125  	about:          "latest current release",
   126  	tools:          []string{"2.0.5-quantal-amd64", "2.0.1-quantal-i386", "2.3.3-quantal-amd64"},
   127  	currentVersion: "2.0.0-quantal-amd64",
   128  	agentVersion:   "2.0.0",
   129  	expectVersion:  "2.0.5",
   130  }, {
   131  	about:          "latest current release matching CLI, major version, no matching major tools",
   132  	tools:          []string{"2.8.2-quantal-amd64"},
   133  	currentVersion: "3.0.2-quantal-amd64",
   134  	agentVersion:   "2.8.2",
   135  	expectVersion:  "2.8.2",
   136  }, {
   137  	about:          "latest current release matching CLI, major version, no matching tools",
   138  	tools:          []string{"3.3.0-quantal-amd64"},
   139  	currentVersion: "3.0.2-quantal-amd64",
   140  	agentVersion:   "2.8.2",
   141  	expectVersion:  "2.8.2",
   142  }, {
   143  	about:          "no next supported available",
   144  	tools:          []string{"2.2.0-quantal-amd64", "2.2.5-quantal-i386", "2.3.3-quantal-amd64", "2.1-dev1-quantal-amd64"},
   145  	currentVersion: "2.0.0-quantal-amd64",
   146  	agentVersion:   "2.0.0",
   147  	expectErr:      "no more recent supported versions available",
   148  }, {
   149  	about:          "latest supported stable, when client is dev",
   150  	tools:          []string{"2.1-dev1-quantal-amd64", "2.1.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"},
   151  	currentVersion: "2.1-dev0-quantal-amd64",
   152  	agentVersion:   "2.0.0",
   153  	expectVersion:  "2.1.0",
   154  }, {
   155  	about:          "latest current, when agent is dev",
   156  	tools:          []string{"2.1-dev1-quantal-amd64", "2.2.0-quantal-amd64", "2.3-dev0-quantal-amd64", "3.0.1-quantal-amd64"},
   157  	currentVersion: "2.0.0-quantal-amd64",
   158  	agentVersion:   "2.1-dev0",
   159  	expectVersion:  "2.2.0",
   160  }, {
   161  	about:          "specified version",
   162  	tools:          []string{"2.3-dev0-quantal-amd64"},
   163  	currentVersion: "2.0.0-quantal-amd64",
   164  	agentVersion:   "2.0.0",
   165  	args:           []string{"--version", "2.3-dev0"},
   166  	expectVersion:  "2.3-dev0",
   167  }, {
   168  	about:          "specified major version",
   169  	tools:          []string{"3.0.2-quantal-amd64"},
   170  	currentVersion: "3.0.2-quantal-amd64",
   171  	agentVersion:   "2.8.2",
   172  	args:           []string{"--version", "3.0.2"},
   173  	expectVersion:  "3.0.2",
   174  	upgradeMap:     map[int]version.Number{3: version.MustParse("2.8.2")},
   175  }, {
   176  	about:          "specified version missing, but already set",
   177  	currentVersion: "3.0.0-quantal-amd64",
   178  	agentVersion:   "3.0.0",
   179  	args:           []string{"--version", "3.0.0"},
   180  	expectVersion:  "3.0.0",
   181  }, {
   182  	about:          "specified version, no tools",
   183  	currentVersion: "3.0.0-quantal-amd64",
   184  	agentVersion:   "3.0.0",
   185  	args:           []string{"--version", "3.2.0"},
   186  	expectErr:      "no tools available",
   187  }, {
   188  	about:          "specified version, no matching major version",
   189  	tools:          []string{"4.2.0-quantal-amd64"},
   190  	currentVersion: "3.0.0-quantal-amd64",
   191  	agentVersion:   "3.0.0",
   192  	args:           []string{"--version", "3.2.0"},
   193  	expectErr:      "no matching tools available",
   194  }, {
   195  	about:          "specified version, no matching minor version",
   196  	tools:          []string{"3.4.0-quantal-amd64"},
   197  	currentVersion: "3.0.0-quantal-amd64",
   198  	agentVersion:   "3.0.0",
   199  	args:           []string{"--version", "3.2.0"},
   200  	expectErr:      "no matching tools available",
   201  }, {
   202  	about:          "specified version, no matching patch version",
   203  	tools:          []string{"3.2.5-quantal-amd64"},
   204  	currentVersion: "3.0.0-quantal-amd64",
   205  	agentVersion:   "3.0.0",
   206  	args:           []string{"--version", "3.2.0"},
   207  	expectErr:      "no matching tools available",
   208  }, {
   209  	about:          "specified version, no matching build version",
   210  	tools:          []string{"3.2.0.2-quantal-amd64"},
   211  	currentVersion: "3.0.0-quantal-amd64",
   212  	agentVersion:   "3.0.0",
   213  	args:           []string{"--version", "3.2.0"},
   214  	expectErr:      "no matching tools available",
   215  }, {
   216  	about:          "incompatible version (minor != 0)",
   217  	tools:          []string{"3.2.0-quantal-amd64"},
   218  	currentVersion: "4.2.0-quantal-amd64",
   219  	agentVersion:   "3.2.0",
   220  	args:           []string{"--version", "3.2.0"},
   221  	expectErr:      "cannot upgrade a 3.2.0 model with a 4.2.0 client",
   222  }, {
   223  	about:          "incompatible version (model major > client major)",
   224  	tools:          []string{"3.2.0-quantal-amd64"},
   225  	currentVersion: "3.2.0-quantal-amd64",
   226  	agentVersion:   "4.2.0",
   227  	args:           []string{"--version", "3.2.0"},
   228  	expectErr:      "cannot upgrade a 4.2.0 model with a 3.2.0 client",
   229  }, {
   230  	about:          "incompatible version (model major < client major - 1)",
   231  	tools:          []string{"3.2.0-quantal-amd64"},
   232  	currentVersion: "4.0.2-quantal-amd64",
   233  	agentVersion:   "2.0.0",
   234  	args:           []string{"--version", "3.2.0"},
   235  	expectErr:      "cannot upgrade a 2.0.0 model with a 4.0.2 client",
   236  }, {
   237  	about:          "minor version downgrade to incompatible version",
   238  	tools:          []string{"3.2.0-quantal-amd64"},
   239  	currentVersion: "3.2.0-quantal-amd64",
   240  	agentVersion:   "3.3-dev0",
   241  	args:           []string{"--version", "3.2.0"},
   242  	expectErr:      "cannot change version from 3.3-dev0 to 3.2.0",
   243  }, {
   244  	about:          "nothing available",
   245  	currentVersion: "2.0.0-quantal-amd64",
   246  	agentVersion:   "2.0.0",
   247  	expectVersion:  "2.0.0",
   248  }, {
   249  	about:          "nothing available 2",
   250  	currentVersion: "2.0.0-quantal-amd64",
   251  	tools:          []string{"3.2.0-quantal-amd64"},
   252  	agentVersion:   "2.0.0",
   253  	expectVersion:  "2.0.0",
   254  }, {
   255  	about:          "upload with default series",
   256  	currentVersion: "2.2.0-quantal-amd64",
   257  	agentVersion:   "2.0.0",
   258  	args:           []string{"--upload-tools"},
   259  	expectVersion:  "2.2.0.1",
   260  	expectUploaded: []string{"2.2.0.1-quantal-amd64", "2.2.0.1-%LTS%-amd64", "2.2.0.1-raring-amd64"},
   261  }, {
   262  	about:          "upload with explicit version",
   263  	currentVersion: "2.2.0-quantal-amd64",
   264  	agentVersion:   "2.0.0",
   265  	args:           []string{"--upload-tools", "--version", "2.7.3"},
   266  	expectVersion:  "2.7.3.1",
   267  	expectUploaded: []string{"2.7.3.1-quantal-amd64", "2.7.3.1-%LTS%-amd64", "2.7.3.1-raring-amd64"},
   268  }, {
   269  	about:          "upload dev version, currently on release version",
   270  	currentVersion: "2.1.0-quantal-amd64",
   271  	agentVersion:   "2.0.0",
   272  	args:           []string{"--upload-tools"},
   273  	expectVersion:  "2.1.0.1",
   274  	expectUploaded: []string{"2.1.0.1-quantal-amd64", "2.1.0.1-%LTS%-amd64", "2.1.0.1-raring-amd64"},
   275  }, {
   276  	about:          "upload bumps version when necessary",
   277  	tools:          []string{"2.4.6-quantal-amd64", "2.4.8-quantal-amd64"},
   278  	currentVersion: "2.4.6-quantal-amd64",
   279  	agentVersion:   "2.4.0",
   280  	args:           []string{"--upload-tools"},
   281  	expectVersion:  "2.4.6.1",
   282  	expectUploaded: []string{"2.4.6.1-quantal-amd64", "2.4.6.1-%LTS%-amd64", "2.4.6.1-raring-amd64"},
   283  }, {
   284  	about:          "upload re-bumps version when necessary",
   285  	tools:          []string{"2.4.6-quantal-amd64", "2.4.6.2-saucy-i386", "2.4.8-quantal-amd64"},
   286  	currentVersion: "2.4.6-quantal-amd64",
   287  	agentVersion:   "2.4.6.2",
   288  	args:           []string{"--upload-tools"},
   289  	expectVersion:  "2.4.6.3",
   290  	expectUploaded: []string{"2.4.6.3-quantal-amd64", "2.4.6.3-%LTS%-amd64", "2.4.6.3-raring-amd64"},
   291  }, {
   292  	about:          "upload with explicit version bumps when necessary",
   293  	currentVersion: "2.2.0-quantal-amd64",
   294  	tools:          []string{"2.7.3.1-quantal-amd64"},
   295  	agentVersion:   "2.0.0",
   296  	args:           []string{"--upload-tools", "--version", "2.7.3"},
   297  	expectVersion:  "2.7.3.2",
   298  	expectUploaded: []string{"2.7.3.2-quantal-amd64", "2.7.3.2-%LTS%-amd64", "2.7.3.2-raring-amd64"},
   299  }, {
   300  	about:          "latest supported stable release",
   301  	tools:          []string{"1.21.3-quantal-amd64", "1.22.1-quantal-amd64"},
   302  	currentVersion: "1.22.1-quantal-amd64",
   303  	agentVersion:   "1.20.14",
   304  	expectVersion:  "1.21.3",
   305  }}
   306  
   307  func (s *UpgradeJujuSuite) TestUpgradeJuju(c *gc.C) {
   308  	for i, test := range upgradeJujuTests {
   309  		c.Logf("\ntest %d: %s", i, test.about)
   310  		s.Reset(c)
   311  		tools.DefaultBaseURL = ""
   312  
   313  		// Set up apparent CLI version and initialize the command.
   314  		current := version.MustParseBinary(test.currentVersion)
   315  		s.PatchValue(&jujuversion.Current, current.Number)
   316  		s.PatchValue(&arch.HostArch, func() string { return current.Arch })
   317  		s.PatchValue(&series.HostSeries, func() string { return current.Series })
   318  		com := newUpgradeJujuCommand(test.upgradeMap)
   319  		if err := coretesting.InitCommand(com, test.args); err != nil {
   320  			if test.expectInitErr != "" {
   321  				c.Check(err, gc.ErrorMatches, test.expectInitErr)
   322  			} else {
   323  				c.Check(err, jc.ErrorIsNil)
   324  			}
   325  			continue
   326  		}
   327  
   328  		// Set up state and environ, and run the command.
   329  		toolsDir := c.MkDir()
   330  		updateAttrs := map[string]interface{}{
   331  			"agent-version":      test.agentVersion,
   332  			"agent-metadata-url": "file://" + toolsDir + "/tools",
   333  		}
   334  		err := s.State.UpdateModelConfig(updateAttrs, nil, nil)
   335  		c.Assert(err, jc.ErrorIsNil)
   336  		versions := make([]version.Binary, len(test.tools))
   337  		for i, v := range test.tools {
   338  			versions[i] = version.MustParseBinary(v)
   339  		}
   340  		if len(versions) > 0 {
   341  			stor, err := filestorage.NewFileStorageWriter(toolsDir)
   342  			c.Assert(err, jc.ErrorIsNil)
   343  			envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...)
   344  		}
   345  
   346  		err = com.Run(coretesting.Context(c))
   347  		if test.expectErr != "" {
   348  			c.Check(err, gc.ErrorMatches, test.expectErr)
   349  			continue
   350  		} else if !c.Check(err, jc.ErrorIsNil) {
   351  			continue
   352  		}
   353  
   354  		// Check expected changes to environ/state.
   355  		cfg, err := s.State.ModelConfig()
   356  		c.Check(err, jc.ErrorIsNil)
   357  		agentVersion, ok := cfg.AgentVersion()
   358  		c.Check(ok, jc.IsTrue)
   359  		c.Check(agentVersion, gc.Equals, version.MustParse(test.expectVersion))
   360  
   361  		for _, uploaded := range test.expectUploaded {
   362  			// Substitute latest LTS for placeholder in expected series for uploaded tools
   363  			uploaded = strings.Replace(uploaded, "%LTS%", series.LatestLts(), 1)
   364  			vers := version.MustParseBinary(uploaded)
   365  			s.checkToolsUploaded(c, vers, agentVersion)
   366  		}
   367  	}
   368  }
   369  
   370  func (s *UpgradeJujuSuite) checkToolsUploaded(c *gc.C, vers version.Binary, agentVersion version.Number) {
   371  	storage, err := s.State.ToolsStorage()
   372  	c.Assert(err, jc.ErrorIsNil)
   373  	defer storage.Close()
   374  	_, r, err := storage.Open(vers.String())
   375  	if !c.Check(err, jc.ErrorIsNil) {
   376  		return
   377  	}
   378  	data, err := ioutil.ReadAll(r)
   379  	r.Close()
   380  	c.Check(err, jc.ErrorIsNil)
   381  	expectContent := version.Binary{
   382  		Number: agentVersion,
   383  		Arch:   arch.HostArch(),
   384  		Series: series.HostSeries(),
   385  	}
   386  	checkToolsContent(c, data, "jujud contents "+expectContent.String())
   387  }
   388  
   389  func checkToolsContent(c *gc.C, data []byte, uploaded string) {
   390  	zr, err := gzip.NewReader(bytes.NewReader(data))
   391  	c.Check(err, jc.ErrorIsNil)
   392  	defer zr.Close()
   393  	tr := tar.NewReader(zr)
   394  	found := false
   395  	for {
   396  		hdr, err := tr.Next()
   397  		if err == io.EOF {
   398  			break
   399  		}
   400  		c.Check(err, jc.ErrorIsNil)
   401  		if strings.ContainsAny(hdr.Name, "/\\") {
   402  			c.Fail()
   403  		}
   404  		if hdr.Typeflag != tar.TypeReg {
   405  			c.Fail()
   406  		}
   407  		content, err := ioutil.ReadAll(tr)
   408  		c.Check(err, jc.ErrorIsNil)
   409  		c.Check(string(content), gc.Equals, uploaded)
   410  		found = true
   411  	}
   412  	c.Check(found, jc.IsTrue)
   413  }
   414  
   415  // JujuConnSuite very helpfully uploads some default
   416  // tools to the environment's storage. We don't want
   417  // 'em there; but we do want a consistent default-series
   418  // in the environment state.
   419  func (s *UpgradeJujuSuite) Reset(c *gc.C) {
   420  	s.JujuConnSuite.Reset(c)
   421  	envtesting.RemoveTools(c, s.DefaultToolsStorage, s.Environ.Config().AgentStream())
   422  	updateAttrs := map[string]interface{}{
   423  		"default-series": "raring",
   424  		"agent-version":  "1.2.3",
   425  	}
   426  	err := s.State.UpdateModelConfig(updateAttrs, nil, nil)
   427  	c.Assert(err, jc.ErrorIsNil)
   428  	s.PatchValue(&sync.BuildToolsTarball, toolstesting.GetMockBuildTools(c))
   429  
   430  	// Set API host ports so FindTools works.
   431  	hostPorts := [][]network.HostPort{
   432  		network.NewHostPorts(1234, "0.1.2.3"),
   433  	}
   434  	err = s.State.SetAPIHostPorts(hostPorts)
   435  	c.Assert(err, jc.ErrorIsNil)
   436  
   437  	s.CmdBlockHelper = cmdcommon.NewCmdBlockHelper(s.APIState)
   438  	c.Assert(s.CmdBlockHelper, gc.NotNil)
   439  	s.AddCleanup(func(*gc.C) { s.CmdBlockHelper.Close() })
   440  }
   441  
   442  func (s *UpgradeJujuSuite) TestUpgradeJujuWithRealUpload(c *gc.C) {
   443  	s.Reset(c)
   444  	s.PatchValue(&jujuversion.Current, version.MustParse("1.99.99"))
   445  	cmd := newUpgradeJujuCommand(map[int]version.Number{2: version.MustParse("1.99.99")})
   446  	_, err := coretesting.RunCommand(c, cmd, "--upload-tools")
   447  	c.Assert(err, jc.ErrorIsNil)
   448  	vers := version.Binary{
   449  		Number: jujuversion.Current,
   450  		Arch:   arch.HostArch(),
   451  		Series: series.HostSeries(),
   452  	}
   453  	vers.Build = 1
   454  	s.checkToolsUploaded(c, vers, vers.Number)
   455  }
   456  
   457  func (s *UpgradeJujuSuite) TestBlockUpgradeJujuWithRealUpload(c *gc.C) {
   458  	s.Reset(c)
   459  	s.PatchValue(&jujuversion.Current, version.MustParse("1.99.99"))
   460  	cmd := newUpgradeJujuCommand(map[int]version.Number{2: version.MustParse("1.99.99")})
   461  	// Block operation
   462  	s.BlockAllChanges(c, "TestBlockUpgradeJujuWithRealUpload")
   463  	_, err := coretesting.RunCommand(c, cmd, "--upload-tools")
   464  	s.AssertBlocked(c, err, ".*TestBlockUpgradeJujuWithRealUpload.*")
   465  }
   466  
   467  type DryRunTest struct {
   468  	about             string
   469  	cmdArgs           []string
   470  	tools             []string
   471  	currentVersion    string
   472  	agentVersion      string
   473  	expectedCmdOutput string
   474  }
   475  
   476  func (s *UpgradeJujuSuite) TestUpgradeDryRun(c *gc.C) {
   477  	tests := []DryRunTest{
   478  		{
   479  			about:          "dry run outputs and doesn't change anything when uploading tools",
   480  			cmdArgs:        []string{"--upload-tools", "--dry-run"},
   481  			tools:          []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"},
   482  			currentVersion: "2.1.3-quantal-amd64",
   483  			agentVersion:   "2.0.0",
   484  			expectedCmdOutput: `available tools:
   485      2.1.3-quantal-amd64
   486  best version:
   487      2.1.3
   488  upgrade to this version by running
   489      juju upgrade-juju --version="2.1.3"
   490  `,
   491  		},
   492  		{
   493  			about:          "dry run outputs and doesn't change anything",
   494  			cmdArgs:        []string{"--dry-run"},
   495  			tools:          []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "2.1-dev1-quantal-amd64", "2.2.3-quantal-amd64"},
   496  			currentVersion: "2.0.0-quantal-amd64",
   497  			agentVersion:   "2.0.0",
   498  			expectedCmdOutput: `available tools:
   499      2.1-dev1-quantal-amd64
   500      2.1.0-quantal-amd64
   501      2.1.2-quantal-i386
   502      2.1.3-quantal-amd64
   503      2.2.3-quantal-amd64
   504  best version:
   505      2.1.3
   506  upgrade to this version by running
   507      juju upgrade-juju --version="2.1.3"
   508  `,
   509  		},
   510  		{
   511  			about:          "dry run ignores unknown series",
   512  			cmdArgs:        []string{"--dry-run"},
   513  			tools:          []string{"2.1.0-quantal-amd64", "2.1.2-quantal-i386", "2.1.3-quantal-amd64", "1.2.3-myawesomeseries-amd64"},
   514  			currentVersion: "2.0.0-quantal-amd64",
   515  			agentVersion:   "2.0.0",
   516  			expectedCmdOutput: `available tools:
   517      2.1.0-quantal-amd64
   518      2.1.2-quantal-i386
   519      2.1.3-quantal-amd64
   520  best version:
   521      2.1.3
   522  upgrade to this version by running
   523      juju upgrade-juju --version="2.1.3"
   524  `,
   525  		},
   526  	}
   527  
   528  	for i, test := range tests {
   529  		c.Logf("\ntest %d: %s", i, test.about)
   530  		s.Reset(c)
   531  		tools.DefaultBaseURL = ""
   532  
   533  		s.setUpEnvAndTools(c, test.currentVersion, test.agentVersion, test.tools)
   534  
   535  		com := newUpgradeJujuCommand(nil)
   536  		err := coretesting.InitCommand(com, test.cmdArgs)
   537  		c.Assert(err, jc.ErrorIsNil)
   538  
   539  		ctx := coretesting.Context(c)
   540  		err = com.Run(ctx)
   541  		c.Assert(err, jc.ErrorIsNil)
   542  
   543  		// Check agent version doesn't change
   544  		cfg, err := s.State.ModelConfig()
   545  		c.Assert(err, jc.ErrorIsNil)
   546  		agentVer, ok := cfg.AgentVersion()
   547  		c.Assert(ok, jc.IsTrue)
   548  		c.Assert(agentVer, gc.Equals, version.MustParse(test.agentVersion))
   549  		output := coretesting.Stderr(ctx)
   550  		c.Assert(output, gc.Equals, test.expectedCmdOutput)
   551  	}
   552  }
   553  
   554  func (s *UpgradeJujuSuite) setUpEnvAndTools(c *gc.C, currentVersion string, agentVersion string, tools []string) {
   555  	current := version.MustParseBinary(currentVersion)
   556  	s.PatchValue(&jujuversion.Current, current.Number)
   557  	s.PatchValue(&arch.HostArch, func() string { return current.Arch })
   558  	s.PatchValue(&series.HostSeries, func() string { return current.Series })
   559  
   560  	toolsDir := c.MkDir()
   561  	updateAttrs := map[string]interface{}{
   562  		"agent-version":      agentVersion,
   563  		"agent-metadata-url": "file://" + toolsDir + "/tools",
   564  	}
   565  
   566  	err := s.State.UpdateModelConfig(updateAttrs, nil, nil)
   567  	c.Assert(err, jc.ErrorIsNil)
   568  	versions := make([]version.Binary, len(tools))
   569  	for i, v := range tools {
   570  		versions[i], err = version.ParseBinary(v)
   571  		if err != nil {
   572  			c.Assert(err, jc.Satisfies, series.IsUnknownOSForSeriesError)
   573  		}
   574  	}
   575  	if len(versions) > 0 {
   576  		stor, err := filestorage.NewFileStorageWriter(toolsDir)
   577  		c.Assert(err, jc.ErrorIsNil)
   578  		envtesting.MustUploadFakeToolsVersions(stor, s.Environ.Config().AgentStream(), versions...)
   579  	}
   580  }
   581  
   582  func (s *UpgradeJujuSuite) TestUpgradesDifferentMajor(c *gc.C) {
   583  	toolsList49Only := `available tools:
   584      4.9.0-trusty-amd64
   585  best version:
   586      4.9.0
   587  `
   588  	tests := []struct {
   589  		about             string
   590  		cmdArgs           []string
   591  		tools             []string
   592  		currentVersion    string
   593  		agentVersion      string
   594  		expectedVersion   string
   595  		expectedCmdOutput string
   596  		expectedLogOutput string
   597  		excludedLogOutput string
   598  		expectedErr       string
   599  		upgradeMap        map[int]version.Number
   600  	}{{
   601  		about:             "upgrade previous major to latest previous major",
   602  		tools:             []string{"5.0.1-trusty-amd64", "4.9.0-trusty-amd64"},
   603  		currentVersion:    "5.0.0-trusty-amd64",
   604  		agentVersion:      "4.8.5",
   605  		expectedVersion:   "4.9.0",
   606  		expectedCmdOutput: toolsList49Only,
   607  		expectedLogOutput: `.*version 4.9.0 incompatible with this client \(5.0.0\).*started upgrade to 4.9.0.*`,
   608  	}, {
   609  		about:             "upgrade previous major to latest previous major --dry-run still warns",
   610  		tools:             []string{"5.0.1-trusty-amd64", "4.9.0-trusty-amd64"},
   611  		currentVersion:    "5.0.1-trusty-amd64",
   612  		agentVersion:      "4.8.5",
   613  		expectedVersion:   "4.9.0",
   614  		expectedCmdOutput: toolsList49Only,
   615  		expectedLogOutput: `.*version 4.9.0 incompatible with this client \(5.0.1\).*started upgrade to 4.9.0.*`,
   616  	}, {
   617  		about:             "upgrade previous major to latest previous major with --version",
   618  		cmdArgs:           []string{"--version=4.9.0"},
   619  		tools:             []string{"5.0.2-trusty-amd64", "4.9.0-trusty-amd64", "4.8.0-trusty-amd64"},
   620  		currentVersion:    "5.0.2-trusty-amd64",
   621  		agentVersion:      "4.7.5",
   622  		expectedVersion:   "4.9.0",
   623  		expectedCmdOutput: toolsList49Only,
   624  		expectedLogOutput: `.*version 4.9.0 incompatible with this client \(5.0.2\).*started upgrade to 4.9.0.*`,
   625  	}, {
   626  		about:             "can upgrade lower major version to current major version at minimum level",
   627  		cmdArgs:           []string{"--version=6.0.5"},
   628  		tools:             []string{"6.0.5-trusty-amd64", "5.9.9-trusty-amd64"},
   629  		currentVersion:    "6.0.0-trusty-amd64",
   630  		agentVersion:      "5.9.8",
   631  		expectedVersion:   "6.0.5",
   632  		excludedLogOutput: `incompatible with this client (6.0.0)`,
   633  		upgradeMap:        map[int]version.Number{6: version.MustParse("5.9.8")},
   634  	}, {
   635  		about:             "can upgrade lower major version to current major version above minimum level",
   636  		cmdArgs:           []string{"--version=6.0.5"},
   637  		tools:             []string{"6.0.5-trusty-amd64", "5.11.0-trusty-amd64"},
   638  		currentVersion:    "6.0.1-trusty-amd64",
   639  		agentVersion:      "5.10.8",
   640  		expectedVersion:   "6.0.5",
   641  		excludedLogOutput: `incompatible with this client (6.0.1)`,
   642  		upgradeMap:        map[int]version.Number{6: version.MustParse("5.9.8")},
   643  	}, {
   644  		about:           "can upgrade current to next major version",
   645  		cmdArgs:         []string{"--version=6.0.5"},
   646  		tools:           []string{"6.0.5-trusty-amd64", "5.11.0-trusty-amd64"},
   647  		currentVersion:  "5.10.8-trusty-amd64",
   648  		agentVersion:    "5.10.8",
   649  		expectedVersion: "6.0.5",
   650  		upgradeMap:      map[int]version.Number{6: version.MustParse("5.9.8")},
   651  	}, {
   652  		about:             "upgrade fails if not at minimum version",
   653  		cmdArgs:           []string{"--version=7.0.1"},
   654  		tools:             []string{"7.0.1-trusty-amd64"},
   655  		currentVersion:    "7.0.1-trusty-amd64",
   656  		agentVersion:      "6.0.0",
   657  		expectedVersion:   "6.0.0",
   658  		expectedCmdOutput: "upgrades to a new major version must first go through 6.7.8\n",
   659  		expectedErr:       "unable to upgrade to requested version",
   660  		upgradeMap:        map[int]version.Number{7: version.MustParse("6.7.8")},
   661  	}, {
   662  		about:             "upgrade fails if not a minor of 0",
   663  		cmdArgs:           []string{"--version=7.1.1"},
   664  		tools:             []string{"7.0.1-trusty-amd64", "7.1.1-trusty-amd64"},
   665  		currentVersion:    "7.0.1-trusty-amd64",
   666  		agentVersion:      "6.7.8",
   667  		expectedVersion:   "6.7.8",
   668  		expectedCmdOutput: "upgrades to 7.1.1 must first go through juju 7.0\n",
   669  		expectedErr:       "unable to upgrade to requested version",
   670  		upgradeMap:        map[int]version.Number{7: version.MustParse("6.7.8")},
   671  	}, {
   672  		about:           "upgrade fails if not at minimum version and not a minor of 0",
   673  		cmdArgs:         []string{"--version=7.1.1"},
   674  		tools:           []string{"7.0.1-trusty-amd64", "7.1.1-trusty-amd64"},
   675  		currentVersion:  "7.0.1-trusty-amd64",
   676  		agentVersion:    "6.0.0",
   677  		expectedVersion: "6.0.0",
   678  		expectedCmdOutput: "upgrades to 7.1.1 must first go through juju 7.0\n" +
   679  			"upgrades to a new major version must first go through 6.7.8\n",
   680  		expectedErr: "unable to upgrade to requested version",
   681  		upgradeMap:  map[int]version.Number{7: version.MustParse("6.7.8")},
   682  	}}
   683  	for i, test := range tests {
   684  		c.Logf("\ntest %d: %s", i, test.about)
   685  		s.Reset(c)
   686  		tools.DefaultBaseURL = ""
   687  
   688  		s.setUpEnvAndTools(c, test.currentVersion, test.agentVersion, test.tools)
   689  
   690  		com := newUpgradeJujuCommand(test.upgradeMap)
   691  		err := coretesting.InitCommand(com, test.cmdArgs)
   692  		c.Assert(err, jc.ErrorIsNil)
   693  
   694  		ctx := coretesting.Context(c)
   695  		err = com.Run(ctx)
   696  		if test.expectedErr != "" {
   697  			c.Check(err, gc.ErrorMatches, test.expectedErr)
   698  		} else if !c.Check(err, jc.ErrorIsNil) {
   699  			continue
   700  		}
   701  
   702  		// Check agent version doesn't change
   703  		cfg, err := s.State.ModelConfig()
   704  		c.Assert(err, jc.ErrorIsNil)
   705  		agentVer, ok := cfg.AgentVersion()
   706  		c.Assert(ok, jc.IsTrue)
   707  		c.Check(agentVer, gc.Equals, version.MustParse(test.expectedVersion))
   708  		output := coretesting.Stderr(ctx)
   709  		if test.expectedCmdOutput != "" {
   710  			c.Check(output, gc.Equals, test.expectedCmdOutput)
   711  		}
   712  		if test.expectedLogOutput != "" {
   713  			c.Check(strings.Replace(c.GetTestLog(), "\n", " ", -1), gc.Matches, test.expectedLogOutput)
   714  		}
   715  		if test.excludedLogOutput != "" {
   716  			c.Check(c.GetTestLog(), gc.Not(jc.Contains), test.excludedLogOutput)
   717  		}
   718  	}
   719  }
   720  
   721  func (s *UpgradeJujuSuite) TestUpgradeUnknownSeriesInStreams(c *gc.C) {
   722  	fakeAPI := NewFakeUpgradeJujuAPI(c, s.State)
   723  	fakeAPI.addTools("2.1.0-weird-amd64")
   724  	fakeAPI.patch(s)
   725  
   726  	cmd := &upgradeJujuCommand{}
   727  	err := coretesting.InitCommand(modelcmd.Wrap(cmd), []string{})
   728  	c.Assert(err, jc.ErrorIsNil)
   729  
   730  	err = modelcmd.Wrap(cmd).Run(coretesting.Context(c))
   731  	c.Assert(err, gc.IsNil)
   732  
   733  	// ensure find tools was called
   734  	c.Assert(fakeAPI.findToolsCalled, jc.IsTrue)
   735  	c.Assert(fakeAPI.tools, gc.DeepEquals, []string{"2.1.0-weird-amd64", fakeAPI.nextVersion.String()})
   736  }
   737  
   738  func (s *UpgradeJujuSuite) TestUpgradeInProgress(c *gc.C) {
   739  	fakeAPI := NewFakeUpgradeJujuAPI(c, s.State)
   740  	fakeAPI.setVersionErr = &params.Error{
   741  		Message: "a message from the server about the problem",
   742  		Code:    params.CodeUpgradeInProgress,
   743  	}
   744  	fakeAPI.patch(s)
   745  	cmd := &upgradeJujuCommand{}
   746  	err := coretesting.InitCommand(modelcmd.Wrap(cmd), []string{})
   747  	c.Assert(err, jc.ErrorIsNil)
   748  
   749  	err = modelcmd.Wrap(cmd).Run(coretesting.Context(c))
   750  	c.Assert(err, gc.ErrorMatches, "a message from the server about the problem\n"+
   751  		"\n"+
   752  		"Please wait for the upgrade to complete or if there was a problem with\n"+
   753  		"the last upgrade that has been resolved, consider running the\n"+
   754  		"upgrade-juju command with the --reset-previous-upgrade flag.",
   755  	)
   756  }
   757  
   758  func (s *UpgradeJujuSuite) TestBlockUpgradeInProgress(c *gc.C) {
   759  	fakeAPI := NewFakeUpgradeJujuAPI(c, s.State)
   760  	fakeAPI.setVersionErr = common.OperationBlockedError("the operation has been blocked")
   761  	fakeAPI.patch(s)
   762  	cmd := &upgradeJujuCommand{}
   763  	err := coretesting.InitCommand(modelcmd.Wrap(cmd), []string{})
   764  	c.Assert(err, jc.ErrorIsNil)
   765  
   766  	// Block operation
   767  	s.BlockAllChanges(c, "TestBlockUpgradeInProgress")
   768  	err = modelcmd.Wrap(cmd).Run(coretesting.Context(c))
   769  	s.AssertBlocked(c, err, ".*To unblock changes.*")
   770  }
   771  
   772  func (s *UpgradeJujuSuite) TestResetPreviousUpgrade(c *gc.C) {
   773  	fakeAPI := NewFakeUpgradeJujuAPI(c, s.State)
   774  	fakeAPI.patch(s)
   775  
   776  	ctx := coretesting.Context(c)
   777  	var stdin bytes.Buffer
   778  	ctx.Stdin = &stdin
   779  
   780  	run := func(answer string, expect bool, args ...string) {
   781  		stdin.Reset()
   782  		if answer != "" {
   783  			stdin.WriteString(answer)
   784  		}
   785  
   786  		fakeAPI.reset()
   787  
   788  		cmd := &upgradeJujuCommand{}
   789  		err := coretesting.InitCommand(modelcmd.Wrap(cmd),
   790  			append([]string{"--reset-previous-upgrade"}, args...))
   791  		c.Assert(err, jc.ErrorIsNil)
   792  		err = modelcmd.Wrap(cmd).Run(ctx)
   793  		if expect {
   794  			c.Assert(err, jc.ErrorIsNil)
   795  		} else {
   796  			c.Assert(err, gc.ErrorMatches, "previous upgrade not reset and no new upgrade triggered")
   797  		}
   798  
   799  		c.Assert(fakeAPI.abortCurrentUpgradeCalled, gc.Equals, expect)
   800  		expectedVersion := version.Number{}
   801  		if expect {
   802  			expectedVersion = fakeAPI.nextVersion.Number
   803  		}
   804  		c.Assert(fakeAPI.setVersionCalledWith, gc.Equals, expectedVersion)
   805  	}
   806  
   807  	const expectUpgrade = true
   808  	const expectNoUpgrade = false
   809  
   810  	// EOF on stdin - equivalent to answering no.
   811  	run("", expectNoUpgrade)
   812  
   813  	// -y on command line - no confirmation required
   814  	run("", expectUpgrade, "-y")
   815  
   816  	// --yes on command line - no confirmation required
   817  	run("", expectUpgrade, "--yes")
   818  
   819  	// various ways of saying "yes" to the prompt
   820  	for _, answer := range []string{"y", "Y", "yes", "YES"} {
   821  		run(answer, expectUpgrade)
   822  	}
   823  
   824  	// various ways of saying "no" to the prompt
   825  	for _, answer := range []string{"n", "N", "no", "foo"} {
   826  		run(answer, expectNoUpgrade)
   827  	}
   828  }
   829  
   830  func NewFakeUpgradeJujuAPI(c *gc.C, st *state.State) *fakeUpgradeJujuAPI {
   831  	nextVersion := version.Binary{
   832  		Number: jujuversion.Current,
   833  		Arch:   arch.HostArch(),
   834  		Series: series.HostSeries(),
   835  	}
   836  	nextVersion.Minor++
   837  	return &fakeUpgradeJujuAPI{
   838  		c:           c,
   839  		st:          st,
   840  		nextVersion: nextVersion,
   841  	}
   842  }
   843  
   844  type fakeUpgradeJujuAPI struct {
   845  	c                         *gc.C
   846  	st                        *state.State
   847  	nextVersion               version.Binary
   848  	setVersionErr             error
   849  	abortCurrentUpgradeCalled bool
   850  	setVersionCalledWith      version.Number
   851  	tools                     []string
   852  	findToolsCalled           bool
   853  }
   854  
   855  func (a *fakeUpgradeJujuAPI) reset() {
   856  	a.setVersionErr = nil
   857  	a.abortCurrentUpgradeCalled = false
   858  	a.setVersionCalledWith = version.Number{}
   859  	a.tools = []string{}
   860  	a.findToolsCalled = false
   861  }
   862  
   863  func (a *fakeUpgradeJujuAPI) patch(s *UpgradeJujuSuite) {
   864  	s.PatchValue(&getUpgradeJujuAPI, func(*upgradeJujuCommand) (upgradeJujuAPI, error) {
   865  		return a, nil
   866  	})
   867  }
   868  
   869  func (a *fakeUpgradeJujuAPI) addTools(tools ...string) {
   870  	for _, tool := range tools {
   871  		a.tools = append(a.tools, tool)
   872  	}
   873  }
   874  
   875  func (a *fakeUpgradeJujuAPI) ModelGet() (map[string]interface{}, error) {
   876  	config, err := a.st.ModelConfig()
   877  	if err != nil {
   878  		return make(map[string]interface{}), err
   879  	}
   880  	return config.AllAttrs(), nil
   881  }
   882  
   883  func (a *fakeUpgradeJujuAPI) FindTools(majorVersion, minorVersion int, series, arch string) (
   884  	result params.FindToolsResult, err error,
   885  ) {
   886  	a.findToolsCalled = true
   887  	a.tools = append(a.tools, a.nextVersion.String())
   888  	tools := toolstesting.MakeTools(a.c, a.c.MkDir(), "released", a.tools)
   889  	return params.FindToolsResult{
   890  		List:  tools,
   891  		Error: nil,
   892  	}, nil
   893  }
   894  
   895  func (a *fakeUpgradeJujuAPI) UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (coretools.List, error) {
   896  	panic("not implemented")
   897  }
   898  
   899  func (a *fakeUpgradeJujuAPI) AbortCurrentUpgrade() error {
   900  	a.abortCurrentUpgradeCalled = true
   901  	return nil
   902  }
   903  
   904  func (a *fakeUpgradeJujuAPI) SetModelAgentVersion(v version.Number) error {
   905  	a.setVersionCalledWith = v
   906  	return a.setVersionErr
   907  }
   908  
   909  func (a *fakeUpgradeJujuAPI) Close() error {
   910  	return nil
   911  }