]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
pybind/mgr/selftest: add "mgr self-test eval" command
authorKefu Chai <kchai@redhat.com>
Sat, 29 May 2021 17:10:25 +0000 (01:10 +0800)
committerKefu Chai <kchai@redhat.com>
Tue, 1 Jun 2021 03:03:10 +0000 (11:03 +0800)
and a simple REPL client allowing developer to peek and poke the
selftest module. if this turns out to be useful, we can promote this
method into a dedicated mix-in class, so other module can use it if
developer wants to test it manually.

Signed-off-by: Kefu Chai <kchai@redhat.com>
src/pybind/ceph_mgr_repl.py [new file with mode: 0755]
src/pybind/mgr/selftest/module.py

diff --git a/src/pybind/ceph_mgr_repl.py b/src/pybind/ceph_mgr_repl.py
new file mode 100755 (executable)
index 0000000..ea35719
--- /dev/null
@@ -0,0 +1,131 @@
+#!/usr/bin/python3
+# -*- mode:python -*-
+# vim: ts=4 sw=4 smarttab expandtab
+
+__all__ = ['ConsoleOptions', 'MgrModuleInterpreter']
+
+import readline
+import sys
+from code import InteractiveConsole
+from collections import namedtuple
+from pathlib import Path
+
+from ceph_argparse import json_command
+
+
+ConsoleOptions = namedtuple('ConsoleOptions',
+                            ['name', 'conffile', 'prefix', 'timeout'])
+
+
+class MgrModuleInteractiveConsole(InteractiveConsole):
+    def __init__(self, rados, opt, filename="<console>"):
+        super().__init__(filename)
+        self.cmd_prefix = opt.prefix
+        self.timeout = opt.timeout
+        self.cluster = rados.Rados(name=opt.name,
+                                   conffile=opt.conffile)
+        self.cluster.connect(timeout=opt.timeout)
+
+    def _do_runsource(self, source):
+        ret, buf, s = json_command(self.cluster,
+                                   prefix=self.cmd_prefix,
+                                   target=('mon-mgr',),
+                                   inbuf=source.encode(),
+                                   timeout=self.timeout)
+        if ret == 0:
+            # TODO: better way to encode the outputs
+            sys.stdout.write(buf.decode())
+            sys.stderr.write(s)
+        else:
+            # needs more
+            self.write("the input is not complete")
+
+    def runsource(self, source, filename='<input>', symbol='single'):
+        try:
+            # just validate the syntax
+            code = self.compile(source, filename, symbol)
+        except (OverflowError, SyntaxError, ValueError):
+            # Case 1
+            self.showsyntaxerror(filename)
+            return False
+
+        if code is None:
+            # Case 2
+            return True
+
+        # Case 3
+        self._do_runsource(source)
+        return False
+
+    def runcode(self, code):
+        # code object cannot be pickled
+        raise NotImplementedError()
+
+
+def show_env():
+    prog = Path(__file__).resolve()
+    ceph_dir = prog.parents[2]
+    python_path = ':'.join([f'{ceph_dir}/src/pybind',
+                            f'{ceph_dir}/build/lib/cython_modules/lib.3',
+                            f'{ceph_dir}/src/python-common',
+                            '$PYTHONPATH'])
+    ld_library_path = ':'.join([f'{ceph_dir}/build/lib',
+                                '$LD_LIBRARY_PATH'])
+    return f'''
+    $ export PYTHONPATH={python_path}
+    $ export LD_LIBRARY_PATH={ld_library_path}'''.strip('\n')
+
+
+def main():
+    import argparse
+    try:
+        import rados
+    except ImportError:
+        print(f'''Unable to import rados python binding.
+Please set the environment variables first:
+{show_env()}''',
+              file=sys.stderr)
+        exit(1)
+
+    prog = Path(__file__).name
+    epilog = f'''Usage:
+    {prog} -c "print(mgr.release_name)"'''
+    parser = argparse.ArgumentParser(epilog=epilog)
+    parser.add_argument('--name', action='store',
+                        default='client.admin',
+                        help='user name for connecting to cluster')
+    parser.add_argument('--conffile', action='store',
+                        default=rados.Rados.DEFAULT_CONF_FILES,
+                        help='path to ceph.conf')
+    parser.add_argument('--prefix', action='store',
+                        default='mgr self-test eval',
+                        help='command prefix for eval the source')
+    parser.add_argument('--timeout', action='store',
+                        default=10,
+                        help='timeout in seconds')
+    parser.add_argument('--show-env', action='store_true',
+                        help='show instructions to set environment variables')
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument('-c', action='store',
+                       help='optional statement',
+                       dest='command')
+    group.add_argument('script', nargs='?', type=argparse.FileType('r'))
+    args = parser.parse_args()
+    options = ConsoleOptions(name=args.name,
+                             conffile=args.conffile,
+                             prefix=args.prefix,
+                             timeout=args.timeout)
+    console = MgrModuleInteractiveConsole(rados, options)
+    if args.show_env:
+        print(show_env())
+    elif args.command:
+        console.runsource(args.command)
+    elif args.script:
+        console.runsource(args.script.read())
+    else:
+        sys.ps1 = f'[{args.prefix}] >>> '
+        console.interact()
+
+
+if __name__ == '__main__':
+    main()
index 7cf44fdcdf5099b0b2f0dd5bae79eafb9e45e5ca..e9e6ca4159c7563d28a5c329cef36ef328e9c2e6 100644 (file)
@@ -1,10 +1,13 @@
 
-from mgr_module import MgrModule, CommandResult, CLICommand, Option
+from mgr_module import MgrModule, CommandResult, HandleCommandResult, CLICommand, Option
 import enum
 import json
 import random
 import sys
 import threading
+from code import InteractiveInterpreter
+from contextlib import redirect_stderr, redirect_stdout
+from io import StringIO
 from typing import Any, Dict, List, Optional, Tuple
 
 
@@ -60,6 +63,7 @@ class Module(MgrModule):
         self._event = threading.Event()
         self._workload: Optional[Workload] = None
         self._health: Dict[str, Dict[str, Any]] = {}
+        self._repl = InteractiveInterpreter(dict(mgr=self))
 
     @CLICommand('mgr self-test python-version', perm='r')
     def python_version(self) -> Tuple[int, str, str]:
@@ -464,6 +468,31 @@ class Module(MgrModule):
         self._event.clear()
         self.log.info("Ended command_spam workload...")
 
+    @CLICommand('mgr self-test eval')
+    def eval(self,
+             s: Optional[str] = None,
+             inbuf: Optional[str] = None) -> HandleCommandResult:
+        '''
+        eval given source
+        '''
+        source = s or inbuf
+        if source is None:
+            return HandleCommandResult(-1, '', 'source is not specified')
+
+        err = StringIO()
+        out = StringIO()
+        with redirect_stderr(err), redirect_stdout(out):
+            needs_more = self._repl.runsource(source)
+            if needs_more:
+                retval = 2
+                stdout = ''
+                stderr = ''
+            else:
+                retval = 0
+                stdout = out.getvalue()
+                stderr = err.getvalue()
+            return HandleCommandResult(retval, stdout, stderr)
+
     def serve(self) -> None:
         while True:
             if self._workload == Workload.COMMAND_SPAM: