Merge lp:~adrien-delhorme/duplicity/hubic into lp:~duplicity-team/duplicity/0.7-series

Proposed by Adrien Delhorme
Status: Merged
Merged at revision: 1025
Proposed branch: lp:~adrien-delhorme/duplicity/hubic
Merge into: lp:~duplicity-team/duplicity/0.7-series
Diff against target: 421 lines (+368/-1)
5 files modified
bin/duplicity.1 (+29/-1)
duplicity/backends/hubicbackend.py (+62/-0)
duplicity/backends/pyrax_identity/__init__.py (+20/-0)
duplicity/backends/pyrax_identity/hubic.py (+256/-0)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~adrien-delhorme/duplicity/hubic
Reviewer Review Type Date Requested Status
edso Approve
Review via email: mp+240539@code.launchpad.net

Description of the change

Add Hubic support through pyrax and a custom pyrax_identity module.

To post a comment you must log in.
Revision history for this message
edso (ed.so) wrote :

hi,

1. please update the manpage bin/duplicity.1, sections Requirements, URL Format as needed

2. please use the prefix cf+hubic to make clear that it depends on cf support, pyrax will become cf soon as the latter is deprecated

.e.de/duply.net

review: Needs Fixing
lp:~adrien-delhorme/duplicity/hubic updated
1015. By Adrien Delhorme

Update man page
Add hubic identity module

Revision history for this message
Adrien Delhorme (adrien-delhorme) wrote :

Hi,

I've pushed some fixes yesterday. Let me know if I can improve something.

Revision history for this message
edso (ed.so) wrote :

please recheck.. at least in manpage you have things like

<<<<<<< TREE
 and
======

which are leftovers from a conflict.

also you can join hubic & cloudfiles under REQUIREMENTS, as they both need pyrax.

is "hubiC" a typo?

thx ..ede

Revision history for this message
edso (ed.so) wrote :

ok, sorry.. no leftovers. actually your manpage file conflicts with the branch. there were changes in between.

update your local branch and readd/merge your changes to the manpage again. then repush your branch to this location.

..ede

lp:~adrien-delhorme/duplicity/hubic updated
1016. By Adrien Delhorme

Update man page

1017. By Adrien Delhorme

Merge lp:duplicity onto lp:~adrien-delhorme/duplicity/hubic

Revision history for this message
Adrien Delhorme (adrien-delhorme) wrote :

I did the merge.

hubiC, with a capital C is the real name: it stands for "hub in the Cloud".

Revision history for this message
edso (ed.so) wrote :

thanks.. ede

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/duplicity.1'
2--- bin/duplicity.1 2014-10-30 17:22:39 +0000
3+++ bin/duplicity.1 2014-11-28 06:24:34 +0000
4@@ -64,7 +64,7 @@
5 .B Cloud Files Python API (deprecated)
6 - http://www.rackspace.com/knowledge_center/article/python-api-installation-for-cloud-files
7 .TP
8-.BR "cfpyrax backend" " (Rackspace Cloud)"
9+.BR "cfpyrax backend" " (Rackspace Cloud) and " "hubic backend" " (hubic.com)"
10 .B Rackspace CloudFiles Pyrax API
11 - http://docs.rackspace.com/sdks/guide/content/python.html
12 .TP
13@@ -1143,6 +1143,15 @@
14 hsi://user[:password]@other.host/some_dir
15 .RE
16 .PP
17+.B "hubiC"
18+.PP
19+.RS
20+cf+hubic://container_name
21+.PP
22+See also
23+.B "A NOTE ON HUBIC"
24+.RE
25+.PP
26 .B "IMAP email storage"
27 .PP
28 .RS
29@@ -1635,6 +1644,25 @@
30 Create Access Keys:
31 https://code.google.com/apis/console#:storage:legacy
32
33+.SH A NOTE ON HUBIC
34+The hubic backend requires the pyrax library to be installed on the system. See REQUIREMENTS above.
35+You will need to set your credentials for hubiC in a file called ~/.hubic_credentials, following this
36+pattern:
37+.PP
38+.RS
39+[hubic]
40+.br
41+email = your_email
42+.br
43+password = your_password
44+.br
45+client_id = api_client_id
46+.br
47+client_secret = api_secret_key
48+.br
49+redirect_uri = http://localhost/
50+.RE
51+
52 .SH A NOTE ON IMAP
53 An IMAP account can be used as a target for the upload. The userid may
54 be specified and the password will be requested.
55
56=== added file 'duplicity/backends/hubicbackend.py'
57--- duplicity/backends/hubicbackend.py 1970-01-01 00:00:00 +0000
58+++ duplicity/backends/hubicbackend.py 2014-11-28 06:24:34 +0000
59@@ -0,0 +1,62 @@
60+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
61+#
62+# Copyright 2013 J.P. Krauss <jkrauss@asymworks.com>
63+#
64+# This file is part of duplicity.
65+#
66+# Duplicity is free software; you can redistribute it and/or modify it
67+# under the terms of the GNU General Public License as published by the
68+# Free Software Foundation; either version 2 of the License, or (at your
69+# option) any later version.
70+#
71+# Duplicity is distributed in the hope that it will be useful, but
72+# WITHOUT ANY WARRANTY; without even the implied warranty of
73+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
74+# General Public License for more details.
75+#
76+# You should have received a copy of the GNU General Public License
77+# along with duplicity; if not, write to the Free Software Foundation,
78+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
79+
80+import os
81+
82+import duplicity.backend
83+from duplicity import log
84+from duplicity import util
85+from duplicity.errors import BackendException
86+from ._cf_pyrax import PyraxBackend
87+
88+
89+class HubicBackend(PyraxBackend):
90+ """
91+ Backend for Hubic using Pyrax
92+ """
93+ def __init__(self, parsed_url):
94+ try:
95+ import pyrax
96+ except ImportError:
97+ raise BackendException("This backend requires the pyrax "
98+ "library available from Rackspace.")
99+
100+ # Inform Pyrax that we're talking to Hubic
101+ pyrax.set_setting("identity_type", "duplicity.backends.pyrax_identity.hubic.HubicIdentity")
102+
103+ CREDENTIALS_FILE = os.path.expanduser("~/.hubic_credentials")
104+ if os.path.exists(CREDENTIALS_FILE):
105+ try:
106+ pyrax.set_credential_file(CREDENTIALS_FILE)
107+ except Exception as e:
108+ log.FatalError("Connection failed, please check your credentials: %s %s"
109+ % (e.__class__.__name__, util.uexc(e)),
110+ log.ErrorCode.connection_failed)
111+
112+ else:
113+ raise BackendException("No ~/.hubic_credentials file found.")
114+
115+ container = parsed_url.path.lstrip('/')
116+
117+ self.client_exc = pyrax.exceptions.ClientException
118+ self.nso_exc = pyrax.exceptions.NoSuchObject
119+ self.container = pyrax.cloudfiles.create_container(container)
120+
121+duplicity.backend.register_backend("cf+hubic", HubicBackend)
122
123=== added directory 'duplicity/backends/pyrax_identity'
124=== added file 'duplicity/backends/pyrax_identity/__init__.py'
125--- duplicity/backends/pyrax_identity/__init__.py 1970-01-01 00:00:00 +0000
126+++ duplicity/backends/pyrax_identity/__init__.py 2014-11-28 06:24:34 +0000
127@@ -0,0 +1,20 @@
128+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
129+#
130+# Copyright 2002 Ben Escoto <ben@emerose.org>
131+# Copyright 2007 Kenneth Loafman <kenneth@loafman.com>
132+#
133+# This file is part of duplicity.
134+#
135+# Duplicity is free software; you can redistribute it and/or modify it
136+# under the terms of the GNU General Public License as published by the
137+# Free Software Foundation; either version 2 of the License, or (at your
138+# option) any later version.
139+#
140+# Duplicity is distributed in the hope that it will be useful, but
141+# WITHOUT ANY WARRANTY; without even the implied warranty of
142+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
143+# General Public License for more details.
144+#
145+# You should have received a copy of the GNU General Public License
146+# along with duplicity; if not, write to the Free Software Foundation,
147+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
148\ No newline at end of file
149
150=== added file 'duplicity/backends/pyrax_identity/hubic.py'
151--- duplicity/backends/pyrax_identity/hubic.py 1970-01-01 00:00:00 +0000
152+++ duplicity/backends/pyrax_identity/hubic.py 2014-11-28 06:24:34 +0000
153@@ -0,0 +1,256 @@
154+#!/usr/bin/env python
155+# -*- coding: utf-8 -*-
156+# Copyright (c) 2014 Gu1
157+# Licensed under the MIT license
158+
159+import os
160+import pyrax
161+import pyrax.exceptions as exc
162+import requests
163+import re
164+import urlparse
165+import ConfigParser
166+import time
167+from pyrax.base_identity import BaseIdentity, Service
168+from requests.compat import quote, quote_plus
169+
170+
171+OAUTH_ENDPOINT = "https://api.hubic.com/oauth/"
172+API_ENDPOINT = "https://api.hubic.com/1.0/"
173+TOKENS_FILE = os.path.expanduser("~/.hubic_tokens")
174+
175+
176+class BearerTokenAuth(requests.auth.AuthBase):
177+ def __init__(self, token):
178+ self.token = token
179+
180+ def __call__(self, req):
181+ req.headers['Authorization'] = 'Bearer '+self.token
182+ return req
183+
184+
185+class HubicIdentity(BaseIdentity):
186+
187+ def _get_auth_endpoint(self):
188+ return ""
189+
190+ def set_credentials(self, email, password, client_id,
191+ client_secret, redirect_uri,
192+ authenticate=False):
193+ """Sets the username and password directly."""
194+ self._email = email
195+ self._password = password
196+ self._client_id = client_id
197+ self.tenant_id = client_id
198+ self._client_secret = client_secret
199+ self._redirect_uri = redirect_uri
200+ if authenticate:
201+ self.authenticate()
202+
203+ def _read_credential_file(self, cfg):
204+ """
205+ Parses the credential file with Rackspace-specific labels.
206+ """
207+ self._email = cfg.get("hubic", "email")
208+ self._password = cfg.get("hubic", "password")
209+ self._client_id = cfg.get("hubic", "client_id")
210+ self.tenant_id = self._client_id
211+ self._client_secret = cfg.get("hubic", "client_secret")
212+ self._redirect_uri = cfg.get("hubic", "redirect_uri")
213+
214+ def _parse_error(self, resp):
215+ if not 'location' in resp.headers:
216+ return None
217+ query = urlparse.urlsplit(resp.headers['location']).query
218+ qs = dict(urlparse.parse_qsl(query))
219+ return {'error': qs['error'], 'error_description': qs['error_description']}
220+
221+ def _get_access_token(self, code):
222+ r = requests.post(
223+ OAUTH_ENDPOINT+'token/',
224+ data={
225+ 'code': code,
226+ 'redirect_uri': self._redirect_uri,
227+ 'grant_type': 'authorization_code',
228+ },
229+ auth=(self._client_id, self._client_secret)
230+ )
231+ if r.status_code != 200:
232+ try:
233+ err = r.json()
234+ err['code'] = r.status_code
235+ except:
236+ err = {}
237+
238+ raise exc.AuthenticationFailed("Unable to get oauth access token, "
239+ "wrong client_id or client_secret ? (%s)" % str(err))
240+
241+ oauth_token = r.json()
242+
243+ config = ConfigParser.ConfigParser()
244+ config.read(TOKENS_FILE)
245+
246+ if not config.has_section("hubic"):
247+ config.add_section("hubic")
248+
249+ if oauth_token['access_token'] is not None:
250+ config.set("hubic", "access_token", oauth_token['access_token'])
251+ with open(TOKENS_FILE, 'wb') as configfile:
252+ config.write(configfile)
253+ else:
254+ raise exc.AuthenticationFailed("Unable to get oauth access token, wrong client_id or client_secret ? (%s)" % str(err))
255+
256+ if oauth_token['refresh_token'] is not None:
257+ config.set("hubic", "refresh_token", oauth_token['refresh_token'])
258+ with open(TOKENS_FILE, 'wb') as configfile:
259+ config.write(configfile)
260+ else:
261+ raise exc.AuthenticationFailed("Unable to get the refresh token.")
262+
263+ # removing username and password from .hubic_tokens
264+ if config.has_option("hubic", "email"):
265+ config.remove_option("hubic", "email")
266+ with open(TOKENS_FILE, 'wb') as configfile:
267+ config.write(configfile)
268+ print "username has been removed from the .hubic_tokens file sent to the CE."
269+ if config.has_option("hubic", "password"):
270+ config.remove_option("hubic", "password")
271+ with open(TOKENS_FILE, 'wb') as configfile:
272+ config.write(configfile)
273+ print "password has been removed from the .hubic_tokens file sent to the CE."
274+
275+ return oauth_token
276+
277+ def _refresh_access_token(self):
278+
279+ config = ConfigParser.ConfigParser()
280+ config.read(TOKENS_FILE)
281+ refresh_token = config.get("hubic", "refresh_token")
282+
283+ if refresh_token is None:
284+ raise exc.AuthenticationFailed("refresh_token is null. Not acquiered before ?")
285+
286+ success = False
287+ max_retries = 20
288+ retries = 0
289+ sleep_time = 30
290+ max_sleep_time = 3600
291+
292+ while retries < max_retries and not success:
293+ r = requests.post(
294+ OAUTH_ENDPOINT+'token/',
295+ data={
296+ 'refresh_token': refresh_token,
297+ 'grant_type': 'refresh_token',
298+ },
299+ auth=(self._client_id, self._client_secret)
300+ )
301+ if r.status_code != 200:
302+ if r.status_code == 509:
303+ print "status_code 509: attempt #", retries, " failed"
304+ retries += 1
305+ time.sleep(sleep_time)
306+ sleep_time = sleep_time * 2
307+ if sleep_time > max_sleep_time:
308+ sleep_time = max_sleep_time
309+ else:
310+ try:
311+ err = r.json()
312+ err['code'] = r.status_code
313+ except:
314+ err = {}
315+
316+ raise exc.AuthenticationFailed("Unable to get oauth access token, wrong client_id or client_secret ? (%s)" % str(err))
317+ else:
318+ success = True
319+
320+ if not success:
321+ raise exc.AuthenticationFailed("All the attempts failed to get the refresh token: status_code = 509: Bandwidth Limit Exceeded")
322+
323+ oauth_token = r.json()
324+
325+ if oauth_token['access_token'] is not None:
326+ return oauth_token
327+ else:
328+ raise exc.AuthenticationFailed("Unable to get oauth access token from json")
329+
330+ def authenticate(self):
331+ config = ConfigParser.ConfigParser()
332+ config.read(TOKENS_FILE)
333+
334+ if config.has_option("hubic", "refresh_token"):
335+ oauth_token = self._refresh_access_token()
336+ else:
337+ r = requests.get(
338+ OAUTH_ENDPOINT+'auth/?client_id={0}&redirect_uri={1}'
339+ '&scope=credentials.r,account.r&response_type=code&state={2}'.format(
340+ quote(self._client_id),
341+ quote_plus(self._redirect_uri),
342+ pyrax.utils.random_ascii() # csrf ? wut ?..
343+ ),
344+ allow_redirects=False
345+ )
346+ if r.status_code != 200:
347+ raise exc.AuthenticationFailed("Incorrect/unauthorized "
348+ "client_id (%s)" % str(self._parse_error(r)))
349+
350+ try:
351+ from lxml import html as lxml_html
352+ except ImportError:
353+ lxml_html = None
354+
355+ if lxml_html:
356+ oauth = lxml_html.document_fromstring(r.content).xpath('//input[@name="oauth"]')
357+ oauth = oauth[0].value if oauth else None
358+ else:
359+ oauth = re.search(r'<input\s+[^>]*name=[\'"]?oauth[\'"]?\s+[^>]*value=[\'"]?(\d+)[\'"]?>', r.content)
360+ oauth = oauth.group(1) if oauth else None
361+
362+ if not oauth:
363+ raise exc.AuthenticationFailed("Unable to get oauth_id from authorization page")
364+
365+ if self._email is None or self._password is None:
366+ raise exc.AuthenticationFailed("Cannot retrieve email and/or password. Please run expresslane-hubic-setup.sh")
367+
368+ r = requests.post(
369+ OAUTH_ENDPOINT+'auth/',
370+ data={
371+ 'action': 'accepted',
372+ 'oauth': oauth,
373+ 'login': self._email,
374+ 'user_pwd': self._password,
375+ 'account': 'r',
376+ 'credentials': 'r',
377+
378+ },
379+ allow_redirects=False
380+ )
381+
382+ try:
383+ query = urlparse.urlsplit(r.headers['location']).query
384+ code = dict(urlparse.parse_qsl(query))['code']
385+ except:
386+ raise exc.AuthenticationFailed("Unable to authorize client_id, invalid login/password ?")
387+
388+ oauth_token = self._get_access_token(code)
389+
390+ if oauth_token['token_type'].lower() != 'bearer':
391+ raise exc.AuthenticationFailed("Unsupported access token type")
392+
393+ r = requests.get(
394+ API_ENDPOINT+'account/credentials',
395+ auth=BearerTokenAuth(oauth_token['access_token']),
396+ )
397+
398+ swift_token = r.json()
399+ self.authenticated = True
400+ self.token = swift_token['token']
401+ self.expires = swift_token['expires']
402+ self.services['object_store'] = Service(self, {
403+ 'name': 'HubiC',
404+ 'type': 'cloudfiles',
405+ 'endpoints': [
406+ {'public_url': swift_token['endpoint']}
407+ ]
408+ })
409+ self.username = self.password = None
410
411=== modified file 'setup.py'
412--- setup.py 2014-10-27 02:27:36 +0000
413+++ setup.py 2014-11-28 06:24:34 +0000
414@@ -132,6 +132,7 @@
415 url="http://duplicity.nongnu.org/index.html",
416 packages = ['duplicity',
417 'duplicity.backends',
418+ 'duplicity.backends.pyrax_identity',
419 'testing',
420 'testing.functional',
421 'testing.overrides',

Subscribers

People subscribed via source and target branches