tasks_mirror.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import unicode_literals, absolute_import
  8. import base64
  9. import datetime
  10. import logging
  11. import os
  12. import stat
  13. import struct
  14. import six
  15. import werkzeug.utils
  16. from celery import Celery
  17. from cryptography import utils
  18. from cryptography.hazmat.backends import default_backend
  19. from cryptography.hazmat.primitives.asymmetric import rsa
  20. from cryptography.hazmat.primitives import serialization
  21. import pagure.lib.query
  22. from pagure.config import config as pagure_config
  23. from pagure.lib.tasks_utils import pagure_task
  24. from pagure.utils import ssh_urlpattern
  25. # logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})
  26. _log = logging.getLogger(__name__)
  27. if os.environ.get("PAGURE_BROKER_URL"): # pragma: no-cover
  28. broker_url = os.environ["PAGURE_BROKER_URL"]
  29. elif pagure_config.get("BROKER_URL"):
  30. broker_url = pagure_config["BROKER_URL"]
  31. else:
  32. broker_url = "redis://%s" % pagure_config["REDIS_HOST"]
  33. conn = Celery("tasks_mirror", broker=broker_url, backend=broker_url)
  34. conn.conf.update(pagure_config["CELERY_CONFIG"])
  35. # Code from:
  36. # https://github.com/pyca/cryptography/blob/6b08aba7f1eb296461528328a3c9871fa7594fc4/src/cryptography/hazmat/primitives/serialization.py#L161
  37. # Taken from upstream cryptography since the version we have is too old
  38. # and doesn't have this code (yet)
  39. def _ssh_write_string(data):
  40. return struct.pack(">I", len(data)) + data
  41. def _ssh_write_mpint(value):
  42. data = utils.int_to_bytes(value)
  43. if six.indexbytes(data, 0) & 0x80:
  44. data = b"\x00" + data
  45. return _ssh_write_string(data)
  46. # Code from _openssh_public_key_bytes at:
  47. # https://github.com/pyca/cryptography/tree/6b08aba7f1eb296461528328a3c9871fa7594fc4/src/cryptography/hazmat/backends/openssl#L1616
  48. # Taken from upstream cryptography since the version we have is too old
  49. # and doesn't have this code (yet)
  50. def _serialize_public_ssh_key(key):
  51. if isinstance(key, rsa.RSAPublicKey):
  52. public_numbers = key.public_numbers()
  53. return b"ssh-rsa " + base64.b64encode(
  54. _ssh_write_string(b"ssh-rsa")
  55. + _ssh_write_mpint(public_numbers.e)
  56. + _ssh_write_mpint(public_numbers.n)
  57. )
  58. else:
  59. # Since we only write RSA keys, drop the other serializations
  60. return
  61. def _create_ssh_key(keyfile):
  62. """Create the public and private ssh keys.
  63. The specified file name will be the private key and the public one will
  64. be in a similar file name ending with a '.pub'.
  65. """
  66. private_key = rsa.generate_private_key(
  67. public_exponent=65537, key_size=4096, backend=default_backend()
  68. )
  69. private_pem = private_key.private_bytes(
  70. encoding=serialization.Encoding.PEM,
  71. format=serialization.PrivateFormat.TraditionalOpenSSL,
  72. encryption_algorithm=serialization.NoEncryption(),
  73. )
  74. with os.fdopen(
  75. os.open(keyfile, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "wb"
  76. ) as stream:
  77. stream.write(private_pem)
  78. public_key = private_key.public_key()
  79. public_pem = _serialize_public_ssh_key(public_key)
  80. if public_pem:
  81. with open(keyfile + ".pub", "wb") as stream:
  82. stream.write(public_pem)
  83. @conn.task(queue=pagure_config["MIRRORING_QUEUE"], bind=True)
  84. @pagure_task
  85. def setup_mirroring(self, session, username, namespace, name):
  86. """Setup the specified project for mirroring."""
  87. plugin = pagure.lib.plugins.get_plugin("Mirroring")
  88. plugin.db_object()
  89. project = pagure.lib.query._get_project(
  90. session, namespace=namespace, name=name, user=username
  91. )
  92. public_key_name = werkzeug.utils.secure_filename(project.fullname)
  93. ssh_folder = pagure_config["MIRROR_SSHKEYS_FOLDER"]
  94. if not os.path.exists(ssh_folder):
  95. os.makedirs(ssh_folder, mode=0o700)
  96. else:
  97. if os.path.islink(ssh_folder):
  98. raise pagure.exceptions.PagureException("SSH folder is a link")
  99. folder_stat = os.stat(ssh_folder)
  100. filemode = stat.S_IMODE(folder_stat.st_mode)
  101. if filemode != int("0700", 8):
  102. raise pagure.exceptions.PagureException(
  103. "SSH folder had invalid permissions"
  104. )
  105. if (
  106. folder_stat.st_uid != os.getuid()
  107. or folder_stat.st_gid != os.getgid()
  108. ):
  109. raise pagure.exceptions.PagureException(
  110. "SSH folder does not belong to the user or group running "
  111. "this task"
  112. )
  113. public_key_file = os.path.join(ssh_folder, "%s.pub" % public_key_name)
  114. _log.info("Public key of interest: %s", public_key_file)
  115. if os.path.exists(public_key_file):
  116. raise pagure.exceptions.PagureException("SSH key already exists")
  117. _log.info("Creating public key")
  118. _create_ssh_key(os.path.join(ssh_folder, public_key_name))
  119. with open(public_key_file) as stream:
  120. public_key = stream.read()
  121. if project.mirror_hook.public_key != public_key:
  122. _log.info("Updating information in the DB")
  123. project.mirror_hook.public_key = public_key
  124. session.add(project.mirror_hook)
  125. session.commit()
  126. @conn.task(queue=pagure_config["MIRRORING_QUEUE"], bind=True)
  127. @pagure_task
  128. def teardown_mirroring(self, session, username, namespace, name):
  129. """Stop the mirroring of the specified project."""
  130. plugin = pagure.lib.plugins.get_plugin("Mirroring")
  131. plugin.db_object()
  132. project = pagure.lib.query._get_project(
  133. session, namespace=namespace, name=name, user=username
  134. )
  135. ssh_folder = pagure_config["MIRROR_SSHKEYS_FOLDER"]
  136. public_key_name = werkzeug.utils.secure_filename(project.fullname)
  137. private_key_file = os.path.join(ssh_folder, public_key_name)
  138. public_key_file = os.path.join(ssh_folder, "%s.pub" % public_key_name)
  139. if os.path.exists(private_key_file):
  140. os.unlink(private_key_file)
  141. if os.path.exists(public_key_file):
  142. os.unlink(public_key_file)
  143. project.mirror_hook.public_key = None
  144. session.add(project.mirror_hook)
  145. session.commit()
  146. @conn.task(queue=pagure_config["MIRRORING_QUEUE"], bind=True)
  147. @pagure_task
  148. def mirror_project(self, session, username, namespace, name):
  149. """Does the actual mirroring of the specified project."""
  150. plugin = pagure.lib.plugins.get_plugin("Mirroring")
  151. plugin.db_object()
  152. project = pagure.lib.query._get_project(
  153. session, namespace=namespace, name=name, user=username
  154. )
  155. repofolder = pagure_config["GIT_FOLDER"]
  156. repopath = os.path.join(repofolder, project.path)
  157. if not os.path.exists(repopath):
  158. _log.warning("Git folder not found at: %s, bailing", repopath)
  159. return
  160. ssh_folder = pagure_config["MIRROR_SSHKEYS_FOLDER"]
  161. public_key_name = werkzeug.utils.secure_filename(project.fullname)
  162. private_key_file = os.path.join(ssh_folder, public_key_name)
  163. if not os.path.exists(private_key_file):
  164. _log.warning("No %s key found, bailing", private_key_file)
  165. project.mirror_hook.last_log = "Private key not found on disk, bailing"
  166. session.add(project.mirror_hook)
  167. session.commit()
  168. return
  169. # Add the utility script allowing this feature to work on old(er) git.
  170. here = os.path.join(os.path.dirname(os.path.abspath(__file__)))
  171. script_file = os.path.join(here, "ssh_script.sh")
  172. # Get the list of remotes
  173. remotes = [
  174. remote.strip()
  175. for remote in project.mirror_hook.target.split("\n")
  176. if project.mirror_hook
  177. and remote.strip()
  178. and ssh_urlpattern.match(remote.strip())
  179. ]
  180. # Push
  181. logs = []
  182. for remote in remotes:
  183. _log.info(
  184. "Pushing to remote %s using key: %s", remote, private_key_file
  185. )
  186. (stdout, stderr) = pagure.lib.git.read_git_lines(
  187. ["push", "--mirror", remote],
  188. abspath=repopath,
  189. error=True,
  190. env={"SSHKEY": private_key_file, "GIT_SSH": script_file},
  191. )
  192. log = "Output from the push (%s):\n stdout: %s\n stderr: %s" % (
  193. datetime.datetime.utcnow().isoformat(),
  194. stdout,
  195. stderr,
  196. )
  197. logs.append(log)
  198. if logs:
  199. project.mirror_hook.last_log = "\n".join(logs)
  200. session.add(project.mirror_hook)
  201. session.commit()
  202. _log.info("\n".join(logs))