github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/openstack/cinder_test.go (about)

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