github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/openstack/cinder_test.go (about)

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