|
| 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