github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/environ_broker_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd_test
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  
    10  	"github.com/golang/mock/gomock"
    11  	jc "github.com/juju/testing/checkers"
    12  	"github.com/juju/utils/arch"
    13  	"github.com/lxc/lxd/shared/api"
    14  	gc "gopkg.in/check.v1"
    15  
    16  	"github.com/juju/juju/cloudconfig/cloudinit"
    17  	containerlxd "github.com/juju/juju/container/lxd"
    18  	lxdtesting "github.com/juju/juju/container/lxd/testing"
    19  	"github.com/juju/juju/core/constraints"
    20  	"github.com/juju/juju/environs/context"
    21  	"github.com/juju/juju/provider/lxd"
    22  )
    23  
    24  type environBrokerSuite struct {
    25  	lxd.EnvironSuite
    26  
    27  	callCtx           *context.CloudCallContext
    28  	defaultProfile    *api.Profile
    29  	invalidCredential bool
    30  }
    31  
    32  var _ = gc.Suite(&environBrokerSuite{})
    33  
    34  func (s *environBrokerSuite) SetUpTest(c *gc.C) {
    35  	s.BaseSuite.SetUpTest(c)
    36  	s.callCtx = &context.CloudCallContext{
    37  		InvalidateCredentialFunc: func(string) error {
    38  			s.invalidCredential = true
    39  			return nil
    40  		},
    41  	}
    42  	s.defaultProfile = &api.Profile{
    43  		ProfilePut: api.ProfilePut{
    44  			Devices: map[string]map[string]string{
    45  				"eth0": {},
    46  			},
    47  		},
    48  	}
    49  }
    50  
    51  func (s *environBrokerSuite) TearDownTest(c *gc.C) {
    52  	s.invalidCredential = false
    53  	s.BaseSuite.TearDownTest(c)
    54  }
    55  
    56  // containerSpecMatcher is a gomock matcher for testing a container spec
    57  // with a supplied validation func.
    58  type containerSpecMatcher struct {
    59  	check func(spec containerlxd.ContainerSpec) bool
    60  }
    61  
    62  func (m containerSpecMatcher) Matches(arg interface{}) bool {
    63  	if spec, ok := arg.(containerlxd.ContainerSpec); ok {
    64  		return m.check(spec)
    65  	}
    66  	return false
    67  }
    68  
    69  func (m containerSpecMatcher) String() string {
    70  	return fmt.Sprintf("%T", m.check)
    71  }
    72  
    73  func matchesContainerSpec(check func(spec containerlxd.ContainerSpec) bool) gomock.Matcher {
    74  	return containerSpecMatcher{check: check}
    75  }
    76  
    77  func (s *environBrokerSuite) TestStartInstanceDefaultNIC(c *gc.C) {
    78  	ctrl := gomock.NewController(c)
    79  	defer ctrl.Finish()
    80  	svr := lxd.NewMockServer(ctrl)
    81  
    82  	// Check that no custom devices were passed - vanilla cloud-init.
    83  	check := func(spec containerlxd.ContainerSpec) bool {
    84  		if spec.Config[containerlxd.NetworkConfigKey] != "" {
    85  			return false
    86  		}
    87  		return !(len(spec.Devices) > 0)
    88  	}
    89  
    90  	exp := svr.EXPECT()
    91  	gomock.InOrder(
    92  		exp.HostArch().Return(arch.AMD64),
    93  		exp.FindImage("bionic", arch.AMD64, gomock.Any(), true, gomock.Any()).Return(containerlxd.SourcedImage{}, nil),
    94  		exp.GetNICsFromProfile("default").Return(s.defaultProfile.Devices, nil),
    95  		exp.CreateContainerFromSpec(matchesContainerSpec(check)).Return(&containerlxd.Container{}, nil),
    96  		exp.HostArch().Return(arch.AMD64),
    97  	)
    98  
    99  	env := s.NewEnviron(c, svr, nil)
   100  	_, err := env.StartInstance(s.callCtx, s.GetStartInstanceArgs(c, "bionic"))
   101  	c.Assert(err, jc.ErrorIsNil)
   102  }
   103  
   104  func (s *environBrokerSuite) TestStartInstanceNonDefaultNIC(c *gc.C) {
   105  	ctrl := gomock.NewController(c)
   106  	defer ctrl.Finish()
   107  	svr := lxd.NewMockServer(ctrl)
   108  
   109  	nics := map[string]map[string]string{
   110  		"eno9": {
   111  			"name":    "eno9",
   112  			"mtu":     "9000",
   113  			"nictype": "bridged",
   114  			"parent":  "lxdbr0",
   115  			"hwaddr":  "00:00:00:00:00",
   116  		},
   117  	}
   118  
   119  	// Check that the non-standard devices were passed explicitly,
   120  	// And that we have disabled the standard network config.
   121  	check := func(spec containerlxd.ContainerSpec) bool {
   122  		if !reflect.DeepEqual(spec.Devices, nics) {
   123  			return false
   124  		}
   125  		return spec.Config[containerlxd.NetworkConfigKey] == cloudinit.CloudInitNetworkConfigDisabled
   126  	}
   127  
   128  	exp := svr.EXPECT()
   129  	gomock.InOrder(
   130  		exp.HostArch().Return(arch.AMD64),
   131  		exp.FindImage("bionic", arch.AMD64, gomock.Any(), true, gomock.Any()).Return(containerlxd.SourcedImage{}, nil),
   132  		exp.GetNICsFromProfile("default").Return(nics, nil),
   133  		exp.CreateContainerFromSpec(matchesContainerSpec(check)).Return(&containerlxd.Container{}, nil),
   134  		exp.HostArch().Return(arch.AMD64),
   135  	)
   136  
   137  	env := s.NewEnviron(c, svr, nil)
   138  	_, err := env.StartInstance(s.callCtx, s.GetStartInstanceArgs(c, "bionic"))
   139  	c.Assert(err, jc.ErrorIsNil)
   140  }
   141  
   142  func (s *environBrokerSuite) TestStartInstanceWithPlacementAvailable(c *gc.C) {
   143  	ctrl := gomock.NewController(c)
   144  	defer ctrl.Finish()
   145  	svr := lxd.NewMockServer(ctrl)
   146  
   147  	target := lxdtesting.NewMockContainerServer(ctrl)
   148  	tExp := target.EXPECT()
   149  	serverRet := &api.Server{}
   150  	image := &api.Image{Filename: "container-image"}
   151  
   152  	tExp.GetServer().Return(serverRet, lxdtesting.ETag, nil)
   153  	tExp.GetImageAlias("juju/bionic/amd64").Return(&api.ImageAliasesEntry{}, lxdtesting.ETag, nil)
   154  	tExp.GetImage("").Return(image, lxdtesting.ETag, nil)
   155  
   156  	jujuTarget, err := containerlxd.NewServer(target)
   157  	c.Assert(err, jc.ErrorIsNil)
   158  
   159  	members := []api.ClusterMember{
   160  		{
   161  			ServerName: "node01",
   162  			Status:     "ONLINE",
   163  		},
   164  		{
   165  			ServerName: "node02",
   166  			Status:     "ONLINE",
   167  		},
   168  	}
   169  
   170  	createOp := lxdtesting.NewMockRemoteOperation(ctrl)
   171  	createOp.EXPECT().Wait().Return(nil)
   172  	createOp.EXPECT().GetTarget().Return(&api.Operation{StatusCode: api.Success}, nil)
   173  
   174  	startOp := lxdtesting.NewMockOperation(ctrl)
   175  	startOp.EXPECT().Wait().Return(nil)
   176  
   177  	sExp := svr.EXPECT()
   178  	gomock.InOrder(
   179  		sExp.HostArch().Return(arch.AMD64),
   180  		sExp.IsClustered().Return(true),
   181  		sExp.GetClusterMembers().Return(members, nil),
   182  		sExp.UseTargetServer("node01").Return(jujuTarget, nil),
   183  		sExp.GetNICsFromProfile("default").Return(s.defaultProfile.Devices, nil),
   184  		sExp.HostArch().Return(arch.AMD64),
   185  	)
   186  
   187  	// CreateContainerFromSpec is tested in container/lxd.
   188  	// we don't bother with detailed parameter assertions here.
   189  	tExp.CreateContainerFromImage(gomock.Any(), gomock.Any(), gomock.Any()).Return(createOp, nil)
   190  	tExp.UpdateContainerState(gomock.Any(), gomock.Any(), "").Return(startOp, nil)
   191  	tExp.GetContainer(gomock.Any()).Return(&api.Container{}, lxdtesting.ETag, nil)
   192  
   193  	env := s.NewEnviron(c, svr, nil)
   194  
   195  	args := s.GetStartInstanceArgs(c, "bionic")
   196  	args.Placement = "zone=node01"
   197  
   198  	_, err = env.StartInstance(s.callCtx, args)
   199  	c.Assert(err, jc.ErrorIsNil)
   200  }
   201  
   202  func (s *environBrokerSuite) TestStartInstanceWithPlacementNotPresent(c *gc.C) {
   203  	ctrl := gomock.NewController(c)
   204  	defer ctrl.Finish()
   205  	svr := lxd.NewMockServer(ctrl)
   206  
   207  	members := []api.ClusterMember{{
   208  		ServerName: "node01",
   209  		Status:     "ONLINE",
   210  	}}
   211  
   212  	sExp := svr.EXPECT()
   213  	gomock.InOrder(
   214  		sExp.HostArch().Return(arch.AMD64),
   215  		sExp.IsClustered().Return(true),
   216  		sExp.GetClusterMembers().Return(members, nil),
   217  	)
   218  
   219  	env := s.NewEnviron(c, svr, nil)
   220  
   221  	args := s.GetStartInstanceArgs(c, "bionic")
   222  	args.Placement = "zone=node03"
   223  
   224  	_, err := env.StartInstance(s.callCtx, args)
   225  	c.Assert(err, gc.ErrorMatches, `availability zone "node03" not valid`)
   226  }
   227  
   228  func (s *environBrokerSuite) TestStartInstanceWithPlacementNotAvailable(c *gc.C) {
   229  	ctrl := gomock.NewController(c)
   230  	defer ctrl.Finish()
   231  	svr := lxd.NewMockServer(ctrl)
   232  
   233  	members := []api.ClusterMember{{
   234  		ServerName: "node01",
   235  		Status:     "OFFLINE",
   236  	}}
   237  
   238  	sExp := svr.EXPECT()
   239  	gomock.InOrder(
   240  		sExp.HostArch().Return(arch.AMD64),
   241  		sExp.IsClustered().Return(true),
   242  		sExp.GetClusterMembers().Return(members, nil),
   243  	)
   244  
   245  	env := s.NewEnviron(c, svr, nil)
   246  
   247  	args := s.GetStartInstanceArgs(c, "bionic")
   248  	args.Placement = "zone=node01"
   249  
   250  	_, err := env.StartInstance(s.callCtx, args)
   251  	c.Assert(err, gc.ErrorMatches, "availability zone \"node01\" is unavailable")
   252  }
   253  
   254  func (s *environBrokerSuite) TestStartInstanceWithPlacementBadArgument(c *gc.C) {
   255  	ctrl := gomock.NewController(c)
   256  	defer ctrl.Finish()
   257  	svr := lxd.NewMockServer(ctrl)
   258  
   259  	sExp := svr.EXPECT()
   260  	gomock.InOrder(
   261  		sExp.HostArch().Return(arch.AMD64),
   262  	)
   263  	env := s.NewEnviron(c, svr, nil)
   264  
   265  	args := s.GetStartInstanceArgs(c, "bionic")
   266  	args.Placement = "breakfast=eggs"
   267  
   268  	_, err := env.StartInstance(s.callCtx, args)
   269  	c.Assert(err, gc.ErrorMatches, "unknown placement directive.*")
   270  }
   271  
   272  func (s *environBrokerSuite) TestStartInstanceWithConstraints(c *gc.C) {
   273  	ctrl := gomock.NewController(c)
   274  	defer ctrl.Finish()
   275  	svr := lxd.NewMockServer(ctrl)
   276  
   277  	// Check that the constraints were passed through to spec.Config.
   278  	check := func(spec containerlxd.ContainerSpec) bool {
   279  		cfg := spec.Config
   280  		if cfg["limits.cpu"] != "2" {
   281  			return false
   282  		}
   283  		if cfg["limits.memory"] != "2048MiB" {
   284  			return false
   285  		}
   286  		return spec.InstanceType == "t2.micro"
   287  	}
   288  
   289  	exp := svr.EXPECT()
   290  	gomock.InOrder(
   291  		exp.HostArch().Return(arch.AMD64),
   292  		exp.FindImage("bionic", arch.AMD64, gomock.Any(), true, gomock.Any()).Return(containerlxd.SourcedImage{}, nil),
   293  		exp.GetNICsFromProfile("default").Return(s.defaultProfile.Devices, nil),
   294  		exp.CreateContainerFromSpec(matchesContainerSpec(check)).Return(&containerlxd.Container{}, nil),
   295  		exp.HostArch().Return(arch.AMD64),
   296  	)
   297  
   298  	args := s.GetStartInstanceArgs(c, "bionic")
   299  	cores := uint64(2)
   300  	mem := uint64(2048)
   301  	it := "t2.micro"
   302  	args.Constraints = constraints.Value{
   303  		CpuCores:     &cores,
   304  		Mem:          &mem,
   305  		InstanceType: &it,
   306  	}
   307  
   308  	env := s.NewEnviron(c, svr, nil)
   309  	_, err := env.StartInstance(s.callCtx, args)
   310  	c.Assert(err, jc.ErrorIsNil)
   311  }
   312  
   313  func (s *environBrokerSuite) TestStartInstanceWithCharmLXDProfile(c *gc.C) {
   314  	ctrl := gomock.NewController(c)
   315  	defer ctrl.Finish()
   316  	svr := lxd.NewMockServer(ctrl)
   317  
   318  	// Check that the lxd profile name was passed through to spec.Config.
   319  	check := func(spec containerlxd.ContainerSpec) bool {
   320  		profiles := spec.Profiles
   321  		if len(profiles) != 3 {
   322  			return false
   323  		}
   324  		if profiles[0] != "default" {
   325  			return false
   326  		}
   327  		if profiles[1] != "juju-" {
   328  			return false
   329  		}
   330  		return profiles[2] == "juju-model-test-0"
   331  	}
   332  
   333  	exp := svr.EXPECT()
   334  	gomock.InOrder(
   335  		exp.HostArch().Return(arch.AMD64),
   336  		exp.FindImage("bionic", arch.AMD64, gomock.Any(), true, gomock.Any()).Return(containerlxd.SourcedImage{}, nil),
   337  		exp.GetNICsFromProfile("default").Return(s.defaultProfile.Devices, nil),
   338  		exp.CreateContainerFromSpec(matchesContainerSpec(check)).Return(&containerlxd.Container{}, nil),
   339  		exp.HostArch().Return(arch.AMD64),
   340  	)
   341  
   342  	args := s.GetStartInstanceArgs(c, "bionic")
   343  	args.CharmLXDProfiles = []string{"juju-model-test-0"}
   344  
   345  	env := s.NewEnviron(c, svr, nil)
   346  	_, err := env.StartInstance(s.callCtx, args)
   347  	c.Assert(err, jc.ErrorIsNil)
   348  }
   349  
   350  func (s *environBrokerSuite) TestStartInstanceNoTools(c *gc.C) {
   351  	ctrl := gomock.NewController(c)
   352  	defer ctrl.Finish()
   353  	svr := lxd.NewMockServer(ctrl)
   354  
   355  	exp := svr.EXPECT()
   356  	exp.HostArch().Return(arch.PPC64EL)
   357  
   358  	env := s.NewEnviron(c, svr, nil)
   359  	_, err := env.StartInstance(s.callCtx, s.GetStartInstanceArgs(c, "bionic"))
   360  	c.Assert(err, gc.ErrorMatches, "no matching agent binaries available")
   361  }
   362  
   363  func (s *environBrokerSuite) TestStartInstanceInvalidCredentials(c *gc.C) {
   364  	c.Assert(s.invalidCredential, jc.IsFalse)
   365  	ctrl := gomock.NewController(c)
   366  	defer ctrl.Finish()
   367  	svr := lxd.NewMockServer(ctrl)
   368  
   369  	exp := svr.EXPECT()
   370  	gomock.InOrder(
   371  		exp.HostArch().Return(arch.AMD64),
   372  		exp.FindImage("bionic", arch.AMD64, gomock.Any(), true, gomock.Any()).Return(containerlxd.SourcedImage{}, nil),
   373  		exp.GetNICsFromProfile("default").Return(s.defaultProfile.Devices, nil),
   374  		exp.CreateContainerFromSpec(gomock.Any()).Return(&containerlxd.Container{}, fmt.Errorf("not authorized")),
   375  	)
   376  
   377  	env := s.NewEnviron(c, svr, nil)
   378  	_, err := env.StartInstance(s.callCtx, s.GetStartInstanceArgs(c, "bionic"))
   379  	c.Assert(err, gc.ErrorMatches, "not authorized")
   380  	c.Assert(s.invalidCredential, jc.IsTrue)
   381  }
   382  
   383  func (s *environBrokerSuite) TestStopInstances(c *gc.C) {
   384  	ctrl := gomock.NewController(c)
   385  	defer ctrl.Finish()
   386  	svr := lxd.NewMockServer(ctrl)
   387  
   388  	svr.EXPECT().RemoveContainers([]string{"juju-f75cba-1", "juju-f75cba-2"})
   389  
   390  	env := s.NewEnviron(c, svr, nil)
   391  	err := env.StopInstances(s.callCtx, "juju-f75cba-1", "juju-f75cba-2", "not-in-namespace-so-ignored")
   392  	c.Assert(err, jc.ErrorIsNil)
   393  }
   394  
   395  func (s *environBrokerSuite) TestStopInstancesInvalidCredentials(c *gc.C) {
   396  	c.Assert(s.invalidCredential, jc.IsFalse)
   397  	ctrl := gomock.NewController(c)
   398  	defer ctrl.Finish()
   399  	svr := lxd.NewMockServer(ctrl)
   400  
   401  	svr.EXPECT().RemoveContainers([]string{"juju-f75cba-1", "juju-f75cba-2"}).Return(fmt.Errorf("not authorized"))
   402  
   403  	env := s.NewEnviron(c, svr, nil)
   404  	err := env.StopInstances(s.callCtx, "juju-f75cba-1", "juju-f75cba-2", "not-in-namespace-so-ignored")
   405  	c.Assert(err, gc.ErrorMatches, "not authorized")
   406  	c.Assert(s.invalidCredential, jc.IsTrue)
   407  }
   408  
   409  func (s *environBrokerSuite) TestImageSourcesDefault(c *gc.C) {
   410  	ctrl := gomock.NewController(c)
   411  	defer ctrl.Finish()
   412  	svr := lxd.NewMockServer(ctrl)
   413  
   414  	sources, err := lxd.GetImageSources(s.NewEnviron(c, svr, nil))
   415  	c.Assert(err, jc.ErrorIsNil)
   416  
   417  	s.checkSources(c, sources, []string{
   418  		"https://streams.canonical.com/juju/images/releases/",
   419  		"https://cloud-images.ubuntu.com/releases/",
   420  	})
   421  }
   422  
   423  func (s *environBrokerSuite) TestImageMetadataURL(c *gc.C) {
   424  	ctrl := gomock.NewController(c)
   425  	defer ctrl.Finish()
   426  	svr := lxd.NewMockServer(ctrl)
   427  
   428  	env := s.NewEnviron(c, svr, map[string]interface{}{
   429  		"image-metadata-url": "https://my-test.com/images/",
   430  	})
   431  
   432  	sources, err := lxd.GetImageSources(env)
   433  	c.Assert(err, jc.ErrorIsNil)
   434  
   435  	s.checkSources(c, sources, []string{
   436  		"https://my-test.com/images/",
   437  		"https://streams.canonical.com/juju/images/releases/",
   438  		"https://cloud-images.ubuntu.com/releases/",
   439  	})
   440  }
   441  
   442  func (s *environBrokerSuite) TestImageMetadataURLEnsuresHTTPS(c *gc.C) {
   443  	ctrl := gomock.NewController(c)
   444  	defer ctrl.Finish()
   445  	svr := lxd.NewMockServer(ctrl)
   446  
   447  	// HTTP should be converted to HTTPS.
   448  	env := s.NewEnviron(c, svr, map[string]interface{}{
   449  		"image-metadata-url": "http://my-test.com/images/",
   450  	})
   451  
   452  	sources, err := lxd.GetImageSources(env)
   453  	c.Assert(err, jc.ErrorIsNil)
   454  
   455  	s.checkSources(c, sources, []string{
   456  		"https://my-test.com/images/",
   457  		"https://streams.canonical.com/juju/images/releases/",
   458  		"https://cloud-images.ubuntu.com/releases/",
   459  	})
   460  }
   461  
   462  func (s *environBrokerSuite) TestImageStreamReleased(c *gc.C) {
   463  	ctrl := gomock.NewController(c)
   464  	defer ctrl.Finish()
   465  	svr := lxd.NewMockServer(ctrl)
   466  
   467  	env := s.NewEnviron(c, svr, map[string]interface{}{
   468  		"image-stream": "released",
   469  	})
   470  
   471  	sources, err := lxd.GetImageSources(env)
   472  	c.Assert(err, jc.ErrorIsNil)
   473  
   474  	s.checkSources(c, sources, []string{
   475  		"https://streams.canonical.com/juju/images/releases/",
   476  		"https://cloud-images.ubuntu.com/releases/",
   477  	})
   478  }
   479  
   480  func (s *environBrokerSuite) TestImageStreamDaily(c *gc.C) {
   481  	ctrl := gomock.NewController(c)
   482  	defer ctrl.Finish()
   483  	svr := lxd.NewMockServer(ctrl)
   484  
   485  	env := s.NewEnviron(c, svr, map[string]interface{}{
   486  		"image-stream": "daily",
   487  	})
   488  
   489  	sources, err := lxd.GetImageSources(env)
   490  	c.Assert(err, jc.ErrorIsNil)
   491  
   492  	s.checkSources(c, sources, []string{
   493  		"https://streams.canonical.com/juju/images/daily/",
   494  		"https://cloud-images.ubuntu.com/daily/",
   495  	})
   496  }
   497  
   498  func (s *environBrokerSuite) checkSources(c *gc.C, sources []containerlxd.ServerSpec, expectedURLs []string) {
   499  	var sourceURLs []string
   500  	for _, source := range sources {
   501  		sourceURLs = append(sourceURLs, source.Host)
   502  	}
   503  	c.Check(sourceURLs, gc.DeepEquals, expectedURLs)
   504  }