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  }