github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/juju/bootstrap_test.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  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	gitjujutesting "github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "launchpad.net/gocheck"
    16  
    17  	"github.com/juju/juju/cmd"
    18  	"github.com/juju/juju/cmd/envcmd"
    19  	"github.com/juju/juju/constraints"
    20  	"github.com/juju/juju/environs"
    21  	"github.com/juju/juju/environs/config"
    22  	"github.com/juju/juju/environs/configstore"
    23  	"github.com/juju/juju/environs/filestorage"
    24  	"github.com/juju/juju/environs/imagemetadata"
    25  	imtesting "github.com/juju/juju/environs/imagemetadata/testing"
    26  	"github.com/juju/juju/environs/simplestreams"
    27  	"github.com/juju/juju/environs/storage"
    28  	"github.com/juju/juju/environs/sync"
    29  	envtesting "github.com/juju/juju/environs/testing"
    30  	envtools "github.com/juju/juju/environs/tools"
    31  	toolstesting "github.com/juju/juju/environs/tools/testing"
    32  	"github.com/juju/juju/juju/arch"
    33  	"github.com/juju/juju/provider/dummy"
    34  	coretesting "github.com/juju/juju/testing"
    35  	coretools "github.com/juju/juju/tools"
    36  	"github.com/juju/juju/version"
    37  )
    38  
    39  type BootstrapSuite struct {
    40  	coretesting.FakeJujuHomeSuite
    41  	coretesting.MgoSuite
    42  	envtesting.ToolsFixture
    43  }
    44  
    45  var _ = gc.Suite(&BootstrapSuite{})
    46  
    47  func (s *BootstrapSuite) SetUpSuite(c *gc.C) {
    48  	s.FakeJujuHomeSuite.SetUpSuite(c)
    49  	s.MgoSuite.SetUpSuite(c)
    50  }
    51  
    52  func (s *BootstrapSuite) SetUpTest(c *gc.C) {
    53  	s.FakeJujuHomeSuite.SetUpTest(c)
    54  	s.MgoSuite.SetUpTest(c)
    55  	s.ToolsFixture.SetUpTest(c)
    56  
    57  	// Set up a local source with tools.
    58  	sourceDir := createToolsSource(c, vAll)
    59  	s.PatchValue(&envtools.DefaultBaseURL, sourceDir)
    60  
    61  	s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c))
    62  }
    63  
    64  func (s *BootstrapSuite) TearDownSuite(c *gc.C) {
    65  	s.MgoSuite.TearDownSuite(c)
    66  	s.FakeJujuHomeSuite.TearDownSuite(c)
    67  }
    68  
    69  func (s *BootstrapSuite) TearDownTest(c *gc.C) {
    70  	s.ToolsFixture.TearDownTest(c)
    71  	s.MgoSuite.TearDownTest(c)
    72  	s.FakeJujuHomeSuite.TearDownTest(c)
    73  	dummy.Reset()
    74  }
    75  
    76  type bootstrapRetryTest struct {
    77  	info               string
    78  	args               []string
    79  	expectedAllowRetry []bool
    80  	err                string
    81  	// If version != "", version.Current will be
    82  	// set to it for the duration of the test.
    83  	version string
    84  	// If addVersionToSource is true, then "version"
    85  	// above will be populated in the tools source.
    86  	addVersionToSource bool
    87  }
    88  
    89  var noToolsAvailableMessage = "cannot upload bootstrap tools: Juju cannot bootstrap because no tools are available for your environment.*"
    90  var toolsNotFoundMessage = "cannot find bootstrap tools: tools not found"
    91  
    92  var bootstrapRetryTests = []bootstrapRetryTest{{
    93  	info:               "no tools uploaded, first check has no retries; no matching binary in source; no second attempt",
    94  	expectedAllowRetry: []bool{false},
    95  	err:                noToolsAvailableMessage,
    96  	version:            "1.16.0-precise-amd64",
    97  }, {
    98  	info:               "no tools uploaded, first check has no retries; matching binary in source; check after upload has retries",
    99  	expectedAllowRetry: []bool{false, true},
   100  	err:                toolsNotFoundMessage,
   101  	version:            "1.17.0-precise-amd64", // dev version to force upload
   102  	addVersionToSource: true,
   103  }, {
   104  	info:               "no tools uploaded, first check has no retries; no matching binary in source; check after upload has retries",
   105  	expectedAllowRetry: []bool{false, true},
   106  	err:                toolsNotFoundMessage,
   107  	version:            "1.15.1-precise-amd64", // dev version to force upload
   108  }, {
   109  	info:               "new tools uploaded, so we want to allow retries to give them a chance at showing up",
   110  	args:               []string{"--upload-tools"},
   111  	expectedAllowRetry: []bool{true},
   112  	err:                noToolsAvailableMessage,
   113  }}
   114  
   115  // Test test checks that bootstrap calls FindTools with the expected allowRetry flag.
   116  func (s *BootstrapSuite) TestAllowRetries(c *gc.C) {
   117  	for i, test := range bootstrapRetryTests {
   118  		c.Logf("test %d: %s\n", i, test.info)
   119  		s.runAllowRetriesTest(c, test)
   120  	}
   121  }
   122  
   123  func (s *BootstrapSuite) runAllowRetriesTest(c *gc.C, test bootstrapRetryTest) {
   124  	toolsVersions := envtesting.VAll
   125  	if test.version != "" {
   126  		useVersion := strings.Replace(test.version, "%LTS%", config.LatestLtsSeries(), 1)
   127  		testVersion := version.MustParseBinary(useVersion)
   128  		s.PatchValue(&version.Current, testVersion)
   129  		if test.addVersionToSource {
   130  			toolsVersions = append([]version.Binary{}, toolsVersions...)
   131  			toolsVersions = append(toolsVersions, testVersion)
   132  		}
   133  	}
   134  	resetJujuHome(c)
   135  	sourceDir := createToolsSource(c, toolsVersions)
   136  	s.PatchValue(&envtools.DefaultBaseURL, sourceDir)
   137  
   138  	var findToolsRetryValues []bool
   139  	mockFindTools := func(cloudInst environs.ConfigGetter, majorVersion, minorVersion int,
   140  		filter coretools.Filter, allowRetry bool) (list coretools.List, err error) {
   141  		findToolsRetryValues = append(findToolsRetryValues, allowRetry)
   142  		return nil, errors.NotFoundf("tools")
   143  	}
   144  
   145  	restore := envtools.TestingPatchBootstrapFindTools(mockFindTools)
   146  	defer restore()
   147  
   148  	_, errc := runCommand(nullContext(c), envcmd.Wrap(new(BootstrapCommand)), test.args...)
   149  	err := <-errc
   150  	c.Check(findToolsRetryValues, gc.DeepEquals, test.expectedAllowRetry)
   151  	stripped := strings.Replace(err.Error(), "\n", "", -1)
   152  	c.Check(stripped, gc.Matches, test.err)
   153  }
   154  
   155  func (s *BootstrapSuite) TestTest(c *gc.C) {
   156  	for i, test := range bootstrapTests {
   157  		c.Logf("\ntest %d: %s", i, test.info)
   158  		test.run(c)
   159  	}
   160  }
   161  
   162  type bootstrapTest struct {
   163  	info string
   164  	// binary version string used to set version.Current
   165  	version string
   166  	sync    bool
   167  	args    []string
   168  	err     string
   169  	// binary version strings for expected tools; if set, no default tools
   170  	// will be uploaded before running the test.
   171  	uploads     []string
   172  	constraints constraints.Value
   173  	placement   string
   174  	hostArch    string
   175  }
   176  
   177  func (test bootstrapTest) run(c *gc.C) {
   178  	// Create home with dummy provider and remove all
   179  	// of its envtools.
   180  	env := resetJujuHome(c)
   181  
   182  	if test.version != "" {
   183  		useVersion := strings.Replace(test.version, "%LTS%", config.LatestLtsSeries(), 1)
   184  		origVersion := version.Current
   185  		version.Current = version.MustParseBinary(useVersion)
   186  		defer func() { version.Current = origVersion }()
   187  	}
   188  
   189  	if test.hostArch != "" {
   190  		origVersion := arch.HostArch
   191  		arch.HostArch = func() string {
   192  			return test.hostArch
   193  		}
   194  		defer func() { arch.HostArch = origVersion }()
   195  	}
   196  
   197  	uploadCount := len(test.uploads)
   198  	if uploadCount == 0 {
   199  		usefulVersion := version.Current
   200  		usefulVersion.Series = config.PreferredSeries(env.Config())
   201  		envtesting.AssertUploadFakeToolsVersions(c, env.Storage(), usefulVersion)
   202  	}
   203  
   204  	// Run command and check for uploads.
   205  	opc, errc := runCommand(nullContext(c), envcmd.Wrap(new(BootstrapCommand)), test.args...)
   206  	// Check for remaining operations/errors.
   207  	if test.err != "" {
   208  		err := <-errc
   209  		stripped := strings.Replace(err.Error(), "\n", "", -1)
   210  		c.Check(stripped, gc.Matches, test.err)
   211  		return
   212  	}
   213  	if !c.Check(<-errc, gc.IsNil) {
   214  		return
   215  	}
   216  
   217  	if uploadCount > 0 {
   218  		for i := 0; i < uploadCount; i++ {
   219  			c.Check((<-opc).(dummy.OpPutFile).Env, gc.Equals, "peckham")
   220  		}
   221  		list, err := envtools.FindTools(
   222  			env, version.Current.Major, version.Current.Minor, coretools.Filter{}, envtools.DoNotAllowRetry)
   223  		c.Check(err, gc.IsNil)
   224  		c.Logf("found: " + list.String())
   225  		urls := list.URLs()
   226  		c.Check(urls, gc.HasLen, len(test.uploads))
   227  		for _, v := range test.uploads {
   228  			v := strings.Replace(v, "%LTS%", config.LatestLtsSeries(), 1)
   229  			c.Logf("seeking: " + v)
   230  			vers := version.MustParseBinary(v)
   231  			_, found := urls[vers]
   232  			c.Check(found, gc.Equals, true)
   233  		}
   234  	}
   235  	if len(test.uploads) > 0 {
   236  		indexFile := (<-opc).(dummy.OpPutFile)
   237  		c.Check(indexFile.FileName, gc.Equals, "tools/streams/v1/index.json")
   238  		productFile := (<-opc).(dummy.OpPutFile)
   239  		c.Check(productFile.FileName, gc.Equals, "tools/streams/v1/com.ubuntu.juju:released:tools.json")
   240  	}
   241  	opPutBootstrapVerifyFile := (<-opc).(dummy.OpPutFile)
   242  	c.Check(opPutBootstrapVerifyFile.Env, gc.Equals, "peckham")
   243  	c.Check(opPutBootstrapVerifyFile.FileName, gc.Equals, environs.VerificationFilename)
   244  
   245  	opPutBootstrapInitFile := (<-opc).(dummy.OpPutFile)
   246  	c.Check(opPutBootstrapInitFile.Env, gc.Equals, "peckham")
   247  	c.Check(opPutBootstrapInitFile.FileName, gc.Equals, "provider-state")
   248  
   249  	opBootstrap := (<-opc).(dummy.OpBootstrap)
   250  	c.Check(opBootstrap.Env, gc.Equals, "peckham")
   251  	c.Check(opBootstrap.Args.Constraints, gc.DeepEquals, test.constraints)
   252  	c.Check(opBootstrap.Args.Placement, gc.Equals, test.placement)
   253  
   254  	store, err := configstore.Default()
   255  	c.Assert(err, gc.IsNil)
   256  	// Check a CA cert/key was generated by reloading the environment.
   257  	env, err = environs.NewFromName("peckham", store)
   258  	c.Assert(err, gc.IsNil)
   259  	_, hasCert := env.Config().CACert()
   260  	c.Check(hasCert, gc.Equals, true)
   261  	_, hasKey := env.Config().CAPrivateKey()
   262  	c.Check(hasKey, gc.Equals, true)
   263  }
   264  
   265  var bootstrapTests = []bootstrapTest{{
   266  	info: "no args, no error, no uploads, no constraints",
   267  }, {
   268  	info: "bad --constraints",
   269  	args: []string{"--constraints", "bad=wrong"},
   270  	err:  `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`,
   271  }, {
   272  	info: "conflicting --constraints",
   273  	args: []string{"--constraints", "instance-type=foo mem=4G"},
   274  	err:  `ambiguous constraints: "instance-type" overlaps with "mem"`,
   275  }, {
   276  	info: "bad --series",
   277  	args: []string{"--series", "1bad1"},
   278  	err:  `invalid value "1bad1" for flag --series: invalid series name "1bad1"`,
   279  }, {
   280  	info: "lonely --series",
   281  	args: []string{"--series", "fine"},
   282  	err:  `--series requires --upload-tools`,
   283  }, {
   284  	info: "lonely --upload-series",
   285  	args: []string{"--upload-series", "fine"},
   286  	err:  `--upload-series requires --upload-tools`,
   287  }, {
   288  	info: "--upload-series with --series",
   289  	args: []string{"--upload-tools", "--upload-series", "foo", "--series", "bar"},
   290  	err:  `--upload-series and --series can't be used together`,
   291  }, {
   292  	info:    "bad environment",
   293  	version: "1.2.3-%LTS%-amd64",
   294  	args:    []string{"-e", "brokenenv"},
   295  	err:     `dummy.Bootstrap is broken`,
   296  }, {
   297  	info:        "constraints",
   298  	args:        []string{"--constraints", "mem=4G cpu-cores=4"},
   299  	constraints: constraints.MustParse("mem=4G cpu-cores=4"),
   300  }, {
   301  	info:        "unsupported constraint passed through but no error",
   302  	args:        []string{"--constraints", "mem=4G cpu-cores=4 cpu-power=10"},
   303  	constraints: constraints.MustParse("mem=4G cpu-cores=4 cpu-power=10"),
   304  }, {
   305  	info:    "--upload-tools picks all reasonable series",
   306  	version: "1.2.3-saucy-amd64",
   307  	args:    []string{"--upload-tools"},
   308  	uploads: []string{
   309  		"1.2.3.1-saucy-amd64",  // from version.Current
   310  		"1.2.3.1-raring-amd64", // from env.Config().DefaultSeries()
   311  		"1.2.3.1-precise-amd64",
   312  		"1.2.3.1-trusty-amd64",
   313  	},
   314  }, {
   315  	info:     "--upload-tools uses arch from constraint if it matches current version",
   316  	version:  "1.3.3-saucy-ppc64",
   317  	hostArch: "ppc64",
   318  	args:     []string{"--upload-tools", "--constraints", "arch=ppc64"},
   319  	uploads: []string{
   320  		"1.3.3.1-saucy-ppc64",  // from version.Current
   321  		"1.3.3.1-raring-ppc64", // from env.Config().DefaultSeries()
   322  		"1.3.3.1-precise-ppc64",
   323  		"1.3.3.1-trusty-ppc64",
   324  	},
   325  	constraints: constraints.MustParse("arch=ppc64"),
   326  }, {
   327  	info:    "--upload-tools only uploads each file once",
   328  	version: "1.2.3-%LTS%-amd64",
   329  	args:    []string{"--upload-tools"},
   330  	uploads: []string{
   331  		"1.2.3.1-raring-amd64",
   332  		"1.2.3.1-precise-amd64",
   333  		"1.2.3.1-trusty-amd64",
   334  	},
   335  }, {
   336  	info:    "--upload-tools rejects invalid series",
   337  	version: "1.2.3-saucy-amd64",
   338  	args:    []string{"--upload-tools", "--upload-series", "ping,ping,pong"},
   339  	err:     `invalid series "ping"`,
   340  }, {
   341  	info:     "--upload-tools rejects mismatched arch",
   342  	version:  "1.3.3-saucy-amd64",
   343  	hostArch: "amd64",
   344  	args:     []string{"--upload-tools", "--constraints", "arch=ppc64"},
   345  	err:      `cannot build tools for "ppc64" using a machine running on "amd64"`,
   346  }, {
   347  	info:     "--upload-tools rejects non-supported arch",
   348  	version:  "1.3.3-saucy-arm64",
   349  	hostArch: "arm64",
   350  	args:     []string{"--upload-tools"},
   351  	err:      `environment "peckham" of type dummy does not support instances running on "arm64"`,
   352  }, {
   353  	info:    "--upload-tools always bumps build number",
   354  	version: "1.2.3.4-raring-amd64",
   355  	args:    []string{"--upload-tools"},
   356  	uploads: []string{
   357  		"1.2.3.5-raring-amd64",
   358  		"1.2.3.5-precise-amd64",
   359  		"1.2.3.5-trusty-amd64",
   360  	},
   361  }, {
   362  	info:      "placement",
   363  	args:      []string{"--to", "something"},
   364  	placement: "something",
   365  }, {
   366  	info: "additional args",
   367  	args: []string{"anything", "else"},
   368  	err:  `unrecognized args: \["anything" "else"\]`,
   369  }}
   370  
   371  func (s *BootstrapSuite) TestBootstrapTwice(c *gc.C) {
   372  	env := resetJujuHome(c)
   373  	defaultSeriesVersion := version.Current
   374  	defaultSeriesVersion.Series = config.PreferredSeries(env.Config())
   375  	// Force a dev version by having an odd minor version number.
   376  	// This is because we have not uploaded any tools and auto
   377  	// upload is only enabled for dev versions.
   378  	defaultSeriesVersion.Minor = 11
   379  	s.PatchValue(&version.Current, defaultSeriesVersion)
   380  
   381  	_, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}))
   382  	c.Assert(err, gc.IsNil)
   383  
   384  	_, err = coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}))
   385  	c.Assert(err, gc.ErrorMatches, "environment is already bootstrapped")
   386  }
   387  
   388  func (s *BootstrapSuite) TestSeriesDeprecation(c *gc.C) {
   389  	ctx := s.checkSeriesArg(c, "--series")
   390  	c.Check(coretesting.Stderr(ctx), gc.Equals,
   391  		"Use of --series is deprecated. Please use --upload-series instead.\n")
   392  }
   393  
   394  func (s *BootstrapSuite) TestNoDeprecationWithUploadSeries(c *gc.C) {
   395  	ctx := s.checkSeriesArg(c, "--upload-series")
   396  	c.Check(coretesting.Stderr(ctx), gc.Equals, "")
   397  }
   398  
   399  func (s *BootstrapSuite) checkSeriesArg(c *gc.C, argVariant string) *cmd.Context {
   400  	_bootstrap := &fakeBootstrapFuncs{}
   401  	s.PatchValue(&getBootstrapFuncs, func() BootstrapInterface {
   402  		return _bootstrap
   403  	})
   404  	resetJujuHome(c)
   405  
   406  	ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--upload-tools", argVariant, "foo,bar")
   407  
   408  	c.Assert(err, gc.IsNil)
   409  	c.Check(_bootstrap.uploadToolsSeries, gc.DeepEquals, []string{"foo", "bar"})
   410  	return ctx
   411  }
   412  
   413  func (s *BootstrapSuite) TestBootstrapJenvWarning(c *gc.C) {
   414  	env := resetJujuHome(c)
   415  	defaultSeriesVersion := version.Current
   416  	defaultSeriesVersion.Series = config.PreferredSeries(env.Config())
   417  	// Force a dev version by having an odd minor version number.
   418  	// This is because we have not uploaded any tools and auto
   419  	// upload is only enabled for dev versions.
   420  	defaultSeriesVersion.Minor = 11
   421  	s.PatchValue(&version.Current, defaultSeriesVersion)
   422  
   423  	store, err := configstore.Default()
   424  	c.Assert(err, gc.IsNil)
   425  	ctx := coretesting.Context(c)
   426  	environs.PrepareFromName("peckham", ctx, store)
   427  
   428  	logger := "jenv.warning.test"
   429  	testWriter := &loggo.TestWriter{}
   430  	loggo.RegisterWriter(logger, testWriter, loggo.WARNING)
   431  	defer loggo.RemoveWriter(logger)
   432  
   433  	_, errc := runCommand(ctx, envcmd.Wrap(new(BootstrapCommand)), "-e", "peckham")
   434  	c.Assert(<-errc, gc.IsNil)
   435  	c.Assert(testWriter.Log, jc.LogMatches, []string{"ignoring environments.yaml: using bootstrap config in .*"})
   436  }
   437  
   438  func (s *BootstrapSuite) TestInvalidLocalSource(c *gc.C) {
   439  	s.PatchValue(&version.Current.Number, version.MustParse("1.2.0"))
   440  	env := resetJujuHome(c)
   441  
   442  	// Bootstrap the environment with an invalid source.
   443  	// The command returns with an error.
   444  	_, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", c.MkDir())
   445  	c.Check(err, gc.ErrorMatches, "cannot upload bootstrap tools: Juju "+
   446  		"cannot bootstrap because no tools are available for your "+
   447  		"environment(.|\n)*")
   448  
   449  	// Now check that there are no tools available.
   450  	_, err = envtools.FindTools(
   451  		env, version.Current.Major, version.Current.Minor, coretools.Filter{}, envtools.DoNotAllowRetry)
   452  	c.Assert(err, gc.FitsTypeOf, errors.NotFoundf(""))
   453  }
   454  
   455  // createImageMetadata creates some image metadata in a local directory.
   456  func createImageMetadata(c *gc.C) (string, []*imagemetadata.ImageMetadata) {
   457  	// Generate some image metadata.
   458  	im := []*imagemetadata.ImageMetadata{
   459  		{
   460  			Id:         "1234",
   461  			Arch:       "amd64",
   462  			Version:    "13.04",
   463  			RegionName: "region",
   464  			Endpoint:   "endpoint",
   465  		},
   466  	}
   467  	cloudSpec := &simplestreams.CloudSpec{
   468  		Region:   "region",
   469  		Endpoint: "endpoint",
   470  	}
   471  	sourceDir := c.MkDir()
   472  	sourceStor, err := filestorage.NewFileStorageWriter(sourceDir)
   473  	c.Assert(err, gc.IsNil)
   474  	err = imagemetadata.MergeAndWriteMetadata("raring", im, cloudSpec, sourceStor)
   475  	c.Assert(err, gc.IsNil)
   476  	return sourceDir, im
   477  }
   478  
   479  // checkImageMetadata checks that the environment contains the expected image metadata.
   480  func checkImageMetadata(c *gc.C, stor storage.StorageReader, expected []*imagemetadata.ImageMetadata) {
   481  	metadata := imtesting.ParseMetadataFromStorage(c, stor)
   482  	c.Assert(metadata, gc.HasLen, 1)
   483  	c.Assert(expected[0], gc.DeepEquals, metadata[0])
   484  }
   485  
   486  func (s *BootstrapSuite) TestUploadLocalImageMetadata(c *gc.C) {
   487  	sourceDir, expected := createImageMetadata(c)
   488  	env := resetJujuHome(c)
   489  
   490  	// Bootstrap the environment with the valid source.
   491  	// Force a dev version by having an odd minor version number.
   492  	// This is because we have not uploaded any tools and auto
   493  	// upload is only enabled for dev versions.
   494  	devVersion := version.Current
   495  	devVersion.Minor = 11
   496  	s.PatchValue(&version.Current, devVersion)
   497  
   498  	_, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir)
   499  	c.Assert(err, gc.IsNil)
   500  	c.Assert(imagemetadata.DefaultBaseURL, gc.Equals, imagemetadata.UbuntuCloudImagesURL)
   501  
   502  	// Now check the image metadata has been uploaded.
   503  	checkImageMetadata(c, env.Storage(), expected)
   504  }
   505  
   506  func (s *BootstrapSuite) TestValidateConstraintsCalledWithMetadatasource(c *gc.C) {
   507  	sourceDir, _ := createImageMetadata(c)
   508  	resetJujuHome(c)
   509  	var calledFuncs []string
   510  	s.PatchValue(&uploadCustomMetadata, func(metadataDir string, env environs.Environ) error {
   511  		c.Assert(metadataDir, gc.DeepEquals, sourceDir)
   512  		calledFuncs = append(calledFuncs, "uploadCustomMetadata")
   513  		return nil
   514  	})
   515  	s.PatchValue(&validateConstraints, func(cons constraints.Value, env environs.Environ) error {
   516  		c.Assert(cons, gc.DeepEquals, constraints.MustParse("mem=4G"))
   517  		calledFuncs = append(calledFuncs, "validateConstraints")
   518  		return nil
   519  	})
   520  	_, err := coretesting.RunCommand(
   521  		c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir, "--constraints", "mem=4G")
   522  	c.Assert(err, gc.IsNil)
   523  	c.Assert(calledFuncs, gc.DeepEquals, []string{"uploadCustomMetadata", "validateConstraints"})
   524  }
   525  
   526  func (s *BootstrapSuite) TestValidateConstraintsCalledWithoutMetadatasource(c *gc.C) {
   527  	validateCalled := 0
   528  	s.PatchValue(&validateConstraints, func(cons constraints.Value, env environs.Environ) error {
   529  		c.Assert(cons, gc.DeepEquals, constraints.MustParse("mem=4G"))
   530  		validateCalled++
   531  		return nil
   532  	})
   533  	resetJujuHome(c)
   534  	_, err := coretesting.RunCommand(
   535  		c, envcmd.Wrap(&BootstrapCommand{}), "--constraints", "mem=4G")
   536  	c.Assert(err, gc.IsNil)
   537  	c.Assert(validateCalled, gc.Equals, 1)
   538  }
   539  
   540  func (s *BootstrapSuite) TestAutoSyncLocalSource(c *gc.C) {
   541  	sourceDir := createToolsSource(c, vAll)
   542  	s.PatchValue(&version.Current.Number, version.MustParse("1.2.0"))
   543  	env := resetJujuHome(c)
   544  
   545  	// Bootstrap the environment with the valid source.
   546  	// The bootstrapping has to show no error, because the tools
   547  	// are automatically synchronized.
   548  	_, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}), "--metadata-source", sourceDir)
   549  	c.Assert(err, gc.IsNil)
   550  
   551  	// Now check the available tools which are the 1.2.0 envtools.
   552  	checkTools(c, env, v120All)
   553  }
   554  
   555  func (s *BootstrapSuite) setupAutoUploadTest(c *gc.C, vers, series string) environs.Environ {
   556  	s.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c))
   557  	sourceDir := createToolsSource(c, vAll)
   558  	s.PatchValue(&envtools.DefaultBaseURL, sourceDir)
   559  
   560  	// Change the tools location to be the test location and also
   561  	// the version and ensure their later restoring.
   562  	// Set the current version to be something for which there are no tools
   563  	// so we can test that an upload is forced.
   564  	origVersion := version.Current
   565  	version.Current.Number = version.MustParse(vers)
   566  	version.Current.Series = series
   567  	s.AddCleanup(func(*gc.C) { version.Current = origVersion })
   568  
   569  	// Create home with dummy provider and remove all
   570  	// of its envtools.
   571  	return resetJujuHome(c)
   572  }
   573  
   574  func (s *BootstrapSuite) TestAutoUploadAfterFailedSync(c *gc.C) {
   575  	s.PatchValue(&version.Current.Series, config.LatestLtsSeries())
   576  	otherSeries := "quantal"
   577  
   578  	env := s.setupAutoUploadTest(c, "1.7.3", otherSeries)
   579  	// Run command and check for that upload has been run for tools matching the current juju version.
   580  	opc, errc := runCommand(nullContext(c), envcmd.Wrap(new(BootstrapCommand)))
   581  	c.Assert(<-errc, gc.IsNil)
   582  	c.Assert((<-opc).(dummy.OpPutFile).Env, gc.Equals, "peckham")
   583  	list, err := envtools.FindTools(env, version.Current.Major, version.Current.Minor, coretools.Filter{}, false)
   584  	c.Assert(err, gc.IsNil)
   585  	c.Logf("found: " + list.String())
   586  	urls := list.URLs()
   587  
   588  	// We expect:
   589  	//     supported LTS series precise, trusty,
   590  	//     the specified series (quantal),
   591  	//     and the environment's default series (raring).
   592  	expectedVers := []version.Binary{
   593  		version.MustParseBinary(fmt.Sprintf("1.7.3.1-%s-%s", "quantal", version.Current.Arch)),
   594  		version.MustParseBinary(fmt.Sprintf("1.7.3.1-%s-%s", "raring", version.Current.Arch)),
   595  		version.MustParseBinary(fmt.Sprintf("1.7.3.1-%s-%s", "precise", version.Current.Arch)),
   596  		version.MustParseBinary(fmt.Sprintf("1.7.3.1-%s-%s", "trusty", version.Current.Arch)),
   597  	}
   598  	c.Assert(urls, gc.HasLen, len(expectedVers))
   599  	for _, vers := range expectedVers {
   600  		c.Logf("seeking: " + vers.String())
   601  		_, found := urls[vers]
   602  		c.Check(found, gc.Equals, true)
   603  	}
   604  }
   605  
   606  func (s *BootstrapSuite) TestAutoUploadOnlyForDev(c *gc.C) {
   607  	s.setupAutoUploadTest(c, "1.8.3", "precise")
   608  	_, errc := runCommand(nullContext(c), envcmd.Wrap(new(BootstrapCommand)))
   609  	err := <-errc
   610  	stripped := strings.Replace(err.Error(), "\n", "", -1)
   611  	c.Assert(stripped, gc.Matches, noToolsAvailableMessage)
   612  }
   613  
   614  func (s *BootstrapSuite) TestMissingToolsError(c *gc.C) {
   615  	s.setupAutoUploadTest(c, "1.8.3", "precise")
   616  
   617  	_, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}))
   618  	c.Assert(err, gc.ErrorMatches, "cannot upload bootstrap tools: Juju "+
   619  		"cannot bootstrap because no tools are available for your "+
   620  		"environment(.|\n)*")
   621  }
   622  
   623  func uploadToolsAlwaysFails(stor storage.Storage, forceVersion *version.Number, series ...string) (*coretools.Tools, error) {
   624  	return nil, fmt.Errorf("an error")
   625  }
   626  
   627  func (s *BootstrapSuite) TestMissingToolsUploadFailedError(c *gc.C) {
   628  	s.setupAutoUploadTest(c, "1.7.3", "precise")
   629  	s.PatchValue(&sync.Upload, uploadToolsAlwaysFails)
   630  
   631  	ctx, err := coretesting.RunCommand(c, envcmd.Wrap(&BootstrapCommand{}))
   632  
   633  	c.Check(coretesting.Stderr(ctx), gc.Matches,
   634  		"uploading tools for series \\[precise raring .*\\]\n")
   635  	c.Check(err, gc.ErrorMatches, "cannot upload bootstrap tools: an error")
   636  }
   637  
   638  func (s *BootstrapSuite) TestBootstrapDestroy(c *gc.C) {
   639  	resetJujuHome(c)
   640  	devVersion := version.Current
   641  	// Force a dev version by having an odd minor version number.
   642  	// This is because we have not uploaded any tools and auto
   643  	// upload is only enabled for dev versions.
   644  	devVersion.Minor = 11
   645  	s.PatchValue(&version.Current, devVersion)
   646  	opc, errc := runCommand(nullContext(c), envcmd.Wrap(new(BootstrapCommand)), "-e", "brokenenv")
   647  	err := <-errc
   648  	c.Assert(err, gc.ErrorMatches, "dummy.Bootstrap is broken")
   649  	var opDestroy *dummy.OpDestroy
   650  	for opDestroy == nil {
   651  		select {
   652  		case op := <-opc:
   653  			switch op := op.(type) {
   654  			case dummy.OpDestroy:
   655  				opDestroy = &op
   656  			}
   657  		default:
   658  			c.Error("expected call to env.Destroy")
   659  			return
   660  		}
   661  	}
   662  	c.Assert(opDestroy.Error, gc.ErrorMatches, "dummy.Destroy is broken")
   663  }
   664  
   665  // createToolsSource writes the mock tools and metadata into a temporary
   666  // directory and returns it.
   667  func createToolsSource(c *gc.C, versions []version.Binary) string {
   668  	versionStrings := make([]string, len(versions))
   669  	for i, vers := range versions {
   670  		versionStrings[i] = vers.String()
   671  	}
   672  	source := c.MkDir()
   673  	toolstesting.MakeTools(c, source, "releases", versionStrings)
   674  	return source
   675  }
   676  
   677  // resetJujuHome restores an new, clean Juju home environment without tools.
   678  func resetJujuHome(c *gc.C) environs.Environ {
   679  	jenvDir := gitjujutesting.HomePath(".juju", "environments")
   680  	err := os.RemoveAll(jenvDir)
   681  	c.Assert(err, gc.IsNil)
   682  	coretesting.WriteEnvironments(c, envConfig)
   683  	dummy.Reset()
   684  	store, err := configstore.Default()
   685  	c.Assert(err, gc.IsNil)
   686  	env, err := environs.PrepareFromName("peckham", nullContext(c), store)
   687  	c.Assert(err, gc.IsNil)
   688  	envtesting.RemoveAllTools(c, env)
   689  	return env
   690  }
   691  
   692  // checkTools check if the environment contains the passed envtools.
   693  func checkTools(c *gc.C, env environs.Environ, expected []version.Binary) {
   694  	list, err := envtools.FindTools(
   695  		env, version.Current.Major, version.Current.Minor, coretools.Filter{}, envtools.DoNotAllowRetry)
   696  	c.Check(err, gc.IsNil)
   697  	c.Logf("found: " + list.String())
   698  	urls := list.URLs()
   699  	c.Check(urls, gc.HasLen, len(expected))
   700  }
   701  
   702  var (
   703  	v100d64 = version.MustParseBinary("1.0.0-raring-amd64")
   704  	v100p64 = version.MustParseBinary("1.0.0-precise-amd64")
   705  	v100q32 = version.MustParseBinary("1.0.0-quantal-i386")
   706  	v100q64 = version.MustParseBinary("1.0.0-quantal-amd64")
   707  	v120d64 = version.MustParseBinary("1.2.0-raring-amd64")
   708  	v120p64 = version.MustParseBinary("1.2.0-precise-amd64")
   709  	v120q32 = version.MustParseBinary("1.2.0-quantal-i386")
   710  	v120q64 = version.MustParseBinary("1.2.0-quantal-amd64")
   711  	v120t32 = version.MustParseBinary("1.2.0-trusty-i386")
   712  	v120t64 = version.MustParseBinary("1.2.0-trusty-amd64")
   713  	v190p32 = version.MustParseBinary("1.9.0-precise-i386")
   714  	v190q64 = version.MustParseBinary("1.9.0-quantal-amd64")
   715  	v200p64 = version.MustParseBinary("2.0.0-precise-amd64")
   716  	v100All = []version.Binary{
   717  		v100d64, v100p64, v100q64, v100q32,
   718  	}
   719  	v120All = []version.Binary{
   720  		v120d64, v120p64, v120q64, v120q32, v120t32, v120t64,
   721  	}
   722  	v190All = []version.Binary{
   723  		v190p32, v190q64,
   724  	}
   725  	v200All = []version.Binary{
   726  		v200p64,
   727  	}
   728  	vAll = joinBinaryVersions(v100All, v120All, v190All, v200All)
   729  )
   730  
   731  func joinBinaryVersions(versions ...[]version.Binary) []version.Binary {
   732  	var all []version.Binary
   733  	for _, versions := range versions {
   734  		all = append(all, versions...)
   735  	}
   736  	return all
   737  }
   738  
   739  // TODO(menn0): This fake BootstrapInterface implementation is
   740  // currently quite minimal but could be easily extended to cover more
   741  // test scenarios. This could help improve some of the tests in this
   742  // file which execute large amounts of external functionality.
   743  type fakeBootstrapFuncs struct {
   744  	uploadToolsSeries []string
   745  }
   746  
   747  func (fake *fakeBootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
   748  	return nil
   749  }
   750  
   751  func (fake *fakeBootstrapFuncs) UploadTools(ctx environs.BootstrapContext, env environs.Environ, toolsArch *string, forceVersion bool, bootstrapSeries ...string) error {
   752  	fake.uploadToolsSeries = bootstrapSeries
   753  	return nil
   754  }
   755  
   756  func (fake fakeBootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args environs.BootstrapParams) error {
   757  	return nil
   758  }