glusterfs: Fix issues in backend instrumentation
- allow share or sub-directory access to more than one guest/ip address by modifying the GlusterFS volume configuration options that change the Gluster-NFS server settings. `nfs.export-dir` option does not interpret a spec of '/dir(host1),/dir(host2)'as we expected, ie. two independent access specifiers -- instead, it just discards the latter. The required format looks as follows: '/dir(host1|host2)'. Update the export-dir generating routines accordingly. - properly remove the last share or sub-directory access setting in a GlusterFS volume when needed by an API request to changes access rules of shares. `volume set` option in GlusterFS volume settings cannot take empty value, use instead `volume reset` to go back to defaults - GlusterFS volume contains all the shares. It has a default setting that allows NFS access to its entire contents. So turn off this setting as a part of the initial driver setup. turn off `nfs.export-volumes` setting. Change-Id: I652a13d4d34d1b4c5ee3d31af40143a098c38074
This commit is contained in:
parent
67aa5bcf2d
commit
99857a4983
|
@ -47,7 +47,8 @@ GlusterfsManilaShare_opts = [
|
|||
CONF = cfg.CONF
|
||||
CONF.register_opts(GlusterfsManilaShare_opts)
|
||||
|
||||
_nfs_export_dir = 'nfs.export-dir'
|
||||
NFS_EXPORT_DIR = 'nfs.export-dir'
|
||||
NFS_EXPORT_VOL = 'nfs.export-volumes'
|
||||
|
||||
|
||||
class GlusterAddress(object):
|
||||
|
@ -107,6 +108,17 @@ class GlusterfsShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||
|
||||
self._ensure_gluster_vol_mounted()
|
||||
|
||||
# exporting the whole volume must be prohibited
|
||||
# to not to defeat access control
|
||||
args, kw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
NFS_EXPORT_VOL, 'off')
|
||||
try:
|
||||
self._execute(*args, **kw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume set: %s") % exc.stderr)
|
||||
raise
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Is called after do_setup method. Nothing to do."""
|
||||
pass
|
||||
|
@ -152,7 +164,7 @@ class GlusterfsShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||
with open(config_file) as f:
|
||||
return f.readline().strip()
|
||||
|
||||
def _get_export_dir_list(self):
|
||||
def _get_export_dir_dict(self):
|
||||
try:
|
||||
args, kw = self.gluster_address.make_gluster_args(
|
||||
'--xml',
|
||||
|
@ -177,13 +189,23 @@ class GlusterfsShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||
for o, v in \
|
||||
((e.find(a).text for a in ('name', 'value'))
|
||||
for e in vix.findall(".//option")):
|
||||
if o == _nfs_export_dir:
|
||||
if o == NFS_EXPORT_DIR:
|
||||
export_dir = v
|
||||
break
|
||||
edh = {}
|
||||
if export_dir:
|
||||
return export_dir.split(',')
|
||||
else:
|
||||
return []
|
||||
# see
|
||||
# https://github.com/gluster/glusterfs
|
||||
# /blob/aa19909/xlators/nfs/server/src/nfs.c#L1582
|
||||
# regarding the format of nfs.export-dir
|
||||
edl = export_dir.split(',')
|
||||
# parsing export_dir into a dict of {dir: [hostpec,..]..}
|
||||
# format
|
||||
r = re.compile('\A/(.*)\((.*)\)\Z')
|
||||
for ed in edl:
|
||||
d, e = r.match(ed).groups()
|
||||
edh[d] = e.split('|')
|
||||
return edh
|
||||
|
||||
def _ensure_gluster_vol_mounted(self):
|
||||
"""Ensure that a Gluster volume is native-mounted on Manila host.
|
||||
|
@ -247,42 +269,63 @@ class GlusterfsShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||
pass
|
||||
|
||||
def _manage_access(self, context, share, access, cbk):
|
||||
"""Manage share access by adjusting the export list with cbk
|
||||
"""Manage share access with cbk.
|
||||
|
||||
cbk is a callable of args (dl, acc), where dl is a list of strings
|
||||
and acc is a string. It should return True (or a value of Boolean
|
||||
reduct True) if it leaves dl intact, and False (or a value of Boolean
|
||||
reduct False) if it makes a change on dl
|
||||
Adjust the exports of the Gluster-NFS server using cbk.
|
||||
|
||||
cbk will be called with dl being list of currently exported dirs and
|
||||
acc being a textual specification derived from access.
|
||||
:param share: share object
|
||||
:param access: access object
|
||||
:param cbk: callback to adjust the exports of NFS server
|
||||
|
||||
Following is the description of cbk(ddict, edir, host).
|
||||
|
||||
:param ddict: association of shares with ips that have access to them
|
||||
:type ddict: dict
|
||||
:param edir: name of share i.e. export directory
|
||||
:type edir: string
|
||||
:param host: ip address derived from the access object
|
||||
:type host: string
|
||||
:returns: bool (cbk leaves ddict intact) or None (cbk modifies ddict)
|
||||
"""
|
||||
|
||||
if access['access_type'] != 'ip':
|
||||
raise exception.InvalidShareAccess('only ip access type allowed')
|
||||
export_dir_list = self._get_export_dir_list()
|
||||
access_spec = "/%s(%s)" % (share['name'], access['access_to'])
|
||||
if cbk(export_dir_list, access_spec):
|
||||
export_dir_dict = self._get_export_dir_dict()
|
||||
if cbk(export_dir_dict, share['name'], access['access_to']):
|
||||
return
|
||||
|
||||
export_dir_new = ",".join(export_dir_list)
|
||||
try:
|
||||
if export_dir_dict:
|
||||
export_dir_new = ",".join("/%s(%s)" % (d, "|".join(v))
|
||||
for d, v in export_dir_dict.items())
|
||||
args, kw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
_nfs_export_dir, export_dir_new)
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
NFS_EXPORT_DIR, export_dir_new)
|
||||
else:
|
||||
args, kw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'reset', self.gluster_address.volume,
|
||||
NFS_EXPORT_DIR)
|
||||
try:
|
||||
self._execute(*args, **kw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume set: %s") % exc.stderr)
|
||||
raise
|
||||
|
||||
def allow_access(self, context, share, access):
|
||||
"""NFS export a dir to a volume"""
|
||||
self._manage_access(context, share, access,
|
||||
lambda dl, acc:
|
||||
True if acc in dl else dl.append(acc))
|
||||
"""Allow access to a share."""
|
||||
def cbk(ddict, edir, host):
|
||||
if edir not in ddict:
|
||||
ddict[edir] = []
|
||||
if host in ddict[edir]:
|
||||
return True
|
||||
ddict[edir].append(host)
|
||||
self._manage_access(context, share, access, cbk)
|
||||
|
||||
def deny_access(self, context, share, access):
|
||||
"""Deny access to the share."""
|
||||
self._manage_access(context, share, access,
|
||||
lambda dl, acc:
|
||||
True if acc not in dl else dl.remove(acc))
|
||||
"""Deny access to a share."""
|
||||
def cbk(ddict, edir, host):
|
||||
if edir not in ddict or host not in ddict[edir]:
|
||||
return True
|
||||
ddict[edir].remove(host)
|
||||
if not ddict[edir]:
|
||||
ddict.pop(edir)
|
||||
self._manage_access(context, share, access, cbk)
|
||||
|
|
|
@ -133,25 +133,49 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
|
||||
def test_do_setup(self):
|
||||
self._driver._read_gluster_vol_from_config =\
|
||||
Mock(return_value='testuser@127.0.0.1:/testvol/fakename')
|
||||
Mock(return_value='127.0.0.1:/testvol')
|
||||
self._driver._ensure_gluster_vol_mounted = Mock()
|
||||
expected_exec = ['mount.glusterfs']
|
||||
exec_cmd1 = 'mount.glusterfs'
|
||||
exec_cmd2 = 'gluster volume set testvol nfs.export-volumes off'
|
||||
expected_exec = [exec_cmd1, exec_cmd2]
|
||||
self._driver.do_setup(self._context)
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
self._driver._ensure_gluster_vol_mounted.assert_called_once_with()
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
|
||||
def test_do_setup_mount_glusterfs_not_installed(self):
|
||||
self._driver._read_gluster_vol_from_config =\
|
||||
Mock(return_value='testuser@127.0.0.1:/testvol/fakename')
|
||||
Mock(return_value='127.0.0.1:/testvol')
|
||||
self._driver._ensure_gluster_vol_mounted = Mock()
|
||||
|
||||
def exec_runner(*ignore_args, **ignore_kw):
|
||||
def exec_runner(*ignore_args, **ignore_kwargs):
|
||||
raise OSError(errno.ENOENT, os.strerror(errno.ENOENT))
|
||||
|
||||
expected_exec = ['mount.glusterfs']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
|
||||
self.assertRaises(exception.GlusterfsException, self._driver.do_setup,
|
||||
self._context)
|
||||
|
||||
def test_do_setup_mount_glusterfs_error_gluster_vol_set(self):
|
||||
self._driver._read_gluster_vol_from_config =\
|
||||
Mock(return_value='127.0.0.1:/testvol')
|
||||
self._driver._ensure_gluster_vol_mounted = Mock()
|
||||
glusterfs.LOG.error = Mock()
|
||||
|
||||
def exec_runner(*ignore_args, **ignore_kwargs):
|
||||
raise exception.ProcessExecutionError(stderr='testvol')
|
||||
expected_exec = ['gluster volume set testvol nfs.export-volumes off']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
|
||||
try:
|
||||
self._driver.do_setup(self._context)
|
||||
except exception.ProcessExecutionError:
|
||||
pass
|
||||
else:
|
||||
self.fail('Expecting exception.ProcessExecutionError')
|
||||
|
||||
glusterfs.LOG.error.assert_called_with("Error in gluster volume set: "
|
||||
"testvol")
|
||||
|
||||
def test_do_mount(self):
|
||||
expected_exec = ['true']
|
||||
ret = self._driver._do_mount(expected_exec, False)
|
||||
|
@ -220,15 +244,15 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
self.assertRaises(exception.GlusterfsException,
|
||||
self._driver._ensure_gluster_vol_mounted)
|
||||
|
||||
def test_get_export_dir_list_empty_volinfo(self):
|
||||
def test_get_export_dir_dict_empty_volinfo(self):
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
expected_exec = ['true']
|
||||
self.assertRaises(exception.GlusterfsException,
|
||||
self._driver._get_export_dir_list)
|
||||
self._driver._get_export_dir_dict)
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
|
||||
def test_get_export_dir_list_failing_volinfo(self):
|
||||
def test_get_export_dir_dict_failing_volinfo(self):
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
|
||||
|
@ -236,10 +260,10 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
raise RuntimeError('fake error')
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
self.assertRaises(RuntimeError, self._driver._get_export_dir_list)
|
||||
self.assertRaises(RuntimeError, self._driver._get_export_dir_dict)
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
|
||||
def test_get_export_dir_list_ambiguous_volinfo(self):
|
||||
def test_get_export_dir_dict_ambiguous_volinfo(self):
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
|
||||
|
@ -257,10 +281,10 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
self.assertRaises(exception.InvalidShare,
|
||||
self._driver._get_export_dir_list)
|
||||
self._driver._get_export_dir_dict)
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
|
||||
def test_get_export_dir_list_trivial_volinfo(self):
|
||||
def test_get_export_dir_dict_trivial_volinfo(self):
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
|
||||
|
@ -279,11 +303,11 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
""", ''
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
ret = self._driver._get_export_dir_list()
|
||||
ret = self._driver._get_export_dir_dict()
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
self.assertEqual(ret, [])
|
||||
self.assertEqual(ret, {})
|
||||
|
||||
def test_get_export_dir_list(self):
|
||||
def test_get_export_dir_dict(self):
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
|
||||
|
@ -297,7 +321,7 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
<options>
|
||||
<option>
|
||||
<name>nfs.export-dir</name>
|
||||
<value>foo,bar</value>
|
||||
<value>/foo(10.0.0.1|10.0.0.2),/bar(10.0.0.1)</value>
|
||||
</option>
|
||||
</options>
|
||||
</volume>
|
||||
|
@ -308,9 +332,11 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
""", ''
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
ret = self._driver._get_export_dir_list()
|
||||
ret = self._driver._get_export_dir_dict()
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
self.assertEqual(ret, ['foo', 'bar'])
|
||||
self.assertEqual(ret,
|
||||
{'foo': ['10.0.0.1', '10.0.0.2'], 'bar': ['10.0.0.1']}
|
||||
)
|
||||
|
||||
def test_get_local_share_path(self):
|
||||
with patch.object(os, 'access', return_value=True):
|
||||
|
@ -377,8 +403,8 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
|
||||
def test_manage_access_noop(self):
|
||||
cbk = Mock(return_value=True)
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list = Mock()
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict = Mock()
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
expected_exec = []
|
||||
|
@ -388,10 +414,12 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
self.assertEqual(ret, None)
|
||||
|
||||
def test_manage_access_adding_entry(self):
|
||||
cbk = list.append
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list =\
|
||||
Mock(return_value=['/example.com(0.0.0.0)'])
|
||||
def cbk(d, key, value):
|
||||
d[key].append(value)
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.2'}
|
||||
self._driver._get_export_dir_dict =\
|
||||
Mock(return_value={'example.com': ['10.0.0.1'],
|
||||
'fakename': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
expected_exec = ['true']
|
||||
|
@ -402,13 +430,15 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'/example.com(0.0.0.0),/fakename(0.0.0.0)')
|
||||
'/example.com(10.0.0.1),/fakename(10.0.0.1|10.0.0.2)')
|
||||
|
||||
def test_manage_access_adding_entry_cmd_fail(self):
|
||||
cbk = list.append
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list =\
|
||||
Mock(return_value=['/example.com(0.0.0.0)'])
|
||||
def cbk(d, key, value):
|
||||
d[key].append(value)
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.2'}
|
||||
self._driver._get_export_dir_dict =\
|
||||
Mock(return_value={'example.com': ['10.0.0.1'],
|
||||
'fakename': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
expected_exec = ['true']
|
||||
|
@ -424,45 +454,67 @@ class GlusterfsShareDriverTestCase(test.TestCase):
|
|||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'/example.com(0.0.0.0),/fakename(0.0.0.0)')
|
||||
'/example.com(10.0.0.1),/fakename(10.0.0.1|10.0.0.2)')
|
||||
|
||||
def test_manage_access_removing_last_entry(self):
|
||||
def cbk(d, key, value):
|
||||
d.pop(key)
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict =\
|
||||
Mock(return_value={'fakename': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
expected_exec = ['true']
|
||||
ret = self._driver._manage_access(self._context, self.share, access,
|
||||
cbk)
|
||||
self.assertEqual(fake_utils.fake_execute_get_log(), expected_exec)
|
||||
self.assertEqual(ret, None)
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][1],
|
||||
'reset')
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'nfs.export-dir')
|
||||
|
||||
def test_allow_access_with_share_having_noaccess(self):
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list =\
|
||||
Mock(return_value=['/example.com(0.0.0.0)'])
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict =\
|
||||
Mock(return_value={'example.com': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
self._driver.allow_access(self._context, self.share, access)
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'/example.com(0.0.0.0),/fakename(0.0.0.0)')
|
||||
'/example.com(10.0.0.1),/fakename(10.0.0.1)')
|
||||
|
||||
def test_allow_access_with_share_having_access(self):
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list = \
|
||||
Mock(return_value=['/fakename(0.0.0.0)'])
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict = \
|
||||
Mock(return_value={'fakename': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
self._driver.allow_access(self._context, self.share, access)
|
||||
self.assertFalse(self._driver.gluster_address.make_gluster_args.called)
|
||||
|
||||
def test_deny_access_with_share_having_noaccess(self):
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list = Mock(return_value=[])
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict = Mock(return_value={})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
self._driver.deny_access(self._context, self.share, access)
|
||||
self.assertFalse(self._driver.gluster_address.make_gluster_args.called)
|
||||
|
||||
def test_deny_access_with_share_having_access(self):
|
||||
access = {'access_type': 'ip', 'access_to': '0.0.0.0'}
|
||||
self._driver._get_export_dir_list = \
|
||||
Mock(return_value=['/fakename(0.0.0.0)', '/example.com(0.0.0.0)'])
|
||||
access = {'access_type': 'ip', 'access_to': '10.0.0.1'}
|
||||
self._driver._get_export_dir_dict = \
|
||||
Mock(return_value={'fakename': ['10.0.0.1'],
|
||||
'example.com': ['10.0.0.1']})
|
||||
self._driver.gluster_address = Mock(make_gluster_args=
|
||||
Mock(return_value=(('true',), {})))
|
||||
self._driver.deny_access(self._context, self.share, access)
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'/example.com(0.0.0.0)')
|
||||
'/example.com(10.0.0.1)')
|
||||
|
|
Loading…
Reference in New Issue