1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
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"
29
mockVolSize = 1024 * 2
31
mockServerId = "mock-server-id"
32
mockVolJson = `{"volume":{"id": "` + mockVolId + `", "size":1,"name":"` + mockVolName + `"}}`
36
mockVolumeTag = names.NewVolumeTag(mockVolName)
37
mockMachineTag = names.NewMachineTag("456")
40
var _ = gc.Suite(&cinderVolumeSourceSuite{})
42
type cinderVolumeSourceSuite struct {
47
// Override attempt strategy to speed things up.
48
openstack.CinderAttempt.Delay = 0
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{
65
volSource := openstack.NewCinderVolumeSource(mockAdapter)
66
results, err := volSource.AttachVolumes([]storage.VolumeAttachmentParams{{
67
Volume: mockVolumeTag,
69
AttachmentParams: storage.AttachmentParams{
70
Provider: openstack.CinderProviderType,
71
Machine: mockMachineTag,
72
InstanceId: instance.Id(mockServerId),
75
c.Assert(err, jc.ErrorIsNil)
76
c.Check(results, jc.DeepEquals, []storage.AttachVolumesResult{{
77
VolumeAttachment: &storage.VolumeAttachment{
80
storage.VolumeAttachmentInfo{
87
func (s *cinderVolumeSourceSuite) TestCreateVolume(c *gc.C) {
89
requestedSize = 2 * 1024
90
providedSize = 3 * 1024
93
s.PatchValue(openstack.CinderAttempt, utils.AttemptStrategy{Min: 3})
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",
102
return &cinder.Volume{
106
getVolume: func(volumeId string) (*cinder.Volume, error) {
109
if getVolumeCalls > 1 {
112
return &cinder.Volume{
114
Size: providedSize / 1024,
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{
130
volSource := openstack.NewCinderVolumeSource(mockAdapter)
131
results, err := volSource.CreateVolumes([]storage.VolumeParams{{
132
Provider: openstack.CinderProviderType,
135
Attachment: &storage.VolumeAttachmentParams{
136
AttachmentParams: storage.AttachmentParams{
137
Provider: openstack.CinderProviderType,
138
Machine: mockMachineTag,
139
InstanceId: instance.Id(mockServerId),
143
c.Assert(err, jc.ErrorIsNil)
144
c.Assert(results, gc.HasLen, 1)
145
c.Assert(results[0].Error, jc.ErrorIsNil)
147
c.Check(results[0].Volume, jc.DeepEquals, &storage.Volume{
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)
161
func (s *cinderVolumeSourceSuite) TestResourceTags(c *gc.C) {
163
mockAdapter := &mockAdapter{
164
createVolume: func(args cinder.CreateVolumeVolumeParams) (*cinder.Volume, error) {
166
c.Assert(args, jc.DeepEquals, cinder.CreateVolumeVolumeParams{
168
Name: "juju-testenv-volume-123",
169
Metadata: map[string]string{
170
"ResourceTag1": "Value1",
171
"ResourceTag2": "Value2",
174
return &cinder.Volume{ID: mockVolId}, nil
176
getVolume: func(volumeId string) (*cinder.Volume, error) {
177
return &cinder.Volume{
183
attachVolume: func(serverId, volId, mountPoint string) (*nova.VolumeAttachment, error) {
184
return &nova.VolumeAttachment{
193
volSource := openstack.NewCinderVolumeSource(mockAdapter)
194
_, err := volSource.CreateVolumes([]storage.VolumeParams{{
195
Provider: openstack.CinderProviderType,
198
ResourceTags: map[string]string{
199
"ResourceTag1": "Value1",
200
"ResourceTag2": "Value2",
202
Attachment: &storage.VolumeAttachmentParams{
203
AttachmentParams: storage.AttachmentParams{
204
Provider: openstack.CinderProviderType,
205
Machine: mockMachineTag,
206
InstanceId: instance.Id(mockServerId),
210
c.Assert(err, jc.ErrorIsNil)
211
c.Assert(created, jc.IsTrue)
214
func (s *cinderVolumeSourceSuite) TestListVolumes(c *gc.C) {
215
mockAdapter := &mockAdapter{
216
getVolumesDetail: func() ([]cinder.Volume, error) {
217
return []cinder.Volume{{
221
Metadata: map[string]string{
222
tags.JujuModel: "something-else",
226
Metadata: map[string]string{
227
tags.JujuModel: testing.ModelTag.Id(),
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"})
238
func (s *cinderVolumeSourceSuite) TestDescribeVolumes(c *gc.C) {
239
mockAdapter := &mockAdapter{
240
getVolumesDetail: func() ([]cinder.Volume, error) {
241
return []cinder.Volume{{
243
Size: mockVolSize / 1024,
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{
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}},
271
func (s *cinderVolumeSourceSuite) TestDestroyVolumesAttached(c *gc.C) {
272
statuses := []string{"in-use", "detaching", "available"}
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{
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},
295
"GetVolume", []interface{}{mockVolId},
297
"GetVolume", []interface{}{mockVolId},
299
"DeleteVolume", []interface{}{mockVolId},
303
func (s *cinderVolumeSourceSuite) TestDetachVolumes(c *gc.C) {
304
const mockServerId2 = mockServerId + "2"
306
var numListCalls, numDetachCalls int
307
mockAdapter := &mockAdapter{
308
listVolumeAttachments: func(serverId string) ([]nova.VolumeAttachment, error) {
310
if serverId == mockServerId2 {
314
c.Check(serverId, gc.Equals, mockServerId)
315
return []nova.VolumeAttachment{{
318
ServerId: mockServerId,
322
detachVolume: func(serverId, volId string) error {
324
c.Check(serverId, gc.Equals, mockServerId)
325
c.Check(volId, gc.Equals, mockVolId)
330
volSource := openstack.NewCinderVolumeSource(mockAdapter)
331
errs, err := volSource.DetachVolumes([]storage.VolumeAttachmentParams{{
332
Volume: names.NewVolumeTag("123"),
334
AttachmentParams: storage.AttachmentParams{
335
Machine: names.NewMachineTag("0"),
336
InstanceId: mockServerId,
339
Volume: names.NewVolumeTag("42"),
341
AttachmentParams: storage.AttachmentParams{
342
Machine: names.NewMachineTag("0"),
343
InstanceId: mockServerId2,
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},
352
"DetachVolume", []interface{}{mockServerId, mockVolId},
354
"ListVolumeAttachments", []interface{}{mockServerId2},
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) {
363
if numCreateCalls == 3 {
364
return nil, errors.New("no volume for you")
366
return &cinder.Volume{
367
ID: fmt.Sprint(numCreateCalls),
371
deleteVolume: func(volId string) error {
373
c.Assert(volId, gc.Equals, "2")
374
return errors.New("destroy fails")
376
getVolume: func(volumeId string) (*cinder.Volume, error) {
378
if numGetCalls == 2 {
379
return nil, errors.New("no volume details for you")
381
return &cinder.Volume{
383
Size: mockVolSize / 1024,
389
volSource := openstack.NewCinderVolumeSource(mockAdapter)
390
volumeParams := []storage.VolumeParams{{
391
Provider: openstack.CinderProviderType,
392
Tag: names.NewVolumeTag("0"),
394
Attachment: &storage.VolumeAttachmentParams{
395
AttachmentParams: storage.AttachmentParams{
396
Provider: openstack.CinderProviderType,
397
Machine: mockMachineTag,
398
InstanceId: instance.Id(mockServerId),
402
Provider: openstack.CinderProviderType,
403
Tag: names.NewVolumeTag("1"),
405
Attachment: &storage.VolumeAttachmentParams{
406
AttachmentParams: storage.AttachmentParams{
407
Provider: openstack.CinderProviderType,
408
Machine: mockMachineTag,
409
InstanceId: instance.Id(mockServerId),
413
Provider: openstack.CinderProviderType,
414
Tag: names.NewVolumeTag("2"),
416
Attachment: &storage.VolumeAttachmentParams{
417
AttachmentParams: storage.AttachmentParams{
418
Provider: openstack.CinderProviderType,
419
Machine: mockMachineTag,
420
InstanceId: instance.Id(mockServerId),
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)
435
type mockAdapter struct {
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)
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)
452
return &cinder.Volume{
458
func (ma *mockAdapter) GetVolumesDetail() ([]cinder.Volume, error) {
459
ma.MethodCall(ma, "GetVolumesDetail")
460
if ma.getVolumesDetail != nil {
461
return ma.getVolumesDetail()
466
func (ma *mockAdapter) DeleteVolume(volId string) error {
467
ma.MethodCall(ma, "DeleteVolume", volId)
468
if ma.deleteVolume != nil {
469
return ma.deleteVolume(volId)
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)
479
return nil, errors.NotImplementedf("CreateVolume")
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)
487
return nil, errors.NotImplementedf("AttachVolume")
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)
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)
506
type testEndpointResolver struct {
507
regionEndpoints map[string]identity.ServiceURLs
510
func (r testEndpointResolver) EndpointsForRegion(region string) identity.ServiceURLs {
511
return r.regionEndpoints[region]
514
func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolume(c *gc.C) {
515
client := testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{
516
"west": map[string]string{"volume": "http://cinder.testing/v1"},
518
url, err := openstack.GetVolumeEndpointURL(client, "west")
519
c.Assert(err, jc.ErrorIsNil)
520
c.Assert(url.String(), gc.Equals, "http://cinder.testing/v1")
523
func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointVolumeV2(c *gc.C) {
524
client := testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{
525
"west": map[string]string{"volumev2": "http://cinder.testing/v2"},
527
url, err := openstack.GetVolumeEndpointURL(client, "west")
528
c.Assert(err, jc.ErrorIsNil)
529
c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2")
532
func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointPreferV2(c *gc.C) {
533
client := testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{
534
"south": map[string]string{
535
"volume": "http://cinder.testing/v1",
536
"volumev2": "http://cinder.testing/v2",
539
url, err := openstack.GetVolumeEndpointURL(client, "south")
540
c.Assert(err, jc.ErrorIsNil)
541
c.Assert(url.String(), gc.Equals, "http://cinder.testing/v2")
544
func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointMissing(c *gc.C) {
545
client := testEndpointResolver{}
546
url, err := openstack.GetVolumeEndpointURL(client, "east")
547
c.Assert(err, gc.ErrorMatches, `endpoint "volume" in region "east" not found`)
548
c.Assert(err, jc.Satisfies, errors.IsNotFound)
549
c.Assert(url, gc.IsNil)
552
func (s *cinderVolumeSourceSuite) TestGetVolumeEndpointBadURL(c *gc.C) {
553
client := testEndpointResolver{regionEndpoints: map[string]identity.ServiceURLs{
554
"north": map[string]string{"volumev2": "some %4"},
556
url, err := openstack.GetVolumeEndpointURL(client, "north")
557
c.Assert(err, gc.ErrorMatches, `parse some %4: .*`)
558
c.Assert(url, gc.IsNil)