mirror of
https://github.com/apache/sqoop.git
synced 2025-05-04 00:43:42 +08:00
284 lines
12 KiB
Python
Executable File
284 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# 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 script will take your local git changes and upload them as a patch JIRA and review
|
|
# board. This script has been written to support Sqoop workflow but can work for any project
|
|
# that uses JIRA and review board.
|
|
#
|
|
# This tool depends on reviewboard python APIs, please download them
|
|
# from here: https://www.reviewboard.org/downloads/rbtools/
|
|
#
|
|
#
|
|
# Future improvement ideas
|
|
# * When submitting review request open an editor to let user fill in the details?
|
|
# * Add protection against uploading the same file (patch) twice?
|
|
# * Migrate all HTTP calls from urllib2 to requests?
|
|
import sys, os, re, urllib2, base64, subprocess, tempfile, shutil
|
|
import json
|
|
import datetime
|
|
import ConfigParser
|
|
import requests
|
|
from optparse import OptionParser
|
|
from rbtools.api.client import RBClient
|
|
|
|
# Resource file location
|
|
RC_PATH = os.path.expanduser("~/.upload-patch.rc")
|
|
|
|
# Default option values
|
|
DEFAULT_JIRA_URL = 'https://issues.apache.org/jira'
|
|
DEFAULT_JIRA_RB_LABEL = "Review board"
|
|
DEFAULT_JIRA_TRANSITION = "Patch Available"
|
|
DEFAULT_RB_URL = 'https://reviews.apache.org'
|
|
DEFAULT_RB_REPOSITORY = 'sqoop-sqoop2'
|
|
DEFAULT_RB_GROUP = 'sqoop'
|
|
DEFAULT_JIRA_USER = None
|
|
DEFAULT_JIRA_PASSWORD = None
|
|
DEFAULT_RB_USER = None
|
|
DEFAULT_RB_PASSWORD = None
|
|
|
|
# Loading resource file that can contain some parameters
|
|
if os.path.exists(RC_PATH):
|
|
rc = ConfigParser.RawConfigParser()
|
|
rc.read(RC_PATH)
|
|
# And override faults from the rc file
|
|
DEFAULT_JIRA_USER = rc.get("jira", "username")
|
|
DEFAULT_JIRA_PASSWORD = rc.get("jira", "password")
|
|
DEFAULT_RB_USER = rc.get("reviewboard", "username")
|
|
DEFAULT_RB_PASSWORD = rc.get("reviewboard", "password")
|
|
print "Loaded JIRA username from resource file: %s" % DEFAULT_JIRA_USER
|
|
print "Loaded Review board username from resource file: %s" % DEFAULT_RB_USER
|
|
else:
|
|
print "Resource file %s not found." % RC_PATH
|
|
|
|
# Options
|
|
parser = OptionParser("Usage: %prog [options]")
|
|
parser.add_option("--jira", dest="jira", help="JIRA number that this patch is for", metavar="SQOOP-1234")
|
|
parser.add_option("--jira-url", dest="jira_url", default=DEFAULT_JIRA_URL, help="URL to JIRA instance", metavar="http://jira.com/")
|
|
parser.add_option("--jira-user", dest="jira_user", default=DEFAULT_JIRA_USER, help="JIRA username", metavar="jarcec")
|
|
parser.add_option("--jira-transition",dest="jira_transition", default=DEFAULT_JIRA_TRANSITION,help="Name of the transition when uploading patch", metavar="Patch Available")
|
|
parser.add_option("--jira-password", dest="jira_password", default=DEFAULT_JIRA_PASSWORD, help="JIRA passowrd", metavar="secret")
|
|
parser.add_option("--jira-rb-label", dest="jira_rb_label", default=DEFAULT_JIRA_RB_LABEL, help="Label to be used in JIRA for the review board link", metavar="Review")
|
|
parser.add_option("--rb-url", dest="rb_url", default=DEFAULT_RB_URL, help="URL to Review board instance", metavar="http://rb.com/")
|
|
parser.add_option("--rb-group", dest="rb_group", default=DEFAULT_RB_GROUP, help="Review group for new review entry", metavar="sqoop")
|
|
parser.add_option("--rb-repository", dest="rb_repository", default=DEFAULT_RB_REPOSITORY, help="Review board's repository", metavar="sqoop2")
|
|
parser.add_option("--rb-user", dest="rb_user", default=DEFAULT_RB_USER, help="Review board username", metavar="jarcec")
|
|
parser.add_option("--rb-password", dest="rb_password", default=DEFAULT_RB_PASSWORD, help="Review board passowrd", metavar="secret")
|
|
parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, help="Print more debug information while execution")
|
|
|
|
# Execute given command on command line
|
|
def execute(cmd, options):
|
|
if options.verbose:
|
|
print "Executing command: %s" % (cmd)
|
|
return subprocess.call(cmd, shell=True)
|
|
|
|
# End program execution with given message and return code
|
|
def exit(message, ret=1):
|
|
print "FATAL: %s" % message
|
|
sys.exit(ret)
|
|
|
|
# Convert given number of bytes to human readable one
|
|
# Source: http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
|
|
def human_readable_size(num, suffix='B'):
|
|
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
|
|
if abs(num) < 1024.0:
|
|
return "%3.1f%s%s" % (num, unit, suffix)
|
|
num /= 1024.0
|
|
return "%.1f%s%s" % (num, 'Yi', suffix)
|
|
|
|
# Load given file entirely into memory
|
|
def get_file_content(filepath):
|
|
f = open(filepath, mode="r")
|
|
diff = f.read()
|
|
f.close()
|
|
return diff
|
|
|
|
# Geneate request to JIRA instance
|
|
def jira_request(url, options, data, headers):
|
|
request = urllib2.Request(url, data, headers)
|
|
if options.verbose:
|
|
print "JIRA Request: URL = %s, Username = %s, data = %s, headers = %s" % (url, options.jira_user, data, str(headers))
|
|
if options.jira_user and options.jira_password:
|
|
base64string = base64.encodestring('%s:%s' % (options.jira_user, options.jira_password)).replace('\n', '')
|
|
request.add_header("Authorization", "Basic %s" % base64string)
|
|
return urllib2.urlopen(request)
|
|
|
|
# Get response from JIRA in form of JSON and parse the JSON for downstream consumption
|
|
def jira_json(url, options, data, headers):
|
|
body = jira_request(url, options, data, headers).read()
|
|
if options.verbose:
|
|
print "Response: %s" % body
|
|
return json.loads(body)
|
|
|
|
# General details of JIRA issue
|
|
def jira_get_issue(options):
|
|
url = "%s/rest/api/2/issue/%s" % (options.jira_url, options.jira)
|
|
return jira_json(url, options, None, {})
|
|
|
|
# Links associated with the JIRA
|
|
def jira_get_links(options):
|
|
url = "%s/rest/api/2/issue/%s/remotelink" % (options.jira_url, options.jira)
|
|
return jira_json(url, options, None, {})
|
|
|
|
# Create new link
|
|
def jira_post_links(link_url, title, options):
|
|
url = "%s/rest/api/2/issue/%s/remotelink" % (options.jira_url, options.jira)
|
|
data = '{"object" : {"url" : "%s", "title" : "%s"}}' % (link_url, title)
|
|
jira_request(url, options, data, {"Content-Type" : "application/json"})
|
|
|
|
# Possible transitions for JIRA
|
|
def jira_get_transitions(options):
|
|
url = "%s/rest/api/2/issue/%s/transitions?expand=transititions.fields" % (options.jira_url, options.jira)
|
|
return jira_json(url, options, None, {})
|
|
|
|
# Transition JIRA to give state
|
|
def jira_post_transitions(transitionId, options):
|
|
url = "%s/rest/api/2/issue/%s/transitions" % (options.jira_url, options.jira)
|
|
data = '{"transition" : {"id" : "%s"}}' % transitionId
|
|
jira_request(url, options, data, {"Content-Type" : "application/json"})
|
|
|
|
# Create new attachement
|
|
def jira_post_attachments(f, options):
|
|
url = "%s/rest/api/2/issue/%s/attachments" % (options.jira_url, options.jira)
|
|
files = {'file':open(f)}
|
|
headers = {"X-Atlassian-Token" : "no-check"}
|
|
requests.post(url, files=files, headers=headers, auth=(options.jira_user, options.jira_password)).text
|
|
|
|
# Parse and validate arguments
|
|
(options, args) = parser.parse_args()
|
|
if not options.jira:
|
|
exit("Missing argument --jira")
|
|
|
|
# Main execution
|
|
patch = "%s.patch" % options.jira
|
|
execute("git diff HEAD > %s" % patch, options)
|
|
if not os.path.exists(patch):
|
|
exit("Can't generate patch locally")
|
|
|
|
# Verify size of the patch
|
|
patchSize = os.path.getsize(patch)
|
|
if patchSize == 0:
|
|
exit("Generated empty patch, ending gracefully", 0)
|
|
else:
|
|
print "Created patch %s (%s)" % (patch, human_readable_size(patchSize))
|
|
|
|
# Retrive link to review board if it exists already
|
|
reviewBoardUrl = None
|
|
linksJson = jira_get_links(options)
|
|
for link in linksJson:
|
|
if link.get("object").get("title") == options.jira_rb_label:
|
|
reviewBoardUrl = link.get("object").get("url")
|
|
break
|
|
if options.verbose:
|
|
if reviewBoardUrl:
|
|
print "Found associated review board: %s" % reviewBoardUrl
|
|
else:
|
|
print "No associated review board entry found"
|
|
|
|
# Saving details of the JIRA for various use
|
|
print "Getting details for JIRA %s" % (options.jira)
|
|
jiraDetails = jira_get_issue(options)
|
|
|
|
# Verify that JIRA is properly marked with versions (otherwise precommit hook would fail)
|
|
versions = []
|
|
for version in jiraDetails.get("fields").get("versions"):
|
|
versions = versions + [version.get("name")]
|
|
for version in jiraDetails.get("fields").get("fixVersions"):
|
|
versions = versions + [version.get("name")]
|
|
if not versions:
|
|
exit("Both 'Affected Version(s)' and 'Fix Version(s)' JIRA fields are empty. Please fill one of them with desired version first.")
|
|
|
|
# Review board handling
|
|
rbClient = RBClient(options.rb_url, username=options.rb_user, password=options.rb_password)
|
|
rbRoot = rbClient.get_root()
|
|
|
|
# The RB REST API don't have call to return repository by name, only by ID, so one have to
|
|
# manually go through all the repositories and find the one that matches the corrent name.
|
|
rbRepoId = -1
|
|
for repo in rbRoot.get_repositories(max_results=500):
|
|
if repo.name == options.rb_repository:
|
|
rbRepoId = repo.id
|
|
break
|
|
# Verification that we have found required repository
|
|
if rbRepoId == -1:
|
|
exit("Did not found repository '%s' on review board" % options.rb_repository)
|
|
else:
|
|
if options.verbose:
|
|
print "Review board repository %s has id %s" % (options.rb_repository, rbRepoId)
|
|
|
|
# If review doesn't exists we need to create one, otherwise we will update existing one
|
|
if reviewBoardUrl:
|
|
# For review board REST APIs we need to get just the ID (the number)
|
|
linkSplit = reviewBoardUrl.split('/')
|
|
reviewId = linkSplit[len(linkSplit)-1]
|
|
print "Updating existing review request %s with new patch" % reviewId
|
|
# Review request itself
|
|
reviewRequest = rbRoot.get_review_request(review_request_id=reviewId)
|
|
# Update diff (the patch) and publish the changes
|
|
reviewRequest.get_diffs().upload_diff(get_file_content(patch))
|
|
draft = reviewRequest.get_draft()
|
|
draft.update(public=True)
|
|
else:
|
|
print "Creating new review request"
|
|
jiraSummary = jiraDetails.get('fields').get('summary')
|
|
jiraDescription = jiraDetails.get('fields').get('description')
|
|
# Create review request
|
|
reviewRequest = rbRoot.get_review_requests().create(repository=rbRepoId)
|
|
# Attach patch
|
|
reviewRequest.get_diffs().upload_diff(get_file_content(patch))
|
|
# And add details
|
|
draft = reviewRequest.get_draft()
|
|
draft = draft.update(
|
|
summary='%s: %s' % (options.jira, jiraSummary),
|
|
description=jiraDescription,
|
|
target_groups=options.rb_group,
|
|
target_people=options.rb_user,
|
|
bugs_closed=options.jira
|
|
)
|
|
draft.update(public=True)
|
|
linkSplit = draft.links.review_request.href.split('/')
|
|
reviewId = linkSplit[len(linkSplit)-2]
|
|
reviewBoardUrl = "%s/r/%s" % (options.rb_url, reviewId)
|
|
jira_post_links(reviewBoardUrl, options.jira_rb_label, options)
|
|
print "Created new review: %s" % reviewBoardUrl
|
|
|
|
# Verify state of the JIRA to see if it's in the right state
|
|
if jiraDetails.get("fields").get("status").get("name") != options.jira_transition:
|
|
# JIRA REST API needs transition ID and not the human readable name, so we have to translate it first
|
|
jiraTransitions = jira_get_transitions(options)
|
|
transitionId = -1
|
|
for transition in jiraTransitions.get("transitions"):
|
|
if transition.get("to").get("name") == options.jira_transition:
|
|
transitionId = transition.get("id")
|
|
if transitionId == -1:
|
|
exit("Did not find valid transition id for %s" % options.jira_transition)
|
|
else:
|
|
if options.verbose:
|
|
print "Transition id for transition %s is %s" % (options.jira_transition, transitionId)
|
|
# And finally switch to patch available state
|
|
jira_post_transitions(transitionId, options)
|
|
print "Switch JIRA %s to %s state" % (options.jira, options.jira_transition)
|
|
else:
|
|
if options.verbose:
|
|
print "JIRA %s is already in %s" % (options.jira, options.jira_transition)
|
|
|
|
# Upload generated patch to JIRA itself
|
|
jira_post_attachments(patch, options)
|
|
|
|
# And that's it!
|
|
print "And we're done!" |