-
Notifications
You must be signed in to change notification settings - Fork 16
/
hub.py
312 lines (238 loc) · 9.08 KB
/
hub.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#
# Copyright (c) 2010-2013 Liraz Siri <[email protected]>
# Copyright (c) 2010 Alon Swartz <[email protected]>
#
# This file is part of TKLBAM (TurnKey GNU/Linux BAckup and Migration).
#
# TKLBAM is open source software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of
# the License, or (at your option) any later version.
#
"""TurnKey Hub API - Backup
Notes:
- Default URL: https://hub.turnkeylinux.org/api/backup/
- REST compliant (GET, POST, PUT)
- Responses are returned in application/json format
- API subkey must be sent in the header for all calls (except subkey/)
subkey/
method: GET
fields: apikey
return: subkey
credentials/
method: GET
fields:
return: accesskey, secretkey, usertoken, producttoken
record/create/
method: POST
fields: key, turnkey_version, [server_id]
return: backuprecord
record/update/
method: PUT
fields: address
return: <response_code>
record/<backup_id>/
method: GET
fields:
return: backuprecord
record/<backup_id>/
method: PUT
fields: key
return: backuprecord
records/
method: GET
fields:
return: [ backuprecord, ... ]
archive/
method: GET
fields: turnkey_version
return: archive_content
archive/timestamp/
method: GET
fields: turnkey_version
return: archive_timestamp
Exceptions::
400 Request.MissingHeader
400 Request.MissingArgument
401 HubAccount.Forbidden
400 HubAccount.InvalidApiKey
400 BackupAccount.InvalidSubKey
401 BackupAccount.MalformedSubKey
404 BackupAccount.NotFound
401 BackupAccount.NotSubscribed
404 BackupRecord.NotFound
401 BackupRecord.LimitExceeded
400 BackupRecord.ServerIDNotFound
404 BackupArchive.NotFound
"""
import os
import base64
import tempfile
from datetime import datetime
import executil
from pycurl_wrapper import API as _API
from utils import AttrDict
class Error(Exception):
def __init__(self, description, *args):
Exception.__init__(self, description, *args)
self.description = description
def __str__(self):
return self.description
class APIError(Error, _API.Error):
def __init__(self, code, name, description):
_API.Error.__init__(self, code, name, description)
class NotSubscribed(Error):
DESC = """\
Backups are not yet enabled for your TurnKey Hub account. Log
into the Hub and go to the "Backups" section for instructions."""
def __init__(self, desc=DESC):
Error.__init__(self, desc)
class InvalidBackupError(Error):
pass
class API(_API):
def request(self, method, url, attrs={}, headers={}):
try:
return _API.request(self, method, url, attrs, headers)
except self.Error, e:
if e.name == "BackupRecord.NotFound":
raise InvalidBackupError(e.description)
if e.name in ("BackupAccount.NotSubscribed",
"BackupAccount.NotFound"):
raise NotSubscribed()
raise APIError(e.code, e.name, e.description)
class BackupRecord(AttrDict):
@staticmethod
def _parse_datetime(s):
# return datetime("Y-M-D h:m:s")
if not s:
return None
return datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
def __init__(self, response):
self.key = response['key']
self.address = response['address']
self.backup_id = response['backup_id']
self.server_id = response['server_id']
self.profile_id = response['turnkey_version']
self.created = self._parse_datetime(response['date_created'])
self.updated = self._parse_datetime(response['date_updated'])
self.size = int(response['size']) # in MBs
self.label = response['description']
# no interface for this in tklbam, so not returned from hub
self.sessions = []
AttrDict.__init__(self)
class BaseCredentials(AttrDict):
def __init__(self):
self.type = self.__class__.__name__.lower()
class Credentials:
class IAMRole(BaseCredentials):
def __init__(self, accesskey, secretkey, sessiontoken, expiration):
self.accesskey = accesskey
self.secretkey = secretkey
self.sessiontoken = sessiontoken
self.expiration = expiration
BaseCredentials.__init__(self)
class IAMUser(BaseCredentials):
def __init__(self, accesskey, secretkey, sessiontoken):
self.accesskey = accesskey
self.secretkey = secretkey
self.sessiontoken = sessiontoken
BaseCredentials.__init__(self)
class DevPay(BaseCredentials):
def __init__(self, accesskey, secretkey, usertoken, producttoken):
self.accesskey = accesskey
self.secretkey = secretkey
self.usertoken = usertoken
self.producttoken = producttoken
BaseCredentials.__init__(self)
@classmethod
def from_dict(cls, d):
creds_types = dict((subcls.__name__.lower(), subcls)
for subcls in cls.__dict__.values()
if isinstance(subcls, type) and issubclass(subcls, BaseCredentials))
creds_type = d.get('type')
kwargs = d.copy()
try:
del kwargs['type']
except KeyError:
pass
# implicit devpay if no type (backwards compat with existing registry)
if not creds_type:
return cls.DevPay(**kwargs)
if creds_type not in creds_types:
raise Error('unknown credentials type "%s"' % creds_type)
return(creds_types[creds_type](**kwargs))
class Backups:
API_URL = os.getenv('TKLBAM_APIURL', 'https://hub.turnkeylinux.org/api/backup/')
Error = Error
class NotInitialized(Error):
pass
def __init__(self, subkey=None):
if subkey is None:
raise self.NotInitialized("no APIKEY - tklbam not linked to the Hub")
self.subkey = subkey
self.api = API()
def _api(self, method, uri, attrs={}):
headers = { 'subkey': str(self.subkey) }
return self.api.request(method, self.API_URL + uri, attrs, headers)
@classmethod
def get_sub_apikey(cls, apikey):
response = API().request('GET', cls.API_URL + 'subkey/', {'apikey': apikey})
return response['subkey']
def get_credentials(self):
response = self._api('GET', 'credentials/')
return Credentials.from_dict(response)
def get_new_profile(self, profile_id, profile_timestamp):
"""
Gets a profile for <profile_id> that is newer than <profile_timestamp>.
If there's a new profile, returns a ProfileArchive instance.
Otherwise returns None.
Raises an exception if no profile exists for profile_id.
"""
#attrs = {'profile_id': profile_id}
attrs = {'turnkey_version': profile_id} # quick hack until we fix the Hub API
response = self._api('GET', 'archive/timestamp/', attrs)
archive_timestamp = int(response['archive_timestamp'])
if profile_timestamp and profile_timestamp >= archive_timestamp:
return None
response = self._api('GET', 'archive/', attrs)
content = base64.urlsafe_b64decode(str(response['archive_content']))
fd, archive_path = tempfile.mkstemp(prefix="archive.")
fh = os.fdopen(fd, "w")
fh.write(content)
fh.close()
return ProfileArchive(profile_id, archive_path, archive_timestamp)
def new_backup_record(self, key, profile_id, server_id=None):
attrs = {'key': key, 'turnkey_version': profile_id}
if server_id:
attrs['server_id'] = server_id
response = self._api('POST', 'record/create/', attrs)
return BackupRecord(response)
def get_backup_record(self, backup_id):
response = self._api('GET', 'record/%s/' % backup_id)
return BackupRecord(response)
def set_backup_inprogress(self, backup_id, bool):
response = self._api('PUT', 'record/%s/inprogress/' % backup_id,
{'bool': bool})
return response
def update_key(self, backup_id, key):
response = self._api('PUT', 'record/%s/' % backup_id, {'key': key})
return BackupRecord(response)
def updated_backup(self, address):
response = self._api('PUT', 'record/update/', {'address': address})
return response
def list_backups(self):
response = self._api('GET', 'records/')
return map(lambda r: BackupRecord(r), response)
class ProfileArchive:
def __init__(self, profile_id, archive, timestamp):
self.path_archive = archive
self.timestamp = timestamp
self.profile_id = profile_id
def extract(self, path):
executil.system("tar -zxf %s -C %s" % (self.path_archive, path))
def __del__(self):
if os.path.exists(self.path_archive):
os.remove(self.path_archive)
from conf import Conf
if os.environ.get("TKLBAM_DUMMYHUB") or os.path.exists(os.path.join(Conf.DEFAULT_PATH, "dummyhub")):
from dummyhub import Backups