URL: https://github.com/SSSD/sssd/pull/5913
Author: justin-stephenson
 Title: #5913: Analyzer: Remove python-click dependency
Action: opened

PR body:
"""
As python-click will not be in RHEL9, switch to using the builtin argparse 
python module.
"""

To pull the PR as Git branch:
git remote add ghsssd https://github.com/SSSD/sssd
git fetch ghsssd pull/5913/head:pr5913
git checkout pr5913
From 9ba686c5c807a4b712bde0c41a2a4e13e24ed01e Mon Sep 17 00:00:00 2001
From: Justin Stephenson <jstep...@redhat.com>
Date: Tue, 7 Dec 2021 10:21:36 -0500
Subject: [PATCH] Analyzer: Remove python-click dependency

As python-click will not be in RHEL9, switch to using the builtin
argparse python module.
---
 contrib/sssd.spec.in                  |   3 +-
 src/tools/analyzer/modules/request.py | 129 +++++++++++---------
 src/tools/analyzer/sss_analyze.py     | 167 +++++++++++++++++++++++---
 3 files changed, 227 insertions(+), 72 deletions(-)

diff --git a/contrib/sssd.spec.in b/contrib/sssd.spec.in
index 7f75b1b5a3..d327143576 100644
--- a/contrib/sssd.spec.in
+++ b/contrib/sssd.spec.in
@@ -221,9 +221,8 @@ Requires: sssd-common = %{version}-%{release}
 Requires: python3-sss = %{version}-%{release}
 Requires: python3-sssdconfig = %{version}-%{release}
 Requires: libsss_certmap = %{version}-%{release}
-# required by sss_analyze
+# for logger=journald support with sss_analyze
 Requires: python3-systemd
-Requires: python3-click
 Recommends: sssd-dbus
 
 %description tools
diff --git a/src/tools/analyzer/modules/request.py b/src/tools/analyzer/modules/request.py
index 098a9197bb..ff9592e308 100644
--- a/src/tools/analyzer/modules/request.py
+++ b/src/tools/analyzer/modules/request.py
@@ -1,67 +1,91 @@
 import re
 import copy
-import click
 import logging
+import argparse
 
 from enum import Enum
 from source_files import Files
 from source_journald import Journald
+from sss_analyze import SubparsersAction
+from sss_analyze import Option
+from sss_analyze import Analyzer
 
 logger = logging.getLogger()
 
 
-@click.group(help="Request module")
-def request():
-    pass
-
-
-@request.command()
-@click.option("-v", "--verbose", is_flag=True, help="Enables verbose output")
-@click.option("--pam", is_flag=True, help="Filter only PAM requests")
-@click.pass_obj
-def list(ctx, verbose, pam):
-    analyzer = RequestAnalyzer()
-    source = analyzer.load(ctx)
-    analyzer.list_requests(source, verbose, pam)
-
-
-@request.command()
-@click.argument("cid", nargs=1, type=int, required=True)
-@click.option("--merge", is_flag=True, help="Merge logs together sorted"
-              " by timestamp (requires debug_microseconds = True)")
-@click.option("--cachereq", is_flag=True, help="Include cache request "
-              "related logs")
-@click.option("--pam", is_flag=True, help="Track only PAM requests")
-@click.pass_obj
-def show(ctx, cid, merge, cachereq, pam):
-    analyzer = RequestAnalyzer()
-    source = analyzer.load(ctx)
-    analyzer.track_request(source, cid, merge, cachereq, pam)
-
-
 class RequestAnalyzer:
     """
     A request analyzer module, handles request tracking logic
     and analysis. Parses input generated from a source Reader.
     """
+    module_parser = None
     consumed_logs = []
     done = ""
+    list_opts = [
+            Option('--verbose', 'Verbose output', bool, '-v'),
+            Option('--pam', 'Filter only PAM requests', bool),
+    ]
+
+    show_opts = [
+            Option('cid', 'Track request with this ID', int),
+            Option('--cachereq', 'Include cache request logs', bool),
+            Option('--merge', 'Merge logs together sorted by timestamp', bool),
+            Option('--pam', 'Track only PAM requests', bool),
+    ]
+
+    def print_module_help(self, args):
+        """
+        Print the module parser help output
+
+        Args:
+            args (Namespace): argparse parsed arguments
+        """
+        self.module_parser.print_help()
+
+    def setup_args(self, parser_grp):
+        """
+        Setup module parser, subcommands, and options
 
-    def load(self, ctx):
+        Args:
+            parser_grp (argparse.Action): Parser group to nest
+               module and subcommands under
+        """
+        desc = "Analyze request tracking module"
+        self.module_parser = parser_grp.add_parser('request',
+                                                   description=desc,
+                                                   help='Request tracking')
+
+        subparser = self.module_parser.add_subparsers(title=None,
+                                                      dest='subparser',
+                                                      action=SubparsersAction,
+                                                      metavar='COMMANDS')
+
+        cli = Analyzer()
+        subcmd_grp = subparser.add_parser_group('Operation Modes')
+        cli.add_subcommand(subcmd_grp, 'list', 'List recent requests',
+                           self.list_requests, self.list_opts)
+        cli.add_subcommand(subcmd_grp, 'show', 'Track individual request ID',
+                           self.track_request, self.show_opts)
+
+        self.module_parser.set_defaults(func=self.print_module_help)
+
+        return self.module_parser
+
+    def load(self, args):
         """
         Load the appropriate source reader.
 
         Args:
-            ctx (click.ctx): command line state object
+            args (Namespace): argparse parsed arguments
 
         Returns:
             Instantiated source object
         """
-        if ctx.source == "journald":
+        if args.source == "journald":
             import source_journald
             source = Journald()
         else:
-            source = Files(ctx.logdir)
+            source = Files(args.logdir)
         return source
 
     def matched_line(self, source, patterns):
@@ -184,25 +208,21 @@ def print_formatted(self, line, verbose):
                     print("       - " + id)
                     self.done = cr
 
-    def list_requests(self, source, verbose, pam):
+    def list_requests(self, args):
         """
         List component (default: NSS) responder requests
 
         Args:
-            line (str): line to process
-            source (Reader): source Reader object
-            verbose (bool): True if --verbose cli option is provided, enables
-                verbose output
-            pam (bool): True if --pam cli option is provided, list requests
-                in the PAM responder only
+            args (Namespace):  populated argparse namespace
         """
+        source = self.load(args)
         component = source.Component.NSS
         resp = "nss"
         patterns = ['\[cmd']
         patterns.append("(cache_req_send|cache_req_process_input|"
                         "cache_req_search_send)")
         consume = True
-        if pam:
+        if args.pam:
             component = source.Component.PAM
             resp = "pam"
 
@@ -214,20 +234,17 @@ def list_requests(self, source, verbose, pam):
             if isinstance(source, Journald):
                 print(line)
             else:
-                self.print_formatted(line, verbose)
+                self.print_formatted(line, args.verbose)
 
-    def track_request(self, source, cid, merge, cachereq, pam):
+    def track_request(self, args):
         """
         Print Logs pertaining to individual SSSD client request
 
         Args:
-            source (Reader): source Reader object
-            cid (int): client ID number to show
-            merge (bool): True when --merge is provided, merge logs together
-                by timestamp
-            pam (bool): True if --pam cli option is provided, track requests
-                in the PAM responder
+            args (Namespace):  populated argparse namespace
         """
+        source = self.load(args)
+        cid = args.cid
         resp_results = False
         be_results = False
         component = source.Component.NSS
@@ -235,7 +252,7 @@ def track_request(self, source, cid, merge, cachereq, pam):
         pattern = [f'REQ_TRACE.*\[CID #{cid}\\]']
         pattern.append(f"\[CID #{cid}\\].*connected")
 
-        if pam:
+        if args.pam:
             component = source.Component.PAM
             resp = "pam"
             pam_data_regex = f'pam_print_data.*\[CID #{cid}\]'
@@ -243,13 +260,13 @@ def track_request(self, source, cid, merge, cachereq, pam):
         logger.info(f"******** Checking {resp} responder for Client ID"
                     f" {cid} *******")
         source.set_component(component)
-        if cachereq:
+        if args.cachereq:
             cr_id_regex = 'CR #[0-9]+'
             cr_ids = self.get_linked_ids(source, pattern, cr_id_regex)
             [pattern.append(f'{id}\:') for id in cr_ids]
 
         for match in self.matched_line(source, pattern):
-            resp_results = self.consume_line(match, source, merge)
+            resp_results = self.consume_line(match, source, args.merge)
 
         logger.info(f"********* Checking Backend for Client ID {cid} ********")
         pattern = [f'REQ_TRACE.*\[sssd.{resp} CID #{cid}\]']
@@ -261,12 +278,12 @@ def track_request(self, source, cid, merge, cachereq, pam):
         pattern.clear()
         [pattern.append(f'\\{id}') for id in be_ids]
 
-        if pam:
+        if args.pam:
             pattern.append(pam_data_regex)
         for match in self.matched_line(source, pattern):
-            be_results = self.consume_line(match, source, merge)
+            be_results = self.consume_line(match, source, args.merge)
 
-        if merge:
+        if args.merge:
             # sort by date/timestamp
             sorted_list = sorted(self.consumed_logs,
                                  key=lambda s: s.split(')')[0])
diff --git a/src/tools/analyzer/sss_analyze.py b/src/tools/analyzer/sss_analyze.py
index 89684a3f75..3063cc0af9 100755
--- a/src/tools/analyzer/sss_analyze.py
+++ b/src/tools/analyzer/sss_analyze.py
@@ -1,27 +1,166 @@
 #!/usr/bin/env python
 
-import click
+import argparse
 
 import source_files
 
 from modules import request
 
 
-class Analyzer(object):
-    def __init__(self, source="files", logdir="/var/log/sssd/"):
-        self.source = source
-        self.logdir = logdir
+# Based on patch from https://bugs.python.org/issue9341
+class SubparsersAction(argparse._SubParsersAction):
+    """
+    Provide a subparser action that can create subparsers with ability of
+    grouping arguments.
 
+    It is based on the patch from:
 
-@click.group(help="Analyzer tool to assist with SSSD Log parsing")
-@click.option('--source', default='files')
-@click.option('--logdir', default='/var/log/sssd/', help="SSSD Log directory "
-              "to parse log files from")
-@click.pass_context
-def cli(ctx, source, logdir):
-    ctx.obj = Analyzer(source, logdir)
+        - https://bugs.python.org/issue9341
+    """
+
+    class _PseudoGroup(argparse.Action):
+
+        def __init__(self, container, title):
+            super().__init__(option_strings=[], dest=title)
+            self.container = container
+            self._choices_actions = []
+
+        def add_parser(self, name, **kwargs):
+            # add the parser to the main Action, but move the pseudo action
+            # in the group's own list
+            parser = self.container.add_parser(name, **kwargs)
+            choice_action = self.container._choices_actions.pop()
+            self._choices_actions.append(choice_action)
+            return parser
+
+        def _get_subactions(self):
+            return self._choices_actions
+
+        def add_parser_group(self, title):
+            # the formatter can handle recursive subgroups
+            grp = SubparsersAction._PseudoGroup(self, title)
+            self._choices_actions.append(grp)
+            return grp
+
+    def add_parser_group(self, title):
+        """
+        Add new parser group.
+
+        :param title: Title.
+        :type title: str
+        :return: Parser group that can have additional parsers attached.
+        :rtype: ``argparse.Action`` extended with ``add_parser`` method
+        """
+        grp = self._PseudoGroup(self, title)
+        self._choices_actions.append(grp)
+        return grp
+
+
+class Option:
+    """
+    Group option attributes for command/subcommand options
+    """
+    def __init__(self, name, help_msg, opt_type, short_opt=None):
+        self.name = name
+        self.short_opt = short_opt
+        self.help_msg = help_msg
+        self.opt_type = opt_type
+
+
+class Analyzer:
+    def add_subcommand(self, subcmd_grp, name, help_msg, func, opts):
+        """
+        Add subcommand to existing subcommand group
+
+        Args:
+            name(str): Subcommand name
+            help_msg(str): Help message for subcommand
+            func(function): Function to call on execution
+            opts(list of Object()): List of Option objects to add to subcommand
+        """
+        # Create parser
+        req_parser = subcmd_grp.add_parser(name, help=help_msg)
+
+        # Add subcommand options
+        self._add_subcommand_options(req_parser, opts)
+
+        # Execute func() when argument is called
+        req_parser.set_defaults(func=func)
+
+    def _add_subcommand_options(self, parser, opts):
+        """
+        Add subcommand options to subcommand parser
+
+        Args:
+            parser(str): Subcommand group parser
+            opts(list of Object()): List of Option objects to add to subcommand
+        """
+        for opt in opts:
+            if opt.opt_type is bool:
+                if opt.short_opt is None:
+                    parser.add_argument(opt.name, help=opt.help_msg,
+                                        action='store_true')
+                else:
+                    parser.add_argument(opt.name, opt.short_opt,
+                                        help=opt.help_msg, action='store_true')
+            if opt.opt_type is int:
+                parser.add_argument(opt.name, help=opt.help_msg,
+                                    type=int)
+
+    def load_modules(self, parser, parser_grp):
+        """
+        Initialize analyzer modules from modules/*
+
+        Args:
+            parser (ArgumentParser): Base parser object
+            parser_grp (argparse.Action): Parser group that can have
+                additional parsers attached.
+        """
+        # Currently only the 'request' module exists
+        req = request.RequestAnalyzer()
+
+        module_parser = req.setup_args(parser_grp)
+
+    def setup_args(self):
+        """
+        Top-level argument setup function.
+        Setup analyzer argument parsers and subcommand parser/options.
+
+        Returns:
+            parser (ArgumentParser): Base parser object
+        """
+        # top level parser
+        formatter = argparse.RawTextHelpFormatter
+        parser = argparse.ArgumentParser(description='Analyzer tool to assist '
+                                         'with SSSD log parsing',
+                                         formatter_class=formatter)
+        parser.add_argument('--source', default='files', choices=['files',
+                            'journald'])
+        parser.add_argument('--logdir', default='/var/log/sssd/',
+                            help='SSSD Log directory to parse log files from')
+
+        # Modules parser group
+        subparser = parser.add_subparsers(title=None,
+                                          action=SubparsersAction,
+                                          metavar='COMMANDS')
+        parser_grp = subparser.add_parser_group('Modules')
+
+        # Load modules, subcommands are added in module.setup_args()
+        self.load_modules(parser, parser_grp)
+
+        return parser
+
+    def main(self):
+        parser = self.setup_args()
+        args = parser.parse_args()
+
+        if not hasattr(args, 'func'):
+            parser.print_help()
+            return 0
+
+        args.func(args)
 
 
 if __name__ == '__main__':
-    cli.add_command(request.request)
-    cli()
+    analyzer = Analyzer()
+    analyzer.main()
_______________________________________________
sssd-devel mailing list -- sssd-devel@lists.fedorahosted.org
To unsubscribe send an email to sssd-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: 
https://docs.fedoraproject.org/en-US/project/code-of-conduct/
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/sssd-devel@lists.fedorahosted.org
Do not reply to spam on the list, report it: 
https://pagure.io/fedora-infrastructure

Reply via email to