| # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| # not use this file except in compliance with the License. You may obtain |
| # a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| |
| import time |
| |
| from tempest.api.compute import base |
| from tempest.common import waiters |
| from tempest import config |
| from tempest.lib import decorators |
| from tempest.lib import exceptions as lib_exc |
| |
| CONF = config.CONF |
| |
| |
| class TestVolumeSwapBase(base.BaseV2ComputeAdminTest): |
| create_default_network = True |
| |
| @classmethod |
| def setup_credentials(cls): |
| cls.prepare_instance_network() |
| super(TestVolumeSwapBase, cls).setup_credentials() |
| |
| @classmethod |
| def skip_checks(cls): |
| super(TestVolumeSwapBase, cls).skip_checks() |
| if not CONF.service_available.cinder: |
| raise cls.skipException("Cinder is not available") |
| if not CONF.compute_feature_enabled.swap_volume: |
| raise cls.skipException("Swapping volumes is not supported.") |
| |
| def wait_for_server_volume_swap(self, server_id, old_volume_id, |
| new_volume_id): |
| """Waits for a server to swap the old volume to a new one.""" |
| volume_attachments = self.servers_client.list_volume_attachments( |
| server_id)['volumeAttachments'] |
| attached_volume_ids = [attachment['volumeId'] |
| for attachment in volume_attachments] |
| start = int(time.time()) |
| |
| while (old_volume_id in attached_volume_ids) \ |
| or (new_volume_id not in attached_volume_ids): |
| time.sleep(self.servers_client.build_interval) |
| volume_attachments = self.servers_client.list_volume_attachments( |
| server_id)['volumeAttachments'] |
| attached_volume_ids = [attachment['volumeId'] |
| for attachment in volume_attachments] |
| |
| if int(time.time()) - start >= self.servers_client.build_timeout: |
| old_vol_bdm_status = 'in BDM' \ |
| if old_volume_id in attached_volume_ids else 'not in BDM' |
| new_vol_bdm_status = 'in BDM' \ |
| if new_volume_id in attached_volume_ids else 'not in BDM' |
| message = ('Failed to swap old volume %(old_volume_id)s ' |
| '(current %(old_vol_bdm_status)s) to new volume ' |
| '%(new_volume_id)s (current %(new_vol_bdm_status)s)' |
| ' on server %(server_id)s within the required time ' |
| '(%(timeout)s s)' % |
| {'old_volume_id': old_volume_id, |
| 'old_vol_bdm_status': old_vol_bdm_status, |
| 'new_volume_id': new_volume_id, |
| 'new_vol_bdm_status': new_vol_bdm_status, |
| 'server_id': server_id, |
| 'timeout': self.servers_client.build_timeout}) |
| raise lib_exc.TimeoutException(message) |
| |
| |
| class TestVolumeSwap(TestVolumeSwapBase): |
| """The test suite for swapping of volume with admin user""" |
| |
| # NOTE(mriedem): This is an uncommon scenario to call the compute API |
| # to swap volumes directly; swap volume is primarily only for volume |
| # live migration and retype callbacks from the volume service, and is slow |
| # so it's marked as such. |
| @decorators.attr(type='slow') |
| @decorators.idempotent_id('1769f00d-a693-4d67-a631-6a3496773813') |
| def test_volume_swap(self): |
| """Test swapping of volume attached to server with admin user |
| |
| The following is the scenario outline: |
| |
| 1. Create a volume "volume1" with non-admin. |
| 2. Create a volume "volume2" with non-admin. |
| 3. Boot an instance "instance1" with non-admin. |
| 4. Attach "volume1" to "instance1" with non-admin. |
| 5. Swap volume from "volume1" to "volume2" as admin. |
| 6. Check the swap volume is successful and "volume2" |
| is attached to "instance1" and "volume1" is in available state. |
| 7. Swap volume from "volume2" to "volume1" as admin. |
| 8. Check the swap volume is successful and "volume1" |
| is attached to "instance1" and "volume2" is in available state. |
| """ |
| # Create two volumes. |
| # NOTE(gmann): Volumes are created before server creation so that |
| # volumes cleanup can happen successfully irrespective of which volume |
| # is attached to server. |
| volume1 = self.create_volume() |
| volume2 = self.create_volume() |
| # Boot server |
| validation_resources = self.get_class_validation_resources( |
| self.os_primary) |
| # NOTE(gibi): We need to wait for the guest to fully boot as the test |
| # will attach a volume to the server and therefore cleanup will try to |
| # detach it. See bug 1960346 for details. |
| server = self.create_test_server( |
| validatable=True, |
| validation_resources=validation_resources, |
| wait_until='SSHABLE' |
| ) |
| # Attach "volume1" to server |
| self.attach_volume(server, volume1) |
| # Swap volume from "volume1" to "volume2" |
| self.admin_servers_client.update_attached_volume( |
| server['id'], volume1['id'], volumeId=volume2['id']) |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume1['id'], 'available') |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume2['id'], 'in-use') |
| self.wait_for_server_volume_swap(server['id'], volume1['id'], |
| volume2['id']) |
| # Verify "volume2" is attached to the server |
| vol_attachments = self.servers_client.list_volume_attachments( |
| server['id'])['volumeAttachments'] |
| self.assertEqual(1, len(vol_attachments)) |
| self.assertIn(volume2['id'], vol_attachments[0]['volumeId']) |
| |
| # Swap volume from "volume2" to "volume1" |
| self.admin_servers_client.update_attached_volume( |
| server['id'], volume2['id'], volumeId=volume1['id']) |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume2['id'], 'available') |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume1['id'], 'in-use') |
| self.wait_for_server_volume_swap(server['id'], volume2['id'], |
| volume1['id']) |
| # Verify "volume1" is attached to the server |
| vol_attachments = self.servers_client.list_volume_attachments( |
| server['id'])['volumeAttachments'] |
| self.assertEqual(1, len(vol_attachments)) |
| self.assertIn(volume1['id'], vol_attachments[0]['volumeId']) |
| |
| |
| class TestMultiAttachVolumeSwap(TestVolumeSwapBase): |
| """Test swapping volume attached to multiple servers |
| |
| Test swapping volume attached to multiple servers with microversion |
| greater than 2.59 |
| """ |
| |
| min_microversion = '2.60' |
| max_microversion = 'latest' |
| |
| @classmethod |
| def skip_checks(cls): |
| super(TestMultiAttachVolumeSwap, cls).skip_checks() |
| if not CONF.compute_feature_enabled.volume_multiattach: |
| raise cls.skipException('Volume multi-attach is not available.') |
| |
| @classmethod |
| def setup_clients(cls): |
| super(TestMultiAttachVolumeSwap, cls).setup_clients() |
| # Need this to set readonly volumes. |
| cls.admin_volumes_client = cls.os_admin.volumes_client_latest |
| |
| # NOTE(mriedem): This is an uncommon scenario to call the compute API |
| # to swap volumes directly; swap volume is primarily only for volume |
| # live migration and retype callbacks from the volume service, and is slow |
| # so it's marked as such. |
| @decorators.attr(type='slow') |
| @decorators.idempotent_id('e8f8f9d1-d7b7-4cd2-8213-ab85ef697b6e') |
| # For some reason this test intermittently fails on teardown when there are |
| # multiple compute nodes and the servers are split across the computes. |
| # For now, just skip this test if there are multiple computes. |
| # Alternatively we could put the servers in an affinity group if there are |
| # multiple computes but that would just side-step the underlying bug. |
| @decorators.skip_because(bug='1807723', |
| condition=CONF.compute.min_compute_nodes > 1) |
| def test_volume_swap_with_multiattach(self): |
| """Test swapping volume attached to multiple servers |
| |
| The following is the scenario outline: |
| |
| 1. Create a volume "volume1" with non-admin. |
| 2. Create a volume "volume2" with non-admin. |
| 3. Boot 2 instances "server1" and "server2" with non-admin. |
| 4. Attach "volume1" to "server1" with non-admin. |
| 5. Attach "volume1" to "server2" with non-admin. |
| 6. Swap "volume1" to "volume2" on "server1" |
| 7. Check "volume1" is attached to "server2" and not attached to |
| "server1" |
| 8. Check "volume2" is attached to "server1". |
| """ |
| multiattach_vol_type = CONF.volume.volume_type_multiattach |
| # Create two volumes. |
| # NOTE(gmann): Volumes are created before server creation so that |
| # volumes cleanup can happen successfully irrespective of which volume |
| # is attached to server. |
| volume1 = self.create_volume(volume_type=multiattach_vol_type) |
| # Make volume1 read-only since you can't swap from a volume with |
| # multiple read/write attachments, and you can't change the readonly |
| # flag on an in-use volume so we have to do this before attaching |
| # volume1 to anything. If the compute API ever supports per-attachment |
| # attach modes, then we can handle this differently. |
| self.admin_volumes_client.update_volume_readonly( |
| volume1['id'], readonly=True) |
| volume2 = self.create_volume(volume_type=multiattach_vol_type) |
| |
| # Create two servers and wait for them to be ACTIVE. |
| validation_resources = self.get_class_validation_resources( |
| self.os_primary) |
| # NOTE(gibi): We need to wait for the guests to fully boot as the test |
| # will attach volumes to the servers and therefore cleanup will try to |
| # detach them. See bug 1960346 for details. |
| reservation_id = self.create_test_server( |
| validatable=True, |
| validation_resources=validation_resources, |
| wait_until='SSHABLE', |
| min_count=2, |
| return_reservation_id=True, |
| )['reservation_id'] |
| # Get the servers using the reservation_id. |
| servers = self.servers_client.list_servers( |
| reservation_id=reservation_id)['servers'] |
| self.assertEqual(2, len(servers)) |
| # Attach volume1 to server1 |
| server1 = servers[0] |
| self.attach_volume(server1, volume1) |
| # Attach volume1 to server2 |
| server2 = servers[1] |
| self.attach_volume(server2, volume1) |
| |
| # Swap volume1 to volume2 on server1, volume1 should remain attached |
| # to server 2 |
| self.admin_servers_client.update_attached_volume( |
| server1['id'], volume1['id'], volumeId=volume2['id']) |
| # volume1 will return to in-use after the swap |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume1['id'], 'in-use') |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume2['id'], 'in-use') |
| self.wait_for_server_volume_swap(server1['id'], volume1['id'], |
| volume2['id']) |
| |
| # Verify volume2 is attached to server1 |
| vol_attachments = self.servers_client.list_volume_attachments( |
| server1['id'])['volumeAttachments'] |
| self.assertEqual(1, len(vol_attachments)) |
| self.assertIn(volume2['id'], vol_attachments[0]['volumeId']) |
| |
| # Verify volume1 is still attached to server2 |
| vol_attachments = self.servers_client.list_volume_attachments( |
| server2['id'])['volumeAttachments'] |
| self.assertEqual(1, len(vol_attachments)) |
| self.assertIn(volume1['id'], vol_attachments[0]['volumeId']) |