614 lines
22 KiB
Python
614 lines
22 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 json
|
|
from unittest import mock
|
|
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import units
|
|
|
|
from manila import context
|
|
from manila import exception
|
|
from manila.share import configuration as conf
|
|
from manila.share.drivers.nexenta.ns4 import nexenta_nas
|
|
from manila import test
|
|
|
|
PATH_TO_RPC = 'requests.post'
|
|
CODE = mock.PropertyMock(return_value=200)
|
|
|
|
|
|
class FakeResponse(object):
|
|
|
|
def __init__(self, response={}):
|
|
self.content = json.dumps(response)
|
|
super(FakeResponse, self).__init__()
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
class RequestParams(object):
|
|
def __init__(self, scheme, host, port, path, user, password):
|
|
self.scheme = scheme.lower()
|
|
self.host = host
|
|
self.port = port
|
|
self.path = path
|
|
self.user = user
|
|
self.password = password
|
|
|
|
@property
|
|
def url(self):
|
|
return '%s://%s:%s%s' % (self.scheme, self.host, self.port, self.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, obj, method, *args):
|
|
data = jsonutils.dumps({
|
|
'object': obj,
|
|
'method': method,
|
|
'params': args,
|
|
})
|
|
return data
|
|
|
|
|
|
class TestNexentaNasDriver(test.TestCase):
|
|
|
|
def _get_share_path(self, share_name):
|
|
return '%s/%s/%s' % (self.volume, self.share, share_name)
|
|
|
|
def setUp(self):
|
|
def _safe_get(opt):
|
|
return getattr(self.cfg, opt)
|
|
|
|
self.cfg = mock.Mock(spec=conf.Configuration)
|
|
self.cfg.nexenta_nas_host = '1.1.1.1'
|
|
super(TestNexentaNasDriver, self).setUp()
|
|
|
|
self.ctx = context.get_admin_context()
|
|
self.cfg.safe_get = mock.Mock(side_effect=_safe_get)
|
|
self.cfg.nexenta_rest_port = 1000
|
|
self.cfg.reserved_share_percentage = 0
|
|
self.cfg.reserved_share_from_snapshot_percentage = 0
|
|
self.cfg.reserved_share_extend_percentage = 0
|
|
self.cfg.max_over_subscription_ratio = 0
|
|
self.cfg.nexenta_rest_protocol = 'auto'
|
|
self.cfg.nexenta_volume = 'volume'
|
|
self.cfg.nexenta_nfs_share = 'nfs_share'
|
|
self.cfg.nexenta_user = 'user'
|
|
self.cfg.nexenta_password = 'password'
|
|
self.cfg.nexenta_thin_provisioning = False
|
|
self.cfg.enabled_share_protocols = 'NFS'
|
|
self.cfg.nexenta_mount_point_base = '$state_path/mnt'
|
|
self.cfg.share_backend_name = 'NexentaStor'
|
|
self.cfg.nexenta_dataset_compression = 'on'
|
|
self.cfg.nexenta_smb = 'on'
|
|
self.cfg.nexenta_nfs = 'on'
|
|
self.cfg.nexenta_dataset_dedupe = 'on'
|
|
|
|
self.cfg.network_config_group = 'DEFAULT'
|
|
self.cfg.admin_network_config_group = (
|
|
'fake_admin_network_config_group')
|
|
self.cfg.driver_handles_share_servers = False
|
|
|
|
self.request_params = RequestParams(
|
|
'http', self.cfg.nexenta_nas_host, self.cfg.nexenta_rest_port,
|
|
'/rest/nms/', self.cfg.nexenta_user, self.cfg.nexenta_password)
|
|
|
|
self.drv = nexenta_nas.NexentaNasDriver(configuration=self.cfg)
|
|
self.drv.do_setup(self.ctx)
|
|
|
|
self.volume = self.cfg.nexenta_volume
|
|
self.share = self.cfg.nexenta_nfs_share
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_check_for_setup_error__volume_doesnt_exist(self, post):
|
|
post.return_value = FakeResponse()
|
|
|
|
self.assertRaises(
|
|
exception.NexentaException, self.drv.check_for_setup_error)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_check_for_setup_error__folder_doesnt_exist(self, post):
|
|
folder = '%s/%s' % (self.volume, self.share)
|
|
create_folder_props = {
|
|
'recordsize': '4K',
|
|
'quota': '1G',
|
|
'compression': self.cfg.nexenta_dataset_compression,
|
|
'sharesmb': self.cfg.nexenta_smb,
|
|
'sharenfs': self.cfg.nexenta_nfs,
|
|
}
|
|
|
|
share_opts = {
|
|
'read_write': '*',
|
|
'read_only': '',
|
|
'root': 'nobody',
|
|
'extra_options': 'anon=0',
|
|
'recursive': 'true',
|
|
'anonymous_rw': 'true',
|
|
}
|
|
|
|
def my_side_effect(*args, **kwargs):
|
|
if kwargs['data'] == self.request_params.build_post_args(
|
|
'volume', 'object_exists', self.volume):
|
|
return FakeResponse({'result': 'OK'})
|
|
elif kwargs['data'] == self.request_params.build_post_args(
|
|
'folder', 'object_exists', folder):
|
|
return FakeResponse()
|
|
elif kwargs['data'] == self.request_params.build_post_args(
|
|
'folder', 'create_with_props', self.volume, self.share,
|
|
create_folder_props):
|
|
return FakeResponse()
|
|
elif kwargs['data'] == self.request_params.build_post_args(
|
|
'netstorsvc', 'share_folder',
|
|
'svc:/network/nfs/server:default', folder, share_opts):
|
|
return FakeResponse()
|
|
else:
|
|
raise exception.ManilaException('Unexpected request')
|
|
post.side_effect = my_side_effect
|
|
|
|
self.assertRaises(
|
|
exception.ManilaException, self.drv.check_for_setup_error)
|
|
post.assert_any_call(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'volume', 'object_exists', self.volume),
|
|
headers=self.request_params.headers)
|
|
post.assert_any_call(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'folder', 'object_exists', folder),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_share(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
self.cfg.nexenta_thin_provisioning = False
|
|
path = '%s/%s/%s' % (self.volume, self.share, share['name'])
|
|
location = {'path': '%s:/volumes/%s' % (
|
|
self.cfg.nexenta_nas_host, path)}
|
|
post.return_value = FakeResponse()
|
|
|
|
self.assertEqual([location],
|
|
self.drv.create_share(self.ctx, share))
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_share__wrong_proto(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': 'A_VERY_WRONG_PROTO'
|
|
}
|
|
post.return_value = FakeResponse()
|
|
|
|
self.assertRaises(exception.InvalidShare, self.drv.create_share,
|
|
self.ctx, share)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_share__thin_provisioning(self, post):
|
|
share = {'name': 'share', 'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols}
|
|
create_folder_props = {
|
|
'recordsize': '4K',
|
|
'quota': '1G',
|
|
'compression': self.cfg.nexenta_dataset_compression,
|
|
}
|
|
parent_path = '%s/%s' % (self.volume, self.share)
|
|
post.return_value = FakeResponse()
|
|
self.cfg.nexenta_thin_provisioning = True
|
|
|
|
self.drv.create_share(self.ctx, share)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url,
|
|
data=self.request_params.build_post_args(
|
|
'folder',
|
|
'create_with_props',
|
|
parent_path,
|
|
share['name'],
|
|
create_folder_props),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_share__thick_provisioning(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
quota = '%sG' % share['size']
|
|
create_folder_props = {
|
|
'recordsize': '4K',
|
|
'quota': quota,
|
|
'compression': self.cfg.nexenta_dataset_compression,
|
|
'reservation': quota,
|
|
}
|
|
parent_path = '%s/%s' % (self.volume, self.share)
|
|
post.return_value = FakeResponse()
|
|
self.cfg.nexenta_thin_provisioning = False
|
|
|
|
self.drv.create_share(self.ctx, share)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url,
|
|
data=self.request_params.build_post_args(
|
|
'folder',
|
|
'create_with_props',
|
|
parent_path,
|
|
share['name'],
|
|
create_folder_props),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_share_from_snapshot(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
snapshot = {'name': 'sn1', 'share_name': share['name']}
|
|
post.return_value = FakeResponse()
|
|
path = '%s/%s/%s' % (self.volume, self.share, share['name'])
|
|
location = {'path': '%s:/volumes/%s' % (
|
|
self.cfg.nexenta_nas_host, path)}
|
|
snapshot_name = '%s/%s/%s@%s' % (
|
|
self.volume, self.share, snapshot['share_name'], snapshot['name'])
|
|
|
|
self.assertEqual([location], self.drv.create_share_from_snapshot(
|
|
self.ctx, share, snapshot))
|
|
post.assert_any_call(
|
|
self.request_params.url,
|
|
data=self.request_params.build_post_args(
|
|
'folder',
|
|
'clone',
|
|
snapshot_name,
|
|
'%s/%s/%s' % (self.volume, self.share, share['name'])),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_share(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
post.return_value = FakeResponse()
|
|
folder = '%s/%s/%s' % (self.volume, self.share, share['name'])
|
|
|
|
self.drv.delete_share(self.ctx, share)
|
|
|
|
post.assert_any_call(
|
|
self.request_params.url,
|
|
data=self.request_params.build_post_args(
|
|
'folder',
|
|
'destroy',
|
|
folder.strip(),
|
|
'-r'),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_share__exists_error(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = exception.NexentaException('does not exist')
|
|
|
|
self.drv.delete_share(self.ctx, share)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_share__some_error(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = exception.ManilaException('Some error')
|
|
|
|
self.assertRaises(
|
|
exception.ManilaException, self.drv.delete_share, self.ctx, share)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_extend_share__thin_provisoning(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
new_size = 5
|
|
quota = '%sG' % new_size
|
|
post.return_value = FakeResponse()
|
|
self.cfg.nexenta_thin_provisioning = True
|
|
|
|
self.drv.extend_share(share, new_size)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url,
|
|
data=self.request_params.build_post_args(
|
|
'folder',
|
|
'set_child_prop',
|
|
'%s/%s/%s' % (self.volume, self.share, share['name']),
|
|
'quota', quota),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_extend_share__thick_provisoning(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'size': 1,
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
new_size = 5
|
|
post.return_value = FakeResponse()
|
|
self.cfg.nexenta_thin_provisioning = False
|
|
|
|
self.drv.extend_share(share, new_size)
|
|
|
|
post.assert_not_called()
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_create_snapshot(self, post):
|
|
snapshot = {'share_name': 'share', 'name': 'share@first'}
|
|
post.return_value = FakeResponse()
|
|
folder = '%s/%s/%s' % (self.volume, self.share, snapshot['share_name'])
|
|
|
|
self.drv.create_snapshot(self.ctx, snapshot)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'folder', 'create_snapshot', folder, snapshot['name'], '-r'),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_snapshot(self, post):
|
|
snapshot = {'share_name': 'share', 'name': 'share@first'}
|
|
post.return_value = FakeResponse()
|
|
|
|
self.drv.delete_snapshot(self.ctx, snapshot)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'snapshot', 'destroy', '%s@%s' % (
|
|
self._get_share_path(snapshot['share_name']),
|
|
snapshot['name']),
|
|
''),
|
|
headers=self.request_params.headers)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_snapshot__nexenta_error_1(self, post):
|
|
snapshot = {'share_name': 'share', 'name': 'share@first'}
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = exception.NexentaException('does not exist')
|
|
|
|
self.drv.delete_snapshot(self.ctx, snapshot)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_snapshot__nexenta_error_2(self, post):
|
|
snapshot = {'share_name': 'share', 'name': 'share@first'}
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = exception.NexentaException('has dependent clones')
|
|
|
|
self.drv.delete_snapshot(self.ctx, snapshot)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_delete_snapshot__some_error(self, post):
|
|
snapshot = {'share_name': 'share', 'name': 'share@first'}
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = exception.ManilaException('Some error')
|
|
|
|
self.assertRaises(exception.ManilaException, self.drv.delete_snapshot,
|
|
self.ctx, snapshot)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_update_access__unsupported_access_type(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
access = {
|
|
'access_type': 'group',
|
|
'access_to': 'ordinary_users',
|
|
'access_level': 'rw'
|
|
}
|
|
|
|
self.assertRaises(exception.InvalidShareAccess,
|
|
self.drv.update_access,
|
|
self.ctx,
|
|
share,
|
|
[access],
|
|
None,
|
|
None)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_update_access__cidr(self, post):
|
|
share = {
|
|
'name': 'share',
|
|
'share_proto': self.cfg.enabled_share_protocols
|
|
}
|
|
access1 = {
|
|
'access_type': 'ip',
|
|
'access_to': '1.1.1.1/24',
|
|
'access_level': 'rw'
|
|
}
|
|
access2 = {
|
|
'access_type': 'ip',
|
|
'access_to': '1.2.3.4',
|
|
'access_level': 'rw'
|
|
}
|
|
access_rules = [access1, access2]
|
|
|
|
share_opts = {
|
|
'auth_type': 'none',
|
|
'read_write': '%s:%s' % (
|
|
access1['access_to'], access2['access_to']),
|
|
'read_only': '',
|
|
'recursive': 'true',
|
|
'anonymous_rw': 'true',
|
|
'anonymous': 'true',
|
|
'extra_options': 'anon=0',
|
|
}
|
|
|
|
def my_side_effect(*args, **kwargs):
|
|
if kwargs['data'] == self.request_params.build_post_args(
|
|
'netstorsvc', 'share_folder',
|
|
'svc:/network/nfs/server:default',
|
|
self._get_share_path(share['name']), share_opts):
|
|
return FakeResponse()
|
|
else:
|
|
raise exception.ManilaException('Unexpected request')
|
|
|
|
post.return_value = FakeResponse()
|
|
post.side_effect = my_side_effect
|
|
|
|
self.drv.update_access(self.ctx, share, access_rules, None, None)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'netstorsvc', 'share_folder',
|
|
'svc:/network/nfs/server:default',
|
|
self._get_share_path(share['name']), share_opts),
|
|
headers=self.request_params.headers)
|
|
self.assertRaises(exception.ManilaException, self.drv.update_access,
|
|
self.ctx, share,
|
|
[access1, {'access_type': 'ip',
|
|
'access_to': '2.2.2.2',
|
|
'access_level': 'rw'}],
|
|
None, None)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_update_access__add_one_ip_to_empty_access_list(self, post):
|
|
share = {'name': 'share',
|
|
'share_proto': self.cfg.enabled_share_protocols}
|
|
access = {
|
|
'access_type': 'ip',
|
|
'access_to': '1.1.1.1',
|
|
'access_level': 'rw'
|
|
}
|
|
|
|
rw_list = None
|
|
share_opts = {
|
|
'auth_type': 'none',
|
|
'read_write': access['access_to'],
|
|
'read_only': '',
|
|
'recursive': 'true',
|
|
'anonymous_rw': 'true',
|
|
'anonymous': 'true',
|
|
'extra_options': 'anon=0',
|
|
}
|
|
|
|
def my_side_effect(*args, **kwargs):
|
|
if kwargs['data'] == self.request_params.build_post_args(
|
|
'netstorsvc', 'get_shareopts',
|
|
'svc:/network/nfs/server:default',
|
|
self._get_share_path(share['name'])):
|
|
return FakeResponse({'result': {'read_write': rw_list}})
|
|
elif kwargs['data'] == self.request_params.build_post_args(
|
|
'netstorsvc', 'share_folder',
|
|
'svc:/network/nfs/server:default',
|
|
self._get_share_path(share['name']), share_opts):
|
|
return FakeResponse()
|
|
else:
|
|
raise exception.ManilaException('Unexpected request')
|
|
post.return_value = FakeResponse()
|
|
|
|
self.drv.update_access(self.ctx, share, [access], None, None)
|
|
|
|
post.assert_called_with(
|
|
self.request_params.url, data=self.request_params.build_post_args(
|
|
'netstorsvc', 'share_folder',
|
|
'svc:/network/nfs/server:default',
|
|
self._get_share_path(share['name']), share_opts),
|
|
headers=self.request_params.headers)
|
|
|
|
post.side_effect = my_side_effect
|
|
|
|
self.assertRaises(exception.ManilaException, self.drv.update_access,
|
|
self.ctx, share,
|
|
[{'access_type': 'ip',
|
|
'access_to': '1111',
|
|
'access_level': 'rw'}],
|
|
None, None)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_deny_access__unsupported_access_type(self, post):
|
|
share = {'name': 'share',
|
|
'share_proto': self.cfg.enabled_share_protocols}
|
|
access = {
|
|
'access_type': 'group',
|
|
'access_to': 'ordinary_users',
|
|
'access_level': 'rw'
|
|
}
|
|
|
|
self.assertRaises(exception.InvalidShareAccess, self.drv.update_access,
|
|
self.ctx, share, [access], None, None)
|
|
|
|
def test_share_backend_name(self):
|
|
self.assertEqual('NexentaStor', self.drv.share_backend_name)
|
|
|
|
@mock.patch(PATH_TO_RPC)
|
|
def test_get_capacity_info(self, post):
|
|
post.return_value = FakeResponse({'result': {
|
|
'available': 9 * units.Gi, 'used': 1 * units.Gi}})
|
|
|
|
self.assertEqual(
|
|
(10, 9, 1), self.drv.helper._get_capacity_info())
|
|
|
|
@mock.patch('manila.share.drivers.nexenta.ns4.nexenta_nfs_helper.'
|
|
'NFSHelper._get_capacity_info')
|
|
@mock.patch('manila.share.driver.ShareDriver._update_share_stats')
|
|
def test_update_share_stats(self, super_stats, info):
|
|
info.return_value = (100, 90, 10)
|
|
stats = {
|
|
'vendor_name': 'Nexenta',
|
|
'storage_protocol': 'NFS',
|
|
'nfs_mount_point_base': self.cfg.nexenta_mount_point_base,
|
|
'driver_version': '1.0',
|
|
'share_backend_name': self.cfg.share_backend_name,
|
|
'pools': [{
|
|
'total_capacity_gb': 100,
|
|
'free_capacity_gb': 90,
|
|
'pool_name': 'volume',
|
|
'reserved_percentage': (
|
|
self.cfg.reserved_share_percentage),
|
|
'reserved_snapshot_percentage': (
|
|
self.cfg.reserved_share_from_snapshot_percentage),
|
|
'reserved_share_extend_percentage': (
|
|
self.cfg.reserved_share_extend_percentage),
|
|
'compression': True,
|
|
'dedupe': True,
|
|
'thin_provisioning': self.cfg.nexenta_thin_provisioning,
|
|
'max_over_subscription_ratio': (
|
|
self.cfg.safe_get(
|
|
'max_over_subscription_ratio')),
|
|
}],
|
|
}
|
|
|
|
self.drv._update_share_stats()
|
|
|
|
self.assertEqual(stats, self.drv._stats)
|