swift.py 34 KB

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