GPFS: Add update_access()

Add update_access() implementation to replace the deprecated
allow_access() and deny_access().

Change-Id: I206f1284bddc02452087e24061619ca3c04395a4
Implements: blueprint gpfs-update-access
This commit is contained in:
Mark Sturdevant 2016-12-23 19:02:05 -08:00
parent ba8e8d1ada
commit cbda16bc57
2 changed files with 466 additions and 87 deletions

View File

@ -512,15 +512,20 @@ class GPFSShareDriver(driver.ExecuteMixin, driver.GaneshaMixin,
def ensure_share(self, ctx, share, share_server=None):
"""Ensure that storage are mounted and exported."""
def allow_access(self, ctx, share, access, share_server=None):
"""Allow access to the share."""
def update_access(self, context, share, access_rules, add_rules,
delete_rules, share_server=None):
"""Update access rules for given share."""
helper = self._get_helper(share)
location = self._get_share_path(share)
self._get_helper(share).allow_access(location, share, access)
def deny_access(self, ctx, share, access, share_server=None):
"""Deny access to the share."""
location = self._get_share_path(share)
self._get_helper(share).deny_access(location, share, access)
for access in delete_rules:
helper.deny_access(location, share, access)
for access in add_rules:
helper.allow_access(location, share, access)
if not (add_rules or delete_rules):
helper.resync_access(location, share, access_rules)
def check_for_setup_error(self):
"""Returns an error if prerequisites aren't met."""
@ -787,7 +792,7 @@ class NASHelperBase(object):
"""Construct location of new export."""
return ':'.join([self.configuration.gpfs_share_export_ip, local_path])
def get_export_options(self, share, access, helper, options_not_allowed):
def get_export_options(self, share, access, helper):
"""Get the export options."""
extra_specs = share_types.get_extra_specs_from_share(share)
if helper == 'KNFS':
@ -797,11 +802,13 @@ class NASHelperBase(object):
else:
export_options = None
if export_options:
options = export_options.lower().split(',')
else:
options = []
options = self._get_validated_opt_list(export_options)
options.append(self.get_access_option(access))
return ','.join(options)
def _validate_export_options(self, options):
"""Validate the export options."""
options_not_allowed = self._get_options_not_allowed()
invalid_options = [
option for option in options if option in options_not_allowed
]
@ -811,32 +818,39 @@ class NASHelperBase(object):
'it is set by access_type.'
% invalid_options)
if access['access_level'] == constants.ACCESS_LEVEL_RO:
if helper == 'KNFS':
options.append(constants.ACCESS_LEVEL_RO)
elif helper == 'CES':
options.append('access_type=ro')
def _get_validated_opt_list(self, export_options):
"""Validate the export options and return an option list."""
if export_options:
options = export_options.lower().split(',')
self._validate_export_options(options)
else:
if helper == 'KNFS':
options.append(constants.ACCESS_LEVEL_RW)
elif helper == 'CES':
options.append('access_type=rw')
options = []
return options
return ','.join(options)
@abc.abstractmethod
def get_access_option(self, access):
"""Get access option string based on access level."""
@abc.abstractmethod
def _get_options_not_allowed(self):
"""Get access options that are not allowed in extra-specs."""
@abc.abstractmethod
def remove_export(self, local_path, share):
"""Remove export."""
@abc.abstractmethod
def allow_access(self, local_path, share, access_type, access):
def allow_access(self, local_path, share, access):
"""Allow access to the host."""
@abc.abstractmethod
def deny_access(self, local_path, share, access_type, access,
force=False):
def deny_access(self, local_path, share, access):
"""Deny access to the host."""
@abc.abstractmethod
def resync_access(self, local_path, share, access_rules):
"""Re-sync all access rules for given share."""
class KNFSHelper(NASHelperBase):
"""Wrapper for Kernel NFS Commands."""
@ -921,39 +935,56 @@ class KNFSHelper(NASHelperBase):
def remove_export(self, local_path, share):
"""Remove export."""
def allow_access(self, local_path, share, access):
def get_access_option(self, access):
"""Get access option string based on access level."""
return access['access_level']
def _get_options_not_allowed(self):
"""Get access options that are not allowed in extra-specs."""
return list(constants.ACCESS_LEVELS)
def _get_exports(self):
"""Get exportfs output."""
try:
out, __ = self._execute('exportfs', run_as_root=True)
except exception.ProcessExecutionError as e:
msg = (_('Failed to check exports on the systems. '
' Error: %s.') % e)
LOG.error(msg)
raise exception.GPFSException(msg)
return out
def allow_access(self, local_path, share, access, error_on_exists=True):
"""Allow access to one or more vm instances."""
if access['access_type'] != 'ip':
raise exception.InvalidShareAccess(reason='Only ip access type '
'supported.')
out = self._has_client_access(local_path, access['access_to'])
if error_on_exists:
# check if present in export
out = re.search(
re.escape(local_path) + '[\s\n]*'
+ re.escape(access['access_to']), self._get_exports())
if out:
access_type = access['access_type']
access_to = access['access_to']
raise exception.ShareAccessExists(access_type=access_type,
access=access_to)
if out is not None:
access_type = access['access_type']
access_to = access['access_to']
raise exception.ShareAccessExists(access_type=access_type,
access=access_to)
options_not_allowed = list(constants.ACCESS_LEVELS)
export_opts = self.get_export_options(share, access, 'KNFS',
options_not_allowed)
export_opts = self.get_export_options(share, access, 'KNFS')
cmd = ['exportfs', '-o', export_opts,
':'.join([access['access_to'], local_path])]
try:
self._publish_access(*cmd)
except exception.ProcessExecutionError as e:
msg = (_('Failed to allow access for share %(sharename)s. '
'Error: %(excmsg)s.') %
{'sharename': share['name'],
'excmsg': e})
LOG.error(msg)
except exception.ProcessExecutionError:
msg = _('Failed to allow access for share %s.') % share['name']
LOG.exception(msg)
raise exception.GPFSException(msg)
def deny_access(self, local_path, share, access, force=False):
def _deny_ip(self, local_path, share, ip):
"""Remove access for one or more vm instances."""
ip = access['access_to']
cmd = ['exportfs', '-u', ':'.join([ip, local_path])]
try:
# Can get exit code 0 for success or 1 for already gone (also
@ -970,6 +1001,25 @@ class KNFSHelper(NASHelperBase):
# So, verify that the IP access was completely removed.
self._verify_denied_access(local_path, share, ip)
def deny_access(self, local_path, share, access):
"""Remove access for one or more vm instances."""
self._deny_ip(local_path, share, access['access_to'])
def _remove_other_access(self, local_path, share, access_rules):
"""Remove any client access that is not in access_rules."""
exports = self._get_exports()
gpfs_ips = set(NFSHelper.get_host_list(exports, local_path))
manila_ips = set([x['access_to'] for x in access_rules])
remove_ips = gpfs_ips - manila_ips
for ip in remove_ips:
self._deny_ip(local_path, share, ip)
def resync_access(self, local_path, share, access_rules):
"""Re-sync all access rules for given share."""
for access in access_rules:
self.allow_access(local_path, share, access, error_on_exists=False)
self._remove_other_access(local_path, share, access_rules)
class CESHelper(NASHelperBase):
"""Wrapper for NFS by Spectrum Scale CES"""
@ -988,15 +1038,64 @@ class CESHelper(NASHelperBase):
raise exception.GPFSException(msg)
return out
@staticmethod
def _fix_export_data(data, headers):
"""Export data split by ':' may need fixing if client had colons."""
# If an IPv6 client shows up then ':' delimiters don't work.
# So use header positions to get data before/after Clients.
# Then what is left in between can be joined back into a client IP.
client_index = headers.index('Clients')
# reverse_client_index is distance from end.
reverse_client_index = len(headers) - (client_index + 1)
after_client_index = len(data) - reverse_client_index
before_client = data[:client_index]
client = data[client_index: after_client_index]
after_client = data[after_client_index:]
result_data = before_client
result_data.append(':'.join(client)) # Fixes colons in client IP
result_data.extend(after_client)
return result_data
def _get_nfs_client_exports(self, local_path):
"""Get the current NFS client export details from GPFS."""
out = self._execute_mmnfs_command(
('list', '-n', local_path, '-Y'),
'Failed to get exports from the system.')
# Remove the header line and use the headers to describe the data
lines = out.splitlines()
for line in lines:
data = line.split(':')
if "HEADER" in data:
headers = data
lines.remove(line)
break
else:
msg = _('Failed to parse exports for path %s. '
'No HEADER found.') % local_path
LOG.error(msg)
raise exception.GPFSException(msg)
exports = []
for line in lines:
data = line.split(':')
if len(data) < 3:
continue # Skip empty lines (and anything less than minimal).
result_data = self._fix_export_data(data, headers)
exports.append(dict(zip(headers, result_data)))
return exports
def _has_client_access(self, local_path, access_to=None):
"""Check path for any export or for one with a specific IP address."""
err_msg = 'Failed to check exports on the system.'
out = self._execute_mmnfs_command(('list', '-n', local_path, '-Y'),
err_msg)
if access_to:
return ':' + access_to + ':' in out
else:
return ':' + local_path + ':' in out
gpfs_clients = self._get_nfs_client_exports(local_path)
return gpfs_clients and (access_to is None or access_to in [
x['Clients'] for x in gpfs_clients])
def remove_export(self, local_path, share):
"""Remove export."""
@ -1005,6 +1104,17 @@ class CESHelper(NASHelperBase):
% share['name'])
self._execute_mmnfs_command(('remove', local_path), err_msg)
def _get_options_not_allowed(self):
"""Get access options that are not allowed in extra-specs."""
return ['access_type=ro', 'access_type=rw']
def get_access_option(self, access):
"""Get access option string based on access level."""
if access['access_level'] == constants.ACCESS_LEVEL_RO:
return 'access_type=ro'
else:
return 'access_type=rw'
def allow_access(self, local_path, share, access):
"""Allow access to the host."""
@ -1013,9 +1123,7 @@ class CESHelper(NASHelperBase):
'supported.')
has_exports = self._has_client_access(local_path)
options_not_allowed = ['access_type=ro', 'access_type=rw']
export_opts = self.get_export_options(share, access, 'CES',
options_not_allowed)
export_opts = self.get_export_options(share, access, 'CES')
if not has_exports:
cmd = ['add', local_path, '-c',
@ -1040,3 +1148,80 @@ class CESHelper(NASHelperBase):
self._execute_mmnfs_command(('change', local_path,
'--nfsremove', access['access_to']),
err_msg)
def _get_client_opts(self, access, opts_list):
"""Get client options string for access rule and NFS options."""
nfs_opts = ','.join([self.get_access_option(access)] + opts_list)
return '%(ip)s(%(nfs_opts)s)' % {'ip': access['access_to'],
'nfs_opts': nfs_opts}
def _get_share_opts(self, share):
"""Get a list of NFS options from the share's share type."""
extra_specs = share_types.get_extra_specs_from_share(share)
opts_list = self._get_validated_opt_list(
extra_specs.get('ces:export_options'))
return opts_list
def _nfs_change(self, local_path, share, access_rules, gpfs_clients):
"""Bulk add/update/remove of access rules for share."""
opts_list = self._get_share_opts(share)
# Create a map of existing client access rules from GPFS.
# Key from 'Clients' is an IP address or
# Value from 'Access_Type' is RW|RO (case varies)
gpfs_map = {
x['Clients']: x['Access_Type'].lower() for x in gpfs_clients}
gpfs_ips = set(gpfs_map.keys())
manila_ips = set([x['access_to'] for x in access_rules])
add_ips = manila_ips - gpfs_ips
update_ips = gpfs_ips.intersection(manila_ips)
remove_ips = gpfs_ips - manila_ips
adds = []
updates = []
if add_ips or update_ips:
for access in access_rules:
ip = access['access_to']
if ip in add_ips:
adds.append(self._get_client_opts(access, opts_list))
elif (ip in update_ips
and access['access_level'] != gpfs_map[ip]):
updates.append(self._get_client_opts(access, opts_list))
if remove_ips or adds or updates:
cmd = ['change', local_path]
if remove_ips:
cmd.append('--nfsremove')
cmd.append(','.join(remove_ips))
if adds:
cmd.append('--nfsadd')
cmd.append(';'.join(adds))
if updates:
cmd.append('--nfschange')
cmd.append(';'.join(updates))
err_msg = ('Failed to resync access for share %s.' % share['name'])
self._execute_mmnfs_command(cmd, err_msg)
def _nfs_add(self, access_rules, local_path, share):
"""Bulk add of access rules to share."""
if not access_rules:
return
opts_list = self._get_share_opts(share)
client_options = []
for access in access_rules:
client_options.append(self._get_client_opts(access, opts_list))
cmd = ['add', local_path, '-c', ';'.join(client_options)]
err_msg = ('Failed to resync access for share %s.' % share['name'])
self._execute_mmnfs_command(cmd, err_msg)
def resync_access(self, local_path, share, access_rules):
"""Re-sync all access rules for given share."""
gpfs_clients = self._get_nfs_client_exports(local_path)
if not gpfs_clients:
self._nfs_add(access_rules, local_path, share)
else:
self._nfs_change(local_path, share, access_rules, gpfs_clients)

View File

@ -447,25 +447,88 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
'0:10G')
self._driver._get_gpfs_device.assert_called_once_with()
def test_allow_access(self):
def test_update_access_allow(self):
"""Test allow_access functionality via update_access."""
self._driver._get_share_path = mock.Mock(
return_value=self.fakesharepath
)
self._helper_fake.allow_access = mock.Mock()
self._driver.allow_access(self._context, self.share,
self.access, share_server=None)
self._driver.update_access(self._context,
self.share,
["ignored"],
[self.access],
[],
share_server=None)
self._helper_fake.allow_access.assert_called_once_with(
self.fakesharepath, self.share, self.access)
self.assertFalse(self._helper_fake.resync_access.called)
self._driver._get_share_path.assert_called_once_with(self.share)
def test_deny_access(self):
def test_update_access_deny(self):
"""Test deny_access functionality via update_access."""
self._driver._get_share_path = mock.Mock(return_value=self.
fakesharepath)
self._helper_fake.deny_access = mock.Mock()
self._driver.deny_access(self._context, self.share,
self.access, share_server=None)
self._driver.update_access(self._context,
self.share,
["ignored"],
[],
[self.access],
share_server=None)
self._helper_fake.deny_access.assert_called_once_with(
self.fakesharepath, self.share, self.access)
self.assertFalse(self._helper_fake.resync_access.called)
self._driver._get_share_path.assert_called_once_with(self.share)
def test_update_access_both(self):
"""Test update_access with allow and deny lists."""
self._driver._get_share_path = mock.Mock(return_value=self.
fakesharepath)
self._helper_fake.deny_access = mock.Mock()
self._helper_fake.allow_access = mock.Mock()
self._helper_fake.resync_access = mock.Mock()
access_1 = fake_share.fake_access(access_to="1.1.1.1")
access_2 = fake_share.fake_access(access_to="2.2.2.2")
self._driver.update_access(self._context,
self.share,
["ignore"],
[access_1],
[access_2],
share_server=None)
self.assertFalse(self._helper_fake.resync_access.called)
self._helper_fake.allow_access.assert_called_once_with(
self.fakesharepath, self.share, access_1)
self._helper_fake.deny_access.assert_called_once_with(
self.fakesharepath, self.share, access_2)
self._driver._get_share_path.assert_called_once_with(self.share)
def test_update_access_resync(self):
"""Test recovery mode update_access."""
self._driver._get_share_path = mock.Mock(return_value=self.
fakesharepath)
self._helper_fake.deny_access = mock.Mock()
self._helper_fake.allow_access = mock.Mock()
self._helper_fake.resync_access = mock.Mock()
access_1 = fake_share.fake_access(access_to="1.1.1.1")
access_2 = fake_share.fake_access(access_to="2.2.2.2")
self._driver.update_access(self._context,
self.share,
[access_1, access_2],
[],
[],
share_server=None)
self._helper_fake.resync_access.assert_called_once_with(
self.fakesharepath, self.share, [access_1, access_2])
self.assertFalse(self._helper_fake.allow_access.called)
self.assertFalse(self._helper_fake.allow_access.called)
self._driver._get_share_path.assert_called_once_with(self.share)
def test__check_gpfs_state_active(self):
@ -1020,23 +1083,49 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
)
self._driver.configuration.gpfs_share_export_ip = orig_value
def test_knfs_get_export_options(self):
def test_knfs_resync_access(self):
self._knfs_helper.allow_access = mock.Mock()
path = self.fakesharepath
to_remove = '3.3.3.3'
fake_exportfs_before = ('%(path)s\n\t\t%(ip)s\n'
'/other/path\n\t\t4.4.4.4\n' %
{'path': path, 'ip': to_remove})
fake_exportfs_after = '/other/path\n\t\t4.4.4.4\n'
self._knfs_helper._execute = mock.Mock(
return_value=(fake_exportfs_before, ''))
self._knfs_helper._publish_access = mock.Mock(
side_effect=[[(fake_exportfs_before, '')],
[(fake_exportfs_after, '')]])
access_1 = fake_share.fake_access(access_to="1.1.1.1")
access_2 = fake_share.fake_access(access_to="2.2.2.2")
self._knfs_helper.resync_access(path, self.share, [access_1, access_2])
self._knfs_helper.allow_access.assert_has_calls([
mock.call(path, self.share, access_1, error_on_exists=False),
mock.call(path, self.share, access_2, error_on_exists=False)])
self._knfs_helper._execute.assert_called_once_with(
'exportfs', run_as_root=True)
self._knfs_helper._publish_access.assert_has_calls([
mock.call('exportfs', '-u',
'%(ip)s:%(path)s' % {'ip': to_remove, 'path': path},
check_exit_code=[0, 1]),
mock.call('exportfs')])
@ddt.data('rw', 'ro')
def test_knfs_get_export_options(self, access_level):
mock_out = {"knfs:export_options": "no_root_squash"}
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['rw', 'ro']
out = self._knfs_helper.get_export_options(self.share, access,
'KNFS', options_not_allowed)
self.assertEqual("no_root_squash,rw", out)
access = fake_share.fake_access(access_level=access_level)
out = self._knfs_helper.get_export_options(self.share, access, 'KNFS')
self.assertEqual("no_root_squash,%s" % access_level, out)
def test_knfs_get_export_options_default(self):
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value={}))
access = self.access
options_not_allowed = ['rw', 'ro']
out = self._knfs_helper.get_export_options(self.share, access,
'KNFS', options_not_allowed)
out = self._knfs_helper.get_export_options(self.share, access, 'KNFS')
self.assertEqual("rw", out)
def test_knfs_get_export_options_invalid_option_ro(self):
@ -1044,22 +1133,20 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['rw', 'ro']
share = fake_share.fake_share(share_type="fake_share_type")
self.assertRaises(exception.InvalidInput,
self._knfs_helper.get_export_options,
share, access, 'KNFS', options_not_allowed)
share, access, 'KNFS')
def test_knfs_get_export_options_invalid_option_rw(self):
mock_out = {"knfs:export_options": "rw"}
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['rw', 'ro']
share = fake_share.fake_share(share_type="fake_share_type")
self.assertRaises(exception.InvalidInput,
self._knfs_helper.get_export_options,
share, access, 'KNFS', options_not_allowed)
share, access, 'KNFS')
@ddt.data(("/gpfs0/share-fakeid\t10.0.0.1", None),
("", None),
@ -1094,15 +1181,13 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
)
self._knfs_helper._publish_access = mock.Mock()
access = self.access
options_not_allowed = ['rw', 'ro']
local_path = self.fakesharepath
self._knfs_helper.allow_access(local_path, self.share, access)
self._knfs_helper._execute.assert_called_once_with('exportfs',
run_as_root=True)
self.assertTrue(re.search.called)
self._knfs_helper.get_export_options.assert_any_call(
self.share, access, 'KNFS',
options_not_allowed)
self.share, access, 'KNFS')
cmd = ['exportfs', '-o', export_opts, ':'.join([access['access_to'],
local_path])]
self._knfs_helper._publish_access.assert_called_once_with(*cmd)
@ -1122,6 +1207,21 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
self.assertTrue(re.search.called)
self.assertFalse(self._knfs_helper.get_export_options.called)
def test_knfs_allow_access_publish_exception(self):
self._knfs_helper.get_export_options = mock.Mock()
self._knfs_helper._publish_access = mock.Mock(
side_effect=exception.ProcessExecutionError('boom'))
self.assertRaises(exception.GPFSException,
self._knfs_helper.allow_access,
self.fakesharepath,
self.share,
self.access,
error_on_exists=False)
self.assertTrue(self._knfs_helper.get_export_options.called)
self.assertTrue(self._knfs_helper._publish_access.called)
def test_knfs_allow_access_invalid_access(self):
access = fake_share.fake_access(access_type='test')
self.assertRaises(exception.InvalidShareAccess,
@ -1270,23 +1370,22 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
check_exit_code=True, run_as_root=False),
mock.call(fake_command, check_exit_code=True, run_as_root=True)])
def test_ces_get_export_options(self):
@ddt.data('rw', 'ro')
def test_ces_get_export_options(self, access_level):
mock_out = {"ces:export_options": "squash=no_root_squash"}
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['access_type=ro', 'access_type=rw']
out = self._ces_helper.get_export_options(self.share, access,
'CES', options_not_allowed)
self.assertEqual("squash=no_root_squash,access_type=rw", out)
access = fake_share.fake_access(access_level=access_level)
out = self._ces_helper.get_export_options(self.share, access, 'CES')
self.assertEqual("squash=no_root_squash,access_type=%s" % access_level,
out)
def test_ces_get_export_options_default(self):
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value={}))
access = self.access
options_not_allowed = ['access_type=ro', 'access_type=rw']
out = self._ces_helper.get_export_options(self.share, access,
'CES', options_not_allowed)
'CES')
self.assertEqual("access_type=rw", out)
def test_ces_get_export_options_invalid_option_ro(self):
@ -1294,22 +1393,47 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['access_type=ro', 'access_type=rw']
share = fake_share.fake_share(share_type="fake_share_type")
self.assertRaises(exception.InvalidInput,
self._ces_helper.get_export_options,
share, access, 'CES', options_not_allowed)
share, access, 'CES')
def test_ces_get_export_options_invalid_option_rw(self):
mock_out = {"ces:export_options": "access_type=rw"}
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value=mock_out))
access = self.access
options_not_allowed = ['access_type=ro', 'access_type=rw']
share = fake_share.fake_share(share_type="fake_share_type")
self.assertRaises(exception.InvalidInput,
self._ces_helper.get_export_options,
share, access, 'CES', options_not_allowed)
share, access, 'CES')
def test__get_nfs_client_exports_exception(self):
self._ces_helper._execute = mock.Mock(return_value=('junk', ''))
local_path = self.fakesharepath
self.assertRaises(exception.GPFSException,
self._ces_helper._get_nfs_client_exports,
local_path)
self._ces_helper._execute.assert_called_once_with(
'mmnfs', 'export', 'list', '-n', local_path, '-Y')
@ddt.data('44.3.2.11', '1:2:3:4:5:6:7:8')
def test__fix_export_data(self, ip):
data = None
for line in self.fake_ces_exports.splitlines():
if "HEADER" in line:
headers = line.split(':')
if ip in line:
data = line.split(':')
break
self.assertIsNotNone(
data, "Test data did not contain a line with the test IP.")
result_data = self._ces_helper._fix_export_data(data, headers)
self.assertEqual(ip, result_data[headers.index('Clients')])
@ddt.data((None, True),
('44.3.2.11', True),
@ -1317,6 +1441,9 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
('4.3.2.1', False),
('4.3.2.11', False),
('1.2.3.4', False),
('', False),
('*', False),
('.', False),
('1:2:3:4:5:6:7:8', True))
@ddt.unpack
def test_ces__has_client_access(self, ip, has_access):
@ -1441,3 +1568,70 @@ mmcesnfslsexport:nfsexports:HEADER:version:reserved:reserved:Path:Delegations:Cl
self.assertRaises(exception.GPFSException,
self._ces_helper.deny_access, local_path,
self.share, access)
def test_ces_resync_access_add(self):
mock_out = self.fake_ces_exports_not_found
self._ces_helper._execute = mock.Mock(return_value=(mock_out, ''))
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value={}))
access_rules = [self.access]
local_path = self.fakesharepath
self._ces_helper.resync_access(local_path, self.share, access_rules)
self._ces_helper._execute.assert_has_calls([
mock.call('mmnfs', 'export', 'list', '-n', local_path, '-Y'),
mock.call('mmnfs', 'export', 'add', local_path, '-c',
self.access['access_to'] + '(' + "access_type=rw" + ')')
])
share_types.get_extra_specs_from_share.assert_called_once_with(
self.share)
def test_ces_resync_access_change(self):
class SortedMatch(object):
def __init__(self, f, expected):
self.assertEqual = f
self.expected = expected
def __eq__(self, actual):
expected_list = self.expected.split(',')
actual_list = actual.split(',')
self.assertEqual(sorted(expected_list), sorted(actual_list))
return True
mock_out = self.fake_ces_exports
self._ces_helper._execute = mock.Mock(
return_value=(mock_out, ''))
self.mock_object(share_types, 'get_extra_specs_from_share',
mock.Mock(return_value={}))
access_rules = [fake_share.fake_access(access_to='1.1.1.1'),
fake_share.fake_access(
access_to='10.0.0.1', access_level='ro')]
local_path = self.fakesharepath
self._ces_helper.resync_access(local_path, self.share, access_rules)
share_types.get_extra_specs_from_share.assert_called_once_with(
self.share)
to_remove = '1:2:3:4:5:6:7:8,44.3.2.11'
to_add = access_rules[0]['access_to'] + '(' + "access_type=rw" + ')'
to_change = access_rules[1]['access_to'] + '(' + "access_type=ro" + ')'
self._ces_helper._execute.assert_has_calls([
mock.call('mmnfs', 'export', 'list', '-n', local_path, '-Y'),
mock.call('mmnfs', 'export', 'change', local_path,
'--nfsremove', SortedMatch(self.assertEqual, to_remove),
'--nfsadd', to_add,
'--nfschange', to_change)
])
def test_ces_resync_nothing(self):
"""Test that hits the add-no-rules case."""
mock_out = self.fake_ces_exports_not_found
self._ces_helper._execute = mock.Mock(return_value=(mock_out, ''))
local_path = self.fakesharepath
self._ces_helper.resync_access(local_path, None, [])
self._ces_helper._execute.assert_called_once_with(
'mmnfs', 'export', 'list', '-n', local_path, '-Y')