files.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. """
  2. Module providing easy API for working with remote files and folders.
  3. """
  4. from __future__ import with_statement
  5. import hashlib
  6. import os
  7. from StringIO import StringIO
  8. from functools import partial
  9. from fabric.api import run, sudo, hide, settings, env, put, abort
  10. from fabric.utils import apply_lcwd
  11. def exists(path, use_sudo=False, verbose=False):
  12. """
  13. Return True if given path exists on the current remote host.
  14. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  15. `exists` will, by default, hide all output (including the run line, stdout,
  16. stderr and any warning resulting from the file not existing) in order to
  17. avoid cluttering output. You may specify ``verbose=True`` to change this
  18. behavior.
  19. .. versionchanged:: 1.13
  20. Replaced internal use of ``test -e`` with ``stat`` for improved remote
  21. cross-platform (e.g. Windows) compatibility.
  22. """
  23. func = use_sudo and sudo or run
  24. cmd = 'stat %s' % _expand_path(path)
  25. # If verbose, run normally
  26. if verbose:
  27. with settings(warn_only=True):
  28. return not func(cmd).failed
  29. # Otherwise, be quiet
  30. with settings(hide('everything'), warn_only=True):
  31. return not func(cmd).failed
  32. def is_link(path, use_sudo=False, verbose=False):
  33. """
  34. Return True if the given path is a symlink on the current remote host.
  35. If ``use_sudo`` is True, will use `.sudo` instead of `.run`.
  36. `.is_link` will, by default, hide all output. Give ``verbose=True`` to
  37. change this.
  38. """
  39. func = sudo if use_sudo else run
  40. cmd = 'test -L "$(echo %s)"' % path
  41. args, kwargs = [], {'warn_only': True}
  42. if not verbose:
  43. args = [hide('everything')]
  44. with settings(*args, **kwargs):
  45. return func(cmd).succeeded
  46. def first(*args, **kwargs):
  47. """
  48. Given one or more file paths, returns first one found, or None if none
  49. exist. May specify ``use_sudo`` and ``verbose`` which are passed to
  50. `exists`.
  51. """
  52. for directory in args:
  53. if exists(directory, **kwargs):
  54. return directory
  55. def upload_template(filename, destination, context=None, use_jinja=False,
  56. template_dir=None, use_sudo=False, backup=True, mirror_local_mode=False,
  57. mode=None, pty=None, keep_trailing_newline=False, temp_dir=''):
  58. """
  59. Render and upload a template text file to a remote host.
  60. Returns the result of the inner call to `~fabric.operations.put` -- see its
  61. documentation for details.
  62. ``filename`` should be the path to a text file, which may contain `Python
  63. string interpolation formatting
  64. <http://docs.python.org/library/stdtypes.html#string-formatting>`_ and will
  65. be rendered with the given context dictionary ``context`` (if given.)
  66. Alternately, if ``use_jinja`` is set to True and you have the Jinja2
  67. templating library available, Jinja will be used to render the template
  68. instead. Templates will be loaded from the invoking user's current working
  69. directory by default, or from ``template_dir`` if given.
  70. The resulting rendered file will be uploaded to the remote file path
  71. ``destination``. If the destination file already exists, it will be
  72. renamed with a ``.bak`` extension unless ``backup=False`` is specified.
  73. By default, the file will be copied to ``destination`` as the logged-in
  74. user; specify ``use_sudo=True`` to use `sudo` instead.
  75. The ``mirror_local_mode``, ``mode``, and ``temp_dir`` kwargs are passed
  76. directly to an internal `~fabric.operations.put` call; please see its
  77. documentation for details on these two options.
  78. The ``pty`` kwarg will be passed verbatim to any internal
  79. `~fabric.operations.run`/`~fabric.operations.sudo` calls, such as those
  80. used for testing directory-ness, making backups, etc.
  81. The ``keep_trailing_newline`` kwarg will be passed when creating
  82. Jinja2 Environment which is False by default, same as Jinja2's
  83. behaviour.
  84. .. versionchanged:: 1.1
  85. Added the ``backup``, ``mirror_local_mode`` and ``mode`` kwargs.
  86. .. versionchanged:: 1.9
  87. Added the ``pty`` kwarg.
  88. .. versionchanged:: 1.11
  89. Added the ``keep_trailing_newline`` kwarg.
  90. .. versionchanged:: 1.11
  91. Added the ``temp_dir`` kwarg.
  92. """
  93. func = use_sudo and sudo or run
  94. if pty is not None:
  95. func = partial(func, pty=pty)
  96. # Normalize destination to be an actual filename, due to using StringIO
  97. with settings(hide('everything'), warn_only=True):
  98. if func('test -d %s' % _expand_path(destination)).succeeded:
  99. sep = "" if destination.endswith('/') else "/"
  100. destination += sep + os.path.basename(filename)
  101. # Use mode kwarg to implement mirror_local_mode, again due to using
  102. # StringIO
  103. if mirror_local_mode and mode is None:
  104. mode = os.stat(apply_lcwd(filename, env)).st_mode
  105. # To prevent put() from trying to do this
  106. # logic itself
  107. mirror_local_mode = False
  108. # Process template
  109. text = None
  110. if use_jinja:
  111. try:
  112. template_dir = template_dir or os.getcwd()
  113. template_dir = apply_lcwd(template_dir, env)
  114. from jinja2 import Environment, FileSystemLoader
  115. jenv = Environment(loader=FileSystemLoader(template_dir),
  116. keep_trailing_newline=keep_trailing_newline)
  117. text = jenv.get_template(filename).render(**context or {})
  118. # Force to a byte representation of Unicode, or str()ification
  119. # within Paramiko's SFTP machinery may cause decode issues for
  120. # truly non-ASCII characters.
  121. text = text.encode('utf-8')
  122. except ImportError:
  123. import traceback
  124. tb = traceback.format_exc()
  125. abort(tb + "\nUnable to import Jinja2 -- see above.")
  126. else:
  127. if template_dir:
  128. filename = os.path.join(template_dir, filename)
  129. filename = apply_lcwd(filename, env)
  130. with open(os.path.expanduser(filename)) as inputfile:
  131. text = inputfile.read()
  132. if context:
  133. text = text % context
  134. # Back up original file
  135. if backup and exists(destination):
  136. func("cp %s{,.bak}" % _expand_path(destination))
  137. # Upload the file.
  138. return put(
  139. local_path=StringIO(text),
  140. remote_path=destination,
  141. use_sudo=use_sudo,
  142. mirror_local_mode=mirror_local_mode,
  143. mode=mode,
  144. temp_dir=temp_dir
  145. )
  146. def sed(filename, before, after, limit='', use_sudo=False, backup='.bak',
  147. flags='', shell=False):
  148. """
  149. Run a search-and-replace on ``filename`` with given regex patterns.
  150. Equivalent to ``sed -i<backup> -r -e "/<limit>/ s/<before>/<after>/<flags>g"
  151. <filename>``. Setting ``backup`` to an empty string will, disable backup
  152. file creation.
  153. For convenience, ``before`` and ``after`` will automatically escape forward
  154. slashes, single quotes and parentheses for you, so you don't need to
  155. specify e.g. ``http:\/\/foo\.com``, instead just using ``http://foo\.com``
  156. is fine.
  157. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  158. The ``shell`` argument will be eventually passed to `run`/`sudo`. It
  159. defaults to False in order to avoid problems with many nested levels of
  160. quotes and backslashes. However, setting it to True may help when using
  161. ``~fabric.operations.cd`` to wrap explicit or implicit ``sudo`` calls.
  162. (``cd`` by it's nature is a shell built-in, not a standalone command, so it
  163. should be called within a shell.)
  164. Other options may be specified with sed-compatible regex flags -- for
  165. example, to make the search and replace case insensitive, specify
  166. ``flags="i"``. The ``g`` flag is always specified regardless, so you do not
  167. need to remember to include it when overriding this parameter.
  168. .. versionadded:: 1.1
  169. The ``flags`` parameter.
  170. .. versionadded:: 1.6
  171. Added the ``shell`` keyword argument.
  172. """
  173. func = use_sudo and sudo or run
  174. # Characters to be escaped in both
  175. for char in "/'":
  176. before = before.replace(char, r'\%s' % char)
  177. after = after.replace(char, r'\%s' % char)
  178. # Characters to be escaped in replacement only (they're useful in regexen
  179. # in the 'before' part)
  180. for char in "()":
  181. after = after.replace(char, r'\%s' % char)
  182. if limit:
  183. limit = r'/%s/ ' % limit
  184. context = {
  185. 'script': r"'%ss/%s/%s/%sg'" % (limit, before, after, flags),
  186. 'filename': _expand_path(filename),
  187. 'backup': backup
  188. }
  189. # Test the OS because of differences between sed versions
  190. with hide('running', 'stdout'):
  191. platform = run("uname", shell=False, pty=False)
  192. if platform in ('NetBSD', 'OpenBSD', 'QNX'):
  193. # Attempt to protect against failures/collisions
  194. hasher = hashlib.sha1()
  195. hasher.update(env.host_string)
  196. hasher.update(filename)
  197. context['tmp'] = "/tmp/%s" % hasher.hexdigest()
  198. # Use temp file to work around lack of -i
  199. expr = r"""cp -p %(filename)s %(tmp)s \
  200. && sed -r -e %(script)s %(filename)s > %(tmp)s \
  201. && cp -p %(filename)s %(filename)s%(backup)s \
  202. && mv %(tmp)s %(filename)s"""
  203. else:
  204. context['extended_regex'] = '-E' if platform == 'Darwin' else '-r'
  205. expr = r"sed -i%(backup)s %(extended_regex)s -e %(script)s %(filename)s"
  206. command = expr % context
  207. return func(command, shell=shell)
  208. def uncomment(filename, regex, use_sudo=False, char='#', backup='.bak',
  209. shell=False):
  210. """
  211. Attempt to uncomment all lines in ``filename`` matching ``regex``.
  212. The default comment delimiter is `#` and may be overridden by the ``char``
  213. argument.
  214. This function uses the `sed` function, and will accept the same
  215. ``use_sudo``, ``shell`` and ``backup`` keyword arguments that `sed` does.
  216. `uncomment` will remove a single whitespace character following the comment
  217. character, if it exists, but will preserve all preceding whitespace. For
  218. example, ``# foo`` would become ``foo`` (the single space is stripped) but
  219. `` # foo`` would become `` foo`` (the single space is still stripped,
  220. but the preceding 4 spaces are not.)
  221. .. versionchanged:: 1.6
  222. Added the ``shell`` keyword argument.
  223. """
  224. return sed(
  225. filename,
  226. before=r'^([[:space:]]*)%s[[:space:]]?' % char,
  227. after=r'\1',
  228. limit=regex,
  229. use_sudo=use_sudo,
  230. backup=backup,
  231. shell=shell
  232. )
  233. def comment(filename, regex, use_sudo=False, char='#', backup='.bak',
  234. shell=False):
  235. """
  236. Attempt to comment out all lines in ``filename`` matching ``regex``.
  237. The default commenting character is `#` and may be overridden by the
  238. ``char`` argument.
  239. This function uses the `sed` function, and will accept the same
  240. ``use_sudo``, ``shell`` and ``backup`` keyword arguments that `sed` does.
  241. `comment` will prepend the comment character to the beginning of the line,
  242. so that lines end up looking like so::
  243. this line is uncommented
  244. #this line is commented
  245. # this line is indented and commented
  246. In other words, comment characters will not "follow" indentation as they
  247. sometimes do when inserted by hand. Neither will they have a trailing space
  248. unless you specify e.g. ``char='# '``.
  249. .. note::
  250. In order to preserve the line being commented out, this function will
  251. wrap your ``regex`` argument in parentheses, so you don't need to. It
  252. will ensure that any preceding/trailing ``^`` or ``$`` characters are
  253. correctly moved outside the parentheses. For example, calling
  254. ``comment(filename, r'^foo$')`` will result in a `sed` call with the
  255. "before" regex of ``r'^(foo)$'`` (and the "after" regex, naturally, of
  256. ``r'#\\1'``.)
  257. .. versionadded:: 1.5
  258. Added the ``shell`` keyword argument.
  259. """
  260. carot, dollar = '', ''
  261. if regex.startswith('^'):
  262. carot = '^'
  263. regex = regex[1:]
  264. if regex.endswith('$'):
  265. dollar = '$'
  266. regex = regex[:-1]
  267. regex = "%s(%s)%s" % (carot, regex, dollar)
  268. return sed(
  269. filename,
  270. before=regex,
  271. after=r'%s\1' % char,
  272. use_sudo=use_sudo,
  273. backup=backup,
  274. shell=shell
  275. )
  276. def contains(filename, text, exact=False, use_sudo=False, escape=True,
  277. shell=False, case_sensitive=True):
  278. """
  279. Return True if ``filename`` contains ``text`` (which may be a regex.)
  280. By default, this function will consider a partial line match (i.e. where
  281. ``text`` only makes up part of the line it's on). Specify ``exact=True`` to
  282. change this behavior so that only a line containing exactly ``text``
  283. results in a True return value.
  284. This function leverages ``egrep`` on the remote end (so it may not follow
  285. Python regular expression syntax perfectly), and skips ``env.shell``
  286. wrapper by default.
  287. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  288. If ``escape`` is False, no extra regular expression related escaping is
  289. performed (this includes overriding ``exact`` so that no ``^``/``$`` is
  290. added.)
  291. The ``shell`` argument will be eventually passed to ``run/sudo``. See
  292. description of the same argument in ``~fabric.contrib.sed`` for details.
  293. If ``case_sensitive`` is False, the `-i` flag will be passed to ``egrep``.
  294. .. versionchanged:: 1.0
  295. Swapped the order of the ``filename`` and ``text`` arguments to be
  296. consistent with other functions in this module.
  297. .. versionchanged:: 1.4
  298. Updated the regular expression related escaping to try and solve
  299. various corner cases.
  300. .. versionchanged:: 1.4
  301. Added ``escape`` keyword argument.
  302. .. versionadded:: 1.6
  303. Added the ``shell`` keyword argument.
  304. .. versionadded:: 1.11
  305. Added the ``case_sensitive`` keyword argument.
  306. """
  307. func = use_sudo and sudo or run
  308. if escape:
  309. text = _escape_for_regex(text)
  310. if exact:
  311. text = "^%s$" % text
  312. with settings(hide('everything'), warn_only=True):
  313. egrep_cmd = 'egrep "%s" %s' % (text, _expand_path(filename))
  314. if not case_sensitive:
  315. egrep_cmd = egrep_cmd.replace('egrep', 'egrep -i', 1)
  316. return func(egrep_cmd, shell=shell).succeeded
  317. def append(filename, text, use_sudo=False, partial=False, escape=True,
  318. shell=False):
  319. """
  320. Append string (or list of strings) ``text`` to ``filename``.
  321. When a list is given, each string inside is handled independently (but in
  322. the order given.)
  323. If ``text`` is already found in ``filename``, the append is not run, and
  324. None is returned immediately. Otherwise, the given text is appended to the
  325. end of the given ``filename`` via e.g. ``echo '$text' >> $filename``.
  326. The test for whether ``text`` already exists defaults to a full line match,
  327. e.g. ``^<text>$``, as this seems to be the most sensible approach for the
  328. "append lines to a file" use case. You may override this and force partial
  329. searching (e.g. ``^<text>``) by specifying ``partial=True``.
  330. Because ``text`` is single-quoted, single quotes will be transparently
  331. backslash-escaped. This can be disabled with ``escape=False``.
  332. If ``use_sudo`` is True, will use `sudo` instead of `run`.
  333. The ``shell`` argument will be eventually passed to ``run/sudo``. See
  334. description of the same argumnet in ``~fabric.contrib.sed`` for details.
  335. .. versionchanged:: 0.9.1
  336. Added the ``partial`` keyword argument.
  337. .. versionchanged:: 1.0
  338. Swapped the order of the ``filename`` and ``text`` arguments to be
  339. consistent with other functions in this module.
  340. .. versionchanged:: 1.0
  341. Changed default value of ``partial`` kwarg to be ``False``.
  342. .. versionchanged:: 1.4
  343. Updated the regular expression related escaping to try and solve
  344. various corner cases.
  345. .. versionadded:: 1.6
  346. Added the ``shell`` keyword argument.
  347. """
  348. func = use_sudo and sudo or run
  349. # Normalize non-list input to be a list
  350. if isinstance(text, basestring):
  351. text = [text]
  352. for line in text:
  353. regex = '^' + _escape_for_regex(line) + ('' if partial else '$')
  354. if (exists(filename, use_sudo=use_sudo) and line
  355. and contains(filename, regex, use_sudo=use_sudo, escape=False,
  356. shell=shell)):
  357. continue
  358. line = line.replace("'", r"'\\''") if escape else line
  359. func("echo '%s' >> %s" % (line, _expand_path(filename)))
  360. def _escape_for_regex(text):
  361. """Escape ``text`` to allow literal matching using egrep"""
  362. re_specials = '\\^$|(){}[]*+?.'
  363. sh_specials = '\\$`"'
  364. re_chars = []
  365. sh_chars = []
  366. for c in text:
  367. if c in re_specials:
  368. re_chars.append('\\')
  369. re_chars.append(c)
  370. for c in re_chars:
  371. if c in sh_specials:
  372. sh_chars.append('\\')
  373. sh_chars.append(c)
  374. return ''.join(sh_chars)
  375. def is_win():
  376. """
  377. Return True if remote SSH server is running Windows, False otherwise.
  378. The idea is based on echoing quoted text: \*NIX systems will echo quoted
  379. text only, while Windows echoes quotation marks as well.
  380. """
  381. with settings(hide('everything'), warn_only=True):
  382. return '"' in run('echo "Will you echo quotation marks"')
  383. def _expand_path(path):
  384. """
  385. Return a path expansion
  386. E.g. ~/some/path -> /home/myuser/some/path
  387. /user/\*/share -> /user/local/share
  388. More examples can be found here: http://linuxcommand.org/lc3_lts0080.php
  389. .. versionchanged:: 1.0
  390. Avoid breaking remote Windows commands which does not support expansion.
  391. """
  392. return path if is_win() else '"$(echo %s)"' % path