diff options
Diffstat (limited to 'src/tools/analyzer/modules/request.py')
-rw-r--r-- | src/tools/analyzer/modules/request.py | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/src/tools/analyzer/modules/request.py b/src/tools/analyzer/modules/request.py new file mode 100644 index 0000000..d661ddd --- /dev/null +++ b/src/tools/analyzer/modules/request.py @@ -0,0 +1,320 @@ +import re +import logging + +from sssd.parser import SubparsersAction +from sssd.parser import Option + +logger = logging.getLogger() + + +class RequestAnalyzer: + """ + A request analyzer module, handles request tracking logic + and analysis. Parses input generated from a source Reader. + """ + module_parser = None + consumed_logs = [] + 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('--child', 'Include child process 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, cli): + """ + Setup module parser, subcommands, and options + + 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') + + 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: + args (Namespace): argparse parsed arguments + + Returns: + Instantiated source object + """ + if args.source == "journald": + from sssd.source_journald import Journald + source = Journald() + else: + from sssd.source_files import Files + source = Files(args.logdir) + return source + + def matched_line(self, source, patterns): + """ + Yield lines which match any number of patterns (OR) in + provided patterns list. + + Args: + source (Reader): source Reader object + Yields: + lines matching the provided pattern(s) + """ + for line in source: + for pattern in patterns: + re_obj = re.compile(pattern) + if re_obj.search(line): + if line.startswith(' * '): + continue + yield line + + def get_linked_ids(self, source, pattern, regex): + """ + Retrieve list of associated REQ_TRACE ids. Filter + only source lines by pattern, then parse out the + linked id with the provided regex. + + Args: + source (Reader): source Reader object + pattern (list of str): regex pattern(s) used for finding + linked ids + regex (str): regular expression used to extract linked id + + Returns: + List of linked ids discovered + """ + linked_ids = [] + for match in self.matched_line(source, pattern): + id_re = re.compile(regex) + match = id_re.search(match) + if match: + found = match.group(0) + linked_ids.append(found) + return linked_ids + + def consume_line(self, line, source, consume): + """ + Print or consume a line, if merge cli option is provided then consume + boolean is set to True + + Args: + line (str): line to process + source (Reader): source Reader object + consume (bool): If True, line is added to consume_logs + list, otherwise print line + + Returns: + True if line was processed, otherwise False + """ + found_results = True + if consume: + self.consumed_logs.append(line.rstrip(line[-1])) + else: + # files source includes newline + if type(source).__name__ == 'Files': + print(line, end='') + else: + print(line) + return found_results + + def print_formatted_verbose(self, source): + """ + Parse log file and print formatted verbose list_requests output + + Args: + source (Reader): source Reader object + """ + data = {} + # collect cid log lines from single run through of parsing the log + # into dictionary # (cid, ts) -> logline_output + for line in source: + if "CID#" not in line: + continue + + # parse CID and ts from line, key is a tuple of (cid,ts) + fields = line.split("[") + # timestamp to the minute, cut off seconds, ms + ts = fields[0][:17] + result = re.search('CID#[0-9]*', fields[3]) + cid = result.group(0) + + # if mapping exists, append line to output. Otherwise create new mapping + if (cid, ts) in data.keys(): + data[(cid, ts)] += line + else: + data[(cid, ts)] = line + + # pretty print the data + for k, v in data.items(): + cr_done = [] + id_done = [] + for cidline in v.splitlines(): + plugin = "" + name = "" + id = "" + + # CR number + fields = cidline.split("[") + cr_field = fields[3][7:] + cr = cr_field.split(":")[0][4:] + # Client connected, top-level info line + if re.search(r'\[cmd', cidline): + self.print_formatted(cidline) + # CR Plugin name + if re.search("cache_req_send", cidline): + plugin = cidline.split('\'')[1] + id_done.clear() + # Extract CR number + fields = cidline.split("[") + cr_field = fields[3][7:] + cr = cr_field.split(":")[0][4:] + # CR Input name + elif re.search("cache_req_process_input", cidline): + name = cidline.rsplit('[')[-1] + # CR Input id + elif re.search("cache_req_search_send", cidline): + id = cidline.rsplit()[-1] + + if plugin: + print(" - " + plugin) + if name: + # Avoid duplicate output with the same CR # + if cr not in cr_done: + print(" - " + name[:-1]) + cr_done.append(cr) + if (id and ("UID" in cidline or "GID" in cidline)): + if id not in id_done and bool(re.search(r'\d', id)): + print(" - " + id) + id_done.append(id) + + def print_formatted(self, line): + """ + Parse line and print formatted list_requests output + + Args: + line (str): line to parse + Returns: + Client ID from printed line, 0 otherwise + """ + # exclude backtrace logs + if line.startswith(' * '): + return 0 + if "refreshed" in line: + return 0 + ts = line.split(")")[0] + ts = ts[1:] + fields = line.split("[") + cid = fields[3][4:-9] + cmd = fields[4][4:-1] + uid = fields[5][4:-1] + if not uid.isnumeric(): + uid = fields[6][4:-1] + print(f'{ts}: [uid {uid}] CID #{cid}: {cmd}') + return cid + + def list_requests(self, args): + """ + List component (default: NSS) responder requests + + Args: + args (Namespace): populated argparse namespace + """ + source = self.load(args) + component = source.Component.NSS + resp = "nss" + # Log messages matching the following regex patterns contain + # the useful info we need to produce list output + patterns = [r'\[cmd'] + if args.pam: + component = source.Component.PAM + resp = "pam" + + logger.info(f"******** Listing {resp} client requests ********") + source.set_component(component, False) + + if args.verbose: + self.print_formatted_verbose(source) + else: + for line in self.matched_line(source, patterns): + if type(source).__name__ == 'Journald': + print(line) + else: + self.print_formatted(line) + + def track_request(self, args): + """ + Print Logs pertaining to individual SSSD client request + + Args: + args (Namespace): populated argparse namespace + """ + source = self.load(args) + cid = args.cid + resp_results = False + be_results = False + component = source.Component.NSS + resp = "nss" + pattern = [rf"\[CID#{cid}\]"] + + if args.pam: + component = source.Component.PAM + resp = "pam" + + logger.info(f"******** Checking {resp} responder for Client ID" + f" {cid} *******") + source.set_component(component, args.child) + for match in self.matched_line(source, pattern): + resp_results = self.consume_line(match, source, args.merge) + + logger.info(f"********* Checking Backend for Client ID {cid} ********") + pattern = [rf'REQ_TRACE.*\[sssd.{resp} CID #{cid}\]'] + source.set_component(source.Component.BE, args.child) + + be_id_regex = r'\[RID#[0-9]+\]' + be_ids = self.get_linked_ids(source, pattern, be_id_regex) + + pattern.clear() + [pattern.append(f'\\{id}') for id in be_ids] + + for match in self.matched_line(source, pattern): + be_results = self.consume_line(match, source, args.merge) + + if args.merge: + # sort by date/timestamp + sorted_list = sorted(self.consumed_logs, + key=lambda s: s.split(')')[0]) + for entry in sorted_list: + print(entry) + if not resp_results and not be_results: + logger.warn(f"ID {cid} not found in logs!") |