diff --git a/rally-jobs/nova.yaml b/rally-jobs/nova.yaml index 803df341..5f27cc26 100755 --- a/rally-jobs/nova.yaml +++ b/rally-jobs/nova.yaml @@ -1351,3 +1351,26 @@ sla: failure_rate: max: 0 + + NovaServers.boot_server_attach_volume_and_list_attachments: + - + args: + flavor: + name: {{flavor_name}} + image: + name: {{image_name}} + volume_size: 1 + volume_num: 2 + boot_server_kwargs: {} + create_volume_kwargs: {} + runner: + type: "constant" + times: 2 + concurrency: 2 + context: + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 diff --git a/rally/plugins/openstack/scenarios/nova/servers.py b/rally/plugins/openstack/scenarios/nova/servers.py index 2fee8ab0..434ecaf1 100755 --- a/rally/plugins/openstack/scenarios/nova/servers.py +++ b/rally/plugins/openstack/scenarios/nova/servers.py @@ -482,6 +482,55 @@ class BootServerAttachCreatedVolumeAndResize(utils.NovaScenario, self._delete_server(server, force=force_delete) +@validation.add("number", param_name="volume_num", minval=1, + integer_only=True) +@validation.add("number", param_name="volume_size", minval=1, + integer_only=True) +@types.convert(image={"type": "glance_image"}, + flavor={"type": "nova_flavor"}) +@validation.add("image_valid_on_flavor", flavor_param="flavor", + image_param="image", validate_disk=False) +@validation.add("required_services", services=[consts.Service.NOVA, + consts.Service.CINDER]) +@validation.add("required_platform", platform="openstack", users=True) +@scenario.configure(context={"cleanup": ["cinder", "nova"]}, + name=("NovaServers.boot_server" + "_attach_volume_and_list_attachments")) +class BootServerAttachVolumeAndListAttachments(utils.NovaScenario, + cinder_utils.CinderBasic): + + def run(self, image, flavor, volume_size=1, volume_num=2, + boot_server_kwargs=None, create_volume_kwargs=None): + """Create a VM, attach N volume to it and list server's attachemnt. + + Measure the "nova volume-attachments" command performance. + + :param image: Glance image name to use for the VM + :param flavor: VM flavor name + :param volume_size: volume size (in GB), default 1G + :param volume_num: the num of attached volume + :param boot_server_kwargs: optional arguments for VM creation + :param create_volume_kwargs: optional arguments for volume creation + """ + boot_server_kwargs = boot_server_kwargs or {} + create_volume_kwargs = create_volume_kwargs or {} + + server = self._boot_server(image, flavor, **boot_server_kwargs) + attachments = [] + for i in range(volume_num): + volume = self.cinder.create_volume(volume_size, + **create_volume_kwargs) + attachments.append(self._attach_volume(server, volume)) + + list_attachments = self._list_attachments(server.id) + + for attachment in attachments: + msg = ("attachment not included into list of available" + "attachments\n attachment: {}\n" + "list attachments: {}").format(attachment, list_attachments) + self.assertIn(attachment, list_attachments, err_msg=msg) + + @types.convert(image={"type": "glance_image"}, flavor={"type": "nova_flavor"}, to_flavor={"type": "nova_flavor"}) diff --git a/rally/plugins/openstack/scenarios/nova/utils.py b/rally/plugins/openstack/scenarios/nova/utils.py index 51484ef1..c80477a8 100644 --- a/rally/plugins/openstack/scenarios/nova/utils.py +++ b/rally/plugins/openstack/scenarios/nova/utils.py @@ -691,6 +691,15 @@ class NovaScenario(scenario.OpenStackScenario): ) return attachment + @atomic.action_timer("nova.list_attachments") + def _list_attachments(self, server_id): + """Get a list of all the attached volumes for the given server ID. + + :param server_id: The ID of the server + :rtype: list of :class:`Volume` + """ + return self.clients("nova").volumes.get_server_volumes(server_id) + @atomic.action_timer("nova.detach_volume") def _detach_volume(self, server, volume, attachment=None): server_id = server.id diff --git a/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.json b/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.json new file mode 100644 index 00000000..5cef165d --- /dev/null +++ b/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.json @@ -0,0 +1,36 @@ +{% set flavor_name = flavor_name or "m1.tiny" %} +{% set image_name = "^(cirros.*-disk|TestVM)$" %} +{ + "NovaServers.boot_server_attach_volume_and_list_attachments": [ + { + "args": { + "flavor": { + "name": "{{flavor_name}}" + }, + "image": { + "name": "{{image_name}}" + }, + "volume_size": 1, + "volume_num": 2, + "boot_server_kwargs": {}, + "create_volume_kwargs": {} + }, + "runner": { + "type": "constant", + "times": 5, + "concurrency": 2 + }, + "context": { + "users": { + "tenants": 2, + "users_per_tenant": 2 + } + }, + "sla": { + "failure_rate": { + "max": 0 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.yaml b/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.yaml new file mode 100644 index 00000000..a734ffa7 --- /dev/null +++ b/samples/tasks/scenarios/nova/boot-server-attach-volume-and-list-attachments.yaml @@ -0,0 +1,25 @@ +{% set flavor_name = flavor_name or "m1.tiny" %} +{% set image_name = "^(cirros.*-disk|TestVM)$" %} +--- + NovaServers.boot_server_attach_volume_and_list_attachments: + - + args: + flavor: + name: "{{flavor_name}}" + image: + name: "{{image_name}}" + volume_size: 1 + volume_num: 2 + boot_server_kwargs: {} + create_volume_kwargs: {} + runner: + type: "constant" + times: 5 + concurrency: 2 + context: + users: + tenants: 2 + users_per_tenant: 2 + sla: + failure_rate: + max: 0 diff --git a/tests/unit/plugins/openstack/scenarios/nova/test_servers.py b/tests/unit/plugins/openstack/scenarios/nova/test_servers.py index d28343c7..62c041ae 100644 --- a/tests/unit/plugins/openstack/scenarios/nova/test_servers.py +++ b/tests/unit/plugins/openstack/scenarios/nova/test_servers.py @@ -568,6 +568,94 @@ class NovaServersTestCase(test.ScenarioTestCase): scenario._delete_server.assert_called_once_with(fake_server, force=False) + @mock.patch("rally.plugins.openstack.services.storage.block.BlockStorage") + def test_list_attachments(self, mock_block_storage): + mock_volume_service = mock_block_storage.return_value + fake_volume = mock.MagicMock() + fake_server = mock.MagicMock() + flavor = mock.MagicMock() + fake_attachment = mock.MagicMock() + list_attachments = [mock.MagicMock(), + fake_attachment, + mock.MagicMock()] + context = self.context + context.update({ + "admin": { + "id": "fake_user_id", + "credential": mock.MagicMock() + }, + "user": {"id": "fake_user_id", + "credential": mock.MagicMock()}, + "tenant": {"id": "fake", "name": "fake", + "volumes": [{"id": "uuid", "size": 1}], + "servers": [1]}}) + scenario = servers.BootServerAttachVolumeAndListAttachments( + context) + scenario._boot_server = mock.MagicMock(return_value=fake_server) + scenario._attach_volume = mock.MagicMock() + scenario._list_attachments = mock.MagicMock() + mock_volume_service.create_volume.return_value = fake_volume + scenario._list_attachments.return_value = list_attachments + + img_name = "img" + volume_size = 10 + volume_num = 1 + + scenario._attach_volume.return_value = fake_attachment + scenario.run(img_name, flavor, volume_size, volume_num) + + scenario._boot_server.assert_called_once_with(img_name, flavor) + mock_volume_service.create_volume.assert_called_once_with(volume_size) + scenario._attach_volume.assert_called_once_with(fake_server, + fake_volume) + scenario._list_attachments.assert_called_once_with(fake_server.id) + + @mock.patch("rally.plugins.openstack.services.storage.block.BlockStorage") + def test_list_attachments_fails(self, mock_block_storage): + mock_volume_service = mock_block_storage.return_value + fake_volume = mock.MagicMock() + fake_server = mock.MagicMock() + flavor = mock.MagicMock() + fake_attachment = mock.MagicMock() + list_attachments = [mock.MagicMock(), + mock.MagicMock(), + mock.MagicMock()] + + context = self.context + context.update({ + "admin": { + "id": "fake_user_id", + "credential": mock.MagicMock() + }, + "user": {"id": "fake_user_id", + "credential": mock.MagicMock()}, + "tenant": {"id": "fake", "name": "fake", + "volumes": [{"id": "uuid", "size": 1}], + "servers": [1]}}) + scenario = servers.BootServerAttachVolumeAndListAttachments( + context) + scenario._boot_server = mock.MagicMock(return_value=fake_server) + mock_volume_service.create_volume.return_value = fake_volume + scenario._attach_volume = mock.MagicMock() + scenario._list_attachments = mock.MagicMock() + scenario._attach_volume.return_value = fake_attachment + scenario._list_attachments.return_value = list_attachments + + img_name = "img" + volume_size = 10 + + # Negative case: attachment not included into list of + # available attachments + self.assertRaises(rally_exceptions.RallyAssertionError, + scenario.run, + img_name, flavor, volume_size) + + scenario._boot_server.assert_called_with(img_name, flavor) + mock_volume_service.create_volume.assert_called_with(volume_size) + scenario._attach_volume.assert_called_with(fake_server, + fake_volume) + scenario._list_attachments.assert_called_with(fake_server.id) + @ddt.data({"confirm": True, "do_delete": True}, {"confirm": False, "do_delete": True}) @ddt.unpack diff --git a/tests/unit/plugins/openstack/scenarios/nova/test_utils.py b/tests/unit/plugins/openstack/scenarios/nova/test_utils.py index 0835fc7d..49f29b83 100644 --- a/tests/unit/plugins/openstack/scenarios/nova/test_utils.py +++ b/tests/unit/plugins/openstack/scenarios/nova/test_utils.py @@ -617,6 +617,18 @@ class NovaScenarioTestCase(test.ScenarioTestCase): self._test_atomic_action_timer(nova_scenario.atomic_actions(), "nova.attach_volume") + def test__list_attachments(self): + expect_attachments = [mock.MagicMock()] + (self.clients("nova").volumes.get_server_volumes + .return_value) = expect_attachments + nova_scenario = utils.NovaScenario(context=self.context) + list_attachments = nova_scenario._list_attachments(self.server.id) + self.assertEqual(expect_attachments, list_attachments) + (self.clients("nova").volumes.get_server_volumes + .assert_called_once_with(self.server.id)) + self._test_atomic_action_timer(nova_scenario.atomic_actions(), + "nova.list_attachments") + def test__detach_volume(self): attach = mock.MagicMock(id="attach_id") self.clients("nova").volumes.delete_server_volume.return_value = None