swift.py 42 KB

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