1from contextlib import contextmanager 2from threading import Event 3 4try: 5 from invoke.vendor.six import StringIO 6 from invoke.vendor.decorator import decorator 7 from invoke.vendor.six import string_types 8except ImportError: 9 from six import StringIO 10 from decorator import decorator 11 from six import string_types 12import socket 13 14from invoke import Context 15from invoke.exceptions import ThreadException 16from paramiko.agent import AgentRequestHandler 17from paramiko.client import SSHClient, AutoAddPolicy 18from paramiko.config import SSHConfig 19from paramiko.proxy import ProxyCommand 20 21from .config import Config 22from .exceptions import InvalidV1Env 23from .transfer import Transfer 24from .tunnels import TunnelManager, Tunnel 25 26 27@decorator 28def opens(method, self, *args, **kwargs): 29 self.open() 30 return method(self, *args, **kwargs) 31 32 33def derive_shorthand(host_string): 34 user_hostport = host_string.rsplit("@", 1) 35 hostport = user_hostport.pop() 36 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 if hostport.count(":") > 1: 42 host = hostport 43 port = None 44 # IPv4: can split on ':' reliably. 45 else: 46 host_port = hostport.rsplit(":", 1) 47 host = host_port.pop(0) or None 48 port = host_port[0] if host_port and host_port[0] else None 49 50 if port is not None: 51 port = int(port) 52 53 return {"user": user, "host": host, "port": port} 54 55 56class 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 host = None 138 original_host = None 139 user = None 140 port = None 141 ssh_config = None 142 gateway = None 143 forward_agent = None 144 connect_timeout = None 145 connect_kwargs = None 146 client = None 147 transport = None 148 _sftp = None 149 _agent_handler = None 150 151 @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 if not env.host_string: 180 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 connect_kwargs = kwargs.setdefault("connect_kwargs", {}) 187 kwargs.setdefault("host", env.host_string) 188 shorthand = derive_shorthand(env.host_string) 189 # TODO: don't we need to do the below skipping for user too? 190 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 if not shorthand["port"]: 195 # Run port through int(); v1 inexplicably has a string default... 196 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 if env.key_filename is not None: 201 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 if "config" not in kwargs: 206 kwargs["config"] = Config.from_v1(env) 207 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 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 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 if config is None: 376 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 elif not isinstance(config, Config): 381 config = config.clone(into=Config) 382 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 shorthand = self.derive_shorthand(host) 387 host = shorthand["host"] 388 err = "You supplied the {} via both shorthand and kwarg! Please pick one." # noqa 389 if shorthand["user"] is not None: 390 if user is not None: 391 raise ValueError(err.format("user")) 392 user = shorthand["user"] 393 if shorthand["port"] is not None: 394 if port is not None: 395 raise ValueError(err.format("port")) 396 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 self.ssh_config = self.config.base_ssh_config.lookup(host) 402 403 self.original_host = host 404 #: The hostname of the target server. 405 self.host = host 406 if "hostname" in self.ssh_config: 407 # TODO: log that this occurred? 408 self.host = self.ssh_config["hostname"] 409 410 #: The username this connection will use to connect to the remote end. 411 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 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 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 if forward_agent is None: 429 # Default to config... 430 forward_agent = self.config.forward_agent 431 # But if ssh_config is present, it wins 432 if "forwardagent" in self.ssh_config: 433 # TODO: SSHConfig really, seriously needs some love here, god 434 map_ = {"yes": True, "no": False} 435 forward_agent = map_[self.ssh_config["forwardagent"]] 436 #: Whether agent forwarding is enabled. 437 self.forward_agent = forward_agent 438 439 if connect_timeout is None: 440 connect_timeout = self.ssh_config.get( 441 "connecttimeout", self.config.timeouts.connect 442 ) 443 if connect_timeout is not None: 444 connect_timeout = int(connect_timeout) 445 #: Connection timeout 446 self.connect_timeout = connect_timeout 447 448 #: Keyword arguments given to `paramiko.client.SSHClient.connect` when 449 #: `open` is called. 450 self.connect_kwargs = self.resolve_connect_kwargs(connect_kwargs) 451 452 #: The `paramiko.client.SSHClient` instance this connection wraps. 453 client = SSHClient() 454 client.set_missing_host_key_policy(AutoAddPolicy()) 455 self.client = client 456 457 #: A convenience handle onto the return value of 458 #: ``self.client.get_transport()``. 459 self.transport = None 460 461 if inline_ssh_env is None: 462 inline_ssh_env = self.config.inline_ssh_env 463 #: Whether to construct remote command lines with env vars prefixed 464 #: inline. 465 self.inline_ssh_env = inline_ssh_env 466 467 def resolve_connect_kwargs(self, connect_kwargs): 468 # Grab connect_kwargs from config if not explicitly given. 469 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 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 elif "key_filename" in self.config.connect_kwargs: 480 kwarg_val = connect_kwargs.get("key_filename", []) 481 conf_val = self.config.connect_kwargs["key_filename"] 482 # Config value comes before kwarg value (because it may contain 483 # CLI flag value.) 484 connect_kwargs["key_filename"] = conf_val + kwarg_val 485 486 # SSH config identityfile values come last in the key_filename 487 # 'hierarchy'. 488 if "identityfile" in self.ssh_config: 489 connect_kwargs.setdefault("key_filename", []) 490 connect_kwargs["key_filename"].extend( 491 self.ssh_config["identityfile"] 492 ) 493 494 return connect_kwargs 495 496 def get_gateway(self): 497 # SSH config wins over Invoke-style config 498 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 hops = reversed(self.ssh_config["proxyjump"].split(",")) 503 prev_gw = None 504 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 if self.derive_shorthand(hop)["host"] == self.host: 511 return None 512 # Happily, ProxyJump uses identical format to our host 513 # shorthand... 514 kwargs = dict(config=self.config.clone()) 515 if prev_gw is not None: 516 kwargs["gateway"] = prev_gw 517 cxn = Connection(hop, **kwargs) 518 prev_gw = cxn 519 return prev_gw 520 elif "proxycommand" in self.ssh_config: 521 # Just a string, which we interpret as a proxy command.. 522 return self.ssh_config["proxycommand"] 523 # Fallback: config value (may be None). 524 return self.config.gateway 525 526 def __repr__(self): 527 # Host comes first as it's the most common differentiator by far 528 bits = [("host", self.host)] 529 # TODO: maybe always show user regardless? Explicit is good... 530 if self.user != self.config.user: 531 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 if self.port != self.config.port: 535 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 if self.gateway: 542 # Displaying type because gw params would probs be too verbose 543 val = "proxyjump" 544 if isinstance(self.gateway, string_types): 545 val = "proxycommand" 546 bits.append(("gw", val)) 547 return "<Connection {}>".format( 548 " ".join("{}={}".format(*x) for x in bits) 549 ) 550 551 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 return (self.host, self.user, self.port) 556 557 def __eq__(self, other): 558 if not isinstance(other, Connection): 559 return False 560 return self._identity() == other._identity() 561 562 def __lt__(self, other): 563 return self._identity() < other._identity() 564 565 def __hash__(self): 566 # NOTE: this departs from Context/DataProxy, which is not usefully 567 # hashable. 568 return hash(self._identity()) 569 570 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 return derive_shorthand(host_string) 575 576 @property 577 def is_connected(self): 578 """ 579 Whether or not this connection is actually open. 580 581 .. versionadded:: 2.0 582 """ 583 return self.transport.active if self.transport else False 584 585 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 if self.is_connected: 603 return 604 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 for key in """ 607 hostname 608 port 609 username 610 """.split(): 611 if key in self.connect_kwargs: 612 raise ValueError(err.format(key)) 613 # These may be given one way or the other, but not both 614 if ( 615 "timeout" in self.connect_kwargs 616 and self.connect_timeout is not None 617 ): 618 raise ValueError(err.format("timeout")) 619 # No conflicts -> merge 'em together 620 kwargs = dict( 621 self.connect_kwargs, 622 username=self.user, 623 hostname=self.host, 624 port=self.port, 625 ) 626 if self.gateway: 627 kwargs["sock"] = self.open_gateway() 628 if self.connect_timeout: 629 kwargs["timeout"] = self.connect_timeout 630 # Strip out empty defaults for less noisy debugging 631 if "key_filename" in kwargs and not kwargs["key_filename"]: 632 del kwargs["key_filename"] 633 # Actually connect! 634 self.client.connect(**kwargs) 635 self.transport = self.client.get_transport() 636 637 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 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 ssh_conf = SSHConfig() 654 dummy = "Host {}\n ProxyCommand {}" 655 ssh_conf.parse(StringIO(dummy.format(self.host, self.gateway))) 656 return ProxyCommand(ssh_conf.lookup(self.host)["proxycommand"]) 657 # Handle inner-Connection gateway type here. 658 # TODO: logging 659 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 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 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 if self.is_connected: 686 self.client.close() 687 if self.forward_agent and self._agent_handler is not None: 688 self._agent_handler.close() 689 690 def __enter__(self): 691 return self 692 693 def __exit__(self, *exc): 694 self.close() 695 696 @opens 697 def create_session(self): 698 channel = self.transport.open_session() 699 if self.forward_agent: 700 self._agent_handler = AgentRequestHandler(channel) 701 return channel 702 703 def _remote_runner(self): 704 return self.config.runners.remote(self, inline_env=self.inline_ssh_env) 705 706 @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 return self._run(self._remote_runner(), command, **kwargs) 722 723 @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 return self._sudo(self._remote_runner(), command, **kwargs) 736 737 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 return super(Connection, self).run(*args, **kwargs) 749 750 @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 if self._sftp is None: 763 self._sftp = self.client.open_sftp() 764 return self._sftp 765 766 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 return Transfer(self).get(*args, **kwargs) 776 777 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 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 @contextmanager 792 @opens 793 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 if not remote_port: 841 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 finished = Event() 847 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 manager.start() 857 858 # Return control to caller now that things ought to be operational 859 try: 860 yield 861 # Teardown once user exits block 862 finally: 863 # Signal to manager that it should close all open tunnels 864 finished.set() 865 # Then wait for it to do so 866 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 wrapper = manager.exception() 879 if wrapper is not None: 880 if wrapper.type is ThreadException: 881 raise wrapper.value 882 else: 883 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 @contextmanager 891 @opens 892 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 if not local_port: 944 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 tunnels = [] 961 962 def callback(channel, src_addr_tup, dst_addr_tup): 963 sock = socket.socket() 964 # TODO: handle connection failure such that channel, etc get closed 965 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 tunnel = Tunnel(channel=channel, sock=sock, finished=Event()) 970 tunnel.start() 971 # Communication between ourselves & the Paramiko handling subthread 972 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 try: 978 self.transport.request_port_forward( 979 address=remote_host, port=remote_port, handler=callback 980 ) 981 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 for tunnel in tunnels: 989 tunnel.finished.set() 990 tunnel.join() 991 self.transport.cancel_port_forward( 992 address=remote_host, port=remote_port 993 ) 994