Coverage for manila/api/extensions.py: 72%

172 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2011 OpenStack LLC. 

2# Copyright 2011 Justin Santa Barbara 

3# All Rights Reserved. 

4# 

5# Licensed under the Apache License, Version 2.0 (the "License"); you may 

6# not use this file except in compliance with the License. You may obtain 

7# a copy of the License at 

8# 

9# http://www.apache.org/licenses/LICENSE-2.0 

10# 

11# Unless required by applicable law or agreed to in writing, software 

12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

14# License for the specific language governing permissions and limitations 

15# under the License. 

16 

17import os 

18 

19from oslo_config import cfg 

20from oslo_log import log 

21from oslo_utils import importutils 

22import webob.dec 

23import webob.exc 

24 

25from manila.api.openstack import wsgi 

26from manila import exception 

27from manila import policy 

28 

29CONF = cfg.CONF 

30LOG = log.getLogger(__name__) 

31 

32 

33class ExtensionDescriptor(object): 

34 """Base class that defines the contract for extensions. 

35 

36 Note that you don't have to derive from this class to have a valid 

37 extension; it is purely a convenience. 

38 

39 """ 

40 

41 # The name of the extension, e.g., 'Fox In Socks' 

42 name = None 

43 

44 # The alias for the extension, e.g., 'FOXNSOX' 

45 alias = None 

46 

47 # Description comes from the docstring for the class 

48 

49 # The timestamp when the extension was last updated, e.g., 

50 # '2011-01-22T13:25:27-06:00' 

51 updated = None 

52 

53 def __init__(self, ext_mgr): 

54 """Register extension with the extension manager.""" 

55 

56 ext_mgr.register(self) 

57 self.ext_mgr = ext_mgr 

58 

59 def get_resources(self): 

60 """List of extensions.ResourceExtension extension objects. 

61 

62 Resources define new nouns, and are accessible through URLs. 

63 

64 """ 

65 resources = [] 

66 return resources 

67 

68 def get_controller_extensions(self): 

69 """List of extensions.ControllerExtension extension objects. 

70 

71 Controller extensions are used to extend existing controllers. 

72 """ 

73 controller_exts = [] 

74 return controller_exts 

75 

76 

77class ExtensionsResource(wsgi.Resource): 

78 

79 def __init__(self, extension_manager): 

80 self.extension_manager = extension_manager 

81 super(ExtensionsResource, self).__init__(None) 

82 

83 def _translate(self, ext): 

84 ext_data = {} 

85 ext_data['name'] = ext.name 

86 ext_data['alias'] = ext.alias 

87 ext_data['description'] = ext.__doc__ 

88 ext_data['updated'] = ext.updated 

89 ext_data['links'] = [] # TODO(dprince): implement extension links 

90 return ext_data 

91 

92 def index(self, req): 

93 extensions = [] 

94 for _alias, ext in self.extension_manager.extensions.items(): 

95 extensions.append(self._translate(ext)) 

96 return dict(extensions=extensions) 

97 

98 def show(self, req, id): 

99 try: 

100 # NOTE(dprince): the extensions alias is used as the 'id' for show 

101 ext = self.extension_manager.extensions[id] 

102 except KeyError: 

103 raise webob.exc.HTTPNotFound() 

104 

105 return dict(extension=self._translate(ext)) 

106 

107 def delete(self, req, id): 

108 raise webob.exc.HTTPNotFound() 

109 

110 def create(self, req): 

111 raise webob.exc.HTTPNotFound() 

112 

113 

114class ExtensionManager(object): 

115 """Load extensions from the configured extension path. 

116 

117 See manila/tests/api/extensions/foxinsocks/extension.py for an 

118 example extension implementation. 

119 

120 """ 

121 

122 def __init__(self): 

123 LOG.info('Initializing extension manager.') 

124 

125 self.cls_list = CONF.osapi_share_extension 

126 

127 self.extensions = {} 

128 self._load_extensions() 

129 

130 def register(self, ext): 

131 # Do nothing if the extension doesn't check out 

132 if not self._check_extension(ext): 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true

133 return 

134 

135 alias = ext.alias 

136 LOG.info('Loaded extension: %s', alias) 

137 

138 if alias in self.extensions: 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true

139 raise exception.Error("Found duplicate extension: %s" % alias) 

140 self.extensions[alias] = ext 

141 

142 def get_resources(self): 

143 """Returns a list of ResourceExtension objects.""" 

144 

145 resources = [] 

146 resources.append(ResourceExtension('extensions', 

147 ExtensionsResource(self))) 

148 

149 for ext in self.extensions.values(): 

150 try: 

151 resources.extend(ext.get_resources()) 

152 except AttributeError: 

153 # NOTE(dprince): Extension aren't required to have resource 

154 # extensions 

155 pass 

156 return resources 

157 

158 def get_controller_extensions(self): 

159 """Returns a list of ControllerExtension objects.""" 

160 controller_exts = [] 

161 for ext in self.extensions.values(): 

162 try: 

163 get_ext_method = ext.get_controller_extensions 

164 except AttributeError: 

165 # NOTE(Vek): Extensions aren't required to have 

166 # controller extensions 

167 continue 

168 controller_exts.extend(get_ext_method()) 

169 return controller_exts 

170 

171 def _check_extension(self, extension): 

172 """Checks for required methods in extension objects.""" 

173 try: 

174 LOG.debug('Ext name: %s', extension.name) 

175 LOG.debug('Ext alias: %s', extension.alias) 

176 LOG.debug('Ext description: %s', 

177 ' '.join(extension.__doc__.strip().split())) 

178 LOG.debug('Ext updated: %s', extension.updated) 

179 except AttributeError: 

180 LOG.exception("Exception loading extension.") 

181 return False 

182 

183 return True 

184 

185 def load_extension(self, ext_factory): 

186 """Execute an extension factory. 

187 

188 Loads an extension. The 'ext_factory' is the name of a 

189 callable that will be imported and called with one 

190 argument--the extension manager. The factory callable is 

191 expected to call the register() method at least once. 

192 """ 

193 

194 LOG.debug("Loading extension %s", ext_factory) 

195 

196 # Load the factory 

197 factory = importutils.import_class(ext_factory) 

198 

199 # Call it 

200 LOG.debug("Calling extension factory %s", ext_factory) 

201 factory(self) 

202 

203 def _load_extensions(self): 

204 """Load extensions specified on the command line.""" 

205 

206 extensions = list(self.cls_list) 

207 

208 # NOTE(thingee): Backwards compat for the old extension loader path. 

209 # We can drop this post-grizzly in the H release. 

210 old_contrib_path = ('manila.api.openstack.share.contrib.' 

211 'standard_extensions') 

212 new_contrib_path = 'manila.api.contrib.standard_extensions' 

213 if old_contrib_path in extensions: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true

214 LOG.warning('osapi_share_extension is set to deprecated path: ' 

215 '%s.', 

216 old_contrib_path) 

217 LOG.warning('Please set your flag or manila.conf settings for ' 

218 'osapi_share_extension to: %s.', new_contrib_path) 

219 extensions = [e.replace(old_contrib_path, new_contrib_path) 

220 for e in extensions] 

221 

222 for ext_factory in extensions: 

223 try: 

224 self.load_extension(ext_factory) 

225 except Exception as exc: 

226 LOG.warning('Failed to load extension %(ext_factory)s: ' 

227 '%(exc)s.', 

228 {"ext_factory": ext_factory, "exc": exc}) 

229 

230 

231class ControllerExtension(object): 

232 """Extend core controllers of manila OpenStack API. 

233 

234 Provide a way to extend existing manila OpenStack API core 

235 controllers. 

236 """ 

237 

238 def __init__(self, extension, collection, controller): 

239 self.extension = extension 

240 self.collection = collection 

241 self.controller = controller 

242 

243 

244class ResourceExtension(object): 

245 """Add top level resources to the OpenStack API in manila.""" 

246 

247 def __init__(self, collection, controller, parent=None, 

248 collection_actions=None, member_actions=None, 

249 custom_routes_fn=None): 

250 if not collection_actions: 250 ↛ 252line 250 didn't jump to line 252 because the condition on line 250 was always true

251 collection_actions = {} 

252 if not member_actions: 252 ↛ 254line 252 didn't jump to line 254 because the condition on line 252 was always true

253 member_actions = {} 

254 self.collection = collection 

255 self.controller = controller 

256 self.parent = parent 

257 self.collection_actions = collection_actions 

258 self.member_actions = member_actions 

259 self.custom_routes_fn = custom_routes_fn 

260 

261 

262def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None): 

263 """Registers all standard API extensions.""" 

264 

265 # Walk through all the modules in our directory... 

266 our_dir = path[0] 

267 for dirpath, dirnames, filenames in os.walk(our_dir): 

268 # Compute the relative package name from the dirpath 

269 relpath = os.path.relpath(dirpath, our_dir) 

270 if relpath == '.': 270 ↛ 273line 270 didn't jump to line 273 because the condition on line 270 was always true

271 relpkg = '' 

272 else: 

273 relpkg = '.%s' % '.'.join(relpath.split(os.sep)) 

274 

275 # Now, consider each file in turn, only considering .py and .pyc files 

276 for fname in filenames: 

277 root, ext = os.path.splitext(fname) 

278 

279 # Skip __init__ and anything that's not .py and .pyc 

280 if (ext not in ('.py', '.pyc')) or root == '__init__': 280 ↛ 284line 280 didn't jump to line 284 because the condition on line 280 was always true

281 continue 

282 

283 # If .pyc and .py both exist, skip .pyc 

284 if ext == '.pyc' and ((root + '.py') in filenames): 

285 continue 

286 

287 # Try loading it 

288 classname = "%s%s" % (root[0].upper(), root[1:]) 

289 classpath = ("%s%s.%s.%s" % 

290 (package, relpkg, root, classname)) 

291 

292 if ext_list is not None and classname not in ext_list: 

293 logger.debug("Skipping extension: %s" % classpath) 

294 continue 

295 

296 try: 

297 ext_mgr.load_extension(classpath) 

298 except Exception as exc: 

299 logger.warning('Failed to load extension %(classpath)s: ' 

300 '%(exc)s.', 

301 {"classpath": classpath, "exc": exc}) 

302 

303 # Now, let's consider any subdirectories we may have... 

304 subdirs = [] 

305 for dname in dirnames: 305 ↛ 307line 305 didn't jump to line 307 because the loop on line 305 never started

306 # Skip it if it does not have __init__.py 

307 if not os.path.exists(os.path.join(dirpath, dname, 

308 '__init__.py')): 

309 continue 

310 

311 # If it has extension(), delegate... 

312 ext_name = ("%s%s.%s.extension" % 

313 (package, relpkg, dname)) 

314 try: 

315 ext = importutils.import_class(ext_name) 

316 except ImportError: 

317 # extension() doesn't exist on it, so we'll explore 

318 # the directory for ourselves 

319 subdirs.append(dname) 

320 else: 

321 try: 

322 ext(ext_mgr) 

323 except Exception as exc: 

324 logger.warning('Failed to load extension ' 

325 '%(ext_name)s: %(exc)s.', 

326 {"ext_name": ext_name, "exc": exc}) 

327 

328 # Update the list of directories we'll explore... 

329 dirnames[:] = subdirs 

330 

331 

332def extension_authorizer(api_name, extension_name): 

333 def authorize(context, target=None, action=None): 

334 target = target or policy.default_target(context) 

335 if action is None: 

336 act = '%s_extension:%s' % (api_name, extension_name) 

337 else: 

338 act = '%s_extension:%s:%s' % (api_name, extension_name, action) 

339 policy.enforce(context, act, target) 

340 return authorize