Gorka Eguileor | cbaf22e | 2023-04-20 17:09:43 +0200 | [diff] [blame] | 1 | # Copyright 2023 Red Hat |
| 2 | # All Rights Reserved. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 5 | # not use this file except in compliance with the License. You may obtain |
| 6 | # a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | # License for the specific language governing permissions and limitations |
| 14 | # under the License. |
| 15 | |
| 16 | from unittest import mock |
| 17 | |
| 18 | from tempest.common import utils |
| 19 | from tempest.common import waiters |
| 20 | from tempest import config |
| 21 | from tempest.lib import decorators |
| 22 | from tempest.lib import exceptions |
| 23 | from tempest.scenario import manager |
| 24 | |
| 25 | CONF = config.CONF |
| 26 | |
| 27 | |
| 28 | class BaseAttachmentTest(manager.ScenarioTest): |
Ghanshyam Mann | 2803b57 | 2023-08-04 12:11:59 -0700 | [diff] [blame] | 29 | |
| 30 | @classmethod |
| 31 | def skip_checks(cls): |
| 32 | super(BaseAttachmentTest, cls).skip_checks() |
| 33 | if not CONF.service_available.cinder: |
| 34 | raise cls.skipException("Cinder is not available") |
| 35 | |
Gorka Eguileor | cbaf22e | 2023-04-20 17:09:43 +0200 | [diff] [blame] | 36 | @classmethod |
| 37 | def setup_clients(cls): |
| 38 | super().setup_clients() |
| 39 | cls.attachments_client = cls.os_primary.attachments_client_latest |
| 40 | cls.admin_volume_client = cls.os_admin.volumes_client_latest |
| 41 | |
| 42 | def _call_with_fake_service_token(self, valid_token, |
| 43 | client, method_name, *args, **kwargs): |
| 44 | """Call client method with non-service service token |
| 45 | |
| 46 | Add a service token header that can be a valid normal user token (which |
| 47 | won't have the service role) or an invalid token altogether. |
| 48 | """ |
| 49 | original_raw_request = client.raw_request |
| 50 | |
| 51 | def raw_request(url, method, headers=None, body=None, chunked=False, |
| 52 | log_req_body=None): |
| 53 | token = headers['X-Auth-Token'] |
| 54 | if not valid_token: |
| 55 | token = token[:-1] + ('a' if token[-1] != 'a' else 'b') |
| 56 | headers['X-Service-Token'] = token |
| 57 | return original_raw_request(url, method, headers=headers, |
| 58 | body=body, chunked=chunked, |
| 59 | log_req_body=log_req_body) |
| 60 | |
| 61 | client_method = getattr(client, method_name) |
| 62 | with mock.patch.object(client, 'raw_request', raw_request): |
| 63 | return client_method(*args, **kwargs) |
| 64 | |
| 65 | |
| 66 | class TestServerVolumeAttachmentScenario(BaseAttachmentTest): |
| 67 | |
| 68 | """Test server attachment behaviors |
| 69 | |
| 70 | This tests that volume attachments to servers may not be removed directly |
| 71 | and are only allowed through the compute service (bug #2004555). |
| 72 | """ |
| 73 | |
| 74 | @decorators.attr(type='slow') |
| 75 | @decorators.idempotent_id('be615530-f105-437a-8afe-ce998c9535d9') |
| 76 | @utils.services('compute', 'volume', 'image', 'network') |
| 77 | def test_server_detach_rules(self): |
| 78 | """Test that various methods of detaching a volume honors the rules""" |
Ghanshyam Mann | 51c0f9a | 2023-07-21 14:09:40 -0500 | [diff] [blame] | 79 | volume = self.create_volume(wait_until=None) |
| 80 | volume2 = self.create_volume(wait_until=None) |
| 81 | |
Gorka Eguileor | cbaf22e | 2023-04-20 17:09:43 +0200 | [diff] [blame] | 82 | server = self.create_server(wait_until='SSHABLE') |
| 83 | servers = self.servers_client.list_servers()['servers'] |
| 84 | self.assertIn(server['id'], [x['id'] for x in servers]) |
| 85 | |
Ghanshyam Mann | 51c0f9a | 2023-07-21 14:09:40 -0500 | [diff] [blame] | 86 | waiters.wait_for_volume_resource_status(self.volumes_client, |
| 87 | volume['id'], 'available') |
| 88 | # The volume retrieved on creation has a non-up-to-date status. |
| 89 | # Retrieval after it becomes active ensures correct details. |
| 90 | volume = self.volumes_client.show_volume(volume['id'])['volume'] |
Gorka Eguileor | cbaf22e | 2023-04-20 17:09:43 +0200 | [diff] [blame] | 91 | |
| 92 | volume = self.nova_volume_attach(server, volume) |
| 93 | self.addCleanup(self.nova_volume_detach, server, volume) |
| 94 | att_id = volume['attachments'][0]['attachment_id'] |
| 95 | |
| 96 | # Test user call to detach volume is rejected |
| 97 | self.assertRaises((exceptions.Forbidden, exceptions.Conflict), |
| 98 | self.volumes_client.detach_volume, volume['id']) |
| 99 | |
| 100 | # Test user call to terminate connection is rejected |
| 101 | self.assertRaises((exceptions.Forbidden, exceptions.Conflict), |
| 102 | self.volumes_client.terminate_connection, |
| 103 | volume['id'], connector={}) |
| 104 | |
| 105 | # Test faking of service token on call to detach, force detach, |
| 106 | # terminate_connection |
| 107 | for valid_token in (True, False): |
| 108 | valid_exceptions = [exceptions.Forbidden, exceptions.Conflict] |
| 109 | if not valid_token: |
| 110 | valid_exceptions.append(exceptions.Unauthorized) |
| 111 | self.assertRaises( |
| 112 | tuple(valid_exceptions), |
| 113 | self._call_with_fake_service_token, |
| 114 | valid_token, |
| 115 | self.volumes_client, |
| 116 | 'detach_volume', |
| 117 | volume['id']) |
| 118 | self.assertRaises( |
| 119 | tuple(valid_exceptions), |
| 120 | self._call_with_fake_service_token, |
| 121 | valid_token, |
| 122 | self.volumes_client, |
| 123 | 'terminate_connection', |
| 124 | volume['id'], connector={}) |
| 125 | |
| 126 | # Reset volume's status to error |
| 127 | self.admin_volume_client.reset_volume_status(volume['id'], |
| 128 | status='error') |
| 129 | waiters.wait_for_volume_resource_status(self.volumes_client, |
| 130 | volume['id'], 'error') |
| 131 | |
| 132 | # For the cleanup, we need to reset the volume status to in-use before |
| 133 | # the other cleanup steps try to detach it. |
| 134 | self.addCleanup(waiters.wait_for_volume_resource_status, |
| 135 | self.volumes_client, volume['id'], 'in-use') |
| 136 | self.addCleanup(self.admin_volume_client.reset_volume_status, |
| 137 | volume['id'], status='in-use') |
| 138 | |
| 139 | # Test user call to force detach volume is rejected |
| 140 | self.assertRaises( |
| 141 | (exceptions.Forbidden, exceptions.Conflict), |
| 142 | self.admin_volume_client.force_detach_volume, |
| 143 | volume['id'], connector=None, |
| 144 | attachment_id=att_id) |
| 145 | |
| 146 | # Test trying to override detach with force and service token |
| 147 | for valid_token in (True, False): |
| 148 | valid_exceptions = [exceptions.Forbidden, exceptions.Conflict] |
| 149 | if not valid_token: |
| 150 | valid_exceptions.append(exceptions.Unauthorized) |
| 151 | self.assertRaises( |
| 152 | tuple(valid_exceptions), |
| 153 | self._call_with_fake_service_token, |
| 154 | valid_token, |
| 155 | self.admin_volume_client, |
| 156 | 'force_detach_volume', |
| 157 | volume['id'], connector=None, attachment_id=att_id) |
| 158 | |
| 159 | # Test user call to detach with mismatch is rejected |
Ghanshyam Mann | 51c0f9a | 2023-07-21 14:09:40 -0500 | [diff] [blame] | 160 | waiters.wait_for_volume_resource_status(self.volumes_client, |
| 161 | volume2['id'], 'available') |
| 162 | # The volume retrieved on creation has a non-up-to-date status. |
| 163 | # Retrieval after it becomes active ensures correct details. |
| 164 | volume2 = self.volumes_client.show_volume(volume2['id'])['volume'] |
| 165 | |
Gorka Eguileor | cbaf22e | 2023-04-20 17:09:43 +0200 | [diff] [blame] | 166 | volume2 = self.nova_volume_attach(server, volume2) |
| 167 | att_id2 = volume2['attachments'][0]['attachment_id'] |
| 168 | self.assertRaises( |
| 169 | (exceptions.Forbidden, exceptions.BadRequest), |
| 170 | self.volumes_client.detach_volume, |
| 171 | volume['id'], attachment_id=att_id2) |
| 172 | |
| 173 | |
| 174 | class TestServerVolumeAttachScenarioOldVersion(BaseAttachmentTest): |
| 175 | volume_min_microversion = '3.27' |
| 176 | volume_max_microversion = 'latest' |
| 177 | |
| 178 | @decorators.attr(type='slow') |
| 179 | @decorators.idempotent_id('6f4d2144-99f4-495c-8b0b-c6a537971418') |
| 180 | @utils.services('compute', 'volume', 'image', 'network') |
| 181 | def test_old_versions_reject(self): |
| 182 | server = self.create_server(wait_until='SSHABLE') |
| 183 | servers = self.servers_client.list_servers()['servers'] |
| 184 | self.assertIn(server['id'], [x['id'] for x in servers]) |
| 185 | |
| 186 | volume = self.create_volume() |
| 187 | |
| 188 | volume = self.nova_volume_attach(server, volume) |
| 189 | self.addCleanup(self.nova_volume_detach, server, volume) |
| 190 | att_id = volume['attachments'][0]['attachment_id'] |
| 191 | |
| 192 | for valid_token in (True, False): |
| 193 | valid_exceptions = [exceptions.Forbidden, |
| 194 | exceptions.Conflict] |
| 195 | if not valid_token: |
| 196 | valid_exceptions.append(exceptions.Unauthorized) |
| 197 | self.assertRaises( |
| 198 | tuple(valid_exceptions), |
| 199 | self._call_with_fake_service_token, |
| 200 | valid_token, |
| 201 | self.attachments_client, |
| 202 | 'delete_attachment', |
| 203 | att_id) |
| 204 | |
| 205 | self.assertRaises( |
| 206 | (exceptions.Forbidden, exceptions.Conflict), |
| 207 | self.attachments_client.delete_attachment, |
| 208 | att_id) |