338 lines
11 KiB
Python
338 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# Copyright (c) 2012 OpenStack, LLC.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
# This is designed to be called by a gerrit hook. It searched new
|
|
# patchsets for strings like "bug FOO" and updates corresponding Launchpad
|
|
# bugs status.
|
|
|
|
# You want to test this? I use a command line a bit like this:
|
|
# python notify_impact.py --change 55607 \
|
|
# --change-url https://review.openstack.org/55607 --project nova/ \
|
|
# --branch master --commit c262de4417d48be599c3a7496ef94de5c84b188c \
|
|
# --impact DocImpact --dest-address none@localhost --dryrun \
|
|
# --config foo.yaml \
|
|
# change-merged
|
|
#
|
|
# But you'll need a git repository at /home/gerrit2/review_site/git/nova.git
|
|
# for that to work
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import re
|
|
import smtplib
|
|
import subprocess
|
|
|
|
from email.mime import text
|
|
from launchpadlib import launchpad
|
|
from launchpadlib import uris
|
|
import yaml
|
|
|
|
from jeepyb import projects
|
|
|
|
|
|
logger = logging.getLogger('notify_impact')
|
|
|
|
DOC_TAG = "doc"
|
|
GERRIT_GIT_DIR = os.environ.get(
|
|
'GERRIT_GIT_DIR', '/home/gerrit2/review_site/git')
|
|
EMAIL_TEMPLATE = """
|
|
Hi, I'd like you to take a look at this patch for potential
|
|
%s.
|
|
%s
|
|
|
|
Log:
|
|
%s
|
|
"""
|
|
|
|
GERRIT_CACHE_DIR = os.path.expanduser(
|
|
os.environ.get('GERRIT_CACHE_DIR',
|
|
'~/.launchpadlib/cache'))
|
|
GERRIT_CREDENTIALS = os.path.expanduser(
|
|
os.environ.get('GERRIT_CREDENTIALS',
|
|
'~/.launchpadlib/creds'))
|
|
|
|
|
|
class BugActionsReal(object):
|
|
"""Things we do to bugs."""
|
|
|
|
def __init__(self, lpconn):
|
|
self.lpconn = lpconn
|
|
|
|
def create(self, project, bug_title, bug_descr, args):
|
|
# If the bug report is not targeting the 'openstack-manuals'
|
|
# project, add an extra doc tag to make the bug easier to
|
|
# look up.
|
|
lp_target_project = str(project).split('/')[-1]
|
|
tags = args.project.split('/')[1]
|
|
if lp_target_project != 'openstack-manuals':
|
|
tags = [tags, DOC_TAG]
|
|
|
|
buginfo = self.lpconn.bugs.createBug(
|
|
target=project, title=bug_title,
|
|
description=bug_descr, tags=tags)
|
|
buglink = buginfo.web_link
|
|
return buginfo, buglink
|
|
|
|
def subscribe(self, buginfo, subscriber):
|
|
user = self.lpconn.people[subscriber]
|
|
if user:
|
|
buginfo.subscribe(person=user)
|
|
|
|
|
|
class BugActionsDryRun(object):
|
|
def __init__(self, lpconn):
|
|
self.lpconn = lpconn
|
|
|
|
def create(self, project, bug_title, bug_descr, args):
|
|
print('I would have created a bug in %s, but I am in dry run mode.\n\n'
|
|
'Title: %s\n'
|
|
'Description:\n'
|
|
'%s' % (project, bug_title, bug_descr))
|
|
return None, None
|
|
|
|
def subscribe(self, buginfo, subscriber):
|
|
print('I would have added %s as a subscriber to the bug, '
|
|
'but I am in dry run mode' % subscriber)
|
|
|
|
|
|
def create_bug(git_log, args, config):
|
|
"""Create a bug for a change.
|
|
|
|
Create a launchpad bug in a LP project, titled with the first line of
|
|
the git commit message, with the content of the git_log prepended
|
|
with the Gerrit review URL. Tag the bug with the name of the repository
|
|
it came from. Returns link to the bug.
|
|
"""
|
|
|
|
# Determine what LP project to use
|
|
prelude = ('\nDear bug triager. This bug was created since a '
|
|
'commit was marked with DOCIMPACT.\n')
|
|
project_name = args.project.rstrip('/')
|
|
lp_project = projects.docimpact_target(project_name)
|
|
if lp_project == 'unknown':
|
|
|
|
prelude = ('\nDear bug triager. This bug was created since a '
|
|
'commit was marked with DOCIMPACT.\n'
|
|
'Your project "%s" is set up so that we directly report '
|
|
'the documentation bugs against it. If this needs '
|
|
'changing, the docimpact-group option needs to be added '
|
|
'for the project. You can ask the '
|
|
'OpenStack infra team (#openstack-infra on freenode) for '
|
|
'help if you need to.\n'
|
|
% args.project)
|
|
lp_project = project_name
|
|
|
|
lpconn = launchpad.Launchpad.login_with(
|
|
'Gerrit User Sync',
|
|
uris.LPNET_SERVICE_ROOT,
|
|
GERRIT_CACHE_DIR,
|
|
credentials_file=GERRIT_CREDENTIALS,
|
|
version='devel')
|
|
|
|
if args.dryrun:
|
|
actions = BugActionsDryRun(lpconn)
|
|
else:
|
|
actions = BugActionsReal(lpconn)
|
|
|
|
lines_in_log = git_log.split('\n')
|
|
bug_title = lines_in_log[4]
|
|
bug_descr = args.change_url + prelude + '\n' + git_log
|
|
project = lpconn.projects[lp_project]
|
|
|
|
buglink = None
|
|
author_class = None
|
|
|
|
buginfo, buglink = actions.create(project, bug_title, bug_descr, args)
|
|
logger.info('Created a bug in project %(project)s with title "%(title)s": '
|
|
'%(buglink)s'
|
|
% {'project': project,
|
|
'title': bug_title,
|
|
'buglink': buglink})
|
|
|
|
# If the author of the merging patch matches our configured
|
|
# subscriber lists, then subscribe the configured victims.
|
|
for email_address in config.get('author_map', {}):
|
|
email_re = re.compile('^Author:.*%s.*' % email_address)
|
|
for line in bug_descr.split('\n'):
|
|
m = email_re.match(line)
|
|
if m:
|
|
author_class = config['author_map'][email_address]
|
|
|
|
if author_class:
|
|
config = config.get('subscriber_map', {}).get(author_class, [])
|
|
for subscriber in config:
|
|
actions.subscribe(buginfo, subscriber)
|
|
logger.info('Subscribed %(subscriber)s to bug %(buglink)s'
|
|
% {'subscriber': subscriber,
|
|
'buglink': buglink})
|
|
|
|
return buglink
|
|
|
|
|
|
def smtp_connection(args):
|
|
"""Create SMTP connection based on command line arguments, falling
|
|
back to sensible defaults if no arguments are provided.
|
|
"""
|
|
conn = None
|
|
if args.smtp_ssl:
|
|
port = 465 if not args.smtp_port else args.smtp_port
|
|
conn = smtplib.SMTP_SSL(args.smtp_host, port)
|
|
else:
|
|
port = 25 if not args.smtp_port else args.smtp_port
|
|
conn = smtplib.SMTP(args.smtp_host, port)
|
|
|
|
if args.smtp_starttls:
|
|
conn.starttls()
|
|
conn.ehlo()
|
|
|
|
if args.smtp_user and args.smtp_pass:
|
|
conn.login(args.smtp_user, args.smtp_pass)
|
|
|
|
return conn
|
|
|
|
|
|
def process_impact(git_log, args, config):
|
|
"""Process DocImpact flag.
|
|
|
|
If the 'DocImpact' flag is present for a change that is merged,
|
|
create a new documentation bug in
|
|
the openstack-manuals launchpad project based on the git_log.
|
|
For non-documentation impacts at all states of merge
|
|
notify the mailing list of impact.
|
|
"""
|
|
if args.impact.lower() == 'docimpact':
|
|
if args.hook == "change-merged":
|
|
create_bug(git_log, args, config)
|
|
return
|
|
|
|
email_content = EMAIL_TEMPLATE % (args.impact,
|
|
args.change_url, git_log)
|
|
|
|
msg = text.MIMEText(email_content)
|
|
msg['Subject'] = '[%s] %s review request change %s' % \
|
|
(args.project, args.impact, args.change)
|
|
msg['From'] = args.smtp_from
|
|
msg['To'] = args.dest_address
|
|
|
|
s = smtp_connection(args)
|
|
s.sendmail(args.smtp_from, args.dest_address, msg.as_string())
|
|
s.quit()
|
|
|
|
|
|
def impacted(git_log, impact_string):
|
|
"""Determine if a changes log indicates there is an impact."""
|
|
return re.search(impact_string, git_log, re.IGNORECASE)
|
|
|
|
|
|
def extract_git_log(args):
|
|
"""Extract git log of all merged commits."""
|
|
cmd = ['git',
|
|
'--git-dir=' + GERRIT_GIT_DIR + '/' + args.project + '.git',
|
|
'log', '--no-merges', args.commit + '^1..' + args.commit]
|
|
return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('hook')
|
|
|
|
# common
|
|
parser.add_argument('--change', default=None)
|
|
parser.add_argument('--change-url', default=None)
|
|
parser.add_argument('--project', default=None)
|
|
parser.add_argument('--branch', default=None)
|
|
parser.add_argument('--commit', default=None)
|
|
parser.add_argument('--topic', default=None)
|
|
parser.add_argument('--change-owner', default=None)
|
|
|
|
# patchset-abandoned
|
|
parser.add_argument('--abandoner', default=None)
|
|
parser.add_argument('--reason', default=None)
|
|
|
|
# change-merged
|
|
parser.add_argument('--submitter', default=None)
|
|
parser.add_argument('--newrev', default=None)
|
|
|
|
# patchset-created
|
|
parser.add_argument('--uploader', default=None)
|
|
parser.add_argument('--patchset', default=None)
|
|
parser.add_argument('--is-draft', default=None)
|
|
parser.add_argument('--kind', default=None)
|
|
|
|
# Not passed by gerrit:
|
|
parser.add_argument('--impact', default=None)
|
|
parser.add_argument('--dest-address', default=None)
|
|
|
|
# Automatic config: config contains a mapping of email addresses to
|
|
# subscribers.
|
|
parser.add_argument('--config', type=argparse.FileType('r'),
|
|
default=None)
|
|
|
|
# Don't actually create the bug
|
|
parser.add_argument('--dryrun', dest='dryrun', action='store_true')
|
|
parser.add_argument('--no-dryrun', dest='dryrun', action='store_false')
|
|
parser.set_defaults(dryrun=False)
|
|
|
|
# SMTP configuration
|
|
parser.add_argument('--smtp-from', dest='smtp_from',
|
|
default='gerrit2@review.openstack.org')
|
|
|
|
parser.add_argument('--smtp-host', dest='smtp_host', default="localhost")
|
|
parser.add_argument('--smtp-port', dest='smtp_port')
|
|
|
|
parser.add_argument('--smtp-ssl', dest='smtp_ssl', action='store_true')
|
|
parser.add_argument('--smtp-starttls', dest='smtp_starttls',
|
|
action='store_true')
|
|
|
|
parser.add_argument('--smtp-user', dest='smtp_user',
|
|
default=os.getenv('SMTP_USER'))
|
|
parser.add_argument('--smtp-pass', dest='smtp_pass',
|
|
default=os.getenv('SMTP_PASS'))
|
|
|
|
args = parser.parse_args()
|
|
|
|
# NOTE(mikal): the basic idea here is to let people watch
|
|
# docimpact bugs filed by people of interest. For example
|
|
# my team's tech writer wants to be subscribed to all the
|
|
# docimpact bugs we create. The config for that would be
|
|
# something like:
|
|
#
|
|
# author_map:
|
|
# mikal@stillhq.com: rcbau
|
|
# grumpy@dwarves.com: rcbau
|
|
#
|
|
# subscriber_map:
|
|
# rcbau: ['mikalstill', 'grumpypants']
|
|
#
|
|
# Where the entries in the author map are email addresses
|
|
# to match in author lines, and the subscriber map is a
|
|
# list of launchpad user ids.
|
|
config = {}
|
|
if args.config:
|
|
config = yaml.load(args.config.read())
|
|
|
|
# Get git log
|
|
git_log = extract_git_log(args)
|
|
|
|
# Process impacts found in git log
|
|
if impacted(git_log, args.impact):
|
|
process_impact(git_log, args, config)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|