network.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. """
  2. Classes and subroutines dealing with network connections and related topics.
  3. """
  4. from __future__ import with_statement
  5. from functools import wraps
  6. import getpass
  7. import os
  8. import re
  9. import time
  10. import socket
  11. import sys
  12. from StringIO import StringIO
  13. from fabric.auth import get_password, set_password
  14. from fabric.utils import handle_prompt_abort, warn
  15. from fabric.exceptions import NetworkError
  16. try:
  17. import warnings
  18. warnings.simplefilter('ignore', DeprecationWarning)
  19. import paramiko as ssh
  20. except ImportError, e:
  21. import traceback
  22. traceback.print_exc()
  23. msg = """
  24. There was a problem importing our SSH library (see traceback above).
  25. Please make sure all dependencies are installed and importable.
  26. """.rstrip()
  27. sys.stderr.write(msg + '\n')
  28. sys.exit(1)
  29. ipv6_regex = re.compile(
  30. '^\[?(?P<host>[0-9A-Fa-f:]+(?:%[a-z]+\d+)?)\]?(:(?P<port>\d+))?$')
  31. def direct_tcpip(client, host, port):
  32. return client.get_transport().open_channel(
  33. 'direct-tcpip',
  34. (host, int(port)),
  35. ('', 0)
  36. )
  37. def is_key_load_error(e):
  38. return (
  39. e.__class__ is ssh.SSHException
  40. and 'Unable to parse key file' in str(e)
  41. )
  42. def _tried_enough(tries):
  43. from fabric.state import env
  44. return tries >= env.connection_attempts
  45. def get_gateway(host, port, cache, replace=False):
  46. """
  47. Create and return a gateway socket, if one is needed.
  48. This function checks ``env`` for gateway or proxy-command settings and
  49. returns the necessary socket-like object for use by a final host
  50. connection.
  51. :param host:
  52. Hostname of target server.
  53. :param port:
  54. Port to connect to on target server.
  55. :param cache:
  56. A ``HostConnectionCache`` object, in which gateway ``SSHClient``
  57. objects are to be retrieved/cached.
  58. :param replace:
  59. Whether to forcibly replace a cached gateway client object.
  60. :returns:
  61. A ``socket.socket``-like object, or ``None`` if none was created.
  62. """
  63. from fabric.state import env, output
  64. sock = None
  65. proxy_command = ssh_config().get('proxycommand', None)
  66. if env.gateway:
  67. gateway = normalize_to_string(env.gateway)
  68. # ensure initial gateway connection
  69. if replace or gateway not in cache:
  70. if output.debug:
  71. print "Creating new gateway connection to %r" % gateway
  72. cache[gateway] = connect(*normalize(gateway) + (cache, False))
  73. # now we should have an open gw connection and can ask it for a
  74. # direct-tcpip channel to the real target. (bypass cache's own
  75. # __getitem__ override to avoid hilarity - this is usually called
  76. # within that method.)
  77. sock = direct_tcpip(dict.__getitem__(cache, gateway), host, port)
  78. elif proxy_command:
  79. sock = ssh.ProxyCommand(proxy_command)
  80. return sock
  81. class HostConnectionCache(dict):
  82. """
  83. Dict subclass allowing for caching of host connections/clients.
  84. This subclass will intelligently create new client connections when keys
  85. are requested, or return previously created connections instead.
  86. It also handles creating new socket-like objects when required to implement
  87. gateway connections and `ProxyCommand`, and handing them to the inner
  88. connection methods.
  89. Key values are the same as host specifiers throughout Fabric: optional
  90. username + ``@``, mandatory hostname, optional ``:`` + port number.
  91. Examples:
  92. * ``example.com`` - typical Internet host address.
  93. * ``firewall`` - atypical, but still legal, local host address.
  94. * ``user@example.com`` - with specific username attached.
  95. * ``bob@smith.org:222`` - with specific nonstandard port attached.
  96. When the username is not given, ``env.user`` is used. ``env.user``
  97. defaults to the currently running user at startup but may be overwritten by
  98. user code or by specifying a command-line flag.
  99. Note that differing explicit usernames for the same hostname will result in
  100. multiple client connections being made. For example, specifying
  101. ``user1@example.com`` will create a connection to ``example.com``, logged
  102. in as ``user1``; later specifying ``user2@example.com`` will create a new,
  103. 2nd connection as ``user2``.
  104. The same applies to ports: specifying two different ports will result in
  105. two different connections to the same host being made. If no port is given,
  106. 22 is assumed, so ``example.com`` is equivalent to ``example.com:22``.
  107. """
  108. def connect(self, key):
  109. """
  110. Force a new connection to ``key`` host string.
  111. """
  112. from fabric.state import env
  113. user, host, port = normalize(key)
  114. key = normalize_to_string(key)
  115. seek_gateway = True
  116. # break the loop when the host is gateway itself
  117. if env.gateway:
  118. seek_gateway = normalize_to_string(env.gateway) != key
  119. self[key] = connect(
  120. user, host, port, cache=self, seek_gateway=seek_gateway)
  121. def __getitem__(self, key):
  122. """
  123. Autoconnect + return connection object
  124. """
  125. key = normalize_to_string(key)
  126. if key not in self:
  127. self.connect(key)
  128. return dict.__getitem__(self, key)
  129. #
  130. # Dict overrides that normalize input keys
  131. #
  132. def __setitem__(self, key, value):
  133. return dict.__setitem__(self, normalize_to_string(key), value)
  134. def __delitem__(self, key):
  135. return dict.__delitem__(self, normalize_to_string(key))
  136. def __contains__(self, key):
  137. return dict.__contains__(self, normalize_to_string(key))
  138. def ssh_config(host_string=None):
  139. """
  140. Return ssh configuration dict for current env.host_string host value.
  141. Memoizes the loaded SSH config file, but not the specific per-host results.
  142. This function performs the necessary "is SSH config enabled?" checks and
  143. will simply return an empty dict if not. If SSH config *is* enabled and the
  144. value of env.ssh_config_path is not a valid file, it will abort.
  145. May give an explicit host string as ``host_string``.
  146. """
  147. from fabric.state import env
  148. dummy = {}
  149. if not env.use_ssh_config:
  150. return dummy
  151. if '_ssh_config' not in env:
  152. try:
  153. conf = ssh.SSHConfig()
  154. path = os.path.expanduser(env.ssh_config_path)
  155. with open(path) as fd:
  156. conf.parse(fd)
  157. env._ssh_config = conf
  158. except IOError:
  159. warn("Unable to load SSH config file '%s'" % path)
  160. return dummy
  161. host = parse_host_string(host_string or env.host_string)['host']
  162. return env._ssh_config.lookup(host)
  163. def key_filenames():
  164. """
  165. Returns list of SSH key filenames for the current env.host_string.
  166. Takes into account ssh_config and env.key_filename, including normalization
  167. to a list. Also performs ``os.path.expanduser`` expansion on any key
  168. filenames.
  169. """
  170. from fabric.state import env
  171. keys = env.key_filename
  172. # For ease of use, coerce stringish key filename into list
  173. if isinstance(env.key_filename, basestring) or env.key_filename is None:
  174. keys = [keys]
  175. # Strip out any empty strings (such as the default value...meh)
  176. keys = filter(bool, keys)
  177. # Honor SSH config
  178. conf = ssh_config()
  179. if 'identityfile' in conf:
  180. # Assume a list here as we require Paramiko 1.10+
  181. keys.extend(conf['identityfile'])
  182. return map(os.path.expanduser, keys)
  183. def key_from_env(passphrase=None):
  184. """
  185. Returns a paramiko-ready key from a text string of a private key
  186. """
  187. from fabric.state import env, output
  188. if 'key' in env:
  189. if output.debug:
  190. # NOTE: this may not be the most secure thing; OTOH anybody running
  191. # the process must by definition have access to the key value,
  192. # so only serious problem is if they're logging the output.
  193. sys.stderr.write("Trying to honor in-memory key %r\n" % env.key)
  194. for pkey_class in (ssh.rsakey.RSAKey, ssh.dsskey.DSSKey):
  195. if output.debug:
  196. sys.stderr.write("Trying to load it as %s\n" % pkey_class)
  197. try:
  198. return pkey_class.from_private_key(StringIO(env.key), passphrase)
  199. except Exception, e:
  200. # File is valid key, but is encrypted: raise it, this will
  201. # cause cxn loop to prompt for passphrase & retry
  202. if 'Private key file is encrypted' in e:
  203. raise
  204. # Otherwise, it probably means it wasn't a valid key of this
  205. # type, so try the next one.
  206. else:
  207. pass
  208. def parse_host_string(host_string):
  209. # Split host_string to user (optional) and host/port
  210. user_hostport = host_string.rsplit('@', 1)
  211. hostport = user_hostport.pop()
  212. user = user_hostport[0] if user_hostport and user_hostport[0] else None
  213. # Split host/port string to host and optional port
  214. # For IPv6 addresses square brackets are mandatory for host/port separation
  215. if hostport.count(':') > 1:
  216. # Looks like IPv6 address
  217. r = ipv6_regex.match(hostport).groupdict()
  218. host = r['host'] or None
  219. port = r['port'] or None
  220. else:
  221. # Hostname or IPv4 address
  222. host_port = hostport.rsplit(':', 1)
  223. host = host_port.pop(0) or None
  224. port = host_port[0] if host_port and host_port[0] else None
  225. return {'user': user, 'host': host, 'port': port}
  226. def normalize(host_string, omit_port=False):
  227. """
  228. Normalizes a given host string, returning explicit host, user, port.
  229. If ``omit_port`` is given and is True, only the host and user are returned.
  230. This function will process SSH config files if Fabric is configured to do
  231. so, and will use them to fill in some default values or swap in hostname
  232. aliases.
  233. Regarding SSH port used:
  234. * Ports explicitly given within host strings always win, no matter what.
  235. * When the host string lacks a port, SSH-config driven port configurations
  236. are used next.
  237. * When the SSH config doesn't specify a port (at all - including a default
  238. ``Host *`` block), Fabric's internal setting ``env.port`` is consulted.
  239. * If ``env.port`` is empty, ``env.default_port`` is checked (which should
  240. always be, as one would expect, port ``22``).
  241. """
  242. from fabric.state import env
  243. # Gracefully handle "empty" input by returning empty output
  244. if not host_string:
  245. return ('', '') if omit_port else ('', '', '')
  246. # Parse host string (need this early on to look up host-specific ssh_config
  247. # values)
  248. r = parse_host_string(host_string)
  249. host = r['host']
  250. # Env values (using defaults if somehow earlier defaults were replaced with
  251. # empty values)
  252. user = env.user or env.local_user
  253. # SSH config data
  254. conf = ssh_config(host_string)
  255. # Only use ssh_config values if the env value appears unmodified from
  256. # the true defaults. If the user has tweaked them, that new value
  257. # takes precedence.
  258. if user == env.local_user and 'user' in conf:
  259. user = conf['user']
  260. # Also override host if needed
  261. if 'hostname' in conf:
  262. host = conf['hostname']
  263. # Merge explicit user/port values with the env/ssh_config derived ones
  264. # (Host is already done at this point.)
  265. user = r['user'] or user
  266. if omit_port:
  267. return user, host
  268. # determine port from ssh config if enabled
  269. ssh_config_port = None
  270. if env.use_ssh_config:
  271. ssh_config_port = conf.get('port', None)
  272. # port priority order (as in docstring)
  273. port = r['port'] or ssh_config_port or env.port or env.default_port
  274. return user, host, port
  275. def to_dict(host_string):
  276. user, host, port = normalize(host_string)
  277. return {
  278. 'user': user, 'host': host, 'port': port, 'host_string': host_string
  279. }
  280. def from_dict(arg):
  281. return join_host_strings(arg['user'], arg['host'], arg['port'])
  282. def denormalize(host_string):
  283. """
  284. Strips out default values for the given host string.
  285. If the user part is the default user, it is removed;
  286. if the port is port 22, it also is removed.
  287. """
  288. from fabric.state import env
  289. r = parse_host_string(host_string)
  290. user = ''
  291. if r['user'] is not None and r['user'] != env.user:
  292. user = r['user'] + '@'
  293. port = ''
  294. if r['port'] is not None and r['port'] != '22':
  295. port = ':' + r['port']
  296. host = r['host']
  297. host = '[%s]' % host if port and host.count(':') > 1 else host
  298. return user + host + port
  299. def join_host_strings(user, host, port=None):
  300. """
  301. Turns user/host/port strings into ``user@host:port`` combined string.
  302. This function is not responsible for handling missing user/port strings;
  303. for that, see the ``normalize`` function.
  304. If ``host`` looks like IPv6 address, it will be enclosed in square brackets
  305. If ``port`` is omitted, the returned string will be of the form
  306. ``user@host``.
  307. """
  308. if port:
  309. # Square brackets are necessary for IPv6 host/port separation
  310. template = "%s@[%s]:%s" if host.count(':') > 1 else "%s@%s:%s"
  311. return template % (user, host, port)
  312. else:
  313. return "%s@%s" % (user, host)
  314. def normalize_to_string(host_string):
  315. """
  316. normalize() returns a tuple; this returns another valid host string.
  317. """
  318. return join_host_strings(*normalize(host_string))
  319. def connect(user, host, port, cache, seek_gateway=True):
  320. """
  321. Create and return a new SSHClient instance connected to given host.
  322. :param user: Username to connect as.
  323. :param host: Network hostname.
  324. :param port: SSH daemon port.
  325. :param cache:
  326. A ``HostConnectionCache`` instance used to cache/store gateway hosts
  327. when gatewaying is enabled.
  328. :param seek_gateway:
  329. Whether to try setting up a gateway socket for this connection. Used so
  330. the actual gateway connection can prevent recursion.
  331. """
  332. from fabric.state import env, output
  333. #
  334. # Initialization
  335. #
  336. # Init client
  337. client = ssh.SSHClient()
  338. # Load system hosts file (e.g. /etc/ssh/ssh_known_hosts)
  339. known_hosts = env.get('system_known_hosts')
  340. if known_hosts:
  341. client.load_system_host_keys(known_hosts)
  342. # Load known host keys (e.g. ~/.ssh/known_hosts) unless user says not to.
  343. if not env.disable_known_hosts:
  344. client.load_system_host_keys()
  345. # Unless user specified not to, accept/add new, unknown host keys
  346. if not env.reject_unknown_hosts:
  347. client.set_missing_host_key_policy(ssh.AutoAddPolicy())
  348. #
  349. # Connection attempt loop
  350. #
  351. # Initialize loop variables
  352. connected = False
  353. password = get_password(user, host, port, login_only=True)
  354. tries = 0
  355. sock = None
  356. # Loop until successful connect (keep prompting for new password)
  357. while not connected:
  358. # Attempt connection
  359. try:
  360. tries += 1
  361. # (Re)connect gateway socket, if needed.
  362. # Nuke cached client object if not on initial try.
  363. if seek_gateway:
  364. sock = get_gateway(host, port, cache, replace=tries > 0)
  365. # Set up kwargs (this lets us skip GSS-API kwargs unless explicitly
  366. # set; otherwise older Paramiko versions will be cranky.)
  367. kwargs = dict(
  368. hostname=host,
  369. port=int(port),
  370. username=user,
  371. password=password,
  372. pkey=key_from_env(password),
  373. key_filename=key_filenames(),
  374. timeout=env.timeout,
  375. allow_agent=not env.no_agent,
  376. look_for_keys=not env.no_keys,
  377. sock=sock,
  378. )
  379. for suffix in ('auth', 'deleg_creds', 'kex'):
  380. name = "gss_" + suffix
  381. val = env.get(name, None)
  382. if val is not None:
  383. kwargs[name] = val
  384. # Ready to connect
  385. client.connect(**kwargs)
  386. connected = True
  387. # set a keepalive if desired
  388. if env.keepalive:
  389. client.get_transport().set_keepalive(env.keepalive)
  390. return client
  391. # BadHostKeyException corresponds to key mismatch, i.e. what on the
  392. # command line results in the big banner error about man-in-the-middle
  393. # attacks.
  394. except ssh.BadHostKeyException, e:
  395. raise NetworkError("Host key for %s did not match pre-existing key! Server's key was changed recently, or possible man-in-the-middle attack." % host, e)
  396. # Prompt for new password to try on auth failure
  397. except (
  398. ssh.AuthenticationException,
  399. ssh.PasswordRequiredException,
  400. ssh.SSHException
  401. ), e:
  402. msg = str(e)
  403. # If we get SSHExceptionError and the exception message indicates
  404. # SSH protocol banner read failures, assume it's caused by the
  405. # server load and try again.
  406. #
  407. # If we are using a gateway, we will get a ChannelException if
  408. # connection to the downstream host fails. We should retry.
  409. if (e.__class__ is ssh.SSHException \
  410. and msg == 'Error reading SSH protocol banner') \
  411. or e.__class__ is ssh.ChannelException:
  412. if _tried_enough(tries):
  413. raise NetworkError(msg, e)
  414. continue
  415. # For whatever reason, empty password + no ssh key or agent
  416. # results in an SSHException instead of an
  417. # AuthenticationException. Since it's difficult to do
  418. # otherwise, we must assume empty password + SSHException ==
  419. # auth exception.
  420. #
  421. # Conversely: if we get SSHException and there
  422. # *was* a password -- it is probably something non auth
  423. # related, and should be sent upwards. (This is not true if the
  424. # exception message does indicate key parse problems.)
  425. #
  426. # This also holds true for rejected/unknown host keys: we have to
  427. # guess based on other heuristics.
  428. if (
  429. e.__class__ is ssh.SSHException
  430. and (
  431. password
  432. or msg.startswith('Unknown server')
  433. or "not found in known_hosts" in msg
  434. )
  435. and not is_key_load_error(e)
  436. ):
  437. raise NetworkError(msg, e)
  438. # Otherwise, assume an auth exception, and prompt for new/better
  439. # password.
  440. # Paramiko doesn't handle prompting for locked private
  441. # keys (i.e. keys with a passphrase and not loaded into an agent)
  442. # so we have to detect this and tweak our prompt slightly.
  443. # (Otherwise, however, the logic flow is the same, because
  444. # ssh's connect() method overrides the password argument to be
  445. # either the login password OR the private key passphrase. Meh.)
  446. #
  447. # NOTE: This will come up if you normally use a
  448. # passphrase-protected private key with ssh-agent, and enter an
  449. # incorrect remote username, because ssh.connect:
  450. # * Tries the agent first, which will fail as you gave the wrong
  451. # username, so obviously any loaded keys aren't gonna work for a
  452. # nonexistent remote account;
  453. # * Then tries the on-disk key file, which is passphrased;
  454. # * Realizes there's no password to try unlocking that key with,
  455. # because you didn't enter a password, because you're using
  456. # ssh-agent;
  457. # * In this condition (trying a key file, password is None)
  458. # ssh raises PasswordRequiredException.
  459. text = None
  460. if e.__class__ is ssh.PasswordRequiredException \
  461. or is_key_load_error(e):
  462. # NOTE: we can't easily say WHICH key's passphrase is needed,
  463. # because ssh doesn't provide us with that info, and
  464. # env.key_filename may be a list of keys, so we can't know
  465. # which one raised the exception. Best not to try.
  466. prompt = "[%s] Passphrase for private key"
  467. text = prompt % env.host_string
  468. password = prompt_for_password(text)
  469. # Update env.password, env.passwords if empty
  470. set_password(user, host, port, password)
  471. # Ctrl-D / Ctrl-C for exit
  472. # TODO: this may no longer actually serve its original purpose and may
  473. # also hide TypeErrors from paramiko. Double check in v2.
  474. except (EOFError, TypeError):
  475. # Print a newline (in case user was sitting at prompt)
  476. print('')
  477. sys.exit(0)
  478. # Handle DNS error / name lookup failure
  479. except socket.gaierror, e:
  480. raise NetworkError('Name lookup failed for %s' % host, e)
  481. # Handle timeouts and retries, including generic errors
  482. # NOTE: In 2.6, socket.error subclasses IOError
  483. except socket.error, e:
  484. not_timeout = type(e) is not socket.timeout
  485. giving_up = _tried_enough(tries)
  486. # Baseline error msg for when debug is off
  487. msg = "Timed out trying to connect to %s" % host
  488. # Expanded for debug on
  489. err = msg + " (attempt %s of %s)" % (tries, env.connection_attempts)
  490. if giving_up:
  491. err += ", giving up"
  492. err += ")"
  493. # Debuggin'
  494. if output.debug:
  495. sys.stderr.write(err + '\n')
  496. # Having said our piece, try again
  497. if not giving_up:
  498. # Sleep if it wasn't a timeout, so we still get timeout-like
  499. # behavior
  500. if not_timeout:
  501. time.sleep(env.timeout)
  502. continue
  503. # Override eror msg if we were retrying other errors
  504. if not_timeout:
  505. msg = "Low level socket error connecting to host %s on port %s: %s" % (
  506. host, port, e[1]
  507. )
  508. # Here, all attempts failed. Tweak error msg to show # tries.
  509. # TODO: find good humanization module, jeez
  510. s = "s" if env.connection_attempts > 1 else ""
  511. msg += " (tried %s time%s)" % (env.connection_attempts, s)
  512. raise NetworkError(msg, e)
  513. # Ensure that if we terminated without connecting and we were given an
  514. # explicit socket, close it out.
  515. finally:
  516. if not connected and sock is not None:
  517. sock.close()
  518. def _password_prompt(prompt, stream):
  519. # NOTE: Using encode-to-ascii to prevent (Windows, at least) getpass from
  520. # choking if given Unicode.
  521. return getpass.getpass(prompt.encode('ascii', 'ignore'), stream)
  522. def prompt_for_password(prompt=None, no_colon=False, stream=None):
  523. """
  524. Prompts for and returns a new password if required; otherwise, returns
  525. None.
  526. A trailing colon is appended unless ``no_colon`` is True.
  527. If the user supplies an empty password, the user will be re-prompted until
  528. they enter a non-empty password.
  529. ``prompt_for_password`` autogenerates the user prompt based on the current
  530. host being connected to. To override this, specify a string value for
  531. ``prompt``.
  532. ``stream`` is the stream the prompt will be printed to; if not given,
  533. defaults to ``sys.stderr``.
  534. """
  535. from fabric.state import env
  536. handle_prompt_abort("a connection or sudo password")
  537. stream = stream or sys.stderr
  538. # Construct prompt
  539. default = "[%s] Login password for '%s'" % (env.host_string, env.user)
  540. password_prompt = prompt if (prompt is not None) else default
  541. if not no_colon:
  542. password_prompt += ": "
  543. # Get new password value
  544. new_password = _password_prompt(password_prompt, stream)
  545. # Otherwise, loop until user gives us a non-empty password (to prevent
  546. # returning the empty string, and to avoid unnecessary network overhead.)
  547. while not new_password:
  548. print("Sorry, you can't enter an empty password. Please try again.")
  549. new_password = _password_prompt(password_prompt, stream)
  550. return new_password
  551. def needs_host(func):
  552. """
  553. Prompt user for value of ``env.host_string`` when ``env.host_string`` is
  554. empty.
  555. This decorator is basically a safety net for silly users who forgot to
  556. specify the host/host list in one way or another. It should be used to wrap
  557. operations which require a network connection.
  558. Due to how we execute commands per-host in ``main()``, it's not possible to
  559. specify multiple hosts at this point in time, so only a single host will be
  560. prompted for.
  561. Because this decorator sets ``env.host_string``, it will prompt once (and
  562. only once) per command. As ``main()`` clears ``env.host_string`` between
  563. commands, this decorator will also end up prompting the user once per
  564. command (in the case where multiple commands have no hosts set, of course.)
  565. """
  566. from fabric.state import env
  567. @wraps(func)
  568. def host_prompting_wrapper(*args, **kwargs):
  569. while not env.get('host_string', False):
  570. handle_prompt_abort("the target host connection string")
  571. host_string = raw_input("No hosts found. Please specify (single)"
  572. " host string for connection: ")
  573. env.update(to_dict(host_string))
  574. return func(*args, **kwargs)
  575. host_prompting_wrapper.undecorated = func
  576. return host_prompting_wrapper
  577. def disconnect_all():
  578. """
  579. Disconnect from all currently connected servers.
  580. Used at the end of ``fab``'s main loop, and also intended for use by
  581. library users.
  582. """
  583. from fabric.state import connections, output
  584. # Explicitly disconnect from all servers
  585. for key in connections.keys():
  586. if output.status:
  587. # Here we can't use the py3k print(x, end=" ")
  588. # because 2.5 backwards compatibility
  589. sys.stdout.write("Disconnecting from %s... " % denormalize(key))
  590. connections[key].close()
  591. del connections[key]
  592. if output.status:
  593. sys.stdout.write("done.\n")