Lightbits LightOS driver
This commit introduces the LightOS driver for os-brick. LightOS is a software-defined disaggregated clustered storage solution running on commodity servers with commodity SSDs. It it developed by Lightbits Labs (https://www.lightbitslabs.com) and is actively developed and maintained. LightOS is proprietary but the openstack drivers are licensed under Apache v2.0. The Cinder driver for LightOS currently supports the following functionality: Create volume Delete volume Attach volume Detach volume Create image from volume create volume from image Live migration Volume replication Thin provisioning Multi-attach Extend volume Create snapshot Delete snapshot Create volume from snapshot Create volume from volume (clone) This driver has been developed and has been in use for a couple of years by Lightbits and our clients. We have tested it extensively internally with multiple openstack versions, including Queens, Rocky, Stein, and Train. We have also tested it with master (19.1 xena) and we are working to extend testing to cover additional openstack releases. We are glad to join the openstack community and hope to get your feedback and comments on this driver, and if it is acceptable, to see it merged into the tree. Signed-off-by: Yuval Brave yuval@lightbitslabs.com Change-Id: I2e86fa84049053b7c75421d33ad1a1af459ef4e0
This commit is contained in:
parent
7a6a09fc84
commit
627da6a40b
@ -56,3 +56,4 @@ GPFS = "GPFS"
|
||||
STORPOOL = "STORPOOL"
|
||||
NVME = "NVME"
|
||||
NVMEOF = "NVMEOF"
|
||||
LIGHTOS = "LIGHTOS"
|
||||
|
@ -64,6 +64,7 @@ unix_connector_list = [
|
||||
'os_brick.initiator.connectors.vmware.VmdkConnector',
|
||||
'os_brick.initiator.connectors.storpool.StorPoolConnector',
|
||||
'os_brick.initiator.connectors.nvmeof.NVMeOFConnector',
|
||||
'os_brick.initiator.connectors.lightos.LightOSConnector',
|
||||
]
|
||||
|
||||
|
||||
@ -114,6 +115,8 @@ _connector_mapping_linux = {
|
||||
'os_brick.initiator.connectors.nvmeof.NVMeOFConnector',
|
||||
initiator.NVMEOF:
|
||||
'os_brick.initiator.connectors.nvmeof.NVMeOFConnector',
|
||||
initiator.LIGHTOS:
|
||||
'os_brick.initiator.connectors.lightos.LightOSConnector',
|
||||
}
|
||||
|
||||
# Mapping for the S390X platform
|
||||
|
346
os_brick/initiator/connectors/lightos.py
Normal file
346
os_brick/initiator/connectors/lightos.py
Normal file
@ -0,0 +1,346 @@
|
||||
# Copyright (C) 2016-2022 Lightbits Labs Ltd.
|
||||
# Copyright (C) 2020 Intel Corporation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 glob
|
||||
import http.client
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
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.i18n import _
|
||||
from os_brick.initiator.connectors import base
|
||||
from os_brick.privileged import lightos as priv_lightos
|
||||
from os_brick.privileged import nvmeof as priv_nvme
|
||||
from os_brick import utils
|
||||
|
||||
|
||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 5
|
||||
DISCOVERY_CLIENT_PORT = 6060
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
||||
nvmec_pattern = ".*nvme[0-9]+[cp][0-9]+.*"
|
||||
nvmec_match = re.compile(nvmec_pattern)
|
||||
|
||||
|
||||
class LightOSConnector(base.BaseLinuxConnector):
|
||||
"""Connector class to attach/detach LightOS volumes using NVMe/TCP."""
|
||||
|
||||
WAIT_DEVICE_TIMEOUT = 60
|
||||
|
||||
def __init__(self,
|
||||
root_helper,
|
||||
driver=None,
|
||||
execute=None,
|
||||
device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
||||
message_queue=None,
|
||||
*args,
|
||||
**kwargs):
|
||||
super(LightOSConnector, self).__init__(
|
||||
root_helper,
|
||||
driver=driver,
|
||||
execute=execute,
|
||||
device_scan_attempts=device_scan_attempts,
|
||||
*args, **kwargs)
|
||||
self.message_queue = message_queue
|
||||
self.DISCOVERY_DIR_PATH = '/etc/discovery-client/discovery.d/'
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(root_helper, *args, **kwargs):
|
||||
"""The LightOS connector properties."""
|
||||
props = {}
|
||||
lightos_connector = LightOSConnector(root_helper=root_helper,
|
||||
message_queue=None,
|
||||
execute=kwargs.get('execute'))
|
||||
hostnqn = lightos_connector.get_hostnqn()
|
||||
found_dsc = lightos_connector.find_dsc()
|
||||
|
||||
if not found_dsc:
|
||||
LOG.debug('LIGHTOS: did not find dsc, continuing anyway.')
|
||||
|
||||
if hostnqn:
|
||||
LOG.debug("LIGHTOS: finally hostnqn: %s dsc: %s",
|
||||
hostnqn, found_dsc)
|
||||
props['hostnqn'] = hostnqn
|
||||
props['found_dsc'] = found_dsc
|
||||
else:
|
||||
LOG.debug('LIGHTOS: no hostnqn found.')
|
||||
|
||||
return props
|
||||
|
||||
def dsc_file_name(self, uuid):
|
||||
return os.path.join(self.DISCOVERY_DIR_PATH, "%s.conf" % uuid)
|
||||
|
||||
def find_dsc(self):
|
||||
conn = http.client.HTTPConnection("localhost", DISCOVERY_CLIENT_PORT)
|
||||
try:
|
||||
conn.request("HEAD", "/metrics")
|
||||
resp = conn.getresponse()
|
||||
return 'found' if resp.status == http.client.OK else ''
|
||||
except Exception as e:
|
||||
LOG.debug(f'LIGHTOS: {e}')
|
||||
out = ''
|
||||
return out
|
||||
|
||||
def dsc_need_connect(self, connection_info):
|
||||
return not os.path.isfile(self.dsc_file_name(connection_info['uuid']))
|
||||
|
||||
def dsc_connect_volume(self, connection_info):
|
||||
if not self.dsc_need_connect(connection_info):
|
||||
return
|
||||
|
||||
subsysnqn = connection_info['nqn']
|
||||
uuid = connection_info['uuid']
|
||||
hostnqn = self.get_hostnqn()
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as dscfile:
|
||||
dscfile.write('# os_brick connector dsc file for LightOS'
|
||||
' volume: {}\n'.format(uuid))
|
||||
for (ip, node) in connection_info['lightos_nodes'].items():
|
||||
transport = node['transport_type']
|
||||
host = node['target_portal']
|
||||
port = node['target_port']
|
||||
dscfile.write('-t {} -a {} -s {} -q {} -n {}\n'.format(
|
||||
transport, host, port, hostnqn, subsysnqn))
|
||||
dscfile.flush()
|
||||
try:
|
||||
dest_name = self.dsc_file_name(uuid)
|
||||
priv_lightos.move_dsc_file(dscfile.name, dest_name)
|
||||
except Exception:
|
||||
LOG.warning(
|
||||
"LIGHTOS: Failed to create dsc file for connection with"
|
||||
f" uuid:{uuid}")
|
||||
raise
|
||||
|
||||
def dsc_disconnect_volume(self, connection_info):
|
||||
uuid = connection_info['uuid']
|
||||
try:
|
||||
priv_lightos.delete_dsc_file(self.dsc_file_name(uuid))
|
||||
except Exception:
|
||||
LOG.warning("LIGHTOS: Failed delete dsc file uuid:{}".format(uuid))
|
||||
raise
|
||||
|
||||
def monitor_db(self, lightos_db):
|
||||
for connection_info in lightos_db.values():
|
||||
self.dsc_connect_volume(connection_info)
|
||||
|
||||
def monitor_message_queue(self, message_queue, lightos_db):
|
||||
while not message_queue.empty():
|
||||
msg = message_queue.get()
|
||||
op, connection = msg
|
||||
LOG.debug("LIGHTOS: queue got op: %s, connection: %s",
|
||||
op, connection)
|
||||
if op == 'delete':
|
||||
LOG.info("LIGHTOS: Removing volume: %s from db",
|
||||
connection['uuid'])
|
||||
if connection['uuid'] in lightos_db:
|
||||
del lightos_db[connection['uuid']]
|
||||
else:
|
||||
LOG.warning("LIGHTOS: No volume: %s found in db",
|
||||
connection['uuid'])
|
||||
elif op == 'add':
|
||||
LOG.info("LIGHTOS: Adding volume: %s to db",
|
||||
connection['uuid'])
|
||||
lightos_db[connection['uuid']] = connection
|
||||
|
||||
def lightos_monitor(self, lightos_db, message_queue):
|
||||
'''Bookkeeping lightos connections.
|
||||
|
||||
This is useful when the connector is comming up to a running node with
|
||||
connected volumes already exists.
|
||||
This is used in the Nova driver to restore connections after reboot
|
||||
'''
|
||||
first_time = True
|
||||
while True:
|
||||
self.monitor_db(lightos_db)
|
||||
# give us some time before trying to access the MQ
|
||||
# for the first time
|
||||
if first_time:
|
||||
time.sleep(5)
|
||||
first_time = False
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
self.monitor_message_queue(message_queue, lightos_db)
|
||||
|
||||
# This is part of our abstract interface
|
||||
def get_search_path(self):
|
||||
return '/dev'
|
||||
|
||||
# This is part of our abstract interface
|
||||
def get_volume_paths(self, connection_properties):
|
||||
path = connection_properties['device_path']
|
||||
return [path]
|
||||
|
||||
def _check_device_exists_using_dev_lnk(self, uuid):
|
||||
lnk_path = f"/dev/disk/by-id/nvme-uuid.{uuid}"
|
||||
if os.path.exists(lnk_path):
|
||||
devname = os.path.realpath(lnk_path)
|
||||
if devname.startswith("/dev/nvme"):
|
||||
LOG.info("LIGHTOS: devpath %s detected for uuid %s",
|
||||
devname, uuid)
|
||||
return devname
|
||||
return None
|
||||
|
||||
def _check_device_exists_reading_block_class(self, uuid):
|
||||
file_path = "/sys/class/block/*/wwid"
|
||||
wwid = "uuid." + uuid
|
||||
for match_path in glob.glob(file_path):
|
||||
try:
|
||||
with open(match_path, "r") as f:
|
||||
match_wwid = f.readline()
|
||||
except Exception:
|
||||
LOG.warning("LIGHTOS: Failed to read file %s",
|
||||
match_path)
|
||||
continue
|
||||
|
||||
if wwid != match_wwid.strip():
|
||||
continue
|
||||
|
||||
# skip slave nvme devices, for example: nvme0c0n1
|
||||
if nvmec_match.match(match_path.split("/")[-2]):
|
||||
continue
|
||||
|
||||
LOG.info("LIGHTOS: matching uuid %s was found"
|
||||
" for device path %s", uuid, match_path)
|
||||
return os.path.join("/dev", match_path.split("/")[-2])
|
||||
return None
|
||||
|
||||
@utils.trace
|
||||
def _get_device_by_uuid(self, uuid):
|
||||
endtime = time.time() + self.WAIT_DEVICE_TIMEOUT
|
||||
while time.time() < endtime:
|
||||
try:
|
||||
device = self._check_device_exists_using_dev_lnk(uuid)
|
||||
if device:
|
||||
return device
|
||||
except Exception as e:
|
||||
LOG.debug(f'LIGHTOS: {e}')
|
||||
device = self._check_device_exists_reading_block_class(uuid)
|
||||
if device:
|
||||
return device
|
||||
|
||||
time.sleep(1)
|
||||
return None
|
||||
|
||||
def _get_size_by_uuid(self, uuid):
|
||||
devpath = self._get_device_by_uuid(uuid)
|
||||
devname = devpath.split("/")[-1]
|
||||
try:
|
||||
size_path_name = os.path.join("/sys/class/block/", devname, "size")
|
||||
with open(size_path_name, "r") as f:
|
||||
size_blks = f.read().strip()
|
||||
bytesize = int(size_blks) * 512
|
||||
return bytesize
|
||||
except Exception:
|
||||
LOG.warning("LIGHTOS: Could not find the size at for"
|
||||
" uuid %s in %s", uuid, devpath)
|
||||
return None
|
||||
|
||||
@utils.trace
|
||||
@synchronized('volume_op')
|
||||
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
|
||||
"""
|
||||
device_info = {'type': 'block'}
|
||||
uuid = connection_properties['uuid']
|
||||
LOG.info("LIGHTOS: connect_volume called for volume %s, connection"
|
||||
" properties: %s",
|
||||
uuid, connection_properties)
|
||||
self.dsc_connect_volume(connection_properties)
|
||||
|
||||
device_path = self._get_device_by_uuid(uuid)
|
||||
if not device_path:
|
||||
msg = _("Device with uuid %s did not show up" % uuid)
|
||||
priv_lightos.delete_dsc_file(self.dsc_file_name(uuid))
|
||||
raise exception.BrickException(message=msg)
|
||||
|
||||
device_info['path'] = device_path
|
||||
|
||||
# bookkeeping lightos connections - add connection
|
||||
if self.message_queue:
|
||||
self.message_queue.put(('add', connection_properties))
|
||||
|
||||
return device_info
|
||||
|
||||
@utils.trace
|
||||
@synchronized('volume_op')
|
||||
def disconnect_volume(self, connection_properties, device_info,
|
||||
force=False, ignore_errors=False):
|
||||
"""Disconnect a volume from the local host.
|
||||
|
||||
The connection_properties are the same as from connect_volume.
|
||||
The device_info is returned from connect_volume.
|
||||
:param connection_properties: The dictionary that describes all
|
||||
of the target volume attributes.
|
||||
:type connection_properties: dict
|
||||
:param device_info: historical difference, but same as connection_props
|
||||
:type device_info: dict
|
||||
:param force: Whether to forcefully disconnect even if flush fails.
|
||||
:type force: bool
|
||||
:param ignore_errors: When force is True, this will decide whether to
|
||||
ignore errors or raise an exception once finished
|
||||
the operation. Default is False.
|
||||
:type ignore_errors: bool
|
||||
"""
|
||||
if force:
|
||||
# if operator want to force detach - we just return and cinder
|
||||
# driver will terminate the connection by updating the ACL
|
||||
return
|
||||
uuid = connection_properties['uuid']
|
||||
LOG.debug('LIGHTOS: disconnect_volume called for volume %s', uuid)
|
||||
device_path = self._get_device_by_uuid(uuid)
|
||||
try:
|
||||
if device_path:
|
||||
self._linuxscsi.flush_device_io(device_path)
|
||||
except putils.ProcessExecutionError:
|
||||
if not ignore_errors:
|
||||
raise
|
||||
self.dsc_disconnect_volume(connection_properties)
|
||||
# bookkeeping lightos connections - delete connection
|
||||
if self.message_queue:
|
||||
self.message_queue.put(('delete', connection_properties))
|
||||
|
||||
@utils.trace
|
||||
@synchronized('volume_op')
|
||||
def extend_volume(self, connection_properties):
|
||||
uuid = connection_properties['uuid']
|
||||
new_size = self._get_size_by_uuid(uuid)
|
||||
return new_size
|
||||
|
||||
def get_hostnqn(self):
|
||||
try:
|
||||
with open('/etc/nvme/hostnqn', 'r') as f:
|
||||
host_nqn = f.read().strip()
|
||||
except IOError:
|
||||
host_nqn = priv_nvme.create_hostnqn()
|
||||
except Exception:
|
||||
host_nqn = None
|
||||
return host_nqn
|
30
os_brick/privileged/lightos.py
Normal file
30
os_brick/privileged/lightos.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (C) 2016-2022 Lightbits Labs Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 shutil
|
||||
|
||||
from oslo_utils import fileutils
|
||||
|
||||
import os_brick.privileged
|
||||
|
||||
|
||||
@os_brick.privileged.default.entrypoint
|
||||
def delete_dsc_file(file_name):
|
||||
return fileutils.delete_if_exists(file_name)
|
||||
|
||||
|
||||
@os_brick.privileged.default.entrypoint
|
||||
def move_dsc_file(src, dst):
|
||||
return shutil.move(src, dst)
|
239
os_brick/tests/initiator/connectors/test_lightos.py
Normal file
239
os_brick/tests/initiator/connectors/test_lightos.py
Normal file
@ -0,0 +1,239 @@
|
||||
# Copyright (C) 2016-2020 Lightbits Labs Ltd.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 glob
|
||||
import http.client
|
||||
import queue
|
||||
from unittest import mock
|
||||
from unittest.mock import mock_open
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.connectors import lightos
|
||||
from os_brick.initiator import linuxscsi
|
||||
from os_brick.privileged import lightos as priv_lightos
|
||||
from os_brick.tests.initiator import test_connector
|
||||
|
||||
FAKE_NQN = "nqn.fake.qnq"
|
||||
|
||||
FAKE_LIGHTOS_CLUSTER_NODES = {
|
||||
"nodes": [
|
||||
{"UUID": "926e6df8-73e1-11ec-a624-000000000001",
|
||||
"nvmeEndpoint": "192.168.75.10:4420"},
|
||||
{"UUID": "926e6df8-73e1-11ec-a624-000000000002",
|
||||
"nvmeEndpoint": "192.168.75.11:4420"},
|
||||
{"UUID": "926e6df8-73e1-11ec-a624-000000000003",
|
||||
"nvmeEndpoint": "192.168.75.12:4420"}
|
||||
]
|
||||
}
|
||||
|
||||
FAKE_SUBSYSNQN = "nqn.2014-08.org.nvmexpress:NVMf:uuid:"
|
||||
FAKE_LIGHTOS_CLUSTER_INFO = {
|
||||
'UUID': "926e6df8-73e1-11ec-a624-07ba3880f6cc",
|
||||
'subsystemNQN': "nqn.2014-08.org.nvmexpress:NVMf:uuid:"
|
||||
"f4a89ce0-9fc2-4900-bfa3-00ad27995e7b",
|
||||
'nodes_ips': ["10.17.167.4", "10.17.167.5", "10.17.167.6"]
|
||||
}
|
||||
FAKE_VOLUME_UUID = "926e6df8-73e1-11ec-a624-07ba3880f6cd"
|
||||
NUM_BLOCKS_IN_GIB = 2097152
|
||||
BLOCK_SIZE = 512
|
||||
|
||||
|
||||
def get_http_response_mock(status):
|
||||
resp = mock.Mock()
|
||||
resp.status = status
|
||||
return resp
|
||||
|
||||
|
||||
class LightosConnectorTestCase(test_connector.ConnectorTestCase):
|
||||
|
||||
"""Test cases for NVMe initiator class."""
|
||||
|
||||
def setUp(self):
|
||||
super(LightosConnectorTestCase, self).setUp()
|
||||
self.connector = lightos.LightOSConnector(None,
|
||||
execute=self.fake_execute)
|
||||
|
||||
@staticmethod
|
||||
def _get_connection_info():
|
||||
lightos_nodes = {}
|
||||
for ip in FAKE_LIGHTOS_CLUSTER_INFO['nodes_ips']:
|
||||
lightos_nodes[ip] = dict(
|
||||
transport_type='tcp',
|
||||
target_portal=ip,
|
||||
target_port=8009
|
||||
)
|
||||
return dict(
|
||||
nqn=FAKE_LIGHTOS_CLUSTER_INFO['subsystemNQN'],
|
||||
uuid=FAKE_LIGHTOS_CLUSTER_INFO['UUID'],
|
||||
lightos_nodes=lightos_nodes
|
||||
)
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, 'get_hostnqn',
|
||||
return_value=FAKE_NQN)
|
||||
@mock.patch.object(lightos.LightOSConnector, 'find_dsc',
|
||||
return_value=True)
|
||||
def test_get_connector_properties(self, mock_nqn, mock_dsc):
|
||||
props = self.connector.get_connector_properties(None)
|
||||
expected_props = {"hostnqn": FAKE_NQN, "found_dsc": True}
|
||||
self.assertEqual(expected_props, props)
|
||||
|
||||
@mock.patch.object(lightos.http.client.HTTPConnection, "request",
|
||||
return_value=None)
|
||||
@mock.patch.object(lightos.http.client.HTTPConnection, "getresponse",
|
||||
return_value=get_http_response_mock(http.client.OK))
|
||||
def test_find_dsc_success(self, mocked_connection, mocked_response):
|
||||
mocked_connection.request.return_value = None
|
||||
mocked_response.getresponse.return_value = get_http_response_mock(
|
||||
http.client.OK)
|
||||
self.assertEqual(self.connector.find_dsc(), 'found')
|
||||
|
||||
@mock.patch.object(lightos.http.client.HTTPConnection, "request",
|
||||
return_value=None)
|
||||
@mock.patch.object(lightos.http.client.HTTPConnection, "getresponse",
|
||||
return_value=get_http_response_mock(
|
||||
http.client.NOT_FOUND))
|
||||
def test_find_dsc_failure(self, mocked_connection, mocked_response):
|
||||
mocked_connection.request.return_value = None
|
||||
mocked_response.getresponse.return_value = get_http_response_mock(
|
||||
http.client.OK)
|
||||
self.assertEqual(self.connector.find_dsc(), '')
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, 'get_hostnqn',
|
||||
return_value=FAKE_NQN)
|
||||
@mock.patch.object(lightos.priv_lightos, 'move_dsc_file',
|
||||
return_value="/etc/discovery_client/discovery.d/v0")
|
||||
@mock.patch.object(lightos.LightOSConnector,
|
||||
'_check_device_exists_using_dev_lnk',
|
||||
return_value="/dev/nvme0n1")
|
||||
def test_connect_volume_succeed(self, mock_nqn, mock_move_file,
|
||||
mock_check_device):
|
||||
self.connector.connect_volume(self._get_connection_info())
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, 'get_hostnqn',
|
||||
return_value=FAKE_NQN)
|
||||
@mock.patch.object(lightos.priv_lightos, 'move_dsc_file',
|
||||
return_value="/etc/discovery_client/discovery.d/v0")
|
||||
@mock.patch.object(lightos.priv_lightos, 'delete_dsc_file',
|
||||
return_value=None)
|
||||
@mock.patch.object(lightos.LightOSConnector, '_get_device_by_uuid',
|
||||
return_value=None)
|
||||
def test_connect_volume_failure(self, mock_nqn, mock_move_file,
|
||||
mock_delete_file, mock_get_device):
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector.connect_volume,
|
||||
self._get_connection_info())
|
||||
|
||||
@mock.patch.object(priv_lightos, 'delete_dsc_file', return_value=True)
|
||||
def test_dsc_disconnect_volume_succeed(self, mock_priv_lightos):
|
||||
self.connector.dsc_disconnect_volume(self._get_connection_info())
|
||||
|
||||
@mock.patch.object(priv_lightos, 'delete_dsc_file',
|
||||
side_effect=OSError("failed to delete file"))
|
||||
def test_dsc_disconnect_volume_failure(self, execute_mock):
|
||||
self.assertRaises(OSError,
|
||||
self.connector.dsc_disconnect_volume,
|
||||
self._get_connection_info())
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector,
|
||||
'_check_device_exists_using_dev_lnk',
|
||||
return_value=("/dev/nvme0n1"))
|
||||
def test_get_device_by_uuid_succeed_with_link(self, execute_mock):
|
||||
self.assertEqual(self.connector._get_device_by_uuid(FAKE_VOLUME_UUID),
|
||||
"/dev/nvme0n1")
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector,
|
||||
'_check_device_exists_reading_block_class',
|
||||
return_value=("/dev/nvme0n1"))
|
||||
def test_get_device_by_uuid_succeed_with_block_class(self, execute_mock):
|
||||
self.assertEqual(self.connector._get_device_by_uuid(FAKE_VOLUME_UUID),
|
||||
"/dev/nvme0n1")
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector,
|
||||
'_check_device_exists_using_dev_lnk',
|
||||
side_effect=[None, False, "/dev/nvme0n1"])
|
||||
@mock.patch.object(lightos.LightOSConnector,
|
||||
'_check_device_exists_reading_block_class',
|
||||
side_effect=[None, False, "/dev/nvme0n1"])
|
||||
def test_get_device_by_uuid_many_attempts(self, execute_mock, glob_mock):
|
||||
self.assertEqual(self.connector._get_device_by_uuid(FAKE_VOLUME_UUID),
|
||||
'/dev/nvme0n1')
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, 'dsc_connect_volume',
|
||||
return_value=None)
|
||||
@mock.patch.object(lightos.LightOSConnector, '_get_device_by_uuid',
|
||||
return_value="/dev/nvme0n1")
|
||||
def test_connect_volume(self, dsc_connect, path):
|
||||
connection_properties = {"hostnqn": FAKE_NQN, "found_dsc": True,
|
||||
"uuid": "123"}
|
||||
expected_device_info = {'type': 'block', "path": "/dev/nvme0n1"}
|
||||
device_info = self.connector.connect_volume(connection_properties)
|
||||
|
||||
self.assertEqual(expected_device_info, device_info)
|
||||
|
||||
@mock.patch.object(linuxscsi.LinuxSCSI, 'flush_device_io', autospec=True)
|
||||
@mock.patch.object(lightos.LightOSConnector, '_get_device_by_uuid',
|
||||
return_value="/dev/nvme0n1")
|
||||
@mock.patch.object(lightos.LightOSConnector, 'dsc_disconnect_volume')
|
||||
def test_disconnect_volume(self, mock_disconnect, mock_uuid, mock_flush):
|
||||
connection_properties = {"hostnqn": FAKE_NQN, "found_dsc": True,
|
||||
"uuid": "123"}
|
||||
self.connector.disconnect_volume(connection_properties, None)
|
||||
mock_disconnect.assert_called_once_with(connection_properties)
|
||||
mock_flush.assert_called_once_with(mock.ANY, "/dev/nvme0n1")
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, '_get_device_by_uuid',
|
||||
return_value="/dev/nvme0n1")
|
||||
@mock.patch("builtins.open", new_callable=mock_open,
|
||||
read_data=f"{str(NUM_BLOCKS_IN_GIB)}\n")
|
||||
def test_extend_volume(self, mock_execute, m_open):
|
||||
connection_properties = {'uuid': FAKE_VOLUME_UUID}
|
||||
self.assertEqual(self.connector.extend_volume(connection_properties),
|
||||
NUM_BLOCKS_IN_GIB * BLOCK_SIZE)
|
||||
|
||||
def test_monitor_message_queue_delete(self):
|
||||
message_queue = queue.Queue()
|
||||
connection = {"uuid": "123"}
|
||||
message_queue.put(("delete", connection))
|
||||
lightos_db = {"123": "fake_connection"}
|
||||
self.connector.monitor_message_queue(message_queue, lightos_db)
|
||||
self.assertEqual(len(lightos_db), 0)
|
||||
|
||||
def test_monitor_message_queue_add(self):
|
||||
message_queue = queue.Queue()
|
||||
connection = {"uuid": "123"}
|
||||
lightos_db = {}
|
||||
message_queue.put(("add", connection))
|
||||
self.connector.monitor_message_queue(message_queue, lightos_db)
|
||||
self.assertEqual(len(lightos_db), 1)
|
||||
|
||||
@mock.patch.object(lightos.os.path, 'exists', return_value=True)
|
||||
@mock.patch.object(lightos.os.path, 'realpath',
|
||||
return_value="/dev/nvme0n1")
|
||||
def test_check_device_exists_using_dev_lnk_succeed(self, mock_path_exists,
|
||||
mock_realpath):
|
||||
found_dev = self.connector._check_device_exists_using_dev_lnk(
|
||||
FAKE_VOLUME_UUID)
|
||||
self.assertEqual("/dev/nvme0n1", found_dev)
|
||||
|
||||
def test_check_device_exists_using_dev_lnk_false(self):
|
||||
self.assertIsNone(self.connector._check_device_exists_using_dev_lnk(
|
||||
FAKE_VOLUME_UUID))
|
||||
|
||||
@mock.patch.object(glob, "glob", return_value=['/path/nvme0n1/wwid'])
|
||||
@mock.patch("builtins.open", new_callable=mock_open,
|
||||
read_data=f"uuid.{FAKE_VOLUME_UUID}\n")
|
||||
def test_check_device_exists_reading_block_class(self, mock_glob, m_open):
|
||||
found_dev = self.connector._check_device_exists_reading_block_class(
|
||||
FAKE_VOLUME_UUID)
|
||||
self.assertEqual("/dev/nvme0n1", found_dev)
|
@ -24,6 +24,7 @@ from os_brick.initiator import connector
|
||||
from os_brick.initiator.connectors import base
|
||||
from os_brick.initiator.connectors import fake
|
||||
from os_brick.initiator.connectors import iscsi
|
||||
from os_brick.initiator.connectors import lightos
|
||||
from os_brick.initiator.connectors import nvmeof
|
||||
from os_brick.initiator import linuxfc
|
||||
from os_brick.privileged import rootwrap as priv_rootwrap
|
||||
@ -41,6 +42,8 @@ class ZeroIntervalLoopingCall(loopingcall.FixedIntervalLoopingCall):
|
||||
|
||||
class ConnectorUtilsTestCase(test_base.TestCase):
|
||||
|
||||
@mock.patch.object(lightos.LightOSConnector, 'get_hostnqn',
|
||||
return_value=None)
|
||||
@mock.patch.object(nvmeof.NVMeOFConnector, '_get_system_uuid',
|
||||
return_value=None)
|
||||
@mock.patch.object(nvmeof.NVMeOFConnector, '_get_host_uuid',
|
||||
@ -63,6 +66,7 @@ class ConnectorUtilsTestCase(test_base.TestCase):
|
||||
mock_nqn,
|
||||
mock_hostuuid,
|
||||
mock_sysuuid,
|
||||
mock_lightos_hostnqn,
|
||||
host='fakehost'):
|
||||
props_actual = connector.get_connector_properties('sudo',
|
||||
MY_IP,
|
||||
|
Loading…
Reference in New Issue
Block a user