launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/tools/tools_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package tools_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/loggo/loggo"
    17  	gc "launchpad.net/gocheck"
    18  
    19  	"launchpad.net/juju-core/environs"
    20  	"launchpad.net/juju-core/environs/config"
    21  	"launchpad.net/juju-core/environs/configstore"
    22  	"launchpad.net/juju-core/environs/simplestreams"
    23  	"launchpad.net/juju-core/environs/storage"
    24  	envtesting "launchpad.net/juju-core/environs/testing"
    25  	envtools "launchpad.net/juju-core/environs/tools"
    26  	"launchpad.net/juju-core/errors"
    27  	"launchpad.net/juju-core/provider/dummy"
    28  	jc "launchpad.net/juju-core/testing/checkers"
    29  	"launchpad.net/juju-core/testing/testbase"
    30  	coretools "launchpad.net/juju-core/tools"
    31  	"launchpad.net/juju-core/version"
    32  )
    33  
    34  type SimpleStreamsToolsSuite struct {
    35  	env environs.Environ
    36  	testbase.LoggingSuite
    37  	envtesting.ToolsFixture
    38  	origCurrentVersion version.Binary
    39  	customToolsDir     string
    40  	publicToolsDir     string
    41  }
    42  
    43  func setupToolsTests() {
    44  	gc.Suite(&SimpleStreamsToolsSuite{})
    45  }
    46  
    47  func (s *SimpleStreamsToolsSuite) SetUpSuite(c *gc.C) {
    48  	s.LoggingSuite.SetUpSuite(c)
    49  	s.customToolsDir = c.MkDir()
    50  	s.publicToolsDir = c.MkDir()
    51  }
    52  
    53  func (s *SimpleStreamsToolsSuite) SetUpTest(c *gc.C) {
    54  	s.ToolsFixture.DefaultBaseURL = "file://" + s.publicToolsDir
    55  	s.LoggingSuite.SetUpTest(c)
    56  	s.ToolsFixture.SetUpTest(c)
    57  	s.origCurrentVersion = version.Current
    58  	s.reset(c, nil)
    59  }
    60  
    61  func (s *SimpleStreamsToolsSuite) TearDownTest(c *gc.C) {
    62  	dummy.Reset()
    63  	version.Current = s.origCurrentVersion
    64  	s.ToolsFixture.TearDownTest(c)
    65  	s.LoggingSuite.TearDownTest(c)
    66  }
    67  
    68  func (s *SimpleStreamsToolsSuite) reset(c *gc.C, attrs map[string]interface{}) {
    69  	final := map[string]interface{}{
    70  		"tools-metadata-url": "file://" + s.customToolsDir,
    71  	}
    72  	for k, v := range attrs {
    73  		final[k] = v
    74  	}
    75  	s.resetEnv(c, final)
    76  }
    77  
    78  func (s *SimpleStreamsToolsSuite) removeTools(c *gc.C) {
    79  	for _, dir := range []string{s.customToolsDir, s.publicToolsDir} {
    80  		files, err := ioutil.ReadDir(dir)
    81  		c.Assert(err, gc.IsNil)
    82  		for _, f := range files {
    83  			err := os.RemoveAll(filepath.Join(dir, f.Name()))
    84  			c.Assert(err, gc.IsNil)
    85  		}
    86  	}
    87  }
    88  
    89  type metadataFile struct {
    90  	path string
    91  	data []byte
    92  }
    93  
    94  func (s *SimpleStreamsToolsSuite) generateMetadata(c *gc.C, verses ...version.Binary) []metadataFile {
    95  	var metadata = make([]*envtools.ToolsMetadata, len(verses))
    96  	for i, vers := range verses {
    97  		basePath := fmt.Sprintf("releases/tools-%s.tar.gz", vers.String())
    98  		metadata[i] = &envtools.ToolsMetadata{
    99  			Release: vers.Series,
   100  			Version: vers.Number.String(),
   101  			Arch:    vers.Arch,
   102  			Path:    basePath,
   103  		}
   104  	}
   105  	index, products, err := envtools.MarshalToolsMetadataJSON(metadata, time.Now())
   106  	c.Assert(err, gc.IsNil)
   107  	objects := []metadataFile{
   108  		{simplestreams.UnsignedIndex, index},
   109  		{envtools.ProductMetadataPath, products},
   110  	}
   111  	return objects
   112  }
   113  
   114  func (s *SimpleStreamsToolsSuite) uploadToStorage(c *gc.C, stor storage.Storage, verses ...version.Binary) map[version.Binary]string {
   115  	uploaded := map[version.Binary]string{}
   116  	if len(verses) == 0 {
   117  		return uploaded
   118  	}
   119  	var err error
   120  	for _, vers := range verses {
   121  		filename := fmt.Sprintf("tools/releases/tools-%s.tar.gz", vers.String())
   122  		// Put a file in images since the dummy storage provider requires a
   123  		// file to exist before the URL can be found. This is to ensure it behaves
   124  		// the same way as MAAS.
   125  		err = stor.Put(filename, strings.NewReader("dummy"), 5)
   126  		c.Assert(err, gc.IsNil)
   127  		uploaded[vers], err = stor.URL(filename)
   128  		c.Assert(err, gc.IsNil)
   129  	}
   130  	objects := s.generateMetadata(c, verses...)
   131  	for _, object := range objects {
   132  		toolspath := path.Join("tools", object.path)
   133  		err = stor.Put(toolspath, bytes.NewReader(object.data), int64(len(object.data)))
   134  		c.Assert(err, gc.IsNil)
   135  	}
   136  	return uploaded
   137  }
   138  
   139  func (s *SimpleStreamsToolsSuite) uploadVersions(c *gc.C, dir string, verses ...version.Binary) map[version.Binary]string {
   140  	uploaded := map[version.Binary]string{}
   141  	if len(verses) == 0 {
   142  		return uploaded
   143  	}
   144  	for _, vers := range verses {
   145  		basePath := fmt.Sprintf("releases/tools-%s.tar.gz", vers.String())
   146  		uploaded[vers] = fmt.Sprintf("file://%s/%s", dir, basePath)
   147  	}
   148  	objects := s.generateMetadata(c, verses...)
   149  	for _, object := range objects {
   150  		path := filepath.Join(dir, object.path)
   151  		dir := filepath.Dir(path)
   152  		if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
   153  			c.Assert(err, gc.IsNil)
   154  		}
   155  		err := ioutil.WriteFile(path, object.data, 0644)
   156  		c.Assert(err, gc.IsNil)
   157  	}
   158  	return uploaded
   159  }
   160  
   161  func (s *SimpleStreamsToolsSuite) uploadCustom(c *gc.C, verses ...version.Binary) map[version.Binary]string {
   162  	return s.uploadVersions(c, s.customToolsDir, verses...)
   163  }
   164  
   165  func (s *SimpleStreamsToolsSuite) uploadPublic(c *gc.C, verses ...version.Binary) map[version.Binary]string {
   166  	return s.uploadVersions(c, s.publicToolsDir, verses...)
   167  }
   168  
   169  func (s *SimpleStreamsToolsSuite) resetEnv(c *gc.C, attrs map[string]interface{}) {
   170  	version.Current = s.origCurrentVersion
   171  	dummy.Reset()
   172  	cfg, err := config.New(config.NoDefaults, dummy.SampleConfig().Merge(attrs))
   173  	c.Assert(err, gc.IsNil)
   174  	env, err := environs.Prepare(cfg, configstore.NewMem())
   175  	c.Assert(err, gc.IsNil)
   176  	s.env = env
   177  	s.removeTools(c)
   178  }
   179  
   180  var findToolsTests = []struct {
   181  	info   string
   182  	major  int
   183  	minor  int
   184  	custom []version.Binary
   185  	public []version.Binary
   186  	expect []version.Binary
   187  	err    error
   188  }{{
   189  	info:  "none available anywhere",
   190  	major: 1,
   191  	err:   envtools.ErrNoTools,
   192  }, {
   193  	info:   "custom/private tools only, none matching",
   194  	major:  1,
   195  	minor:  2,
   196  	custom: envtesting.V220all,
   197  	err:    coretools.ErrNoMatches,
   198  }, {
   199  	info:   "custom tools found",
   200  	major:  1,
   201  	minor:  2,
   202  	custom: envtesting.VAll,
   203  	expect: envtesting.V120all,
   204  }, {
   205  	info:   "public tools found",
   206  	major:  1,
   207  	minor:  1,
   208  	public: envtesting.VAll,
   209  	expect: envtesting.V110all,
   210  }, {
   211  	info:   "public and custom tools found, only taken from custom",
   212  	major:  1,
   213  	minor:  1,
   214  	custom: envtesting.V110p,
   215  	public: envtesting.VAll,
   216  	expect: envtesting.V110p,
   217  }, {
   218  	info:   "custom tools completely block public ones",
   219  	major:  1,
   220  	minor:  -1,
   221  	custom: envtesting.V220all,
   222  	public: envtesting.VAll,
   223  	expect: envtesting.V1all,
   224  }, {
   225  	info:   "tools matching major version only",
   226  	major:  1,
   227  	minor:  -1,
   228  	public: envtesting.VAll,
   229  	expect: envtesting.V1all,
   230  }}
   231  
   232  func (s *SimpleStreamsToolsSuite) TestFindTools(c *gc.C) {
   233  	for i, test := range findToolsTests {
   234  		c.Logf("\ntest %d: %s", i, test.info)
   235  		s.reset(c, nil)
   236  		custom := s.uploadCustom(c, test.custom...)
   237  		public := s.uploadPublic(c, test.public...)
   238  		actual, err := envtools.FindTools(s.env, test.major, test.minor, coretools.Filter{}, envtools.DoNotAllowRetry)
   239  		if test.err != nil {
   240  			if len(actual) > 0 {
   241  				c.Logf(actual.String())
   242  			}
   243  			c.Check(err, jc.Satisfies, errors.IsNotFoundError)
   244  			continue
   245  		}
   246  		expect := map[version.Binary]string{}
   247  		for _, expected := range test.expect {
   248  			// If the tools exist in custom, that's preferred.
   249  			var ok bool
   250  			if expect[expected], ok = custom[expected]; !ok {
   251  				expect[expected] = public[expected]
   252  			}
   253  		}
   254  		c.Check(actual.URLs(), gc.DeepEquals, expect)
   255  	}
   256  }
   257  
   258  func (s *SimpleStreamsToolsSuite) TestFindToolsInControlBucket(c *gc.C) {
   259  	s.reset(c, nil)
   260  	custom := s.uploadToStorage(c, s.env.Storage(), envtesting.V110p...)
   261  	s.uploadPublic(c, envtesting.VAll...)
   262  	actual, err := envtools.FindTools(s.env, 1, 1, coretools.Filter{}, envtools.DoNotAllowRetry)
   263  	c.Assert(err, gc.IsNil)
   264  	expect := map[version.Binary]string{}
   265  	for _, expected := range envtesting.V110p {
   266  		expect[expected] = custom[expected]
   267  	}
   268  	c.Assert(actual.URLs(), gc.DeepEquals, expect)
   269  }
   270  
   271  func (s *SimpleStreamsToolsSuite) TestFindToolsFiltering(c *gc.C) {
   272  	tw := &loggo.TestWriter{}
   273  	c.Assert(loggo.RegisterWriter("filter-tester", tw, loggo.DEBUG), gc.IsNil)
   274  	defer loggo.RemoveWriter("filter-tester")
   275  	_, err := envtools.FindTools(
   276  		s.env, 1, -1, coretools.Filter{Number: version.Number{Major: 1, Minor: 2, Patch: 3}}, envtools.DoNotAllowRetry)
   277  	c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
   278  	// This is slightly overly prescriptive, but feel free to change or add
   279  	// messages. This still helps to ensure that all log messages are
   280  	// properly formed.
   281  	messages := []jc.SimpleMessage{
   282  		{loggo.INFO, "reading tools with major version 1"},
   283  		{loggo.INFO, "filtering tools by version: \\d+\\.\\d+\\.\\d+"},
   284  		{loggo.DEBUG, "no architecture specified when finding tools, looking for any"},
   285  		{loggo.DEBUG, "no series specified when finding tools, looking for any"},
   286  	}
   287  	sources, err := envtools.GetMetadataSources(s.env)
   288  	c.Assert(err, gc.IsNil)
   289  	for i := 0; i < 2*len(sources); i++ {
   290  		messages = append(messages,
   291  			jc.SimpleMessage{loggo.DEBUG, `fetchData failed for .*`},
   292  			jc.SimpleMessage{loggo.DEBUG, `cannot load index .*`})
   293  	}
   294  	c.Check(tw.Log, jc.LogMatches, messages)
   295  }
   296  
   297  func (s *SimpleStreamsToolsSuite) TestFindBootstrapTools(c *gc.C) {
   298  	// Remove the default tools URL from the search path, just look in cloud storage.
   299  	s.PatchValue(&envtools.DefaultBaseURL, "")
   300  	for i, test := range envtesting.BootstrapToolsTests {
   301  		c.Logf("\ntest %d: %s", i, test.Info)
   302  		attrs := map[string]interface{}{
   303  			"development": test.Development,
   304  		}
   305  		var agentVersion *version.Number
   306  		if test.AgentVersion != version.Zero {
   307  			attrs["agent-version"] = test.AgentVersion.String()
   308  			agentVersion = &test.AgentVersion
   309  		}
   310  		s.reset(c, attrs)
   311  		version.Current = test.CliVersion
   312  		available := s.uploadCustom(c, test.Available...)
   313  
   314  		params := envtools.BootstrapToolsParams{
   315  			Version: agentVersion,
   316  			Series:  test.DefaultSeries,
   317  			Arch:    &test.Arch,
   318  		}
   319  		actual, err := envtools.FindBootstrapTools(s.env, params)
   320  		if test.Err != nil {
   321  			if len(actual) > 0 {
   322  				c.Logf(actual.String())
   323  			}
   324  			c.Check(err, jc.Satisfies, errors.IsNotFoundError)
   325  			continue
   326  		}
   327  		expect := map[version.Binary]string{}
   328  		for _, expected := range test.Expect {
   329  			expect[expected] = available[expected]
   330  		}
   331  		c.Check(actual.URLs(), gc.DeepEquals, expect)
   332  	}
   333  }
   334  
   335  var findInstanceToolsTests = []struct {
   336  	info         string
   337  	available    []version.Binary
   338  	agentVersion version.Number
   339  	series       string
   340  	arch         string
   341  	expect       []version.Binary
   342  	err          error
   343  }{{
   344  	info:         "nothing at all",
   345  	agentVersion: envtesting.V120,
   346  	series:       "precise",
   347  	err:          envtools.ErrNoTools,
   348  }, {
   349  	info:         "nothing matching 1",
   350  	available:    envtesting.V100Xall,
   351  	agentVersion: envtesting.V120,
   352  	series:       "precise",
   353  	err:          coretools.ErrNoMatches,
   354  }, {
   355  	info:         "nothing matching 2",
   356  	available:    envtesting.V120all,
   357  	agentVersion: envtesting.V110,
   358  	series:       "precise",
   359  	err:          coretools.ErrNoMatches,
   360  }, {
   361  	info:         "nothing matching 3",
   362  	available:    envtesting.V120q,
   363  	agentVersion: envtesting.V120,
   364  	series:       "precise",
   365  	err:          coretools.ErrNoMatches,
   366  }, {
   367  	info:         "nothing matching 4",
   368  	available:    envtesting.V120q,
   369  	agentVersion: envtesting.V120,
   370  	series:       "quantal",
   371  	arch:         "arm",
   372  	err:          coretools.ErrNoMatches,
   373  }, {
   374  	info:         "actual match 1",
   375  	available:    envtesting.VAll,
   376  	agentVersion: envtesting.V1001,
   377  	series:       "precise",
   378  	expect:       []version.Binary{envtesting.V1001p64},
   379  }, {
   380  	info:         "actual match 2",
   381  	available:    envtesting.VAll,
   382  	agentVersion: envtesting.V120,
   383  	series:       "quantal",
   384  	expect:       []version.Binary{envtesting.V120q64, envtesting.V120q32},
   385  }, {
   386  	info:         "actual match 3",
   387  	available:    envtesting.VAll,
   388  	agentVersion: envtesting.V110,
   389  	series:       "quantal",
   390  	arch:         "i386",
   391  	expect:       []version.Binary{envtesting.V110q32},
   392  }}
   393  
   394  func (s *SimpleStreamsToolsSuite) TestFindInstanceTools(c *gc.C) {
   395  	for i, test := range findInstanceToolsTests {
   396  		c.Logf("\ntest %d: %s", i, test.info)
   397  		s.reset(c, map[string]interface{}{
   398  			"agent-version": test.agentVersion.String(),
   399  		})
   400  		available := s.uploadCustom(c, test.available...)
   401  
   402  		agentVersion, _ := s.env.Config().AgentVersion()
   403  		actual, err := envtools.FindInstanceTools(s.env, agentVersion, test.series, &test.arch)
   404  		if test.err != nil {
   405  			if len(actual) > 0 {
   406  				c.Logf(actual.String())
   407  			}
   408  			c.Check(err, jc.Satisfies, errors.IsNotFoundError)
   409  			continue
   410  		}
   411  		expect := map[version.Binary]string{}
   412  		for _, expected := range test.expect {
   413  			expect[expected] = available[expected]
   414  		}
   415  		c.Check(actual.URLs(), gc.DeepEquals, expect)
   416  	}
   417  }
   418  
   419  var findExactToolsTests = []struct {
   420  	info   string
   421  	custom []version.Binary
   422  	public []version.Binary
   423  	seek   version.Binary
   424  	err    error
   425  }{{
   426  	info: "nothing available",
   427  	seek: envtesting.V100p64,
   428  	err:  envtools.ErrNoTools,
   429  }, {
   430  	info:   "only non-matches available in custom",
   431  	custom: append(envtesting.V110all, envtesting.V100p32, envtesting.V100q64, envtesting.V1001p64),
   432  	seek:   envtesting.V100p64,
   433  	err:    coretools.ErrNoMatches,
   434  }, {
   435  	info:   "exact match available in custom",
   436  	custom: []version.Binary{envtesting.V100p64},
   437  	seek:   envtesting.V100p64,
   438  }, {
   439  	info:   "only non-matches available in public",
   440  	custom: append(envtesting.V110all, envtesting.V100p32, envtesting.V100q64, envtesting.V1001p64),
   441  	seek:   envtesting.V100p64,
   442  	err:    coretools.ErrNoMatches,
   443  }, {
   444  	info:   "exact match available in public",
   445  	public: []version.Binary{envtesting.V100p64},
   446  	seek:   envtesting.V100p64,
   447  }, {
   448  	info:   "exact match in public not blocked by custom",
   449  	custom: envtesting.V110all,
   450  	public: []version.Binary{envtesting.V100p64},
   451  	seek:   envtesting.V100p64,
   452  }}
   453  
   454  func (s *SimpleStreamsToolsSuite) TestFindExactTools(c *gc.C) {
   455  	for i, test := range findExactToolsTests {
   456  		c.Logf("\ntest %d: %s", i, test.info)
   457  		s.reset(c, nil)
   458  		custom := s.uploadCustom(c, test.custom...)
   459  		public := s.uploadPublic(c, test.public...)
   460  		actual, err := envtools.FindExactTools(s.env, test.seek.Number, test.seek.Series, test.seek.Arch)
   461  		if test.err == nil {
   462  			if !c.Check(err, gc.IsNil) {
   463  				continue
   464  			}
   465  			c.Check(actual.Version, gc.Equals, test.seek)
   466  			if _, ok := custom[actual.Version]; ok {
   467  				c.Check(actual.URL, gc.DeepEquals, custom[actual.Version])
   468  			} else {
   469  				c.Check(actual.URL, gc.DeepEquals, public[actual.Version])
   470  			}
   471  		} else {
   472  			c.Check(err, jc.Satisfies, errors.IsNotFoundError)
   473  		}
   474  	}
   475  }
   476  
   477  // fakeToolsForSeries fakes a Tools object with just enough information for
   478  // testing the handling its OS series.
   479  func fakeToolsForSeries(series string) *coretools.Tools {
   480  	return &coretools.Tools{Version: version.Binary{Series: series}}
   481  }
   482  
   483  // fakeToolsList fakes a envtools.List containing Tools objects for the given
   484  // respective series, in the same number and order.
   485  func fakeToolsList(series ...string) coretools.List {
   486  	list := coretools.List{}
   487  	for _, name := range series {
   488  		list = append(list, fakeToolsForSeries(name))
   489  	}
   490  	return list
   491  }
   492  
   493  type ToolsListSuite struct{}
   494  
   495  func (s *ToolsListSuite) TestCheckToolsSeriesRequiresTools(c *gc.C) {
   496  	err := envtools.CheckToolsSeries(fakeToolsList(), "precise")
   497  	c.Assert(err, gc.NotNil)
   498  	c.Check(err, gc.ErrorMatches, "expected single series, got \\[\\]")
   499  }
   500  
   501  func (s *ToolsListSuite) TestCheckToolsSeriesAcceptsOneSetOfTools(c *gc.C) {
   502  	names := []string{"precise", "raring"}
   503  	for _, series := range names {
   504  		list := fakeToolsList(series)
   505  		err := envtools.CheckToolsSeries(list, series)
   506  		c.Check(err, gc.IsNil)
   507  	}
   508  }
   509  
   510  func (s *ToolsListSuite) TestCheckToolsSeriesAcceptsMultipleForSameSeries(c *gc.C) {
   511  	series := "quantal"
   512  	list := fakeToolsList(series, series, series)
   513  	err := envtools.CheckToolsSeries(list, series)
   514  	c.Check(err, gc.IsNil)
   515  }
   516  
   517  func (s *ToolsListSuite) TestCheckToolsSeriesRejectsToolsForOtherSeries(c *gc.C) {
   518  	list := fakeToolsList("hoary")
   519  	err := envtools.CheckToolsSeries(list, "warty")
   520  	c.Assert(err, gc.NotNil)
   521  	c.Check(err, gc.ErrorMatches, "tools mismatch: expected series warty, got hoary")
   522  }
   523  
   524  func (s *ToolsListSuite) TestCheckToolsSeriesRejectsToolsForMixedSeries(c *gc.C) {
   525  	list := fakeToolsList("precise", "raring")
   526  	err := envtools.CheckToolsSeries(list, "precise")
   527  	c.Assert(err, gc.NotNil)
   528  	c.Check(err, gc.ErrorMatches, "expected single series, got .*")
   529  }