Provide Kaminario K2 all-flash array iSCSI driver
The purpose is to provide cinder driver for Kaminario K2 all-flash iSCSI array. It will include the minimum set of features required by the Newton(N) release. DocImpact Implements: blueprint kaminario-iscsi-cinder-driver Co-Authored-By: Lakshmi Narayana<Lakshmi.Narayana.ctr@kaminario.com> Co-Authored-By: Sreedhar Varma<Sreedhar.Varma.ctr@kaminario.com> Co-Authored-By: Ido Benda<Ido.Benda@kaminario.com> Change-Id: Iad05e85ae512b0a97da5420cb66a08e23aa457db
This commit is contained in:
parent
3a3aa5cb18
commit
7f875697e0
@ -1146,3 +1146,8 @@ class GCSApiFailure(BackupDriverException):
|
||||
|
||||
class GCSOAuth2Failure(BackupDriverException):
|
||||
message = _("Google Cloud Storage oauth2 failure: %(reason)s")
|
||||
|
||||
|
||||
# Kaminario K2
|
||||
class KaminarioCinderDriverException(VolumeDriverException):
|
||||
message = _("KaminarioCinderDriver failure: %(reason)s")
|
||||
|
@ -126,6 +126,8 @@ from cinder.volume.drivers.ibm import xiv_ds8k as \
|
||||
cinder_volume_drivers_ibm_xivds8k
|
||||
from cinder.volume.drivers.infortrend.eonstor_ds_cli import common_cli as \
|
||||
cinder_volume_drivers_infortrend_eonstor_ds_cli_commoncli
|
||||
from cinder.volume.drivers.kaminario import kaminario_common as \
|
||||
cinder_volume_drivers_kaminario_kaminariocommon
|
||||
from cinder.volume.drivers.lenovo import lenovo_common as \
|
||||
cinder_volume_drivers_lenovo_lenovocommon
|
||||
from cinder.volume.drivers import lvm as cinder_volume_drivers_lvm
|
||||
@ -310,6 +312,8 @@ def list_opts():
|
||||
[cinder_scheduler_scheduleroptions.
|
||||
scheduler_json_config_location_opt],
|
||||
cinder_volume_drivers_zfssa_zfssanfs.ZFSSA_OPTS,
|
||||
cinder_volume_drivers_kaminario_kaminariocommon.
|
||||
kaminario1_opts,
|
||||
cinder_volume_drivers_disco_disco.disco_opts,
|
||||
cinder_volume_drivers_hgst.hgst_opts,
|
||||
cinder_message_api.messages_opts,
|
||||
|
235
cinder/tests/unit/volume/drivers/test_kaminario.py
Normal file
235
cinder/tests/unit/volume/drivers/test_kaminario.py
Normal file
@ -0,0 +1,235 @@
|
||||
# Copyright (c) 2016 by Kaminario Technologies, 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.
|
||||
"""Unit tests for kaminario driver."""
|
||||
import mock
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.tests.unit import fake_snapshot
|
||||
from cinder.tests.unit import fake_volume
|
||||
from cinder import utils
|
||||
from cinder.volume import configuration
|
||||
from cinder.volume.drivers.kaminario import kaminario_iscsi
|
||||
from cinder.volume import utils as vol_utils
|
||||
|
||||
|
||||
CONNECTOR = {'initiator': 'iqn.1993-08.org.debian:01:84cd7e88bb5a',
|
||||
'ip': '192.168.6.5', 'platform': 'x86_64', 'host': 'il-ksm1-055',
|
||||
'os_type': 'linux2', 'multipath': False}
|
||||
|
||||
|
||||
class FakeK2Obj(object):
|
||||
id = 548
|
||||
lun = 548
|
||||
|
||||
|
||||
class FakeSaveObject(FakeK2Obj):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.ntype = kwargs.get('ntype')
|
||||
self.ip_address = '10.0.0.1'
|
||||
self.iscsi_qualified_target_name = "xyztlnxyz"
|
||||
self.snapshot = FakeK2Obj()
|
||||
self.name = 'test'
|
||||
|
||||
def save(self):
|
||||
return FakeSaveObject()
|
||||
|
||||
def delete(self):
|
||||
return None
|
||||
|
||||
|
||||
class FakeSaveObjectExp(FakeSaveObject):
|
||||
def save(self):
|
||||
raise exception.KaminarioCinderDriverException("test")
|
||||
|
||||
def delete(self):
|
||||
raise exception.KaminarioCinderDriverException("test")
|
||||
|
||||
|
||||
class FakeSearchObject(object):
|
||||
hits = [FakeSaveObject()]
|
||||
total = 1
|
||||
|
||||
def __init__(self, *args):
|
||||
if args and "mappings" in args[0]:
|
||||
self.total = 0
|
||||
|
||||
|
||||
class FakeSearchObjectExp(object):
|
||||
hits = [FakeSaveObjectExp()]
|
||||
total = 1
|
||||
|
||||
|
||||
class FakeKrest(object):
|
||||
def search(self, *args, **argv):
|
||||
return FakeSearchObject(*args)
|
||||
|
||||
def new(self, *args, **argv):
|
||||
return FakeSaveObject()
|
||||
|
||||
|
||||
class FakeKrestException(object):
|
||||
def search(self, *args, **argv):
|
||||
return FakeSearchObjectExp()
|
||||
|
||||
def new(self, *args, **argv):
|
||||
return FakeSaveObjectExp()
|
||||
|
||||
|
||||
class TestKaminarioISCSI(test.TestCase):
|
||||
driver = None
|
||||
conf = None
|
||||
|
||||
def setUp(self):
|
||||
self._setup_config()
|
||||
self._setup_driver()
|
||||
super(TestKaminarioISCSI, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.vol = fake_volume.fake_volume_obj(self.context)
|
||||
self.vol.volume_type = fake_volume.fake_volume_type_obj(self.context)
|
||||
self.snap = fake_snapshot.fake_snapshot_obj(self.context)
|
||||
self.snap.volume = self.vol
|
||||
|
||||
def _setup_config(self):
|
||||
self.conf = mock.Mock(spec=configuration.Configuration)
|
||||
self.conf.kaminario_dedup_type_name = "dedup"
|
||||
self.conf.volume_dd_blocksize = 2
|
||||
|
||||
def _setup_driver(self):
|
||||
self.driver = (kaminario_iscsi.
|
||||
KaminarioISCSIDriver(configuration=self.conf))
|
||||
device = mock.Mock(return_value={'device': {'path': '/dev'}})
|
||||
self.driver._connect_device = device
|
||||
self.driver._protocol = False
|
||||
self.driver.client = FakeKrest()
|
||||
|
||||
def test_create_volume(self):
|
||||
"""Test create_volume."""
|
||||
result = self.driver.create_volume(self.vol)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_create_volume_with_exception(self):
|
||||
"""Test create_volume_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.create_volume, self.vol)
|
||||
|
||||
def test_delete_volume(self):
|
||||
"""Test delete_volume."""
|
||||
result = self.driver.delete_volume(self.vol)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_delete_volume_with_exception(self):
|
||||
"""Test delete_volume_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.delete_volume, self.vol)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
"""Test create_snapshot."""
|
||||
result = self.driver.create_snapshot(self.snap)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_create_snapshot_with_exception(self):
|
||||
"""Test create_snapshot_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.create_snapshot, self.snap)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
"""Test delete_snapshot."""
|
||||
result = self.driver.delete_snapshot(self.snap)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_delete_snapshot_with_exception(self):
|
||||
"""Test delete_snapshot_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.delete_snapshot, self.snap)
|
||||
|
||||
@mock.patch.object(utils, 'brick_get_connector_properties')
|
||||
@mock.patch.object(vol_utils, 'copy_volume')
|
||||
def test_create_volume_from_snapshot(self, mock_copy_volume,
|
||||
mock_brick_get):
|
||||
"""Test create_volume_from_snapshot."""
|
||||
mock_brick_get.return_value = CONNECTOR
|
||||
mock_copy_volume.return_value = None
|
||||
result = self.driver.create_volume_from_snapshot(self.vol, self.snap)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch.object(utils, 'brick_get_connector_properties')
|
||||
@mock.patch.object(vol_utils, 'copy_volume')
|
||||
def test_create_volume_from_snapshot_with_exception(self, mock_copy_volume,
|
||||
mock_brick_get):
|
||||
"""Test create_volume_from_snapshot_with_exception."""
|
||||
mock_brick_get.return_value = CONNECTOR
|
||||
mock_copy_volume.return_value = None
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.create_volume_from_snapshot, self.vol,
|
||||
self.snap)
|
||||
|
||||
@mock.patch.object(utils, 'brick_get_connector_properties')
|
||||
@mock.patch.object(vol_utils, 'copy_volume')
|
||||
def test_create_cloned_volume(self, mock_copy_volume, mock_brick_get):
|
||||
"""Test create_cloned_volume."""
|
||||
mock_brick_get.return_value = CONNECTOR
|
||||
mock_copy_volume.return_value = None
|
||||
result = self.driver.create_cloned_volume(self.vol, self.vol)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch.object(utils, 'brick_get_connector_properties')
|
||||
@mock.patch.object(vol_utils, 'copy_volume')
|
||||
def test_create_cloned_volume_with_exception(self, mock_copy_volume,
|
||||
mock_brick_get):
|
||||
"""Test create_cloned_volume_with_exception."""
|
||||
mock_brick_get.return_value = CONNECTOR
|
||||
mock_copy_volume.return_value = None
|
||||
self.driver.terminate_connection = mock.Mock()
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.create_cloned_volume, self.vol, self.vol)
|
||||
|
||||
def test_extend_volume(self):
|
||||
"""Test extend_volume."""
|
||||
new_size = 256
|
||||
result = self.driver.extend_volume(self.vol, new_size)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_extend_volume_with_exception(self):
|
||||
"""Test extend_volume_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
new_size = 256
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.extend_volume, self.vol, new_size)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
"""Test initialize_connection."""
|
||||
conn_info = self.driver.initialize_connection(self.vol, CONNECTOR)
|
||||
self.assertIn('data', conn_info)
|
||||
self.assertIn('target_iqn', conn_info['data'])
|
||||
|
||||
def test_initialize_connection_with_exception(self):
|
||||
"""Test initialize_connection_with_exception."""
|
||||
self.driver.client = FakeKrestException()
|
||||
self.assertRaises(exception.KaminarioCinderDriverException,
|
||||
self.driver.initialize_connection, self.vol,
|
||||
CONNECTOR)
|
||||
|
||||
def test_terminate_connection(self):
|
||||
"""Test terminate_connection."""
|
||||
result = self.driver.terminate_connection(self.vol, CONNECTOR)
|
||||
self.assertIsNone(result)
|
0
cinder/volume/drivers/kaminario/__init__.py
Normal file
0
cinder/volume/drivers/kaminario/__init__.py
Normal file
433
cinder/volume/drivers/kaminario/kaminario_common.py
Normal file
433
cinder/volume/drivers/kaminario/kaminario_common.py
Normal file
@ -0,0 +1,433 @@
|
||||
# Copyright (c) 2016 by Kaminario Technologies, 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.
|
||||
"""Volume driver for Kaminario K2 all-flash arrays."""
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import units
|
||||
from oslo_utils import versionutils
|
||||
|
||||
import cinder
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LE
|
||||
from cinder import utils
|
||||
from cinder.volume.drivers.san import san
|
||||
from cinder.volume import utils as vol_utils
|
||||
|
||||
K2_MIN_VERSION = '2.2.0'
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
kaminario1_opts = [
|
||||
cfg.StrOpt('kaminario_nodedup_substring',
|
||||
default='K2-nodedup',
|
||||
help="If volume-type name contains this substring "
|
||||
"nodedup volume will be created, otherwise "
|
||||
"dedup volume wil be created.")]
|
||||
kaminario2_opts = [
|
||||
cfg.BoolOpt('auto_calc_max_oversubscription_ratio',
|
||||
default=False,
|
||||
help="K2 driver will calculate max_oversubscription_ratio "
|
||||
"on setting this option as True.")]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(kaminario1_opts)
|
||||
|
||||
|
||||
def kaminario_logger(func):
|
||||
"""Return a function wrapper.
|
||||
|
||||
The wrapper adds log for entry and exit to the function.
|
||||
"""
|
||||
def func_wrapper(*args, **kwargs):
|
||||
LOG.debug('Entering %(function)s of %(class)s with arguments: '
|
||||
' %(args)s, %(kwargs)s',
|
||||
{'class': args[0].__class__.__name__,
|
||||
'function': func.__name__,
|
||||
'args': args[1:],
|
||||
'kwargs': kwargs})
|
||||
ret = func(*args, **kwargs)
|
||||
LOG.debug('Exiting %(function)s of %(class)s '
|
||||
'having return value: %(ret)s',
|
||||
{'class': args[0].__class__.__name__,
|
||||
'function': func.__name__,
|
||||
'ret': ret})
|
||||
return ret
|
||||
return func_wrapper
|
||||
|
||||
|
||||
class KaminarioCinderDriver(cinder.volume.driver.ISCSIDriver):
|
||||
VENDOR = "Kaminario"
|
||||
VERSION = "1.0"
|
||||
stats = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KaminarioCinderDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
self.configuration.append_config_values(kaminario2_opts)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
if self.krest is None:
|
||||
msg = _("Unable to import 'krest' python module.")
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
else:
|
||||
conf = self.configuration
|
||||
self.client = self.krest.EndPoint(conf.san_ip,
|
||||
conf.san_login,
|
||||
conf.san_password,
|
||||
ssl_validate=False)
|
||||
v_rs = self.client.search("system/state")
|
||||
if hasattr(v_rs, 'hits') and v_rs.total != 0:
|
||||
ver = v_rs.hits[0].rest_api_version
|
||||
ver_exist = versionutils.convert_version_to_int(ver)
|
||||
ver_min = versionutils.convert_version_to_int(K2_MIN_VERSION)
|
||||
if ver_exist < ver_min:
|
||||
msg = _("K2 rest api version should be "
|
||||
">= %s.") % K2_MIN_VERSION
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
|
||||
else:
|
||||
msg = _("K2 rest api version search failed.")
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
|
||||
@kaminario_logger
|
||||
def _check_ops(self):
|
||||
"""Ensure that the options we care about are set."""
|
||||
required_ops = ['san_ip', 'san_login', 'san_password']
|
||||
for attr in required_ops:
|
||||
if not getattr(self.configuration, attr, None):
|
||||
raise exception.InvalidInput(reason=_('%s is not set.') % attr)
|
||||
|
||||
@kaminario_logger
|
||||
def do_setup(self, context):
|
||||
super(KaminarioCinderDriver, self).do_setup(context)
|
||||
self._check_ops()
|
||||
self.krest = importutils.try_import("krest")
|
||||
|
||||
@kaminario_logger
|
||||
def create_volume(self, volume):
|
||||
"""Volume creation in K2 needs a volume group.
|
||||
|
||||
- create a volume group
|
||||
- create a volume in the volume group
|
||||
"""
|
||||
vg_name = self.get_volume_group_name(volume.id)
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
if CONF.kaminario_nodedup_substring in volume.volume_type.name:
|
||||
prov_type = False
|
||||
else:
|
||||
prov_type = True
|
||||
try:
|
||||
LOG.debug("Creating volume group with name: %(name)s, "
|
||||
"quota: unlimited and dedup_support: %(dedup)s",
|
||||
{'name': vg_name, 'dedup': prov_type})
|
||||
|
||||
vg = self.client.new("volume_groups", name=vg_name, quota=0,
|
||||
is_dedup=prov_type).save()
|
||||
LOG.debug("Creating volume with name: %(name)s, size: %(size)s "
|
||||
"GB, volume_group: %(vg)s",
|
||||
{'name': vol_name, 'size': volume.size, 'vg': vg_name})
|
||||
self.client.new("volumes", name=vol_name,
|
||||
size=volume.size * units.Mi,
|
||||
volume_group=vg).save()
|
||||
except Exception as ex:
|
||||
vg_rs = self.client.search("volume_groups", name=vg_name)
|
||||
if vg_rs.total != 0:
|
||||
LOG.debug("Deleting vg: %s for failed volume in K2.", vg_name)
|
||||
vg_rs.hits[0].delete()
|
||||
LOG.exception(_LE("Creation of volume %s failed."), vol_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Create volume from snapshot.
|
||||
|
||||
- search for snapshot and retention_policy
|
||||
- create a view from snapshot and attach view
|
||||
- create a volume and attach volume
|
||||
- copy data from attached view to attached volume
|
||||
- detach volume and view and finally delete view
|
||||
"""
|
||||
snap_name = self.get_snap_name(snapshot.id)
|
||||
view_name = self.get_view_name(volume.id)
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
cview = src_attach_info = dest_attach_info = None
|
||||
rpolicy = self.get_policy()
|
||||
properties = utils.brick_get_connector_properties()
|
||||
LOG.debug("Searching for snapshot: %s in K2.", snap_name)
|
||||
snap_rs = self.client.search("snapshots", short_name=snap_name)
|
||||
if hasattr(snap_rs, 'hits') and snap_rs.total != 0:
|
||||
snap = snap_rs.hits[0]
|
||||
LOG.debug("Creating a view: %(view)s from snapshot: %(snap)s",
|
||||
{'view': view_name, 'snap': snap_name})
|
||||
try:
|
||||
cview = self.client.new("snapshots",
|
||||
short_name=view_name,
|
||||
source=snap, retention_policy=rpolicy,
|
||||
is_exposable=True).save()
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Creating a view: %(view)s from snapshot: "
|
||||
"%(snap)s failed"), {"view": view_name,
|
||||
"snap": snap_name})
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
else:
|
||||
msg = _("Snapshot: %s search failed in K2.") % snap_name
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
|
||||
try:
|
||||
conn = self.initialize_connection(cview, properties)
|
||||
src_attach_info = self._connect_device(conn)
|
||||
self.create_volume(volume)
|
||||
conn = self.initialize_connection(volume, properties)
|
||||
dest_attach_info = self._connect_device(conn)
|
||||
vol_utils.copy_volume(src_attach_info['device']['path'],
|
||||
dest_attach_info['device']['path'],
|
||||
snapshot.volume.size * units.Ki,
|
||||
self.configuration.volume_dd_blocksize,
|
||||
sparse=True)
|
||||
self.terminate_connection(volume, properties)
|
||||
self.terminate_connection(cview, properties)
|
||||
except Exception as ex:
|
||||
self.terminate_connection(cview, properties)
|
||||
self.terminate_connection(volume, properties)
|
||||
cview.delete()
|
||||
self.delete_volume(volume)
|
||||
LOG.exception(_LE("Copy to volume: %(vol)s from view: %(view)s "
|
||||
"failed"), {"vol": vol_name, "view": view_name})
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Create a clone from source volume.
|
||||
|
||||
- attach source volume
|
||||
- create and attach new volume
|
||||
- copy data from attached source volume to attached new volume
|
||||
- detach both volumes
|
||||
"""
|
||||
clone_name = self.get_volume_name(volume.id)
|
||||
src_name = self.get_volume_name(src_vref.id)
|
||||
src_vol = self.client.search("volumes", name=src_name)
|
||||
src_map = self.client.search("mappings", volume=src_vol)
|
||||
if src_map.total != 0:
|
||||
msg = _("K2 driver does not support clone of a attached volume. "
|
||||
"To get this done, create a snapshot from the attached "
|
||||
"volume and then create a volume from the snapshot.")
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
try:
|
||||
properties = utils.brick_get_connector_properties()
|
||||
conn = self.initialize_connection(src_vref, properties)
|
||||
src_attach_info = self._connect_device(conn)
|
||||
self.create_volume(volume)
|
||||
conn = self.initialize_connection(volume, properties)
|
||||
dest_attach_info = self._connect_device(conn)
|
||||
vol_utils.copy_volume(src_attach_info['device']['path'],
|
||||
dest_attach_info['device']['path'],
|
||||
src_vref.size * units.Ki,
|
||||
self.configuration.volume_dd_blocksize,
|
||||
sparse=True)
|
||||
|
||||
self.terminate_connection(volume, properties)
|
||||
self.terminate_connection(src_vref, properties)
|
||||
except Exception as ex:
|
||||
self.terminate_connection(src_vref, properties)
|
||||
self.terminate_connection(volume, properties)
|
||||
self.delete_volume(volume)
|
||||
LOG.exception(_LE("Create a clone: %s failed."), clone_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def delete_volume(self, volume):
|
||||
"""Volume in K2 exists in a volume group.
|
||||
|
||||
- delete the volume
|
||||
- delete the corresponding volume group
|
||||
"""
|
||||
vg_name = self.get_volume_group_name(volume.id)
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
try:
|
||||
LOG.debug("Searching and deleting volume: %s in K2.", vol_name)
|
||||
vol_rs = self.client.search("volumes", name=vol_name)
|
||||
if vol_rs.total != 0:
|
||||
vol_rs.hits[0].delete()
|
||||
LOG.debug("Searching and deleting vg: %s in K2.", vg_name)
|
||||
vg_rs = self.client.search("volume_groups", name=vg_name)
|
||||
if vg_rs.total != 0:
|
||||
vg_rs.hits[0].delete()
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Deletion of volume %s failed."), vol_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def get_volume_stats(self, refresh=False):
|
||||
if refresh:
|
||||
self.update_volume_stats()
|
||||
stats = self.stats
|
||||
stats['storage_protocol'] = self._protocol
|
||||
stats['driver_version'] = self.VERSION
|
||||
stats['vendor_name'] = self.VENDOR
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
stats['volume_backend_name'] = (backend_name or
|
||||
self.__class__.__name__)
|
||||
return stats
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
@kaminario_logger
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Create a snapshot from a volume_group."""
|
||||
vg_name = self.get_volume_group_name(snapshot.volume_id)
|
||||
snap_name = self.get_snap_name(snapshot.id)
|
||||
rpolicy = self.get_policy()
|
||||
try:
|
||||
LOG.debug("Searching volume_group: %s in K2.", vg_name)
|
||||
vg = self.client.search("volume_groups", name=vg_name).hits[0]
|
||||
LOG.debug("Creating a snapshot: %(snap)s from vg: %(vg)s",
|
||||
{'snap': snap_name, 'vg': vg_name})
|
||||
self.client.new("snapshots", short_name=snap_name,
|
||||
source=vg, retention_policy=rpolicy).save()
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Creation of snapshot: %s failed."), snap_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Delete a snapshot."""
|
||||
snap_name = self.get_snap_name(snapshot.id)
|
||||
try:
|
||||
LOG.debug("Searching and deleting snapshot: %s in K2.", snap_name)
|
||||
snap_rs = self.client.search("snapshots", short_name=snap_name)
|
||||
if snap_rs.total != 0:
|
||||
snap_rs.hits[0].delete()
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Deletion of snapshot: %s failed."), snap_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend volume."""
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
try:
|
||||
LOG.debug("Searching volume: %s in K2.", vol_name)
|
||||
vol = self.client.search("volumes", name=vol_name).hits[0]
|
||||
vol.size = new_size * units.Mi
|
||||
LOG.debug("Extending volume: %s in K2.", vol_name)
|
||||
vol.save()
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Extending volume: %s failed."), vol_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
@kaminario_logger
|
||||
def update_volume_stats(self):
|
||||
conf = self.configuration
|
||||
LOG.debug("Searching system capacity in K2.")
|
||||
cap = self.client.search("system/capacity").hits[0]
|
||||
LOG.debug("Searching total volumes in K2 for updating stats.")
|
||||
total_volumes = self.client.search("volumes").total - 1
|
||||
provisioned_vol = cap.provisioned_volumes
|
||||
if (conf.auto_calc_max_oversubscription_ratio and cap.provisioned
|
||||
and (cap.total - cap.free) != 0):
|
||||
ratio = provisioned_vol / float(cap.total - cap.free)
|
||||
else:
|
||||
ratio = conf.max_over_subscription_ratio
|
||||
self.stats = {'QoS_support': False,
|
||||
'free_capacity_gb': cap.free / units.Mi,
|
||||
'total_capacity_gb': cap.total / units.Mi,
|
||||
'thin_provisioning_support': True,
|
||||
'sparse_copy_volume': True,
|
||||
'total_volumes': total_volumes,
|
||||
'thick_provisioning_support': False,
|
||||
'provisioned_capacity_gb': provisioned_vol / units.Mi,
|
||||
'max_oversubscription_ratio': ratio}
|
||||
|
||||
@kaminario_logger
|
||||
def get_initiator_host_name(self, connector):
|
||||
"""Return the initiator host name.
|
||||
|
||||
Valid characters: 0-9, a-z, A-Z, '-', '_'
|
||||
All other characters are replaced with '_'.
|
||||
Total characters in initiator host name: 32
|
||||
"""
|
||||
return re.sub('[^0-9a-zA-Z-_]', '_', connector['host'])[:32]
|
||||
|
||||
@kaminario_logger
|
||||
def get_volume_group_name(self, vid):
|
||||
"""Return the volume group name."""
|
||||
return "cvg-{0}".format(vid)
|
||||
|
||||
@kaminario_logger
|
||||
def get_volume_name(self, vid):
|
||||
"""Return the volume name."""
|
||||
return "cv-{0}".format(vid)
|
||||
|
||||
@kaminario_logger
|
||||
def get_snap_name(self, sid):
|
||||
"""Return the snapshot name."""
|
||||
return "cs-{0}".format(sid)
|
||||
|
||||
@kaminario_logger
|
||||
def get_view_name(self, vid):
|
||||
"""Return the view name."""
|
||||
return "cview-{0}".format(vid)
|
||||
|
||||
@kaminario_logger
|
||||
def delete_host_by_name(self, name):
|
||||
"""Deleting host by name."""
|
||||
host_rs = self.client.search("hosts", name=name)
|
||||
if hasattr(host_rs, "hits") and host_rs.total != 0:
|
||||
host = host_rs.hits[0]
|
||||
host.delete()
|
||||
|
||||
@kaminario_logger
|
||||
def get_policy(self):
|
||||
"""Return the retention policy."""
|
||||
try:
|
||||
LOG.debug("Searching for retention_policy in K2.")
|
||||
return self.client.search("retention_policies",
|
||||
name="Best_Effort_Retention").hits[0]
|
||||
except Exception as ex:
|
||||
LOG.exception(_LE("Retention policy search failed in K2."))
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
pass
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
pass
|
153
cinder/volume/drivers/kaminario/kaminario_iscsi.py
Normal file
153
cinder/volume/drivers/kaminario/kaminario_iscsi.py
Normal file
@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2016 by Kaminario Technologies, 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.
|
||||
"""Volume driver for Kaminario K2 all-flash arrays."""
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LW
|
||||
from cinder import interface
|
||||
from cinder.volume.drivers.kaminario import kaminario_common as common
|
||||
from oslo_log import log as logging
|
||||
|
||||
ISCSI_TCP_PORT = "3260"
|
||||
LOG = logging.getLogger(__name__)
|
||||
kaminario_logger = common.kaminario_logger
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class KaminarioISCSIDriver(common.KaminarioCinderDriver):
|
||||
"""Kaminario K2 iSCSI Volume Driver."""
|
||||
|
||||
@kaminario_logger
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KaminarioISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self._protocol = 'iSCSI'
|
||||
|
||||
@kaminario_logger
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Get volume object and map to initiator host."""
|
||||
if type(volume).__name__ != 'RestObject':
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
LOG.debug("Searching volume : %s in K2.", vol_name)
|
||||
vol_rs = self.client.search("volumes", name=vol_name)
|
||||
if not hasattr(vol_rs, 'hits') or vol_rs.total == 0:
|
||||
msg = _("Unable to find volume: %s from K2.") % vol_name
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
vol = vol_rs.hits[0]
|
||||
else:
|
||||
vol = volume
|
||||
"""Get target_portal"""
|
||||
LOG.debug("Searching first iscsi port ip without wan in K2.")
|
||||
iscsi_ip_rs = self.client.search("system/net_ips", wan_port="")
|
||||
iscsi_ip = target_iqn = None
|
||||
if hasattr(iscsi_ip_rs, 'hits') and iscsi_ip_rs.total != 0:
|
||||
iscsi_ip = iscsi_ip_rs.hits[0].ip_address
|
||||
if not iscsi_ip:
|
||||
msg = _("Unable to get ISCSI IP addres from K2.")
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
iscsi_portal = "{0}:{1}".format(iscsi_ip, ISCSI_TCP_PORT)
|
||||
LOG.debug("Searching system state for target iqn in K2.")
|
||||
sys_state_rs = self.client.search("system/state")
|
||||
|
||||
if hasattr(sys_state_rs, 'hits') and sys_state_rs.total != 0:
|
||||
target_iqn = sys_state_rs.hits[0].iscsi_qualified_target_name
|
||||
|
||||
if not target_iqn:
|
||||
msg = _("Unable to get target iqn from K2.")
|
||||
LOG.error(msg)
|
||||
raise exception.KaminarioCinderDriverException(reason=msg)
|
||||
host_name = self.get_initiator_host_name(connector)
|
||||
LOG.debug("Searching initiator hostname: %s in K2.", host_name)
|
||||
host_rs = self.client.search("hosts", name=host_name)
|
||||
"""Create a host if not exists."""
|
||||
if host_rs.total == 0:
|
||||
try:
|
||||
LOG.debug("Creating initiator hostname: %s in K2.", host_name)
|
||||
host = self.client.new("hosts", name=host_name,
|
||||
type="Linux").save()
|
||||
LOG.debug("Adding iqn: %(iqn)s to host: %(host)s in K2.",
|
||||
{'iqn': connector['initiator'], 'host': host_name})
|
||||
iqn = self.client.new("host_iqns", iqn=connector['initiator'],
|
||||
host=host)
|
||||
iqn.save()
|
||||
except Exception as ex:
|
||||
LOG.debug("Unable to create host : %s in K2.", host_name)
|
||||
self.delete_host_by_name(host_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
else:
|
||||
LOG.debug("Use existing initiator hostname: %s in K2.", host_name)
|
||||
host = host_rs.hits[0]
|
||||
try:
|
||||
LOG.debug("Mapping volume: %(vol)s to host: %(host)s",
|
||||
{'host': host_name, 'vol': vol.name})
|
||||
mapping = self.client.new("mappings", volume=vol, host=host).save()
|
||||
except Exception as ex:
|
||||
if host_rs.total == 0:
|
||||
LOG.debug("Unable to mapping volume:%(vol)s to host: %(host)s",
|
||||
{'host': host_name, 'vol': vol.name})
|
||||
self.delete_host_by_name(host_name)
|
||||
raise exception.KaminarioCinderDriverException(
|
||||
reason=six.text_type(ex.message))
|
||||
if type(volume).__name__ == 'RestObject':
|
||||
volsnap = None
|
||||
LOG.debug("Searching volsnaps in K2.")
|
||||
volsnaps = self.client.search("volsnaps")
|
||||
for v in volsnaps.hits:
|
||||
if v.snapshot.id == vol.id:
|
||||
volsnap = v
|
||||
break
|
||||
LOG.debug("Searching mapping of volsnap in K2.")
|
||||
rv = self.client.search("mappings", volume=volsnap)
|
||||
lun = rv.hits[0].lun
|
||||
|
||||
else:
|
||||
lun = mapping.lun
|
||||
return {"driver_volume_type": "iscsi",
|
||||
"data": {"target_iqn": target_iqn,
|
||||
"target_portal": iscsi_portal,
|
||||
"target_lun": lun,
|
||||
"target_discovered": True}}
|
||||
|
||||
@kaminario_logger
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Terminate connection of volume from host."""
|
||||
# Get volume object
|
||||
if type(volume).__name__ != 'RestObject':
|
||||
vol_name = self.get_volume_name(volume.id)
|
||||
LOG.debug("Searching volume: %s in K2.", vol_name)
|
||||
volume_rs = self.client.search("volumes", name=vol_name)
|
||||
if hasattr(volume_rs, "hits") and volume_rs.total != 0:
|
||||
volume = volume_rs.hits[0]
|
||||
else:
|
||||
vol_name = volume.name
|
||||
|
||||
# Get host object.
|
||||
host_name = self.get_initiator_host_name(connector)
|
||||
host_rs = self.client.search("hosts", name=host_name)
|
||||
if hasattr(host_rs, "hits") and host_rs.total != 0 and volume:
|
||||
host = host_rs.hits[0]
|
||||
LOG.debug("Searching and deleting mapping of volume: %(name)s to "
|
||||
"host: %(host)s", {'host': host_name, 'name': vol_name})
|
||||
map_rs = self.client.search("mappings", volume=volume, host=host)
|
||||
if hasattr(map_rs, "hits") and map_rs.total != 0:
|
||||
map_rs.hits[0].delete()
|
||||
if self.client.search("mappings", host=host).total == 0:
|
||||
LOG.debug("Deleting initiator hostname: %s in K2.", host_name)
|
||||
host.delete()
|
||||
else:
|
||||
LOG.warning(_LW("Host: %s not found on K2."), host_name)
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Add iSCSI cinder volume driver for Kaminario K2 all-flash arrays.
|
Loading…
x
Reference in New Issue
Block a user