Codebase list fabric / 0a4d0a8
Import upstream version 2.6.0+git20210708.1.05433a2 Debian Janitor 2 years ago
39 changed file(s) with 1084 addition(s) and 418 deletion(s). Raw diff Collapse all Expand all
+0
-3
.coveragerc less more
0 [run]
1 branch = True
2 include = fabric/*
+0
-20
.gitignore less more
0 *~
1 *.pyc
2 *.pyo
3 *.pyt
4 *.pytc
5 *.egg
6 .DS_Store
7 .*.swp
8 *.egg-info
9 .coverage
10 sites/*/_build
11 dist
12 build/
13 tags
14 TAGS
15 .tox
16 tox.ini
17 .idea/
18 htmlcov
19 .cache
+0
-87
.travis.yml less more
0 language: python
1 sudo: required
2 dist: trusty
3 cache:
4 directories:
5 - $HOME/.cache/pip
6 python:
7 - "2.7"
8 - "3.4"
9 - "3.5"
10 - "3.6"
11 - "pypy"
12 - "pypy3"
13 matrix:
14 # pypy3 (as of 2.4.0) has a wacky arity issue in its source loader. Allow it
15 # to fail until we can test on, and require, PyPy3.3+. See
16 # pyinvoke/invoke#358.
17 # NOTE: both pypy flavors are weirdly unstable on Travis nowadays, even
18 # pre-test-run.
19 allow_failures:
20 - python: pypy
21 - python: pypy3
22 # Disabled per https://github.com/travis-ci/travis-ci/issues/1696
23 # fast_finish: true
24 install:
25 # TODO: real test matrix with at least some cells combining different invoke
26 # and/or paramiko versions, released versions, etc
27 # Invoke from master for parity
28 - "pip install -e git+https://github.com/pyinvoke/invoke#egg=invoke"
29 # And invocations, ditto
30 - "pip install -e git+https://github.com/pyinvoke/invocations#egg=invocations"
31 # Paramiko ditto
32 - "pip install -e git+https://github.com/paramiko/paramiko#egg=paramiko"
33 # Self
34 - pip install -e .
35 # Limit setuptools as some newer versions have Issues(tm). This needs doing
36 # as its own step; trying to do it via requirements.txt isn't always
37 # sufficient.
38 - pip install "setuptools<34"
39 # Dev requirements
40 # TODO: follow invoke and split it up a bit so we're not pulling down
41 # conflicting or unused-by-travis deps?
42 - pip install -r dev-requirements.txt
43 # Sanity test of the Invoke layer, if that's busted everything is
44 - inv --list
45 # Sanity test of Fabric itself
46 - fab --version
47 before_script:
48 # Create 'sudouser' w/ sudo password & perms on Travis' homedir
49 - inv travis.make-sudouser
50 # Allow us to SSH passwordless to localhost
51 - inv travis.make-sshable
52 script:
53 # Fast syntax check failures for more rapid feedback to submitters
54 # (Travis-oriented metatask that version checks Python, installs, runs.)
55 - inv travis.blacken
56 # I have this in my git pre-push hook, but contributors probably don't
57 - flake8
58 # Execute full test suite + coverage, as the new sudo-capable user
59 - inv travis.sudo-coverage
60 # Execute integration tests too. TODO: merge under coverage...somehow
61 # NOTE: this also runs as the sudo-capable user, even if it's not necessarily
62 # doing any sudo'ing itself - the sudo-capable user is also the ssh-able
63 # user...
64 - inv travis.sudo-run "inv integration"
65 # Websites build OK? (Not on PyPy3, Sphinx is all "who the hell are you?" =/
66 - "if [[ $TRAVIS_PYTHON_VERSION != 'pypy3' ]]; then inv sites www.doctest docs.doctest; fi"
67 # Did we break setup.py?
68 - inv travis.test-installation --package=fabric --sanity="fab --version"
69 # Test distribution builds.
70 - inv travis.test-packaging --package=fabric --sanity="fab --version"
71 # Again, but as 'fabric2'
72 - rm -rf tmp
73 - pip uninstall -y fabric
74 - "PACKAGE_AS_FABRIC2=yes inv travis.test-packaging --package=fabric2 --sanity=\"fab2 --version\""
75 - inv sanity-test-from-v1
76 after_success:
77 # Upload coverage data to codecov
78 - codecov
79 notifications:
80 irc:
81 channels: "irc.freenode.org#fabric"
82 template:
83 - "%{repository_name}@%{branch}: %{message} (%{build_url})"
84 on_success: change
85 on_failure: change
86 email: false
0 Copyright (c) 2019 Jeff Forcier.
0 Copyright (c) 2020 Jeff Forcier.
11 All rights reserved.
22
33 Redistribution and use in source and binary forms, with or without
0 Metadata-Version: 2.1
1 Name: fabric
2 Version: 2.6.0
3 Summary: High level SSH command execution
4 Home-page: http://fabfile.org
5 Author: Jeff Forcier
6 Author-email: jeff@bitprophet.org
7 License: BSD
8 Description:
9 To find out what's new in this version of Fabric, please see `the changelog
10 <http://fabfile.org/changelog.html>`_.
11
12 Welcome to Fabric!
13 ==================
14
15 Fabric is a high level Python (2.7, 3.4+) library designed to execute shell
16 commands remotely over SSH, yielding useful Python objects in return. It builds
17 on top of `Invoke <http://pyinvoke.org>`_ (subprocess command execution and
18 command-line features) and `Paramiko <http://paramiko.org>`_ (SSH protocol
19 implementation), extending their APIs to complement one another and provide
20 additional functionality.
21
22 For a high level introduction, including example code, please see
23 `our main project website <http://fabfile.org>`_; or for detailed API docs, see
24 `the versioned API website <http://docs.fabfile.org>`_.
25
26
27 Platform: UNKNOWN
28 Classifier: Development Status :: 5 - Production/Stable
29 Classifier: Environment :: Console
30 Classifier: Intended Audience :: Developers
31 Classifier: Intended Audience :: System Administrators
32 Classifier: License :: OSI Approved :: BSD License
33 Classifier: Operating System :: POSIX
34 Classifier: Operating System :: Unix
35 Classifier: Operating System :: MacOS :: MacOS X
36 Classifier: Operating System :: Microsoft :: Windows
37 Classifier: Programming Language :: Python
38 Classifier: Programming Language :: Python :: 2
39 Classifier: Programming Language :: Python :: 2.7
40 Classifier: Programming Language :: Python :: 3
41 Classifier: Programming Language :: Python :: 3.4
42 Classifier: Programming Language :: Python :: 3.5
43 Classifier: Programming Language :: Python :: 3.6
44 Classifier: Programming Language :: Python :: 3.7
45 Classifier: Topic :: Software Development
46 Classifier: Topic :: Software Development :: Build Tools
47 Classifier: Topic :: Software Development :: Libraries
48 Classifier: Topic :: Software Development :: Libraries :: Python Modules
49 Classifier: Topic :: System :: Clustering
50 Classifier: Topic :: System :: Software Distribution
51 Classifier: Topic :: System :: Systems Administration
52 Provides-Extra: pytest
53 Provides-Extra: testing
+0
-2
codecov.yml less more
0 # No codecov comments at all, please - just the github 'checks' is sufficient
1 comment: off
1313 # Linting!
1414 flake8==3.6.0
1515 # Coverage!
16 coverage==3.7.1
17 codecov==1.6.3
16 coverage==5.3.1
17 codecov==2.1.11
1818 # Documentation tools
1919 sphinx>=1.4,<1.7
2020 alabaster==0.7.12
00 # flake8: noqa
11 from ._version import __version_info__, __version__
22 from .connection import Config, Connection
3 from .runners import Remote, Result
3 from .runners import Remote, RemoteShell, Result
44 from .group import Group, SerialGroup, ThreadingGroup, GroupResult
55 from .tasks import task, Task
66 from .executor import Executor
0 __version_info__ = (2, 5, 0)
0 __version_info__ = (2, 6, 0)
11 __version__ = ".".join(map(str, __version_info__))
44 from invoke.config import Config as InvokeConfig, merge_dicts
55 from paramiko.config import SSHConfig
66
7 from .runners import Remote
7 from .runners import Remote, RemoteShell
88 from .util import get_local_user, debug
99
1010
307307 "inline_ssh_env": False,
308308 "load_ssh_configs": True,
309309 "port": 22,
310 "run": {"replace_env": True},
311 "runners": {"remote": Remote},
310 "runners": {"remote": Remote, "remote_shell": RemoteShell},
312311 "ssh_config_path": None,
313312 "tasks": {"collection_name": "fabfile"},
314313 # TODO: this becomes an override/extend once Invoke grows execution
299299
300300 Default: ``config.timeouts.connect``.
301301
302 .. _connect_kwargs-arg:
303302
304303 :param dict connect_kwargs:
304
305 .. _connect_kwargs-arg:
306
305307 Keyword arguments handed verbatim to
306308 `SSHClient.connect <paramiko.client.SSHClient.connect>` (when
307309 `.open` is called).
700702 return channel
701703
702704 def _remote_runner(self):
703 return self.config.runners.remote(self, inline_env=self.inline_ssh_env)
705 return self.config.runners.remote(
706 context=self, inline_env=self.inline_ssh_env
707 )
704708
705709 @opens
706710 def run(self, command, **kwargs):
733737 """
734738 return self._sudo(self._remote_runner(), command, **kwargs)
735739
740 @opens
741 def shell(self, **kwargs):
742 """
743 Run an interactive login shell on the remote end, as with ``ssh``.
744
745 This method is intended strictly for use cases where you can't know
746 what remote shell to invoke, or are connecting to a non-POSIX-server
747 environment such as a network appliance or other custom SSH server.
748 Nearly every other use case, including interactively-focused ones, will
749 be better served by using `run` plus an explicit remote shell command
750 (eg ``bash``).
751
752 `shell` has the following differences in behavior from `run`:
753
754 - It still returns a `~invoke.runners.Result` instance, but the object
755 will have a less useful set of attributes than with `run` or `local`:
756
757 - ``command`` will be ``None``, as there is no such input argument.
758 - ``stdout`` will contain a full record of the session, including
759 all interactive input, as that is echoed back to the user. This
760 can be useful for logging but is much less so for doing
761 programmatic things after the method returns.
762 - ``stderr`` will always be empty (same as `run` when
763 ``pty==True``).
764 - ``pty`` will always be True (because one was automatically used).
765 - ``exited`` and similar attributes will only reflect the overall
766 session, which may vary by shell or appliance but often has no
767 useful relationship with the internally executed commands' exit
768 codes.
769
770 - This method behaves as if ``warn`` is set to ``True``: even if the
771 remote shell exits uncleanly, no exception will be raised.
772 - A pty is always allocated remotely, as with ``pty=True`` under `run`.
773 - The ``inline_env`` setting is ignored, as there is no default shell
774 command to add the parameters to (and no guarantee the remote end
775 even is a shell!)
776
777 It supports **only** the following kwargs, which behave identically to
778 their counterparts in `run` unless otherwise stated:
779
780 - ``encoding``
781 - ``env``
782 - ``in_stream`` (useful in niche cases, but make sure regular `run`
783 with this argument isn't more suitable!)
784 - ``replace_env``
785 - ``watchers`` (note that due to pty echoing your stdin back to stdout,
786 a watcher will see your input as well as program stdout!)
787
788 Those keyword arguments also honor the ``run.*`` configuration tree, as
789 in `run`/`sudo`.
790
791 :returns: `Result`
792
793 :raises:
794 `.ThreadException` (if the background I/O threads encountered
795 exceptions other than `.WatcherError`).
796
797 .. versionadded:: 2.7
798 """
799 runner = self.config.runners.remote_shell(context=self)
800 # Reinstate most defaults as explicit kwargs to ensure user's config
801 # doesn't make this mode break horribly. Then override a few that need
802 # to change, like pty.
803 allowed = ("encoding", "env", "in_stream", "replace_env", "watchers")
804 new_kwargs = {}
805 for key, value in self.config.global_defaults()["run"].items():
806 if key in allowed:
807 # Use allowed kwargs if given, otherwise also fill them from
808 # defaults
809 new_kwargs[key] = kwargs.pop(key, self.config.run[key])
810 else:
811 new_kwargs[key] = value
812 new_kwargs.update(pty=True)
813 # At this point, any leftover kwargs would be ignored, so yell instead
814 if kwargs:
815 err = "shell() got unexpected keyword arguments: {!r}"
816 raise TypeError(err.format(list(kwargs.keys())))
817 return runner.run(command=None, **new_kwargs)
818
736819 def local(self, *args, **kwargs):
737820 """
738821 Execute a shell command on the local system.
775858
776859 def put(self, *args, **kwargs):
777860 """
778 Put a remote file (or file-like object) to the remote filesystem.
861 Put a local file (or file-like object) to the remote filesystem.
779862
780863 Simply a wrapper for `.Transfer.put`. Please see its documentation for
781864 all details.
1717 concrete subclasses (such as `.SerialGroup` or `.ThreadingGroup`) or
1818 you'll get ``NotImplementedError`` on most of the methods.
1919
20 Most methods in this class mirror those of `.Connection`, taking the same
21 arguments; however their return values and exception-raising behavior
22 differs:
20 Most methods in this class wrap those of `.Connection` and will accept the
21 same arguments; however their return values and exception-raising behavior
22 differ:
2323
2424 - Return values are dict-like objects (`.GroupResult`) mapping
2525 `.Connection` objects to the return value for the respective connections:
9898 group.extend(connections)
9999 return group
100100
101 def _do(self, method, *args, **kwargs):
102 # TODO: rename this something public & commit to an API for user
103 # subclasses
104 raise NotImplementedError
105
101106 def run(self, *args, **kwargs):
102107 """
103108 Executes `.Connection.run` on all member `Connections <.Connection>`.
106111
107112 .. versionadded:: 2.0
108113 """
109 # TODO: probably best to suck it up & match actual run() sig?
110114 # TODO: how to change method of execution across contents? subclass,
111115 # kwargs, additional methods, inject an executor? Doing subclass for
112116 # now, but not 100% sure it's the best route.
113117 # TODO: also need way to deal with duplicate connections (see THOUGHTS)
114 # TODO: and errors - probably FailureSet? How to handle other,
115 # regular, non Failure, exceptions though? Still need an aggregate
116 # exception type either way, whether it is FailureSet or what...
117 # TODO: OTOH, users may well want to be able to operate on the hosts
118 # that did not fail (esp if failure % is low) so we really _do_ want
119 # something like a result object mixing success and failure, or maybe a
120 # golang style two-tuple of successes and failures?
121 # TODO: or keep going w/ a "return or except", but the object is
122 # largely similar (if not identical) in both situations, with the
123 # exception just being the signal that Shit Broke?
124 raise NotImplementedError
125
126 # TODO: how to handle sudo? Probably just an inner worker method that takes
127 # the method name to actually call (run, sudo, etc)?
118 return self._do("run", *args, **kwargs)
119
120 def sudo(self, *args, **kwargs):
121 """
122 Executes `.Connection.sudo` on all member `Connections <.Connection>`.
123
124 :returns: a `.GroupResult`.
125
126 .. versionadded:: 2.6
127 """
128 # TODO: see run() TODOs
129 return self._do("sudo", *args, **kwargs)
128130
129131 # TODO: this all needs to mesh well with similar strategies applied to
130132 # entire tasks - so that may still end up factored out into Executors or
132134
133135 # TODO: local? Invoke wants ability to do that on its own though, which
134136 # would be distinct from Group. (May want to switch Group to use that,
135 # though, whatever it ends up being?)
137 # though, whatever it ends up being? Eg many cases where you do want to do
138 # some local thing either N times identically, or parameterized by remote
139 # cxn values)
140
141 def put(self, *args, **kwargs):
142 """
143 Executes `.Connection.put` on all member `Connections <.Connection>`.
144
145 This is a straightforward application: aside from whatever the concrete
146 group subclass does for concurrency or lack thereof, the effective
147 result is like running a loop over the connections and calling their
148 ``put`` method.
149
150 :returns:
151 a `.GroupResult` whose values are `.transfer.Result` instances.
152
153 .. versionadded:: 2.6
154 """
155 return self._do("put", *args, **kwargs)
136156
137157 def get(self, *args, **kwargs):
138158 """
139159 Executes `.Connection.get` on all member `Connections <.Connection>`.
140160
141 :returns: a `.GroupResult`.
142
143 .. versionadded:: 2.0
144 """
145 # TODO: probably best to suck it up & match actual get() sig?
146 # TODO: actually implement on subclasses
147 raise NotImplementedError
161 .. note::
162 This method changes some behaviors over e.g. directly calling
163 `.Connection.get` on a ``for`` loop of connections; the biggest is
164 that the implied default value for the ``local`` parameter is
165 ``"{host}/"``, which triggers use of local path parameterization
166 based on each connection's target hostname.
167
168 Thus, unless you override ``local`` yourself, a copy of the
169 downloaded file will be stored in (relative) directories named
170 after each host in the group.
171
172 .. warning::
173 Using file-like objects as the ``local`` argument is not currently
174 supported, as it would be equivalent to supplying that same object
175 to a series of individual ``get()`` calls.
176
177 :returns:
178 a `.GroupResult` whose values are `.transfer.Result` instances.
179
180 .. versionadded:: 2.6
181 """
182 # TODO: consider a backwards incompat change after we drop Py2 that
183 # just makes a lot of these kwarg-only methods? then below could become
184 # kwargs.setdefault() if desired.
185 # TODO: do we care enough to handle explicitly given, yet falsey,
186 # values? it's a lot more complexity for a corner case.
187 if len(args) < 2 and "local" not in kwargs:
188 kwargs["local"] = "{host}/"
189 return self._do("get", *args, **kwargs)
148190
149191 def close(self):
150192 """
169211 .. versionadded:: 2.0
170212 """
171213
172 def run(self, *args, **kwargs):
214 def _do(self, method, *args, **kwargs):
173215 results = GroupResult()
174216 excepted = False
175217 for cxn in self:
176218 try:
177 results[cxn] = cxn.run(*args, **kwargs)
219 results[cxn] = getattr(cxn, method)(*args, **kwargs)
178220 except Exception as e:
179221 results[cxn] = e
180222 excepted = True
183225 return results
184226
185227
186 def thread_worker(cxn, queue, args, kwargs):
187 result = cxn.run(*args, **kwargs)
228 def thread_worker(cxn, queue, method, args, kwargs):
229 result = getattr(cxn, method)(*args, **kwargs)
188230 # TODO: namedtuple or attrs object?
189231 queue.put((cxn, result))
190232
196238 .. versionadded:: 2.0
197239 """
198240
199 def run(self, *args, **kwargs):
241 def _do(self, method, *args, **kwargs):
200242 results = GroupResult()
201243 queue = Queue()
202244 threads = []
203245 for cxn in self:
204 my_kwargs = dict(cxn=cxn, queue=queue, args=args, kwargs=kwargs)
205246 thread = ExceptionHandlingThread(
206 target=thread_worker, kwargs=my_kwargs
247 target=thread_worker,
248 kwargs=dict(
249 cxn=cxn,
250 queue=queue,
251 method=method,
252 args=args,
253 kwargs=kwargs,
254 ),
207255 )
208256 threads.append(thread)
209257 for thread in threads:
210258 thread.start()
211259 for thread in threads:
212260 # TODO: configurable join timeout
213 # TODO: (in sudo's version) configurability around interactive
214 # prompting resulting in an exception instead, as in v1
215261 thread.join()
216262 # Get non-exception results from queue
217263 while not queue.empty():
0 import signal
1
02 from invoke import Runner, pty_size, Result as InvokeResult
13
24
3436 def start(self, command, shell, env, timeout=None):
3537 self.channel = self.context.create_session()
3638 if self.using_pty:
37 rows, cols = pty_size()
38 self.channel.get_pty(width=rows, height=cols)
39 # Set initial size to match local size
40 cols, rows = pty_size()
41 self.channel.get_pty(width=cols, height=rows)
42 # If platform supports, also respond to SIGWINCH (window change) by
43 # sending the sshd a window-change message to update
44 if hasattr(signal, "SIGWINCH"):
45 signal.signal(signal.SIGWINCH, self.handle_window_change)
3946 if env:
4047 # TODO: honor SendEnv from ssh_config (but if we do, _should_ we
4148 # honor it even when prefixing? That would depart from OpenSSH
5461 command = "export {} && {}".format(parameters, command)
5562 else:
5663 self.channel.update_environment(env)
64 self.send_start_message(command)
65
66 def send_start_message(self, command):
5767 self.channel.exec_command(command)
68
69 def run(self, command, **kwargs):
70 kwargs.setdefault("replace_env", True)
71 return super(Remote, self).run(command, **kwargs)
5872
5973 def read_proc_stdout(self, num_bytes):
6074 return self.channel.recv(num_bytes)
107121 # belong in invoke.Runner anyways?
108122 self.channel.close()
109123
124 def handle_window_change(self, signum, frame):
125 """
126 Respond to a `signal.SIGWINCH` (as a standard signal handler).
127
128 Sends a window resize command via Paramiko channel method.
129 """
130 self.channel.resize_pty(*pty_size())
131
110132 # TODO: shit that is in fab 1 run() but could apply to invoke.Local too:
111133 # * see rest of stuff in _run_command/_execute in operations.py...there is
112134 # a bunch that applies generally like optional exit codes, etc
127149 # * agent-forward close()
128150
129151
152 class RemoteShell(Remote):
153 def send_start_message(self, command):
154 self.channel.invoke_shell()
155
156
130157 class Result(InvokeResult):
131158 """
132159 An `invoke.runners.Result` exposing which `.Connection` was run against.
6565 # TODO: just leverage attrs, maybe vendored into Invoke so we don't
6666 # grow more dependencies? Ehhh
6767 return "<{} cmd={!r}>".format(self.__class__.__name__, self.cmd)
68
69 def expect_execution(self, channel):
70 """
71 Assert that the ``channel`` was used to run this command.
72
73 .. versionadded:: 2.7
74 """
75 channel.exec_command.assert_called_with(self.cmd or ANY)
76
77
78 class ShellCommand(Command):
79 """
80 A pseudo-command that expects an interactive shell to be executed.
81
82 .. versionadded:: 2.7
83 """
84
85 def expect_execution(self, channel):
86 channel.invoke_shell.assert_called_once_with()
6887
6988
7089 class MockChannel(Mock):
253272 for channel, command in zip(self.channels, self.commands):
254273 # Expect an open_session for each command exec
255274 session_opens.append(call())
256 # Expect that the channel gets an exec_command
257 channel.exec_command.assert_called_with(command.cmd or ANY)
275 # Expect that the channel gets an exec_command or etc
276 command.expect_execution(channel=channel)
258277 # Expect written stdin, if given
259278 if command.in_:
260279 assert channel._stdin.getvalue() == command.in_
375394 # Set up mocks
376395 self.os_patcher = patch("fabric.transfer.os")
377396 self.client_patcher = patch("fabric.connection.SSHClient")
397 self.path_patcher = patch("fabric.transfer.Path")
378398 mock_os = self.os_patcher.start()
379399 Client = self.client_patcher.start()
400 self.path_patcher.start()
380401 sftp = Client.return_value.open_sftp.return_value
381402
382403 # Handle common filepath massage actions; tests will assume these.
383404 def fake_abspath(path):
384 return "/local/{}".format(path)
405 # Run normpath to avoid tests not seeing abspath wrinkles (like
406 # trailing slash chomping)
407 return "/local/{}".format(os.path.normpath(path))
385408
386409 mock_os.path.abspath.side_effect = fake_abspath
387410 sftp.getcwd.return_value = "/remote"
391414 sftp.stat.return_value.st_mode = fake_mode
392415 mock_os.stat.return_value.st_mode = fake_mode
393416 # Not super clear to me why the 'wraps' functionality in mock isn't
394 # working for this :(
395 mock_os.path.basename.side_effect = os.path.basename
417 # working for this :( reinstate a bunch of os(.path) so it still works
418 mock_os.sep = os.sep
419 for name in ("basename", "split", "join", "normpath"):
420 getattr(mock_os.path, name).side_effect = getattr(os.path, name)
396421 # Return the sftp and OS mocks for use by decorator use case.
397422 return sftp, mock_os
398423
399424 def stop(self):
400425 self.os_patcher.stop()
401426 self.client_patcher.stop()
427 self.path_patcher.stop()
103103 """
104104 mock = MockSFTP(autostart=False)
105105 client, mock_os = mock.start()
106 # Regular ol transfer to save some time
106107 transfer = Transfer(Connection("host"))
107108 yield transfer, client, mock_os
108109 # TODO: old mock_sftp() lacked any 'stop'...why? feels bad man
44 import os
55 import posixpath
66 import stat
7
8 try:
9 from pathlib import Path
10 except ImportError:
11 from pathlib2 import Path
712
813 from .util import debug # TODO: actual logging! LOL
914
3944
4045 def get(self, remote, local=None, preserve_mode=True):
4146 """
42 Download a file from the current connection to the local filesystem.
47 Copy a file from wrapped connection's host to the local filesystem.
4348
4449 :param str remote:
4550 Remote file to download.
5964
6065 **If None or another 'falsey'/empty value is given** (the default),
6166 the remote file is downloaded to the current working directory (as
62 seen by `os.getcwd`) using its remote filename.
67 seen by `os.getcwd`) using its remote filename. (This is equivalent
68 to giving ``"{basename}"``; see the below subsection on
69 interpolation.)
6370
6471 **If a string is given**, it should be a path to a local directory
6572 or file and is subject to similar behavior as that seen by common
7077 '/tmp/')`` would result in creation or overwriting of
7178 ``/tmp/file.txt``).
7279
73 .. note::
74 When dealing with nonexistent file paths, normal Python file
75 handling concerns come into play - for example, a ``local``
76 path containing non-leaf directories which do not exist, will
77 typically result in an `OSError`.
80 This path will be **interpolated** with some useful parameters,
81 using `str.format`:
82
83 - The `.Connection` object's ``host``, ``user`` and ``port``
84 attributes.
85 - The ``basename`` and ``dirname`` of the ``remote`` path, as
86 derived by `os.path` (specifically, its ``posixpath`` flavor, so
87 that the resulting values are useful on remote POSIX-compatible
88 SFTP servers even if the local client is Windows).
89 - Thus, for example, ``"/some/path/{user}@{host}/{basename}"`` will
90 yield different local paths depending on the properties of both
91 the connection and the remote path.
92
93 .. note::
94 If nonexistent directories are present in this path (including
95 the final path component, if it ends in `os.sep`) they will be
96 created automatically using `os.makedirs`.
7897
7998 **If a file-like object is given**, the contents of the remote file
8099 are simply written into it.
86105 :returns: A `.Result` object.
87106
88107 .. versionadded:: 2.0
108 .. versionchanged:: 2.6
109 Added ``local`` path interpolation of connection & remote file
110 attributes.
111 .. versionchanged:: 2.6
112 Create missing ``local`` directories automatically.
89113 """
90114 # TODO: how does this API change if we want to implement
91115 # remote-to-remote file transfer? (Is that even realistic?)
92 # TODO: handle v1's string interpolation bits, especially the default
93 # one, or at least think about how that would work re: split between
94 # single and multiple server targets.
95116 # TODO: callback support
96117 # TODO: how best to allow changing the behavior/semantics of
97118 # remote/local (e.g. users might want 'safer' behavior that complains
106127 self.sftp.getcwd() or self.sftp.normalize("."), remote
107128 )
108129
109 # Massage local path:
110 # - handle file-ness
111 # - if path, fill with remote name if empty, & make absolute
130 # Massage local path
112131 orig_local = local
113132 is_file_like = hasattr(local, "write") and callable(local.write)
133 remote_filename = posixpath.basename(remote)
114134 if not local:
115 local = posixpath.basename(remote)
135 local = remote_filename
136 # Path-driven local downloads need interpolation, abspath'ing &
137 # directory creation
116138 if not is_file_like:
139 local = local.format(
140 host=self.connection.host,
141 user=self.connection.user,
142 port=self.connection.port,
143 dirname=posixpath.dirname(remote),
144 basename=remote_filename,
145 )
146 # Must treat dir vs file paths differently, lest we erroneously
147 # mkdir what was intended as a filename, and so that non-empty
148 # dir-like paths still get remote filename tacked on.
149 if local.endswith(os.sep):
150 dir_path = local
151 local = os.path.join(local, remote_filename)
152 else:
153 dir_path, _ = os.path.split(local)
117154 local = os.path.abspath(local)
155 Path(dir_path).mkdir(parents=True, exist_ok=True)
156 # TODO: reimplement mkdir (or otherwise write a testing function)
157 # allowing us to track what was created so we can revert if
158 # transfer fails.
159 # TODO: Alternately, transfer to temp location and then move, but
160 # that's basically inverse of v1's sudo-put which gets messy
118161
119162 # Run Paramiko-level .get() (side-effects only. womp.)
120163 # TODO: push some of the path handling into Paramiko; it should be
0 Metadata-Version: 2.1
1 Name: fabric
2 Version: 2.6.0
3 Summary: High level SSH command execution
4 Home-page: http://fabfile.org
5 Author: Jeff Forcier
6 Author-email: jeff@bitprophet.org
7 License: BSD
8 Description:
9 To find out what's new in this version of Fabric, please see `the changelog
10 <http://fabfile.org/changelog.html>`_.
11
12 Welcome to Fabric!
13 ==================
14
15 Fabric is a high level Python (2.7, 3.4+) library designed to execute shell
16 commands remotely over SSH, yielding useful Python objects in return. It builds
17 on top of `Invoke <http://pyinvoke.org>`_ (subprocess command execution and
18 command-line features) and `Paramiko <http://paramiko.org>`_ (SSH protocol
19 implementation), extending their APIs to complement one another and provide
20 additional functionality.
21
22 For a high level introduction, including example code, please see
23 `our main project website <http://fabfile.org>`_; or for detailed API docs, see
24 `the versioned API website <http://docs.fabfile.org>`_.
25
26
27 Platform: UNKNOWN
28 Classifier: Development Status :: 5 - Production/Stable
29 Classifier: Environment :: Console
30 Classifier: Intended Audience :: Developers
31 Classifier: Intended Audience :: System Administrators
32 Classifier: License :: OSI Approved :: BSD License
33 Classifier: Operating System :: POSIX
34 Classifier: Operating System :: Unix
35 Classifier: Operating System :: MacOS :: MacOS X
36 Classifier: Operating System :: Microsoft :: Windows
37 Classifier: Programming Language :: Python
38 Classifier: Programming Language :: Python :: 2
39 Classifier: Programming Language :: Python :: 2.7
40 Classifier: Programming Language :: Python :: 3
41 Classifier: Programming Language :: Python :: 3.4
42 Classifier: Programming Language :: Python :: 3.5
43 Classifier: Programming Language :: Python :: 3.6
44 Classifier: Programming Language :: Python :: 3.7
45 Classifier: Topic :: Software Development
46 Classifier: Topic :: Software Development :: Build Tools
47 Classifier: Topic :: Software Development :: Libraries
48 Classifier: Topic :: Software Development :: Libraries :: Python Modules
49 Classifier: Topic :: System :: Clustering
50 Classifier: Topic :: System :: Software Distribution
51 Classifier: Topic :: System :: Systems Administration
52 Provides-Extra: pytest
53 Provides-Extra: testing
0 LICENSE
1 MANIFEST.in
2 README.rst
3 dev-requirements.txt
4 setup.cfg
5 setup.py
6 tasks.py
7 fabric/__init__.py
8 fabric/__main__.py
9 fabric/_version.py
10 fabric/config.py
11 fabric/connection.py
12 fabric/exceptions.py
13 fabric/executor.py
14 fabric/group.py
15 fabric/main.py
16 fabric/runners.py
17 fabric/tasks.py
18 fabric/transfer.py
19 fabric/tunnels.py
20 fabric/util.py
21 fabric.egg-info/PKG-INFO
22 fabric.egg-info/SOURCES.txt
23 fabric.egg-info/dependency_links.txt
24 fabric.egg-info/entry_points.txt
25 fabric.egg-info/requires.txt
26 fabric.egg-info/top_level.txt
27 fabric/testing/__init__.py
28 fabric/testing/base.py
29 fabric/testing/fixtures.py
30 integration/concurrency.py
31 integration/connection.py
32 integration/group.py
33 integration/transfer.py
34 integration/_support/file.txt
35 integration/_support/funky-perms.txt
36 sites/shared_conf.py
37 sites/_shared_static/logo.png
38 sites/docs/cli.rst
39 sites/docs/conf.py
40 sites/docs/getting-started.rst
41 sites/docs/index.rst
42 sites/docs/upgrading.rst
43 sites/docs/api/config.rst
44 sites/docs/api/connection.rst
45 sites/docs/api/exceptions.rst
46 sites/docs/api/executor.rst
47 sites/docs/api/group.rst
48 sites/docs/api/runners.rst
49 sites/docs/api/tasks.rst
50 sites/docs/api/testing.rst
51 sites/docs/api/transfer.rst
52 sites/docs/api/tunnels.rst
53 sites/docs/api/util.rst
54 sites/docs/concepts/authentication.rst
55 sites/docs/concepts/configuration.rst
56 sites/docs/concepts/networking.rst
57 sites/www/changelog-v1.rst
58 sites/www/changelog.rst
59 sites/www/conf.py
60 sites/www/contact.rst
61 sites/www/development.rst
62 sites/www/faq.rst
63 sites/www/index.rst
64 sites/www/installing-1.x.rst
65 sites/www/installing.rst
66 sites/www/roadmap.rst
67 sites/www/troubleshooting.rst
68 sites/www/upgrading.rst
69 tests/_util.py
70 tests/config.py
71 tests/conftest.py
72 tests/connection.py
73 tests/executor.py
74 tests/group.py
75 tests/init.py
76 tests/main.py
77 tests/runners.py
78 tests/task.py
79 tests/transfer.py
80 tests/util.py
81 tests/_support/config.yml
82 tests/_support/fabfile.py
83 tests/_support/prompting.py
84 tests/_support/runtime_fabfile.py
85 tests/_support/json_conf/fabfile.py
86 tests/_support/json_conf/fabric.json
87 tests/_support/py_conf/fabfile.py
88 tests/_support/py_conf/fabric.py
89 tests/_support/ssh_config/both_proxies.conf
90 tests/_support/ssh_config/overridden_hostname.conf
91 tests/_support/ssh_config/proxyjump.conf
92 tests/_support/ssh_config/proxyjump_multi.conf
93 tests/_support/ssh_config/proxyjump_multi_recursive.conf
94 tests/_support/ssh_config/proxyjump_recursive.conf
95 tests/_support/ssh_config/runtime.conf
96 tests/_support/ssh_config/runtime_identity.conf
97 tests/_support/ssh_config/system.conf
98 tests/_support/ssh_config/user.conf
99 tests/_support/yaml_conf/fabfile.py
100 tests/_support/yaml_conf/fabric.yaml
101 tests/_support/yml_conf/fabfile.py
102 tests/_support/yml_conf/fabric.yml
0 [console_scripts]
1 fab = fabric.main:program.run
2
0 invoke<2.0,>=1.3
1 paramiko>=2.4
2 pathlib2
3
4 [pytest]
5 mock<3.0,>=2.0.0
6 pytest<4.0,>=3.2.5
7
8 [testing]
9 mock<3.0,>=2.0.0
+0
-1
fabric2 less more
0 fabric
00 import os
11 import time
22
3 try:
4 from invoke.vendor.six import StringIO
5 except ImportError:
6 from six import StringIO
7
38 from invoke import pty_size, CommandTimedOut
49 from pytest import skip, raises
10 from pytest_relaxed import trap
511
612 from fabric import Connection, Config
713
5359 assert tuple(map(int, found)), rows == cols
5460 # PTYs use \r\n, not \n, line separation
5561 assert "\r\n" in result.stdout
62 assert result.pty is True
63
64 class shell:
65 @trap
66 def base_case(self):
67 result = Connection("localhost").shell(
68 in_stream=StringIO("exit\n")
69 )
70 assert result.command is None
71 # Will also include any shell prompt, etc but just looking for the
72 # mirrored input is most test-env-agnostic way to spot check...
73 assert "exit" in result.stdout
74 assert result.stderr == ""
75 assert result.exited == 0
5676 assert result.pty is True
5777
5878 class local:
1111 [tool:pytest]
1212 testpaths = tests
1313 python_files = *
14
15 [egg_info]
16 tag_build =
17 tag_date = 0
18
6666 author="Jeff Forcier",
6767 author_email="jeff@bitprophet.org",
6868 url="http://fabfile.org",
69 install_requires=["invoke>=1.3,<2.0", "paramiko>=2.4"],
69 install_requires=["invoke>=1.3,<2.0", "paramiko>=2.4", "pathlib2"],
7070 extras_require={
7171 "testing": testing_deps,
7272 "pytest": testing_deps + pytest_deps,
4545 Overrides of Invoke-level defaults
4646 ----------------------------------
4747
48 - ``run.replace_env``: ``True``, instead of ``False``, so that remote commands
49 run with a 'clean', empty environment instead of inheriting a copy of the
50 current process' environment.
48 - ``run.replace_env``: defaults to ``True``, instead of ``False``, so that
49 remote commands run with a 'clean', empty environment instead of inheriting
50 a copy of the current process' environment.
5151
5252 This is for security purposes: leaking local environment data remotely by
5353 default would be unsanitary. It's also compatible with the behavior of
5555
5656 .. seealso::
5757 The warning under `paramiko.channel.Channel.set_environment_variable`.
58
59 .. note::
60 This is currently accomplished with a keyword argument override, as the
61 config setting itself was applying to both ``run`` and ``local``. Future
62 updates will ensure the two methods use separate config values.
5863
5964 Extensions to Invoke-level defaults
6065 -----------------------------------
1313 on top; user code will most often import from the ``fabric`` package, but
1414 you'll sometimes import directly from ``invoke`` or ``paramiko`` too:
1515
16 - `Invoke <https://pyinvoke.org>`_ implements CLI parsing, task organization,
16 - `Invoke <https://www.pyinvoke.org>`_ implements CLI parsing, task organization,
1717 and shell command execution (a generic framework plus specific implementation
1818 for local commands.)
1919
2323 - Fabric users will frequently import Invoke objects, in cases where Fabric
2424 itself has no need to subclass or otherwise modify what Invoke provides.
2525
26 - `Paramiko <https://paramiko.org>`_ implements low/mid level SSH
26 - `Paramiko <https://www.paramiko.org>`_ implements low/mid level SSH
2727 functionality - SSH and SFTP sessions, key management, etc.
2828
2929 - Fabric mostly uses this under the hood; users will only rarely import
290290
291291 >>> from fabric import Connection
292292 >>> for host in ('web1', 'web2', 'mac1'):
293 >>> result = Connection(host).run('uname -s')
293 ... result = Connection(host).run('uname -s')
294294 ... print("{}: {}".format(host, result.stdout.strip()))
295295 ...
296296 ...
44 .. note::
55 Looking for the Fabric 1.x changelog? See :doc:`/changelog-v1`.
66
7 - :feature:`-` Add `~fabric.connection.Connection.shell`, a belated port of
8 the v1 ``open_shell()`` feature.
9
10 - This wasn't needed initially, as the modern implementation of
11 `~fabric.connection.Connection.run` is as good or better for full
12 interaction than ``open_shell()`` was, provided you're happy supplying a
13 specific shell to execute.
14 - `~fabric.connection.Connection.shell` serves the corner case where you
15 *aren't* happy doing that, eg when you're speaking to network appliances or
16 other targets which are not typical Unix server environments.
17 - Like ``open_shell()``, this new method is primarily for interactive use,
18 and has a slightly less useful return value. See its API docs for more
19 details.
20
21 - :feature:`-` Forward local terminal resizes to the remote end, when
22 applicable. (For the technical: this means we now turn ``SIGWINCH`` into SSH
23 ``window-change`` messages.)
24 - :bug:`2142 major` Update `~fabric.connection.Connection` temporarily so that
25 it doesn't incidentally apply ``replace_env=True`` to local shell commands,
26 only remote ones. On Windows under Python 3.7+, this was causing local
27 commands to fail due to lack of some environment variables. Future updates
28 will cleanly separate the config tree for remote vs local methods.
29
30 Thanks to Bartosz Lachowicz for the report and David JM Emmett for the patch.
31 - :release:`2.6.0 <2021-01-18>`
32 - :bug:`- major` Fix a handful of issues in the handling and
33 mocking of SFTP local paths and ``os.path`` members within
34 :ref:`fabric.testing <testing-subpackage>`; this should remove some
35 occasional "useless Mocks" as well as hewing closer to the real behavior of
36 things like ``os.path.abspath`` re: path normalization.
37 - :feature:`-` When the ``local`` path argument to
38 `Transfer.get <fabric.transfer.Transfer.get>` contains nonexistent
39 directories, they are now created instead of raising an error.
40
41 .. warning::
42 This change introduces a new runtime dependency: ``pathlib2``.
43
44 - :feature:`1868` Ported a feature from v1: interpolating the local path
45 argument in `Transfer.get <fabric.transfer.Transfer.get>` with connection
46 and remote filepath attributes.
47
48 For example, ``cxn.get(remote="/var/log/foo.log", local="{host}/")`` is now
49 feasible for storing a file in per-host-named directories or files, and in
50 fact `Group.get <fabric.group.Group.get>` does this by default.
51 - :feature:`1810` Add `put <fabric.group.Group.put>`/`get
52 <fabric.group.Group.get>` support to `~fabric.group.Group`.
53 - :feature:`1999` Add `sudo <fabric.group.Group.sudo>` support to
54 `~fabric.group.Group`. Thanks to Bonnie Hardin for the report and to Winston
55 Nolan for an early patchset.
756 - :release:`2.5.0 <2019-08-06>`
857 - :support:`-` Update minimum Invoke version requirement to ``>=1.3``.
958 - :feature:`1985` Add support for explicitly closing remote subprocess' stdin
3737
3838 .. _irc:
3939
40 IRC
41 ---
40 Blog posts
41 ----------
4242
43 We maintain a semi-official IRC channel at ``#fabric`` on Freenode
44 (``irc://irc.freenode.net``) where the developers and other users may be found.
45 As always with IRC, we can't promise immediate responses, but some folks keep
46 logs of the channel and will try to get back to you when they can.
43 The developer posts occasional (but usually important) news on his blog; there
44 is a dedicated Fabric category: http://bitprophet.org/categories/fabric/
803803 * - ``open_shell`` for obtaining interactive-friendly remote shell sessions
804804 (something that ``run`` historically was bad at )
805805 - Ported
806 - Technically "removed", but only because the new version of
807 ``run`` is vastly improved and can deal with interactive sessions at
808 least as well as the old ``open_shell`` did, if not moreso.
809 ``c.run("/my/favorite/shell", pty=True)`` should be all you need.
806 - Not only is the new version of ``run`` vastly improved and able to deal
807 with interactive sessions at least as well as the old ``open_shell``
808 (provided you supply ``pty=True``), but for corner cases there's also a
809 direct port: `~fabric.connection.Connection.shell`.
810810
811811 ``run``
812812 ~~~~~~~
841841
842842 * - ``shell`` / ``env.use_shell`` designating whether or not to wrap
843843 commands within an explicit call to e.g. ``/bin/sh -c "real command"``
844 - `Pending <https://github.com/pyinvoke/invoke/issues/344>`__/Removed
844 - `Pending <https://github.com/pyinvoke/invoke/issues/459>`__/Removed
845845 - See the note above under ``run`` for details on shell wrapping
846846 as a general strategy; unfortunately for ``sudo``, some sort of manual
847847 wrapping is still necessary for nontrivial commands (i.e. anything
876876 `os.execve` (or similar) is used instead of `subprocess.Popen`.
877877 Behavior is much the same: no shell wrapping (as in legacy ``run``),
878878 just informing the operating system what actual program to run.
879
880 ``open_shell``
881 ~~~~~~~~~~~~~~
882
883 As noted in the main list, this is now `~fabric.connection.Connection.shell`,
884 and behaves similarly to ``open_shell`` (exit codes, if any, are ignored; a PTY
885 is assumed; etc). It has some improvements too, such as a return value (which
886 is slightly lacking compared to that from `~fabric.connection.Connection.run`
887 but still a big improvement over ``None``).
888
889 .. list-table::
890 :widths: 40 10 50
891
892 * - ``command`` optional kwarg allowing 'prefilling' the input stream with
893 a specific command string plus newline
894 - Removed
895 - If you needed this, you should instead try the modern version of
896 `~fabric.connection.Connection.run`, which is equally capable of
897 interaction as `~fabric.connection.Connection.shell` but takes a
898 command to execute. There's a small chance we'll add this back later if
899 anybody misses it (there's a few corner cases that could possibly want
900 it).
879901
880902 .. _upgrading-utility:
881903
11261148 own, so it's gone.
11271149 * - Naming downloaded files after some aspect of the remote destination, to
11281150 avoid overwriting during multi-server actions
1129 - `Pending <https://github.com/fabric/fabric/issues/1868>`__
1130 - This falls under the `~fabric.group.Group` family, which still needs
1131 some work in this regard.
1151 - Ported
1152 - Added back (to `fabric.transfer.Transfer.get`) in Fabric 2.6.
11321153
11331154
11341155 .. _upgrading-configuration:
55 from invoke.vendor.lexicon import Lexicon
66 from pytest_relaxed import trap
77
8 from fabric import Connection as Connection_, Config as Config_
98 from fabric.main import make_program
10 from paramiko import SSHConfig
119
1210
1311 support = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_support")
5048 assert False, err.format(test)
5149
5250
53 # Locally override Connection, Config with versions that supply a dummy
54 # SSHConfig and thus don't load any test-running user's own ssh_config files.
55 # TODO: find a cleaner way to do this, though I don't really see any that isn't
56 # adding a ton of fixtures everywhere (and thus, opening up to forgetting it
57 # for new tests...)
58 class Config(Config_):
59 def __init__(self, *args, **kwargs):
60 wat = "You're giving ssh_config explicitly, please use Config_!"
61 assert "ssh_config" not in kwargs, wat
62 # Give ssh_config explicitly -> shorter way of turning off loading
63 kwargs["ssh_config"] = SSHConfig()
64 super(Config, self).__init__(*args, **kwargs)
65
66
67 class Connection(Connection_):
68 def __init__(self, *args, **kwargs):
69 # Make sure we're using our tweaked Config if none was given.
70 kwargs.setdefault("config", Config())
71 super(Connection, self).__init__(*args, **kwargs)
72
73
7451 def faux_v1_env():
7552 # Close enough to v1 _AttributeDict...
7653 # Contains a copy of enough of v1's defaults to prevent us having to do a
11 from os.path import join, expanduser
22
33 from paramiko.config import SSHConfig
4 from invoke import Local
45 from invoke.vendor.lexicon import Lexicon
56
6 from fabric import Config
7 from fabric import Config, Remote, RemoteShell
78 from fabric.util import get_local_user
89
910 from mock import patch, call
4950
5051 def overrides_some_Invoke_defaults(self):
5152 config = Config()
52 # This value defaults to False in Invoke proper.
53 assert config.run.replace_env is True
5453 assert config.tasks.collection_name == "fabfile"
54
55 def amends_Invoke_runners_map(self):
56 config = Config()
57 assert config.runners == dict(
58 remote=Remote, remote_shell=RemoteShell, local=Local
59 )
5560
5661 def uses_Fabric_prefix(self):
5762 # NOTE: see also the integration-esque tests in tests/main.py; this
00 # flake8: noqa
11 from fabric.testing.fixtures import client, remote, sftp, sftp_objs, transfer
2
3 from os.path import isfile, expanduser
4
5 from pytest import fixture
6
7 from mock import patch
8
9
10 # TODO: does this want to end up in the public fixtures module too?
11 @fixture(autouse=True)
12 def no_user_ssh_config():
13 """
14 Cowardly refuse to ever load what looks like user SSH config paths.
15
16 Prevents the invoking user's real config from gumming up test results or
17 inflating test runtime (eg if it sets canonicalization on, which will incur
18 DNS lookups for nearly all of this suite's bogus names).
19 """
20 # An ugly, but effective, hack. I am not proud. I also don't see anything
21 # that's >= as bulletproof and less ugly?
22 # TODO: ideally this should expand to cover system config paths too, but
23 # that's even less likely to be an issue.
24 def no_config_for_you(path):
25 if path == expanduser("~/.ssh/config"):
26 return False
27 return isfile(path)
28
29 with patch("fabric.config.os.path.isfile", no_config_for_you):
30 yield
00 from itertools import chain, repeat
11
22 try:
3 from invoke.vendor.six import b
3 from invoke.vendor.six import b, StringIO
44 except ImportError:
5 from six import b
5 from six import b, StringIO
6
67 import errno
78 from os.path import join
89 import socket
1112 from mock import patch, Mock, call, ANY
1213 from paramiko.client import SSHClient, AutoAddPolicy
1314 from paramiko import SSHConfig
14 import pytest # for mark
15 import pytest # for mark, internal raises
1516 from pytest import skip, param
1617 from pytest_relaxed import raises
1718 from invoke.vendor.lexicon import Lexicon
1920 from invoke.config import Config as InvokeConfig
2021 from invoke.exceptions import ThreadException
2122
22 from fabric import Config as Config_
23 from fabric import Config, Connection
2324 from fabric.exceptions import InvalidV1Env
2425 from fabric.util import get_local_user
2526
26 from _util import support, Connection, Config, faux_v1_env
27 from _util import support, faux_v1_env
2728
2829
2930 # Remote is woven in as a config default, so must be patched there
3031 remote_path = "fabric.config.Remote"
32 remote_shell_path = "fabric.config.RemoteShell"
3133
3234
3335 def _select_result(obj):
264266 runtime_path = join(support, "ssh_config", confname)
265267 if overrides is None:
266268 overrides = {}
267 return Config_(
269 return Config(
268270 runtime_ssh_path=runtime_path, overrides=overrides
269271 )
270272
273275 return Connection("runtime", config=config)
274276
275277 def effectively_blank_when_no_loaded_config(self):
276 c = Config_(ssh_config=SSHConfig())
278 c = Config(ssh_config=SSHConfig())
277279 cxn = Connection("host", config=c)
278280 # NOTE: paramiko always injects this even if you look up a host
279281 # that has no rules, even wildcard ones.
305307 path = join(
306308 support, "ssh_config", "overridden_hostname.conf"
307309 )
308 config = Config_(runtime_ssh_path=path)
310 config = Config(runtime_ssh_path=path)
309311 cxn = Connection("aliasname", config=config)
310312 assert cxn.host == "realname"
311313 assert cxn.original_host == "aliasname"
858860 config_kwargs["overrides"] = {
859861 "connect_kwargs": {"key_filename": ["configured.key"]}
860862 }
861 conf = Config_(**config_kwargs)
863 conf = Config(**config_kwargs)
862864 connect_kwargs = {}
863865 if kwarg:
864866 # Stitch in connect_kwargs value
949951 self, Remote, client
950952 ):
951953 remote = Remote.return_value
952 sentinel = object()
953 remote.run.return_value = sentinel
954954 c = Connection("host")
955955 r1 = c.run("command")
956956 r2 = c.run("command", warn=True, hide="stderr")
958958 # .assert_called_with()) stopped working, apparently triggered by
959959 # our code...somehow...after commit (roughly) 80906c7.
960960 # And yet, .call_args_list and its brethren work fine. Wha?
961 Remote.assert_any_call(c, inline_env=False)
961 Remote.assert_any_call(context=c, inline_env=False)
962962 remote.run.assert_has_calls(
963963 [call("command"), call("command", warn=True, hide="stderr")]
964964 )
965965 for r in (r1, r2):
966 assert r is sentinel
966 assert r is remote.run.return_value
967
968 class shell:
969 def setup(self):
970 self.defaults = Config.global_defaults()["run"]
971
972 @patch(remote_shell_path)
973 def calls_RemoteShell_run_with_all_kwargs_and_returns_its_result(
974 self, RemoteShell, client
975 ):
976 remote = RemoteShell.return_value
977 cxn = Connection("host")
978 kwargs = dict(
979 env={"foo": "bar"},
980 replace_env=True,
981 encoding="utf-16",
982 in_stream=StringIO("meh"),
983 watchers=["meh"],
984 )
985 result = cxn.shell(**kwargs)
986 RemoteShell.assert_any_call(context=cxn)
987 assert remote.run.call_count == 1
988 # Expect explicit use of default values for all kwarg-settings
989 # besides what shell() itself tweaks
990 expected = dict(self.defaults, pty=True, command=None, **kwargs)
991 assert remote.run.call_args[1] == expected
992 assert result is remote.run.return_value
993
994 def raises_TypeError_for_disallowed_kwargs(self, client):
995 for key in self.defaults.keys():
996 if key in (
997 "env",
998 "replace_env",
999 "encoding",
1000 "in_stream",
1001 "watchers",
1002 ):
1003 continue
1004 with pytest.raises(
1005 TypeError,
1006 match=r"unexpected keyword arguments: \['{}'\]".format(
1007 key
1008 ),
1009 ):
1010 Connection("host").shell(**{key: "whatever"})
1011
1012 @patch(remote_shell_path)
1013 def honors_config_system_for_allowed_kwargs(self, RemoteShell, client):
1014 remote = RemoteShell.return_value
1015 allowed = dict(
1016 env={"foo": "bar"},
1017 replace_env=True,
1018 encoding="utf-16",
1019 in_stream="sentinel",
1020 watchers=["sentinel"],
1021 )
1022 ignored = dict(echo=True, hide="foo") # Spot check
1023 config = Config({"run": dict(allowed, **ignored)})
1024 cxn = Connection("host", config=config)
1025 cxn.shell()
1026 kwargs = remote.run.call_args[1]
1027 for key, value in allowed.items():
1028 assert kwargs[key] == value
1029 for key, value in ignored.items():
1030 assert kwargs[key] == self.defaults[key]
9671031
9681032 class local:
9691033 # NOTE: most tests for this functionality live in Invoke's runner
10011065 # Remote.return_value is two different Mocks now, despite Remote's
10021066 # own Mock having the same ID here and in code under test. WTF!!)
10031067 expected = [
1004 call(cxn, inline_env=False),
1068 call(context=cxn, inline_env=False),
10051069 call().run(cmd, watchers=ANY),
10061070 ]
10071071 assert Remote.mock_calls == expected
00 from mock import Mock, patch, call
1 from pytest_relaxed import raises
1 from pytest import mark, raises
22
33 from fabric import Connection, Group, SerialGroup, ThreadingGroup, GroupResult
44 from fabric.group import thread_worker
55 from fabric.exceptions import GroupException
6
7
8 RUNNER_METHODS = ("run", "sudo")
9 TRANSFER_METHODS = ("put", "get")
10 ALL_METHODS = RUNNER_METHODS + TRANSFER_METHODS
11 runner_args = ("command",)
12 runner_kwargs = dict(hide=True, warn=True)
13 transfer_args = tuple()
14 transfer_kwargs = dict(local="yokel", remote="goat")
15 ARGS_BY_METHOD = dict(
16 run=runner_args, sudo=runner_args, put=transfer_args, get=transfer_args
17 )
18 KWARGS_BY_METHOD = dict(
19 run=runner_kwargs,
20 sudo=runner_kwargs,
21 put=transfer_kwargs,
22 get=transfer_kwargs,
23 )
624
725
826 class Group_:
4058 for c in g:
4159 assert isinstance(c, Connection)
4260
43 class run:
44 @raises(NotImplementedError)
45 def not_implemented_in_base_class(self):
46 Group().run()
61 @mark.parametrize("method", ALL_METHODS)
62 def abstract_methods_not_implemented(self, method):
63 group = Group()
64 with raises(NotImplementedError):
65 getattr(group, method)()
4766
4867 class close_and_contextmanager_behavior:
4968 def close_closes_all_member_connections(self):
6180 for c in cxns:
6281 c.close.assert_called_once_with()
6382
64
65 def _make_serial_tester(cxns, index, args, kwargs):
83 class get:
84 class local_defaults_to_host_interpolated_path:
85 def when_no_arg_or_kwarg_given(self):
86 g = Group("host1", "host2")
87 g._do = Mock()
88 g.get(remote="whatever")
89 g._do.assert_called_with(
90 "get", remote="whatever", local="{host}/"
91 )
92
93 def not_when_arg_given(self):
94 g = Group("host1", "host2")
95 g._do = Mock()
96 g.get("whatever", "lol")
97 # No local kwarg passed.
98 g._do.assert_called_with("get", "whatever", "lol")
99
100 def not_when_kwarg_given(self):
101 g = Group("host1", "host2")
102 g._do = Mock()
103 g.get(remote="whatever", local="lol")
104 # Doesn't stomp given local arg
105 g._do.assert_called_with("get", remote="whatever", local="lol")
106
107
108 def _make_serial_tester(method, cxns, index, args, kwargs):
66109 args = args[:]
67110 kwargs = kwargs.copy()
68111
71114 predecessors = cxns[:car]
72115 successors = cxns[cdr:]
73116 for predecessor in predecessors:
74 predecessor.run.assert_called_with(*args, **kwargs)
117 getattr(predecessor, method).assert_called_with(*args, **kwargs)
75118 for successor in successors:
76 assert not successor.run.called
119 assert not getattr(successor, method).called
77120
78121 return tester
79122
80123
81124 class SerialGroup_:
82 class run:
83 def executes_arguments_on_contents_run_serially(self):
84 "executes arguments on contents' run() serially"
85 cxns = [Connection(x) for x in ("host1", "host2", "host3")]
86 args = ("command",)
87 kwargs = {"hide": True, "warn": True}
88 for index, cxn in enumerate(cxns):
89 side_effect = _make_serial_tester(cxns, index, args, kwargs)
90 cxn.run = Mock(side_effect=side_effect)
91 g = SerialGroup.from_connections(cxns)
92 g.run(*args, **kwargs)
93 # Sanity check, e.g. in case none of them were actually run
94 for cxn in cxns:
95 cxn.run.assert_called_with(*args, **kwargs)
96
97 def errors_in_execution_capture_and_continue_til_end(self):
98 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
99
100 class OhNoz(Exception):
101 pass
102
103 onoz = OhNoz()
104 cxns[1].run.side_effect = onoz
105 g = SerialGroup.from_connections(cxns)
106 try:
107 g.run("whatever", hide=True)
108 except GroupException as e:
109 result = e.result
110 else:
111 assert False, "Did not raise GroupException!"
112 succeeded = {
113 cxns[0]: cxns[0].run.return_value,
114 cxns[2]: cxns[2].run.return_value,
115 }
116 failed = {cxns[1]: onoz}
117 expected = succeeded.copy()
118 expected.update(failed)
119 assert result == expected
120 assert result.succeeded == succeeded
121 assert result.failed == failed
122
123 def returns_results_mapping(self):
124 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
125 g = SerialGroup.from_connections(cxns)
126 result = g.run("whatever", hide=True)
127 assert isinstance(result, GroupResult)
128 expected = {x: x.run.return_value for x in cxns}
129 assert result == expected
130 assert result.succeeded == expected
131 assert result.failed == {}
125 @mark.parametrize("method", ALL_METHODS)
126 def executes_arguments_on_contents_run_serially(self, method):
127 "executes arguments on contents' run() serially"
128 cxns = [Connection(x) for x in ("host1", "host2", "host3")]
129 args = ARGS_BY_METHOD[method]
130 kwargs = KWARGS_BY_METHOD[method]
131 for index, cxn in enumerate(cxns):
132 side_effect = _make_serial_tester(
133 method, cxns, index, args, kwargs
134 )
135 setattr(cxn, method, Mock(side_effect=side_effect))
136 g = SerialGroup.from_connections(cxns)
137 getattr(g, method)(*args, **kwargs)
138 # Sanity check, e.g. in case none of them were actually run
139 for cxn in cxns:
140 getattr(cxn, method).assert_called_with(*args, **kwargs)
141
142 @mark.parametrize("method", ALL_METHODS)
143 def errors_in_execution_capture_and_continue_til_end(self, method):
144 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
145
146 class OhNoz(Exception):
147 pass
148
149 onoz = OhNoz()
150 getattr(cxns[1], method).side_effect = onoz
151 g = SerialGroup.from_connections(cxns)
152 try:
153 getattr(g, method)("whatever", hide=True)
154 except GroupException as e:
155 result = e.result
156 else:
157 assert False, "Did not raise GroupException!"
158 succeeded = {
159 cxns[0]: getattr(cxns[0], method).return_value,
160 cxns[2]: getattr(cxns[2], method).return_value,
161 }
162 failed = {cxns[1]: onoz}
163 expected = succeeded.copy()
164 expected.update(failed)
165 assert result == expected
166 assert result.succeeded == succeeded
167 assert result.failed == failed
168
169 @mark.parametrize("method", ALL_METHODS)
170 def returns_results_mapping(self, method):
171 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
172 g = SerialGroup.from_connections(cxns)
173 result = getattr(g, method)("whatever", hide=True)
174 assert isinstance(result, GroupResult)
175 expected = {x: getattr(x, method).return_value for x in cxns}
176 assert result == expected
177 assert result.succeeded == expected
178 assert result.failed == {}
132179
133180
134181 class ThreadingGroup_:
135182 def setup(self):
136183 self.cxns = [Connection(x) for x in ("host1", "host2", "host3")]
137 self.args = ("command",)
138 self.kwargs = {"hide": True, "warn": True}
139
140 class run:
141 @patch("fabric.group.Queue")
142 @patch("fabric.group.ExceptionHandlingThread")
143 def executes_arguments_on_contents_run_via_threading(
144 self, Thread, Queue
184
185 @mark.parametrize("method", ALL_METHODS)
186 @patch("fabric.group.Queue")
187 @patch("fabric.group.ExceptionHandlingThread")
188 def executes_arguments_on_contents_run_via_threading(
189 self, Thread, Queue, method
190 ):
191 queue = Queue.return_value
192 g = ThreadingGroup.from_connections(self.cxns)
193 # Make sure .exception() doesn't yield truthy Mocks. Otherwise we
194 # end up with 'exceptions' that cause errors due to all being the
195 # same.
196 Thread.return_value.exception.return_value = None
197 args = ARGS_BY_METHOD[method]
198 kwargs = KWARGS_BY_METHOD[method]
199 getattr(g, method)(*args, **kwargs)
200 # Testing that threads were used the way we expect is mediocre but
201 # I honestly can't think of another good way to assert "threading
202 # was used & concurrency occurred"...
203 instantiations = [
204 call(
205 target=thread_worker,
206 kwargs=dict(
207 cxn=cxn,
208 queue=queue,
209 method=method,
210 args=args,
211 kwargs=kwargs,
212 ),
213 )
214 for cxn in self.cxns
215 ]
216 Thread.assert_has_calls(instantiations, any_order=True)
217 # These ought to work as by default a Mock.return_value is a
218 # singleton mock object
219 expected = len(self.cxns)
220 for name, got in (
221 ("start", Thread.return_value.start.call_count),
222 ("join", Thread.return_value.join.call_count),
145223 ):
146 queue = Queue.return_value
147 g = ThreadingGroup.from_connections(self.cxns)
148 # Make sure .exception() doesn't yield truthy Mocks. Otherwise we
149 # end up with 'exceptions' that cause errors due to all being the
150 # same.
151 Thread.return_value.exception.return_value = None
152 g.run(*self.args, **self.kwargs)
153 # Testing that threads were used the way we expect is mediocre but
154 # I honestly can't think of another good way to assert "threading
155 # was used & concurrency occurred"...
156 instantiations = [
157 call(
158 target=thread_worker,
159 kwargs=dict(
160 cxn=cxn,
161 queue=queue,
162 args=self.args,
163 kwargs=self.kwargs,
164 ),
165 )
166 for cxn in self.cxns
167 ]
168 Thread.assert_has_calls(instantiations, any_order=True)
169 # These ought to work as by default a Mock.return_value is a
170 # singleton mock object
171 expected = len(self.cxns)
172 for name, got in (
173 ("start", Thread.return_value.start.call_count),
174 ("join", Thread.return_value.join.call_count),
175 ):
176 err = (
177 "Expected {} calls to ExceptionHandlingThread.{}, got {}"
178 ) # noqa
179 err = err.format(expected, name, got)
180 assert expected, got == err
181
182 @patch("fabric.group.Queue")
183 def queue_used_to_return_results(self, Queue):
184 # Regular, explicit, mocks for Connections
185 cxns = [Mock(host=x) for x in ("host1", "host2", "host3")]
186 # Set up Queue with enough behavior to work / assert
187 queue = Queue.return_value
188 # Ending w/ a True will terminate a while-not-empty loop
189 queue.empty.side_effect = (False, False, False, True)
190 fakes = [(x, x.run.return_value) for x in cxns]
191 queue.get.side_effect = fakes[:]
192 # Execute & inspect results
193 g = ThreadingGroup.from_connections(cxns)
194 results = g.run(*self.args, **self.kwargs)
195 expected = {x: x.run.return_value for x in cxns}
196 assert results == expected
197 # Make sure queue was used as expected within worker &
198 # ThreadingGroup.run()
199 puts = [call(x) for x in fakes]
200 queue.put.assert_has_calls(puts, any_order=True)
201 assert queue.empty.called
202 gets = [call(block=False) for _ in cxns]
203 queue.get.assert_has_calls(gets)
204
205 def bubbles_up_errors_within_threads(self):
206 # TODO: I feel like this is the first spot where a raw
207 # ThreadException might need tweaks, at least presentation-wise,
208 # since we're no longer dealing with truly background threads (IO
209 # workers and tunnels), but "middle-ground" threads the user is
210 # kind of expecting (and which they might expect to encounter
211 # failures).
212 cxns = [Mock(host=x) for x in ("host1", "host2", "host3")]
213
214 class OhNoz(Exception):
215 pass
216
217 onoz = OhNoz()
218 cxns[1].run.side_effect = onoz
219 g = ThreadingGroup.from_connections(cxns)
220 try:
221 g.run(*self.args, **self.kwargs)
222 except GroupException as e:
223 result = e.result
224 else:
225 assert False, "Did not raise GroupException!"
226 succeeded = {
227 cxns[0]: cxns[0].run.return_value,
228 cxns[2]: cxns[2].run.return_value,
229 }
230 failed = {cxns[1]: onoz}
231 expected = succeeded.copy()
232 expected.update(failed)
233 assert result == expected
234 assert result.succeeded == succeeded
235 assert result.failed == failed
236
237 def returns_results_mapping(self):
238 # TODO: update if/when we implement ResultSet
239 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
240 g = ThreadingGroup.from_connections(cxns)
241 result = g.run("whatever", hide=True)
242 assert isinstance(result, GroupResult)
243 expected = {x: x.run.return_value for x in cxns}
244 assert result == expected
245 assert result.succeeded == expected
246 assert result.failed == {}
224 err = (
225 "Expected {} calls to ExceptionHandlingThread.{}, got {}"
226 ) # noqa
227 err = err.format(expected, name, got)
228 assert expected, got == err
229
230 @mark.parametrize("method", ALL_METHODS)
231 @patch("fabric.group.Queue")
232 def queue_used_to_return_results(self, Queue, method):
233 # Regular, explicit, mocks for Connections
234 cxns = [Mock(host=x) for x in ("host1", "host2", "host3")]
235 # Set up Queue with enough behavior to work / assert
236 queue = Queue.return_value
237 # Ending w/ a True will terminate a while-not-empty loop
238 queue.empty.side_effect = (False, False, False, True)
239 fakes = [(x, getattr(x, method).return_value) for x in cxns]
240 queue.get.side_effect = fakes[:]
241 # Execute & inspect results
242 g = ThreadingGroup.from_connections(cxns)
243 results = getattr(g, method)(
244 *ARGS_BY_METHOD[method], **KWARGS_BY_METHOD[method]
245 )
246 expected = {x: getattr(x, method).return_value for x in cxns}
247 assert results == expected
248 # Make sure queue was used as expected within worker &
249 # ThreadingGroup.run()
250 puts = [call(x) for x in fakes]
251 queue.put.assert_has_calls(puts, any_order=True)
252 assert queue.empty.called
253 gets = [call(block=False) for _ in cxns]
254 queue.get.assert_has_calls(gets)
255
256 @mark.parametrize("method", ALL_METHODS)
257 def bubbles_up_errors_within_threads(self, method):
258 # TODO: I feel like this is the first spot where a raw
259 # ThreadException might need tweaks, at least presentation-wise,
260 # since we're no longer dealing with truly background threads (IO
261 # workers and tunnels), but "middle-ground" threads the user is
262 # kind of expecting (and which they might expect to encounter
263 # failures).
264 cxns = [Mock(host=x) for x in ("host1", "host2", "host3")]
265
266 class OhNoz(Exception):
267 pass
268
269 onoz = OhNoz()
270 getattr(cxns[1], method).side_effect = onoz
271 g = ThreadingGroup.from_connections(cxns)
272 try:
273 getattr(g, method)(
274 *ARGS_BY_METHOD[method], **KWARGS_BY_METHOD[method]
275 )
276 except GroupException as e:
277 result = e.result
278 else:
279 assert False, "Did not raise GroupException!"
280 succeeded = {
281 cxns[0]: getattr(cxns[0], method).return_value,
282 cxns[2]: getattr(cxns[2], method).return_value,
283 }
284 failed = {cxns[1]: onoz}
285 expected = succeeded.copy()
286 expected.update(failed)
287 assert result == expected
288 assert result.succeeded == succeeded
289 assert result.failed == failed
290
291 @mark.parametrize("method", ALL_METHODS)
292 def returns_results_mapping(self, method):
293 cxns = [Mock(name=x) for x in ("host1", "host2", "host3")]
294 g = ThreadingGroup.from_connections(cxns)
295 result = getattr(g, method)("whatever", hide=True)
296 assert isinstance(result, GroupResult)
297 expected = {x: getattr(x, method).return_value for x in cxns}
298 assert result == expected
299 assert result.succeeded == expected
300 assert result.failed == {}
1313
1414 def Remote(self):
1515 assert fabric.Remote is runners.Remote
16
17 def RemoteShell(self):
18 assert fabric.RemoteShell is runners.RemoteShell
1619
1720 def Result(self):
1821 assert fabric.Result is runners.Result
22 except ImportError:
33 from six import StringIO
44
5 from mock import Mock
5 from mock import Mock, patch
66 from pytest import skip # noqa
77
88 from invoke import pty_size, Result
99
10 from fabric import Config, Connection, Remote
10 from fabric import Config, Connection, Remote, RemoteShell
1111
1212
1313 # On most systems this will explode if actually executed as a shell command;
2929 c = _Connection("host")
3030 assert Remote(context=c).context is c
3131
32 class env:
33 def replaces_when_replace_env_True(self, remote):
34 env = _runner().run(CMD, env={"JUST": "ME"}, replace_env=True).env
35 assert env == {"JUST": "ME"}
36
37 def augments_when_replace_env_False(self, remote):
38 env = _runner().run(CMD, env={"JUST": "ME"}, replace_env=False).env
39 assert (
40 "PATH" in env
41 ) # assuming this will be in every test environment
42 assert "JUST" in env
43 assert env["JUST"] == "ME"
44
3245 class run:
3346 def calls_expected_paramiko_bits(self, remote):
3447 # remote mocking makes generic sanity checks like "were
4255 fakeout = StringIO()
4356 _runner().run(CMD, out_stream=fakeout)
4457 assert fakeout.getvalue() == "hello yes this is dog"
45
46 def pty_True_uses_paramiko_get_pty(self, remote):
47 chan = remote.expect()
48 _runner().run(CMD, pty=True)
49 cols, rows = pty_size()
50 chan.get_pty.assert_called_with(width=cols, height=rows)
5158
5259 def return_value_is_Result_subclass_exposing_cxn_used(self, remote):
5360 c = _Connection("host")
106113 else:
107114 assert False, "Weird, Oops never got raised..."
108115
116 class pty_True:
117 def uses_paramiko_get_pty_with_local_size(self, remote):
118 chan = remote.expect()
119 _runner().run(CMD, pty=True)
120 cols, rows = pty_size()
121 chan.get_pty.assert_called_with(width=cols, height=rows)
122
123 @patch("fabric.runners.signal")
124 def no_SIGWINCH_means_no_handler(self, signal, remote):
125 delattr(signal, "SIGWINCH")
126 remote.expect()
127 _runner().run(CMD, pty=True)
128 assert not signal.signal.called
129
130 @patch("fabric.runners.signal")
131 def SIGWINCH_handled_when_present(self, signal, remote):
132 remote.expect()
133 runner = _runner()
134 runner.run(CMD, pty=True)
135 signal.signal.assert_called_once_with(
136 signal.SIGWINCH, runner.handle_window_change
137 )
138
139 def window_change_handler_uses_resize_pty(self):
140 runner = _runner()
141 runner.channel = Mock()
142 runner.handle_window_change(None, None)
143 cols, rows = pty_size()
144 runner.channel.resize_pty.assert_called_once_with(cols, rows)
145
109146 # TODO: how much of Invoke's tests re: the upper level run() (re:
110147 # things like returning Result, behavior of Result, etc) to
111148 # duplicate here? Ideally none or very few core ones.
132169 r.run(CMD, env={"PATH": "/opt/bin", "DEBUG": "1"})
133170 assert not chan.update_environment.called
134171
172 def send_start_message_sends_exec_command(self):
173 runner = Remote(context=None)
174 runner.channel = Mock()
175 runner.send_start_message(command="whatever")
176 runner.channel.exec_command.assert_called_once_with("whatever")
177
135178 def kill_closes_the_channel(self):
136179 runner = _runner()
137180 runner.channel = Mock()
138181 runner.kill()
139182 runner.channel.close.assert_called_once_with()
183
184
185 class RemoteShell_:
186 def send_start_message_sends_invoke_shell(self):
187 runner = RemoteShell(context=None)
188 runner.channel = Mock()
189 runner.send_start_message(command=None)
190 runner.channel.invoke_shell.assert_called_once_with()
22 except ImportError:
33 from six import StringIO
44
5 from mock import Mock, call
5 from mock import Mock, call, patch
66 from pytest_relaxed import raises
77 from pytest import skip # noqa
88 from paramiko import SFTPAttributes
9191 def remote_arg_cannot_be_empty_string(self, transfer):
9292 transfer.get("")
9393
94 class local_arg_interpolation:
95 def connection_params(self, transfer):
96 result = transfer.get("somefile", "{user}@{host}-{port}")
97 expected = "/local/{}@host-22".format(transfer.connection.user)
98 assert result.local == expected
99
100 def connection_params_as_dir(self, transfer):
101 result = transfer.get("somefile", "{host}/")
102 assert result.local == "/local/host/somefile"
103
104 def remote_path_posixpath_bits(self, transfer):
105 result = transfer.get(
106 "parent/mid/leaf", "foo/{dirname}/bar/{basename}"
107 )
108 # Recall that test harness sets remote apparent cwd as
109 # /remote/, thus dirname is /remote/parent/mid
110 assert result.local == "/local/foo/remote/parent/mid/bar/leaf"
111
94112 class file_like_local_paths:
95113 "file-like local paths"
96114
131149 client.stat.return_value = self.attrs
132150 transfer.get("file", local="meh", preserve_mode=False)
133151 assert not mock_os.chmod.called
152
153 class local_directory_creation:
154 @patch("fabric.transfer.Path")
155 def without_trailing_slash_means_leaf_file(self, Path, sftp_objs):
156 transfer, client = sftp_objs
157 transfer.get(remote="file", local="top/middle/leaf")
158 client.get.assert_called_with(
159 localpath="/local/top/middle/leaf",
160 remotepath="/remote/file",
161 )
162 Path.assert_called_with("top/middle")
163 Path.return_value.mkdir.assert_called_with(
164 parents=True, exist_ok=True
165 )
166
167 @patch("fabric.transfer.Path")
168 def with_trailing_slash_means_mkdir_entire_arg(
169 self, Path, sftp_objs
170 ):
171 transfer, client = sftp_objs
172 transfer.get(remote="file", local="top/middle/leaf/")
173 client.get.assert_called_with(
174 localpath="/local/top/middle/leaf/file",
175 remotepath="/remote/file",
176 )
177 Path.assert_called_with("top/middle/leaf/")
178 Path.return_value.mkdir.assert_called_with(
179 parents=True, exist_ok=True
180 )
134181
135182 class put:
136183 class basics: