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:
parent
a1ed8a6b29
commit
51368daf42
@ -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'))
|
||||
|
@ -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))
|
@ -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'))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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'),
|
||||
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user