fabric / fabric
1 2
from contextlib import contextmanager
2 2
from threading import Event
3

4 2
try:
5 2
    from invoke.vendor.six import StringIO
6 2
    from invoke.vendor.decorator import decorator
7 2
    from invoke.vendor.six import string_types
8 0
except ImportError:
9 0
    from six import StringIO
10 0
    from decorator import decorator
11 0
    from six import string_types
12 2
import socket
13

14

15 2
from invoke import Context
16 2
from invoke.exceptions import ThreadException
17 2
from paramiko.agent import AgentRequestHandler
18 2
from paramiko.client import SSHClient, AutoAddPolicy
19 2
from paramiko.config import SSHConfig
20 2
from paramiko.proxy import ProxyCommand
21

22 2
from .config import Config
23 2
from .transfer import Transfer
24 2
from .tunnels import TunnelManager, Tunnel
25

26

27 2
@decorator
28
def opens(method, self, *args, **kwargs):
29 2
    self.open()
30 2
    return method(self, *args, **kwargs)
31

32

33 2
class Connection(Context):
34
    """
35
    A connection to an SSH daemon, with methods for commands and file transfer.
36

37
    **Basics**
38

39
    This class inherits from Invoke's `~invoke.context.Context`, as it is a
40
    context within which commands, tasks etc can operate. It also encapsulates
41
    a Paramiko `~paramiko.client.SSHClient` instance, performing useful high
42
    level operations with that `~paramiko.client.SSHClient` and
43
    `~paramiko.channel.Channel` instances generated from it.
44

45
    .. _connect_kwargs:
46

47
    .. note::
48
        Many SSH specific options -- such as specifying private keys and
49
        passphrases, timeouts, disabling SSH agents, etc -- are handled
50
        directly by Paramiko and should be specified via the
51
        :ref:`connect_kwargs argument <connect_kwargs-arg>` of the constructor.
52

53
    **Lifecycle**
54

55
    `.Connection` has a basic "`create <__init__>`, `connect/open <open>`, `do
56
    work <run>`, `disconnect/close <close>`" lifecycle:
57

58
    * `Instantiation <__init__>` imprints the object with its connection
59
      parameters (but does **not** actually initiate the network connection).
60
    * Methods like `run`, `get` etc automatically trigger a call to
61
      `open` if the connection is not active; users may of course call `open`
62
      manually if desired.
63
    * Connections do not always need to be explicitly closed; much of the
64
      time, Paramiko's garbage collection hooks or Python's own shutdown
65
      sequence will take care of things. **However**, should you encounter edge
66
      cases (for example, sessions hanging on exit) it's helpful to explicitly
67
      close connections when you're done with them.
68

69
      This can be accomplished by manually calling `close`, or by using the
70
      object as a contextmanager::
71

72
        with Connection('host') as c:
73
            c.run('command')
74
            c.put('file')
75

76
    .. note::
77
        This class rebinds `invoke.context.Context.run` to `.local` so both
78
        remote and local command execution can coexist.
79

80
    **Configuration**
81

82
    Most `.Connection` parameters honor :doc:`Invoke-style configuration
83
    </concepts/configuration>` as well as any applicable :ref:`SSH config file
84
    directives <connection-ssh-config>`. For example, to end up with a
85
    connection to ``admin@myhost``, one could:
86

87
    - Use any built-in config mechanism, such as ``/etc/fabric.yml``,
88
      ``~/.fabric.json``, collection-driven configuration, env vars, etc,
89
      stating ``user: admin`` (or ``{"user": "admin"}``, depending on config
90
      format.) Then ``Connection('myhost')`` would implicitly have a ``user``
91
      of ``admin``.
92
    - Use an SSH config file containing ``User admin`` within any applicable
93
      ``Host`` header (``Host myhost``, ``Host *``, etc.) Again,
94
      ``Connection('myhost')`` will default to an ``admin`` user.
95
    - Leverage host-parameter shorthand (described in `.Config.__init__`), i.e.
96
      ``Connection('admin@myhost')``.
97
    - Give the parameter directly: ``Connection('myhost', user='admin')``.
98

99
    The same applies to agent forwarding, gateways, and so forth.
100

101
    .. versionadded:: 2.0
102
    """
103

104
    # NOTE: these are initialized here to hint to invoke.Config.__setattr__
105
    # that they should be treated as real attributes instead of config proxies.
106
    # (Additionally, we're doing this instead of using invoke.Config._set() so
107
    # we can take advantage of Sphinx's attribute-doc-comment static analysis.)
108
    # Once an instance is created, these values will usually be non-None
109
    # because they default to the default config values.
110 2
    host = None
111 2
    original_host = None
112 2
    user = None
113 2
    port = None
114 2
    ssh_config = None
115 2
    gateway = None
116 2
    forward_agent = None
117 2
    connect_timeout = None
118 2
    connect_kwargs = None
119 2
    client = None
120 2
    transport = None
121 2
    _sftp = None
122 2
    _agent_handler = None
123

124
    # TODO: should "reopening" an existing Connection object that has been
125
    # closed, be allowed? (See e.g. how v1 detects closed/semi-closed
126
    # connections & nukes them before creating a new client to the same host.)
127
    # TODO: push some of this into paramiko.client.Client? e.g. expand what
128
    # Client.exec_command does, it already allows configuring a subset of what
129
    # we do / will eventually do / did in 1.x. It's silly to have to do
130
    # .get_transport().open_session().
131 2
    def __init__(
132
        self,
133
        host,
134
        user=None,
135
        port=None,
136
        config=None,
137
        gateway=None,
138
        forward_agent=None,
139
        connect_timeout=None,
140
        connect_kwargs=None,
141
        inline_ssh_env=None,
142
    ):
143
        """
144
        Set up a new object representing a server connection.
145

146
        :param str host:
147
            the hostname (or IP address) of this connection.
148

149
            May include shorthand for the ``user`` and/or ``port`` parameters,
150
            of the form ``user@host``, ``host:port``, or ``user@host:port``.
151

152
            .. note::
153
                Due to ambiguity, IPv6 host addresses are incompatible with the
154
                ``host:port`` shorthand (though ``user@host`` will still work
155
                OK). In other words, the presence of >1 ``:`` character will
156
                prevent any attempt to derive a shorthand port number; use the
157
                explicit ``port`` parameter instead.
158

159
            .. note::
160
                If ``host`` matches a ``Host`` clause in loaded SSH config
161
                data, and that ``Host`` clause contains a ``Hostname``
162
                directive, the resulting `.Connection` object will behave as if
163
                ``host`` is equal to that ``Hostname`` value.
164

165
                In all cases, the original value of ``host`` is preserved as
166
                the ``original_host`` attribute.
167

168
                Thus, given SSH config like so::
169

170
                    Host myalias
171
                        Hostname realhostname
172

173
                a call like ``Connection(host='myalias')`` will result in an
174
                object whose ``host`` attribute is ``realhostname``, and whose
175
                ``original_host`` attribute is ``myalias``.
176

177
        :param str user:
178
            the login user for the remote connection. Defaults to
179
            ``config.user``.
180

181
        :param int port:
182
            the remote port. Defaults to ``config.port``.
183

184
        :param config:
185
            configuration settings to use when executing methods on this
186
            `.Connection` (e.g. default SSH port and so forth).
187

188
            Should be a `.Config` or an `invoke.config.Config`
189
            (which will be turned into a `.Config`).
190

191
            Default is an anonymous `.Config` object.
192

193
        :param gateway:
194
            An object to use as a proxy or gateway for this connection.
195

196
            This parameter accepts one of the following:
197

198
            - another `.Connection` (for a ``ProxyJump`` style gateway);
199
            - a shell command string (for a ``ProxyCommand`` style style
200
              gateway).
201

202
            Default: ``None``, meaning no gatewaying will occur (unless
203
            otherwise configured; if one wants to override a configured gateway
204
            at runtime, specify ``gateway=False``.)
205

206
            .. seealso:: :ref:`ssh-gateways`
207

208
        :param bool forward_agent:
209
            Whether to enable SSH agent forwarding.
210

211
            Default: ``config.forward_agent``.
212

213
        :param int connect_timeout:
214
            Connection timeout, in seconds.
215

216
            Default: ``config.timeouts.connect``.
217

218
        .. _connect_kwargs-arg:
219

220
        :param dict connect_kwargs:
221
            Keyword arguments handed verbatim to
222
            `SSHClient.connect <paramiko.client.SSHClient.connect>` (when
223
            `.open` is called).
224

225
            `.Connection` tries not to grow additional settings/kwargs of its
226
            own unless it is adding value of some kind; thus,
227
            ``connect_kwargs`` is currently the right place to hand in paramiko
228
            connection parameters such as ``pkey`` or ``key_filename``. For
229
            example::
230

231
                c = Connection(
232
                    host="hostname",
233
                    user="admin",
234
                    connect_kwargs={
235
                        "key_filename": "/home/myuser/.ssh/private.key",
236
                    },
237
                )
238

239
            Default: ``config.connect_kwargs``.
240

241
        :param bool inline_ssh_env:
242
            Whether to send environment variables "inline" as prefixes in front
243
            of command strings (``export VARNAME=value && mycommand here``),
244
            instead of trying to submit them through the SSH protocol itself
245
            (which is the default behavior). This is necessary if the remote
246
            server has a restricted ``AcceptEnv`` setting (which is the common
247
            default).
248

249
            The default value is the value of the ``inline_ssh_env``
250
            :ref:`configuration value <default-values>` (which itself defaults
251
            to ``False``).
252

253
            .. warning::
254
                This functionality does **not** currently perform any shell
255
                escaping on your behalf! Be careful when using nontrivial
256
                values, and note that you can put in your own quoting,
257
                backslashing etc if desired.
258

259
                Consider using a different approach (such as actual
260
                remote shell scripts) if you run into too many issues here.
261

262
            .. note::
263
                When serializing into prefixed ``FOO=bar`` format, we apply the
264
                builtin `sorted` function to the env dictionary's keys, to
265
                remove what would otherwise be ambiguous/arbitrary ordering.
266

267
            .. note::
268
                This setting has no bearing on *local* shell commands; it only
269
                affects remote commands, and thus, methods like `.run` and
270
                `.sudo`.
271

272
        :raises ValueError:
273
            if user or port values are given via both ``host`` shorthand *and*
274
            their own arguments. (We `refuse the temptation to guess`_).
275

276
        .. _refuse the temptation to guess:
277
            http://zen-of-python.info/
278
            in-the-face-of-ambiguity-refuse-the-temptation-to-guess.html#12
279

280
        .. versionchanged:: 2.3
281
            Added the ``inline_ssh_env`` parameter.
282
        """
283
        # NOTE: parent __init__ sets self._config; for now we simply overwrite
284
        # that below. If it's somehow problematic we would want to break parent
285
        # __init__ up in a manner that is more cleanly overrideable.
286 2
        super(Connection, self).__init__(config=config)
287

288
        #: The .Config object referenced when handling default values (for e.g.
289
        #: user or port, when not explicitly given) or deciding how to behave.
290 2
        if config is None:
291 2
            config = Config()
292
        # Handle 'vanilla' Invoke config objects, which need cloning 'into' one
293
        # of our own Configs (which grants the new defaults, etc, while not
294
        # squashing them if the Invoke-level config already accounted for them)
295 2
        elif not isinstance(config, Config):
296 2
            config = config.clone(into=Config)
297 2
        self._set(_config=config)
298
        # TODO: when/how to run load_files, merge, load_shell_env, etc?
299
        # TODO: i.e. what is the lib use case here (and honestly in invoke too)
300

301 2
        shorthand = self.derive_shorthand(host)
302 2
        host = shorthand["host"]
303 2
        err = "You supplied the {} via both shorthand and kwarg! Please pick one."  # noqa
304 2
        if shorthand["user"] is not None:
305 2
            if user is not None:
306 2
                raise ValueError(err.format("user"))
307 2
            user = shorthand["user"]
308 2
        if shorthand["port"] is not None:
309 2
            if port is not None:
310 2
                raise ValueError(err.format("port"))
311 2
            port = shorthand["port"]
312

313
        # NOTE: we load SSH config data as early as possible as it has
314
        # potential to affect nearly every other attribute.
315
        #: The per-host SSH config data, if any. (See :ref:`ssh-config`.)
316 2
        self.ssh_config = self.config.base_ssh_config.lookup(host)
317

318 2
        self.original_host = host
319
        #: The hostname of the target server.
320 2
        self.host = host
321 2
        if "hostname" in self.ssh_config:
322
            # TODO: log that this occurred?
323 2
            self.host = self.ssh_config["hostname"]
324

325
        #: The username this connection will use to connect to the remote end.
326 2
        self.user = user or self.ssh_config.get("user", self.config.user)
327
        # TODO: is it _ever_ possible to give an empty user value (e.g.
328
        # user='')? E.g. do some SSH server specs allow for that?
329

330
        #: The network port to connect on.
331 2
        self.port = port or int(self.ssh_config.get("port", self.config.port))
332

333
        # Gateway/proxy/bastion/jump setting: non-None values - string,
334
        # Connection, even eg False - get set directly; None triggers seek in
335
        # config/ssh_config
336
        #: The gateway `.Connection` or ``ProxyCommand`` string to be used,
337
        #: if any.
338 2
        self.gateway = gateway if gateway is not None else self.get_gateway()
339
        # NOTE: we use string above, vs ProxyCommand obj, to avoid spinning up
340
        # the ProxyCommand subprocess at init time, vs open() time.
341
        # TODO: make paramiko.proxy.ProxyCommand lazy instead?
342

343 2
        if forward_agent is None:
344
            # Default to config...
345 2
            forward_agent = self.config.forward_agent
346
            # But if ssh_config is present, it wins
347 2
            if "forwardagent" in self.ssh_config:
348
                # TODO: SSHConfig really, seriously needs some love here, god
349 2
                map_ = {"yes": True, "no": False}
350 2
                forward_agent = map_[self.ssh_config["forwardagent"]]
351
        #: Whether agent forwarding is enabled.
352 2
        self.forward_agent = forward_agent
353

354 2
        if connect_timeout is None:
355 2
            connect_timeout = self.ssh_config.get(
356
                "connecttimeout", self.config.timeouts.connect
357
            )
358 2
        if connect_timeout is not None:
359 2
            connect_timeout = int(connect_timeout)
360
        #: Connection timeout
361 2
        self.connect_timeout = connect_timeout
362

363
        #: Keyword arguments given to `paramiko.client.SSHClient.connect` when
364
        #: `open` is called.
365 2
        self.connect_kwargs = self.resolve_connect_kwargs(connect_kwargs)
366

367
        #: The `paramiko.client.SSHClient` instance this connection wraps.
368 2
        client = SSHClient()
369 2
        client.set_missing_host_key_policy(AutoAddPolicy())
370 2
        self.client = client
371

372
        #: A convenience handle onto the return value of
373
        #: ``self.client.get_transport()``.
374 2
        self.transport = None
375

376 2
        if inline_ssh_env is None:
377 2
            inline_ssh_env = self.config.inline_ssh_env
378
        #: Whether to construct remote command lines with env vars prefixed
379
        #: inline.
380 2
        self.inline_ssh_env = inline_ssh_env
381

382 2
    def resolve_connect_kwargs(self, connect_kwargs):
383
        # Grab connect_kwargs from config if not explicitly given.
384 2
        if connect_kwargs is None:
385
            # TODO: is it better to pre-empt conflicts w/ manually-handled
386
            # connect() kwargs (hostname, username, etc) here or in open()?
387
            # We're doing open() for now in case e.g. someone manually modifies
388
            # .connect_kwargs attributewise, but otherwise it feels better to
389
            # do it early instead of late.
390 2
            connect_kwargs = self.config.connect_kwargs
391
        # Special case: key_filename gets merged instead of overridden.
392
        # TODO: probably want some sorta smart merging generally, special cases
393
        # are bad.
394 2
        elif "key_filename" in self.config.connect_kwargs:
395 2
            kwarg_val = connect_kwargs.get("key_filename", [])
396 2
            conf_val = self.config.connect_kwargs["key_filename"]
397
            # Config value comes before kwarg value (because it may contain
398
            # CLI flag value.)
399 2
            connect_kwargs["key_filename"] = conf_val + kwarg_val
400

401
        # SSH config identityfile values come last in the key_filename
402
        # 'hierarchy'.
403 2
        if "identityfile" in self.ssh_config:
404 2
            connect_kwargs.setdefault("key_filename", [])
405 2
            connect_kwargs["key_filename"].extend(
406
                self.ssh_config["identityfile"]
407
            )
408

409 2
        return connect_kwargs
410

411 2
    def get_gateway(self):
412
        # SSH config wins over Invoke-style config
413 2
        if "proxyjump" in self.ssh_config:
414
            # Reverse hop1,hop2,hop3 style ProxyJump directive so we start
415
            # with the final (itself non-gatewayed) hop and work up to
416
            # the front (actual, supplied as our own gateway) hop
417 2
            hops = reversed(self.ssh_config["proxyjump"].split(","))
418 2
            prev_gw = None
419 2
            for hop in hops:
420
                # Short-circuit if we appear to be our own proxy, which would
421
                # be a RecursionError. Implies SSH config wildcards.
422
                # TODO: in an ideal world we'd check user/port too in case they
423
                # differ, but...seriously? They can file a PR with those extra
424
                # half dozen test cases in play, E_NOTIME
425 2
                if self.derive_shorthand(hop)["host"] == self.host:
426 2
                    return None
427
                # Happily, ProxyJump uses identical format to our host
428
                # shorthand...
429 2
                kwargs = dict(config=self.config.clone())
430 2
                if prev_gw is not None:
431 2
                    kwargs["gateway"] = prev_gw
432 2
                cxn = Connection(hop, **kwargs)
433 2
                prev_gw = cxn
434 2
            return prev_gw
435 2
        elif "proxycommand" in self.ssh_config:
436
            # Just a string, which we interpret as a proxy command..
437 2
            return self.ssh_config["proxycommand"]
438
        # Fallback: config value (may be None).
439 2
        return self.config.gateway
440

441 2
    def __repr__(self):
442
        # Host comes first as it's the most common differentiator by far
443 2
        bits = [("host", self.host)]
444
        # TODO: maybe always show user regardless? Explicit is good...
445 2
        if self.user != self.config.user:
446 2
            bits.append(("user", self.user))
447
        # TODO: harder to make case for 'always show port'; maybe if it's
448
        # non-22 (even if config has overridden the local default)?
449 2
        if self.port != self.config.port:
450 2
            bits.append(("port", self.port))
451
        # NOTE: sometimes self.gateway may be eg False if someone wants to
452
        # explicitly override a configured non-None value (as otherwise it's
453
        # impossible for __init__ to tell if a None means "nothing given" or
454
        # "seriously please no gatewaying". So, this must always be a vanilla
455
        # truth test and not eg "is not None".
456 2
        if self.gateway:
457
            # Displaying type because gw params would probs be too verbose
458 2
            val = "proxyjump"
459 2
            if isinstance(self.gateway, string_types):
460 2
                val = "proxycommand"
461 2
            bits.append(("gw", val))
462 2
        return "<Connection {}>".format(
463
            " ".join("{}={}".format(*x) for x in bits)
464
        )
465

466 2
    def _identity(self):
467
        # TODO: consider including gateway and maybe even other init kwargs?
468
        # Whether two cxns w/ same user/host/port but different
469
        # gateway/keys/etc, should be considered "the same", is unclear.
470 2
        return (self.host, self.user, self.port)
471

472 2
    def __eq__(self, other):
473 2
        if not isinstance(other, Connection):
474 1
            return False
475 2
        return self._identity() == other._identity()
476

477 2
    def __lt__(self, other):
478 2
        return self._identity() < other._identity()
479

480 2
    def __hash__(self):
481
        # NOTE: this departs from Context/DataProxy, which is not usefully
482
        # hashable.
483 2
        return hash(self._identity())
484

485 2
    def derive_shorthand(self, host_string):
486 2
        user_hostport = host_string.rsplit("@", 1)
487 2
        hostport = user_hostport.pop()
488 2
        user = user_hostport[0] if user_hostport and user_hostport[0] else None
489

490
        # IPv6: can't reliably tell where addr ends and port begins, so don't
491
        # try (and don't bother adding special syntax either, user should avoid
492
        # this situation by using port=).
493 2
        if hostport.count(":") > 1:
494 2
            host = hostport
495 2
            port = None
496
        # IPv4: can split on ':' reliably.
497
        else:
498 2
            host_port = hostport.rsplit(":", 1)
499 2
            host = host_port.pop(0) or None
500 2
            port = host_port[0] if host_port and host_port[0] else None
501

502 2
        if port is not None:
503 2
            port = int(port)
504

505 2
        return {"user": user, "host": host, "port": port}
506

507 2
    @property
508
    def is_connected(self):
509
        """
510
        Whether or not this connection is actually open.
511

512
        .. versionadded:: 2.0
513
        """
514 2
        return self.transport.active if self.transport else False
515

516 2
    def open(self):
517
        """
518
        Initiate an SSH connection to the host/port this object is bound to.
519

520
        This may include activating the configured gateway connection, if one
521
        is set.
522

523
        Also saves a handle to the now-set Transport object for easier access.
524

525
        Various connect-time settings (and/or their corresponding :ref:`SSH
526
        config options <ssh-config>`) are utilized here in the call to
527
        `SSHClient.connect <paramiko.client.SSHClient.connect>`. (For details,
528
        see :doc:`the configuration docs </concepts/configuration>`.)
529

530
        .. versionadded:: 2.0
531
        """
532
        # Short-circuit
533 2
        if self.is_connected:
534 2
            return
535 2
        err = "Refusing to be ambiguous: connect() kwarg '{}' was given both via regular arg and via connect_kwargs!"  # noqa
536
        # These may not be given, period
537 2
        for key in """
538
            hostname
539
            port
540
            username
541
        """.split():
542 2
            if key in self.connect_kwargs:
543 2
                raise ValueError(err.format(key))
544
        # These may be given one way or the other, but not both
545 2
        if (
546
            "timeout" in self.connect_kwargs
547
            and self.connect_timeout is not None
548
        ):
549 2
            raise ValueError(err.format("timeout"))
550
        # No conflicts -> merge 'em together
551 2
        kwargs = dict(
552
            self.connect_kwargs,
553
            username=self.user,
554
            hostname=self.host,
555
            port=self.port,
556
        )
557 2
        if self.gateway:
558 2
            kwargs["sock"] = self.open_gateway()
559 2
        if self.connect_timeout:
560 2
            kwargs["timeout"] = self.connect_timeout
561
        # Strip out empty defaults for less noisy debugging
562 2
        if "key_filename" in kwargs and not kwargs["key_filename"]:
563 0
            del kwargs["key_filename"]
564
        # Actually connect!
565 2
        self.client.connect(**kwargs)
566 2
        self.transport = self.client.get_transport()
567

568 2
    def open_gateway(self):
569
        """
570
        Obtain a socket-like object from `gateway`.
571

572
        :returns:
573
            A ``direct-tcpip`` `paramiko.channel.Channel`, if `gateway` was a
574
            `.Connection`; or a `~paramiko.proxy.ProxyCommand`, if `gateway`
575
            was a string.
576

577
        .. versionadded:: 2.0
578
        """
579
        # ProxyCommand is faster to set up, so do it first.
580 2
        if isinstance(self.gateway, string_types):
581
            # Leverage a dummy SSHConfig to ensure %h/%p/etc are parsed.
582
            # TODO: use real SSH config once loading one properly is
583
            # implemented.
584 2
            ssh_conf = SSHConfig()
585 2
            dummy = "Host {}\n    ProxyCommand {}"
586 2
            ssh_conf.parse(StringIO(dummy.format(self.host, self.gateway)))
587 2
            return ProxyCommand(ssh_conf.lookup(self.host)["proxycommand"])
588
        # Handle inner-Connection gateway type here.
589
        # TODO: logging
590 2
        self.gateway.open()
591
        # TODO: expose the opened channel itself as an attribute? (another
592
        # possible argument for separating the two gateway types...) e.g. if
593
        # someone wanted to piggyback on it for other same-interpreter socket
594
        # needs...
595
        # TODO: and the inverse? allow users to supply their own socket/like
596
        # object they got via $WHEREEVER?
597
        # TODO: how best to expose timeout param? reuse general connection
598
        # timeout from config?
599 2
        return self.gateway.transport.open_channel(
600
            kind="direct-tcpip",
601
            dest_addr=(self.host, int(self.port)),
602
            # NOTE: src_addr needs to be 'empty but not None' values to
603
            # correctly encode into a network message. Theoretically Paramiko
604
            # could auto-interpret None sometime & save us the trouble.
605
            src_addr=("", 0),
606
        )
607

608 2
    def close(self):
609
        """
610
        Terminate the network connection to the remote end, if open.
611

612
        If no connection is open, this method does nothing.
613

614
        .. versionadded:: 2.0
615
        """
616 2
        if self.is_connected:
617 2
            self.client.close()
618 2
            if self.forward_agent and self._agent_handler is not None:
619 2
                self._agent_handler.close()
620

621 2
    def __enter__(self):
622 2
        return self
623

624 2
    def __exit__(self, *exc):
625 2
        self.close()
626

627 2
    @opens
628
    def create_session(self):
629 2
        channel = self.transport.open_session()
630 2
        if self.forward_agent:
631 2
            self._agent_handler = AgentRequestHandler(channel)
632 2
        return channel
633

634 2
    def _remote_runner(self):
635 2
        return self.config.runners.remote(self, inline_env=self.inline_ssh_env)
636

637 2
    @opens
638
    def run(self, command, **kwargs):
639
        """
640
        Execute a shell command on the remote end of this connection.
641

642
        This method wraps an SSH-capable implementation of
643
        `invoke.runners.Runner.run`; see its documentation for details.
644

645
        .. warning::
646
            There are a few spots where Fabric departs from Invoke's default
647
            settings/behaviors; they are documented under
648
            `.Config.global_defaults`.
649

650
        .. versionadded:: 2.0
651
        """
652 2
        return self._run(self._remote_runner(), command, **kwargs)
653

654 2
    @opens
655
    def sudo(self, command, **kwargs):
656
        """
657
        Execute a shell command, via ``sudo``, on the remote end.
658

659
        This method is identical to `invoke.context.Context.sudo` in every way,
660
        except in that -- like `run` -- it honors per-host/per-connection
661
        configuration overrides in addition to the generic/global ones. Thus,
662
        for example, per-host sudo passwords may be configured.
663

664
        .. versionadded:: 2.0
665
        """
666 2
        return self._sudo(self._remote_runner(), command, **kwargs)
667

668 2
    def local(self, *args, **kwargs):
669
        """
670
        Execute a shell command on the local system.
671

672
        This method is effectively a wrapper of `invoke.run`; see its docs for
673
        details and call signature.
674

675
        .. versionadded:: 2.0
676
        """
677
        # Superclass run() uses runners.local, so we can literally just call it
678
        # straight.
679 2
        return super(Connection, self).run(*args, **kwargs)
680

681 2
    @opens
682
    def sftp(self):
683
        """
684
        Return a `~paramiko.sftp_client.SFTPClient` object.
685

686
        If called more than one time, memoizes the first result; thus, any
687
        given `.Connection` instance will only ever have a single SFTP client,
688
        and state (such as that managed by
689
        `~paramiko.sftp_client.SFTPClient.chdir`) will be preserved.
690

691
        .. versionadded:: 2.0
692
        """
693 2
        if self._sftp is None:
694 2
            self._sftp = self.client.open_sftp()
695 2
        return self._sftp
696

697 2
    def get(self, *args, **kwargs):
698
        """
699
        Get a remote file to the local filesystem or file-like object.
700

701
        Simply a wrapper for `.Transfer.get`. Please see its documentation for
702
        all details.
703

704
        .. versionadded:: 2.0
705
        """
706 2
        return Transfer(self).get(*args, **kwargs)
707

708 2
    def put(self, *args, **kwargs):
709
        """
710
        Put a local file (or file-like object) to the remote filesystem.
711

712
        Simply a wrapper for `.Transfer.put`. Please see its documentation for
713
        all details.
714

715
        .. versionadded:: 2.0
716
        """
717 2
        return Transfer(self).put(*args, **kwargs)
718

719
    # TODO: yield the socket for advanced users? Other advanced use cases
720
    # (perhaps factor out socket creation itself)?
721
    # TODO: probably push some of this down into Paramiko
722 2
    @contextmanager
723 2
    @opens
724 2
    def forward_local(
725
        self,
726
        local_port,
727
        remote_port=None,
728
        remote_host="localhost",
729
        local_host="localhost",
730
    ):
731
        """
732
        Open a tunnel connecting ``local_port`` to the server's environment.
733

734
        For example, say you want to connect to a remote PostgreSQL database
735
        which is locked down and only accessible via the system it's running
736
        on. You have SSH access to this server, so you can temporarily make
737
        port 5432 on your local system act like port 5432 on the server::
738

739
            import psycopg2
740
            from fabric import Connection
741

742
            with Connection('my-db-server').forward_local(5432):
743
                db = psycopg2.connect(
744
                    host='localhost', port=5432, database='mydb'
745
                )
746
                # Do things with 'db' here
747

748
        This method is analogous to using the ``-L`` option of OpenSSH's
749
        ``ssh`` program.
750

751
        :param int local_port: The local port number on which to listen.
752

753
        :param int remote_port:
754
            The remote port number. Defaults to the same value as
755
            ``local_port``.
756

757
        :param str local_host:
758
            The local hostname/interface on which to listen. Default:
759
            ``localhost``.
760

761
        :param str remote_host:
762
            The remote hostname serving the forwarded remote port. Default:
763
            ``localhost`` (i.e., the host this `.Connection` is connected to.)
764

765
        :returns:
766
            Nothing; this method is only useful as a context manager affecting
767
            local operating system state.
768

769
        .. versionadded:: 2.0
770
        """
771 2
        if not remote_port:
772 2
            remote_port = local_port
773

774
        # TunnelManager does all of the work, sitting in the background (so we
775
        # can yield) and spawning threads every time somebody connects to our
776
        # local port.
777 2
        finished = Event()
778 2
        manager = TunnelManager(
779
            local_port=local_port,
780
            local_host=local_host,
781
            remote_port=remote_port,
782
            remote_host=remote_host,
783
            # TODO: not a huge fan of handing in our transport, but...?
784
            transport=self.transport,
785
            finished=finished,
786
        )
787 2
        manager.start()
788

789
        # Return control to caller now that things ought to be operational
790 2
        try:
791 2
            yield
792
        # Teardown once user exits block
793
        finally:
794
            # Signal to manager that it should close all open tunnels
795 2
            finished.set()
796
            # Then wait for it to do so
797 2
            manager.join()
798
            # Raise threading errors from within the manager, which would be
799
            # one of:
800
            # - an inner ThreadException, which was created by the manager on
801
            # behalf of its Tunnels; this gets directly raised.
802
            # - some other exception, which would thus have occurred in the
803
            # manager itself; we wrap this in a new ThreadException.
804
            # NOTE: in these cases, some of the metadata tracking in
805
            # ExceptionHandlingThread/ExceptionWrapper/ThreadException (which
806
            # is useful when dealing with multiple nearly-identical sibling IO
807
            # threads) is superfluous, but it doesn't feel worth breaking
808
            # things up further; we just ignore it for now.
809 2
            wrapper = manager.exception()
810 2
            if wrapper is not None:
811 2
                if wrapper.type is ThreadException:
812 2
                    raise wrapper.value
813
                else:
814 2
                    raise ThreadException([wrapper])
815

816
            # TODO: cancel port forward on transport? Does that even make sense
817
            # here (where we used direct-tcpip) vs the opposite method (which
818
            # is what uses forward-tcpip)?
819

820
    # TODO: probably push some of this down into Paramiko
821 2
    @contextmanager
822 2
    @opens
823 2
    def forward_remote(
824
        self,
825
        remote_port,
826
        local_port=None,
827
        remote_host="127.0.0.1",
828
        local_host="localhost",
829
    ):
830
        """
831
        Open a tunnel connecting ``remote_port`` to the local environment.
832

833
        For example, say you're running a daemon in development mode on your
834
        workstation at port 8080, and want to funnel traffic to it from a
835
        production or staging environment.
836

837
        In most situations this isn't possible as your office/home network
838
        probably blocks inbound traffic. But you have SSH access to this
839
        server, so you can temporarily make port 8080 on that server act like
840
        port 8080 on your workstation::
841

842
            from fabric import Connection
843

844
            c = Connection('my-remote-server')
845
            with c.forward_remote(8080):
846
                c.run("remote-data-writer --port 8080")
847
                # Assuming remote-data-writer runs until interrupted, this will
848
                # stay open until you Ctrl-C...
849

850
        This method is analogous to using the ``-R`` option of OpenSSH's
851
        ``ssh`` program.
852

853
        :param int remote_port: The remote port number on which to listen.
854

855
        :param int local_port:
856
            The local port number. Defaults to the same value as
857
            ``remote_port``.
858

859
        :param str local_host:
860
            The local hostname/interface the forwarded connection talks to.
861
            Default: ``localhost``.
862

863
        :param str remote_host:
864
            The remote interface address to listen on when forwarding
865
            connections. Default: ``127.0.0.1`` (i.e. only listen on the remote
866
            localhost).
867

868
        :returns:
869
            Nothing; this method is only useful as a context manager affecting
870
            local operating system state.
871

872
        .. versionadded:: 2.0
873
        """
874 2
        if not local_port:
875 2
            local_port = remote_port
876
        # Callback executes on each connection to the remote port and is given
877
        # a Channel hooked up to said port. (We don't actually care about the
878
        # source/dest host/port pairs at all; only whether the channel has data
879
        # to read and suchlike.)
880
        # We then pair that channel with a new 'outbound' socket connection to
881
        # the local host/port being forwarded, in a new Tunnel.
882
        # That Tunnel is then added to a shared data structure so we can track
883
        # & close them during shutdown.
884
        #
885
        # TODO: this approach is less than ideal because we have to share state
886
        # between ourselves & the callback handed into the transport's own
887
        # thread handling (which is roughly analogous to our self-controlled
888
        # TunnelManager for local forwarding). See if we can use more of
889
        # Paramiko's API (or improve it and then do so) so that isn't
890
        # necessary.
891 2
        tunnels = []
892

893 2
        def callback(channel, src_addr_tup, dst_addr_tup):
894 2
            sock = socket.socket()
895
            # TODO: handle connection failure such that channel, etc get closed
896 2
            sock.connect((local_host, local_port))
897
            # TODO: we don't actually need to generate the Events at our level,
898
            # do we? Just let Tunnel.__init__ do it; all we do is "press its
899
            # button" on shutdown...
900 2
            tunnel = Tunnel(channel=channel, sock=sock, finished=Event())
901 2
            tunnel.start()
902
            # Communication between ourselves & the Paramiko handling subthread
903 2
            tunnels.append(tunnel)
904

905
        # Ask Paramiko (really, the remote sshd) to call our callback whenever
906
        # connections are established on the remote iface/port.
907
        # transport.request_port_forward(remote_host, remote_port, callback)
908 2
        try:
909 2
            self.transport.request_port_forward(
910
                address=remote_host, port=remote_port, handler=callback
911
            )
912 2
            yield
913
        finally:
914
            # TODO: see above re: lack of a TunnelManager
915
            # TODO: and/or also refactor with TunnelManager re: shutdown logic.
916
            # E.g. maybe have a non-thread TunnelManager-alike with a method
917
            # that acts as the callback? At least then there's a tiny bit more
918
            # encapsulation...meh.
919 2
            for tunnel in tunnels:
920 2
                tunnel.finished.set()
921 2
                tunnel.join()
922 2
            self.transport.cancel_port_forward(
923
                address=remote_host, port=remote_port
924
            )

Read our documentation on viewing source code .

Loading