From patchwork Thu Sep 9 13:48:06 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Kinsella X-Patchwork-Id: 98438 X-Patchwork-Delegate: thomas@monjalon.net Return-Path: X-Original-To: patchwork@inbox.dpdk.org Delivered-To: patchwork@inbox.dpdk.org Received: from mails.dpdk.org (mails.dpdk.org [217.70.189.124]) by inbox.dpdk.org (Postfix) with ESMTP id 56E49A0547; Thu, 9 Sep 2021 15:50:41 +0200 (CEST) Received: from [217.70.189.124] (localhost [127.0.0.1]) by mails.dpdk.org (Postfix) with ESMTP id 3E49641149; Thu, 9 Sep 2021 15:50:32 +0200 (CEST) Received: from mga12.intel.com (mga12.intel.com [192.55.52.136]) by mails.dpdk.org (Postfix) with ESMTP id 8A4C241162 for ; Thu, 9 Sep 2021 15:50:30 +0200 (CEST) X-IronPort-AV: E=McAfee;i="6200,9189,10101"; a="200318423" X-IronPort-AV: E=Sophos;i="5.85,280,1624345200"; d="scan'208";a="200318423" Received: from orsmga004.jf.intel.com ([10.7.209.38]) by fmsmga106.fm.intel.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 09 Sep 2021 06:50:30 -0700 X-ExtLoop1: 1 X-IronPort-AV: E=Sophos;i="5.85,280,1624345200"; d="scan'208";a="580894512" Received: from silpixa00396680.ir.intel.com (HELO silpixa00396680.ger.corp.intel.com) ([10.237.223.54]) by orsmga004.jf.intel.com with ESMTP; 09 Sep 2021 06:50:27 -0700 From: Ray Kinsella To: dev@dpdk.org Cc: bruce.richardson@intel.com, stephen@networkplumber.org, ferruh.yigit@intel.com, thomas@monjalon.net, ktraynor@redhat.com, mdr@ashroe.eu, aconole@redhat.com, roy.fan.zhang@intel.com, arkadiuszx.kusztal@intel.com, gakhil@marvell.com Date: Thu, 9 Sep 2021 14:48:06 +0100 Message-Id: <20210909134808.1585777-3-mdr@ashroe.eu> X-Mailer: git-send-email 2.26.2 In-Reply-To: <20210909134808.1585777-1-mdr@ashroe.eu> References: <20210618163659.85933-1-mdr@ashroe.eu> <20210909134808.1585777-1-mdr@ashroe.eu> MIME-Version: 1.0 Subject: [dpdk-dev] [PATCH v13 2/4] devtools: script to send notifications of expired symbols X-BeenThere: dev@dpdk.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: DPDK patches and discussions List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@dpdk.org Sender: "dev" Use this script with the output of the DPDK symbol tool, to notify maintainers of expired symbols by email. You need to define the environment variable DPDK_GETMAINTAINER_PATH for this tool to work. Use terminal output to review the emails before sending. e.g. $ devtools/symbol-tool.py list-expired --format-output csv \ | DPDK_GETMAINTAINER_PATH=/get_maintainer.pl \ devtools/notify_expired_symbols.py --format-output terminal Then use email output to send the emails to the maintainers. e.g. $ devtools/symbol-tool.py list-expired --format-output csv \ | DPDK_GETMAINTAINER_PATH=/get_maintainer.pl \ devtools/notify_expired_symbols.py --format-output email \ --smtp-server --sender \ --password --cc Signed-off-by: Ray Kinsella --- devtools/notify-symbol-maintainers.py | 302 ++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100755 devtools/notify-symbol-maintainers.py diff --git a/devtools/notify-symbol-maintainers.py b/devtools/notify-symbol-maintainers.py new file mode 100755 index 0000000000..edf330f88b --- /dev/null +++ b/devtools/notify-symbol-maintainers.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2021 Intel Corporation +# pylint: disable=invalid-name +'''Tool to notify maintainers of expired symbols''' +import os +import smtplib +import ssl +import sys +import subprocess +import argparse +from argparse import RawTextHelpFormatter +import time +from email.message import EmailMessage +from pathlib import Path + +DESCRIPTION = ''' +Use this script with the output of the DPDK symbol tool, to notify maintainers +and contributors of expired symbols by email. You need to define the environment +variable DPDK_GETMAINTAINER_PATH for this tool to work. + +Use terminal output to review the emails before sending. +e.g. +$ devtools/symbol-tool.py list-expired --format-output csv \\ +| DPDK_GETMAINTAINER_PATH=/get_maintainer.pl \\ +{s} --format-output terminal + +Then use email output to send the emails to the maintainers. +e.g. +$ devtools/symbol-tool.py list-expired --format-output csv \\ +| DPDK_GETMAINTAINER_PATH=/get_maintainer.pl \\ +{s} --format-output email \\ +--smtp-server --sender --password \\ +--cc +''' # noqa: E501 + +EMAIL_TEMPLATE = '''Hi there, + +Please note the symbols listed below have expired. In line with the DPDK ABI +policy, they should be scheduled for removal, in the next DPDK release. + +For more information, please see the DPDK ABI Policy, section 3.5.3. +https://doc.dpdk.org/guides/contributing/abi_policy.html + +Thanks, + +The DPDK Symbol Bot + +''' # noqa: E501 + +ABI_POLICY = 'doc/guides/contributing/abi_policy.rst' +DPDK_GMP_ENV_VAR = 'DPDK_GETMAINTAINER_PATH' +MAINTAINERS = 'MAINTAINERS' +get_maintainer = ['devtools/get-maintainer.sh', + '--email', '-f'] + + +class EnvironException(Exception): + '''Subclass exception for Pylint\'s happiness.''' + + +def _die_on_exception(e): + '''Print an exception, and quit''' + + print('Fatal Error: ' + str(e)) + sys.exit() + + +def _check_get_maintainers_env(): + '''Check get maintainers scripts are setup''' + + if not Path(get_maintainer[0]).is_file(): + raise EnvironException('Cannot locate DPDK\'s get maintainers script, ' + ' usually at $' + get_maintainer[0] + '.') + + if DPDK_GMP_ENV_VAR not in os.environ: + raise EnvironException(DPDK_GMP_ENV_VAR + ' is not defined.') + + if not Path(os.environ[DPDK_GMP_ENV_VAR]).is_file(): + raise EnvironException('Cannot locate get maintainers script, usually' + ' at ' + DPDK_GMP_ENV_VAR + '.') + + +def _get_maintainers(libpath): + '''Get the maintainers for given library''' + + try: + _check_get_maintainers_env() + except EnvironException as e: + _die_on_exception(e) + + try: + cmd = get_maintainer + [libpath] + result = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True) + except subprocess.CalledProcessError as e: + _die_on_exception(e) + + if result is None: + return None + + email = result.stdout.decode('utf-8') + if email == '': + return None + + email = list(filter(None, email.split('\n'))) + return email + + +default_maintainers = _get_maintainers(ABI_POLICY) + \ + _get_maintainers(MAINTAINERS) + + +def get_maintainers(libpath): + '''Get the maintainers for given library''' + maintainers = _get_maintainers(libpath) + + if maintainers is None: + maintainers = default_maintainers + + return maintainers + + +def get_message(library, symbols, config): + '''Build email message from symbols, config and maintainers''' + contributors = {} + message = {} + maintainers = get_maintainers(library) + + if maintainers != default_maintainers: + message['CC'] = default_maintainers.copy() + + if 'CC' in config: + message.setdefault('CC', []).append(config['CC']) + + message['Subject'] = 'Expired symbols in {}\n'.format(library) + + body = EMAIL_TEMPLATE + body += '{:<50}{:<25}{:<25}\n'.format('Symbol', 'Contributor', 'Email') + for sym in symbols: + body += ('{:<50}{:<25}{:<25}\n'.format(sym, + symbols[sym]['name'], + symbols[sym]['email'])) + email = symbols[sym]['email'] + contributors[email] = '' + + contributors = list(contributors.keys()) + + message['To'] = maintainers + contributors + message['Body'] = body + + return message + + +class OutputEmail(): + '''Format the output for email''' + + def __init__(self, config): + self.config = config + + self.terminal = OutputTerminal(config) + context = ssl.create_default_context() + + # Try to log in to server and send email + try: + self.server = smtplib.SMTP(config['smtp_server'], 587) + self.server.starttls(context=context) # Secure the connection + self.server.login(config['sender'], config['password']) + except EnvironException as e: + _die_on_exception(e) + + def message(self, message): + '''send email''' + self.terminal.message(message) + + msg = EmailMessage() + msg.set_content(message.pop('Body')) + + for key in message.keys(): + msg[key] = message[key] + + msg['From'] = self.config['sender'] + msg['Reply-To'] = 'no-reply@dpdk.org' + + self.server.send_message(msg) + + time.sleep(1) + + def __del__(self): + self.server.quit() + + +class OutputTerminal(): # pylint: disable=too-few-public-methods + '''Format the output for the terminal''' + + def __init__(self, config): + self.config = config + + def message(self, message): + '''Print email to terminal''' + + terminal = 'To:' + ', '.join(message['To']) + '\n' + if 'sender' in self.config.keys(): + terminal += 'From:' + self.config['sender'] + '\n' + + terminal += 'Reply-To:' + 'no-reply@dpdk.org' + '\n' + + if 'CC' in message: + terminal += 'CC:' + ', '.join(message['CC']) + '\n' + + terminal += 'Subject:' + message['Subject'] + '\n' + terminal += 'Body:' + message['Body'] + '\n' + + print(terminal) + print('-' * 80) + + +def parse_config(args): + '''put the command line args in the right places''' + config = {} + error_msg = None + + outputs = { + None: OutputTerminal, + 'terminal': OutputTerminal, + 'email': OutputEmail + } + + if args.format_output == 'email': + if args.smtp_server is None: + error_msg = 'SMTP server' + else: + config['smtp_server'] = args.smtp_server + + if args.sender is None: + error_msg = 'sender' + else: + config['sender'] = args.sender + + if args.password is None: + error_msg = 'password' + else: + config['password'] = args.password + + if args.cc is not None: + config['CC'] = args.cc + + if error_msg is not None: + print('Please specify a {} for email output'.format(error_msg)) + return None + + config['output'] = outputs[args.format_output] + return config + + +def main(): + '''Main entry point''' + parser = \ + argparse.ArgumentParser(description=DESCRIPTION.format(s=__file__), + formatter_class=RawTextHelpFormatter) + parser.add_argument('--format-output', + choices=['terminal', 'email'], + default='terminal') + parser.add_argument('--smtp-server') + parser.add_argument('--password') + parser.add_argument('--sender') + parser.add_argument('--cc') + + args = parser.parse_args() + config = parse_config(args) + if config is None: + return + + symbols = {} + lastlib = library = '' + + output = config['output'](config) + + for line in sys.stdin: + line = line.rstrip('\n') + + if line.find('mapfile') >= 0: + continue + library, symbol, name, email = line.split(',') + + if library != lastlib: + message = get_message(lastlib, symbols, config) + output.message(message) + symbols = {} + + lastlib = library + symbols[symbol] = {'name': name, 'email': email} + + # print the last library + message = get_message(lastlib, symbols, config) + output.message(message) + + +if __name__ == '__main__': + main()