| # Copyright 2021 Red Hat, Inc. |
| # All Rights Reserved. |
| # |
| # 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 contextlib |
| |
| from oslo_log import log |
| |
| from tempest.common import waiters |
| from tempest import config |
| from tempest.lib.common.utils import data_utils |
| from tempest.lib.common.utils import test_utils |
| from tempest.lib import exceptions as lib_exc |
| |
| from tempest.scenario import manager |
| |
| CONF = config.CONF |
| |
| LOG = log.getLogger(__name__) |
| |
| |
| class ScenarioTest(manager.ScenarioTest): |
| |
| credentials = ['primary', 'admin'] |
| |
| @classmethod |
| def setup_clients(cls): |
| super(ScenarioTest, cls).setup_clients() |
| cls.admin_volume_types_client = cls.os_admin.volume_types_client_latest |
| |
| def _attached_volume_name( |
| self, disks_list_before_attach, ip_address, private_key): |
| ssh = self.get_remote_client(ip_address, private_key=private_key) |
| |
| def _wait_for_volume_available_on_system(): |
| disks_list_after_attach = ssh.list_disks() |
| return len(disks_list_after_attach) > len(disks_list_before_attach) |
| |
| if not test_utils.call_until_true(_wait_for_volume_available_on_system, |
| CONF.compute.build_timeout, |
| CONF.compute.build_interval): |
| raise lib_exc.TimeoutException |
| |
| disks_list_after_attach = ssh.list_disks() |
| volume_name = [item for item in disks_list_after_attach |
| if item not in disks_list_before_attach][0] |
| return volume_name |
| |
| @contextlib.contextmanager |
| def mount_dev_path(self, ssh_client, dev_name, mount_path): |
| if dev_name is not None: |
| ssh_client.exec_command('sudo mount /dev/%s %s' % (dev_name, |
| mount_path)) |
| yield |
| ssh_client.exec_command('sudo umount %s' % mount_path) |
| else: |
| yield |
| |
| def _get_file_md5(self, ip_address, filename, dev_name=None, |
| mount_path='/mnt', private_key=None, server=None): |
| |
| ssh_client = self.get_remote_client(ip_address, |
| private_key=private_key, |
| server=server) |
| with self.mount_dev_path(ssh_client, dev_name, mount_path): |
| md5_sum = ssh_client.exec_command( |
| 'sudo md5sum %s/%s|cut -c 1-32' % (mount_path, filename)) |
| return md5_sum |
| |
| def _count_files(self, ip_address, dev_name=None, mount_path='/mnt', |
| private_key=None, server=None): |
| ssh_client = self.get_remote_client(ip_address, |
| private_key=private_key, |
| server=server) |
| with self.mount_dev_path(ssh_client, dev_name, mount_path): |
| count = ssh_client.exec_command( |
| 'sudo ls -l %s | wc -l' % mount_path) |
| # We subtract 2 from the count since `wc -l` also includes the count |
| # of new line character and while creating the filesystem, a |
| # lost+found folder is also created |
| return int(count) - 2 |
| |
| def _make_fs(self, ip_address, private_key, server, dev_name, fs='ext4'): |
| ssh_client = self.get_remote_client(ip_address, |
| private_key=private_key, |
| server=server) |
| |
| ssh_client.make_fs(dev_name, fs=fs) |
| |
| def create_md5_new_file(self, ip_address, filename, dev_name=None, |
| mount_path='/mnt', private_key=None, server=None): |
| ssh_client = self.get_remote_client(ip_address, |
| private_key=private_key, |
| server=server) |
| |
| with self.mount_dev_path(ssh_client, dev_name, mount_path): |
| ssh_client.exec_command( |
| 'sudo dd bs=1024 count=100 if=/dev/urandom of=/%s/%s' % |
| (mount_path, filename)) |
| md5 = ssh_client.exec_command( |
| 'sudo md5sum -b %s/%s|cut -c 1-32' % (mount_path, filename)) |
| ssh_client.exec_command('sudo sync') |
| return md5 |
| |
| def get_md5_from_file(self, instance, instance_ip, filename, |
| dev_name=None): |
| |
| md5_sum = self._get_file_md5(instance_ip, filename=filename, |
| dev_name=dev_name, |
| private_key=self.keypair['private_key'], |
| server=instance) |
| count = self._count_files(instance_ip, dev_name=dev_name, |
| private_key=self.keypair['private_key'], |
| server=instance) |
| return count, md5_sum |
| |
| def write_data_to_device(self, ip_address, out_dev, in_dev='/dev/urandom', |
| bs=1024, count=100, private_key=None, |
| server=None, sha_sum=False): |
| ssh_client = self.get_remote_client( |
| ip_address, private_key=private_key, server=server) |
| |
| # Write data to device |
| write_command = ( |
| 'sudo dd bs=%(bs)s count=%(count)s if=%(in_dev)s of=%(out_dev)s ' |
| '&& sudo dd bs=%(bs)s count=%(count)s if=%(out_dev)s' % |
| {'bs': str(bs), 'count': str(count), 'in_dev': in_dev, |
| 'out_dev': out_dev}) |
| if sha_sum: |
| # If we want to read sha1sum instead of the device data |
| write_command += ' | sha1sum | head -c 40' |
| data = ssh_client.exec_command(write_command) |
| |
| return data |
| |
| def read_data_from_device(self, ip_address, in_dev, bs=1024, count=100, |
| private_key=None, server=None, sha_sum=False): |
| ssh_client = self.get_remote_client( |
| ip_address, private_key=private_key, server=server) |
| |
| # Read data from device |
| read_command = ('sudo dd bs=%(bs)s count=%(count)s if=%(in_dev)s' % |
| {'bs': bs, 'count': count, 'in_dev': in_dev}) |
| if sha_sum: |
| # If we want to read sha1sum instead of the device data |
| read_command += ' | sha1sum | head -c 40' |
| data = ssh_client.exec_command(read_command) |
| |
| return data |
| |
| def _attach_and_get_volume_device_name(self, server, volume, instance_ip, |
| private_key): |
| ssh_client = self.get_remote_client( |
| instance_ip, private_key=private_key, |
| server=server) |
| # List disks before volume attachment |
| disks_list_before_attach = ssh_client.list_disks() |
| # Attach volume |
| attachment = self.attach_volume(server, volume) |
| # Find the difference between disks before and after attachment that |
| # gives us the volume device name |
| volume_device_name = self._attached_volume_name( |
| disks_list_before_attach, instance_ip, private_key) |
| return volume_device_name, attachment |
| |
| def create_volume_type(self, client=None, name=None, extra_specs=None): |
| if not client: |
| client = self.os_admin.volume_types_client_latest |
| if not name: |
| class_name = self.__class__.__name__ |
| name = data_utils.rand_name(class_name + '-volume-type') |
| randomized_name = data_utils.rand_name('scenario-type-' + name) |
| |
| LOG.debug("Creating a volume type: %s with extra_specs %s", |
| randomized_name, extra_specs) |
| if extra_specs is None: |
| extra_specs = {} |
| volume_type = self.admin_volume_types_client.create_volume_type( |
| name=randomized_name, extra_specs=extra_specs)['volume_type'] |
| self.addCleanup(self.cleanup_volume_type, volume_type) |
| return volume_type |
| |
| def attach_volume(self, server, volume, device=None, tag=None): |
| """Attaches volume to server and waits for 'in-use' volume status. |
| |
| The volume will be detached when the test tears down. |
| |
| :param server: The server to which the volume will be attached. |
| :param volume: The volume to attach. |
| :param device: Optional mountpoint for the attached volume. Note that |
| this is not guaranteed for all hypervisors and is not recommended. |
| :param tag: Optional device role tag to apply to the volume. |
| """ |
| attach_kwargs = dict(volumeId=volume['id']) |
| if device: |
| attach_kwargs['device'] = device |
| if tag: |
| attach_kwargs['tag'] = tag |
| |
| attachment = self.servers_client.attach_volume( |
| server['id'], **attach_kwargs)['volumeAttachment'] |
| # On teardown detach the volume and for multiattach volumes wait for |
| # the attachment to be removed. For non-multiattach volumes wait for |
| # the state of the volume to change to available. This is so we don't |
| # error out when trying to delete the volume during teardown. |
| if volume['multiattach']: |
| att = waiters.wait_for_volume_attachment_create( |
| self.volumes_client, volume['id'], server['id']) |
| self.addCleanup(waiters.wait_for_volume_attachment_remove, |
| self.volumes_client, volume['id'], |
| att['attachment_id']) |
| else: |
| self.addCleanup(waiters.wait_for_volume_resource_status, |
| self.volumes_client, volume['id'], 'available') |
| waiters.wait_for_volume_resource_status(self.volumes_client, |
| volume['id'], 'in-use') |
| # Ignore 404s on detach in case the server is deleted or the volume |
| # is already detached. |
| self.addCleanup(self._detach_volume, server, volume) |
| return attachment |
| |
| def _detach_volume(self, server, volume): |
| """Helper method to detach a volume. |
| |
| Ignores 404 responses if the volume or server do not exist, or the |
| volume is already detached from the server. |
| """ |
| try: |
| volume = self.volumes_client.show_volume(volume['id'])['volume'] |
| # Check the status. You can only detach an in-use volume, otherwise |
| # the compute API will return a 400 response. |
| if volume['status'] == 'in-use': |
| self.servers_client.detach_volume(server['id'], volume['id']) |
| except lib_exc.NotFound: |
| # Ignore 404s on detach in case the server is deleted or the volume |
| # is already detached. |
| pass |