2
0

swift.py 34 KB

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