1
# Copyright (C) 2006 by Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 
 
11
# # GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
18
from getpass import getpass
 
 
20
from urlparse import urlsplit, urlunsplit
 
 
25
import bzrlib.errors as errors
 
 
29
export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
 
 
32
class LaunchpadService(object):
 
 
33
    """A service to talk to Launchpad via XMLRPC.
 
 
35
    See http://bazaar-vcs.org/Specs/LaunchpadRpc for the methods we can call.
 
 
38
    # NB: this should always end in a slash to avoid xmlrpclib appending
 
 
40
    DEFAULT_SERVICE_URL = 'https://xmlrpc.launchpad.net/bazaar/'
 
 
43
    registrant_email = None
 
 
44
    registrant_password = None
 
 
47
    def __init__(self, transport=None):
 
 
48
        """Construct a new service talking to the launchpad rpc server"""
 
 
50
            uri_type = urllib.splittype(self.service_url)[0]
 
 
51
            if uri_type == 'https':
 
 
52
                transport = xmlrpclib.SafeTransport()
 
 
54
                transport = xmlrpclib.Transport()
 
 
55
            transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
 
 
56
                    % (bzrlib.__version__, xmlrpclib.__version__)
 
 
57
        self.transport = transport
 
 
61
    def service_url(self):
 
 
62
        """Return the http or https url for the xmlrpc server.
 
 
64
        This does not include the username/password credentials.
 
 
66
        key = 'BZR_LP_XMLRPC_URL'
 
 
68
            return os.environ[key]
 
 
70
            return self.DEFAULT_SERVICE_URL
 
 
73
        """Return the proxy for XMLRPC requests."""
 
 
74
        # auth info must be in url
 
 
75
        # TODO: if there's no registrant email perhaps we should just connect
 
 
77
        scheme, hostinfo, path = urlsplit(self.service_url)[:3]
 
 
78
        assert '@' not in hostinfo
 
 
79
        assert self.registrant_email is not None
 
 
80
        assert self.registrant_password is not None
 
 
81
        # TODO: perhaps fully quote the password to make it very slightly
 
 
83
        # TODO: can we perhaps add extra Authorization headers directly to the 
 
 
84
        # request, rather than putting this into the url?  perhaps a bit more 
 
 
85
        # secure against accidentally revealing it.  std66 s3.2.1 discourages putting
 
 
86
        # the password in the url.
 
 
87
        hostinfo = '%s:%s@%s' % (urllib.quote(self.registrant_email),
 
 
88
                                 urllib.quote(self.registrant_password),
 
 
90
        url = urlunsplit((scheme, hostinfo, path, '', ''))
 
 
91
        return xmlrpclib.ServerProxy(url, transport=self.transport)
 
 
93
    def gather_user_credentials(self):
 
 
94
        """Get the password from the user."""
 
 
95
        config = bzrlib.config.GlobalConfig()
 
 
96
        self.registrant_email = config.user_email()
 
 
97
        if self.registrant_password is None:
 
 
98
            prompt = 'launchpad.net password for %s: ' % \
 
 
100
            self.registrant_password = getpass(prompt)
 
 
102
    def send_request(self, method_name, method_params):
 
 
103
        proxy = self.get_proxy()
 
 
105
        method = getattr(proxy, method_name)
 
 
107
            result = method(*method_params)
 
 
108
        except xmlrpclib.ProtocolError, e:
 
 
110
                # TODO: This can give a ProtocolError representing a 301 error, whose
 
 
111
                # e.headers['location'] tells where to go and e.errcode==301; should
 
 
112
                # probably log something and retry on the new url.
 
 
113
                raise NotImplementedError("should resend request to %s, but this isn't implemented"
 
 
114
                        % e.headers.get('Location', 'NO-LOCATION-PRESENT'))
 
 
116
                # we don't want to print the original message because its
 
 
117
                # str representation includes the plaintext password.
 
 
118
                # TODO: print more headers to help in tracking down failures
 
 
119
                raise errors.BzrError("xmlrpc protocol error connecting to %s: %s %s"
 
 
120
                        % (self.service_url, e.errcode, e.errmsg))
 
 
124
class BaseRequest(object):
 
 
125
    """Base request for talking to a XMLRPC server."""
 
 
127
    # Set this to the XMLRPC method name.
 
 
130
    def _request_params(self):
 
 
131
        """Return the arguments to pass to the method"""
 
 
132
        raise NotImplementedError(self._request_params)
 
 
134
    def submit(self, service):
 
 
135
        """Submit request to Launchpad XMLRPC server.
 
 
137
        :param service: LaunchpadService indicating where to send
 
 
138
            the request and the authentication credentials.
 
 
140
        return service.send_request(self._methodname, self._request_params())
 
 
143
class DryRunLaunchpadService(LaunchpadService):
 
 
144
    """Service that just absorbs requests without sending to server.
 
 
146
    The dummy service does not need authentication.
 
 
149
    def send_request(self, method_name, method_params):
 
 
152
    def gather_user_credentials(self):
 
 
156
class BranchRegistrationRequest(BaseRequest):
 
 
157
    """Request to tell Launchpad about a bzr branch."""
 
 
159
    _methodname = 'register_branch'
 
 
161
    def __init__(self, branch_url,
 
 
164
                 branch_description='',
 
 
169
        self.branch_url = branch_url
 
 
171
            self.branch_name = branch_name
 
 
173
            self.branch_name = self._find_default_branch_name(self.branch_url)
 
 
174
        self.branch_title = branch_title
 
 
175
        self.branch_description = branch_description
 
 
176
        self.author_email = author_email
 
 
177
        self.product_name = product_name
 
 
179
    def _request_params(self):
 
 
180
        """Return xmlrpc request parameters"""
 
 
181
        # This must match the parameter tuple expected by Launchpad for this
 
 
183
        return (self.branch_url,
 
 
186
                self.branch_description,
 
 
191
    def _find_default_branch_name(self, branch_url):
 
 
192
        i = branch_url.rfind('/')
 
 
193
        return branch_url[i+1:]
 
 
196
class BranchBugLinkRequest(BaseRequest):
 
 
197
    """Request to link a bzr branch in Launchpad to a bug."""
 
 
199
    _methodname = 'link_branch_to_bug'
 
 
201
    def __init__(self, branch_url, bug_id):
 
 
204
        self.branch_url = branch_url
 
 
206
    def _request_params(self):
 
 
207
        """Return xmlrpc request parameters"""
 
 
208
        # This must match the parameter tuple expected by Launchpad for this
 
 
210
        return (self.branch_url, self.bug_id, '')