gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / contrib / swift.py @ 959
History | View | Annotate | Download (34.5 KB)
1 |
# swift.py -- Repo implementation atop OpenStack SWIFT
|
---|---|
2 |
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
|
3 |
#
|
4 |
# Author: Fabien Boucher <fabien.boucher@enovance.com>
|
5 |
#
|
6 |
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
|
7 |
# General Public License as public by the Free Software Foundation; version 2.0
|
8 |
# or (at your option) any later version. You can redistribute it and/or
|
9 |
# modify it under the terms of either of these two licenses.
|
10 |
#
|
11 |
# Unless required by applicable law or agreed to in writing, software
|
12 |
# distributed under the License is distributed on an "AS IS" BASIS,
|
13 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14 |
# See the License for the specific language governing permissions and
|
15 |
# limitations under the License.
|
16 |
#
|
17 |
# You should have received a copy of the licenses; if not, see
|
18 |
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
|
19 |
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
|
20 |
# License, Version 2.0.
|
21 |
#
|
22 |
|
23 |
"""Repo implementation atop OpenStack SWIFT."""
|
24 |
|
25 |
# TODO: Refactor to share more code with dulwich/repo.py.
|
26 |
# TODO(fbo): Second attempt to _send() must be notified via real log
|
27 |
# TODO(fbo): More logs for operations
|
28 |
|
29 |
import os |
30 |
import stat |
31 |
import zlib |
32 |
import tempfile |
33 |
import posixpath |
34 |
|
35 |
try:
|
36 |
import urlparse |
37 |
except ImportError: |
38 |
import urllib.parse as urlparse |
39 |
|
40 |
from io import BytesIO |
41 |
try:
|
42 |
from ConfigParser import ConfigParser |
43 |
except ImportError: |
44 |
from configparser import ConfigParser |
45 |
from geventhttpclient import HTTPClient |
46 |
|
47 |
from dulwich.greenthreads import ( |
48 |
GreenThreadsMissingObjectFinder, |
49 |
GreenThreadsObjectStoreIterator, |
50 |
) |
51 |
|
52 |
from dulwich.lru_cache import LRUSizeCache |
53 |
from dulwich.objects import ( |
54 |
Blob, |
55 |
Commit, |
56 |
Tree, |
57 |
Tag, |
58 |
S_ISGITLINK, |
59 |
) |
60 |
from dulwich.object_store import ( |
61 |
PackBasedObjectStore, |
62 |
PACKDIR, |
63 |
INFODIR, |
64 |
) |
65 |
from dulwich.pack import ( |
66 |
PackData, |
67 |
Pack, |
68 |
PackIndexer, |
69 |
PackStreamCopier, |
70 |
write_pack_header, |
71 |
compute_file_sha, |
72 |
iter_sha1, |
73 |
write_pack_index_v2, |
74 |
load_pack_index_file, |
75 |
read_pack_header, |
76 |
_compute_object_size, |
77 |
unpack_object, |
78 |
write_pack_object, |
79 |
) |
80 |
from dulwich.protocol import TCP_GIT_PORT |
81 |
from dulwich.refs import ( |
82 |
InfoRefsContainer, |
83 |
read_info_refs, |
84 |
write_info_refs, |
85 |
) |
86 |
from dulwich.repo import ( |
87 |
BaseRepo, |
88 |
OBJECTDIR, |
89 |
) |
90 |
from dulwich.server import ( |
91 |
Backend, |
92 |
TCPGitServer, |
93 |
) |
94 |
|
95 |
try:
|
96 |
from simplejson import loads as json_loads |
97 |
from simplejson import dumps as json_dumps |
98 |
except ImportError: |
99 |
from json import loads as json_loads |
100 |
from json import dumps as json_dumps |
101 |
|
102 |
import sys |
103 |
|
104 |
|
105 |
"""
|
106 |
# Configuration file sample
|
107 |
[swift]
|
108 |
# Authentication URL (Keystone or Swift)
|
109 |
auth_url = http://127.0.0.1:5000/v2.0
|
110 |
# Authentication version to use
|
111 |
auth_ver = 2
|
112 |
# The tenant and username separated by a semicolon
|
113 |
username = admin;admin
|
114 |
# The user password
|
115 |
password = pass
|
116 |
# The Object storage region to use (auth v2) (Default RegionOne)
|
117 |
region_name = RegionOne
|
118 |
# The Object storage endpoint URL to use (auth v2) (Default internalURL)
|
119 |
endpoint_type = internalURL
|
120 |
# Concurrency to use for parallel tasks (Default 10)
|
121 |
concurrency = 10
|
122 |
# Size of the HTTP pool (Default 10)
|
123 |
http_pool_length = 10
|
124 |
# Timeout delay for HTTP connections (Default 20)
|
125 |
http_timeout = 20
|
126 |
# Chunk size to read from pack (Bytes) (Default 12228)
|
127 |
chunk_length = 12228
|
128 |
# Cache size (MBytes) (Default 20)
|
129 |
cache_length = 20
|
130 |
"""
|
131 |
|
132 |
|
133 |
class PackInfoObjectStoreIterator(GreenThreadsObjectStoreIterator): |
134 |
|
135 |
def __len__(self): |
136 |
while len(self.finder.objects_to_send): |
137 |
for _ in range(0, len(self.finder.objects_to_send)): |
138 |
sha = self.finder.next()
|
139 |
self._shas.append(sha)
|
140 |
return len(self._shas) |
141 |
|
142 |
|
143 |
class PackInfoMissingObjectFinder(GreenThreadsMissingObjectFinder): |
144 |
|
145 |
def next(self): |
146 |
while True: |
147 |
if not self.objects_to_send: |
148 |
return None |
149 |
(sha, name, leaf) = self.objects_to_send.pop()
|
150 |
if sha not in self.sha_done: |
151 |
break
|
152 |
if not leaf: |
153 |
info = self.object_store.pack_info_get(sha)
|
154 |
if info[0] == Commit.type_num: |
155 |
self.add_todo([(info[2], "", False)]) |
156 |
elif info[0] == Tree.type_num: |
157 |
self.add_todo([tuple(i) for i in info[1]]) |
158 |
elif info[0] == Tag.type_num: |
159 |
self.add_todo([(info[1], None, False)]) |
160 |
if sha in self._tagged: |
161 |
self.add_todo([(self._tagged[sha], None, True)]) |
162 |
self.sha_done.add(sha)
|
163 |
self.progress("counting objects: %d\r" % len(self.sha_done)) |
164 |
return (sha, name)
|
165 |
|
166 |
|
167 |
def load_conf(path=None, file=None): |
168 |
"""Load configuration in global var CONF
|
169 |
|
170 |
:param path: The path to the configuration file
|
171 |
:param file: If provided read instead the file like object
|
172 |
"""
|
173 |
conf = ConfigParser() |
174 |
if file: |
175 |
conf.readfp(file)
|
176 |
return conf
|
177 |
confpath = None
|
178 |
if not path: |
179 |
try:
|
180 |
confpath = os.environ['DULWICH_SWIFT_CFG']
|
181 |
except KeyError: |
182 |
raise Exception("You need to specify a configuration file") |
183 |
else:
|
184 |
confpath = path |
185 |
if not os.path.isfile(confpath): |
186 |
raise Exception("Unable to read configuration file %s" % confpath) |
187 |
conf.read(confpath) |
188 |
return conf
|
189 |
|
190 |
|
191 |
def swift_load_pack_index(scon, filename): |
192 |
"""Read a pack index file from Swift
|
193 |
|
194 |
:param scon: a `SwiftConnector` instance
|
195 |
:param filename: Path to the index file objectise
|
196 |
:return: a `PackIndexer` instance
|
197 |
"""
|
198 |
f = scon.get_object(filename) |
199 |
try:
|
200 |
return load_pack_index_file(filename, f)
|
201 |
finally:
|
202 |
f.close() |
203 |
|
204 |
|
205 |
def pack_info_create(pack_data, pack_index): |
206 |
pack = Pack.from_objects(pack_data, pack_index) |
207 |
info = {} |
208 |
for obj in pack.iterobjects(): |
209 |
# Commit
|
210 |
if obj.type_num == Commit.type_num:
|
211 |
info[obj.id] = (obj.type_num, obj.parents, obj.tree) |
212 |
# Tree
|
213 |
elif obj.type_num == Tree.type_num:
|
214 |
shas = [(s, n, not stat.S_ISDIR(m)) for |
215 |
n, m, s in obj.items() if not S_ISGITLINK(m)] |
216 |
info[obj.id] = (obj.type_num, shas) |
217 |
# Blob
|
218 |
elif obj.type_num == Blob.type_num:
|
219 |
info[obj.id] = None
|
220 |
# Tag
|
221 |
elif obj.type_num == Tag.type_num:
|
222 |
info[obj.id] = (obj.type_num, obj.object[1])
|
223 |
return zlib.compress(json_dumps(info))
|
224 |
|
225 |
|
226 |
def load_pack_info(filename, scon=None, file=None): |
227 |
if not file: |
228 |
f = scon.get_object(filename) |
229 |
else:
|
230 |
f = file
|
231 |
if not f: |
232 |
return None |
233 |
try:
|
234 |
return json_loads(zlib.decompress(f.read()))
|
235 |
finally:
|
236 |
f.close() |
237 |
|
238 |
|
239 |
class SwiftException(Exception): |
240 |
pass
|
241 |
|
242 |
|
243 |
class SwiftConnector(object): |
244 |
"""A Connector to swift that manage authentication and errors catching
|
245 |
"""
|
246 |
|
247 |
def __init__(self, root, conf): |
248 |
""" Initialize a SwiftConnector
|
249 |
|
250 |
:param root: The swift container that will act as Git bare repository
|
251 |
:param conf: A ConfigParser Object
|
252 |
"""
|
253 |
self.conf = conf
|
254 |
self.auth_ver = self.conf.get("swift", "auth_ver") |
255 |
if self.auth_ver not in ["1", "2"]: |
256 |
raise NotImplementedError( |
257 |
"Wrong authentication version use either 1 or 2")
|
258 |
self.auth_url = self.conf.get("swift", "auth_url") |
259 |
self.user = self.conf.get("swift", "username") |
260 |
self.password = self.conf.get("swift", "password") |
261 |
self.concurrency = self.conf.getint('swift', 'concurrency') or 10 |
262 |
self.http_timeout = self.conf.getint('swift', 'http_timeout') or 20 |
263 |
self.http_pool_length = \
|
264 |
self.conf.getint('swift', 'http_pool_length') or 10 |
265 |
self.region_name = self.conf.get("swift", "region_name") or "RegionOne" |
266 |
self.endpoint_type = \
|
267 |
self.conf.get("swift", "endpoint_type") or "internalURL" |
268 |
self.cache_length = self.conf.getint("swift", "cache_length") or 20 |
269 |
self.chunk_length = self.conf.getint("swift", "chunk_length") or 12228 |
270 |
self.root = root
|
271 |
block_size = 1024 * 12 # 12KB |
272 |
if self.auth_ver == "1": |
273 |
self.storage_url, self.token = self.swift_auth_v1() |
274 |
else:
|
275 |
self.storage_url, self.token = self.swift_auth_v2() |
276 |
|
277 |
token_header = {'X-Auth-Token': str(self.token)} |
278 |
self.httpclient = \
|
279 |
HTTPClient.from_url(str(self.storage_url), |
280 |
concurrency=self.http_pool_length,
|
281 |
block_size=block_size, |
282 |
connection_timeout=self.http_timeout,
|
283 |
network_timeout=self.http_timeout,
|
284 |
headers=token_header) |
285 |
self.base_path = str( |
286 |
posixpath.join(urlparse.urlparse(self.storage_url).path, self.root)) |
287 |
|
288 |
def swift_auth_v1(self): |
289 |
self.user = self.user.replace(";", ":") |
290 |
auth_httpclient = HTTPClient.from_url( |
291 |
self.auth_url,
|
292 |
connection_timeout=self.http_timeout,
|
293 |
network_timeout=self.http_timeout,
|
294 |
) |
295 |
headers = {'X-Auth-User': self.user, |
296 |
'X-Auth-Key': self.password} |
297 |
path = urlparse.urlparse(self.auth_url).path
|
298 |
|
299 |
ret = auth_httpclient.request('GET', path, headers=headers)
|
300 |
|
301 |
# Should do something with redirections (301 in my case)
|
302 |
|
303 |
if ret.status_code < 200 or ret.status_code >= 300: |
304 |
raise SwiftException('AUTH v1.0 request failed on ' + |
305 |
'%s with error code %s (%s)'
|
306 |
% (str(auth_httpclient.get_base_url()) +
|
307 |
path, ret.status_code, |
308 |
str(ret.items())))
|
309 |
storage_url = ret['X-Storage-Url']
|
310 |
token = ret['X-Auth-Token']
|
311 |
return storage_url, token
|
312 |
|
313 |
def swift_auth_v2(self): |
314 |
self.tenant, self.user = self.user.split(';') |
315 |
auth_dict = {} |
316 |
auth_dict['auth'] = {'passwordCredentials': |
317 |
{ |
318 |
'username': self.user, |
319 |
'password': self.password, |
320 |
}, |
321 |
'tenantName': self.tenant} |
322 |
auth_json = json_dumps(auth_dict) |
323 |
headers = {'Content-Type': 'application/json'} |
324 |
auth_httpclient = HTTPClient.from_url( |
325 |
self.auth_url,
|
326 |
connection_timeout=self.http_timeout,
|
327 |
network_timeout=self.http_timeout,
|
328 |
) |
329 |
path = urlparse.urlparse(self.auth_url).path
|
330 |
if not path.endswith('tokens'): |
331 |
path = posixpath.join(path, 'tokens')
|
332 |
ret = auth_httpclient.request('POST', path,
|
333 |
body=auth_json, |
334 |
headers=headers) |
335 |
|
336 |
if ret.status_code < 200 or ret.status_code >= 300: |
337 |
raise SwiftException('AUTH v2.0 request failed on ' + |
338 |
'%s with error code %s (%s)'
|
339 |
% (str(auth_httpclient.get_base_url()) +
|
340 |
path, ret.status_code, |
341 |
str(ret.items())))
|
342 |
auth_ret_json = json_loads(ret.read()) |
343 |
token = auth_ret_json['access']['token']['id'] |
344 |
catalogs = auth_ret_json['access']['serviceCatalog'] |
345 |
object_store = [o_store for o_store in catalogs if |
346 |
o_store['type'] == 'object-store'][0] |
347 |
endpoints = object_store['endpoints']
|
348 |
endpoint = [endp for endp in endpoints if |
349 |
endp["region"] == self.region_name][0] |
350 |
return endpoint[self.endpoint_type], token |
351 |
|
352 |
def test_root_exists(self): |
353 |
"""Check that Swift container exist
|
354 |
|
355 |
:return: True if exist or None it not
|
356 |
"""
|
357 |
ret = self.httpclient.request('HEAD', self.base_path) |
358 |
if ret.status_code == 404: |
359 |
return None |
360 |
if ret.status_code < 200 or ret.status_code > 300: |
361 |
raise SwiftException('HEAD request failed with error code %s' |
362 |
% ret.status_code) |
363 |
return True |
364 |
|
365 |
def create_root(self): |
366 |
"""Create the Swift container
|
367 |
|
368 |
:raise: `SwiftException` if unable to create
|
369 |
"""
|
370 |
if not self.test_root_exists(): |
371 |
ret = self.httpclient.request('PUT', self.base_path) |
372 |
if ret.status_code < 200 or ret.status_code > 300: |
373 |
raise SwiftException('PUT request failed with error code %s' |
374 |
% ret.status_code) |
375 |
|
376 |
def get_container_objects(self): |
377 |
"""Retrieve objects list in a container
|
378 |
|
379 |
:return: A list of dict that describe objects
|
380 |
or None if container does not exist
|
381 |
"""
|
382 |
qs = '?format=json'
|
383 |
path = self.base_path + qs
|
384 |
ret = self.httpclient.request('GET', path) |
385 |
if ret.status_code == 404: |
386 |
return None |
387 |
if ret.status_code < 200 or ret.status_code > 300: |
388 |
raise SwiftException('GET request failed with error code %s' |
389 |
% ret.status_code) |
390 |
content = ret.read() |
391 |
return json_loads(content)
|
392 |
|
393 |
def get_object_stat(self, name): |
394 |
"""Retrieve object stat
|
395 |
|
396 |
:param name: The object name
|
397 |
:return: A dict that describe the object
|
398 |
or None if object does not exist
|
399 |
"""
|
400 |
path = self.base_path + '/' + name |
401 |
ret = self.httpclient.request('HEAD', path) |
402 |
if ret.status_code == 404: |
403 |
return None |
404 |
if ret.status_code < 200 or ret.status_code > 300: |
405 |
raise SwiftException('HEAD request failed with error code %s' |
406 |
% ret.status_code) |
407 |
resp_headers = {} |
408 |
for header, value in ret.items(): |
409 |
resp_headers[header.lower()] = value |
410 |
return resp_headers
|
411 |
|
412 |
def put_object(self, name, content): |
413 |
"""Put an object
|
414 |
|
415 |
:param name: The object name
|
416 |
:param content: A file object
|
417 |
:raise: `SwiftException` if unable to create
|
418 |
"""
|
419 |
content.seek(0)
|
420 |
data = content.read() |
421 |
path = self.base_path + '/' + name |
422 |
headers = {'Content-Length': str(len(data))} |
423 |
|
424 |
def _send(): |
425 |
ret = self.httpclient.request('PUT', path, |
426 |
body=data, |
427 |
headers=headers) |
428 |
return ret
|
429 |
|
430 |
try:
|
431 |
# Sometime got Broken Pipe - Dirty workaround
|
432 |
ret = _send() |
433 |
except Exception: |
434 |
# Second attempt work
|
435 |
ret = _send() |
436 |
|
437 |
if ret.status_code < 200 or ret.status_code > 300: |
438 |
raise SwiftException('PUT request failed with error code %s' |
439 |
% ret.status_code) |
440 |
|
441 |
def get_object(self, name, range=None): |
442 |
"""Retrieve an object
|
443 |
|
444 |
:param name: The object name
|
445 |
:param range: A string range like "0-10" to
|
446 |
retrieve specified bytes in object content
|
447 |
:return: A file like instance
|
448 |
or bytestring if range is specified
|
449 |
"""
|
450 |
headers = {} |
451 |
if range: |
452 |
headers['Range'] = 'bytes=%s' % range |
453 |
path = self.base_path + '/' + name |
454 |
ret = self.httpclient.request('GET', path, headers=headers) |
455 |
if ret.status_code == 404: |
456 |
return None |
457 |
if ret.status_code < 200 or ret.status_code > 300: |
458 |
raise SwiftException('GET request failed with error code %s' |
459 |
% ret.status_code) |
460 |
content = ret.read() |
461 |
|
462 |
if range: |
463 |
return content
|
464 |
return BytesIO(content)
|
465 |
|
466 |
def del_object(self, name): |
467 |
"""Delete an object
|
468 |
|
469 |
:param name: The object name
|
470 |
:raise: `SwiftException` if unable to delete
|
471 |
"""
|
472 |
path = self.base_path + '/' + name |
473 |
ret = self.httpclient.request('DELETE', path) |
474 |
if ret.status_code < 200 or ret.status_code > 300: |
475 |
raise SwiftException('DELETE request failed with error code %s' |
476 |
% ret.status_code) |
477 |
|
478 |
def del_root(self): |
479 |
"""Delete the root container by removing container content
|
480 |
|
481 |
:raise: `SwiftException` if unable to delete
|
482 |
"""
|
483 |
for obj in self.get_container_objects(): |
484 |
self.del_object(obj['name']) |
485 |
ret = self.httpclient.request('DELETE', self.base_path) |
486 |
if ret.status_code < 200 or ret.status_code > 300: |
487 |
raise SwiftException('DELETE request failed with error code %s' |
488 |
% ret.status_code) |
489 |
|
490 |
|
491 |
class SwiftPackReader(object): |
492 |
"""A SwiftPackReader that mimic read and sync method
|
493 |
|
494 |
The reader allows to read a specified amount of bytes from
|
495 |
a given offset of a Swift object. A read offset is kept internaly.
|
496 |
The reader will read from Swift a specified amount of data to complete
|
497 |
its internal buffer. chunk_length specifiy the amount of data
|
498 |
to read from Swift.
|
499 |
"""
|
500 |
|
501 |
def __init__(self, scon, filename, pack_length): |
502 |
"""Initialize a SwiftPackReader
|
503 |
|
504 |
:param scon: a `SwiftConnector` instance
|
505 |
:param filename: the pack filename
|
506 |
:param pack_length: The size of the pack object
|
507 |
"""
|
508 |
self.scon = scon
|
509 |
self.filename = filename
|
510 |
self.pack_length = pack_length
|
511 |
self.offset = 0 |
512 |
self.base_offset = 0 |
513 |
self.buff = b'' |
514 |
self.buff_length = self.scon.chunk_length |
515 |
|
516 |
def _read(self, more=False): |
517 |
if more:
|
518 |
self.buff_length = self.buff_length * 2 |
519 |
l = self.base_offset
|
520 |
r = min(self.base_offset + self.buff_length, self.pack_length) |
521 |
ret = self.scon.get_object(self.filename, range="%s-%s" % (l, r)) |
522 |
self.buff = ret
|
523 |
|
524 |
def read(self, length): |
525 |
"""Read a specified amount of Bytes form the pack object
|
526 |
|
527 |
:param length: amount of bytes to read
|
528 |
:return: bytestring
|
529 |
"""
|
530 |
end = self.offset+length
|
531 |
if self.base_offset + end > self.pack_length: |
532 |
data = self.buff[self.offset:] |
533 |
self.offset = end
|
534 |
return data
|
535 |
if end > len(self.buff): |
536 |
# Need to read more from swift
|
537 |
self._read(more=True) |
538 |
return self.read(length) |
539 |
data = self.buff[self.offset:end] |
540 |
self.offset = end
|
541 |
return data
|
542 |
|
543 |
def seek(self, offset): |
544 |
"""Seek to a specified offset
|
545 |
|
546 |
:param offset: the offset to seek to
|
547 |
"""
|
548 |
self.base_offset = offset
|
549 |
self._read()
|
550 |
self.offset = 0 |
551 |
|
552 |
def read_checksum(self): |
553 |
"""Read the checksum from the pack
|
554 |
|
555 |
:return: the checksum bytestring
|
556 |
"""
|
557 |
return self.scon.get_object(self.filename, range="-20") |
558 |
|
559 |
|
560 |
class SwiftPackData(PackData): |
561 |
"""The data contained in a packfile.
|
562 |
|
563 |
We use the SwiftPackReader to read bytes from packs stored in Swift
|
564 |
using the Range header feature of Swift.
|
565 |
"""
|
566 |
|
567 |
def __init__(self, scon, filename): |
568 |
""" Initialize a SwiftPackReader
|
569 |
|
570 |
:param scon: a `SwiftConnector` instance
|
571 |
:param filename: the pack filename
|
572 |
"""
|
573 |
self.scon = scon
|
574 |
self._filename = filename
|
575 |
self._header_size = 12 |
576 |
headers = self.scon.get_object_stat(self._filename) |
577 |
self.pack_length = int(headers['content-length']) |
578 |
pack_reader = SwiftPackReader(self.scon, self._filename, |
579 |
self.pack_length)
|
580 |
(version, self._num_objects) = read_pack_header(pack_reader.read)
|
581 |
self._offset_cache = LRUSizeCache(1024*1024*self.scon.cache_length, |
582 |
compute_size=_compute_object_size) |
583 |
self.pack = None |
584 |
|
585 |
def get_object_at(self, offset): |
586 |
if offset in self._offset_cache: |
587 |
return self._offset_cache[offset] |
588 |
assert offset >= self._header_size |
589 |
pack_reader = SwiftPackReader(self.scon, self._filename, |
590 |
self.pack_length)
|
591 |
pack_reader.seek(offset) |
592 |
unpacked, _ = unpack_object(pack_reader.read) |
593 |
return (unpacked.pack_type_num, unpacked._obj())
|
594 |
|
595 |
def get_stored_checksum(self): |
596 |
pack_reader = SwiftPackReader(self.scon, self._filename, |
597 |
self.pack_length)
|
598 |
return pack_reader.read_checksum()
|
599 |
|
600 |
def close(self): |
601 |
pass
|
602 |
|
603 |
|
604 |
class SwiftPack(Pack): |
605 |
"""A Git pack object.
|
606 |
|
607 |
Same implementation as pack.Pack except that _idx_load and
|
608 |
_data_load are bounded to Swift version of load_pack_index and
|
609 |
PackData.
|
610 |
"""
|
611 |
|
612 |
def __init__(self, *args, **kwargs): |
613 |
self.scon = kwargs['scon'] |
614 |
del kwargs['scon'] |
615 |
super(SwiftPack, self).__init__(*args, **kwargs) |
616 |
self._pack_info_path = self._basename + '.info' |
617 |
self._pack_info = None |
618 |
self._pack_info_load = lambda: load_pack_info(self._pack_info_path, |
619 |
self.scon)
|
620 |
self._idx_load = lambda: swift_load_pack_index(self.scon, |
621 |
self._idx_path)
|
622 |
self._data_load = lambda: SwiftPackData(self.scon, self._data_path) |
623 |
|
624 |
@property
|
625 |
def pack_info(self): |
626 |
"""The pack data object being used."""
|
627 |
if self._pack_info is None: |
628 |
self._pack_info = self._pack_info_load() |
629 |
return self._pack_info |
630 |
|
631 |
|
632 |
class SwiftObjectStore(PackBasedObjectStore): |
633 |
"""A Swift Object Store
|
634 |
|
635 |
Allow to manage a bare Git repository from Openstack Swift.
|
636 |
This object store only supports pack files and not loose objects.
|
637 |
"""
|
638 |
def __init__(self, scon): |
639 |
"""Open a Swift object store.
|
640 |
|
641 |
:param scon: A `SwiftConnector` instance
|
642 |
"""
|
643 |
super(SwiftObjectStore, self).__init__() |
644 |
self.scon = scon
|
645 |
self.root = self.scon.root |
646 |
self.pack_dir = posixpath.join(OBJECTDIR, PACKDIR)
|
647 |
self._alternates = None |
648 |
|
649 |
@property
|
650 |
def packs(self): |
651 |
"""List with pack objects."""
|
652 |
if not self._pack_cache: |
653 |
self._update_pack_cache()
|
654 |
return self._pack_cache.values() |
655 |
|
656 |
def _update_pack_cache(self): |
657 |
for pack in self._load_packs(): |
658 |
self._pack_cache[pack._basename] = pack
|
659 |
|
660 |
def _iter_loose_objects(self): |
661 |
"""Loose objects are not supported by this repository
|
662 |
"""
|
663 |
return []
|
664 |
|
665 |
def iter_shas(self, finder): |
666 |
"""An iterator over pack's ObjectStore.
|
667 |
|
668 |
:return: a `ObjectStoreIterator` or `GreenThreadsObjectStoreIterator`
|
669 |
instance if gevent is enabled
|
670 |
"""
|
671 |
shas = iter(finder.next, None) |
672 |
return PackInfoObjectStoreIterator(
|
673 |
self, shas, finder, self.scon.concurrency) |
674 |
|
675 |
def find_missing_objects(self, *args, **kwargs): |
676 |
kwargs['concurrency'] = self.scon.concurrency |
677 |
return PackInfoMissingObjectFinder(self, *args, **kwargs) |
678 |
|
679 |
def _load_packs(self): |
680 |
"""Load all packs from Swift
|
681 |
|
682 |
:return: a list of `SwiftPack` instances
|
683 |
"""
|
684 |
objects = self.scon.get_container_objects()
|
685 |
pack_files = [o['name'].replace(".pack", "") |
686 |
for o in objects if o['name'].endswith(".pack")] |
687 |
return [SwiftPack(pack, scon=self.scon) for pack in pack_files] |
688 |
|
689 |
def pack_info_get(self, sha): |
690 |
for pack in self.packs: |
691 |
if sha in pack: |
692 |
return pack.pack_info[sha]
|
693 |
|
694 |
def _collect_ancestors(self, heads, common=set()): |
695 |
def _find_parents(commit): |
696 |
for pack in self.packs: |
697 |
if commit in pack: |
698 |
try:
|
699 |
parents = pack.pack_info[commit][1]
|
700 |
except KeyError: |
701 |
# Seems to have no parents
|
702 |
return []
|
703 |
return parents
|
704 |
|
705 |
bases = set()
|
706 |
commits = set()
|
707 |
queue = [] |
708 |
queue.extend(heads) |
709 |
while queue:
|
710 |
e = queue.pop(0)
|
711 |
if e in common: |
712 |
bases.add(e) |
713 |
elif e not in commits: |
714 |
commits.add(e) |
715 |
parents = _find_parents(e) |
716 |
queue.extend(parents) |
717 |
return (commits, bases)
|
718 |
|
719 |
def add_pack(self): |
720 |
"""Add a new pack to this object store.
|
721 |
|
722 |
:return: Fileobject to write to and a commit function to
|
723 |
call when the pack is finished.
|
724 |
"""
|
725 |
f = BytesIO() |
726 |
|
727 |
def commit(): |
728 |
f.seek(0)
|
729 |
pack = PackData(file=f, filename="")
|
730 |
entries = pack.sorted_entries() |
731 |
if len(entries): |
732 |
basename = posixpath.join(self.pack_dir,
|
733 |
"pack-%s" %
|
734 |
iter_sha1(entry[0] for |
735 |
entry in entries))
|
736 |
index = BytesIO() |
737 |
write_pack_index_v2(index, entries, pack.get_stored_checksum()) |
738 |
self.scon.put_object(basename + ".pack", f) |
739 |
f.close() |
740 |
self.scon.put_object(basename + ".idx", index) |
741 |
index.close() |
742 |
final_pack = SwiftPack(basename, scon=self.scon)
|
743 |
final_pack.check_length_and_checksum() |
744 |
self._add_known_pack(basename, final_pack)
|
745 |
return final_pack
|
746 |
else:
|
747 |
return None |
748 |
|
749 |
def abort(): |
750 |
pass
|
751 |
return f, commit, abort
|
752 |
|
753 |
def add_object(self, obj): |
754 |
self.add_objects([(obj, None), ]) |
755 |
|
756 |
def _pack_cache_stale(self): |
757 |
return False |
758 |
|
759 |
def _get_loose_object(self, sha): |
760 |
return None |
761 |
|
762 |
def add_thin_pack(self, read_all, read_some): |
763 |
"""Read a thin pack
|
764 |
|
765 |
Read it from a stream and complete it in a temporary file.
|
766 |
Then the pack and the corresponding index file are uploaded to Swift.
|
767 |
"""
|
768 |
fd, path = tempfile.mkstemp(prefix='tmp_pack_')
|
769 |
f = os.fdopen(fd, 'w+b')
|
770 |
try:
|
771 |
indexer = PackIndexer(f, resolve_ext_ref=self.get_raw)
|
772 |
copier = PackStreamCopier(read_all, read_some, f, |
773 |
delta_iter=indexer) |
774 |
copier.verify() |
775 |
return self._complete_thin_pack(f, path, copier, indexer) |
776 |
finally:
|
777 |
f.close() |
778 |
os.unlink(path) |
779 |
|
780 |
def _complete_thin_pack(self, f, path, copier, indexer): |
781 |
entries = list(indexer)
|
782 |
|
783 |
# Update the header with the new number of objects.
|
784 |
f.seek(0)
|
785 |
write_pack_header(f, len(entries) + len(indexer.ext_refs())) |
786 |
|
787 |
# Must flush before reading (http://bugs.python.org/issue3207)
|
788 |
f.flush() |
789 |
|
790 |
# Rescan the rest of the pack, computing the SHA with the new header.
|
791 |
new_sha = compute_file_sha(f, end_ofs=-20)
|
792 |
|
793 |
# Must reposition before writing (http://bugs.python.org/issue3207)
|
794 |
f.seek(0, os.SEEK_CUR)
|
795 |
|
796 |
# Complete the pack.
|
797 |
for ext_sha in indexer.ext_refs(): |
798 |
assert len(ext_sha) == 20 |
799 |
type_num, data = self.get_raw(ext_sha)
|
800 |
offset = f.tell() |
801 |
crc32 = write_pack_object(f, type_num, data, sha=new_sha) |
802 |
entries.append((ext_sha, offset, crc32)) |
803 |
pack_sha = new_sha.digest() |
804 |
f.write(pack_sha) |
805 |
f.flush() |
806 |
|
807 |
# Move the pack in.
|
808 |
entries.sort() |
809 |
pack_base_name = posixpath.join( |
810 |
self.pack_dir,
|
811 |
'pack-' + iter_sha1(e[0] for e in entries).decode(sys.getfilesystemencoding())) |
812 |
self.scon.put_object(pack_base_name + '.pack', f) |
813 |
|
814 |
# Write the index.
|
815 |
filename = pack_base_name + '.idx'
|
816 |
index_file = BytesIO() |
817 |
write_pack_index_v2(index_file, entries, pack_sha) |
818 |
self.scon.put_object(filename, index_file)
|
819 |
|
820 |
# Write pack info.
|
821 |
f.seek(0)
|
822 |
pack_data = PackData(filename="", file=f)
|
823 |
index_file.seek(0)
|
824 |
pack_index = load_pack_index_file('', index_file)
|
825 |
serialized_pack_info = pack_info_create(pack_data, pack_index) |
826 |
f.close() |
827 |
index_file.close() |
828 |
pack_info_file = BytesIO(serialized_pack_info) |
829 |
filename = pack_base_name + '.info'
|
830 |
self.scon.put_object(filename, pack_info_file)
|
831 |
pack_info_file.close() |
832 |
|
833 |
# Add the pack to the store and return it.
|
834 |
final_pack = SwiftPack(pack_base_name, scon=self.scon)
|
835 |
final_pack.check_length_and_checksum() |
836 |
self._add_known_pack(pack_base_name, final_pack)
|
837 |
return final_pack
|
838 |
|
839 |
|
840 |
class SwiftInfoRefsContainer(InfoRefsContainer): |
841 |
"""Manage references in info/refs object.
|
842 |
"""
|
843 |
|
844 |
def __init__(self, scon, store): |
845 |
self.scon = scon
|
846 |
self.filename = 'info/refs' |
847 |
self.store = store
|
848 |
f = self.scon.get_object(self.filename) |
849 |
if not f: |
850 |
f = BytesIO(b'')
|
851 |
super(SwiftInfoRefsContainer, self).__init__(f) |
852 |
|
853 |
def _load_check_ref(self, name, old_ref): |
854 |
self._check_refname(name)
|
855 |
f = self.scon.get_object(self.filename) |
856 |
if not f: |
857 |
return {}
|
858 |
refs = read_info_refs(f) |
859 |
if old_ref is not None: |
860 |
if refs[name] != old_ref:
|
861 |
return False |
862 |
return refs
|
863 |
|
864 |
def _write_refs(self, refs): |
865 |
f = BytesIO() |
866 |
f.writelines(write_info_refs(refs, self.store))
|
867 |
self.scon.put_object(self.filename, f) |
868 |
|
869 |
def set_if_equals(self, name, old_ref, new_ref): |
870 |
"""Set a refname to new_ref only if it currently equals old_ref.
|
871 |
"""
|
872 |
if name == 'HEAD': |
873 |
return True |
874 |
refs = self._load_check_ref(name, old_ref)
|
875 |
if not isinstance(refs, dict): |
876 |
return False |
877 |
refs[name] = new_ref |
878 |
self._write_refs(refs)
|
879 |
self._refs[name] = new_ref
|
880 |
return True |
881 |
|
882 |
def remove_if_equals(self, name, old_ref): |
883 |
"""Remove a refname only if it currently equals old_ref.
|
884 |
"""
|
885 |
if name == 'HEAD': |
886 |
return True |
887 |
refs = self._load_check_ref(name, old_ref)
|
888 |
if not isinstance(refs, dict): |
889 |
return False |
890 |
del refs[name]
|
891 |
self._write_refs(refs)
|
892 |
del self._refs[name] |
893 |
return True |
894 |
|
895 |
def allkeys(self): |
896 |
try:
|
897 |
self._refs['HEAD'] = self._refs['refs/heads/master'] |
898 |
except KeyError: |
899 |
pass
|
900 |
return self._refs.keys() |
901 |
|
902 |
|
903 |
class SwiftRepo(BaseRepo): |
904 |
|
905 |
def __init__(self, root, conf): |
906 |
"""Init a Git bare Repository on top of a Swift container.
|
907 |
|
908 |
References are managed in info/refs objects by
|
909 |
`SwiftInfoRefsContainer`. The root attribute is the Swift
|
910 |
container that contain the Git bare repository.
|
911 |
|
912 |
:param root: The container which contains the bare repo
|
913 |
:param conf: A ConfigParser object
|
914 |
"""
|
915 |
self.root = root.lstrip('/') |
916 |
self.conf = conf
|
917 |
self.scon = SwiftConnector(self.root, self.conf) |
918 |
objects = self.scon.get_container_objects()
|
919 |
if not objects: |
920 |
raise Exception('There is not any GIT repo here : %s' % self.root) |
921 |
objects = [o['name'].split('/')[0] for o in objects] |
922 |
if OBJECTDIR not in objects: |
923 |
raise Exception('This repository (%s) is not bare.' % self.root) |
924 |
self.bare = True |
925 |
self._controldir = self.root |
926 |
object_store = SwiftObjectStore(self.scon)
|
927 |
refs = SwiftInfoRefsContainer(self.scon, object_store)
|
928 |
BaseRepo.__init__(self, object_store, refs)
|
929 |
|
930 |
def _determine_file_mode(self): |
931 |
"""Probe the file-system to determine whether permissions can be trusted.
|
932 |
|
933 |
:return: True if permissions can be trusted, False otherwise.
|
934 |
"""
|
935 |
return False |
936 |
|
937 |
def _put_named_file(self, filename, contents): |
938 |
"""Put an object in a Swift container
|
939 |
|
940 |
:param filename: the path to the object to put on Swift
|
941 |
:param contents: the content as bytestring
|
942 |
"""
|
943 |
f = BytesIO() |
944 |
f.write(contents) |
945 |
self.scon.put_object(filename, f)
|
946 |
f.close() |
947 |
|
948 |
@classmethod
|
949 |
def init_bare(cls, scon, conf): |
950 |
"""Create a new bare repository.
|
951 |
|
952 |
:param scon: a `SwiftConnector` instance
|
953 |
:param conf: a ConfigParser object
|
954 |
:return: a `SwiftRepo` instance
|
955 |
"""
|
956 |
scon.create_root() |
957 |
for obj in [posixpath.join(OBJECTDIR, PACKDIR), |
958 |
posixpath.join(INFODIR, 'refs')]:
|
959 |
scon.put_object(obj, BytesIO(b''))
|
960 |
ret = cls(scon.root, conf) |
961 |
ret._init_files(True)
|
962 |
return ret
|
963 |
|
964 |
|
965 |
class SwiftSystemBackend(Backend): |
966 |
|
967 |
def __init__(self, logger, conf): |
968 |
self.conf = conf
|
969 |
self.logger = logger
|
970 |
|
971 |
def open_repository(self, path): |
972 |
self.logger.info('opening repository at %s', path) |
973 |
return SwiftRepo(path, self.conf) |
974 |
|
975 |
|
976 |
def cmd_daemon(args): |
977 |
"""Entry point for starting a TCP git server."""
|
978 |
import optparse |
979 |
parser = optparse.OptionParser() |
980 |
parser.add_option("-l", "--listen_address", dest="listen_address", |
981 |
default="127.0.0.1",
|
982 |
help="Binding IP address.")
|
983 |
parser.add_option("-p", "--port", dest="port", type=int, |
984 |
default=TCP_GIT_PORT, |
985 |
help="Binding TCP port.")
|
986 |
parser.add_option("-c", "--swift_config", dest="swift_config", |
987 |
default="",
|
988 |
help="Path to the configuration file for Swift backend.")
|
989 |
options, args = parser.parse_args(args) |
990 |
|
991 |
try:
|
992 |
import gevent |
993 |
import geventhttpclient |
994 |
except ImportError: |
995 |
print("gevent and geventhttpclient libraries are mandatory "
|
996 |
" for use the Swift backend.")
|
997 |
sys.exit(1)
|
998 |
import gevent.monkey |
999 |
gevent.monkey.patch_socket() |
1000 |
from dulwich.contrib.swift import load_conf |
1001 |
from dulwich import log_utils |
1002 |
logger = log_utils.getLogger(__name__) |
1003 |
conf = load_conf(options.swift_config) |
1004 |
backend = SwiftSystemBackend(logger, conf) |
1005 |
|
1006 |
log_utils.default_logging_config() |
1007 |
server = TCPGitServer(backend, options.listen_address, |
1008 |
port=options.port) |
1009 |
server.serve_forever() |
1010 |
|
1011 |
|
1012 |
def cmd_init(args): |
1013 |
import optparse |
1014 |
parser = optparse.OptionParser() |
1015 |
parser.add_option("-c", "--swift_config", dest="swift_config", |
1016 |
default="",
|
1017 |
help="Path to the configuration file for Swift backend.")
|
1018 |
options, args = parser.parse_args(args) |
1019 |
|
1020 |
conf = load_conf(options.swift_config) |
1021 |
if args == []:
|
1022 |
parser.error("missing repository name")
|
1023 |
repo = args[0]
|
1024 |
scon = SwiftConnector(repo, conf) |
1025 |
SwiftRepo.init_bare(scon, conf) |
1026 |
|
1027 |
|
1028 |
def main(argv=sys.argv): |
1029 |
commands = { |
1030 |
"init": cmd_init,
|
1031 |
"daemon": cmd_daemon,
|
1032 |
} |
1033 |
|
1034 |
if len(sys.argv) < 2: |
1035 |
print("Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys()))) |
1036 |
sys.exit(1)
|
1037 |
|
1038 |
cmd = sys.argv[1]
|
1039 |
if not cmd in commands: |
1040 |
print("No such subcommand: %s" % cmd)
|
1041 |
sys.exit(1)
|
1042 |
commands[cmd](sys.argv[2:])
|
1043 |
|
1044 |
if __name__ == '__main__': |
1045 |
main() |