swift.py 33 KB

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