Coverage for manila/api/v2/share_servers.py: 76%

338 statements  

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

1# Copyright 2019 NetApp, 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 

16from http import client as http_client 

17 

18from oslo_log import log 

19import webob 

20from webob import exc 

21 

22from manila.api import common 

23from manila.api.openstack import wsgi 

24from manila.api.views import share_server_migration as server_migration_views 

25from manila.api.views import share_servers as share_servers_views 

26from manila.common import constants 

27from manila.db import api as db_api 

28from manila import exception 

29from manila.i18n import _ 

30from manila import share 

31from manila.share import utils as share_utils 

32from manila import utils 

33 

34LOG = log.getLogger(__name__) 

35 

36 

37class ShareServerController(wsgi.Controller, wsgi.AdminActionsMixin): 

38 """The Share Server API V2 controller for the OpenStack API.""" 

39 

40 _view_builder_class = share_servers_views.ViewBuilder 

41 resource_name = 'share_server' 

42 

43 def __init__(self): 

44 self.share_api = share.API() 

45 super().__init__() 

46 self._migration_view_builder = server_migration_views.ViewBuilder() 

47 

48 valid_statuses = { 

49 'status': set(constants.SHARE_SERVER_STATUSES), 

50 'task_state': set(constants.SERVER_TASK_STATE_STATUSES), 

51 } 

52 

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

54 return db_api.share_server_get(*args, **kwargs) 

55 

56 def _update(self, context, id, update): 

57 db_api.share_server_update(context, id, update) 

58 

59 @wsgi.Controller.authorize 

60 def index(self, req): 

61 """Returns a list of share servers.""" 

62 

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

64 

65 search_opts = {} 

66 search_opts.update(req.GET) 

67 share_servers = db_api.share_server_get_all(context) 

68 for s in share_servers: 

69 try: 

70 share_network = db_api.share_network_get( 

71 context, s.share_network_id) 

72 s.project_id = share_network['project_id'] 

73 if share_network['name']: 

74 s.share_network_name = share_network['name'] 

75 else: 

76 s.share_network_name = share_network['id'] 

77 except exception.ShareNetworkNotFound: 

78 # NOTE(dviroel): The share-network may already be deleted while 

79 # the share-server is in 'deleting' state. In this scenario, 

80 # we will return some empty values. 

81 LOG.debug("Unable to retrieve share network details for share " 

82 "server %(server)s, the network %(network)s was " 

83 "not found.", 

84 {'server': s.id, 'network': s.share_network_id}) 

85 s.project_id = '' 

86 s.share_network_name = '' 

87 if search_opts: 

88 for k, v in search_opts.items(): 

89 share_servers = [s for s in share_servers if 

90 (hasattr(s, k) and 

91 s[k] == v or k == 'share_network' and 

92 v in [s.share_network_name, 

93 s.share_network_id] or 

94 k == 'share_network_subnet_id' and 

95 v in s.share_network_subnet_ids)] 

96 return self._view_builder.build_share_servers(req, share_servers) 

97 

98 @wsgi.Controller.authorize 

99 def show(self, req, id): 

100 """Return data about the requested share server.""" 

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

102 try: 

103 server = db_api.share_server_get(context, id) 

104 share_network = db_api.share_network_get( 

105 context, server['share_network_id']) 

106 server.project_id = share_network['project_id'] 

107 if share_network['name']: 

108 server.share_network_name = share_network['name'] 

109 else: 

110 server.share_network_name = share_network['id'] 

111 except exception.ShareServerNotFound as e: 

112 raise exc.HTTPNotFound(explanation=e.msg) 

113 except exception.ShareNetworkNotFound: 

114 msg = _("Share server could not be found. Its associated share " 

115 "network %s does not exist.") % server['share_network_id'] 

116 raise exc.HTTPNotFound(explanation=msg) 

117 return self._view_builder.build_share_server(req, server) 

118 

119 @wsgi.Controller.authorize 

120 def details(self, req, id): 

121 """Return details for requested share server.""" 

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

123 try: 

124 share_server = db_api.share_server_get(context, id) 

125 except exception.ShareServerNotFound as e: 

126 raise exc.HTTPNotFound(explanation=e.msg) 

127 

128 return self._view_builder.build_share_server_details( 

129 share_server['backend_details']) 

130 

131 @wsgi.Controller.authorize 

132 def delete(self, req, id): 

133 """Delete specified share server.""" 

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

135 try: 

136 share_server = db_api.share_server_get(context, id) 

137 except exception.ShareServerNotFound as e: 

138 raise exc.HTTPNotFound(explanation=e.msg) 

139 allowed_statuses = [constants.STATUS_ERROR, constants.STATUS_ACTIVE] 

140 if share_server['status'] not in allowed_statuses: 

141 data = { 

142 'status': share_server['status'], 

143 'allowed_statuses': allowed_statuses, 

144 } 

145 msg = _("Share server's actual status is %(status)s, allowed " 

146 "statuses for deletion are %(allowed_statuses)s.") % (data) 

147 raise exc.HTTPForbidden(explanation=msg) 

148 LOG.debug("Deleting share server with id: %s.", id) 

149 try: 

150 self.share_api.delete_share_server(context, share_server) 

151 except exception.ShareServerInUse as e: 

152 raise exc.HTTPConflict(explanation=e.msg) 

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

154 

155 @wsgi.Controller.api_version('2.49') 

156 @wsgi.action('reset_status') 

157 def share_server_reset_status(self, req, id, body): 

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

159 

160 @wsgi.Controller.authorize('manage_share_server') 

161 def _manage(self, req, body): 

162 """Manage a share server.""" 

163 LOG.debug("Manage Share Server with id: %s", id) 

164 

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

166 identifier, host, share_network, driver_opts, network_subnet = ( 

167 self._validate_manage_share_server_parameters(context, body)) 

168 

169 try: 

170 result = self.share_api.manage_share_server( 

171 context, identifier, host, network_subnet, driver_opts) 

172 except exception.InvalidInput as e: 

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

174 except exception.PolicyNotAuthorized as e: 

175 raise exc.HTTPForbidden(explanation=e.msg) 

176 

177 result.project_id = share_network["project_id"] 

178 if share_network['name']: 

179 result.share_network_name = share_network['name'] 

180 else: 

181 result.share_network_name = share_network['id'] 

182 return self._view_builder.build_share_server(req, result) 

183 

184 @wsgi.Controller.api_version('2.51') 

185 @wsgi.response(202) 

186 def manage(self, req, body): 

187 return self._manage(req, body) 

188 

189 @wsgi.Controller.api_version('2.49') # noqa 

190 @wsgi.response(202) 

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

192 body.get('share_server', {}).pop('share_network_subnet_id', None) 

193 return self._manage(req, body) 

194 

195 @wsgi.Controller.authorize('unmanage_share_server') 

196 def _unmanage(self, req, id, body=None): 

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

198 

199 LOG.debug("Unmanage Share Server with id: %s", id) 

200 

201 # force's default value is False 

202 # force will be True if body is {'unmanage': {'force': True}} 

203 force = (body.get('unmanage') or {}).get('force', False) or False 

204 

205 try: 

206 share_server = db_api.share_server_get( 

207 context, id) 

208 except exception.ShareServerNotFound as e: 

209 raise exc.HTTPNotFound(explanation=e.msg) 

210 

211 if len(share_server['share_network_subnets']) > 1: 

212 msg = _("Cannot unmanage the share server containing multiple " 

213 "subnets.") 

214 raise exc.HTTPBadRequest(explanation=msg) 

215 

216 if share_server.get('encryption_key_ref'): 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true

217 msg = _("Cannot unmanage the share server containing encryption " 

218 "key reference") 

219 raise exc.HTTPBadRequest(explanation=msg) 

220 

221 share_network_id = share_server['share_network_id'] 

222 share_network = db_api.share_network_get(context, share_network_id) 

223 common.check_share_network_is_active(share_network) 

224 

225 allowed_statuses = [constants.STATUS_ERROR, constants.STATUS_ACTIVE, 

226 constants.STATUS_MANAGE_ERROR, 

227 constants.STATUS_UNMANAGE_ERROR] 

228 if share_server['status'] not in allowed_statuses: 

229 data = { 

230 'status': share_server['status'], 

231 'allowed_statuses': ', '.join(allowed_statuses), 

232 } 

233 msg = _("Share server's actual status is %(status)s, allowed " 

234 "statuses for unmanaging are " 

235 "%(allowed_statuses)s.") % data 

236 raise exc.HTTPBadRequest(explanation=msg) 

237 

238 try: 

239 self.share_api.unmanage_share_server( 

240 context, share_server, force=force) 

241 except (exception.ShareServerInUse, 

242 exception.PolicyNotAuthorized) as e: 

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

244 

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

246 

247 @wsgi.Controller.api_version("2.49") 

248 @wsgi.action('unmanage') 

249 def unmanage(self, req, id, body=None): 

250 """Unmanage a share server.""" 

251 return self._unmanage(req, id, body) 

252 

253 def _validate_manage_share_server_parameters(self, context, body): 

254 

255 if not (body and self.is_valid_body(body, 'share_server')): 

256 msg = _("Share Server entity not found in request body") 

257 raise exc.HTTPUnprocessableEntity(explanation=msg) 

258 

259 required_parameters = ('host', 'share_network_id', 'identifier') 

260 data = body['share_server'] 

261 

262 for parameter in required_parameters: 

263 if parameter not in data: 

264 msg = _("Required parameter %s not found") % parameter 

265 raise exc.HTTPBadRequest(explanation=msg) 

266 if not data.get(parameter): 

267 msg = _("Required parameter %s is empty") % parameter 

268 raise exc.HTTPBadRequest(explanation=msg) 

269 

270 identifier = data['identifier'] 

271 host, share_network_id = data['host'], data['share_network_id'] 

272 

273 network_subnet_id = data.get('share_network_subnet_id') 

274 if network_subnet_id: 

275 try: 

276 network_subnets = ( 

277 db_api.share_network_subnet_get_all_with_same_az( 

278 context, network_subnet_id)) 

279 except exception.ShareNetworkSubnetNotFound: 

280 msg = _("The share network subnet %s does not " 

281 "exist.") % network_subnet_id 

282 raise exc.HTTPBadRequest(explanation=msg) 

283 else: 

284 network_subnets = db_api.share_network_subnet_get_default_subnets( 

285 context, share_network_id) 

286 

287 if not network_subnets: 

288 msg = _("The share network %s does have a default subnet. Create " 

289 "one or use a specific subnet to manage this share server " 

290 "with API version >= 2.51.") % share_network_id 

291 raise exc.HTTPBadRequest(explanation=msg) 

292 

293 if len(network_subnets) > 1: 

294 msg = _("Cannot manage the share server, since the share network " 

295 "subnet %s has more subnets in its availability " 

296 "zone and share network.") % network_subnet_id 

297 raise exc.HTTPBadRequest(explanation=msg) 

298 

299 network_subnet = network_subnets[0] 

300 common.check_share_network_is_active(network_subnet['share_network']) 

301 

302 if share_utils.extract_host(host, 'pool'): 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true

303 msg = _("Host parameter should not contain pool.") 

304 raise exc.HTTPBadRequest(explanation=msg) 

305 

306 try: 

307 utils.validate_service_host( 

308 context, share_utils.extract_host(host)) 

309 except exception.ServiceNotFound as e: 

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

311 except exception.PolicyNotAuthorized as e: 

312 raise exc.HTTPForbidden(explanation=e.msg) 

313 except exception.AdminRequired as e: 

314 raise exc.HTTPForbidden(explanation=e.msg) 

315 except exception.ServiceIsDown as e: 

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

317 

318 try: 

319 share_network = db_api.share_network_get( 

320 context, share_network_id) 

321 except exception.ShareNetworkNotFound as e: 

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

323 

324 driver_opts = data.get('driver_options') 

325 if driver_opts is not None and not isinstance(driver_opts, dict): 

326 msg = _("Driver options must be in dictionary format.") 

327 raise exc.HTTPBadRequest(explanation=msg) 

328 

329 return identifier, host, share_network, driver_opts, network_subnet 

330 

331 @wsgi.Controller.api_version('2.57', experimental=True) 

332 @wsgi.action("migration_start") 

333 @wsgi.Controller.authorize 

334 @wsgi.response(http_client.ACCEPTED) 

335 def share_server_migration_start(self, req, id, body): 

336 """Migrate a share server to the specified host.""" 

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

338 try: 

339 share_server = db_api.share_server_get( 

340 context, id) 

341 except exception.ShareServerNotFound as e: 

342 raise exc.HTTPNotFound(explanation=e.msg) 

343 

344 params = body.get('migration_start') 

345 

346 if not params: 

347 raise exc.HTTPBadRequest(explanation=_("Request is missing body.")) 

348 

349 if share_server['encryption_key_ref']: 349 ↛ 350line 349 didn't jump to line 350 because the condition on line 349 was never true

350 msg = _("Cannot migrate the share server containing encryption " 

351 "key reference") 

352 raise exc.HTTPBadRequest(explanation=msg) 

353 

354 bool_params = ['writable', 'nondisruptive', 'preserve_snapshots'] 

355 mandatory_params = bool_params + ['host'] 

356 

357 utils.check_params_exist(mandatory_params, params) 

358 bool_param_values = utils.check_params_are_boolean(bool_params, params) 

359 

360 pool_was_specified = len(params['host'].split('#')) > 1 

361 

362 if pool_was_specified: 

363 msg = _('The destination host can not contain pool information.') 

364 raise exc.HTTPBadRequest(explanation=msg) 

365 

366 new_share_network = None 

367 

368 new_share_network_id = params.get('new_share_network_id', None) 

369 if new_share_network_id: 

370 try: 

371 new_share_network = db_api.share_network_get( 

372 context, new_share_network_id) 

373 except exception.NotFound: 

374 msg = _("Share network %s not " 

375 "found.") % new_share_network_id 

376 raise exc.HTTPBadRequest(explanation=msg) 

377 common.check_share_network_is_active(new_share_network) 

378 else: 

379 share_network_id = ( 

380 share_server['share_network_id']) 

381 current_share_network = db_api.share_network_get( 

382 context, share_network_id) 

383 common.check_share_network_is_active(current_share_network) 

384 

385 try: 

386 self.share_api.share_server_migration_start( 

387 context, share_server, params['host'], 

388 bool_param_values['writable'], 

389 bool_param_values['nondisruptive'], 

390 bool_param_values['preserve_snapshots'], 

391 new_share_network=new_share_network) 

392 except exception.ServiceIsDown as e: 

393 # NOTE(dviroel): user should check if the host is healthy 

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

395 except exception.InvalidShareServer as e: 

396 # NOTE(dviroel): invalid share server meaning that some internal 

397 # resource have a invalid state. 

398 raise exc.HTTPConflict(explanation=e.msg) 

399 except exception.InvalidInput as e: 

400 # User provided controversial parameters in the request 

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

402 

403 @wsgi.Controller.api_version('2.57', experimental=True) 

404 @wsgi.action("migration_complete") 

405 @wsgi.Controller.authorize 

406 def share_server_migration_complete(self, req, id, body): 

407 """Invokes 2nd phase of share server migration.""" 

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

409 try: 

410 share_server = db_api.share_server_get( 

411 context, id) 

412 except exception.ShareServerNotFound as e: 

413 raise exc.HTTPNotFound(explanation=e.msg) 

414 

415 try: 

416 result = self.share_api.share_server_migration_complete( 

417 context, share_server) 

418 except (exception.InvalidShareServer, 

419 exception.ServiceIsDown) as e: 

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

421 

422 return self._migration_view_builder.migration_complete(req, result) 

423 

424 @wsgi.Controller.api_version('2.57', experimental=True) 

425 @wsgi.action("migration_cancel") 

426 @wsgi.Controller.authorize 

427 @wsgi.response(http_client.ACCEPTED) 

428 def share_server_migration_cancel(self, req, id, body): 

429 """Attempts to cancel share migration.""" 

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

431 try: 

432 share_server = db_api.share_server_get( 

433 context, id) 

434 except exception.ShareServerNotFound as e: 

435 raise exc.HTTPNotFound(explanation=e.msg) 

436 

437 try: 

438 self.share_api.share_server_migration_cancel(context, share_server) 

439 except (exception.InvalidShareServer, 

440 exception.ServiceIsDown) as e: 

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

442 

443 @wsgi.Controller.api_version('2.57', experimental=True) 

444 @wsgi.action("migration_get_progress") 

445 @wsgi.Controller.authorize 

446 def share_server_migration_get_progress(self, req, id, body): 

447 """Retrieve share server migration progress for a given share.""" 

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

449 try: 

450 result = self.share_api.share_server_migration_get_progress( 

451 context, id) 

452 except exception.ServiceIsDown as e: 

453 raise exc.HTTPConflict(explanation=e.msg) 

454 except exception.InvalidShareServer as e: 

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

456 

457 return self._migration_view_builder.get_progress(req, result) 

458 

459 @wsgi.Controller.api_version('2.57', experimental=True) 

460 @wsgi.action("reset_task_state") 

461 @wsgi.Controller.authorize 

462 def share_server_reset_task_state(self, req, id, body): 

463 return self._reset_status(req, id, body, status_attr='task_state') 

464 

465 @wsgi.Controller.api_version('2.57', experimental=True) 

466 @wsgi.action("migration_check") 

467 @wsgi.Controller.authorize 

468 def share_server_migration_check(self, req, id, body): 

469 """Check if can migrate a share server to the specified host.""" 

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

471 try: 

472 share_server = db_api.share_server_get( 

473 context, id) 

474 except exception.ShareServerNotFound as e: 

475 raise exc.HTTPNotFound(explanation=e.msg) 

476 

477 params = body.get('migration_check') 

478 

479 if not params: 

480 raise exc.HTTPBadRequest(explanation=_("Request is missing body.")) 

481 

482 if share_server.get('encryption_key_ref'): 482 ↛ 483line 482 didn't jump to line 483 because the condition on line 482 was never true

483 msg = _("Cannot migrate the share server containing encryption " 

484 "key reference") 

485 raise exc.HTTPBadRequest(explanation=msg) 

486 

487 bool_params = ['writable', 'nondisruptive', 'preserve_snapshots'] 

488 mandatory_params = bool_params + ['host'] 

489 

490 utils.check_params_exist(mandatory_params, params) 

491 bool_param_values = utils.check_params_are_boolean(bool_params, params) 

492 

493 pool_was_specified = len(params['host'].split('#')) > 1 

494 

495 if pool_was_specified: 495 ↛ 496line 495 didn't jump to line 496 because the condition on line 495 was never true

496 msg = _('The destination host can not contain pool information.') 

497 raise exc.HTTPBadRequest(explanation=msg) 

498 

499 new_share_network = None 

500 new_share_network_id = params.get('new_share_network_id', None) 

501 if new_share_network_id: 501 ↛ 511line 501 didn't jump to line 511 because the condition on line 501 was always true

502 try: 

503 new_share_network = db_api.share_network_get( 

504 context, new_share_network_id) 

505 except exception.NotFound: 

506 msg = _("Share network %s not " 

507 "found.") % new_share_network_id 

508 raise exc.HTTPBadRequest(explanation=msg) 

509 common.check_share_network_is_active(new_share_network) 

510 else: 

511 share_network_id = ( 

512 share_server['share_network_id']) 

513 current_share_network = db_api.share_network_get( 

514 context, share_network_id) 

515 common.check_share_network_is_active(current_share_network) 

516 

517 try: 

518 result = self.share_api.share_server_migration_check( 

519 context, share_server, params['host'], 

520 bool_param_values['writable'], 

521 bool_param_values['nondisruptive'], 

522 bool_param_values['preserve_snapshots'], 

523 new_share_network=new_share_network) 

524 except exception.ServiceIsDown as e: 

525 # NOTE(dviroel): user should check if the host is healthy 

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

527 except exception.InvalidShareServer as e: 

528 # NOTE(dviroel): invalid share server meaning that some internal 

529 # resource have a invalid state. 

530 raise exc.HTTPConflict(explanation=e.msg) 

531 

532 return self._migration_view_builder.build_check_migration( 

533 req, params, result) 

534 

535 

536def create_resource(): 

537 return wsgi.Resource(ShareServerController())