Adds Nexenta NFS driver
Implements: blueprint nexenta-nfs-volume-driver Change-Id: I369ce83113cf05b1cd2ecf86d5f6fd25eed12d85
This commit is contained in:
parent
59df774239
commit
96677735f6
@ -23,11 +23,14 @@ import base64
|
||||
import urllib2
|
||||
|
||||
import mox as mox_lib
|
||||
from mox import stubout
|
||||
|
||||
from cinder import test
|
||||
from cinder import units
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers import nexenta
|
||||
from cinder.volume.drivers.nexenta import jsonrpc
|
||||
from cinder.volume.drivers.nexenta import nfs
|
||||
from cinder.volume.drivers.nexenta import volume
|
||||
|
||||
|
||||
@ -351,3 +354,228 @@ class TestNexentaJSONRPC(test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
self.assertRaises(jsonrpc.NexentaJSONException,
|
||||
self.proxy, 'arg1', 'arg2')
|
||||
|
||||
|
||||
class TestNexentaNfsDriver(test.TestCase):
|
||||
TEST_EXPORT1 = 'host1:/volumes/stack/share'
|
||||
TEST_NMS1 = 'http://admin:nexenta@host1:2000'
|
||||
|
||||
TEST_EXPORT2 = 'host2:/volumes/stack/share'
|
||||
TEST_NMS2 = 'http://admin:nexenta@host2:2000'
|
||||
|
||||
TEST_EXPORT2_OPTIONS = '-o intr'
|
||||
|
||||
TEST_FILE_NAME = 'test.txt'
|
||||
TEST_SHARES_CONFIG_FILE = '/etc/cinder/nexenta-shares.conf'
|
||||
|
||||
TEST_SHARE_SVC = 'svc:/network/nfs/server:default'
|
||||
|
||||
TEST_SHARE_OPTS = {
|
||||
'read_only': '',
|
||||
'read_write': '*',
|
||||
'recursive': 'true',
|
||||
'anonymous_rw': 'true',
|
||||
'extra_options': 'anon=0',
|
||||
'root': 'nobody'
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestNexentaNfsDriver, self).setUp()
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
self.configuration = mox_lib.MockObject(conf.Configuration)
|
||||
self.configuration.nexenta_shares_config = None
|
||||
self.configuration.nexenta_mount_point_base = '$state_path/mnt'
|
||||
self.configuration.nexenta_sparsed_volumes = True
|
||||
self.configuration.nexenta_volume_compression = 'on'
|
||||
self.nms_mock = self.mox.CreateMockAnything()
|
||||
for mod in ('appliance', 'folder', 'server', 'volume', 'netstorsvc'):
|
||||
setattr(self.nms_mock, mod, self.mox.CreateMockAnything())
|
||||
self.stubs.Set(jsonrpc, 'NexentaJSONProxy',
|
||||
lambda *_, **__: self.nms_mock)
|
||||
self.drv = nfs.NexentaNfsDriver(configuration=self.configuration)
|
||||
self.drv.shares = {}
|
||||
self.drv.share2nms = {}
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
self.drv.share2nms = {
|
||||
'host1:/volumes/stack/share': self.nms_mock
|
||||
}
|
||||
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.volume.object_exists('stack').AndReturn(True)
|
||||
self.nms_mock.folder.object_exists('stack/share').AndReturn(True)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv.check_for_setup_error()
|
||||
|
||||
self.mox.ResetAll()
|
||||
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.volume.object_exists('stack').AndReturn(False)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.assertRaises(LookupError, self.drv.check_for_setup_error)
|
||||
|
||||
self.mox.ResetAll()
|
||||
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.volume.object_exists('stack').AndReturn(True)
|
||||
self.nms_mock.folder.object_exists('stack/share').AndReturn(False)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.assertRaises(LookupError, self.drv.check_for_setup_error)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
self.drv.shares = {
|
||||
self.TEST_EXPORT1: None
|
||||
}
|
||||
volume = {
|
||||
'provider_location': self.TEST_EXPORT1,
|
||||
'name': 'volume'
|
||||
}
|
||||
result = self.drv.initialize_connection(volume, None)
|
||||
self.assertEqual(result['data']['export'],
|
||||
'%s/volume' % self.TEST_EXPORT1)
|
||||
|
||||
def test_do_create_volume(self):
|
||||
volume = {
|
||||
'provider_location': self.TEST_EXPORT1,
|
||||
'size': 1,
|
||||
'name': 'volume-1'
|
||||
}
|
||||
self.drv.shares = {self.TEST_EXPORT1: None}
|
||||
self.drv.share2nms = {self.TEST_EXPORT1: self.nms_mock}
|
||||
|
||||
compression = self.configuration.nexenta_volume_compression
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.folder.create('stack', 'share/volume-1',
|
||||
'-o compression=%s' % compression)
|
||||
self.nms_mock.netstorsvc.share_folder(self.TEST_SHARE_SVC,
|
||||
'stack/share/volume-1',
|
||||
self.TEST_SHARE_OPTS)
|
||||
self.nms_mock.appliance.execute(
|
||||
'dd if=/dev/zero of=/volumes/stack/share/volume-1/volume bs=1M '
|
||||
'count=0 seek=1024'
|
||||
)
|
||||
self.nms_mock.appliance.execute('chmod ugo+rw '
|
||||
'/volumes/stack/share/volume-1/volume')
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._do_create_volume(volume)
|
||||
|
||||
self.mox.ResetAll()
|
||||
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.folder.create('stack', 'share/volume-1',
|
||||
'-o compression=%s' % compression)
|
||||
self.nms_mock.netstorsvc.share_folder(
|
||||
self.TEST_SHARE_SVC, 'stack/share/volume-1',
|
||||
self.TEST_SHARE_OPTS).AndRaise(nexenta.NexentaException('-'))
|
||||
self.nms_mock.folder.destroy('stack/share/volume-1')
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.assertRaises(nexenta.NexentaException, self.drv._do_create_volume,
|
||||
volume)
|
||||
|
||||
def test_create_sparsed_file(self):
|
||||
self.nms_mock.appliance.execute('dd if=/dev/zero of=/tmp/path bs=1M '
|
||||
'count=0 seek=1024')
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._create_sparsed_file(self.nms_mock, '/tmp/path', 1)
|
||||
|
||||
def test_create_regular_file(self):
|
||||
self.nms_mock.appliance.execute('dd if=/dev/zero of=/tmp/path bs=1M '
|
||||
'count=1024')
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._create_regular_file(self.nms_mock, '/tmp/path', 1)
|
||||
|
||||
def test_set_rw_permissions_for_all(self):
|
||||
path = '/tmp/path'
|
||||
self.nms_mock.appliance.execute('chmod ugo+rw %s' % path)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._set_rw_permissions_for_all(self.nms_mock, path)
|
||||
|
||||
def test_local_path(self):
|
||||
volume = {'provider_location': self.TEST_EXPORT1, 'name': 'volume-1'}
|
||||
path = self.drv.local_path(volume)
|
||||
self.assertEqual(
|
||||
path,
|
||||
'$state_path/mnt/b3f660847a52b29ac330d8555e4ad669/volume-1/volume'
|
||||
)
|
||||
|
||||
def test_remote_path(self):
|
||||
volume = {'provider_location': self.TEST_EXPORT1, 'name': 'volume-1'}
|
||||
path = self.drv.remote_path(volume)
|
||||
self.assertEqual(path, '/volumes/stack/share/volume-1/volume')
|
||||
|
||||
def test_share_folder(self):
|
||||
path = 'stack/share/folder'
|
||||
self.nms_mock.netstorsvc.share_folder(self.TEST_SHARE_SVC, path,
|
||||
self.TEST_SHARE_OPTS)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._share_folder(self.nms_mock, 'stack', 'share/folder')
|
||||
|
||||
def test_load_shares_config(self):
|
||||
self.drv.configuration.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
|
||||
|
||||
self.mox.StubOutWithMock(self.drv, '_read_config_file')
|
||||
config_data = [
|
||||
'%s %s' % (self.TEST_EXPORT1, self.TEST_NMS1),
|
||||
'# %s %s' % (self.TEST_EXPORT2, self.TEST_NMS2),
|
||||
'',
|
||||
'%s %s %s' % (self.TEST_EXPORT2, self.TEST_NMS2,
|
||||
self.TEST_EXPORT2_OPTIONS)
|
||||
]
|
||||
|
||||
self.drv._read_config_file(self.TEST_SHARES_CONFIG_FILE).\
|
||||
AndReturn(config_data)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.drv._load_shares_config(self.drv.configuration.nfs_shares_config)
|
||||
|
||||
self.assertIn(self.TEST_EXPORT1, self.drv.shares)
|
||||
self.assertIn(self.TEST_EXPORT2, self.drv.shares)
|
||||
self.assertEqual(len(self.drv.shares), 2)
|
||||
|
||||
self.assertIn(self.TEST_EXPORT1, self.drv.share2nms)
|
||||
self.assertIn(self.TEST_EXPORT2, self.drv.share2nms)
|
||||
self.assertEqual(len(self.drv.share2nms.keys()), 2)
|
||||
|
||||
self.assertEqual(self.drv.shares[self.TEST_EXPORT2],
|
||||
self.TEST_EXPORT2_OPTIONS)
|
||||
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_get_capacity_info(self):
|
||||
self.drv.share2nms = {self.TEST_EXPORT1: self.nms_mock}
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.nms_mock.folder.get_child_props('stack/share', '').AndReturn({
|
||||
'available': '1G',
|
||||
'used': '2G'
|
||||
})
|
||||
self.mox.ReplayAll()
|
||||
total, free, allocated = self.drv._get_capacity_info(self.TEST_EXPORT1)
|
||||
|
||||
self.assertEqual(total, 3 * units.GiB)
|
||||
self.assertEqual(free, units.GiB)
|
||||
self.assertEqual(allocated, 2 * units.GiB)
|
||||
|
||||
def test_get_share_datasets(self):
|
||||
self.drv.share2nms = {self.TEST_EXPORT1: self.nms_mock}
|
||||
self.nms_mock.server.get_prop('volroot').AndReturn('/volumes')
|
||||
self.mox.ReplayAll()
|
||||
|
||||
volume_name, folder_name = \
|
||||
self.drv._get_share_datasets(self.TEST_EXPORT1)
|
||||
|
||||
self.assertEqual(volume_name, 'stack')
|
||||
self.assertEqual(folder_name, 'share')
|
||||
|
385
cinder/volume/drivers/nexenta/nfs.py
Normal file
385
cinder/volume/drivers/nexenta/nfs.py
Normal file
@ -0,0 +1,385 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2013 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.
|
||||
"""
|
||||
:mod:`nexenta.volume` -- Driver to store volumes on Nexenta Appliance
|
||||
=====================================================================
|
||||
|
||||
.. automodule:: nexenta.nfs
|
||||
.. moduleauthor:: Mikhail Khodos <hodosmb@gmail.com>
|
||||
.. moduleauthor:: Victor Rodionov <victor.rodionov@nexenta.com>
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import units
|
||||
from cinder.volume.drivers import nexenta
|
||||
from cinder.volume.drivers.nexenta import jsonrpc
|
||||
from cinder.volume.drivers.nexenta import options
|
||||
from cinder.volume.drivers.nexenta import utils
|
||||
from cinder.volume.drivers import nfs
|
||||
|
||||
VERSION = '1.0.0'
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(options.NEXENTA_NFS_OPTIONS)
|
||||
|
||||
|
||||
class NexentaNfsDriver(nfs.NfsDriver): # pylint: disable=R0921
|
||||
"""Executes volume driver commands on Nexenta Appliance."""
|
||||
|
||||
driver_prefix = 'nexenta'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NexentaNfsDriver, self).__init__(*args, **kwargs)
|
||||
if self.configuration:
|
||||
self.configuration.append_config_values(
|
||||
options.NEXENTA_NFS_OPTIONS)
|
||||
|
||||
def do_setup(self, context):
|
||||
super(NexentaNfsDriver, self).do_setup(context)
|
||||
self._load_shares_config(getattr(self.configuration,
|
||||
self.driver_prefix +
|
||||
'_shares_config'))
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Verify that the volume for our folder exists.
|
||||
|
||||
:raise: :py:exc:`LookupError`
|
||||
"""
|
||||
if self.share2nms:
|
||||
for nfs_share in self.share2nms:
|
||||
nms = self.share2nms[nfs_share]
|
||||
volume_name, dataset = self._get_share_datasets(nfs_share)
|
||||
if not nms.volume.object_exists(volume_name):
|
||||
raise LookupError(_("Volume %s does not exist in Nexenta "
|
||||
"Store appliance"), volume_name)
|
||||
folder = '%s/%s' % (volume_name, dataset)
|
||||
if not nms.folder.object_exists(folder):
|
||||
raise LookupError(_("Folder %s does not exist in Nexenta "
|
||||
"Store appliance"), folder)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Allow connection to connector and return connection info.
|
||||
|
||||
:param volume: volume reference
|
||||
:param connector: connector reference
|
||||
"""
|
||||
export = '%s/%s' % (volume['provider_location'], volume['name'])
|
||||
data = {'export': export, 'name': 'volume'}
|
||||
if volume['provider_location'] in self.shares:
|
||||
data['options'] = self.shares[volume['provider_location']]
|
||||
return {
|
||||
'driver_volume_type': self.driver_volume_type,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _do_create_volume(self, volume):
|
||||
nfs_share = volume['provider_location']
|
||||
nms = self.share2nms[nfs_share]
|
||||
|
||||
vol, dataset = self._get_share_datasets(nfs_share)
|
||||
folder = '%s/%s' % (dataset, volume['name'])
|
||||
LOG.debug(_('Creating folder on Nexenta Store %s'), folder)
|
||||
nms.folder.create(
|
||||
vol, folder,
|
||||
'-o compression=%s' % self.configuration.nexenta_volume_compression
|
||||
)
|
||||
|
||||
volume_path = self.remote_path(volume)
|
||||
volume_size = volume['size']
|
||||
try:
|
||||
self._share_folder(nms, vol, folder)
|
||||
|
||||
if getattr(self.configuration,
|
||||
self.driver_prefix + '_sparsed_volumes'):
|
||||
self._create_sparsed_file(nms, volume_path, volume_size)
|
||||
else:
|
||||
compression = nms.folder.get('compression')
|
||||
if compression != 'off':
|
||||
# Disable compression, because otherwise will not use space
|
||||
# on disk
|
||||
nms.folder.set('compression', 'off')
|
||||
try:
|
||||
self._create_regular_file(nms, volume_path, volume_size)
|
||||
finally:
|
||||
if compression != 'off':
|
||||
# Backup default compression value if it was changed.
|
||||
nms.folder.set('compression', compression)
|
||||
|
||||
self._set_rw_permissions_for_all(nms, volume_path)
|
||||
except nexenta.NexentaException as exc:
|
||||
try:
|
||||
nms.folder.destroy('%s/%s' % (vol, folder))
|
||||
except nexenta.NexentaException:
|
||||
LOG.warning(_("Cannot destroy created folder: "
|
||||
"%(vol)s/%(folder)s"),
|
||||
{'vol': vol, 'folder': folder})
|
||||
raise exc
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Create new volume from other's snapshot on appliance.
|
||||
|
||||
:param volume: reference of volume to be created
|
||||
:param snapshot: reference of source snapshot
|
||||
"""
|
||||
self._ensure_shares_mounted()
|
||||
|
||||
snapshot_vol = self._get_snapshot_volume(snapshot)
|
||||
nfs_share = snapshot_vol['provider_location']
|
||||
volume['provider_location'] = nfs_share
|
||||
nms = self.share2nms[nfs_share]
|
||||
|
||||
vol, dataset = self._get_share_datasets(nfs_share)
|
||||
snapshot_name = '%s/%s/%s@%s' % (vol, dataset, snapshot['volume_name'],
|
||||
snapshot['name'])
|
||||
folder = '%s/%s' % (dataset, volume['name'])
|
||||
nms.folder.clone(snapshot_name, '%s/%s' % (vol, folder))
|
||||
|
||||
try:
|
||||
self._share_folder(nms, vol, folder)
|
||||
except nexenta.NexentaException:
|
||||
try:
|
||||
nms.folder.destroy('%s/%s' % (vol, folder), '')
|
||||
except nexenta.NexentaException:
|
||||
LOG.warning(_("Cannot destroy cloned folder: "
|
||||
"%(vol)s/%(folder)s"),
|
||||
{'vol': vol, 'folder': folder})
|
||||
raise
|
||||
|
||||
return {'provider_location': volume['provider_location']}
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a clone of the specified volume.
|
||||
|
||||
:param volume: new volume reference
|
||||
:param src_vref: source volume reference
|
||||
"""
|
||||
LOG.info(_('Creating clone of volume: %s'), src_vref['id'])
|
||||
snapshot = {'volume_name': src_vref['name'],
|
||||
'name': 'cinder-clone-snap-%(id)s' % volume}
|
||||
# We don't delete this snapshot, because this snapshot will be origin
|
||||
# of new volume. This snapshot will be automatically promoted by NMS
|
||||
# when user will delete its origin.
|
||||
self.create_snapshot(snapshot)
|
||||
try:
|
||||
self.create_volume_from_snapshot(volume, snapshot)
|
||||
except nexenta.NexentaException:
|
||||
LOG.error(_('Volume creation failed, deleting created snapshot '
|
||||
'%(volume_name)s@%(name)s'), snapshot)
|
||||
try:
|
||||
self.delete_snapshot(snapshot)
|
||||
except (nexenta.NexentaException, exception.SnapshotIsBusy):
|
||||
LOG.warning(_('Failed to delete zfs snapshot '
|
||||
'%(volume_name)s@%(name)s'), snapshot)
|
||||
raise
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Deletes a logical volume.
|
||||
|
||||
:param volume: volume reference
|
||||
"""
|
||||
super(NexentaNfsDriver, self).delete_volume(volume)
|
||||
|
||||
nfs_share = volume['provider_location']
|
||||
if nfs_share:
|
||||
nms = self.share2nms[nfs_share]
|
||||
vol, parent_folder = self._get_share_datasets(nfs_share)
|
||||
folder = '%s/%s/%s' % (vol, parent_folder, volume['name'])
|
||||
nms.folder.destroy(folder, '')
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot.
|
||||
|
||||
:param snapshot: snapshot reference
|
||||
"""
|
||||
volume = self._get_snapshot_volume(snapshot)
|
||||
nfs_share = volume['provider_location']
|
||||
nms = self.share2nms[nfs_share]
|
||||
vol, dataset = self._get_share_datasets(nfs_share)
|
||||
folder = '%s/%s/%s' % (vol, dataset, volume['name'])
|
||||
nms.folder.create_snapshot(folder, snapshot['name'], '-r')
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot.
|
||||
|
||||
:param snapshot: snapshot reference
|
||||
"""
|
||||
volume = self._get_snapshot_volume(snapshot)
|
||||
nfs_share = volume['provider_location']
|
||||
nms = self.share2nms[nfs_share]
|
||||
vol, dataset = self._get_share_datasets(nfs_share)
|
||||
folder = '%s/%s/%s' % (vol, dataset, volume['name'])
|
||||
nms.snapshot.destroy('%s@%s' % (folder, snapshot['name']), '')
|
||||
|
||||
def _create_sparsed_file(self, nms, path, size):
|
||||
"""Creates file with 0 disk usage."""
|
||||
block_size_mb = 1
|
||||
block_count = size * units.GiB / (block_size_mb * units.MiB)
|
||||
|
||||
nms.appliance.execute(
|
||||
'dd if=/dev/zero of=%(path)s bs=%(bs)dM count=0 seek=%(count)d' % {
|
||||
'path': path,
|
||||
'bs': block_size_mb,
|
||||
'count': block_count
|
||||
}
|
||||
)
|
||||
|
||||
def _create_regular_file(self, nms, path, size):
|
||||
"""Creates regular file of given size.
|
||||
|
||||
Takes a lot of time for large files.
|
||||
"""
|
||||
block_size_mb = 1
|
||||
block_count = size * units.GiB / (block_size_mb * units.MiB)
|
||||
|
||||
LOG.info(_('Creating regular file: %s.'
|
||||
'This may take some time.') % path)
|
||||
|
||||
nms.appliance.execute(
|
||||
'dd if=/dev/zero of=%(path)s bs=%(bs)dM count=%(count)d' % {
|
||||
'path': path,
|
||||
'bs': block_size_mb,
|
||||
'count': block_count
|
||||
}
|
||||
)
|
||||
|
||||
LOG.info(_('Regular file: %s created.') % path)
|
||||
|
||||
def _set_rw_permissions_for_all(self, nms, path):
|
||||
"""Sets 666 permissions for the path."""
|
||||
nms.appliance.execute('chmod ugo+rw %s' % path)
|
||||
|
||||
def local_path(self, volume):
|
||||
"""Get volume path (mounted locally fs path) for given volume.
|
||||
|
||||
:param volume: volume reference
|
||||
"""
|
||||
nfs_share = volume['provider_location']
|
||||
return os.path.join(self._get_mount_point_for_share(nfs_share),
|
||||
volume['name'], 'volume')
|
||||
|
||||
def _get_mount_point_for_share(self, nfs_share):
|
||||
"""
|
||||
:param nfs_share: example 172.18.194.100:/var/nfs
|
||||
"""
|
||||
return os.path.join(self.configuration.nexenta_mount_point_base,
|
||||
hashlib.md5(nfs_share).hexdigest())
|
||||
|
||||
def remote_path(self, volume):
|
||||
"""Get volume path (mounted remotely fs path) for given volume.
|
||||
|
||||
:param volume: volume reference
|
||||
"""
|
||||
nfs_share = volume['provider_location']
|
||||
share = nfs_share.split(':')[1].rstrip('/')
|
||||
return '%s/%s/volume' % (share, volume['name'])
|
||||
|
||||
def _share_folder(self, nms, volume, folder):
|
||||
path = '%s/%s' % (volume, folder.lstrip('/'))
|
||||
share_opts = {
|
||||
'read_write': '*',
|
||||
'read_only': '',
|
||||
'root': 'nobody',
|
||||
'extra_options': 'anon=0',
|
||||
'recursive': 'true',
|
||||
'anonymous_rw': 'true',
|
||||
}
|
||||
LOG.debug(_('Sharing folder %s on Nexenta Store'), folder)
|
||||
nms.netstorsvc.share_folder('svc:/network/nfs/server:default', path,
|
||||
share_opts)
|
||||
|
||||
def _load_shares_config(self, share_file):
|
||||
self.shares = {}
|
||||
self.share2nms = {}
|
||||
|
||||
for share in self._read_config_file(share_file):
|
||||
# A configuration line may be either:
|
||||
# host:/share_name http://user:pass@host:[port]/
|
||||
# or
|
||||
# host:/share_name http://user:pass@host:[port]/
|
||||
# -o options=123,rw --other
|
||||
if not share.strip():
|
||||
continue
|
||||
if share.startswith('#'):
|
||||
continue
|
||||
|
||||
share_info = share.split(' ', 2)
|
||||
|
||||
share_address = share_info[0].strip().decode('unicode_escape')
|
||||
nms_url = share_info[1].strip()
|
||||
share_opts = share_info[2].strip() if len(share_info) > 2 else None
|
||||
|
||||
self.shares[share_address] = share_opts
|
||||
self.share2nms[share_address] = self._get_nms_for_url(nms_url)
|
||||
|
||||
LOG.debug(_('Shares loaded: %s') % self.shares)
|
||||
|
||||
def _get_capacity_info(self, nfs_share):
|
||||
"""Calculate available space on the NFS share.
|
||||
|
||||
:param nfs_share: example 172.18.194.100:/var/nfs
|
||||
"""
|
||||
nms = self.share2nms[nfs_share]
|
||||
ns_volume, ns_folder = self._get_share_datasets(nfs_share)
|
||||
folder_props = nms.folder.get_child_props('%s/%s' % (ns_volume,
|
||||
ns_folder), '')
|
||||
free = utils.str2size(folder_props['available'])
|
||||
allocated = utils.str2size(folder_props['used'])
|
||||
return free + allocated, free, allocated
|
||||
|
||||
def _get_nms_for_url(self, nms_url):
|
||||
pr = urlparse.urlparse(nms_url)
|
||||
scheme = pr.scheme
|
||||
auto = scheme == 'auto'
|
||||
if auto:
|
||||
scheme = 'http'
|
||||
user = 'admin'
|
||||
password = 'nexenta'
|
||||
if '@' not in pr.netloc:
|
||||
host_and_port = pr.netloc
|
||||
else:
|
||||
user_and_password, host_and_port = pr.netloc.split('@', 1)
|
||||
if ':' in user_and_password:
|
||||
user, password = user_and_password.split(':')
|
||||
else:
|
||||
user = user_and_password
|
||||
if ':' in host_and_port:
|
||||
host, port = host_and_port.split(':', 1)
|
||||
else:
|
||||
host, port = host_and_port, '2000'
|
||||
url = '%s://%s:%s/rest/nms/' % (scheme, host, port)
|
||||
return jsonrpc.NexentaJSONProxy(url, user, password, auto=auto)
|
||||
|
||||
def _get_snapshot_volume(self, snapshot):
|
||||
ctxt = context.get_admin_context()
|
||||
return self.db.volume_get(ctxt, snapshot['volume_id'])
|
||||
|
||||
def _get_share_datasets(self, nfs_share):
|
||||
nms = self.share2nms[nfs_share]
|
||||
volroot = nms.server.get_prop('volroot')
|
||||
path = nfs_share.split(':')[1][len(volroot):].strip('/')
|
||||
volume_name = path.split('/')[0]
|
||||
folder_name = '/'.join(path.split('/')[1:])
|
||||
return volume_name, folder_name
|
@ -60,6 +60,37 @@ NEXENTA_ISCSI_OPTIONS = [
|
||||
help='prefix for iSCSI target groups on SA'),
|
||||
]
|
||||
|
||||
NEXENTA_NFS_OPTIONS = [
|
||||
cfg.StrOpt('nexenta_shares_config',
|
||||
default='/etc/cinder/nfs_shares',
|
||||
help='File with the list of available nfs shares'),
|
||||
cfg.StrOpt('nexenta_mount_point_base',
|
||||
default='$state_path/mnt',
|
||||
help='Base dir containing mount points for nfs shares'),
|
||||
cfg.BoolOpt('nexenta_sparsed_volumes',
|
||||
default=True,
|
||||
help=('Create volumes as sparsed files which take no space.'
|
||||
'If set to False volume is created as regular file.'
|
||||
'In such case volume creation takes a lot of time.')),
|
||||
cfg.StrOpt('nexenta_volume_compression',
|
||||
default='on',
|
||||
help='Default compression value for new ZFS folders.'),
|
||||
cfg.StrOpt('nexenta_mount_options',
|
||||
default=None,
|
||||
help='Mount options passed to the nfs client. See section '
|
||||
'of the nfs man page for details'),
|
||||
cfg.FloatOpt('nexenta_used_ratio',
|
||||
default=0.95,
|
||||
help=('Percent of ACTUAL usage of the underlying volume '
|
||||
'before no new volumes can be allocated to the volume '
|
||||
'destination.')),
|
||||
cfg.FloatOpt('nexenta_oversub_ratio',
|
||||
default=1.0,
|
||||
help=('This will compare the allocated to available space on '
|
||||
'the volume destination. If the ratio exceeds this '
|
||||
'number, the destination will no longer be valid.'))
|
||||
]
|
||||
|
||||
NEXENTA_VOLUME_OPTIONS = [
|
||||
cfg.StrOpt('nexenta_blocksize',
|
||||
default='',
|
||||
|
46
cinder/volume/drivers/nexenta/utils.py
Normal file
46
cinder/volume/drivers/nexenta/utils.py
Normal file
@ -0,0 +1,46 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# Copyright 2013 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 re
|
||||
|
||||
|
||||
def str2size(s, scale=1024):
|
||||
"""Convert size-string.
|
||||
|
||||
String format: <value>[:space:]<B | K | M | ...> to bytes.
|
||||
|
||||
:param s: size-string
|
||||
:param scale: base size
|
||||
"""
|
||||
if not s:
|
||||
return 0
|
||||
|
||||
if isinstance(s, (int, long)):
|
||||
return s
|
||||
|
||||
match = re.match(r'^([\.\d]+)\s*([BbKkMmGgTtPpEeZzYy]?)', s)
|
||||
if match is None:
|
||||
raise ValueError(_('Invalid value: "%s"') % s)
|
||||
|
||||
groups = match.groups()
|
||||
value = float(groups[0])
|
||||
suffix = len(groups) > 1 and groups[1].upper() or 'B'
|
||||
|
||||
types = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
|
||||
for i, t in enumerate(types):
|
||||
if suffix == t:
|
||||
return int(value * pow(scale, i))
|
@ -1288,6 +1288,36 @@
|
||||
# prefix for iSCSI target groups on SA (string value)
|
||||
#nexenta_target_group_prefix=cinder/
|
||||
|
||||
# File with the list of available nfs shares (string value)
|
||||
#nexenta_shares_config=/etc/cinder/nfs_shares
|
||||
|
||||
# Base dir containing mount points for nfs shares (string
|
||||
# value)
|
||||
#nexenta_mount_point_base=$state_path/mnt
|
||||
|
||||
# Create volumes as sparsed files which take no space.If set
|
||||
# to False volume is created as regular file.In such case
|
||||
# volume creation takes a lot of time. (boolean value)
|
||||
#nexenta_sparsed_volumes=true
|
||||
|
||||
# Default compression value for new ZFS folders. (string
|
||||
# value)
|
||||
#nexenta_volume_compression=on
|
||||
|
||||
# Mount options passed to the nfs client. See section of the
|
||||
# nfs man page for details (string value)
|
||||
#nexenta_mount_options=<None>
|
||||
|
||||
# Percent of ACTUAL usage of the underlying volume before no
|
||||
# new volumes can be allocated to the volume destination.
|
||||
# (floating point value)
|
||||
#nexenta_used_ratio=0.95
|
||||
|
||||
# This will compare the allocated to available space on the
|
||||
# volume destination. If the ratio exceeds this number, the
|
||||
# destination will no longer be valid. (floating point value)
|
||||
#nexenta_oversub_ratio=1.0
|
||||
|
||||
# block size for volumes (blank=default,8KB) (string value)
|
||||
#nexenta_blocksize=
|
||||
|
||||
@ -1662,4 +1692,4 @@
|
||||
#volume_dd_blocksize=1M
|
||||
|
||||
|
||||
# Total option count: 355
|
||||
# Total option count: 362
|
||||
|
Loading…
Reference in New Issue
Block a user