Skip to content

Commit f44abb0

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "tests: Split external service fixtures out"
2 parents eb3926e + 212f89a commit f44abb0

File tree

8 files changed

+1835
-1703
lines changed

8 files changed

+1835
-1703
lines changed

nova/tests/fixtures/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
from .api_paste import ApiPasteNoProjectId # noqa: F401
1414
from .api_paste import ApiPasteV21Fixture # noqa: F401
1515
from .cast_as_call import CastAsCallFixture # noqa: F401
16-
from .conf import ConfFixture # noqa: F401
16+
from .cinder import CinderFixture # noqa: F401
17+
from .conf import ConfFixture # noqa: F401, F403
18+
from .cyborg import CyborgFixture # noqa: F401
19+
from .glance import GlanceFixture # noqa: F401
20+
from .neutron import NeutronFixture # noqa: F401
1721
from .nova import * # noqa: F401, F403
1822
from .policy import OverridePolicyFixture # noqa: F401
1923
from .policy import PolicyFixture # noqa: F401

nova/tests/fixtures/cinder.py

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""Cinder fixture."""
14+
15+
import collections
16+
17+
import fixtures
18+
from oslo_log import log as logging
19+
from oslo_utils import uuidutils
20+
21+
from nova import exception
22+
from nova.tests.fixtures import nova as nova_fixtures
23+
24+
LOG = logging.getLogger(__name__)
25+
26+
27+
class CinderFixture(fixtures.Fixture):
28+
"""A fixture to volume operations with the new Cinder attach/detach API"""
29+
30+
# the default project_id in OSAPIFixtures
31+
tenant_id = nova_fixtures.PROJECT_ID
32+
33+
SWAP_OLD_VOL = 'a07f71dc-8151-4e7d-a0cc-cd24a3f11113'
34+
SWAP_NEW_VOL = '227cc671-f30b-4488-96fd-7d0bf13648d8'
35+
SWAP_ERR_OLD_VOL = '828419fa-3efb-4533-b458-4267ca5fe9b1'
36+
SWAP_ERR_NEW_VOL = '9c6d9c2d-7a8f-4c80-938d-3bf062b8d489'
37+
SWAP_ERR_ATTACH_ID = '4a3cd440-b9c2-11e1-afa6-0800200c9a66'
38+
MULTIATTACH_VOL = '4757d51f-54eb-4442-8684-3399a6431f67'
39+
40+
# This represents a bootable image-backed volume to test
41+
# boot-from-volume scenarios.
42+
IMAGE_BACKED_VOL = '6ca404f3-d844-4169-bb96-bc792f37de98'
43+
# This represents a bootable image-backed volume with required traits
44+
# as part of volume image metadata
45+
IMAGE_WITH_TRAITS_BACKED_VOL = '6194fc02-c60e-4a01-a8e5-600798208b5f'
46+
47+
def __init__(self, test, az='nova'):
48+
"""Initialize this instance of the CinderFixture.
49+
50+
:param test: The TestCase using this fixture.
51+
:param az: The availability zone to return in volume GET responses.
52+
Defaults to "nova" since that is the default we would see
53+
from Cinder's storage_availability_zone config option.
54+
"""
55+
super().__init__()
56+
self.test = test
57+
self.swap_volume_instance_uuid = None
58+
self.swap_volume_instance_error_uuid = None
59+
self.attachment_error_id = None
60+
self.az = az
61+
# A dict, keyed by volume id, to a dict, keyed by attachment id,
62+
# with keys:
63+
# - id: the attachment id
64+
# - instance_uuid: uuid of the instance attached to the volume
65+
# - connector: host connector dict; None if not connected
66+
# Note that a volume can have multiple attachments even without
67+
# multi-attach, as some flows create a blank 'reservation' attachment
68+
# before deleting another attachment. However, a non-multiattach volume
69+
# can only have at most one attachment with a host connector at a time.
70+
self.volume_to_attachment = collections.defaultdict(dict)
71+
72+
def setUp(self):
73+
super().setUp()
74+
75+
def fake_get(self_api, context, volume_id, microversion=None):
76+
# Check for the special swap volumes.
77+
attachments = self.volume_to_attachment[volume_id]
78+
79+
if volume_id in (self.SWAP_OLD_VOL, self.SWAP_ERR_OLD_VOL):
80+
volume = {
81+
'status': 'available',
82+
'display_name': 'TEST1',
83+
'attach_status': 'detached',
84+
'id': volume_id,
85+
'multiattach': False,
86+
'size': 1
87+
}
88+
if (
89+
(
90+
self.swap_volume_instance_uuid and
91+
volume_id == self.SWAP_OLD_VOL
92+
) or (
93+
self.swap_volume_instance_error_uuid and
94+
volume_id == self.SWAP_ERR_OLD_VOL
95+
)
96+
):
97+
if volume_id == self.SWAP_OLD_VOL:
98+
instance_uuid = self.swap_volume_instance_uuid
99+
else:
100+
instance_uuid = self.swap_volume_instance_error_uuid
101+
102+
if attachments:
103+
attachment = list(attachments.values())[0]
104+
105+
volume.update({
106+
'status': 'in-use',
107+
'attachments': {
108+
instance_uuid: {
109+
'mountpoint': '/dev/vdb',
110+
'attachment_id': attachment['id']
111+
}
112+
},
113+
'attach_status': 'attached',
114+
})
115+
return volume
116+
117+
# Check to see if the volume is attached.
118+
if attachments:
119+
# The volume is attached.
120+
attachment = list(attachments.values())[0]
121+
volume = {
122+
'status': 'in-use',
123+
'display_name': volume_id,
124+
'attach_status': 'attached',
125+
'id': volume_id,
126+
'multiattach': volume_id == self.MULTIATTACH_VOL,
127+
'size': 1,
128+
'attachments': {
129+
attachment['instance_uuid']: {
130+
'attachment_id': attachment['id'],
131+
'mountpoint': '/dev/vdb'
132+
}
133+
}
134+
}
135+
else:
136+
# This is a test that does not care about the actual details.
137+
volume = {
138+
'status': 'available',
139+
'display_name': 'TEST2',
140+
'attach_status': 'detached',
141+
'id': volume_id,
142+
'multiattach': volume_id == self.MULTIATTACH_VOL,
143+
'size': 1
144+
}
145+
146+
if 'availability_zone' not in volume:
147+
volume['availability_zone'] = self.az
148+
149+
# Check for our special image-backed volume.
150+
if volume_id in (
151+
self.IMAGE_BACKED_VOL, self.IMAGE_WITH_TRAITS_BACKED_VOL,
152+
):
153+
# Make it a bootable volume.
154+
volume['bootable'] = True
155+
if volume_id == self.IMAGE_BACKED_VOL:
156+
# Add the image_id metadata.
157+
volume['volume_image_metadata'] = {
158+
# There would normally be more image metadata in here.
159+
'image_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6'
160+
}
161+
elif volume_id == self.IMAGE_WITH_TRAITS_BACKED_VOL:
162+
# Add the image_id metadata with traits.
163+
volume['volume_image_metadata'] = {
164+
'image_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6',
165+
"trait:HW_CPU_X86_SGX": "required",
166+
}
167+
168+
return volume
169+
170+
def fake_migrate_volume_completion(
171+
_self, context, old_volume_id, new_volume_id, error,
172+
):
173+
return {'save_volume_id': new_volume_id}
174+
175+
def _find_attachment(attachment_id):
176+
"""Find attachment corresponding to ``attachment_id``.
177+
178+
:returns: A tuple of the volume ID, an attachment dict for the
179+
given attachment ID, and a dict (keyed by attachment id) of
180+
attachment dicts for the volume.
181+
"""
182+
for volume_id, attachments in self.volume_to_attachment.items():
183+
for attachment in attachments.values():
184+
if attachment_id == attachment['id']:
185+
return volume_id, attachment, attachments
186+
187+
raise exception.VolumeAttachmentNotFound(
188+
attachment_id=attachment_id)
189+
190+
def fake_attachment_create(
191+
_self, context, volume_id, instance_uuid, connector=None,
192+
mountpoint=None,
193+
):
194+
attachment_id = uuidutils.generate_uuid()
195+
if self.attachment_error_id is not None:
196+
attachment_id = self.attachment_error_id
197+
attachment = {'id': attachment_id, 'connection_info': {'data': {}}}
198+
self.volume_to_attachment[volume_id][attachment_id] = {
199+
'id': attachment_id,
200+
'instance_uuid': instance_uuid,
201+
'connector': connector,
202+
}
203+
LOG.info(
204+
'Created attachment %s for volume %s. Total attachments '
205+
'for volume: %d',
206+
attachment_id, volume_id,
207+
len(self.volume_to_attachment[volume_id]))
208+
209+
return attachment
210+
211+
def fake_attachment_delete(_self, context, attachment_id):
212+
# 'attachment' is a tuple defining a attachment-instance mapping
213+
volume_id, attachment, attachments = (
214+
_find_attachment(attachment_id))
215+
del attachments[attachment_id]
216+
LOG.info(
217+
'Deleted attachment %s for volume %s. Total attachments '
218+
'for volume: %d',
219+
attachment_id, volume_id, len(attachments))
220+
221+
def fake_attachment_update(
222+
_self, context, attachment_id, connector, mountpoint=None,
223+
):
224+
# Ensure the attachment exists
225+
volume_id, attachment, attachments = _find_attachment(
226+
attachment_id)
227+
# Cinder will only allow one "connected" attachment per
228+
# non-multiattach volume at a time.
229+
if volume_id != self.MULTIATTACH_VOL:
230+
for _attachment in attachments.values():
231+
if _attachment['connector'] is not None:
232+
raise exception.InvalidInput(
233+
'Volume %s is already connected with attachment '
234+
'%s on host %s' % (
235+
volume_id, _attachment['id'],
236+
_attachment['connector'].get('host')))
237+
238+
attachment['connector'] = connector
239+
LOG.info('Updating volume attachment: %s', attachment_id)
240+
attachment_ref = {
241+
'id': attachment_id,
242+
'connection_info': {
243+
'driver_volume_type': 'fake',
244+
'data': {
245+
'foo': 'bar',
246+
'target_lun': '1'
247+
}
248+
}
249+
}
250+
if attachment_id == self.SWAP_ERR_ATTACH_ID:
251+
# This intentionally triggers a TypeError for the
252+
# instance.volume_swap.error versioned notification tests.
253+
attachment_ref = {'connection_info': ()}
254+
return attachment_ref
255+
256+
def fake_attachment_get(_self, context, attachment_id):
257+
# Ensure the attachment exists
258+
_find_attachment(attachment_id)
259+
attachment_ref = {
260+
'driver_volume_type': 'fake_type',
261+
'id': attachment_id,
262+
'connection_info': {
263+
'data': {
264+
'foo': 'bar',
265+
'target_lun': '1',
266+
},
267+
},
268+
}
269+
return attachment_ref
270+
271+
def fake_get_all_volume_types(*args, **kwargs):
272+
return [{
273+
# This is used in the 2.67 API sample test.
274+
'id': '5f9204ec-3e94-4f27-9beb-fe7bb73b6eb9',
275+
'name': 'lvm-1'
276+
}]
277+
278+
def fake_attachment_complete(_self, _context, attachment_id):
279+
# Ensure the attachment exists
280+
_find_attachment(attachment_id)
281+
LOG.info('Completing volume attachment: %s', attachment_id)
282+
283+
self.test.stub_out(
284+
'nova.volume.cinder.API.attachment_create', fake_attachment_create)
285+
self.test.stub_out(
286+
'nova.volume.cinder.API.attachment_delete', fake_attachment_delete)
287+
self.test.stub_out(
288+
'nova.volume.cinder.API.attachment_update', fake_attachment_update)
289+
self.test.stub_out(
290+
'nova.volume.cinder.API.attachment_complete',
291+
fake_attachment_complete)
292+
self.test.stub_out(
293+
'nova.volume.cinder.API.attachment_get', fake_attachment_get)
294+
self.test.stub_out(
295+
'nova.volume.cinder.API.begin_detaching',
296+
lambda *args, **kwargs: None)
297+
self.test.stub_out('nova.volume.cinder.API.get', fake_get)
298+
self.test.stub_out(
299+
'nova.volume.cinder.API.migrate_volume_completion',
300+
fake_migrate_volume_completion)
301+
self.test.stub_out(
302+
'nova.volume.cinder.API.roll_detaching',
303+
lambda *args, **kwargs: None)
304+
self.test.stub_out(
305+
'nova.volume.cinder.is_microversion_supported',
306+
lambda ctxt, microversion: None)
307+
self.test.stub_out(
308+
'nova.volume.cinder.API.check_attached',
309+
lambda *args, **kwargs: None)
310+
self.test.stub_out(
311+
'nova.volume.cinder.API.get_all_volume_types',
312+
fake_get_all_volume_types)
313+
314+
def volume_ids_for_instance(self, instance_uuid):
315+
for volume_id, attachments in self.volume_to_attachment.items():
316+
for attachment in attachments.values():
317+
if attachment['instance_uuid'] == instance_uuid:
318+
# we might have multiple volumes attached to this instance
319+
# so yield rather than return
320+
yield volume_id
321+
break
322+
323+
def attachment_ids_for_instance(self, instance_uuid):
324+
attachment_ids = []
325+
for volume_id, attachments in self.volume_to_attachment.items():
326+
for attachment in attachments.values():
327+
if attachment['instance_uuid'] == instance_uuid:
328+
attachment_ids.append(attachment['id'])
329+
return attachment_ids

0 commit comments

Comments
 (0)