github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/openstack/cinder_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package openstack_test 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/juju/errors" 11 gitjujutesting "github.com/juju/testing" 12 jc "github.com/juju/testing/checkers" 13 "github.com/juju/utils" 14 gc "gopkg.in/check.v1" 15 "gopkg.in/goose.v1/cinder" 16 "gopkg.in/goose.v1/identity" 17 "gopkg.in/goose.v1/nova" 18 "gopkg.in/juju/names.v2" 19 20 "github.com/juju/juju/environs/tags" 21 "github.com/juju/juju/instance" 22 "github.com/juju/juju/provider/openstack" 23 "github.com/juju/juju/storage" 24 "github.com/juju/juju/testing" 25 ) 26 27 const ( 28 mockVolId = "0" 29 mockVolSize = 1024 * 2 30 mockVolName = "123" 31 mockServerId = "mock-server-id" 32 mockVolJson = `{"volume":{"id": "` + mockVolId + `", "size":1,"name":"` + mockVolName + `"}}` 33 ) 34 35 var ( 36 mockVolumeTag = names.NewVolumeTag(mockVolName) 37 mockMachineTag = names.NewMachineTag("456") 38 ) 39 40 var _ = gc.Suite(&cinderVolumeSourceSuite{}) 41 42 type cinderVolumeSourceSuite struct { 43 testing.BaseSuite 44 } 45 46 func init() { 47 // Override attempt strategy to speed things up. 48 openstack.CinderAttempt.Delay = 0 49 } 50 51 func (s *cinderVolumeSourceSuite) TestAttachVolumes(c *gc.C) { 52 mockAdapter := &mockAdapter{ 53 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 54 c.Check(volId, gc.Equals, mockVolId) 55 c.Check(serverId, gc.Equals, mockServerId) 56 return &nova.VolumeAttachment{ 57 Id: volId, 58 VolumeId: volId, 59 ServerId: serverId, 60 Device: "/dev/sda", 61 }, nil 62 }, 63 } 64 65 volSource := openstack.NewCinderVolumeSource(mockAdapter) 66 results, err := volSource.AttachVolumes([]storage.VolumeAttachmentParams{{ 67 Volume: mockVolumeTag, 68 VolumeId: mockVolId, 69 AttachmentParams: storage.AttachmentParams{ 70 Provider: openstack.CinderProviderType, 71 Machine: mockMachineTag, 72 InstanceId: instance.Id(mockServerId), 73 }}, 74 }) 75 c.Assert(err, jc.ErrorIsNil) 76 c.Check(results, jc.DeepEquals, []storage.AttachVolumesResult{{ 77 VolumeAttachment: &storage.VolumeAttachment{ 78 mockVolumeTag, 79 mockMachineTag, 80 storage.VolumeAttachmentInfo{ 81 DeviceName: "sda", 82 }, 83 }, 84 }}) 85 } 86 87 func (s *cinderVolumeSourceSuite) TestCreateVolume(c *gc.C) { 88 const ( 89 requestedSize = 2 * 1024 90 providedSize = 3 * 1024 91 ) 92 93 s.PatchValue(openstack.CinderAttempt, utils.AttemptStrategy{Min: 3}) 94 95 var getVolumeCalls int 96 mockAdapter := &mockAdapter{ 97 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 98 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 99 Size: requestedSize / 1024, 100 Name: "juju-testenv-volume-123", 101 }) 102 return &cinder.Volume{ 103 ID: mockVolId, 104 }, nil 105 }, 106 getVolume: func(volumeId string) (*cinder.Volume, error) { 107 var status string 108 getVolumeCalls++ 109 if getVolumeCalls > 1 { 110 status = "available" 111 } 112 return &cinder.Volume{ 113 ID: volumeId, 114 Size: providedSize / 1024, 115 Status: status, 116 }, nil 117 }, 118 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 119 c.Check(volId, gc.Equals, mockVolId) 120 c.Check(serverId, gc.Equals, mockServerId) 121 return &nova.VolumeAttachment{ 122 Id: volId, 123 VolumeId: volId, 124 ServerId: serverId, 125 Device: "/dev/sda", 126 }, nil 127 }, 128 } 129 130 volSource := openstack.NewCinderVolumeSource(mockAdapter) 131 results, err := volSource.CreateVolumes([]storage.VolumeParams{{ 132 Provider: openstack.CinderProviderType, 133 Tag: mockVolumeTag, 134 Size: requestedSize, 135 Attachment: &storage.VolumeAttachmentParams{ 136 AttachmentParams: storage.AttachmentParams{ 137 Provider: openstack.CinderProviderType, 138 Machine: mockMachineTag, 139 InstanceId: instance.Id(mockServerId), 140 }, 141 }, 142 }}) 143 c.Assert(err, jc.ErrorIsNil) 144 c.Assert(results, gc.HasLen, 1) 145 c.Assert(results[0].Error, jc.ErrorIsNil) 146 147 c.Check(results[0].Volume, jc.DeepEquals, &storage.Volume{ 148 mockVolumeTag, 149 storage.VolumeInfo{ 150 VolumeId: mockVolId, 151 Size: providedSize, 152 Persistent: true, 153 }, 154 }) 155 156 // should have been 2 calls to GetVolume: twice initially 157 // to wait until the volume became available. 158 c.Check(getVolumeCalls, gc.Equals, 2) 159 } 160 161 func (s *cinderVolumeSourceSuite) TestResourceTags(c *gc.C) { 162 var created bool 163 mockAdapter := &mockAdapter{ 164 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 165 created = true 166 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 167 Size: 1, 168 Name: "juju-testenv-volume-123", 169 Metadata: map[string]string{ 170 "ResourceTag1": "Value1", 171 "ResourceTag2": "Value2", 172 }, 173 }) 174 return &cinder.Volume{ID: mockVolId}, nil 175 }, 176 getVolume: func(volumeId string) (*cinder.Volume, error) { 177 return &cinder.Volume{ 178 ID: volumeId, 179 Size: 1, 180 Status: "available", 181 }, nil 182 }, 183 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 184 return &nova.VolumeAttachment{ 185 Id: volId, 186 VolumeId: volId, 187 ServerId: serverId, 188 Device: "/dev/sda", 189 }, nil 190 }, 191 } 192 193 volSource := openstack.NewCinderVolumeSource(mockAdapter) 194 _, err := volSource.CreateVolumes([]storage.VolumeParams{{ 195 Provider: openstack.CinderProviderType, 196 Tag: mockVolumeTag, 197 Size: 1024, 198 ResourceTags: map[string]string{ 199 "ResourceTag1": "Value1", 200 "ResourceTag2": "Value2", 201 }, 202 Attachment: &storage.VolumeAttachmentParams{ 203 AttachmentParams: storage.AttachmentParams{ 204 Provider: openstack.CinderProviderType, 205 Machine: mockMachineTag, 206 InstanceId: instance.Id(mockServerId), 207 }, 208 }, 209 }}) 210 c.Assert(err, jc.ErrorIsNil) 211 c.Assert(created, jc.IsTrue) 212 } 213 214 func (s *cinderVolumeSourceSuite) TestListVolumes(c *gc.C) { 215 mockAdapter := &mockAdapter{ 216 getVolumesDetail: func() ([]cinder.Volume, error) { 217 return []cinder.Volume{{ 218 ID: "volume-1", 219 }, { 220 ID: "volume-2", 221 Metadata: map[string]string{ 222 tags.JujuModel: "something-else", 223 }, 224 }, { 225 ID: "volume-3", 226 Metadata: map[string]string{ 227 tags.JujuModel: testing.ModelTag.Id(), 228 }, 229 }}, nil 230 }, 231 } 232 volSource := openstack.NewCinderVolumeSource(mockAdapter) 233 volumeIds, err := volSource.ListVolumes() 234 c.Assert(err, jc.ErrorIsNil) 235 c.Check(volumeIds, jc.DeepEquals, []string{"volume-3"}) 236 } 237 238 func (s *cinderVolumeSourceSuite) TestDescribeVolumes(c *gc.C) { 239 mockAdapter := &mockAdapter{ 240 getVolumesDetail: func() ([]cinder.Volume, error) { 241 return []cinder.Volume{{ 242 ID: mockVolId, 243 Size: mockVolSize / 1024, 244 }}, nil 245 }, 246 } 247 volSource := openstack.NewCinderVolumeSource(mockAdapter) 248 volumes, err := volSource.DescribeVolumes([]string{mockVolId}) 249 c.Assert(err, jc.ErrorIsNil) 250 c.Check(volumes, jc.DeepEquals, []storage.DescribeVolumesResult{{ 251 VolumeInfo: &storage.VolumeInfo{ 252 VolumeId: mockVolId, 253 Size: mockVolSize, 254 Persistent: true, 255 }, 256 }}) 257 } 258 259 func (s *cinderVolumeSourceSuite) TestDestroyVolumes(c *gc.C) { 260 mockAdapter := &mockAdapter{} 261 volSource := openstack.NewCinderVolumeSource(mockAdapter) 262 errs, err := volSource.DestroyVolumes([]string{mockVolId}) 263 c.Assert(err, jc.ErrorIsNil) 264 c.Assert(errs, jc.DeepEquals, []error{nil}) 265 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 266 {"GetVolume", []interface{}{mockVolId}}, 267 {"DeleteVolume", []interface{}{mockVolId}}, 268 }) 269 } 270 271 func (s *cinderVolumeSourceSuite) TestDestroyVolumesAttached(c *gc.C) { 272 statuses := []string{"in-use", "detaching", "available"} 273 274 mockAdapter := &mockAdapter{ 275 getVolume: func(volId string) (*cinder.Volume, error) { 276 c.Assert(statuses, gc.Not(gc.HasLen), 0) 277 status := statuses[0] 278 statuses = statuses[1:] 279 return &cinder.Volume{ 280 ID: volId, 281 Status: status, 282 }, nil 283 }, 284 } 285 286 volSource := openstack.NewCinderVolumeSource(mockAdapter) 287 errs, err := volSource.DestroyVolumes([]string{mockVolId}) 288 c.Assert(err, jc.ErrorIsNil) 289 c.Assert(errs, gc.HasLen, 1) 290 c.Assert(errs[0], jc.ErrorIsNil) 291 c.Assert(statuses, gc.HasLen, 0) 292 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{{ 293 "GetVolume", []interface{}{mockVolId}, 294 }, { 295 "GetVolume", []interface{}{mockVolId}, 296 }, { 297 "GetVolume", []interface{}{mockVolId}, 298 }, { 299 "DeleteVolume", []interface{}{mockVolId}, 300 }}) 301 } 302 303 func (s *cinderVolumeSourceSuite) TestDetachVolumes(c *gc.C) { 304 const mockServerId2 = mockServerId + "2" 305 306 var numListCalls, numDetachCalls int 307 mockAdapter := &mockAdapter{ 308 listVolumeAttachments: func(serverId string) ([]nova.VolumeAttachment, error) { 309 numListCalls++ 310 if serverId == mockServerId2 { 311 // no attachments 312 return nil, nil 313 } 314 c.Check(serverId, gc.Equals, mockServerId) 315 return []nova.VolumeAttachment{{ 316 Id: mockVolId, 317 VolumeId: mockVolId, 318 ServerId: mockServerId, 319 Device: "/dev/sda", 320 }}, nil 321 }, 322 detachVolume: func(serverId, volId string) error { 323 numDetachCalls++ 324 c.Check(serverId, gc.Equals, mockServerId) 325 c.Check(volId, gc.Equals, mockVolId) 326 return nil 327 }, 328 } 329 330 volSource := openstack.NewCinderVolumeSource(mockAdapter) 331 errs, err := volSource.DetachVolumes([]storage.VolumeAttachmentParams{{ 332 Volume: names.NewVolumeTag("123"), 333 VolumeId: mockVolId, 334 AttachmentParams: storage.AttachmentParams{ 335 Machine: names.NewMachineTag("0"), 336 InstanceId: mockServerId, 337 }, 338 }, { 339 Volume: names.NewVolumeTag("42"), 340 VolumeId: "42", 341 AttachmentParams: storage.AttachmentParams{ 342 Machine: names.NewMachineTag("0"), 343 InstanceId: mockServerId2, 344 }, 345 }}) 346 c.Assert(err, jc.ErrorIsNil) 347 c.Assert(errs, jc.DeepEquals, []error{nil, nil}) 348 // DetachVolume should only be called for existing attachments. 349 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{{ 350 "ListVolumeAttachments", []interface{}{mockServerId}, 351 }, { 352 "DetachVolume", []interface{}{mockServerId, mockVolId}, 353 }, { 354 "ListVolumeAttachments", []interface{}{mockServerId2}, 355 }}) 356 } 357 358 func (s *cinderVolumeSourceSuite) TestCreateVolumeCleanupDestroys(c *gc.C) { 359 var numCreateCalls, numDestroyCalls, numGetCalls int 360 mockAdapter := &mockAdapter{ 361 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 362 numCreateCalls++ 363 if numCreateCalls == 3 { 364 return nil, errors.New("no volume for you") 365 } 366 return &cinder.Volume{ 367 ID: fmt.Sprint(numCreateCalls), 368 Status: "", 369 }, nil 370 }, 371 deleteVolume: func(volId string) error { 372 numDestroyCalls++ 373 c.Assert(volId, gc.Equals, "2") 374 return errors.New("destroy fails") 375 }, 376 getVolume: func(volumeId string) (*cinder.Volume, error) { 377 numGetCalls++ 378 if numGetCalls == 2 { 379 return nil, errors.New("no volume details for you") 380 } 381 return &cinder.Volume{ 382 ID: "4", 383 Size: mockVolSize / 1024, 384 Status: "available", 385 }, nil 386 }, 387 } 388 389 volSource := openstack.NewCinderVolumeSource(mockAdapter) 390 volumeParams := []storage.VolumeParams{{ 391 Provider: openstack.CinderProviderType, 392 Tag: names.NewVolumeTag("0"), 393 Size: mockVolSize, 394 Attachment: &storage.VolumeAttachmentParams{ 395 AttachmentParams: storage.AttachmentParams{ 396 Provider: openstack.CinderProviderType, 397 Machine: mockMachineTag, 398 InstanceId: instance.Id(mockServerId), 399 }, 400 }, 401 }, { 402 Provider: openstack.CinderProviderType, 403 Tag: names.NewVolumeTag("1"), 404 Size: mockVolSize, 405 Attachment: &storage.VolumeAttachmentParams{ 406 AttachmentParams: storage.AttachmentParams{ 407 Provider: openstack.CinderProviderType, 408 Machine: mockMachineTag, 409 InstanceId: instance.Id(mockServerId), 410 }, 411 }, 412 }, { 413 Provider: openstack.CinderProviderType, 414 Tag: names.NewVolumeTag("2"), 415 Size: mockVolSize, 416 Attachment: &storage.VolumeAttachmentParams{ 417 AttachmentParams: storage.AttachmentParams{ 418 Provider: openstack.CinderProviderType, 419 Machine: mockMachineTag, 420 InstanceId: instance.Id(mockServerId), 421 }, 422 }, 423 }} 424 results, err := volSource.CreateVolumes(volumeParams) 425 c.Assert(err, jc.ErrorIsNil) 426 c.Assert(results, gc.HasLen, 3) 427 c.Assert(results[0].Error, jc.ErrorIsNil) 428 c.Assert(results[1].Error, gc.ErrorMatches, "waiting for volume to be provisioned: getting volume: no volume details for you") 429 c.Assert(results[2].Error, gc.ErrorMatches, "no volume for you") 430 c.Assert(numCreateCalls, gc.Equals, 3) 431 c.Assert(numGetCalls, gc.Equals, 2) 432 c.Assert(numDestroyCalls, gc.Equals, 1) 433 } 434 435 type mockAdapter struct { 436 gitjujutesting.Stub 437 getVolume func(string) (*cinder.Volume, error) 438 getVolumesDetail func() ([]cinder.Volume, error) 439 deleteVolume func(string) error 440 createVolume func(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) 441 attachVolume func(string, string, string) (*nova.VolumeAttachment, error) 442 volumeStatusNotifier func(string, string, int, time.Duration) <-chan error 443 detachVolume func(string, string) error 444 listVolumeAttachments func(string) ([]nova.VolumeAttachment, error) 445 } 446 447 func (ma *mockAdapter) GetVolume(volumeId string) (*cinder.Volume, error) { 448 ma.MethodCall(ma, "GetVolume", volumeId) 449 if ma.getVolume != nil { 450 return ma.getVolume(volumeId) 451 } 452 return &cinder.Volume{ 453 ID: volumeId, 454 Status: "available", 455 }, nil 456 } 457 458 func (ma *mockAdapter) GetVolumesDetail() ([]cinder.Volume, error) { 459 ma.MethodCall(ma, "GetVolumesDetail") 460 if ma.getVolumesDetail != nil { 461 return ma.getVolumesDetail() 462 } 463 return nil, nil 464 } 465 466 func (ma *mockAdapter) DeleteVolume(volId string) error { 467 ma.MethodCall(ma, "DeleteVolume", volId) 468 if ma.deleteVolume != nil { 469 return ma.deleteVolume(volId) 470 } 471 return nil 472 } 473 474 func (ma *mockAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 475 ma.MethodCall(ma, "CreateVolume", args) 476 if ma.createVolume != nil { 477 return ma.createVolume(args) 478 } 479 return nil, errors.NotImplementedf("CreateVolume") 480 } 481 482 func (ma *mockAdapter) AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error) { 483 ma.MethodCall(ma, "AttachVolume", serverId, volumeId, mountPoint) 484 if ma.attachVolume != nil { 485 return ma.attachVolume(serverId, volumeId, mountPoint) 486 } 487 return nil, errors.NotImplementedf("AttachVolume") 488 } 489 490 func (ma *mockAdapter) DetachVolume(serverId, attachmentId string) error { 491 ma.MethodCall(ma, "DetachVolume", serverId, attachmentId) 492 if ma.detachVolume != nil { 493 return ma.detachVolume(serverId, attachmentId) 494 } 495 return nil 496 } 497 498 func (ma *mockAdapter) ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error) { 499 ma.MethodCall(ma, "ListVolumeAttachments", serverId) 500 if ma.listVolumeAttachments != nil { 501 return ma.listVolumeAttachments(serverId) 502 } 503 return nil, nil 504 } 505 506 type testEndpointResolver struct { 507 authenticated bool 508 regionEndpoints map[string]identity.ServiceURLs 509 } 510 511 func (r *testEndpointResolver) IsAuthenticated() bool { 512 return r.authenticated 513 } 514 515 func (r *testEndpointResolver) Authenticate() error { 516 r.authenticated = true 517 return nil 518 } 519 520 func (r *testEndpointResolver) EndpointsForRegion(region string) identity.ServiceURLs { 521 if !r.authenticated { 522 return identity.ServiceURLs{} 523 } 524 return r.regionEndpoints[region] 525 } 526 527 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolume(c *gc.C) { 528 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 529 "west": map[string]string{"volume": "http://cinder.testing/v1"}, 530 }} 531 url, err := openstack.GetVolumeEndpointURL(client, "west") 532 c.Assert(err, jc.ErrorIsNil) 533 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v1") 534 } 535 536 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolumeV2(c *gc.C) { 537 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 538 "west": map[string]string{"volumev2": "http://cinder.testing/v2"}, 539 }} 540 url, err := openstack.GetVolumeEndpointURL(client, "west") 541 c.Assert(err, jc.ErrorIsNil) 542 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2") 543 } 544 545 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointPreferV2(c *gc.C) { 546 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 547 "south": map[string]string{ 548 "volume": "http://cinder.testing/v1", 549 "volumev2": "http://cinder.testing/v2", 550 }, 551 }} 552 url, err := openstack.GetVolumeEndpointURL(client, "south") 553 c.Assert(err, jc.ErrorIsNil) 554 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2") 555 } 556 557 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointMissing(c *gc.C) { 558 client := &testEndpointResolver{} 559 url, err := openstack.GetVolumeEndpointURL(client, "east") 560 c.Assert(err, gc.ErrorMatches, `endpoint "volume" in region "east" not found`) 561 c.Assert(err, jc.Satisfies, errors.IsNotFound) 562 c.Assert(url, gc.IsNil) 563 } 564 565 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointBadURL(c *gc.C) { 566 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 567 "north": map[string]string{"volumev2": "some %4"}, 568 }} 569 url, err := openstack.GetVolumeEndpointURL(client, "north") 570 c.Assert(err, gc.ErrorMatches, `parse some %4: .*`) 571 c.Assert(url, gc.IsNil) 572 }