summaryrefslogtreecommitdiffstats
path: root/apps/patchwork
diff options
context:
space:
mode:
authorJeremy Kerr <jk@ozlabs.org>2008-09-09 09:28:33 +1000
committerJeremy Kerr <jk@ozlabs.org>2008-09-09 09:28:33 +1000
commita66875053f1014de3767937b1a918949c667ac74 (patch)
treef1cd259e995b8323dbfdb654c8da3f5ae3c75c12 /apps/patchwork
parent18638f005afea23e13f7329ed3ebbdae8d35582c (diff)
downloadpatchwork-a66875053f1014de3767937b1a918949c667ac74.tar.bz2
patchwork-a66875053f1014de3767937b1a918949c667ac74.tar.xz
Add 'update' method to pwclient
This requires a new xmlrpc function, 'patch_set'. To do this, we need HTTP Authentication support, which means changing to a custom XMLRPC distpatcher that is aware of the Authorization: header. Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
Diffstat (limited to 'apps/patchwork')
-rwxr-xr-xapps/patchwork/bin/pwclient.py87
-rw-r--r--apps/patchwork/urls.py1
-rw-r--r--apps/patchwork/views/xmlrpc.py (renamed from apps/patchwork/xmlrpc.py)161
3 files changed, 240 insertions, 9 deletions
diff --git a/apps/patchwork/bin/pwclient.py b/apps/patchwork/bin/pwclient.py
index a50df73..0831086 100755
--- a/apps/patchwork/bin/pwclient.py
+++ b/apps/patchwork/bin/pwclient.py
@@ -26,6 +26,7 @@ import getopt
import string
import tempfile
import subprocess
+import base64
import ConfigParser
# Default Patchwork remote XML-RPC server URL
@@ -78,6 +79,24 @@ class Filter:
"""Return human-readable description of the filter."""
return str(self.d)
+class BasicHTTPAuthTransport(xmlrpclib.Transport):
+
+ def __init__(self, username = None, password = None):
+ self.username = username
+ self.password = password
+ xmlrpclib.Transport.__init__(self)
+
+ def authenticated(self):
+ return self.username != None and self.password != None
+
+ def send_host(self, connection, host):
+ xmlrpclib.Transport.send_host(self, connection, host)
+ if not self.authenticated():
+ return
+ credentials = '%s:%s' % (self.username, self.password)
+ auth = 'Basic ' + base64.encodestring(credentials).strip()
+ connection.putheader('Authorization', auth)
+
def usage():
sys.stderr.write("Usage: %s <action> [options]\n\n" % \
(os.path.basename(sys.argv[0])))
@@ -235,9 +254,39 @@ def action_apply(rpc, patch_id):
sys.stderr.write("Error: No patch content found\n")
sys.exit(1)
+def action_update_patch(rpc, patch_id, state = None, commit = None):
+ patch = rpc.patch_get(patch_id)
+ if patch == {}:
+ sys.stderr.write("Error getting information on patch ID %d\n" % \
+ patch_id)
+ sys.exit(1)
+
+ params = {}
+
+ if state:
+ state_id = state_id_by_name(rpc, state)
+ if state_id == 0:
+ sys.stderr.write("Error: No State found matching %s*\n" % state)
+ sys.exit(1)
+ params['state'] = state_id
+
+ if commit:
+ params['commit_ref'] = commit
+
+ success = False
+ try:
+ success = rpc.patch_set(patch_id, params)
+ except xmlrpclib.Fault, f:
+ sys.stderr.write("Error updating patch: %s\n" % f.faultString)
+
+ if not success:
+ sys.stderr.write("Patch not updated\n")
+
+auth_actions = ['update']
+
def main():
try:
- opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:')
+ opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:')
except getopt.GetoptError, err:
print str(err)
usage()
@@ -252,6 +301,8 @@ def main():
submitter_str = ""
delegate_str = ""
project_str = ""
+ commit_str = ""
+ state_str = ""
url = DEFAULT_URL
config = ConfigParser.ConfigParser()
@@ -266,13 +317,15 @@ def main():
for name, value in opts:
if name == '-s':
- filt.add("state", value)
+ state_str = value
elif name == '-p':
project_str = value
elif name == '-w':
submitter_str = value
elif name == '-d':
delegate_str = value
+ elif name == '-c':
+ commit_str = value
elif name == '-n':
try:
filt.add("max_count", int(value))
@@ -287,11 +340,29 @@ def main():
sys.stderr.write("Too many arguments specified\n")
usage()
+ (username, password) = (None, None)
+ transport = None
+ if action in auth_actions:
+ if config.has_option('auth', 'username') and \
+ config.has_option('auth', 'password'):
+
+ transport = BasicHTTPAuthTransport( \
+ config.get('auth', 'username'),
+ config.get('auth', 'password'))
+
+ else:
+ sys.stderr.write(("The %s action requires authentication, "
+ "but no username or password\nis configured\n") % action)
+ sys.exit(1)
+
if project_str:
filt.add("project", project_str)
+ if state_str:
+ filt.add("state", state_str)
+
try:
- rpc = xmlrpclib.Server(url)
+ rpc = xmlrpclib.Server(url, transport = transport)
except:
sys.stderr.write("Unable to connect to %s\n" % url)
sys.exit(1)
@@ -336,6 +407,16 @@ def main():
action_apply(rpc, patch_id)
+ elif action == 'update':
+ try:
+ patch_id = int(args[0])
+ except:
+ sys.stderr.write("Invalid patch ID given\n")
+ sys.exit(1)
+
+ action_update_patch(rpc, patch_id, state = state_str,
+ commit = commit_str)
+
else:
sys.stderr.write("Unknown action '%s'\n" % action)
usage()
diff --git a/apps/patchwork/urls.py b/apps/patchwork/urls.py
index f7c942a..ef1f2ad 100644
--- a/apps/patchwork/urls.py
+++ b/apps/patchwork/urls.py
@@ -58,6 +58,7 @@ urlpatterns = patterns('',
if settings.ENABLE_XMLRPC:
urlpatterns += patterns('',
+ (r'xmlrpc/$', 'patchwork.views.xmlrpc.xmlrpc'),
(r'^pwclient.py/$', 'patchwork.views.pwclient'),
(r'^project/(?P<project_id>[^/]+)/pwclientrc/$',
'patchwork.views.pwclientrc'),
diff --git a/apps/patchwork/xmlrpc.py b/apps/patchwork/views/xmlrpc.py
index fb64a7d..f493cf7 100644
--- a/apps/patchwork/xmlrpc.py
+++ b/apps/patchwork/views/xmlrpc.py
@@ -1,5 +1,5 @@
# Patchwork - automated patch tracking system
-# Copyright (C) 2008 Nate Case <ncase@xes-inc.com>
+# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
#
# This file is part of the Patchwork package.
#
@@ -17,14 +17,123 @@
# along with Patchwork; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
-# The XML-RPC interface provides a watered down, read-only interface to
-# the Patchwork database. It's intended to be safe to export to the public
-# Internet. A small subset of the object data is included, and the type
-# of requests/queries you can do is limited by the methods
-# that we export.
+# Patchwork XMLRPC interface
+#
+from django.core.exceptions import ImproperlyConfigured
+from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
+from django.http import HttpResponse, HttpResponseRedirect, \
+ HttpResponseServerError
+from django.conf import settings
+from django.core import urlresolvers
+from django.shortcuts import render_to_response
+from django.contrib.auth import authenticate
from patchwork.models import Patch, Project, Person, Bundle, State
+import sys
+import base64
+import xmlrpclib
+
+class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
+ def __init__(self):
+ if sys.version_info[:3] >= (2,5,):
+ SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
+ encoding=None)
+ else:
+ SimpleXMLRPCDispatcher.__init__(self)
+
+ # map of name => (auth, func)
+ self.func_map = {}
+
+
+ def register_function(self, fn, auth_required):
+ self.func_map[fn.__name__] = (auth_required, fn)
+
+
+ def _user_for_request(self, request):
+ if not request.META.has_key('HTTP_AUTHORIZATION'):
+ raise Exception("No authentication credentials given")
+
+ str = request.META.get('HTTP_AUTHORIZATION').strip()
+ if not str.startswith('Basic '):
+ raise Exception("Authentication scheme not supported")
+
+ str = str[len('Basic '):].strip()
+
+ try:
+ decoded = base64.decodestring(str)
+ username, password = decoded.split(':', 1)
+ except:
+ raise Exception("Invalid authentication credentials")
+
+ return authenticate(username = username, password = password)
+
+
+ def _dispatch(self, request, method, params):
+ if method not in self.func_map.keys():
+ raise Exception('method "%s" is not supported' % method)
+
+ auth_required, fn = self.func_map[method]
+
+ if auth_required:
+ user = self._user_for_request(request)
+ if not user:
+ raise Exception("Invalid username/password")
+
+ params = (user,) + params
+
+ return fn(*params)
+
+ def _marshaled_dispatch(self, request):
+ try:
+ params, method = xmlrpclib.loads(request.raw_post_data)
+
+ response = self._dispatch(request, method, params)
+ # wrap response in a singleton tuple
+ response = (response,)
+ response = xmlrpclib.dumps(response, methodresponse=1,
+ allow_none=self.allow_none, encoding=self.encoding)
+ except xmlrpclib.Fault, fault:
+ response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
+ encoding=self.encoding)
+ except:
+ # report exception back to server
+ response = xmlrpclib.dumps(
+ xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
+ encoding=self.encoding, allow_none=self.allow_none,
+ )
+
+ return response
+
+dispatcher = PatchworkXMLRPCDispatcher()
+
+# XMLRPC view function
+def xmlrpc(request):
+ if request.method != 'POST':
+ return HttpResponseRedirect(
+ urlresolvers.reverse('patchwork.views.help',
+ kwargs = {'path': 'pwclient/'}))
+
+ response = HttpResponse()
+ try:
+ ret = dispatcher._marshaled_dispatch(request)
+ response.write(ret)
+ except Exception, e:
+ return HttpResponseServerError()
+
+ return response
+
+# decorator for XMLRPC methods. Setting login_required to true will call
+# the decorated function with a non-optional user as the first argument.
+def xmlrpc_method(login_required = False):
+ def wrap(f):
+ dispatcher.register_function(f, login_required)
+ return f
+
+ return wrap
+
+
+
# We allow most of the Django field lookup types for remote queries
LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt",
"in", "startswith", "istartswith", "endswith",
@@ -100,10 +209,12 @@ def state_to_dict(obj):
# Public XML-RPC methods
#######################################################################
+@xmlrpc_method(False)
def pw_rpc_version():
"""Return Patchwork XML-RPC interface version."""
return 1
+@xmlrpc_method(False)
def project_list(search_str="", max_count=0):
"""Get a list of projects matching the given filters."""
try:
@@ -119,6 +230,7 @@ def project_list(search_str="", max_count=0):
except:
return []
+@xmlrpc_method(False)
def project_get(project_id):
"""Return structure for the given project ID."""
try:
@@ -127,6 +239,7 @@ def project_get(project_id):
except:
return {}
+@xmlrpc_method(False)
def person_list(search_str="", max_count=0):
"""Get a list of Person objects matching the given filters."""
try:
@@ -144,6 +257,7 @@ def person_list(search_str="", max_count=0):
except:
return []
+@xmlrpc_method(False)
def person_get(person_id):
"""Return structure for the given person ID."""
try:
@@ -152,6 +266,7 @@ def person_get(person_id):
except:
return {}
+@xmlrpc_method(False)
def patch_list(filter={}):
"""Get a list of patches matching the given filters."""
try:
@@ -210,6 +325,7 @@ def patch_list(filter={}):
except:
return []
+@xmlrpc_method(False)
def patch_get(patch_id):
"""Return structure for the given patch ID."""
try:
@@ -218,6 +334,7 @@ def patch_get(patch_id):
except:
return {}
+@xmlrpc_method(False)
def patch_get_mbox(patch_id):
"""Return mbox string for the given patch ID."""
try:
@@ -226,6 +343,7 @@ def patch_get_mbox(patch_id):
except:
return ""
+@xmlrpc_method(False)
def patch_get_diff(patch_id):
"""Return diff for the given patch ID."""
try:
@@ -234,6 +352,36 @@ def patch_get_diff(patch_id):
except:
return ""
+@xmlrpc_method(True)
+def patch_set(user, patch_id, params):
+ """Update a patch with the key,value pairs in params. Only some parameters
+ can be set"""
+ try:
+ ok_params = ['state', 'commit_ref', 'archived']
+
+ patch = Patch.objects.get(id = patch_id)
+
+ if not patch.is_editable(user):
+ raise Exception('No permissions to edit this patch')
+
+ for (k, v) in params.iteritems():
+ if k not in ok_params:
+ continue
+
+ if k == 'state':
+ patch.state = State.objects.get(id = v)
+
+ else:
+ setattr(patch, k, v)
+
+ patch.save()
+
+ return True
+
+ except:
+ raise
+
+@xmlrpc_method(False)
def state_list(search_str="", max_count=0):
"""Get a list of state structures matching the given search string."""
try:
@@ -249,6 +397,7 @@ def state_list(search_str="", max_count=0):
except:
return []
+@xmlrpc_method(False)
def state_get(state_id):
"""Return structure for the given state ID."""
try: