From 9dd629ca7ea5bafe883f48d257386a8c5d32c1c7 Mon Sep 17 00:00:00 2001 From: Avan Thakkar Date: Wed, 29 Jul 2020 23:26:04 +0530 Subject: [PATCH] mgr/dashboard: REST API returns 500 when no Content-Type is specified Fixes: https://tracker.ceph.com/issues/41060 Signed-off-by: Avan Thakkar (cherry picked from commit ea031de0908a249b416617a1d9cc806c356520e4) (cherry picked from commit a5bf74e6a6c7678bbb3689a4b52fe817418da58b) --- src/pybind/mgr/dashboard/controllers/__init__.py | 15 +++++++++++++++ src/pybind/mgr/dashboard/controllers/auth.py | 4 +++- src/pybind/mgr/dashboard/controllers/cephfs.py | 7 +++++-- src/pybind/mgr/dashboard/controllers/docs.py | 4 +++- src/pybind/mgr/dashboard/controllers/host.py | 4 +++- .../mgr/dashboard/controllers/mgr_modules.py | 5 ++++- src/pybind/mgr/dashboard/controllers/osd.py | 13 +++++++++++-- src/pybind/mgr/dashboard/controllers/rbd.py | 9 ++++++++- .../mgr/dashboard/controllers/rbd_mirroring.py | 4 +++- src/pybind/mgr/dashboard/controllers/rgw.py | 10 +++++++++- src/pybind/mgr/dashboard/controllers/user.py | 4 +++- src/pybind/mgr/dashboard/module.py | 2 +- 12 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index de66b394589..092e55c1a10 100644 --- a/src/pybind/mgr/dashboard/controllers/__init__.py +++ b/src/pybind/mgr/dashboard/controllers/__init__.py @@ -481,6 +481,7 @@ class BaseController(object): """ An instance of this class represents an endpoint. """ + def __init__(self, ctrl, func): self.ctrl = ctrl self.inst = None @@ -949,3 +950,17 @@ def UpdatePermission(func): # noqa: N802 """ _set_func_permissions(func, Permission.UPDATE) return func + + +# Empty request body decorator + +def allow_empty_body(func): # noqa: N802 + """ + The POST/PUT request methods decorated with ``@allow_empty_body`` + are allowed to send empty request body. + """ + try: + func._cp_config['tools.json_in.force'] = False + except (AttributeError, KeyError): + func._cp_config = {'tools.json_in.force': False} + return func diff --git a/src/pybind/mgr/dashboard/controllers/auth.py b/src/pybind/mgr/dashboard/controllers/auth.py index 6f28379c6a2..ee638a61718 100644 --- a/src/pybind/mgr/dashboard/controllers/auth.py +++ b/src/pybind/mgr/dashboard/controllers/auth.py @@ -4,7 +4,8 @@ from __future__ import absolute_import import logging import cherrypy -from . import ApiController, RESTController, ControllerDoc, EndpointDoc +from . import ApiController, RESTController, \ + allow_empty_body, ControllerDoc, EndpointDoc from .. import mgr from ..exceptions import DashboardException from ..services.auth import AuthManager, JwtManager @@ -56,6 +57,7 @@ class Auth(RESTController): component='auth') @RESTController.Collection('POST') + @allow_empty_body def logout(self): logger.debug('Logout successful') token = JwtManager.get_token_from_header() diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 65969f8d8fd..41708f93d2d 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -6,8 +6,8 @@ import os import cherrypy import cephfs -from . import ApiController, ControllerDoc, RESTController, \ - UiApiController, EndpointDoc +from . import ApiController, ControllerDoc, RESTController, UiApiController, \ + allow_empty_body, EndpointDoc from .. import mgr from ..exceptions import DashboardException from ..security import Scope @@ -390,6 +390,7 @@ class CephFS(RESTController): return path @RESTController.Resource('POST', path='/tree') + @allow_empty_body def mk_tree(self, fs_id, path): """ Create a directory. @@ -410,6 +411,7 @@ class CephFS(RESTController): cfs.rm_dir(path) @RESTController.Resource('PUT', path='/quota') + @allow_empty_body def quota(self, fs_id, path, max_bytes=None, max_files=None): """ Set the quotas of the specified path. @@ -441,6 +443,7 @@ class CephFS(RESTController): return cfs.get_quotas(path) @RESTController.Resource('POST', path='/snapshot') + @allow_empty_body def snapshot(self, fs_id, path, name=None): """ Create a snapshot. diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py index fce5341193c..f5888c0c4c9 100644 --- a/src/pybind/mgr/dashboard/controllers/docs.py +++ b/src/pybind/mgr/dashboard/controllers/docs.py @@ -5,7 +5,8 @@ from typing import Any, Dict, Union import logging import cherrypy -from . import Controller, BaseController, Endpoint, ENDPOINT_MAP +from . import Controller, BaseController, Endpoint, ENDPOINT_MAP, \ + allow_empty_body from .. import mgr from ..tools import str_to_bool @@ -457,5 +458,6 @@ class Docs(BaseController): @Endpoint('POST', path="/", json_response=False, query_params="{all_endpoints}") + @allow_empty_body def _with_token(self, token, all_endpoints=False): return self._swagger_ui_page(all_endpoints, token) diff --git a/src/pybind/mgr/dashboard/controllers/host.py b/src/pybind/mgr/dashboard/controllers/host.py index 1d502e6cc68..86889ae15f7 100644 --- a/src/pybind/mgr/dashboard/controllers/host.py +++ b/src/pybind/mgr/dashboard/controllers/host.py @@ -10,7 +10,7 @@ import cherrypy from mgr_util import merge_dicts from orchestrator import HostSpec from . import ApiController, RESTController, Task, Endpoint, ReadPermission, \ - UiApiController, BaseController, EndpointDoc, ControllerDoc + UiApiController, BaseController, allow_empty_body, ControllerDoc, EndpointDoc from .orchestrator import raise_if_no_orchestrator from .. import mgr from ..exceptions import DashboardException @@ -144,10 +144,12 @@ class Host(RESTController): orch_client = OrchClient.instance() self._check_orchestrator_host_op(orch_client, hostname, True) orch_client.hosts.add(hostname) + create._cp_config = {'tools.json_in.force': False} # pylint: disable=W0212 @raise_if_no_orchestrator([OrchFeature.HOST_LIST, OrchFeature.HOST_DELETE]) @handle_orchestrator_error('host') @host_task('delete', {'hostname': '{hostname}'}) + @allow_empty_body def delete(self, hostname): # pragma: no cover - requires realtime env orch_client = OrchClient.instance() self._check_orchestrator_host_op(orch_client, hostname, False) diff --git a/src/pybind/mgr/dashboard/controllers/mgr_modules.py b/src/pybind/mgr/dashboard/controllers/mgr_modules.py index ca834b96f89..da31044f24b 100644 --- a/src/pybind/mgr/dashboard/controllers/mgr_modules.py +++ b/src/pybind/mgr/dashboard/controllers/mgr_modules.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from . import ApiController, RESTController, ControllerDoc, EndpointDoc +from . import ApiController, RESTController, \ + allow_empty_body, ControllerDoc, EndpointDoc from .. import mgr from ..security import Scope from ..services.ceph_service import CephService @@ -93,6 +94,7 @@ class MgrModules(RESTController): @RESTController.Resource('POST') @handle_send_command_error('mgr_modules') + @allow_empty_body def enable(self, module_name): """ Enable the specified Ceph Mgr module. @@ -105,6 +107,7 @@ class MgrModules(RESTController): @RESTController.Resource('POST') @handle_send_command_error('mgr_modules') + @allow_empty_body def disable(self, module_name): """ Disable the specified Ceph Mgr module. diff --git a/src/pybind/mgr/dashboard/controllers/osd.py b/src/pybind/mgr/dashboard/controllers/osd.py index 6db31c59c72..75e0d565b72 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -7,8 +7,9 @@ import time from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError from mgr_util import get_most_recent_rate -from . import ApiController, RESTController, Endpoint, Task, EndpointDoc, ControllerDoc -from . import CreatePermission, ReadPermission, UpdatePermission, DeletePermission +from . import ApiController, RESTController, Endpoint, Task +from . import CreatePermission, ReadPermission, UpdatePermission, DeletePermission, \ + allow_empty_body, ControllerDoc, EndpointDoc from .orchestrator import raise_if_no_orchestrator from .. import mgr from ..exceptions import DashboardException @@ -191,23 +192,28 @@ class Osd(RESTController): @RESTController.Resource('POST', query_params=['deep']) @UpdatePermission + @allow_empty_body def scrub(self, svc_id, deep=False): api_scrub = "osd deep-scrub" if str_to_bool(deep) else "osd scrub" CephService.send_command("mon", api_scrub, who=svc_id) @RESTController.Resource('POST') + @allow_empty_body def mark_out(self, svc_id): CephService.send_command('mon', 'osd out', ids=[svc_id]) @RESTController.Resource('POST') + @allow_empty_body def mark_in(self, svc_id): CephService.send_command('mon', 'osd in', ids=[svc_id]) @RESTController.Resource('POST') + @allow_empty_body def mark_down(self, svc_id): CephService.send_command('mon', 'osd down', ids=[svc_id]) @RESTController.Resource('POST') + @allow_empty_body def reweight(self, svc_id, weight): """ Reweights the OSD temporarily. @@ -229,6 +235,7 @@ class Osd(RESTController): weight=float(weight)) @RESTController.Resource('POST') + @allow_empty_body def mark_lost(self, svc_id): """ Note: osd must be marked `down` before marking lost. @@ -282,6 +289,7 @@ class Osd(RESTController): component='osd', http_status_code=400, msg='Unknown method: {}'.format(method)) @RESTController.Resource('POST') + @allow_empty_body def purge(self, svc_id): """ Note: osd must be marked `down` before removal. @@ -290,6 +298,7 @@ class Osd(RESTController): yes_i_really_mean_it=True) @RESTController.Resource('POST') + @allow_empty_body def destroy(self, svc_id): """ Mark osd as being destroyed. Keeps the ID intact (allowing reuse), but diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index a3ccf634276..ac2188c8015 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -11,7 +11,7 @@ from datetime import datetime import rbd from . import ApiController, RESTController, Task, UpdatePermission, \ - DeletePermission, CreatePermission, EndpointDoc, ControllerDoc + DeletePermission, CreatePermission, allow_empty_body, ControllerDoc, EndpointDoc from .. import mgr from ..exceptions import DashboardException from ..security import Scope @@ -191,6 +191,7 @@ class Rbd(RESTController): 'dest_namespace': '{dest_namespace}', 'dest_image_name': '{dest_image_name}'}, 2.0) @RESTController.Resource('POST') + @allow_empty_body def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name, snapshot_name=None, obj_size=None, features=None, stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): @@ -221,6 +222,7 @@ class Rbd(RESTController): @RbdTask('flatten', ['{image_spec}'], 2.0) @RESTController.Resource('POST') @UpdatePermission + @allow_empty_body def flatten(self, image_spec): def _flatten(ioctx, image): @@ -236,6 +238,7 @@ class Rbd(RESTController): @RbdTask('trash/move', ['{image_spec}'], 2.0) @RESTController.Resource('POST') + @allow_empty_body def move_trash(self, image_spec, delay=0): """Move an image to the trash. Images, even ones actively in-use by clones, @@ -290,6 +293,7 @@ class RbdSnapshot(RESTController): ['{image_spec}', '{snapshot_name}'], 5.0) @RESTController.Resource('POST') @UpdatePermission + @allow_empty_body def rollback(self, image_spec, snapshot_name): def _rollback(ioctx, img, snapshot_name): img.rollback_to_snap(snapshot_name) @@ -303,6 +307,7 @@ class RbdSnapshot(RESTController): 'child_namespace': '{child_namespace}', 'child_image_name': '{child_image_name}'}, 2.0) @RESTController.Resource('POST') + @allow_empty_body def clone(self, image_spec, snapshot_name, child_pool_name, child_image_name, child_namespace=None, obj_size=None, features=None, stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): @@ -389,6 +394,7 @@ class RbdTrash(RESTController): @RbdTask('trash/purge', ['{pool_name}'], 2.0) @RESTController.Collection('POST', query_params=['pool_name']) @DeletePermission + @allow_empty_body def purge(self, pool_name=None): """Remove all expired images from trash.""" now = "{}Z".format(datetime.utcnow().isoformat()) @@ -405,6 +411,7 @@ class RbdTrash(RESTController): @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0) @RESTController.Resource('POST') @CreatePermission + @allow_empty_body def restore(self, image_id_spec, new_image_name): """Restore an image from trash.""" pool_name, namespace, image_id = parse_image_spec(image_id_spec) diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py index c089e0a7275..8056de489f1 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -12,7 +12,7 @@ import cherrypy import rbd from . import ApiController, Endpoint, Task, BaseController, ReadPermission, \ - UpdatePermission, RESTController, EndpointDoc, ControllerDoc + UpdatePermission, RESTController, allow_empty_body, ControllerDoc, EndpointDoc from .. import mgr from ..security import Scope @@ -453,6 +453,7 @@ class RbdMirroringPoolBootstrap(BaseController): @Endpoint(method='POST', path='token') @handle_rbd_mirror_error() @UpdatePermission + @allow_empty_body def create_token(self, pool_name): ioctx = mgr.rados.open_ioctx(pool_name) token = rbd.RBD().mirror_peer_bootstrap_create(ioctx) @@ -461,6 +462,7 @@ class RbdMirroringPoolBootstrap(BaseController): @Endpoint(method='POST', path='peer') @handle_rbd_mirror_error() @UpdatePermission + @allow_empty_body def import_token(self, pool_name, direction, token): ioctx = mgr.rados.open_ioctx(pool_name) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 6f0ded0b513..8b8eaba2faa 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -6,7 +6,7 @@ import json import cherrypy from . import ApiController, BaseController, RESTController, Endpoint, \ - ReadPermission, ControllerDoc, EndpointDoc + ReadPermission, allow_empty_body, ControllerDoc, EndpointDoc from ..exceptions import DashboardException from ..rest_client import RequestException from ..security import Scope, Permission @@ -236,6 +236,7 @@ class RgwBucket(RgwRESTController): return self._append_bid(result) + @allow_empty_body def create(self, bucket, uid, zonegroup=None, placement_target=None, lock_enabled='false', lock_mode=None, lock_retention_period_days=None, @@ -254,6 +255,7 @@ class RgwBucket(RgwRESTController): except RequestException as e: # pragma: no cover - handling is too obvious raise DashboardException(e, http_status_code=500, component='rgw') + @allow_empty_body def set(self, bucket, bucket_id, uid, versioning_state=None, mfa_delete=None, mfa_token_serial=None, mfa_token_pin=None, lock_mode=None, lock_retention_period_days=None, @@ -360,6 +362,7 @@ class RgwUser(RgwRESTController): emails.append(user["email"]) return emails + @allow_empty_body def create(self, uid, display_name, email=None, max_buckets=None, suspended=None, generate_key=None, access_key=None, secret_key=None): @@ -381,6 +384,7 @@ class RgwUser(RgwRESTController): result = self.proxy('PUT', 'user', params) return self._append_uid(result) + @allow_empty_body def set(self, uid, display_name=None, email=None, max_buckets=None, suspended=None): params = {'uid': uid} @@ -410,6 +414,7 @@ class RgwUser(RgwRESTController): # pylint: disable=redefined-builtin @RESTController.Resource(method='POST', path='/capability', status=201) + @allow_empty_body def create_cap(self, uid, type, perm): return self.proxy('PUT', 'user?caps', { 'uid': uid, @@ -425,6 +430,7 @@ class RgwUser(RgwRESTController): }) @RESTController.Resource(method='POST', path='/key', status=201) + @allow_empty_body def create_key(self, uid, key_type='s3', subuser=None, generate_key='true', access_key=None, secret_key=None): params = {'uid': uid, 'key-type': key_type, 'generate-key': generate_key} @@ -450,6 +456,7 @@ class RgwUser(RgwRESTController): return self.proxy('GET', 'user?quota', {'uid': uid}) @RESTController.Resource(method='PUT', path='/quota') + @allow_empty_body def set_quota(self, uid, quota_type, enabled, max_size_kb, max_objects): return self.proxy('PUT', 'user?quota', { 'uid': uid, @@ -460,6 +467,7 @@ class RgwUser(RgwRESTController): }, json_response=False) @RESTController.Resource(method='POST', path='/subuser', status=201) + @allow_empty_body def create_subuser(self, uid, subuser, access, key_type='s3', generate_secret='true', access_key=None, secret_key=None): diff --git a/src/pybind/mgr/dashboard/controllers/user.py b/src/pybind/mgr/dashboard/controllers/user.py index bd50da5c0be..e44748f2ab5 100644 --- a/src/pybind/mgr/dashboard/controllers/user.py +++ b/src/pybind/mgr/dashboard/controllers/user.py @@ -7,7 +7,8 @@ import time import cherrypy -from . import BaseController, ApiController, RESTController, Endpoint, ControllerDoc, EndpointDoc +from . import BaseController, ApiController, RESTController, Endpoint, \ + allow_empty_body, ControllerDoc, EndpointDoc from .. import mgr from ..exceptions import DashboardException, UserAlreadyExists, \ UserDoesNotExist, PasswordPolicyException, PwdExpirationDateNotValid @@ -160,6 +161,7 @@ class User(RESTController): class UserPasswordPolicy(RESTController): @Endpoint('POST') + @allow_empty_body def validate_password(self, password, username=None, old_password=None): """ Check if the password meets the password policy. diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index c0c2d8cde43..27501f963b9 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -140,7 +140,7 @@ class CherryPyConfig(object): 'application/javascript', ], 'tools.json_in.on': True, - 'tools.json_in.force': False, + 'tools.json_in.force': True, 'tools.plugin_hooks_filter_request.on': True, } -- 2.39.5