]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: clean-up python unit tests
authorErnesto Puerta <epuertat@redhat.com>
Fri, 19 Jul 2019 15:23:56 +0000 (17:23 +0200)
committerErnesto Puerta <epuertat@redhat.com>
Fri, 16 Aug 2019 13:24:31 +0000 (15:24 +0200)
Fix pylint issues.

Refactor JwtManager check to avoid code duplication (found by pylint
when --jobs=1).

Fix issue with DashboardException.code property, using abs() on
potentially None attribute.

Disables Doctests in services/rbd.py that are actually integration tests
(they check the value of rbd.RBD_FEATURES_NAME_MAPPING). Ideally, these
kind of tests should be explicitly executed in an integration testing
stage, rather that unit-testing. Disabled tests have been prepended with
@DISABLEDOCTEST token.

Fixes: https://tracker.ceph.com/issues/40487
Signed-off-by: Ernesto Puerta <epuertat@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/saml2.py
src/pybind/mgr/dashboard/exceptions.py
src/pybind/mgr/dashboard/plugins/__init__.py
src/pybind/mgr/dashboard/plugins/feature_toggles.py
src/pybind/mgr/dashboard/plugins/interfaces.py
src/pybind/mgr/dashboard/plugins/lru_cache.py
src/pybind/mgr/dashboard/plugins/pluggy.py
src/pybind/mgr/dashboard/plugins/ttl_cache.py
src/pybind/mgr/dashboard/services/auth.py
src/pybind/mgr/dashboard/services/ceph_service.py
src/pybind/mgr/dashboard/services/ganesha.py
src/pybind/mgr/dashboard/services/rbd.py
src/pybind/mgr/dashboard/tests/__init__.py

index 28f5998e876c7c2c6fb27b7324a0459dc6e2f6e0..5affb0d7e075427b2009a0fd993b9b667508fe2e 100644 (file)
@@ -30,13 +30,13 @@ def EndpointDoc(description="", group="", parameters=None, responses=None):
     if not isinstance(description, str):
         raise Exception("%s has been called with a description that is not a string: %s"
                         % (EndpointDoc.__name__, description))
-    elif not isinstance(group, str):
+    if not isinstance(group, str):
         raise Exception("%s has been called with a groupname that is not a string: %s"
                         % (EndpointDoc.__name__, group))
-    elif parameters and not isinstance(parameters, dict):
+    if parameters and not isinstance(parameters, dict):
         raise Exception("%s has been called with parameters that is not a dict: %s"
                         % (EndpointDoc.__name__, parameters))
-    elif responses and not isinstance(responses, dict):
+    if responses and not isinstance(responses, dict):
         raise Exception("%s has been called with responses that is not a dict: %s"
                         % (EndpointDoc.__name__, responses))
 
index 824b42b50dbc0b99f6df7c0d14849343517b3239..90752a03f4d401a2537b9e1371ba4fb364bbd781 100644 (file)
@@ -2,13 +2,11 @@
 from __future__ import absolute_import
 
 import cherrypy
-import jwt
 
 from . import ApiController, RESTController
 from .. import logger, mgr
 from ..exceptions import DashboardException
 from ..services.auth import AuthManager, JwtManager
-from ..services.access_control import UserDoesNotExist
 
 
 @ApiController('/auth', secure=False)
@@ -57,29 +55,13 @@ class Auth(RESTController):
     @RESTController.Collection('POST')
     def check(self, token):
         if token:
-            try:
-                token = JwtManager.decode_token(token)
-                if not JwtManager.is_blacklisted(token['jti']):
-                    user = AuthManager.get_user(token['username'])
-                    if user.lastUpdate <= token['iat']:
-                        return {
-                            'username': user.username,
-                            'permissions': user.permissions_dict(),
-                            'sso': mgr.SSO_DB.protocol == 'saml2'
-                        }
-
-                    logger.debug("AMT: user info changed after token was"
-                                 " issued, iat=%s lastUpdate=%s",
-                                 token['iat'], user.lastUpdate)
-                else:
-                    logger.debug('AMT: Token is black-listed')
-            except jwt.exceptions.ExpiredSignatureError:
-                logger.debug("AMT: Token has expired")
-            except jwt.exceptions.InvalidTokenError:
-                logger.debug("AMT: Failed to decode token")
-            except UserDoesNotExist:
-                logger.debug("AMT: Invalid token: user %s does not exist",
-                             token['username'])
+            user = JwtManager.get_user(token)
+            if user:
+                return {
+                    'username': user.username,
+                    'permissions': user.permissions_dict(),
+                    'sso': mgr.SSO_DB.protocol == 'saml2'
+                }
         return {
-            'login_url': self._get_login_url()
+            'login_url': self._get_login_url(),
         }
index cb4f1cea7721f5879258eee2331d001813f6dcbe..f32ddbc699de1dc0fd535db49367ae66ef2e7fb2 100644 (file)
@@ -12,6 +12,8 @@ import rbd
 
 from . import ApiController, Endpoint, Task, BaseController, ReadPermission, \
     RESTController
+from .rbd import _rbd_call
+
 from .. import logger, mgr
 from ..security import Scope
 from ..services.ceph_service import CephService
@@ -37,11 +39,6 @@ def RbdMirroringTask(name, metadata, wait_for):
     return composed_decorator
 
 
-def _rbd_call(pool_name, func, *args, **kwargs):
-    with mgr.rados.open_ioctx(pool_name) as ioctx:
-        func(ioctx, *args, **kwargs)
-
-
 @ViewCache()
 def get_daemons_and_pools():  # pylint: disable=R0915
     def get_daemons():
index 51bda8e8d62d09e31338b395bdab4e7148a56fb4..7d897e3979322fa44f1486159113029d49e9e9ae 100644 (file)
@@ -75,12 +75,12 @@ class Saml2(BaseController):
             token = token.decode('utf-8')
             logger.debug("JWT Token: %s", token)
             raise cherrypy.HTTPRedirect("{}/#/login?access_token={}".format(url_prefix, token))
-        else:
-            return {
-                'is_authenticated': auth.is_authenticated(),
-                'errors': errors,
-                'reason': auth.get_last_error_reason()
-            }
+
+        return {
+            'is_authenticated': auth.is_authenticated(),
+            'errors': errors,
+            'reason': auth.get_last_error_reason()
+        }
 
     @Endpoint(xml=True)
     def metadata(self):
index b44a3f15b03287fbaa8be6ae0c011d7f8f1dfb84..83de90f061924a1dea30707716afcb620c09f214 100644 (file)
@@ -42,7 +42,7 @@ class DashboardException(Exception):
     def code(self):
         if self._code:
             return str(self._code)
-        return str(abs(self.errno))
+        return str(abs(self.errno)) if self.errno is not None else 'Error'
 
 
 # access control module exceptions
index d3c54ce6c2b1e5fb4c04c7e68b7304ccf669d631..33027eff625b414a057cb41ee191c8a6db7cf009 100644 (file)
@@ -59,4 +59,4 @@ class DashboardPluginManager(object):
 PLUGIN_MANAGER = DashboardPluginManager("ceph-mgr.dashboard")
 
 # Load all interfaces and their hooks
-from . import interfaces
+from . import interfaces  # pylint: disable=wrong-import-position,cyclic-import
index 7edbfa8f6adafb7f8e0b5d36072b7ae1c00faf12..ac8ebcfd2890ef2e93b7ac0555addda98a435fa6 100644 (file)
@@ -44,6 +44,7 @@ class Actions(Enum):
     STATUS = 'status'
 
 
+# pylint: disable=too-many-ancestors
 @PM.add_plugin
 class FeatureToggles(I.CanMgr, I.CanLog, I.Setupable, I.HasOptions,
                      I.HasCommands, I.FilterRequest.BeforeHandler,
@@ -54,6 +55,7 @@ class FeatureToggles(I.CanMgr, I.CanLog, I.Setupable, I.HasOptions,
 
     @PM.add_hook
     def setup(self):
+        # pylint: disable=attribute-defined-outside-init
         self.Controller2Feature = {
             controller: feature
             for feature, controllers in Feature2Controller.items()
@@ -132,8 +134,9 @@ class FeatureToggles(I.CanMgr, I.CanLog, I.Setupable, I.HasOptions,
         @ApiController('/feature_toggles')
         class FeatureTogglesEndpoint(RESTController):
 
-            def list(_):
+            def list(_):  # pylint: disable=no-self-argument
                 return {
+                    # pylint: disable=protected-access
                     feature.value: self._is_feature_enabled(feature)
                     for feature in Features
                 }
index e990b5abce92f052e45faee35ca3b0475fa9cab1..3ef41fd331272233c117b2d14bfcbcbbeff463e6 100644 (file)
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-from . import PLUGIN_MANAGER as PM, Interface
+from . import PLUGIN_MANAGER as PM, Interface  # pylint: disable=cyclic-import
 
 
 class CanMgr(Interface):
@@ -22,29 +22,32 @@ class Setupable(Interface):
         Placeholder for plugin setup, right after server start.
         CanMgr.mgr and CanLog.log are initialized by then.
         """
-        pass
 
 
 @PM.add_interface
 class HasOptions(Interface):
     @PM.add_abcspec
-    def get_options(self): pass
+    def get_options(self):
+        pass
 
 
 @PM.add_interface
 class HasCommands(Interface):
     @PM.add_abcspec
-    def register_commands(self): pass
+    def register_commands(self):
+        pass
 
 
 @PM.add_interface
 class HasControllers(Interface):
     @PM.add_abcspec
-    def get_controllers(self): pass
+    def get_controllers(self):
+        pass
 
 
-class FilterRequest:
+class FilterRequest(object):
     @PM.add_interface
     class BeforeHandler(Interface):
         @PM.add_abcspec
-        def filter_request_before_handler(self, request): pass
+        def filter_request_before_handler(self, request):
+            pass
index 19ad1b85062d34afa29c02dd0af5b78225a8179f..f5f300171c23d849b2c2e60526326a3391f01954 100644 (file)
@@ -19,12 +19,9 @@ def lru_cache(maxsize=128, typed=False):
         cache = OrderedDict()
         stats = [0, 0]
         rlock = RLock()
-        setattr(
-            function,
-            'cache_info',
-            lambda:
-            "hits={}, misses={}, maxsize={}, currsize={}".format(
-                stats[0], stats[1], maxsize, len(cache)))
+        setattr(function, 'cache_info', lambda:
+                "hits={}, misses={}, maxsize={}, currsize={}".format(
+                    stats[0], stats[1], maxsize, len(cache)))
 
         @wraps(function)
         def wrapper(*args, **kwargs):
index 7517e6d65829cfc991bc646009c19d0bf3d6d3ba..844103883fdea2f8df7cf0e000715644e81b4b7b 100644 (file)
@@ -21,8 +21,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
-"""
-
+"""\
 """
 CAVEAT:
 This is a minimal implementation of python-pluggy (based on 0.8.0 interface:
@@ -103,9 +102,9 @@ class PluginManager(object):
 
     def add_hookspecs(self, module_or_class):
         """ Dummy method"""
-        pass
 
-    def register(self, plugin, name=None):
+    def register(self, plugin, name=None):  # pylint: disable=unused-argument
         for attr in dir(plugin):
             if self.parse_hookimpl_opts(plugin, attr) is not None:
+                # pylint: disable=protected-access
                 self.hook._add_hookimpl(attr, getattr(plugin, attr))
index a0698542b43a82219612cdce23ec30f195346470..4677e20dfbf151ba56d9093be5e79ce8c4e67cdf 100644 (file)
@@ -19,12 +19,9 @@ def ttl_cache(ttl, maxsize=128, typed=False):
         cache = OrderedDict()
         stats = [0, 0, 0]
         rlock = RLock()
-        setattr(
-            function,
-            'cache_info',
-            lambda:
-            "hits={}, misses={}, expired={}, maxsize={}, currsize={}".format(
-                stats[0], stats[1], stats[2], maxsize, len(cache)))
+        setattr(function, 'cache_info', lambda:
+                "hits={}, misses={}, expired={}, maxsize={}, currsize={}".format(
+                    stats[0], stats[1], stats[2], maxsize, len(cache)))
 
         @wraps(function)
         def wrapper(*args, **kwargs):
index be5967394d1e946a1a5864f4852f6a4770479eb2..ee94626d79afb916dad52f28f8643cac1f34bf93 100644 (file)
@@ -69,17 +69,37 @@ class JwtManager(object):
         return None
 
     @classmethod
-    def set_user(cls, token):
-        cls.LOCAL_USER.username = token['username']
+    def set_user(cls, username):
+        cls.LOCAL_USER.username = username
 
     @classmethod
     def reset_user(cls):
-        cls.set_user({'username': None, 'permissions': None})
+        cls.set_user(None)
 
     @classmethod
     def get_username(cls):
         return getattr(cls.LOCAL_USER, 'username', None)
 
+    @classmethod
+    def get_user(cls, token):
+        try:
+            dtoken = JwtManager.decode_token(token)
+            if not JwtManager.is_blacklisted(dtoken['jti']):
+                user = AuthManager.get_user(dtoken['username'])
+                if user.lastUpdate <= dtoken['iat']:
+                    return user
+                logger.debug("AMT: user info changed after token was issued, iat=%s lastUpdate=%s",
+                             dtoken['iat'], user.lastUpdate)
+            else:
+                logger.debug('AMT: Token is black-listed')
+        except jwt.exceptions.ExpiredSignatureError:
+            logger.debug("AMT: Token has expired")
+        except jwt.exceptions.InvalidTokenError:
+            logger.debug("AMT: Failed to decode token")
+        except UserDoesNotExist:
+            logger.debug("AMT: Invalid token: user %s does not exist", dtoken['username'])
+        return None
+
     @classmethod
     def blacklist_token(cls, token):
         token = jwt.decode(token, verify=False)
@@ -139,40 +159,23 @@ class AuthManagerTool(cherrypy.Tool):
         token = JwtManager.get_token_from_header()
         logger.debug("AMT: token: %s", token)
         if token:
-            try:
-                token = JwtManager.decode_token(token)
-                if not JwtManager.is_blacklisted(token['jti']):
-                    user = AuthManager.get_user(token['username'])
-                    if user.lastUpdate <= token['iat']:
-                        self._check_authorization(token)
-                        return
-
-                    logger.debug("AMT: user info changed after token was"
-                                 " issued, iat=%s lastUpdate=%s",
-                                 token['iat'], user.lastUpdate)
-                else:
-                    logger.debug('AMT: Token is black-listed')
-            except jwt.exceptions.ExpiredSignatureError:
-                logger.debug("AMT: Token has expired")
-            except jwt.exceptions.InvalidTokenError:
-                logger.debug("AMT: Failed to decode token")
-            except UserDoesNotExist:
-                logger.debug("AMT: Invalid token: user %s does not exist",
-                             token['username'])
-
+            user = JwtManager.get_user(token)
+            if user:
+                self._check_authorization(user.username)
+                return
         logger.debug('AMT: Unauthorized access to %s',
                      cherrypy.url(relative='server'))
         raise cherrypy.HTTPError(401, 'You are not authorized to access '
                                       'that resource')
 
-    def _check_authorization(self, token):
+    def _check_authorization(self, username):
         logger.debug("AMT: checking authorization...")
-        username = token['username']
+        username = username
         handler = cherrypy.request.handler.callable
         controller = handler.__self__
         sec_scope = getattr(controller, '_security_scope', None)
         sec_perms = getattr(handler, '_security_permissions', None)
-        JwtManager.set_user(token)
+        JwtManager.set_user(username)
 
         if not sec_scope:
             # controller does not define any authorization restrictions
index 2c96bab16fd101c73084c73012af014ef58cca44..e36628224ac24308caa284ef422df4f0f49b4d03 100644 (file)
@@ -167,11 +167,11 @@ class CephService(object):
                                                                                     kwargs)
             logger.error(msg)
             raise SendCommandError(outs, prefix, argdict, r)
-        else:
-            try:
-                return json.loads(outb)
-            except Exception:  # pylint: disable=broad-except
-                return outb
+
+        try:
+            return json.loads(outb)
+        except Exception:  # pylint: disable=broad-except
+            return outb
 
     @classmethod
     def get_rates(cls, svc_type, svc_name, path):
index 4a525a54da7a6866d127c8681695542d3b624179..e8b96cd6ba20e8192d5fc3b855c626dbd620e51b 100644 (file)
@@ -429,8 +429,7 @@ class RGWFSal(FSal):
                 raise NFSException('Cannot create bucket "{}" as it already '
                                    'exists, and belongs to other user.'
                                    .format(path))
-            else:
-                raise exp
+            raise exp
         if not exists:
             logger.info('Creating new RGW bucket "%s" for user "%s"', path,
                         self.rgw_user_id)
index bb21e0abed8668e6da59117eb6e233577db670c2..65df6e63b748cde96bbb3cf5a953adf8be1031c8 100644 (file)
@@ -25,7 +25,7 @@ def format_bitmask(features):
     """
     Formats the bitmask:
 
-    >>> format_bitmask(45)
+    @DISABLEDOCTEST: >>> format_bitmask(45)
     ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']
     """
     names = [val for key, val in RBD_FEATURES_NAME_MAPPING.items()
@@ -37,13 +37,14 @@ def format_features(features):
     """
     Converts the features list to bitmask:
 
-    >>> format_features(['deep-flatten', 'exclusive-lock', 'layering', 'object-map'])
+    @DISABLEDOCTEST: >>> format_features(['deep-flatten', 'exclusive-lock',
+        'layering', 'object-map'])
     45
 
-    >>> format_features(None) is None
+    @DISABLEDOCTEST: >>> format_features(None) is None
     True
 
-    >>> format_features('deep-flatten, exclusive-lock')
+    @DISABLEDOCTEST: >>> format_features('deep-flatten, exclusive-lock')
     32
     """
     if isinstance(features, six.string_types):
index dddcac4e2e299f1835787110f430dc293df37fd7..49adbb55c80e2eaeae7e4099113ed5f4a0c0cd85 100644 (file)
@@ -201,13 +201,13 @@ class ControllerTestCase(helper.CPWebCase):
             elif method == 'DELETE':
                 self.status = '204 No Content'
             return
+
+        if 'status' in thread.res_task['exception']:
+            self.status = thread.res_task['exception']['status']
         else:
-            if 'status' in thread.res_task['exception']:
-                self.status = thread.res_task['exception']['status']
-            else:
-                self.status = 500
-            self.body = json.dumps(thread.res_task['exception'])
-            return
+            self.status = 500
+        self.body = json.dumps(thread.res_task['exception'])
+        return
 
     def _task_post(self, url, data=None, timeout=60):
         self._task_request('POST', url, data, timeout)