cinder/cinder/tests/unit/test_nexenta_edge_nbd.py
Aleksey Ruban ca9e590f82 Nexenta: Add NBD driver for NexentaEdge.
DocImpact
Implements: blueprint nexentaedge-ndb-driver

Change-Id: Iead87f00d75bfa93a06c7d2c1c6ab708bc5d804b
2016-06-22 12:52:54 -06:00

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))