
DocImpact Implements: blueprint nexentaedge-ndb-driver Change-Id: Iead87f00d75bfa93a06c7d2c1c6ab708bc5d804b
519 lines
19 KiB
Python
519 lines
19 KiB
Python
# 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))
|