diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py index 749403f8c9..9712dba515 100644 --- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py +++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py @@ -50,6 +50,16 @@ class BaremetalClient(base.BaremetalClient): """List all existing port groups.""" return self._list_request('portgroups', **kwargs) + @base.handle_errors + def list_volume_connectors(self, **kwargs): + """List all existing volume connectors.""" + return self._list_request('volume/connectors', **kwargs) + + @base.handle_errors + def list_volume_targets(self, **kwargs): + """List all existing volume targets.""" + return self._list_request('volume/targets', **kwargs) + @base.handle_errors def list_node_ports(self, uuid): """List all ports associated with the node.""" @@ -123,6 +133,24 @@ class BaremetalClient(base.BaremetalClient): """ return self._show_request('portgroups', portgroup_ident) + @base.handle_errors + def show_volume_connector(self, volume_connector_ident): + """Gets a specific volume connector. + + :param volume_connector_ident: UUID of the volume connector. + :return: Serialized volume connector as a dictionary. + """ + return self._show_request('volume/connectors', volume_connector_ident) + + @base.handle_errors + def show_volume_target(self, volume_target_ident): + """Gets a specific volume target. + + :param volume_target_ident: UUID of the volume target. + :return: Serialized volume target as a dictionary. + """ + return self._show_request('volume/targets', volume_target_ident) + @base.handle_errors def show_port_by_address(self, address): """Gets a specific port by address. @@ -238,6 +266,52 @@ class BaremetalClient(base.BaremetalClient): return self._create_request('portgroups', portgroup) + @base.handle_errors + def create_volume_connector(self, node_uuid, **kwargs): + """Create a volume connector with the specified parameters. + + :param node_uuid: The UUID of the node which owns the volume connector. + :param kwargs: + type: type of the volume connector. + connector_id: connector_id of the volume connector. + uuid: UUID of the volume connector. Optional. + extra: meta data of the volume connector; a dictionary. Optional. + :return: A tuple with the server response and the created volume + connector. + """ + volume_connector = {'node_uuid': node_uuid} + + for arg in ('type', 'connector_id', 'uuid', 'extra'): + if arg in kwargs: + volume_connector[arg] = kwargs[arg] + + return self._create_request('volume/connectors', volume_connector) + + @base.handle_errors + def create_volume_target(self, node_uuid, **kwargs): + """Create a volume target with the specified parameters. + + :param node_uuid: The UUID of the node which owns the volume target. + :param kwargs: + volume_type: type of the volume target. + volume_id: volume_id of the volume target. + boot_index: boot index of the volume target. + uuid: UUID of the volume target. Optional. + extra: meta data of the volume target; a dictionary. Optional. + properties: properties related to the type of the volume target; + a dictionary. Optional. + :return: A tuple with the server response and the created volume + target. + """ + volume_target = {'node_uuid': node_uuid} + + for arg in ('volume_type', 'volume_id', 'boot_index', 'uuid', 'extra', + 'properties'): + if arg in kwargs: + volume_target[arg] = kwargs[arg] + + return self._create_request('volume/targets', volume_target) + @base.handle_errors def delete_node(self, uuid): """Deletes a node having the specified UUID. @@ -277,6 +351,25 @@ class BaremetalClient(base.BaremetalClient): """ return self._delete_request('portgroups', portgroup_ident) + @base.handle_errors + def delete_volume_connector(self, volume_connector_ident): + """Deletes a volume connector having the specified UUID. + + :param volume_connector_ident: UUID of the volume connector. + :return: A tuple with the server response and the response body. + """ + return self._delete_request('volume/connectors', + volume_connector_ident) + + @base.handle_errors + def delete_volume_target(self, volume_target_ident): + """Deletes a volume target having the specified UUID. + + :param volume_target_ident: UUID of the volume target. + :return: A tuple with the server response and the response body. + """ + return self._delete_request('volume/targets', volume_target_ident) + @base.handle_errors def update_node(self, uuid, patch=None, **kwargs): """Update the specified node. @@ -326,6 +419,32 @@ class BaremetalClient(base.BaremetalClient): return self._patch_request('ports', uuid, patch) + @base.handle_errors + def update_volume_connector(self, uuid, patch): + """Update the specified volume connector. + + :param uuid: The unique identifier of the volume connector. + :param patch: List of dicts representing json patches. Each dict + has keys 'path', 'op' and 'value'; to update a field. + :return: A tuple with the server response and the updated volume + connector. + """ + + return self._patch_request('volume/connectors', uuid, patch) + + @base.handle_errors + def update_volume_target(self, uuid, patch): + """Update the specified volume target. + + :param uuid: The unique identifier of the volume target. + :param patch: List of dicts representing json patches. Each dict + has keys 'path', 'op' and 'value'; to update a field. + :return: A tuple with the server response and the updated volume + target. + """ + + return self._patch_request('volume/targets', uuid, patch) + @base.handle_errors def set_node_power_state(self, node_uuid, state): """Set power state of the specified node. diff --git a/ironic_tempest_plugin/tests/api/admin/base.py b/ironic_tempest_plugin/tests/api/admin/base.py index ff51c86f1e..2e7a4ff1d5 100644 --- a/ironic_tempest_plugin/tests/api/admin/base.py +++ b/ironic_tempest_plugin/tests/api/admin/base.py @@ -33,7 +33,8 @@ SUPPORTED_DRIVERS = ['fake'] # NOTE(jroll): resources must be deleted in a specific order, this list # defines the resource types to clean up, and the correct order. -RESOURCE_TYPES = ['port', 'node', 'chassis', 'portgroup'] +RESOURCE_TYPES = ['port', 'portgroup', 'volume_connector', 'volume_target', + 'node', 'chassis'] def creates(resource): @@ -221,6 +222,34 @@ class BaseBaremetalTest(api_version_utils.BaseMicroversionTest, return resp, body + @classmethod + @creates('volume_connector') + def create_volume_connector(cls, node_uuid, **kwargs): + """Wrapper utility for creating test volume connector. + + :param node_uuid: The unique identifier of the node. + :return: A tuple with the server response and the created volume + connector. + """ + resp, body = cls.client.create_volume_connector(node_uuid=node_uuid, + **kwargs) + + return resp, body + + @classmethod + @creates('volume_target') + def create_volume_target(cls, node_uuid, **kwargs): + """Wrapper utility for creating test volume target. + + :param node_uuid: The unique identifier of the node. + :return: A tuple with the server response and the created volume + target. + """ + resp, body = cls.client.create_volume_target(node_uuid=node_uuid, + **kwargs) + + return resp, body + @classmethod def delete_chassis(cls, chassis_id): """Deletes a chassis having the specified UUID. @@ -283,6 +312,35 @@ class BaseBaremetalTest(api_version_utils.BaseMicroversionTest, return resp + @classmethod + def delete_volume_connector(cls, volume_connector_id): + """Deletes a volume connector having the specified UUID. + + :param volume_connector_id: The UUID of the volume connector. + :return: Server response. + """ + resp, body = cls.client.delete_volume_connector(volume_connector_id) + + if volume_connector_id in cls.created_objects['volume_connector']: + cls.created_objects['volume_connector'].remove( + volume_connector_id) + + return resp + + @classmethod + def delete_volume_target(cls, volume_target_id): + """Deletes a volume target having the specified UUID. + + :param volume_target_id: The UUID of the volume target. + :return: Server response. + """ + resp, body = cls.client.delete_volume_target(volume_target_id) + + if volume_target_id in cls.created_objects['volume_target']: + cls.created_objects['volume_target'].remove(volume_target_id) + + return resp + def validate_self_link(self, resource, uuid, link): """Check whether the given self link formatted correctly.""" expected_link = "{base}/{pref}/{res}/{uuid}".format( diff --git a/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py b/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py new file mode 100644 index 0000000000..0b936a208b --- /dev/null +++ b/ironic_tempest_plugin/tests/api/admin/test_volume_connector.py @@ -0,0 +1,227 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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. + + +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions as lib_exc + +from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture +from ironic_tempest_plugin.tests.api.admin import base + + +class TestVolumeConnector(base.BaseBaremetalTest): + """Basic test cases for volume connector.""" + + min_microversion = '1.32' + extra = {'key1': 'value1', 'key2': 'value2'} + + def setUp(self): + super(TestVolumeConnector, self).setUp() + self.useFixture( + api_microversion_fixture.APIMicroversionFixture( + self.min_microversion)) + _, self.chassis = self.create_chassis() + _, self.node = self.create_node(self.chassis['uuid']) + _, self.volume_connector = self.create_volume_connector( + self.node['uuid'], type='iqn', + connector_id=data_utils.rand_name('connector_id'), + extra=self.extra) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('3c3cbf45-488a-4386-a811-bf0aa2589c58') + def test_create_volume_connector_error(self): + """Create a volume connector. + + Fail when creating a volume connector with same connector_id + & type as an existing volume connector. + """ + regex_str = (r'.*A volume connector .*already exists') + + self.assertRaisesRegex( + lib_exc.Conflict, regex_str, + self.create_volume_connector, + self.node['uuid'], + type=self.volume_connector['type'], + connector_id=self.volume_connector['connector_id']) + + @decorators.idempotent_id('5795f816-0789-42e6-bb9c-91b4876ad13f') + def test_delete_volume_connector(self): + """Delete a volume connector.""" + # Powering off the Node before deleting a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.delete_volume_connector(self.volume_connector['uuid']) + self.assertRaises(lib_exc.NotFound, self.client.show_volume_connector, + self.volume_connector['uuid']) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('ccbda5e6-52b7-400c-94d7-25eec1d590f0') + def test_delete_volume_connector_error(self): + """Delete a volume connector + + Fail when deleting a volume connector on node + with powered on state. + """ + + # Powering on the Node before deleting a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power on') + + regex_str = (r'.*The requested action \\\\"volume connector ' + r'deletion\\\\" can not be performed on node*') + + self.assertRaisesRegex(lib_exc.BadRequest, + regex_str, + self.delete_volume_connector, + self.volume_connector['uuid']) + + @decorators.idempotent_id('6e4f50b7-0f4f-41c2-971e-d751abcac4e0') + def test_show_volume_connector(self): + """Show a specified volume connector.""" + _, volume_connector = self.client.show_volume_connector( + self.volume_connector['uuid']) + self._assertExpected(self.volume_connector, volume_connector) + + @decorators.idempotent_id('a4725778-e164-4ee5-96a0-66119a35f783') + def test_list_volume_connectors(self): + """List volume connectors.""" + _, body = self.client.list_volume_connectors() + self.assertIn(self.volume_connector['uuid'], + [i['uuid'] for i in body['connectors']]) + self.assertIn(self.volume_connector['type'], + [i['type'] for i in body['connectors']]) + self.assertIn(self.volume_connector['connector_id'], + [i['connector_id'] for i in body['connectors']]) + + @decorators.idempotent_id('1d0459ad-01c0-46db-b930-7301bc2a3c98') + def test_list_with_limit(self): + """List volume connectors with limit.""" + _, body = self.client.list_volume_connectors(limit=3) + + next_marker = body['connectors'][-1]['uuid'] + self.assertIn(next_marker, body['next']) + + @decorators.idempotent_id('3c6f8354-e9bd-4f21-aae2-6deb96b04be7') + def test_update_volume_connector_replace(self): + """Update a volume connector with new connector id.""" + new_connector_id = data_utils.rand_name('connector_id') + + patch = [{'path': '/connector_id', + 'op': 'replace', + 'value': new_connector_id}] + + # Powering off the Node before updating a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.client.update_volume_connector( + self.volume_connector['uuid'], patch) + + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + self.assertEqual(new_connector_id, body['connector_id']) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('5af8dc7a-9965-4787-8184-e60aeaf30957') + def test_update_volume_connector_replace_error(self): + """Updating a volume connector. + + Fail when updating a volume connector on node + with power on state. + """ + + new_connector_id = data_utils.rand_name('connector_id') + + patch = [{'path': '/connector_id', + 'op': 'replace', + 'value': new_connector_id}] + + # Powering on the Node before updating a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power on') + + regex_str = (r'.*The requested action \\\\"volume connector ' + r'update\\\\" can not be performed on node*') + self.assertRaisesRegex(lib_exc.BadRequest, + regex_str, + self.client.update_volume_connector, + self.volume_connector['uuid'], + patch) + + @decorators.idempotent_id('b95c75eb-4048-482e-99ff-fe1d32538383') + def test_update_volume_connector_remove_item(self): + """Update a volume connector by removing one item from collection.""" + new_extra = {'key1': 'value1'} + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + connector_id = body['connector_id'] + connector_type = body['type'] + + # Powering off the Node before updating a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + # Removing one item from the collection + self.client.update_volume_connector(self.volume_connector['uuid'], + [{'path': '/extra/key2', + 'op': 'remove'}]) + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + self.assertEqual(new_extra, body['extra']) + + # Assert nothing else was changed + self.assertEqual(connector_id, body['connector_id']) + self.assertEqual(connector_type, body['type']) + + @decorators.idempotent_id('8de03acd-532a-476f-8bc9-0e8b23bfe609') + def test_update_volume_connector_remove_collection(self): + """Update a volume connector by removing collection.""" + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + connector_id = body['connector_id'] + connector_type = body['type'] + + # Powering off the Node before updating a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + # Removing the collection + self.client.update_volume_connector(self.volume_connector['uuid'], + [{'path': '/extra', + 'op': 'remove'}]) + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + self.assertEqual({}, body['extra']) + + # Assert nothing else was changed + self.assertEqual(connector_id, body['connector_id']) + self.assertEqual(connector_type, body['type']) + + @decorators.idempotent_id('bfb0ca6b-086d-4663-9b25-e0eaf42da55b') + def test_update_volume_connector_add(self): + """Update a volume connector by adding one item to collection.""" + new_extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'} + + patch = [{'path': '/extra/key3', + 'op': 'add', + 'value': new_extra['key3']}, + {'path': '/extra/key3', + 'op': 'add', + 'value': new_extra['key3']}] + + # Powering off the Node before updating a volume connector. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.client.update_volume_connector( + self.volume_connector['uuid'], patch) + + _, body = self.client.show_volume_connector( + self.volume_connector['uuid']) + self.assertEqual(new_extra, body['extra']) diff --git a/ironic_tempest_plugin/tests/api/admin/test_volume_target.py b/ironic_tempest_plugin/tests/api/admin/test_volume_target.py new file mode 100644 index 0000000000..731467ca06 --- /dev/null +++ b/ironic_tempest_plugin/tests/api/admin/test_volume_target.py @@ -0,0 +1,210 @@ +# Copyright 2017 FUJITSU LIMITED +# +# 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. + + +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib import exceptions as lib_exc + +from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture +from ironic_tempest_plugin.tests.api.admin import base + + +class TestVolumeTarget(base.BaseBaremetalTest): + """Basic test cases for volume target.""" + + min_microversion = '1.32' + extra = {'key1': 'value1', 'key2': 'value2'} + + def setUp(self): + super(TestVolumeTarget, self).setUp() + self.useFixture( + api_microversion_fixture.APIMicroversionFixture( + self.min_microversion)) + _, self.chassis = self.create_chassis() + _, self.node = self.create_node(self.chassis['uuid']) + _, self.volume_target = self.create_volume_target( + self.node['uuid'], volume_type=data_utils.rand_name('volume_type'), + volume_id=data_utils.rand_name('volume_id'), + boot_index=10, + extra=self.extra) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('da5c27d4-68cc-499f-b8ab-3048b87d3bca') + def test_create_volume_target_error(self): + """Create a volume target. + + Fail when creating a volume target with same boot index as the + existing volume target. + """ + regex_str = (r'.*A volume target .*already exists') + + self.assertRaisesRegex( + lib_exc.Conflict, regex_str, + self.create_volume_target, + self.node['uuid'], + volume_type=data_utils.rand_name('volume_type'), + volume_id=data_utils.rand_name('volume_id'), + boot_index=self.volume_target['boot_index']) + + @decorators.idempotent_id('ea3a9b2e-8971-4830-9274-abaf0239f1ce') + def test_delete_volume_target(self): + """Delete a volume target.""" + # Powering off the Node before deleting a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.delete_volume_target(self.volume_target['uuid']) + self.assertRaises(lib_exc.NotFound, self.client.show_volume_target, + self.volume_target['uuid']) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('532a06bc-a9b2-44b0-828a-c53279c87cb2') + def test_delete_volume_target_error(self): + """Fail when deleting a volume target on node with power on state.""" + # Powering on the Node before deleting a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power on') + + regex_str = (r'.*The requested action \\\\"volume target ' + r'deletion\\\\" can not be performed on node*') + + self.assertRaisesRegex(lib_exc.BadRequest, + regex_str, + self.delete_volume_target, + self.volume_target['uuid']) + + @decorators.idempotent_id('a2598388-8f61-4b7e-944f-f37e4f60e1e2') + def test_show_volume_target(self): + """Show a specified volume target.""" + _, volume_target = self.client.show_volume_target( + self.volume_target['uuid']) + self._assertExpected(self.volume_target, volume_target) + + @decorators.idempotent_id('ae99a986-d93c-4324-9cdc-41d89e3a659f') + def test_list_volume_targets(self): + """List volume targets.""" + _, body = self.client.list_volume_targets() + self.assertIn(self.volume_target['uuid'], + [i['uuid'] for i in body['targets']]) + self.assertIn(self.volume_target['volume_type'], + [i['volume_type'] for i in body['targets']]) + self.assertIn(self.volume_target['volume_id'], + [i['volume_id'] for i in body['targets']]) + + @decorators.idempotent_id('9da25447-0370-4b33-9c1f-d4503f5950ae') + def test_list_with_limit(self): + """List volume targets with limit.""" + _, body = self.client.list_volume_targets(limit=3) + + next_marker = body['targets'][-1]['uuid'] + self.assertIn(next_marker, body['next']) + + @decorators.idempotent_id('8559cd08-feae-4f1a-a0ad-5bad8ea12b76') + def test_update_volume_target_replace(self): + """Update a volume target by replacing volume id.""" + new_volume_id = data_utils.rand_name('volume_id') + + patch = [{'path': '/volume_id', + 'op': 'replace', + 'value': new_volume_id}] + + # Powering off the Node before updating a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.client.update_volume_target(self.volume_target['uuid'], patch) + + _, body = self.client.show_volume_target(self.volume_target['uuid']) + self.assertEqual(new_volume_id, body['volume_id']) + + @decorators.attr(type=['negative']) + @decorators.idempotent_id('fd5266d3-4f3c-4dce-9c87-bfdea2b756c7') + def test_update_volume_target_replace_error(self): + """Fail when updating a volume target on node with power on state.""" + new_volume_id = data_utils.rand_name('volume_id') + + patch = [{'path': '/volume_id', + 'op': 'replace', + 'value': new_volume_id}] + + # Powering on the Node before updating a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power on') + + regex_str = (r'.*The requested action \\\\"volume target ' + r'update\\\\" can not be performed on node*') + + self.assertRaisesRegex(lib_exc.BadRequest, + regex_str, + self.client.update_volume_target, + self.volume_target['uuid'], + patch) + + @decorators.idempotent_id('1c13a4ee-1a49-4739-8c19-77960fbd1af8') + def test_update_volume_target_remove_item(self): + """Update a volume target by removing one item from the collection.""" + new_extra = {'key1': 'value1'} + _, body = self.client.show_volume_target(self.volume_target['uuid']) + volume_id = body['volume_id'] + volume_type = body['volume_type'] + + # Powering off the Node before updating a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + # Removing one item from the collection + self.client.update_volume_target(self.volume_target['uuid'], + [{'path': '/extra/key2', + 'op': 'remove'}]) + + _, body = self.client.show_volume_target(self.volume_target['uuid']) + self.assertEqual(new_extra, body['extra']) + + # Assert nothing else was changed + self.assertEqual(volume_id, body['volume_id']) + self.assertEqual(volume_type, body['volume_type']) + + @decorators.idempotent_id('6784ddb0-9144-41ea-b8a0-f888ad5c5b62') + def test_update_volume_target_remove_collection(self): + """Update a volume target by removing the collection.""" + _, body = self.client.show_volume_target(self.volume_target['uuid']) + volume_id = body['volume_id'] + volume_type = body['volume_type'] + + # Powering off the Node before updating a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + # Removing the collection + self.client.update_volume_target(self.volume_target['uuid'], + [{'path': '/extra', + 'op': 'remove'}]) + _, body = self.client.show_volume_target(self.volume_target['uuid']) + self.assertEqual({}, body['extra']) + + # Assert nothing else was changed + self.assertEqual(volume_id, body['volume_id']) + self.assertEqual(volume_type, body['volume_type']) + + @decorators.idempotent_id('9629715d-57ba-423b-b985-232674cc3a25') + def test_update_volume_target_add(self): + """Update a volume target by adding to the collection.""" + new_extra = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'} + + patch = [{'path': '/extra/key3', + 'op': 'add', + 'value': new_extra['key3']}] + + # Powering off the Node before updating a volume target. + self.client.set_node_power_state(self.node['uuid'], 'power off') + + self.client.update_volume_target(self.volume_target['uuid'], patch) + + _, body = self.client.show_volume_target(self.volume_target['uuid']) + self.assertEqual(new_extra, body['extra'])