diff --git a/rally-jobs/rally-neutron.yaml b/rally-jobs/rally-neutron.yaml index 58a782ff5f..e7f3a5705b 100644 --- a/rally-jobs/rally-neutron.yaml +++ b/rally-jobs/rally-neutron.yaml @@ -628,6 +628,23 @@ failure_rate: max: 0 + CinderVolumes.modify_volume_metadata: + - + args: {} + runner: + type: "constant" + times: 3 + concurrency: 3 + context: + volumes: + size: 1 + users: + tenants: 1 + users_per_tenant: 1 + sla: + failure_rate: + max: 0 + CinderVolumes.list_volumes: - args: diff --git a/rally/plugins/openstack/scenarios/cinder/utils.py b/rally/plugins/openstack/scenarios/cinder/utils.py index 2b4acda452..74527a9b7e 100644 --- a/rally/plugins/openstack/scenarios/cinder/utils.py +++ b/rally/plugins/openstack/scenarios/cinder/utils.py @@ -20,7 +20,7 @@ from oslo_config import cfg from rally.benchmark.scenarios import base from rally.benchmark import utils as bench_utils - +from rally import exceptions CINDER_BENCHMARK_OPTS = [ cfg.FloatOpt("cinder_volume_create_prepoll_delay", @@ -65,6 +65,55 @@ class CinderScenario(base.Scenario): return self.clients("cinder").volume_snapshots.list(detailed) + def _set_metadata(self, volume, sets=10, set_size=3): + """Set volume metadata. + + :param volume: The volume to set metadata on + :param sets: how many operations to perform + :param set_size: number of metadata keys to set in each operation + :returns: A list of keys that were set + """ + key = "cinder.set_%s_metadatas_%s_times" % (set_size, sets) + with base.AtomicAction(self, key): + keys = [] + for i in range(sets): + metadata = {} + for j in range(set_size): + key = self._generate_random_name() + keys.append(key) + metadata[key] = self._generate_random_name() + + self.clients("cinder").volumes.set_metadata(volume, metadata) + return keys + + def _delete_metadata(self, volume, keys, deletes=10, delete_size=3): + """Delete volume metadata keys. + + Note that ``len(keys)`` must be greater than or equal to + ``deletes * delete_size``. + + :param volume: The volume to delete metadata from + :param deletes: how many operations to perform + :param delete_size: number of metadata keys to delete in each operation + :param keys: a list of keys to choose deletion candidates from + """ + if len(keys) < deletes * delete_size: + raise exceptions.InvalidArgumentsException( + "Not enough metadata keys to delete: " + "%(num_keys)s keys, but asked to delete %(num_deletes)s" % + {"num_keys": len(keys), + "num_deletes": deletes * delete_size}) + # make a shallow copy of the list of keys so that, when we pop + # from it later, we don't modify the original list. + keys = list(keys) + random.shuffle(keys) + action_name = "cinder.delete_%s_metadatas_%s_times" % (delete_size, + deletes) + with base.AtomicAction(self, action_name): + for i in range(deletes): + to_del = keys[i * delete_size:(i + 1) * delete_size] + self.clients("cinder").volumes.delete_metadata(volume, to_del) + @base.atomic_action_timer("cinder.create_volume") def _create_volume(self, size, **kwargs): """Create one volume. diff --git a/rally/plugins/openstack/scenarios/cinder/volumes.py b/rally/plugins/openstack/scenarios/cinder/volumes.py index 0d6e904cba..0bb3f0360d 100644 --- a/rally/plugins/openstack/scenarios/cinder/volumes.py +++ b/rally/plugins/openstack/scenarios/cinder/volumes.py @@ -20,6 +20,7 @@ from rally.benchmark import types as types from rally.benchmark import validation from rally.common import log as logging from rally import consts +from rally import exceptions from rally.plugins.openstack.scenarios.cinder import utils from rally.plugins.openstack.scenarios.glance import utils as glance_utils from rally.plugins.openstack.scenarios.nova import utils as nova_utils @@ -135,6 +136,36 @@ class CinderVolumes(utils.CinderScenario, self._create_volume(size, **kwargs) + @validation.required_services(consts.Service.CINDER) + @validation.required_openstack(users=True) + @validation.required_contexts("volumes") + @base.scenario(context={"cleanup": ["cinder"]}) + def modify_volume_metadata(self, sets=10, set_size=3, + deletes=5, delete_size=3): + """Modify a volume's metadata. + + This requires a volume to be created with the volumes + context. Additionally, ``sets * set_size`` must be greater + than or equal to ``deletes * delete_size``. + + :param sets: how many set_metadata operations to perform + :param set_size: number of metadata keys to set in each + set_metadata operation + :param deletes: how many delete_metadata operations to perform + :param delete_size: number of metadata keys to delete in each + delete_metadata operation + """ + if sets * set_size < deletes * delete_size: + raise exceptions.InvalidArgumentsException( + "Not enough metadata keys will be created: " + "Setting %(num_keys)s keys, but deleting %(num_deletes)s" % + {"num_keys": sets * set_size, + "num_deletes": deletes * delete_size}) + + volume = random.choice(self.context["tenant"]["volumes"]) + keys = self._set_metadata(volume["id"], sets, set_size) + self._delete_metadata(volume["id"], keys, deletes, delete_size) + @validation.required_services(consts.Service.CINDER) @validation.required_openstack(users=True) @base.scenario(context={"cleanup": ["cinder"]}) diff --git a/samples/tasks/scenarios/cinder/modify-volume-metadata.json b/samples/tasks/scenarios/cinder/modify-volume-metadata.json new file mode 100644 index 0000000000..dcfe109693 --- /dev/null +++ b/samples/tasks/scenarios/cinder/modify-volume-metadata.json @@ -0,0 +1,21 @@ +{ + "CinderVolumes.modify_volume_metadata": [ + { + "args": {}, + "runner": { + "type": "constant", + "times": 10, + "concurrency": 2 + }, + "context": { + "volumes": { + "size": 1 + }, + "users": { + "tenants": 2, + "users_per_tenant": 2 + } + } + } + ] +} diff --git a/samples/tasks/scenarios/cinder/modify-volume-metadata.yaml b/samples/tasks/scenarios/cinder/modify-volume-metadata.yaml new file mode 100644 index 0000000000..4c42def21c --- /dev/null +++ b/samples/tasks/scenarios/cinder/modify-volume-metadata.yaml @@ -0,0 +1,14 @@ +--- + CinderVolumes.modify_volume_metadata: + - + args: {} + runner: + type: "constant" + times: 10 + concurrency: 2 + context: + volumes: + size: 1 + users: + tenants: 2 + users_per_tenant: 2 diff --git a/tests/unit/plugins/openstack/scenarios/cinder/test_utils.py b/tests/unit/plugins/openstack/scenarios/cinder/test_utils.py index ba3e536058..4ca615c4d3 100644 --- a/tests/unit/plugins/openstack/scenarios/cinder/test_utils.py +++ b/tests/unit/plugins/openstack/scenarios/cinder/test_utils.py @@ -17,7 +17,9 @@ import mock from oslo_config import cfg from oslotest import mockpatch +from rally import exceptions from rally.plugins.openstack.scenarios.cinder import utils +from tests.unit import fakes from tests.unit import test BM_UTILS = "rally.benchmark.utils" @@ -59,6 +61,51 @@ class CinderScenarioTestCase(test.TestCase): self._test_atomic_action_timer(self.scenario.atomic_actions(), "cinder.list_snapshots") + @mock.patch(CINDER_UTILS + ".CinderScenario.clients") + def test__set_metadata(self, mock_clients): + volume = fakes.FakeVolume() + + self.scenario._set_metadata(volume, sets=2, set_size=4) + calls = mock_clients("cinder").volumes.set_metadata.call_args_list + self.assertEqual(len(calls), 2) + for call in calls: + call_volume, metadata = call[0] + self.assertEqual(call_volume, volume) + self.assertEqual(len(metadata), 4) + + self._test_atomic_action_timer(self.scenario.atomic_actions(), + "cinder.set_4_metadatas_2_times") + + @mock.patch(CINDER_UTILS + ".CinderScenario.clients") + def test__delete_metadata(self, mock_clients): + volume = fakes.FakeVolume() + + keys = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"] + self.scenario._delete_metadata(volume, keys, deletes=3, delete_size=4) + calls = mock_clients("cinder").volumes.delete_metadata.call_args_list + self.assertEqual(len(calls), 3) + all_deleted = [] + for call in calls: + call_volume, del_keys = call[0] + self.assertEqual(call_volume, volume) + self.assertEqual(len(del_keys), 4) + for key in del_keys: + self.assertIn(key, keys) + self.assertNotIn(key, all_deleted) + all_deleted.append(key) + + self._test_atomic_action_timer(self.scenario.atomic_actions(), + "cinder.delete_4_metadatas_3_times") + + @mock.patch(CINDER_UTILS + ".CinderScenario.clients") + def test__delete_metadata_not_enough_keys(self, mock_clients): + volume = fakes.FakeVolume() + + keys = ["a", "b", "c", "d", "e"] + self.assertRaises(exceptions.InvalidArgumentsException, + self.scenario._delete_metadata, + volume, keys, deletes=2, delete_size=3) + @mock.patch(CINDER_UTILS + ".CinderScenario.clients") def test__create_volume(self, mock_clients): CONF = cfg.CONF diff --git a/tests/unit/plugins/openstack/scenarios/cinder/test_volumes.py b/tests/unit/plugins/openstack/scenarios/cinder/test_volumes.py index 09c7b3f766..104193d89b 100644 --- a/tests/unit/plugins/openstack/scenarios/cinder/test_volumes.py +++ b/tests/unit/plugins/openstack/scenarios/cinder/test_volumes.py @@ -66,6 +66,21 @@ class CinderServersTestCase(test.TestCase): scenario.create_volume(1, fakearg="f") scenario._create_volume.assert_called_once_with(1, fakearg="f") + def test_create_volume_and_modify_metadata(self): + scenario = volumes.CinderVolumes( + context={"user": {"tenant_id": "fake"}, + "tenant": {"id": "fake", "name": "fake", + "volumes": [{"id": "uuid"}]}}) + scenario._set_metadata = mock.Mock() + scenario._delete_metadata = mock.Mock() + + scenario.modify_volume_metadata(sets=5, set_size=4, + deletes=3, delete_size=2) + scenario._set_metadata.assert_called_once_with("uuid", 5, 4) + scenario._delete_metadata.assert_called_once_with( + "uuid", + scenario._set_metadata.return_value, 3, 2) + def test_create_and_extend_volume(self): fake_volume = mock.MagicMock()