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 }