"Fossies" - the Fresh Open Source Software Archive

Member "manila-8.1.4/manila/share/drivers/helpers.py" (19 Nov 2020, 25531 Bytes) of package /linux/misc/openstack/manila-8.1.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "helpers.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 8.1.3_vs_8.1.4.

    1 # Copyright 2015 Mirantis Inc.
    2 # All Rights Reserved.
    3 #
    4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 # not use this file except in compliance with the License. You may obtain
    6 # a copy of the License at
    7 #
    8 # http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 # Unless required by applicable law or agreed to in writing, software
   11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 # License for the specific language governing permissions and limitations
   14 # under the License.
   15 
   16 import copy
   17 import ipaddress
   18 import os
   19 import re
   20 import six
   21 
   22 from oslo_log import log
   23 
   24 from manila.common import constants as const
   25 from manila import exception
   26 from manila.i18n import _
   27 from manila import utils
   28 
   29 LOG = log.getLogger(__name__)
   30 
   31 
   32 class NASHelperBase(object):
   33     """Interface to work with share."""
   34 
   35     def __init__(self, execute, ssh_execute, config_object):
   36         self.configuration = config_object
   37         self._execute = execute
   38         self._ssh_exec = ssh_execute
   39 
   40     def init_helper(self, server):
   41         pass
   42 
   43     def create_exports(self, server, share_name, recreate=False):
   44         """Create new exports, delete old ones if exist."""
   45         raise NotImplementedError()
   46 
   47     def remove_exports(self, server, share_name):
   48         """Remove exports."""
   49         raise NotImplementedError()
   50 
   51     def configure_access(self, server, share_name):
   52         """Configure server before allowing access."""
   53         pass
   54 
   55     def update_access(self, server, share_name, access_rules, add_rules,
   56                       delete_rules):
   57         """Update access rules for given share.
   58 
   59         This driver has two different behaviors according to parameters:
   60         1. Recovery after error - 'access_rules' contains all access_rules,
   61         'add_rules' and 'delete_rules' shall be empty. Previously existing
   62         access rules are cleared and then added back according
   63         to 'access_rules'.
   64 
   65         2. Adding/Deleting of several access rules - 'access_rules' contains
   66         all access_rules, 'add_rules' and 'delete_rules' contain rules which
   67         should be added/deleted. Rules in 'access_rules' are ignored and
   68         only rules from 'add_rules' and 'delete_rules' are applied.
   69 
   70         :param server: None or Share server's backend details
   71         :param share_name: Share's path according to id.
   72         :param access_rules: All access rules for given share
   73         :param add_rules: Empty List or List of access rules which should be
   74                added. access_rules already contains these rules.
   75         :param delete_rules: Empty List or List of access rules which should be
   76                removed. access_rules doesn't contain these rules.
   77         """
   78         raise NotImplementedError()
   79 
   80     @staticmethod
   81     def _verify_server_has_public_address(server):
   82         if 'public_address' in server:
   83             pass
   84         elif 'public_addresses' in server:
   85             if not isinstance(server['public_addresses'], list):
   86                 raise exception.ManilaException(_("public_addresses must be "
   87                                                   "a list"))
   88         else:
   89             raise exception.ManilaException(
   90                 _("Can not get public_address(es) for generation of export."))
   91 
   92     def _get_export_location_template(self, export_location_or_path):
   93         """Returns template of export location.
   94 
   95         Example for NFS:
   96             %s:/path/to/share
   97         Example for CIFS:
   98             \\\\%s\\cifs_share_name
   99         """
  100         raise NotImplementedError()
  101 
  102     def get_exports_for_share(self, server, export_location_or_path):
  103         """Returns list of exports based on server info."""
  104         self._verify_server_has_public_address(server)
  105         export_location_template = self._get_export_location_template(
  106             export_location_or_path)
  107         export_locations = []
  108 
  109         if 'public_addresses' in server:
  110             pairs = list(map(lambda addr: (addr, False),
  111                              server['public_addresses']))
  112         else:
  113             pairs = [(server['public_address'], False)]
  114 
  115         # NOTE(vponomaryov):
  116         # Generic driver case: 'admin_ip' exists only in case of DHSS=True
  117         # mode and 'ip' exists in case of DHSS=False mode.
  118         # Use one of these for creation of export location for service needs.
  119         service_address = server.get("admin_ip", server.get("ip"))
  120         if service_address:
  121             pairs.append((service_address, True))
  122         for ip, is_admin in pairs:
  123             export_locations.append({
  124                 "path": export_location_template % ip,
  125                 "is_admin_only": is_admin,
  126                 "metadata": {
  127                     # TODO(vponomaryov): remove this fake metadata when
  128                     # proper appears.
  129                     "export_location_metadata_example": "example",
  130                 },
  131             })
  132         return export_locations
  133 
  134     def get_share_path_by_export_location(self, server, export_location):
  135         """Returns share path by its export location."""
  136         raise NotImplementedError()
  137 
  138     def disable_access_for_maintenance(self, server, share_name):
  139         """Disables access to share to perform maintenance operations."""
  140 
  141     def restore_access_after_maintenance(self, server, share_name):
  142         """Enables access to share after maintenance operations were done."""
  143 
  144     @staticmethod
  145     def validate_access_rules(access_rules, allowed_types, allowed_levels):
  146         """Validates access rules according to access_type and access_level.
  147 
  148         :param access_rules: List of access rules to be validated.
  149         :param allowed_types: tuple of allowed type values.
  150         :param allowed_levels: tuple of allowed level values.
  151         """
  152         for access in (access_rules or []):
  153             access_type = access['access_type']
  154             access_level = access['access_level']
  155             if access_type not in allowed_types:
  156                 reason = _("Only %s access type allowed.") % (
  157                     ', '.join(tuple(["'%s'" % x for x in allowed_types])))
  158                 raise exception.InvalidShareAccess(reason=reason)
  159             if access_level not in allowed_levels:
  160                 raise exception.InvalidShareAccessLevel(level=access_level)
  161 
  162     def _get_maintenance_file_path(self, share_name):
  163         return os.path.join(self.configuration.share_mount_path,
  164                             "%s.maintenance" % share_name)
  165 
  166 
  167 def nfs_synchronized(f):
  168 
  169     def wrapped_func(self, *args, **kwargs):
  170         key = "nfs-%s" % args[0].get("lock_name", args[0]["instance_id"])
  171 
  172         # NOTE(vponomaryov): 'external' lock is required for DHSS=False
  173         # mode of LVM and Generic drivers, that may have lots of
  174         # driver instances on single host.
  175         @utils.synchronized(key, external=True)
  176         def source_func(self, *args, **kwargs):
  177             return f(self, *args, **kwargs)
  178 
  179         return source_func(self, *args, **kwargs)
  180 
  181     return wrapped_func
  182 
  183 
  184 class NFSHelper(NASHelperBase):
  185     """Interface to work with share."""
  186 
  187     def create_exports(self, server, share_name, recreate=False):
  188         path = os.path.join(self.configuration.share_mount_path, share_name)
  189         server_copy = copy.copy(server)
  190         public_addresses = []
  191         if 'public_addresses' in server_copy:
  192             for address in server_copy['public_addresses']:
  193                 public_addresses.append(
  194                     self._escaped_address(address))
  195             server_copy['public_addresses'] = public_addresses
  196 
  197         for t in ['public_address', 'admin_ip', 'ip']:
  198             address = server_copy.get(t)
  199             if address is not None:
  200                 server_copy[t] = self._escaped_address(address)
  201 
  202         return self.get_exports_for_share(server_copy, path)
  203 
  204     @staticmethod
  205     def _escaped_address(address):
  206         addr = ipaddress.ip_address(six.text_type(address))
  207         if addr.version == 4:
  208             return six.text_type(addr)
  209         else:
  210             return '[%s]' % six.text_type(addr)
  211 
  212     def init_helper(self, server):
  213         try:
  214             self._ssh_exec(server, ['sudo', 'exportfs'])
  215         except exception.ProcessExecutionError as e:
  216             if 'command not found' in e.stderr:
  217                 raise exception.ManilaException(
  218                     _('NFS server is not installed on %s')
  219                     % server['instance_id'])
  220             LOG.error(e.stderr)
  221 
  222     def remove_exports(self, server, share_name):
  223         """Remove exports."""
  224 
  225     @nfs_synchronized
  226     def update_access(self, server, share_name, access_rules, add_rules,
  227                       delete_rules):
  228         """Update access rules for given share.
  229 
  230         Please refer to base class for a more in-depth description.
  231         """
  232         local_path = os.path.join(self.configuration.share_mount_path,
  233                                   share_name)
  234         out, err = self._ssh_exec(server, ['sudo', 'exportfs'])
  235         # Recovery mode
  236         if not (add_rules or delete_rules):
  237 
  238             self.validate_access_rules(
  239                 access_rules, ('ip',),
  240                 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW))
  241 
  242             hosts = self.get_host_list(out, local_path)
  243             for host in hosts:
  244                 parsed_host = self._get_parsed_address_or_cidr(host)
  245                 self._ssh_exec(server, ['sudo', 'exportfs', '-u',
  246                                         ':'.join((parsed_host, local_path))])
  247             self._sync_nfs_temp_and_perm_files(server)
  248             for access in access_rules:
  249                 rules_options = '%s,no_subtree_check,no_root_squash'
  250                 access_to = self._get_parsed_address_or_cidr(
  251                     access['access_to'])
  252                 self._ssh_exec(
  253                     server,
  254                     ['sudo', 'exportfs', '-o',
  255                      rules_options % access['access_level'],
  256                      ':'.join((access_to, local_path))])
  257             self._sync_nfs_temp_and_perm_files(server)
  258         # Adding/Deleting specific rules
  259         else:
  260 
  261             self.validate_access_rules(
  262                 add_rules, ('ip',),
  263                 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW))
  264 
  265             for access in delete_rules:
  266                 try:
  267                     self.validate_access_rules(
  268                         [access], ('ip',),
  269                         (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW))
  270                 except (exception.InvalidShareAccess,
  271                         exception.InvalidShareAccessLevel):
  272                     LOG.warning(
  273                         "Unsupported access level %(level)s or access type "
  274                         "%(type)s, skipping removal of access rule to "
  275                         "%(to)s.", {'level': access['access_level'],
  276                                     'type': access['access_type'],
  277                                     'to': access['access_to']})
  278                     continue
  279                 access_to = self._get_parsed_address_or_cidr(
  280                     access['access_to'])
  281                 try:
  282                     self._ssh_exec(server, ['sudo', 'exportfs', '-u',
  283                                             ':'.join((access_to, local_path))])
  284                 except exception.ProcessExecutionError as e:
  285                     if "could not find" in e.stderr.lower():
  286                         LOG.debug(
  287                             "Client/s with IP address/es %(host)s did not "
  288                             "have access to %(share)s. Nothing to deny.",
  289                             {'host': access_to, 'share': share_name})
  290                     else:
  291                         raise
  292 
  293             if delete_rules:
  294                 self._sync_nfs_temp_and_perm_files(server)
  295             for access in add_rules:
  296                 access_to = self._get_parsed_address_or_cidr(
  297                     access['access_to'])
  298                 found_item = re.search(
  299                     re.escape(local_path) + '[\s\n]*' + re.escape(access_to),
  300                     out)
  301                 if found_item is not None:
  302                     LOG.warning("Access rule %(type)s:%(to)s already "
  303                                 "exists for share %(name)s", {
  304                                     'to': access['access_to'],
  305                                     'type': access['access_type'],
  306                                     'name': share_name
  307                                 })
  308                 else:
  309                     rules_options = '%s,no_subtree_check,no_root_squash'
  310                     self._ssh_exec(
  311                         server,
  312                         ['sudo', 'exportfs', '-o',
  313                          rules_options % access['access_level'],
  314                          ':'.join((access_to, local_path))])
  315             if add_rules:
  316                 self._sync_nfs_temp_and_perm_files(server)
  317 
  318     @staticmethod
  319     def _get_parsed_address_or_cidr(access_to):
  320         network = ipaddress.ip_network(six.text_type(access_to))
  321         mask_length = network.prefixlen
  322         address = six.text_type(network.network_address)
  323         if mask_length == 0:
  324             # Special case because Linux exports don't support /0 netmasks
  325             return '*'
  326         if network.version == 4:
  327             if mask_length == 32:
  328                 return address
  329             return '%s/%s' % (address, mask_length)
  330         if mask_length == 128:
  331             return "[%s]" % address
  332         return "[%s]/%s" % (address, mask_length)
  333 
  334     @staticmethod
  335     def get_host_list(output, local_path):
  336         entries = []
  337         output = output.replace('\n\t\t', ' ')
  338         lines = output.split('\n')
  339         for line in lines:
  340             items = line.split(' ')
  341             if local_path == items[0]:
  342                 entries.append(items[1])
  343         return entries
  344 
  345     def _sync_nfs_temp_and_perm_files(self, server):
  346         """Sync changes of exports with permanent NFS config file.
  347 
  348         This is required to ensure, that after share server reboot, exports
  349         still exist.
  350         """
  351         sync_cmd = [
  352             'sudo', 'cp', const.NFS_EXPORTS_FILE_TEMP, const.NFS_EXPORTS_FILE
  353         ]
  354         self._ssh_exec(server, sync_cmd)
  355         self._ssh_exec(server, ['sudo', 'exportfs', '-a'])
  356         out, _ = self._ssh_exec(
  357             server,
  358             ['sudo', 'systemctl', 'is-active', 'nfs-kernel-server'],
  359             check_exit_code=False)
  360         if "inactive" in out:
  361             self._ssh_exec(
  362                 server, ['sudo', 'systemctl', 'restart', 'nfs-kernel-server'])
  363 
  364     def _get_export_location_template(self, export_location_or_path):
  365         path = export_location_or_path.split(':')[-1]
  366         return '%s:' + path
  367 
  368     def get_share_path_by_export_location(self, server, export_location):
  369         return export_location.split(':')[-1]
  370 
  371     @nfs_synchronized
  372     def disable_access_for_maintenance(self, server, share_name):
  373         maintenance_file = self._get_maintenance_file_path(share_name)
  374         backup_exports = [
  375             'cat', const.NFS_EXPORTS_FILE,
  376             '|', 'grep', share_name,
  377             '|', 'sudo', 'tee', maintenance_file
  378         ]
  379         self._ssh_exec(server, backup_exports)
  380 
  381         local_path = os.path.join(self.configuration.share_mount_path,
  382                                   share_name)
  383         out, err = self._ssh_exec(server, ['sudo', 'exportfs'])
  384         hosts = self.get_host_list(out, local_path)
  385         for host in hosts:
  386             self._ssh_exec(server, ['sudo', 'exportfs', '-u',
  387                                     ':'.join((host, local_path))])
  388         self._sync_nfs_temp_and_perm_files(server)
  389 
  390     @nfs_synchronized
  391     def restore_access_after_maintenance(self, server, share_name):
  392         maintenance_file = self._get_maintenance_file_path(share_name)
  393         restore_exports = [
  394             'cat', maintenance_file,
  395             '|', 'sudo', 'tee', '-a', const.NFS_EXPORTS_FILE,
  396             '&&', 'sudo', 'exportfs', '-r',
  397             '&&', 'sudo', 'rm', '-f', maintenance_file
  398         ]
  399         self._ssh_exec(server, restore_exports)
  400 
  401 
  402 class CIFSHelperBase(NASHelperBase):
  403     @staticmethod
  404     def _get_share_group_name_from_export_location(export_location):
  405         if '/' in export_location and '\\' in export_location:
  406             pass
  407         elif export_location.startswith('\\\\'):
  408             return export_location.split('\\')[-1]
  409         elif export_location.startswith('//'):
  410             return export_location.split('/')[-1]
  411 
  412         msg = _("Got incorrect CIFS export location '%s'.") % export_location
  413         raise exception.InvalidShare(reason=msg)
  414 
  415     def _get_export_location_template(self, export_location_or_path):
  416         group_name = self._get_share_group_name_from_export_location(
  417             export_location_or_path)
  418         return ('\\\\%s' + ('\\%s' % group_name))
  419 
  420 
  421 class CIFSHelperIPAccess(CIFSHelperBase):
  422     """Manage shares in samba server by net conf tool.
  423 
  424     Class provides functionality to operate with CIFS shares.
  425     Samba server should be configured to use registry as configuration
  426     backend to allow dynamically share managements. This class allows
  427     to define access to shares by IPs with RW access level.
  428     """
  429     def __init__(self, *args):
  430         super(CIFSHelperIPAccess, self).__init__(*args)
  431         self.parameters = {
  432             'browseable': 'yes',
  433             'create mask': '0755',
  434             'hosts deny': '0.0.0.0/0',  # deny all by default
  435             'hosts allow': '127.0.0.1',
  436             'read only': 'no',
  437         }
  438 
  439     def init_helper(self, server):
  440         # This is smoke check that we have required dependency
  441         self._ssh_exec(server, ['sudo', 'net', 'conf', 'list'])
  442 
  443     def create_exports(self, server, share_name, recreate=False):
  444         """Create share at samba server."""
  445         share_path = os.path.join(self.configuration.share_mount_path,
  446                                   share_name)
  447         create_cmd = [
  448             'sudo', 'net', 'conf', 'addshare', share_name, share_path,
  449             'writeable=y', 'guest_ok=y',
  450         ]
  451         try:
  452             self._ssh_exec(
  453                 server, ['sudo', 'net', 'conf', 'showshare', share_name, ])
  454         except exception.ProcessExecutionError:
  455             # Share does not exist, create it
  456             try:
  457                 self._ssh_exec(server, create_cmd)
  458             except Exception:
  459                 msg = _("Could not create CIFS export %s.") % share_name
  460                 LOG.exception(msg)
  461                 raise exception.ManilaException(reason=msg)
  462         else:
  463             # Share exists
  464             if recreate:
  465                 self._ssh_exec(
  466                     server, ['sudo', 'net', 'conf', 'delshare', share_name, ])
  467                 try:
  468                     self._ssh_exec(server, create_cmd)
  469                 except Exception:
  470                     msg = _("Could not create CIFS export %s.") % share_name
  471                     LOG.exception(msg)
  472                     raise exception.ManilaException(reason=msg)
  473             else:
  474                 msg = _('Share section %s already defined.') % share_name
  475                 raise exception.ShareBackendException(msg=msg)
  476 
  477         for param, value in self.parameters.items():
  478             self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm',
  479                            share_name, param, value])
  480 
  481         return self.get_exports_for_share(server, '\\\\%s\\' + share_name)
  482 
  483     def remove_exports(self, server, share_name):
  484         """Remove share definition from samba server."""
  485         try:
  486             self._ssh_exec(
  487                 server, ['sudo', 'net', 'conf', 'delshare', share_name])
  488         except exception.ProcessExecutionError as e:
  489             LOG.warning("Caught error trying delete share: %(error)s, try"
  490                         "ing delete it forcibly.", {'error': e.stderr})
  491             self._ssh_exec(server, ['sudo', 'smbcontrol', 'all', 'close-share',
  492                                     share_name])
  493 
  494     def update_access(self, server, share_name, access_rules, add_rules,
  495                       delete_rules):
  496         """Update access rules for given share.
  497 
  498         Please refer to base class for a more in-depth description. For this
  499         specific implementation, add_rules and delete_rules parameters are not
  500         used.
  501         """
  502         hosts = []
  503 
  504         self.validate_access_rules(
  505             access_rules, ('ip',), (const.ACCESS_LEVEL_RW,))
  506 
  507         for access in access_rules:
  508             hosts.append(access['access_to'])
  509         self._set_allow_hosts(server, hosts, share_name)
  510 
  511     def _get_allow_hosts(self, server, share_name):
  512         (out, _) = self._ssh_exec(server, ['sudo', 'net', 'conf', 'getparm',
  513                                            share_name, 'hosts allow'])
  514         return out.split()
  515 
  516     def _set_allow_hosts(self, server, hosts, share_name):
  517         value = ' '.join(hosts) or ' '
  518         self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
  519                                 'hosts allow', value])
  520 
  521     def get_share_path_by_export_location(self, server, export_location):
  522         # Get name of group that contains share data on CIFS server
  523         group_name = self._get_share_group_name_from_export_location(
  524             export_location)
  525 
  526         # Get parameter 'path' from group that belongs to current share
  527         (out, __) = self._ssh_exec(
  528             server, ['sudo', 'net', 'conf', 'getparm', group_name, 'path'])
  529 
  530         # Remove special symbols from response and return path
  531         return out.strip()
  532 
  533     def disable_access_for_maintenance(self, server, share_name):
  534         maintenance_file = self._get_maintenance_file_path(share_name)
  535         allowed_hosts = " ".join(self._get_allow_hosts(server, share_name))
  536 
  537         backup_exports = [
  538             'echo', "'%s'" % allowed_hosts, '|', 'sudo', 'tee',
  539             maintenance_file
  540         ]
  541         self._ssh_exec(server, backup_exports)
  542         self._set_allow_hosts(server, [], share_name)
  543         self._kick_out_users(server, share_name)
  544 
  545     def _kick_out_users(self, server, share_name):
  546         """Kick out all users of share"""
  547         (out, _) = self._ssh_exec(server, ['sudo', 'smbstatus', '-S'])
  548 
  549         shares = []
  550         header = True
  551         regexp = r"^(?P<share>[^ ]+)\s+(?P<pid>[0-9]+)\s+(?P<machine>[^ ]+).*"
  552         for line in out.splitlines():
  553             line = line.strip()
  554             if not header and line:
  555                 match = re.match(regexp, line)
  556                 if match:
  557                     shares.append(match.groupdict())
  558                 else:
  559                     raise exception.ShareBackendException(
  560                         msg="Failed to obtain smbstatus for %s!" % share_name)
  561             elif line.startswith('----'):
  562                 header = False
  563         to_kill = [s['pid'] for s in shares if
  564                    share_name == s['share'] or share_name is None]
  565         if to_kill:
  566             self._ssh_exec(server, ['sudo', 'kill', '-15'] + to_kill)
  567 
  568     def restore_access_after_maintenance(self, server, share_name):
  569         maintenance_file = self._get_maintenance_file_path(share_name)
  570         (exports, __) = self._ssh_exec(server, ['cat', maintenance_file])
  571         self._set_allow_hosts(server, exports.split(), share_name)
  572         self._ssh_exec(server, ['sudo', 'rm', '-f', maintenance_file])
  573 
  574 
  575 class CIFSHelperUserAccess(CIFSHelperIPAccess):
  576     """Manage shares in samba server by net conf tool.
  577 
  578     Class provides functionality to operate with CIFS shares.
  579     Samba server should be configured to use registry as configuration
  580     backend to allow dynamically share managements. This class allows
  581     to define access to shares by usernames with either RW or RO access levels.
  582     """
  583     def __init__(self, *args):
  584         super(CIFSHelperUserAccess, self).__init__(*args)
  585         self.parameters = {
  586             'browseable': 'yes',
  587             'create mask': '0755',
  588             'hosts allow': '0.0.0.0/0',
  589             'read only': 'no',
  590         }
  591 
  592     def update_access(self, server, share_name, access_rules, add_rules,
  593                       delete_rules):
  594         """Update access rules for given share.
  595 
  596         Please refer to base class for a more in-depth description. For this
  597         specific implementation, add_rules and delete_rules parameters are not
  598         used.
  599         """
  600         all_users_rw = []
  601         all_users_ro = []
  602 
  603         self.validate_access_rules(
  604             access_rules, ('user',),
  605             (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW))
  606 
  607         for access in access_rules:
  608             if access['access_level'] == const.ACCESS_LEVEL_RW:
  609                 all_users_rw.append(access['access_to'])
  610             else:
  611                 all_users_ro.append(access['access_to'])
  612         self._set_valid_users(
  613             server, all_users_rw, share_name, const.ACCESS_LEVEL_RW)
  614         self._set_valid_users(
  615             server, all_users_ro, share_name, const.ACCESS_LEVEL_RO)
  616 
  617     def _get_conf_param(self, access_level):
  618         if access_level == const.ACCESS_LEVEL_RW:
  619             return 'valid users'
  620         else:
  621             return 'read list'
  622 
  623     def _set_valid_users(self, server, users, share_name, access_level):
  624         value = ' '.join(users)
  625         param = self._get_conf_param(access_level)
  626         self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name,
  627                                 param, value])