Coverage for manila/api/openstack/wsgi.py: 92%

662 statements  

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

1# Copyright 2011 OpenStack LLC. 

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 functools 

17from http import client as http_client 

18import inspect 

19import math 

20import time 

21 

22from oslo_log import log 

23from oslo_serialization import jsonutils 

24from oslo_utils import encodeutils 

25from oslo_utils import strutils 

26import webob 

27import webob.exc 

28 

29from manila.api.openstack import api_version_request as api_version 

30from manila.api.openstack import versioned_method 

31from manila.common import constants 

32from manila import exception 

33from manila.i18n import _ 

34from manila import policy 

35from manila import utils 

36from manila.wsgi import common as wsgi 

37 

38LOG = log.getLogger(__name__) 

39 

40SUPPORTED_CONTENT_TYPES = ( 

41 'application/json', 

42) 

43 

44_MEDIA_TYPE_MAP = { 

45 'application/json': 'json', 

46} 

47 

48# name of attribute to keep version method information 

49VER_METHOD_ATTR = 'versioned_methods' 

50 

51# Name of header used by clients to request a specific version 

52# of the REST API 

53API_VERSION_REQUEST_HEADER = 'X-OpenStack-Manila-API-Version' 

54EXPERIMENTAL_API_REQUEST_HEADER = 'X-OpenStack-Manila-API-Experimental' 

55 

56V1_SCRIPT_NAME = '/v1' 

57V2_SCRIPT_NAME = '/v2' 

58 

59 

60class Request(webob.Request): 

61 """Add some OpenStack API-specific logic to the base webob.Request.""" 

62 

63 def __init__(self, *args, **kwargs): 

64 super(Request, self).__init__(*args, **kwargs) 

65 self._resource_cache = {} 

66 if not hasattr(self, 'api_version_request'): 

67 self.api_version_request = api_version.APIVersionRequest() 

68 

69 def cache_resource(self, resource_to_cache, id_attribute='id', name=None): 

70 """Cache the given resource. 

71 

72 Allow API methods to cache objects, such as results from a DB query, 

73 to be used by API extensions within the same API request. 

74 

75 The resource_to_cache can be a list or an individual resource, 

76 but ultimately resources are cached individually using the given 

77 id_attribute. 

78 

79 Different resources types might need to be cached during the same 

80 request, they can be cached using the name parameter. For example: 

81 

82 Controller 1: 

83 request.cache_resource(db_volumes, 'volumes') 

84 request.cache_resource(db_volume_types, 'types') 

85 Controller 2: 

86 db_volumes = request.cached_resource('volumes') 

87 db_type_1 = request.cached_resource_by_id('1', 'types') 

88 

89 If no name is given, a default name will be used for the resource. 

90 

91 An instance of this class only lives for the lifetime of a 

92 single API request, so there's no need to implement full 

93 cache management. 

94 """ 

95 if not isinstance(resource_to_cache, list): 

96 resource_to_cache = [resource_to_cache] 

97 if not name: 

98 name = self.path 

99 cached_resources = self._resource_cache.setdefault(name, {}) 

100 for resource in resource_to_cache: 

101 cached_resources[resource[id_attribute]] = resource 

102 

103 def cached_resource(self, name=None): 

104 """Get the cached resources cached under the given resource name. 

105 

106 Allow an API extension to get previously stored objects within 

107 the same API request. 

108 

109 Note that the object data will be slightly stale. 

110 

111 :returns: a dict of id_attribute to the resource from the cached 

112 resources, an empty map if an empty collection was cached, 

113 or None if nothing has been cached yet under this name 

114 """ 

115 if not name: 

116 name = self.path 

117 if name not in self._resource_cache: 

118 # Nothing has been cached for this key yet 

119 return None 

120 return self._resource_cache[name] 

121 

122 def cached_resource_by_id(self, resource_id, name=None): 

123 """Get a resource by ID cached under the given resource name. 

124 

125 Allow an API extension to get a previously stored object 

126 within the same API request. This is basically a convenience method 

127 to lookup by ID on the dictionary of all cached resources. 

128 

129 Note that the object data will be slightly stale. 

130 

131 :returns: the cached resource or None if the item is not in the cache 

132 """ 

133 resources = self.cached_resource(name) 

134 if not resources: 

135 # Nothing has been cached yet for this key yet 

136 return None 

137 return resources.get(resource_id) 

138 

139 def cache_db_items(self, key, items, item_key='id'): 

140 """Cache db items. 

141 

142 Allow API methods to store objects from a DB query to be 

143 used by API extensions within the same API request. 

144 An instance of this class only lives for the lifetime of a 

145 single API request, so there's no need to implement full 

146 cache management. 

147 """ 

148 self.cache_resource(items, item_key, key) 

149 

150 def get_db_items(self, key): 

151 """Get db item by key. 

152 

153 Allow an API extension to get previously stored objects within 

154 the same API request. 

155 Note that the object data will be slightly stale. 

156 """ 

157 return self.cached_resource(key) 

158 

159 def get_db_item(self, key, item_key): 

160 """Get db item by key and item key. 

161 

162 Allow an API extension to get a previously stored object 

163 within the same API request. 

164 Note that the object data will be slightly stale. 

165 """ 

166 return self.get_db_items(key).get(item_key) 

167 

168 def cache_db_share_types(self, share_types): 

169 self.cache_db_items('share_types', share_types, 'id') 

170 

171 def cache_db_share_type(self, share_type): 

172 self.cache_db_items('share_types', [share_type], 'id') 

173 

174 def get_db_share_types(self): 

175 return self.get_db_items('share_types') 

176 

177 def get_db_share_type(self, share_type_id): 

178 return self.get_db_item('share_types', share_type_id) 

179 

180 def best_match_content_type(self): 

181 """Determine the requested response content-type.""" 

182 if 'manila.best_content_type' not in self.environ: 

183 # Calculate the best MIME type 

184 content_type = None 

185 

186 # Check URL path suffix 

187 parts = self.path.rsplit('.', 1) 

188 if len(parts) > 1: 

189 possible_type = 'application/' + parts[1] 

190 if possible_type in SUPPORTED_CONTENT_TYPES: 

191 content_type = possible_type 

192 

193 if not content_type: 

194 content_type = self.accept.best_match(SUPPORTED_CONTENT_TYPES) 

195 

196 self.environ['manila.best_content_type'] = (content_type or 

197 'application/json') 

198 

199 return self.environ['manila.best_content_type'] 

200 

201 def get_content_type(self): 

202 """Determine content type of the request body. 

203 

204 Does not do any body introspection, only checks header. 

205 """ 

206 if "Content-Type" not in self.headers: 

207 return None 

208 

209 allowed_types = SUPPORTED_CONTENT_TYPES 

210 content_type = self.content_type 

211 

212 if content_type not in allowed_types: 

213 raise exception.InvalidContentType(content_type=content_type) 

214 

215 return content_type 

216 

217 def set_api_version_request(self): 

218 """Set API version request based on the request header information. 

219 

220 Microversions starts with /v2, so if a client sends a /v1 URL, then 

221 ignore the headers and request 1.0 APIs. 

222 """ 

223 if not self.script_name or not (V1_SCRIPT_NAME in self.script_name or 

224 V2_SCRIPT_NAME in self.script_name): 

225 # The request is on the base URL without a major version specified 

226 self.api_version_request = api_version.APIVersionRequest() 

227 elif V1_SCRIPT_NAME in self.script_name: 

228 self.api_version_request = api_version.APIVersionRequest('1.0') 

229 else: 

230 if API_VERSION_REQUEST_HEADER in self.headers: 

231 hdr_string = self.headers[API_VERSION_REQUEST_HEADER] 

232 self.api_version_request = api_version.APIVersionRequest( 

233 hdr_string) 

234 

235 # Check that the version requested is within the global 

236 # minimum/maximum of supported API versions 

237 if not self.api_version_request.matches( 

238 api_version.min_api_version(), 

239 api_version.max_api_version()): 

240 raise exception.InvalidGlobalAPIVersion( 

241 req_ver=self.api_version_request.get_string(), 

242 min_ver=api_version.min_api_version().get_string(), 

243 max_ver=api_version.max_api_version().get_string()) 

244 

245 else: 

246 self.api_version_request = api_version.APIVersionRequest( 

247 api_version.DEFAULT_API_VERSION) 

248 

249 # Check if experimental API was requested 

250 if EXPERIMENTAL_API_REQUEST_HEADER in self.headers: 

251 self.api_version_request.experimental = strutils.bool_from_string( 

252 self.headers[EXPERIMENTAL_API_REQUEST_HEADER]) 

253 

254 

255class ActionDispatcher(object): 

256 """Maps method name to local methods through action name.""" 

257 

258 def dispatch(self, *args, **kwargs): 

259 """Find and call local method.""" 

260 action = kwargs.pop('action', 'default') 

261 action_method = getattr(self, str(action), self.default) 

262 return action_method(*args, **kwargs) 

263 

264 def default(self, data): 

265 raise NotImplementedError() 

266 

267 

268class TextDeserializer(ActionDispatcher): 

269 """Default request body deserialization.""" 

270 

271 def deserialize(self, datastring, action='default'): 

272 return self.dispatch(datastring, action=action) 

273 

274 def default(self, datastring): 

275 return {} 

276 

277 

278class JSONDeserializer(TextDeserializer): 

279 

280 def _from_json(self, datastring): 

281 try: 

282 return jsonutils.loads(datastring) 

283 except ValueError: 

284 msg = _("cannot understand JSON") 

285 raise exception.MalformedRequestBody(reason=msg) 

286 

287 def default(self, datastring): 

288 return {'body': self._from_json(datastring)} 

289 

290 

291class DictSerializer(ActionDispatcher): 

292 """Default request body serialization.""" 

293 

294 def serialize(self, data, action='default'): 

295 return self.dispatch(data, action=action) 

296 

297 def default(self, data): 

298 return "" 

299 

300 

301class JSONDictSerializer(DictSerializer): 

302 """Default JSON request body serialization.""" 

303 

304 def default(self, data): 

305 return jsonutils.dump_as_bytes(data) 

306 

307 

308def serializers(**serializers): 

309 """Attaches serializers to a method. 

310 

311 This decorator associates a dictionary of serializers with a 

312 method. Note that the function attributes are directly 

313 manipulated; the method is not wrapped. 

314 """ 

315 

316 def decorator(func): 

317 if not hasattr(func, 'wsgi_serializers'): 

318 func.wsgi_serializers = {} 

319 func.wsgi_serializers.update(serializers) 

320 return func 

321 return decorator 

322 

323 

324def deserializers(**deserializers): 

325 """Attaches deserializers to a method. 

326 

327 This decorator associates a dictionary of deserializers with a 

328 method. Note that the function attributes are directly 

329 manipulated; the method is not wrapped. 

330 """ 

331 

332 def decorator(func): 

333 if not hasattr(func, 'wsgi_deserializers'): 333 ↛ 335line 333 didn't jump to line 335 because the condition on line 333 was always true

334 func.wsgi_deserializers = {} 

335 func.wsgi_deserializers.update(deserializers) 

336 return func 

337 return decorator 

338 

339 

340def response(code): 

341 """Attaches response code to a method. 

342 

343 This decorator associates a response code with a method. Note 

344 that the function attributes are directly manipulated; the method 

345 is not wrapped. 

346 """ 

347 

348 def decorator(func): 

349 func.wsgi_code = code 

350 return func 

351 return decorator 

352 

353 

354class ResponseObject(object): 

355 """Bundles a response object with appropriate serializers. 

356 

357 Object that app methods may return in order to bind alternate 

358 serializers with a response object to be serialized. Its use is 

359 optional. 

360 """ 

361 

362 def __init__(self, obj, code=None, headers=None, **serializers): 

363 """Binds serializers with an object. 

364 

365 Takes keyword arguments akin to the @serializer() decorator 

366 for specifying serializers. Serializers specified will be 

367 given preference over default serializers or method-specific 

368 serializers on return. 

369 """ 

370 

371 self.obj = obj 

372 self.serializers = serializers 

373 self._default_code = 200 

374 self._code = code 

375 self._headers = headers or {} 

376 self.serializer = None 

377 self.media_type = None 

378 

379 def __getitem__(self, key): 

380 """Retrieves a header with the given name.""" 

381 

382 return self._headers[key.lower()] 

383 

384 def __setitem__(self, key, value): 

385 """Sets a header with the given name to the given value.""" 

386 

387 self._headers[key.lower()] = value 

388 

389 def __delitem__(self, key): 

390 """Deletes the header with the given name.""" 

391 

392 del self._headers[key.lower()] 

393 

394 def _bind_method_serializers(self, meth_serializers): 

395 """Binds method serializers with the response object. 

396 

397 Binds the method serializers with the response object. 

398 Serializers specified to the constructor will take precedence 

399 over serializers specified to this method. 

400 

401 :param meth_serializers: A dictionary with keys mapping to 

402 response types and values containing 

403 serializer objects. 

404 """ 

405 

406 # We can't use update because that would be the wrong 

407 # precedence 

408 for mtype, serializer in meth_serializers.items(): 

409 self.serializers.setdefault(mtype, serializer) 

410 

411 def get_serializer(self, content_type, default_serializers=None): 

412 """Returns the serializer for the wrapped object. 

413 

414 Returns the serializer for the wrapped object subject to the 

415 indicated content type. If no serializer matching the content 

416 type is attached, an appropriate serializer drawn from the 

417 default serializers will be used. If no appropriate 

418 serializer is available, raises InvalidContentType. 

419 """ 

420 

421 default_serializers = default_serializers or {} 

422 

423 try: 

424 mtype = _MEDIA_TYPE_MAP.get(content_type, content_type) 

425 if mtype in self.serializers: 

426 return mtype, self.serializers[mtype] 

427 else: 

428 return mtype, default_serializers[mtype] 

429 except (KeyError, TypeError): 

430 raise exception.InvalidContentType(content_type=content_type) 

431 

432 def preserialize(self, content_type, default_serializers=None): 

433 """Prepares the serializer that will be used to serialize. 

434 

435 Determines the serializer that will be used and prepares an 

436 instance of it for later call. This allows the serializer to 

437 be accessed by extensions for, e.g., template extension. 

438 """ 

439 

440 mtype, serializer = self.get_serializer(content_type, 

441 default_serializers) 

442 self.media_type = mtype 

443 self.serializer = serializer() 

444 

445 def attach(self, **kwargs): 

446 """Attach slave templates to serializers.""" 

447 

448 if self.media_type in kwargs: 

449 self.serializer.attach(kwargs[self.media_type]) 

450 

451 def serialize(self, request, content_type, default_serializers=None): 

452 """Serializes the wrapped object. 

453 

454 Utility method for serializing the wrapped object. Returns a 

455 webob.Response object. 

456 """ 

457 

458 if self.serializer: 

459 serializer = self.serializer 

460 else: 

461 _mtype, _serializer = self.get_serializer(content_type, 

462 default_serializers) 

463 serializer = _serializer() 

464 

465 response = webob.Response() 

466 response.status_int = self.code 

467 for hdr, value in self._headers.items(): 

468 response.headers[hdr] = str(value) 

469 response.headers['Content-Type'] = str(content_type) 

470 if self.obj is not None: 

471 response.body = serializer.serialize(self.obj) 

472 

473 return response 

474 

475 @property 

476 def code(self): 

477 """Retrieve the response status.""" 

478 

479 return self._code or self._default_code 

480 

481 @property 

482 def headers(self): 

483 """Retrieve the headers.""" 

484 

485 return self._headers.copy() 

486 

487 

488def action_peek_json(body): 

489 """Determine action to invoke.""" 

490 

491 try: 

492 decoded = jsonutils.loads(body) 

493 except ValueError: 

494 msg = _("cannot understand JSON") 

495 raise exception.MalformedRequestBody(reason=msg) 

496 

497 # Make sure there's exactly one key... 

498 if len(decoded) != 1: 

499 msg = _("too many body keys") 

500 raise exception.MalformedRequestBody(reason=msg) 

501 

502 # Return the action and the decoded body... 

503 return list(decoded.keys())[0] 

504 

505 

506class ResourceExceptionHandler(object): 

507 """Context manager to handle Resource exceptions. 

508 

509 Used when processing exceptions generated by API implementation 

510 methods (or their extensions). Converts most exceptions to Fault 

511 exceptions, with the appropriate logging. 

512 """ 

513 

514 def __enter__(self): 

515 return None 

516 

517 def __exit__(self, ex_type, ex_value, ex_traceback): 

518 if not ex_value: 

519 return True 

520 

521 msg = str(ex_value) 

522 if isinstance(ex_value, exception.NotAuthorized): 

523 raise Fault(webob.exc.HTTPForbidden(explanation=msg)) 

524 elif isinstance(ex_value, exception.VersionNotFoundForAPIMethod): 

525 raise 

526 elif isinstance(ex_value, exception.Invalid): 

527 raise Fault(exception.ConvertedException( 

528 code=ex_value.code, explanation=msg)) 

529 elif isinstance(ex_value, TypeError): 529 ↛ 530line 529 didn't jump to line 530 because the condition on line 529 was never true

530 exc_info = (ex_type, ex_value, ex_traceback) 

531 LOG.error('Exception handling resource: %s', 

532 ex_value, exc_info=exc_info) 

533 raise Fault(webob.exc.HTTPBadRequest()) 

534 elif isinstance(ex_value, Fault): 534 ↛ 535line 534 didn't jump to line 535 because the condition on line 534 was never true

535 LOG.info("Fault thrown: %s", ex_value) 

536 raise ex_value 

537 elif isinstance(ex_value, webob.exc.HTTPException): 

538 LOG.info("HTTP exception thrown: %s", ex_value) 

539 raise Fault(ex_value) 

540 

541 # We didn't handle the exception 

542 return False 

543 

544 

545class Resource(wsgi.Application): 

546 """WSGI app that handles (de)serialization and controller dispatch. 

547 

548 WSGI app that reads routing information supplied by RoutesMiddleware 

549 and calls the requested action method upon its controller. All 

550 controller action methods must accept a 'req' argument, which is the 

551 incoming wsgi.Request. If the operation is a PUT or POST, the controller 

552 method must also accept a 'body' argument (the deserialized request body). 

553 They may raise a webob.exc exception or return a dict, which will be 

554 serialized by requested content type. 

555 

556 Exceptions derived from webob.exc.HTTPException will be automatically 

557 wrapped in Fault() to provide API friendly error responses. 

558 """ 

559 support_api_request_version = True 

560 

561 def __init__(self, controller, action_peek=None, **deserializers): 

562 """init method of Resource. 

563 

564 :param controller: object that implement methods created by routes lib 

565 :param action_peek: dictionary of routines for peeking into an action 

566 request body to determine the desired action 

567 """ 

568 

569 self.controller = controller 

570 

571 default_deserializers = dict(json=JSONDeserializer) 

572 default_deserializers.update(deserializers) 

573 

574 self.default_deserializers = default_deserializers 

575 self.default_serializers = dict(json=JSONDictSerializer) 

576 

577 self.action_peek = dict(json=action_peek_json) 

578 self.action_peek.update(action_peek or {}) 

579 

580 # Copy over the actions dictionary 

581 self.wsgi_actions = {} 

582 if controller: 

583 self.register_actions(controller) 

584 

585 # Save a mapping of extensions 

586 self.wsgi_extensions = {} 

587 self.wsgi_action_extensions = {} 

588 

589 def register_actions(self, controller): 

590 """Registers controller actions with this resource.""" 

591 

592 actions = getattr(controller, 'wsgi_actions', {}) 

593 for key, method_name in actions.items(): 

594 self.wsgi_actions[key] = getattr(controller, method_name) 

595 

596 def register_extensions(self, controller): 

597 """Registers controller extensions with this resource.""" 

598 

599 extensions = getattr(controller, 'wsgi_extensions', []) 

600 for method_name, action_name in extensions: 

601 # Look up the extending method 

602 extension = getattr(controller, method_name) 

603 

604 if action_name: 

605 # Extending an action... 

606 if action_name not in self.wsgi_action_extensions: 606 ↛ 608line 606 didn't jump to line 608 because the condition on line 606 was always true

607 self.wsgi_action_extensions[action_name] = [] 

608 self.wsgi_action_extensions[action_name].append(extension) 

609 else: 

610 # Extending a regular method 

611 if method_name not in self.wsgi_extensions: 611 ↛ 613line 611 didn't jump to line 613 because the condition on line 611 was always true

612 self.wsgi_extensions[method_name] = [] 

613 self.wsgi_extensions[method_name].append(extension) 

614 

615 def get_action_args(self, request_environment): 

616 """Parse dictionary created by routes library.""" 

617 

618 # NOTE(Vek): Check for get_action_args() override in the 

619 # controller 

620 if hasattr(self.controller, 'get_action_args'): 

621 return self.controller.get_action_args(request_environment) 

622 

623 try: 

624 args = request_environment['wsgiorg.routing_args'][1].copy() 

625 except (KeyError, IndexError, AttributeError): 

626 return {} 

627 

628 try: 

629 del args['controller'] 

630 except KeyError: 

631 pass 

632 

633 try: 

634 del args['format'] 

635 except KeyError: 

636 pass 

637 

638 return args 

639 

640 def get_body(self, request): 

641 try: 

642 content_type = request.get_content_type() 

643 except exception.InvalidContentType: 

644 LOG.debug("Unrecognized Content-Type provided in request") 

645 return None, '' 

646 

647 if not content_type: 

648 LOG.debug("No Content-Type provided in request") 

649 return None, '' 

650 

651 if len(request.body) <= 0: 

652 LOG.debug("Empty body provided in request") 

653 return None, '' 

654 

655 return content_type, request.body 

656 

657 def deserialize(self, meth, content_type, body): 

658 meth_deserializers = getattr(meth, 'wsgi_deserializers', {}) 

659 try: 

660 mtype = _MEDIA_TYPE_MAP.get(content_type, content_type) 

661 if mtype in meth_deserializers: 661 ↛ 662line 661 didn't jump to line 662 because the condition on line 661 was never true

662 deserializer = meth_deserializers[mtype] 

663 else: 

664 deserializer = self.default_deserializers[mtype] 

665 except (KeyError, TypeError): 

666 raise exception.InvalidContentType(content_type=content_type) 

667 

668 return deserializer().deserialize(body) 

669 

670 def pre_process_extensions(self, extensions, request, action_args): 

671 # List of callables for post-processing extensions 

672 post = [] 

673 

674 for ext in extensions: 

675 if inspect.isgeneratorfunction(ext): 

676 response = None 

677 

678 # If it's a generator function, the part before the 

679 # yield is the preprocessing stage 

680 try: 

681 with ResourceExceptionHandler(): 

682 gen = ext(req=request, **action_args) 

683 response = next(gen) 

684 except Fault as ex: 

685 response = ex 

686 

687 # We had a response... 

688 if response: 

689 return response, [] 

690 

691 # No response, queue up generator for post-processing 

692 post.append(gen) 

693 else: 

694 # Regular functions only perform post-processing 

695 post.append(ext) 

696 

697 # Run post-processing in the reverse order 

698 return None, reversed(post) 

699 

700 def post_process_extensions(self, extensions, resp_obj, request, 

701 action_args): 

702 for ext in extensions: 

703 response = None 

704 if inspect.isgenerator(ext): 

705 # If it's a generator, run the second half of 

706 # processing 

707 try: 

708 with ResourceExceptionHandler(): 

709 response = ext.send(resp_obj) 

710 except StopIteration: 

711 # Normal exit of generator 

712 continue 

713 except Fault as ex: 

714 response = ex 

715 else: 

716 # Regular functions get post-processing... 

717 try: 

718 with ResourceExceptionHandler(): 

719 response = ext(req=request, resp_obj=resp_obj, 

720 **action_args) 

721 except exception.VersionNotFoundForAPIMethod: 

722 # If an attached extension (@wsgi.extends) for the 

723 # method has no version match its not an error. We 

724 # just don't run the extends code 

725 continue 

726 except Fault as ex: 

727 response = ex 

728 

729 # We had a response... 

730 if response: 

731 return response 

732 

733 return None 

734 

735 @webob.dec.wsgify(RequestClass=Request) 

736 def __call__(self, request): 

737 """WSGI method that controls (de)serialization and method dispatch.""" 

738 

739 LOG.info("%(method)s %(url)s", {"method": request.method, 

740 "url": request.url}) 

741 if self.support_api_request_version: 741 ↛ 754line 741 didn't jump to line 754 because the condition on line 741 was always true

742 # Set the version of the API requested based on the header 

743 try: 

744 request.set_api_version_request() 

745 except exception.InvalidAPIVersionString as e: 

746 return Fault(webob.exc.HTTPBadRequest( 

747 explanation=e.msg)) 

748 except exception.InvalidGlobalAPIVersion as e: 

749 return Fault(webob.exc.HTTPNotAcceptable( 

750 explanation=e.msg)) 

751 

752 # Identify the action, its arguments, and the requested 

753 # content type 

754 action_args = self.get_action_args(request.environ) 

755 action = action_args.pop('action', None) 

756 content_type, body = self.get_body(request) 

757 accept = request.best_match_content_type() 

758 

759 # NOTE(Vek): Splitting the function up this way allows for 

760 # auditing by external tools that wrap the existing 

761 # function. If we try to audit __call__(), we can 

762 # run into troubles due to the @webob.dec.wsgify() 

763 # decorator. 

764 return self._process_stack(request, action, action_args, 

765 content_type, body, accept) 

766 

767 def _process_stack(self, request, action, action_args, 

768 content_type, body, accept): 

769 """Implement the processing stack.""" 

770 

771 # Get the implementing method 

772 try: 

773 meth, extensions = self.get_method(request, action, 

774 content_type, body) 

775 except (AttributeError, TypeError): 

776 return Fault(webob.exc.HTTPNotFound()) 

777 except KeyError as ex: 

778 msg = _("There is no such action: %s") % ex.args[0] 

779 return Fault(webob.exc.HTTPBadRequest(explanation=msg)) 

780 except exception.MalformedRequestBody: 

781 msg = _("Malformed request body") 

782 return Fault(webob.exc.HTTPBadRequest(explanation=msg)) 

783 

784 try: 

785 method_name = meth.__qualname__ 

786 except AttributeError: 

787 method_name = 'Controller: %s Method: %s' % ( 

788 str(self.controller), meth.__name__) 

789 

790 if body: 

791 decoded_body = encodeutils.safe_decode(body, errors='ignore') 

792 msg = ("Action: '%(action)s', calling method: %(meth)s, body: " 

793 "%(body)s") % {'action': action, 

794 'body': decoded_body, 

795 'meth': method_name} 

796 LOG.debug(strutils.mask_password(msg)) 

797 else: 

798 LOG.debug("Calling method '%(meth)s'", {'meth': method_name}) 

799 

800 # Now, deserialize the request body... 

801 try: 

802 if content_type: 

803 contents = self.deserialize(meth, content_type, body) 

804 else: 

805 contents = {} 

806 except exception.InvalidContentType: 

807 msg = _("Unsupported Content-Type") 

808 return Fault(webob.exc.HTTPBadRequest(explanation=msg)) 

809 except exception.MalformedRequestBody: 

810 msg = _("Malformed request body") 

811 return Fault(webob.exc.HTTPBadRequest(explanation=msg)) 

812 

813 # Update the action args 

814 action_args.update(contents) 

815 

816 project_id = action_args.pop("project_id", None) 

817 context = request.environ.get('manila.context') 

818 if (context and project_id and (project_id != context.project_id)): 818 ↛ 819line 818 didn't jump to line 819 because the condition on line 818 was never true

819 msg = _("Malformed request url") 

820 return Fault(webob.exc.HTTPBadRequest(explanation=msg)) 

821 

822 # Run pre-processing extensions 

823 response, post = self.pre_process_extensions(extensions, 

824 request, action_args) 

825 

826 if not response: 826 ↛ 833line 826 didn't jump to line 833 because the condition on line 826 was always true

827 try: 

828 with ResourceExceptionHandler(): 

829 action_result = self.dispatch(meth, request, action_args) 

830 except Fault as ex: 

831 response = ex 

832 

833 if not response: 

834 # No exceptions; convert action_result into a 

835 # ResponseObject 

836 resp_obj = None 

837 if type(action_result) is dict or action_result is None: 

838 resp_obj = ResponseObject(action_result) 

839 elif isinstance(action_result, ResponseObject): 839 ↛ 840line 839 didn't jump to line 840 because the condition on line 839 was never true

840 resp_obj = action_result 

841 else: 

842 response = action_result 

843 

844 # Run post-processing extensions 

845 if resp_obj: 

846 _set_request_id_header(request, resp_obj) 

847 # Do a preserialize to set up the response object 

848 serializers = getattr(meth, 'wsgi_serializers', {}) 

849 resp_obj._bind_method_serializers(serializers) 

850 if hasattr(meth, 'wsgi_code'): 

851 resp_obj._default_code = meth.wsgi_code 

852 resp_obj.preserialize(accept, self.default_serializers) 

853 

854 # Process post-processing extensions 

855 response = self.post_process_extensions(post, resp_obj, 

856 request, action_args) 

857 

858 if resp_obj and not response: 

859 response = resp_obj.serialize(request, accept, 

860 self.default_serializers) 

861 

862 try: 

863 msg_dict = dict(url=request.url, status=response.status_int) 

864 msg = _("%(url)s returned with HTTP %(status)s") % msg_dict 

865 except AttributeError as e: 

866 msg_dict = dict(url=request.url, e=e) 

867 msg = _("%(url)s returned a fault: %(e)s") % msg_dict 

868 

869 LOG.info(msg) 

870 

871 if hasattr(response, 'headers'): 

872 for hdr, val in response.headers.items(): 

873 val = utils.convert_str(val) 

874 response.headers[hdr] = val 

875 _set_request_id_header(request, response.headers) 

876 if not request.api_version_request.is_null(): 

877 response.headers[API_VERSION_REQUEST_HEADER] = ( 

878 request.api_version_request.get_string()) 

879 if request.api_version_request.experimental: 

880 # NOTE(vponomaryov): Translate our boolean header 

881 # to string explicitly to avoid 'TypeError' failure 

882 # running manila API under Apache + mod-wsgi. 

883 # It is safe to do so, because all headers are returned as 

884 # strings anyway. 

885 response.headers[EXPERIMENTAL_API_REQUEST_HEADER] = ( 

886 '%s' % request.api_version_request.experimental) 

887 response.headers['Vary'] = API_VERSION_REQUEST_HEADER 

888 

889 return response 

890 

891 def get_method(self, request, action, content_type, body): 

892 """Look up the action-specific method and its extensions.""" 

893 

894 # Look up the method 

895 try: 

896 if not self.controller: 896 ↛ 897line 896 didn't jump to line 897 because the condition on line 896 was never true

897 meth = getattr(self, action) 

898 else: 

899 meth = getattr(self.controller, action) 

900 except AttributeError: 

901 if (not self.wsgi_actions or 

902 action not in ['action', 'create', 'delete']): 

903 # Propagate the error 

904 raise 

905 else: 

906 return meth, self.wsgi_extensions.get(action, []) 

907 

908 if action == 'action': 

909 # OK, it's an action; figure out which action... 

910 mtype = _MEDIA_TYPE_MAP.get(content_type) 

911 action_name = self.action_peek[mtype](body) 

912 LOG.debug("Action body: %s", body) 

913 else: 

914 action_name = action 

915 

916 # Look up the action method 

917 return (self.wsgi_actions[action_name], 

918 self.wsgi_action_extensions.get(action_name, [])) 

919 

920 def dispatch(self, method, request, action_args): 

921 """Dispatch a call to the action-specific method.""" 

922 

923 try: 

924 return method(req=request, **action_args) 

925 except exception.VersionNotFoundForAPIMethod: 

926 # We deliberately don't return any message information 

927 # about the exception to the user so it looks as if 

928 # the method is simply not implemented. 

929 return Fault(webob.exc.HTTPNotFound()) 

930 

931 

932def action(name): 

933 """Mark a function as an action. 

934 

935 The given name will be taken as the action key in the body. 

936 

937 This is also overloaded to allow extensions to provide 

938 non-extending definitions of create and delete operations. 

939 """ 

940 

941 def decorator(func): 

942 func.wsgi_action = name 

943 return func 

944 return decorator 

945 

946 

947def extends(*args, **kwargs): 

948 """Indicate a function extends an operation. 

949 

950 Can be used as either:: 

951 

952 @extends 

953 def index(...): 

954 pass 

955 

956 or as:: 

957 

958 @extends(action='resize') 

959 def _action_resize(...): 

960 pass 

961 """ 

962 

963 def decorator(func): 

964 # Store enough information to find what we're extending 

965 func.wsgi_extends = (func.__name__, kwargs.get('action')) 

966 return func 

967 

968 # If we have positional arguments, call the decorator 

969 if args: 

970 return decorator(*args) 

971 

972 # OK, return the decorator instead 

973 return decorator 

974 

975 

976class ControllerMetaclass(type): 

977 """Controller metaclass. 

978 

979 This metaclass automates the task of assembling a dictionary 

980 mapping action keys to method names. 

981 """ 

982 

983 def __new__(mcs, name, bases, cls_dict): 

984 """Adds the wsgi_actions dictionary to the class.""" 

985 

986 # Find all actions 

987 actions = {} 

988 extensions = [] 

989 versioned_methods = None 

990 # start with wsgi actions from base classes 

991 for base in bases: 

992 actions.update(getattr(base, 'wsgi_actions', {})) 

993 

994 if base.__name__ == "Controller": 

995 # NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute 

996 # between API controller class creations. This allows us 

997 # to use a class decorator on the API methods that doesn't 

998 # require naming explicitly what method is being versioned as 

999 # it can be implicit based on the method decorated. It is a bit 

1000 # ugly. 

1001 if VER_METHOD_ATTR in base.__dict__: 

1002 versioned_methods = getattr(base, VER_METHOD_ATTR) 

1003 delattr(base, VER_METHOD_ATTR) 

1004 

1005 for key, value in cls_dict.items(): 

1006 if not callable(value): 

1007 continue 

1008 if getattr(value, 'wsgi_action', None): 

1009 actions[value.wsgi_action] = key 

1010 elif getattr(value, 'wsgi_extends', None): 

1011 extensions.append(value.wsgi_extends) 

1012 

1013 # Add the actions and extensions to the class dict 

1014 cls_dict['wsgi_actions'] = actions 

1015 cls_dict['wsgi_extensions'] = extensions 

1016 if versioned_methods: 

1017 cls_dict[VER_METHOD_ATTR] = versioned_methods 

1018 

1019 return super(ControllerMetaclass, mcs).__new__(mcs, name, bases, 

1020 cls_dict) 

1021 

1022 

1023class Controller(metaclass=ControllerMetaclass): 

1024 """Default controller.""" 

1025 

1026 _view_builder_class = None 

1027 

1028 def __init__(self, view_builder=None): 

1029 """Initialize controller with a view builder instance.""" 

1030 if view_builder: 1030 ↛ 1031line 1030 didn't jump to line 1031 because the condition on line 1030 was never true

1031 self._view_builder = view_builder 

1032 elif self._view_builder_class: 

1033 # pylint: disable=not-callable 

1034 self._view_builder = self._view_builder_class() 

1035 else: 

1036 self._view_builder = None 

1037 

1038 def __getattribute__(self, key): 

1039 

1040 def version_select(*args, **kwargs): 

1041 """Select and call the matching version of the specified method. 

1042 

1043 Look for the method which matches the name supplied and version 

1044 constraints and calls it with the supplied arguments. 

1045 

1046 :returns: Returns the result of the method called 

1047 :raises: VersionNotFoundForAPIMethod if there is no method which 

1048 matches the name and version constraints 

1049 """ 

1050 

1051 # The first arg to all versioned methods is always the request 

1052 # object. The version for the request is attached to the 

1053 # request object 

1054 if len(args) == 0: 

1055 version_request = kwargs['req'].api_version_request 

1056 else: 

1057 version_request = args[0].api_version_request 

1058 

1059 func_list = self.versioned_methods[key] 

1060 for func in func_list: 

1061 if version_request.matches_versioned_method(func): 

1062 # Update the version_select wrapper function so 

1063 # other decorator attributes like wsgi.response 

1064 # are still respected. 

1065 functools.update_wrapper(version_select, func.func) 

1066 return func.func(self, *args, **kwargs) 

1067 

1068 # No version match 

1069 raise exception.VersionNotFoundForAPIMethod( 

1070 version=version_request) 

1071 

1072 try: 

1073 version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR) 

1074 except AttributeError: 

1075 # No versioning on this class 

1076 return object.__getattribute__(self, key) 

1077 

1078 if (version_meth_dict and 

1079 key in object.__getattribute__(self, VER_METHOD_ATTR)): 

1080 return version_select 

1081 

1082 return object.__getattribute__(self, key) 

1083 

1084 # NOTE(cyeoh): This decorator MUST appear first (the outermost 

1085 # decorator) on an API method for it to work correctly 

1086 @classmethod 

1087 def api_version(cls, min_ver, max_ver=None, experimental=False): 

1088 """Decorator for versioning API methods. 

1089 

1090 Add the decorator to any method which takes a request object 

1091 as the first parameter and belongs to a class which inherits from 

1092 wsgi.Controller. 

1093 

1094 :param min_ver: string representing minimum version 

1095 :param max_ver: optional string representing maximum version 

1096 :param experimental: flag indicating an API is experimental and is 

1097 subject to change or removal at any time 

1098 """ 

1099 

1100 def decorator(f): 

1101 obj_min_ver = api_version.APIVersionRequest(min_ver) 

1102 if max_ver: 

1103 obj_max_ver = api_version.APIVersionRequest(max_ver) 

1104 else: 

1105 obj_max_ver = api_version.APIVersionRequest() 

1106 

1107 # Add to list of versioned methods registered 

1108 func_name = f.__name__ 

1109 new_func = versioned_method.VersionedMethod( 

1110 func_name, obj_min_ver, obj_max_ver, experimental, f) 

1111 

1112 func_dict = getattr(cls, VER_METHOD_ATTR, {}) 

1113 if not func_dict: 

1114 setattr(cls, VER_METHOD_ATTR, func_dict) 

1115 

1116 func_list = func_dict.get(func_name, []) 

1117 if not func_list: 

1118 func_dict[func_name] = func_list 

1119 func_list.append(new_func) 

1120 # Ensure the list is sorted by minimum version (reversed) 

1121 # so later when we work through the list in order we find 

1122 # the method which has the latest version which supports 

1123 # the version requested. 

1124 # TODO(cyeoh): Add check to ensure that there are no overlapping 

1125 # ranges of valid versions as that is ambiguous 

1126 func_list.sort(reverse=True) 

1127 

1128 return f 

1129 

1130 return decorator 

1131 

1132 @staticmethod 

1133 def authorize(arg): 

1134 """Decorator for checking the policy on API methods. 

1135 

1136 Add this decorator to any API method which takes a request object 

1137 as the first parameter and belongs to a class which inherits from 

1138 wsgi.Controller. The class must also have a class member called 

1139 'resource_name' which specifies the resource for the policy check. 

1140 

1141 Can be used in any of the following forms 

1142 @authorize 

1143 @authorize('my_action_name') 

1144 

1145 :param arg: Can either be the function being decorated or a str 

1146 containing the 'action' for the policy check. If no action name is 

1147 provided, the function name is assumed to be the action name. 

1148 """ 

1149 action_name = None 

1150 

1151 def decorator(f): 

1152 @functools.wraps(f) 

1153 def wrapper(self, req, *args, **kwargs): 

1154 action = action_name or f.__name__ 

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

1156 try: 

1157 policy.check_policy(context, self.resource_name, action) 

1158 except exception.PolicyNotAuthorized: 

1159 raise webob.exc.HTTPForbidden() 

1160 return f(self, req, *args, **kwargs) 

1161 return wrapper 

1162 

1163 if callable(arg): 

1164 return decorator(arg) 

1165 else: 

1166 action_name = arg 

1167 return decorator 

1168 

1169 @staticmethod 

1170 def is_valid_body(body, entity_name): 

1171 if not (body and entity_name in body): 

1172 return False 

1173 

1174 def is_dict(d): 

1175 try: 

1176 d.get(None) 

1177 return True 

1178 except AttributeError: 

1179 return False 

1180 

1181 if not is_dict(body[entity_name]): 

1182 return False 

1183 

1184 return True 

1185 

1186 

1187class AdminActionsMixin(object): 

1188 """Mixin class for API controllers with admin actions.""" 

1189 

1190 body_attributes = { 

1191 'status': 'reset_status', 

1192 'replica_state': 'reset_replica_state', 

1193 'task_state': 'reset_task_state', 

1194 } 

1195 

1196 valid_statuses = { 

1197 'status': set([ 

1198 constants.STATUS_CREATING, 

1199 constants.STATUS_AVAILABLE, 

1200 constants.STATUS_DELETING, 

1201 constants.STATUS_ERROR, 

1202 constants.STATUS_ERROR_DELETING, 

1203 constants.STATUS_MIGRATING, 

1204 constants.STATUS_MIGRATING_TO, 

1205 constants.STATUS_SERVER_MIGRATING, 

1206 ]), 

1207 'replica_state': set([ 

1208 constants.REPLICA_STATE_ACTIVE, 

1209 constants.REPLICA_STATE_IN_SYNC, 

1210 constants.REPLICA_STATE_OUT_OF_SYNC, 

1211 constants.STATUS_ERROR, 

1212 ]), 

1213 'task_state': set(constants.TASK_STATE_STATUSES), 

1214 } 

1215 

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

1217 raise NotImplementedError() 

1218 

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

1220 raise NotImplementedError() 

1221 

1222 def _delete(self, *args, **kwargs): 

1223 raise NotImplementedError() 

1224 

1225 def validate_update(self, body, status_attr='status'): 

1226 update = {} 

1227 try: 

1228 update[status_attr] = body[status_attr] 

1229 except (TypeError, KeyError): 

1230 msg = _("Must specify '%s'") % status_attr 

1231 raise webob.exc.HTTPBadRequest(explanation=msg) 

1232 if update[status_attr] not in self.valid_statuses[status_attr]: 

1233 expl = (_("Invalid state. Valid states: %s.") % 

1234 ", ".join(str(i) for i in 

1235 self.valid_statuses[status_attr])) 

1236 raise webob.exc.HTTPBadRequest(explanation=expl) 

1237 return update 

1238 

1239 @Controller.authorize('reset_status') 

1240 def _reset_status(self, req, id, body, status_attr='status', 

1241 resource=None): 

1242 """Reset the status_attr specified on the resource. 

1243 

1244 :param req: API request object 

1245 :param id: ID of the resource 

1246 :param body: API request body 

1247 :param status_attr: Attribute on the resource denoting the status 

1248 to be reset 

1249 :param resource: Resource model or dict if we need to avoid fetching it 

1250 """ 

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

1252 body_attr = self.body_attributes[status_attr] 

1253 update = self.validate_update( 

1254 body.get(body_attr, body.get('-'.join(('os', body_attr)))), 

1255 status_attr=status_attr) 

1256 msg = "Updating %(resource)s '%(id)s' with '%(update)r'" 

1257 LOG.debug(msg, {'resource': self.resource_name, 'id': id, 

1258 'update': update}) 

1259 try: 

1260 resource = resource or self._get(context, id) 

1261 except exception.NotFound as e: 

1262 raise webob.exc.HTTPNotFound(e.message) 

1263 

1264 if (status_attr == 'replica_state' and 

1265 resource.get('replica_state') == 

1266 constants.REPLICA_STATE_ACTIVE): 

1267 msg = _("Cannot reset replica_state of an active replica") 

1268 raise webob.exc.HTTPBadRequest(explanation=msg) 

1269 try: 

1270 policy.check_policy(context, 

1271 self.resource_name, 

1272 "reset_status", 

1273 target_obj=resource) 

1274 except exception.NotAuthorized as e: 

1275 raise webob.exc.HTTPForbidden(e.message) 

1276 self._update(context, id, update) 

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

1278 

1279 @Controller.authorize('force_delete') 

1280 def _force_delete(self, req, id, body): 

1281 """Delete a resource, bypassing the check for status.""" 

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

1283 try: 

1284 resource = self._get(context, id) 

1285 except exception.NotFound as e: 

1286 raise webob.exc.HTTPNotFound(e.message) 

1287 policy.check_policy(context, 

1288 self.resource_name, 

1289 "force_delete", 

1290 target_obj=resource) 

1291 self._delete(context, resource, force=True) 

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

1293 

1294 

1295class Fault(webob.exc.HTTPException): 

1296 """Wrap webob.exc.HTTPException to provide API friendly response.""" 

1297 

1298 _fault_names = {400: "badRequest", 

1299 401: "unauthorized", 

1300 403: "forbidden", 

1301 404: "itemNotFound", 

1302 405: "badMethod", 

1303 409: "conflictingRequest", 

1304 413: "overLimit", 

1305 415: "badMediaType", 

1306 501: "notImplemented", 

1307 503: "serviceUnavailable"} 

1308 

1309 def __init__(self, exception): 

1310 """Create a Fault for the given webob.exc.exception.""" 

1311 self.wrapped_exc = exception 

1312 self.status_int = exception.status_int 

1313 

1314 @webob.dec.wsgify(RequestClass=Request) 

1315 def __call__(self, req): 

1316 """Generate a WSGI response based on the exception passed to ctor.""" 

1317 # Replace the body with fault details. 

1318 code = self.wrapped_exc.status_int 

1319 fault_name = self._fault_names.get(code, "computeFault") 

1320 fault_data = { 

1321 fault_name: { 

1322 'code': code, 

1323 'message': self.wrapped_exc.explanation}} 

1324 if code == 413: 

1325 retry = self.wrapped_exc.headers['Retry-After'] 

1326 fault_data[fault_name]['retryAfter'] = '%s' % retry 

1327 

1328 if not req.api_version_request.is_null(): 

1329 self.wrapped_exc.headers[API_VERSION_REQUEST_HEADER] = ( 

1330 req.api_version_request.get_string()) 

1331 if req.api_version_request.experimental: 

1332 # NOTE(vponomaryov): Translate our boolean header 

1333 # to string explicitly to avoid 'TypeError' failure 

1334 # running manila API under Apache + mod-wsgi. 

1335 # It is safe to do so, because all headers are returned as 

1336 # strings anyway. 

1337 self.wrapped_exc.headers[EXPERIMENTAL_API_REQUEST_HEADER] = ( 

1338 '%s' % req.api_version_request.experimental) 

1339 self.wrapped_exc.headers['Vary'] = API_VERSION_REQUEST_HEADER 

1340 

1341 content_type = req.best_match_content_type() 

1342 serializer = { 

1343 'application/json': JSONDictSerializer(), 

1344 }[content_type] 

1345 

1346 self.wrapped_exc.body = serializer.serialize(fault_data) 

1347 self.wrapped_exc.content_type = content_type 

1348 _set_request_id_header(req, self.wrapped_exc.headers) 

1349 

1350 return self.wrapped_exc 

1351 

1352 def __str__(self): 

1353 return self.wrapped_exc.__str__() 

1354 

1355 

1356def _set_request_id_header(req, headers): 

1357 context = req.environ.get('manila.context') 

1358 if context: 

1359 headers['x-compute-request-id'] = context.request_id 

1360 

1361 

1362class OverLimitFault(webob.exc.HTTPException): 

1363 """Rate-limited request response.""" 

1364 

1365 def __init__(self, message, details, retry_time): 

1366 """Initialize new `OverLimitFault` with relevant information.""" 

1367 hdrs = OverLimitFault._retry_after(retry_time) 

1368 self.wrapped_exc = webob.exc.HTTPRequestEntityTooLarge(headers=hdrs) 

1369 self.content = { 

1370 "overLimitFault": { 

1371 "code": self.wrapped_exc.status_int, 

1372 "message": message, 

1373 "details": details, 

1374 }, 

1375 } 

1376 

1377 @staticmethod 

1378 def _retry_after(retry_time): 

1379 delay = int(math.ceil(retry_time - time.time())) 

1380 retry_after = delay if delay > 0 else 0 

1381 headers = {'Retry-After': '%s' % retry_after} 

1382 return headers 

1383 

1384 @webob.dec.wsgify(RequestClass=Request) 

1385 def __call__(self, request): 

1386 """Wrap the exception. 

1387 

1388 Wrap the exception with a serialized body conforming to our 

1389 error format. 

1390 """ 

1391 content_type = request.best_match_content_type() 

1392 

1393 serializer = { 

1394 'application/json': JSONDictSerializer(), 

1395 }[content_type] 

1396 

1397 content = serializer.serialize(self.content) 

1398 self.wrapped_exc.body = content 

1399 

1400 return self.wrapped_exc