201 lines
6.7 KiB
Python
201 lines
6.7 KiB
Python
# 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 json
|
|
import string
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import requests
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder.volume import configuration
|
|
from cinder.volume.targets import nvmeof
|
|
from cinder.volume import volume_utils
|
|
|
|
spdk_opts = [
|
|
cfg.StrOpt('spdk_rpc_ip',
|
|
help='The NVMe target remote configuration IP address.'),
|
|
cfg.PortOpt('spdk_rpc_port',
|
|
default=8000,
|
|
help='The NVMe target remote configuration port.'),
|
|
cfg.StrOpt('spdk_rpc_username',
|
|
help='The NVMe target remote configuration username.'),
|
|
cfg.StrOpt('spdk_rpc_password',
|
|
help='The NVMe target remote configuration password.',
|
|
secret=True),
|
|
cfg.StrOpt('spdk_rpc_protocol',
|
|
choices=['http', 'https'],
|
|
default='http',
|
|
help='Protocol to be used with SPDK RPC proxy'),
|
|
cfg.IntOpt('spdk_max_queue_depth',
|
|
default=64,
|
|
min=1, max=128,
|
|
help='Queue depth for rdma transport.'),
|
|
]
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(spdk_opts, group=configuration.SHARED_CONF_GROUP)
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class SpdkNvmf(nvmeof.NVMeOF):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(SpdkNvmf, self).__init__(*args, **kwargs)
|
|
|
|
self.configuration.append_config_values(spdk_opts)
|
|
self.url = ('%(protocol)s://%(ip)s:%(port)s/' %
|
|
{'protocol': self.configuration.spdk_rpc_protocol,
|
|
'ip': self.configuration.spdk_rpc_ip,
|
|
'port': self.configuration.spdk_rpc_port})
|
|
|
|
# SPDK NVMe-oF Target application requires one time creation
|
|
# of RDMA transport type each time it is started. It will
|
|
# fail on second attempt which is expected behavior.
|
|
try:
|
|
params = {
|
|
'trtype': 'rdma',
|
|
'max_queue_depth':
|
|
self.configuration.spdk_max_queue_depth
|
|
}
|
|
self._rpc_call('nvmf_create_transport', params)
|
|
except Exception:
|
|
pass
|
|
|
|
def _rpc_call(self, method, params=None):
|
|
payload = {}
|
|
payload['jsonrpc'] = '2.0'
|
|
payload['id'] = 1
|
|
payload['method'] = method
|
|
if params is not None:
|
|
payload['params'] = params
|
|
|
|
req = requests.post(self.url,
|
|
data=json.dumps(payload),
|
|
auth=(self.configuration.spdk_rpc_username,
|
|
self.configuration.spdk_rpc_password),
|
|
verify=self.configuration.driver_ssl_cert_verify,
|
|
timeout=30)
|
|
|
|
if not req.ok:
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_('SPDK target responded with error: %s') % req.text)
|
|
|
|
return req.json()['result']
|
|
|
|
def _get_spdk_volume_name(self, name):
|
|
output = self._rpc_call('bdev_get_bdevs')
|
|
|
|
for bdev in output:
|
|
for alias in bdev['aliases']:
|
|
if name in alias:
|
|
return bdev['name']
|
|
|
|
def _get_nqn_with_volume_name(self, name):
|
|
output = self._rpc_call('nvmf_get_subsystems')
|
|
|
|
spdk_name = self._get_spdk_volume_name(name)
|
|
|
|
if spdk_name is not None:
|
|
for subsystem in output[1:]:
|
|
for namespace in subsystem['namespaces']:
|
|
if spdk_name in namespace['bdev_name']:
|
|
return subsystem['nqn']
|
|
|
|
def _get_first_free_node(self):
|
|
cnode_num = []
|
|
|
|
output = self._rpc_call('nvmf_get_subsystems')
|
|
|
|
# Get node numbers for nqn string like this: nqn.2016-06.io.spdk:cnode1
|
|
|
|
for subsystem in output[1:]:
|
|
cnode_num.append(int(subsystem['nqn'].split("cnode")[1]))
|
|
|
|
test_set = set(range(1, len(cnode_num) + 2))
|
|
|
|
return list(test_set.difference(cnode_num))[0]
|
|
|
|
def create_nvmeof_target(self,
|
|
volume_id,
|
|
subsystem_name,
|
|
target_ip,
|
|
target_port,
|
|
transport_type,
|
|
nvmet_port_id,
|
|
ns_id,
|
|
volume_path):
|
|
|
|
LOG.debug('SPDK create target')
|
|
|
|
nqn = self._get_nqn_with_volume_name(volume_id)
|
|
|
|
if nqn is None:
|
|
node = self._get_first_free_node()
|
|
nqn = '%s:cnode%s' % (subsystem_name, node)
|
|
choice = string.ascii_uppercase + string.digits
|
|
serial = ''.join(
|
|
volume_utils.generate_password(length=12, symbolgroups=choice))
|
|
|
|
params = {
|
|
'nqn': nqn,
|
|
'allow_any_host': True,
|
|
'serial_number': serial,
|
|
}
|
|
self._rpc_call('nvmf_create_subsystem', params)
|
|
|
|
listen_address = {
|
|
'trtype': transport_type,
|
|
'traddr': target_ip,
|
|
'trsvcid': str(target_port),
|
|
}
|
|
params = {
|
|
'nqn': nqn,
|
|
'listen_address': listen_address,
|
|
}
|
|
self._rpc_call('nvmf_subsystem_add_listener', params)
|
|
|
|
ns = {
|
|
'bdev_name': self._get_spdk_volume_name(volume_id),
|
|
'nsid': ns_id,
|
|
}
|
|
params = {
|
|
'nqn': nqn,
|
|
'namespace': ns,
|
|
}
|
|
self._rpc_call('nvmf_subsystem_add_ns', params)
|
|
|
|
location = self.get_nvmeof_location(
|
|
nqn,
|
|
target_ip,
|
|
target_port,
|
|
transport_type,
|
|
ns_id)
|
|
|
|
return {'location': location, 'auth': '', 'provider_id': nqn}
|
|
|
|
def delete_nvmeof_target(self, target_name):
|
|
LOG.debug('SPDK delete target: %s', target_name)
|
|
|
|
nqn = self._get_nqn_with_volume_name(target_name.name)
|
|
|
|
if nqn is not None:
|
|
try:
|
|
params = {'nqn': nqn}
|
|
self._rpc_call('nvmf_delete_subsystem', params)
|
|
LOG.debug('SPDK subsystem %s deleted', nqn)
|
|
except Exception as e:
|
|
LOG.debug('SPDK ERROR: subsystem not deleted: %s', e)
|