diff --git a/docs/man/man1/ansible-playbook.1.asciidoc.in b/docs/man/man1/ansible-playbook.1.asciidoc.in index 00682567e84..2a1a94c5cdf 100644 --- a/docs/man/man1/ansible-playbook.1.asciidoc.in +++ b/docs/man/man1/ansible-playbook.1.asciidoc.in @@ -151,6 +151,11 @@ run operations with su as this user (default=root) Run operations with sudo (nopasswd) (deprecated, use become) +*--ssh-extra-args=*''-o ProxyCommand="ssh -W %h:%p ..." ...'':: + +Add the specified arguments to any ssh command-line. Useful to set a +ProxyCommand to use a jump host, but any arguments may be specified. + *-U*, 'SUDO_USER', *--sudo-user=*'SUDO_USER':: Desired sudo user (default=root) (deprecated, use become). diff --git a/docs/man/man1/ansible-pull.1.asciidoc.in b/docs/man/man1/ansible-pull.1.asciidoc.in index b78b7e67a2b..520a60bf212 100644 --- a/docs/man/man1/ansible-pull.1.asciidoc.in +++ b/docs/man/man1/ansible-pull.1.asciidoc.in @@ -105,6 +105,11 @@ Purge the checkout after the playbook is run. Sleep for random interval (between 0 and SLEEP number of seconds) before starting. This is a useful way ot disperse git requests. +*--ssh-extra-args=*''-o ProxyCommand="ssh -W %h:%p ..." ...'':: + +Add the specified arguments to any ssh command-line. Useful to set a +ProxyCommand to use a jump host, but any arguments may be specified. + *-t* 'TAGS', *--tags=*'TAGS':: Only run plays and tasks tagged with these values. diff --git a/docs/man/man1/ansible.1.asciidoc.in b/docs/man/man1/ansible.1.asciidoc.in index aaaac33c2af..7578e8f8be1 100644 --- a/docs/man/man1/ansible.1.asciidoc.in +++ b/docs/man/man1/ansible.1.asciidoc.in @@ -143,6 +143,11 @@ Run operations with su as this user (default=root) Run the command as the user given by -u and sudo to root. +*--ssh-extra-args=*''-o ProxyCommand="ssh -W %h:%p ..." ...'':: + +Add the specified arguments to any ssh command-line. Useful to set a +ProxyCommand to use a jump host, but any arguments may be specified. + *-U* 'SUDO_USERNAME', *--sudo-user=*'SUDO_USERNAME':: Sudo to 'SUDO_USERNAME' instead of root. Implies --sudo. diff --git a/docsite/rst/faq.rst b/docsite/rst/faq.rst index 4635bb57d9b..9dca5bde271 100644 --- a/docsite/rst/faq.rst +++ b/docsite/rst/faq.rst @@ -55,6 +55,37 @@ consider managing from a Fedora or openSUSE client even though you are managing We keep paramiko as the default as if you are first installing Ansible on an EL box, it offers a better experience for new users. +.. _use_ssh_jump_hosts: + +How do I configure a jump host to access servers that I have no direct access to? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +With Ansible version 2, it's possible to set `ansible_ssh_extra_args` as +an inventory variable. Any arguments specified this way are added to the +ssh command line when connecting to the relevant host(s), so it's a good +way to set a `ProxyCommand`. Consider the following inventory group: + + [gatewayed] + foo ansible_ssh_host=192.0.2.1 + bar ansible_ssh_host=192.0.2.2 + +You can create `group_vars/gatewayed.yml` with the following contents: + + ansible_ssh_extra_args: '-o ProxyCommand="ssh -W %h:%p -q user@gateway.example.com"' + +Ansible will then add these arguments when trying to connect to any host +in the group `gatewayed`. (These arguments are added to any `ssh_args` +that may be configured, so it isn't necessary to repeat the default +`ControlPath` settings in `ansible_ssh_extra_args`.) + +Note that `ssh -W` is available only with OpenSSH 5.4 or later. With +older versions, it's necessary to execute `nc %h:%p` or some equivalent +command on the bastion host. + +With earlier versions of Ansible, it was necessary to configure a +suitable `ProxyCommand` for one or more hosts in `~/.ssh/config`, +or globally by setting `ssh_args` in `ansible.cfg`. + .. _ec2_cloud_performance: How do I speed up management inside EC2? diff --git a/docsite/rst/intro_inventory.rst b/docsite/rst/intro_inventory.rst index 5afffb0fe50..f4b149685de 100644 --- a/docsite/rst/intro_inventory.rst +++ b/docsite/rst/intro_inventory.rst @@ -211,6 +211,11 @@ SSH connection:: The ssh password to use (this is insecure, we strongly recommend using --ask-pass or SSH keys) ansible_ssh_private_key_file Private key file used by ssh. Useful if using multiple keys and you don't want to use SSH agent. + ansible_ssh_args + This setting overrides any ``ssh_args`` configured in ``ansible.cfg``. + ansible_ssh_extra_args + Additional arguments for ssh. Useful to configure a ``ProxyCommand`` for a certain host (or group). + This is used in addition to any ``ssh_args`` configured in ``ansible.cfg`` or the inventory. Privilege escalation (see :doc:`Ansible Privilege Escalation` for further details):: diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 1de7106c824..e2231809037 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -315,6 +315,8 @@ class CLI(object): help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT) parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout', help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT) + parser.add_option('--ssh-extra-args', default='', dest='ssh_extra_args', + help="specify extra arguments to pass to ssh (e.g. ProxyCommand)") if async_opts: parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int', dest='poll_interval', diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py index d90220e2271..9888de6d847 100644 --- a/lib/ansible/playbook/play_context.py +++ b/lib/ansible/playbook/play_context.py @@ -163,6 +163,7 @@ class PlayContext(Base): _private_key_file = FieldAttribute(isa='string', default=C.DEFAULT_PRIVATE_KEY_FILE) _timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT) _shell = FieldAttribute(isa='string') + _ssh_extra_args = FieldAttribute(isa='string') _connection_lockfd= FieldAttribute(isa='int') # privilege escalation fields @@ -252,6 +253,7 @@ class PlayContext(Base): self.remote_user = options.remote_user self.private_key_file = options.private_key_file + self.ssh_extra_args = options.ssh_extra_args # privilege escalation self.become = options.become diff --git a/lib/ansible/plugins/connections/ssh.py b/lib/ansible/plugins/connections/ssh.py index 6dae226722b..81440c819a1 100644 --- a/lib/ansible/plugins/connections/ssh.py +++ b/lib/ansible/plugins/connections/ssh.py @@ -58,12 +58,37 @@ class Connection(ConnectionBase): super(Connection, self).__init__(*args, **kwargs) self.host = self._play_context.remote_addr + self.ssh_extra_args = '' + self.ssh_args = '' + + def set_host_overrides(self, host): + v = host.get_vars() + if 'ansible_ssh_extra_args' in v: + self.ssh_extra_args = v['ansible_ssh_extra_args'] + if 'ansible_ssh_args' in v: + self.ssh_args = v['ansible_ssh_args'] @property def transport(self): ''' used to identify this connection object from other classes ''' return 'ssh' + def _split_args(self, argstring): + """ + Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a + list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to + the argument list. The list will not contain any empty elements. + """ + return [x.strip() for x in shlex.split(argstring) if x.strip()] + + def add_args(self, explanation, args): + """ + Adds the given args to _common_args and displays a + caller-supplied explanation of why they were added. + """ + self._common_args += args + self._display.vvvvv('SSH: ' + explanation + ': (%s)' % ')('.join(args), host=self._play_context.remote_addr) + def _connect(self): ''' connect to the remote host ''' @@ -72,16 +97,25 @@ class Connection(ConnectionBase): if self._connected: return self - extra_args = C.ANSIBLE_SSH_ARGS - if extra_args is not None: - # make sure there is no empty string added as this can produce weird errors - self._common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()] + # We start with ansible_ssh_args from the inventory if it's set, + # or [ssh_connection]ssh_args from ansible.cfg, or the default + # Control* settings. + + if self.ssh_args: + args = self._split_args(self.ssh_args) + self.add_args("inventory set ansible_ssh_args", args) + elif C.ANSIBLE_SSH_ARGS: + args = self._split_args(C.ANSIBLE_SSH_ARGS) + self.add_args("ansible.cfg set ssh_args", args) else: - self._common_args += ( + args = ( "-o", "ControlMaster=auto", - "-o", "ControlPersist=60s", - "-o", "ControlPath=\"{0}\"".format(C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir)), + "-o", "ControlPersist=60s" ) + self.add_args("default arguments", args) + + # If any of the above have set ControlPersist but not a + # ControlPath, add one ourselves. cp_in_use = False cp_path_set = False @@ -92,27 +126,61 @@ class Connection(ConnectionBase): cp_path_set = True if cp_in_use and not cp_path_set: - self._common_args += ("-o", "ControlPath=\"{0}\"".format( + args = ("-o", "ControlPath=\"{0}\"".format( C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir)) ) + self.add_args("found only ControlPersist; added ControlPath", args) if not C.HOST_KEY_CHECKING: - self._common_args += ("-o", "StrictHostKeyChecking=no") + self.add_args( + "ANSIBLE_HOST_KEY_CHECKING/host_key_checking disabled", + ("-o", "StrictHostKeyChecking=no") + ) if self._play_context.port is not None: - self._common_args += ("-o", "Port={0}".format(self._play_context.port)) - if self._play_context.private_key_file is not None: - self._common_args += ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(self._play_context.private_key_file))) - if self._play_context.password: - self._common_args += ("-o", "GSSAPIAuthentication=no", - "-o", "PubkeyAuthentication=no") - else: - self._common_args += ("-o", "KbdInteractiveAuthentication=no", - "-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey", - "-o", "PasswordAuthentication=no") - if self._play_context.remote_user is not None and self._play_context.remote_user != pwd.getpwuid(os.geteuid())[0]: - self._common_args += ("-o", "User={0}".format(self._play_context.remote_user)) - self._common_args += ("-o", "ConnectTimeout={0}".format(self._play_context.timeout)) + self.add_args( + "ANSIBLE_REMOTE_PORT/remote_port/ansible_ssh_port set", + ("-o", "Port={0}".format(self._play_context.port)) + ) + + key = self._play_context.private_key_file + if key: + self.add_args( + "ANSIBLE_PRIVATE_KEY_FILE/private_key_file/ansible_ssh_private_key_file set", + ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(key))) + ) + + if not self._play_context.password: + self.add_args( + "ansible_password/ansible_ssh_pass not set", ( + "-o", "KbdInteractiveAuthentication=no", + "-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey", + "-o", "PasswordAuthentication=no" + ) + ) + + user = self._play_context.remote_user + if user and user != pwd.getpwuid(os.geteuid())[0]: + self.add_args( + "ANSIBLE_REMOTE_USER/remote_user/ansible_ssh_user/user/-u set", + ("-o", "User={0}".format(self._play_context.remote_user)) + ) + + self.add_args( + "ANSIBLE_TIMEOUT/timeout set", + ("-o", "ConnectTimeout={0}".format(self._play_context.timeout)) + ) + + # If any extra SSH arguments are specified in the inventory for + # this host, or specified as an override on the command line, + # add them in. + + if self._play_context.ssh_extra_args: + args = self._split_args(self._play_context.ssh_extra_args) + self.add_args("command-line added --ssh-extra-args", args) + elif self.ssh_extra_args: + args = self._split_args(self.ssh_extra_args) + self.add_args("inventory added ansible_ssh_extra_args", args) self._connected = True