blob: 4d9ca37e1c295c9ba90010b1d8f3aadfb32db9e4 [file] [log] [blame]
#!/usr/bin/env python3.4
# -*- coding: utf-8 -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 the BugZilla scanner plugin for Kible """
import os
import re
import json
import time
import hashlib
import datetime
from threading import Thread, Lock
import plugins.utils.jsonapi
import urllib
title = "Scanner for BugZilla"
version = "0.1.0"
def accepts(source):
""" Determine if this is a BugZilla source """
if source['type'] == 'bugzilla':
return True
if source['type'] == 'issuetracker':
bz = re.match(r"(https?://\S+?)(/jsonrpc\.cgi)?[\s:?]+(.+)", source['sourceURL'])
if bz:
return True
return False
def getTime(string):
return time.mktime(time.strptime(re.sub(r"[zZ]", "", str(string)), "%Y-%m-%dT%H:%M:%S"))
def assigned(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'assignee':
return True
return False
def wfi(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'status' and item['toString'] == "Waiting for Infra":
return True
return False
def wfu(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'status' and item['toString'] == "Waiting for user":
return True
return False
def moved(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'Key' and item['toString'].find("INFRA-") != -1:
return True
return False
def wasclosed(js):
if 'changelog' in js:
cjs = js['changelog']['histories']
for citem in cjs:
if 'items' in citem:
for item in citem['items']:
if item['field'] == 'status' and (item['toString'] == 'Closed' or item['toString'] == 'Resolved'):
return (True, citem['author'])
else:
if 'items' in js:
for item in js['items']:
if item['field'] == 'status' and item['toString'] == 'Closed':
return (True, None)
return (False, None)
def resolved(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'resolution' and (item['toString'] != 'Pending Closed' and item['toString'] != 'Unresolved'):
return True
return False
def pchange(js):
if 'items' in js:
for item in js['items']:
if item['field'] == 'priority':
return True
return False
def scanTicket(bug, KibbleBit, source, openTickets, u, dom):
try:
key = bug['id']
dhash = hashlib.sha224( ("%s-%s-%s" % (source['organisation'], source['sourceURL'], key) ).encode('ascii', errors='replace')).hexdigest()
found = KibbleBit.exists('issue', dhash)
parseIt = False
if not found:
parseIt = True
else:
ticket = KibbleBit.get('issue', dhash)
if ticket['status'] == 'closed' and key in openTickets:
KibbleBit.pprint("Ticket was reopened, reparsing")
parseIt = True
elif ticket['status'] == 'open' and not key in openTickets:
KibbleBit.pprint("Ticket was recently closed, parsing it")
parseIt = True
else:
pass
#print("Ticket hasn't changed, ignoring...")
if parseIt:
KibbleBit.pprint("Parsing data from BugZilla for #%s" % key)
params = {
'ids': [int(key)],
'limit': 0
}
if source['creds'] and 'username' in source['creds'] and source['creds']['username'] and len(source['creds']['username']) > 0:
params['Bugzilla_login'] = source['creds']['username']
params['Bugzilla_password'] = source['creds']['password']
ticketsURL = "%s?method=Bug.get&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
js = plugins.utils.jsonapi.get(ticketsURL)
js= js['result']['bugs'][0]
creator = {
'name': bug['creator'],
'email': js['creator']
}
closer = {}
cd = getTime(js['creation_time'])
rd = None
status = 'open'
if js['status'] in ["CLOSED", "RESOLVED"]:
status = 'closed'
KibbleBit.pprint("%s was closed, finding out who did that" % key)
ticketsURL = "%s?method=Bug.history&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
hjs = plugins.utils.jsonapi.get(ticketsURL)
history = hjs['result']['bugs'][0]['history']
for item in history:
for change in item['changes']:
if change['field_name'] == 'status' and 'added' in change and change['added'] in ['CLOSED', 'RESOLVED']:
rd = getTime(item['when'])
closer = {
'name': item['who'],
'email': item['who']
}
break
KibbleBit.pprint("Counting comments for %s..." % key)
ticketsURL = "%s?method=Bug.comments&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
hjs = plugins.utils.jsonapi.get(ticketsURL)
comments = len(hjs['result']['bugs'][str(key)]['comments'])
title = bug['summary']
del params['ids']
if closer:
pid = hashlib.sha1( ("%s%s" % (source['organisation'], closer['email'])).encode('ascii', errors='replace')).hexdigest()
found = KibbleBit.exists('person', pid)
if not found:
params['names'] = [closer['email']]
ticketsURL = "%s?method=User.get&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
try:
ujs = plugins.utils.jsonapi.get(ticketsURL)
displayName = ujs['result']['users'][0]['real_name']
except:
displayName = closer['email']
if displayName and len(displayName) > 0:
# Add to people db
jsp = {
'name': displayName,
'email': closer['email'],
'organisation': source['organisation'],
'id' :pid
}
#print("Updating person DB for closer: %s (%s)" % (displayName, closerEmail))
KibbleBit.index('person', pid, jsp)
if creator:
pid = hashlib.sha1( ("%s%s" % (source['organisation'], creator['email'])).encode('ascii', errors='replace')).hexdigest()
found = KibbleBit.exists('person', pid)
if not found:
if not creator['name']:
params['names'] = [creator['email']]
ticketsURL = "%s?method=User.get&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
try:
ujs = plugins.utils.jsonapi.get(ticketsURL)
creator['name'] = ujs['result']['users'][0]['real_name']
except:
creator['name'] = creator['email']
if creator['name'] and len(creator['name']) > 0:
# Add to people db
jsp = {
'name': creator['name'],
'email': creator['email'],
'organisation': source['organisation'],
'id' :pid
}
KibbleBit.index('person', pid, jsp)
jso = {
'id': dhash,
'key': key,
'organisation': source['organisation'],
'sourceID': source['sourceID'],
'url': "%s/show_bug.cgi?id=%s" % (dom, key),
'status': status,
'created': cd,
'closed': rd,
'issuetype': 'issue',
'issueCloser': closer['email'] if 'email' in closer else None,
'createdDate': time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(cd)),
'closedDate': time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(rd)) if rd else None,
'changeDate': time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(rd if rd else cd)),
'assignee': None,
'issueCreator': creator['email'],
'comments': comments,
'title': title
}
KibbleBit.append('issue', jso)
time.sleep(0.5) # BugZilla is notoriously slow. Maybe remove this later
return True
except Exception as err:
KibbleBit.pprint(err)
return False
class bzThread(Thread):
def __init__(self, KibbleBit, source, block, pt, ot, u, dom):
super(bzThread, self).__init__()
self.KibbleBit = KibbleBit
self.source = source
self.block = block
self.pendingTickets = pt
self.openTickets = ot
self.u = u
self.dom = dom
def run(self):
badOnes = 0
while len(self.pendingTickets) > 0 and badOnes <= 50:
if len(self.pendingTickets) % 10 == 0:
self.KibbleBit.pprint("%u elements left to count" % len(self.pendingTickets))
self.block.acquire()
try:
rl = self.pendingTickets.pop(0)
except Exception as err: # list empty, likely
self.block.release()
return
if not rl:
self.block.release()
return
self.block.release()
if not scanTicket(rl, self.KibbleBit, self.source, self.openTickets, self.u, self.dom):
self.KibbleBit.pprint("Ticket %s seems broken, skipping" % rl['id'])
badOnes += 1
if badOnes > 50:
self.KibbleBit.pprint("Too many errors, bailing!")
self.source['steps']['issues'] = {
'time': time.time(),
'status': 'Too many errors while parsing at ' + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(time.time())),
'running': False,
'good': False
}
self.KibbleBit.updateSource(self.source)
return
else:
badOnes = 0
def scan(KibbleBit, source):
path = source['sourceID']
url = source['sourceURL']
source['steps']['issues'] = {
'time': time.time(),
'status': 'Parsing BugZilla changes...',
'running': True,
'good': True
}
KibbleBit.updateSource(source)
bz = re.match(r"(https?://\S+?)(/jsonrpc\.cgi)?[\s:?]+(.+)", url)
if bz:
if source['creds'] and 'username' in source['creds'] and source['creds']['username'] and len(source['creds']['username']) > 0:
creds = "%s:%s" % (source['creds']['username'], source['creds']['password'])
badOnes = 0
pendingTickets = []
openTickets = []
# Get base URL, list and domain to parse
dom = bz.group(1)
dom = re.sub(r"/+$", "", dom)
u = "%s/jsonrpc.cgi" % dom
instance = bz.group(3)
lastTicket = 0
params = {
'product': [instance],
'status': ["RESOLVED", "CLOSED", "NEW","UNCOMFIRMED","ASSIGNED","REOPENED","VERIFIED"],
'include_fields': ['id', 'creation_time', 'status', 'summary', 'creator'],
'limit': 10000,
'offset': 1
}
# If * is requested, just omit the product name
if instance == '*':
params = {
'status': ["RESOLVED", "CLOSED", "NEW","UNCOMFIRMED","ASSIGNED","REOPENED","VERIFIED"],
'include_fields': ['id', 'creation_time', 'status', 'summary', 'creator'],
'limit': 10000,
'offset': 1
}
ticketsURL = "%s?method=Bug.search&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
while True:
try:
js = plugins.utils.jsonapi.get(ticketsURL, auth = creds)
except:
KibbleBit.pprint("Couldn't fetch more tickets, bailing")
break
if len(js['result']['bugs']) > 0:
KibbleBit.pprint("%s: Found %u tickets..." % (source['sourceURL'], ((params.get('offset', 1)-1) + len(js['result']['bugs']))))
for bug in js['result']['bugs']:
pendingTickets.append(bug)
if not bug['status'] in ['RESOLVED', 'CLOSED']:
openTickets.append(bug['id'])
params['offset'] += 10000
ticketsURL = "%s?method=Bug.search&params=[%s]" % (u, urllib.parse.quote(json.dumps(params)))
else:
KibbleBit.pprint("No more tickets left to scan")
break
KibbleBit.pprint("Found %u open tickets, %u closed." % (len(openTickets), len(pendingTickets) - len(openTickets)))
badOnes = 0
block = Lock()
threads = []
for i in range(0,4):
t = bzThread(KibbleBit, source, block, pendingTickets, openTickets, u, dom)
threads.append(t)
t.start()
for t in threads:
t.join()
source['steps']['issues'] = {
'time': time.time(),
'status': 'Issue tracker (BugZilla) successfully scanned at ' + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(time.time())),
'running': False,
'good': True
}
KibbleBit.updateSource(source)