Coverage for manila/api/v2/share_replicas.py: 97%

254 statements  

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

1# Copyright 2015 Goutham Pacha Ravi 

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 

16"""The Share Replication API.""" 

17 

18from http import client as http_client 

19 

20from oslo_utils import strutils 

21import webob 

22from webob import exc 

23 

24from manila.api import common 

25from manila.api.openstack import api_version_request as api_version 

26from manila.api.openstack import wsgi 

27from manila.api.views import share_replicas as replication_view 

28from manila.common import constants 

29from manila import db 

30from manila import exception 

31from manila.i18n import _ 

32from manila import share 

33 

34 

35MIN_SUPPORTED_API_VERSION = '2.11' 

36PRE_GRADUATION_VERSION = '2.55' 

37GRADUATION_VERSION = '2.56' 

38 

39 

40class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin): 

41 """The Share Replication API controller for the OpenStack API.""" 

42 

43 resource_name = 'share_replica' 

44 _view_builder_class = replication_view.ReplicationViewBuilder 

45 

46 def __init__(self): 

47 super(ShareReplicationController, self).__init__() 

48 self.share_api = share.API() 

49 

50 def _update(self, *args, **kwargs): 

51 db.share_replica_update(*args, **kwargs) 

52 

53 def _get(self, *args, **kwargs): 

54 return db.share_replica_get(*args, **kwargs) 

55 

56 def _delete(self, context, resource, force=True): 

57 try: 

58 self.share_api.delete_share_replica(context, resource, force=True) 

59 except exception.ReplicationException as e: 

60 raise exc.HTTPBadRequest(explanation=e.msg) 

61 

62 @wsgi.Controller.api_version( 

63 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

64 def index(self, req): 

65 """Return a summary list of replicas.""" 

66 return self._get_replicas(req) 

67 

68 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

69 def index(self, req): # pylint: disable=function-redefined # noqa F811 

70 """Return a summary list of replicas.""" 

71 return self._get_replicas(req) 

72 

73 @wsgi.Controller.api_version( 

74 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

75 def detail(self, req): 

76 """Returns a detailed list of replicas.""" 

77 return self._get_replicas(req, is_detail=True) 

78 

79 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

80 def detail(self, req): # pylint: disable=function-redefined # noqa F811 

81 """Returns a detailed list of replicas.""" 

82 return self._get_replicas(req, is_detail=True) 

83 

84 @wsgi.Controller.authorize('get_all') 

85 def _get_replicas(self, req, is_detail=False): 

86 """Returns list of replicas.""" 

87 context = req.environ['manila.context'] 

88 

89 share_id = req.params.get('share_id') 

90 if share_id: 

91 try: 

92 replicas = db.share_replicas_get_all_by_share( 

93 context, share_id) 

94 except exception.NotFound: 

95 msg = _("Share with share ID %s not found.") % share_id 

96 raise exc.HTTPNotFound(explanation=msg) 

97 else: 

98 replicas = db.share_replicas_get_all(context) 

99 

100 limited_list = common.limited(replicas, req) 

101 if is_detail: 

102 replicas = self._view_builder.detail_list(req, limited_list) 

103 else: 

104 replicas = self._view_builder.summary_list(req, limited_list) 

105 

106 return replicas 

107 

108 @wsgi.Controller.api_version( 

109 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

110 def show(self, req, id): 

111 """Return data about the given replica.""" 

112 return self._show(req, id) 

113 

114 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

115 def show(self, req, id): # pylint: disable=function-redefined # noqa F811 

116 """Return data about the given replica.""" 

117 return self._show(req, id) 

118 

119 @wsgi.Controller.authorize('show') 

120 def _show(self, req, id): 

121 """Return data about the given replica.""" 

122 context = req.environ['manila.context'] 

123 

124 try: 

125 replica = db.share_replica_get(context, id) 

126 except exception.ShareReplicaNotFound: 

127 msg = _("Replica %s not found.") % id 

128 raise exc.HTTPNotFound(explanation=msg) 

129 

130 return self._view_builder.detail(req, replica) 

131 

132 def _validate_body(self, body): 

133 if not self.is_valid_body(body, 'share_replica'): 

134 msg = _("Body does not contain 'share_replica' information.") 

135 raise exc.HTTPUnprocessableEntity(explanation=msg) 

136 

137 @wsgi.Controller.api_version( 

138 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

139 @wsgi.response(202) 

140 def create(self, req, body): 

141 return self._create(req, body) 

142 

143 @wsgi.Controller.api_version(GRADUATION_VERSION, "2.66") # noqa 

144 @wsgi.response(202) 

145 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

146 return self._create(req, body) 

147 

148 @wsgi.Controller.api_version("2.67") # noqa 

149 @wsgi.response(202) 

150 def create(self, req, body): # pylint: disable=function-redefined # noqa F811 

151 return self._create(req, body, allow_scheduler_hints=True) 

152 

153 @wsgi.Controller.authorize('create') 

154 def _create(self, req, body, allow_scheduler_hints=False): 

155 """Add a replica to an existing share.""" 

156 context = req.environ['manila.context'] 

157 self._validate_body(body) 

158 share_id = body.get('share_replica').get('share_id') 

159 availability_zone = body.get('share_replica').get('availability_zone') 

160 scheduler_hints = None 

161 if allow_scheduler_hints: 

162 scheduler_hints = body.get('share_replica').get('scheduler_hints') 

163 

164 if not share_id: 

165 msg = _("Must provide Share ID to add replica.") 

166 raise exc.HTTPBadRequest(explanation=msg) 

167 

168 try: 

169 share_ref = db.share_get(context, share_id) 

170 except exception.NotFound: 

171 msg = _("No share exists with ID %s.") 

172 raise exc.HTTPNotFound(explanation=msg % share_id) 

173 

174 if share_ref.get('is_soft_deleted'): 

175 msg = _("Replica cannot be created for share '%s' " 

176 "since it has been soft deleted.") % share_id 

177 raise exc.HTTPForbidden(explanation=msg) 

178 

179 if share_ref.get('encryption_key_ref'): 

180 msg = _("Replica cannot be created for share '%s' " 

181 "since it is encrypted.") % share_id 

182 raise exc.HTTPForbidden(explanation=msg) 

183 

184 share_network_id = body.get('share_replica').get('share_network_id') 

185 if share_network_id: 

186 if req.api_version_request < api_version.APIVersionRequest("2.72"): 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true

187 msg = _("'share_network_id' option is not supported by this " 

188 "microversion. Use 2.72 or greater microversion to " 

189 "be able to use 'share_network_id'.") 

190 raise exc.HTTPBadRequest(explanation=msg) 

191 else: 

192 share_network_id = share_ref.get('share_network_id', None) 

193 

194 try: 

195 if share_network_id: 195 ↛ 202line 195 didn't jump to line 202 because the condition on line 195 was always true

196 share_network = db.share_network_get(context, share_network_id) 

197 common.check_share_network_is_active(share_network) 

198 except exception.ShareNetworkNotFound: 

199 msg = _("No share network exists with ID %s.") 

200 raise exc.HTTPBadRequest(explanation=msg % share_network_id) 

201 

202 try: 

203 new_replica = self.share_api.create_share_replica( 

204 context, share_ref, availability_zone=availability_zone, 

205 share_network_id=share_network_id, 

206 scheduler_hints=scheduler_hints) 

207 except exception.AvailabilityZoneNotFound as e: 

208 raise exc.HTTPBadRequest(explanation=e.msg) 

209 except exception.ReplicationException as e: 

210 raise exc.HTTPBadRequest(explanation=e.msg) 

211 except exception.ShareBusyException as e: 

212 raise exc.HTTPBadRequest(explanation=e.msg) 

213 

214 return self._view_builder.detail(req, new_replica) 

215 

216 @wsgi.Controller.api_version( 

217 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

218 def delete(self, req, id): 

219 return self._delete_share_replica(req, id) 

220 

221 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

222 def delete(self, req, id): # pylint: disable=function-redefined # noqa F811 

223 return self._delete_share_replica(req, id) 

224 

225 @wsgi.Controller.authorize('delete') 

226 def _delete_share_replica(self, req, id): 

227 """Delete a replica.""" 

228 context = req.environ['manila.context'] 

229 

230 try: 

231 replica = db.share_replica_get(context, id) 

232 except exception.ShareReplicaNotFound: 

233 msg = _("No replica exists with ID %s.") 

234 raise exc.HTTPNotFound(explanation=msg % id) 

235 

236 try: 

237 self.share_api.delete_share_replica(context, replica) 

238 except exception.ReplicationException as e: 

239 raise exc.HTTPBadRequest(explanation=e.msg) 

240 

241 return webob.Response(status_int=http_client.ACCEPTED) 

242 

243 @wsgi.Controller.api_version( 

244 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

245 @wsgi.response(202) 

246 @wsgi.action('promote') 

247 def promote(self, req, id, body): 

248 return self._promote(req, id, body) 

249 

250 @wsgi.Controller.api_version(GRADUATION_VERSION, "2.74") # noqa 

251 @wsgi.response(202) 

252 @wsgi.action('promote') 

253 def promote(self, req, id, body): # pylint: disable=function-redefined # noqa F811 

254 return self._promote(req, id, body) 

255 

256 @wsgi.Controller.api_version("2.75") # noqa 

257 @wsgi.response(202) 

258 @wsgi.action('promote') 

259 def promote(self, req, id, body): # pylint: disable=function-redefined # noqa F811 

260 return self._promote(req, id, body, allow_quiesce_wait_time=True) 

261 

262 @wsgi.Controller.authorize('promote') 

263 def _promote(self, req, id, body, 

264 allow_quiesce_wait_time=False): 

265 """Promote a replica to active state.""" 

266 context = req.environ['manila.context'] 

267 

268 try: 

269 replica = db.share_replica_get(context, id) 

270 except exception.ShareReplicaNotFound: 

271 msg = _("No replica exists with ID %s.") 

272 raise exc.HTTPNotFound(explanation=msg % id) 

273 

274 share_network_id = replica.get('share_network_id') 

275 if share_network_id: 275 ↛ 279line 275 didn't jump to line 279 because the condition on line 275 was always true

276 share_network = db.share_network_get(context, share_network_id) 

277 common.check_share_network_is_active(share_network) 

278 

279 replica_state = replica.get('replica_state') 

280 

281 if replica_state == constants.REPLICA_STATE_ACTIVE: 

282 return webob.Response(status_int=http_client.OK) 

283 

284 quiesce_wait_time = None 

285 if allow_quiesce_wait_time: 

286 # NOTE(carloss): there is a chance that we receive 

287 # {'promote': null}, so we need to prevent that 

288 promote_data = body.get('promote', {}) 

289 promote_data = {} if promote_data is None else promote_data 

290 wait_time = promote_data.get('quiesce_wait_time') 

291 if wait_time: 291 ↛ 299line 291 didn't jump to line 299 because the condition on line 291 was always true

292 if not strutils.is_int_like(wait_time) or int(wait_time) <= 0: 

293 msg = _("quiesce_wait_time must be an integer and " 

294 "greater than 0.") 

295 raise exc.HTTPBadRequest(explanation=msg) 

296 else: 

297 quiesce_wait_time = int(wait_time) 

298 

299 try: 

300 replica = self.share_api.promote_share_replica( 

301 context, replica, 

302 quiesce_wait_time=quiesce_wait_time) 

303 except exception.ReplicationException as e: 

304 raise exc.HTTPBadRequest(explanation=e.msg) 

305 except exception.AdminRequired as e: 

306 raise exc.HTTPForbidden(explanation=e.message) 

307 

308 return self._view_builder.detail(req, replica) 

309 

310 @wsgi.Controller.api_version( 

311 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

312 @wsgi.action('reset_status') 

313 def reset_status(self, req, id, body): 

314 """Reset the 'status' attribute in the database.""" 

315 return self._reset_status(req, id, body) 

316 

317 # pylint: disable=function-redefined 

318 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

319 @wsgi.action('reset_status') 

320 def reset_status(self, req, id, body): # noqa F811 

321 """Reset the 'status' attribute in the database.""" 

322 return self._reset_status(req, id, body) 

323 

324 # pylint: enable=function-redefined 

325 @wsgi.Controller.api_version( 

326 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

327 @wsgi.action('force_delete') 

328 def force_delete(self, req, id, body): 

329 """Force deletion on the database, attempt on the backend.""" 

330 return self._force_delete(req, id, body) 

331 

332 # pylint: disable=function-redefined 

333 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

334 @wsgi.action('force_delete') 

335 def force_delete(self, req, id, body): # noqa F811 

336 """Force deletion on the database, attempt on the backend.""" 

337 return self._force_delete(req, id, body) 

338 

339 # pylint: enable=function-redefined 

340 @wsgi.Controller.api_version( 

341 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

342 @wsgi.action('reset_replica_state') 

343 @wsgi.Controller.authorize 

344 def reset_replica_state(self, req, id, body): 

345 """Reset the 'replica_state' attribute in the database.""" 

346 return self._reset_status(req, id, body, status_attr='replica_state') 

347 

348 # pylint: disable=function-redefined 

349 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

350 @wsgi.action('reset_replica_state') 

351 @wsgi.Controller.authorize 

352 def reset_replica_state(self, req, id, body): # noqa F811 

353 """Reset the 'replica_state' attribute in the database.""" 

354 return self._reset_status(req, id, body, status_attr='replica_state') 

355 

356 # pylint: enable=function-redefined 

357 @wsgi.Controller.api_version( 

358 MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True) 

359 @wsgi.response(202) 

360 @wsgi.action('resync') 

361 def resync(self, req, id, body): 

362 return self._resync(req, id, body) 

363 

364 @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa 

365 @wsgi.response(202) 

366 @wsgi.action('resync') 

367 def resync(self, req, id, body): # pylint: disable=function-redefined # noqa F811 

368 return self._resync(req, id, body) 

369 

370 @wsgi.Controller.authorize('resync') 

371 def _resync(self, req, id, body): 

372 """Attempt to update/sync the replica with its source.""" 

373 context = req.environ['manila.context'] 

374 try: 

375 replica = db.share_replica_get(context, id) 

376 except exception.ShareReplicaNotFound: 

377 msg = _("No replica exists with ID %s.") 

378 raise exc.HTTPNotFound(explanation=msg % id) 

379 

380 replica_state = replica.get('replica_state') 

381 

382 if replica_state == constants.REPLICA_STATE_ACTIVE: 

383 return webob.Response(status_int=http_client.OK) 

384 

385 try: 

386 self.share_api.update_share_replica(context, replica) 

387 except exception.InvalidHost as e: 

388 raise exc.HTTPBadRequest(explanation=e.msg) 

389 

390 

391def create_resource(): 

392 return wsgi.Resource(ShareReplicationController())