Ray Kinsella <m...@ashroe.eu> writes:

> 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=<somewhere>/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=<somewhere>/get_maintainer.pl \
> devtools/notify_expired_symbols.py --format-output email \
> --smtp-server <server> --sender <some...@somewhere.com> \
> --password <password> --cc <some...@somewhere.com>
>
> Signed-off-by: Ray Kinsella <m...@ashroe.eu>
> ---
>  devtools/notify-symbol-maintainers.py | 256 ++++++++++++++++++++++++++
>  1 file changed, 256 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..ee554687ff
> --- /dev/null
> +++ b/devtools/notify-symbol-maintainers.py
> @@ -0,0 +1,256 @@
> +#!/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 smtplib
> +import ssl
> +import sys
> +import subprocess
> +import argparse
> +from argparse import RawTextHelpFormatter
> +import time
> +from email.message import EmailMessage
> +
> +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=<somewhere>/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=<somewhere>/get_maintainer.pl \\
> +{s} --format-output email \\
> +--smtp-server <server> --sender <some...@somewhere.com> --password 
> <password> \\
> +--cc <some...@somewhere.com>
> +'''
> +
> +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
> +
> +'''
> +
> +ABI_POLICY = 'doc/guides/contributing/abi_policy.rst'
> +MAINTAINERS = 'MAINTAINERS'
> +get_maintainer = ['devtools/get-maintainer.sh', \
> +                  '--email', '-f']

Maybe it's best to make this something that can be overridden.  There's
a series to change the .sh files to .py files.  Perhaps an environment
variable or argument?

> +def _get_maintainers(libpath):
> +    '''Get the maintainers for given library'''
> +    try:
> +        cmd = get_maintainer + [libpath]
> +        result = subprocess.run(cmd, \
> +                                stdout=subprocess.PIPE, \
> +                                stderr=subprocess.PIPE,
> +                                check=True)
> +    except subprocess.CalledProcessError:
> +        return None

You might consider handling

   except FileNotFoundError:
      ....

With a graceful exit and error message.  In case the get_maintainers
path changes.

> +    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 Exception as exception:
> +            print(exception)
> +            raise exception
> +
> +    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-re...@dpdk.org'
> +
> +        self.server.send_message(msg)
> +
> +        time.sleep(1)

Why this sleep is needed?

> +
> +    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-re...@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()

Reply via email to