NexentaStor5: sessions and HTTPS support

REST calls now use session and are able to work through secure HTTP
connection. Also REST API was changed, because of that some methods of
the driver underwent changes too.

Implements: blueprint nexentastore5-https
Change-Id: I03df1fe2c8f66f9689deef8a24b361f0bfff3699
This commit is contained in:
Aleksey Ruban 2016-11-21 09:14:20 -07:00
parent a1ed8a6b29
commit 51368daf42
8 changed files with 631 additions and 444 deletions

View File

@ -18,9 +18,7 @@ Unit tests for OpenStack Cinder volume driver
import mock
from mock import patch
from oslo_serialization import jsonutils
from oslo_utils import units
import requests
from cinder import context
from cinder import db
@ -74,7 +72,7 @@ class TestNexentaISCSIDriver(test.TestCase):
self.cfg.nexenta_password = 'nexenta'
self.cfg.nexenta_volume = 'cinder'
self.cfg.nexenta_rest_port = 2000
self.cfg.nexenta_rest_protocol = 'http'
self.cfg.nexenta_use_https = False
self.cfg.nexenta_iscsi_target_portal_port = 8080
self.cfg.nexenta_target_prefix = 'iqn:'
self.cfg.nexenta_target_group_prefix = 'cinder/'
@ -91,6 +89,7 @@ class TestNexentaISCSIDriver(test.TestCase):
self.drv = iscsi.NexentaISCSIDriver(
configuration=self.cfg)
self.drv.db = db
self.drv._fetch_volumes = lambda: None
self.drv.do_setup(self.ctxt)
def _create_volume_db_entry(self):
@ -132,7 +131,9 @@ class TestNexentaISCSIDriver(test.TestCase):
'sparseVolume': self.cfg.nexenta_sparse})
def test_delete_volume(self):
self.nef_mock.delete.side_effect = exception.NexentaException()
self.drv.collect_zfs_garbage = lambda x: None
self.nef_mock.delete.side_effect = exception.NexentaException(
'Failed to destroy snapshot')
self.assertIsNone(self.drv.delete_volume(self.TEST_VOLUME_REF))
url = 'storage/pools/pool/volumeGroups'
data = {'name': 'dsg', 'volumeBlockSize': 32768}
@ -194,134 +195,49 @@ class TestNexentaISCSIDriver(test.TestCase):
self.nef_mock.put.assert_called_with(url, {
'volumeSize': 2 * units.Gi})
def test_get_target_by_alias(self):
self.nef_mock.get.return_value = {'data': []}
self.assertIsNone(self.drv._get_target_by_alias('1.1.1.1-0'))
def test_do_export(self):
target_name = 'new_target'
lun = 0
self.nef_mock.get.return_value = {'data': [{'name': 'iqn-0'}]}
self.assertEqual(
{'name': 'iqn-0'}, self.drv._get_target_by_alias('1.1.1.1-0'))
class GetSideEffect(object):
def __init__(self):
self.lm_counter = -1
def test_target_group_exists(self):
self.nef_mock.get.return_value = {'data': []}
self.assertFalse(
self.drv._target_group_exists({'data': [{'name': 'iqn-0'}]}))
def __call__(self, *args, **kwargs):
# Find out whether the volume is exported
if 'san/lunMappings?volume=' in args[0]:
self.lm_counter += 1
# a value for the first call
if self.lm_counter == 0:
return {'data': []}
else:
return {'data': [{'lun': lun}]}
# Get the name of just created target
elif 'san/iscsi/targets' in args[0]:
return {'data': [{'name': target_name}]}
self.nef_mock.get.return_value = {'data': [{'name': '1.1.1.1-0'}]}
self.assertTrue(self.drv._target_group_exists(
{'data': [{'name': 'iqn-0'}]}))
def post_side_effect(*args, **kwargs):
if 'san/iscsi/targets' in args[0]:
return {'data': [{'name': target_name}]}
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_target_by_alias')
def test_create_target(self, target):
self.nef_mock.get.return_value = {}
target.return_value = {'name': 'iqn-0'}
self.assertEqual('iqn-0', self.drv._create_target(0))
self.nef_mock.get.side_effect = GetSideEffect()
self.nef_mock.post.side_effect = post_side_effect
res = self.drv._do_export(self.ctxt, self.TEST_VOLUME_REF)
provider_location = '%(host)s:%(port)s,1 %(name)s %(lun)s' % {
'host': self.cfg.nexenta_host,
'port': self.cfg.nexenta_iscsi_target_portal_port,
'name': target_name,
'lun': lun,
}
expected = {'provider_location': provider_location}
self.assertEqual(expected, res)
target.return_value = None
self.assertRaises(TypeError, self.drv._create_target, 0)
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._create_target')
def test_get_target_name(self, target_name):
self._create_volume_db_entry()
self.drv.targets = {}
target_name.return_value = 'iqn-0'
self.drv.targets['iqn-0'] = []
self.assertEqual(
'iqn-0', self.drv._get_target_name(self.TEST_VOLUME_REF))
volume = self.TEST_VOLUME_REF
volume['provider_location'] = '1.1.1.1:8080,1 iqn-0 0'
self.nef_mock.get.return_value = {'data': [{'alias': '1.1.1.1-0'}]}
self.assertEqual(
'iqn-0', self.drv._get_target_name(self.TEST_VOLUME_REF))
self.assertEqual('1.1.1.1-0', self.drv.targetgroups['iqn-0'])
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._create_target')
def test_get_targetgroup_name(self, target_name):
self.TEST_VOLUME_REF['provider_location'] = '1.1.1.1:8080,1 iqn-0 0'
self._create_volume_db_entry()
target_name = 'iqn-0'
self.drv.targetgroups[target_name] = '1.1.1.1-0'
self.assertEqual(
'1.1.1.1-0', self.drv._get_targetgroup_name(self.TEST_VOLUME_REF))
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_targetgroup_name')
def test_get_lun_id(self, targetgroup):
targetgroup.return_value = '1.1.1.1-0'
self.nef_mock.get.return_value = {'data': [{'guid': '0'}]}
self.assertEqual('0', self.drv._get_lun_id(self.TEST_VOLUME_REF))
self.nef_mock.get.return_value = {}
self.assertRaises(
LookupError, self.drv._get_lun_id, self.TEST_VOLUME_REF)
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_lun_id')
def test_lu_exists(self, lun_id):
lun_id.return_value = '0'
self.assertTrue(self.drv._lu_exists(self.TEST_VOLUME_REF))
lun_id.side_effect = LookupError
self.assertFalse(self.drv._lu_exists(self.TEST_VOLUME_REF))
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_lun_id')
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_targetgroup_name')
def test_get_lun(self, targetgroup, lun_id):
lun_id.return_value = '0'
targetgroup.return_value = '1.1.1.1-0'
self.nef_mock.get.return_value = {'data': [{'lunNumber': 0}]}
self.assertEqual(0, self.drv._get_lun(self.TEST_VOLUME_REF))
self.nef_mock.get.return_value = {}
self.assertRaises(
LookupError, self.drv._get_lun, self.TEST_VOLUME_REF)
lun_id.side_effect = LookupError()
self.assertIsNone(self.drv._get_lun(self.TEST_VOLUME_REF))
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_target_name')
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_targetgroup_name')
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._lu_exists')
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_lun')
def test_do_export(self, get_lun, lu_exists, targetgroup, target):
target.return_value = 'iqn-0'
targetgroup.return_value = '1.1.1.1-0'
lu_exists.return_value = False
get_lun.return_value = 0
self.assertEqual(
{'provider_location': '1.1.1.1:8080,1 iqn-0 0'},
self.drv._do_export({}, self.TEST_VOLUME_REF))
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_targetgroup_name')
@patch('cinder.volume.drivers.nexenta.ns5.iscsi.'
'NexentaISCSIDriver._get_lun_id')
def test_remove_export(self, lun_id, tg_name):
lun_id.return_value = '0'
tg_name.return_value = '1.1.1.1-0'
self.nef_mock.delete.side_effect = exception.NexentaException(
'No such logical unit in target group')
self.assertIsNone(
self.drv.remove_export(self.ctxt, self.TEST_VOLUME_REF))
self.nef_mock.delete.side_effect = exception.NexentaException(
'Error')
self.assertRaises(
exception.NexentaException,
self.drv.remove_export, self.ctxt, self.TEST_VOLUME_REF)
lun_id.side_effect = LookupError()
self.assertIsNone(
self.drv.remove_export(self.ctxt, self.TEST_VOLUME_REF))
def test_remove_export(self):
mapping_id = '1234567890'
self.nef_mock.get.return_value = {'data': [{'id': mapping_id}]}
self.drv.remove_export(self.ctxt, self.TEST_VOLUME_REF)
url = 'san/lunMappings/%s' % mapping_id
self.nef_mock.delete.assert_called_with(url)
def test_update_volume_stats(self):
self.nef_mock.get.return_value = {
@ -353,45 +269,3 @@ class TestNexentaISCSIDriver(test.TestCase):
}
self.drv._update_volume_stats()
self.assertEqual(stats, self.drv._stats)
class TestNexentaJSONProxy(test.TestCase):
def __init__(self, method):
super(TestNexentaJSONProxy, self).__init__(method)
@patch('requests.Response.close')
@patch('requests.get')
@patch('requests.post')
def test_call(self, post, get, close):
nef_get = jsonrpc.NexentaJSONProxy(
'http', '1.1.1.1', '8080', 'user', 'pass', method='get')
nef_post = jsonrpc.NexentaJSONProxy(
'http', '1.1.1.1', '8080', 'user', 'pass', method='post')
data = {'key': 'value'}
get.return_value = requests.Response()
post.return_value = requests.Response()
get.return_value.__setstate__({
'status_code': 200, '_content': jsonutils.dumps(data)})
self.assertEqual({'key': 'value'}, nef_get('url'))
get.return_value.__setstate__({
'status_code': 201, '_content': ''})
self.assertEqual('Success', nef_get('url'))
data2 = {'links': [{'href': 'redirect_url'}]}
post.return_value.__setstate__({
'status_code': 202, '_content': jsonutils.dumps(data2)})
get.return_value.__setstate__({
'status_code': 200, '_content': jsonutils.dumps(data)})
self.assertEqual({'key': 'value'}, nef_post('url'))
get.return_value.__setstate__({
'status_code': 200, '_content': ''})
self.assertEqual('Success', nef_post('url', data))
get.return_value.__setstate__({
'status_code': 400,
'_content': jsonutils.dumps({'code': 'ENOENT'})})
self.assertRaises(exception.NexentaException, lambda: nef_get('url'))

View File

@ -0,0 +1,251 @@
# Copyright 2016 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.
"""
Unit tests for NexentaStor 5 REST API helper
"""
import uuid
import mock
from mock import patch
from oslo_serialization import jsonutils
import requests
from requests import adapters
from cinder import exception
from cinder import test
from cinder.volume.drivers.nexenta.ns5 import jsonrpc
HOST = '1.1.1.1'
USERNAME = 'user'
PASSWORD = 'pass'
def gen_response(code=200, json=None):
r = requests.Response()
r.headers['Content-Type'] = 'application/json'
r.encoding = 'utf8'
r.status_code = code
r.reason = 'FAKE REASON'
r.raw = mock.Mock()
r._content = ''
if json:
r._content = jsonutils.dumps(json)
return r
class TestNexentaJSONProxyAuth(test.TestCase):
@patch('cinder.volume.drivers.nexenta.ns5.jsonrpc.requests.post')
def test_https_auth(self, post):
use_https = True
port = 8443
auth_uri = 'auth/login'
rnd_url = 'some/random/url'
class PostSideEffect(object):
def __call__(self, *args, **kwargs):
r = gen_response()
if args[0] == '%(scheme)s://%(host)s:%(port)s/%(uri)s' % {
'scheme': 'https',
'host': HOST,
'port': port,
'uri': auth_uri}:
token = uuid.uuid4().hex
content = {'token': token}
r._content = jsonutils.dumps(content)
return r
post_side_effect = PostSideEffect()
post.side_effect = post_side_effect
class TestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(TestAdapter, self).__init__()
self.counter = 0
def send(self, request, *args, **kwargs):
# an url is being requested for the second time
if self.counter == 1:
# make the fake backend respond 401
r = gen_response(401)
r._content = ''
r.connection = mock.Mock()
r_ = gen_response(json={'data': []})
r.connection.send = lambda prep, **kwargs_: r_
else:
r = gen_response(json={'data': []})
r.request = request
self.counter += 1
return r
nef = jsonrpc.NexentaJSONProxy(HOST, port, USERNAME, PASSWORD,
use_https)
adapter = TestAdapter()
nef.session.mount(
'%(scheme)s://%(host)s:%(port)s/%(uri)s' % {
'scheme': 'https',
'host': HOST,
'port': port,
'uri': rnd_url},
adapter)
# successful authorization
self.assertEqual({'data': []}, nef.get(rnd_url))
# session timeout simulation. Client must authenticate newly
self.assertEqual({'data': []}, nef.get(rnd_url))
# auth URL must be requested two times at this moment
self.assertEqual(2, post.call_count)
# continue with the last (second) token
self.assertEqual(nef.get(rnd_url), {'data': []})
# auth URL must be requested two times
self.assertEqual(2, post.call_count)
class TestNexentaJSONProxy(test.TestCase):
def setUp(self):
super(TestNexentaJSONProxy, self).setUp()
self.nef = jsonrpc.NexentaJSONProxy(HOST, 0, USERNAME, PASSWORD, False)
def gen_adapter(self, code, json=None):
class TestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(TestAdapter, self).__init__()
def send(self, request, *args, **kwargs):
r = gen_response(code, json)
r.request = request
return r
return TestAdapter()
def _mount_adapter(self, url, adapter):
self.nef.session.mount(
'%(scheme)s://%(host)s:%(port)s/%(uri)s' % {
'scheme': 'http',
'host': HOST,
'port': 8080,
'uri': url},
adapter)
def test_post(self):
random_dict = {'data': uuid.uuid4().hex}
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, self.gen_adapter(201, random_dict))
self.assertEqual(random_dict, self.nef.post(rnd_url))
def test_delete(self):
random_dict = {'data': uuid.uuid4().hex}
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, self.gen_adapter(201, random_dict))
self.assertEqual(random_dict, self.nef.delete(rnd_url))
def test_put(self):
random_dict = {'data': uuid.uuid4().hex}
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, self.gen_adapter(201, random_dict))
self.assertEqual(random_dict, self.nef.put(rnd_url))
def test_get_200(self):
random_dict = {'data': uuid.uuid4().hex}
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, self.gen_adapter(200, random_dict))
self.assertEqual(random_dict, self.nef.get(rnd_url))
def test_get_201(self):
random_dict = {'data': uuid.uuid4().hex}
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, self.gen_adapter(201, random_dict))
self.assertEqual(random_dict, self.nef.get(rnd_url))
def test_get_500(self):
class TestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(TestAdapter, self).__init__()
def send(self, request, *args, **kwargs):
json = {
'code': 'NEF_ERROR',
'message': 'Some error'
}
r = gen_response(500, json)
r.request = request
return r
adapter = TestAdapter()
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, adapter)
self.assertRaises(exception.NexentaException, self.nef.get, rnd_url)
def test_get__not_nef_error(self):
class TestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(TestAdapter, self).__init__()
def send(self, request, *args, **kwargs):
r = gen_response(404)
r._content = 'Page Not Found'
r.request = request
return r
adapter = TestAdapter()
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, adapter)
self.assertRaises(exception.VolumeBackendAPIException, self.nef.get,
rnd_url)
def test_get__not_nef_error_empty_body(self):
class TestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(TestAdapter, self).__init__()
def send(self, request, *args, **kwargs):
r = gen_response(404)
r.request = request
return r
adapter = TestAdapter()
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, adapter)
self.assertRaises(exception.VolumeBackendAPIException, self.nef.get,
rnd_url)
def test_202(self):
redirect_url = 'redirect/url'
class RedirectTestAdapter(adapters.HTTPAdapter):
def __init__(self):
super(RedirectTestAdapter, self).__init__()
def send(self, request, *args, **kwargs):
json = {
'links': [{'href': redirect_url}]
}
r = gen_response(202, json)
r.request = request
return r
rnd_url = 'some/random/url'
self._mount_adapter(rnd_url, RedirectTestAdapter())
self._mount_adapter(redirect_url, self.gen_adapter(201))
self.assertIsNone(self.nef.get(rnd_url))

View File

@ -22,6 +22,7 @@ from mock import patch
from cinder import context
from cinder import db
from cinder import test
from cinder.tests.unit.fake_volume import fake_volume_obj
from cinder.volume import configuration as conf
from cinder.volume.drivers.nexenta.ns5 import jsonrpc
from cinder.volume.drivers.nexenta.ns5 import nfs
@ -36,23 +37,25 @@ class TestNexentaNfsDriver(test.TestCase):
TEST_VOLUME_NAME = 'volume1'
TEST_VOLUME_NAME2 = 'volume2'
TEST_VOLUME = {
TEST_VOLUME = fake_volume_obj(None, **{
'name': TEST_VOLUME_NAME,
'id': '1',
'size': 1,
'status': 'available',
'provider_location': TEST_SHARE
}
TEST_VOLUME2 = {
})
TEST_VOLUME2 = fake_volume_obj(None, **{
'name': TEST_VOLUME_NAME2,
'size': 1,
'size': 2,
'id': '2',
'status': 'in-use'
}
})
TEST_SNAPSHOT = {
'name': TEST_SNAPSHOT_NAME,
'volume_name': TEST_VOLUME_NAME,
'volume_size': 1,
'volume_id': '1'
}
@ -71,16 +74,16 @@ class TestNexentaNfsDriver(test.TestCase):
self.cfg.nfs_mount_attempts = 3
self.cfg.nas_mount_options = 'vers=4'
self.cfg.reserved_percentage = 20
self.cfg.nexenta_rest_protocol = 'http'
self.cfg.nexenta_rest_port = 8080
self.cfg.nexenta_use_https = False
self.cfg.nexenta_rest_port = 0
self.cfg.nexenta_user = 'user'
self.cfg.nexenta_password = 'pass'
self.cfg.max_over_subscription_ratio = 20.0
self.cfg.nas_host = '1.1.1.1'
self.cfg.nas_share_path = 'pool/share'
self.nef_mock = mock.Mock()
self.mock_object(jsonrpc, 'NexentaJSONProxy',
return_value=self.nef_mock)
self.stubs.Set(jsonrpc, 'NexentaJSONProxy',
lambda *_, **__: self.nef_mock)
self.drv = nfs.NexentaNfsDriver(configuration=self.cfg)
self.drv.db = db
self.drv.do_setup(self.ctxt)
@ -123,7 +126,7 @@ class TestNexentaNfsDriver(test.TestCase):
url = 'storage/pools/pool/filesystems'
data = {
'name': 'share/volume1',
'name': 'share/volume-1',
'compressionMode': 'on',
'dedupMode': 'off',
}
@ -136,7 +139,7 @@ class TestNexentaNfsDriver(test.TestCase):
self.nef_mock.get.return_value = {}
self.drv.delete_volume(self.TEST_VOLUME)
self.nef_mock.delete.assert_called_with(
'storage/pools/pool/filesystems/share%2Fvolume1?snapshots=true')
'storage/pools/pool/filesystems/share%2Fvolume-1?snapshots=true')
def test_create_snapshot(self):
self._create_volume_db_entry()
@ -153,20 +156,27 @@ class TestNexentaNfsDriver(test.TestCase):
self.drv.delete_snapshot(self.TEST_SNAPSHOT)
self.nef_mock.delete.assert_called_with(url)
@patch('cinder.volume.drivers.nexenta.ns5.nfs.'
'NexentaNfsDriver.local_path')
@patch('cinder.volume.drivers.nexenta.ns5.nfs.'
'NexentaNfsDriver._share_folder')
def test_create_volume_from_snapshot(self, share):
def test_create_volume_from_snapshot(self, share, path):
self._create_volume_db_entry()
url = ('storage/filesystems/pool%2Fshare%2Fvolume2/promote')
url = ('storage/pools/%(pool)s/'
'filesystems/%(fs)s/snapshots/%(snap)s/clone') % {
'pool': 'pool',
'fs': '%2F'.join(['share', 'volume-1']),
'snap': self.TEST_SNAPSHOT['name']
}
path = '/'.join(['pool/share', self.TEST_VOLUME2['name']])
data = {'targetPath': path}
self.drv.create_volume_from_snapshot(
self.TEST_VOLUME2, self.TEST_SNAPSHOT)
self.nef_mock.post.assert_called_with(url)
self.nef_mock.post.assert_called_with(url, data)
def test_get_capacity_info(self):
self.nef_mock.get.return_value = {
'bytesAvailable': 1000,
'bytesUsed': 100}
self.assertEqual(
(1000, 900, 100), self.drv._get_capacity_info('pool/share'))

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import uuid
from oslo_log import log as logging
from oslo_utils import units
@ -26,16 +28,19 @@ from cinder.volume.drivers.nexenta.ns5 import jsonrpc
from cinder.volume.drivers.nexenta import options
from cinder.volume.drivers.nexenta import utils
VERSION = '1.0.0'
VERSION = '1.1.0'
LOG = logging.getLogger(__name__)
TARGET_GROUP_PREFIX = 'cinder-tg-'
@interface.volumedriver
class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
class NexentaISCSIDriver(driver.ISCSIDriver):
"""Executes volume driver commands on Nexenta Appliance.
Version history:
1.0.0 - Initial driver version.
1.1.0 - Added HTTPS support.
Added use of sessions for REST calls.
"""
VERSION = VERSION
@ -46,8 +51,10 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
def __init__(self, *args, **kwargs):
super(NexentaISCSIDriver, self).__init__(*args, **kwargs)
self.nef = None
# mapping of targets and groups. Groups are the keys
self.targets = {}
self.targetgroups = {}
# list of volumes in target group. Groups are the keys
self.volumes = {}
if self.configuration:
self.configuration.append_config_values(
options.NEXENTA_CONNECTION_OPTS)
@ -57,7 +64,7 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
options.NEXENTA_DATASET_OPTS)
self.configuration.append_config_values(
options.NEXENTA_RRMGR_OPTS)
self.nef_protocol = self.configuration.nexenta_rest_protocol
self.use_https = self.configuration.nexenta_use_https
self.nef_host = self.configuration.nexenta_host
self.nef_port = self.configuration.nexenta_rest_port
self.nef_user = self.configuration.nexenta_user
@ -82,13 +89,9 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
return backend_name
def do_setup(self, context):
if self.nef_protocol == 'auto':
protocol, auto = 'http', True
else:
protocol, auto = self.nef_protocol, False
self.nef = jsonrpc.NexentaJSONProxy(
protocol, self.nef_host, self.nef_port, self.nef_user,
self.nef_password, auto=auto)
self.nef_host, self.nef_port, self.nef_user,
self.nef_password, self.use_https)
url = 'storage/pools/%s/volumeGroups' % self.storage_pool
data = {
'name': self.volume_group,
@ -103,6 +106,16 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
else:
raise
self._fetch_volumes()
def _fetch_volumes(self):
url = 'san/iscsi/targets?fields=alias,name&limit=50000'
for target in self.nef.get(url)['data']:
tg_name = target['alias']
if tg_name.startswith(TARGET_GROUP_PREFIX):
self.targets[tg_name] = target['name']
self._fill_volumes(tg_name)
def check_for_setup_error(self):
"""Verify that the zfs volumes exist.
@ -131,63 +144,6 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
return '%s/%s/%s' % (self.storage_pool, self.volume_group,
volume['name'])
def _create_target(self, target_idx):
target_alias = '%s-%i' % (
self.nef_host,
target_idx
)
target = self._get_target_by_alias(target_alias)
if not target:
url = 'san/iscsi/targets'
data = {'alias': target_alias}
self.nef.post(url, data)
target = self._get_target_by_alias(target_alias)
if not self._target_group_exists(target_alias):
url = 'san/targetgroups'
data = {'name': target_alias, 'targets': [target['name']]}
self.nef.post(url, data)
self.targetgroups[target['name']] = target_alias
self.targets[target['name']] = []
return target['name']
def _get_target_name(self, volume):
"""Return iSCSI target name with least LUs."""
provider_location = volume.get('provider_location')
target_names = list(self.targets)
if provider_location:
target_name = provider_location.split(',1 ')[1].split(' ')[0]
if not self.targets.get(target_name):
self.targets[target_name] = []
if not(volume['name'] in self.targets[target_name]):
self.targets[target_name].append(volume['name'])
if not self.targetgroups.get(target_name):
url = 'san/iscsi/targets'
data = self.nef.get(url).get('data')
target_alias = data[0]['alias']
self.targetgroups[target_name] = target_alias
elif not target_names:
# create first target and target group
target_name = self._create_target(0)
self.targets[target_name].append(volume['name'])
else:
target_name = target_names[0]
for target in target_names:
# find target with minimum number of volumes
if len(self.targets[target]) < len(self.targets[target_name]):
target_name = target
if len(self.targets[target_name]) >= 20:
# create new target and target group
target_name = self._create_target(len(target_names))
if not(volume['name'] in self.targets[target_name]):
self.targets[target_name].append(volume['name'])
return target_name
def _get_targetgroup_name(self, volume):
target_name = self._get_target_name(volume)
return self.targetgroups[target_name]
@staticmethod
def _get_clone_snapshot_name(volume):
"""Return name for snapshot that will be used to clone the volume."""
@ -217,12 +173,12 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
:param volume: volume reference
"""
pool, group, name = self._get_volume_path(volume).split('/')
url = ('storage/pools/%(pool)s/volumeGroups/%(group)s'
'/volumes/%(name)s') % {
'pool': pool,
'group': group,
'name': name
'pool': self.storage_pool,
'group': self.volume_group,
'name': volume['name']
}
try:
self.nef.delete(url)
@ -312,16 +268,7 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
'volume': snapshot_vol,
'snapshot': snapshot['name']
}
targetPath = self._get_volume_path(volume)
self.nef.post(url, {'targetPath': targetPath})
url = ('storage/pools/%(pool)s/volumeGroups/'
'%(group)s/volumes/%(name)s/promote') % {
'pool': pool,
'group': group,
'name': volume['name'],
}
self.nef.post(url)
self.nef.post(url, {'targetPath': self._get_volume_path(volume)})
if (('size' in volume) and (
volume['size'] > snapshot['volume_size'])):
self.extend_volume(volume, volume['size'])
@ -338,10 +285,6 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
'name': self._get_clone_snapshot_name(volume)}
LOG.debug('Creating temp snapshot of the original volume: '
'%s@%s', snapshot['volume_name'], snapshot['name'])
# We don't delete this snapshot, because this snapshot will be origin
# of new volume. This snapshot will be automatically promoted by NEF
# when user will delete origin volume. But when cloned volume deleted
# we check its origin property and delete source snapshot if needed.
self.create_snapshot(snapshot)
try:
self.create_volume_from_snapshot(volume, snapshot)
@ -361,93 +304,81 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
ctxt = context.get_admin_context()
return db.volume_get(ctxt, snapshot['volume_id'])
def _get_target_by_alias(self, alias):
"""Get an iSCSI target by it's alias.
:param alias: target alias
:return: First found target, else None
"""
url = 'san/iscsi/targets?alias=%s' % alias
targets = self.nef.get(url).get('data')
if not targets:
return None
return targets[0]
def _target_group_exists(self, target_group):
"""Check if target group exist.
:param target_group: target group
:return: True if target group exist, else False
"""
url = 'san/targetgroups?name=%s' % target_group
return bool(self.nef.get(url).get('data'))
def _lu_exists(self, volume):
"""Check if LU exists on appliance.
:param volume: cinder volume
:return: True if LU exists, else False
"""
try:
self._get_lun_id(volume)
except LookupError:
return False
return True
def _get_lun_id(self, volume):
"""Get lun id for zfs volume.
:param volume: cinder volume
:raises: LookupError if zfs volume does not exist or not mapped to LU
:return: LUN
"""
volume_path = self._get_volume_path(volume)
targetgroup_name = self._get_targetgroup_name(volume)
url = 'san/targetgroups/%s/luns?volume=%s' % (
targetgroup_name, volume_path.replace('/', '%2F'))
data = self.nef.get(url).get('data')
if not data:
raise LookupError(_("LU does not exist for volume: %s"),
volume['name'])
else:
return data[0]['guid']
def _get_lun(self, volume):
try:
lun_id = self._get_lun_id(volume)
except LookupError:
return None
targetgroup_name = self._get_targetgroup_name(volume)
url = 'san/targetgroups/%s/luns/%s/views' % (
targetgroup_name, lun_id)
data = self.nef.get(url).get('data')
if not data:
raise LookupError(_("No views found for LUN: %s"), lun_id)
return data[0]['lunNumber']
def _do_export(self, _ctx, volume):
"""Do all steps to get zfs volume exported at separate target.
:param volume: reference of volume to be exported
"""
volume_path = self._get_volume_path(volume)
target_name = self._get_target_name(volume)
targetgroup_name = self._get_targetgroup_name(volume)
entry = {}
if not self._lu_exists(volume):
url = 'san/targetgroups/%s/luns' % targetgroup_name
data = {'volume': volume_path}
self.nef.post(url, data)
entry['lun'] = self._get_lun(volume)
# Find out whether the volume is exported
vol_map_url = 'san/lunMappings?volume=%s&fields=lun' % (
volume_path.replace('/', '%2F'))
data = self.nef.get(vol_map_url).get('data')
if data:
model_update = {}
else:
# Choose the best target group among existing ones
tg_name = None
for tg in self.volumes.keys():
if len(self.volumes[tg]) < 20:
tg_name = tg
break
if tg_name:
target_name = self.targets[tg_name]
else:
tg_name = TARGET_GROUP_PREFIX + uuid.uuid4().hex
# Create new target
url = 'san/iscsi/targets'
data = {
"portals": [
{"address": self.nef_host}
],
'alias': tg_name
}
self.nef.post(url, data)
# Get the name of just created target
data = self.nef.get(
'%(url)s?fields=name&alias=%(tg_name)s' % {
'url': url,
'tg_name': tg_name
})['data']
target_name = data[0]['name']
self._create_target_group(tg_name, target_name)
self.targets[tg_name] = target_name
self.volumes[tg_name] = set()
# Export the volume
url = 'san/lunMappings'
data = {
"hostGroup": "all",
"targetGroup": tg_name,
'volume': volume_path
}
try:
self.nef.post(url, data)
self.volumes[tg_name].add(volume_path)
except exception.NexentaException as e:
if 'No such target group' in e.args[0]:
self._create_target_group(tg_name, target_name)
self._fill_volumes(tg_name)
self.nef.post(url, data)
else:
raise
# Get LUN of just created volume
data = self.nef.get(vol_map_url).get('data')
lun = data[0]['lun']
model_update = {}
if entry.get('lun') is not None:
provider_location = '%(host)s:%(port)s,1 %(name)s %(lun)s' % {
'host': self.nef_host,
'port': self.configuration.nexenta_iscsi_target_portal_port,
'name': target_name,
'lun': entry['lun'],
'lun': lun,
}
model_update = {'provider_location': provider_location}
return model_update
@ -473,20 +404,22 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
:param volume: reference of volume to be unexported
"""
try:
lun_id = self._get_lun_id(volume)
except LookupError:
return
targetgroup_name = self._get_targetgroup_name(volume)
url = 'san/targetgroups/%s/luns/%s' % (
targetgroup_name, lun_id)
try:
volume_path = self._get_volume_path(volume)
# Get ID of a LUN mapping if the volume is exported
url = 'san/lunMappings?volume=%s&fields=id' % (
volume_path.replace('/', '%2F'))
data = self.nef.get(url)['data']
if data:
url = 'san/lunMappings/%s' % data[0]['id']
self.nef.delete(url)
except exception.NexentaException as exc:
if 'No such logical unit in target group' in exc.args[0]:
LOG.debug('LU already deleted from appliance')
else:
raise
else:
LOG.debug('LU already deleted from appliance')
for tg in self.volumes:
if volume_path in self.volumes[tg]:
self.volumes[tg].remove(volume_path)
break
def get_volume_stats(self, refresh=False):
"""Get volume stats.
@ -502,7 +435,8 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
"""Retrieve stats info for NexentaStor appliance."""
LOG.debug('Updating volume stats')
url = 'storage/pools/%(pool)s/volumeGroups/%(group)s' % {
url = ('storage/pools/%(pool)s/volumeGroups/%(group)s'
'?fields=bytesAvailable,bytesUsed') % {
'pool': self.storage_pool,
'group': self.volume_group,
}
@ -533,3 +467,18 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
'iscsi_target_portal_port': self.iscsi_target_portal_port,
'nef_url': self.nef.url
}
def _fill_volumes(self, tg_name):
url = ('san/lunMappings?targetGroup=%s&fields=volume'
'&limit=50000' % tg_name)
self.volumes[tg_name] = {
mapping['volume'] for mapping in self.nef.get(url)['data']}
def _create_target_group(self, tg_name, target_name):
# Create new target group
url = 'san/targetgroups'
data = {
'name': tg_name,
'members': [target_name]
}
self.nef.post(url, data)

View File

@ -1,4 +1,4 @@
# Copyright 2011 Nexenta Systems, Inc.
# Copyright 2016 Nexenta Systems, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -13,84 +13,188 @@
# License for the specific language governing permissions and limitations
# under the License.
import base64
import json
import requests
import time
from oslo_log import log as logging
from oslo_serialization import jsonutils
import requests
from cinder import exception
from cinder.i18n import _
from cinder.utils import retry
LOG = logging.getLogger(__name__)
TIMEOUT = 60
def check_error(response):
code = response.status_code
if code not in (200, 201, 202):
reason = response.reason
body = response.content
try:
content = jsonutils.loads(body) if body else None
except ValueError:
raise exception.VolumeBackendAPIException(
data=_(
'Could not parse response: %(code)s %(reason)s '
'%(content)s') % {
'code': code, 'reason': reason, 'content': body})
if content and 'code' in content:
raise exception.NexentaException(content)
raise exception.VolumeBackendAPIException(
data=_(
'Got bad response: %(code)s %(reason)s %(content)s') % {
'code': code, 'reason': reason, 'content': content})
class RESTCaller(object):
retry_exc_tuple = (
requests.exceptions.ConnectionError,
requests.exceptions.ConnectTimeout
)
def __init__(self, proxy, method):
self.__proxy = proxy
self.__method = method
def get_full_url(self, path):
return '/'.join((self.__proxy.url, path))
@retry(retry_exc_tuple, interval=1, retries=6)
def __call__(self, *args):
url = self.get_full_url(args[0])
kwargs = {'timeout': TIMEOUT, 'verify': False}
data = None
if len(args) > 1:
data = args[1]
kwargs['json'] = data
LOG.debug('Sending JSON data: %s, method: %s, data: %s',
url, self.__method, data)
response = getattr(self.__proxy.session, self.__method)(url, **kwargs)
check_error(response)
content = (jsonutils.loads(response.content)
if response.content else None)
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
'code': response.status_code,
'reason': response.reason,
'content': content})
if response.status_code == 202 and content:
url = self.get_full_url(content['links'][0]['href'])
keep_going = True
while keep_going:
time.sleep(1)
response = self.__proxy.session.get(url, verify=False)
check_error(response)
LOG.debug("Got response: %(code)s %(reason)s", {
'code': response.status_code,
'reason': response.reason})
content = response.json() if response.content else None
keep_going = response.status_code == 202
return content
class HTTPSAuth(requests.auth.AuthBase):
def __init__(self, url, username, password):
self.url = url
self.username = username
self.password = password
self.token = None
def __eq__(self, other):
return all([
self.url == getattr(other, 'url', None),
self.username == getattr(other, 'username', None),
self.password == getattr(other, 'password', None),
self.token == getattr(other, 'token', None)
])
def __ne__(self, other):
return not self == other
def handle_401(self, r, **kwargs):
if r.status_code == 401:
LOG.debug('Got 401. Trying to reauth...')
self.token = self.https_auth()
# Consume content and release the original connection
# to allow our new request to reuse the same one.
r.content
r.close()
prep = r.request.copy()
requests.cookies.extract_cookies_to_jar(
prep._cookies, r.request, r.raw)
prep.prepare_cookies(prep._cookies)
prep.headers['Authorization'] = 'Bearer %s' % self.token
_r = r.connection.send(prep, **kwargs)
_r.history.append(r)
_r.request = prep
return _r
return r
def __call__(self, r):
if not self.token:
self.token = self.https_auth()
r.headers['Authorization'] = 'Bearer %s' % self.token
r.register_hook('response', self.handle_401)
return r
def https_auth(self):
LOG.debug('Sending auth request...')
url = '/'.join((self.url, 'auth/login'))
headers = {'Content-Type': 'application/json'}
data = {'username': self.username, 'password': self.password}
response = requests.post(url, json=data, verify=False,
headers=headers, timeout=TIMEOUT)
check_error(response)
response.close()
if response.content:
content = jsonutils.loads(response.content)
token = content['token']
del content['token']
LOG.debug("Got response: %(code)s %(reason)s %(content)s", {
'code': response.status_code,
'reason': response.reason,
'content': content})
return token
raise exception.VolumeBackendAPIException(
data=_(
'Got bad response: %(code)s %(reason)s') % {
'code': response.status_code, 'reason': response.reason})
class NexentaJSONProxy(object):
def __init__(self, scheme, host, port, user,
password, auto=False, method=None):
self.scheme = scheme
def __init__(self, host, port, user, password, use_https):
self.session = requests.Session()
self.session.headers.update({'Content-Type': 'application/json'})
self.host = host
self.port = port
self.user = user
self.password = password
self.auto = True
self.method = method
if use_https:
self.scheme = 'https'
self.port = port if port else 8443
self.session.auth = HTTPSAuth(self.url, user, password)
else:
self.scheme = 'http'
self.port = port if port else 8080
self.session.auth = (user, password)
@property
def url(self):
return '%s://%s:%s/' % (self.scheme, self.host, self.port)
return '%(scheme)s://%(host)s:%(port)s' % {
'scheme': self.scheme,
'host': self.host,
'port': self.port}
def __getattr__(self, method=None):
if method:
return NexentaJSONProxy(
self.scheme, self.host, self.port,
self.user, self.password, self.auto, method)
def __hash__(self):
return self.url.__hash__()
def __getattr__(self, name):
if name in ('get', 'post', 'put', 'delete'):
return RESTCaller(self, name)
return super(NexentaJSONProxy, self).__getattribute__(name)
def __repr__(self):
return 'NEF proxy: %s' % self.url
def __call__(self, path, data=None):
auth = base64.b64encode(
('%s:%s' % (self.user, self.password)).encode('utf-8'))[:-1]
headers = {
'Content-Type': 'application/json',
'Authorization': 'Basic %s' % auth
}
url = self.url + path
if data:
data = jsonutils.dumps(data)
LOG.debug('Sending JSON to url: %s, data: %s, method: %s',
path, data, self.method)
resp = getattr(requests, self.method)(url, data=data, headers=headers)
if resp.status_code == 201 or (
resp.status_code == 200 and not resp.content):
LOG.debug('Got response: Success')
return 'Success'
response = json.loads(resp.content)
resp.close()
if response and resp.status_code == 202:
url = self.url + response['links'][0]['href']
while resp.status_code == 202:
time.sleep(1)
resp = requests.get(url)
if resp.status_code == 201 or (
resp.status_code == 200 and not resp.content):
LOG.debug('Got response: Success')
return 'Success'
else:
response = json.loads(resp.content)
resp.close()
if response.get('code'):
raise exception.NexentaException(response)
LOG.debug('Got response: %s', response)
return response
return 'HTTP JSON proxy: %s' % self.url

View File

@ -28,16 +28,18 @@ from cinder.volume.drivers.nexenta import options
from cinder.volume.drivers.nexenta import utils
from cinder.volume.drivers import nfs
VERSION = '1.0.0'
VERSION = '1.1.0'
LOG = logging.getLogger(__name__)
@interface.volumedriver
class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
class NexentaNfsDriver(nfs.NfsDriver):
"""Executes volume driver commands on Nexenta Appliance.
Version history:
1.0.0 - Initial driver version.
1.1.0 - Added HTTPS support.
Added use of sessions for REST calls.
"""
driver_prefix = 'nexenta'
@ -65,7 +67,7 @@ class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
self.configuration.nexenta_dataset_description)
self.sparsed_volumes = self.configuration.nexenta_sparsed_volumes
self.nef = None
self.nef_protocol = self.configuration.nexenta_rest_protocol
self.use_https = self.configuration.nexenta_use_https
self.nef_host = self.configuration.nas_host
self.share = self.configuration.nas_share_path
self.nef_port = self.configuration.nexenta_rest_port
@ -82,13 +84,9 @@ class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
return backend_name
def do_setup(self, context):
if self.nef_protocol == 'auto':
protocol, auto = 'http', True
else:
protocol, auto = self.nef_protocol, False
self.nef = jsonrpc.NexentaJSONProxy(
protocol, self.nef_host, self.nef_port, self.nef_user,
self.nef_password, auto=auto)
self.nef_host, self.nef_port, self.nef_user,
self.nef_password, self.use_https)
def check_for_setup_error(self):
"""Verify that the volume for our folder exists.
@ -97,14 +95,9 @@ class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
"""
pool_name, fs = self._get_share_datasets(self.share)
url = 'storage/pools/%s' % (pool_name)
if not self.nef.get(url):
raise LookupError(_("Pool %s does not exist in Nexenta "
"Store appliance") % pool_name)
url = 'storage/pools/%s/filesystems/%s' % (
pool_name, fs)
if not self.nef.get(url):
raise LookupError(_("filesystem %s does not exist in "
"Nexenta Store appliance") % fs)
self.nef.get(url)
url = 'storage/pools/%s/filesystems/%s' % (pool_name, fs)
self.nef.get(url)
path = '/'.join([pool_name, fs])
shared = False
@ -285,9 +278,6 @@ class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
path = '/'.join([pool, fs, volume['name']])
data = {'targetPath': path}
self.nef.post(url, data)
path = '%2F'.join([pool, fs, volume['name']])
url = 'storage/filesystems/%s/promote' % path
self.nef.post(url)
try:
self._share_folder(fs, volume['name'])

View File

@ -51,12 +51,17 @@ NEXENTA_CONNECTION_OPTS = [
default='',
help='IP address of Nexenta SA'),
cfg.IntOpt('nexenta_rest_port',
default=8080,
help='HTTP port to connect to Nexenta REST API server'),
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 '
'is used'),
cfg.StrOpt('nexenta_rest_protocol',
default='auto',
choices=['http', 'https', 'auto'],
help='Use http or https for REST connection (default auto)'),
cfg.BoolOpt('nexenta_use_https',
default=True,
help='Use secure HTTP for REST connection (default True)'),
cfg.StrOpt('nexenta_user',
default='admin',
help='User name to connect to Nexenta SA'),

View File

@ -0,0 +1,4 @@
---
features:
- Added secure HTTP support for REST API calls in the NexentaStor5 driver.
Use of HTTPS is set True by default with option nexenta_use_https.