Coverage for manila/share/drivers/ganesha/__init__.py: 97%
150 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2014 Red Hat, 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.
16import abc
17import errno
18import os
19import re
21from oslo_config import cfg
22from oslo_log import log
24from manila.common import constants
25from manila import exception
26from manila.i18n import _
27from manila.share.drivers.ganesha import manager as ganesha_manager
28from manila.share.drivers.ganesha import utils as ganesha_utils
30CONF = cfg.CONF
31LOG = log.getLogger(__name__)
34class NASHelperBase(metaclass=abc.ABCMeta):
35 """Interface to work with share."""
37 # drivers that use a helper derived from this class
38 # should pass the following attributes to
39 # ganesha_utils.validate_access_rule in their
40 # update_access implementation.
41 supported_access_types = ()
42 supported_access_levels = ()
44 def __init__(self, execute, config, **kwargs):
45 self.configuration = config
46 self._execute = execute
48 def init_helper(self):
49 """Initializes protocol-specific NAS drivers."""
51 @abc.abstractmethod
52 def update_access(self, context, share, access_rules, add_rules,
53 delete_rules, update_rules, share_server=None,
54 sub_name=None):
55 """Update access rules of share."""
57 def get_backend_info(self, context):
58 raise NotImplementedError
60 def ensure_shares(self, context, shares):
61 raise NotImplementedError
64class GaneshaNASHelper(NASHelperBase):
65 """Perform share access changes using Ganesha version < 2.4."""
67 supported_access_types = ('ip', )
68 supported_access_levels = (constants.ACCESS_LEVEL_RW,
69 constants.ACCESS_LEVEL_RO)
71 def __init__(self, execute, config, tag='<no name>', **kwargs):
72 super(GaneshaNASHelper, self).__init__(execute, config, **kwargs)
73 self.tag = tag
75 _confrx = re.compile(r'\.(conf|json)\Z')
77 def _load_conf_dir(self, dirpath, must_exist=True):
78 """Load Ganesha config files in dirpath in alphabetic order."""
79 try:
80 dirlist = os.listdir(dirpath)
81 except OSError as e:
82 if e.errno != errno.ENOENT or must_exist:
83 raise
84 dirlist = []
85 LOG.info('Loading Ganesha config from %s.', dirpath)
86 conf_files = list(filter(self._confrx.search, dirlist))
87 conf_files.sort()
88 export_template = {}
89 for conf_file in conf_files:
90 with open(os.path.join(dirpath, conf_file)) as f:
91 ganesha_utils.patch(
92 export_template,
93 ganesha_manager.parseconf(f.read()))
94 return export_template
96 def init_helper(self):
97 """Initializes protocol-specific NAS drivers."""
98 self.ganesha = ganesha_manager.GaneshaManager(
99 self._execute,
100 self.tag,
101 ganesha_config_path=self.configuration.ganesha_config_path,
102 ganesha_export_dir=self.configuration.ganesha_export_dir,
103 ganesha_db_path=self.configuration.ganesha_db_path,
104 ganesha_service_name=self.configuration.ganesha_service_name)
105 system_export_template = self._load_conf_dir(
106 self.configuration.ganesha_export_template_dir,
107 must_exist=False)
108 if system_export_template:
109 self.export_template = system_export_template
110 else:
111 self.export_template = self._default_config_hook()
113 def _default_config_hook(self):
114 """The default export block.
116 Subclass this to add FSAL specific defaults.
118 Suggested approach: take the return value of superclass'
119 method, patch with dict containing your defaults, and
120 return the result. However, you can also provide your
121 defaults from scratch with no regard to superclass.
122 """
124 return self._load_conf_dir(ganesha_utils.path_from(__file__, "conf"))
126 def _fsal_hook(self, base_path, share, access, sub_name=None):
127 """Subclass this to create FSAL block."""
128 return {}
130 def _cleanup_fsal_hook(self, base_path, share, access, sub_name=None):
131 """Callback for FSAL specific cleanup after removing an export."""
132 pass
134 def _allow_access(self, base_path, share, access, sub_name=None):
135 """Allow access to the share."""
136 ganesha_utils.validate_access_rule(
137 self.supported_access_types, self.supported_access_levels,
138 access, abort=True)
140 access = ganesha_utils.fixup_access_rule(access)
142 cf = {}
143 accid = access['id']
144 name = share['name']
145 export_name = "%s--%s" % (name, accid)
146 ganesha_utils.patch(cf, self.export_template, {
147 'EXPORT': {
148 'Export_Id': self.ganesha.get_export_id(),
149 'Path': os.path.join(base_path, name),
150 'Pseudo': os.path.join(base_path, export_name),
151 'Tag': accid,
152 'CLIENT': {
153 'Clients': access['access_to']
154 },
155 'FSAL': self._fsal_hook(
156 base_path, share, access, sub_name=sub_name)
157 }
158 })
159 self.ganesha.add_export(export_name, cf)
161 def _deny_access(self, base_path, share, access):
162 """Deny access to the share."""
163 self.ganesha.remove_export("%s--%s" % (share['name'], access['id']))
165 def update_access(self, context, share, access_rules, add_rules,
166 delete_rules, update_rules, share_server=None,
167 sub_name=None):
168 """Update access rules of share."""
169 rule_state_map = {}
170 if not (add_rules or delete_rules):
171 add_rules = access_rules
172 self.ganesha.reset_exports()
173 self.ganesha.restart_service()
175 for rule in add_rules:
176 try:
177 self._allow_access('/', share, rule)
178 except (exception.InvalidShareAccess,
179 exception.InvalidShareAccessLevel):
180 rule_state_map[rule['id']] = {'state': 'error'}
181 continue
183 for rule in delete_rules:
184 self._deny_access('/', share, rule)
185 return rule_state_map
188class GaneshaNASHelper2(GaneshaNASHelper):
189 """Perform share access changes using Ganesha version >= 2.4."""
191 def __init__(self, execute, config, tag='<no name>', **kwargs):
192 super(GaneshaNASHelper2, self).__init__(execute, config, **kwargs)
193 if self.configuration.ganesha_rados_store_enable:
194 self.rados_client = kwargs.pop('rados_client')
196 def init_helper(self):
197 """Initializes protocol-specific NAS drivers."""
198 kwargs = {
199 'ganesha_config_path': self.configuration.ganesha_config_path,
200 'ganesha_export_dir': self.configuration.ganesha_export_dir,
201 'ganesha_service_name': self.configuration.ganesha_service_name
202 }
203 if self.configuration.ganesha_rados_store_enable:
204 kwargs['ganesha_rados_store_enable'] = (
205 self.configuration.ganesha_rados_store_enable)
206 if not self.configuration.ganesha_rados_store_pool_name:
207 raise exception.GaneshaException(
208 _('"ganesha_rados_store_pool_name" config option is not '
209 'set in the driver section.'))
210 kwargs['ganesha_rados_store_pool_name'] = (
211 self.configuration.ganesha_rados_store_pool_name)
212 kwargs['ganesha_rados_export_index'] = (
213 self.configuration.ganesha_rados_export_index)
214 kwargs['ganesha_rados_export_counter'] = (
215 self.configuration.ganesha_rados_export_counter)
216 kwargs['rados_client'] = self.rados_client
217 else:
218 kwargs['ganesha_db_path'] = self.configuration.ganesha_db_path
219 self.ganesha = ganesha_manager.GaneshaManager(
220 self._execute, self.tag, **kwargs)
221 system_export_template = self._load_conf_dir(
222 self.configuration.ganesha_export_template_dir,
223 must_exist=False)
224 if system_export_template:
225 self.export_template = system_export_template
226 else:
227 self.export_template = self._default_config_hook()
229 def _get_export_path(self, share, sub_name=None):
230 """Subclass this to return export path."""
231 raise NotImplementedError()
233 def _get_export_pseudo_path(self, share, sub_name=None):
234 """Subclass this to return export pseudo path."""
235 raise NotImplementedError()
237 def update_access(self, context, share, access_rules, add_rules,
238 delete_rules, update_rules, share_server=None,
239 sub_name=None):
240 """Update access rules of share.
242 Creates an export per share. Modifies access rules of shares by
243 dynamically updating exports via DBUS.
244 """
246 confdict = {}
247 existing_access_rules = []
248 rule_state_map = {}
250 # TODO(carloss): check if share['name'] can cause us troubles
251 if self.ganesha.check_export_exists(share['name']):
252 confdict = self.ganesha._read_export(share['name'])
253 existing_access_rules = confdict["EXPORT"]["CLIENT"]
254 if not isinstance(existing_access_rules, list):
255 existing_access_rules = [existing_access_rules]
256 else:
257 if not access_rules:
258 LOG.warning("Trying to remove export file '%s' but it's "
259 "already gone",
260 self.ganesha._getpath(share['name']))
261 return
263 wanted_rw_clients, wanted_ro_clients = [], []
264 for rule in access_rules:
266 try:
267 ganesha_utils.validate_access_rule(
268 self.supported_access_types, self.supported_access_levels,
269 rule, True)
270 except (exception.InvalidShareAccess,
271 exception.InvalidShareAccessLevel):
272 rule_state_map[rule['id']] = {'state': 'error'}
273 continue
275 rule = ganesha_utils.fixup_access_rule(rule)
276 if rule['access_level'] == 'rw':
277 wanted_rw_clients.append(rule['access_to'])
278 elif rule['access_level'] == 'ro': 278 ↛ 264line 278 didn't jump to line 264 because the condition on line 278 was always true
279 wanted_ro_clients.append(rule['access_to'])
281 if access_rules:
282 # Add or Update export.
283 clients = []
284 if wanted_ro_clients:
285 clients.append({
286 'Access_Type': 'ro',
287 'Clients': ','.join(wanted_ro_clients)
288 })
289 if wanted_rw_clients:
290 clients.append({
291 'Access_Type': 'rw',
292 'Clients': ','.join(wanted_rw_clients)
293 })
295 if clients: # Empty list if no rules passed validation
296 if existing_access_rules:
297 # Update existing export.
298 ganesha_utils.patch(confdict, {
299 'EXPORT': {
300 'CLIENT': clients
301 }
302 })
303 self.ganesha.update_export(share['name'], confdict)
304 else:
305 # Add new export.
306 ganesha_utils.patch(confdict, self.export_template, {
307 'EXPORT': {
308 'Export_Id': self.ganesha.get_export_id(),
309 'Path': self._get_export_path(
310 share, sub_name=sub_name),
311 'Pseudo': self._get_export_pseudo_path(
312 share, sub_name=sub_name),
313 'Tag': share['name'],
314 'CLIENT': clients,
315 'FSAL': self._fsal_hook(
316 None, share, None, sub_name=sub_name
317 )
318 }
319 })
320 self.ganesha.add_export(share['name'], confdict)
321 else:
322 # No clients have access to the share. Remove export.
323 self.ganesha.remove_export(share['name'])
324 self._cleanup_fsal_hook(None, share, None, sub_name=sub_name)
325 return rule_state_map