]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: REST API returns 500 when no Content-Type is specified 34831/head
authorAvan Thakkar <athakkar@redhat.com>
Wed, 29 Jul 2020 17:56:04 +0000 (23:26 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Thu, 3 Sep 2020 06:34:14 +0000 (12:04 +0530)
Fixes: https://tracker.ceph.com/issues/41060
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
(cherry picked from commit ea031de0908a249b416617a1d9cc806c356520e4)
(cherry picked from commit a5bf74e6a6c7678bbb3689a4b52fe817418da58b)

12 files changed:
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/controllers/docs.py
src/pybind/mgr/dashboard/controllers/host.py
src/pybind/mgr/dashboard/controllers/mgr_modules.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/controllers/user.py
src/pybind/mgr/dashboard/module.py

index de66b394589c721278d10eaf2b25eb79604179fd..092e55c1a10074a7e043da9f9a2656fe6a88c480 100644 (file)
@@ -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
index 6f28379c6a213629aa9f1239a3dbf7fa6b671471..ee638a617189f6bdcb0e6bce4446fa34d1d99055 100644 (file)
@@ -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()
index 65969f8d8fdcd76de35b17d11d42088c0a5b48b1..41708f93d2daab254a740aabba78711a64f3eb26 100644 (file)
@@ -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.
index fce5341193c3115215a4fa5bc063bdb7fcf72822..f5888c0c4c96d580877ea2bb663f7d6fdecc0cc3 100644 (file)
@@ -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)
index 1d502e6cc68f92d8441f5a6074529d7b02958216..86889ae15f7bbcf04da98103715a8d9dd92c8c74 100644 (file)
@@ -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)
index ca834b96f893d18be0588199e5e4913836992732..da31044f24b9b93fe068bee8fe2003e1de2e2d4e 100644 (file)
@@ -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.
index 6db31c59c72edab3af7af75ed22bc95961fb17d4..75e0d565b72f9156256967b2596f0c5c2797467f 100644 (file)
@@ -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
index a3ccf634276f651e2e522d4e4765223298066000..ac2188c80156ddfe62f3709f0629f24f894a63a0 100644 (file)
@@ -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)
index c089e0a7275199c4a2af9ca4ec65c56bafdfc086..8056de489f12908d6b9d558ddc9466b17ff0f171 100644 (file)
@@ -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)
 
index 6f0ded0b513f38a2eda66937c1c9d5ddbf99e3ec..8b8eaba2faa424f6a43f4257e04a91f6471b542a 100644 (file)
@@ -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):
index bd50da5c0befc9e1c5e2220b09f97ef8eaf8b883..e44748f2ab5a9cc53d91b4b5d8ec31030f1ab754 100644 (file)
@@ -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.
index c0c2d8cde431a191329c9a214fcd6248cc3eb0c3..27501f963b9b1cc24ed91452e98b3cd198f6a9af 100644 (file)
@@ -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,
         }