~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2009-07-18 21:09:00 UTC
  • mfrom: (4416.8.1 bzr.dev)
  • mto: This revision was merged to the branch mainline in revision 4547.
  • Revision ID: jelmer@samba.org-20090718210900-oht5snx25j1jyeha
Merge create_prefix fix necessary for bzr-svn.

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
19
18
import os
 
19
import socket
20
20
from urlparse import urlsplit, urlunsplit
21
21
import urllib
22
22
import xmlrpclib
23
23
 
24
 
import bzrlib.config
25
 
import bzrlib.errors as errors
 
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
    )
26
34
 
27
35
# for testing, do
28
36
'''
29
37
export BZR_LP_XMLRPC_URL=http://xmlrpc.staging.launchpad.net/bazaar/
30
38
'''
31
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
 
32
56
class LaunchpadService(object):
33
57
    """A service to talk to Launchpad via XMLRPC.
34
 
    
 
58
 
35
59
    See http://bazaar-vcs.org/Specs/LaunchpadRpc for the methods we can call.
36
60
    """
37
61
 
38
 
    # NB: this should always end in a slash to avoid xmlrpclib appending
 
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
39
71
    # '/RPC2'
40
 
    DEFAULT_SERVICE_URL = 'https://xmlrpc.launchpad.net/bazaar/'
 
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]
41
82
 
42
83
    transport = None
43
84
    registrant_email = None
44
85
    registrant_password = None
45
86
 
46
87
 
47
 
    def __init__(self, transport=None):
 
88
    def __init__(self, transport=None, lp_instance=None):
48
89
        """Construct a new service talking to the launchpad rpc server"""
 
90
        self._lp_instance = lp_instance
49
91
        if transport is None:
50
92
            uri_type = urllib.splittype(self.service_url)[0]
51
93
            if uri_type == 'https':
53
95
            else:
54
96
                transport = xmlrpclib.Transport()
55
97
            transport.user_agent = 'bzr/%s (xmlrpclib/%s)' \
56
 
                    % (bzrlib.__version__, xmlrpclib.__version__)
 
98
                    % (_bzrlib_version, xmlrpclib.__version__)
57
99
        self.transport = transport
58
100
 
59
101
 
66
108
        key = 'BZR_LP_XMLRPC_URL'
67
109
        if key in os.environ:
68
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)
69
116
        else:
70
117
            return self.DEFAULT_SERVICE_URL
71
118
 
72
 
    def get_proxy(self):
 
119
    def get_proxy(self, authenticated):
73
120
        """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, '', ''))
 
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
91
145
        return xmlrpclib.ServerProxy(url, transport=self.transport)
92
146
 
93
147
    def gather_user_credentials(self):
94
148
        """Get the password from the user."""
95
 
        config = bzrlib.config.GlobalConfig()
96
 
        self.registrant_email = config.user_email()
 
149
        the_config = config.GlobalConfig()
 
150
        self.registrant_email = the_config.user_email()
97
151
        if self.registrant_password is None:
 
152
            auth = config.AuthenticationConfig()
 
153
            scheme, hostinfo = urlsplit(self.service_url)[:2]
98
154
            prompt = 'launchpad.net password for %s: ' % \
99
155
                    self.registrant_email
100
 
            self.registrant_password = getpass(prompt)
 
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)
101
161
 
102
 
    def send_request(self, method_name, method_params):
103
 
        proxy = self.get_proxy()
104
 
        assert method_name
 
162
    def send_request(self, method_name, method_params, authenticated):
 
163
        proxy = self.get_proxy(authenticated)
105
164
        method = getattr(proxy, method_name)
106
165
        try:
107
166
            result = method(*method_params)
118
177
                # TODO: print more headers to help in tracking down failures
119
178
                raise errors.BzrError("xmlrpc protocol error connecting to %s: %s %s"
120
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)
121
184
        return result
122
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
 
123
220
 
124
221
class BaseRequest(object):
125
222
    """Base request for talking to a XMLRPC server."""
126
223
 
127
224
    # Set this to the XMLRPC method name.
128
225
    _methodname = None
 
226
    _authenticated = True
129
227
 
130
228
    def _request_params(self):
131
229
        """Return the arguments to pass to the method"""
137
235
        :param service: LaunchpadService indicating where to send
138
236
            the request and the authentication credentials.
139
237
        """
140
 
        return service.send_request(self._methodname, self._request_params())
 
238
        return service.send_request(self._methodname, self._request_params(),
 
239
                                    self._authenticated)
141
240
 
142
241
 
143
242
class DryRunLaunchpadService(LaunchpadService):
144
243
    """Service that just absorbs requests without sending to server.
145
 
    
 
244
 
146
245
    The dummy service does not need authentication.
147
246
    """
148
247
 
149
 
    def send_request(self, method_name, method_params):
 
248
    def send_request(self, method_name, method_params, authenticated):
150
249
        pass
151
250
 
152
251
    def gather_user_credentials(self):
165
264
                 author_email='',
166
265
                 product_name='',
167
266
                 ):
168
 
        assert branch_url
 
267
        if not branch_url:
 
268
            raise errors.InvalidURL(branch_url, "You need to specify a non-empty branch URL.")
169
269
        self.branch_url = branch_url
170
270
        if branch_name:
171
271
            self.branch_name = branch_name
199
299
    _methodname = 'link_branch_to_bug'
200
300
 
201
301
    def __init__(self, branch_url, bug_id):
202
 
        assert branch_url
203
302
        self.bug_id = bug_id
204
303
        self.branch_url = branch_url
205
304
 
208
307
        # This must match the parameter tuple expected by Launchpad for this
209
308
        # method
210
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,)