#!/usr/bin/python import os import sys import locale import optparse import re from getpass import getpass try: import bugzilla except ImportError: print "Unable to import bugzilla. Is python-bugzilla installed?" sys.exit(1) try: from simplemediawiki import MediaWiki except ImportError: print "Unable to import simplemediawiki. Is python-simpemediawiki installed?" sys.exit(1) BUG_STATUS = ['NEW', 'ASSIGNED', 'ON_DEV', 'MODIFIED', 'POST', 'ON_QA', 'FAILS_QA', 'PASSES_QA', 'REOPENED', 'VERIFIED', 'RELEASE_PENDING'] # Blocker query values BLOCKER_ACCEPTED = {'status_whiteboard': 'AcceptedBlocker', 'status_whiteboard_type': 'anywords'} BLOCKER_PROPOSED = {'status_whiteboard': 'AcceptedBlocker RejectedBlocker', 'status_whiteboard_type': 'nowords'} # NTH query values NTH_ACCEPTED = {'status_whiteboard': 'AcceptedNTH', 'status_whiteboard_type': 'anywords'} NTH_PROPOSED = {'status_whiteboard': 'AcceptedNTH RejectedNTH', 'status_whiteboard_type': 'nowords'} COOKIE_FILE = os.path.join(os.environ.get('HOME','/tmp'), '.fedora_cookiefile') # UTF8 helper - "borrored" from python-bugzilla def to_encoding(ustring): if isinstance(ustring, basestring): if isinstance(ustring, unicode): return ustring.encode(locale.getpreferredencoding(), 'replace') return ustring return u'' # Display list of bugs, organized by components def display_bugs_by_component(bugs_by_component, bugs_by_id): buf = '' components = sorted(bugs_by_component.keys()) for component in components: buf += '\n=== %s ===\n' % component # sorted list bugs = sorted(bugs_by_component.get(component,[])) for b in bugs: b = bugs_by_id[b] buf += '* [https://bugzilla.redhat.com/%s %s] (%s) - %s\n' % (b.bug_id, b.bug_id, b.bug_status, b.short_desc) return buf def parse_args(): '''Set up the option parser''' parser = optparse.OptionParser(usage="%prog [options]") parser.add_option('-v', '--verbose', action='store_true', default=False, help='Enable verbosity') parser.add_option('--hotdog', action='store_true', default=False, help='Enable hot dog (default: %default)') parser.add_option('--cookiefile', action='store', default=COOKIE_FILE, help='Cookiejar path to store wiki login cookies (default: %default)') optgrp = optparse.OptionGroup(parser, "Required options") optgrp.add_option('-n', '--name', action='store', default=None, help='Wiki page name to save results') optgrp.add_option('--blocker', action='store', default=None, help='Blocker tracking bug number') optgrp.add_option('--nth', action='store', default=None, help='Nice-to-Have tracking bug number') optgrp.add_option('-u', '--user', action='store', default=None, help='Mediawiki username') optgrp.add_option('-p', '--passwd', action='store', default=None, help='Mediawiki password') parser.add_option_group(optgrp) (opts, args) = parser.parse_args() # sanitize helper def sanitize_input(parser, value, label, ispass=False): if value is None and sys.stdin.isatty(): prompt = 'Enter %s: ' % label if ispass: value = getpass(prompt) else: value = raw_input(prompt) if value is None: parser.error('Must provide a valid %s' % label) return value def valid_cookiefile(parser, cookiefile): # if it doesn't exist, it can't be valid if not os.path.exists(cookiefile): return False # Check if it contains valid data fd = open(cookiefile, 'r') buf = fd.read() fd.close() valid = False for line in buf.split('\n'): # Skip empty lines or lines with only comments if re.match(r'^(\s*#|\s*$)', line): continue return True # Otherwise, no valid content found return False if not valid_cookiefile(parser, opts.cookiefile): opts.user = sanitize_input(parser, opts.user, "username") opts.passwd = sanitize_input(parser, opts.passwd, "password", ispass=True) opts.blocker = sanitize_input(parser, opts.blocker, "Blocker bug number") opts.nth = sanitize_input(parser, opts.nth, "Nice-to-have bug number") opts.name = sanitize_input(parser, opts.name, "Wiki page name") return opts if __name__ == '__main__': opts = parse_args() # Connect to bugzilla bz = bugzilla.RHBugzilla3(url='https://bugzilla.redhat.com/xmlrpc.cgi') # Get a list of accepted blocker bugs if opts.verbose: print 'Querying accepted blocker bugs ...' q = {'bug_status': BUG_STATUS, 'value0-0-0': opts.blocker, 'type0-0-0': 'substring', 'field0-0-0': 'blocked'} q.update(BLOCKER_ACCEPTED) accepted_blockers = bz.query(q) # Get a list of proposed blocker bugs if opts.verbose: print 'Querying proposed blocker bugs ...' q = {'bug_status': BUG_STATUS, 'value0-0-0': opts.blocker, 'type0-0-0': 'substring', 'field0-0-0': 'blocked'} q.update(BLOCKER_PROPOSED) proposed_blockers = bz.query(q) # Get a list of accepted NTH bugs if opts.verbose: print 'Querying accepted nice-to-have bugs ...' q = {'bug_status': BUG_STATUS, 'value0-0-0': opts.nth, 'type0-0-0': 'substring', 'field0-0-0': 'blocked'} q.update(NTH_ACCEPTED) accepted_nths = bz.query(q) # Get a list of proposed NTH bugs if opts.verbose: print 'Querying proposed nice-to-have bugs ...' q = {'bug_status': BUG_STATUS, 'value0-0-0': opts.nth, 'type0-0-0': 'substring', 'field0-0-0': 'blocked'} q.update(NTH_PROPOSED) proposed_nths = bz.query(q) # Organize bugs for later reference if opts.verbose: print 'Organizing bugs ...' bugs_by_id = dict() # walk accepted blockers accepted_blocker_by_component = dict() for b in accepted_blockers: if not accepted_blocker_by_component.has_key(b.component): accepted_blocker_by_component[b.component] = list() accepted_blocker_by_component[b.component].append(b.bug_id) bugs_by_id[b.bug_id] = b # walk proposed blockers proposed_blocker_by_component = dict() for b in proposed_blockers: if not proposed_blocker_by_component.has_key(b.component): proposed_blocker_by_component[b.component] = list() proposed_blocker_by_component[b.component].append(b.bug_id) bugs_by_id[b.bug_id] = b # walk accepted nths accepted_nth_by_component = dict() for b in accepted_nths: if not accepted_nth_by_component.has_key(b.component): accepted_nth_by_component[b.component] = list() accepted_nth_by_component[b.component].append(b.bug_id) bugs_by_id[b.bug_id] = b # walk proposed nths proposed_nth_by_component = dict() for b in proposed_nths: if not proposed_nth_by_component.has_key(b.component): proposed_nth_by_component[b.component] = list() proposed_nth_by_component[b.component].append(b.bug_id) bugs_by_id[b.bug_id] = b # Generate page content page_content = ''' ''This page generated automatically using [http://fedorapeople.org/gitweb?p=jlaska/public_git/scripts.git %s].'' ''' % (os.path.basename(sys.argv[0]),) # Start with the dog if opts.hotdog: page_content += '[[File:Hotdog.gif|right]]' else: page_content += '[[File:F{{FedoraVersionNumber|next}}_anaconda_center.png|right]]' def join_lists(l): ''' Takes a list of lists, and joins them into a single list. For example: [[1,2,3],[4],[5,6,7]] will become [1,2,3,4,5,6,7] ''' return [item for sublist in l for item in sublist] # Display approved blockers page_content += ''' == Approved Blockers == The following list of bugs are approved blockers that must be resolved. There are %s bug(s) affecting %s component(s). ''' % (len(join_lists(accepted_blocker_by_component.values())), len(accepted_blocker_by_component)) # Sorted list of approved bugs page_content += display_bugs_by_component(accepted_blocker_by_component, bugs_by_id) # Display proposed blockers page_content += ''' == Proposed Blockers == The following list of bugs are not yet approved to block the release. There are %s bug(s) affecting %s component(s). For guidance on reviewing the following bugs, refer to [[QA:SOP_blocker_bug_process]]. ''' % (len(join_lists(proposed_blocker_by_component.values())), len(proposed_blocker_by_component)) # Sorted list of proposed bugs page_content += display_bugs_by_component(proposed_blocker_by_component, bugs_by_id) # Display approved nths page_content += ''' == Approved NICE-TO-HAVE == The following list of bugs are approved nths that must be resolved. There are %s bug(s) affecting %s component(s). ''' % (len(join_lists(accepted_nth_by_component.values())), len(accepted_nth_by_component)) # Sorted list of approved bugs page_content += display_bugs_by_component(accepted_nth_by_component, bugs_by_id) # Display proposed nths page_content += ''' == Proposed NICE-TO-HAVE == The following list of bugs are not yet approved to block the release. There are %s bug(s) affecting %s component(s). For guidance on reviewing the following bugs, refer to [[QA:SOP_nth_bug_process]]. ''' % (len(join_lists(proposed_nth_by_component.values())), len(proposed_nth_by_component)) # Sorted list of proposed bugs page_content += display_bugs_by_component(proposed_nth_by_component, bugs_by_id) # Create mediawiki handle if opts.verbose: print 'Connecting to mediawiki ...' wiki = MediaWiki('https://fedoraproject.org/w/api.php', cookie_file=opts.cookiefile) # Login to the wiki if opts.user: if opts.verbose: print 'Logging into mediawiki ...' if not wiki.login(opts.user, opts.passwd): print "Error: invalid mediawiki username or password" sys.exit(1) else: if opts.verbose: print 'Using cookies in %s for authentication' % opts.cookiefile # Get an edit token q = dict(action='query', prop='info', intoken='edit', titles=opts.name) response = wiki.call(q) pages = [v for k,v in response.get('query', {}).get('pages',{}).items()] edit_token = list() if pages: edit_token = pages[0] # Make the wiki edit q = dict(action='edit', token=edit_token.get('edittoken',''), starttimestamp=edit_token.get('starttimestamp',''), summary='This scripted update brought to you by python-simplemediawiki and python-bugzilla.', text=to_encoding(u'%s' % page_content), title=opts.name) response = wiki.call(q) url = "https://fedoraproject.org/wiki/%s" % q.get('title') # Failure - Captcha if response.get('edit',{}).get('result','').lower() == 'failure': # Is a captcha required? captcha = response.get('edit', {}).get('captcha', False) if captcha: q.update(dict(captchaid=captcha.get('id'), captchaword=eval(captcha.get('question')))) response = wiki.call(q) # Error - Permission denied if response.has_key('error'): print "Failed to update '%s'\n%s" % (q.get('title'), response.get('error',{}).get('info','')) sys.exit(1) if response.get('result','').lower() == 'failure': print "Failed to update '%s'\n%s" % (q.get('title'), response) sys.exit(1) # Info - nochange elif response.get('edit',{}).has_key('nochange'): print "No changes, %s not updated" % (q.get('title'),) # Success elif response.get('edit',{}).has_key('newrevid'): print "Updated %s (revision: %s)" % (url, response.get('edit',{}).get('newrevid')) # Unknown else: print "Unknown result while updating %s\n%s" % (q.get('title'), response) sys.exit(0)