project.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. """
  2. Useful non-core functionality, e.g. functions composing multiple operations.
  3. """
  4. from __future__ import with_statement
  5. from os import getcwd, sep
  6. import os.path
  7. from tempfile import mkdtemp
  8. from fabric.network import needs_host, key_filenames, normalize
  9. from fabric.operations import local, run, sudo, put
  10. from fabric.state import env, output
  11. from fabric.context_managers import cd
  12. __all__ = ['rsync_project', 'upload_project']
  13. @needs_host
  14. def rsync_project(
  15. remote_dir,
  16. local_dir=None,
  17. exclude=(),
  18. delete=False,
  19. extra_opts='',
  20. ssh_opts='',
  21. capture=False,
  22. upload=True,
  23. default_opts='-pthrvz'
  24. ):
  25. """
  26. Synchronize a remote directory with the current project directory via rsync.
  27. Where ``upload_project()`` makes use of ``scp`` to copy one's entire
  28. project every time it is invoked, ``rsync_project()`` uses the ``rsync``
  29. command-line utility, which only transfers files newer than those on the
  30. remote end.
  31. ``rsync_project()`` is thus a simple wrapper around ``rsync``; for
  32. details on how ``rsync`` works, please see its manpage. ``rsync`` must be
  33. installed on both your local and remote systems in order for this operation
  34. to work correctly.
  35. This function makes use of Fabric's ``local()`` operation, and returns the
  36. output of that function call; thus it will return the stdout, if any, of
  37. the resultant ``rsync`` call.
  38. ``rsync_project()`` uses the current Fabric connection parameters (user,
  39. host, port) by default, adding them to rsync's ssh options (then mixing in
  40. ``ssh_opts``, if given -- see below.)
  41. ``rsync_project()`` takes the following parameters:
  42. * ``remote_dir``: the only required parameter, this is the path to the
  43. directory on the remote server. Due to how ``rsync`` is implemented, the
  44. exact behavior depends on the value of ``local_dir``:
  45. * If ``local_dir`` ends with a trailing slash, the files will be
  46. dropped inside of ``remote_dir``. E.g.
  47. ``rsync_project("/home/username/project/", "foldername/")`` will drop
  48. the contents of ``foldername`` inside of ``/home/username/project``.
  49. * If ``local_dir`` does **not** end with a trailing slash (and this
  50. includes the default scenario, when ``local_dir`` is not specified),
  51. ``remote_dir`` is effectively the "parent" directory, and a new
  52. directory named after ``local_dir`` will be created inside of it. So
  53. ``rsync_project("/home/username", "foldername")`` would create a new
  54. directory ``/home/username/foldername`` (if needed) and place the
  55. files there.
  56. * ``local_dir``: by default, ``rsync_project`` uses your current working
  57. directory as the source directory. This may be overridden by specifying
  58. ``local_dir``, which is a string passed verbatim to ``rsync``, and thus
  59. may be a single directory (``"my_directory"``) or multiple directories
  60. (``"dir1 dir2"``). See the ``rsync`` documentation for details.
  61. * ``exclude``: optional, may be a single string, or an iterable of strings,
  62. and is used to pass one or more ``--exclude`` options to ``rsync``.
  63. * ``delete``: a boolean controlling whether ``rsync``'s ``--delete`` option
  64. is used. If True, instructs ``rsync`` to remove remote files that no
  65. longer exist locally. Defaults to False.
  66. * ``extra_opts``: an optional, arbitrary string which you may use to pass
  67. custom arguments or options to ``rsync``.
  68. * ``ssh_opts``: Like ``extra_opts`` but specifically for the SSH options
  69. string (rsync's ``--rsh`` flag.)
  70. * ``capture``: Sent directly into an inner `~fabric.operations.local` call.
  71. * ``upload``: a boolean controlling whether file synchronization is
  72. performed up or downstream. Upstream by default.
  73. * ``default_opts``: the default rsync options ``-pthrvz``, override if
  74. desired (e.g. to remove verbosity, etc).
  75. Furthermore, this function transparently honors Fabric's port and SSH key
  76. settings. Calling this function when the current host string contains a
  77. nonstandard port, or when ``env.key_filename`` is non-empty, will use the
  78. specified port and/or SSH key filename(s).
  79. For reference, the approximate ``rsync`` command-line call that is
  80. constructed by this function is the following::
  81. rsync [--delete] [--exclude exclude[0][, --exclude[1][, ...]]] \\
  82. [default_opts] [extra_opts] <local_dir> <host_string>:<remote_dir>
  83. .. versionadded:: 1.4.0
  84. The ``ssh_opts`` keyword argument.
  85. .. versionadded:: 1.4.1
  86. The ``capture`` keyword argument.
  87. .. versionadded:: 1.8.0
  88. The ``default_opts`` keyword argument.
  89. """
  90. # Turn single-string exclude into a one-item list for consistency
  91. if not hasattr(exclude, '__iter__'):
  92. exclude = (exclude,)
  93. # Create --exclude options from exclude list
  94. exclude_opts = ' --exclude "%s"' * len(exclude)
  95. # Double-backslash-escape
  96. exclusions = tuple([str(s).replace('"', '\\\\"') for s in exclude])
  97. # Honor SSH key(s)
  98. key_string = ""
  99. keys = key_filenames()
  100. if keys:
  101. key_string = "-i " + " -i ".join(keys)
  102. # Port
  103. user, host, port = normalize(env.host_string)
  104. port_string = "-p %s" % port
  105. # RSH
  106. rsh_string = ""
  107. if env.gateway is None:
  108. gateway_opts = ""
  109. else:
  110. gw_user, gw_host, gw_port = normalize(env.gateway)
  111. gw_str = "-A -o \"ProxyCommand=ssh %s -p %s %s@%s nc %s %s\""
  112. gateway_opts = gw_str % (
  113. key_string, gw_port, gw_user, gw_host, host, port
  114. )
  115. rsh_parts = [key_string, port_string, ssh_opts, gateway_opts]
  116. if any(rsh_parts):
  117. rsh_string = "--rsh='ssh %s'" % " ".join(rsh_parts)
  118. # Set up options part of string
  119. options_map = {
  120. 'delete': '--delete' if delete else '',
  121. 'exclude': exclude_opts % exclusions,
  122. 'rsh': rsh_string,
  123. 'default': default_opts,
  124. 'extra': extra_opts,
  125. }
  126. options = "%(delete)s%(exclude)s %(default)s %(extra)s %(rsh)s" % options_map
  127. # Get local directory
  128. if local_dir is None:
  129. local_dir = '../' + getcwd().split(sep)[-1]
  130. # Create and run final command string
  131. if host.count(':') > 1:
  132. # Square brackets are mandatory for IPv6 rsync address,
  133. # even if port number is not specified
  134. remote_prefix = "[%s@%s]" % (user, host)
  135. else:
  136. remote_prefix = "%s@%s" % (user, host)
  137. if upload:
  138. cmd = "rsync %s %s %s:%s" % (options, local_dir, remote_prefix, remote_dir)
  139. else:
  140. cmd = "rsync %s %s:%s %s" % (options, remote_prefix, remote_dir, local_dir)
  141. if output.running:
  142. print("[%s] rsync_project: %s" % (env.host_string, cmd))
  143. return local(cmd, capture=capture)
  144. def upload_project(local_dir=None, remote_dir="", use_sudo=False):
  145. """
  146. Upload the current project to a remote system via ``tar``/``gzip``.
  147. ``local_dir`` specifies the local project directory to upload, and defaults
  148. to the current working directory.
  149. ``remote_dir`` specifies the target directory to upload into (meaning that
  150. a copy of ``local_dir`` will appear as a subdirectory of ``remote_dir``)
  151. and defaults to the remote user's home directory.
  152. ``use_sudo`` specifies which method should be used when executing commands
  153. remotely. ``sudo`` will be used if use_sudo is True, otherwise ``run`` will
  154. be used.
  155. This function makes use of the ``tar`` and ``gzip`` programs/libraries,
  156. thus it will not work too well on Win32 systems unless one is using Cygwin
  157. or something similar. It will attempt to clean up the local and remote
  158. tarfiles when it finishes executing, even in the event of a failure.
  159. .. versionchanged:: 1.1
  160. Added the ``local_dir`` and ``remote_dir`` kwargs.
  161. .. versionchanged:: 1.7
  162. Added the ``use_sudo`` kwarg.
  163. """
  164. runner = use_sudo and sudo or run
  165. local_dir = local_dir or os.getcwd()
  166. # Remove final '/' in local_dir so that basename() works
  167. local_dir = local_dir.rstrip(os.sep)
  168. local_path, local_name = os.path.split(local_dir)
  169. local_path = local_path or '.'
  170. tar_file = "%s.tar.gz" % local_name
  171. target_tar = os.path.join(remote_dir, tar_file)
  172. tmp_folder = mkdtemp()
  173. try:
  174. tar_path = os.path.join(tmp_folder, tar_file)
  175. local("tar -czf %s -C %s %s" % (tar_path, local_path, local_name))
  176. put(tar_path, target_tar, use_sudo=use_sudo)
  177. with cd(remote_dir):
  178. try:
  179. runner("tar -xzf %s" % tar_file)
  180. finally:
  181. runner("rm -f %s" % tar_file)
  182. finally:
  183. local("rm -rf %s" % tmp_folder)