/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-07-20 08:56:45 UTC
  • mfrom: (4526.9.23 apply-inventory-delta)
  • Revision ID: pqm@pqm.ubuntu.com-20090720085645-54mtgybxua0yx6hw
(robertc) Add checks for inventory deltas which try to ensure that
        deltas that are not an exact fit are not applied. (Robert
        Collins, bug 397705, bug 367633)

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
 
18
import os
 
19
import socket
 
20
from urlparse import urlsplit, urlunsplit
 
21
import urllib
 
22
import xmlrpclib
 
23
 
 
24
from bzrlib.lazy_import import lazy_import
 
25
lazy_import(globals(), """
 
26
from bzrlib import urlutils
 
27
""")
 
28
 
 
29
from bzrlib import (
 
30
    config,
 
31
    errors,
 
32
    __version__ as _bzrlib_version,
 
33
    )
 
34
 
 
35
# for testing, do
 
36
'''
 
37
export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
 
38
'''
 
39
 
 
40
class InvalidLaunchpadInstance(errors.BzrError):
 
41
 
 
42
    _fmt = "%(lp_instance)s is not a valid Launchpad instance."
 
43
 
 
44
    def __init__(self, lp_instance):
 
45
        errors.BzrError.__init__(self, lp_instance=lp_instance)
 
46
 
 
47
 
 
48
class NotLaunchpadBranch(errors.BzrError):
 
49
 
 
50
    _fmt = "%(url)s is not registered on Launchpad."
 
51
 
 
52
    def __init__(self, url):
 
53
        errors.BzrError.__init__(self, url=url)
 
54
 
 
55
 
 
56
class LaunchpadService(object):
 
57
    """A service to talk to Launchpad via XMLRPC.
 
58
 
 
59
    See http://bazaar-vcs.org/Specs/LaunchpadRpc for the methods we can call.
 
60
    """
 
61
 
 
62
    LAUNCHPAD_DOMAINS = {
 
63
        'production': 'launchpad.net',
 
64
        'edge': 'edge.launchpad.net',
 
65
        'staging': 'staging.launchpad.net',
 
66
        'demo': 'demo.launchpad.net',
 
67
        'dev': 'launchpad.dev',
 
68
        }
 
69
 
 
70
    # NB: these should always end in a slash to avoid xmlrpclib appending
 
71
    # '/RPC2'
 
72
    LAUNCHPAD_INSTANCE = {}
 
73
    for instance, domain in LAUNCHPAD_DOMAINS.iteritems():
 
74
        LAUNCHPAD_INSTANCE[instance] = 'https://xmlrpc.%s/bazaar/' % domain
 
75
 
 
76
    # We use edge as the default because:
 
77
    # Beta users get redirected to it
 
78
    # All users can use it
 
79
    # There is a bug in the launchpad side where redirection causes an OOPS.
 
80
    DEFAULT_INSTANCE = 'edge'
 
81
    DEFAULT_SERVICE_URL = LAUNCHPAD_INSTANCE[DEFAULT_INSTANCE]
 
82
 
 
83
    transport = None
 
84
    registrant_email = None
 
85
    registrant_password = None
 
86
 
 
87
 
 
88
    def __init__(self, transport=None, lp_instance=None):
 
89
        """Construct a new service talking to the launchpad rpc server"""
 
90
        self._lp_instance = lp_instance
 
91
        if transport is None:
 
92
            uri_type = urllib.splittype(self.service_url)[0]
 
93
            if uri_type == 'https':
 
94
                transport = xmlrpclib.SafeTransport()
 
95
            else:
 
96
                transport = xmlrpclib.Transport()
 
97
            transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
 
98
                    % (_bzrlib_version, xmlrpclib.__version__)
 
99
        self.transport = transport
 
100
 
 
101
 
 
102
    @property
 
103
    def service_url(self):
 
104
        """Return the http or https url for the xmlrpc server.
 
105
 
 
106
        This does not include the username/password credentials.
 
107
        """
 
108
        key = 'BZR_LP_XMLRPC_URL'
 
109
        if key in os.environ:
 
110
            return os.environ[key]
 
111
        elif self._lp_instance is not None:
 
112
            try:
 
113
                return self.LAUNCHPAD_INSTANCE[self._lp_instance]
 
114
            except KeyError:
 
115
                raise InvalidLaunchpadInstance(self._lp_instance)
 
116
        else:
 
117
            return self.DEFAULT_SERVICE_URL
 
118
 
 
119
    def get_proxy(self, authenticated):
 
120
        """Return the proxy for XMLRPC requests."""
 
121
        if authenticated:
 
122
            # auth info must be in url
 
123
            # TODO: if there's no registrant email perhaps we should
 
124
            # just connect anonymously?
 
125
            scheme, hostinfo, path = urlsplit(self.service_url)[:3]
 
126
            if '@' in hostinfo:
 
127
                raise AssertionError(hostinfo)
 
128
            if self.registrant_email is None:
 
129
                raise AssertionError()
 
130
            if self.registrant_password is None:
 
131
                raise AssertionError()
 
132
            # TODO: perhaps fully quote the password to make it very slightly
 
133
            # obscured
 
134
            # TODO: can we perhaps add extra Authorization headers
 
135
            # directly to the request, rather than putting this into
 
136
            # the url?  perhaps a bit more secure against accidentally
 
137
            # revealing it.  std66 s3.2.1 discourages putting the
 
138
            # password in the url.
 
139
            hostinfo = '%s:%s@%s' % (urllib.quote(self.registrant_email),
 
140
                                     urllib.quote(self.registrant_password),
 
141
                                     hostinfo)
 
142
            url = urlunsplit((scheme, hostinfo, path, '', ''))
 
143
        else:
 
144
            url = self.service_url
 
145
        return xmlrpclib.ServerProxy(url, transport=self.transport)
 
146
 
 
147
    def gather_user_credentials(self):
 
148
        """Get the password from the user."""
 
149
        the_config = config.GlobalConfig()
 
150
        self.registrant_email = the_config.user_email()
 
151
        if self.registrant_password is None:
 
152
            auth = config.AuthenticationConfig()
 
153
            scheme, hostinfo = urlsplit(self.service_url)[:2]
 
154
            prompt = 'launchpad.net password for %s: ' % \
 
155
                    self.registrant_email
 
156
            # We will reuse http[s] credentials if we can, prompt user
 
157
            # otherwise
 
158
            self.registrant_password = auth.get_password(scheme, hostinfo,
 
159
                                                         self.registrant_email,
 
160
                                                         prompt=prompt)
 
161
 
 
162
    def send_request(self, method_name, method_params, authenticated):
 
163
        proxy = self.get_proxy(authenticated)
 
164
        method = getattr(proxy, method_name)
 
165
        try:
 
166
            result = method(*method_params)
 
167
        except xmlrpclib.ProtocolError, e:
 
168
            if e.errcode == 301:
 
169
                # TODO: This can give a ProtocolError representing a 301 error, whose
 
170
                # e.headers['location'] tells where to go and e.errcode==301; should
 
171
                # probably log something and retry on the new url.
 
172
                raise NotImplementedError("should resend request to %s, but this isn't implemented"
 
173
                        % e.headers.get('Location', 'NO-LOCATION-PRESENT'))
 
174
            else:
 
175
                # we don't want to print the original message because its
 
176
                # str representation includes the plaintext password.
 
177
                # TODO: print more headers to help in tracking down failures
 
178
                raise errors.BzrError("xmlrpc protocol error connecting to %s: %s %s"
 
179
                        % (self.service_url, e.errcode, e.errmsg))
 
180
        except socket.gaierror, e:
 
181
            raise errors.ConnectionError(
 
182
                "Could not resolve '%s'" % self.domain,
 
183
                orig_error=e)
 
184
        return result
 
185
 
 
186
    @property
 
187
    def domain(self):
 
188
        if self._lp_instance is None:
 
189
            instance = self.DEFAULT_INSTANCE
 
190
        else:
 
191
            instance = self._lp_instance
 
192
        return self.LAUNCHPAD_DOMAINS[instance]
 
193
 
 
194
    def get_web_url_from_branch_url(self, branch_url, _request_factory=None):
 
195
        """Get the Launchpad web URL for the given branch URL.
 
196
 
 
197
        :raise errors.InvalidURL: if 'branch_url' cannot be identified as a
 
198
            Launchpad branch URL.
 
199
        :return: The URL of the branch on Launchpad.
 
200
        """
 
201
        scheme, hostinfo, path = urlsplit(branch_url)[:3]
 
202
        if _request_factory is None:
 
203
            _request_factory = ResolveLaunchpadPathRequest
 
204
        if scheme == 'lp':
 
205
            resolve = _request_factory(path)
 
206
            try:
 
207
                result = resolve.submit(self)
 
208
            except xmlrpclib.Fault, fault:
 
209
                raise errors.InvalidURL(branch_url, str(fault))
 
210
            branch_url = result['urls'][0]
 
211
            path = urlsplit(branch_url)[2]
 
212
        else:
 
213
            domains = (
 
214
                'bazaar.%s' % domain
 
215
                for domain in self.LAUNCHPAD_DOMAINS.itervalues())
 
216
            if hostinfo not in domains:
 
217
                raise NotLaunchpadBranch(branch_url)
 
218
        return urlutils.join('https://code.%s' % self.domain, path)
 
219
 
 
220
 
 
221
class BaseRequest(object):
 
222
    """Base request for talking to a XMLRPC server."""
 
223
 
 
224
    # Set this to the XMLRPC method name.
 
225
    _methodname = None
 
226
    _authenticated = True
 
227
 
 
228
    def _request_params(self):
 
229
        """Return the arguments to pass to the method"""
 
230
        raise NotImplementedError(self._request_params)
 
231
 
 
232
    def submit(self, service):
 
233
        """Submit request to Launchpad XMLRPC server.
 
234
 
 
235
        :param service: LaunchpadService indicating where to send
 
236
            the request and the authentication credentials.
 
237
        """
 
238
        return service.send_request(self._methodname, self._request_params(),
 
239
                                    self._authenticated)
 
240
 
 
241
 
 
242
class DryRunLaunchpadService(LaunchpadService):
 
243
    """Service that just absorbs requests without sending to server.
 
244
 
 
245
    The dummy service does not need authentication.
 
246
    """
 
247
 
 
248
    def send_request(self, method_name, method_params, authenticated):
 
249
        pass
 
250
 
 
251
    def gather_user_credentials(self):
 
252
        pass
 
253
 
 
254
 
 
255
class BranchRegistrationRequest(BaseRequest):
 
256
    """Request to tell Launchpad about a bzr branch."""
 
257
 
 
258
    _methodname = 'register_branch'
 
259
 
 
260
    def __init__(self, branch_url,
 
261
                 branch_name='',
 
262
                 branch_title='',
 
263
                 branch_description='',
 
264
                 author_email='',
 
265
                 product_name='',
 
266
                 ):
 
267
        if not branch_url:
 
268
            raise errors.InvalidURL(branch_url, "You need to specify a non-empty branch URL.")
 
269
        self.branch_url = branch_url
 
270
        if branch_name:
 
271
            self.branch_name = branch_name
 
272
        else:
 
273
            self.branch_name = self._find_default_branch_name(self.branch_url)
 
274
        self.branch_title = branch_title
 
275
        self.branch_description = branch_description
 
276
        self.author_email = author_email
 
277
        self.product_name = product_name
 
278
 
 
279
    def _request_params(self):
 
280
        """Return xmlrpc request parameters"""
 
281
        # This must match the parameter tuple expected by Launchpad for this
 
282
        # method
 
283
        return (self.branch_url,
 
284
                self.branch_name,
 
285
                self.branch_title,
 
286
                self.branch_description,
 
287
                self.author_email,
 
288
                self.product_name,
 
289
               )
 
290
 
 
291
    def _find_default_branch_name(self, branch_url):
 
292
        i = branch_url.rfind('/')
 
293
        return branch_url[i+1:]
 
294
 
 
295
 
 
296
class BranchBugLinkRequest(BaseRequest):
 
297
    """Request to link a bzr branch in Launchpad to a bug."""
 
298
 
 
299
    _methodname = 'link_branch_to_bug'
 
300
 
 
301
    def __init__(self, branch_url, bug_id):
 
302
        self.bug_id = bug_id
 
303
        self.branch_url = branch_url
 
304
 
 
305
    def _request_params(self):
 
306
        """Return xmlrpc request parameters"""
 
307
        # This must match the parameter tuple expected by Launchpad for this
 
308
        # method
 
309
        return (self.branch_url, self.bug_id, '')
 
310
 
 
311
 
 
312
class ResolveLaunchpadPathRequest(BaseRequest):
 
313
    """Request to resolve the path component of an lp: URL."""
 
314
 
 
315
    _methodname = 'resolve_lp_path'
 
316
    _authenticated = False
 
317
 
 
318
    def __init__(self, path):
 
319
        if not path:
 
320
            raise errors.InvalidURL(path=path,
 
321
                                    extra="You must specify a project.")
 
322
        self.path = path
 
323
 
 
324
    def _request_params(self):
 
325
        """Return xmlrpc request parameters"""
 
326
        return (self.path,)