# Copyright (c) 2014 Red Hat, 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 abc import errno import os import re from oslo_config import cfg from oslo_log import log import six from manila.common import constants from manila import exception from manila.share.drivers.ganesha import manager as ganesha_manager from manila.share.drivers.ganesha import utils as ganesha_utils CONF = cfg.CONF LOG = log.getLogger(__name__) @six.add_metaclass(abc.ABCMeta) class NASHelperBase(object): """Interface to work with share.""" # drivers that use a helper derived from this class # should pass the following attributes to # ganesha_utils.validate_acces_rule in their # update_access implementation. supported_access_types = () supported_access_levels = () def __init__(self, execute, config, **kwargs): self.configuration = config self._execute = execute def init_helper(self): """Initializes protocol-specific NAS drivers.""" @abc.abstractmethod def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): """Update access rules of share.""" class GaneshaNASHelper(NASHelperBase): """Perform share access changes using Ganesha version < 2.4.""" supported_access_types = ('ip', ) supported_access_levels = (constants.ACCESS_LEVEL_RW, ) def __init__(self, execute, config, tag='', **kwargs): super(GaneshaNASHelper, self).__init__(execute, config, **kwargs) self.tag = tag _confrx = re.compile('\.(conf|json)\Z') def _load_conf_dir(self, dirpath, must_exist=True): """Load Ganesha config files in dirpath in alphabetic order.""" try: dirlist = os.listdir(dirpath) except OSError as e: if e.errno != errno.ENOENT or must_exist: raise dirlist = [] LOG.info('Loading Ganesha config from %s.', dirpath) conf_files = list(filter(self._confrx.search, dirlist)) conf_files.sort() export_template = {} for conf_file in conf_files: with open(os.path.join(dirpath, conf_file)) as f: ganesha_utils.patch( export_template, ganesha_manager.parseconf(f.read())) return export_template def init_helper(self): """Initializes protocol-specific NAS drivers.""" self.ganesha = ganesha_manager.GaneshaManager( self._execute, self.tag, ganesha_config_path=self.configuration.ganesha_config_path, ganesha_export_dir=self.configuration.ganesha_export_dir, ganesha_db_path=self.configuration.ganesha_db_path, ganesha_service_name=self.configuration.ganesha_service_name) system_export_template = self._load_conf_dir( self.configuration.ganesha_export_template_dir, must_exist=False) if system_export_template: self.export_template = system_export_template else: self.export_template = self._default_config_hook() def _default_config_hook(self): """The default export block. Subclass this to add FSAL specific defaults. Suggested approach: take the return value of superclass' method, patch with dict containing your defaults, and return the result. However, you can also provide your defaults from scratch with no regard to superclass. """ return self._load_conf_dir(ganesha_utils.path_from(__file__, "conf")) def _fsal_hook(self, base_path, share, access): """Subclass this to create FSAL block.""" return {} def _cleanup_fsal_hook(self, base_path, share, access): """Callback for FSAL specific cleanup after removing an export.""" pass def _allow_access(self, base_path, share, access): """Allow access to the share.""" if access['access_type'] != 'ip': raise exception.InvalidShareAccess('Only IP access type allowed') cf = {} accid = access['id'] name = share['name'] export_name = "%s--%s" % (name, accid) ganesha_utils.patch(cf, self.export_template, { 'EXPORT': { 'Export_Id': self.ganesha.get_export_id(), 'Path': os.path.join(base_path, name), 'Pseudo': os.path.join(base_path, export_name), 'Tag': accid, 'CLIENT': { 'Clients': access['access_to'] }, 'FSAL': self._fsal_hook(base_path, share, access) } }) self.ganesha.add_export(export_name, cf) def _deny_access(self, base_path, share, access): """Deny access to the share.""" self.ganesha.remove_export("%s--%s" % (share['name'], access['id'])) def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): """Update access rules of share.""" if not (add_rules or delete_rules): add_rules = access_rules self.ganesha.reset_exports() self.ganesha.restart_service() for rule in add_rules: self._allow_access('/', share, rule) for rule in delete_rules: self._deny_access('/', share, rule) class GaneshaNASHelper2(GaneshaNASHelper): """Perform share access changes using Ganesha version >= 2.4.""" def _get_export_path(self, share): """Subclass this to return export path.""" raise NotImplementedError() def _get_export_pseudo_path(self, share): """Subclass this to return export pseudo path.""" raise NotImplementedError() def update_access(self, context, share, access_rules, add_rules, delete_rules, share_server=None): """Update access rules of share. Creates an export per share. Modifies access rules of shares by dynamically updating exports via DBUS. """ confdict = {} existing_access_rules = [] if self.ganesha._check_export_file_exists(share['name']): confdict = self.ganesha._read_export_file(share['name']) existing_access_rules = confdict["EXPORT"]["CLIENT"] if not isinstance(existing_access_rules, list): existing_access_rules = [existing_access_rules] else: if not access_rules: LOG.warning("Trying to remove export file '%s' but it's " "already gone", self.ganesha._getpath(share['name'])) return wanted_rw_clients, wanted_ro_clients = [], [] for rule in access_rules: if rule['access_level'] == 'rw': wanted_rw_clients.append(rule['access_to']) elif rule['access_level'] == 'ro': wanted_ro_clients.append(rule['access_to']) if access_rules: # Add or Update export. clients = [] if wanted_ro_clients: clients.append({ 'Access_Type': 'ro', 'Clients': ','.join(wanted_ro_clients) }) if wanted_rw_clients: clients.append({ 'Access_Type': 'rw', 'Clients': ','.join(wanted_rw_clients) }) if existing_access_rules: # Update existing export. ganesha_utils.patch(confdict, { 'EXPORT': { 'CLIENT': clients } }) self.ganesha.update_export(share['name'], confdict) else: # Add new export. ganesha_utils.patch(confdict, self.export_template, { 'EXPORT': { 'Export_Id': self.ganesha.get_export_id(), 'Path': self._get_export_path(share), 'Pseudo': self._get_export_pseudo_path(share), 'Tag': share['name'], 'CLIENT': clients, 'FSAL': self._fsal_hook(None, share, None) } }) self.ganesha.add_export(share['name'], confdict) else: # No clients have access to the share. Remove export. self.ganesha.remove_export(share['name']) self._cleanup_fsal_hook(None, share, None)