Merge "Lightbits LightOS driver"
This commit is contained in:
commit
5cf395b385
@ -133,6 +133,7 @@ from cinder.volume.drivers.kioxia import kumoscale as \
|
||||
cinder_volume_drivers_kioxia_kumoscale
|
||||
from cinder.volume.drivers.lenovo import lenovo_common as \
|
||||
cinder_volume_drivers_lenovo_lenovocommon
|
||||
from cinder.volume.drivers import lightos as cinder_volume_drivers_lightos
|
||||
from cinder.volume.drivers import linstordrv as \
|
||||
cinder_volume_drivers_linstordrv
|
||||
from cinder.volume.drivers import lvm as cinder_volume_drivers_lvm
|
||||
@ -352,6 +353,7 @@ def list_opts():
|
||||
kaminario_opts,
|
||||
cinder_volume_drivers_lenovo_lenovocommon.common_opts,
|
||||
cinder_volume_drivers_lenovo_lenovocommon.iscsi_opts,
|
||||
cinder_volume_drivers_lightos.lightos_opts,
|
||||
cinder_volume_drivers_linstordrv.linstor_opts,
|
||||
cinder_volume_drivers_lvm.volume_opts,
|
||||
cinder_volume_drivers_macrosan_driver.config.macrosan_opts,
|
||||
|
702
cinder/tests/unit/volume/drivers/lightos/test_lightos_storage.py
Normal file
702
cinder/tests/unit/volume/drivers/lightos/test_lightos_storage.py
Normal file
@ -0,0 +1,702 @@
|
||||
# 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.
|
||||
|
||||
|
||||
from copy import deepcopy
|
||||
import functools
|
||||
import hashlib
|
||||
import http.client as httpstatus
|
||||
import json
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
from cinder import context
|
||||
from cinder import db
|
||||
from cinder import exception
|
||||
from cinder.tests.unit import test
|
||||
from cinder.tests.unit import utils as test_utils
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers import lightos
|
||||
|
||||
|
||||
FAKE_LIGHTOS_CLUSTER_NODES: Dict[str, List] = {
|
||||
"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_LIGHTOS_CLUSTER_INFO: Dict[str, str] = {
|
||||
'UUID': "926e6df8-73e1-11ec-a624-07ba3880f6cc",
|
||||
'subsystemNQN': "nqn.2014-08.org.nvmexpress:NVMf:uuid:"
|
||||
"f4a89ce0-9fc2-4900-bfa3-00ad27995e7b"
|
||||
}
|
||||
|
||||
FAKE_CLIENT_HOSTNQN = "hostnqn1"
|
||||
VOLUME_BACKEND_NAME = "lightos_backend"
|
||||
RESERVED_PERCENTAGE = 30
|
||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 5
|
||||
LIGHTOS_API_SERVICE_TIMEOUT = 30
|
||||
VOLUME_BACKEND_NAME = "lightos_backend"
|
||||
RESERVED_PERCENTAGE = 30
|
||||
|
||||
|
||||
class InitiatorConnectorFactoryMocker:
|
||||
@staticmethod
|
||||
def factory(protocol, root_helper, driver=None,
|
||||
use_multipath=False,
|
||||
device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT,
|
||||
arch=None,
|
||||
*args, **kwargs):
|
||||
return InitialConnectorMock()
|
||||
|
||||
|
||||
class InitialConnectorMock:
|
||||
hostnqn = FAKE_CLIENT_HOSTNQN
|
||||
found_discovery_client = True
|
||||
|
||||
def get_hostnqn(self):
|
||||
return self.__class__.hostnqn
|
||||
|
||||
def find_dsc(self):
|
||||
return self.__class__.found_discovery_client
|
||||
|
||||
|
||||
def get_connector_properties():
|
||||
connector = InitialConnectorMock()
|
||||
return dict(hostnqn=connector.get_hostnqn(),
|
||||
found_dsc=connector.find_dsc())
|
||||
|
||||
|
||||
def get_vol_etag(volume):
|
||||
v = deepcopy(volume)
|
||||
v.pop("ETag", None)
|
||||
dump = json.dumps(v, sort_keys=True).encode('utf-8')
|
||||
return hashlib.md5(dump).hexdigest()
|
||||
|
||||
|
||||
class DBMock(object):
|
||||
|
||||
def __init__(self):
|
||||
self.data = {
|
||||
"projects": {},
|
||||
}
|
||||
|
||||
def get_or_create_project(self, project_name) -> Dict:
|
||||
project = self.data["projects"].setdefault(project_name, {})
|
||||
return project
|
||||
|
||||
def get_project(self, project_name) -> Dict:
|
||||
project = self.data["projects"].get(project_name, None)
|
||||
return project if project else None
|
||||
|
||||
def delete_project(self, project_name) -> Dict:
|
||||
assert project_name != "default", "can't delete default project"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return None
|
||||
self.data["projects"].remove(project)
|
||||
return project
|
||||
|
||||
def create_volume(self, volume) -> Tuple[int, Dict]:
|
||||
assert volume["project_name"] and volume["name"], "must be provided"
|
||||
project = self.get_or_create_project(volume["project_name"])
|
||||
volumes = project.setdefault("volumes", [])
|
||||
existing_volume = next(iter([vol for vol in volumes
|
||||
if vol["name"] == volume["name"]]), None)
|
||||
if not existing_volume:
|
||||
volume["UUID"] = str(uuid.uuid4())
|
||||
volumes.append(volume)
|
||||
return httpstatus.OK, volume
|
||||
return httpstatus.CONFLICT, None
|
||||
|
||||
def get_volume_by_uuid(self, project_name,
|
||||
volume_uuid) -> Tuple[int, Dict]:
|
||||
assert project_name and volume_uuid, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_vols = project.get("volumes", None)
|
||||
if not proj_vols:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
vol = next(iter([vol for vol in proj_vols
|
||||
if vol["UUID"] == volume_uuid]), None)
|
||||
return (httpstatus.OK, vol) if vol else (httpstatus.NOT_FOUND, None)
|
||||
|
||||
def update_volume_by_uuid(self, project_name,
|
||||
volume_uuid, **kwargs) -> Tuple[int, Dict]:
|
||||
error_code, volume = self.get_volume_by_uuid(project_name, volume_uuid)
|
||||
if error_code != httpstatus.OK:
|
||||
return error_code, None
|
||||
etag = kwargs.get("etag", None)
|
||||
if etag:
|
||||
vol_etag = volume.get("ETag", None)
|
||||
if etag != vol_etag:
|
||||
return httpstatus.BAD_REQUEST, None
|
||||
if kwargs.get("size", None):
|
||||
volume["size"] = kwargs["size"]
|
||||
if kwargs.get("acl", None):
|
||||
volume["acl"] = {'values': kwargs.get('acl')}
|
||||
volume["ETag"] = get_vol_etag(volume)
|
||||
return httpstatus.OK, volume
|
||||
|
||||
def get_volume_by_name(self, project_name,
|
||||
volume_name) -> Tuple[int, Dict]:
|
||||
assert project_name and volume_name, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_vols = project.get("volumes", None)
|
||||
if not proj_vols:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
vol = next(iter([vol for vol in proj_vols
|
||||
if vol["name"] == volume_name]), None)
|
||||
return (httpstatus.OK, vol) if vol else (httpstatus.NOT_FOUND, None)
|
||||
|
||||
def delete_volume(self, project_name, volume_uuid) -> Tuple[int, Dict]:
|
||||
assert project_name and volume_uuid, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_vols = project.get("volumes", None)
|
||||
if not proj_vols:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
for vol in proj_vols:
|
||||
if vol["UUID"] == volume_uuid:
|
||||
proj_vols.remove(vol)
|
||||
return httpstatus.OK, vol
|
||||
|
||||
def update_volume(self, **kwargs):
|
||||
assert("project_name" in kwargs and kwargs["project_name"]), \
|
||||
"project_name must be provided"
|
||||
|
||||
def create_snapshot(self, snapshot) -> Tuple[int, Dict]:
|
||||
assert snapshot["project_name"] and snapshot["name"], \
|
||||
"must be provided"
|
||||
project = self.get_or_create_project(snapshot["project_name"])
|
||||
snapshots = project.setdefault("snapshots", [])
|
||||
existing_snap = next(iter([snap for snap in snapshots
|
||||
if snap["name"] == snapshot["name"]]), None)
|
||||
if not existing_snap:
|
||||
snapshot["UUID"] = str(uuid.uuid4())
|
||||
snapshots.append(snapshot)
|
||||
return httpstatus.OK, snapshot
|
||||
return httpstatus.CONFLICT, None
|
||||
|
||||
def delete_snapshot(self, project_name, snapshot_uuid) -> Tuple[int, Dict]:
|
||||
assert project_name and snapshot_uuid, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_snaps = project.get("snapshots", None)
|
||||
if not proj_snaps:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
for snap in proj_snaps:
|
||||
if snap["UUID"] == snapshot_uuid:
|
||||
proj_snaps.remove(snap)
|
||||
return httpstatus.OK, snap
|
||||
|
||||
def get_snapshot_by_name(self, project_name,
|
||||
snapshot_name) -> Tuple[int, Dict]:
|
||||
assert project_name and snapshot_name, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_snaps = project.get("snapshots", None)
|
||||
if not proj_snaps:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
snap = next(iter([snap for snap in proj_snaps
|
||||
if snap["name"] == snapshot_name]), None)
|
||||
return (httpstatus.OK, snap) if snap else (httpstatus.NOT_FOUND, None)
|
||||
|
||||
def get_snapshot_by_uuid(self, project_name,
|
||||
snapshot_uuid) -> Tuple[int, Dict]:
|
||||
assert project_name and snapshot_uuid, "must be provided"
|
||||
project = self.get_project(project_name)
|
||||
if not project:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
proj_snaps = project.get("snapshots", None)
|
||||
if not proj_snaps:
|
||||
return httpstatus.NOT_FOUND, None
|
||||
snap = next(iter([snap for snap in proj_snaps
|
||||
if snap["UUID"] == snapshot_uuid]), None)
|
||||
return (httpstatus.OK, snap) if snap else (httpstatus.NOT_FOUND, None)
|
||||
|
||||
|
||||
class LightOSStorageVolumeDriverTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize LightOS Storage Driver."""
|
||||
super(LightOSStorageVolumeDriverTest, self).setUp()
|
||||
|
||||
configuration = mock.Mock(conf.Configuration)
|
||||
|
||||
configuration.lightos_api_address = \
|
||||
"10.10.10.71,10.10.10.72,10.10.10.73"
|
||||
configuration.lightos_api_port = 443
|
||||
configuration.lightos_jwt = None
|
||||
configuration.lightos_snapshotname_prefix = 'openstack_'
|
||||
configuration.lightos_intermediate_snapshot_name_prefix = 'for_clone_'
|
||||
configuration.lightos_default_compression_enabled = False
|
||||
configuration.lightos_default_num_replicas = 3
|
||||
configuration.num_volume_device_scan_tries = (
|
||||
DEVICE_SCAN_ATTEMPTS_DEFAULT)
|
||||
configuration.lightos_api_service_timeout = LIGHTOS_API_SERVICE_TIMEOUT
|
||||
configuration.driver_ssl_cert_verify = False
|
||||
# for some reason this value is not initialized by the driver parent
|
||||
# configs
|
||||
configuration.volume_name_template = 'volume-%s'
|
||||
configuration.initiator_connector = (
|
||||
"cinder.tests.unit.volume.drivers.lightos."
|
||||
"test_lightos_storage.InitiatorConnectorFactoryMocker")
|
||||
configuration.volume_backend_name = VOLUME_BACKEND_NAME
|
||||
configuration.reserved_percentage = RESERVED_PERCENTAGE
|
||||
|
||||
def mocked_safe_get(config, variable_name):
|
||||
if hasattr(config, variable_name):
|
||||
return config.__getattribute__(variable_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
configuration.safe_get = functools.partial(mocked_safe_get,
|
||||
configuration)
|
||||
self.driver = lightos.LightOSVolumeDriver(configuration=configuration)
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.db: DBMock = DBMock()
|
||||
|
||||
# define a default send_cmd override to return default values.
|
||||
def send_cmd_default_mock(cmd, timeout, **kwargs):
|
||||
if cmd == "get_nodes":
|
||||
return (httpstatus.OK, FAKE_LIGHTOS_CLUSTER_NODES)
|
||||
if cmd == "get_node":
|
||||
self.assertTrue(kwargs["UUID"])
|
||||
for node in FAKE_LIGHTOS_CLUSTER_NODES["nodes"]:
|
||||
if kwargs["UUID"] == node["UUID"]:
|
||||
return (httpstatus.OK, node)
|
||||
return (httpstatus.NOT_FOUND, node)
|
||||
elif cmd == "get_cluster_info":
|
||||
return (httpstatus.OK, FAKE_LIGHTOS_CLUSTER_INFO)
|
||||
elif cmd == "create_volume":
|
||||
project_name = kwargs["project_name"]
|
||||
volume = {
|
||||
"project_name": project_name,
|
||||
"name": kwargs["name"],
|
||||
"size": kwargs["size"],
|
||||
"n_replicas": kwargs["n_replicas"],
|
||||
"compression": kwargs["compression"],
|
||||
"src_snapshot_name": kwargs["src_snapshot_name"],
|
||||
"acl": {'values': kwargs.get('acl')},
|
||||
"state": "Available",
|
||||
}
|
||||
volume["ETag"] = get_vol_etag(volume)
|
||||
code, new_vol = self.db.create_volume(volume)
|
||||
return (code, new_vol)
|
||||
elif cmd == "delete_volume":
|
||||
return self.db.delete_volume(kwargs["project_name"],
|
||||
kwargs["volume_uuid"])
|
||||
elif cmd == "get_volume":
|
||||
return self.db.get_volume_by_uuid(kwargs["project_name"],
|
||||
kwargs["volume_uuid"])
|
||||
elif cmd == "get_volume_by_name":
|
||||
return self.db.get_volume_by_name(kwargs["project_name"],
|
||||
kwargs["volume_name"])
|
||||
elif cmd == "extend_volume":
|
||||
size = kwargs.get("size", None)
|
||||
return self.db.update_volume_by_uuid(kwargs["project_name"],
|
||||
kwargs["volume_uuid"],
|
||||
size=size)
|
||||
elif cmd == "create_snapshot":
|
||||
snapshot = {
|
||||
"project_name": kwargs.get("project_name", None),
|
||||
"name": kwargs.get("name", None),
|
||||
"state": "Available",
|
||||
}
|
||||
return self.db.create_snapshot(snapshot)
|
||||
elif cmd == "delete_snapshot":
|
||||
return self.db.delete_snapshot(kwargs["project_name"],
|
||||
kwargs["snapshot_uuid"])
|
||||
elif cmd == "get_snapshot":
|
||||
return self.db.get_snapshot_by_uuid(kwargs["project_name"],
|
||||
kwargs["snapshot_uuid"])
|
||||
elif cmd == "get_snapshot_by_name":
|
||||
return self.db.get_snapshot_by_name(kwargs["project_name"],
|
||||
kwargs["snapshot_name"])
|
||||
elif cmd == "update_volume":
|
||||
return self.db.update_volume_by_uuid(**kwargs)
|
||||
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"'{cmd}' is not implemented. kwargs: {kwargs}")
|
||||
|
||||
self.driver.cluster.send_cmd = send_cmd_default_mock
|
||||
|
||||
def test_setup_should_fail_if_lightos_client_cant_auth_cluster(self):
|
||||
"""Verify lightos_client fail with bad auth."""
|
||||
|
||||
def side_effect(cmd, timeout):
|
||||
if cmd == "get_cluster_info":
|
||||
return (httpstatus.UNAUTHORIZED, None)
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
self.driver.cluster.send_cmd = side_effect
|
||||
self.assertRaises(exception.InvalidAuthKey,
|
||||
self.driver.do_setup, None)
|
||||
|
||||
def test_setup_should_succeed(self):
|
||||
"""Test that lightos_client succeed."""
|
||||
self.driver.do_setup(None)
|
||||
|
||||
def test_create_volume_should_succeed(self):
|
||||
"""Test that lightos_client succeed."""
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_create_volume_same_volume_twice_succeed(self):
|
||||
"""Test succeed to create an exiting volume."""
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_create_volume_in_failed_state(self):
|
||||
"""Verify scenario of created volume in failed state:
|
||||
|
||||
Driver is expected to issue a deletion command and raise exception
|
||||
"""
|
||||
def send_cmd_mock(cmd, **kwargs):
|
||||
if cmd == "create_volume":
|
||||
project_name = kwargs["project_name"]
|
||||
volume = {
|
||||
"project_name": project_name,
|
||||
"name": kwargs["name"],
|
||||
"size": kwargs["size"],
|
||||
"n_replicas": kwargs["n_replicas"],
|
||||
"compression": kwargs["compression"],
|
||||
"src_snapshot_name": kwargs["src_snapshot_name"],
|
||||
"acl": {'values': kwargs.get('acl')},
|
||||
"state": "Failed",
|
||||
}
|
||||
volume["ETag"] = get_vol_etag(volume)
|
||||
code, new_vol = self.db.create_volume(volume)
|
||||
return (code, new_vol)
|
||||
elif cmd == "delete_volume":
|
||||
return self.db.delete_volume(kwargs["project_name"],
|
||||
kwargs["volume_uuid"])
|
||||
elif cmd == "get_volume":
|
||||
return self.db.get_volume_by_uuid(kwargs["project_name"],
|
||||
kwargs["volume_uuid"])
|
||||
elif cmd == "get_volume_by_name":
|
||||
return self.db.get_volume_by_name(kwargs["project_name"],
|
||||
kwargs["volume_name"])
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"'{cmd}' is not implemented. kwargs: {kwargs}")
|
||||
|
||||
self.driver.do_setup(None)
|
||||
self.driver.cluster.send_cmd = send_cmd_mock
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.create_volume, volume)
|
||||
proj = self.db.data["projects"][lightos.LIGHTOS_DEFAULT_PROJECT_NAME]
|
||||
actual_volumes = proj["volumes"]
|
||||
self.assertEqual(0, len(actual_volumes))
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_delete_volume_fail_if_not_created(self):
|
||||
"""Test that lightos_client fail creating an already exists volume."""
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_extend_volume_should_succeed(self):
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.extend_volume(volume, 6)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_extend_volume_should_fail_if_volume_does_not_exist(self):
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
|
||||
self.assertRaises(exception.VolumeNotFound,
|
||||
self.driver.extend_volume, volume, 6)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
snapshot = test_utils.create_snapshot(self.ctxt, volume_id=volume.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.create_snapshot(snapshot)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
snapshot = test_utils.create_snapshot(self.ctxt, volume_id=volume.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.create_snapshot(snapshot)
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
snapshot = test_utils.create_snapshot(self.ctxt, volume_id=volume.id)
|
||||
self.driver.create_volume_from_snapshot(volume, snapshot)
|
||||
proj = self.db.data["projects"][lightos.LIGHTOS_DEFAULT_PROJECT_NAME]
|
||||
actual_volumes = proj["volumes"]
|
||||
self.assertEqual(1, len(actual_volumes))
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
db.snapshot_destroy(self.ctxt, snapshot.id)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
connection_props = \
|
||||
self.driver.initialize_connection(volume,
|
||||
get_connector_properties())
|
||||
self.assertIn('driver_volume_type', connection_props)
|
||||
self.assertEqual('lightos', connection_props['driver_volume_type'])
|
||||
self.assertEqual(FAKE_CLIENT_HOSTNQN,
|
||||
connection_props['data']['hostnqn'])
|
||||
self.assertEqual(FAKE_LIGHTOS_CLUSTER_INFO['subsystemNQN'],
|
||||
connection_props['data']['nqn'])
|
||||
self.assertEqual(
|
||||
self.db.data['projects']['default']['volumes'][0]['UUID'],
|
||||
connection_props['data']['uuid'])
|
||||
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_initialize_connection_no_hostnqn_should_fail(self):
|
||||
InitialConnectorMock.hostnqn = ""
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.initialize_connection, volume,
|
||||
get_connector_properties())
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_initialize_connection_no_dsc_should_fail(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = False
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.initialize_connection, volume,
|
||||
get_connector_properties())
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_terminate_connection_with_hostnqn(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.terminate_connection(volume, get_connector_properties())
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_terminate_connection_with_empty_hostnqn_should_fail(self):
|
||||
InitialConnectorMock.hostnqn = ""
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.terminate_connection, volume,
|
||||
get_connector_properties())
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_force_terminate_connection_with_empty_hostnqn(self):
|
||||
InitialConnectorMock.hostnqn = ""
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.terminate_connection(volume, get_connector_properties(),
|
||||
force=True)
|
||||
self.driver.delete_volume(volume)
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_check_for_setup_error_no_subsysnqn_should_fail(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
self.driver.cluster.subsystemNQN = ""
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
def test_check_for_setup_error_no_hostnqn_should_fail(self):
|
||||
InitialConnectorMock.hostnqn = ""
|
||||
InitialConnectorMock.found_discovery_client = True
|
||||
self.driver.do_setup(None)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
def test_check_for_setup_error_no_dsc_should_succeed(self):
|
||||
InitialConnectorMock.hostnqn = "hostnqn1"
|
||||
InitialConnectorMock.found_discovery_client = False
|
||||
self.driver.do_setup(None)
|
||||
self.driver.check_for_setup_error()
|
||||
|
||||
def test_create_clone(self):
|
||||
self.driver.do_setup(None)
|
||||
|
||||
vol_type = test_utils.create_volume_type(self.ctxt, self,
|
||||
name='my_vol_type')
|
||||
volume = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
clone = test_utils.create_volume(self.ctxt, size=4,
|
||||
volume_type_id=vol_type.id)
|
||||
|
||||
self.driver.create_volume(volume)
|
||||
self.driver.create_cloned_volume(clone, volume)
|
||||
self.driver.delete_volume(volume)
|
||||
self.driver.delete_volume(clone)
|
||||
|
||||
db.volume_destroy(self.ctxt, volume.id)
|
||||
db.volume_destroy(self.ctxt, clone.id)
|
||||
|
||||
def test_get_volume_stats(self):
|
||||
"""Test that lightos_client succeed."""
|
||||
self.driver.do_setup(None)
|
||||
volumes_data = self.driver.get_volume_stats(refresh=False)
|
||||
assert len(volumes_data) == 0, "Expected empty config"
|
||||
|
||||
volumes_data = self.driver.get_volume_stats(refresh=True)
|
||||
assert volumes_data['vendor_name'] == 'LightOS Storage', \
|
||||
"Expected 'LightOS Storage', received %s" % \
|
||||
volumes_data['vendor_name']
|
||||
assert volumes_data['volume_backend_name'] == VOLUME_BACKEND_NAME, \
|
||||
"Expected %s, received %s" % \
|
||||
(VOLUME_BACKEND_NAME, volumes_data['volume_backend_name'])
|
||||
assert volumes_data['driver_version'] == self.driver.VERSION, \
|
||||
"Expected %s, received %s" % \
|
||||
(self.driver.VERSION, volumes_data['driver_version'])
|
||||
assert volumes_data['storage_protocol'] == "lightos", \
|
||||
"Expected 'lightos', received %s" % \
|
||||
volumes_data['storage_protocol']
|
||||
assert volumes_data['reserved_percentage'] == RESERVED_PERCENTAGE, \
|
||||
"Expected %d, received %s" % \
|
||||
(RESERVED_PERCENTAGE, volumes_data['reserved_percentage'])
|
||||
assert volumes_data['QoS_support'] is False, \
|
||||
"Expected False, received %s" % volumes_data['QoS_support']
|
||||
assert volumes_data['online_extend_support'] is True, \
|
||||
"Expected True, received %s" % \
|
||||
volumes_data['online_extend_support']
|
||||
assert volumes_data['thin_provisioning_support'] is True, \
|
||||
"Expected True, received %s" % \
|
||||
volumes_data['thin_provisioning_support']
|
||||
assert volumes_data['compression'] is False, \
|
||||
"Expected False, received %s" % volumes_data['compression']
|
||||
assert volumes_data['multiattach'] is True, \
|
||||
"Expected True, received %s" % volumes_data['multiattach']
|
||||
assert volumes_data['free_capacity_gb'] == 'infinite', \
|
||||
"Expected 'infinite', received %s" % \
|
||||
volumes_data['free_capacity_gb']
|
1481
cinder/volume/drivers/lightos.py
Normal file
1481
cinder/volume/drivers/lightos.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,140 @@
|
||||
===============================
|
||||
Lightbits LightOS Cinder Driver
|
||||
===============================
|
||||
|
||||
The Lightbits(TM) LightOS(R) OpenStack driver enables OpenStack
|
||||
clusters to use LightOS clustered storage servers. This documentation
|
||||
explains how to configure Cinder for use with the Lightbits LightOS
|
||||
storage backend system.
|
||||
|
||||
Supported operations
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Create volume
|
||||
- Delete volume
|
||||
- Attach volume
|
||||
- Detach volume
|
||||
- Create image from volume
|
||||
- Live migration
|
||||
- Volume replication
|
||||
- Thin provisioning
|
||||
- Multi-attach
|
||||
- Supported vendor driver
|
||||
- Extend volume
|
||||
- Create snapshot
|
||||
- Delete snapshot
|
||||
- Create volume from snapshot
|
||||
- Create volume from volume (clone)
|
||||
|
||||
LightOS OpenStack Driver Components
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The LightOS OpenStack driver has three components:
|
||||
- Cinder driver
|
||||
- Nova libvirt volume driver
|
||||
- os_brick initiator connector
|
||||
|
||||
In addition, it requires the LightOS ``discovery-client``, provided
|
||||
with LightOS. The os_brick connector uses the LightOS
|
||||
``discovery-client`` to communicate with LightOS NVMe/TCP discovery
|
||||
services.
|
||||
|
||||
The Cinder Driver
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Cinder driver integrates with Cinder and performs REST operations
|
||||
against the LightOS cluster. To enable the driver, add the following
|
||||
to Cinder's configuration file
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
enabled_backends = lightos,<any other storage backend you use>
|
||||
|
||||
and
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[lightos]
|
||||
volume_driver = cinder.volume.drivers.lightos.LightOSVolumeDriver
|
||||
volume_backend_name = lightos
|
||||
lightos_api_address = <TARGET_ACCESS_IPS>
|
||||
lightos_api_port = 443
|
||||
lightos_jwt=<LIGHTOS_JWT>
|
||||
lightos_default_num_replicas = 3
|
||||
lightos_default_compression_enabled = False
|
||||
lightos_api_service_timeout=30
|
||||
|
||||
- ``TARGET_ACCESS_IPS`` are the LightOS cluster nodes access
|
||||
IPs. Multiple nodes should be separated by commas. For example:
|
||||
``lightos_api_address =
|
||||
192.168.67.78,192.168.34.56,192.168.12.17``. These IPs are where the
|
||||
driver looks for the LightOS clusters REST API servers.
|
||||
- ``LIGHTOS_JWT`` is the JWT (JSON Web Token) that is located at the
|
||||
LightOS installation controller. You can find the jwt at
|
||||
``~/lightos-default-admin-jwt``.
|
||||
- The default number of replicas for volumes is 3, and valid values
|
||||
for ``lightos_default_num_replicas`` are 1, 2, or 3.
|
||||
- The default compression setting is False (i.e., data is
|
||||
uncompressed). The default compression setting can also be
|
||||
True. This can also be changed on a per-volume basis.
|
||||
- The default time to wait for API service response is 30 seconds per
|
||||
API endpoint.
|
||||
|
||||
Creating volumes with non-default compression and number of replicas
|
||||
settings can be done through the volume types mechanism. To create a
|
||||
new volume type with compression enabled:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume type create --property compression='<is> True' volume-with-compression
|
||||
|
||||
To create a new volume type with one replica:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume type create --property lightos:num_replicas=1 volume-with-one-replica
|
||||
|
||||
To create a new type for a compressed volume with three replicas:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume type create --property compression='<is> True' --property lightos:num_replicas=3 volume-with-three-replicas-and-compression
|
||||
|
||||
Then create a new volume with one of these volume types:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume create --size <size> --type <type name> <vol name>
|
||||
|
||||
NVNe/TCP and Asymmetric Namespace Access (ANA)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The LightOS clusters expose their volumes using NVMe/TCP Asynchronous
|
||||
Namespace Access (ANA). ANA is a relatively new feature in the
|
||||
NVMe/TCP stack in Linux but it is fully supported in Ubuntu
|
||||
20.04. Each compute host in the OpenStack cluster needs to be
|
||||
ANA-capable to provide OpenStack VMs with LightOS volumes over
|
||||
NVMe/TCP. For more information on how to set up the compute nodes to
|
||||
use ANA, see the CentOS Linux Cluster Client Software Installation
|
||||
section of the Lightbits(TM) LightOS(R) Cluster Installation and
|
||||
Initial Configuration Guide.
|
||||
|
||||
Note
|
||||
~~~~
|
||||
|
||||
In the current version, if any of the cluster nodes changes its access
|
||||
IPs, the Cinder driver's configuration file should be updated with the
|
||||
cluster nodes access IPs and restarted. As long as the Cinder driver
|
||||
can access at least one cluster access IP it will work, but will be
|
||||
susceptible to cluster node failures.
|
||||
|
||||
Driver options
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The following table contains the configuration options supported by the
|
||||
Lightbits LightOS Cinder driver.
|
||||
|
||||
.. config-table::
|
||||
:config-target: Lightbits LightOS
|
||||
|
||||
cinder.volume.drivers.lightos
|
@ -123,6 +123,9 @@ title=Kioxia Kumoscale Driver (NVMeOF)
|
||||
[driver.lenovo]
|
||||
title=Lenovo Storage Driver (FC, iSCSI)
|
||||
|
||||
[driver.lightbits_lightos]
|
||||
title=Lightbits LightOS Storage Driver (NVMeTCP)
|
||||
|
||||
[driver.linbit_linstor]
|
||||
title=LINBIT DRBD/LINSTOR Driver (DRBD)
|
||||
|
||||
@ -256,6 +259,7 @@ driver.inspur_as13000=complete
|
||||
driver.kaminario=complete
|
||||
driver.kioxia_kumoscale=complete
|
||||
driver.lenovo=complete
|
||||
driver.lightbits_lightos=complete
|
||||
driver.linbit_linstor=complete
|
||||
driver.lvm=complete
|
||||
driver.macrosan=complete
|
||||
@ -327,6 +331,7 @@ driver.inspur_as13000=complete
|
||||
driver.kaminario=complete
|
||||
driver.kioxia_kumoscale=complete
|
||||
driver.lenovo=complete
|
||||
driver.lightbits_lightos=complete
|
||||
driver.linbit_linstor=complete
|
||||
driver.lvm=complete
|
||||
driver.macrosan=complete
|
||||
@ -401,6 +406,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=missing
|
||||
driver.macrosan=complete
|
||||
@ -474,6 +480,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=complete
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=missing
|
||||
driver.macrosan=complete
|
||||
@ -548,6 +555,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=missing
|
||||
driver.macrosan=missing
|
||||
@ -621,6 +629,7 @@ driver.inspur_as13000=complete
|
||||
driver.kaminario=complete
|
||||
driver.kioxia_kumoscale=complete
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=complete
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=complete
|
||||
driver.macrosan=complete
|
||||
@ -695,6 +704,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=missing
|
||||
driver.macrosan=complete
|
||||
@ -769,6 +779,7 @@ driver.inspur_as13000=complete
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=complete
|
||||
driver.lightbits_lightos=complete
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=complete
|
||||
driver.macrosan=missing
|
||||
@ -840,6 +851,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=complete
|
||||
driver.macrosan=missing
|
||||
@ -915,6 +927,7 @@ driver.inspur_as13000=missing
|
||||
driver.kaminario=missing
|
||||
driver.kioxia_kumoscale=missing
|
||||
driver.lenovo=missing
|
||||
driver.lightbits_lightos=missing
|
||||
driver.linbit_linstor=missing
|
||||
driver.lvm=missing
|
||||
driver.macrosan=complete
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Lightbits LightOS driver: new Cinder driver for Lightbits(TM)
|
||||
LightOS(R). Lightbits Labs (http://www.lightbitslabs.com) LightOS
|
||||
is software-defined, cloud native, high-performance, clustered
|
||||
scale-out and redundant NVMe/TCP storage that performs like local
|
||||
NVMe flash.
|
Loading…
Reference in New Issue
Block a user