~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/plugins/launchpad/lp_registration.py

Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
 
 
18
from getpass import getpass
 
19
import os
 
20
from urlparse import urlsplit, urlunsplit
 
21
import urllib
 
22
import xmlrpclib
 
23
 
 
24
import bzrlib.config
 
25
import bzrlib.errors as errors
 
26
 
 
27
# for testing, do
 
28
'''
 
29
export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
 
30
'''
 
31
 
 
32
class LaunchpadService(object):
 
33
    """A service to talk to Launchpad via XMLRPC.
 
34
    
 
35
    See http://bazaar-vcs.org/Specs/LaunchpadRpc for the methods we can call.
 
36
    """
 
37
 
 
38
    # NB: this should always end in a slash to avoid xmlrpclib appending
 
39
    # '/RPC2'
 
40
    DEFAULT_SERVICE_URL = 'https://xmlrpc.launchpad.net/bazaar/'
 
41
 
 
42
    transport = None
 
43
    registrant_email = None
 
44
    registrant_password = None
 
45
 
 
46
 
 
47
    def __init__(self, transport=None):
 
48
        """Construct a new service talking to the launchpad rpc server"""
 
49
        if transport is None:
 
50
            uri_type = urllib.splittype(self.service_url)[0]
 
51
            if uri_type == 'https':
 
52
                transport = xmlrpclib.SafeTransport()
 
53
            else:
 
54
                transport = xmlrpclib.Transport()
 
55
            transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
 
56
                    % (bzrlib.__version__, xmlrpclib.__version__)
 
57
        self.transport = transport
 
58
 
 
59
 
 
60
    @property
 
61
    def service_url(self):
 
62
        """Return the http or https url for the xmlrpc server.
 
63
 
 
64
        This does not include the username/password credentials.
 
65
        """
 
66
        key = 'BZR_LP_XMLRPC_URL'
 
67
        if key in os.environ:
 
68
            return os.environ[key]
 
69
        else:
 
70
            return self.DEFAULT_SERVICE_URL
 
71
 
 
72
    def get_proxy(self):
 
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
 
76
        # anonymously?
 
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
 
82
        # obscured
 
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),
 
89
                                 hostinfo)
 
90
        url = urlunsplit((scheme, hostinfo, path, '', ''))
 
91
        return xmlrpclib.ServerProxy(url, transport=self.transport)
 
92
 
 
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: ' % \
 
99
                    self.registrant_email
 
100
            self.registrant_password = getpass(prompt)
 
101
 
 
102
    def send_request(self, method_name, method_params):
 
103
        proxy = self.get_proxy()
 
104
        assert method_name
 
105
        method = getattr(proxy, method_name)
 
106
        try:
 
107
            result = method(*method_params)
 
108
        except xmlrpclib.ProtocolError, e:
 
109
            if e.errcode == 301:
 
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'))
 
115
            else:
 
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))
 
121
        return result
 
122
 
 
123
 
 
124
class BaseRequest(object):
 
125
    """Base request for talking to a XMLRPC server."""
 
126
 
 
127
    # Set this to the XMLRPC method name.
 
128
    _methodname = None
 
129
 
 
130
    def _request_params(self):
 
131
        """Return the arguments to pass to the method"""
 
132
        raise NotImplementedError(self._request_params)
 
133
 
 
134
    def submit(self, service):
 
135
        """Submit request to Launchpad XMLRPC server.
 
136
 
 
137
        :param service: LaunchpadService indicating where to send
 
138
            the request and the authentication credentials.
 
139
        """
 
140
        return service.send_request(self._methodname, self._request_params())
 
141
 
 
142
 
 
143
class DryRunLaunchpadService(LaunchpadService):
 
144
    """Service that just absorbs requests without sending to server.
 
145
    
 
146
    The dummy service does not need authentication.
 
147
    """
 
148
 
 
149
    def send_request(self, method_name, method_params):
 
150
        pass
 
151
 
 
152
    def gather_user_credentials(self):
 
153
        pass
 
154
 
 
155
 
 
156
class BranchRegistrationRequest(BaseRequest):
 
157
    """Request to tell Launchpad about a bzr branch."""
 
158
 
 
159
    _methodname = 'register_branch'
 
160
 
 
161
    def __init__(self, branch_url,
 
162
                 branch_name='',
 
163
                 branch_title='',
 
164
                 branch_description='',
 
165
                 author_email='',
 
166
                 product_name='',
 
167
                 ):
 
168
        assert branch_url
 
169
        self.branch_url = branch_url
 
170
        if branch_name:
 
171
            self.branch_name = branch_name
 
172
        else:
 
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
 
178
 
 
179
    def _request_params(self):
 
180
        """Return xmlrpc request parameters"""
 
181
        # This must match the parameter tuple expected by Launchpad for this
 
182
        # method
 
183
        return (self.branch_url,
 
184
                self.branch_name,
 
185
                self.branch_title,
 
186
                self.branch_description,
 
187
                self.author_email,
 
188
                self.product_name,
 
189
               )
 
190
 
 
191
    def _find_default_branch_name(self, branch_url):
 
192
        i = branch_url.rfind('/')
 
193
        return branch_url[i+1:]
 
194
 
 
195
 
 
196
class BranchBugLinkRequest(BaseRequest):
 
197
    """Request to link a bzr branch in Launchpad to a bug."""
 
198
 
 
199
    _methodname = 'link_branch_to_bug'
 
200
 
 
201
    def __init__(self, branch_url, bug_id):
 
202
        assert branch_url
 
203
        self.bug_id = bug_id
 
204
        self.branch_url = branch_url
 
205
 
 
206
    def _request_params(self):
 
207
        """Return xmlrpc request parameters"""
 
208
        # This must match the parameter tuple expected by Launchpad for this
 
209
        # method
 
210
        return (self.branch_url, self.bug_id, '')