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

4 1
try:
5 1
    from invoke.vendor.six import StringIO
6 1
    from invoke.vendor.decorator import decorator
7 1
    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 1
import socket
13

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

21 1
from .config import Config
22 1
from .exceptions import InvalidV1Env
23 1
from .transfer import Transfer
24 1
from .tunnels import TunnelManager, Tunnel
25

26

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

32

33 1
def derive_shorthand(host_string):
34 1
    user_hostport = host_string.rsplit("@", 1)
35 1
    hostport = user_hostport.pop()
36 1
    user = user_hostport[0] if user_hostport and user_hostport[0] else None
37

38
    # IPv6: can't reliably tell where addr ends and port begins, so don't
39
    # try (and don't bother adding special syntax either, user should avoid
40
    # this situation by using port=).
41 1
    if hostport.count(":") > 1:
42 1
        host = hostport
43 1
        port = None
44
    # IPv4: can split on ':' reliably.
45
    else:
46 1
        host_port = hostport.rsplit(":", 1)
47 1
        host = host_port.pop(0) or None
48 1
        port = host_port[0] if host_port and host_port[0] else None
49

50 1
    if port is not None:
51 1
        port = int(port)
52

53 1
    return {"user": user, "host": host, "port": port}
54

55

56 1
class Connection(Context):
57
    """
58
    A connection to an SSH daemon, with methods for commands and file transfer.
59

60
    **Basics**
61

62
    This class inherits from Invoke's `~invoke.context.Context`, as it is a
63
    context within which commands, tasks etc can operate. It also encapsulates
64
    a Paramiko `~paramiko.client.SSHClient` instance, performing useful high
65
    level operations with that `~paramiko.client.SSHClient` and
66
    `~paramiko.channel.Channel` instances generated from it.
67

68
    .. _connect_kwargs:
69

70
    .. note::
71
        Many SSH specific options -- such as specifying private keys and
72
        passphrases, timeouts, disabling SSH agents, etc -- are handled
73
        directly by Paramiko and should be specified via the
74
        :ref:`connect_kwargs argument <connect_kwargs-arg>` of the constructor.
75

76
    **Lifecycle**
77

78
    `.Connection` has a basic "`create <__init__>`, `connect/open <open>`, `do
79
    work <run>`, `disconnect/close <close>`" lifecycle:
80

81
    - `Instantiation <__init__>` imprints the object with its connection
82
      parameters (but does **not** actually initiate the network connection).
83

84
        - An alternate constructor exists for users :ref:`upgrading piecemeal
85
          from Fabric 1 <from-v1>`: `from_v1`
86

87
    - Methods like `run`, `get` etc automatically trigger a call to
88
      `open` if the connection is not active; users may of course call `open`
89
      manually if desired.
90
    - Connections do not always need to be explicitly closed; much of the
91
      time, Paramiko's garbage collection hooks or Python's own shutdown
92
      sequence will take care of things. **However**, should you encounter edge
93
      cases (for example, sessions hanging on exit) it's helpful to explicitly
94
      close connections when you're done with them.
95

96
      This can be accomplished by manually calling `close`, or by using the
97
      object as a contextmanager::
98

99
        with Connection('host') as c:
100
            c.run('command')
101
            c.put('file')
102

103
    .. note::
104
        This class rebinds `invoke.context.Context.run` to `.local` so both
105
        remote and local command execution can coexist.
106

107
    **Configuration**
108

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

114
    - Use any built-in config mechanism, such as ``/etc/fabric.yml``,
115
      ``~/.fabric.json``, collection-driven configuration, env vars, etc,
116
      stating ``user: admin`` (or ``{"user": "admin"}``, depending on config
117
      format.) Then ``Connection('myhost')`` would implicitly have a ``user``
118
      of ``admin``.
119
    - Use an SSH config file containing ``User admin`` within any applicable
120
      ``Host`` header (``Host myhost``, ``Host *``, etc.) Again,
121
      ``Connection('myhost')`` will default to an ``admin`` user.
122
    - Leverage host-parameter shorthand (described in `.Config.__init__`), i.e.
123
      ``Connection('admin@myhost')``.
124
    - Give the parameter directly: ``Connection('myhost', user='admin')``.
125

126
    The same applies to agent forwarding, gateways, and so forth.
127

128
    .. versionadded:: 2.0
129
    """
130

131
    # NOTE: these are initialized here to hint to invoke.Config.__setattr__
132
    # that they should be treated as real attributes instead of config proxies.
133
    # (Additionally, we're doing this instead of using invoke.Config._set() so
134
    # we can take advantage of Sphinx's attribute-doc-comment static analysis.)
135
    # Once an instance is created, these values will usually be non-None
136
    # because they default to the default config values.
137 1
    host = None
138 1
    original_host = None
139 1
    user = None
140 1
    port = None
141 1
    ssh_config = None
142 1
    gateway = None
143 1
    forward_agent = None
144 1
    connect_timeout = None
145 1
    connect_kwargs = None
146 1
    client = None
147 1
    transport = None
148 1
    _sftp = None
149 1
    _agent_handler = None
150

151 1
    @classmethod
152
    def from_v1(cls, env, **kwargs):
153
        """
154
        Alternate constructor which uses Fabric 1's ``env`` dict for settings.
155

156
        All keyword arguments besides ``env`` are passed unmolested into the
157
        primary constructor.
158

159
        .. warning::
160
            Because your own config overrides will win over data from ``env``,
161
            make sure you only set values you *intend* to change from your v1
162
            environment!
163

164
        For details on exactly which ``env`` vars are imported and what they
165
        become in the new API, please see :ref:`v1-env-var-imports`.
166

167
        :param env:
168
            An explicit Fabric 1 ``env`` dict (technically, any
169
            ``fabric.utils._AttributeDict`` instance should work) to pull
170
            configuration from.
171

172
        .. versionadded:: 2.4
173
        """
174
        # TODO: import fabric.state.env (need good way to test it first...)
175
        # TODO: how to handle somebody accidentally calling this in a process
176
        # where 'fabric' is fabric 2, and there's no fabric 1? Probably just a
177
        # re-raise of ImportError??
178
        # Our only requirement is a non-empty host_string
179 1
        if not env.host_string:
180 1
            raise InvalidV1Env(
181
                "Supplied v1 env has an empty `host_string` value! Please make sure you're calling Connection.from_v1 within a connected Fabric 1 session."  # noqa
182
            )
183
        # TODO: detect collisions with kwargs & except instead of overwriting?
184
        # (More Zen of Python compliant, but also, effort, and also, makes it
185
        # harder for users to intentionally overwrite!)
186 1
        connect_kwargs = kwargs.setdefault("connect_kwargs", {})
187 1
        kwargs.setdefault("host", env.host_string)
188 1
        shorthand = derive_shorthand(env.host_string)
189
        # TODO: don't we need to do the below skipping for user too?
190 1
        kwargs.setdefault("user", env.user)
191
        # Skip port if host string seemed to have it; otherwise we hit our own
192
        # ambiguity clause in __init__. v1 would also have been doing this
193
        # anyways (host string wins over other settings).
194 1
        if not shorthand["port"]:
195
            # Run port through int(); v1 inexplicably has a string default...
196 1
            kwargs.setdefault("port", int(env.port))
197
        # key_filename defaults to None in v1, but in v2, we expect it to be
198
        # either unset, or set to a list. Thus, we only pull it over if it is
199
        # not None.
200 1
        if env.key_filename is not None:
201 0
            connect_kwargs.setdefault("key_filename", env.key_filename)
202
        # Obtain config values, if not given, from its own from_v1
203
        # NOTE: not using setdefault as we truly only want to call
204
        # Config.from_v1 when necessary.
205 1
        if "config" not in kwargs:
206 1
            kwargs["config"] = Config.from_v1(env)
207 1
        return cls(**kwargs)
208

209
    # TODO: should "reopening" an existing Connection object that has been
210
    # closed, be allowed? (See e.g. how v1 detects closed/semi-closed
211
    # connections & nukes them before creating a new client to the same host.)
212
    # TODO: push some of this into paramiko.client.Client? e.g. expand what
213
    # Client.exec_command does, it already allows configuring a subset of what
214
    # we do / will eventually do / did in 1.x. It's silly to have to do
215
    # .get_transport().open_session().
216 1
    def __init__(
217
        self,
218
        host,
219
        user=None,
220
        port=None,
221
        config=None,
222
        gateway=None,
223
        forward_agent=None,
224
        connect_timeout=None,
225
        connect_kwargs=None,
226
        inline_ssh_env=None,
227
    ):
228
        """
229
        Set up a new object representing a server connection.
230

231
        :param str host:
232
            the hostname (or IP address) of this connection.
233

234
            May include shorthand for the ``user`` and/or ``port`` parameters,
235
            of the form ``user@host``, ``host:port``, or ``user@host:port``.
236

237
            .. note::
238
                Due to ambiguity, IPv6 host addresses are incompatible with the
239
                ``host:port`` shorthand (though ``user@host`` will still work
240
                OK). In other words, the presence of >1 ``:`` character will
241
                prevent any attempt to derive a shorthand port number; use the
242
                explicit ``port`` parameter instead.
243

244
            .. note::
245
                If ``host`` matches a ``Host`` clause in loaded SSH config
246
                data, and that ``Host`` clause contains a ``Hostname``
247
                directive, the resulting `.Connection` object will behave as if
248
                ``host`` is equal to that ``Hostname`` value.
249

250
                In all cases, the original value of ``host`` is preserved as
251
                the ``original_host`` attribute.
252

253
                Thus, given SSH config like so::
254

255
                    Host myalias
256
                        Hostname realhostname
257

258
                a call like ``Connection(host='myalias')`` will result in an
259
                object whose ``host`` attribute is ``realhostname``, and whose
260
                ``original_host`` attribute is ``myalias``.
261

262
        :param str user:
263
            the login user for the remote connection. Defaults to
264
            ``config.user``.
265

266
        :param int port:
267
            the remote port. Defaults to ``config.port``.
268

269
        :param config:
270
            configuration settings to use when executing methods on this
271
            `.Connection` (e.g. default SSH port and so forth).
272

273
            Should be a `.Config` or an `invoke.config.Config`
274
            (which will be turned into a `.Config`).
275

276
            Default is an anonymous `.Config` object.
277

278
        :param gateway:
279
            An object to use as a proxy or gateway for this connection.
280

281
            This parameter accepts one of the following:
282

283
            - another `.Connection` (for a ``ProxyJump`` style gateway);
284
            - a shell command string (for a ``ProxyCommand`` style style
285
              gateway).
286

287
            Default: ``None``, meaning no gatewaying will occur (unless
288
            otherwise configured; if one wants to override a configured gateway
289
            at runtime, specify ``gateway=False``.)
290

291
            .. seealso:: :ref:`ssh-gateways`
292

293
        :param bool forward_agent:
294
            Whether to enable SSH agent forwarding.
295

296
            Default: ``config.forward_agent``.
297

298
        :param int connect_timeout:
299
            Connection timeout, in seconds.
300

301
            Default: ``config.timeouts.connect``.
302

303
        .. _connect_kwargs-arg:
304

305
        :param dict connect_kwargs:
306
            Keyword arguments handed verbatim to
307
            `SSHClient.connect <paramiko.client.SSHClient.connect>` (when
308
            `.open` is called).
309

310
            `.Connection` tries not to grow additional settings/kwargs of its
311
            own unless it is adding value of some kind; thus,
312
            ``connect_kwargs`` is currently the right place to hand in paramiko
313
            connection parameters such as ``pkey`` or ``key_filename``. For
314
            example::
315

316
                c = Connection(
317
                    host="hostname",
318
                    user="admin",
319
                    connect_kwargs={
320
                        "key_filename": "/home/myuser/.ssh/private.key",
321
                    },
322
                )
323

324
            Default: ``config.connect_kwargs``.
325

326
        :param bool inline_ssh_env:
327
            Whether to send environment variables "inline" as prefixes in front
328
            of command strings (``export VARNAME=value && mycommand here``),
329
            instead of trying to submit them through the SSH protocol itself
330
            (which is the default behavior). This is necessary if the remote
331
            server has a restricted ``AcceptEnv`` setting (which is the common
332
            default).
333

334
            The default value is the value of the ``inline_ssh_env``
335
            :ref:`configuration value <default-values>` (which itself defaults
336
            to ``False``).
337

338
            .. warning::
339
                This functionality does **not** currently perform any shell
340
                escaping on your behalf! Be careful when using nontrivial
341
                values, and note that you can put in your own quoting,
342
                backslashing etc if desired.
343

344
                Consider using a different approach (such as actual
345
                remote shell scripts) if you run into too many issues here.
346

347
            .. note::
348
                When serializing into prefixed ``FOO=bar`` format, we apply the
349
                builtin `sorted` function to the env dictionary's keys, to
350
                remove what would otherwise be ambiguous/arbitrary ordering.
351

352
            .. note::
353
                This setting has no bearing on *local* shell commands; it only
354
                affects remote commands, and thus, methods like `.run` and
355
                `.sudo`.
356

357
        :raises ValueError:
358
            if user or port values are given via both ``host`` shorthand *and*
359
            their own arguments. (We `refuse the temptation to guess`_).
360

361
        .. _refuse the temptation to guess:
362
            http://zen-of-python.info/
363
            in-the-face-of-ambiguity-refuse-the-temptation-to-guess.html#12
364

365
        .. versionchanged:: 2.3
366
            Added the ``inline_ssh_env`` parameter.
367
        """
368
        # NOTE: parent __init__ sets self._config; for now we simply overwrite
369
        # that below. If it's somehow problematic we would want to break parent
370
        # __init__ up in a manner that is more cleanly overrideable.
371 1
        super(Connection, self).__init__(config=config)
372

373
        #: The .Config object referenced when handling default values (for e.g.
374
        #: user or port, when not explicitly given) or deciding how to behave.
375 1
        if config is None:
376 1
            config = Config()
377
        # Handle 'vanilla' Invoke config objects, which need cloning 'into' one
378
        # of our own Configs (which grants the new defaults, etc, while not
379
        # squashing them if the Invoke-level config already accounted for them)
380 1
        elif not isinstance(config, Config):
381 1
            config = config.clone(into=Config)
382 1
        self._set(_config=config)
383
        # TODO: when/how to run load_files, merge, load_shell_env, etc?
384
        # TODO: i.e. what is the lib use case here (and honestly in invoke too)
385

386 1
        shorthand = self.derive_shorthand(host)
387 1
        host = shorthand["host"]
388 1
        err = "You supplied the {} via both shorthand and kwarg! Please pick one."  # noqa
389 1
        if shorthand["user"] is not None:
390 1
            if user is not None:
391 1
                raise ValueError(err.format("user"))
392 1
            user = shorthand["user"]
393 1
        if shorthand["port"] is not None:
394 1
            if port is not None:
395 1
                raise ValueError(err.format("port"))
396 1
            port = shorthand["port"]
397

398
        # NOTE: we load SSH config data as early as possible as it has
399
        # potential to affect nearly every other attribute.
400
        #: The per-host SSH config data, if any. (See :ref:`ssh-config`.)
401 1
        self.ssh_config = self.config.base_ssh_config.lookup(host)
402

403 1
        self.original_host = host
404
        #: The hostname of the target server.
405 1
        self.host = host
406 1
        if "hostname" in self.ssh_config:
407
            # TODO: log that this occurred?
408 1
            self.host = self.ssh_config["hostname"]
409

410
        #: The username this connection will use to connect to the remote end.
411 1
        self.user = user or self.ssh_config.get("user", self.config.user)
412
        # TODO: is it _ever_ possible to give an empty user value (e.g.
413
        # user='')? E.g. do some SSH server specs allow for that?
414

415
        #: The network port to connect on.
416 1
        self.port = port or int(self.ssh_config.get("port", self.config.port))
417

418
        # Gateway/proxy/bastion/jump setting: non-None values - string,
419
        # Connection, even eg False - get set directly; None triggers seek in
420
        # config/ssh_config
421
        #: The gateway `.Connection` or ``ProxyCommand`` string to be used,
422
        #: if any.
423 1
        self.gateway = gateway if gateway is not None else self.get_gateway()
424
        # NOTE: we use string above, vs ProxyCommand obj, to avoid spinning up
425
        # the ProxyCommand subprocess at init time, vs open() time.
426
        # TODO: make paramiko.proxy.ProxyCommand lazy instead?
427

428 1
        if forward_agent is None:
429
            # Default to config...
430 1
            forward_agent = self.config.forward_agent
431
            # But if ssh_config is present, it wins
432 1
            if "forwardagent" in self.ssh_config:
433
                # TODO: SSHConfig really, seriously needs some love here, god
434 1
                map_ = {"yes": True, "no": False}
435 1
                forward_agent = map_[self.ssh_config["forwardagent"]]
436
        #: Whether agent forwarding is enabled.
437 1
        self.forward_agent = forward_agent
438

439 1
        if connect_timeout is None:
440 1
            connect_timeout = self.ssh_config.get(
441
                "connecttimeout", self.config.timeouts.connect
442
            )
443 1
        if connect_timeout is not None:
444 1
            connect_timeout = int(connect_timeout)
445
        #: Connection timeout
446 1
        self.connect_timeout = connect_timeout
447

448
        #: Keyword arguments given to `paramiko.client.SSHClient.connect` when
449
        #: `open` is called.
450 1
        self.connect_kwargs = self.resolve_connect_kwargs(connect_kwargs)
451

452
        #: The `paramiko.client.SSHClient` instance this connection wraps.
453 1
        client = SSHClient()
454 1
        client.set_missing_host_key_policy(AutoAddPolicy())
455 1
        self.client = client
456

457
        #: A convenience handle onto the return value of
458
        #: ``self.client.get_transport()``.
459 1
        self.transport = None
460

461 1
        if inline_ssh_env is None:
462 1
            inline_ssh_env = self.config.inline_ssh_env
463
        #: Whether to construct remote command lines with env vars prefixed
464
        #: inline.
465 1
        self.inline_ssh_env = inline_ssh_env
466

467 1
    def resolve_connect_kwargs(self, connect_kwargs):
468
        # Grab connect_kwargs from config if not explicitly given.
469 1
        if connect_kwargs is None:
470
            # TODO: is it better to pre-empt conflicts w/ manually-handled
471
            # connect() kwargs (hostname, username, etc) here or in open()?
472
            # We're doing open() for now in case e.g. someone manually modifies
473
            # .connect_kwargs attributewise, but otherwise it feels better to
474
            # do it early instead of late.
475 1
            connect_kwargs = self.config.connect_kwargs
476
        # Special case: key_filename gets merged instead of overridden.
477
        # TODO: probably want some sorta smart merging generally, special cases
478
        # are bad.
479 1
        elif "key_filename" in self.config.connect_kwargs:
480 1
            kwarg_val = connect_kwargs.get("key_filename", [])
481 1
            conf_val = self.config.connect_kwargs["key_filename"]
482
            # Config value comes before kwarg value (because it may contain
483
            # CLI flag value.)
484 1
            connect_kwargs["key_filename"] = conf_val + kwarg_val
485

486
        # SSH config identityfile values come last in the key_filename
487
        # 'hierarchy'.
488 1
        if "identityfile" in self.ssh_config:
489 1
            connect_kwargs.setdefault("key_filename", [])
490 1
            connect_kwargs["key_filename"].extend(
491
                self.ssh_config["identityfile"]
492
            )
493

494 1
        return connect_kwargs
495

496 1
    def get_gateway(self):
497
        # SSH config wins over Invoke-style config
498 1
        if "proxyjump" in self.ssh_config:
499
            # Reverse hop1,hop2,hop3 style ProxyJump directive so we start
500
            # with the final (itself non-gatewayed) hop and work up to
501
            # the front (actual, supplied as our own gateway) hop
502 1
            hops = reversed(self.ssh_config["proxyjump"].split(","))
503 1
            prev_gw = None
504 1
            for hop in hops:
505
                # Short-circuit if we appear to be our own proxy, which would
506
                # be a RecursionError. Implies SSH config wildcards.
507
                # TODO: in an ideal world we'd check user/port too in case they
508
                # differ, but...seriously? They can file a PR with those extra
509
                # half dozen test cases in play, E_NOTIME
510 1
                if self.derive_shorthand(hop)["host"] == self.host:
511 1
                    return None
512
                # Happily, ProxyJump uses identical format to our host
513
                # shorthand...
514 1
                kwargs = dict(config=self.config.clone())
515 1
                if prev_gw is not None:
516 1
                    kwargs["gateway"] = prev_gw
517 1
                cxn = Connection(hop, **kwargs)
518 1
                prev_gw = cxn
519 1
            return prev_gw
520 1
        elif "proxycommand" in self.ssh_config:
521
            # Just a string, which we interpret as a proxy command..
522 1
            return self.ssh_config["proxycommand"]
523
        # Fallback: config value (may be None).
524 1
        return self.config.gateway
525

526 1
    def __repr__(self):
527
        # Host comes first as it's the most common differentiator by far
528 1
        bits = [("host", self.host)]
529
        # TODO: maybe always show user regardless? Explicit is good...
530 1
        if self.user != self.config.user:
531 1
            bits.append(("user", self.user))
532
        # TODO: harder to make case for 'always show port'; maybe if it's
533
        # non-22 (even if config has overridden the local default)?
534 1
        if self.port != self.config.port:
535 1
            bits.append(("port", self.port))
536
        # NOTE: sometimes self.gateway may be eg False if someone wants to
537
        # explicitly override a configured non-None value (as otherwise it's
538
        # impossible for __init__ to tell if a None means "nothing given" or
539
        # "seriously please no gatewaying". So, this must always be a vanilla
540
        # truth test and not eg "is not None".
541 1
        if self.gateway:
542
            # Displaying type because gw params would probs be too verbose
543 1
            val = "proxyjump"
544 1
            if isinstance(self.gateway, string_types):
545 1
                val = "proxycommand"
546 1
            bits.append(("gw", val))
547 1
        return "<Connection {}>".format(
548
            " ".join("{}={}".format(*x) for x in bits)
549
        )
550

551 1
    def _identity(self):
552
        # TODO: consider including gateway and maybe even other init kwargs?
553
        # Whether two cxns w/ same user/host/port but different
554
        # gateway/keys/etc, should be considered "the same", is unclear.
555 1
        return (self.host, self.user, self.port)
556

557 1
    def __eq__(self, other):
558 1
        if not isinstance(other, Connection):
559 1
            return False
560 1
        return self._identity() == other._identity()
561

562 1
    def __lt__(self, other):
563 1
        return self._identity() < other._identity()
564

565 1
    def __hash__(self):
566
        # NOTE: this departs from Context/DataProxy, which is not usefully
567
        # hashable.
568 1
        return hash(self._identity())
569

570 1
    def derive_shorthand(self, host_string):
571
        # NOTE: used to be defined inline; preserving API call for both
572
        # backwards compatibility and because it seems plausible we may want to
573
        # modify behavior later, using eg config or other attributes.
574 1
        return derive_shorthand(host_string)
575

576 1
    @property
577
    def is_connected(self):
578
        """
579
        Whether or not this connection is actually open.
580

581
        .. versionadded:: 2.0
582
        """
583 1
        return self.transport.active if self.transport else False
584

585 1
    def open(self):
586
        """
587
        Initiate an SSH connection to the host/port this object is bound to.
588

589
        This may include activating the configured gateway connection, if one
590
        is set.
591

592
        Also saves a handle to the now-set Transport object for easier access.
593

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

599
        .. versionadded:: 2.0
600
        """
601
        # Short-circuit
602 1
        if self.is_connected:
603 1
            return
604 1
        err = "Refusing to be ambiguous: connect() kwarg '{}' was given both via regular arg and via connect_kwargs!"  # noqa
605
        # These may not be given, period
606 1
        for key in """
607
            hostname
608
            port
609
            username
610
        """.split():
611 1
            if key in self.connect_kwargs:
612 1
                raise ValueError(err.format(key))
613
        # These may be given one way or the other, but not both
614 1
        if (
615
            "timeout" in self.connect_kwargs
616
            and self.connect_timeout is not None
617
        ):
618 1
            raise ValueError(err.format("timeout"))
619
        # No conflicts -> merge 'em together
620 1
        kwargs = dict(
621
            self.connect_kwargs,
622
            username=self.user,
623
            hostname=self.host,
624
            port=self.port,
625
        )
626 1
        if self.gateway:
627 1
            kwargs["sock"] = self.open_gateway()
628 1
        if self.connect_timeout:
629 1
            kwargs["timeout"] = self.connect_timeout
630
        # Strip out empty defaults for less noisy debugging
631 1
        if "key_filename" in kwargs and not kwargs["key_filename"]:
632 0
            del kwargs["key_filename"]
633
        # Actually connect!
634 1
        self.client.connect(**kwargs)
635 1
        self.transport = self.client.get_transport()
636

637 1
    def open_gateway(self):
638
        """
639
        Obtain a socket-like object from `gateway`.
640

641
        :returns:
642
            A ``direct-tcpip`` `paramiko.channel.Channel`, if `gateway` was a
643
            `.Connection`; or a `~paramiko.proxy.ProxyCommand`, if `gateway`
644
            was a string.
645

646
        .. versionadded:: 2.0
647
        """
648
        # ProxyCommand is faster to set up, so do it first.
649 1
        if isinstance(self.gateway, string_types):
650
            # Leverage a dummy SSHConfig to ensure %h/%p/etc are parsed.
651
            # TODO: use real SSH config once loading one properly is
652
            # implemented.
653 1
            ssh_conf = SSHConfig()
654 1
            dummy = "Host {}\n    ProxyCommand {}"
655 1
            ssh_conf.parse(StringIO(dummy.format(self.host, self.gateway)))
656 1
            return ProxyCommand(ssh_conf.lookup(self.host)["proxycommand"])
657
        # Handle inner-Connection gateway type here.
658
        # TODO: logging
659 1
        self.gateway.open()
660
        # TODO: expose the opened channel itself as an attribute? (another
661
        # possible argument for separating the two gateway types...) e.g. if
662
        # someone wanted to piggyback on it for other same-interpreter socket
663
        # needs...
664
        # TODO: and the inverse? allow users to supply their own socket/like
665
        # object they got via $WHEREEVER?
666
        # TODO: how best to expose timeout param? reuse general connection
667
        # timeout from config?
668 1
        return self.gateway.transport.open_channel(
669
            kind="direct-tcpip",
670
            dest_addr=(self.host, int(self.port)),
671
            # NOTE: src_addr needs to be 'empty but not None' values to
672
            # correctly encode into a network message. Theoretically Paramiko
673
            # could auto-interpret None sometime & save us the trouble.
674
            src_addr=("", 0),
675
        )
676

677 1
    def close(self):
678
        """
679
        Terminate the network connection to the remote end, if open.
680

681
        If no connection is open, this method does nothing.
682

683
        .. versionadded:: 2.0
684
        """
685 1
        if self.is_connected:
686 1
            self.client.close()
687 1
            if self.forward_agent and self._agent_handler is not None:
688 1
                self._agent_handler.close()
689

690 1
    def __enter__(self):
691 1
        return self
692

693 1
    def __exit__(self, *exc):
694 1
        self.close()
695

696 1
    @opens
697
    def create_session(self):
698 1
        channel = self.transport.open_session()
699 1
        if self.forward_agent:
700 1
            self._agent_handler = AgentRequestHandler(channel)
701 1
        return channel
702

703 1
    def _remote_runner(self):
704 1
        return self.config.runners.remote(self, inline_env=self.inline_ssh_env)
705

706 1
    @opens
707
    def run(self, command, **kwargs):
708
        """
709
        Execute a shell command on the remote end of this connection.
710

711
        This method wraps an SSH-capable implementation of
712
        `invoke.runners.Runner.run`; see its documentation for details.
713

714
        .. warning::
715
            There are a few spots where Fabric departs from Invoke's default
716
            settings/behaviors; they are documented under
717
            `.Config.global_defaults`.
718

719
        .. versionadded:: 2.0
720
        """
721 1
        return self._run(self._remote_runner(), command, **kwargs)
722

723 1
    @opens
724
    def sudo(self, command, **kwargs):
725
        """
726
        Execute a shell command, via ``sudo``, on the remote end.
727

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

733
        .. versionadded:: 2.0
734
        """
735 1
        return self._sudo(self._remote_runner(), command, **kwargs)
736

737 1
    def local(self, *args, **kwargs):
738
        """
739
        Execute a shell command on the local system.
740

741
        This method is effectively a wrapper of `invoke.run`; see its docs for
742
        details and call signature.
743

744
        .. versionadded:: 2.0
745
        """
746
        # Superclass run() uses runners.local, so we can literally just call it
747
        # straight.
748 1
        return super(Connection, self).run(*args, **kwargs)
749

750 1
    @opens
751
    def sftp(self):
752
        """
753
        Return a `~paramiko.sftp_client.SFTPClient` object.
754

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

760
        .. versionadded:: 2.0
761
        """
762 1
        if self._sftp is None:
763 1
            self._sftp = self.client.open_sftp()
764 1
        return self._sftp
765

766 1
    def get(self, *args, **kwargs):
767
        """
768
        Get a remote file to the local filesystem or file-like object.
769

770
        Simply a wrapper for `.Transfer.get`. Please see its documentation for
771
        all details.
772

773
        .. versionadded:: 2.0
774
        """
775 1
        return Transfer(self).get(*args, **kwargs)
776

777 1
    def put(self, *args, **kwargs):
778
        """
779
        Put a remote file (or file-like object) to the remote filesystem.
780

781
        Simply a wrapper for `.Transfer.put`. Please see its documentation for
782
        all details.
783

784
        .. versionadded:: 2.0
785
        """
786 1
        return Transfer(self).put(*args, **kwargs)
787

788
    # TODO: yield the socket for advanced users? Other advanced use cases
789
    # (perhaps factor out socket creation itself)?
790
    # TODO: probably push some of this down into Paramiko
791 1
    @contextmanager
792 1
    @opens
793 1
    def forward_local(
794
        self,
795
        local_port,
796
        remote_port=None,
797
        remote_host="localhost",
798
        local_host="localhost",
799
    ):
800
        """
801
        Open a tunnel connecting ``local_port`` to the server's environment.
802

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

808
            import psycopg2
809
            from fabric import Connection
810

811
            with Connection('my-db-server').forward_local(5432):
812
                db = psycopg2.connect(
813
                    host='localhost', port=5432, database='mydb'
814
                )
815
                # Do things with 'db' here
816

817
        This method is analogous to using the ``-L`` option of OpenSSH's
818
        ``ssh`` program.
819

820
        :param int local_port: The local port number on which to listen.
821

822
        :param int remote_port:
823
            The remote port number. Defaults to the same value as
824
            ``local_port``.
825

826
        :param str local_host:
827
            The local hostname/interface on which to listen. Default:
828
            ``localhost``.
829

830
        :param str remote_host:
831
            The remote hostname serving the forwarded remote port. Default:
832
            ``localhost`` (i.e., the host this `.Connection` is connected to.)
833

834
        :returns:
835
            Nothing; this method is only useful as a context manager affecting
836
            local operating system state.
837

838
        .. versionadded:: 2.0
839
        """
840 1
        if not remote_port:
841 1
            remote_port = local_port
842

843
        # TunnelManager does all of the work, sitting in the background (so we
844
        # can yield) and spawning threads every time somebody connects to our
845
        # local port.
846 1
        finished = Event()
847 1
        manager = TunnelManager(
848
            local_port=local_port,
849
            local_host=local_host,
850
            remote_port=remote_port,
851
            remote_host=remote_host,
852
            # TODO: not a huge fan of handing in our transport, but...?
853
            transport=self.transport,
854
            finished=finished,
855
        )
856 1
        manager.start()
857

858
        # Return control to caller now that things ought to be operational
859 1
        try:
860 1
            yield
861
        # Teardown once user exits block
862
        finally:
863
            # Signal to manager that it should close all open tunnels
864 1
            finished.set()
865
            # Then wait for it to do so
866 1
            manager.join()
867
            # Raise threading errors from within the manager, which would be
868
            # one of:
869
            # - an inner ThreadException, which was created by the manager on
870
            # behalf of its Tunnels; this gets directly raised.
871
            # - some other exception, which would thus have occurred in the
872
            # manager itself; we wrap this in a new ThreadException.
873
            # NOTE: in these cases, some of the metadata tracking in
874
            # ExceptionHandlingThread/ExceptionWrapper/ThreadException (which
875
            # is useful when dealing with multiple nearly-identical sibling IO
876
            # threads) is superfluous, but it doesn't feel worth breaking
877
            # things up further; we just ignore it for now.
878 1
            wrapper = manager.exception()
879 1
            if wrapper is not None:
880 1
                if wrapper.type is ThreadException:
881 1
                    raise wrapper.value
882
                else:
883 1
                    raise ThreadException([wrapper])
884

885
            # TODO: cancel port forward on transport? Does that even make sense
886
            # here (where we used direct-tcpip) vs the opposite method (which
887
            # is what uses forward-tcpip)?
888

889
    # TODO: probably push some of this down into Paramiko
890 1
    @contextmanager
891 1
    @opens
892 1
    def forward_remote(
893
        self,
894
        remote_port,
895
        local_port=None,
896
        remote_host="127.0.0.1",
897
        local_host="localhost",
898
    ):
899
        """
900
        Open a tunnel connecting ``remote_port`` to the local environment.
901

902
        For example, say you're running a daemon in development mode on your
903
        workstation at port 8080, and want to funnel traffic to it from a
904
        production or staging environment.
905

906
        In most situations this isn't possible as your office/home network
907
        probably blocks inbound traffic. But you have SSH access to this
908
        server, so you can temporarily make port 8080 on that server act like
909
        port 8080 on your workstation::
910

911
            from fabric import Connection
912

913
            c = Connection('my-remote-server')
914
            with c.forward_remote(8080):
915
                c.run("remote-data-writer --port 8080")
916
                # Assuming remote-data-writer runs until interrupted, this will
917
                # stay open until you Ctrl-C...
918

919
        This method is analogous to using the ``-R`` option of OpenSSH's
920
        ``ssh`` program.
921

922
        :param int remote_port: The remote port number on which to listen.
923

924
        :param int local_port:
925
            The local port number. Defaults to the same value as
926
            ``remote_port``.
927

928
        :param str local_host:
929
            The local hostname/interface the forwarded connection talks to.
930
            Default: ``localhost``.
931

932
        :param str remote_host:
933
            The remote interface address to listen on when forwarding
934
            connections. Default: ``127.0.0.1`` (i.e. only listen on the remote
935
            localhost).
936

937
        :returns:
938
            Nothing; this method is only useful as a context manager affecting
939
            local operating system state.
940

941
        .. versionadded:: 2.0
942
        """
943 1
        if not local_port:
944 1
            local_port = remote_port
945
        # Callback executes on each connection to the remote port and is given
946
        # a Channel hooked up to said port. (We don't actually care about the
947
        # source/dest host/port pairs at all; only whether the channel has data
948
        # to read and suchlike.)
949
        # We then pair that channel with a new 'outbound' socket connection to
950
        # the local host/port being forwarded, in a new Tunnel.
951
        # That Tunnel is then added to a shared data structure so we can track
952
        # & close them during shutdown.
953
        #
954
        # TODO: this approach is less than ideal because we have to share state
955
        # between ourselves & the callback handed into the transport's own
956
        # thread handling (which is roughly analogous to our self-controlled
957
        # TunnelManager for local forwarding). See if we can use more of
958
        # Paramiko's API (or improve it and then do so) so that isn't
959
        # necessary.
960 1
        tunnels = []
961

962 1
        def callback(channel, src_addr_tup, dst_addr_tup):
963 1
            sock = socket.socket()
964
            # TODO: handle connection failure such that channel, etc get closed
965 1
            sock.connect((local_host, local_port))
966
            # TODO: we don't actually need to generate the Events at our level,
967
            # do we? Just let Tunnel.__init__ do it; all we do is "press its
968
            # button" on shutdown...
969 1
            tunnel = Tunnel(channel=channel, sock=sock, finished=Event())
970 1
            tunnel.start()
971
            # Communication between ourselves & the Paramiko handling subthread
972 1
            tunnels.append(tunnel)
973

974
        # Ask Paramiko (really, the remote sshd) to call our callback whenever
975
        # connections are established on the remote iface/port.
976
        # transport.request_port_forward(remote_host, remote_port, callback)
977 1
        try:
978 1
            self.transport.request_port_forward(
979
                address=remote_host, port=remote_port, handler=callback
980
            )
981 1
            yield
982
        finally:
983
            # TODO: see above re: lack of a TunnelManager
984
            # TODO: and/or also refactor with TunnelManager re: shutdown logic.
985
            # E.g. maybe have a non-thread TunnelManager-alike with a method
986
            # that acts as the callback? At least then there's a tiny bit more
987
            # encapsulation...meh.
988 1
            for tunnel in tunnels:
989 1
                tunnel.finished.set()
990 1
                tunnel.join()
991 1
            self.transport.cancel_port_forward(
992
                address=remote_host, port=remote_port
993
            )

Read our documentation on viewing source code .

Loading