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

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. 

15 

16import abc 

17import errno 

18import os 

19import re 

20 

21from oslo_config import cfg 

22from oslo_log import log 

23 

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 

29 

30CONF = cfg.CONF 

31LOG = log.getLogger(__name__) 

32 

33 

34class NASHelperBase(metaclass=abc.ABCMeta): 

35 """Interface to work with share.""" 

36 

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 = () 

43 

44 def __init__(self, execute, config, **kwargs): 

45 self.configuration = config 

46 self._execute = execute 

47 

48 def init_helper(self): 

49 """Initializes protocol-specific NAS drivers.""" 

50 

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.""" 

56 

57 def get_backend_info(self, context): 

58 raise NotImplementedError 

59 

60 def ensure_shares(self, context, shares): 

61 raise NotImplementedError 

62 

63 

64class GaneshaNASHelper(NASHelperBase): 

65 """Perform share access changes using Ganesha version < 2.4.""" 

66 

67 supported_access_types = ('ip', ) 

68 supported_access_levels = (constants.ACCESS_LEVEL_RW, 

69 constants.ACCESS_LEVEL_RO) 

70 

71 def __init__(self, execute, config, tag='<no name>', **kwargs): 

72 super(GaneshaNASHelper, self).__init__(execute, config, **kwargs) 

73 self.tag = tag 

74 

75 _confrx = re.compile(r'\.(conf|json)\Z') 

76 

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 

95 

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() 

112 

113 def _default_config_hook(self): 

114 """The default export block. 

115 

116 Subclass this to add FSAL specific defaults. 

117 

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 """ 

123 

124 return self._load_conf_dir(ganesha_utils.path_from(__file__, "conf")) 

125 

126 def _fsal_hook(self, base_path, share, access, sub_name=None): 

127 """Subclass this to create FSAL block.""" 

128 return {} 

129 

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 

133 

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) 

139 

140 access = ganesha_utils.fixup_access_rule(access) 

141 

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) 

160 

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'])) 

164 

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() 

174 

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 

182 

183 for rule in delete_rules: 

184 self._deny_access('/', share, rule) 

185 return rule_state_map 

186 

187 

188class GaneshaNASHelper2(GaneshaNASHelper): 

189 """Perform share access changes using Ganesha version >= 2.4.""" 

190 

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') 

195 

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() 

228 

229 def _get_export_path(self, share, sub_name=None): 

230 """Subclass this to return export path.""" 

231 raise NotImplementedError() 

232 

233 def _get_export_pseudo_path(self, share, sub_name=None): 

234 """Subclass this to return export pseudo path.""" 

235 raise NotImplementedError() 

236 

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. 

241 

242 Creates an export per share. Modifies access rules of shares by 

243 dynamically updating exports via DBUS. 

244 """ 

245 

246 confdict = {} 

247 existing_access_rules = [] 

248 rule_state_map = {} 

249 

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 

262 

263 wanted_rw_clients, wanted_ro_clients = [], [] 

264 for rule in access_rules: 

265 

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 

274 

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']) 

280 

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 }) 

294 

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