diff --git a/cinder/exception.py b/cinder/exception.py index 20d17d7db5f..c692b38e246 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1349,3 +1349,8 @@ class ServiceUserTokenNoAuth(CinderException): message = _("The [service_user] send_service_user_token option was " "requested, but no service auth could be loaded. Please check " "the [service_user] configuration section.") + + +class UnsupportedNVMETProtocol(Invalid): + message = _("An invalid 'target_protocol' " + "value was provided: %(protocol)s") diff --git a/cinder/opts.py b/cinder/opts.py index cebb32b7964..9ec766c9e44 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -254,6 +254,7 @@ def list_opts(): [cinder_volume_api.az_cache_time_opt], cinder_volume_driver.volume_opts, cinder_volume_driver.iser_opts, + cinder_volume_driver.nvmet_opts, cinder_volume_drivers_datacore_driver.datacore_opts, cinder_volume_drivers_datacore_iscsi.datacore_iscsi_opts, cinder_volume_drivers_inspur_instorage_instoragecommon. @@ -283,6 +284,7 @@ def list_opts(): itertools.chain( cinder_volume_driver.volume_opts, cinder_volume_driver.iser_opts, + cinder_volume_driver.nvmet_opts, cinder_volume_drivers_coprhd_common.volume_opts, cinder_volume_drivers_coprhd_scaleio.scaleio_opts, cinder_volume_drivers_datera_dateraiscsi.d_opts, diff --git a/cinder/tests/unit/targets/test_nvmeof_driver.py b/cinder/tests/unit/targets/test_nvmeof_driver.py new file mode 100644 index 00000000000..31f80ef3051 --- /dev/null +++ b/cinder/tests/unit/targets/test_nvmeof_driver.py @@ -0,0 +1,139 @@ +# 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 mock + +from oslo_utils import timeutils + +from cinder import context +from cinder import exception +from cinder.tests.unit.targets import targets_fixture as tf +from cinder import utils +from cinder.volume.targets import nvmeof + + +class FakeNVMeOFDriver(nvmeof.NVMeOF): + def __init__(self, *args, **kwargs): + super(FakeNVMeOFDriver, self).__init__(*args, **kwargs) + + def create_nvmeof_target( + self, target_name, target_ip, target_port, + transport_type, ns_id, volume_path): + pass + + def delete_nvmeof_target(self, target_name): + pass + + +class TestNVMeOFDriver(tf.TargetDriverFixture): + + def setUp(self): + super(TestNVMeOFDriver, self).setUp() + + self.configuration.target_protocol = 'nvmet_rdma' + self.target = FakeNVMeOFDriver(root_helper=utils.get_root_helper(), + configuration=self.configuration) + + self.target_ip = self.configuration.target_ip_address + self.target_port = self.configuration.target_port + self.nvmet_subsystem_name = self.configuration.target_prefix + self.nvmet_ns_id = self.configuration.nvmet_ns_id + self.nvmet_port_id = self.configuration.nvmet_port_id + self.nvme_transport_type = 'rdma' + + self.fake_volume_id = 'c446b9a2-c968-4260-b95f-a18a7b41c004' + self.testvol_path = ( + '/dev/stack-volumes-lvmdriver-1/volume-%s' % self.fake_volume_id) + self.fake_project_id = 'ed2c1fd4-5555-1111-aa15-123b93f75cba' + self.testvol = ( + {'project_id': self.fake_project_id, + 'name': 'testvol', + 'size': 1, + 'id': self.fake_volume_id, + 'volume_type_id': None, + 'provider_location': + self.target.get_nvmeof_location( + "ngn.%s-%s" % ( + self.nvmet_subsystem_name, + self.fake_volume_id), + self.target_ip, + self.target_port, + self.nvme_transport_type, + self.nvmet_ns_id + ), + 'provider_auth': None, + 'provider_geometry': None, + 'created_at': timeutils.utcnow(), + 'host': 'fake_host@lvm#lvm'}) + + def test_initialize_connection(self): + mock_connector = {'initiator': 'fake_init'} + mock_testvol = self.testvol + expected_return = { + 'driver_volume_type': 'nvmeof', + 'data': self.target._get_connection_properties(mock_testvol) + } + self.assertEqual(expected_return, + self.target.initialize_connection(mock_testvol, + mock_connector)) + + @mock.patch.object(FakeNVMeOFDriver, 'create_nvmeof_target') + def test_create_export(self, mock_create_nvme_target): + ctxt = context.get_admin_context() + self.target.create_export(ctxt, self.testvol, self.testvol_path) + mock_create_nvme_target.assert_called_once_with( + self.fake_volume_id, + self.configuration.target_prefix, + self.target_ip, + self.target_port, + self.nvme_transport_type, + self.nvmet_port_id, + self.nvmet_ns_id, + self.testvol_path + ) + + @mock.patch.object(FakeNVMeOFDriver, 'delete_nvmeof_target') + def test_remove_export(self, mock_delete_nvmeof_target): + ctxt = context.get_admin_context() + self.target.remove_export(ctxt, self.testvol) + mock_delete_nvmeof_target.assert_called_once_with( + self.testvol + ) + + def test_get_connection_properties(self): + expected_return = { + 'target_portal': self.target_ip, + 'target_port': str(self.target_port), + 'nqn': "ngn.%s-%s" % ( + self.nvmet_subsystem_name, self.fake_volume_id), + 'transport_type': self.nvme_transport_type, + 'ns_id': str(self.nvmet_ns_id) + } + self.assertEqual(expected_return, + self.target._get_connection_properties(self.testvol)) + + def test_validate_connector(self): + mock_connector = {'initiator': 'fake_init'} + self.assertTrue(self.target.validate_connector(mock_connector)) + + def test_validate_connector_not_found(self): + mock_connector = {'fake_init'} + self.assertRaises(exception.InvalidConnectorException, + self.target.validate_connector, + mock_connector) + + def test_invalid_target_protocol(self): + self.configuration.target_protocol = 'iser' + self.assertRaises(exception.UnsupportedNVMETProtocol, + FakeNVMeOFDriver, + root_helper=utils.get_root_helper(), + configuration=self.configuration) diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 75de2aed8c8..8ce0f200414 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -146,12 +146,13 @@ volume_opts = [ cfg.StrOpt('target_protocol', deprecated_name='iscsi_protocol', default='iscsi', - choices=['iscsi', 'iser'], - help='Determines the iSCSI protocol for new iSCSI volumes, ' - 'created with tgtadm or lioadm target helpers. In ' - 'order to enable RDMA, this parameter should be set ' + choices=['iscsi', 'iser', 'nvmet_rdma'], + help='Determines the target protocol for new volumes, ' + 'created with tgtadm, lioadm and nvmet target helpers. ' + 'In order to enable RDMA, this parameter should be set ' 'with the value "iser". The supported iSCSI protocol ' - 'values are "iscsi" and "iser".'), + 'values are "iscsi" and "iser", in case of nvmet target ' + 'set to "nvmet_rdma".'), cfg.StrOpt('driver_client_cert_key', help='The path to the client certificate key for verification, ' 'if the driver supports it.'), @@ -304,12 +305,24 @@ iser_opts = [ help='The name of the iSER target user-land tool to use'), ] +nvmet_opts = [ + cfg.PortOpt('nvmet_port_id', + default=1, + help='The port that the NVMe target is listening on.'), + cfg.IntOpt('nvmet_ns_id', + default=10, + help='The namespace id associated with the subsystem ' + 'that will be created with the path for the LVM volume.'), +] + CONF = cfg.CONF CONF.register_opts(volume_opts, group=configuration.SHARED_CONF_GROUP) CONF.register_opts(iser_opts, group=configuration.SHARED_CONF_GROUP) +CONF.register_opts(nvmet_opts, group=configuration.SHARED_CONF_GROUP) CONF.register_opts(volume_opts) CONF.register_opts(iser_opts) +CONF.register_opts(nvmet_opts) CONF.import_opt('backup_use_same_host', 'cinder.backup.api') @@ -367,6 +380,7 @@ class BaseVD(object): if self.configuration: self.configuration.append_config_values(volume_opts) self.configuration.append_config_values(iser_opts) + self.configuration.append_config_values(nvmet_opts) utils.setup_tracing(self.configuration.safe_get('trace_flags')) # NOTE(geguileo): Don't allow to start if we are enabling diff --git a/cinder/volume/targets/nvmeof.py b/cinder/volume/targets/nvmeof.py new file mode 100644 index 00000000000..a7b4bbe5ed7 --- /dev/null +++ b/cinder/volume/targets/nvmeof.py @@ -0,0 +1,157 @@ +# 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 abc + +from oslo_log import log as logging + +from cinder import exception +from cinder.volume.targets import driver + + +LOG = logging.getLogger(__name__) + + +class NVMeOF(driver.Target): + + """Target object for block storage devices with RDMA transport.""" + + protocol = 'nvmeof' + target_protocol_map = { + 'nvmet_rdma': 'rdma', + } + + def __init__(self, *args, **kwargs): + """Reads NVMeOF configurations.""" + + super(NVMeOF, self).__init__(*args, **kwargs) + self.target_ip = self.configuration.target_ip_address + self.target_port = self.configuration.target_port + self.nvmet_port_id = self.configuration.nvmet_port_id + self.nvmet_ns_id = self.configuration.nvmet_ns_id + self.nvmet_subsystem_name = self.configuration.target_prefix + target_protocol = self.configuration.target_protocol + if target_protocol in self.target_protocol_map: + self.nvme_transport_type = self.target_protocol_map[ + target_protocol] + else: + raise exception.UnsupportedNVMETProtocol( + protocol=target_protocol + ) + + def initialize_connection(self, volume, connector): + """Returns the connection info. + + In NVMeOF driver, :driver_volume_type: is set to 'nvmeof', + :data: is the driver data that has the value of + _get_connection_properties + Example return value:: + + .. code-block:: json + + { + 'driver_volume_type': 'nvmeof' + 'data': { + 'target_portal': '1.1.1.1', + 'target_port': 4420, + 'nqn': 'nqn.volume-0001', + 'transport_type': 'rdma', + 'ns_id': 10, + } + } + """ + return { + 'driver_volume_type': self.protocol, + 'data': self._get_connection_properties(volume) + } + + def _get_connection_properties(self, volume): + """Gets NVMeOF connection configuration. + + :return: dictionary of the following keys: + :target_portal: NVMe target IP address + :target_port: NVMe target port + :nqn: NQN of the NVMe target + :transport_type: Network fabric being used for an + NVMe-over-Fabrics network + :ns_id: namespace id associated with the subsystem + """ + + location = volume['provider_location'] + target_connection, nvme_transport_type, nqn, nvmet_ns_id = ( + location.split(' ')) + target_portal, target_port = target_connection.split(':') + + return { + 'target_portal': target_portal, + 'target_port': target_port, + 'nqn': nqn, + 'transport_type': nvme_transport_type, + 'ns_id': nvmet_ns_id + } + + def get_nvmeof_location(self, nqn, target_ip, target_port, + nvme_transport_type, nvmet_ns_id): + """Serializes driver data into single line string.""" + + return "%(ip)s:%(port)s %(transport)s %(nqn)s %(ns_id)s" % ( + {'ip': target_ip, + 'port': target_port, + 'transport': nvme_transport_type, + 'nqn': nqn, + 'ns_id': nvmet_ns_id}) + + def terminate_connection(self, volume, connector, **kwargs): + pass + + def create_export(self, context, volume, volume_path): + """Creates export data for a logical volume.""" + + return self.create_nvmeof_target( + volume['id'], + self.configuration.target_prefix, + self.target_ip, + self.target_port, + self.nvme_transport_type, + self.nvmet_port_id, + self.nvmet_ns_id, + volume_path) + + def ensure_export(self, context, volume, volume_path): + pass + + def remove_export(self, context, volume): + return self.delete_nvmeof_target(volume) + + def validate_connector(self, connector): + if 'initiator' not in connector: + LOG.error('The volume driver requires the NVMe initiator ' + 'name in the connector.') + raise exception.InvalidConnectorException( + missing='initiator') + return True + + @abc.abstractmethod + def create_nvmeof_target(self, + volume_id, + subsystem_name, + target_ip, + target_port, + transport_type, + nvmet_port_id, + ns_id, + volume_path): + pass + + @abc.abstractmethod + def delete_nvmeof_target(self, target_name): + pass