2014-10-08 14:31:55 +02:00
|
|
|
#!/usr/bin/python
|
2016-08-05 07:41:30 +02:00
|
|
|
# Copyright (c) 2014-2016 SUSE LLC
|
2014-10-08 14:31:55 +02:00
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
|
|
# in the Software without restriction, including without limitation the rights
|
|
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
#
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
# SOFTWARE.
|
|
|
|
|
|
|
|
from pprint import pprint
|
|
|
|
import os, sys, re
|
|
|
|
import logging
|
|
|
|
from optparse import OptionParser
|
|
|
|
import cmdln
|
2015-03-31 11:33:39 +02:00
|
|
|
from collections import namedtuple
|
2017-02-10 15:41:45 -06:00
|
|
|
from osclib.comments import CommentAPI
|
2015-05-28 13:06:10 +02:00
|
|
|
from osclib.memoize import memoize
|
2015-05-22 14:46:29 +02:00
|
|
|
import signal
|
|
|
|
import datetime
|
2016-12-23 13:59:17 +01:00
|
|
|
from collections import namedtuple
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
from xml.etree import cElementTree as ET
|
|
|
|
except ImportError:
|
|
|
|
import cElementTree as ET
|
|
|
|
|
|
|
|
import osc.conf
|
|
|
|
import osc.core
|
|
|
|
import urllib2
|
|
|
|
|
|
|
|
class ReviewBot(object):
|
|
|
|
"""
|
|
|
|
A generic obs request reviewer
|
|
|
|
Inherit from this class and implement check functions for each action type:
|
|
|
|
|
|
|
|
def check_action_<type>(self, req, action):
|
|
|
|
return (None|True|False)
|
|
|
|
"""
|
|
|
|
|
2015-05-13 13:27:44 +02:00
|
|
|
DEFAULT_REVIEW_MESSAGES = { 'accepted' : 'ok', 'declined': 'review failed' }
|
2016-08-05 07:41:30 +02:00
|
|
|
REVIEW_CHOICES = ('normal', 'no', 'accept', 'accept-onpass', 'fallback-onfail', 'fallback-always')
|
2015-05-13 13:27:44 +02:00
|
|
|
|
2017-02-10 15:41:45 -06:00
|
|
|
COMMENT_MARKER_REGEX = re.compile(r'<!-- (?P<bot>[^ ]+) state=(?P<state>[^ ]+)(?: result=(?P<result>[^ ]+))? -->')
|
|
|
|
|
2017-01-13 15:36:34 +01:00
|
|
|
# map of default config entries
|
|
|
|
config_defaults = {
|
2017-01-31 17:28:23 +01:00
|
|
|
# list of tuples (prefix, apiurl, submitrequestprefix)
|
2017-01-13 15:36:34 +01:00
|
|
|
# set this if the obs instance maps another instance into it's
|
|
|
|
# namespace
|
|
|
|
'project_namespace_api_map' : [
|
2017-01-31 17:28:23 +01:00
|
|
|
('openSUSE.org:', 'https://api.opensuse.org', 'obsrq'),
|
2017-01-13 15:36:34 +01:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
2015-03-27 15:01:45 +01:00
|
|
|
def __init__(self, apiurl = None, dryrun = False, logger = None, user = None, group = None):
|
2014-10-08 14:31:55 +02:00
|
|
|
self.apiurl = apiurl
|
2017-01-17 20:57:06 -06:00
|
|
|
self.ibs = apiurl.startswith('https://api.suse.de')
|
2014-10-08 14:31:55 +02:00
|
|
|
self.dryrun = dryrun
|
|
|
|
self.logger = logger
|
|
|
|
self.review_user = user
|
2015-03-27 15:01:45 +01:00
|
|
|
self.review_group = group
|
2014-10-08 14:31:55 +02:00
|
|
|
self.requests = []
|
2015-05-13 13:27:44 +02:00
|
|
|
self.review_messages = ReviewBot.DEFAULT_REVIEW_MESSAGES
|
2016-03-08 16:43:54 +01:00
|
|
|
self._review_mode = 'normal'
|
|
|
|
self.fallback_user = None
|
|
|
|
self.fallback_group = None
|
2017-02-10 15:41:45 -06:00
|
|
|
self.comment_api = CommentAPI(self.apiurl)
|
2016-03-08 16:43:54 +01:00
|
|
|
|
2016-12-23 13:59:17 +01:00
|
|
|
self.load_config()
|
|
|
|
|
|
|
|
def _load_config(self, handle = None):
|
2017-01-13 15:36:34 +01:00
|
|
|
d = self.__class__.config_defaults
|
2016-12-23 13:59:17 +01:00
|
|
|
y = yaml.safe_load(handle) if handle is not None else {}
|
|
|
|
return namedtuple('BotConfig', sorted(d.keys()))(*[ y.get(p, d[p]) for p in sorted(d.keys()) ])
|
|
|
|
|
|
|
|
def load_config(self, filename = None):
|
|
|
|
if filename:
|
|
|
|
fh = open(filename, 'r')
|
|
|
|
self.config = self._load_config(fh)
|
|
|
|
close(fh)
|
|
|
|
else:
|
|
|
|
self.config = self._load_config()
|
|
|
|
|
2016-03-08 16:43:54 +01:00
|
|
|
@property
|
|
|
|
def review_mode(self):
|
|
|
|
return self._review_mode
|
|
|
|
|
|
|
|
@review_mode.setter
|
|
|
|
def review_mode(self, value):
|
2016-03-17 16:15:31 +01:00
|
|
|
if value not in self.REVIEW_CHOICES:
|
2016-03-10 13:49:39 +01:00
|
|
|
raise Exception("invalid review option: %s"%value)
|
2016-03-08 16:43:54 +01:00
|
|
|
self._review_mode = value
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
def set_request_ids(self, ids):
|
|
|
|
for rqid in ids:
|
|
|
|
u = osc.core.makeurl(self.apiurl, [ 'request', rqid ], { 'withhistory' : '1' })
|
|
|
|
r = osc.core.http_GET(u)
|
|
|
|
root = ET.parse(r).getroot()
|
|
|
|
req = osc.core.Request()
|
|
|
|
req.read(root)
|
|
|
|
self.requests.append(req)
|
|
|
|
|
2016-07-28 15:26:14 +02:00
|
|
|
# function called before requests are reviewed
|
|
|
|
def prepare_review(self):
|
|
|
|
pass
|
|
|
|
|
2014-10-08 14:31:55 +02:00
|
|
|
def check_requests(self):
|
2016-03-08 16:43:54 +01:00
|
|
|
|
2016-07-28 15:26:14 +02:00
|
|
|
# give implementations a chance to do something before single requests
|
|
|
|
self.prepare_review()
|
2014-10-08 14:31:55 +02:00
|
|
|
for req in self.requests:
|
2016-06-28 22:30:27 +02:00
|
|
|
self.logger.info("checking %s"%req.reqid)
|
2016-12-29 00:34:56 -06:00
|
|
|
self.request = req
|
2014-10-08 14:31:55 +02:00
|
|
|
good = self.check_one_request(req)
|
|
|
|
|
2016-03-08 16:43:54 +01:00
|
|
|
if self.review_mode == 'no':
|
|
|
|
good = None
|
|
|
|
elif self.review_mode == 'accept':
|
|
|
|
good = True
|
|
|
|
|
2014-10-08 14:31:55 +02:00
|
|
|
if good is None:
|
2015-05-22 11:07:33 +02:00
|
|
|
self.logger.info("%s ignored"%req.reqid)
|
2014-10-08 14:31:55 +02:00
|
|
|
elif good:
|
|
|
|
self._set_review(req, 'accepted')
|
2016-08-05 07:41:30 +02:00
|
|
|
elif self.review_mode != 'accept-onpass':
|
2016-06-07 14:41:03 +02:00
|
|
|
self._set_review(req, 'declined')
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
def _set_review(self, req, state):
|
2015-03-27 15:01:45 +01:00
|
|
|
doit = self.can_accept_review(req.reqid)
|
2015-02-20 13:10:26 +01:00
|
|
|
if doit is None:
|
2015-03-27 15:01:45 +01:00
|
|
|
self.logger.info("can't change state, %s does not have the reviewer"%(req.reqid))
|
|
|
|
|
2016-06-07 14:41:03 +02:00
|
|
|
newstate = state
|
|
|
|
|
|
|
|
by_user = self.fallback_user
|
|
|
|
by_group = self.fallback_group
|
|
|
|
|
|
|
|
if state == 'declined':
|
|
|
|
if self.review_mode == 'fallback-onfail':
|
|
|
|
self.logger.info("%s needs fallback reviewer"%req.reqid)
|
2016-08-24 16:46:57 +02:00
|
|
|
# don't check duplicates, in case review was re-opened
|
2016-06-07 14:41:03 +02:00
|
|
|
self.add_review(req, by_group=by_group, by_user=by_user)
|
|
|
|
newstate = 'accepted'
|
|
|
|
elif self.review_mode == 'fallback-always':
|
|
|
|
self.add_review(req, by_group=by_group, by_user=by_user)
|
|
|
|
|
2016-06-09 17:30:09 +02:00
|
|
|
msg = self.review_messages[state] if state in self.review_messages else state
|
|
|
|
self.logger.info("%s %s: %s"%(req.reqid, state, msg))
|
2016-06-07 14:41:03 +02:00
|
|
|
|
2015-02-20 13:10:26 +01:00
|
|
|
if doit == True:
|
2014-10-08 14:31:55 +02:00
|
|
|
self.logger.debug("setting %s to %s"%(req.reqid, state))
|
|
|
|
if not self.dryrun:
|
|
|
|
osc.core.change_review_state(apiurl = self.apiurl,
|
2016-06-07 14:41:03 +02:00
|
|
|
reqid = req.reqid, newstate = newstate,
|
2015-03-27 15:01:45 +01:00
|
|
|
by_group=self.review_group,
|
2014-10-08 14:31:55 +02:00
|
|
|
by_user=self.review_user, message=msg)
|
|
|
|
else:
|
2015-02-20 13:10:26 +01:00
|
|
|
self.logger.debug("%s review not changed"%(req.reqid))
|
2014-10-08 14:31:55 +02:00
|
|
|
|
2016-08-24 16:46:57 +02:00
|
|
|
# note we intentionally don't check for duplicate review here!
|
2014-10-08 14:31:55 +02:00
|
|
|
def add_review(self, req, by_group=None, by_user=None, by_project = None, by_package = None, msg=None):
|
|
|
|
query = {
|
|
|
|
'cmd': 'addreview'
|
|
|
|
}
|
|
|
|
if by_group:
|
|
|
|
query['by_group'] = by_group
|
|
|
|
elif by_user:
|
|
|
|
query['by_user'] = by_user
|
|
|
|
elif by_project:
|
|
|
|
query['by_project'] = by_project
|
|
|
|
if by_package:
|
|
|
|
query['by_package'] = by_package
|
|
|
|
else:
|
|
|
|
raise osc.oscerr.WrongArgs("missing by_*")
|
|
|
|
|
|
|
|
u = osc.core.makeurl(self.apiurl, ['request', req.reqid], query)
|
|
|
|
if self.dryrun:
|
|
|
|
self.logger.info('POST %s' % u)
|
|
|
|
return True
|
|
|
|
|
|
|
|
try:
|
|
|
|
r = osc.core.http_POST(u, data=msg)
|
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
self.logger.error(e)
|
|
|
|
return False
|
|
|
|
|
|
|
|
code = ET.parse(r).getroot().attrib['code']
|
|
|
|
if code != 'ok':
|
|
|
|
self.logger.error("invalid return code %s"%code)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def check_one_request(self, req):
|
|
|
|
"""
|
|
|
|
check all actions in one request.
|
|
|
|
|
|
|
|
calls helper functions for each action type
|
|
|
|
|
|
|
|
return None if nothing to do, True to accept, False to reject
|
|
|
|
"""
|
|
|
|
overall = None
|
|
|
|
for a in req.actions:
|
|
|
|
fn = 'check_action_%s'%a.type
|
|
|
|
if not hasattr(self, fn):
|
2016-06-01 13:43:46 +02:00
|
|
|
fn = 'check_action__default'
|
|
|
|
func = getattr(self, fn)
|
|
|
|
ret = func(req, a)
|
2014-10-08 14:31:55 +02:00
|
|
|
if ret == False or overall is None and ret is not None:
|
|
|
|
overall = ret
|
|
|
|
return overall
|
|
|
|
|
2015-03-19 16:42:18 +01:00
|
|
|
def check_action_maintenance_incident(self, req, a):
|
2015-05-13 15:55:00 +02:00
|
|
|
dst_package = a.src_package
|
2016-08-05 15:16:39 +02:00
|
|
|
# Ignoring patchinfo package for checking
|
|
|
|
if a.src_package == 'patchinfo':
|
|
|
|
self.logger.info("package is patchinfo, ignoring")
|
|
|
|
return None
|
2015-05-13 15:55:00 +02:00
|
|
|
# dirty obs crap
|
2015-05-26 10:20:43 +02:00
|
|
|
if a.tgt_releaseproject is not None:
|
|
|
|
ugly_suffix = '.'+a.tgt_releaseproject.replace(':', '_')
|
|
|
|
if dst_package.endswith(ugly_suffix):
|
|
|
|
dst_package = dst_package[:-len(ugly_suffix)]
|
2015-05-13 15:55:00 +02:00
|
|
|
return self.check_source_submission(a.src_project, a.src_package, a.src_rev, a.tgt_releaseproject, dst_package)
|
2015-03-19 16:42:18 +01:00
|
|
|
|
|
|
|
def check_action_maintenance_release(self, req, a):
|
|
|
|
pkgname = a.src_package
|
|
|
|
if pkgname == 'patchinfo':
|
|
|
|
return None
|
|
|
|
linkpkg = self._get_linktarget_self(a.src_project, pkgname)
|
|
|
|
if linkpkg is not None:
|
|
|
|
pkgname = linkpkg
|
|
|
|
# packages in maintenance have links to the target. Use that
|
|
|
|
# to find the real package name
|
|
|
|
(linkprj, linkpkg) = self._get_linktarget(a.src_project, pkgname)
|
|
|
|
if linkpkg is None or linkprj is None or linkprj != a.tgt_project:
|
|
|
|
self.logger.error("%s/%s is not a link to %s"%(a.src_project, pkgname, a.tgt_project))
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
pkgname = linkpkg
|
2015-03-31 11:33:39 +02:00
|
|
|
return self.check_source_submission(a.src_project, a.src_package, None, a.tgt_project, pkgname)
|
2015-03-19 16:42:18 +01:00
|
|
|
|
|
|
|
def check_action_submit(self, req, a):
|
2015-03-31 11:33:39 +02:00
|
|
|
return self.check_source_submission(a.src_project, a.src_package, a.src_rev, a.tgt_project, a.tgt_package)
|
2015-03-19 16:42:18 +01:00
|
|
|
|
2016-06-01 13:43:46 +02:00
|
|
|
def check_action__default(self, req, a):
|
|
|
|
self.logger.error("unhandled request type %s"%a.type)
|
|
|
|
ret = None
|
|
|
|
|
2015-03-19 16:42:18 +01:00
|
|
|
def check_source_submission(self, src_project, src_package, src_rev, target_project, target_package):
|
|
|
|
""" default implemention does nothing """
|
|
|
|
self.logger.info("%s/%s@%s -> %s/%s"%(src_project, src_package, src_rev, target_project, target_package))
|
|
|
|
return None
|
|
|
|
|
2015-05-28 13:06:10 +02:00
|
|
|
@staticmethod
|
|
|
|
@memoize(session=True)
|
|
|
|
def _get_sourceinfo(apiurl, project, package, rev=None):
|
2014-10-08 14:31:55 +02:00
|
|
|
query = { 'view': 'info' }
|
2015-03-31 11:33:39 +02:00
|
|
|
if rev is not None:
|
2014-10-08 14:31:55 +02:00
|
|
|
query['rev'] = rev
|
2015-05-28 13:06:10 +02:00
|
|
|
url = osc.core.makeurl(apiurl, ('source', project, package), query=query)
|
2014-10-08 14:31:55 +02:00
|
|
|
try:
|
2015-05-28 13:06:10 +02:00
|
|
|
return ET.parse(osc.core.http_GET(url)).getroot()
|
2015-07-07 14:15:34 +08:00
|
|
|
except (urllib2.HTTPError, urllib2.URLError):
|
2014-10-08 14:31:55 +02:00
|
|
|
return None
|
|
|
|
|
2015-05-28 13:06:10 +02:00
|
|
|
def get_originproject(self, project, package, rev=None):
|
|
|
|
root = ReviewBot._get_sourceinfo(self.apiurl, project, package, rev)
|
|
|
|
if root is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
originproject = root.find('originproject')
|
|
|
|
if originproject is not None:
|
|
|
|
return originproject.text
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
def get_sourceinfo(self, project, package, rev=None):
|
|
|
|
root = ReviewBot._get_sourceinfo(self.apiurl, project, package, rev)
|
2015-03-31 11:33:39 +02:00
|
|
|
if root is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
props = ('package', 'rev', 'vrev', 'srcmd5', 'lsrcmd5', 'verifymd5')
|
|
|
|
return namedtuple('SourceInfo', props)(*[ root.get(p) for p in props ])
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
# TODO: what if there is more than _link?
|
|
|
|
def _get_linktarget_self(self, src_project, src_package):
|
|
|
|
""" if it's a link to a package in the same project return the name of the package"""
|
|
|
|
prj, pkg = self._get_linktarget(src_project, src_package)
|
|
|
|
if prj is None or prj == src_project:
|
|
|
|
return pkg
|
|
|
|
|
|
|
|
def _get_linktarget(self, src_project, src_package):
|
|
|
|
|
|
|
|
query = {}
|
|
|
|
url = osc.core.makeurl(self.apiurl, ('source', src_project, src_package), query=query)
|
|
|
|
try:
|
|
|
|
root = ET.parse(osc.core.http_GET(url)).getroot()
|
|
|
|
except urllib2.HTTPError:
|
|
|
|
return (None, None)
|
|
|
|
|
|
|
|
if root is not None:
|
|
|
|
linkinfo = root.find("linkinfo")
|
|
|
|
if linkinfo is not None:
|
|
|
|
return (linkinfo.get('project'), linkinfo.get('package'))
|
|
|
|
|
|
|
|
return (None, None)
|
|
|
|
|
2016-12-29 00:34:56 -06:00
|
|
|
def get_devel_project(self, project, package):
|
|
|
|
try:
|
|
|
|
m = osc.core.show_package_meta(self.apiurl, project, package)
|
|
|
|
node = ET.fromstring(''.join(m)).find('devel')
|
|
|
|
if node is not None:
|
|
|
|
return node.get('project'), node.get('package', None)
|
2017-01-31 20:15:52 -06:00
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
if e.code == 404:
|
|
|
|
pass
|
2016-12-29 00:34:56 -06:00
|
|
|
return None, None
|
|
|
|
|
2015-03-27 15:01:45 +01:00
|
|
|
def can_accept_review(self, request_id):
|
|
|
|
"""return True if there is a new review for the specified reviewer"""
|
2015-02-20 13:10:26 +01:00
|
|
|
states = set()
|
2014-10-08 14:31:55 +02:00
|
|
|
url = osc.core.makeurl(self.apiurl, ('request', str(request_id)))
|
|
|
|
try:
|
|
|
|
root = ET.parse(osc.core.http_GET(url)).getroot()
|
2015-03-27 15:12:52 +01:00
|
|
|
if self.review_user:
|
|
|
|
by_what = 'by_user'
|
|
|
|
reviewer = self.review_user
|
|
|
|
elif self.review_group:
|
|
|
|
by_what = 'by_group'
|
|
|
|
reviewer = self.review_group
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
states = set([review.get('state') for review in root.findall('review') if review.get(by_what) == reviewer])
|
2014-10-08 14:31:55 +02:00
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
print('ERROR in URL %s [%s]' % (url, e))
|
2015-02-20 13:10:26 +01:00
|
|
|
if not states:
|
|
|
|
return None
|
|
|
|
elif 'new' in states:
|
|
|
|
return True
|
|
|
|
return False
|
2014-10-08 14:31:55 +02:00
|
|
|
|
2015-03-27 15:01:45 +01:00
|
|
|
def set_request_ids_search_review(self):
|
|
|
|
if self.review_user:
|
|
|
|
review = "@by_user='%s'+and+@state='new'"%self.review_user
|
|
|
|
else:
|
|
|
|
review = "@by_group='%s'+and+@state='new'"%self.review_group
|
2014-10-08 14:31:55 +02:00
|
|
|
url = osc.core.makeurl(self.apiurl, ('search', 'request'), "match=state/@name='review'+and+review[%s]&withhistory=1"%review)
|
|
|
|
root = ET.parse(osc.core.http_GET(url)).getroot()
|
|
|
|
|
2015-10-05 10:49:45 +02:00
|
|
|
self.requests = []
|
|
|
|
|
2014-10-08 14:31:55 +02:00
|
|
|
for request in root.findall('request'):
|
|
|
|
req = osc.core.Request()
|
|
|
|
req.read(request)
|
|
|
|
self.requests.append(req)
|
|
|
|
|
2015-09-29 08:44:04 +02:00
|
|
|
def set_request_ids_project(self, project, typename):
|
|
|
|
url = osc.core.makeurl(self.apiurl, ('search', 'request'),
|
|
|
|
"match=(state/@name='review'+or+state/@name='new')+and+(action/target/@project='%s'+and+action/@type='%s')&withhistory=1"%(project, typename))
|
|
|
|
root = ET.parse(osc.core.http_GET(url)).getroot()
|
|
|
|
|
|
|
|
self.requests = []
|
|
|
|
|
|
|
|
for request in root.findall('request'):
|
|
|
|
req = osc.core.Request()
|
|
|
|
req.read(request)
|
|
|
|
self.requests.append(req)
|
|
|
|
|
2017-02-10 15:41:45 -06:00
|
|
|
def comment_handler_add(self, level=logging.INFO):
|
|
|
|
"""Add handler to start recording log messages for comment."""
|
|
|
|
self.comment_handler = CommentFromLogHandler(level)
|
|
|
|
self.logger.addHandler(self.comment_handler)
|
|
|
|
|
|
|
|
def comment_handler_remove(self):
|
|
|
|
self.logger.removeHandler(self.comment_handler)
|
|
|
|
|
|
|
|
def comment_find(self, request=None, state=None, result=None):
|
|
|
|
"""Return previous comments by current bot and matching criteria."""
|
|
|
|
# Case-insensitive for backwards compatibility.
|
|
|
|
bot = self.__class__.__name__.lower()
|
|
|
|
comments = self.comment_api.get_comments(request_id=request.reqid)
|
|
|
|
for c in comments.values():
|
|
|
|
m = ReviewBot.COMMENT_MARKER_REGEX.match(c['comment'])
|
|
|
|
if m and \
|
|
|
|
bot == m.group('bot').lower() and \
|
|
|
|
(state is None or state == m.group('state')) and \
|
|
|
|
(result is None or result == m.group('result')):
|
|
|
|
return c['id'], m.group('state'), m.group('result'), c['comment']
|
|
|
|
return None, None, None, None
|
|
|
|
|
|
|
|
def comment_write(self, state='done', result=None, request=None, message=None):
|
|
|
|
"""Write comment from log messages if not similar to previous comment."""
|
|
|
|
if request is None:
|
|
|
|
request = self.request
|
|
|
|
if message is None:
|
|
|
|
message = '\n\n'.join(self.comment_handler.lines)
|
|
|
|
|
|
|
|
marker = '<!-- {} state={} result={} -->'.format(self.__class__.__name__, state, result)
|
|
|
|
message = marker + '\n\n' + message
|
|
|
|
|
|
|
|
comment_id, _, _, comment_text = self.comment_find(request, state, result)
|
|
|
|
if comment_id is not None and comment_text.count('\n') == message.count('\n'):
|
|
|
|
# Assume same state/result and number of lines in message is duplicate.
|
|
|
|
self.logger.debug('previous comment too similar to bother commenting again')
|
|
|
|
return
|
|
|
|
|
|
|
|
self.logger.debug('adding comment to {}: {}'.format(request.reqid, message))
|
|
|
|
|
|
|
|
if not self.dryrun:
|
|
|
|
if comment_id is not None:
|
|
|
|
self.comment_api.delete(comment_id)
|
|
|
|
self.comment_api.add_comment(request_id=request.reqid, comment=str(message))
|
|
|
|
|
|
|
|
self.comment_handler_remove()
|
|
|
|
|
|
|
|
|
|
|
|
class CommentFromLogHandler(logging.Handler):
|
|
|
|
def __init__(self, level=logging.INFO):
|
|
|
|
super(CommentFromLogHandler, self).__init__(level)
|
|
|
|
self.lines = []
|
|
|
|
|
|
|
|
def emit(self, record):
|
|
|
|
self.lines.append(record.getMessage())
|
|
|
|
|
|
|
|
|
2014-10-08 14:31:55 +02:00
|
|
|
class CommandLineInterface(cmdln.Cmdln):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
cmdln.Cmdln.__init__(self, args, kwargs)
|
2017-01-02 02:34:13 -06:00
|
|
|
self.clazz = ReviewBot
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
def get_optparser(self):
|
2015-05-28 13:07:51 +02:00
|
|
|
parser = cmdln.Cmdln.get_optparser(self)
|
2014-10-08 14:31:55 +02:00
|
|
|
parser.add_option("--apiurl", '-A', metavar="URL", help="api url")
|
|
|
|
parser.add_option("--user", metavar="USER", help="reviewer user name")
|
2015-03-27 15:01:45 +01:00
|
|
|
parser.add_option("--group", metavar="GROUP", help="reviewer group name")
|
2014-10-08 14:31:55 +02:00
|
|
|
parser.add_option("--dry", action="store_true", help="dry run")
|
|
|
|
parser.add_option("--debug", action="store_true", help="debug output")
|
|
|
|
parser.add_option("--osc-debug", action="store_true", help="osc debug output")
|
|
|
|
parser.add_option("--verbose", action="store_true", help="verbose")
|
2016-03-08 16:43:54 +01:00
|
|
|
parser.add_option("--review-mode", dest='review_mode', choices=ReviewBot.REVIEW_CHOICES, help="review behavior")
|
|
|
|
parser.add_option("--fallback-user", dest='fallback_user', metavar='USER', help="fallback review user")
|
|
|
|
parser.add_option("--fallback-group", dest='fallback_group', metavar='GROUP', help="fallback review group")
|
2016-12-23 13:59:17 +01:00
|
|
|
parser.add_option('-c', '--config', dest='config', metavar='FILE', help='read config file FILE')
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
return parser
|
|
|
|
|
|
|
|
def postoptparse(self):
|
2015-11-23 16:03:29 +01:00
|
|
|
level = None
|
2014-10-08 14:31:55 +02:00
|
|
|
if (self.options.debug):
|
2015-11-23 16:03:29 +01:00
|
|
|
level = logging.DEBUG
|
2014-10-08 14:31:55 +02:00
|
|
|
elif (self.options.verbose):
|
2015-11-23 16:03:29 +01:00
|
|
|
level = logging.INFO
|
|
|
|
|
|
|
|
logging.basicConfig(level=level)
|
|
|
|
self.logger = logging.getLogger(self.optparser.prog)
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
osc.conf.get_config(override_apiurl = self.options.apiurl)
|
|
|
|
|
|
|
|
if (self.options.osc_debug):
|
|
|
|
osc.conf.config['debug'] = 1
|
|
|
|
|
|
|
|
self.checker = self.setup_checker()
|
2016-12-23 13:59:17 +01:00
|
|
|
if self.options.config:
|
|
|
|
self.checker.load_config(self.options.config)
|
2014-10-08 14:31:55 +02:00
|
|
|
|
2016-03-17 16:15:31 +01:00
|
|
|
if self.options.review_mode:
|
|
|
|
self.checker.review_mode = self.options.review_mode
|
|
|
|
|
|
|
|
if self.options.fallback_user:
|
|
|
|
self.checker.fallback_user = self.options.fallback_user
|
|
|
|
|
|
|
|
if self.options.fallback_group:
|
|
|
|
self.checker.fallback_group = self.options.fallback_group
|
|
|
|
|
2014-10-08 14:31:55 +02:00
|
|
|
def setup_checker(self):
|
|
|
|
""" reimplement this """
|
|
|
|
apiurl = osc.conf.config['apiurl']
|
|
|
|
if apiurl is None:
|
|
|
|
raise osc.oscerr.ConfigError("missing apiurl")
|
|
|
|
user = self.options.user
|
2015-03-27 15:01:45 +01:00
|
|
|
group = self.options.group
|
|
|
|
# if no args are given, use the current oscrc "owner"
|
|
|
|
if user is None and group is None:
|
2014-10-08 14:31:55 +02:00
|
|
|
user = osc.conf.get_apiurl_usr(apiurl)
|
|
|
|
|
2017-01-02 02:34:13 -06:00
|
|
|
return self.clazz(apiurl = apiurl, \
|
2014-10-08 14:31:55 +02:00
|
|
|
dryrun = self.options.dry, \
|
|
|
|
user = user, \
|
2015-03-27 15:01:45 +01:00
|
|
|
group = group, \
|
2014-10-08 14:31:55 +02:00
|
|
|
logger = self.logger)
|
|
|
|
|
|
|
|
def do_id(self, subcmd, opts, *args):
|
2015-09-29 08:44:04 +02:00
|
|
|
"""${cmd_name}: check the specified request ids
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
${cmd_usage}
|
|
|
|
${cmd_option_list}
|
|
|
|
"""
|
|
|
|
self.checker.set_request_ids(args)
|
|
|
|
self.checker.check_requests()
|
|
|
|
|
2015-09-29 08:44:04 +02:00
|
|
|
@cmdln.option('-n', '--interval', metavar="minutes", type="int", help="periodic interval in minutes")
|
2014-10-08 14:31:55 +02:00
|
|
|
def do_review(self, subcmd, opts, *args):
|
2015-09-29 08:44:04 +02:00
|
|
|
"""${cmd_name}: check requests that have the specified user or group as reviewer
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
${cmd_usage}
|
|
|
|
${cmd_option_list}
|
|
|
|
"""
|
2015-03-27 15:01:45 +01:00
|
|
|
if self.checker.review_user is None and self.checker.review_group is None:
|
|
|
|
raise osc.oscerr.WrongArgs("missing reviewer (user or group)")
|
2014-10-08 14:31:55 +02:00
|
|
|
|
2015-09-29 08:44:04 +02:00
|
|
|
def work():
|
|
|
|
self.checker.set_request_ids_search_review()
|
|
|
|
self.checker.check_requests()
|
|
|
|
|
|
|
|
self.runner(work, opts.interval)
|
|
|
|
|
|
|
|
@cmdln.option('-n', '--interval', metavar="minutes", type="int", help="periodic interval in minutes")
|
|
|
|
def do_project(self, subcmd, opts, project, typename):
|
|
|
|
"""${cmd_name}: check all requests of specified type to specified
|
|
|
|
|
|
|
|
${cmd_usage}
|
|
|
|
${cmd_option_list}
|
|
|
|
"""
|
|
|
|
|
|
|
|
def work():
|
|
|
|
self.checker.set_request_ids_project(project, typename)
|
|
|
|
self.checker.check_requests()
|
|
|
|
|
|
|
|
self.runner(work, opts.interval)
|
2014-10-08 14:31:55 +02:00
|
|
|
|
2015-05-22 14:46:29 +02:00
|
|
|
def runner(self, workfunc, interval):
|
|
|
|
""" runs the specified callback every <interval> minutes or
|
|
|
|
once if interval is None or 0
|
|
|
|
"""
|
|
|
|
class ExTimeout(Exception):
|
|
|
|
"""raised on timeout"""
|
|
|
|
|
|
|
|
if interval:
|
|
|
|
def alarm_called(nr, frame):
|
|
|
|
raise ExTimeout()
|
|
|
|
signal.signal(signal.SIGALRM, alarm_called)
|
|
|
|
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
workfunc()
|
|
|
|
except Exception, e:
|
2016-06-17 10:59:29 +02:00
|
|
|
self.logger.exception(e)
|
2015-05-22 14:46:29 +02:00
|
|
|
|
|
|
|
if interval:
|
|
|
|
self.logger.info("sleeping %d minutes. Press enter to check now ..."%interval)
|
|
|
|
signal.alarm(interval*60)
|
|
|
|
try:
|
|
|
|
raw_input()
|
|
|
|
except ExTimeout:
|
|
|
|
pass
|
|
|
|
signal.alarm(0)
|
|
|
|
self.logger.info("recheck at %s"%datetime.datetime.now().isoformat())
|
|
|
|
continue
|
|
|
|
break
|
2014-10-08 14:31:55 +02:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
app = CommandLineInterface()
|
|
|
|
sys.exit( app.main() )
|
|
|
|
|
|
|
|
# vim: sw=4 et
|