swift.py 37 KB

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