diff --git a/rally_openstack/task/scenarios/manila/shares.py b/rally_openstack/task/scenarios/manila/shares.py index 42046216..3d2d318f 100644 --- a/rally_openstack/task/scenarios/manila/shares.py +++ b/rally_openstack/task/scenarios/manila/shares.py @@ -14,16 +14,22 @@ # under the License. from rally.common import logging +from rally import exceptions +from rally.task import types +from rally.task import utils as rally_utils from rally.task import validation from rally_openstack.common import consts from rally_openstack.task.contexts.manila import consts as manila_consts from rally_openstack.task import scenario from rally_openstack.task.scenarios.manila import utils +from rally_openstack.task.scenarios.vm import utils as vm_utils """Scenarios for Manila shares.""" +LOG = logging.getLogger(__name__) + @validation.add("enum", param_name="share_proto", values=["NFS", "CIFS", "GLUSTERFS", "HDFS", "CEPHFS"], @@ -56,6 +62,113 @@ class CreateAndDeleteShare(utils.ManilaScenario): self.sleep_between(min_sleep, max_sleep) self._delete_share(share) +@types.convert(image={"type": "glance_image"}, + flavor={"type": "nova_flavor"}) +@validation.add("image_valid_on_flavor", flavor_param="flavor", + image_param="image", fail_on_404_image=False) +@validation.add("number", param_name="port", minval=1, maxval=65535, + nullable=True, integer_only=True) +@validation.add("external_network_exists", param_name="floating_network") +@validation.add("required_services", services=[consts.Service.MANILA, + consts.Service.NOVA]) +@validation.add("required_platform", platform="openstack", users=True) +@scenario.configure(context={"cleanup@openstack": ["manila", "nova"], + "keypair@openstack": {}, + "allow_ssh@openstack": None}, + name="ManilaShares.create_share_and_access_from_vm", + platform="openstack") +class CreateShareAndAccessFromVM(utils.ManilaScenario, vm_utils.VMScenario): + def run(self, image, flavor, username, size=1, password=None, + floating_network=None, port=22, + use_floating_ip=True, force_delete=False, max_log_length=None, + **kwargs): + """Create a share and access it from a VM. + + - create NFS share + - launch VM + - authorize VM's fip to access the share + - mount share iside the VM + - write to share + - delete VM + - delete share + + :param size: share size in GB, should be greater than 0 + + :param image: glance image name to use for the vm + :param flavor: VM flavor name + :param username: ssh username on server + :param password: Password on SSH authentication + :param floating_network: external network name, for floating ip + :param port: ssh port for SSH connection + :param use_floating_ip: bool, floating or fixed IP for SSH connection + :param force_delete: whether to use force_delete for servers + :param max_log_length: The number of tail nova console-log lines user + would like to retrieve + + + :param kwargs: optional args to create a share or a VM + """ + share_proto = "nfs" + share = self._create_share( + share_proto=share_proto, + size=size, + **kwargs) + location = self._export_location(share) + + server, fip = self._boot_server_with_fip( + image, flavor, use_floating_ip=use_floating_ip, + floating_network=floating_network, + key_name=self.context["user"]["keypair"]["name"], + userdata="#cloud-config\npackages:\n - nfs-common", + **kwargs) + self._allow_access_share(share, "ip", fip["ip"], "rw") + mount_opt = "-t nfs -o nfsvers=4.1,proto=tcp" + script = f"cloud-init status -w;" \ + f"sudo mount {mount_opt} {location[0]} /mnt || exit 1;" \ + f"sudo touch /mnt/testfile || exit 2" + + command = { + "script_inline": script, + "interpreter": "/bin/bash" + } + try: + rally_utils.wait_for_status( + server, + ready_statuses=["ACTIVE"], + update_resource=rally_utils.get_from_manager(), + ) + + code, out, err = self._run_command( + fip["ip"], port, username, password, command=command) + if code: + raise exceptions.ScriptError( + "Error running command %(command)s. " + "Error %(code)s: %(error)s" % { + "command": command, "code": code, "error": err}) + except (exceptions.TimeoutException, + exceptions.SSHTimeout): + console_logs = self._get_server_console_output(server, + max_log_length) + LOG.debug("VM console logs:\n%s" % console_logs) + raise + + finally: + self._delete_server_with_fip(server, fip, + force_delete=force_delete) + self._delete_share(share) + + self.add_output(complete={ + "title": "Script StdOut", + "chart_plugin": "TextArea", + "data": str(out).split("\n") + }) + if err: + self.add_output(complete={ + "title": "Script StdErr", + "chart_plugin": "TextArea", + "data": err.split("\n") + }) + @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @@ -242,9 +355,9 @@ class ListShareServers(utils.ManilaScenario): self._list_share_servers(search_opts=search_opts) -@validation.add("enum", param_name="share_proto", values=["nfs", "cephfs", - "cifs", "glusterfs", "hdfs"], missed=False, - case_insensitive=True) +@validation.add("enum", param_name="share_proto", + values=["nfs", "cephfs", "cifs", "glusterfs", "hdfs"], + missed=False, case_insensitive=True) @validation.add("required_services", services=[consts.Service.MANILA]) @validation.add("required_platform", platform="openstack", users=True) @scenario.configure( diff --git a/rally_openstack/task/scenarios/manila/utils.py b/rally_openstack/task/scenarios/manila/utils.py index 91e710c8..0680ba9d 100644 --- a/rally_openstack/task/scenarios/manila/utils.py +++ b/rally_openstack/task/scenarios/manila/utils.py @@ -86,6 +86,14 @@ class ManilaScenario(scenario.OpenStackScenario): timeout=CONF.openstack.manila_share_delete_timeout, check_interval=CONF.openstack.manila_share_delete_poll_interval) + def _export_location(self, share): + """Export share location. + + :param share: :class:`Share` + """ + location = share.export_locations + return location + def _get_access_from_share(self, share, access_id): """Get access from share diff --git a/samples/tasks/scenarios/manila/create-share-and-access-from-vm.json b/samples/tasks/scenarios/manila/create-share-and-access-from-vm.json new file mode 100644 index 00000000..4c068c73 --- /dev/null +++ b/samples/tasks/scenarios/manila/create-share-and-access-from-vm.json @@ -0,0 +1,44 @@ +{ + "ManilaShares.create_share_and_access_from_vm": [ + { + "args": { + "size": 1, + "share_type": "CephNFStype", + "flavor": { + "name": "m1.tiny" + }, + "image": { + "name": "^cirros.*-disk$" + }, + "username": "ubuntu" + }, + "runner": { + "type": "constant", + "times": 2, + "concurrency": 2 + }, + "context": { + "quotas": { + "manila": { + "shares": -1, + "gigabytes": -1 + } + }, + "users": { + "tenants": 2, + "users_per_tenant": 1 + }, + "network": { + "dns_nameservers": [ + "9.9.9.9" + ] + } + }, + "sla": { + "failure_rate": { + "max": 0 + } + } + } + ] +} \ No newline at end of file diff --git a/samples/tasks/scenarios/manila/create-share-and-access-from-vm.yaml b/samples/tasks/scenarios/manila/create-share-and-access-from-vm.yaml new file mode 100644 index 00000000..20ac33da --- /dev/null +++ b/samples/tasks/scenarios/manila/create-share-and-access-from-vm.yaml @@ -0,0 +1,28 @@ + ManilaShares.create_share_and_access_from_vm: + - + args: + size: 1 + share_type: "CephNFStype" + flavor: + name: "m1.tiny" + image: + name: "^cirros.*-disk$" + username: "ubuntu" + runner: + type: "constant" + times: 2 + concurrency: 2 + context: + quotas: + manila: + shares: -1 + gigabytes: -1 + users: + tenants: 2 + users_per_tenant: 1 + network: + dns_nameservers: + - "9.9.9.9" + sla: + failure_rate: + max: 0 diff --git a/tests/unit/task/scenarios/manila/test_shares.py b/tests/unit/task/scenarios/manila/test_shares.py index 9529b150..f10fcf80 100644 --- a/tests/unit/task/scenarios/manila/test_shares.py +++ b/tests/unit/task/scenarios/manila/test_shares.py @@ -17,6 +17,7 @@ from unittest import mock import ddt +from rally import exceptions from rally_openstack.task.scenarios.manila import shares from tests.unit import test @@ -42,6 +43,164 @@ class ManilaSharesTestCase(test.ScenarioTestCase): scenario.sleep_between.assert_called_once_with(3, 4) scenario._delete_share.assert_called_once_with(fake_share) + def create_env(self, scenario): + fake_share = mock.MagicMock() + scenario = shares.CreateShareAndAccessFromVM(self.context) + self.ip = {"id": "foo_id", "ip": "foo_ip", "is_floating": True} + scenario._boot_server_with_fip = mock.Mock( + return_value=("foo_server", self.ip)) + scenario._delete_server_with_fip = mock.Mock() + scenario._run_command = mock.MagicMock( + return_value=(0, "{\"foo\": 42}", "foo_err")) + scenario.add_output = mock.Mock() + self.context.update({"user": {"keypair": {"name": "keypair_name"}, + "credential": mock.MagicMock()}}) + scenario._create_share = mock.MagicMock(return_value=fake_share) + scenario._delete_share = mock.MagicMock() + scenario._export_location = mock.MagicMock(return_value="fake") + scenario._allow_access_share = mock.MagicMock() + + return scenario, fake_share + + @ddt.data( + {"image": "some_image", + "flavor": "m1.small", "username": "chuck norris"} + ) + @mock.patch("rally.task.utils.get_from_manager") + @mock.patch("rally.task.utils.wait_for_status") + def test_create_share_and_access_from_vm( + self, + params, + mock_rally_task_utils_wait_for_status, + mock_rally_task_utils_get_from_manager): + scenario, fake_share = self.create_env( + shares.CreateShareAndAccessFromVM(self.context)) + scenario.run(**params) + + scenario._create_share.assert_called_once_with( + share_proto="nfs", size=1) + scenario._delete_share.assert_called_once_with(fake_share) + scenario._allow_access_share.assert_called_once_with( + fake_share, "ip", "foo_ip", "rw") + scenario._export_location.assert_called_once_with(fake_share) + scenario._boot_server_with_fip.assert_called_once_with( + "some_image", "m1.small", use_floating_ip=True, + floating_network=None, key_name="keypair_name", + userdata="#cloud-config\npackages:\n - nfs-common") + mock_rally_task_utils_wait_for_status.assert_called_once_with( + "foo_server", ready_statuses=["ACTIVE"], update_resource=mock.ANY) + scenario._delete_server_with_fip.assert_called_once_with( + "foo_server", {"id": "foo_id", "ip": "foo_ip", + "is_floating": True}, + force_delete=False) + scenario.add_output.assert_called_with( + complete={"chart_plugin": "TextArea", + "data": [ + "foo_err"], + "title": "Script StdErr"}) + + @ddt.data( + {"image": "some_image", + "flavor": "m1.small", "username": "chuck norris"} + ) + @mock.patch("rally.task.utils.get_from_manager") + @mock.patch("rally.task.utils.wait_for_status") + def test_create_share_and_access_from_vm_command_timeout( + self, + params, + mock_rally_task_utils_wait_for_status, + mock_rally_task_utils_get_from_manager): + scenario, fake_share = self.create_env( + shares.CreateShareAndAccessFromVM(self.context)) + + scenario._run_command.side_effect = exceptions.SSHTimeout() + self.assertRaises(exceptions.SSHTimeout, + scenario.run, + "foo_flavor", "foo_image", "foo_interpreter", + "foo_script", "foo_username") + scenario._delete_server_with_fip.assert_called_once_with( + "foo_server", self.ip, force_delete=False) + self.assertFalse(scenario.add_output.called) + scenario._delete_share.assert_called_once_with(fake_share) + + @ddt.data( + {"image": "some_image", + "flavor": "m1.small", "username": "chuck norris"} + ) + @mock.patch("rally.task.utils.get_from_manager") + @mock.patch("rally.task.utils.wait_for_status") + def test_create_share_and_access_from_vm_wait_timeout( + self, + params, + mock_rally_task_utils_wait_for_status, + mock_rally_task_utils_get_from_manager): + scenario, fake_share = self.create_env( + shares.CreateShareAndAccessFromVM(self.context)) + + mock_rally_task_utils_wait_for_status.side_effect = \ + exceptions.TimeoutException( + resource_type="foo_resource", + resource_name="foo_name", + resource_id="foo_id", + desired_status="foo_desired_status", + resource_status="foo_resource_status", + timeout=2) + self.assertRaises(exceptions.TimeoutException, + scenario.run, + "foo_flavor", "foo_image", "foo_interpreter", + "foo_script", "foo_username") + scenario._delete_server_with_fip.assert_called_once_with( + "foo_server", self.ip, force_delete=False) + self.assertFalse(scenario.add_output.called) + scenario._delete_share.assert_called_once_with(fake_share) + + @ddt.data( + {"output": (0, "", ""), + "expected": [{"complete": {"chart_plugin": "TextArea", + "data": [""], + "title": "Script StdOut"}}]}, + {"output": (1, "x y z", "error message"), + "raises": exceptions.ScriptError}, + {"output": (0, "[1, 2, 3, 4]", ""), "expected": []} + ) + @ddt.unpack + def test_create_share_and_access_from_vm_add_output(self, output, + expected=None, + raises=None): + scenario, fake_share = self.create_env( + shares.CreateShareAndAccessFromVM(self.context)) + + scenario._run_command.return_value = output + kwargs = {"flavor": "foo_flavor", + "image": "foo_image", + "username": "foo_username", + "password": "foo_password", + "use_floating_ip": "use_fip", + "floating_network": "ext_network", + "force_delete": "foo_force"} + if raises: + self.assertRaises(raises, scenario.run, **kwargs) + self.assertFalse(scenario.add_output.called) + else: + scenario.run(**kwargs) + calls = [mock.call(**kw) for kw in expected] + scenario.add_output.assert_has_calls(calls, any_order=True) + + scenario._create_share.assert_called_once_with( + share_proto="nfs", size=1) + scenario._delete_share.assert_called_once_with(fake_share) + scenario._allow_access_share.assert_called_once_with( + fake_share, "ip", "foo_ip", "rw") + scenario._export_location.assert_called_once_with(fake_share) + scenario._boot_server_with_fip.assert_called_once_with( + "foo_image", "foo_flavor", use_floating_ip="use_fip", + floating_network="ext_network", key_name="keypair_name", + userdata="#cloud-config\npackages:\n - nfs-common") + scenario._delete_server_with_fip.assert_called_once_with( + "foo_server", + {"id": "foo_id", "ip": "foo_ip", "is_floating": True}, + force_delete="foo_force") + @ddt.data( {}, {"detailed": True}, diff --git a/tests/unit/task/scenarios/manila/test_utils.py b/tests/unit/task/scenarios/manila/test_utils.py index 4c7fba0c..6eec260a 100644 --- a/tests/unit/task/scenarios/manila/test_utils.py +++ b/tests/unit/task/scenarios/manila/test_utils.py @@ -77,6 +77,12 @@ class ManilaScenarioTestCase(test.ScenarioTestCase): self.mock_get_from_manager.mock.assert_called_once_with( ("error_deleting", )) + def test_export_location(self): + fake_share = mock.MagicMock() + fake_share.export_locations = "fake_location" + result = self.scenario._export_location(fake_share) + self.assertEqual(result, "fake_location") + @ddt.data( {}, {"detailed": False, "search_opts": None},