Nexenta: Add NBD driver for NexentaEdge.
DocImpact Implements: blueprint nexentaedge-ndb-driver Change-Id: Iead87f00d75bfa93a06c7d2c1c6ab708bc5d804b
This commit is contained in:
parent
45d0f6270c
commit
ca9e590f82
518
cinder/tests/unit/test_nexenta_edge_nbd.py
Normal file
518
cinder/tests/unit/test_nexenta_edge_nbd.py
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from mock import patch
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
from oslo_utils import units
|
||||||
|
|
||||||
|
from cinder import context
|
||||||
|
from cinder import exception
|
||||||
|
from cinder import test
|
||||||
|
from cinder.volume import configuration as conf
|
||||||
|
from cinder.volume.drivers.nexenta.nexentaedge import nbd
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse(object):
|
||||||
|
|
||||||
|
def __init__(self, response):
|
||||||
|
self.response = response
|
||||||
|
super(FakeResponse, self).__init__()
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self.response
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequestParams(object):
|
||||||
|
def __init__(self, scheme, host, port, user, password):
|
||||||
|
self.scheme = scheme.lower()
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def url(self, path=''):
|
||||||
|
return '%s://%s:%s/%s' % (
|
||||||
|
self.scheme, self.host, self.port, path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
auth = base64.b64encode(
|
||||||
|
('%s:%s' % (self.user, self.password)).encode('utf-8'))
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic %s' % auth
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def build_post_args(self, args):
|
||||||
|
return jsonutils.dumps(args)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNexentaEdgeNBDDriver(test.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
def _safe_get(opt):
|
||||||
|
return getattr(self.cfg, opt)
|
||||||
|
super(TestNexentaEdgeNBDDriver, self).setUp()
|
||||||
|
self.cfg = mock.Mock(spec=conf.Configuration)
|
||||||
|
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
|
||||||
|
self.cfg.trace_flags = 'fake_trace_flags'
|
||||||
|
self.cfg.driver_data_namespace = 'fake_driver_data_namespace'
|
||||||
|
self.cfg.nexenta_rest_protocol = 'http'
|
||||||
|
self.cfg.nexenta_rest_address = '127.0.0.1'
|
||||||
|
self.cfg.nexenta_rest_port = 8080
|
||||||
|
self.cfg.nexenta_rest_user = 'admin'
|
||||||
|
self.cfg.nexenta_rest_password = '0'
|
||||||
|
self.cfg.nexenta_lun_container = 'cluster/tenant/bucket'
|
||||||
|
self.cfg.nexenta_nbd_symlinks_dir = '/dev/disk/by-path'
|
||||||
|
self.cfg.volume_dd_blocksize = 512
|
||||||
|
self.cfg.nexenta_blocksize = 512
|
||||||
|
self.cfg.nexenta_chunksize = 4096
|
||||||
|
self.cfg.reserved_percentage = 0
|
||||||
|
|
||||||
|
self.ctx = context.get_admin_context()
|
||||||
|
self.drv = nbd.NexentaEdgeNBDDriver(configuration=self.cfg)
|
||||||
|
self.drv.do_setup(self.ctx)
|
||||||
|
|
||||||
|
self.request_params = RequestParams(
|
||||||
|
'http', self.cfg.nexenta_rest_address, self.cfg.nexenta_rest_port,
|
||||||
|
self.cfg.nexenta_rest_user, self.cfg.nexenta_rest_password)
|
||||||
|
|
||||||
|
def test_check_do_setup__symlinks_dir_not_specified(self):
|
||||||
|
self.drv.symlinks_dir = None
|
||||||
|
self.assertRaises(
|
||||||
|
exception.NexentaException, self.drv.check_for_setup_error)
|
||||||
|
|
||||||
|
def test_check_do_setup__symlinks_dir_doesnt_exist(self):
|
||||||
|
self.drv.symlinks_dir = '/some/random/path'
|
||||||
|
self.assertRaises(
|
||||||
|
exception.NexentaException, self.drv.check_for_setup_error)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
@patch('os.path.exists')
|
||||||
|
def test_check_do_setup__empty_response(self, exists, get):
|
||||||
|
get.return_value = FakeResponse({})
|
||||||
|
exists.return_value = True
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.drv.check_for_setup_error)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
@patch('os.path.exists')
|
||||||
|
def test_check_do_setup(self, exists, get):
|
||||||
|
get.return_value = FakeResponse({'response': 'OK'})
|
||||||
|
exists.return_value = True
|
||||||
|
self.drv.check_for_setup_error()
|
||||||
|
get.assert_any_call(
|
||||||
|
self.request_params.url(self.drv.bucket_url + '/objects/'),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
def test_local_path__error(self):
|
||||||
|
self.drv._get_nbd_number = lambda volume_: -1
|
||||||
|
volume = {'name': 'volume'}
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.drv.local_path, volume)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_local_path(self, get):
|
||||||
|
volume = {
|
||||||
|
'name': 'volume',
|
||||||
|
'host': 'myhost@backend#pool'
|
||||||
|
}
|
||||||
|
_get_host_info__response = {
|
||||||
|
'stats': {
|
||||||
|
'servers': {
|
||||||
|
'host1': {
|
||||||
|
'hostname': 'host1',
|
||||||
|
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
|
||||||
|
'host2': {
|
||||||
|
'hostname': 'myhost',
|
||||||
|
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_get_nbd_devices__response = {
|
||||||
|
'value': jsonutils.dumps([
|
||||||
|
{
|
||||||
|
'objectPath': '/'.join(
|
||||||
|
(self.cfg.nexenta_lun_container, 'some_volume')),
|
||||||
|
'number': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'objectPath': '/'.join(
|
||||||
|
(self.cfg.nexenta_lun_container, volume['name'])),
|
||||||
|
'number': 2
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def my_side_effect(*args, **kwargs):
|
||||||
|
if args[0] == self.request_params.url('system/stats'):
|
||||||
|
return FakeResponse({'response': _get_host_info__response})
|
||||||
|
elif args[0].startswith(
|
||||||
|
self.request_params.url('sysconfig/nbd/devices')):
|
||||||
|
return FakeResponse({'response': _get_nbd_devices__response})
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected request')
|
||||||
|
|
||||||
|
get.side_effect = my_side_effect
|
||||||
|
self.drv.local_path(volume)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_local_path__host_not_found(self, get):
|
||||||
|
volume = {
|
||||||
|
'name': 'volume',
|
||||||
|
'host': 'unknown-host@backend#pool'
|
||||||
|
}
|
||||||
|
_get_host_info__response = {
|
||||||
|
'stats': {
|
||||||
|
'servers': {
|
||||||
|
'host1': {
|
||||||
|
'hostname': 'host1',
|
||||||
|
'ipv6addr': 'fe80::fc16:3eff:fedb:bd69'},
|
||||||
|
'host2': {
|
||||||
|
'hostname': 'myhost',
|
||||||
|
'ipv6addr': 'fe80::fc16:3eff:fedb:bd68'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_get_nbd_devices__response = {
|
||||||
|
'value': jsonutils.dumps([
|
||||||
|
{
|
||||||
|
'objectPath': '/'.join(
|
||||||
|
(self.cfg.nexenta_lun_container, 'some_volume')),
|
||||||
|
'number': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'objectPath': '/'.join(
|
||||||
|
(self.cfg.nexenta_lun_container, volume['name'])),
|
||||||
|
'number': 2
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
def my_side_effect(*args, **kwargs):
|
||||||
|
if args[0] == self.request_params.url('system/stats'):
|
||||||
|
return FakeResponse({'response': _get_host_info__response})
|
||||||
|
elif args[0].startswith(
|
||||||
|
self.request_params.url('sysconfig/nbd/devices')):
|
||||||
|
return FakeResponse({'response': _get_nbd_devices__response})
|
||||||
|
else:
|
||||||
|
raise Exception('Unexpected request')
|
||||||
|
|
||||||
|
get.side_effect = my_side_effect
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.drv.local_path, volume)
|
||||||
|
|
||||||
|
@patch('cinder.utils.execute')
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_create_volume(self, post, execute):
|
||||||
|
post.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
number = 5
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv._get_nbd_number = lambda volume_: number
|
||||||
|
self.drv.create_volume(volume)
|
||||||
|
post.assert_called_with(
|
||||||
|
self.request_params.url('nbd' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
volume['name'])),
|
||||||
|
'volSizeMB': volume['size'] * units.Ki,
|
||||||
|
'blockSize': self.cfg.nexenta_blocksize,
|
||||||
|
'chunkSize': self.cfg.nexenta_chunksize}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.delete')
|
||||||
|
def test_delete_volume(self, delete):
|
||||||
|
delete.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
number = 5
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv._get_nbd_number = lambda volume_: number
|
||||||
|
self.drv.delete_volume(volume)
|
||||||
|
delete.assert_called_with(
|
||||||
|
self.request_params.url('nbd' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
volume['name'])),
|
||||||
|
'number': number}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.delete')
|
||||||
|
def test_delete_volume__not_found(self, delete):
|
||||||
|
delete.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv._get_nbd_number = lambda volume_: -1
|
||||||
|
self.drv.delete_volume(volume)
|
||||||
|
delete.assert_not_called()
|
||||||
|
|
||||||
|
@patch('requests.put')
|
||||||
|
def test_extend_volume(self, put):
|
||||||
|
put.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
new_size = 5
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv.extend_volume(volume, new_size)
|
||||||
|
put.assert_called_with(
|
||||||
|
self.request_params.url('nbd/resize' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
volume['name'])),
|
||||||
|
'newSizeMB': new_size * units.Ki}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_create_snapshot(self, post):
|
||||||
|
post.returning_value = FakeResponse({})
|
||||||
|
snapshot = {
|
||||||
|
'name': 'dsfsdsdgfdf',
|
||||||
|
'volume_name': 'volume'
|
||||||
|
}
|
||||||
|
self.drv.create_snapshot(snapshot)
|
||||||
|
post.assert_called_with(
|
||||||
|
self.request_params.url('nbd/snapshot'),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
snapshot['volume_name'])),
|
||||||
|
'snapName': snapshot['name']}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.delete')
|
||||||
|
def test_delete_snapshot(self, delete):
|
||||||
|
delete.returning_value = FakeResponse({})
|
||||||
|
snapshot = {
|
||||||
|
'name': 'dsfsdsdgfdf',
|
||||||
|
'volume_name': 'volume'
|
||||||
|
}
|
||||||
|
self.drv.delete_snapshot(snapshot)
|
||||||
|
delete.assert_called_with(
|
||||||
|
self.request_params.url('nbd/snapshot'),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
snapshot['volume_name'])),
|
||||||
|
'snapName': snapshot['name']}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.put')
|
||||||
|
def test_create_volume_from_snapshot(self, put):
|
||||||
|
put.returning_value = FakeResponse({})
|
||||||
|
snapshot = {
|
||||||
|
'name': 'dsfsdsdgfdf',
|
||||||
|
'volume_size': 1,
|
||||||
|
'volume_name': 'volume'
|
||||||
|
}
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 2,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv.extend_volume = lambda v, s: None
|
||||||
|
self.drv.create_volume_from_snapshot(volume, snapshot)
|
||||||
|
put.assert_called_with(
|
||||||
|
self.request_params.url('nbd/snapshot/clone' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
snapshot['volume_name'])),
|
||||||
|
'snapName': snapshot['name'],
|
||||||
|
'clonePath': '/'.join((self.cfg.nexenta_lun_container,
|
||||||
|
volume['name']))
|
||||||
|
}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_create_cloned_volume(self, post):
|
||||||
|
post.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
src_vref = {
|
||||||
|
'size': 1,
|
||||||
|
'name': 'qwerty'
|
||||||
|
}
|
||||||
|
container = self.cfg.nexenta_lun_container
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv.create_cloned_volume(volume, src_vref)
|
||||||
|
post.assert_called_with(
|
||||||
|
self.request_params.url('nbd' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((container, volume['name'])),
|
||||||
|
'volSizeMB': src_vref['size'] * units.Ki,
|
||||||
|
'blockSize': self.cfg.nexenta_blocksize,
|
||||||
|
'chunkSize': self.cfg.nexenta_chunksize
|
||||||
|
}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_create_cloned_volume_gt_src(self, post):
|
||||||
|
post.returning_value = FakeResponse({})
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 2,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
src_vref = {
|
||||||
|
'size': 1,
|
||||||
|
'name': 'qwerty'
|
||||||
|
}
|
||||||
|
container = self.cfg.nexenta_lun_container
|
||||||
|
remote_url = ''
|
||||||
|
self.drv._get_remote_url = lambda host_: remote_url
|
||||||
|
self.drv.create_cloned_volume(volume, src_vref)
|
||||||
|
post.assert_called_with(
|
||||||
|
self.request_params.url('nbd' + remote_url),
|
||||||
|
data=self.request_params.build_post_args({
|
||||||
|
'objectPath': '/'.join((container, volume['name'])),
|
||||||
|
'volSizeMB': volume['size'] * units.Ki,
|
||||||
|
'blockSize': self.cfg.nexenta_blocksize,
|
||||||
|
'chunkSize': self.cfg.nexenta_chunksize
|
||||||
|
}),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_get_volume_stats(self, get):
|
||||||
|
self.cfg.volume_backend_name = None
|
||||||
|
get.return_value = FakeResponse({
|
||||||
|
'response': {
|
||||||
|
'stats': {
|
||||||
|
'summary': {
|
||||||
|
'total_capacity': units.Gi,
|
||||||
|
'total_available': units.Gi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||||
|
'driver': self.drv.__class__.__name__,
|
||||||
|
'host': socket.gethostname(),
|
||||||
|
'bucket': self.cfg.nexenta_lun_container
|
||||||
|
}
|
||||||
|
expected = {
|
||||||
|
'vendor_name': 'Nexenta',
|
||||||
|
'driver_version': self.drv.VERSION,
|
||||||
|
'storage_protocol': 'NBD',
|
||||||
|
'reserved_percentage': self.cfg.reserved_percentage,
|
||||||
|
'total_capacity_gb': 1,
|
||||||
|
'free_capacity_gb': 1,
|
||||||
|
'QoS_support': False,
|
||||||
|
'volume_backend_name': self.drv.__class__.__name__,
|
||||||
|
'location_info': location_info,
|
||||||
|
'restapi_url': self.request_params.url()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(expected, self.drv.get_volume_stats())
|
||||||
|
|
||||||
|
@patch('cinder.image.image_utils.fetch_to_raw')
|
||||||
|
def test_copy_image_to_volume(self, fetch_to_raw):
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
self.drv.local_path = lambda host: 'local_path'
|
||||||
|
self.drv.copy_image_to_volume(self.ctx, volume, 'image_service',
|
||||||
|
'image_id')
|
||||||
|
fetch_to_raw.assert_called_with(
|
||||||
|
self.ctx, 'image_service', 'image_id', 'local_path',
|
||||||
|
self.cfg.volume_dd_blocksize, size=volume['size'])
|
||||||
|
|
||||||
|
@patch('cinder.image.image_utils.upload_volume')
|
||||||
|
def test_copy_volume_to_image(self, upload_volume):
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
self.drv.local_path = lambda host: 'local_path'
|
||||||
|
self.drv.copy_volume_to_image(self.ctx, volume, 'image_service',
|
||||||
|
'image_meta')
|
||||||
|
upload_volume.assert_called_with(
|
||||||
|
self.ctx, 'image_service', 'image_meta', 'local_path')
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_validate_connector(self, get):
|
||||||
|
connector = {'host': 'host2'}
|
||||||
|
r = {
|
||||||
|
'stats': {
|
||||||
|
'servers': {
|
||||||
|
'host1': {'hostname': 'host1'},
|
||||||
|
'host2': {'hostname': 'host2'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get.return_value = FakeResponse({'response': r})
|
||||||
|
self.drv.validate_connector(connector)
|
||||||
|
get.assert_called_with(self.request_params.url('system/stats'),
|
||||||
|
headers=self.request_params.headers)
|
||||||
|
|
||||||
|
@patch('requests.get')
|
||||||
|
def test_validate_connector__host_not_found(self, get):
|
||||||
|
connector = {'host': 'host3'}
|
||||||
|
r = {
|
||||||
|
'stats': {
|
||||||
|
'servers': {
|
||||||
|
'host1': {'hostname': 'host1'},
|
||||||
|
'host2': {'hostname': 'host2'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get.return_value = FakeResponse({'response': r})
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.drv.validate_connector, connector)
|
||||||
|
|
||||||
|
def test_initialize_connection(self):
|
||||||
|
connector = {'host': 'host'}
|
||||||
|
volume = {
|
||||||
|
'host': 'host@backend#pool info',
|
||||||
|
'size': 1,
|
||||||
|
'name': 'volume'
|
||||||
|
}
|
||||||
|
self.drv.local_path = lambda host: 'local_path'
|
||||||
|
self.assertEqual({
|
||||||
|
'driver_volume_type': 'local',
|
||||||
|
'data': {'device_path': 'local_path'}},
|
||||||
|
self.drv.initialize_connection(volume, connector))
|
@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
import socket
|
import socket
|
||||||
@ -73,22 +74,27 @@ class NexentaEdgeJSONProxy(object):
|
|||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
data = json.dumps(args[1])
|
data = json.dumps(args[1])
|
||||||
|
|
||||||
auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1]
|
auth = base64.b64encode(
|
||||||
|
('%s:%s' % (self.user, self.password)).encode('utf-8'))
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Basic %s' % auth
|
'Authorization': 'Basic %s' % auth
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug('Sending JSON data: %s, data: %s', self.url, data)
|
LOG.debug('Sending JSON data: %s, method: %s, data: %s', self.url,
|
||||||
|
self.method, data)
|
||||||
|
|
||||||
if self.method == 'get':
|
if self.method == 'get':
|
||||||
req = requests.get(self.url, headers=headers)
|
req = requests.get(self.url, headers=headers)
|
||||||
if self.method == 'post':
|
elif self.method == 'post':
|
||||||
req = requests.post(self.url, data=data, headers=headers)
|
req = requests.post(self.url, data=data, headers=headers)
|
||||||
if self.method == 'put':
|
elif self.method == 'put':
|
||||||
req = requests.put(self.url, data=data, headers=headers)
|
req = requests.put(self.url, data=data, headers=headers)
|
||||||
if self.method == 'delete':
|
elif self.method == 'delete':
|
||||||
req = requests.delete(self.url, data=data, headers=headers)
|
req = requests.delete(self.url, data=data, headers=headers)
|
||||||
|
else:
|
||||||
|
raise exception.VolumeDriverException(
|
||||||
|
message=_('Unsupported method: %s') % self.method)
|
||||||
|
|
||||||
rsp = req.json()
|
rsp = req.json()
|
||||||
req.close()
|
req.close()
|
||||||
@ -96,5 +102,5 @@ class NexentaEdgeJSONProxy(object):
|
|||||||
LOG.debug('Got response: %s', rsp)
|
LOG.debug('Got response: %s', rsp)
|
||||||
if rsp.get('response') is None:
|
if rsp.get('response') is None:
|
||||||
raise exception.VolumeBackendAPIException(
|
raise exception.VolumeBackendAPIException(
|
||||||
_('Error response: %s') % rsp)
|
data=_('Error response: %s') % rsp)
|
||||||
return rsp.get('response')
|
return rsp.get('response')
|
||||||
|
346
cinder/volume/drivers/nexenta/nexentaedge/nbd.py
Normal file
346
cinder/volume/drivers/nexenta/nexentaedge/nbd.py
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import six
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import units
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _, _LE, _LI
|
||||||
|
from cinder.image import image_utils
|
||||||
|
from cinder import interface
|
||||||
|
from cinder import utils as cinder_utils
|
||||||
|
from cinder.volume import driver
|
||||||
|
from cinder.volume.drivers.nexenta.nexentaedge import jsonrpc
|
||||||
|
from cinder.volume.drivers.nexenta import options
|
||||||
|
from cinder.volume.drivers.nexenta import utils as nexenta_utils
|
||||||
|
from cinder.volume import utils as volutils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@interface.volumedriver
|
||||||
|
class NexentaEdgeNBDDriver(driver.VolumeDriver):
|
||||||
|
"""Executes commands relating to NBD Volumes.
|
||||||
|
|
||||||
|
Version history:
|
||||||
|
1.0.0 - Initial driver version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSION = '1.0.0'
|
||||||
|
|
||||||
|
def __init__(self, vg_obj=None, *args, **kwargs):
|
||||||
|
LOG.debug('NexentaEdgeNBDDriver. Trying to initialize.')
|
||||||
|
super(NexentaEdgeNBDDriver, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.configuration:
|
||||||
|
self.configuration.append_config_values(
|
||||||
|
options.NEXENTA_CONNECTION_OPTS)
|
||||||
|
self.configuration.append_config_values(
|
||||||
|
options.NEXENTA_DATASET_OPTS)
|
||||||
|
self.configuration.append_config_values(
|
||||||
|
options.NEXENTA_EDGE_OPTS)
|
||||||
|
self.restapi_protocol = self.configuration.nexenta_rest_protocol
|
||||||
|
self.restapi_host = self.configuration.nexenta_rest_address
|
||||||
|
self.restapi_port = self.configuration.nexenta_rest_port
|
||||||
|
self.restapi_user = self.configuration.nexenta_rest_user
|
||||||
|
self.restapi_password = self.configuration.nexenta_rest_password
|
||||||
|
self.bucket_path = self.configuration.nexenta_lun_container
|
||||||
|
self.blocksize = self.configuration.nexenta_blocksize
|
||||||
|
self.chunksize = self.configuration.nexenta_chunksize
|
||||||
|
self.cluster, self.tenant, self.bucket = self.bucket_path.split('/')
|
||||||
|
self.bucket_url = ('clusters/' + self.cluster + '/tenants/' +
|
||||||
|
self.tenant + '/buckets/' + self.bucket)
|
||||||
|
self.hostname = socket.gethostname()
|
||||||
|
self.symlinks_dir = self.configuration.nexenta_nbd_symlinks_dir
|
||||||
|
self.reserved_percentage = self.configuration.reserved_percentage
|
||||||
|
LOG.debug('NexentaEdgeNBDDriver. Initialized successfully.')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_name(self):
|
||||||
|
backend_name = None
|
||||||
|
if self.configuration:
|
||||||
|
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||||
|
if not backend_name:
|
||||||
|
backend_name = self.__class__.__name__
|
||||||
|
return backend_name
|
||||||
|
|
||||||
|
def do_setup(self, context):
|
||||||
|
if self.restapi_protocol == 'auto':
|
||||||
|
protocol, auto = 'http', True
|
||||||
|
else:
|
||||||
|
protocol, auto = self.restapi_protocol, False
|
||||||
|
|
||||||
|
self.restapi = jsonrpc.NexentaEdgeJSONProxy(
|
||||||
|
protocol, self.restapi_host, self.restapi_port, '',
|
||||||
|
self.restapi_user, self.restapi_password, auto=auto)
|
||||||
|
|
||||||
|
def check_for_setup_error(self):
|
||||||
|
try:
|
||||||
|
if not self.symlinks_dir:
|
||||||
|
msg = _("nexenta_nbd_symlinks_dir option is not specified")
|
||||||
|
raise exception.NexentaException(message=msg)
|
||||||
|
if not os.path.exists(self.symlinks_dir):
|
||||||
|
msg = _("NexentaEdge NBD symlinks directory doesn't exist")
|
||||||
|
raise exception.NexentaException(message=msg)
|
||||||
|
self.restapi.get(self.bucket_url + '/objects/')
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error verifying container %(bkt)s'),
|
||||||
|
{'bkt': self.bucket_path})
|
||||||
|
|
||||||
|
def _get_nbd_devices(self, host):
|
||||||
|
try:
|
||||||
|
rsp = self.restapi.get('sysconfig/nbd/devices' +
|
||||||
|
self._get_remote_url(host))
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error getting NBD list'))
|
||||||
|
return json.loads(rsp['value'])
|
||||||
|
|
||||||
|
def _get_nbd_number(self, volume):
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
nbds = self._get_nbd_devices(host)
|
||||||
|
for dev in nbds:
|
||||||
|
if dev['objectPath'] == self.bucket_path + '/' + volume['name']:
|
||||||
|
return dev['number']
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def _get_host_info(self, host):
|
||||||
|
try:
|
||||||
|
res = self.restapi.get('system/stats')
|
||||||
|
servers = res['stats']['servers']
|
||||||
|
for sid in servers:
|
||||||
|
if host == sid or host == servers[sid]['hostname']:
|
||||||
|
return servers[sid]
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error getting host info'))
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=_('No %s hostname in NEdge cluster') % host)
|
||||||
|
|
||||||
|
def _get_remote_url(self, host):
|
||||||
|
return '?remote=' + (
|
||||||
|
six.text_type(self._get_host_info(host)['ipv6addr']))
|
||||||
|
|
||||||
|
def _get_symlink_path(self, number):
|
||||||
|
return os.path.join(self.symlinks_dir, 'nbd' + six.text_type(number))
|
||||||
|
|
||||||
|
def local_path(self, volume):
|
||||||
|
number = self._get_nbd_number(volume)
|
||||||
|
if number == -1:
|
||||||
|
msg = _('No NBD device for volume %s') % volume['name']
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return self._get_symlink_path(number)
|
||||||
|
|
||||||
|
def create_volume(self, volume):
|
||||||
|
LOG.debug('Create volume')
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
try:
|
||||||
|
self.restapi.post('nbd' + self._get_remote_url(host), {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'volSizeMB': int(volume['size']) * units.Ki,
|
||||||
|
'blockSize': self.blocksize,
|
||||||
|
'chunkSize': self.chunksize
|
||||||
|
})
|
||||||
|
number = self._get_nbd_number(volume)
|
||||||
|
cinder_utils.execute(
|
||||||
|
'ln', '--symbolic', '--force',
|
||||||
|
'/dev/nbd' + six.text_type(number),
|
||||||
|
self._get_symlink_path(number), run_as_root=True,
|
||||||
|
check_exit_code=True)
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating volume'))
|
||||||
|
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
LOG.debug('Delete volume')
|
||||||
|
number = self._get_nbd_number(volume)
|
||||||
|
if number == -1:
|
||||||
|
LOG.info(_LI('Volume %(volume)s does not exist at %(path)s '
|
||||||
|
'path') % {
|
||||||
|
'volume': volume['name'],
|
||||||
|
'path': self.bucket_path
|
||||||
|
})
|
||||||
|
return
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
try:
|
||||||
|
self.restapi.delete('nbd' + self._get_remote_url(host), {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'number': number
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error deleting volume'))
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
LOG.debug('Extend volume')
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
try:
|
||||||
|
self.restapi.put('nbd/resize' + self._get_remote_url(host), {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'newSizeMB': new_size * units.Ki
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error extending volume'))
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
LOG.debug('Create snapshot')
|
||||||
|
try:
|
||||||
|
self.restapi.post('nbd/snapshot', {
|
||||||
|
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||||
|
'snapName': snapshot['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating snapshot'))
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
LOG.debug('Delete snapshot')
|
||||||
|
# There is no way to figure out whether a snapshot exists in current
|
||||||
|
# version of the API. This REST function always reports OK even a
|
||||||
|
# snapshot doesn't exist.
|
||||||
|
try:
|
||||||
|
self.restapi.delete('nbd/snapshot', {
|
||||||
|
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||||
|
'snapName': snapshot['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error deleting snapshot'))
|
||||||
|
|
||||||
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
|
LOG.debug('Create volume from snapshot')
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
remotehost = self._get_remote_url(host)
|
||||||
|
try:
|
||||||
|
self.restapi.put('nbd/snapshot/clone' + remotehost, {
|
||||||
|
'objectPath': self.bucket_path + '/' + snapshot['volume_name'],
|
||||||
|
'snapName': snapshot['name'],
|
||||||
|
'clonePath': self.bucket_path + '/' + volume['name']
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error cloning snapshot'))
|
||||||
|
if volume['size'] > snapshot['volume_size']:
|
||||||
|
self.extend_volume(volume, volume['size'])
|
||||||
|
|
||||||
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
|
LOG.debug('Create cloned volume')
|
||||||
|
vol_url = (self.bucket_url + '/objects/' +
|
||||||
|
src_vref['name'] + '/clone')
|
||||||
|
clone_body = {
|
||||||
|
'tenant_name': self.tenant,
|
||||||
|
'bucket_name': self.bucket,
|
||||||
|
'object_name': volume['name']
|
||||||
|
}
|
||||||
|
host = volutils.extract_host(volume['host'], 'host')
|
||||||
|
size = volume['size'] if volume['size'] > src_vref['size'] else (
|
||||||
|
src_vref['size'])
|
||||||
|
try:
|
||||||
|
self.restapi.post(vol_url, clone_body)
|
||||||
|
self.restapi.post('nbd' + self._get_remote_url(host), {
|
||||||
|
'objectPath': self.bucket_path + '/' + volume['name'],
|
||||||
|
'volSizeMB': int(size) * units.Ki,
|
||||||
|
'blockSize': self.blocksize,
|
||||||
|
'chunkSize': self.chunksize
|
||||||
|
})
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating cloned volume'))
|
||||||
|
|
||||||
|
def migrate_volume(self, ctxt, volume, host, thin=False, mirror_count=0):
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
def get_volume_stats(self, refresh=False):
|
||||||
|
LOG.debug('Get volume stats')
|
||||||
|
try:
|
||||||
|
resp = self.restapi.get('system/stats')
|
||||||
|
location_info = '%(driver)s:%(host)s:%(bucket)s' % {
|
||||||
|
'driver': self.__class__.__name__,
|
||||||
|
'host': self.hostname,
|
||||||
|
'bucket': self.bucket_path
|
||||||
|
}
|
||||||
|
summary = resp['stats']['summary']
|
||||||
|
total = nexenta_utils.str2gib_size(summary['total_capacity'])
|
||||||
|
free = nexenta_utils.str2gib_size(summary['total_available'])
|
||||||
|
return {
|
||||||
|
'vendor_name': 'Nexenta',
|
||||||
|
'driver_version': self.VERSION,
|
||||||
|
'storage_protocol': 'NBD',
|
||||||
|
'reserved_percentage': self.reserved_percentage,
|
||||||
|
'total_capacity_gb': total,
|
||||||
|
'free_capacity_gb': free,
|
||||||
|
'QoS_support': False,
|
||||||
|
'volume_backend_name': self.backend_name,
|
||||||
|
'location_info': location_info,
|
||||||
|
'restapi_url': self.restapi.url
|
||||||
|
}
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error creating snapshot'))
|
||||||
|
|
||||||
|
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||||
|
LOG.debug('Copy image to volume')
|
||||||
|
image_utils.fetch_to_raw(context,
|
||||||
|
image_service,
|
||||||
|
image_id,
|
||||||
|
self.local_path(volume),
|
||||||
|
self.configuration.volume_dd_blocksize,
|
||||||
|
size=volume['size'])
|
||||||
|
|
||||||
|
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
||||||
|
LOG.debug('Copy volume to image')
|
||||||
|
image_utils.upload_volume(context,
|
||||||
|
image_service,
|
||||||
|
image_meta,
|
||||||
|
self.local_path(volume))
|
||||||
|
|
||||||
|
def ensure_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_export(self, context, volume, connector, vg=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate_connector(self, connector):
|
||||||
|
LOG.debug('Validate connector')
|
||||||
|
try:
|
||||||
|
res = self.restapi.get('system/stats')
|
||||||
|
servers = res['stats']['servers']
|
||||||
|
for sid in servers:
|
||||||
|
if (connector['host'] == sid or
|
||||||
|
connector['host'] == servers[sid]['hostname']):
|
||||||
|
return
|
||||||
|
except exception.VolumeBackendAPIException:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(_LE('Error retrieving cluster stats'))
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=_('No %s hostname in NEdge cluster') % connector['host'])
|
||||||
|
|
||||||
|
def initialize_connection(self, volume, connector, initiator_data=None):
|
||||||
|
LOG.debug('Initialize connection')
|
||||||
|
return {
|
||||||
|
'driver_volume_type': 'local',
|
||||||
|
'data': {'device_path': self.local_path(volume)},
|
||||||
|
}
|
@ -17,6 +17,10 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
|
|
||||||
NEXENTA_EDGE_OPTS = [
|
NEXENTA_EDGE_OPTS = [
|
||||||
|
cfg.StrOpt('nexenta_nbd_symlinks_dir',
|
||||||
|
default='/dev/disk/by-path',
|
||||||
|
help='NexentaEdge logical path of directory to store symbolic '
|
||||||
|
'links to NBDs'),
|
||||||
cfg.StrOpt('nexenta_rest_address',
|
cfg.StrOpt('nexenta_rest_address',
|
||||||
default='',
|
default='',
|
||||||
help='IP address of NexentaEdge management REST API endpoint'),
|
help='IP address of NexentaEdge management REST API endpoint'),
|
||||||
|
2
releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml
Normal file
2
releasenotes/notes/nexentaedge-nbd-eb48268723141f12.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
features:
|
||||||
|
- Added NBD driver for NexentaEdge.
|
Loading…
Reference in New Issue
Block a user