Merge "Add NexentaEdge drivers"
This commit is contained in:
commit
875596b4ea
268
cinder/tests/unit/volume/drivers/nexenta/test_nexenta_edge.py
Normal file
268
cinder/tests/unit/volume/drivers/nexenta/test_nexenta_edge.py
Normal file
@ -0,0 +1,268 @@
|
||||
#
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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 json
|
||||
import mock
|
||||
from mock import patch
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import iscsi
|
||||
|
||||
NEDGE_BUCKET = 'c/t/bk'
|
||||
NEDGE_SERVICE = 'isc'
|
||||
NEDGE_URL = 'service/%s/iscsi' % NEDGE_SERVICE
|
||||
NEDGE_BLOCKSIZE = 4096
|
||||
NEDGE_CHUNKSIZE = 16384
|
||||
|
||||
MOCK_VOL = {
|
||||
'id': 'vol1',
|
||||
'name': 'vol1',
|
||||
'size': 1
|
||||
}
|
||||
MOCK_VOL2 = {
|
||||
'id': 'vol2',
|
||||
'name': 'vol2',
|
||||
'size': 1
|
||||
}
|
||||
MOCK_VOL3 = {
|
||||
'id': 'vol3',
|
||||
'name': 'vol3',
|
||||
'size': 2
|
||||
}
|
||||
MOCK_SNAP = {
|
||||
'id': 'snap1',
|
||||
'name': 'snap1',
|
||||
'volume_name': 'vol1',
|
||||
'volume_size': 1
|
||||
}
|
||||
NEW_VOL_SIZE = 2
|
||||
ISCSI_TARGET_NAME = 'iscsi_target_name:'
|
||||
ISCSI_TARGET_STATUS = 'Target 1: ' + ISCSI_TARGET_NAME
|
||||
|
||||
|
||||
class TestNexentaEdgeISCSIDriver(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
def _safe_get(opt):
|
||||
return getattr(self.cfg, opt)
|
||||
super(TestNexentaEdgeISCSIDriver, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.cfg = mock.Mock(spec=conf.Configuration)
|
||||
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
|
||||
self.cfg.trace_flags = 'fake_trace_flags'
|
||||
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
|
||||
self.cfg.nexenta_client_address = '0.0.0.0'
|
||||
self.cfg.nexenta_rest_address = '0.0.0.0'
|
||||
self.cfg.nexenta_rest_port = 8080
|
||||
self.cfg.nexenta_rest_protocol = 'http'
|
||||
self.cfg.nexenta_iscsi_target_portal_port = 3260
|
||||
self.cfg.nexenta_rest_user = 'admin'
|
||||
self.cfg.driver_ssl_cert_verify = False
|
||||
self.cfg.nexenta_rest_password = 'admin'
|
||||
self.cfg.nexenta_lun_container = NEDGE_BUCKET
|
||||
self.cfg.nexenta_iscsi_service = NEDGE_SERVICE
|
||||
self.cfg.nexenta_blocksize = NEDGE_BLOCKSIZE
|
||||
self.cfg.nexenta_chunksize = NEDGE_CHUNKSIZE
|
||||
self.cfg.nexenta_replication_count = 2
|
||||
self.cfg.nexenta_encryption = True
|
||||
self.cfg.replication_device = None
|
||||
self.cfg.nexenta_iops_limit = 0
|
||||
|
||||
mock_exec = mock.Mock()
|
||||
mock_exec.return_value = ('', '')
|
||||
self.driver = iscsi.NexentaEdgeISCSIDriver(execute=mock_exec,
|
||||
configuration=self.cfg)
|
||||
self.api_patcher = mock.patch('cinder.volume.drivers.nexenta.'
|
||||
'nexentaedge.jsonrpc.'
|
||||
'NexentaEdgeJSONProxy.__call__')
|
||||
self.mock_api = self.api_patcher.start()
|
||||
|
||||
self.mock_api.return_value = {
|
||||
'data': {
|
||||
'X-ISCSI-TargetName': ISCSI_TARGET_NAME,
|
||||
'X-ISCSI-TargetID': 1}
|
||||
}
|
||||
self.driver.do_setup(self.context)
|
||||
|
||||
self.addCleanup(self.api_patcher.stop)
|
||||
|
||||
def test_check_do_setup(self):
|
||||
self.assertEqual('%s1' % ISCSI_TARGET_NAME, self.driver.target_name)
|
||||
|
||||
def test_check_do_setup__vip(self):
|
||||
first_vip = '/'.join((self.cfg.nexenta_client_address, '32'))
|
||||
vips = [
|
||||
[{'ip': first_vip}],
|
||||
[{'ip': '0.0.0.1/32'}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
return {'data': {
|
||||
'X-ISCSI-TargetName': ISCSI_TARGET_NAME,
|
||||
'X-ISCSI-TargetID': 1,
|
||||
'X-VIPS': json.dumps(vips)}
|
||||
}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.driver.do_setup(self.context)
|
||||
self.assertEqual(self.driver.ha_vip, first_vip.split('/')[0])
|
||||
|
||||
def test_check_do_setup__vip_not_in_xvips(self):
|
||||
first_vip = '1.2.3.4/32'
|
||||
vips = [
|
||||
[{'ip': first_vip}],
|
||||
[{'ip': '0.0.0.1/32'}]
|
||||
]
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
return {'data': {
|
||||
'X-ISCSI-TargetName': ISCSI_TARGET_NAME,
|
||||
'X-ISCSI-TargetID': 1,
|
||||
'X-VIPS': json.dumps(vips)}
|
||||
}
|
||||
|
||||
self.mock_api.side_effect = my_side_effect
|
||||
self.assertRaises(exception.NexentaException,
|
||||
self.driver.do_setup, self.context)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
self.mock_api.side_effect = exception.VolumeBackendAPIException
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.check_for_setup_error)
|
||||
|
||||
@patch('cinder.volume.drivers.nexenta.nexentaedge.iscsi.'
|
||||
'NexentaEdgeISCSIDriver._get_lu_number')
|
||||
def test_create_volume(self, lun):
|
||||
lun.return_value = 1
|
||||
self.driver.create_volume(MOCK_VOL)
|
||||
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'volSizeMB': MOCK_VOL['size'] * 1024,
|
||||
'blockSize': NEDGE_BLOCKSIZE,
|
||||
'chunkSize': NEDGE_CHUNKSIZE,
|
||||
'optionsObject': {
|
||||
'ccow-replication-count': 2,
|
||||
'ccow-encryption-enabled': True,
|
||||
'ccow-iops-rate-lim': 0}
|
||||
})
|
||||
|
||||
@patch('cinder.volume.drivers.nexenta.nexentaedge.iscsi.'
|
||||
'NexentaEdgeISCSIDriver._get_lu_number')
|
||||
def test_create_volume__vip(self, lun):
|
||||
lun.return_value = 1
|
||||
self.driver.ha_vip = self.cfg.nexenta_client_address + '/32'
|
||||
self.driver.create_volume(MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'volSizeMB': MOCK_VOL['size'] * 1024,
|
||||
'blockSize': NEDGE_BLOCKSIZE,
|
||||
'chunkSize': NEDGE_CHUNKSIZE,
|
||||
'vip': self.cfg.nexenta_client_address + '/32',
|
||||
'optionsObject': {
|
||||
'ccow-replication-count': 2,
|
||||
'ccow-encryption-enabled': True,
|
||||
'ccow-iops-rate-lim': 0}
|
||||
})
|
||||
|
||||
def test_create_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_volume, MOCK_VOL)
|
||||
|
||||
def test_delete_volume(self):
|
||||
self.mock_api.side_effect = exception.VolumeBackendAPIException(
|
||||
'No volume')
|
||||
self.driver.delete_volume(MOCK_VOL)
|
||||
self.mock_api.assert_called_with(NEDGE_URL, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id']
|
||||
})
|
||||
|
||||
def test_delete_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.delete_volume, MOCK_VOL)
|
||||
|
||||
def test_extend_volume(self):
|
||||
self.driver.extend_volume(MOCK_VOL, NEW_VOL_SIZE)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'newSizeMB': NEW_VOL_SIZE * 1024
|
||||
})
|
||||
|
||||
def test_extend_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.extend_volume,
|
||||
MOCK_VOL, NEW_VOL_SIZE)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
self.driver.create_snapshot(MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_create_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_snapshot, MOCK_SNAP)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
self.driver.delete_snapshot(MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_delete_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.delete_snapshot, MOCK_SNAP)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
self.driver.create_volume_from_snapshot(MOCK_VOL2, MOCK_SNAP)
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/snapshot/clone', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_SNAP['volume_name'],
|
||||
'clonePath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||
'snapName': MOCK_SNAP['id']
|
||||
})
|
||||
|
||||
def test_create_volume_from_snapshot_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError,
|
||||
self.driver.create_volume_from_snapshot,
|
||||
MOCK_VOL2, MOCK_SNAP)
|
||||
|
||||
def test_create_cloned_volume(self):
|
||||
self.driver.create_cloned_volume(MOCK_VOL2, MOCK_VOL)
|
||||
url = '%s/snapshot/clone' % NEDGE_URL
|
||||
self.mock_api.assert_called_with(url, {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL['id'],
|
||||
'clonePath': NEDGE_BUCKET + '/' + MOCK_VOL2['id'],
|
||||
'snapName': 'cinder-clone-snapshot-vol2'
|
||||
})
|
||||
|
||||
def test_create_cloned_volume_larger(self):
|
||||
self.driver.create_cloned_volume(MOCK_VOL3, MOCK_VOL)
|
||||
# ignore the clone call, this has been tested before
|
||||
self.mock_api.assert_called_with(NEDGE_URL + '/resize', {
|
||||
'objectPath': NEDGE_BUCKET + '/' + MOCK_VOL3['id'],
|
||||
'newSizeMB': MOCK_VOL3['size'] * 1024
|
||||
})
|
||||
|
||||
def test_create_cloned_volume_fail(self):
|
||||
self.mock_api.side_effect = RuntimeError
|
||||
self.assertRaises(RuntimeError, self.driver.create_cloned_volume,
|
||||
MOCK_VOL2, MOCK_VOL)
|
330
cinder/volume/drivers/nexenta/nexentaedge/iscsi.py
Normal file
330
cinder/volume/drivers/nexenta/nexentaedge/iscsi.py
Normal file
@ -0,0 +1,330 @@
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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 oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import units
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import interface
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||
from cinder.volume.drivers.nexenta import options
|
||||
from cinder.volume.drivers.nexenta import utils as nexenta_utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class NexentaEdgeISCSIDriver(driver.ISCSIDriver):
|
||||
"""Executes volume driver commands on NexentaEdge cluster.
|
||||
|
||||
Version history:
|
||||
1.0.0 - Initial driver version.
|
||||
1.0.1 - Moved opts to options.py.
|
||||
1.0.2 - Added HA support.
|
||||
1.0.3 - Added encryption and replication count support.
|
||||
1.0.4 - Added initialize_connection.
|
||||
1.0.5 - Driver re-introduced in OpenStack.
|
||||
"""
|
||||
|
||||
VERSION = '1.0.5'
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Nexenta_Edge_CI"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NexentaEdgeISCSIDriver, self).__init__(*args, **kwargs)
|
||||
if self.configuration:
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_CONNECTION_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_ISCSI_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_DATASET_OPTS)
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_EDGE_OPTS)
|
||||
if self.configuration.nexenta_rest_address:
|
||||
self.restapi_host = self.configuration.nexenta_rest_address
|
||||
else:
|
||||
self.restapi_host = self.configuration.san_ip
|
||||
|
||||
if self.configuration.nexenta_rest_port:
|
||||
self.restapi_port = self.configuration.nexenta_rest_port
|
||||
else:
|
||||
self.restapi_port = self.configuration.san_api_port
|
||||
|
||||
if self.configuration.nexenta_client_address:
|
||||
self.target_vip = self.configuration.nexenta_client_address
|
||||
else:
|
||||
self.target_vip = self.configuration.target_ip_address
|
||||
if self.configuration.nexenta_rest_password:
|
||||
self.restapi_password = (
|
||||
self.configuration.nexenta_rest_password)
|
||||
else:
|
||||
self.restapi_password = (
|
||||
self.configuration.san_password)
|
||||
if self.configuration.nexenta_rest_user:
|
||||
self.restapi_user = self.configuration.nexenta_rest_user
|
||||
else:
|
||||
self.restapi_user = self.configuration.san_login
|
||||
self.verify_ssl = self.configuration.driver_ssl_cert_verify
|
||||
self.restapi_protocol = self.configuration.nexenta_rest_protocol
|
||||
self.iscsi_service = self.configuration.nexenta_iscsi_service
|
||||
self.bucket_path = self.configuration.nexenta_lun_container
|
||||
self.blocksize = self.configuration.nexenta_blocksize
|
||||
self.chunksize = self.configuration.nexenta_chunksize
|
||||
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
|
||||
self.repcount = self.configuration.nexenta_replication_count
|
||||
self.encryption = self.configuration.nexenta_encryption
|
||||
self.iscsi_target_port = (self.configuration.
|
||||
nexenta_iscsi_target_portal_port)
|
||||
self.ha_vip = None
|
||||
|
||||
@property
|
||||
def backend_name(self):
|
||||
backend_name = None
|
||||
if self.configuration:
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
if not backend_name:
|
||||
backend_name = self.__class__.__name__
|
||||
return backend_name
|
||||
|
||||
def do_setup(self, context):
|
||||
if self.restapi_protocol == 'auto':
|
||||
protocol, auto = 'http', True
|
||||
else:
|
||||
protocol, auto = self.restapi_protocol, False
|
||||
|
||||
try:
|
||||
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
|
||||
protocol, self.restapi_host, self.restapi_port, '/',
|
||||
self.restapi_user, self.restapi_password,
|
||||
self.verify_ssl, auto=auto)
|
||||
|
||||
data = self.restapi.get('service/' + self.iscsi_service)['data']
|
||||
self.target_name = '%s%s' % (
|
||||
data['X-ISCSI-TargetName'], data['X-ISCSI-TargetID'])
|
||||
if 'X-VIPS' in data:
|
||||
if self.target_vip not in data['X-VIPS']:
|
||||
raise exception.NexentaException(
|
||||
'Configured client IP address does not match any VIP'
|
||||
' provided by iSCSI service %s' % self.iscsi_service)
|
||||
else:
|
||||
self.ha_vip = self.target_vip
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('Error verifying iSCSI service %(serv)s on '
|
||||
'host %(hst)s', {
|
||||
'serv': self.iscsi_service,
|
||||
'hst': self.restapi_host})
|
||||
|
||||
def check_for_setup_error(self):
|
||||
url = 'clusters/%s/tenants/%s/buckets' % (self.cluster, self.tenant)
|
||||
if self.bucket not in self.restapi.get(url):
|
||||
raise exception.VolumeBackendAPIException(
|
||||
message=_('Bucket %s does not exist') % self.bucket)
|
||||
|
||||
def _get_lu_number(self, volname):
|
||||
rsp = self.restapi.get('service/' + self.iscsi_service + '/iscsi')
|
||||
path = '%s/%s' % (self.bucket_path, volname)
|
||||
for mapping in rsp['data']:
|
||||
if mapping['objectPath'] == path:
|
||||
return mapping['number']
|
||||
return None
|
||||
|
||||
def _get_provider_location(self, volume):
|
||||
lun = self._get_lu_number(volume['name'])
|
||||
if not lun:
|
||||
return None
|
||||
return '%(host)s:%(port)s,1 %(name)s %(number)s' % {
|
||||
'host': self.target_vip,
|
||||
'port': self.iscsi_target_port,
|
||||
'name': self.target_name,
|
||||
'number': lun
|
||||
}
|
||||
|
||||
def create_volume(self, volume):
|
||||
data = {
|
||||
'objectPath': '%s/%s' % (
|
||||
self.bucket_path, volume['name']),
|
||||
'volSizeMB': int(volume['size']) * units.Ki,
|
||||
'blockSize': self.blocksize,
|
||||
'chunkSize': self.chunksize,
|
||||
'optionsObject': {
|
||||
'ccow-replication-count': self.repcount,
|
||||
'ccow-iops-rate-lim': self.configuration.nexenta_iops_limit}
|
||||
}
|
||||
if self.encryption:
|
||||
data['optionsObject']['ccow-encryption-enabled'] = True
|
||||
if self.ha_vip:
|
||||
data['vip'] = self.ha_vip
|
||||
try:
|
||||
self.restapi.post('service/' + self.iscsi_service + '/iscsi', data)
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(
|
||||
'Error creating LUN for volume %s', volume['name'])
|
||||
return {'provider_location': self._get_provider_location(volume)}
|
||||
|
||||
def delete_volume(self, volume):
|
||||
data = {
|
||||
'objectPath': '%s/%s' % (
|
||||
self.bucket_path, volume['name'])
|
||||
}
|
||||
try:
|
||||
self.restapi.delete(
|
||||
'service/' + self.iscsi_service + '/iscsi', data)
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.info(
|
||||
'Error deleting LUN for volume %s', volume['name'])
|
||||
|
||||
def create_export(self, context, volume, connector=None):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {
|
||||
'target_discovered': False,
|
||||
'encrypted': False,
|
||||
'qos_specs': None,
|
||||
'target_iqn': self.target_name,
|
||||
'target_portal': '%s:%s' % (
|
||||
self.target_vip, self.iscsi_target_port),
|
||||
'volume_id': volume['id'],
|
||||
'target_lun': self._get_lu_number(volume['name']),
|
||||
'access_mode': 'rw',
|
||||
}
|
||||
}
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
try:
|
||||
self.restapi.put('service/' + self.iscsi_service + '/iscsi/resize',
|
||||
{'objectPath': '%s/%s' % (
|
||||
self.bucket_path, volume['name']),
|
||||
'newSizeMB': new_size * units.Ki})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('Error extending volume %s', volume['name'])
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
try:
|
||||
self.restapi.put(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot/clone',
|
||||
{
|
||||
'objectPath': '%s/%s' % (
|
||||
self.bucket_path, snapshot['volume_name']),
|
||||
'clonePath': '%s/%s' % (
|
||||
self.bucket_path, volume['name']),
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(
|
||||
'Error creating volume from snapshot %s', snapshot['name'])
|
||||
if (('size' in volume) and (
|
||||
volume['size'] > snapshot['volume_size'])):
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
try:
|
||||
self.restapi.post(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||
{
|
||||
'objectPath': '%s/%s' % (
|
||||
self.bucket_path, snapshot['volume_name']),
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('Error creating snapshot %s', snapshot['name'])
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
try:
|
||||
self.restapi.delete(
|
||||
'service/' + self.iscsi_service + '/iscsi/snapshot',
|
||||
{
|
||||
'objectPath': '%s/%s' % (
|
||||
self.bucket_path, snapshot['volume_name']),
|
||||
'snapName': snapshot['name']
|
||||
})
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.info('Error deleting snapshot %s', snapshot['name'])
|
||||
|
||||
@staticmethod
|
||||
def _get_clone_snapshot_name(volume):
|
||||
"""Return name for snapshot that will be used to clone the volume."""
|
||||
return 'cinder-clone-snapshot-%(id)s' % volume
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
snapshot = {'volume_name': src_vref['name'],
|
||||
'volume_id': src_vref['id'],
|
||||
'volume_size': src_vref['size'],
|
||||
'name': self._get_clone_snapshot_name(volume)}
|
||||
LOG.debug('Creating temp snapshot of the original volume: '
|
||||
'%s@%s', snapshot['volume_name'], snapshot['name'])
|
||||
self.create_snapshot(snapshot)
|
||||
try:
|
||||
self.create_volume_from_snapshot(volume, snapshot)
|
||||
except exception.NexentaException:
|
||||
LOG.error('Volume creation failed, deleting created snapshot '
|
||||
'%s', '@'.join([snapshot['volume_name'],
|
||||
snapshot['name']]))
|
||||
try:
|
||||
self.delete_snapshot(snapshot)
|
||||
except (exception.NexentaException, exception.SnapshotIsBusy):
|
||||
LOG.warning('Failed to delete zfs snapshot '
|
||||
'%s', '@'.join([snapshot['volume_name'],
|
||||
snapshot['name']]))
|
||||
raise
|
||||
if volume['size'] > src_vref['size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def local_path(self, volume):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
resp = self.restapi.get('system/stats')
|
||||
summary = resp['stats']['summary']
|
||||
total = nexenta_utils.str2gib_size(summary['total_capacity'])
|
||||
free = nexenta_utils.str2gib_size(summary['total_available'])
|
||||
|
||||
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||
'driver': self.__class__.__name__,
|
||||
'host': self.target_vip,
|
||||
'bucket': self.bucket_path
|
||||
}
|
||||
return {
|
||||
'vendor_name': 'Nexenta',
|
||||
'driver_version': self.VERSION,
|
||||
'storage_protocol': 'iSCSI',
|
||||
'reserved_percentage': 0,
|
||||
'total_capacity_gb': total,
|
||||
'free_capacity_gb': free,
|
||||
'QoS_support': False,
|
||||
'volume_backend_name': self.backend_name,
|
||||
'location_info': location_info,
|
||||
'iscsi_target_portal_port': self.iscsi_target_port,
|
||||
'restapi_url': self.restapi.url
|
||||
}
|
97
cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py
Normal file
97
cinder/volume/drivers/nexenta/nexentaedge/jsonrpc.py
Normal file
@ -0,0 +1,97 @@
|
||||
# Copyright 2015 Nexenta Systems, Inc.
|
||||
# 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 json
|
||||
import requests
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.utils import retry
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
TIMEOUT = 60
|
||||
|
||||
|
||||
class NexentaEdgeJSONProxy(object):
|
||||
|
||||
retry_exc_tuple = (
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.ConnectTimeout
|
||||
)
|
||||
|
||||
def __init__(self, protocol, host, port, path, user, password, verify,
|
||||
auto=False, method=None, session=None):
|
||||
if session:
|
||||
self.session = session
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
self.session.auth = (user, password)
|
||||
self.session.headers.update({'Content-Type': 'application/json'})
|
||||
self.protocol = protocol.lower()
|
||||
self.verify = verify
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.path = path
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.auto = auto
|
||||
self.method = method
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return '%s://%s:%s/%s' % (
|
||||
self.protocol, self.host, self.port, self.path)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in ('get', 'post', 'put', 'delete'):
|
||||
return NexentaEdgeJSONProxy(
|
||||
self.protocol, self.host, self.port, self.path, self.user,
|
||||
self.password, self.verify, self.auto, name, self.session)
|
||||
return super(NexentaEdgeJSONProxy, self).__getattr__(name)
|
||||
|
||||
def __hash__(self):
|
||||
return self.url.__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return 'HTTP JSON proxy: %s' % self.url
|
||||
|
||||
@retry(retry_exc_tuple, interval=1, retries=6)
|
||||
def __call__(self, *args):
|
||||
self.path = args[0]
|
||||
kwargs = {'timeout': TIMEOUT, 'verify': self.verify}
|
||||
data = None
|
||||
if len(args) > 1:
|
||||
data = json.dumps(args[1])
|
||||
kwargs['data'] = data
|
||||
|
||||
LOG.debug('Sending JSON data: %s, method: %s, data: %s',
|
||||
self.url, self.method, data)
|
||||
|
||||
func = getattr(self.session, self.method)
|
||||
if func:
|
||||
req = func(self.url, **kwargs)
|
||||
else:
|
||||
raise exception.VolumeDriverException(
|
||||
message=_('Unsupported method: %s') % self.method)
|
||||
|
||||
rsp = req.json()
|
||||
|
||||
LOG.debug('Got response: %s', rsp)
|
||||
if rsp.get('response') is None:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=_('Error response: %s') % rsp)
|
||||
return rsp.get('response')
|
@ -17,20 +17,22 @@ from oslo_config import cfg
|
||||
|
||||
from cinder.volume import configuration as conf
|
||||
|
||||
POLL_RETRIES = 5
|
||||
DEFAULT_ISCSI_PORT = 3260
|
||||
DEFAULT_HOST_GROUP = 'all'
|
||||
DEFAULT_TARGET_GROUP = 'all'
|
||||
|
||||
NEXENTA_EDGE_OPTS = [
|
||||
cfg.StrOpt('nexenta_nbd_symlinks_dir',
|
||||
default='/dev/disk/by-path',
|
||||
help='NexentaEdge logical path of directory to store symbolic '
|
||||
'links to NBDs'),
|
||||
cfg.StrOpt('nexenta_rest_address',
|
||||
default='',
|
||||
help='IP address of NexentaEdge management REST API endpoint'),
|
||||
cfg.StrOpt('nexenta_rest_user',
|
||||
default='admin',
|
||||
help='User name to connect to NexentaEdge'),
|
||||
help='User name to connect to NexentaEdge.'),
|
||||
cfg.StrOpt('nexenta_rest_password',
|
||||
default='nexenta',
|
||||
help='Password to connect to NexentaEdge',
|
||||
help='Password to connect to NexentaEdge.',
|
||||
secret=True),
|
||||
cfg.StrOpt('nexenta_lun_container',
|
||||
default='',
|
||||
@ -39,19 +41,42 @@ NEXENTA_EDGE_OPTS = [
|
||||
default='',
|
||||
help='NexentaEdge iSCSI service name'),
|
||||
cfg.StrOpt('nexenta_client_address',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='iSCSI target address should now be set using'
|
||||
' the common param target_ip_address.',
|
||||
default='',
|
||||
help='NexentaEdge iSCSI Gateway client '
|
||||
'address for non-VIP service'),
|
||||
cfg.IntOpt('nexenta_iops_limit',
|
||||
default=0,
|
||||
help='NexentaEdge iSCSI LUN object IOPS limit'),
|
||||
cfg.IntOpt('nexenta_chunksize',
|
||||
default=32768,
|
||||
help='NexentaEdge iSCSI LUN object chunk size')
|
||||
help='NexentaEdge iSCSI LUN object chunk size'),
|
||||
cfg.IntOpt('nexenta_replication_count',
|
||||
default=3,
|
||||
help='NexentaEdge iSCSI LUN object replication count.'),
|
||||
cfg.BoolOpt('nexenta_encryption',
|
||||
default=False,
|
||||
help='Defines whether NexentaEdge iSCSI LUN object '
|
||||
'has encryption enabled.')
|
||||
]
|
||||
|
||||
NEXENTA_CONNECTION_OPTS = [
|
||||
cfg.StrOpt('nexenta_rest_address',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Rest address should now be set using '
|
||||
'the common param depending on driver type, '
|
||||
'san_ip or nas_host',
|
||||
default='',
|
||||
help='IP address of NexentaEdge management REST API endpoint'),
|
||||
cfg.StrOpt('nexenta_host',
|
||||
default='',
|
||||
help='IP address of Nexenta SA'),
|
||||
cfg.IntOpt('nexenta_rest_port',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Rest address should now be set using '
|
||||
'the common param san_api_port.',
|
||||
default=0,
|
||||
help='HTTP(S) port to connect to Nexenta REST API server. '
|
||||
'If it is equal zero, 8443 for HTTPS and 8080 for HTTP '
|
||||
@ -63,31 +88,59 @@ NEXENTA_CONNECTION_OPTS = [
|
||||
cfg.BoolOpt('nexenta_use_https',
|
||||
default=True,
|
||||
help='Use secure HTTP for REST connection (default True)'),
|
||||
cfg.BoolOpt('nexenta_lu_writebackcache_disabled',
|
||||
default=False,
|
||||
help='Postponed write to backing store or not'),
|
||||
cfg.StrOpt('nexenta_user',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Common user parameters should be used '
|
||||
'depending on the driver type: '
|
||||
'san_login or nas_login',
|
||||
default='admin',
|
||||
help='User name to connect to Nexenta SA'),
|
||||
cfg.StrOpt('nexenta_password',
|
||||
deprecated_for_removal=True,
|
||||
deprecated_reason='Common password parameters should be used '
|
||||
'depending on the driver type: '
|
||||
'san_password or nas_password',
|
||||
default='nexenta',
|
||||
help='Password to connect to Nexenta SA',
|
||||
secret=True),
|
||||
]
|
||||
|
||||
NEXENTA_ISCSI_OPTS = [
|
||||
cfg.StrOpt('nexenta_iscsi_target_portal_groups',
|
||||
default='',
|
||||
help='Nexenta target portal groups'),
|
||||
cfg.StrOpt('nexenta_iscsi_target_portals',
|
||||
default='',
|
||||
help='Comma separated list of portals for NexentaStor5, in'
|
||||
'format of IP1:port1,IP2:port2. Port is optional, '
|
||||
'default=3260. Example: 10.10.10.1:3267,10.10.1.2'),
|
||||
cfg.StrOpt('nexenta_iscsi_target_host_group',
|
||||
default='all',
|
||||
help='Group of hosts which are allowed to access volumes'),
|
||||
cfg.IntOpt('nexenta_iscsi_target_portal_port',
|
||||
default=3260,
|
||||
help='Nexenta target portal port'),
|
||||
cfg.IntOpt('nexenta_luns_per_target',
|
||||
default=100,
|
||||
help='Amount of iSCSI LUNs per each target'),
|
||||
cfg.StrOpt('nexenta_volume',
|
||||
default='cinder',
|
||||
help='SA Pool that holds all volumes'),
|
||||
cfg.StrOpt('nexenta_target_prefix',
|
||||
default='iqn.1986-03.com.sun:02:cinder-',
|
||||
default='iqn.1986-03.com.sun:02:cinder',
|
||||
help='IQN prefix for iSCSI targets'),
|
||||
cfg.StrOpt('nexenta_target_group_prefix',
|
||||
default='cinder/',
|
||||
default='cinder',
|
||||
help='Prefix for iSCSI target groups on SA'),
|
||||
cfg.StrOpt('nexenta_host_group_prefix',
|
||||
default='cinder',
|
||||
help='Prefix for iSCSI host groups on SA'),
|
||||
cfg.StrOpt('nexenta_volume_group',
|
||||
default='iscsi',
|
||||
help='Volume group for ns5'),
|
||||
help='Volume group for NexentaStor5 iSCSI'),
|
||||
]
|
||||
|
||||
NEXENTA_NFS_OPTS = [
|
||||
@ -120,6 +173,9 @@ NEXENTA_DATASET_OPTS = [
|
||||
default='off',
|
||||
choices=['on', 'off', 'sha256', 'verify', 'sha256, verify'],
|
||||
help='Deduplication value for new ZFS folders.'),
|
||||
cfg.StrOpt('nexenta_folder',
|
||||
default='',
|
||||
help='A folder where cinder created datasets will reside.'),
|
||||
cfg.StrOpt('nexenta_dataset_description',
|
||||
default='',
|
||||
help='Human-readable description for the folder.'),
|
||||
|
@ -0,0 +1,89 @@
|
||||
===============================
|
||||
NexentaEdge NBD & iSCSI drivers
|
||||
===============================
|
||||
|
||||
NexentaEdge is designed from the ground-up to deliver high performance Block
|
||||
and Object storage services and limitless scalability to next generation
|
||||
OpenStack clouds, petabyte scale active archives and Big Data applications.
|
||||
NexentaEdge runs on shared nothing clusters of industry standard Linux
|
||||
servers, and builds on Nexenta IP and patent pending Cloud Copy On Write (CCOW)
|
||||
technology to break new ground in terms of reliability, functionality and cost
|
||||
efficiency.
|
||||
|
||||
For NexentaEdge user documentation, visit https://nexentaedge.github.io.
|
||||
|
||||
|
||||
iSCSI driver
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The NexentaEdge cluster must be installed and configured according to the
|
||||
relevant Nexenta documentation. A cluster, tenant, bucket must be pre-created,
|
||||
as well as an iSCSI service on the NexentaEdge gateway node.
|
||||
|
||||
The NexentaEdge iSCSI driver is selected using the normal procedures for one
|
||||
or multiple back-end volume drivers.
|
||||
|
||||
You must configure these items for each NexentaEdge cluster that the iSCSI
|
||||
volume driver controls:
|
||||
|
||||
#. Make the following changes on the volume node ``/etc/cinder/cinder.conf``
|
||||
file.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# Enable Nexenta iSCSI driver
|
||||
volume_driver = cinder.volume.drivers.nexenta.nexentaedge.iscsi.NexentaEdgeISCSIDriver
|
||||
|
||||
# Specify the ip address for Rest API (string value)
|
||||
nexenta_rest_address = MANAGEMENT-NODE-IP
|
||||
|
||||
# Port for Rest API (integer value)
|
||||
nexenta_rest_port=8080
|
||||
|
||||
# Protocol used for Rest calls (string value, default=htpp)
|
||||
nexenta_rest_protocol = http
|
||||
|
||||
# Username for NexentaEdge Rest (string value)
|
||||
nexenta_rest_user=USERNAME
|
||||
|
||||
# Password for NexentaEdge Rest (string value)
|
||||
nexenta_rest_password=PASSWORD
|
||||
|
||||
# Path to bucket containing iSCSI LUNs (string value)
|
||||
nexenta_lun_container = CLUSTER/TENANT/BUCKET
|
||||
|
||||
# Name of pre-created iSCSI service (string value)
|
||||
nexenta_iscsi_service = SERVICE-NAME
|
||||
|
||||
# IP address of the gateway node attached to iSCSI service above or
|
||||
# virtual IP address if an iSCSI Storage Service Group is configured in
|
||||
# HA mode (string value)
|
||||
nexenta_client_address = GATEWAY-NODE-IP
|
||||
|
||||
|
||||
#. Save the changes to the ``/etc/cinder/cinder.conf`` file and
|
||||
restart the ``cinder-volume`` service.
|
||||
|
||||
Supported operations
|
||||
--------------------
|
||||
|
||||
* Create, delete, attach, and detach volumes.
|
||||
|
||||
* Create, list, and delete volume snapshots.
|
||||
|
||||
* Create a volume from a snapshot.
|
||||
|
||||
* Copy an image to a volume.
|
||||
|
||||
* Copy a volume to an image.
|
||||
|
||||
* Clone a volume.
|
||||
|
||||
* Extend a volume.
|
||||
|
||||
Driver options
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Nexenta Driver supports these options:
|
||||
|
||||
.. include:: ../../tables/cinder-nexenta_edge.inc
|
@ -58,6 +58,7 @@ Driver Configuration Reference
|
||||
drivers/nec-storage-m-series-driver
|
||||
drivers/netapp-volume-driver
|
||||
drivers/nimble-volume-driver
|
||||
drivers/nexentaedge-driver
|
||||
drivers/nexentastor4-driver
|
||||
drivers/nexentastor5-driver
|
||||
drivers/prophetstor-dpl-driver
|
||||
|
46
doc/source/configuration/tables/cinder-nexenta_edge.inc
Normal file
46
doc/source/configuration/tables/cinder-nexenta_edge.inc
Normal file
@ -0,0 +1,46 @@
|
||||
..
|
||||
Warning: Do not edit this file. It is automatically generated from the
|
||||
software project's code and your changes will be overwritten.
|
||||
|
||||
The tool to generate this file lives in openstack-doc-tools repository.
|
||||
|
||||
Please make any changes needed in the code, then run the
|
||||
autogenerate-config-doc tool from the openstack-doc-tools repository, or
|
||||
ask for help on the documentation mailing list, IRC channel or meeting.
|
||||
|
||||
.. _cinder-nexenta_edge:
|
||||
|
||||
.. list-table:: Description of NexentaEdge driver configuration options
|
||||
:header-rows: 1
|
||||
:class: config-ref-table
|
||||
|
||||
* - Configuration option = Default value
|
||||
- Description
|
||||
* - **[DEFAULT]**
|
||||
-
|
||||
* - ``nexenta_blocksize`` = ``4096``
|
||||
- (Integer) Block size for datasets
|
||||
* - ``nexenta_chunksize`` = ``32768``
|
||||
- (Integer) NexentaEdge iSCSI LUN object chunk size
|
||||
* - ``nexenta_client_address`` =
|
||||
- (String) NexentaEdge iSCSI Gateway client address for non-VIP service
|
||||
* - ``nexenta_iscsi_service`` =
|
||||
- (String) NexentaEdge iSCSI service name
|
||||
* - ``nexenta_iscsi_target_portal_port`` = ``3260``
|
||||
- (Integer) Nexenta target portal port
|
||||
* - ``nexenta_lun_container`` =
|
||||
- (String) NexentaEdge logical path of bucket for LUNs
|
||||
* - ``nexenta_rest_address`` =
|
||||
- (String) IP address of NexentaEdge management REST API endpoint
|
||||
* - ``nexenta_rest_password`` = ``nexenta``
|
||||
- (String) Password to connect to NexentaEdge
|
||||
* - ``nexenta_rest_port`` = ``8080``
|
||||
- (Integer) HTTP port to connect to Nexenta REST API server
|
||||
* - ``nexenta_rest_protocol`` = ``auto``
|
||||
- (String) Use http or https for REST connection (default auto)
|
||||
* - ``nexenta_rest_user`` = ``admin``
|
||||
- (String) User name to connect to NexentaEdge
|
||||
* - ``nexenta_replication_count`` = ``3``
|
||||
- (String) NexentaEdge iSCSI LUN object replication count.
|
||||
* - ``nexenta_encryption`` = ``False``
|
||||
- (String) NexentaEdge iSCSI LUN object encryption
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- Added backend driver for Nexenta Edge iSCSI storage.
|
||||
|
Loading…
Reference in New Issue
Block a user