Adding NVMEoF for initiator CLI

Added os-brick changes to support NVMEoF for initiator CLI.
A New connector, NVMe connector, is added to handle initiator callsi for
connecting and disconnecting volumes to instances using nvme-cli over
RDMA.

Implements: blueprint nvme-over-fabirc-nova
Needed-By: I67a72c4226e54c18b3a6e4a13b5055fa6e85af09
Needed-By: I7cacd76c63e0ad29eb2d448ce07fbb5176f62721
Co-Authored-By: Ivan Kolodyazhny <e0ne@e0ne.info>
Change-Id: I83697d6b46248edbe0a0ef3b76829b28ed5c048c
This commit is contained in:
Rawan Herzallah 2017-07-11 20:24:26 +03:00 committed by Hamdy Khader
parent c1f1786307
commit 905e73129b
4 changed files with 362 additions and 0 deletions

View File

@ -61,3 +61,4 @@ SHEEPDOG = "SHEEPDOG"
VMDK = "VMDK"
GPFS = "GPFS"
VERITAS_HYPERSCALE = "VERITAS_HYPERSCALE"
NVME = "NVME"

View File

@ -63,6 +63,7 @@ connector_list = [
'os_brick.initiator.windows.fibre_channel.WindowsFCConnector',
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
'os_brick.initiator.connectors.nvme.NVMeConnector',
]
# Mappings used to determine who to contruct in the factory
@ -110,6 +111,8 @@ _connector_mapping_linux = {
'os_brick.initiator.connectors.gpfs.GPFSConnector',
initiator.VERITAS_HYPERSCALE:
'os_brick.initiator.connectors.vrtshyperscale.HyperScaleConnector',
initiator.NVME:
'os_brick.initiator.connectors.nvme.NVMeConnector',
}
# Mapping for the S390X platform

View File

@ -0,0 +1,190 @@
# 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 re
import time
from oslo_concurrency import lockutils
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
from os_brick import exception
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick import utils
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
LOG = logging.getLogger(__name__)
synchronized = lockutils.synchronized_with_prefix('os-brick-')
class NVMeConnector(base.BaseLinuxConnector):
"""Connector class to attach/detach NVMe over fabric volumes."""
def __init__(self, root_helper, driver=None,
device_scan_attempts=initiator.DEVICE_SCAN_ATTEMPTS_DEFAULT,
*args, **kwargs):
super(NVMeConnector, self).__init__(
root_helper,
driver=driver,
device_scan_attempts=device_scan_attempts,
*args, **kwargs)
@staticmethod
def get_connector_properties(root_helper, *args, **kwargs):
"""The NVMe connector properties."""
return {}
def get_search_path(self):
return '/dev'
def get_volume_paths(self, connection_properties):
path = connection_properties['device_path']
LOG.debug("Path of volume to be extended is %(path)s", {'path': path})
return [path]
def _get_nvme_devices(self):
nvme_devices = []
pattern = r'/dev/nvme[0-9]n[0-9]'
cmd = ['nvme', 'list']
try:
(out, err) = self._execute(*cmd, root_helper=self._root_helper,
run_as_root=True)
for line in out.split('\n'):
result = re.match(pattern, line)
if result:
nvme_devices.append(result.group(0))
LOG.debug("_get_nvme_devices returned %(nvme_devices)s",
{'nvme_devices': nvme_devices})
return nvme_devices
except putils.ProcessExecutionError:
LOG.error(
"Failed to list available NVMe connected controllers.")
raise
@utils.trace
@synchronized('connect_volume')
def connect_volume(self, connection_properties):
"""Discover and attach the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties must include:
nqn - NVMe subsystem name to the volume to be connected
target_port - NVMe target port that hosts the nqn sybsystem
target_portal - NVMe target ip that hosts the nqn sybsystem
:type connection_properties: dict
:returns: dict
"""
current_nvme_devices = self._get_nvme_devices()
device_info = {'type': 'block'}
conn_nqn = connection_properties['nqn'].split('.', 1)[1]
target_portal = connection_properties['target_portal']
port = connection_properties['target_port']
nvme_transport_type = connection_properties['transport_type']
cmd = [
'nvme', 'connect',
'-t', nvme_transport_type,
'-n', conn_nqn,
'-a', target_portal,
'-s', port]
try:
self._execute(*cmd, root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError:
LOG.error(
"Failed to connect to NVMe nqn "
"%(conn_nqn)s", {'conn_nqn': conn_nqn})
raise
for retry in range(1, self.device_scan_attempts + 1):
all_nvme_devices = self._get_nvme_devices()
path = set(all_nvme_devices) - set(current_nvme_devices)
if path:
break
time.sleep(retry ** 2)
else:
LOG.error("Failed to retrieve available connected NVMe "
"controllers when running nvme list")
raise exception.TargetPortalNotFound(target_portal)
path = list(path)
LOG.debug("all_nvme_devices are %(all_nvme_devices)s",
{'all_nvme_devices': all_nvme_devices})
device_info['path'] = path[0]
LOG.debug("NVMe device to be connected to is %(path)s",
{'path': path[0]})
return device_info
@utils.trace
@synchronized('disconnect_volume')
def disconnect_volume(self, connection_properties, device_info):
"""Detach and flush the volume.
:param connection_properties: The dictionary that describes all
of the target volume attributes.
connection_properties must include:
device_path - path to the volume to be connected
:type connection_properties: dict
:param device_info: historical difference, but same as connection_props
:type device_info: dict
"""
conn_nqn = connection_properties['nqn']
device_path = connection_properties['device_path']
LOG.debug(
"Trying to disconnect from NVMe nqn "
"%(conn_nqn)s with device_path %(device_path)s",
{'conn_nqn': conn_nqn, 'device_path': device_path})
cmd = [
'nvme',
'disconnect',
'-d',
device_path]
try:
self._execute(
*cmd,
root_helper=self._root_helper,
run_as_root=True)
except putils.ProcessExecutionError:
LOG.error(
"Failed to disconnect from NVMe nqn "
"%(conn_nqn)s with device_path %(device_path)s",
{'conn_nqn': conn_nqn, 'device_path': device_path})
raise
@utils.trace
@synchronized('extend_volume')
def extend_volume(self, connection_properties):
"""Update the local kernel's size information.
Try and update the local kernel's size information
for an LVM volume.
"""
volume_paths = self.get_volume_paths(connection_properties)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths)
else:
LOG.warning("Couldn't find any volume paths on the host to "
"extend volume for %(props)s",
{'props': connection_properties})
raise exception.VolumePathsNotFound()

View File

@ -0,0 +1,168 @@
# 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_concurrency import processutils as putils
from os_brick import exception
from os_brick.initiator.connectors import nvme
from os_brick.initiator import linuxscsi
from os_brick.tests.initiator import test_connector
FAKE_NVME_LIST_OUTPUT = """
Node SN Model \
Namespace Usage Format FW Rev\n
---------------- -------------------- ---------------------------------------\
- --------- -------------------------- ---------------- --------\n
/dev/nvme0n1 67ff9467da6e5567 Linux \
10 1.07 GB / 1.07 GB 512 B + 0 B 4.8.0-58\n
"""
class NVMeConnectorTestCase(test_connector.ConnectorTestCase):
"""Test cases for NVMe initiator class."""
def setUp(self):
super(NVMeConnectorTestCase, self).setUp()
self.connector = nvme.NVMeConnector(None,
execute=self.fake_execute)
def _nvme_list_cmd(self, *args, **kwargs):
return FAKE_NVME_LIST_OUTPUT, None
def test__get_nvme_devices(self):
expected = ['/dev/nvme0n1']
self.connector._execute = self._nvme_list_cmd
actual = self.connector._get_nvme_devices()
self.assertEqual(expected, actual)
@mock.patch.object(nvme.NVMeConnector, '_execute')
def test_get_nvme_devices_raise(self, mock_execute):
mock_execute.side_effect = putils.ProcessExecutionError
self.assertRaises(putils.ProcessExecutionError,
self.connector._get_nvme_devices)
@mock.patch.object(nvme.NVMeConnector, '_get_nvme_devices')
@mock.patch.object(nvme.NVMeConnector, '_execute')
@mock.patch('time.sleep')
def test_connect_volume(self, mock_sleep, mock_execute, mock_devices):
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
mock_devices.side_effect = [
['/dev/nvme0n1'], ['/dev/nvme0n2']]
device_info = self.connector.connect_volume(
connection_properties)
self.assertEqual('/dev/nvme0n2', device_info['path'])
self.assertEqual('block', device_info['type'])
self.assertEqual(2, mock_devices.call_count)
mock_execute.assert_called_once_with(
'nvme', 'connect', '-t',
connection_properties['transport_type'], '-n',
'volume_123',
'-a', connection_properties['target_portal'],
'-s', connection_properties['target_port'],
root_helper=None,
run_as_root=True)
@mock.patch.object(nvme.NVMeConnector, '_execute')
def test_connect_volume_raise(self, mock_execute):
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
mock_execute.side_effect = putils.ProcessExecutionError
self.assertRaises(putils.ProcessExecutionError,
self.connector.connect_volume,
connection_properties)
@mock.patch.object(nvme.NVMeConnector, '_get_nvme_devices')
@mock.patch.object(nvme.NVMeConnector, '_execute')
@mock.patch('time.sleep')
def test_connect_volume_max_retry(
self, mock_sleep, mock_execute, mock_devices):
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
mock_devices.return_value = '/dev/nvme0n1'
self.assertRaises(exception.TargetPortalNotFound,
self.connector.connect_volume,
connection_properties)
@mock.patch.object(nvme.NVMeConnector, '_execute')
def test_disconnect_volume(self, mock_execute):
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
self.connector.disconnect_volume(connection_properties, None)
mock_execute.asert_called_once_with(
'nvme', 'disconnect', '-n',
'volume_123',
root_helper=None,
run_as_root=True)
@mock.patch.object(nvme.NVMeConnector, '_execute')
def test_disconnect_volume_raise(self, mock_execute):
mock_execute.side_effect = putils.ProcessExecutionError
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
self.assertRaises(putils.ProcessExecutionError,
self.connector.disconnect_volume,
connection_properties,
None)
@mock.patch.object(nvme.NVMeConnector, 'get_volume_paths')
def test_extend_volume_no_path(self, mock_volume_paths):
mock_volume_paths.return_value = []
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
self.assertRaises(exception.VolumePathsNotFound,
self.connector.extend_volume,
connection_properties)
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
@mock.patch.object(nvme.NVMeConnector, 'get_volume_paths')
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
fake_new_size = 1024
mock_volume_paths.return_value = ['/dev/vdx']
mock_scsi_extend.return_value = fake_new_size
connection_properties = {'target_portal': 'portal',
'target_port': 1,
'nqn': 'nqn.volume_123',
'device_path': '',
'transport_type': 'rdma'}
new_size = self.connector.extend_volume(connection_properties)
self.assertEqual(fake_new_size, new_size)