From: Avan Thakkar Date: Wed, 29 Jul 2020 17:56:04 +0000 (+0530) Subject: mgr/dashboard: REST API returns 500 when no Content-Type is specified X-Git-Tag: v15.2.9~122^2~48^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=470f6cbec1c5fd8a7b5ba0f5b868938676c2fa32;p=ceph.git 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) (cherry picked from commit 9dd629ca7ea5bafe883f48d257386a8c5d32c1c7) --- diff --git a/src/pybind/mgr/dashboard/controllers/__init__.py b/src/pybind/mgr/dashboard/controllers/__init__.py index a72ed73ce0a..d872b8a3014 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 68c3827b4e8..96a809664b4 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 +from . import ApiController, RESTController, \ + allow_empty_body from .. import mgr from ..exceptions import DashboardException from ..services.auth import AuthManager, JwtManager @@ -47,6 +48,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 499fae94612..8461d943383 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -8,7 +8,8 @@ import os import cherrypy import cephfs -from . import ApiController, ControllerDoc, RESTController, UiApiController +from . import ApiController, ControllerDoc, RESTController, UiApiController, \ + allow_empty_body from .. import mgr from ..exceptions import DashboardException from ..security import Scope @@ -385,6 +386,7 @@ class CephFS(RESTController): return path @RESTController.Resource('POST') + @allow_empty_body def mk_dirs(self, fs_id, path): """ Create a directory. @@ -395,6 +397,7 @@ class CephFS(RESTController): cfs.mk_dirs(path) @RESTController.Resource('POST') + @allow_empty_body def rm_dir(self, fs_id, path): """ Remove a directory. @@ -405,6 +408,7 @@ class CephFS(RESTController): cfs.rm_dir(path) @RESTController.Resource('POST') + @allow_empty_body def mk_snapshot(self, fs_id, path, name=None): """ Create a snapshot. @@ -420,6 +424,7 @@ class CephFS(RESTController): return cfs.mk_snapshot(path, name) @RESTController.Resource('POST') + @allow_empty_body def rm_snapshot(self, fs_id, path, name): """ Remove a snapshot. @@ -444,6 +449,7 @@ class CephFS(RESTController): return cfs.get_quotas(path) @RESTController.Resource('POST') + @allow_empty_body def set_quotas(self, fs_id, path, max_bytes=None, max_files=None): """ Set the quotas of the specified path. diff --git a/src/pybind/mgr/dashboard/controllers/docs.py b/src/pybind/mgr/dashboard/controllers/docs.py index 84de247010c..09b646bc050 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 @@ -449,5 +450,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 db498c8aaad..c8a4d900723 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 + UiApiController, BaseController, allow_empty_body from .orchestrator import raise_if_no_orchestrator from .. import mgr from ..exceptions import DashboardException @@ -121,10 +121,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 @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 0d4cc522256..b558bed0653 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 +from . import ApiController, RESTController, \ + allow_empty_body from .. import mgr from ..security import Scope from ..services.ceph_service import CephService @@ -69,6 +70,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. @@ -81,6 +83,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 2731ad0ffcd..e29a056a167 100644 --- a/src/pybind/mgr/dashboard/controllers/osd.py +++ b/src/pybind/mgr/dashboard/controllers/osd.py @@ -7,7 +7,7 @@ import time from ceph.deployment.drive_group import DriveGroupSpec, DriveGroupValidationError from mgr_util import get_most_recent_rate -from . import ApiController, RESTController, Endpoint, Task +from . import ApiController, RESTController, Endpoint, Task, allow_empty_body from . import CreatePermission, ReadPermission, UpdatePermission, DeletePermission from .orchestrator import raise_if_no_orchestrator from .. import mgr @@ -190,23 +190,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. @@ -228,6 +233,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. @@ -281,6 +287,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. @@ -289,6 +296,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 ec959949381..e4c5f0d798f 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 + DeletePermission, CreatePermission, allow_empty_body from .. import mgr from ..exceptions import DashboardException from ..security import Scope @@ -173,6 +173,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): @@ -203,6 +204,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): @@ -218,6 +220,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, @@ -271,6 +274,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) @@ -284,6 +288,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): @@ -364,6 +369,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()) @@ -380,6 +386,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 02728903085..d6f8ab61ad7 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 + UpdatePermission, RESTController, allow_empty_body from .. import mgr from ..security import Scope @@ -413,6 +413,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) @@ -421,6 +422,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 a0dc444dcab..31fca5cfdbc 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 + ReadPermission, allow_empty_body from ..exceptions import DashboardException from ..rest_client import RequestException from ..security import Scope, Permission @@ -213,6 +213,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, @@ -231,6 +232,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, @@ -334,6 +336,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): @@ -355,6 +358,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} @@ -384,6 +388,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, @@ -399,6 +404,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} @@ -424,6 +430,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, @@ -434,6 +441,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 d32ee4cc833..35c3a05a351 100644 --- a/src/pybind/mgr/dashboard/controllers/user.py +++ b/src/pybind/mgr/dashboard/controllers/user.py @@ -7,7 +7,7 @@ import time import cherrypy -from . import BaseController, ApiController, RESTController, Endpoint +from . import BaseController, ApiController, RESTController, Endpoint, allow_empty_body from .. import mgr from ..exceptions import DashboardException, UserAlreadyExists, \ UserDoesNotExist, PasswordPolicyException, PwdExpirationDateNotValid @@ -146,6 +146,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, }