Import upstream version 2.6.0+git20210708.1.05433a2
Debian Janitor
2 years ago
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 | 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. | |
1 | 1 | All rights reserved. |
2 | 2 | |
3 | 3 | 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 |
13 | 13 | # Linting! |
14 | 14 | flake8==3.6.0 |
15 | 15 | # Coverage! |
16 | coverage==3.7.1 | |
17 | codecov==1.6.3 | |
16 | coverage==5.3.1 | |
17 | codecov==2.1.11 | |
18 | 18 | # Documentation tools |
19 | 19 | sphinx>=1.4,<1.7 |
20 | 20 | alabaster==0.7.12 |
0 | 0 | # flake8: noqa |
1 | 1 | from ._version import __version_info__, __version__ |
2 | 2 | from .connection import Config, Connection |
3 | from .runners import Remote, Result | |
3 | from .runners import Remote, RemoteShell, Result | |
4 | 4 | from .group import Group, SerialGroup, ThreadingGroup, GroupResult |
5 | 5 | from .tasks import task, Task |
6 | 6 | from .executor import Executor |
0 | __version_info__ = (2, 5, 0) | |
0 | __version_info__ = (2, 6, 0) | |
1 | 1 | __version__ = ".".join(map(str, __version_info__)) |
4 | 4 | from invoke.config import Config as InvokeConfig, merge_dicts |
5 | 5 | from paramiko.config import SSHConfig |
6 | 6 | |
7 | from .runners import Remote | |
7 | from .runners import Remote, RemoteShell | |
8 | 8 | from .util import get_local_user, debug |
9 | 9 | |
10 | 10 | |
307 | 307 | "inline_ssh_env": False, |
308 | 308 | "load_ssh_configs": True, |
309 | 309 | "port": 22, |
310 | "run": {"replace_env": True}, | |
311 | "runners": {"remote": Remote}, | |
310 | "runners": {"remote": Remote, "remote_shell": RemoteShell}, | |
312 | 311 | "ssh_config_path": None, |
313 | 312 | "tasks": {"collection_name": "fabfile"}, |
314 | 313 | # TODO: this becomes an override/extend once Invoke grows execution |
299 | 299 | |
300 | 300 | Default: ``config.timeouts.connect``. |
301 | 301 | |
302 | .. _connect_kwargs-arg: | |
303 | 302 | |
304 | 303 | :param dict connect_kwargs: |
304 | ||
305 | .. _connect_kwargs-arg: | |
306 | ||
305 | 307 | Keyword arguments handed verbatim to |
306 | 308 | `SSHClient.connect <paramiko.client.SSHClient.connect>` (when |
307 | 309 | `.open` is called). |
700 | 702 | return channel |
701 | 703 | |
702 | 704 | 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 | ) | |
704 | 708 | |
705 | 709 | @opens |
706 | 710 | def run(self, command, **kwargs): |
733 | 737 | """ |
734 | 738 | return self._sudo(self._remote_runner(), command, **kwargs) |
735 | 739 | |
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 | ||
736 | 819 | def local(self, *args, **kwargs): |
737 | 820 | """ |
738 | 821 | Execute a shell command on the local system. |
775 | 858 | |
776 | 859 | def put(self, *args, **kwargs): |
777 | 860 | """ |
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. | |
779 | 862 | |
780 | 863 | Simply a wrapper for `.Transfer.put`. Please see its documentation for |
781 | 864 | all details. |
17 | 17 | concrete subclasses (such as `.SerialGroup` or `.ThreadingGroup`) or |
18 | 18 | you'll get ``NotImplementedError`` on most of the methods. |
19 | 19 | |
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: | |
23 | 23 | |
24 | 24 | - Return values are dict-like objects (`.GroupResult`) mapping |
25 | 25 | `.Connection` objects to the return value for the respective connections: |
98 | 98 | group.extend(connections) |
99 | 99 | return group |
100 | 100 | |
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 | ||
101 | 106 | def run(self, *args, **kwargs): |
102 | 107 | """ |
103 | 108 | Executes `.Connection.run` on all member `Connections <.Connection>`. |
106 | 111 | |
107 | 112 | .. versionadded:: 2.0 |
108 | 113 | """ |
109 | # TODO: probably best to suck it up & match actual run() sig? | |
110 | 114 | # TODO: how to change method of execution across contents? subclass, |
111 | 115 | # kwargs, additional methods, inject an executor? Doing subclass for |
112 | 116 | # now, but not 100% sure it's the best route. |
113 | 117 | # 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) | |
128 | 130 | |
129 | 131 | # TODO: this all needs to mesh well with similar strategies applied to |
130 | 132 | # entire tasks - so that may still end up factored out into Executors or |
132 | 134 | |
133 | 135 | # TODO: local? Invoke wants ability to do that on its own though, which |
134 | 136 | # 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) | |
136 | 156 | |
137 | 157 | def get(self, *args, **kwargs): |
138 | 158 | """ |
139 | 159 | Executes `.Connection.get` on all member `Connections <.Connection>`. |
140 | 160 | |
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) | |
148 | 190 | |
149 | 191 | def close(self): |
150 | 192 | """ |
169 | 211 | .. versionadded:: 2.0 |
170 | 212 | """ |
171 | 213 | |
172 | def run(self, *args, **kwargs): | |
214 | def _do(self, method, *args, **kwargs): | |
173 | 215 | results = GroupResult() |
174 | 216 | excepted = False |
175 | 217 | for cxn in self: |
176 | 218 | try: |
177 | results[cxn] = cxn.run(*args, **kwargs) | |
219 | results[cxn] = getattr(cxn, method)(*args, **kwargs) | |
178 | 220 | except Exception as e: |
179 | 221 | results[cxn] = e |
180 | 222 | excepted = True |
183 | 225 | return results |
184 | 226 | |
185 | 227 | |
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) | |
188 | 230 | # TODO: namedtuple or attrs object? |
189 | 231 | queue.put((cxn, result)) |
190 | 232 | |
196 | 238 | .. versionadded:: 2.0 |
197 | 239 | """ |
198 | 240 | |
199 | def run(self, *args, **kwargs): | |
241 | def _do(self, method, *args, **kwargs): | |
200 | 242 | results = GroupResult() |
201 | 243 | queue = Queue() |
202 | 244 | threads = [] |
203 | 245 | for cxn in self: |
204 | my_kwargs = dict(cxn=cxn, queue=queue, args=args, kwargs=kwargs) | |
205 | 246 | 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 | ), | |
207 | 255 | ) |
208 | 256 | threads.append(thread) |
209 | 257 | for thread in threads: |
210 | 258 | thread.start() |
211 | 259 | for thread in threads: |
212 | 260 | # TODO: configurable join timeout |
213 | # TODO: (in sudo's version) configurability around interactive | |
214 | # prompting resulting in an exception instead, as in v1 | |
215 | 261 | thread.join() |
216 | 262 | # Get non-exception results from queue |
217 | 263 | while not queue.empty(): |
0 | import signal | |
1 | ||
0 | 2 | from invoke import Runner, pty_size, Result as InvokeResult |
1 | 3 | |
2 | 4 | |
34 | 36 | def start(self, command, shell, env, timeout=None): |
35 | 37 | self.channel = self.context.create_session() |
36 | 38 | 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) | |
39 | 46 | if env: |
40 | 47 | # TODO: honor SendEnv from ssh_config (but if we do, _should_ we |
41 | 48 | # honor it even when prefixing? That would depart from OpenSSH |
54 | 61 | command = "export {} && {}".format(parameters, command) |
55 | 62 | else: |
56 | 63 | self.channel.update_environment(env) |
64 | self.send_start_message(command) | |
65 | ||
66 | def send_start_message(self, command): | |
57 | 67 | 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) | |
58 | 72 | |
59 | 73 | def read_proc_stdout(self, num_bytes): |
60 | 74 | return self.channel.recv(num_bytes) |
107 | 121 | # belong in invoke.Runner anyways? |
108 | 122 | self.channel.close() |
109 | 123 | |
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 | ||
110 | 132 | # TODO: shit that is in fab 1 run() but could apply to invoke.Local too: |
111 | 133 | # * see rest of stuff in _run_command/_execute in operations.py...there is |
112 | 134 | # a bunch that applies generally like optional exit codes, etc |
127 | 149 | # * agent-forward close() |
128 | 150 | |
129 | 151 | |
152 | class RemoteShell(Remote): | |
153 | def send_start_message(self, command): | |
154 | self.channel.invoke_shell() | |
155 | ||
156 | ||
130 | 157 | class Result(InvokeResult): |
131 | 158 | """ |
132 | 159 | An `invoke.runners.Result` exposing which `.Connection` was run against. |
65 | 65 | # TODO: just leverage attrs, maybe vendored into Invoke so we don't |
66 | 66 | # grow more dependencies? Ehhh |
67 | 67 | 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() | |
68 | 87 | |
69 | 88 | |
70 | 89 | class MockChannel(Mock): |
253 | 272 | for channel, command in zip(self.channels, self.commands): |
254 | 273 | # Expect an open_session for each command exec |
255 | 274 | 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) | |
258 | 277 | # Expect written stdin, if given |
259 | 278 | if command.in_: |
260 | 279 | assert channel._stdin.getvalue() == command.in_ |
375 | 394 | # Set up mocks |
376 | 395 | self.os_patcher = patch("fabric.transfer.os") |
377 | 396 | self.client_patcher = patch("fabric.connection.SSHClient") |
397 | self.path_patcher = patch("fabric.transfer.Path") | |
378 | 398 | mock_os = self.os_patcher.start() |
379 | 399 | Client = self.client_patcher.start() |
400 | self.path_patcher.start() | |
380 | 401 | sftp = Client.return_value.open_sftp.return_value |
381 | 402 | |
382 | 403 | # Handle common filepath massage actions; tests will assume these. |
383 | 404 | 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)) | |
385 | 408 | |
386 | 409 | mock_os.path.abspath.side_effect = fake_abspath |
387 | 410 | sftp.getcwd.return_value = "/remote" |
391 | 414 | sftp.stat.return_value.st_mode = fake_mode |
392 | 415 | mock_os.stat.return_value.st_mode = fake_mode |
393 | 416 | # 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) | |
396 | 421 | # Return the sftp and OS mocks for use by decorator use case. |
397 | 422 | return sftp, mock_os |
398 | 423 | |
399 | 424 | def stop(self): |
400 | 425 | self.os_patcher.stop() |
401 | 426 | self.client_patcher.stop() |
427 | self.path_patcher.stop() |
103 | 103 | """ |
104 | 104 | mock = MockSFTP(autostart=False) |
105 | 105 | client, mock_os = mock.start() |
106 | # Regular ol transfer to save some time | |
106 | 107 | transfer = Transfer(Connection("host")) |
107 | 108 | yield transfer, client, mock_os |
108 | 109 | # TODO: old mock_sftp() lacked any 'stop'...why? feels bad man |
4 | 4 | import os |
5 | 5 | import posixpath |
6 | 6 | import stat |
7 | ||
8 | try: | |
9 | from pathlib import Path | |
10 | except ImportError: | |
11 | from pathlib2 import Path | |
7 | 12 | |
8 | 13 | from .util import debug # TODO: actual logging! LOL |
9 | 14 | |
39 | 44 | |
40 | 45 | def get(self, remote, local=None, preserve_mode=True): |
41 | 46 | """ |
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. | |
43 | 48 | |
44 | 49 | :param str remote: |
45 | 50 | Remote file to download. |
59 | 64 | |
60 | 65 | **If None or another 'falsey'/empty value is given** (the default), |
61 | 66 | 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.) | |
63 | 70 | |
64 | 71 | **If a string is given**, it should be a path to a local directory |
65 | 72 | or file and is subject to similar behavior as that seen by common |
70 | 77 | '/tmp/')`` would result in creation or overwriting of |
71 | 78 | ``/tmp/file.txt``). |
72 | 79 | |
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`. | |
78 | 97 | |
79 | 98 | **If a file-like object is given**, the contents of the remote file |
80 | 99 | are simply written into it. |
86 | 105 | :returns: A `.Result` object. |
87 | 106 | |
88 | 107 | .. 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. | |
89 | 113 | """ |
90 | 114 | # TODO: how does this API change if we want to implement |
91 | 115 | # 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. | |
95 | 116 | # TODO: callback support |
96 | 117 | # TODO: how best to allow changing the behavior/semantics of |
97 | 118 | # remote/local (e.g. users might want 'safer' behavior that complains |
106 | 127 | self.sftp.getcwd() or self.sftp.normalize("."), remote |
107 | 128 | ) |
108 | 129 | |
109 | # Massage local path: | |
110 | # - handle file-ness | |
111 | # - if path, fill with remote name if empty, & make absolute | |
130 | # Massage local path | |
112 | 131 | orig_local = local |
113 | 132 | is_file_like = hasattr(local, "write") and callable(local.write) |
133 | remote_filename = posixpath.basename(remote) | |
114 | 134 | if not local: |
115 | local = posixpath.basename(remote) | |
135 | local = remote_filename | |
136 | # Path-driven local downloads need interpolation, abspath'ing & | |
137 | # directory creation | |
116 | 138 | 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) | |
117 | 154 | 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 | |
118 | 161 | |
119 | 162 | # Run Paramiko-level .get() (side-effects only. womp.) |
120 | 163 | # 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 | 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 | fabric |
0 | 0 | import os |
1 | 1 | import time |
2 | 2 | |
3 | try: | |
4 | from invoke.vendor.six import StringIO | |
5 | except ImportError: | |
6 | from six import StringIO | |
7 | ||
3 | 8 | from invoke import pty_size, CommandTimedOut |
4 | 9 | from pytest import skip, raises |
10 | from pytest_relaxed import trap | |
5 | 11 | |
6 | 12 | from fabric import Connection, Config |
7 | 13 | |
53 | 59 | assert tuple(map(int, found)), rows == cols |
54 | 60 | # PTYs use \r\n, not \n, line separation |
55 | 61 | 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 | |
56 | 76 | assert result.pty is True |
57 | 77 | |
58 | 78 | class local: |
11 | 11 | [tool:pytest] |
12 | 12 | testpaths = tests |
13 | 13 | python_files = * |
14 | ||
15 | [egg_info] | |
16 | tag_build = | |
17 | tag_date = 0 | |
18 |
66 | 66 | author="Jeff Forcier", |
67 | 67 | author_email="jeff@bitprophet.org", |
68 | 68 | 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"], | |
70 | 70 | extras_require={ |
71 | 71 | "testing": testing_deps, |
72 | 72 | "pytest": testing_deps + pytest_deps, |
45 | 45 | Overrides of Invoke-level defaults |
46 | 46 | ---------------------------------- |
47 | 47 | |
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. | |
51 | 51 | |
52 | 52 | This is for security purposes: leaking local environment data remotely by |
53 | 53 | default would be unsanitary. It's also compatible with the behavior of |
55 | 55 | |
56 | 56 | .. seealso:: |
57 | 57 | 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. | |
58 | 63 | |
59 | 64 | Extensions to Invoke-level defaults |
60 | 65 | ----------------------------------- |
13 | 13 | on top; user code will most often import from the ``fabric`` package, but |
14 | 14 | you'll sometimes import directly from ``invoke`` or ``paramiko`` too: |
15 | 15 | |
16 | - `Invoke <https://pyinvoke.org>`_ implements CLI parsing, task organization, | |
16 | - `Invoke <https://www.pyinvoke.org>`_ implements CLI parsing, task organization, | |
17 | 17 | and shell command execution (a generic framework plus specific implementation |
18 | 18 | for local commands.) |
19 | 19 | |
23 | 23 | - Fabric users will frequently import Invoke objects, in cases where Fabric |
24 | 24 | itself has no need to subclass or otherwise modify what Invoke provides. |
25 | 25 | |
26 | - `Paramiko <https://paramiko.org>`_ implements low/mid level SSH | |
26 | - `Paramiko <https://www.paramiko.org>`_ implements low/mid level SSH | |
27 | 27 | functionality - SSH and SFTP sessions, key management, etc. |
28 | 28 | |
29 | 29 | - Fabric mostly uses this under the hood; users will only rarely import |
290 | 290 | |
291 | 291 | >>> from fabric import Connection |
292 | 292 | >>> for host in ('web1', 'web2', 'mac1'): |
293 | >>> result = Connection(host).run('uname -s') | |
293 | ... result = Connection(host).run('uname -s') | |
294 | 294 | ... print("{}: {}".format(host, result.stdout.strip())) |
295 | 295 | ... |
296 | 296 | ... |
4 | 4 | .. note:: |
5 | 5 | Looking for the Fabric 1.x changelog? See :doc:`/changelog-v1`. |
6 | 6 | |
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. | |
7 | 56 | - :release:`2.5.0 <2019-08-06>` |
8 | 57 | - :support:`-` Update minimum Invoke version requirement to ``>=1.3``. |
9 | 58 | - :feature:`1985` Add support for explicitly closing remote subprocess' stdin |
37 | 37 | |
38 | 38 | .. _irc: |
39 | 39 | |
40 | IRC | |
41 | --- | |
40 | Blog posts | |
41 | ---------- | |
42 | 42 | |
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/ |
803 | 803 | * - ``open_shell`` for obtaining interactive-friendly remote shell sessions |
804 | 804 | (something that ``run`` historically was bad at ) |
805 | 805 | - 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`. | |
810 | 810 | |
811 | 811 | ``run`` |
812 | 812 | ~~~~~~~ |
841 | 841 | |
842 | 842 | * - ``shell`` / ``env.use_shell`` designating whether or not to wrap |
843 | 843 | 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 | |
845 | 845 | - See the note above under ``run`` for details on shell wrapping |
846 | 846 | as a general strategy; unfortunately for ``sudo``, some sort of manual |
847 | 847 | wrapping is still necessary for nontrivial commands (i.e. anything |
876 | 876 | `os.execve` (or similar) is used instead of `subprocess.Popen`. |
877 | 877 | Behavior is much the same: no shell wrapping (as in legacy ``run``), |
878 | 878 | 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). | |
879 | 901 | |
880 | 902 | .. _upgrading-utility: |
881 | 903 | |
1126 | 1148 | own, so it's gone. |
1127 | 1149 | * - Naming downloaded files after some aspect of the remote destination, to |
1128 | 1150 | 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. | |
1132 | 1153 | |
1133 | 1154 | |
1134 | 1155 | .. _upgrading-configuration: |
5 | 5 | from invoke.vendor.lexicon import Lexicon |
6 | 6 | from pytest_relaxed import trap |
7 | 7 | |
8 | from fabric import Connection as Connection_, Config as Config_ | |
9 | 8 | from fabric.main import make_program |
10 | from paramiko import SSHConfig | |
11 | 9 | |
12 | 10 | |
13 | 11 | support = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_support") |
50 | 48 | assert False, err.format(test) |
51 | 49 | |
52 | 50 | |
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 | ||
74 | 51 | def faux_v1_env(): |
75 | 52 | # Close enough to v1 _AttributeDict... |
76 | 53 | # Contains a copy of enough of v1's defaults to prevent us having to do a |
1 | 1 | from os.path import join, expanduser |
2 | 2 | |
3 | 3 | from paramiko.config import SSHConfig |
4 | from invoke import Local | |
4 | 5 | from invoke.vendor.lexicon import Lexicon |
5 | 6 | |
6 | from fabric import Config | |
7 | from fabric import Config, Remote, RemoteShell | |
7 | 8 | from fabric.util import get_local_user |
8 | 9 | |
9 | 10 | from mock import patch, call |
49 | 50 | |
50 | 51 | def overrides_some_Invoke_defaults(self): |
51 | 52 | config = Config() |
52 | # This value defaults to False in Invoke proper. | |
53 | assert config.run.replace_env is True | |
54 | 53 | 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 | ) | |
55 | 60 | |
56 | 61 | def uses_Fabric_prefix(self): |
57 | 62 | # NOTE: see also the integration-esque tests in tests/main.py; this |
0 | 0 | # flake8: noqa |
1 | 1 | 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 |
0 | 0 | from itertools import chain, repeat |
1 | 1 | |
2 | 2 | try: |
3 | from invoke.vendor.six import b | |
3 | from invoke.vendor.six import b, StringIO | |
4 | 4 | except ImportError: |
5 | from six import b | |
5 | from six import b, StringIO | |
6 | ||
6 | 7 | import errno |
7 | 8 | from os.path import join |
8 | 9 | import socket |
11 | 12 | from mock import patch, Mock, call, ANY |
12 | 13 | from paramiko.client import SSHClient, AutoAddPolicy |
13 | 14 | from paramiko import SSHConfig |
14 | import pytest # for mark | |
15 | import pytest # for mark, internal raises | |
15 | 16 | from pytest import skip, param |
16 | 17 | from pytest_relaxed import raises |
17 | 18 | from invoke.vendor.lexicon import Lexicon |
19 | 20 | from invoke.config import Config as InvokeConfig |
20 | 21 | from invoke.exceptions import ThreadException |
21 | 22 | |
22 | from fabric import Config as Config_ | |
23 | from fabric import Config, Connection | |
23 | 24 | from fabric.exceptions import InvalidV1Env |
24 | 25 | from fabric.util import get_local_user |
25 | 26 | |
26 | from _util import support, Connection, Config, faux_v1_env | |
27 | from _util import support, faux_v1_env | |
27 | 28 | |
28 | 29 | |
29 | 30 | # Remote is woven in as a config default, so must be patched there |
30 | 31 | remote_path = "fabric.config.Remote" |
32 | remote_shell_path = "fabric.config.RemoteShell" | |
31 | 33 | |
32 | 34 | |
33 | 35 | def _select_result(obj): |
264 | 266 | runtime_path = join(support, "ssh_config", confname) |
265 | 267 | if overrides is None: |
266 | 268 | overrides = {} |
267 | return Config_( | |
269 | return Config( | |
268 | 270 | runtime_ssh_path=runtime_path, overrides=overrides |
269 | 271 | ) |
270 | 272 | |
273 | 275 | return Connection("runtime", config=config) |
274 | 276 | |
275 | 277 | def effectively_blank_when_no_loaded_config(self): |
276 | c = Config_(ssh_config=SSHConfig()) | |
278 | c = Config(ssh_config=SSHConfig()) | |
277 | 279 | cxn = Connection("host", config=c) |
278 | 280 | # NOTE: paramiko always injects this even if you look up a host |
279 | 281 | # that has no rules, even wildcard ones. |
305 | 307 | path = join( |
306 | 308 | support, "ssh_config", "overridden_hostname.conf" |
307 | 309 | ) |
308 | config = Config_(runtime_ssh_path=path) | |
310 | config = Config(runtime_ssh_path=path) | |
309 | 311 | cxn = Connection("aliasname", config=config) |
310 | 312 | assert cxn.host == "realname" |
311 | 313 | assert cxn.original_host == "aliasname" |
858 | 860 | config_kwargs["overrides"] = { |
859 | 861 | "connect_kwargs": {"key_filename": ["configured.key"]} |
860 | 862 | } |
861 | conf = Config_(**config_kwargs) | |
863 | conf = Config(**config_kwargs) | |
862 | 864 | connect_kwargs = {} |
863 | 865 | if kwarg: |
864 | 866 | # Stitch in connect_kwargs value |
949 | 951 | self, Remote, client |
950 | 952 | ): |
951 | 953 | remote = Remote.return_value |
952 | sentinel = object() | |
953 | remote.run.return_value = sentinel | |
954 | 954 | c = Connection("host") |
955 | 955 | r1 = c.run("command") |
956 | 956 | r2 = c.run("command", warn=True, hide="stderr") |
958 | 958 | # .assert_called_with()) stopped working, apparently triggered by |
959 | 959 | # our code...somehow...after commit (roughly) 80906c7. |
960 | 960 | # 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) | |
962 | 962 | remote.run.assert_has_calls( |
963 | 963 | [call("command"), call("command", warn=True, hide="stderr")] |
964 | 964 | ) |
965 | 965 | 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] | |
967 | 1031 | |
968 | 1032 | class local: |
969 | 1033 | # NOTE: most tests for this functionality live in Invoke's runner |
1001 | 1065 | # Remote.return_value is two different Mocks now, despite Remote's |
1002 | 1066 | # own Mock having the same ID here and in code under test. WTF!!) |
1003 | 1067 | expected = [ |
1004 | call(cxn, inline_env=False), | |
1068 | call(context=cxn, inline_env=False), | |
1005 | 1069 | call().run(cmd, watchers=ANY), |
1006 | 1070 | ] |
1007 | 1071 | assert Remote.mock_calls == expected |
0 | 0 | from mock import Mock, patch, call |
1 | from pytest_relaxed import raises | |
1 | from pytest import mark, raises | |
2 | 2 | |
3 | 3 | from fabric import Connection, Group, SerialGroup, ThreadingGroup, GroupResult |
4 | 4 | from fabric.group import thread_worker |
5 | 5 | 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 | ) | |
6 | 24 | |
7 | 25 | |
8 | 26 | class Group_: |
40 | 58 | for c in g: |
41 | 59 | assert isinstance(c, Connection) |
42 | 60 | |
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)() | |
47 | 66 | |
48 | 67 | class close_and_contextmanager_behavior: |
49 | 68 | def close_closes_all_member_connections(self): |
61 | 80 | for c in cxns: |
62 | 81 | c.close.assert_called_once_with() |
63 | 82 | |
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): | |
66 | 109 | args = args[:] |
67 | 110 | kwargs = kwargs.copy() |
68 | 111 | |
71 | 114 | predecessors = cxns[:car] |
72 | 115 | successors = cxns[cdr:] |
73 | 116 | for predecessor in predecessors: |
74 | predecessor.run.assert_called_with(*args, **kwargs) | |
117 | getattr(predecessor, method).assert_called_with(*args, **kwargs) | |
75 | 118 | for successor in successors: |
76 | assert not successor.run.called | |
119 | assert not getattr(successor, method).called | |
77 | 120 | |
78 | 121 | return tester |
79 | 122 | |
80 | 123 | |
81 | 124 | 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 == {} | |
132 | 179 | |
133 | 180 | |
134 | 181 | class ThreadingGroup_: |
135 | 182 | def setup(self): |
136 | 183 | 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), | |
145 | 223 | ): |
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 == {} |
13 | 13 | |
14 | 14 | def Remote(self): |
15 | 15 | assert fabric.Remote is runners.Remote |
16 | ||
17 | def RemoteShell(self): | |
18 | assert fabric.RemoteShell is runners.RemoteShell | |
16 | 19 | |
17 | 20 | def Result(self): |
18 | 21 | assert fabric.Result is runners.Result |
2 | 2 | except ImportError: |
3 | 3 | from six import StringIO |
4 | 4 | |
5 | from mock import Mock | |
5 | from mock import Mock, patch | |
6 | 6 | from pytest import skip # noqa |
7 | 7 | |
8 | 8 | from invoke import pty_size, Result |
9 | 9 | |
10 | from fabric import Config, Connection, Remote | |
10 | from fabric import Config, Connection, Remote, RemoteShell | |
11 | 11 | |
12 | 12 | |
13 | 13 | # On most systems this will explode if actually executed as a shell command; |
29 | 29 | c = _Connection("host") |
30 | 30 | assert Remote(context=c).context is c |
31 | 31 | |
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 | ||
32 | 45 | class run: |
33 | 46 | def calls_expected_paramiko_bits(self, remote): |
34 | 47 | # remote mocking makes generic sanity checks like "were |
42 | 55 | fakeout = StringIO() |
43 | 56 | _runner().run(CMD, out_stream=fakeout) |
44 | 57 | 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) | |
51 | 58 | |
52 | 59 | def return_value_is_Result_subclass_exposing_cxn_used(self, remote): |
53 | 60 | c = _Connection("host") |
106 | 113 | else: |
107 | 114 | assert False, "Weird, Oops never got raised..." |
108 | 115 | |
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 | ||
109 | 146 | # TODO: how much of Invoke's tests re: the upper level run() (re: |
110 | 147 | # things like returning Result, behavior of Result, etc) to |
111 | 148 | # duplicate here? Ideally none or very few core ones. |
132 | 169 | r.run(CMD, env={"PATH": "/opt/bin", "DEBUG": "1"}) |
133 | 170 | assert not chan.update_environment.called |
134 | 171 | |
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 | ||
135 | 178 | def kill_closes_the_channel(self): |
136 | 179 | runner = _runner() |
137 | 180 | runner.channel = Mock() |
138 | 181 | runner.kill() |
139 | 182 | 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() |
2 | 2 | except ImportError: |
3 | 3 | from six import StringIO |
4 | 4 | |
5 | from mock import Mock, call | |
5 | from mock import Mock, call, patch | |
6 | 6 | from pytest_relaxed import raises |
7 | 7 | from pytest import skip # noqa |
8 | 8 | from paramiko import SFTPAttributes |
91 | 91 | def remote_arg_cannot_be_empty_string(self, transfer): |
92 | 92 | transfer.get("") |
93 | 93 | |
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 | ||
94 | 112 | class file_like_local_paths: |
95 | 113 | "file-like local paths" |
96 | 114 | |
131 | 149 | client.stat.return_value = self.attrs |
132 | 150 | transfer.get("file", local="meh", preserve_mode=False) |
133 | 151 | 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 | ) | |
134 | 181 | |
135 | 182 | class put: |
136 | 183 | class basics: |