Coverage for nova/share/manila.py: 90%

140 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

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

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

3# a copy of the License at 

4# 

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

6# 

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

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

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

10# License for the specific language governing permissions and limitations 

11# under the License. 

12 

13""" 

14Handles all requests relating to shares + manila. 

15""" 

16 

17from dataclasses import dataclass 

18import functools 

19from typing import Optional 

20 

21from openstack import exceptions as sdk_exc 

22from oslo_log import log as logging 

23 

24import nova.conf 

25from nova import exception 

26from nova import utils 

27 

28CONF = nova.conf.CONF 

29LOG = logging.getLogger(__name__) 

30MIN_SHARE_FILE_SYSTEM_MICROVERSION = "2.82" 

31 

32 

33def _manilaclient(context, admin=False): 

34 """Constructs a manila client object for making API requests. 

35 

36 :return: An openstack.proxy.Proxy object for the specified service_type. 

37 :raise: ConfGroupForServiceTypeNotFound If no conf group name could be 

38 found for the specified service_type. 

39 :raise: ServiceUnavailable if the service is down 

40 """ 

41 

42 return utils.get_sdk_adapter( 

43 "shared-file-system", 

44 admin=admin, 

45 check_service=True, 

46 context=context, 

47 shared_file_system_api_version=MIN_SHARE_FILE_SYSTEM_MICROVERSION, 

48 global_request_id=context.global_id 

49 ) 

50 

51 

52@dataclass(frozen=True) 

53class Share(): 

54 id: str 

55 size: int 

56 availability_zone: Optional[str] 

57 created_at: str 

58 status: str 

59 name: Optional[str] 

60 description: Optional[str] 

61 project_id: str 

62 snapshot_id: Optional[str] 

63 share_network_id: Optional[str] 

64 share_proto: str 

65 export_location: str 

66 metadata: dict 

67 share_type: Optional[str] 

68 is_public: bool 

69 

70 @classmethod 

71 def from_manila_share(cls, manila_share, export_location): 

72 return cls( 

73 id=manila_share.id, 

74 size=manila_share.size, 

75 availability_zone=manila_share.availability_zone, 

76 created_at=manila_share.created_at, 

77 status=manila_share.status, 

78 name=manila_share.name, 

79 description=manila_share.description, 

80 project_id=manila_share.project_id, 

81 snapshot_id=manila_share.snapshot_id, 

82 share_network_id=manila_share.share_network_id, 

83 share_proto=manila_share.share_protocol, 

84 export_location=export_location, 

85 metadata=manila_share.metadata, 

86 share_type=manila_share.share_type, 

87 is_public=manila_share.is_public, 

88 ) 

89 

90 

91@dataclass(frozen=True) 

92class Access(): 

93 id: str 

94 access_level: str 

95 state: str 

96 access_type: str 

97 access_to: str 

98 access_key: Optional[str] 

99 

100 @classmethod 

101 def from_manila_access(cls, manila_access): 

102 return cls( 

103 id=manila_access.id, 

104 access_level=manila_access.access_level, 

105 state=manila_access.state, 

106 access_type=manila_access.access_type, 

107 access_to=manila_access.access_to, 

108 access_key= getattr(manila_access, 'access_key', None) 

109 ) 

110 

111 @classmethod 

112 def from_dict(cls, manila_access): 

113 return cls( 

114 id=manila_access['id'], 

115 access_level=manila_access['access_level'], 

116 state=manila_access['state'], 

117 access_type=manila_access['access_type'], 

118 access_to=manila_access['access_to'], 

119 access_key=manila_access['access_key'], 

120 ) 

121 

122 

123def translate_sdk_exception(method): 

124 """Transforms a manila exception but keeps its traceback intact.""" 

125 @functools.wraps(method) 

126 def wrapper(self, *args, **kwargs): 

127 try: 

128 res = method(self, *args, **kwargs) 

129 except (exception.ServiceUnavailable, 

130 exception.ConfGroupForServiceTypeNotFound) as exc: 

131 raise exception.ManilaConnectionFailed(reason=str(exc)) from exc 

132 except (sdk_exc.BadRequestException) as exc: 

133 raise exception.InvalidInput(reason=str(exc)) from exc 

134 except (sdk_exc.ForbiddenException) as exc: 

135 raise exception.Forbidden(str(exc)) from exc 

136 return res 

137 return wrapper 

138 

139 

140def translate_share_exception(method): 

141 """Transforms the exception for the share but keeps its traceback intact. 

142 """ 

143 

144 def wrapper(self, *args, **kwargs): 

145 try: 

146 res = method(self, *args, **kwargs) 

147 except (sdk_exc.ResourceNotFound) as exc: 

148 raise exception.ShareNotFound( 

149 share_id=args[1], reason=exc) from exc 

150 except (sdk_exc.BadRequestException) as exc: 

151 raise exception.ShareNotFound( 

152 share_id=args[1], reason=exc) from exc 

153 return res 

154 return translate_sdk_exception(wrapper) 

155 

156 

157def translate_allow_exception(method): 

158 """Transforms the exception for allow but keeps its traceback intact. 

159 """ 

160 

161 def wrapper(self, *args, **kwargs): 

162 try: 

163 res = method(self, *args, **kwargs) 

164 except (sdk_exc.BadRequestException) as exc: 

165 raise exception.ShareAccessGrantError( 

166 share_id=args[1], reason=exc) from exc 

167 except (sdk_exc.ResourceNotFound) as exc: 

168 raise exception.ShareNotFound( 

169 share_id=args[1], reason=exc) from exc 

170 return res 

171 return translate_sdk_exception(wrapper) 

172 

173 

174def translate_deny_exception(method): 

175 """Transforms the exception for deny but keeps its traceback intact. 

176 """ 

177 

178 def wrapper(self, *args, **kwargs): 

179 try: 

180 res = method(self, *args, **kwargs) 

181 except (sdk_exc.BadRequestException) as exc: 

182 raise exception.ShareAccessRemovalError( 

183 share_id=args[1], reason=exc) from exc 

184 except (sdk_exc.ResourceNotFound) as exc: 

185 raise exception.ShareNotFound( 

186 share_id=args[1], reason=exc) from exc 

187 return res 

188 return translate_sdk_exception(wrapper) 

189 

190 

191class API(object): 

192 """API for interacting with the share manager.""" 

193 

194 @translate_share_exception 

195 def get(self, context, share_id): 

196 """Get the details about a share given its ID. 

197 

198 :param share_id: the id of the share to get 

199 :raises: ShareNotFound if the share_id specified is not available. 

200 :returns: Share object. 

201 """ 

202 

203 def filter_export_locations(export_locations): 

204 # Return the preferred path otherwise choose the first one 

205 paths = [] 

206 try: 

207 for export_location in export_locations: 207 ↛ 212line 207 didn't jump to line 212 because the loop on line 207 didn't complete

208 if export_location.is_preferred: 208 ↛ 211line 208 didn't jump to line 211 because the condition on line 208 was always true

209 return export_location.path 

210 else: 

211 paths.append(export_location.path) 

212 return paths[0] 

213 except (IndexError, NameError): 

214 return None 

215 

216 client = _manilaclient(context, admin=False) 

217 LOG.debug("Get share id:'%s' data from manila", share_id) 

218 share = client.get_share(share_id) 

219 export_locations = client.export_locations(share.id) 

220 export_location = filter_export_locations(export_locations) 

221 

222 return Share.from_manila_share(share, export_location) 

223 

224 @translate_share_exception 

225 def get_access( 

226 self, 

227 context, 

228 share_id, 

229 access_type, 

230 access_to, 

231 ): 

232 """Get share access 

233 

234 :param share_id: the id of the share to get 

235 :param access_type: the type of access ("ip", "cert", "user") 

236 :param access_to: ip:cidr or cert:cn or user:group or user name 

237 :raises: ShareNotFound if the share_id specified is not available. 

238 :returns: Access object or None if there is no access granted to this 

239 share. 

240 """ 

241 

242 LOG.debug("Get share access id for share id:'%s'", 

243 share_id) 

244 access_list = _manilaclient( 

245 context, admin=True).access_rules(share_id) 

246 

247 for access in access_list: 

248 if ( 

249 access.access_type == access_type and 

250 access.access_to == access_to 

251 ): 

252 return Access.from_manila_access(access) 

253 return None 

254 

255 @translate_allow_exception 

256 def allow( 

257 self, 

258 context, 

259 share_id, 

260 access_type, 

261 access_to, 

262 access_level, 

263 ): 

264 """Allow share access 

265 

266 :param share_id: the id of the share 

267 :param access_type: the type of access ("ip", "cert", "user") 

268 :param access_to: ip:cidr or cert:cn or user:group or user name 

269 :param access_level: "ro" for read only or "rw" for read/write 

270 :raises: ShareNotFound if the share_id specified is not available. 

271 :raises: BadRequest if the share already exists. 

272 :raises: ShareAccessGrantError if the answer from manila allow API is 

273 not the one expected. 

274 """ 

275 

276 def check_manila_access_response(access): 

277 if not ( 277 ↛ 283line 277 didn't jump to line 283 because the condition on line 277 was never true

278 isinstance(access, Access) and 

279 access.access_type == access_type and 

280 access.access_to == access_to and 

281 access.access_level == access_level 

282 ): 

283 raise exception.ShareAccessGrantError(share_id=share_id) 

284 

285 LOG.debug("Allow host access to share id:'%s'", 

286 share_id) 

287 

288 access = _manilaclient(context, admin=True).create_access_rule( 

289 share_id, 

290 access_type=access_type, 

291 access_to=access_to, 

292 access_level=access_level, 

293 lock_visibility=True, 

294 lock_deletion=True, 

295 lock_reason="Lock by nova", 

296 ) 

297 

298 access = Access.from_manila_access(access) 

299 check_manila_access_response(access) 

300 return access 

301 

302 @translate_deny_exception 

303 def deny( 

304 self, 

305 context, 

306 share_id, 

307 access_type, 

308 access_to, 

309 ): 

310 """Deny share access 

311 :param share_id: the id of the share 

312 :param access_type: the type of access ("ip", "cert", "user") 

313 :param access_to: ip:cidr or cert:cn or user:group or user name 

314 :raises: ShareAccessNotFound if the access_id specified is not 

315 available. 

316 :raises: ShareAccessRemovalError if the manila deny API does not 

317 respond with a status code 202. 

318 """ 

319 

320 access = self.get_access( 

321 context, 

322 share_id, 

323 access_type, 

324 access_to, 

325 ) 

326 

327 if access: 

328 client = _manilaclient(context, admin=True) 

329 LOG.debug("Deny host access to share id:'%s'", share_id) 

330 resp = client.delete_access_rule( 

331 access.id, share_id, unrestrict=True 

332 ) 

333 if resp.status_code != 202: 

334 raise exception.ShareAccessRemovalError( 

335 share_id=share_id, reason=resp.reason 

336 ) 

337 else: 

338 raise exception.ShareAccessNotFound(share_id=share_id) 

339 

340 def has_access(self, context, share_id, access_type, access_to): 

341 """Helper method to check if a policy is applied to a share 

342 :param context: The request context. 

343 :param share_id: the id of the share 

344 :param access_type: the type of access ("ip", "cert", "user") 

345 :param access_to: ip:cidr or cert:cn or user:group or user name 

346 :returns: boolean, true means the policy is applied. 

347 """ 

348 access = self.get_access( 

349 context, 

350 share_id, 

351 access_type, 

352 access_to 

353 ) 

354 return access is not None and access.state == 'active'