github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 9 "github.com/go-goose/goose/v5/cinder" 10 gooseerrors "github.com/go-goose/goose/v5/errors" 11 "github.com/go-goose/goose/v5/identity" 12 "github.com/go-goose/goose/v5/nova" 13 "github.com/juju/errors" 14 "github.com/juju/names/v5" 15 gitjujutesting "github.com/juju/testing" 16 jc "github.com/juju/testing/checkers" 17 "github.com/juju/utils/v3" 18 "go.uber.org/mock/gomock" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/juju/core/instance" 22 "github.com/juju/juju/environs/context" 23 "github.com/juju/juju/environs/tags" 24 "github.com/juju/juju/provider/common/mocks" 25 "github.com/juju/juju/provider/openstack" 26 "github.com/juju/juju/storage" 27 "github.com/juju/juju/testing" 28 ) 29 30 const ( 31 mockVolId = "0" 32 mockVolSize = 1024 * 2 33 mockVolName = "123" 34 mockServerId = "mock-server-id" 35 ) 36 37 var ( 38 mockVolumeTag = names.NewVolumeTag(mockVolName) 39 mockMachineTag = names.NewMachineTag("456") 40 ) 41 42 var _ = gc.Suite(&cinderVolumeSourceSuite{}) 43 44 type cinderVolumeSourceSuite struct { 45 testing.BaseSuite 46 47 callCtx *context.CloudCallContext 48 invalidCredential bool 49 env *mocks.MockZonedEnviron 50 } 51 52 func (s *cinderVolumeSourceSuite) SetUpTest(c *gc.C) { 53 s.BaseSuite.SetUpTest(c) 54 s.callCtx = &context.CloudCallContext{ 55 InvalidateCredentialFunc: func(string) error { 56 s.invalidCredential = true 57 return nil 58 }, 59 } 60 } 61 62 func (s *cinderVolumeSourceSuite) TearDownTest(c *gc.C) { 63 s.invalidCredential = false 64 s.BaseSuite.TearDownTest(c) 65 } 66 67 func init() { 68 // Override attempt strategy to speed things up. 69 openstack.CinderAttempt.Delay = 0 70 } 71 72 func toStringPtr(s string) *string { 73 return &s 74 } 75 76 func (s *cinderVolumeSourceSuite) TestAttachVolumes(c *gc.C) { 77 mockAdapter := &mockAdapter{ 78 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 79 c.Check(volId, gc.Equals, mockVolId) 80 c.Check(serverId, gc.Equals, mockServerId) 81 return &nova.VolumeAttachment{ 82 Id: volId, 83 VolumeId: volId, 84 ServerId: serverId, 85 Device: toStringPtr("/dev/sda"), 86 }, nil 87 }, 88 } 89 90 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 91 results, err := volSource.AttachVolumes(s.callCtx, []storage.VolumeAttachmentParams{{ 92 Volume: mockVolumeTag, 93 VolumeId: mockVolId, 94 AttachmentParams: storage.AttachmentParams{ 95 Provider: openstack.CinderProviderType, 96 Machine: mockMachineTag, 97 InstanceId: mockServerId, 98 }}, 99 }) 100 c.Assert(err, jc.ErrorIsNil) 101 c.Check(results, jc.DeepEquals, []storage.AttachVolumesResult{{ 102 VolumeAttachment: &storage.VolumeAttachment{ 103 Volume: mockVolumeTag, 104 Machine: mockMachineTag, 105 VolumeAttachmentInfo: storage.VolumeAttachmentInfo{ 106 DeviceName: "sda", 107 }, 108 }, 109 }}) 110 } 111 112 var testUnauthorisedGooseError = gooseerrors.NewUnauthorisedf(nil, "", "invalid auth") 113 114 func (s *cinderVolumeSourceSuite) TestAttachVolumesInvalidCredential(c *gc.C) { 115 c.Assert(s.invalidCredential, jc.IsFalse) 116 mockAdapter := &mockAdapter{ 117 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 118 return &nova.VolumeAttachment{}, testUnauthorisedGooseError 119 }, 120 } 121 122 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 123 _, err := volSource.AttachVolumes(s.callCtx, []storage.VolumeAttachmentParams{{ 124 Volume: mockVolumeTag, 125 VolumeId: mockVolId, 126 AttachmentParams: storage.AttachmentParams{ 127 Provider: openstack.CinderProviderType, 128 Machine: mockMachineTag, 129 InstanceId: mockServerId, 130 }}, 131 }) 132 c.Assert(err, jc.ErrorIsNil) 133 c.Assert(s.invalidCredential, jc.IsTrue) 134 } 135 136 func (s *cinderVolumeSourceSuite) TestAttachVolumesNoDevice(c *gc.C) { 137 mockAdapter := &mockAdapter{ 138 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 139 return &nova.VolumeAttachment{ 140 Id: volId, 141 VolumeId: volId, 142 ServerId: serverId, 143 Device: nil, 144 }, nil 145 }, 146 } 147 148 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 149 results, err := volSource.AttachVolumes(s.callCtx, []storage.VolumeAttachmentParams{{ 150 Volume: mockVolumeTag, 151 VolumeId: mockVolId, 152 AttachmentParams: storage.AttachmentParams{ 153 Provider: openstack.CinderProviderType, 154 Machine: mockMachineTag, 155 InstanceId: mockServerId, 156 }}, 157 }) 158 c.Assert(err, jc.ErrorIsNil) 159 c.Assert(results, gc.HasLen, 1) 160 c.Assert(results[0].Error, gc.ErrorMatches, "device not assigned to volume attachment") 161 } 162 163 func (s *cinderVolumeSourceSuite) TestCreateVolume(c *gc.C) { 164 defer s.setupMocks(c).Finish() 165 166 const ( 167 requestedSize = 2 * 1024 168 providedSize = 3 * 1024 169 ) 170 171 s.PatchValue(openstack.CinderAttempt, utils.AttemptStrategy{Min: 3}) 172 173 var getVolumeCalls int 174 mockAdapter := &mockAdapter{ 175 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 176 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 177 Size: requestedSize / 1024, 178 Name: "juju-testmodel-volume-123", 179 AvailabilityZone: "zone-1", 180 }) 181 return &cinder.Volume{ 182 ID: mockVolId, 183 }, nil 184 }, 185 listAvailabilityZones: func() ([]cinder.AvailabilityZone, error) { 186 return []cinder.AvailabilityZone{{ 187 Name: "zone-1", 188 State: cinder.AvailabilityZoneState{Available: true}, 189 }}, nil 190 }, 191 getVolume: func(volumeId string) (*cinder.Volume, error) { 192 var status string 193 getVolumeCalls++ 194 if getVolumeCalls > 1 { 195 status = "available" 196 } 197 return &cinder.Volume{ 198 ID: volumeId, 199 Size: providedSize / 1024, 200 Status: status, 201 }, nil 202 }, 203 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 204 c.Check(volId, gc.Equals, mockVolId) 205 c.Check(serverId, gc.Equals, mockServerId) 206 return &nova.VolumeAttachment{ 207 Id: volId, 208 VolumeId: volId, 209 ServerId: serverId, 210 Device: toStringPtr("/dev/sda"), 211 }, nil 212 }, 213 } 214 215 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 216 results, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 217 Provider: openstack.CinderProviderType, 218 Tag: mockVolumeTag, 219 Size: requestedSize, 220 Attachment: &storage.VolumeAttachmentParams{ 221 AttachmentParams: storage.AttachmentParams{ 222 Provider: openstack.CinderProviderType, 223 Machine: mockMachineTag, 224 InstanceId: mockServerId, 225 }, 226 }, 227 }}) 228 c.Assert(err, jc.ErrorIsNil) 229 c.Assert(results, gc.HasLen, 1) 230 c.Assert(results[0].Error, jc.ErrorIsNil) 231 232 c.Check(results[0].Volume, jc.DeepEquals, &storage.Volume{ 233 Tag: mockVolumeTag, 234 VolumeInfo: storage.VolumeInfo{ 235 VolumeId: mockVolId, 236 Size: providedSize, 237 Persistent: true, 238 }, 239 }) 240 241 // should have been 2 calls to GetVolume: twice initially 242 // to wait until the volume became available. 243 c.Check(getVolumeCalls, gc.Equals, 2) 244 } 245 246 func (s *cinderVolumeSourceSuite) TestCreateVolumeNoCompatibleZones(c *gc.C) { 247 defer s.setupMocks(c).Finish() 248 249 var created bool 250 mockAdapter := &mockAdapter{ 251 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 252 created = true 253 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 254 Size: 1, 255 Name: "juju-testmodel-volume-123", 256 }) 257 return &cinder.Volume{ 258 ID: mockVolId, 259 }, nil 260 }, 261 listAvailabilityZones: func() ([]cinder.AvailabilityZone, error) { 262 return []cinder.AvailabilityZone{{ 263 Name: "nova", 264 State: cinder.AvailabilityZoneState{Available: true}, 265 }}, nil 266 }, 267 getVolume: func(volumeId string) (*cinder.Volume, error) { 268 return &cinder.Volume{ 269 ID: volumeId, 270 Size: 1, 271 Status: "available", 272 }, nil 273 }, 274 } 275 276 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 277 _, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 278 Provider: openstack.CinderProviderType, 279 Tag: mockVolumeTag, 280 Size: 1024, 281 }}) 282 c.Assert(err, jc.ErrorIsNil) 283 c.Assert(created, jc.IsTrue) 284 } 285 286 func (s *cinderVolumeSourceSuite) TestCreateVolumeZonesNotSupported(c *gc.C) { 287 defer s.setupMocks(c).Finish() 288 289 var created bool 290 mockAdapter := &mockAdapter{ 291 // listAvailabilityZones not implemented so we get a NotImplemented error. 292 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 293 created = true 294 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 295 Size: 1, 296 Name: "juju-testmodel-volume-123", 297 }) 298 return &cinder.Volume{ 299 ID: mockVolId, 300 }, nil 301 }, 302 getVolume: func(volumeId string) (*cinder.Volume, error) { 303 return &cinder.Volume{ 304 ID: volumeId, 305 Size: 1, 306 Status: "available", 307 }, nil 308 }, 309 } 310 311 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 312 _, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 313 Provider: openstack.CinderProviderType, 314 Tag: mockVolumeTag, 315 Size: 1024, 316 }}) 317 c.Assert(err, jc.ErrorIsNil) 318 c.Assert(created, jc.IsTrue) 319 } 320 321 func (s *cinderVolumeSourceSuite) TestCreateVolumeVolumeType(c *gc.C) { 322 defer s.setupMocks(c).Finish() 323 324 var created bool 325 mockAdapter := &mockAdapter{ 326 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 327 created = true 328 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 329 Size: 1, 330 Name: "juju-testmodel-volume-123", 331 VolumeType: "SSD", 332 }) 333 return &cinder.Volume{ID: mockVolId}, nil 334 }, 335 getVolume: func(volumeId string) (*cinder.Volume, error) { 336 return &cinder.Volume{ 337 ID: volumeId, 338 Size: 1, 339 Status: "available", 340 }, nil 341 }, 342 } 343 344 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 345 _, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 346 Provider: openstack.CinderProviderType, 347 Tag: mockVolumeTag, 348 Size: 1024, 349 Attributes: map[string]interface{}{ 350 "volume-type": "SSD", 351 }, 352 }}) 353 c.Assert(err, jc.ErrorIsNil) 354 c.Assert(created, jc.IsTrue) 355 } 356 357 func (s *cinderVolumeSourceSuite) TestCreateVolumeInvalidCredential(c *gc.C) { 358 defer s.setupMocks(c).Finish() 359 360 c.Assert(s.invalidCredential, jc.IsFalse) 361 mockAdapter := &mockAdapter{ 362 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 363 return &cinder.Volume{}, testUnauthorisedGooseError 364 }, 365 getVolume: func(volumeId string) (*cinder.Volume, error) { 366 return &cinder.Volume{}, testUnauthorisedGooseError 367 }, 368 } 369 370 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 371 _, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 372 Provider: openstack.CinderProviderType, 373 Tag: mockVolumeTag, 374 Size: 1024, 375 Attributes: map[string]interface{}{ 376 "volume-type": "SSD", 377 }, 378 }}) 379 c.Assert(err, jc.ErrorIsNil) 380 c.Assert(s.invalidCredential, jc.IsTrue) 381 } 382 383 func (s *cinderVolumeSourceSuite) TestResourceTags(c *gc.C) { 384 var created bool 385 mockAdapter := &mockAdapter{ 386 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 387 created = true 388 c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{ 389 Size: 1, 390 Name: "juju-testmodel-volume-123", 391 Metadata: map[string]string{ 392 "ResourceTag1": "Value1", 393 "ResourceTag2": "Value2", 394 }, 395 }) 396 return &cinder.Volume{ID: mockVolId}, nil 397 }, 398 getVolume: func(volumeId string) (*cinder.Volume, error) { 399 return &cinder.Volume{ 400 ID: volumeId, 401 Size: 1, 402 Status: "available", 403 }, nil 404 }, 405 attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) { 406 return &nova.VolumeAttachment{ 407 Id: volId, 408 VolumeId: volId, 409 ServerId: serverId, 410 Device: toStringPtr("/dev/sda"), 411 }, nil 412 }, 413 } 414 415 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 416 _, err := volSource.CreateVolumes(s.callCtx, []storage.VolumeParams{{ 417 Provider: openstack.CinderProviderType, 418 Tag: mockVolumeTag, 419 Size: 1024, 420 ResourceTags: map[string]string{ 421 "ResourceTag1": "Value1", 422 "ResourceTag2": "Value2", 423 }, 424 Attachment: &storage.VolumeAttachmentParams{ 425 AttachmentParams: storage.AttachmentParams{ 426 Provider: openstack.CinderProviderType, 427 Machine: mockMachineTag, 428 InstanceId: mockServerId, 429 }, 430 }, 431 }}) 432 c.Assert(err, jc.ErrorIsNil) 433 c.Assert(created, jc.IsTrue) 434 } 435 436 func (s *cinderVolumeSourceSuite) TestListVolumes(c *gc.C) { 437 mockAdapter := &mockAdapter{ 438 getVolumesDetail: func() ([]cinder.Volume, error) { 439 return []cinder.Volume{{ 440 ID: "volume-1", 441 }, { 442 ID: "volume-2", 443 Metadata: map[string]string{ 444 tags.JujuModel: "something-else", 445 }, 446 }, { 447 ID: "volume-3", 448 Metadata: map[string]string{ 449 tags.JujuModel: testing.ModelTag.Id(), 450 }, 451 }}, nil 452 }, 453 } 454 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 455 volumeIds, err := volSource.ListVolumes(s.callCtx) 456 c.Assert(err, jc.ErrorIsNil) 457 c.Check(volumeIds, jc.DeepEquals, []string{"volume-3"}) 458 } 459 460 func (s *cinderVolumeSourceSuite) TestListVolumesInvalidCredential(c *gc.C) { 461 c.Assert(s.invalidCredential, jc.IsFalse) 462 mockAdapter := &mockAdapter{ 463 getVolumesDetail: func() ([]cinder.Volume, error) { 464 return []cinder.Volume{}, testUnauthorisedGooseError 465 }, 466 } 467 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 468 _, err := volSource.ListVolumes(s.callCtx) 469 c.Assert(err, gc.ErrorMatches, "invalid auth") 470 c.Assert(s.invalidCredential, jc.IsTrue) 471 } 472 473 func (s *cinderVolumeSourceSuite) TestDescribeVolumes(c *gc.C) { 474 mockAdapter := &mockAdapter{ 475 getVolumesDetail: func() ([]cinder.Volume, error) { 476 return []cinder.Volume{{ 477 ID: mockVolId, 478 Size: mockVolSize / 1024, 479 }}, nil 480 }, 481 } 482 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 483 volumes, err := volSource.DescribeVolumes(s.callCtx, []string{mockVolId}) 484 c.Assert(err, jc.ErrorIsNil) 485 c.Check(volumes, jc.DeepEquals, []storage.DescribeVolumesResult{{ 486 VolumeInfo: &storage.VolumeInfo{ 487 VolumeId: mockVolId, 488 Size: mockVolSize, 489 Persistent: true, 490 }, 491 }}) 492 } 493 494 func (s *cinderVolumeSourceSuite) TestDescribeVolumesInvalidCredential(c *gc.C) { 495 c.Assert(s.invalidCredential, jc.IsFalse) 496 mockAdapter := &mockAdapter{ 497 getVolumesDetail: func() ([]cinder.Volume, error) { 498 return []cinder.Volume{}, testUnauthorisedGooseError 499 }, 500 } 501 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 502 _, err := volSource.DescribeVolumes(s.callCtx, []string{mockVolId}) 503 c.Assert(err, gc.ErrorMatches, "invalid auth") 504 c.Assert(s.invalidCredential, jc.IsTrue) 505 } 506 507 func (s *cinderVolumeSourceSuite) TestDestroyVolumes(c *gc.C) { 508 mockAdapter := &mockAdapter{} 509 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 510 errs, err := volSource.DestroyVolumes(s.callCtx, []string{mockVolId}) 511 c.Assert(err, jc.ErrorIsNil) 512 c.Assert(errs, jc.DeepEquals, []error{nil}) 513 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 514 {"GetVolume", []interface{}{mockVolId}}, 515 {"DeleteVolume", []interface{}{mockVolId}}, 516 }) 517 } 518 519 func (s *cinderVolumeSourceSuite) TestDestroyVolumesNotFound(c *gc.C) { 520 mockAdapter := &mockAdapter{ 521 getVolume: func(volId string) (*cinder.Volume, error) { 522 return nil, errors.NotFoundf("volume %q", volId) 523 }, 524 } 525 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 526 errs, err := volSource.DestroyVolumes(s.callCtx, []string{mockVolId}) 527 c.Assert(err, jc.ErrorIsNil) 528 c.Assert(errs, jc.DeepEquals, []error{nil}) 529 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 530 {"GetVolume", []interface{}{mockVolId}}, 531 }) 532 } 533 534 func (s *cinderVolumeSourceSuite) TestDestroyVolumesAttached(c *gc.C) { 535 statuses := []string{"in-use", "detaching", "available"} 536 537 mockAdapter := &mockAdapter{ 538 getVolume: func(volId string) (*cinder.Volume, error) { 539 c.Assert(statuses, gc.Not(gc.HasLen), 0) 540 status := statuses[0] 541 statuses = statuses[1:] 542 return &cinder.Volume{ 543 ID: volId, 544 Status: status, 545 }, nil 546 }, 547 } 548 549 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 550 errs, err := volSource.DestroyVolumes(s.callCtx, []string{mockVolId}) 551 c.Assert(err, jc.ErrorIsNil) 552 c.Assert(errs, gc.HasLen, 1) 553 c.Assert(errs[0], jc.ErrorIsNil) 554 c.Assert(statuses, gc.HasLen, 0) 555 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{{ 556 "GetVolume", []interface{}{mockVolId}, 557 }, { 558 "GetVolume", []interface{}{mockVolId}, 559 }, { 560 "GetVolume", []interface{}{mockVolId}, 561 }, { 562 "DeleteVolume", []interface{}{mockVolId}, 563 }}) 564 } 565 566 func (s *cinderVolumeSourceSuite) TestDestroyVolumesInvalidCredential(c *gc.C) { 567 c.Assert(s.invalidCredential, jc.IsFalse) 568 mockAdapter := &mockAdapter{ 569 getVolume: func(volId string) (*cinder.Volume, error) { 570 return &cinder.Volume{}, testUnauthorisedGooseError 571 }, 572 } 573 574 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 575 errs, err := volSource.DestroyVolumes(s.callCtx, []string{mockVolId}) 576 c.Assert(err, jc.ErrorIsNil) 577 c.Assert(errs, gc.HasLen, 1) 578 c.Assert(errs[0], gc.ErrorMatches, "getting volume: invalid auth") 579 c.Assert(s.invalidCredential, jc.IsTrue) 580 mockAdapter.CheckCallNames(c, "GetVolume") 581 } 582 583 func (s *cinderVolumeSourceSuite) TestReleaseVolumes(c *gc.C) { 584 mockAdapter := &mockAdapter{} 585 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 586 errs, err := volSource.ReleaseVolumes(s.callCtx, []string{mockVolId}) 587 c.Assert(err, jc.ErrorIsNil) 588 c.Assert(errs, jc.DeepEquals, []error{nil}) 589 metadata := map[string]string{ 590 "juju-controller-uuid": "", 591 "juju-model-uuid": "", 592 } 593 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 594 {"GetVolume", []interface{}{mockVolId}}, 595 {"SetVolumeMetadata", []interface{}{mockVolId, metadata}}, 596 }) 597 } 598 599 func (s *cinderVolumeSourceSuite) TestReleaseVolumesAttached(c *gc.C) { 600 mockAdapter := &mockAdapter{ 601 getVolume: func(volId string) (*cinder.Volume, error) { 602 return &cinder.Volume{ 603 ID: volId, 604 Status: "in-use", 605 }, nil 606 }, 607 } 608 609 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 610 errs, err := volSource.ReleaseVolumes(s.callCtx, []string{mockVolId}) 611 c.Assert(err, jc.ErrorIsNil) 612 c.Assert(errs, gc.HasLen, 1) 613 c.Assert(errs[0], gc.ErrorMatches, `cannot release volume "0": volume still in-use`) 614 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{{ 615 "GetVolume", []interface{}{mockVolId}, 616 }}) 617 } 618 619 func (s *cinderVolumeSourceSuite) TestReleaseVolumesInvalidCredential(c *gc.C) { 620 c.Assert(s.invalidCredential, jc.IsFalse) 621 mockAdapter := &mockAdapter{ 622 getVolume: func(volId string) (*cinder.Volume, error) { 623 return &cinder.Volume{}, testUnauthorisedGooseError 624 }, 625 } 626 627 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 628 _, err := volSource.ReleaseVolumes(s.callCtx, []string{mockVolId}) 629 c.Assert(err, jc.ErrorIsNil) 630 c.Assert(s.invalidCredential, jc.IsTrue) 631 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{{ 632 "GetVolume", []interface{}{mockVolId}, 633 }}) 634 } 635 636 func (s *cinderVolumeSourceSuite) TestReleaseVolumesDetaching(c *gc.C) { 637 statuses := []string{"detaching", "available"} 638 639 mockAdapter := &mockAdapter{ 640 getVolume: func(volId string) (*cinder.Volume, error) { 641 c.Assert(statuses, gc.Not(gc.HasLen), 0) 642 status := statuses[0] 643 statuses = statuses[1:] 644 return &cinder.Volume{ 645 ID: volId, 646 Status: status, 647 }, nil 648 }, 649 } 650 651 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 652 errs, err := volSource.ReleaseVolumes(s.callCtx, []string{mockVolId}) 653 c.Assert(err, jc.ErrorIsNil) 654 c.Assert(errs, gc.HasLen, 1) 655 c.Assert(errs[0], jc.ErrorIsNil) 656 c.Assert(statuses, gc.HasLen, 0) 657 mockAdapter.CheckCallNames(c, "GetVolume", "GetVolume", "SetVolumeMetadata") 658 } 659 660 func (s *cinderVolumeSourceSuite) TestDetachVolumes(c *gc.C) { 661 const mockServerId2 = mockServerId + "2" 662 663 var numDetachCalls int 664 mockAdapter := &mockAdapter{ 665 detachVolume: func(serverId, volId string) error { 666 numDetachCalls++ 667 if volId == "42" { 668 return errors.NotFoundf("attachment") 669 } 670 c.Check(serverId, gc.Equals, mockServerId) 671 c.Check(volId, gc.Equals, mockVolId) 672 return nil 673 }, 674 } 675 676 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 677 errs, err := volSource.DetachVolumes(s.callCtx, []storage.VolumeAttachmentParams{{ 678 Volume: names.NewVolumeTag("123"), 679 VolumeId: mockVolId, 680 AttachmentParams: storage.AttachmentParams{ 681 Machine: names.NewMachineTag("0"), 682 InstanceId: mockServerId, 683 }, 684 }, { 685 Volume: names.NewVolumeTag("42"), 686 VolumeId: "42", 687 AttachmentParams: storage.AttachmentParams{ 688 Machine: names.NewMachineTag("0"), 689 InstanceId: mockServerId2, 690 }, 691 }}) 692 c.Assert(err, jc.ErrorIsNil) 693 c.Assert(errs, jc.DeepEquals, []error{nil, nil}) 694 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 695 {"DetachVolume", []interface{}{mockServerId, mockVolId}}, 696 {"DetachVolume", []interface{}{mockServerId2, "42"}}, 697 }) 698 } 699 700 func (s *cinderVolumeSourceSuite) TestCreateVolumeCleanupDestroys(c *gc.C) { 701 defer s.setupMocks(c).Finish() 702 703 var numCreateCalls, numDestroyCalls, numGetCalls int 704 mockAdapter := &mockAdapter{ 705 createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 706 numCreateCalls++ 707 if numCreateCalls == 3 { 708 return nil, errors.New("no volume for you") 709 } 710 return &cinder.Volume{ 711 ID: fmt.Sprint(numCreateCalls), 712 Status: "", 713 }, nil 714 }, 715 deleteVolume: func(volId string) error { 716 numDestroyCalls++ 717 c.Assert(volId, gc.Equals, "2") 718 return errors.New("destroy fails") 719 }, 720 getVolume: func(volumeId string) (*cinder.Volume, error) { 721 numGetCalls++ 722 if numGetCalls == 2 { 723 return nil, errors.New("no volume details for you") 724 } 725 return &cinder.Volume{ 726 ID: "4", 727 Size: mockVolSize / 1024, 728 Status: "available", 729 }, nil 730 }, 731 } 732 733 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 734 volumeParams := []storage.VolumeParams{{ 735 Provider: openstack.CinderProviderType, 736 Tag: names.NewVolumeTag("0"), 737 Size: mockVolSize, 738 Attachment: &storage.VolumeAttachmentParams{ 739 AttachmentParams: storage.AttachmentParams{ 740 Provider: openstack.CinderProviderType, 741 Machine: mockMachineTag, 742 InstanceId: instance.Id(mockServerId), 743 }, 744 }, 745 }, { 746 Provider: openstack.CinderProviderType, 747 Tag: names.NewVolumeTag("1"), 748 Size: mockVolSize, 749 Attachment: &storage.VolumeAttachmentParams{ 750 AttachmentParams: storage.AttachmentParams{ 751 Provider: openstack.CinderProviderType, 752 Machine: mockMachineTag, 753 InstanceId: instance.Id(mockServerId), 754 }, 755 }, 756 }, { 757 Provider: openstack.CinderProviderType, 758 Tag: names.NewVolumeTag("2"), 759 Size: mockVolSize, 760 Attachment: &storage.VolumeAttachmentParams{ 761 AttachmentParams: storage.AttachmentParams{ 762 Provider: openstack.CinderProviderType, 763 Machine: mockMachineTag, 764 InstanceId: instance.Id(mockServerId), 765 }, 766 }, 767 }} 768 results, err := volSource.CreateVolumes(s.callCtx, volumeParams) 769 c.Assert(err, jc.ErrorIsNil) 770 c.Assert(results, gc.HasLen, 3) 771 c.Assert(results[0].Error, jc.ErrorIsNil) 772 c.Assert(results[1].Error, gc.ErrorMatches, "waiting for volume to be provisioned: getting volume: no volume details for you") 773 c.Assert(results[2].Error, gc.ErrorMatches, "no volume for you") 774 c.Assert(numCreateCalls, gc.Equals, 3) 775 c.Assert(numGetCalls, gc.Equals, 2) 776 c.Assert(numDestroyCalls, gc.Equals, 1) 777 } 778 779 func (s *cinderVolumeSourceSuite) TestImportVolume(c *gc.C) { 780 mockAdapter := &mockAdapter{ 781 getVolume: func(volumeId string) (*cinder.Volume, error) { 782 return &cinder.Volume{ 783 ID: volumeId, 784 Size: mockVolSize / 1024, 785 Status: "available", 786 }, nil 787 }, 788 } 789 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 790 c.Assert(volSource, gc.Implements, new(storage.VolumeImporter)) 791 792 tags := map[string]string{ 793 "a": "b", 794 "c": "d", 795 } 796 info, err := volSource.(storage.VolumeImporter).ImportVolume(s.callCtx, mockVolId, tags) 797 c.Assert(err, jc.ErrorIsNil) 798 c.Assert(info, jc.DeepEquals, storage.VolumeInfo{ 799 VolumeId: mockVolId, 800 Size: mockVolSize, 801 Persistent: true, 802 }) 803 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 804 {"GetVolume", []interface{}{mockVolId}}, 805 {"SetVolumeMetadata", []interface{}{mockVolId, tags}}, 806 }) 807 } 808 809 func (s *cinderVolumeSourceSuite) TestImportVolumeInUse(c *gc.C) { 810 mockAdapter := &mockAdapter{ 811 getVolume: func(volumeId string) (*cinder.Volume, error) { 812 return &cinder.Volume{ 813 ID: volumeId, 814 Status: "in-use", 815 }, nil 816 }, 817 } 818 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 819 _, err := volSource.(storage.VolumeImporter).ImportVolume(s.callCtx, mockVolId, nil) 820 c.Assert(err, gc.ErrorMatches, `cannot import volume "0" with status "in-use"`) 821 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 822 {"GetVolume", []interface{}{mockVolId}}, 823 }) 824 } 825 826 func (s *cinderVolumeSourceSuite) TestImportVolumeInvalidCredential(c *gc.C) { 827 c.Assert(s.invalidCredential, jc.IsFalse) 828 mockAdapter := &mockAdapter{ 829 getVolume: func(volumeId string) (*cinder.Volume, error) { 830 return &cinder.Volume{}, testUnauthorisedGooseError 831 }, 832 } 833 volSource := openstack.NewCinderVolumeSource(mockAdapter, s.env) 834 _, err := volSource.(storage.VolumeImporter).ImportVolume(s.callCtx, mockVolId, nil) 835 c.Assert(err, gc.ErrorMatches, `getting volume: invalid auth`) 836 mockAdapter.CheckCalls(c, []gitjujutesting.StubCall{ 837 {"GetVolume", []interface{}{mockVolId}}, 838 }) 839 c.Assert(s.invalidCredential, jc.IsTrue) 840 } 841 842 type mockAdapter struct { 843 gitjujutesting.Stub 844 getVolume func(string) (*cinder.Volume, error) 845 getVolumesDetail func() ([]cinder.Volume, error) 846 deleteVolume func(string) error 847 createVolume func(cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) 848 attachVolume func(string, string, string) (*nova.VolumeAttachment, error) 849 detachVolume func(string, string) error 850 listVolumeAttachments func(string) ([]nova.VolumeAttachment, error) 851 setVolumeMetadata func(string, map[string]string) (map[string]string, error) 852 listAvailabilityZones func() ([]cinder.AvailabilityZone, error) 853 } 854 855 func (ma *mockAdapter) GetVolume(volumeId string) (*cinder.Volume, error) { 856 ma.MethodCall(ma, "GetVolume", volumeId) 857 if ma.getVolume != nil { 858 return ma.getVolume(volumeId) 859 } 860 return &cinder.Volume{ 861 ID: volumeId, 862 Status: "available", 863 }, nil 864 } 865 866 func (ma *mockAdapter) GetVolumesDetail() ([]cinder.Volume, error) { 867 ma.MethodCall(ma, "GetVolumesDetail") 868 if ma.getVolumesDetail != nil { 869 return ma.getVolumesDetail() 870 } 871 return nil, nil 872 } 873 874 func (ma *mockAdapter) DeleteVolume(volId string) error { 875 ma.MethodCall(ma, "DeleteVolume", volId) 876 if ma.deleteVolume != nil { 877 return ma.deleteVolume(volId) 878 } 879 return nil 880 } 881 882 func (ma *mockAdapter) CreateVolume(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) { 883 ma.MethodCall(ma, "CreateVolume", args) 884 if ma.createVolume != nil { 885 return ma.createVolume(args) 886 } 887 return nil, errors.NotImplementedf("CreateVolume") 888 } 889 890 func (ma *mockAdapter) AttachVolume(serverId, volumeId, mountPoint string) (*nova.VolumeAttachment, error) { 891 ma.MethodCall(ma, "AttachVolume", serverId, volumeId, mountPoint) 892 if ma.attachVolume != nil { 893 return ma.attachVolume(serverId, volumeId, mountPoint) 894 } 895 return nil, errors.NotImplementedf("AttachVolume") 896 } 897 898 func (ma *mockAdapter) DetachVolume(serverId, attachmentId string) error { 899 ma.MethodCall(ma, "DetachVolume", serverId, attachmentId) 900 if ma.detachVolume != nil { 901 return ma.detachVolume(serverId, attachmentId) 902 } 903 return nil 904 } 905 906 func (ma *mockAdapter) ListVolumeAttachments(serverId string) ([]nova.VolumeAttachment, error) { 907 ma.MethodCall(ma, "ListVolumeAttachments", serverId) 908 if ma.listVolumeAttachments != nil { 909 return ma.listVolumeAttachments(serverId) 910 } 911 return nil, nil 912 } 913 914 func (ma *mockAdapter) SetVolumeMetadata(volumeId string, metadata map[string]string) (map[string]string, error) { 915 ma.MethodCall(ma, "SetVolumeMetadata", volumeId, metadata) 916 if ma.setVolumeMetadata != nil { 917 return ma.setVolumeMetadata(volumeId, metadata) 918 } 919 return nil, nil 920 } 921 922 func (ma *mockAdapter) ListVolumeAvailabilityZones() ([]cinder.AvailabilityZone, error) { 923 ma.MethodCall(ma, "ListAvailabilityZones") 924 if ma.listAvailabilityZones != nil { 925 return ma.listAvailabilityZones() 926 } 927 return nil, gooseerrors.NewNotImplementedf(nil, nil, "ListAvailabilityZones") 928 } 929 930 type testEndpointResolver struct { 931 authenticated bool 932 regionEndpoints map[string]identity.ServiceURLs 933 } 934 935 func (r *testEndpointResolver) IsAuthenticated() bool { 936 return r.authenticated 937 } 938 939 func (r *testEndpointResolver) Authenticate() error { 940 r.authenticated = true 941 return nil 942 } 943 944 func (r *testEndpointResolver) EndpointsForRegion(region string) identity.ServiceURLs { 945 if !r.authenticated { 946 return identity.ServiceURLs{} 947 } 948 return r.regionEndpoints[region] 949 } 950 951 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolume(c *gc.C) { 952 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 953 "west": map[string]string{"volume": "http://cinder.testing/v1"}, 954 }} 955 url, err := openstack.GetVolumeEndpointURL(client, "west") 956 c.Assert(err, jc.ErrorIsNil) 957 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v1") 958 } 959 960 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolumeV2(c *gc.C) { 961 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 962 "west": map[string]string{"volumev2": "http://cinder.testing/v2"}, 963 }} 964 url, err := openstack.GetVolumeEndpointURL(client, "west") 965 c.Assert(err, jc.ErrorIsNil) 966 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2") 967 } 968 969 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointV2IfNoV3(c *gc.C) { 970 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 971 "south": map[string]string{ 972 "volume": "http://cinder.testing/v1", 973 "volumev2": "http://cinder.testing/v2", 974 }, 975 }} 976 url, err := openstack.GetVolumeEndpointURL(client, "south") 977 c.Assert(err, jc.ErrorIsNil) 978 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2") 979 } 980 981 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointPreferV3(c *gc.C) { 982 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 983 "south": map[string]string{ 984 "volume": "http://cinder.testing/v1", 985 "volumev2": "http://cinder.testing/v2", 986 "volumev3": "http://cinder.testing/v3", 987 }, 988 }} 989 url, err := openstack.GetVolumeEndpointURL(client, "south") 990 c.Assert(err, jc.ErrorIsNil) 991 c.Assert(url.String(), gc.Equals, "http://cinder.testing/v3") 992 } 993 994 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointMissing(c *gc.C) { 995 client := &testEndpointResolver{} 996 url, err := openstack.GetVolumeEndpointURL(client, "east") 997 c.Assert(err, gc.ErrorMatches, `endpoint "volume" in region "east" not found`) 998 c.Assert(err, jc.Satisfies, errors.IsNotFound) 999 c.Assert(url, gc.IsNil) 1000 } 1001 1002 func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointBadURL(c *gc.C) { 1003 client := &testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{ 1004 "north": map[string]string{"volumev2": "some %4"}, 1005 }} 1006 url, err := openstack.GetVolumeEndpointURL(client, "north") 1007 // NOTE(achilleasa): go1.14 quotes malformed URLs in error messages 1008 // hence the optional quotes in the regex below. 1009 c.Assert(err, gc.ErrorMatches, `parse ("?)some %4("?): .*`) 1010 c.Assert(url, gc.IsNil) 1011 } 1012 1013 func (s *cinderVolumeSourceSuite) setupMocks(c *gc.C) *gomock.Controller { 1014 ctrl := gomock.NewController(c) 1015 1016 s.env = mocks.NewMockZonedEnviron(ctrl) 1017 s.env.EXPECT().InstanceAvailabilityZoneNames( 1018 s.callCtx, []instance.Id{mockServerId}, 1019 ).Return(map[instance.Id]string{mockServerId: "zone-1"}, nil).AnyTimes() 1020 1021 return ctrl 1022 }